feat: Hinweis, wenn ein Zeitbereich über das Ende der Untertitel reicht

„-z 0-05:00“ bedeutet fünf Stunden und bearbeitete bisher stillschweigend
das ganze Video. vtt2txt.py (und damit extract_text.py und
abschrift_korrigieren.py) nennt jetzt das Ende der Untertitel, bei
Bereichen ab einer Stunde mit dem Tipp „Minute 5 ist 00:05“.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-10-11 21:58:12 +02:00
commit d9512bcedc
4 changed files with 57 additions and 5 deletions

View file

@ -559,6 +559,8 @@ Option kennen `extract_text.py` und `vtt2txt.py`.
> **Vorsicht:** `05:00` bedeutet 5 Stunden, nicht 5 Minuten. Für „5 Minuten“ > **Vorsicht:** `05:00` bedeutet 5 Stunden, nicht 5 Minuten. Für „5 Minuten“
> `00:05` oder `5` schreiben, für 5 Minuten 30 Sekunden `00:05:30`. > `00:05` oder `5` schreiben, für 5 Minuten 30 Sekunden `00:05:30`.
> Reicht ein Bereich über das Ende des Videos hinaus, weist das Programm darauf hin
> und nennt die Länge der Untertitel.
### 10.2 Bereiche ### 10.2 Bereiche
@ -712,7 +714,8 @@ Weitere Einstellungen (Abschnittsgröße, Absatzlängen, Schwellen) sind in der
| `Abschnitt 3 wurde abgeschnitten. CHUNK_SIZE verkleinern und erneut starten.` | Die Antwort des Modells war zu lang. `CHUNK_SIZE` oben in `abschrift_korrigieren.py` verkleinern (z. B. 900). | | `Abschnitt 3 wurde abgeschnitten. CHUNK_SIZE verkleinern und erneut starten.` | Die Antwort des Modells war zu lang. `CHUNK_SIZE` oben in `abschrift_korrigieren.py` verkleinern (z. B. 900). |
| `Absatzgliederung: unbrauchbare Antwort: …` / `Absätze verbinden: unbrauchbare Antwort: …` | Das Modell hat kein gültiges JSON geliefert; selten. Erneut starten, die Korrektur selbst ist zwischengespeichert. Tritt es wiederholt auf, ein anderes Modell versuchen. | | `Absatzgliederung: unbrauchbare Antwort: …` / `Absätze verbinden: unbrauchbare Antwort: …` | Das Modell hat kein gültiges JSON geliefert; selten. Erneut starten, die Korrektur selbst ist zwischengespeichert. Tritt es wiederholt auf, ein anderes Modell versuchen. |
| `Hinweis: keine Wortliste für „de“ (Paket wngerman); …` | `sudo apt install wngerman` (für Englisch `wamerican`). Ohne Wortliste fehlt im Bericht nur der Grund „gültiges Wort ersetzt“. | | `Hinweis: keine Wortliste für „de“ (Paket wngerman); …` | `sudo apt install wngerman` (für Englisch `wamerican`). Ohne Wortliste fehlt im Bericht nur der Grund „gültiges Wort ersetzt“. |
| `Hinweis: keine Untertitel im Bereich …` | Einer von mehreren Bereichen ist leer und wird übersprungen. | | `Hinweis: keine Untertitel im Bereich …` | Einer von mehreren Bereichen ist leer und wird übersprungen. In Klammern steht, wo die Untertitel enden. |
| `Hinweis: Bereich Anfang – 05:00:00 reicht über das Ende der Untertitel hinaus (…)` | Der Bereich endet nach dem Video. Meist war Minute statt Stunde gemeint: `05:00` ist Stunde 5, Minute 5 ist `00:05`. Bearbeitet wird trotzdem, bis zum Ende des Videos. |
| `Keine Berechtigung` beim Start mit `./…` | `chmod +x *.py` oder `python3 programm.py …` aufrufen. | | `Keine Berechtigung` beim Start mit `./…` | `chmod +x *.py` oder `python3 programm.py …` aufrufen. |
| Lauf ist extrem langsam | Das Modell passt nicht in den Grafikspeicher. `OLLAMA_NUM_PARALLEL=1` setzen (siehe [Abschnitt 2.2](#22-grafikkarte)), andere Programme mit Grafikspeicherbedarf schließen oder ein kleineres Modell wählen. Prüfen mit `ollama ps` (Spalte PROCESSOR sollte „100% GPU“ zeigen). | | Lauf ist extrem langsam | Das Modell passt nicht in den Grafikspeicher. `OLLAMA_NUM_PARALLEL=1` setzen (siehe [Abschnitt 2.2](#22-grafikkarte)), andere Programme mit Grafikspeicherbedarf schließen oder ein kleineres Modell wählen. Prüfen mit `ollama ps` (Spalte PROCESSOR sollte „100% GPU“ zeigen). |

View file

@ -182,7 +182,9 @@ Gilt gleich für `vtt2txt.py`, `extract_text.py` und `abschrift_korrigieren.py`.
| `25` | Minute 25 | | `25` | Minute 25 |
Achtung: Zwei Zahlen sind Stunden:Minuten, `05:00` ist also Stunde 5, nicht Achtung: Zwei Zahlen sind Stunden:Minuten, `05:00` ist also Stunde 5, nicht
Minute 5 (dafür `00:05` oder `5`). Minute 5 (dafür `00:05` oder `5`). Reicht ein Bereich über die letzte
Untertitelzeile hinaus, nennt ein Hinweis das Ende der Untertitel, bei Bereichen ab
einer Stunde mit diesem Tipp.
| Aufruf | Text | | Aufruf | Text |
|---|---| |---|---|

View file

@ -366,10 +366,33 @@ def test_vtt_to_text_skips_empty_range(capsys):
assert text == "[00:01:40 - 00:01:41]\nErster Satz ist hier." assert text == "[00:01:40 - 00:01:41]\nErster Satz ist hier."
out = capsys.readouterr().out out = capsys.readouterr().out
assert "keine Untertitel im Bereich 00:00:00 – 00:00:50" in out assert "keine Untertitel im Bereich 00:00:00 – 00:00:50" in out
assert "keine Untertitel im Bereich 00:03:20 – Ende" in out assert "keine Untertitel im Bereich 00:03:20 – Ende (die Untertitel enden bei" in out
assert vtt2txt.vtt_to_text(SPEECH, [(200, None)]) == "" assert vtt2txt.vtt_to_text(SPEECH, [(200, None)]) == ""
def test_vtt_to_text_notes_range_past_last_caption(capsys):
# "-z 0-05:00" means five hours, not five minutes
text = vtt2txt.vtt_to_text(SPEECH, [(0, 5 * 3600)])
assert text.startswith("[00:01:40 - ")
out = capsys.readouterr().out
assert "Bereich 00:00:00 – 05:00:00 reicht über das Ende der Untertitel hinaus" in out
assert "Stunden:Minuten, Minute 5 ist 00:05" in out
@pytest.mark.parametrize("start, end, expected", [
(100, 110, ""), # inside
(None, None, ""),
(100, 200, " (die Untertitel enden bei 00:02:00)"),
(150, None, " (die Untertitel enden bei 00:02:00)"),
(0, 3600, " (die Untertitel enden bei 00:02:00; zwei Zahlen wie 05:00 sind "
"Stunden:Minuten, Minute 5 ist 00:05)"),
(0, 200, ""), # no captions at all: no end known
])
def test_end_hint(start, end, expected):
captions_end = None if (start, end) == (0, 200) else 120
assert vtt2txt.end_hint(start, end, captions_end) == expected
def test_vtt_to_text_without_ranges_has_no_header(): def test_vtt_to_text_without_ranges_has_no_header():
assert vtt2txt.vtt_to_text(SPEECH).splitlines()[0] == "Erster Satz ist hier." assert vtt2txt.vtt_to_text(SPEECH).splitlines()[0] == "Erster Satz ist hier."

View file

@ -307,18 +307,42 @@ def range_label(start, end):
return f"{left} – {right}" return f"{left} – {right}"
def end_hint(start, end, captions_end):
"""Note for a range reaching past the last caption, else ""; two numbers
are hours:minutes, so "05:00" for minute 5 is a likely slip."""
if captions_end is None:
return ""
past_end = (end is not None and end > captions_end) or (
start is not None and start >= captions_end
)
if not past_end:
return ""
hint = f" (die Untertitel enden bei {format_time(captions_end)}"
if max(start or 0, end or 0) >= 3600 > captions_end:
hint += "; zwei Zahlen wie 05:00 sind Stunden:Minuten, Minute 5 ist 00:05"
return hint + ")"
def excerpts(timed, ranges, exact=False): def excerpts(timed, ranges, exact=False):
"""Return (start, end in seconds, lines) of the text block of each time range. """Return (start, end in seconds, lines) of the text block of each time range.
Blocks that overlap after growing to whole sentences are merged; ranges Blocks that overlap after growing to whole sentences are merged; ranges
without captions are skipped with a note. without captions are skipped with a note, ranges reaching past the last
caption get one.
""" """
captions_end = max((end for _, end, _ in timed if end is not None), default=None)
bounds = [] bounds = []
for start, end in ranges: for start, end in ranges:
found = excerpt_bounds(timed, start, end, exact) found = excerpt_bounds(timed, start, end, exact)
hint = end_hint(start, end, captions_end)
if found is None: if found is None:
print(f"Hinweis: keine Untertitel im Bereich {range_label(start, end)}") print(f"Hinweis: keine Untertitel im Bereich {range_label(start, end)}{hint}")
continue continue
if hint:
print(
f"Hinweis: Bereich {range_label(start, end)} reicht über das Ende "
f"der Untertitel hinaus{hint}"
)
if bounds and found[0] < bounds[-1][1]: if bounds and found[0] < bounds[-1][1]:
bounds[-1] = (bounds[-1][0], max(bounds[-1][1], found[1])) bounds[-1] = (bounds[-1][0], max(bounds[-1][1], found[1]))
else: else: