usertea commited on
Commit
8c63258
·
1 Parent(s): 32a99dc

EchoScript : UI/UX Change , Stage 1 - Generate Transcript , Stage 1 - Generate Transcript

Browse files
Files changed (1) hide show
  1. app.py +184 -152
app.py CHANGED
@@ -1,36 +1,36 @@
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
- Two things make repeated clicks cheap:
13
-
14
- 1. Transcript caching. A `gr.State` holds the last Transcript together with
15
- the exact (audio path, time window, source language) signature that
16
- produced it. If "Generate Outputs" is clicked again with that signature
17
- unchanged -- e.g. only the Outputs checkboxes changed -- transcription
18
- is skipped entirely and the cached Transcript is reused.
19
- 2. Per-language translation caching. A second `gr.State` dict caches each
20
- Translation by language code, scoped to the current transcript
21
- signature. Selecting an additional language only translates that new
22
- language; languages already translated (even if briefly deselected and
23
- reselected) are reused rather than recomputed.
24
-
25
- Available translation languages are cascaded from two things:
26
-
27
- - Whether an Anthropic API key is present for this session (see
28
- services/translation.py for the offline-vs-Claude tradeoff). The key is
29
- used only for the request(s) made during this session and is never
30
- written to disk, logged, or cached anywhere server-side.
31
- - The source language, explicit or detected: translating a language into
32
- itself isn't offered. If "Auto Detect" is used, the list is re-filtered
33
- once the actual language is known, after transcription.
34
  """
35
 
36
  from __future__ import annotations
@@ -53,13 +53,8 @@ from services.translation import (
53
  )
54
 
55
  # ---------------------------------------------------------------------------
56
- # Lazy service singletons
57
- #
58
- # Safe to share across requests/users: TranscriptionService holds no
59
- # per-request state, and TranslationService resolves its backend (and
60
- # takes the API key, if any) fresh on every translate() call rather than
61
- # storing it -- see services/translation.py. Per-request caching of
62
- # results (not the services themselves) lives in gr.State, below.
63
  # ---------------------------------------------------------------------------
64
 
65
  _transcription_service: Optional[TranscriptionService] = None
@@ -100,32 +95,33 @@ _TRANSLATION_LABEL_TO_CODE = {
100
  }
101
 
102
  # Sections are pre-built for every language in the full superset (hidden by
103
- # default) so that toggling the API key or source language never needs to
104
- # add/remove components -- only which ones are visible changes.
105
  _TRANSLATION_SECTION_ORDER = list(_TRANSLATION_LABEL_TO_CODE.keys())
106
 
107
- DEFAULT_OUTPUTS = ["Transcript", "English Translation"]
108
 
109
-
110
- def _compute_output_choices(has_key: bool, exclude_code: Optional[str]) -> list[str]:
111
- """The Outputs checkbox list, cascaded from key presence + source language.
112
 
113
  `exclude_code` removes "translate into the language it's already in"
114
- from the list -- it's the explicit source language if one was chosen,
115
- or the detected language once transcription has run.
116
  """
117
  pool = ANTHROPIC_TARGET_LANGUAGES if has_key else MARIAN_TARGET_LANGUAGES
118
- return ["Transcript"] + [
119
- f"{name} Translation" for code, name in pool.items() if code != exclude_code
120
- ]
121
 
122
 
123
- def _on_key_or_source_change(api_key: str, source_language_label: str, current_value: list[str]):
124
- """Re-cascade the Outputs choices when the API key or source language changes."""
 
 
 
 
 
 
125
  has_key = bool((api_key or "").strip())
126
- exclude_code = _NAME_TO_CODE.get(source_language_label) # None when "Auto Detect"
127
- choices = _compute_output_choices(has_key, exclude_code)
128
- filtered_value = [v for v in (current_value or []) if v in choices] or ["Transcript"]
129
  return gr.update(choices=choices, value=filtered_value)
130
 
131
 
@@ -143,15 +139,14 @@ def _write_text_file(text: str, tmp_dir: Path, filename: str) -> str:
143
 
144
 
145
  # ---------------------------------------------------------------------------
146
- # Main processing callback
147
  # ---------------------------------------------------------------------------
148
 
149
- def process_audio(
150
  audio_path: Optional[str],
151
  start_value: str,
152
  end_value: str,
153
  source_language_label: str,
154
- selected_outputs: list[str],
155
  api_key: str,
156
  cached_transcript: Optional[Transcript],
157
  cached_signature,
@@ -168,13 +163,9 @@ def process_audio(
168
 
169
  source_code = _NAME_TO_CODE.get(source_language_label) # None = auto-detect
170
  signature = (audio_path, start, end, source_code)
 
171
 
172
- if cached_transcript is not None and cached_signature == signature:
173
- # Same file, same window, same forced source language as last time
174
- # -- the canonical Transcript hasn't changed, so skip Whisper
175
- # entirely and reuse it.
176
- transcript = cached_transcript
177
- else:
178
  working_path = audio_path
179
  if start is not None or end is not None:
180
  try:
@@ -182,8 +173,8 @@ def process_audio(
182
  except AudioError as exc:
183
  raise gr.Error(str(exc)) from exc
184
 
185
- # Step 1: Audio -> canonical Transcript. This is the only step
186
- # that touches the audio; everything below works off `transcript`.
187
  transcript = get_transcription_service().transcribe(
188
  working_path,
189
  source_filename=Path(audio_path).name,
@@ -193,10 +184,13 @@ def process_audio(
193
  )
194
  cached_signature = signature
195
  cached_translations = {} # old translations were derived from a different transcript
 
 
 
 
196
 
197
  tmp_dir = Path(tempfile.mkdtemp(prefix="echoscript_"))
198
 
199
- # --- Results dashboard ---
200
  detected_language = SUPPORTED_LANGUAGES.get(transcript.language, transcript.language)
201
  dashboard_md = (
202
  f"### \u2713 {detected_language} detected\n\n"
@@ -205,29 +199,63 @@ def process_audio(
205
  f"**Words:** {transcript.word_count:,}"
206
  )
207
 
208
- # Now that the actual language is known (important when "Auto Detect"
209
- # was used), re-cascade the Outputs choices to drop "translate into
210
- # the language it's already in" and drop that selection if it was
211
- # only chosen by default/before detection.
212
- has_key = bool((api_key or "").strip())
213
- updated_choices = _compute_output_choices(has_key, transcript.language)
214
- effective_outputs = [v for v in selected_outputs if v in updated_choices] or ["Transcript"]
215
- outputs_update = gr.update(choices=updated_choices, value=effective_outputs)
216
-
217
- # --- Transcript section (always computed -- it's the source of truth
218
- # -- but only exposed if the user kept "Transcript" checked) ---
219
  transcript_text = transcript.text
220
  transcript_file = _write_text_file(transcript_text, tmp_dir, "transcript.txt")
221
- transcript_visible = "Transcript" in effective_outputs
222
 
223
- # --- Translations: reuse anything already cached for this transcript;
224
- # only compute the languages newly selected since the last click. ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  translation_service = get_translation_service()
226
- section_updates = {} # label -> (visible, text, file_path)
227
 
228
  for label in _TRANSLATION_SECTION_ORDER:
229
  code = _TRANSLATION_LABEL_TO_CODE[label]
230
- if label not in effective_outputs:
231
  section_updates[label] = (False, "", None)
232
  continue
233
 
@@ -235,56 +263,42 @@ def process_audio(
235
  text = cached_translations[code]
236
  else:
237
  try:
238
- translation = translation_service.translate(transcript, code, api_key=api_key)
239
  text = translation.text
240
  cached_translations[code] = text
241
  except TranslationError as exc:
242
- # Surface the failure in that section rather than aborting
243
- # every other output that already succeeded. Deliberately
244
- # not cached, so it's retried on the next click.
245
  text = f"\u26a0\ufe0f Translation failed: {exc}"
246
 
247
  file_path = None
248
  if not text.startswith("\u26a0\ufe0f"):
 
249
  file_path = _write_text_file(text, tmp_dir, f"{code}.txt")
250
  section_updates[label] = (True, text, file_path)
251
 
252
- # --- Subtitles (always derived from the transcript, the canonical
253
- # source of truth -- never regenerated from audio) ---
254
- srt_path = _write_text_file(generate_srt(transcript.segments), tmp_dir, "transcript.srt")
255
- vtt_path = _write_text_file(generate_vtt(transcript.segments), tmp_dir, "transcript.vtt")
256
-
257
- outputs = [
258
- dashboard_md,
259
- transcript_text,
260
- transcript_file,
261
- gr.update(visible=transcript_visible),
262
- outputs_update,
263
- ]
264
-
265
  for label in _TRANSLATION_SECTION_ORDER:
266
  visible, text, file_path = section_updates[label]
267
  outputs += [gr.update(visible=visible), text, file_path]
268
-
269
- outputs += [srt_path, vtt_path, transcript, cached_signature, cached_translations]
270
-
271
  return outputs
272
 
273
 
274
  def reset_session_state():
275
- """Clear cached transcript/translations and the visible results."""
276
- cleared = [None, None, {}]
277
  ui_reset = [
278
- "Upload an audio file and click **Generate Outputs** to begin.",
279
  "",
280
  None,
 
 
281
  gr.update(visible=True),
282
- gr.update(choices=_compute_output_choices(has_key=False, exclude_code=None), value=DEFAULT_OUTPUTS),
283
  ]
284
  for _ in _TRANSLATION_SECTION_ORDER:
285
  ui_reset += [gr.update(visible=False), "", None]
286
- ui_reset += [None, None]
287
- return ui_reset + cleared
288
 
289
 
290
  # ---------------------------------------------------------------------------
@@ -301,9 +315,10 @@ with gr.Blocks(title="EchoScript") as demo:
301
  """
