Spaces:
Sleeping
Sleeping
| """EchoScript v1.0 UI. | |
| Implements the frozen v1.0 workflow: | |
| Upload Audio -> Generate Canonical Transcript -> Preview Results | |
| -> Generate Outputs -> Copy / Download | |
| Services are instantiated lazily (on first use) rather than at import time, | |
| so the app can start up without needing model weights on disk yet, and so | |
| this module stays import-safe in environments without network access. | |
| """ | |
| from __future__ import annotations | |
| import tempfile | |
| from pathlib import Path | |
| from typing import Optional | |
| import gradio as gr | |
| from models.transcript import Transcript | |
| from services.audio import AudioError, extract_window, resolve_window, validate_extension | |
| from services.subtitles import generate_srt, generate_vtt | |
| from services.transcription import SUPPORTED_LANGUAGES, TranscriptionService | |
| from services.translation import SUPPORTED_TARGET_LANGUAGES, TranslationService | |
| # --------------------------------------------------------------------------- | |
| # Lazy service singletons | |
| # --------------------------------------------------------------------------- | |
| _transcription_service: Optional[TranscriptionService] = None | |
| _translation_service: Optional[TranslationService] = None | |
| def get_transcription_service() -> TranscriptionService: | |
| global _transcription_service | |
| if _transcription_service is None: | |
| _transcription_service = TranscriptionService( | |
| model_size="base", | |
| device="cpu", | |
| compute_type="int8", | |
| download_root="/tmp/whisper_models", | |
| ) | |
| return _transcription_service | |
| def get_translation_service() -> TranslationService: | |
| global _translation_service | |
| if _translation_service is None: | |
| _translation_service = TranslationService() | |
| return _translation_service | |
| # --------------------------------------------------------------------------- | |
| # UI <-> service-layer vocabulary | |
| # --------------------------------------------------------------------------- | |
| # "Source Language" dropdown: display name -> ISO 639-1 code (None = auto). | |
| _NAME_TO_CODE = {name: code for code, name in SUPPORTED_LANGUAGES.items()} | |
| SOURCE_LANGUAGE_CHOICES = ["Auto Detect"] + list(SUPPORTED_LANGUAGES.values()) | |
| # "Outputs" checkboxes: display label -> ISO 639-1 code for translations. | |
| _TRANSLATION_LABEL_TO_CODE = { | |
| f"{name} Translation": code for code, name in SUPPORTED_TARGET_LANGUAGES.items() | |
| } | |
| OUTPUT_CHOICES = ["Transcript"] + list(_TRANSLATION_LABEL_TO_CODE.keys()) | |
| DEFAULT_OUTPUTS = ["Transcript", "English Translation"] | |
| # Order in which translation tabs are laid out (fixed; visibility toggles). | |
| _TRANSLATION_TAB_ORDER = ["English Translation", "German Translation", "Persian Translation", "Spanish Translation"] | |
| def _format_duration(seconds: float) -> str: | |
| seconds = max(0, int(round(seconds))) | |
| hours, remainder = divmod(seconds, 3600) | |
| minutes, secs = divmod(remainder, 60) | |
| return f"{hours:02}:{minutes:02}:{secs:02}" | |
| def _write_text_file(text: str, tmp_dir: Path, filename: str) -> str: | |
| path = tmp_dir / filename | |
| path.write_text(text, encoding="utf-8") | |
| return str(path) | |
| # --------------------------------------------------------------------------- | |
| # Main processing callback | |
| # --------------------------------------------------------------------------- | |
| def process_audio( | |
| audio_path: Optional[str], | |
| start_value: str, | |
| end_value: str, | |
| source_language_label: str, | |
| selected_outputs: list[str], | |
| ): | |
| if not audio_path: | |
| raise gr.Error("Please upload an audio file first.") | |
| try: | |
| validate_extension(audio_path) | |
| start, end = resolve_window(start_value, end_value) | |
| except AudioError as exc: | |
| raise gr.Error(str(exc)) from exc | |
| working_path = audio_path | |
| if start is not None or end is not None: | |
| try: | |
| working_path = extract_window(audio_path, start, end) | |
| except AudioError as exc: | |
| raise gr.Error(str(exc)) from exc | |
| language_code = _NAME_TO_CODE.get(source_language_label) # None = auto-detect | |
| # Step 1: Audio -> canonical Transcript. This is the only step that | |
| # touches the audio; everything below works off `transcript`. | |
| transcript: Transcript = get_transcription_service().transcribe( | |
| working_path, | |
| source_filename=Path(audio_path).name, | |
| language=language_code, | |
| window_start=start, | |
| window_end=end, | |
| ) | |
| tmp_dir = Path(tempfile.mkdtemp(prefix="echoscript_")) | |
| # --- Results dashboard --- | |
| detected_language = SUPPORTED_LANGUAGES.get(transcript.language, transcript.language) | |
| dashboard_md = ( | |
| f"### \u2713 {detected_language} detected\n\n" | |
| f"**Confidence:** {transcript.language_probability:.0%} " | |
| f"**Duration:** {_format_duration(transcript.duration)} " | |
| f"**Words:** {transcript.word_count:,}" | |
| ) | |
| # --- Transcript tab (always computed -- it's the source of truth -- | |
| # but only exposed as a tab if the user kept "Transcript" checked) --- | |
| transcript_text = transcript.text | |
| transcript_file = _write_text_file(transcript_text, tmp_dir, "transcript.txt") | |
| transcript_tab_visible = "Transcript" in selected_outputs | |
| # --- Translation tabs --- | |
| translation_service = get_translation_service() | |
| translation_updates = {} # label -> (text, file_path) | |
| for label in _TRANSLATION_TAB_ORDER: | |
| if label in selected_outputs: | |
| code = _TRANSLATION_LABEL_TO_CODE[label] | |
| translation = translation_service.translate(transcript, code) | |
| text = translation.text | |
| file_path = _write_text_file(text, tmp_dir, f"{code}.txt") | |
| translation_updates[label] = (text, file_path) | |
| else: | |
| translation_updates[label] = (None, None) | |
| # --- Subtitles (always derived from the transcript, the canonical | |
| # source of truth -- never regenerated from audio) --- | |
| srt_path = _write_text_file(generate_srt(transcript.segments), tmp_dir, "transcript.srt") | |
| vtt_path = _write_text_file(generate_vtt(transcript.segments), tmp_dir, "transcript.vtt") | |
| outputs = [dashboard_md, transcript_text, transcript_file, gr.update(visible=transcript_tab_visible)] | |
| for label in _TRANSLATION_TAB_ORDER: | |
| text, file_path = translation_updates[label] | |
| visible = text is not None | |
| outputs += [gr.update(visible=visible), text or "", file_path] | |
| outputs += [srt_path, vtt_path] | |
| return outputs | |
| # --------------------------------------------------------------------------- | |
| # UI layout | |
| # --------------------------------------------------------------------------- | |
| with gr.Blocks(title="EchoScript") as demo: | |
| gr.Markdown( | |
| """ | |
| # EchoScript | |
| **Upload Audio → Generate Canonical Transcript → Preview Results → Generate Outputs → Copy / Download** | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Upload Audio") | |
| audio_input = gr.Audio( | |
| label="Drop audio file here or click to browse", | |
| sources=["upload"], | |
| type="filepath", | |
| ) | |
| gr.Markdown("Supported: mp3 · wav · m4a · flac") | |
| gr.Markdown("### Processing Window") | |
| with gr.Row(): | |
| start_input = gr.Textbox(label="Start Time (optional)", placeholder="HH:MM:SS") | |
| end_input = gr.Textbox(label="End Time (optional)", placeholder="HH:MM:SS") | |
| gr.Markdown("Leave blank: entire file") | |
| gr.Markdown("### Processing Options") | |
| language_input = gr.Dropdown( | |
| choices=SOURCE_LANGUAGE_CHOICES, | |
| value="Auto Detect", | |
| label="Source Language", | |
| ) | |
| outputs_input = gr.CheckboxGroup( | |
| choices=OUTPUT_CHOICES, | |
| value=DEFAULT_OUTPUTS, | |
| label="Outputs", | |
| ) | |
| process_button = gr.Button("Generate Outputs", variant="primary") | |
| with gr.Column(scale=2): | |
| gr.Markdown("### Results Dashboard") | |
| dashboard_output = gr.Markdown("Upload an audio file and click **Generate Outputs** to begin.") | |
| with gr.Tabs(): | |
| with gr.Tab("Transcript") as transcript_tab: | |
| transcript_box = gr.Textbox( | |
| label="Transcript", | |
| lines=16, | |
| interactive=True, | |
| buttons=["copy"], | |
| ) | |
| transcript_download = gr.DownloadButton("Download TXT") | |
| translation_tabs = {} | |
| translation_boxes = {} | |
| translation_downloads = {} | |
| for label in _TRANSLATION_TAB_ORDER: | |
| short_name = label.replace(" Translation", "") | |
| with gr.Tab(short_name, visible=False) as tab: | |
| box = gr.Textbox( | |
| label=short_name, | |
| lines=16, | |
| interactive=True, | |
| buttons=["copy"], | |
| ) | |
| download = gr.DownloadButton("Download TXT") | |
| translation_tabs[label] = tab | |
| translation_boxes[label] = box | |
| translation_downloads[label] = download | |
| with gr.Tab("Subtitles"): | |
| gr.Markdown( | |
| "Subtitles are generated from the transcript " | |
| "(source language), so they stay in sync no matter " | |
| "which translations are also generated." | |
| ) | |
| with gr.Row(): | |
| srt_download = gr.DownloadButton("Download SRT") | |
| vtt_download = gr.DownloadButton("Download VTT") | |
| # Build the flat outputs list in the exact order process_audio() returns. | |
| click_outputs = [dashboard_output, transcript_box, transcript_download, transcript_tab] | |
| for label in _TRANSLATION_TAB_ORDER: | |
| click_outputs += [translation_tabs[label], translation_boxes[label], translation_downloads[label]] | |
| click_outputs += [srt_download, vtt_download] | |
| process_button.click( | |
| fn=process_audio, | |
| inputs=[audio_input, start_input, end_input, language_input, outputs_input], | |
| outputs=click_outputs, | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |