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

EchoScript : 1 & 2 - the static 4-language list was the real bug. 3 & 4 - already correct, verified by audit.

Browse files
Files changed (4) hide show
  1. app.py +13 -6
  2. requirements.txt +1 -0
  3. services/__init__.py +2 -2
  4. services/translation.py +56 -13
app.py CHANGED
@@ -47,9 +47,9 @@ from services.subtitles import generate_srt, generate_vtt
47
  from services.transcription import SUPPORTED_LANGUAGES, TranscriptionService
48
  from services.translation import (
49
  ANTHROPIC_TARGET_LANGUAGES,
50
- MARIAN_TARGET_LANGUAGES,
51
  TranslationError,
52
  TranslationService,
 
53
  )
54
 
55
  # ---------------------------------------------------------------------------
@@ -103,11 +103,18 @@ _TRANSLATION_SECTION_ORDER = list(_TRANSLATION_LABEL_TO_CODE.keys())
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
 
@@ -318,7 +325,7 @@ with gr.Blocks(title="EchoScript") as demo:
318
  **Upload Audio → Select Audio Window → Detect Language & Generate Transcript
319
  → Preview & Choose Languages → Generate Translations → Copy / Download**
320
 
321
- <sub>build: 2026-06-27 00:05 UTC &middot; two-stage workflow (transcript, then translations)</sub>
322
  """
323
  )
324
 
 
47
  from services.transcription import SUPPORTED_LANGUAGES, TranscriptionService
48
  from services.translation import (
49
  ANTHROPIC_TARGET_LANGUAGES,
 
50
  TranslationError,
51
  TranslationService,
52
+ available_marian_targets,
53
  )
54
 
55
  # ---------------------------------------------------------------------------
 
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` is the actual transcript language at this point (this
107
+ picker only exists once a Transcript does), never a pre-detection
108
+ guess. Without a key, the Marian options are checked for real against
109
+ the Hub for this specific source language -- not a fixed guess -- so
110
+ a source with no path to a given language (direct or English-pivot)
111
+ simply won't offer it, and one with good coverage offers everything
112
+ that's actually reachable (which can be more than four languages).
113
  """
114
+ if has_key:
115
+ pool = ANTHROPIC_TARGET_LANGUAGES
116
+ else:
117
+ pool = available_marian_targets(exclude_code) if exclude_code else {}
118
  return [f"{name} Translation" for code, name in pool.items() if code != exclude_code]
119
 
120
 
 
325
  **Upload Audio &rarr; Select Audio Window &rarr; Detect Language & Generate Transcript
326
  &rarr; Preview & Choose Languages &rarr; Generate Translations &rarr; Copy / Download**
327
 
328
+ <sub>build: 2026-06-27 00:24 UTC &middot; real per-source-language Marian availability (no more fixed 4-language guess)</sub>
329
  """
330
  )
331
 
requirements.txt CHANGED
@@ -5,3 +5,4 @@ sentencepiece>=0.2
5
  sacremoses>=0.1
6
  torch>=2.0
7
  anthropic>=0.40
 
 
5
  sacremoses>=0.1
6
  torch>=2.0
7
  anthropic>=0.40
8
+ huggingface_hub>=0.24
services/__init__.py CHANGED
@@ -3,9 +3,9 @@ from services.subtitles import generate_srt, generate_vtt
3
  from services.transcription import SUPPORTED_LANGUAGES, TranscriptionService
4
  from services.translation import (
5
  ANTHROPIC_TARGET_LANGUAGES,
6
- MARIAN_TARGET_LANGUAGES,
7
  TranslationError,
8
  TranslationService,
 
9
  )
10
 
11
  __all__ = [
@@ -18,7 +18,7 @@ __all__ = [
18
  "SUPPORTED_LANGUAGES",
19
  "TranscriptionService",
20
  "ANTHROPIC_TARGET_LANGUAGES",
21
- "MARIAN_TARGET_LANGUAGES",
22
  "TranslationError",
23
  "TranslationService",
24
  ]
 
3
  from services.transcription import SUPPORTED_LANGUAGES, TranscriptionService
4
  from services.translation import (
5
  ANTHROPIC_TARGET_LANGUAGES,
 
6
  TranslationError,
7
  TranslationService,
8
+ available_marian_targets,
9
  )
10
 
11
  __all__ = [
 
18
  "SUPPORTED_LANGUAGES",
19
  "TranscriptionService",
20
  "ANTHROPIC_TARGET_LANGUAGES",
21
+ "available_marian_targets",
22
  "TranslationError",
23
  "TranslationService",
24
  ]
services/translation.py CHANGED
@@ -57,16 +57,9 @@ LANGUAGE_NAMES: dict[str, str] = {
57
  "ar": "Arabic",
58
  "ru": "Russian",
59
  "tr": "Turkish",
60
- }
61
-
62
- # Target languages offered in the "Outputs" checkboxes when NO Anthropic
63
- # API key is supplied -- restricted to the set MarianMT can reliably
64
- # reach (directly, or via an English pivot; see _resolve_marian_engines).
65
- MARIAN_TARGET_LANGUAGES: dict[str, str] = {
66
- "en": "English",
67
- "de": "German",
68
- "fa": "Persian",
69
- "es": "Spanish",
70
  }
71
 
72
  # Target languages offered once the person supplies their own Anthropic
@@ -74,9 +67,6 @@ MARIAN_TARGET_LANGUAGES: dict[str, str] = {
74
  # "every language EchoScript knows the name of".
75
  ANTHROPIC_TARGET_LANGUAGES: dict[str, str] = dict(LANGUAGE_NAMES)
76
 
77
- # Backwards-compatible alias (kept in case other modules import this name).
78
- SUPPORTED_TARGET_LANGUAGES = MARIAN_TARGET_LANGUAGES
79
-
80
  # Anthropic model used for translation -- Haiku is fast and inexpensive,
81
  # which fits well for what is otherwise a mechanical translation task.
82
  _ANTHROPIC_MODEL = "claude-haiku-4-5-20251001"
@@ -214,6 +204,59 @@ def _try_load_marian_engine(source_language: str, target_language: str):
214
  return tokenizer, model
215
 
216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  def _resolve_marian_engines(source_language: str, target_language: str) -> list[tuple]:
218
  """Work out which model(s) to chain to get from source to target.
