usertea commited on
Commit
abbbd7b
·
1 Parent(s): 7d761b6

EchoScript : New UI

Browse files
Files changed (2) hide show
  1. app.py +233 -85
  2. requirements.txt +1 -1
app.py CHANGED
@@ -1,84 +1,177 @@
1
- """EchoScript app entrypoint.
2
-
3
- UI is intentionally left as-is for this step -- only the internals changed.
4
- Previously this file talked to faster-whisper directly. It now goes through
5
- the service layer (services/transcription.py, services/translation.py,
6
- services/subtitles.py) operating on the models/transcript.py data model, so
7
- the Audio -> Transcript -> Outputs flow is real code, not just a diagram.
8
- The Gradio UI itself (upload dropzone, time window, language dropdown,
9
- output checkboxes, results dashboard, tabs) is the next step.
 
10
  """
11
 
 
 
12
  import tempfile
13
  from pathlib import Path
14
- from zipfile import ZipFile
15
 
16
  import gradio as gr
17
 
 
 
18
  from services.subtitles import generate_srt, generate_vtt
19
- from services.transcription import TranscriptionService
20
- from services.translation import TranslationService
21
 
22
- transcription_service = TranscriptionService(
23
- model_size="base",
24
- device="cpu",
25
- compute_type="int8",
26
- download_root="/tmp/whisper_models",
27
- )
28
- translation_service = TranslationService()
29
 
 
 
30
 
31
- def process_files(files, mode):
32
 
33
- if not files:
34
- return "", None
 
 
 
 
 
 
 
 
35
 
36
- tmp_dir = Path(tempfile.mkdtemp())
37
- summary_lines = []
38
- zip_path = tmp_dir / "echoscript_results.zip"
39
 
40
- with ZipFile(zip_path, "w") as zipf:
 
 
 
 
41
 
42
- for uploaded_file in files:
43
 
44
- audio_path = uploaded_file
45
- stem = Path(audio_path).stem
 
46
 
47
- # Step 1: Audio -> canonical Transcript. Always "transcribe",
48
- # never "translate" -- see services/translation.py for why.
49
- transcript = transcription_service.transcribe(
50
- audio_path,
51
- source_filename=Path(audio_path).name,
52
- )
53
 
54
- # Step 2: Outputs are derived from the Transcript, never from
55
- # the audio again.
56
- if mode == "Translate to English" and transcript.language != "en":
57
- output = translation_service.translate(transcript, "en")
58
- else:
59
- output = transcript
60
-
61
- txt_file = tmp_dir / f"{stem}.txt"
62
- srt_file = tmp_dir / f"{stem}.srt"
63
- vtt_file = tmp_dir / f"{stem}.vtt"
64
-
65
- txt_file.write_text(output.text, encoding="utf-8")
66
- srt_file.write_text(generate_srt(output.segments), encoding="utf-8")
67
- vtt_file.write_text(generate_vtt(output.segments), encoding="utf-8")
68
-
69
- for f in (txt_file, srt_file, vtt_file):
70
- zipf.write(f, arcname=f.name)
71
-
72
- language_name = transcript.language
73
- summary_lines.append(
74
- f"{stem}\n"
75
- f"Language: {language_name}\n"
76
- f"Confidence: {transcript.language_probability:.2%}\n"
77
- f"Words: {transcript.word_count}\n"
78
- )
79
 
80
- return "\n\n".join(summary_lines), str(zip_path)
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
  with gr.Blocks(title="EchoScript") as demo:
84
 
@@ -86,37 +179,92 @@ with gr.Blocks(title="EchoScript") as demo:
86
  """
87
  # EchoScript
88
 
89
- Transcribe audio files using Faster-Whisper.
90
-
91
- ### Features
92
-
93
- - Automatic language detection
94
- - Multi-language transcription
95
- - Translation to English
96
- - Batch processing
97
- - TXT export
98
- - SRT subtitle export
99
- - VTT subtitle export
100
  """
101
  )
102
 
103
- files_input = gr.Files(label="Upload Audio Files")
 
 
 
 
 
 
 
 
104
 