302
  # EchoScript
303
 
304
- **Upload Audio → Generate Canonical Transcript → Preview Results → Generate Outputs → Copy / Download**
 
305
 
306
- <sub>build: 2026-06-26 19:40 UTC &middot; cascading-languages + caching</sub>
307
  """
308
  )
309
 
@@ -328,6 +343,7 @@ with gr.Blocks(title="EchoScript") as demo:
328
  choices=SOURCE_LANGUAGE_CHOICES,
329
  value="Auto Detect",
330
  label="Source Language",
 
331
  )
332
 
333
  api_key_input = gr.Textbox(
@@ -341,37 +357,15 @@ with gr.Blocks(title="EchoScript") as demo:
341
  ),
342
  )
343
 
344
- outputs_input = gr.CheckboxGroup(
345
- choices=_compute_output_choices(has_key=False, exclude_code=None),
346
- value=DEFAULT_OUTPUTS,
347
- label="Outputs",
348
- )
349
-
350
- api_key_input.input(
351
- fn=_on_key_or_source_change,
352
- inputs=[api_key_input, language_input, outputs_input],
353
- outputs=[outputs_input],
354
- )
355
- api_key_input.change(
356
- fn=_on_key_or_source_change,
357
- inputs=[api_key_input, language_input, outputs_input],
358
- outputs=[outputs_input],
359
- )
360
- language_input.change(
361
- fn=_on_key_or_source_change,
362
- inputs=[api_key_input, language_input, outputs_input],
363
- outputs=[outputs_input],
364
- )
365
-
366
- process_button = gr.Button("Generate Outputs", variant="primary")
367
  reset_button = gr.Button("Reset (clear cache)", size="sm")
368
 
369
  with gr.Column(scale=2):
370
  gr.Markdown("### Results Dashboard")
371
- dashboard_output = gr.Markdown("Upload an audio file and click **Generate Outputs** to begin.")
372
 
373
  with gr.Tabs():
374
- with gr.Tab("Transcript") as transcript_tab:
375
  transcript_box = gr.Textbox(
376
  label="Transcript",
377
  lines=16,
@@ -381,11 +375,15 @@ with gr.Blocks(title="EchoScript") as demo:
381
  transcript_download = gr.DownloadButton("Download TXT")
382
 
383
  with gr.Tab("Translations"):
384
- gr.Markdown(
385
- "Every language selected in **Outputs** appears below at "
386
- "once -- nothing is hidden behind a tab you have to click "
387
- "through."
388
  )
 
 
 
 
 
389
  translation_groups = {}
390
  translation_boxes = {}
391
  translation_downloads = {}
@@ -406,24 +404,29 @@ with gr.Blocks(title="EchoScript") as demo:
406
  with gr.Tab("Subtitles"):
407
  gr.Markdown(
408
  "Subtitles are generated from the transcript "
409
- "(source language), so they stay in sync no matter "
410
- "which translations are also generated."
411
  )
412
  with gr.Row():
413
  srt_download = gr.DownloadButton("Download SRT")
414
  vtt_download = gr.DownloadButton("Download VTT")
415
 
416
- # Build the flat outputs list in the exact order process_audio() returns.
417
- click_outputs = [
418
  dashboard_output,
419
  transcript_box,
420
  transcript_download,
421
- transcript_tab,
422
- outputs_input,
 
423
  ]
424
  for label in _TRANSLATION_SECTION_ORDER:
425
- click_outputs += [translation_groups[label], translation_boxes[label], translation_downloads[label]]
426
- click_outputs += [
 
 
 
 
427
  srt_download,
428
  vtt_download,
429
  transcript_state,
@@ -431,23 +434,52 @@ with gr.Blocks(title="EchoScript") as demo:
431
  translations_state,
432
  ]
433
 
434
- process_button.click(
435
- fn=process_audio,
436
  inputs=[
437
  audio_input,
438
  start_input,
439
  end_input,
440
  language_input,
441
- outputs_input,
442
  api_key_input,
443
  transcript_state,
444
  signature_state,
445
  translations_state,
446
  ],
447
- outputs=click_outputs,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  )
449
 
450
- reset_button.click(fn=reset_session_state, outputs=click_outputs)
451
 
452
  if __name__ == "__main__":
453
  demo.launch()
 
1
  """EchoScript v1.0 UI.