219
 
 
57
  "ar": "Arabic",
58
  "ru": "Russian",
59
  "tr": "Turkish",
60
+ "ja": "Japanese",
61
+ "zh": "Chinese",
62
+ "ko": "Korean",
 
 
 
 
 
 
 
63
  }
64
 
65
  # Target languages offered once the person supplies their own Anthropic
 
67
  # "every language EchoScript knows the name of".
68
  ANTHROPIC_TARGET_LANGUAGES: dict[str, str] = dict(LANGUAGE_NAMES)
69
 
 
 
 
70
  # Anthropic model used for translation -- Haiku is fast and inexpensive,
71
  # which fits well for what is otherwise a mechanical translation task.
72
  _ANTHROPIC_MODEL = "claude-haiku-4-5-20251001"
 
204
  return tokenizer, model
205
 
206
 
207
+ @lru_cache(maxsize=None)
208
+ def _marian_model_exists(source_language: str, target_language: str) -> bool:
209
+ """Check (and cache) whether Helsinki-NLP publishes this exact pair.
210
+
211
+ This is a lightweight existence check (one small metadata request to
212
+ the Hub's model-info API), not a full model/tokenizer download --
213
+ deliberately kept separate from _try_load_marian_engine so the UI can
214
+ cheaply ask "what's actually available for this source language"
215
+ without paying the cost of downloading every candidate model.
216
+ Cached forever per pair: this is public catalog data, not something
217
+ that changes mid-session, and reusing it means the second person (or
218
+ the second transcript) asking about the same source language is
219
+ instant.
220
+ """
221
+ from huggingface_hub import HfApi
222
+ from huggingface_hub.utils import HfHubHTTPError
223
+
224
+ try:
225
+ HfApi().model_info(f"Helsinki-NLP/opus-mt-{source_language}-{target_language}")
226
+ return True
227
+ except HfHubHTTPError:
228
+ return False
229
+ except Exception: # pragma: no cover - network hiccup, treat as unavailable
230
+ return False
231
+
232
+
233
+ def _marian_path_exists(source_language: str, target_language: str) -> bool:
234
+ """Direct pair, or a source->en->target pivot, whichever is real."""
235
+ if source_language == target_language:
236
+ return False
237
+ if _marian_model_exists(source_language, target_language):
238
+ return True
239
+ if source_language != "en" and target_language != "en":
240
+ return _marian_model_exists(source_language, "en") and _marian_model_exists("en", target_language)
241
+ return False
242
+
243
+
244
+ def available_marian_targets(source_language: str) -> dict[str, str]:
245
+ """Every language MarianMT can actually reach from `source_language`.
246
+
247
+ Checked for real against the Hub (direct pair or English pivot) for
248
+ each candidate in LANGUAGE_NAMES, rather than assumed from a fixed
249
+ list -- this is what makes the offered languages correct per source
250
+ language instead of a one-size-fits-all guess (e.g. Persian is only
251
+ offered for a source where a path genuinely exists).
252
+ """
253
+ return {
254
+ code: name
255
+ for code, name in LANGUAGE_NAMES.items()
256
+ if code != source_language and _marian_path_exists(source_language, code)
257
+ }
258
+
259
+
260
  def _resolve_marian_engines(source_language: str, target_language: str) -> list[tuple]:
261
  """Work out which model(s) to chain to get from source to target.
262