Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions src/scripto/core/languages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {}
Expand Down Expand Up @@ -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"))
16 changes: 12 additions & 4 deletions src/scripto/core/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions src/scripto/translate/stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions tests/test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 18 additions & 0 deletions tests/test_translate_stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 == []
Loading