105
- mode_input = gr.Dropdown(
106
- choices=["Transcribe", "Translate to English"],
107
- value="Transcribe",
108
- label="Mode",
109
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
- run_button = gr.Button("Start Processing")
 
 
 
 
 
 
 
 
112
 
113
- summary_output = gr.Textbox(label="Results", lines=12)
114
- download_output = gr.File(label="Download ZIP")
 
 
 
115
 
116
- run_button.click(
117
- fn=process_files,
118
- inputs=[files_input, mode_input],
119
- outputs=[summary_output, download_output],
120
  )
121
 
122
  if __name__ == "__main__":
 
1
+ """EchoScript v1.0 UI.
2
+
3
+ Implements the frozen v1.0 workflow:
4
+
5
+ Upload Audio -> Generate Canonical Transcript -> Preview Results
6
+ -> Generate Outputs -> Copy / Download
7
+
8
+ Services are instantiated lazily (on first use) rather than at import time,
9
+ so the app can start up without needing model weights on disk yet, and so
10
+ this module stays import-safe in environments without network access.
11
  """
12
 
13
+ from __future__ import annotations
14
+
15
  import tempfile
16
  from pathlib import Path
17
+ from typing import Optional
18
 
19
  import gradio as gr
20
 
21
+ from models.transcript import Transcript
22
+ from services.audio import AudioError, extract_window, resolve_window, validate_extension
23
  from services.subtitles import generate_srt, generate_vtt
24
+ from services.transcription import SUPPORTED_LANGUAGES, TranscriptionService
25
+ from services.translation import SUPPORTED_TARGET_LANGUAGES, TranslationService
26
 
27
+ # ---------------------------------------------------------------------------
28
+ # Lazy service singletons
29
+ # ---------------------------------------------------------------------------
 
 
 
 
30
 
31
+ _transcription_service: Optional[TranscriptionService] = None
32
+ _translation_service: Optional[TranslationService] = None
33
 
 
34
 
35
+ def get_transcription_service() -> TranscriptionService:
36
+ global _transcription_service
37
+ if _transcription_service is None:
38
+ _transcription_service = TranscriptionService(
39
+ model_size="base",
40
+ device="cpu",
41
+ compute_type="int8",
42
+ download_root="/tmp/whisper_models",
43
+ )
44
+ return _transcription_service
45
 
 
 
 
46
 
47
+ def get_translation_service() -> TranslationService:
48
+ global _translation_service
49
+ if _translation_service is None:
50
+ _translation_service = TranslationService()
51
+ return _translation_service
52
 
 
53
 
54
+ # ---------------------------------------------------------------------------
55
+ # UI <-> service-layer vocabulary
56
+ # ---------------------------------------------------------------------------
57
 
58
+ # "Source Language" dropdown: display name -> ISO 639-1 code (None = auto).
59
+ _NAME_TO_CODE = {name: code for code, name in SUPPORTED_LANGUAGES.items()}
60
+ SOURCE_LANGUAGE_CHOICES = ["Auto Detect"] + list(SUPPORTED_LANGUAGES.values())
 
 
 
61
 
62
+ # "Outputs" checkboxes: display label -> ISO 639-1 code for translations.
63
+ _TRANSLATION_LABEL_TO_CODE = {
64
+ f"{name} Translation": code for code, name in SUPPORTED_TARGET_LANGUAGES.items()
65
+ }
66
+ OUTPUT_CHOICES = ["Transcript"] + list(_TRANSLATION_LABEL_TO_CODE.keys())
67
+ DEFAULT_OUTPUTS = ["Transcript", "English Translation"]
68
+
69
+ # Order in which translation tabs are laid out (fixed; visibility toggles).
70
+ _TRANSLATION_TAB_ORDER = ["English Translation", "German Translation", "Persian Translation", "Spanish Translation"]
71
+
72
+
73
+ def _format_duration(seconds: float) -> str:
74
+ seconds = max(0, int(round(seconds)))
75
+ hours, remainder = divmod(seconds, 3600)
76
+ minutes, secs = divmod(remainder, 60)
77
+ return f"{hours:02}:{minutes:02}:{secs:02}"
78
+
79
+
80
+ def _write_text_file(text: str, tmp_dir: Path, filename: str) -> str:
81
+ path = tmp_dir / filename
82
+ path.write_text(text, encoding="utf-8")
83
+ return str(path)
 
 
 
84
 
 
85
 
86
+ # ---------------------------------------------------------------------------
87
+ # Main processing callback
88
+ # ---------------------------------------------------------------------------
89
+
90
+ def process_audio(
91
+ audio_path: Optional[str],
92
+ start_value: str,
93
+ end_value: str,
94
+ source_language_label: str,
95
+ selected_outputs: list[str],
96
+ ):
97
+ if not audio_path:
98
+ raise gr.Error("Please upload an audio file first.")
99
+
100
+ try:
101
+ validate_extension(audio_path)
102
+ start, end = resolve_window(start_value, end_value)
103
+ except AudioError as exc:
104
+ raise gr.Error(str(exc)) from exc
105
+
106
+ working_path = audio_path
107
+ if start is not None or end is not None:
108
+ try:
109
+ working_path = extract_window(audio_path, start, end)
110
+ except AudioError as exc:
111
+ raise gr.Error(str(exc)) from exc
112
+
113
+ language_code = _NAME_TO_CODE.get(source_language_label) # None = auto-detect
114
+
115
+ # Step 1: Audio -> canonical Transcript. This is the only step that
116
+ # touches the audio; everything below works off `transcript`.
117
+ transcript: Transcript = get_transcription_service().transcribe(
118
+ working_path,
119
+ source_filename=Path(audio_path).name,
120
+ language=language_code,
121
+ window_start=start,
122
+ window_end=end,
123
+ )
124
+
125
+ tmp_dir = Path(tempfile.mkdtemp(prefix="echoscript_"))
126
+
127
+ # --- Results dashboard ---
128
+ detected_language = SUPPORTED_LANGUAGES.get(transcript.language, transcript.language)
129
+ dashboard_md = (
130
+ f"### \u2713 {detected_language} detected\n\n"
131
+ f"**Confidence:** {transcript.language_probability:.0%}&nbsp;&nbsp;&nbsp;"
132
+ f"**Duration:** {_format_duration(transcript.duration)}&nbsp;&nbsp;&nbsp;"
133
+ f"**Words:** {transcript.word_count:,}"
134
+ )
135
+
136
+ # --- Transcript tab (always computed -- it's the source of truth --
137
+ # but only exposed as a tab if the user kept "Transcript" checked) ---
138
+ transcript_text = transcript.text
139
+ transcript_file = _write_text_file(transcript_text, tmp_dir, "transcript.txt")
140
+ transcript_tab_visible = "Transcript" in selected_outputs
141
+
142
+ # --- Translation tabs ---
143
+ translation_service = get_translation_service()
144
+ translation_updates = {} # label -> (text, file_path)
145
+ for label in _TRANSLATION_TAB_ORDER:
146
+ if label in selected_outputs:
147
+ code = _TRANSLATION_LABEL_TO_CODE[label]
148
+ translation = translation_service.translate(transcript, code)
149
+ text = translation.text
150
+ file_path = _write_text_file(text, tmp_dir, f"{code}.txt")
151
+ translation_updates[label] = (text, file_path)
152
+ else:
153
+ translation_updates[label] = (None, None)
154
+
155
+ # --- Subtitles (always derived from the transcript, the canonical
156
+ # source of truth -- never regenerated from audio) ---
157
+ srt_path = _write_text_file(generate_srt(transcript.segments), tmp_dir, "transcript.srt")
158
+ vtt_path = _write_text_file(generate_vtt(transcript.segments), tmp_dir, "transcript.vtt")
159
+
160
+ outputs = [dashboard_md, transcript_text, transcript_file, gr.update(visible=transcript_tab_visible)]
161
+
162
+ for label in _TRANSLATION_TAB_ORDER:
163
+ text, file_path = translation_updates[label]
164
+ visible = text is not None
165
+ outputs += [gr.update(visible=visible), text or "", file_path]
166
+
167
+ outputs += [srt_path, vtt_path]
168
+
169
+ return outputs
170
+
171
+
172
+ # ---------------------------------------------------------------------------
173
+ # UI layout
174
+ # ---------------------------------------------------------------------------
175
 
176
  with gr.Blocks(title="EchoScript") as demo:
177
 
 
179
  """
180
  # EchoScript
181
 
182
+ **Upload Audio &rarr; Generate Canonical Transcript &rarr; Preview Results &rarr; Generate Outputs &rarr; Copy / Download**
 
 
 
 
 
 
 
 
 
 
183
  """
184
  )
185
 
186
+ with gr.Row():
187
+ with gr.Column(scale=1):
188
+ gr.Markdown("### Upload Audio")
189
+ audio_input = gr.Audio(
190
+ label="Drop audio file here or click to browse",
191
+ sources=["upload"],
192
+ type="filepath",
193
+ )
194
+ gr.Markdown("Supported: mp3 &middot; wav &middot; m4a &middot; flac")
195
 
196
+ gr.Markdown("### Processing Window")
197
+ with gr.Row():
198
+ start_input = gr.Textbox(label="Start Time (optional)", placeholder="HH:MM:SS")
199
+ end_input = gr.Textbox(label="End Time (optional)", placeholder="HH:MM:SS")
200
+ gr.Markdown("Leave blank: entire file")
201
+
202
+ gr.Markdown("### Processing Options")
203
+ language_input = gr.Dropdown(
204
+ choices=SOURCE_LANGUAGE_CHOICES,
205
+ value="Auto Detect",
206
+ label="Source Language",
207
+ )
208
+
209
+ outputs_input = gr.CheckboxGroup(
210
+ choices=OUTPUT_CHOICES,
211
+ value=DEFAULT_OUTPUTS,
212
+ label="Outputs",
213
+ )
214
+
215
+ process_button = gr.Button("Generate Outputs", variant="primary")
216
+
217
+ with gr.Column(scale=2):
218
+ gr.Markdown("### Results Dashboard")
219
+ dashboard_output = gr.Markdown("Upload an audio file and click **Generate Outputs** to begin.")
220
+
221
+ with gr.Tabs():
222
+ with gr.Tab("Transcript") as transcript_tab:
223
+ transcript_box = gr.Textbox(
224
+ label="Transcript",
225
+ lines=16,
226
+ interactive=True,
227
+ buttons=["copy"],
228
+ )
229
+ transcript_download = gr.DownloadButton("Download TXT")
230
+
231
+ translation_tabs = {}
232
+ translation_boxes = {}
233
+ translation_downloads = {}
234
+ for label in _TRANSLATION_TAB_ORDER:
235
+ short_name = label.replace(" Translation", "")
236
+ with gr.Tab(short_name, visible=False) as tab:
237
+ box = gr.Textbox(
238
+ label=short_name,
239
+ lines=16,
240
+ interactive=True,
241
+ buttons=["copy"],
242
+ )
243
+ download = gr.DownloadButton("Download TXT")
244
+ translation_tabs[label] = tab
245
+ translation_boxes[label] = box
246
+ translation_downloads[label] = download
247
 
248
+ with gr.Tab("Subtitles"):
249
+ gr.Markdown(
250
+ "Subtitles are generated from the transcript "
251
+ "(source language), so they stay in sync no matter "
252
+ "which translations are also generated."
253
+ )
254
+ with gr.Row():
255
+ srt_download = gr.DownloadButton("Download SRT")
256
+ vtt_download = gr.DownloadButton("Download VTT")
257
 
258
+ # Build the flat outputs list in the exact order process_audio() returns.
259
+ click_outputs = [dashboard_output, transcript_box, transcript_download, transcript_tab]
260
+ for label in _TRANSLATION_TAB_ORDER:
261
+ click_outputs += [translation_tabs[label], translation_boxes[label], translation_downloads[label]]
262
+ click_outputs += [srt_download, vtt_download]
263
 
264
+ process_button.click(
265
+ fn=process_audio,
266
+ inputs=[audio_input, start_input, end_input, language_input, outputs_input],
267
+ outputs=click_outputs,
268
  )
269
 
270
  if __name__ == "__main__":
requirements.txt CHANGED
@@ -1,4 +1,4 @@
1
- gradio>=4.0
2
  faster-whisper>=1.0
3
  transformers>=4.40
4
  sentencepiece>=0.2
 
1
+ gradio>=6.0
2
  faster-whisper>=1.0
3
  transformers>=4.40
4
  sentencepiece>=0.2