diff --git a/.gitignore b/.gitignore index 9095b07..3581fd6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,11 +13,6 @@ __pycache__/ # Eigene Ergebnisse und Testläufe output/ -# Arbeitsordner je Video, fertige Abschriften und Quellenangaben: nur lokal -abschriften/ -fertig/ -*.quelle.json - # Echte Reden (Urheberrecht): nur lokal; die Tests nutzen tests/daten/ testdaten/ diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index b2782da..4dcbe39 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -16,15 +16,14 @@ ist. Eine knappe technische Übersicht steht in der [README](README.md). 7. [Schritt 3: Korrigieren](#7-schritt-3-korrigieren) 8. [Schritt 4: Ergebnis prüfen](#8-schritt-4-ergebnis-prüfen) 9. [Schritt 5: Nachbessern und erneut laufen lassen](#9-schritt-5-nachbessern-und-erneut-laufen-lassen) -10. [Schritt 6: Abschließen und aufräumen](#10-schritt-6-abschließen-und-aufräumen) -11. [Nur Ausschnitte bearbeiten (Zeitauswahl)](#11-nur-ausschnitte-bearbeiten-zeitauswahl) -12. [Englische Beiträge](#12-englische-beiträge) -13. [Dateien, die entstehen](#13-dateien-die-entstehen) -14. [Zwischenstände, Abbruch und Fortsetzen](#14-zwischenstände-abbruch-und-fortsetzen) -15. [Ein anderes Sprachmodell verwenden](#15-ein-anderes-sprachmodell-verwenden) -16. [Fehlermeldungen und Lösungen](#16-fehlermeldungen-und-lösungen) -17. [Tipps und häufige Fragen](#17-tipps-und-häufige-fragen) -18. [Kurzreferenz aller Befehle](#18-kurzreferenz-aller-befehle) +10. [Nur Ausschnitte bearbeiten (Zeitauswahl)](#10-nur-ausschnitte-bearbeiten-zeitauswahl) +11. [Englische Beiträge](#11-englische-beiträge) +12. [Dateien, die entstehen](#12-dateien-die-entstehen) +13. [Zwischenstände, Abbruch und Fortsetzen](#13-zwischenstände-abbruch-und-fortsetzen) +14. [Ein anderes Sprachmodell verwenden](#14-ein-anderes-sprachmodell-verwenden) +15. [Fehlermeldungen und Lösungen](#15-fehlermeldungen-und-lösungen) +16. [Tipps und häufige Fragen](#16-tipps-und-häufige-fragen) +17. [Kurzreferenz aller Befehle](#17-kurzreferenz-aller-befehle) --- @@ -58,15 +57,9 @@ Die Werkzeuge im Überblick: | `vtt2txt.py` | Untertiteldatei (`.vtt`) → Rohtext | | `glossar_vorschlag.py` | Entwurf für ein Glossar (vermutete Hörfehler bei Namen und Begriffen) | | `aenderungen.py` | Änderungsbericht für vorhandene Dateien erstellen | -| `abschliessen.py` | fertige Abschrift mit Quellenangabe nach `fertig/`, Arbeitsordner aufräumen | -Für den Normalfall braucht man nur `abschrift_korrigieren.py` und am Ende -`abschliessen.py`, für bessere Ergebnisse zusätzlich ein Glossar. - -**Wo die Dateien liegen:** Jedes Video bekommt einen eigenen Arbeitsordner -`abschriften//`, in dem alles zu diesem Video entsteht. Ist die Abschrift -fertig, bleibt nur die Ergebnisdatei in `fertig/`. Gemeinsame Glossare liegen in -`glossare/`. Details in [Abschnitt 13](#13-dateien-die-entstehen). +Für den Normalfall braucht man nur `abschrift_korrigieren.py`, für bessere +Ergebnisse zusätzlich ein Glossar. --- @@ -102,7 +95,7 @@ Environment="OLLAMA_NUM_PARALLEL=1" Mit `OLLAMA_NUM_PARALLEL=4` landet ein Drittel des Modells auf der CPU, und ein Lauf dauert etwa zwölfmal so lange. Mit weniger Grafikspeicher hilft ein kleineres -Modell (siehe [Abschnitt 15](#15-ein-anderes-sprachmodell-verwenden)), auf Kosten +Modell (siehe [Abschnitt 14](#14-ein-anderes-sprachmodell-verwenden)), auf Kosten der Qualität. ### 2.3 Prüfen, ob alles bereit ist @@ -124,10 +117,6 @@ Lassen sich die Programme nicht mit `./…` starten („Keine Berechtigung“), ausführbar machen: `chmod +x *.py`. Alternativ immer `python3 programm.py …` aufrufen. -**Immer im Programmordner arbeiten.** Die Ordner `abschriften/`, `fertig/` und -`glossare/` werden relativ zum aktuellen Verzeichnis angelegt bzw. gesucht. Wer die -Programme aus einem anderen Verzeichnis startet, bekommt dort eigene, leere Ordner. - --- ## 3. Schnellstart @@ -144,25 +133,16 @@ URLs die Shell. Das Programm 1. lädt die Untertitel in der Originalsprache des Videos, -2. legt den Arbeitsordner an, z. B. - `abschriften/GGBLahnstein_Prof_Gabriele_Krone-Schmalz_Respekt_geht_anders_2026-07-30/` - (kurz `abschriften//`), -3. speichert dort den Rohtext `.txt` und die Quellenangabe `.quelle.json` - (Titel, Kanal, Datum, URL), -4. korrigiert den Text abschnittsweise und gliedert ihn in Absätze, -5. schreibt das Ergebnis nach `.korrigiert.txt` -6. und den Änderungsbericht nach `.aenderungen.html`. +2. speichert den Rohtext, z. B. `GGBLahnstein_Prof_Gabriele_Krone-Schmalz_Respekt_geht_anders_2026-07-30.txt`, +3. korrigiert ihn abschnittsweise und gliedert ihn in Absätze, +4. schreibt das Ergebnis nach `…_2026-07-30.korrigiert.txt` +5. und den Änderungsbericht nach `…_2026-07-30.aenderungen.html`. Eine einstündige Rede braucht mit `qwen3:32b` auf einer RTX 3090 etwa 6–8 Minuten. Danach den Bericht im Browser öffnen und die Liste **„Zu prüfen“** -durchgehen (siehe [Abschnitt 8](#8-schritt-4-ergebnis-prüfen)). Ist alles erledigt: +durchgehen (siehe [Abschnitt 8](#8-schritt-4-ergebnis-prüfen)). -```bash -./abschliessen.py abschriften/ -# → fertig/.txt mit Quellenkopf; der Arbeitsordner wird nach Rückfrage gelöscht -``` - -Mit eigenem Dateinamen (ohne Arbeitsordner): +Mit eigenem Dateinamen: ```bash ./abschrift_korrigieren.py -u 'https://www.youtube.com/watch?v=rm6lADgf6GQ' -o lahnstein.korrigiert.txt @@ -175,10 +155,8 @@ und einem passenden Glossar. Ein Lauf dauert etwa zwei bis drei Minuten und zeig Ergebnisse, auch den Änderungsbericht: ```bash -./abschrift_korrigieren.py -i tests/daten/beispiel.de.txt -g tests/daten/beispiel.glossar.txt \ - -o abschriften/beispiel/beispiel.korrigiert.txt -# → abschriften/beispiel/: beispiel.korrigiert.txt, beispiel.aenderungen.html -./abschliessen.py abschriften/beispiel # Abschluss ausprobieren +./abschrift_korrigieren.py -i tests/daten/beispiel.de.txt -g tests/daten/beispiel.glossar.txt -o beispiel.korrigiert.txt +# → beispiel.korrigiert.txt, beispiel.aenderungen.html ``` --- @@ -190,28 +168,22 @@ besonders bei vielen Namen, lohnt dieser Ablauf: ```text YouTube-URL - │ Schritt 1: extract_text.py → abschriften//.txt (Rohtext) + │ Schritt 1: extract_text.py → rede.txt (Rohtext) ▼ Rohtext ansehen - │ Schritt 2: glossar_vorschlag.py → .glossar.entwurf.txt - │ prüfen, übernehmen → .glossar.txt (Video-Glossar) - │ bzw. glossare/.glossar.txt + │ Schritt 2: glossar_vorschlag.py → rede.glossar.entwurf.txt + │ prüfen, übernehmen → glossare/rede.glossar.txt ▼ - │ Schritt 3: abschrift_korrigieren.py → .korrigiert.txt - │ .aenderungen.html + │ Schritt 3: abschrift_korrigieren.py → rede.korrigiert.txt + │ rede.aenderungen.html ▼ - │ Schritt 4: Bericht prüfen („Zu prüfen“), Text von Hand nacharbeiten + │ Schritt 4: Bericht prüfen („Zu prüfen“) ▼ │ Schritt 5: Glossar ergänzen, erneut laufen lassen - │ (nur Geändertes wird neu berechnet) - ▼ - │ Schritt 6: abschliessen.py → fertig/.txt, Arbeitsordner gelöscht - ▼ + ▼ (nur Geändertes wird neu berechnet) fertige Abschrift ``` -Alle Dateien eines Videos liegen bis zum Abschluss in `abschriften//`. - --- ## 5. Schritt 1: Rohtext holen @@ -219,8 +191,7 @@ Alle Dateien eines Videos liegen bis zum Abschluss in `abschriften//`. ### 5.1 Von YouTube: `extract_text.py` ```bash -./extract_text.py -u 'https://www.youtube.com/watch?v=rm6lADgf6GQ' -# → abschriften//.txt und .quelle.json +./extract_text.py -u 'https://www.youtube.com/watch?v=rm6lADgf6GQ' -o rede.txt ``` Das Programm zeigt Titel, gewählte Untertitelspur und Ziel an: @@ -230,10 +201,8 @@ Video: Prof. Gabriele Krone-Schmalz – Respekt geht anders – … Lade automatische Untertitel „de-orig“ … ``` -Ohne `-o` entsteht ein Name aus Kanal, Titel und Datum, und die Datei kommt in den -Arbeitsordner `abschriften//` (siehe [Abschnitt 13.2](#132-standard-dateinamen)). -Mit `-o rede.txt` landet sie dort, wo man will; die Quellenangabe `rede.quelle.json` -entsteht daneben. +Ohne `-o` entsteht ein Name aus Kanal, Titel und Datum (siehe +[Abschnitt 12.2](#122-standard-dateinamen)). **Welche Untertitel werden genommen?** @@ -254,19 +223,16 @@ versteht `abschrift_korrigieren.py`. Soll die Original-Untertiteldatei (`.vtt`, mit Zeitstempeln) erhalten bleiben: ```bash -./yt_untertitel.py -u 'https://www.youtube.com/watch?v=rm6lADgf6GQ' -n Lahnstein -# → abschriften/Lahnstein/Lahnstein.de-orig.vtt (+ Lahnstein.quelle.json) -./vtt2txt.py -i abschriften/Lahnstein/Lahnstein.de-orig.vtt -# → abschriften/Lahnstein/Lahnstein.de-orig.txt +./yt_untertitel.py -u 'https://www.youtube.com/watch?v=rm6lADgf6GQ' -n Lahnstein -d untertitel +# → untertitel/Lahnstein.de-orig.vtt +./vtt2txt.py -i untertitel/Lahnstein.de-orig.vtt +# → untertitel/Lahnstein.de-orig.txt ``` -`-n` bestimmt den Namen von Datei und Arbeitsordner; mit `-d` wählt man ein anderes -Zielverzeichnis. - Achtung: `yt_untertitel.py` nimmt ohne `-l` Deutsch (`de`), nicht die Originalsprache. Für englische Videos `-l en` angeben. -Die `.vtt`-Datei braucht man für die [Zeitauswahl](#11-nur-ausschnitte-bearbeiten-zeitauswahl) +Die `.vtt`-Datei braucht man für die [Zeitauswahl](#10-nur-ausschnitte-bearbeiten-zeitauswahl) mit vorhandenen Dateien. `abschrift_korrigieren.py` nimmt sie auch direkt, der Zwischenschritt über `vtt2txt.py` ist dann nur nötig, um den Rohtext anzusehen oder ein Glossar daraus zu erstellen. @@ -327,43 +293,33 @@ Regeln für feste Ersetzungen: die Ersetzungen ohne Treffer. Bei einem Glossar für genau diesen Beitrag deutet „ohne Treffer“ auf einen Tippfehler. -### 6.2 Gemeinsame Glossare und Video-Glossare +### 6.2 Mehrere Glossare kombinieren Bewährt hat sich ein **gemeinsames Glossar** pro Redner oder Kanal (wiederkehrende -Namen und Hörfehler) plus ein **Video-Glossar** je Beitrag: - -| Art | Ablage | Verwendung | -|---|---|---| -| gemeinsames Glossar | `glossare/krone-schmalz.glossar.txt` | mit `-g` angeben | -| Video-Glossar | `abschriften//.glossar.txt`, heißt wie sein Ordner | **automatisch** | +Namen und Hörfehler) plus eines **je Beitrag**: ```bash -./abschrift_korrigieren.py -i abschriften//.txt -g glossare/krone-schmalz.glossar.txt -# Glossar glossare/krone-schmalz.glossar.txt: 7 Begriffe, 36 feste Ersetzungen -# Glossar abschriften//.glossar.txt (Video-Glossar, automatisch): … +./abschrift_korrigieren.py -i rede.txt -g glossare/krone-schmalz.glossar.txt -g glossare/rede.glossar.txt ``` -Das Video-Glossar wird nach den mit `-g` angegebenen angewendet; so greifen die -gemeinsamen Ersetzungen zuerst. Dasselbe gilt für `glossar_vorschlag.py` und -`aenderungen.py`. Beim Abschluss nennt `abschliessen.py` die Einträge des -Video-Glossars, die noch in keinem gemeinsamen Glossar stehen. Was auch in anderen -Videos vorkommen wird (Namen, Orte), gehört dann ins gemeinsame Glossar; der Rest -verschwindet mit dem Arbeitsordner. +Das gemeinsame Glossar zuerst angeben, damit seine Ersetzungen zuerst greifen. -**Glossare bleiben lokal.** Sie enthalten Namen realer Personen und Hörfehler realer -Beiträge und werden deshalb nicht ins Git-Repository übernommen (`glossare/`, -`abschriften/` und `*.glossar.txt` stehen in `.gitignore`). `glossare/` deshalb -gesondert sichern (siehe [Abschnitt 17](#17-tipps-und-häufige-fragen)). Als Vorlage -eignet sich das kleine Glossar zur erfundenen Beispielrede in -`tests/daten/beispiel.glossar.txt`. +**Ablage:** Glossare gehören ins Verzeichnis `glossare/` im Programmordner, z. B. +`glossare/krone-schmalz.glossar.txt` und `glossare/ramstein.glossar.txt`. Sie +enthalten Namen realer Personen und Hörfehler realer Beiträge und werden deshalb +nicht ins Git-Repository übernommen: `glossare/` und jede Datei `*.glossar.txt` sind +über `.gitignore` ausgeschlossen und bleiben nur auf dem eigenen Rechner. Das ist +gewollt; Glossare deshalb gesondert sichern. Einzige Ausnahme ist das kleine, +leicht nachvollziehbare Glossar zur erfundenen Beispielrede in +`tests/daten/beispiel.glossar.txt`, das sich gut als Vorlage eignet. ### 6.3 Entwurf vom Modell: `glossar_vorschlag.py` Statt den ganzen Rohtext zu lesen, kann das Modell einen ersten Entwurf liefern: ```bash -./glossar_vorschlag.py -i abschriften//.txt -g glossare/krone-schmalz.glossar.txt -# → abschriften//.glossar.entwurf.txt +./glossar_vorschlag.py -i rede.txt -g glossare/krone-schmalz.glossar.txt +# → rede.glossar.entwurf.txt ``` Das Modell sucht abschnittsweise vermutete Hörfehler: Namen, Orte, Fremdwörter, @@ -390,8 +346,8 @@ irrt sich oft (im Beispiel ist „Deckmäntelchen“ richtig). Eine falsche fest Ersetzung verdirbt den Text unbemerkt. Deshalb: 1. Jeden Eintrag prüfen, im Zweifel im Rohtext oder Video nachsehen. -2. Brauchbare Zeilen ohne `# ? ` ins Video-Glossar - `abschriften//.glossar.txt` (oder ins gemeinsame) kopieren. +2. Brauchbare Zeilen ohne `# ? ` ins Glossar des Beitrags (oder ins gemeinsame) + kopieren. 3. Unsichere Namen lieber als **Begriff** (ohne `=>`) aufnehmen. Nach einem Korrekturlauf kann der Entwurf zusätzlich die Wörter auflisten, die das @@ -399,8 +355,7 @@ Modell geändert hat. Das zeigt Hörfehler, die man als feste Ersetzung absicher möchte, und Fehlgriffe des Modells: ```bash -./glossar_vorschlag.py -i abschriften//.txt -k abschriften//.korrigiert.txt \ - -g glossare/krone-schmalz.glossar.txt +./glossar_vorschlag.py -i rede.txt -k rede.korrigiert.txt -g glossare/krone-schmalz.glossar.txt -g glossare/rede.glossar.txt ``` Diese Einträge stehen in einem eigenen Abschnitt mit ihrer Anzahl, z. B. @@ -412,12 +367,11 @@ entfernen. ## 7. Schritt 3: Korrigieren ```bash -./abschrift_korrigieren.py -i abschriften//.txt -g glossare/krone-schmalz.glossar.txt +./abschrift_korrigieren.py -i rede.txt -g glossare/krone-schmalz.glossar.txt -g glossare/rede.glossar.txt ``` -Statt des Rohtexts geht auch die Untertiteldatei (`-i …/.de-orig.vtt`) oder -`-u URL` (direkt von YouTube). Das Ergebnis entsteht neben der Eingabe, also im -Arbeitsordner; das Video-Glossar dort wird automatisch verwendet. +Statt `-i rede.txt` geht auch `-i rede.de.vtt` (Untertiteldatei) oder +`-u URL` (direkt von YouTube). ### 7.1 Was das Programm tut @@ -447,7 +401,7 @@ Ein gekürztes Beispiel mit Erläuterungen: Modell: qwen3:32b Sprache des Beitrags: de Glossar glossare/krone-schmalz.glossar.txt: 7 Begriffe, 36 feste Ersetzungen -Glossar abschriften//.glossar.txt (Video-Glossar, automatisch): 2 Begriffe, 15 feste Ersetzungen +Glossar glossare/ramstein.glossar.txt: 2 Begriffe, 15 feste Ersetzungen Ersetzt 2× „Willly Brand“ → „Willy Brandt“ ← feste Ersetzungen mit Treffern Ersetzt 6× „Rammstein“ → „Ramstein“ 13 Ersetzung(en) ohne Treffer: „Hostelchik“, … ← bei gemeinsamen Glossaren normal @@ -611,60 +565,13 @@ vorhandene Datei als von Hand geändert. --- -## 10. Schritt 6: Abschließen und aufräumen - -Ist der Text geprüft und nachgearbeitet, wird das Video abgeschlossen. Danach zählt -nur noch die Ergebnisdatei; Rohtext, Bericht, Zwischenstände und Video-Glossar -werden nicht mehr gebraucht. - -```bash -./abschliessen.py abschriften/ -``` - -Was dabei passiert: - -1. **Prüfen:** Gibt es noch `[prüfen: …]`-Marken im Text oder eine ungeklärte - `.neu.korrigiert.txt` (siehe [Abschnitt 9](#9-schritt-5-nachbessern-und-erneut-laufen-lassen)), - bricht das Programm ab und nennt die Stellen. Erst klären, dann erneut aufrufen. - Mit `--trotzdem` wird auch mit offenen Marken abgeschlossen. -2. **Übernehmen:** Jedes Ergebnis kommt nach `fertig/`, bei Ausschnitten jedes - einzeln, mit einem Quellenkopf: - - ```text - Titel: Prof. Gabriele Krone-Schmalz – Respekt geht anders - Kanal: GGBLahnstein - Datum: 2026-07-30 - Quelle: https://www.youtube.com/watch?v=rm6lADgf6GQ - Abgeschlossen: 2026-10-11 - - Vielen Dank. Liebe Gäste, … - ``` - - Die Angaben stammen aus `.quelle.json`, die beim Download entsteht. Fehlt - sie (z. B. bei einer eigenen `.vtt`), nennt der Kopf nur die Datei. Eine Datei in - `fertig/` mit anderem Text wird nie überschrieben. -3. **Glossar sichten:** Einträge des Video-Glossars, die in keinem Glossar unter - `glossare/` stehen, werden aufgelistet. Brauchbares jetzt ins gemeinsame Glossar - übernehmen. -4. **Aufräumen:** Der Inhalt des Arbeitsordners wird angezeigt, dann kommt die Frage - `Arbeitsordner … löschen? [j/N]`. Nur `j` löscht. Wer „n“ antwortet, kann - `abschliessen.py` später erneut aufrufen; die Datei in `fertig/` wird dann als - bereits übernommen erkannt. Mit `--ja` wird ohne Frage gelöscht. - -**Schutz vor Versehen:** Nie gelöscht werden Ordner, die andere Dateien enthalten als -die, die das Programm selbst anlegt (`.txt`, `.vtt`, `.html`, `.json`, `*.parts/`), -außerdem das Aufrufverzeichnis und alles in `testdaten/`, `glossare/`, `tests/` und -`fertig/`. Das Programm nennt dann den Grund, und der Ordner bleibt stehen. - ---- - -## 11. Nur Ausschnitte bearbeiten (Zeitauswahl) +## 10. Nur Ausschnitte bearbeiten (Zeitauswahl) Mit `-z` wird nur ein Teil des Videos bearbeitet. Das geht mit `-u` (YouTube) und mit `.vtt`-Dateien, nicht mit reinem Text (der hat keine Zeitstempel). Dieselbe Option kennen `extract_text.py` und `vtt2txt.py`. -### 11.1 Zeitangaben +### 10.1 Zeitangaben | Angabe | Bedeutung | |---|---| @@ -677,7 +584,7 @@ Option kennen `extract_text.py` und `vtt2txt.py`. > Reicht ein Bereich über das Ende des Videos hinaus, weist das Programm darauf hin > und nennt die Länge der Untertitel. -### 11.2 Bereiche +### 10.2 Bereiche | Aufruf | Bearbeitet | |---|---| @@ -694,7 +601,7 @@ und werden nach der Zeit sortiert. Beispiel: ./abschrift_korrigieren.py -u 'https://www.youtube.com/watch?v=…' -z 00:05-00:06 00:25 ``` -### 11.3 Was dabei herauskommt +### 10.3 Was dabei herauskommt Alle Bereiche landen in **einer** Datei, jeder mit einer Überschrift, die seine tatsächlichen Zeiten nennt: @@ -718,7 +625,7 @@ Also, wenn ihr Programme baut mit Claude Code, … --- -## 12. Englische Beiträge +## 11. Englische Beiträge Mit `-u` erkennt das Programm die Sprache des Videos selbst. Bei Dateien die Sprache angeben: @@ -739,44 +646,28 @@ Andere Sprachen lehnt das Programm mit einer Meldung ab. --- -## 13. Dateien, die entstehen +## 12. Dateien, die entstehen -### 13.1 Übersicht +### 12.1 Übersicht -Im Programmordner entstehen diese Verzeichnisse. Alle bleiben nur auf dem eigenen -Rechner, keines geht ins Git-Repository: - -```text -glossare/ gemeinsame Glossare je Sprecher oder Kanal -abschriften// Arbeitsordner, einer je Video -fertig/ abgeschlossene Abschriften, je eine Datei -testdaten// nur für Entwickler: echte Reden für Regressionstests -``` - -Im Arbeitsordner `abschriften//`: +Beispiel für `-i rede.txt` bzw. `-o rede.korrigiert.txt`: | Datei | Inhalt | Wird überschrieben? | |---|---|---| -| `.txt` | Rohtext (bei `-u` automatisch gespeichert) | nie, wenn sie schon mit anderem Inhalt existiert (dann Warnung) | -| `.quelle.json` | Titel, Kanal, Datum, URL (beim Download) | ja, beim nächsten Download | -| `.glossar.txt` | Video-Glossar (legt man selbst an) | nie | -| `.korrigiert.txt` | korrigierte Abschrift | ja, außer sie wurde von Hand geändert (dann `.neu.korrigiert.txt`, siehe [Abschnitt 9](#9-schritt-5-nachbessern-und-erneut-laufen-lassen)) | -| `.aenderungen.html` | Änderungsbericht | ja, bei jedem Lauf | -| `.korrigiert.parts/` | Zwischenstände | wird ergänzt; darf jederzeit gelöscht werden | -| `.glossar.entwurf.txt` | Glossar-Entwurf von `glossar_vorschlag.py` | ja | -| `.glossar.entwurf.parts/` | Zwischenstände des Entwurfs | wird ergänzt | +| `rede.txt` | Rohtext (bei `-u` automatisch gespeichert) | nie, wenn sie schon mit anderem Inhalt existiert (dann Warnung) | +| `rede.korrigiert.txt` | korrigierte Abschrift | ja, außer sie wurde von Hand geändert (dann `rede.neu.korrigiert.txt`, siehe [Abschnitt 9](#9-schritt-5-nachbessern-und-erneut-laufen-lassen)) | +| `rede.aenderungen.html` | Änderungsbericht | ja, bei jedem Lauf | +| `rede.korrigiert.parts/` | Zwischenstände | wird ergänzt; darf jederzeit gelöscht werden | +| `rede.glossar.entwurf.txt` | Glossar-Entwurf von `glossar_vorschlag.py` | ja | +| `rede.glossar.entwurf.parts/` | Zwischenstände des Entwurfs | wird ergänzt | -Ausschnitte (`-z`) desselben Videos landen im selben Ordner, mit den Bereichen im -Namen. Mit `abschliessen.py` kommt jedes Ergebnis als `fertig/.txt` heraus, und -der Arbeitsordner wird gelöscht ([Abschnitt 10](#10-schritt-6-abschließen-und-aufräumen)). +Ohne `-o` liegt das Ergebnis neben der Eingabedatei. Bei `-u` ohne `-o` im +aktuellen Verzeichnis. Fehlende Verzeichnisse in `-o` werden angelegt. -Mit `-i` liegt das Ergebnis neben der Eingabedatei; mit `-o` dort, wo man will -(fehlende Verzeichnisse werden angelegt). +### 12.2 Standard-Dateinamen -### 13.2 Standard-Dateinamen - -Ohne `-o` (bzw. `-n` bei `yt_untertitel.py`) entsteht der Name des Arbeitsordners -und der Dateien aus **Kanal, Titel und Hochladedatum**: +Ohne `-o` (bzw. `-n` bei `yt_untertitel.py`) entsteht der Name aus **Kanal, Titel +und Hochladedatum**: | Video | Name | |---|---| @@ -789,7 +680,7 @@ denselben Namen. --- -## 14. Zwischenstände, Abbruch und Fortsetzen +## 13. Zwischenstände, Abbruch und Fortsetzen Jeder fertig korrigierte Abschnitt, jede Absatzgliederung und jede Entscheidung beim Verbinden kurzer Absätze wird sofort in `.parts/` gespeichert. @@ -800,12 +691,11 @@ beim Verbinden kurzer Absätze wird sofort in `.parts/` gespeichert. - Ändern sich Modell, Anweisung, Glossar oder der Text eines Abschnitts, wird genau dieser Teil neu berechnet; alte Zwischenstände stören nicht. - Der Ordner `*.parts/` darf jederzeit gelöscht werden; dann wird alles neu - berechnet. Er wächst mit jeder Modell- oder Glossaränderung, weil alte Fassungen - liegen bleiben. Beim Abschluss verschwindet er mit dem Arbeitsordner. + berechnet. --- -## 15. Ein anderes Sprachmodell verwenden +## 14. Ein anderes Sprachmodell verwenden ```bash ollama pull qwen3:14b @@ -823,7 +713,7 @@ Weitere Einstellungen (Abschnittsgröße, Absatzlängen, Schwellen) sind in der --- -## 16. Fehlermeldungen und Lösungen +## 15. Fehlermeldungen und Lösungen | Meldung | Ursache und Lösung | |---|---| @@ -835,7 +725,7 @@ Weitere Einstellungen (Abschnittsgröße, Absatzlängen, Schwellen) sind in der | `Keine Untertitel in „de“ (weder manuell noch automatisch in Originalsprache).` | Das Video hat keine passende Spur. Die Meldung listet die vorhandenen Sprachen; ggf. mit `-l` eine davon wählen. Ist das Video z. B. englisch, `-l en` angeben. | | `Der Beitrag ist auf „fr“; unterstützt: de, en (-l).` | Nur Deutsch und Englisch werden korrigiert. | | `-z braucht Zeitstempel: nur mit .vtt-Eingabe oder -u.` | Zeitauswahl geht nicht mit `.txt`. Die `.vtt`-Datei oder `-u URL` verwenden. | -| `-z/--zeitraeume: …` | Zeitangabe ungültig oder Bereiche überschneiden sich, siehe [Abschnitt 11](#11-nur-ausschnitte-bearbeiten-zeitauswahl). | +| `-z/--zeitraeume: …` | Zeitangabe ungültig oder Bereiche überschneiden sich, siehe [Abschnitt 10](#10-nur-ausschnitte-bearbeiten-zeitauswahl). | | `Die Eingabedatei enthält im gewählten Zeitraum keinen Text.` | Im Bereich gibt es keine Untertitel. Zeiten prüfen (`05:00` = 5 Stunden!). | | `Die Eingabedatei ist leer.` | Datei prüfen. | | `Ausgabedatei darf nicht die Eingabedatei sein.` | Mit `-o` einen anderen Namen wählen. | @@ -848,16 +738,12 @@ Weitere Einstellungen (Abschnittsgröße, Absatzlängen, Schwellen) sind in der | `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. 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. | -| `Noch offene Prüfmarken. Erst klären, oder mit --trotzdem abschließen.` | `abschliessen.py`: Im Text stehen noch `[prüfen: …]`-Marken (die Zeilen werden genannt). Auflösen und erneut aufrufen. | -| `Ungeklärter Konflikt: .neu.korrigiert.txt …` | `abschliessen.py`: Erst die eigenen Änderungen übertragen bzw. entscheiden, welche Fassung gilt, und die `.neu`-Datei löschen. | -| `Gibt es schon in fertig mit anderem Text: …` | In `fertig/` liegt schon eine Abschrift dieses Namens mit anderem Inhalt. Sie wird nie überschrieben: umbenennen oder entfernen, dann erneut aufrufen. | -| `Arbeitsordner … bleibt erhalten: Fremde Datei: …` | Im Arbeitsordner liegt eine Datei, die das Programm nicht selbst anlegt (z. B. Notizen). Die fertige Abschrift ist trotzdem in `fertig/`; den Ordner selbst aufräumen. | | `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). | --- -## 17. Tipps und häufige Fragen +## 16. Tipps und häufige Fragen **Wie bekomme ich die besten Ergebnisse?** Mit einem guten Glossar. Das Modell kennt viele Namen nicht oder schreibt sie @@ -891,28 +777,17 @@ for url in 'https://www.youtube.com/watch?v=AAA' 'https://www.youtube.com/watch? done ``` -**Wie sichere ich meine Daten?** -Wertvoll sind auf Dauer nur `glossare/` (gewachsenes Wissen über Namen und Hörfehler) -und `fertig/` (die Ergebnisse). Beide sind bewusst nicht im öffentlichen -Git-Repository. Am einfachsten ist ein zweites, **privates** Git-Repository nur für -diese beiden Ordner, oder man nimmt sie ins normale Backup auf. `abschriften/` muss -nicht gesichert werden; was dort liegt, ist entweder in Arbeit oder lässt sich neu -erzeugen. - -**Welche Videos sind noch in Arbeit?** -Alle, die einen Ordner in `abschriften/` haben: `ls abschriften/`. - **Wo sind die Absätze?** In `*.korrigiert.txt` trennt eine Leerzeile die Absätze. Viele Textprogramme übernehmen das beim Einfügen direkt. **Kann ich das Ergebnis weiterverarbeiten (Word, LibreOffice)?** -Die Dateien in `fertig/` (und die `.korrigiert.txt`) sind reiner UTF-8-Text und -lassen sich überall öffnen oder einfügen. +Die `.korrigiert.txt` ist reiner UTF-8-Text und lässt sich überall öffnen oder +einfügen. --- -## 18. Kurzreferenz aller Befehle +## 17. Kurzreferenz aller Befehle ### `abschrift_korrigieren.py` @@ -920,7 +795,7 @@ lassen sich überall öffnen oder einfügen. |---|---| | `-i DATEI` | Eingabe: Rohtext (`.txt`) oder Untertitel (`.vtt`) | | `-u URL` | statt `-i`: YouTube-Video laden und korrigieren | -| `-o DATEI` | Ausgabedatei (Standard: `.korrigiert.txt`, mit `-u` `abschriften//.korrigiert.txt`) | +| `-o DATEI` | Ausgabedatei (Standard: `.korrigiert.txt` bzw. `__.korrigiert.txt`) | | `-g DATEI` | Glossar, mehrfach möglich (Reihenfolge zählt) | | `-l SPRACHE` | `de` oder `en` (Standard: `de`, mit `-u` die Originalsprache) | | `-m MODELL` | Ollama-Modell (Standard: `qwen3:32b`) | @@ -934,7 +809,7 @@ lassen sich überall öffnen oder einfügen. | Option | Bedeutung | |---|---| | `-u URL` | YouTube-URL (Pflicht) | -| `-o DATEI` | Ausgabedatei (Standard: `abschriften//.txt`) | +| `-o DATEI` | Ausgabedatei (Standard: `__.txt`) | | `-l SPRACHE` | Sprache der Untertitel (Standard: Originalsprache) | | `-f` | Fließtext mit 80 Zeichen pro Zeile | | `-z BEREICH …`, `--exakt` | Zeitauswahl wie oben | @@ -944,9 +819,9 @@ lassen sich überall öffnen oder einfügen. | Option | Bedeutung | |---|---| | `-u URL` | YouTube-URL (Pflicht) | -| `-n NAME` | Name von Datei und Arbeitsordner (Standard: `__`) | +| `-n NAME` | Dateiname ohne Endung (Standard: `__`) | | `-l SPRACHE` | Sprache (Standard: `de`) | -| `-d VERZEICHNIS` | Zielverzeichnis (Standard: `abschriften//`) | +| `-d VERZEICHNIS` | Zielverzeichnis (Standard: aktuelles) | ### `vtt2txt.py` @@ -976,16 +851,4 @@ lassen sich überall öffnen oder einfügen. | `-g DATEI` | Glossare der Korrektur, mehrfach möglich | | `-l SPRACHE` | Sprache für die Wortliste (Standard: `de`) | -### `abschliessen.py` - -| Option | Bedeutung | -|---|---| -| `ORDNER` | Arbeitsordner, z. B. `abschriften/` (Pflicht) | -| `--ziel VERZEICHNIS` | für fertige Abschriften (Standard: `fertig`) | -| `--glossare VERZEICHNIS` | gemeinsame Glossare für den Glossar-Hinweis (Standard: `glossare`) | -| `--ja` | Arbeitsordner ohne Rückfrage löschen | -| `--trotzdem` | auch mit offenen Prüfmarken abschließen | - -Das Video-Glossar `abschriften//.glossar.txt` verwenden -`abschrift_korrigieren.py`, `glossar_vorschlag.py` und `aenderungen.py` automatisch. Jedes Programm zeigt mit `--help` seine Optionen. diff --git a/README.md b/README.md index 1946d0c..275b0fb 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,6 @@ Die Werkzeuge sind einzeln oder hintereinander nutzbar: | `abschrift_korrigieren.py` | Rohabschrift (oder VTT direkt) mit dem Sprachmodell korrigieren | | `aenderungen.py` | Änderungsbericht: was hat das Modell gegenüber dem Rohtext geändert? | | `glossar_vorschlag.py` | Glossar-Entwurf: vermutete Hörfehler bei Namen und Begriffen | -| `abschliessen.py` | fertige Abschrift mit Quellenangabe nach `fertig/`, Arbeitsordner löschen | ## Voraussetzungen @@ -42,31 +41,26 @@ der Lauf wird etwa 12-mal langsamer. ## Aufruf -Von der YouTube-URL zum korrigierten Text in einem Befehl, dann abschließen: +Von der YouTube-URL zum korrigierten Text in einem Befehl: ```bash -./abschrift_korrigieren.py -u 'https://www.youtube.com/watch?v=rm6lADgf6GQ' -g glossare/krone-schmalz.glossar.txt -# → abschriften//: .txt (Rohtext), .quelle.json, -# .korrigiert.txt, .aenderungen.html, .korrigiert.parts/ -./abschliessen.py abschriften/ # → fertig/.txt, Arbeitsordner nach Rückfrage gelöscht +./abschrift_korrigieren.py -u 'https://www.youtube.com/watch?v=rm6lADgf6GQ' -o lahnstein.korrigiert.txt +# → lahnstein.korrigiert.txt, lahnstein.txt (Rohtext), lahnstein.aenderungen.html ``` -`` ist `__`, siehe [Dateiablage](#dateiablage). - Oder in zwei Schritten, um den Rohtext vorher anzusehen: ```bash -./extract_text.py -u 'https://www.youtube.com/watch?v=rm6lADgf6GQ' # → abschriften//.txt -./abschrift_korrigieren.py -i abschriften//.txt # Ergebnis im selben Ordner +./extract_text.py -u 'https://www.youtube.com/watch?v=rm6lADgf6GQ' -o lahnstein.txt +./abschrift_korrigieren.py -i lahnstein.txt # → lahnstein.korrigiert.txt ``` Dasselbe in Einzelschritten, wenn die VTT-Datei erhalten bleiben soll: ```bash -./yt_untertitel.py -u 'https://www.youtube.com/watch?v=rm6lADgf6GQ' -n Lahnstein -# → abschriften/Lahnstein/Lahnstein.de-orig.vtt -./vtt2txt.py -i abschriften/Lahnstein/Lahnstein.de-orig.vtt -./abschrift_korrigieren.py -i abschriften/Lahnstein/Lahnstein.de-orig.txt +./yt_untertitel.py -u 'https://www.youtube.com/watch?v=rm6lADgf6GQ' -n Lahnstein -d untertitel +./vtt2txt.py -i untertitel/Lahnstein.de-orig.vtt +./abschrift_korrigieren.py -i untertitel/Lahnstein.de-orig.txt ``` Der Schritt über `vtt2txt.py` ist optional: `abschrift_korrigieren.py` nimmt auch die @@ -81,7 +75,7 @@ Zwischenschritt im temporären Verzeichnis. Die Spurwahl ist dieselbe wie bei ```bash ./extract_text.py -u 'https://www.youtube.com/watch?v=0LmCveTM788' -# → abschriften/John_Mearsheimer_…_2026-07-30/John_Mearsheimer_…_2026-07-30.txt (+ .quelle.json) +# → John_Mearsheimer_Offensive_Realism_Episode_1_Origins_of_the_War_In_Ukraine_2026-07-30.txt ./extract_text.py -u URL -l de -o texte/rede.txt # Sprache und Ziel ./extract_text.py -u URL -f -o rede.txt # lesbarer Fließtext ./extract_text.py -u URL -z 0:03:17-0:05:25 # nur ein Ausschnitt @@ -93,7 +87,7 @@ Zwischenschritt im temporären Verzeichnis. Die Spurwahl ist dieselbe wie bei |---|---| | `-u`, `--url` | YouTube-URL, Pflicht | | `-l`, `--lang` | Sprache; Standard: Sprache des Original-Audios (`-orig`) | -| `-o`, `--output` | Ausgabedatei (Verzeichnisse werden angelegt, vorhandene Datei überschrieben); Standard: `abschriften//.txt`, siehe unten | +| `-o`, `--output` | Ausgabedatei (Verzeichnisse werden angelegt, vorhandene Datei überschrieben); Standard: `__.txt`, siehe unten | | `-z`, `--zeitraeume` | nur Untertitel in diesen Bereichen, siehe [Zeitauswahl](#zeitauswahl--z) | | `--exakt` | genau an den Zeiten schneiden statt an Satzgrenzen | | `-f`, `--fliesstext` | Fließtext, auf 80 Zeichen umbrochen (Wörter werden nie getrennt), statt der kurzen Untertitelzeilen; beides versteht `abschrift_korrigieren.py` | @@ -101,8 +95,7 @@ Zwischenschritt im temporären Verzeichnis. Die Spurwahl ist dieselbe wie bei ### Standard-Dateinamen für YouTube-Videos Ohne `-o` bzw. `-n` bilden `extract_text.py`, `yt_untertitel.py` und -`abschrift_korrigieren.py -u` den Namen aus Kanal, Titel und Hochladedatum und -schreiben in den Arbeitsordner `abschriften//`: +`abschrift_korrigieren.py -u` den Namen aus Kanal, Titel und Hochladedatum: | Video | Name | |---|---| @@ -115,11 +108,8 @@ Wörter werden mit `_` verbunden. Bindestriche und Punkte im Wort bleiben Video-ID. Mit `-z` kommen die Bereiche dazu, z. B. `…_2026-07-30_00-03-17_bis_00-05-25` (`-z A`: `_ab_…`, `-z 0-B` bzw. `-z -B`: `_bis_…`; mehrere Bereiche mit `+` verbunden: `…_00-05-00_bis_00-06-00+ab_00-25-00`). -Gleiches Video ergibt immer denselben Namen und denselben Ordner, Ausschnitte -eines Videos landen also beisammen. Eine vorhandene Datei wird überschrieben (ein mit -`-u` gespeicherter, abweichender Rohtext und eine von Hand geänderte Korrektur aber -nie). Neben dem Rohtext entsteht `.quelle.json` mit Titel, Kanal, Datum und -URL; daraus macht `abschliessen.py` den Quellenkopf. +Gleiches Video ergibt immer denselben Namen; eine vorhandene Datei wird +überschrieben (ein mit `-u` gespeicherter, abweichender Rohtext aber nie). ### Untertitel herunterladen: `yt_untertitel.py` @@ -140,8 +130,7 @@ 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 # → abschriften//.de-orig.vtt -./yt_untertitel.py -u URL -n Lahnstein # → abschriften/Lahnstein/Lahnstein.de-orig.vtt +./yt_untertitel.py -u URL # → __.de-orig.vtt ./yt_untertitel.py -u URL -n Lahnstein -d untertitel # → untertitel/Lahnstein.de-orig.vtt ``` @@ -150,7 +139,7 @@ Region gefunden: `-l en` findet manuelle Untertitel `en-US`. | `-u`, `--url` | YouTube-URL, Pflicht | | `-n`, `--name` | Dateiname ohne Endung; Standard: `__` | | `-l`, `--lang` | Sprache; Standard: `de` | -| `-d`, `--dir` | Zielverzeichnis (wird angelegt); Standard: Arbeitsordner `abschriften//` | +| `-d`, `--dir` | Zielverzeichnis (wird angelegt); Standard: aktuelles Verzeichnis | Eine vorhandene Datei gleichen Namens wird überschrieben. @@ -243,7 +232,7 @@ werden trotzdem umgewandelt; der Exit-Code ist dann 1. ./abschrift_korrigieren.py -i rede.txt -g glossare/rede.glossar.txt # mit Glossar ./abschrift_korrigieren.py -i rede.txt -g glossare/allgemein.glossar.txt -g glossare/rede.glossar.txt # kombiniert ./abschrift_korrigieren.py -i rede.de.vtt # VTT direkt → rede.de.korrigiert.txt -./abschrift_korrigieren.py -u URL # YouTube → abschriften//.korrigiert.txt +./abschrift_korrigieren.py -u URL # YouTube → __.korrigiert.txt ./abschrift_korrigieren.py -i talk.txt -l en # englischer Beitrag ./abschrift_korrigieren.py -u URL -z 0:03:17-0:05:25 # nur ein Ausschnitt des Videos ./abschrift_korrigieren.py -u URL -z 00:05-00:06 00:25 # mehrere Bereiche, eine Datei @@ -254,10 +243,10 @@ werden trotzdem umgewandelt; der Exit-Code ist dann 1. | `-i`, `--input` | Eingabedatei: Rohabschrift (`.txt`) oder Untertitel (`.vtt`, wird wie mit `vtt2txt.py` umgewandelt) | | `-u`, `--url` | statt `-i`: YouTube-URL; Untertitel wie bei `extract_text.py`. Der Rohtext wird neben der Ausgabe gespeichert (`rede.korrigiert.txt` → `rede.txt`, sonst `.roh.txt`), eine vorhandene, abweichende Datei aber nie überschrieben | | `-l`, `--lang` | Sprache des Beitrags: `de` oder `en` (auch mit Region, z. B. `en-US`); wählt Prompts und Prüfmarke. Mit `-u` zugleich die Sprache der Untertitel. Standard: `de`, mit `-u` die Sprache des Original-Audios | -| `-o`, `--output` | Ausgabedatei; Standard: `.korrigiert.txt` neben der Eingabe, mit `-u` `abschriften//.korrigiert.txt`; mit `-z` zusätzlich die Bereiche im Namen | +| `-o`, `--output` | Ausgabedatei; Standard: `.korrigiert.txt` neben der Eingabe, mit `-u` `__.korrigiert.txt`; mit `-z` zusätzlich die Bereiche im Namen | | `-z`, `--zeitraeume` | nur diese Bereiche korrigieren, siehe [Zeitauswahl](#zeitauswahl--z); nur mit `.vtt`-Eingabe oder `-u`, da reiner Text keine Zeitstempel hat | | `--exakt` | genau an den Zeiten schneiden statt an Satzgrenzen | -| `-g`, `--glossar` | Glossar mit festen Ersetzungen und Schreibweisen; mehrfach angeben zum Kombinieren (Ersetzungen in Reihenfolge der Angabe). Das Video-Glossar des Ausgabeordners kommt automatisch dazu, siehe [Glossar](#glossar) | +| `-g`, `--glossar` | Glossar mit festen Ersetzungen und Schreibweisen; mehrfach angeben zum Kombinieren (Ersetzungen in Reihenfolge der Angabe) | | `-m`, `--model` | Ollama-Modell; Standard: `qwen3:32b` (Konstante `MODEL`) | | `--kein-bericht` | keinen Änderungsbericht schreiben | | `--ueberschreiben` | Ausgabedatei auch ersetzen, wenn sie seit dem letzten Lauf von Hand geändert wurde | @@ -274,31 +263,6 @@ Modell an, an den Rändern der Ausschnitte nichts zu ergänzen. Eine Rohabschrif (`.txt`) mit solchen Überschriften, etwa von `vtt2txt.py -z`, wird ebenso in Blöcken korrigiert. -## Dateiablage - -```text -glossare/ gemeinsame Glossare je Sprecher oder Kanal -abschriften// Arbeitsordner, einer je Video (auch für Ausschnitte) - .txt Rohtext - .quelle.json Titel, Kanal, Datum, URL - .glossar.txt Glossar nur dieses Videos (optional, automatisch genutzt) - [_bereiche].korrigiert.txt Ergebnis, darf von Hand nachbearbeitet werden - [_bereiche].aenderungen.html Änderungsbericht - [_bereiche].korrigiert.parts/ Zwischenstände -fertig/[_bereiche].txt abgeschlossene Abschriften mit Quellenkopf -testdaten// echte Reden als Regressionstest-Eingaben (.vtt, .txt, Glossar) -tests/daten/ erfundene Beispielrede für die Tests (im Repository) -``` - -`glossare/`, `abschriften/`, `fertig/` und `testdaten/` stehen in `.gitignore`: Sie -enthalten Inhalte realer Beiträge und Personen und bleiben nur lokal. Wertvoll sind -davon auf Dauer nur `glossare/` und `fertig/`. Diese beiden gehören in eine eigene -Sicherung, z. B. ein zweites, **privates** Git-Repository. - -Ein Video durchläuft: Rohtext holen → (Video-Glossar anlegen) → korrigieren → -Bericht prüfen und nacharbeiten → mit `abschliessen.py` abschließen. Danach bleibt -nur `fertig/.txt`. - ## Ablauf 0. **Vorprüfung:** Läuft Ollama, ist das Modell installiert? Sonst Abbruch mit @@ -404,22 +368,17 @@ Ersetzungen ohne Treffer werden gesammelt gemeldet. Bei einem Glossar für einen einzelnen Beitrag deutet das auf einen Tippfehler hin, bei einem gemeinsamen Glossar ist es normal. -Es gibt zwei Arten von Glossaren: +Mehrere Glossare lassen sich kombinieren: ein gemeinsames für wiederkehrende Namen +und Hörfehler eines Redners oder Kanals und eines je Beitrag, z. B. +`glossare/krone-schmalz.glossar.txt` (gemeinsam) plus `glossare/ramstein.glossar.txt`. +Das gemeinsame Glossar zuerst angeben, damit seine Ersetzungen vor den speziellen +greifen. -- **Gemeinsame Glossare** für wiederkehrende Namen und Hörfehler eines Redners oder - Kanals liegen in `glossare/`, z. B. `glossare/krone-schmalz.glossar.txt`. Sie werden - mit `-g` angegeben. -- **Video-Glossare** gelten nur für ein Video und liegen in dessen Arbeitsordner, - benannt wie der Ordner: `abschriften//.glossar.txt`. - `abschrift_korrigieren.py`, `glossar_vorschlag.py` und `aenderungen.py` nutzen es - automatisch (Meldung „Video-Glossar, automatisch“), und zwar nach den mit `-g` - angegebenen, damit die gemeinsamen Ersetzungen zuerst greifen. Beim Abschluss nennt - `abschliessen.py` seine Einträge, die noch in keinem gemeinsamen Glossar stehen. - -Glossare gehören zu realen Beiträgen und Personen und werden deshalb nicht -versioniert (`glossare/`, `abschriften/` und `*.glossar.txt` stehen in `.gitignore`); -`glossare/` sollte gesondert gesichert werden. Im Repository ist als Beispiel nur -`tests/daten/beispiel.glossar.txt` zur erfundenen Beispielrede. +Glossare liegen im Verzeichnis `glossare/`. Sie gehören zu realen Beiträgen und +Personen und werden deshalb nicht versioniert: `glossare/` und `*.glossar.txt` stehen +in `.gitignore`, die Glossare bleiben nur lokal und sollten gesondert gesichert +werden. Im Repository ist als Beispiel nur `tests/daten/beispiel.glossar.txt` zur +erfundenen Beispielrede. ### Glossar-Entwurf: `glossar_vorschlag.py` @@ -461,46 +420,6 @@ Der Entwurf ist ein Ausgangspunkt, kein fertiges Glossar: jeden Eintrag prüfen, Brauchbares ins Glossar des Beitrags (oder ins gemeinsame) übernehmen. Die Modellantworten werden in `.parts/` zwischengespeichert. -## Abschluss: `abschliessen.py` - -```bash -./abschliessen.py abschriften/ # mit Rückfrage vor dem Löschen -./abschliessen.py abschriften/ --ja # ohne Rückfrage -``` - -1. Ergebnisse sind alle `*.korrigiert.txt` im Ordner (bei Ausschnitten mehrere). -2. Abbruch, solange ein `*.neu.korrigiert.txt` (Konflikt mit Handarbeit) da ist oder - noch `[prüfen: …]`-Marken im Text stehen (`--trotzdem` schließt dennoch ab). -3. Jedes Ergebnis kommt als `fertig/[_bereiche].txt` mit Quellenkopf: - - ```text - Titel: Prof. Gabriele Krone-Schmalz – Respekt geht anders - Kanal: GGBLahnstein - Datum: 2026-07-30 - Quelle: https://www.youtube.com/watch?v=rm6lADgf6GQ - Abgeschlossen: 2026-10-11 - - - ``` - - Ohne `*.quelle.json` nennt der Kopf nur die Datei. Eine vorhandene Datei in - `fertig/` mit anderem Text wird nie überschrieben; mit demselben Text gilt das - Video als schon übernommen (z. B. nach „nicht löschen“). -4. Einträge des Video-Glossars, die in keinem Glossar unter `glossare/` stehen, werden - aufgelistet, damit Brauchbares vorher ins gemeinsame Glossar wandert. -5. Der Arbeitsordner wird angezeigt und nach Rückfrage gelöscht. Nie gelöscht werden - Ordner mit fremden Dateien (andere als `.txt`, `.vtt`, `.html`, `.json`, - `*.parts/`), das Aufrufverzeichnis und alles in `testdaten/`, `glossare/`, `tests/` - oder `fertig/`. - -| Option | Bedeutung | -|---|---| -| `ordner` | Arbeitsordner, z. B. `abschriften/` | -| `--ziel` | Verzeichnis für fertige Abschriften; Standard: `fertig` | -| `--glossare` | Verzeichnis der gemeinsamen Glossare; Standard: `glossare` | -| `--ja` | ohne Rückfrage löschen | -| `--trotzdem` | auch mit offenen Prüfmarken abschließen | - ## Testdaten **Im Repository:** `tests/daten/` enthält eine frei erfundene Beispielrede @@ -515,29 +434,20 @@ passendes Glossar (`beispiel.glossar.txt`) für die eingebauten Hörfehler wie ``` **Nur lokal:** Echte Reden gehören wegen des Urheberrechts nicht ins Repository; -`testdaten/` ist deshalb in `.gitignore`, ebenso die Glossare. Es enthält je Rede -einen Ordner mit unveränderlichen Eingaben, nie Ausgaben: +`testdaten/` ist deshalb in `.gitignore`, ebenso die Glossare (`glossare/`). +Liegen die drei Reden, mit denen das Programm entwickelt wurde, in `testdaten/` und +ihre Glossare in `glossare/`, laufen zusätzlich die Tests, die diese Glossare +prüfen; sonst werden sie übersprungen: -- `testdaten/ramstein/`: `ramstein.de.vtt`, `ramstein.de.txt`, `ramstein.glossar.txt` - (Rede über die US Air Base Ramstein) -- `testdaten/magdeburg/`: `magdeburg.de-orig.vtt`, `.txt`, `magdeburg.glossar.txt` - (Rede in Magdeburg) -- `testdaten/lahnstein/`: `Lahnstein.de-orig.vtt`, `.txt`, `lahnstein.glossar.txt` - (Rede „Respekt geht anders“, Glossar mit Hilfe von `glossar_vorschlag.py`), zu laden - mit `./yt_untertitel.py -u 'https://www.youtube.com/watch?v=rm6lADgf6GQ' -n Lahnstein -d testdaten/lahnstein` +- `ramstein.de.*`: Rede über die US Air Base Ramstein (Glossar `glossare/ramstein.glossar.txt`) +- `magdeburg.de-orig.*`: Rede in Magdeburg (Glossar `glossare/magdeburg.glossar.txt`) +- `Lahnstein.de-orig.*`: Rede „Respekt geht anders“ in Lahnstein (Glossar + `glossare/lahnstein.glossar.txt`, erstellt mit Hilfe von `glossar_vorschlag.py`), + zu laden mit + `./yt_untertitel.py -u 'https://www.youtube.com/watch?v=rm6lADgf6GQ' -n Lahnstein -d testdaten` -Liegen sie dort und `glossare/krone-schmalz.glossar.txt` daneben, laufen zusätzlich -die Tests, die diese Glossare prüfen; sonst werden sie übersprungen. Eine echte Rede -korrigiert man mit Ausgabe in einen Arbeitsordner, damit `testdaten/` sauber bleibt: - -```bash -./abschrift_korrigieren.py -i testdaten/ramstein/ramstein.de.txt \ - -g glossare/krone-schmalz.glossar.txt -g testdaten/ramstein/ramstein.glossar.txt \ - -o abschriften/ramstein.de/ramstein.de.korrigiert.txt -``` - -Ein Lauf über eine Rede dauert mit `qwen3:32b` vollständig auf einer RTX 3090 etwa -6–8 Minuten. +Alle drei mit dem gemeinsamen `glossare/krone-schmalz.glossar.txt` davor. Ein Lauf über eine +Rede dauert mit `qwen3:32b` vollständig auf einer RTX 3090 etwa 6–8 Minuten. ## Tests @@ -547,9 +457,9 @@ python3 -m pytest Die Tests brauchen weder Ollama noch Netz: Modellaufrufe und `yt-dlp` werden durch Fakes ersetzt. Als Text dient die erfundene Beispielrede in `tests/daten/` (siehe -[Testdaten](#testdaten)). In einem frischen Klon laufen 339 Tests, 7 werden +[Testdaten](#testdaten)). In einem frischen Klon laufen 318 Tests, 7 werden übersprungen; sie prüfen die Glossare der echten Reden und laufen nur, wenn diese -lokal in `testdaten/` liegen (dann 346). +lokal in `testdaten/` liegen (dann 325). `tests/test_abschrift_korrigieren.py` prüft Abschnittsbildung, Satzgrenzen (Abkürzungen und Ordnungszahlen sind kein Satzende), Glossar (jede Ersetzung des @@ -557,8 +467,7 @@ Beispielglossars trifft die Beispielrede; lokal zusätzlich: jede Ersetzung im Glossar einer Rede trifft diese Rede, jede im gemeinsamen Glossar mindestens eine), Prompts (keine Namensteile ergänzen), Streaming-Auswertung, Absatzgliederung (Text bleibt unverändert, 3–10 Sätze), Verbinden kurzer Absätze (Modell wird nur gefragt, -wenn beide Nachbarn passen), Zwischenspeicher/Fortsetzen, Arbeitsordner, -Quellenangabe und automatisches Video-Glossar, Schutz von Handarbeit +wenn beide Nachbarn passen), Zwischenspeicher/Fortsetzen, Schutz von Handarbeit (`.neu.korrigiert.txt`, `--ueberschreiben`), Zeitauswahl, Sprachen und die Kommandozeile. `tests/test_glossar_vorschlag.py` prüft Filter (nur Funde im Text, keine bekannten), @@ -572,9 +481,6 @@ Die Tests nutzen eine kleine eigene Wortliste statt der des Systems. (Original-Audio, manuell vor automatisch) und Fehlerfälle. `tests/test_yt_untertitel.py` prüft Spurauswahl, Aufruf von `yt-dlp` und Fehlerfälle (ohne Netz, `yt-dlp` wird durch Fakes ersetzt). -`tests/test_abschliessen.py` prüft den Abschluss: Quellenkopf, mehrere Ausschnitte, -Abbruch bei Prüfmarken und Konflikten, nie überschreiben, Rückfrage, Schutz vor dem -Löschen fremder und geschützter Ordner und den Hinweis auf Video-Glossar-Einträge. `tests/test_vtt2txt.py` prüft die VTT-Umwandlung: die Beispiel-`.vtt` (und lokal jede Rede in `testdaten/`) ergibt genau die zugehörige `.txt`, echte Wiederholungen bleiben erhalten, auch bei Untertiteln ohne rollendes Fenster, dazu Zeitauswahl @@ -588,9 +494,7 @@ Verbinden kurzer Absätze wird in `.parts/` gespeichert. Ein erneuter Lauf überspringt alles, was schon berechnet ist. Ändern sich Modell (auch per `-m`), Prompt, Glossar oder Text eines Abschnitts, wird genau dieser Teil neu berechnet. Glossar-Begriffe stehen im Prompt; ein neuer Begriff -lässt deshalb alle Abschnitte neu korrigieren. Den Ordner kann man jederzeit löschen; -`abschliessen.py` löscht ihn mit dem Arbeitsordner. Er wächst sonst mit jeder -Prompt- oder Modelländerung, weil alte Fassungen liegen bleiben. +lässt deshalb alle Abschnitte neu korrigieren. Den Ordner kann man jederzeit löschen. `glossar_vorschlag.py` speichert seine Modellantworten ebenso in `.parts/`. @@ -626,15 +530,6 @@ In `vtt2txt.py`: | `MAX_SNAP_SECONDS` | 30 | so weit darf ein Zeitbereich bis zur nächsten Satzgrenze wachsen | | `ROLLING_SHARE` | 0.5 | ab diesem Anteil sich überlappender Blöcke gilt eine Datei als rollend | -In `yt_untertitel.py` und `abschliessen.py` (Pfade relativ zum Aufrufverzeichnis, also -im Programmordner aufrufen): - -| Konstante | Standard | Bedeutung | -|---|---|---| -| `WORK_DIR` | `abschriften` | Arbeitsordner je Video: `abschriften//` | -| `FINISHED_DIR` | `fertig` | abgeschlossene Abschriften (Option `--ziel`) | -| `GLOSSARY_DIR` | `glossare` | gemeinsame Glossare (Option `--glossare`) | - ## Bekannte Grenzen - Das Modell ändert gelegentlich Wörter eigenmächtig, auch sinnverändernd diff --git a/abschliessen.py b/abschliessen.py deleted file mode 100755 index ee1c7d5..0000000 --- a/abschliessen.py +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/env python3 -"""Schließt die Abschrift eines Videos ab: Die korrigierte Textdatei kommt mit einem -Quellenkopf nach fertig/, der Arbeitsordner abschriften// wird nach Rückfrage -gelöscht.""" -from datetime import date -from pathlib import Path -import argparse -import json -import shutil -import sys - -from abschrift_korrigieren import read_glossary, video_glossary -from aenderungen import REVIEW_MARK - -FINISHED_DIR = Path("fertig") -GLOSSARY_DIR = Path("glossare") -# What a working folder may hold; anything else stops the deletion -WORK_SUFFIXES = {".txt", ".vtt", ".html", ".json"} -# Never deleted, nor anything inside them -PROTECTED = {"testdaten", "glossare", "tests", "fertig"} -SHOWN_MARKS = 3 - - -def results(folder): - """Corrected texts of the folder (several for excerpts), without conflicts.""" - return sorted( - p for p in folder.glob("*.korrigiert.txt") - if not p.name.endswith(".neu.korrigiert.txt") - ) - - -def finished_name(result): - """rede_00-05-00_bis_00-06-00.korrigiert.txt -> rede_00-05-00_bis_00-06-00.txt""" - return result.name.removesuffix(".korrigiert.txt") + ".txt" - - -def read_source(folder): - """Contents of the folder's .quelle.json, None without one.""" - for path in sorted(folder.glob("*.quelle.json")): - return json.loads(path.read_text(encoding="utf-8")) - return None - - -def format_date(text): - """"20260328" -> "2026-03-28"; anything else unchanged.""" - if text and len(text) == 8 and text.isdigit(): - return f"{text[:4]}-{text[4:6]}-{text[6:]}" - return text - - -def header(source, result, today): - """Source lines in front of the finished text, then a blank line.""" - lines = [] - if source: - for label, value in ( - ("Titel", source.get("titel")), - ("Kanal", source.get("kanal")), - ("Datum", format_date(source.get("datum"))), - ("Quelle", source.get("url")), - ): - if value: - lines.append(f"{label}: {value}") - else: - lines.append(f"Datei: {result.name}") - lines.append(f"Abgeschlossen: {today}") - return "\n".join(lines) + "\n\n" - - -def already_finished(target, text): - """True if the target holds this text below a header.""" - if not target.exists(): - return False - _, _, body = target.read_text(encoding="utf-8").partition("\n\n") - return body == text - - -def open_marks(text): - """(line number, line) of every line with a review mark.""" - return [ - (number, line.strip()) - for number, line in enumerate(text.splitlines(), start=1) - if REVIEW_MARK.search(line) - ] - - -def unshared_entries(glossary, glossary_dir): - """Entries of the video glossary that are in no shared glossary.""" - shared_terms, shared_replacements = set(), set() - for path in sorted(glossary_dir.glob("*.glossar.txt")): - terms, replacements = read_glossary(path) - shared_terms.update(terms) - shared_replacements.update(replacements) - terms, replacements = read_glossary(glossary) - return [ - f"{wrong} => {right}" for wrong, right in replacements - if (wrong, right) not in shared_replacements - ] + [term for term in terms if term not in shared_terms] - - -def deletion_blockers(folder): - """Reasons not to delete the folder; empty for a plain working folder.""" - resolved = folder.resolve() - cwd = Path.cwd().resolve() - if resolved == cwd or resolved in cwd.parents: - return ["Er ist das Aufrufverzeichnis oder liegt darüber."] - for path in (resolved, *resolved.parents): - if path == cwd: - break - if path.name in PROTECTED: - return [f"Er ist oder liegt in „{path.name}“, das nie gelöscht wird."] - return [ - f"Fremde Datei: {entry.name}" - for entry in sorted(folder.iterdir()) - if not ( - (entry.is_dir() and entry.name.endswith(".parts")) - or (entry.is_file() and entry.suffix.lower() in WORK_SUFFIXES) - ) - ] - - -def size(path): - """Size of a file or folder, readable: "830 B", "17 KB".""" - if path.is_file(): - total = path.stat().st_size - else: - total = sum(p.stat().st_size for p in path.rglob("*") if p.is_file()) - return f"{total} B" if total < 1024 else f"{total / 1024:.0f} KB" - - -def confirm(question): - try: - return input(question).strip().lower() in {"j", "ja", "y", "yes"} - except EOFError: - return False - - -def parse_args(): - parser = argparse.ArgumentParser( - description="Schließt die Abschrift eines Videos ab: Ergebnis mit Quellenkopf " - "nach fertig/, Arbeitsordner nach Rückfrage löschen." - ) - parser.add_argument("ordner", type=Path, help="Arbeitsordner, z. B. abschriften/") - parser.add_argument( - "--ziel", type=Path, default=FINISHED_DIR, - help=f"Verzeichnis für fertige Abschriften (Standard: {FINISHED_DIR})", - ) - parser.add_argument( - "--glossare", type=Path, default=GLOSSARY_DIR, - help=f"Verzeichnis der gemeinsamen Glossare (Standard: {GLOSSARY_DIR})", - ) - parser.add_argument( - "--ja", action="store_true", help="Arbeitsordner ohne Rückfrage löschen" - ) - parser.add_argument( - "--trotzdem", action="store_true", - help="auch abschließen, wenn noch Prüfmarken [prüfen: …] im Text stehen", - ) - args = parser.parse_args() - if not args.ordner.is_dir(): - parser.error(f"Ordner nicht gefunden: {args.ordner}") - return args - - -def main(): - args = parse_args() - folder = args.ordner - - found = results(folder) - if not found: - raise SystemExit(f"Keine korrigierte Abschrift (*.korrigiert.txt) in {folder}.") - conflicts = sorted(folder.glob("*.neu.korrigiert.txt")) - if conflicts: - names = ", ".join(p.name for p in conflicts) - raise SystemExit( - f"Ungeklärter Konflikt: {names}. Erst die Änderungen übertragen bzw. die " - "Datei löschen, dann abschließen." - ) - - texts = {result: result.read_text(encoding="utf-8") for result in found} - marked = {result: open_marks(text) for result, text in texts.items()} - if any(marked.values()): - for result, marks in marked.items(): - if marks: - print(f"{result.name}: {len(marks)} Zeile(n) mit Prüfmarken, z. B.") - for number, line in marks[:SHOWN_MARKS]: - print(f" Zeile {number}: {line[:120]}") - if not args.trotzdem: - raise SystemExit( - "Noch offene Prüfmarken. Erst klären, oder mit --trotzdem abschließen." - ) - - targets = {result: args.ziel / finished_name(result) for result in found} - # A finished text with the same body is from an earlier run (e.g. answered - # "n" when asked to delete); a different one is never overwritten - done = {result for result, target in targets.items() if already_finished(target, texts[result])} - existing = [str(t) for r, t in targets.items() if t.exists() and r not in done] - if existing: - raise SystemExit( - f"Gibt es schon in {args.ziel} mit anderem Text: {', '.join(existing)}. " - "Nichts überschrieben; vorhandene Datei umbenennen oder entfernen." - ) - - source = read_source(folder) - if source is None and len(done) < len(targets): - print("Hinweis: keine Quellenangabe (*.quelle.json); der Kopf nennt nur die Datei.") - today = date.today().isoformat() - args.ziel.mkdir(parents=True, exist_ok=True) - for result, target in targets.items(): - if result in done: - print(f"Bereits fertig: {target}") - continue - target.write_text(header(source, result, today) + texts[result], encoding="utf-8") - print(f"Fertig: {target}") - - glossary = video_glossary(folder) - if glossary is not None: - missing = unshared_entries(glossary, args.glossare) - if missing: - print( - f"\nDiese Einträge aus {glossary} stehen in keinem Glossar in " - f"{args.glossare}/; vor dem Löschen ggf. in ein gemeinsames übernehmen:" - ) - for entry in missing: - print(f" {entry}") - - blockers = deletion_blockers(folder) - if blockers: - print(f"\nArbeitsordner {folder} bleibt erhalten:") - for reason in blockers: - print(f" {reason}") - return - - print(f"\nArbeitsordner {folder}:") - for entry in sorted(folder.iterdir()): - print(f" {entry.name}{'/' if entry.is_dir() else ''} ({size(entry)})") - if not args.ja and not confirm(f"Arbeitsordner {folder} löschen? [j/N] "): - print("Nicht gelöscht.") - return - shutil.rmtree(folder) - print(f"Gelöscht: {folder}") - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/abschrift_korrigieren.py b/abschrift_korrigieren.py index e051cb9..f8c3f69 100755 --- a/abschrift_korrigieren.py +++ b/abschrift_korrigieren.py @@ -21,9 +21,7 @@ from vtt2txt import ( add_time_options, join_segments, range_argv, resolve_time_ranges, split_segments, time_suffix, vtt_to_text, ) -from yt_untertitel import ( - YT_DLP, base_language, default_name, original_language, work_folder, write_source, -) +from yt_untertitel import YT_DLP, base_language, default_name, original_language MODEL = "qwen3:32b" OLLAMA_URL = "http://127.0.0.1:11434/api/chat" @@ -266,22 +264,6 @@ def read_glossary(path): return terms, replacements -def video_glossary(folder): - """Glossary of the video in `folder`, named like the folder: - abschriften//.glossar.txt; None if there is none.""" - path = folder / f"{folder.resolve().name}.glossar.txt" - return path if path.is_file() else None - - -def with_video_glossary(glossaries, folder): - """The given glossaries plus the video glossary of `folder` (last, after the - shared ones), unless it was given; returns (glossaries, added or None).""" - found = video_glossary(folder) - if found is None or any(g.resolve() == found.resolve() for g in glossaries): - return list(glossaries), None - return [*glossaries, found], found - - def apply_replacements(text, replacements): """Replace whole words in file order; report rules without any match.""" unmatched = [] @@ -651,8 +633,8 @@ def parse_args(): parser.add_argument( "-o", "--output", type=Path, help="Ausgabedatei (Standard: .korrigiert.txt, " - "mit -u abschriften//.korrigiert.txt mit " - " = __; mit -z zusätzlich die Bereiche)", + "mit -u __.korrigiert.txt; mit -z zusätzlich " + "die Bereiche)", ) parser.add_argument( "-g", "--glossar", type=Path, action="append", default=[], @@ -727,12 +709,9 @@ def read_source(args): f"Der Beitrag ist auf „{lang}“; unterstützt: {', '.join(PROMPTS)} (-l)." ) suffix = time_suffix(args.bereiche) - name = default_name(info) - target = args.output or work_folder(name) / f"{name}{suffix}.korrigiert.txt" + target = args.output or Path(f"{default_name(info)}{suffix}.korrigiert.txt") raw = text + "\n" - raw_path = raw_text_path(target) - save_raw_text(raw_path, raw) - write_source(info, raw_path.with_suffix(".quelle.json")) + save_raw_text(raw_text_path(target), raw) return raw, target, lang raw = args.input.read_text(encoding="utf-8-sig") @@ -783,12 +762,10 @@ def main(): parts_dir = target.with_suffix(".parts") terms, replacements = [], [] - glossaries, automatic = with_video_glossary(args.glossar, target.parent) - for glossary in glossaries: + for glossary in args.glossar: more_terms, more_replacements = read_glossary(glossary) - note = " (Video-Glossar, automatisch)" if glossary == automatic else "" print( - f"Glossar {glossary}{note}: {len(more_terms)} Begriffe, " + f"Glossar {glossary}: {len(more_terms)} Begriffe, " f"{len(more_replacements)} feste Ersetzungen" ) terms += more_terms diff --git a/aenderungen.py b/aenderungen.py index 32af953..f2eca03 100755 --- a/aenderungen.py +++ b/aenderungen.py @@ -473,16 +473,13 @@ def parse_args(): def main(): # abschrift_korrigieren imports this module: import it only when needed - from abschrift_korrigieren import apply_replacements, read_glossary, with_video_glossary + from abschrift_korrigieren import apply_replacements, read_glossary from yt_untertitel import base_language args = parse_args() raw = read_raw(args.roh) terms, replacements = [], [] - glossaries, automatic = with_video_glossary(args.glossar, args.korrigiert.parent) - if automatic is not None: - print(f"Video-Glossar (automatisch): {automatic}") - for glossary in glossaries: + for glossary in args.glossar: more_terms, more_replacements = read_glossary(glossary) terms += more_terms replacements += more_replacements diff --git a/extract_text.py b/extract_text.py index 1ba11a9..a4c5a76 100755 --- a/extract_text.py +++ b/extract_text.py @@ -14,7 +14,7 @@ from vtt2txt import ( ) from yt_untertitel import ( YT_DLP, choose_track, default_name, download, fetch_info, no_track_message, - original_language, work_folder, write_source, + original_language, ) FLOW_WIDTH = 80 # Zeilenbreite für --fliesstext @@ -31,8 +31,8 @@ def parse_args(): ) parser.add_argument( "-o", "--output", type=Path, - help="Ausgabedatei (Standard: abschriften//.txt mit " - " = __, mit -z zusätzlich die Bereiche)", + help="Ausgabedatei (Standard: __.txt, " + "mit -z zusätzlich die Bereiche)", ) parser.add_argument( "-f", "--fliesstext", action="store_true", @@ -87,8 +87,7 @@ def main(): args = parse_args() info, text = fetch_text(args.url, args.lang, args.bereiche, args.exakt) suffix = time_suffix(args.bereiche) - name = default_name(info) - output = args.output or work_folder(name) / f"{name}{suffix}.txt" + output = args.output or Path(f"{default_name(info)}{suffix}.txt") segments = split_segments(text) if args.fliesstext: @@ -105,7 +104,6 @@ def main(): output.parent.mkdir(parents=True, exist_ok=True) output.write_text(text + "\n", encoding="utf-8") - write_source(info, output.with_suffix(".quelle.json")) print( f"Gespeichert: {output} " f"({len(text.splitlines())} Zeilen, " diff --git a/glossar_vorschlag.py b/glossar_vorschlag.py index 28d532d..2750478 100755 --- a/glossar_vorschlag.py +++ b/glossar_vorschlag.py @@ -13,7 +13,7 @@ import re from aenderungen import compare, read_raw from abschrift_korrigieren import ( MODEL, apply_replacements, chat_request, check_ollama, ollama_open, read_glossary, - split_chunks, with_video_glossary, + split_chunks, ) from vtt2txt import split_segments @@ -226,10 +226,7 @@ def main(): raw = text known, replacements = set(), [] - glossaries, automatic = with_video_glossary(args.glossar, args.input.parent) - if automatic is not None: - print(f"Video-Glossar (automatisch): {automatic}") - for glossary in glossaries: + for glossary in args.glossar: _, more = read_glossary(glossary) replacements += more known.update(wrong for wrong, _ in more) diff --git a/tests/test_abschliessen.py b/tests/test_abschliessen.py deleted file mode 100644 index 40496a1..0000000 --- a/tests/test_abschliessen.py +++ /dev/null @@ -1,162 +0,0 @@ -"""Tests for abschliessen.py (finish a video: result to fertig/, delete working folder).""" -import json -import sys -from datetime import date - -import pytest - -import abschliessen as ab - -TEXT = "Der Kreml ist in Moskau.\n\nDas ist bekannt.\n" -SOURCE = { - "titel": "Eine Rede", "kanal": "Kanal", "datum": "20260328", - "url": "https://www.youtube.com/watch?v=abc123", "id": "abc123", -} - - -@pytest.fixture -def work(tmp_path, monkeypatch): - """abschriften/rede/ with everything a run leaves behind; cwd is tmp_path.""" - monkeypatch.chdir(tmp_path) - folder = tmp_path / "abschriften" / "rede" - parts = folder / "rede.korrigiert.parts" - parts.mkdir(parents=True) - (parts / "part001-abc.txt").write_text("x", encoding="utf-8") - (folder / "rede.txt").write_text("der kremmel ist in moskau\n", encoding="utf-8") - (folder / "rede.korrigiert.txt").write_text(TEXT, encoding="utf-8") - (folder / "rede.aenderungen.html").write_text("

