Spaces:
Sleeping
Sleeping
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- app.py +13 -6
- requirements.txt +1 -0
- services/__init__.py +2 -2
- 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`
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
"""
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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 → Select Audio Window → Detect Language & Generate Transcript
|
| 326 |
→ Preview & Choose Languages → Generate Translations → Copy / Download**
|
| 327 |
|
| 328 |
+
<sub>build: 2026-06-27 00:24 UTC · 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 |
-
"
|
| 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 |
-
|
| 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 |
|