From 8012ae0fbc839b70d8d80bb16d8e30e6d04c58cd Mon Sep 17 00:00:00 2001 From: TN019 Date: Tue, 4 Aug 2026 23:15:21 +1000 Subject: [PATCH] Recognize alias subtitle suffixes (.cn, .chs, .eng, ...) as existing outputs instead of re-translating. --- src/scripto/core/languages.py | 19 +++++++++++++++++-- src/scripto/core/output.py | 16 ++++++++++++---- src/scripto/translate/stage.py | 9 +++++++-- tests/test_output.py | 14 ++++++++++++++ tests/test_translate_stage.py | 18 ++++++++++++++++++ 5 files changed, 68 insertions(+), 8 deletions(-) diff --git a/src/scripto/core/languages.py b/src/scripto/core/languages.py index e9c3a55..8ce3cd3 100644 --- a/src/scripto/core/languages.py +++ b/src/scripto/core/languages.py @@ -16,6 +16,10 @@ class LanguageSpec: code: str # config value, e.g. "zh" prompt_name: str # name used inside translation prompts suffix: str # filename suffix, e.g. ".zh" + # Foreign suffixes accepted as this language when *detecting* existing + # outputs (subtitles named by other tools / by hand). Writing always + # uses ``suffix``. + aliases: tuple[str, ...] = () _REGISTRY: dict[str, LanguageSpec] = {} @@ -43,7 +47,18 @@ def suffix_map() -> dict[str, str]: return {spec.code: spec.suffix for spec in _REGISTRY.values()} -register_language(LanguageSpec(code="en", prompt_name="English", suffix=".en")) -register_language(LanguageSpec(code="zh", prompt_name="Simplified Chinese", suffix=".zh")) +def alias_suffixes(code: str) -> tuple[str, ...]: + """Detection-only suffixes for ``code`` (empty for unknown codes).""" + spec = _REGISTRY.get(code) + return spec.aliases if spec else () + + +register_language(LanguageSpec( + code="en", prompt_name="English", suffix=".en", aliases=(".eng",), +)) +register_language(LanguageSpec( + code="zh", prompt_name="Simplified Chinese", suffix=".zh", + aliases=(".cn", ".chs", ".zh-cn", ".zh-hans", ".zh_cn"), +)) register_language(LanguageSpec(code="ja", prompt_name="Japanese", suffix=".ja")) register_language(LanguageSpec(code="ko", prompt_name="Korean", suffix=".ko")) diff --git a/src/scripto/core/output.py b/src/scripto/core/output.py index 16a216d..3787887 100644 --- a/src/scripto/core/output.py +++ b/src/scripto/core/output.py @@ -16,7 +16,7 @@ from pathlib import Path from ..engines.base import TranscribeResult -from .languages import suffix_map +from .languages import alias_suffixes, known_languages, suffix_map DEFAULT_SUFFIXES = suffix_map() # single source of truth: core/languages.py FORMATS = ("srt", "txt", "vtt", "json") @@ -64,14 +64,22 @@ def existing_transcript( Export-dir runs always re-check the exact default name only. """ suffixes = suffix_map or DEFAULT_SUFFIXES + directory = export_dir if export_dir is not None else source.parent if language: path = output_path( source, language=language, fmt=fmt, suffix_map=suffix_map, export_dir=export_dir, ) - return path if path.exists() else None - directory = export_dir if export_dir is not None else source.parent - for suffix in suffixes.values(): + if path.exists(): + return path + for suffix in alias_suffixes(language): + candidate = directory / f"{source.stem}{suffix}.{fmt}" + if candidate.exists(): + return candidate + return None + detect = list(suffixes.values()) + detect += [a for spec in known_languages() for a in spec.aliases] + for suffix in detect: candidate = directory / f"{source.stem}{suffix}.{fmt}" if candidate.exists(): return candidate diff --git a/src/scripto/translate/stage.py b/src/scripto/translate/stage.py index d38ae38..c2d1858 100644 --- a/src/scripto/translate/stage.py +++ b/src/scripto/translate/stage.py @@ -64,8 +64,13 @@ def translate( if out_path == srt_path: # transcript already carries the target language suffix — nothing to do return [] - if out_path.exists() and not self._overwrite: - return [out_path] + if not self._overwrite: + # Accept alias-named files (lecture.cn.srt dropped in by hand or + # by another tool) as the existing translation — never redo it. + for suffix in (self._target.suffix, *self._target.aliases): + candidate = source.with_name(f"{source.stem}{suffix}.srt") + if candidate != srt_path and candidate.exists(): + return [candidate] content = srt_path.read_text(encoding="utf-8") translated = self.translate_content(content, stop_check=stop_check, progress=progress) diff --git a/tests/test_output.py b/tests/test_output.py index cddfdf3..6834292 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -77,3 +77,17 @@ def test_vtt_txt_json_writers(tmp_path): payload = json.loads((tmp_path / "o.json").read_text(encoding="utf-8")) assert payload["language"] == "en" assert len(payload["segments"]) == 2 + + +def test_existing_transcript_accepts_alias_suffixes(tmp_path): + from scripto.core.output import existing_transcript + + source = tmp_path / "lecture.mp4" + source.write_bytes(b"x") + (tmp_path / "lecture.cn.srt").write_text("1\n", encoding="utf-8") + + forced = existing_transcript(source, fmt="srt", language="zh") + assert forced is not None and forced.name == "lecture.cn.srt" + auto = existing_transcript(source, fmt="srt", language=None) + assert auto is not None and auto.name == "lecture.cn.srt" + assert existing_transcript(source, fmt="srt", language="ja") is None diff --git a/tests/test_translate_stage.py b/tests/test_translate_stage.py index d213a87..c6f7800 100644 --- a/tests/test_translate_stage.py +++ b/tests/test_translate_stage.py @@ -157,3 +157,21 @@ def test_release_unloads_model(tmp_path): stage, _src, _srt = make_stage(client, tmp_path) stage.release() assert client.unloaded + + +def test_translate_skips_when_alias_named_translation_exists(tmp_path): + # A hand-dropped lecture.cn.srt counts as the zh translation: returned + # as-is, nothing sent to the model. + source = tmp_path / "lecture.mp4" + source.write_bytes(b"x") + transcript = tmp_path / "lecture.en.srt" + transcript.write_text(SAMPLE, encoding="utf-8") + existing = tmp_path / "lecture.cn.srt" + existing.write_text(SAMPLE, encoding="utf-8") + + client = FakeClient() + stage = OllamaTranslateStage(client, model="m", target="zh") + out = stage.translate(transcript, source) + + assert out == [existing] + assert client.calls == []