feat: von Hand nachbearbeitete Ausgabe wird nicht mehr überschrieben
abschrift_korrigieren.py merkt sich die Prüfsumme der geschriebenen Ausgabe in <Ausgabe>.parts/ausgabe.sha256. Weicht die vorhandene Datei davon und vom neuen Ergebnis ab, bleibt sie erhalten; das Ergebnis kommt nach <Name>.neu.korrigiert.txt (mit eigenem Bericht) und es gibt eine Warnung. --ueberschreiben ersetzt die Datei trotzdem. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ed721b1773
commit
37e4a2bca0
4 changed files with 142 additions and 5 deletions
|
|
@ -524,9 +524,22 @@ neu, was sich geändert hat:
|
||||||
- Die Absatzgliederung wird für geänderte Textstellen ebenfalls neu bestimmt; das
|
- Die Absatzgliederung wird für geänderte Textstellen ebenfalls neu bestimmt; das
|
||||||
geht schnell.
|
geht schnell.
|
||||||
|
|
||||||
Von Hand nachbearbeitete `*.korrigiert.txt` werden bei einem erneuten Lauf
|
**Handarbeit bleibt erhalten.** Wurde `rede.korrigiert.txt` seit dem letzten Lauf
|
||||||
**überschrieben**. Handarbeit deshalb erst ganz am Ende machen oder die Datei vorher
|
von Hand geändert, überschreibt ein erneuter Lauf sie nicht. Das neue Ergebnis kommt
|
||||||
unter anderem Namen sichern.
|
dann nach `rede.neu.korrigiert.txt` (Bericht: `rede.neu.aenderungen.html`), mit
|
||||||
|
einer Warnung:
|
||||||
|
|
||||||
|
```text
|
||||||
|
WARNUNG: rede.korrigiert.txt wurde seit dem letzten Lauf geändert (von Hand?) und bleibt erhalten. Neues Ergebnis: rede.neu.korrigiert.txt (mit --ueberschreiben wird die Datei ersetzt)
|
||||||
|
```
|
||||||
|
|
||||||
|
Die eigenen Änderungen lassen sich dann mit einem Vergleichsprogramm übertragen,
|
||||||
|
z. B. `meld rede.korrigiert.txt rede.neu.korrigiert.txt`. Soll die Handarbeit
|
||||||
|
verworfen werden: `--ueberschreiben` angeben.
|
||||||
|
|
||||||
|
Das Programm erkennt Handarbeit an einer Prüfsumme in `rede.korrigiert.parts/`.
|
||||||
|
Fehlt sie (erster Lauf mit dieser Version, Ordner gelöscht), gilt jede abweichende
|
||||||
|
vorhandene Datei als von Hand geändert.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -618,7 +631,7 @@ Beispiel für `-i rede.txt` bzw. `-o rede.korrigiert.txt`:
|
||||||
| Datei | Inhalt | Wird überschrieben? |
|
| Datei | Inhalt | Wird überschrieben? |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `rede.txt` | Rohtext (bei `-u` automatisch gespeichert) | nie, wenn sie schon mit anderem Inhalt existiert (dann Warnung) |
|
| `rede.txt` | Rohtext (bei `-u` automatisch gespeichert) | nie, wenn sie schon mit anderem Inhalt existiert (dann Warnung) |
|
||||||
| `rede.korrigiert.txt` | korrigierte Abschrift | ja, bei jedem Lauf |
|
| `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.aenderungen.html` | Änderungsbericht | ja, bei jedem Lauf |
|
||||||
| `rede.korrigiert.parts/` | Zwischenstände | wird ergänzt; darf jederzeit gelöscht werden |
|
| `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.txt` | Glossar-Entwurf von `glossar_vorschlag.py` | ja |
|
||||||
|
|
@ -693,6 +706,7 @@ Weitere Einstellungen (Abschnittsgröße, Absatzlängen, Schwellen) sind in der
|
||||||
| `Die Eingabedatei ist leer.` | Datei prüfen. |
|
| `Die Eingabedatei ist leer.` | Datei prüfen. |
|
||||||
| `Ausgabedatei darf nicht die Eingabedatei sein.` | Mit `-o` einen anderen Namen wählen. |
|
| `Ausgabedatei darf nicht die Eingabedatei sein.` | Mit `-o` einen anderen Namen wählen. |
|
||||||
| `Glossar nicht gefunden: …` | Pfad bei `-g` prüfen. |
|
| `Glossar nicht gefunden: …` | Pfad bei `-g` prüfen. |
|
||||||
|
| `WARNUNG: rede.korrigiert.txt wurde seit dem letzten Lauf geändert (von Hand?) …` | Die Datei wurde nachbearbeitet und bleibt erhalten; das neue Ergebnis steht in `rede.neu.korrigiert.txt`. Änderungen übertragen oder mit `--ueberschreiben` ersetzen, siehe [Abschnitt 9](#9-schritt-5-nachbessern-und-erneut-laufen-lassen). |
|
||||||
| `WARNUNG: rede.txt existiert mit anderem Inhalt und bleibt unverändert.` | Bei `-u` wird ein vorhandener, z. B. von Hand bearbeiteter Rohtext nie überschrieben. Korrigiert wird trotzdem der frisch geladene Text. Wer den neuen Rohtext braucht, die alte Datei umbenennen. |
|
| `WARNUNG: rede.txt existiert mit anderem Inhalt und bleibt unverändert.` | Bei `-u` wird ein vorhandener, z. B. von Hand bearbeiteter Rohtext nie überschrieben. Korrigiert wird trotzdem der frisch geladene Text. Wer den neuen Rohtext braucht, die alte Datei umbenennen. |
|
||||||
| `WARNUNG: Abschnitt 3: 1180 → 1090 Wörter (8% Abweichung) …` | Das Modell hat möglicherweise etwas ausgelassen oder ergänzt. Abschnitt im Bericht prüfen. Bei Bedarf die genannte Datei in `.parts/` löschen und erneut laufen lassen. |
|
| `WARNUNG: Abschnitt 3: 1180 → 1090 Wörter (8% Abweichung) …` | Das Modell hat möglicherweise etwas ausgelassen oder ergänzt. Abschnitt im Bericht prüfen. Bei Bedarf die genannte Datei in `.parts/` löschen und erneut laufen lassen. |
|
||||||
| `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). |
|
||||||
|
|
@ -763,6 +777,7 @@ einfügen.
|
||||||
| `-z BEREICH …` | nur diese Zeitbereiche (mit `.vtt` oder `-u`) |
|
| `-z BEREICH …` | nur diese Zeitbereiche (mit `.vtt` oder `-u`) |
|
||||||
| `--exakt` | genau an den Zeiten schneiden |
|
| `--exakt` | genau an den Zeiten schneiden |
|
||||||
| `--kein-bericht` | keinen Änderungsbericht schreiben |
|
| `--kein-bericht` | keinen Änderungsbericht schreiben |
|
||||||
|
| `--ueberschreiben` | von Hand geänderte Ausgabedatei trotzdem ersetzen |
|
||||||
|
|
||||||
### `extract_text.py`
|
### `extract_text.py`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -247,6 +247,12 @@ werden trotzdem umgewandelt; der Exit-Code ist dann 1.
|
||||||
| `-g`, `--glossar` | Glossar mit festen Ersetzungen und Schreibweisen; mehrfach angeben zum Kombinieren (Ersetzungen in Reihenfolge der Angabe) |
|
| `-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`) |
|
| `-m`, `--model` | Ollama-Modell; Standard: `qwen3:32b` (Konstante `MODEL`) |
|
||||||
| `--kein-bericht` | keinen Änderungsbericht schreiben |
|
| `--kein-bericht` | keinen Änderungsbericht schreiben |
|
||||||
|
| `--ueberschreiben` | Ausgabedatei auch ersetzen, wenn sie seit dem letzten Lauf von Hand geändert wurde |
|
||||||
|
|
||||||
|
Eine von Hand nachbearbeitete Ausgabedatei überschreibt ein erneuter Lauf nicht: Das
|
||||||
|
Ergebnis kommt dann nach `<Name>.neu.korrigiert.txt`, mit Warnung. Erkannt wird das
|
||||||
|
an der Prüfsumme der zuletzt geschriebenen Ausgabe in `<Ausgabe>.parts/ausgabe.sha256`;
|
||||||
|
ohne Prüfsumme gilt jede abweichende vorhandene Datei als von Hand geändert.
|
||||||
|
|
||||||
Mit Zeitauswahl wird jeder Bereich als eigener Block korrigiert und gegliedert;
|
Mit Zeitauswahl wird jeder Bereich als eigener Block korrigiert und gegliedert;
|
||||||
die Überschriften `[00:04:57 - 00:06:04]` bekommt das Modell nicht zu sehen, sie
|
die Überschriften `[00:04:57 - 00:06:04]` bekommt das Modell nicht zu sehen, sie
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ MAX_WORD_DEVIATION = 0.05 # ab dieser Abweichung der Wortzahl wird gewarnt
|
||||||
PARAGRAPH_WINDOW = 150 # Sätze pro Anfrage bei der Absatzgliederung
|
PARAGRAPH_WINDOW = 150 # Sätze pro Anfrage bei der Absatzgliederung
|
||||||
MAX_PARAGRAPH_SENTENCES = 10 # längere Absätze werden nachträglich geteilt
|
MAX_PARAGRAPH_SENTENCES = 10 # längere Absätze werden nachträglich geteilt
|
||||||
MIN_PARAGRAPH_SENTENCES = 3 # kürzere Absätze werden mit dem Nachbarn verbunden
|
MIN_PARAGRAPH_SENTENCES = 3 # kürzere Absätze werden mit dem Nachbarn verbunden
|
||||||
|
OUTPUT_RECORD = "ausgabe.sha256" # in <Ausgabe>.parts/: Prüfsumme der zuletzt geschriebenen Ausgabe
|
||||||
|
|
||||||
SENTENCE_END = re.compile(r"[.!?…][\"'»«“”)\]]*$")
|
SENTENCE_END = re.compile(r"[.!?…][\"'»«“”)\]]*$")
|
||||||
SENTENCE_SPLIT = re.compile(r"(?:(?<=[.!?…])|(?<=[.!?…][\"'»«“”)\]]))\s+")
|
SENTENCE_SPLIT = re.compile(r"(?:(?<=[.!?…])|(?<=[.!?…][\"'»«“”)\]]))\s+")
|
||||||
|
|
@ -641,6 +642,11 @@ def parse_args():
|
||||||
"--kein-bericht", action="store_true",
|
"--kein-bericht", action="store_true",
|
||||||
help="keinen Änderungsbericht <Ausgabe>.aenderungen.html schreiben",
|
help="keinen Änderungsbericht <Ausgabe>.aenderungen.html schreiben",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ueberschreiben", action="store_true",
|
||||||
|
help="die Ausgabedatei auch dann überschreiben, wenn sie seit dem letzten Lauf "
|
||||||
|
"von Hand geändert wurde (sonst entsteht <Name>.neu.korrigiert.txt)",
|
||||||
|
)
|
||||||
add_time_options(parser)
|
add_time_options(parser)
|
||||||
args = parser.parse_args(range_argv(sys.argv[1:]))
|
args = parser.parse_args(range_argv(sys.argv[1:]))
|
||||||
resolve_time_ranges(parser, args)
|
resolve_time_ranges(parser, args)
|
||||||
|
|
@ -708,6 +714,35 @@ def read_source(args):
|
||||||
return raw, args.output, base_language(args.lang or DEFAULT_LANGUAGE)
|
return raw, args.output, base_language(args.lang or DEFAULT_LANGUAGE)
|
||||||
|
|
||||||
|
|
||||||
|
def text_hash(text):
|
||||||
|
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def new_output_path(target):
|
||||||
|
"""rede.korrigiert.txt -> rede.neu.korrigiert.txt, out.txt -> out.neu.txt"""
|
||||||
|
if target.name.endswith(".korrigiert.txt"):
|
||||||
|
stem = target.name.removesuffix(".korrigiert.txt")
|
||||||
|
return target.with_name(f"{stem}.neu.korrigiert.txt")
|
||||||
|
return target.with_name(f"{target.stem}.neu{target.suffix}")
|
||||||
|
|
||||||
|
|
||||||
|
def choose_output(target, content, parts_dir, overwrite=False):
|
||||||
|
"""Where to write `content`: the target, unless it was changed by hand.
|
||||||
|
|
||||||
|
A target counts as changed by hand if it differs both from what the last
|
||||||
|
run wrote there (checksum in parts_dir) and from the new content. Without
|
||||||
|
a checksum (first run, .parts deleted) any differing target counts.
|
||||||
|
"""
|
||||||
|
if overwrite or not target.exists():
|
||||||
|
return target
|
||||||
|
current = target.read_text(encoding="utf-8")
|
||||||
|
record = parts_dir / OUTPUT_RECORD
|
||||||
|
last = record.read_text(encoding="utf-8").strip() if record.exists() else None
|
||||||
|
if current == content or text_hash(current) == last:
|
||||||
|
return target
|
||||||
|
return new_output_path(target)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
global MODEL
|
global MODEL
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
|
|
@ -800,7 +835,18 @@ def main():
|
||||||
corrected_blocks.append((header, paragraphs))
|
corrected_blocks.append((header, paragraphs))
|
||||||
|
|
||||||
text = join_segments(corrected_blocks, header_gap="\n\n")
|
text = join_segments(corrected_blocks, header_gap="\n\n")
|
||||||
target.write_text(text + "\n", encoding="utf-8")
|
content = text + "\n"
|
||||||
|
output = choose_output(target, content, parts_dir, args.ueberschreiben)
|
||||||
|
output.write_text(content, encoding="utf-8")
|
||||||
|
if output == target:
|
||||||
|
(parts_dir / OUTPUT_RECORD).write_text(text_hash(content) + "\n", encoding="utf-8")
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"WARNUNG: {target} wurde seit dem letzten Lauf geändert (von Hand?) und "
|
||||||
|
f"bleibt erhalten. Neues Ergebnis: {output} "
|
||||||
|
"(mit --ueberschreiben wird die Datei ersetzt)"
|
||||||
|
)
|
||||||
|
target = output
|
||||||
print(f"Fertig: {target}")
|
print(f"Fertig: {target}")
|
||||||
|
|
||||||
if not args.kein_bericht:
|
if not args.kein_bericht:
|
||||||
|
|
|
||||||
|
|
@ -414,6 +414,76 @@ def test_main_default_output_resume_and_glossary(sample_copy, monkeypatch, capsy
|
||||||
assert target.read_text(encoding="utf-8") == result
|
assert target.read_text(encoding="utf-8") == result
|
||||||
|
|
||||||
|
|
||||||
|
def shouting(chunk, index, system):
|
||||||
|
"""A different model result than identity_correct."""
|
||||||
|
return chunk.strip().replace("Vielen Dank", "VIELEN DANK")
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_overwrites_own_output(sample_copy, monkeypatch, capsys):
|
||||||
|
monkeypatch.setattr(ak, "correct_chunk", identity_correct)
|
||||||
|
monkeypatch.setattr(ak, "find_paragraph_starts", every_nth_start(5))
|
||||||
|
run_main(monkeypatch, "-i", sample_copy)
|
||||||
|
target = sample_copy.with_name("ramstein.de.korrigiert.txt")
|
||||||
|
|
||||||
|
# Unchanged output of the last run is replaced by a different result
|
||||||
|
monkeypatch.setattr(ak, "correct_chunk", shouting)
|
||||||
|
run_main(monkeypatch, "-i", sample_copy, "-m", "anderes:modell")
|
||||||
|
assert "VIELEN DANK" in target.read_text(encoding="utf-8")
|
||||||
|
assert not ak.new_output_path(target).exists()
|
||||||
|
assert "WARNUNG" not in capsys.readouterr().out
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_keeps_hand_edited_output(sample_copy, monkeypatch, capsys):
|
||||||
|
monkeypatch.setattr(ak, "correct_chunk", identity_correct)
|
||||||
|
monkeypatch.setattr(ak, "find_paragraph_starts", every_nth_start(5))
|
||||||
|
run_main(monkeypatch, "-i", sample_copy)
|
||||||
|
target = sample_copy.with_name("ramstein.de.korrigiert.txt")
|
||||||
|
edited = target.read_text(encoding="utf-8") + "Von Hand ergänzt.\n"
|
||||||
|
target.write_text(edited, encoding="utf-8")
|
||||||
|
capsys.readouterr()
|
||||||
|
|
||||||
|
monkeypatch.setattr(ak, "correct_chunk", shouting)
|
||||||
|
run_main(monkeypatch, "-i", sample_copy, "-m", "anderes:modell")
|
||||||
|
|
||||||
|
new = sample_copy.with_name("ramstein.de.neu.korrigiert.txt")
|
||||||
|
assert target.read_text(encoding="utf-8") == edited
|
||||||
|
assert "VIELEN DANK" in new.read_text(encoding="utf-8")
|
||||||
|
assert sample_copy.with_name("ramstein.de.neu.aenderungen.html").is_file()
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert f"WARNUNG: {target} wurde seit dem letzten Lauf geändert" in out
|
||||||
|
assert f"Fertig: {new}" in out
|
||||||
|
|
||||||
|
# --ueberschreiben replaces the edited file
|
||||||
|
run_main(monkeypatch, "-i", sample_copy, "-m", "anderes:modell", "--ueberschreiben")
|
||||||
|
assert "VIELEN DANK" in target.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_keeps_differing_output_without_checksum(sample_copy, monkeypatch, capsys):
|
||||||
|
target = sample_copy.with_name("ramstein.de.korrigiert.txt")
|
||||||
|
target.write_text("Älteres Ergebnis, Herkunft unbekannt.\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", sample_copy)
|
||||||
|
|
||||||
|
assert target.read_text(encoding="utf-8") == "Älteres Ergebnis, Herkunft unbekannt.\n"
|
||||||
|
assert sample_copy.with_name("ramstein.de.neu.korrigiert.txt").is_file()
|
||||||
|
assert "WARNUNG" in capsys.readouterr().out
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name, expected", [
|
||||||
|
("rede.korrigiert.txt", "rede.neu.korrigiert.txt"),
|
||||||
|
("out.txt", "out.neu.txt"),
|
||||||
|
])
|
||||||
|
def test_new_output_path(name, expected):
|
||||||
|
assert ak.new_output_path(Path("a") / name) == Path("a") / expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_choose_output_same_content_is_no_conflict(tmp_path):
|
||||||
|
target = tmp_path / "rede.korrigiert.txt"
|
||||||
|
target.write_text("gleich\n", encoding="utf-8")
|
||||||
|
assert ak.choose_output(target, "gleich\n", tmp_path / "fehlt.parts") == target
|
||||||
|
|
||||||
|
|
||||||
def test_main_output_option_creates_directories(sample_copy, monkeypatch):
|
def test_main_output_option_creates_directories(sample_copy, monkeypatch):
|
||||||
monkeypatch.setattr(ak, "correct_chunk", identity_correct)
|
monkeypatch.setattr(ak, "correct_chunk", identity_correct)
|
||||||
monkeypatch.setattr(ak, "find_paragraph_starts", every_nth_start(5))
|
monkeypatch.setattr(ak, "find_paragraph_starts", every_nth_start(5))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue