From 3c96de85adcb1377e274d7c2403bcf3964fd021d Mon Sep 17 00:00:00 2001 From: Mariano Gabriel Date: Tue, 9 Jun 2026 17:46:37 -0300 Subject: [PATCH] add extra formats --- meetus/workflow.py | 82 ++++++++++++++++++++++++++++++++++++++++++++-- process_meeting.py | 9 +++++ 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/meetus/workflow.py b/meetus/workflow.py index 902eec8..7006aa3 100644 --- a/meetus/workflow.py +++ b/meetus/workflow.py @@ -18,6 +18,10 @@ from .transcript_merger import TranscriptMerger logger = logging.getLogger(__name__) +# Transcript output formats supported by whisper/whisperx (besides the json base). +# json is always produced because it is the base for the enhanced transcript. +VALID_TRANSCRIPT_FORMATS = {'json', 'srt', 'vtt', 'txt', 'tsv'} + class WorkflowConfig: """Configuration for the processing workflow.""" @@ -36,6 +40,12 @@ class WorkflowConfig: self.diarize = kwargs.get('diarize', False) self.language = kwargs.get('language') + # Additional transcript output formats (json is always written as the + # base for the enhanced transcript). + self.transcript_formats = self._parse_transcript_formats( + kwargs.get('transcript_formats') + ) + # Frame extraction self.scene_detection = kwargs.get('scene_detection', False) self.scene_threshold = kwargs.get('scene_threshold', 15.0) @@ -69,12 +79,44 @@ class WorkflowConfig: self.embed_images = kwargs.get('embed_images', False) self.embed_quality = kwargs.get('embed_quality', 80) + @staticmethod + def _parse_transcript_formats(raw) -> list: + """Parse the --transcript-formats value into a deduplicated list. + + Accepts a comma-separated string (e.g. "srt,txt") or 'all'. json is + always included since it is the base for the enhanced transcript. + """ + formats = ['json'] + if not raw: + return formats + + if isinstance(raw, str): + requested = [f.strip().lower() for f in raw.split(',') if f.strip()] + else: + requested = [str(f).strip().lower() for f in raw if str(f).strip()] + + if 'all' in requested: + requested = [f for f in VALID_TRANSCRIPT_FORMATS if f != 'json'] + + for fmt in requested: + if fmt not in VALID_TRANSCRIPT_FORMATS: + raise ValueError( + f"Invalid transcript format '{fmt}'. " + f"Choose from: {', '.join(sorted(VALID_TRANSCRIPT_FORMATS))}, all" + ) + if fmt not in formats: + formats.append(fmt) + + return formats + def to_dict(self) -> Dict[str, Any]: """Convert config to dictionary for manifest.""" return { "whisper": { "enabled": self.run_whisper, - "model": self.whisper_model + "model": self.whisper_model, + "diarize": self.diarize, + "transcript_formats": self.transcript_formats }, "frame_extraction": { "method": "scene_detection" if self.scene_detection else "interval", @@ -217,12 +259,21 @@ class ProcessingWorkflow: logger.info(f"Running Whisper transcription (model: {self.config.whisper_model})...") logger.info("This may take a few minutes depending on video length...") + # Determine output format. json is always produced (base for the + # enhanced transcript). If extra formats are requested we ask whisper + # for "all" in a single transcription pass, then prune the formats the + # user didn't request (whisper's CLI only accepts one --output_format). + extra_formats = [f for f in self.config.transcript_formats if f != 'json'] + output_format = "all" if extra_formats else "json" + if extra_formats: + logger.info(f"Additional transcript formats requested: {', '.join(extra_formats)}") + # Build command cmd = [ transcribe_cmd, str(self.config.video_path), "--model", self.config.whisper_model, - "--output_format", "json", + "--output_format", output_format, "--output_dir", str(self.output_mgr.output_dir), ] if use_diarize: @@ -244,6 +295,10 @@ class ProcessingWorkflow: transcript_path = self.output_mgr.get_path(f"{self.config.video_path.stem}.json") + # Prune any formats produced by "all" that the user didn't request. + if extra_formats: + self._prune_transcript_formats() + if transcript_path.exists(): logger.info(f"✓ Whisper transcription completed: {transcript_path.name}") @@ -275,6 +330,29 @@ class ProcessingWorkflow: logger.error(f"Whisper failed: {e.stderr}") raise + def _prune_transcript_formats(self): + """Remove transcript files produced by "all" that weren't requested. + + whisper/whisperx only accept a single --output_format, so we run with + "all" and delete the formats the user didn't ask for. json is always + kept since it is the base for the enhanced transcript. + """ + keep = set(self.config.transcript_formats) + stem = self.config.video_path.stem + for fmt in VALID_TRANSCRIPT_FORMATS: + if fmt in keep: + continue + stray = self.output_mgr.get_path(f"{stem}.{fmt}") + if stray.exists(): + try: + stray.unlink() + logger.debug(f"Removed unrequested transcript format: {stray.name}") + except OSError as e: + logger.warning(f"Could not remove {stray.name}: {e}") + + written = [f for f in self.config.transcript_formats] + logger.info(f"✓ Transcript formats written: {', '.join(written)}") + def _extract_frames(self): """Extract frames from video.""" logger.info("Step 1: Extracting frames from video...") diff --git a/process_meeting.py b/process_meeting.py index 1b2df72..a475c82 100644 --- a/process_meeting.py +++ b/process_meeting.py @@ -82,6 +82,15 @@ Examples: help='Language of the audio (e.g. en, es, fr). Default: auto-detect', default=None ) + parser.add_argument( + '--transcript-formats', + help='Comma-separated extra transcript/diarization formats to write ' + 'alongside JSON (choices: srt, vtt, txt, tsv, or all). JSON is ' + 'always written as the base for the enhanced transcript. ' + 'Only produced when transcription actually runs (use ' + '--skip-cache-whisper to regenerate). Example: --transcript-formats srt,txt', + default=None + ) # Output options parser.add_argument(