2
 
3
+ Two-stage workflow, matching the frozen v1.0 product spec exactly:
4
+
5
+ Upload Audio -> Select Audio Window (optional)
6
+ -> Detect Language & Generate Canonical Transcript
7
+ -> Preview Transcript + available translation languages
8
+ -> Generate Translations -> Copy / Download
9
+
10
+ These are two distinct user actions, not one combined click:
11
+
12
+ 1. "Generate Transcript" -- audio, time window, and an optional source-
13
+ language hint go in; a canonical Transcript comes out (detected
14
+ language, confidence, duration, word count, full text). This is the
15
+ only step that touches the audio. Once it's done, the translation-
16
+ language picker appears, scoped to the language that was *actually*
17
+ detected (or forced) -- never a pre-detection guess.
18
+ 2. "Generate Translations" -- pick which languages to translate the
19
+ transcript into (the list depends on whether an Anthropic API key is
20
+ present: more languages with a key, the offline-safe set without one)
21
+ and click again. Always derived from the cached Transcript, never from
22
+ the audio.
23
+
24
+ Caching: a `gr.State` holds the last Transcript plus the exact (audio,
25
+ window, source-language) signature that produced it -- clicking "Generate
26
+ Transcript" again with that signature unchanged reuses it instead of
27
+ re-running Whisper. A second `gr.State` dict caches each Translation by
28
+ language code, scoped to the current transcript, so adding one more
29
+ language to "Generate Translations" doesn't redo the others, and
30
+ deselecting a language doesn't drop it from the cache (just hides it).
31
+
32
+ Services are instantiated lazily (on first use) rather than at import
33
+ time, so the app can start up without needing model weights on disk yet.
34
  """
35
 
36
  from __future__ import annotations
 
53
  )
54
 
55
  # ---------------------------------------------------------------------------
56
+ # Lazy service singletons -- safe to share across requests/users; see
57
+ # services/translation.py for why TranslationService holds no key state.
 
 
 
 
 
58
  # ---------------------------------------------------------------------------
59
 
60
  _transcription_service: Optional[TranscriptionService] = None
 
95
  }
96
 
97
  # Sections are pre-built for every language in the full superset (hidden by
98
+ # default) so that toggling the API key never needs to add/remove
99
+ # components -- only which ones are visible changes.
100
  _TRANSLATION_SECTION_ORDER = list(_TRANSLATION_LABEL_TO_CODE.keys())
101
 
 
102
 
103
+ def _compute_translation_choices(has_key: bool, exclude_code: Optional[str]) -> list[str]:
104
+ """The translate-to picker, cascaded from key presence + source language.
 
