fix: Originalsprache bei Videos mit KI-synchronisierten Tonspuren
Gefunden mit einem englischen Video (0LmCveTM788): ohne -l wurde Arabisch gewählt, denn jede der 20 Synchron-Tonspuren hat eine eigene „-orig“-Spur. - original_language(): aus „language“, sonst Tonspur mit „original“, sonst die einzige „-orig“-Spur - Automatische Untertitel nur in der Originalsprache (keine Synchronisation) - Sprachen auch mit Region: „en“ findet manuelle Untertitel „en-US“ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7e6ffa5899
commit
547dcb358d
4 changed files with 108 additions and 19 deletions
|
|
@ -87,6 +87,12 @@ Automatische Untertitel ohne `-orig` sind Maschinenübersetzungen aus einer ande
|
|||
Sprache und werden nie genommen. Gibt es keine passende Spur, nennt das Skript die
|
||||
vorhandenen Sprachen.
|
||||
|
||||
Videos mit KI-synchronisierten Tonspuren haben für *jede* Synchronsprache eine
|
||||
`-orig`-Spur. Die Originalsprache kommt deshalb aus den Video-Infos (`language`, sonst
|
||||
die Tonspur mit dem Vermerk „original“); automatische Untertitel anderer Sprachen
|
||||
gehören zur Synchronisation und werden nicht genommen. Sprachen werden auch mit
|
||||
Region gefunden: `-l en` findet manuelle Untertitel `en-US`.
|
||||
|
||||
```bash
|
||||
./yt_untertitel.py -u URL # → <Video-ID>.de-orig.vtt
|
||||
./yt_untertitel.py -u URL -n Lahnstein -d testdaten # → testdaten/Lahnstein.de-orig.vtt
|
||||
|
|
|
|||
|
|
@ -46,6 +46,19 @@ def test_default_language_is_original_audio(fake_yt_dlp, tmp_path, monkeypatch,
|
|||
assert [p.name for p in tmp_path.iterdir()] == ["output.txt"]
|
||||
|
||||
|
||||
def test_default_language_ignores_dubbed_tracks(fake_yt_dlp, tmp_path, monkeypatch):
|
||||
info = {
|
||||
"id": "dub", "language": "en-US", "subtitles": {"en-US": []},
|
||||
"automatic_captions": {"ar-orig": [], "en-orig": []},
|
||||
}
|
||||
run = fake_yt_dlp(info, vtt=VTT)
|
||||
monkeypatch.setattr(yt.subprocess, "run", run)
|
||||
run_main(monkeypatch, "-u", URL, "-o", tmp_path / "out.txt")
|
||||
download = run.calls[1]
|
||||
assert download[download.index("--sub-langs") + 1] == "en-US"
|
||||
assert "--write-subs" in download
|
||||
|
||||
|
||||
def test_fliesstext(fake_yt_dlp, tmp_path, monkeypatch, capsys):
|
||||
long_vtt = "WEBVTT\n\n" + "".join(
|
||||
f"00:00:{i:02d}.000 --> 00:00:{i + 1:02d}.000\nSatz Nummer {i} zum EU-Gipfel.\n\n"
|
||||
|
|
|
|||
|
|
@ -47,6 +47,49 @@ def test_original_language(info, expected):
|
|||
assert yt.original_language(info) == expected
|
||||
|
||||
|
||||
# English original with AI-dubbed audio tracks: every dub has its own "-orig"
|
||||
DUBBED = {
|
||||
"id": "dub",
|
||||
"language": "en-US",
|
||||
"subtitles": {"en-US": []},
|
||||
"automatic_captions": {"ar-orig": [], "en-orig": [], "de-DE-orig": [], "de": []},
|
||||
}
|
||||
DUBBED_NO_METADATA = {
|
||||
"formats": [
|
||||
{"format_note": "Arabic, low", "language": "ar"},
|
||||
{"format_note": "English (US) original (default), low", "language": "en-US"},
|
||||
],
|
||||
"subtitles": {},
|
||||
"automatic_captions": {"ar-orig": [], "en-orig": [], "de-DE-orig": []},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("info, lang, expected", [
|
||||
(DUBBED, "en", ("en-US", False)), # region variant of a manual track
|
||||
(DUBBED, "en-US", ("en-US", False)),
|
||||
(DUBBED, "de", None), # only the German AI dub
|
||||
(DUBBED, "ar", None),
|
||||
(DUBBED_NO_METADATA, "en-US", ("en-orig", True)),
|
||||
(DUBBED_NO_METADATA, "en", ("en-orig", True)),
|
||||
(DUBBED_NO_METADATA, "de", None),
|
||||
])
|
||||
def test_choose_track_with_dubbed_audio(info, lang, expected):
|
||||
assert yt.choose_track(info, lang) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("info, expected", [
|
||||
(DUBBED, "en-US"),
|
||||
(DUBBED_NO_METADATA, "en-US"), # from the audio track marked original
|
||||
({"automatic_captions": {"ar-orig": [], "en-orig": []}}, None),
|
||||
])
|
||||
def test_original_language_with_dubbed_audio(info, expected):
|
||||
assert yt.original_language(info) == expected
|
||||
|
||||
|
||||
def test_available_tracks_hides_dubs():
|
||||
assert yt.available_tracks(DUBBED) == (["en-US"], ["en"])
|
||||
|
||||
|
||||
def test_available_tracks():
|
||||
info = {"subtitles": {"fr": []}, "automatic_captions": {"de-orig": [], "de": []}}
|
||||
assert yt.available_tracks(info) == (["fr"], ["de"])
|
||||
|
|
|
|||
|
|
@ -31,39 +31,66 @@ def fetch_info(url):
|
|||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def choose_track(info, lang):
|
||||
"""Pick the subtitle track for `lang`: (track name, is_auto) or None.
|
||||
|
||||
Manual subtitles come first. Of the automatic captions only the original
|
||||
language ("<lang>-orig") is used: plain "<lang>" there is a machine
|
||||
translation whenever the video is in another language.
|
||||
"""
|
||||
if lang in (info.get("subtitles") or {}):
|
||||
return lang, False
|
||||
if f"{lang}-orig" in (info.get("automatic_captions") or {}):
|
||||
return f"{lang}-orig", True
|
||||
return None
|
||||
def base_language(code):
|
||||
""""en-US" -> "en", "de-orig" -> "de"."""
|
||||
return code.removesuffix("-orig").split("-")[0].lower()
|
||||
|
||||
|
||||
def original_language(info):
|
||||
"""Language of the video's audio, or None if yt-dlp does not know it.
|
||||
"""Language of the video's original audio, or None if unknown.
|
||||
|
||||
YouTube marks the automatic captions in the spoken language with "-orig".
|
||||
Not simply the first "<lang>-orig" caption track: videos with AI-dubbed
|
||||
audio tracks have such a track for every dubbed language.
|
||||
"""
|
||||
for name in info.get("automatic_captions") or {}:
|
||||
if name.endswith("-orig"):
|
||||
return name.removesuffix("-orig")
|
||||
return info.get("language")
|
||||
if info.get("language"):
|
||||
return info["language"]
|
||||
for fmt in info.get("formats") or []:
|
||||
if "original" in (fmt.get("format_note") or "") and fmt.get("language"):
|
||||
return fmt["language"]
|
||||
originals = [n for n in info.get("automatic_captions") or {} if n.endswith("-orig")]
|
||||
return originals[0].removesuffix("-orig") if len(originals) == 1 else None
|
||||
|
||||
|
||||
def choose_track(info, lang):
|
||||
"""Pick the subtitle track for `lang`: (track name, is_auto) or None.
|
||||
|
||||
Manual subtitles come first, also with a region ("en" finds "en-US").
|
||||
Automatic captions are used only in the language of the original audio
|
||||
("<lang>-orig"): plain "<lang>" is a machine translation, and "-orig"
|
||||
tracks of other languages belong to AI-dubbed audio tracks.
|
||||
"""
|
||||
manual = info.get("subtitles") or {}
|
||||
if lang in manual:
|
||||
return lang, False
|
||||
same_language = sorted(n for n in manual if base_language(n) == base_language(lang))
|
||||
if same_language:
|
||||
return same_language[0], False
|
||||
|
||||
original = original_language(info)
|
||||
if original and base_language(original) != base_language(lang):
|
||||
return None
|
||||
auto = info.get("automatic_captions") or {}
|
||||
candidates = [f"{lang}-orig", f"{base_language(lang)}-orig"] + sorted(
|
||||
n for n in auto
|
||||
if n.endswith("-orig") and base_language(n) == base_language(lang)
|
||||
)
|
||||
for name in candidates:
|
||||
if name in auto:
|
||||
return name, True
|
||||
return None
|
||||
|
||||
|
||||
def available_tracks(info):
|
||||
"""Manual tracks and the automatic ones usable for this program."""
|
||||
manual = sorted((info.get("subtitles") or {}).keys())
|
||||
original = sorted(
|
||||
original = original_language(info)
|
||||
automatic = sorted(
|
||||
name.removesuffix("-orig")
|
||||
for name in (info.get("automatic_captions") or {})
|
||||
if name.endswith("-orig")
|
||||
and (original is None or base_language(name) == base_language(original))
|
||||
)
|
||||
return manual, original
|
||||
return manual, automatic
|
||||
|
||||
|
||||
def no_track_message(info, lang):
|
||||
|
|
|
|||
Loading…
Reference in a new issue