", encoding="utf-8") - (folder / "rede.quelle.json").write_text(json.dumps(SOURCE), encoding="utf-8") - return folder - - -def run_main(monkeypatch, *args, answer=None): - monkeypatch.setattr(sys, "argv", ["abschliessen.py", *map(str, args)]) - if answer is not None: - monkeypatch.setattr("builtins.input", lambda question: answer) - ab.main() - - -def test_finish_writes_header_and_deletes(work, tmp_path, monkeypatch, capsys): - run_main(monkeypatch, work, "--ja") - - finished = (tmp_path / "fertig" / "rede.txt").read_text(encoding="utf-8") - assert finished == ( - "Titel: Eine Rede\nKanal: Kanal\nDatum: 2026-03-28\n" - "Quelle: https://www.youtube.com/watch?v=abc123\n" - f"Abgeschlossen: {date.today().isoformat()}\n\n" + TEXT - ) - assert not work.exists() - out = capsys.readouterr().out - assert "rede.korrigiert.parts/" in out and f"Gelöscht: {work}" in out - - -def test_answer_no_keeps_folder_and_rerun_deletes(work, tmp_path, monkeypatch, capsys): - run_main(monkeypatch, work, answer="n") - assert work.is_dir() - assert (tmp_path / "fertig" / "rede.txt").is_file() - assert "Nicht gelöscht." in capsys.readouterr().out - - # Later: the result is already in fertig/, only the deletion is left - run_main(monkeypatch, work, answer="j") - assert not work.exists() - assert "Bereits fertig" in capsys.readouterr().out - - -def test_open_review_marks_stop(work, tmp_path, monkeypatch, capsys): - (work / "rede.korrigiert.txt").write_text( - "Der Kreml [prüfen: Kremmel] ist in Moskau.\n", encoding="utf-8" - ) - with pytest.raises(SystemExit, match="offene Prüfmarken"): - run_main(monkeypatch, work, "--ja") - assert "Zeile 1: Der Kreml [prüfen: Kremmel]" in capsys.readouterr().out - assert not (tmp_path / "fertig").exists() and work.is_dir() - - run_main(monkeypatch, work, "--ja", "--trotzdem") - assert (tmp_path / "fertig" / "rede.txt").is_file() - - -def test_conflict_stops(work, monkeypatch): - (work / "rede.neu.korrigiert.txt").write_text(TEXT, encoding="utf-8") - with pytest.raises(SystemExit, match="Ungeklärter Konflikt: rede.neu.korrigiert.txt"): - run_main(monkeypatch, work, "--ja") - assert work.is_dir() - - -def test_no_result_stops(work, monkeypatch): - (work / "rede.korrigiert.txt").unlink() - with pytest.raises(SystemExit, match="Keine korrigierte Abschrift"): - run_main(monkeypatch, work, "--ja") - - -def test_existing_different_target_is_never_overwritten(work, tmp_path, monkeypatch): - (tmp_path / "fertig").mkdir() - (tmp_path / "fertig" / "rede.txt").write_text("Anderer Text\n", encoding="utf-8") - with pytest.raises(SystemExit, match="mit anderem Text"): - run_main(monkeypatch, work, "--ja") - assert (tmp_path / "fertig" / "rede.txt").read_text(encoding="utf-8") == "Anderer Text\n" - assert work.is_dir() - - -def test_without_source_header_names_the_file(work, tmp_path, monkeypatch, capsys): - (work / "rede.quelle.json").unlink() - run_main(monkeypatch, work, "--ja") - finished = (tmp_path / "fertig" / "rede.txt").read_text(encoding="utf-8") - assert finished.startswith("Datei: rede.korrigiert.txt\nAbgeschlossen: ") - assert "keine Quellenangabe" in capsys.readouterr().out - - -def test_every_excerpt_is_finished(work, tmp_path, monkeypatch): - (work / "rede_ab_00-05-00.korrigiert.txt").write_text("[00:05:00 - 00:06:00]\n\nB.\n") - run_main(monkeypatch, work, "--ja") - assert (tmp_path / "fertig" / "rede.txt").is_file() - assert (tmp_path / "fertig" / "rede_ab_00-05-00.txt").read_text().endswith( - "[00:05:00 - 00:06:00]\n\nB.\n" - ) - - -def test_foreign_file_prevents_deletion(work, tmp_path, monkeypatch, capsys): - (work / "notizen.docx").write_bytes(b"x") - run_main(monkeypatch, work, "--ja") - assert work.is_dir() - assert (tmp_path / "fertig" / "rede.txt").is_file() - assert "Fremde Datei: notizen.docx" in capsys.readouterr().out - - -@pytest.mark.parametrize("protected", ["testdaten", "glossare", "tests", "fertig"]) -def test_protected_folders_are_never_deleted(tmp_path, monkeypatch, capsys, protected): - monkeypatch.chdir(tmp_path) - folder = tmp_path / protected / "rede" - folder.mkdir(parents=True) - (folder / "rede.korrigiert.txt").write_text(TEXT, encoding="utf-8") - run_main(monkeypatch, folder, "--ja", "--ziel", tmp_path / "ziel") - assert folder.is_dir() - assert f"„{protected}“, das nie gelöscht wird" in capsys.readouterr().out - - -def test_current_directory_is_never_deleted(tmp_path, monkeypatch, capsys): - monkeypatch.chdir(tmp_path) - (tmp_path / "rede.korrigiert.txt").write_text(TEXT, encoding="utf-8") - run_main(monkeypatch, ".", "--ja") - assert (tmp_path / "rede.korrigiert.txt").is_file() - assert "Aufrufverzeichnis" in capsys.readouterr().out - - -def test_video_glossary_entries_not_shared_are_listed(work, tmp_path, monkeypatch, capsys): - (work / "rede.glossar.txt").write_text( - "Kremmel => Kreml\nNetano => Netanjahu\nEgon Bahr\nMoskau\n", encoding="utf-8" - ) - (tmp_path / "glossare").mkdir() - (tmp_path / "glossare" / "gemeinsam.glossar.txt").write_text( - "Kremmel => Kreml\nMoskau\n", encoding="utf-8" - ) - run_main(monkeypatch, work, "--ja") - out = capsys.readouterr().out - listed = out.split("gemeinsames übernehmen:")[1].split("\n\n")[0] - assert "Netano => Netanjahu" in listed and "Egon Bahr" in listed - assert "Kremmel" not in listed and "Moskau" not in listed - - -def test_missing_folder(tmp_path, monkeypatch, capsys): - with pytest.raises(SystemExit): - run_main(monkeypatch, tmp_path / "fehlt") - assert "Ordner nicht gefunden" in capsys.readouterr().err diff --git a/tests/test_abschrift_korrigieren.py b/tests/test_abschrift_korrigieren.py index c49b739..297e8d2 100644 --- a/tests/test_abschrift_korrigieren.py +++ b/tests/test_abschrift_korrigieren.py @@ -14,8 +14,7 @@ REPO = Path(__file__).resolve().parent.parent # Own, made-up sample speech; real speeches are only kept locally in testdaten/ SAMPLE = REPO / "tests" / "daten" / "beispiel.de.txt" GLOSSARY = REPO / "tests" / "daten" / "beispiel.glossar.txt" -# Real glossaries and speeches are only kept locally (see .gitignore): shared -# glossaries in glossare/, each speech with its glossary in testdaten// +# Real glossaries and speeches are only kept locally (see .gitignore) GLOSSARIES = REPO / "glossare" SHARED_GLOSSARY = GLOSSARIES / "krone-schmalz.glossar.txt" SPEECHES = REPO / "testdaten" @@ -128,18 +127,17 @@ def test_build_system_appends_terms(): assert "keep only the surname" in ak.SYSTEM_EN -# testdaten//: raw text and the glossary of that speech SAMPLE_GLOSSARIES = [ - ("ramstein/ramstein.glossar.txt", "ramstein/ramstein.de.txt"), - ("magdeburg/magdeburg.glossar.txt", "magdeburg/magdeburg.de-orig.txt"), - ("lahnstein/lahnstein.glossar.txt", "lahnstein/Lahnstein.de-orig.txt"), + ("ramstein.glossar.txt", "ramstein.de.txt"), + ("magdeburg.glossar.txt", "magdeburg.de-orig.txt"), + ("lahnstein.glossar.txt", "Lahnstein.de-orig.txt"), ] # Real speeches and their glossaries are not in the repository; without them these skip needs_speeches = pytest.mark.skipif( not all( - (SPEECHES / sample).is_file() and (SPEECHES / glossary).is_file() + (SPEECHES / sample).is_file() and (GLOSSARIES / glossary).is_file() for glossary, sample in SAMPLE_GLOSSARIES ) or not SHARED_GLOSSARY.is_file(), reason="echte Reden (testdaten/) und ihre Glossare nur lokal", @@ -159,7 +157,7 @@ def test_sample_glossary_replacements_all_match(capsys, glossary, sample): text = flat((SPEECHES / sample).read_text(encoding="utf-8")) text = ak.apply_replacements(text, ak.read_glossary(SHARED_GLOSSARY)[1]) capsys.readouterr() - ak.apply_replacements(text, ak.read_glossary(SPEECHES / glossary)[1]) + ak.apply_replacements(text, ak.read_glossary(GLOSSARIES / glossary)[1]) assert "ohne Treffer" not in capsys.readouterr().out @@ -638,63 +636,13 @@ def kreml_glossary(folder): def test_main_url_default_names(from_url, tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) - work = tmp_path / "abschriften" / "Kanal_Rede_2026-03-28" run_main(monkeypatch, "-u", URL, "-g", kreml_glossary(tmp_path)) - raw = (work / "Kanal_Rede_2026-03-28.txt").read_text(encoding="utf-8") + raw = (tmp_path / "Kanal_Rede_2026-03-28.txt").read_text(encoding="utf-8") assert raw == "Der Kremmel ist in Moskau.\nDas ist bekannt.\n" - result = (work / "Kanal_Rede_2026-03-28.korrigiert.txt").read_text(encoding="utf-8") + result = (tmp_path / "Kanal_Rede_2026-03-28.korrigiert.txt").read_text(encoding="utf-8") assert result == "Der Kreml ist in Moskau. Das ist bekannt.\n" - assert (work / "Kanal_Rede_2026-03-28.aenderungen.html").is_file() - source = json.loads((work / "Kanal_Rede_2026-03-28.quelle.json").read_text(encoding="utf-8")) - assert source["titel"] == "Rede" and source["kanal"] == "Kanal" - assert source["datum"] == "20260328" - assert source["url"] == "https://www.youtube.com/watch?v=abc123" - - -def video_folder(tmp_path): - """abschriften/rede/ with raw text and its video glossary.""" - folder = tmp_path / "abschriften" / "rede" - folder.mkdir(parents=True) - (folder / "rede.txt").write_text("Der Kremmel ist in Moskau.\n", encoding="utf-8") - (folder / "rede.glossar.txt").write_text("Kremmel => Kreml\n", encoding="utf-8") - return folder - - -def test_main_uses_video_glossary(tmp_path, monkeypatch, capsys): - folder = video_folder(tmp_path) - monkeypatch.setattr(ak, "correct_chunk", identity_correct) - monkeypatch.setattr(ak, "find_paragraph_starts", every_nth_start(5)) - run_main(monkeypatch, "-i", folder / "rede.txt") - - result = (folder / "rede.korrigiert.txt").read_text(encoding="utf-8") - assert result == "Der Kreml ist in Moskau.\n" - assert "rede.glossar.txt (Video-Glossar, automatisch)" in capsys.readouterr().out - - -def test_main_video_glossary_given_explicitly_counts_once(tmp_path, monkeypatch, capsys): - folder = video_folder(tmp_path) - shared = tmp_path / "gemeinsam.glossar.txt" - shared.write_text("Moskau\n", encoding="utf-8") - monkeypatch.setattr(ak, "correct_chunk", identity_correct) - monkeypatch.setattr(ak, "find_paragraph_starts", every_nth_start(5)) - run_main(monkeypatch, "-i", folder / "rede.txt", "-g", shared, "-g", folder / "rede.glossar.txt") - - out = capsys.readouterr().out - assert out.count("rede.glossar.txt") == 1 - assert "automatisch" not in out - # Shared glossaries first, as given - assert out.index("gemeinsam.glossar.txt") < out.index("rede.glossar.txt") - - -def test_with_video_glossary(tmp_path): - folder = video_folder(tmp_path) - shared = tmp_path / "gemeinsam.glossar.txt" - assert ak.with_video_glossary([shared], folder) == ( - [shared, folder / "rede.glossar.txt"], folder / "rede.glossar.txt" - ) - # Without a glossary named like the folder nothing is added - assert ak.with_video_glossary([shared], tmp_path) == ([shared], None) + assert (tmp_path / "Kanal_Rede_2026-03-28.aenderungen.html").is_file() def test_main_url_output_option_and_kept_raw_text(from_url, tmp_path, monkeypatch, capsys): @@ -814,21 +762,19 @@ def test_cli_errors(sample_copy, monkeypatch, capsys, args, message): def test_main_url_time_range_default_names(from_url, tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) - work = tmp_path / "abschriften" / "Kanal_Rede_2026-03-28" run_main(monkeypatch, "-u", URL, "-z", "0:00:02") name = "Kanal_Rede_2026-03-28_ab_00-00-02" - raw = (work / f"{name}.txt").read_text(encoding="utf-8") + raw = (tmp_path / f"{name}.txt").read_text(encoding="utf-8") assert raw == "[00:00:02 - 00:00:03]\nDas ist bekannt.\n" - result = (work / f"{name}.korrigiert.txt").read_text(encoding="utf-8") + result = (tmp_path / f"{name}.korrigiert.txt").read_text(encoding="utf-8") assert result == "[00:00:02 - 00:00:03]\n\nDas ist bekannt.\n" def test_main_url_range_from_beginning(from_url, tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) - work = tmp_path / "abschriften" / "Kanal_Rede_2026-03-28" run_main(monkeypatch, "-u", URL, "-z", "-0:00:02") - raw = (work / "Kanal_Rede_2026-03-28_bis_00-00-02.txt").read_text(encoding="utf-8") + raw = (tmp_path / "Kanal_Rede_2026-03-28_bis_00-00-02.txt").read_text(encoding="utf-8") assert raw == "[00:00:01 - 00:00:02]\nDer Kremmel ist in Moskau.\n" @@ -980,15 +926,14 @@ def test_main_url_several_ranges(fake_yt_dlp, yt_dlp_installed, tmp_path, monkey monkeypatch.setattr(subprocess, "run", fake_yt_dlp(VIDEO, vtt=MULTI_VTT)) sent = chunk_recorder(monkeypatch) monkeypatch.chdir(tmp_path) - work = tmp_path / "abschriften" / "Kanal_Rede_2026-03-28" run_main(monkeypatch, "-u", URL, "-z", "0:00:00-0:00:30 0:02", "-g", kreml_glossary(tmp_path)) name = "Kanal_Rede_2026-03-28_bis_00-00-30+ab_00-02-00" - assert (work / f"{name}.txt").read_text(encoding="utf-8") == ( + assert (tmp_path / f"{name}.txt").read_text(encoding="utf-8") == ( "[00:00:01 - 00:00:02]\nDer Kremmel ist in Moskau.\n\n" "[00:02:00 - 00:02:01]\nDas ist bekannt.\n" ) - assert (work / f"{name}.korrigiert.txt").read_text(encoding="utf-8") == ( + assert (tmp_path / f"{name}.korrigiert.txt").read_text(encoding="utf-8") == ( "[00:00:01 - 00:00:02]\n\nDer Kreml ist in Moskau.\n\n" "[00:02:00 - 00:02:01]\n\nDas ist bekannt.\n" ) diff --git a/tests/test_aenderungen.py b/tests/test_aenderungen.py index 700c610..ce492dd 100644 --- a/tests/test_aenderungen.py +++ b/tests/test_aenderungen.py @@ -321,19 +321,3 @@ def test_report_and_summary_put_word_forms_last(tmp_path, capsys): out = capsys.readouterr().out.split("Zu prüfen: ")[1] assert out.startswith("2 Stellen (davon 1 Wortformen)") assert out.index("Professorin") < out.index("„das“ → „dass“") - - -def test_cli_uses_video_glossary(tmp_path, monkeypatch, capsys): - folder = tmp_path / "rede" - folder.mkdir() - raw = folder / "rede.txt" - raw.write_text("das Kremmel und\nBar heute\n", encoding="utf-8") - corrected = folder / "rede.korrigiert.txt" - corrected.write_text("Das Kreml und Bahr heute.\n", encoding="utf-8") - (folder / "rede.glossar.txt").write_text("Kremmel => Kreml\nEgon Bahr\n", encoding="utf-8") - - monkeypatch.setattr(sys, "argv", ["aenderungen.py", "-a", str(raw), "-b", str(corrected)]) - ae.main() - out = capsys.readouterr().out - assert "Video-Glossar (automatisch)" in out - assert "Zu prüfen: 0 Stellen" in out diff --git a/tests/test_extract_text.py b/tests/test_extract_text.py index 404b1e7..07131b9 100644 --- a/tests/test_extract_text.py +++ b/tests/test_extract_text.py @@ -1,5 +1,4 @@ """Tests for extract_text.py; yt-dlp is always replaced by fakes (no network).""" -import json import sys import pytest @@ -39,19 +38,14 @@ def test_default_language_is_original_audio(fake_yt_dlp, tmp_path, monkeypatch, monkeypatch.chdir(tmp_path) run_main(monkeypatch, "-u", URL) - work = tmp_path / "abschriften" / "Kanal_Eine_Rede_2026-03-28" - text = (work / "Kanal_Eine_Rede_2026-03-28.txt").read_text(encoding="utf-8") + text = (tmp_path / "Kanal_Eine_Rede_2026-03-28.txt").read_text(encoding="utf-8") assert text == "Guten Abend.\nSchön, dass Sie da sind.\n" assert "de-orig" in run.calls[1] assert "--write-auto-subs" in run.calls[1] out = capsys.readouterr().out assert "Kanal_Eine_Rede_2026-03-28.txt (2 Zeilen, 7 Wörter)" in out - # Only the text file and where it comes from remain, no VTT - assert sorted(p.name for p in work.iterdir()) == [ - "Kanal_Eine_Rede_2026-03-28.quelle.json", "Kanal_Eine_Rede_2026-03-28.txt", - ] - source = json.loads((work / "Kanal_Eine_Rede_2026-03-28.quelle.json").read_text()) - assert source["kanal"] == "Kanal" and source["url"].endswith("v=abc123") + # Only the text file remains, no VTT + assert [p.name for p in tmp_path.iterdir()] == ["Kanal_Eine_Rede_2026-03-28.txt"] def test_default_language_ignores_dubbed_tracks(fake_yt_dlp, tmp_path, monkeypatch): @@ -139,8 +133,7 @@ def test_time_range_default_name(fake_yt_dlp, tmp_path, monkeypatch): run_main(monkeypatch, "-u", URL, "-z", "0:00:02-0:00:03") name = "Kanal_Eine_Rede_2026-03-28_00-00-02_bis_00-00-03.txt" - work = tmp_path / "abschriften" / "Kanal_Eine_Rede_2026-03-28" - assert (work / name).read_text(encoding="utf-8") == ( + assert (tmp_path / name).read_text(encoding="utf-8") == ( "[00:00:02 - 00:00:03]\nSchön, dass Sie da sind.\n" ) @@ -190,8 +183,7 @@ def test_several_ranges_flow_text_keeps_headers(fake_yt_dlp, tmp_path, monkeypat run_main(monkeypatch, "-u", URL, "-z", "0:00:00-0:00:30 0:02", "-f") name = "Kanal_Eine_Rede_2026-03-28_bis_00-00-30+ab_00-02-00.txt" - work = tmp_path / "abschriften" / "Kanal_Eine_Rede_2026-03-28" - assert (work / name).read_text(encoding="utf-8") == ( + assert (tmp_path / name).read_text(encoding="utf-8") == ( "[00:00:01 - 00:00:02]\nErster Satz der\nRede.\n\n" "[00:02:00 - 00:02:01]\nDritter Satz, der\netwas länger ist.\n" ) diff --git a/tests/test_glossar_vorschlag.py b/tests/test_glossar_vorschlag.py index 0e3913e..ac86ec2 100644 --- a/tests/test_glossar_vorschlag.py +++ b/tests/test_glossar_vorschlag.py @@ -160,18 +160,3 @@ def test_time_headers_never_reach_model_or_changes(tmp_path, monkeypatch): draft = out.read_text(encoding="utf-8") assert "# ? Kremmel => Kreml (1×)" in draft assert "00:0" not in draft.replace(str(raw), "") - - -def test_main_uses_video_glossary(tmp_path, monkeypatch, capsys): - folder = tmp_path / "rede" - folder.mkdir() - raw = folder / "rede.txt" - raw.write_text(RAW + "\n", encoding="utf-8") - (folder / "rede.glossar.txt").write_text("Kremmel => Kreml\n", encoding="utf-8") - model = fake_model(ENTRIES) - monkeypatch.setattr(urllib.request, "urlopen", model) - run_main(monkeypatch, "-i", raw) - - sent = model.calls[0]["messages"][1]["content"] - assert "Kreml" in sent and "Kremmel" not in sent - assert "Video-Glossar (automatisch)" in capsys.readouterr().out diff --git a/tests/test_vtt2txt.py b/tests/test_vtt2txt.py index 52abe50..84abb81 100644 --- a/tests/test_vtt2txt.py +++ b/tests/test_vtt2txt.py @@ -124,8 +124,8 @@ def speech(name): @pytest.mark.parametrize("vtt_path", [ SAMPLE_VTT, - speech("ramstein/ramstein.de.vtt"), speech("magdeburg/magdeburg.de-orig.vtt"), - speech("lahnstein/Lahnstein.de-orig.vtt"), + speech("ramstein.de.vtt"), speech("magdeburg.de-orig.vtt"), + speech("Lahnstein.de-orig.vtt"), ]) def test_sample_vtt_gives_sample_txt(vtt_path): vtt = vtt_path.read_text(encoding="utf-8-sig") diff --git a/tests/test_yt_untertitel.py b/tests/test_yt_untertitel.py index 2049e78..7bd446e 100644 --- a/tests/test_yt_untertitel.py +++ b/tests/test_yt_untertitel.py @@ -140,11 +140,8 @@ def test_yt_dlp_runs_in_temporary_directory(fake_yt_dlp, tmp_path, monkeypatch): assert all(cwd is not None and Path(cwd) != tmp_path for cwd in run.cwds) template = run.calls[1][run.calls[1].index("-o") + 1] - # Without -d: working folder abschriften//, named after -n - work = tmp_path / "abschriften" / "Rede" - assert template == f"{work / 'Rede'}.%(ext)s" - assert (work / "Rede.de-orig.vtt").is_file() - assert (work / "Rede.quelle.json").is_file() + assert template == f"{tmp_path / 'Rede'}.%(ext)s" + assert (tmp_path / "Rede.de-orig.vtt").is_file() def test_main_prefers_manual_and_default_name(fake_yt_dlp, tmp_path, monkeypatch): diff --git a/yt_untertitel.py b/yt_untertitel.py index 94c1987..62b99bd 100755 --- a/yt_untertitel.py +++ b/yt_untertitel.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 """Lädt die Untertitel eines YouTube-Videos als VTT-Datei herunter (über yt-dlp), als Eingabe für vtt2txt.py bzw. abschrift_korrigieren.py.""" -from datetime import date from pathlib import Path import argparse import json @@ -15,7 +14,6 @@ YT_DLP = "yt-dlp" NAME_WORD = re.compile(r"\w+(?:[-.]\w+)*") # keeps "Krone-Schmalz", "28.03.2026" MAX_CHANNEL_CHARS = 40 MAX_TITLE_CHARS = 60 -WORK_DIR = Path("abschriften") # ein Arbeitsordner je Video: abschriften// def run_yt_dlp(arguments): @@ -114,31 +112,6 @@ def default_name(info): return "_".join(p for p in parts if p) or info.get("id") or "video" -def work_folder(name): - """Working folder of one video: abschriften//""" - return WORK_DIR / name - - -def write_source(info, path): - """Store where a transcript comes from (.quelle.json), for the - header of the finished result.""" - url = info.get("webpage_url") or ( - f"https://www.youtube.com/watch?v={info['id']}" if info.get("id") else None - ) - source = { - "titel": info.get("title"), - "kanal": info.get("channel") or info.get("uploader"), - "datum": info.get("upload_date"), - "url": url, - "id": info.get("id"), - "dauer": info.get("duration"), - "abgerufen": date.today().isoformat(), - } - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(source, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") - return path - - def available_tracks(info): """Manual tracks and the automatic ones usable for this program.""" manual = sorted((info.get("subtitles") or {}).keys()) @@ -191,8 +164,8 @@ def parse_args(): "-l", "--lang", default="de", help="Sprache der Untertitel (Standard: de)" ) parser.add_argument( - "-d", "--dir", type=Path, - help="Zielverzeichnis (Standard: Arbeitsordner abschriften//)", + "-d", "--dir", type=Path, default=Path("."), + help="Zielverzeichnis (Standard: aktuelles Verzeichnis)", ) args = parser.parse_args() @@ -216,12 +189,9 @@ def main(): kind = "automatische" if is_auto else "manuelle" print(f"Lade {kind} Untertitel „{track}“ …", flush=True) - name = args.name or default_name(info) - folder = args.dir or work_folder(name) - folder.mkdir(parents=True, exist_ok=True) - target_base = folder / name + args.dir.mkdir(parents=True, exist_ok=True) + target_base = args.dir / (args.name or default_name(info)) download(args.url, track, is_auto, target_base) - write_source(info, folder / f"{name}.quelle.json") target = Path(f"{target_base}.{track}.vtt") if not target.is_file():