105
 
106
  `exclude_code` removes "translate into the language it's already in"
107
+ -- always the actual transcript language at this point (this picker
108
+ only exists once a Transcript does), never a pre-detection guess.
109
  """
110
  pool = ANTHROPIC_TARGET_LANGUAGES if has_key else MARIAN_TARGET_LANGUAGES
111
+ return [f"{name} Translation" for code, name in pool.items() if code != exclude_code]
 
 
112
 
113
 
114
+ def _on_api_key_change(api_key: str, cached_transcript: Optional[Transcript], current_value: list[str]):
115
+ """Re-cascade the translate-to picker live as the API key field changes.
116
+
117
+ No-op until a transcript exists -- the picker isn't shown before then,
118
+ so there's nothing yet to cascade.
119
+ """
120
+ if cached_transcript is None:
121
+ return gr.update()
122
  has_key = bool((api_key or "").strip())
123
+ choices = _compute_translation_choices(has_key, cached_transcript.language)
124
+ filtered_value = [v for v in (current_value or []) if v in choices]
 
125
  return gr.update(choices=choices, value=filtered_value)
126
 
127
 
 
139
 
140
 
141
  # ---------------------------------------------------------------------------
142
+ # Stage 1: Generate Transcript
143
  # ---------------------------------------------------------------------------
144
 
145
+ def generate_transcript(
146
  audio_path: Optional[str],
147
  start_value: str,
148
  end_value: str,
149
  source_language_label: str,
 
150
  api_key: str,
151
  cached_transcript: Optional[Transcript],
152
  cached_signature,
 
163
 
164
  source_code = _NAME_TO_CODE.get(source_language_label) # None = auto-detect
165
  signature = (audio_path, start, end, source_code)
166
+ regenerated = not (cached_transcript is not None and cached_signature == signature)
167
 
168
+ if regenerated:
 
 
 
 
 
169
  working_path = audio_path
170
  if start is not None or end is not None:
171
  try:
 
173
  except AudioError as exc:
174
  raise gr.Error(str(exc)) from exc
175
 
176
+ # The only step that touches the audio. Detection happens here too
177
+ # when no source language is forced.
178
  transcript = get_transcription_service().transcribe(
179
  working_path,
180
  source_filename=Path(audio_path).name,
 
184
  )
185
  cached_signature = signature
186
  cached_translations = {} # old translations were derived from a different transcript
187
+ else:
188
+ # Same audio, window, and source-language hint as last time --
189
+ # skip Whisper entirely and reuse the cached Transcript.
190
+ transcript = cached_transcript
191
 
192
  tmp_dir = Path(tempfile.mkdtemp(prefix="echoscript_"))
193
 
 
194
  detected_language = SUPPORTED_LANGUAGES.get(transcript.language, transcript.language)
195
  dashboard_md = (
196
  f"### \u2713 {detected_language} detected\n\n"
 
199
  f"**Words:** {transcript.word_count:,}"
200
  )
201
 
 
 
 
 
 
 
 
 
 
 
 
202
  transcript_text = transcript.text
203
  transcript_file = _write_text_file(transcript_text, tmp_dir, "transcript.txt")
 
204
 
205
+ # Now that the actual language is known -- the whole point of doing
206
+ # this as its own step -- compute the translate-to picker against it,
207
+ # never against a pre-detection guess.
208
+ has_key = bool((api_key or "").strip())
209
+ translation_choices = _compute_translation_choices(has_key, transcript.language)
210
+ default_targets = ["English Translation"] if "English Translation" in translation_choices else []
211
+ translate_choices_update = gr.update(choices=translation_choices, value=default_targets)
212
+
213
+ srt_path = _write_text_file(generate_srt(transcript.segments), tmp_dir, "transcript.srt")
214
+ vtt_path = _write_text_file(generate_vtt(transcript.segments), tmp_dir, "transcript.vtt")
215
+
216
+ # Per-language result sections: only reset (hide + clear) if the
217
+ # transcript actually changed. If it was reused, leave whatever
218
+ # translations are already showing exactly as they are.
219
+ section_outputs = []
220
+ if regenerated:
221
+ for _ in _TRANSLATION_SECTION_ORDER:
222
+ section_outputs += [gr.update(visible=False), "", None]
223
+ else:
224
+ for _ in _TRANSLATION_SECTION_ORDER:
225
+ section_outputs += [gr.update(), gr.update(), gr.update()]
226
+
227
+ outputs = [
228
+ dashboard_md,
229
+ transcript_text,
230
+ transcript_file,
231
+ translate_choices_update,
232
+ gr.update(visible=True), # reveal the "choose languages" picker
233
+ gr.update(visible=False), # hide the "generate a transcript first" placeholder
234
+ ]
235
+ outputs += section_outputs
236
+ outputs += [srt_path, vtt_path, transcript, cached_signature, cached_translations]
237
+ return outputs
238
+
239
+
240
+ # ---------------------------------------------------------------------------
241
+ # Stage 2: Generate Translations
242
+ # ---------------------------------------------------------------------------
243
+
244
+ def generate_translations(
245
+ selected_languages: list[str],
246
+ api_key: str,
247
+ cached_transcript: Optional[Transcript],
248
+ cached_translations: dict,
249
+ ):
250
+ if cached_transcript is None:
251
+ raise gr.Error("Generate a transcript first.")
252
+
253
  translation_service = get_translation_service()
254
+ section_updates = {}
255
 
256
  for label in _TRANSLATION_SECTION_ORDER:
257
  code = _TRANSLATION_LABEL_TO_CODE[label]
258
+ if label not in (selected_languages or []):
259
  section_updates[label] = (False, "", None)
260
  continue
261
 
 
263
  text = cached_translations[code]
264
  else:
265
  try:
266
+ translation = translation_service.translate(cached_transcript, code, api_key=api_key)
267
  text = translation.text
268
  cached_translations[code] = text
269
  except TranslationError as exc:
270
+ # Surface the failure for this language only; deliberately
271
+ # not cached, so the next click retries it.
 
272
  text = f"\u26a0\ufe0f Translation failed: {exc}"
273
 
274
  file_path = None
275
  if not text.startswith("\u26a0\ufe0f"):
276
+ tmp_dir = Path(tempfile.mkdtemp(prefix="echoscript_"))
277
  file_path = _write_text_file(text, tmp_dir, f"{code}.txt")
278
  section_updates[label] = (True, text, file_path)
279
 
280
+ outputs = []
 
 
 
 
 
 
 
 
 
 
 
 
281
  for label in _TRANSLATION_SECTION_ORDER:
282
  visible, text, file_path = section_updates[label]
283
  outputs += [gr.update(visible=visible), text, file_path]
284
+ outputs.append(cached_translations)
 
 
285
  return outputs
286
 
287
 
288
  def reset_session_state():
289
+ """Clear cached transcript/translations and everything on screen."""
 
290
  ui_reset = [
291
+ "Upload an audio file and click **Generate Transcript** to begin.",
292
  "",
293
  None,
294
+ gr.update(choices=[], value=[]),
295
+ gr.update(visible=False),
296
  gr.update(visible=True),
 
297
  ]
298
  for _ in _TRANSLATION_SECTION_ORDER:
299
  ui_reset += [gr.update(visible=False), "", None]
300
+ ui_reset += [None, None, None, None, {}]
301
+ return ui_reset
302
 
303
 
304
  # ---------------------------------------------------------------------------
 
315
  """
316
  # EchoScript
317
 
318
+ **Upload Audio &rarr; Select Audio Window &rarr; Detect Language & Generate Transcript
319
+ &rarr; Preview & Choose Languages &rarr; Generate Translations &rarr; Copy / Download**
320
 
321
+ <sub>build: 2026-06-27 00:05 UTC &middot; two-stage workflow (transcript, then translations)</sub>
322
  """
323
  )
324
 
 
343
  choices=SOURCE_LANGUAGE_CHOICES,
344
  value="Auto Detect",
345
  label="Source Language",
346
+ info="A hint for transcription, not a guess at translation targets.",
347
  )
348
 
349
  api_key_input = gr.Textbox(
 
357
  ),
358
  )
359
 
360
+ generate_transcript_button = gr.Button("Generate Transcript", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  reset_button = gr.Button("Reset (clear cache)", size="sm")
362
 
363
  with gr.Column(scale=2):
364
  gr.Markdown("### Results Dashboard")
365
+ dashboard_output = gr.Markdown("Upload an audio file and click **Generate Transcript** to begin.")
366
 
367
  with gr.Tabs():
368
+ with gr.Tab("Transcript"):
369
  transcript_box = gr.Textbox(
370
  label="Transcript",
371
  lines=16,
 
375
  transcript_download = gr.DownloadButton("Download TXT")
376
 
377
  with gr.Tab("Translations"):
378
+ translations_placeholder = gr.Markdown(
379
+ "Generate a transcript first to see the languages available to "
380
+ "translate it into."
 
381
  )
382
+ with gr.Group(visible=False) as translations_picker_group:
383
+ gr.Markdown("Translate to:")
384
+ translate_choices_input = gr.CheckboxGroup(choices=[], value=[], label=None)
385
+ generate_translations_button = gr.Button("Generate Translations", variant="primary")
386
+
387
  translation_groups = {}
388
  translation_boxes = {}
389
  translation_downloads = {}
 
404
  with gr.Tab("Subtitles"):
405
  gr.Markdown(
406
  "Subtitles are generated from the transcript "
407
+ "(source language) as soon as it's ready -- no "
408
+ "translation needed."
409
  )
410
  with gr.Row():
411
  srt_download = gr.DownloadButton("Download SRT")
412
  vtt_download = gr.DownloadButton("Download VTT")
413
 
414
+ # Outputs shared by Stage 1 (Generate Transcript) and Reset.
415
+ transcript_stage_outputs = [
416
  dashboard_output,
417
  transcript_box,
418
  transcript_download,
419
+ translate_choices_input,
420
+ translations_picker_group,
421
+ translations_placeholder,
422
  ]
423
  for label in _TRANSLATION_SECTION_ORDER:
424
+ transcript_stage_outputs += [
425
+ translation_groups[label],
426
+ translation_boxes[label],
427
+ translation_downloads[label],
428
+ ]
429
+ transcript_stage_outputs += [
430
  srt_download,
431
  vtt_download,
432
  transcript_state,
 
434
  translations_state,
435
  ]
436
 
437
+ generate_transcript_button.click(
438
+ fn=generate_transcript,
439
  inputs=[
440
  audio_input,
441
  start_input,
442
  end_input,
443
  language_input,
 
444
  api_key_input,
445
  transcript_state,
446
  signature_state,
447
  translations_state,
448
  ],
449
+ outputs=transcript_stage_outputs,
450
+ )
451
+
452
+ # Outputs for Stage 2 (Generate Translations): just the per-language
453
+ # sections plus the translation cache.
454
+ translation_stage_outputs = []
455
+ for label in _TRANSLATION_SECTION_ORDER:
456
+ translation_stage_outputs += [
457
+ translation_groups[label],
458
+ translation_boxes[label],
459
+ translation_downloads[label],
460
+ ]
461
+ translation_stage_outputs += [translations_state]
462
+
463
+ generate_translations_button.click(
464
+ fn=generate_translations,
465
+ inputs=[translate_choices_input, api_key_input, transcript_state, translations_state],
466
+ outputs=translation_stage_outputs,
467
+ )
468
+
469
+ # Cascade the translate-to picker live as the key changes, once a
470
+ # transcript exists (no-op before that -- the picker isn't shown yet).
471
+ api_key_input.input(
472
+ fn=_on_api_key_change,
473
+ inputs=[api_key_input, transcript_state, translate_choices_input],
474
+ outputs=[translate_choices_input],
475
+ )
476
+ api_key_input.change(
477
+ fn=_on_api_key_change,
478
+ inputs=[api_key_input, transcript_state, translate_choices_input],
479
+ outputs=[translate_choices_input],
480
  )
481
 
482
+ reset_button.click(fn=reset_session_state, outputs=transcript_stage_outputs)
483
 
484
  if __name__ == "__main__":
485
  demo.launch()