Transkript-Editor — Design-Spec¶
| Status | Datum | Phase | Pfad-Vorgänger |
|---|---|---|---|
| Approved | 2026-05-27 | V1 — Segment-Text-Edits | WebGUI V1 (feat/webgui → main, shipped 2026-05-22) |
1 · Ziel¶
Whisper macht regelmäßig Fehler in Eigennamen, Fachbegriffen und Fremdwörtern. Aktuell muss der Nutzer entweder mit Tippfehlern leben oder die gesamte Pipeline neu starten. Der Transkript-Editor erlaubt es, zwischen Transcribe und Meta den Text pro Segment zu korrigieren — ohne Re-Transcribe.
Ergebnis: Bessere YouTube-Metadaten (Titel/Beschreibung/Kapitel basieren auf dem Transkript), korrekte SRT-Untertitel im Render, weniger Frust.
2 · Scope¶
In Scope (V1)¶
- Segment-Text-Edits — pro Segment den
text-String editieren. Speaker, Timecodes, Word-Level-Timings bleiben unverändert. - Opt-in Pause nach Transcribe — Start-Form-Checkbox
[ ] Pause after transcribe for editing(default off, persisted insettings.json). Pipeline läuft Transcribe → stoppt → Run-Detail zeigt Edit-CTA → User editiert oder skippt → klickt "Continue" → Meta + Render starten. - Nachträglich editieren — solange
<stem>.whisperx.jsonexistiert: Edit-Link in Run-Detail. Save invalidiert Meta + Render (Status →pending), bestehende Click-to-restart-Phase-Mechanik übernimmt den Re-Run. - Backup — bei erstem Save wird
<stem>.whisperx.original.jsoneinmalig angelegt. Folgende Saves überschreiben nur die Live-JSON. - SRT/TXT-Regeneration — Save schreibt
<stem>.whisperx.jsonneu und regeneriert<stem>.srtund<stem>.txtaus dem editierten Inhalt (gleiche Logik wietranscribe.py:transcribe()). - Stale-Flag — editierte Segmente bekommen
_edited: true(für künftige Sichtbarkeit in der UI / Phase 2). Word-Level-Timings bleiben physisch erhalten, sind aber semantisch stale, wenn der Segment-Text geändert wurde — V1 ignoriert das.
Out of Scope (Phase 2 oder später)¶
- Word-Level-Edits (einzelne Wörter mit Timecode anpassen)
- Speaker-Re-Labelling (SPEAKER_00 → "Anna")
- Segment-Merge / Segment-Split
- Word-Timing-Re-Alignment nach Text-Edit
- Diff-View vs. Original
- Undo-Stack / Edit-History
- Bulk-Replace (Find & Replace)
- Re-Diarization-Trigger
- Spell-Check / LLM-Korrektur-Vorschlag
3 · Architektur¶
3.1 Datei-Struktur¶
Additiv — keine bestehende Datei wird gelöscht oder grundlegend umgebaut. Bestehende --skip-meta --skip-render --skip-upload-Flags von pipeline.py werden für Pause-Logik wiederverwendet (keine pipeline.py-Änderung nötig).
whisper-pipeline/
├── transcript_editor.py ← NEU: load_segments, save_edits, regenerate_srt_txt, invalidate_downstream
├── webgui/
│ ├── app.py ← +2 Routes: GET /runs/{stem}/edit, POST /runs/{stem}/edit
│ ├── runner.py ← spawn_pipeline akzeptiert pause_after_transcribe Flag (→ --skip-meta --skip-render --skip-upload)
│ ├── settings.py ← +1 Field: pause_after_transcribe (bool, default false)
│ └── templates/
│ ├── index.html ← +1 Checkbox im Config-Form
│ ├── run_edit.html ← NEU: Editor-Page
│ └── _partials/
│ └── edit_cta.html ← NEU: "Edit Transcript" + "Continue"-Buttons (im run_detail eingebunden)
└── tests/
├── test_transcript_editor.py ← NEU: load, save, backup-once, regenerate, invalidate
├── test_webgui_edit_routes.py ← NEU: GET render, POST save, 404 ohne Transcribe
└── test_pause_after_transcribe.py ← NEU: Settings-Flag → Pipeline läuft nur Transcribe
3.2 Datenmodell¶
Transcribe-JSON (<stem>.whisperx.json) — Struktur (heute):
{
"segments": [
{
"start": 0.04,
"end": 0.24,
"text": " All right.",
"words": [...],
"speaker": "SPEAKER_00"
},
...
]
}
Nach V1-Edit:
{
"segments": [
{
"start": 0.04,
"end": 0.24,
"text": "All right.",
"_edited": true,
"words": [...],
"speaker": "SPEAKER_00"
},
...
]
}
_edited: truewird nur gesetzt, wenn der neue Text vom Original abweicht (verglichen mit<stem>.whisperx.original.json).words[]bleibt unverändert. Es wird nicht versucht, Word-Timings ans neue Text-Layout anzupassen.
Backup (<stem>.whisperx.original.json):
- Wird einmalig beim ersten Save erzeugt — exakte Kopie der
<stem>.whisperx.jsonvor dem ersten Edit. - Bleibt persistent. Folgende Saves überschreiben sie NICHT.
3.3 Lifecycle — opt-in Pause-Flow¶
- User wählt Audio + setzt Checkbox
[x] Pause after transcribe for editing→ POST/api/runs runner.py:spawn_pipeline()baut Command mit--skip-meta --skip-render --skip-upload- Pipeline läuft Transcribe → schreibt
<stem>.whisperx.json+ SRT/TXT → setztrun-state.jsontranscribe.status = done, andere Phasen bleibenpending/skipped - Run-Detail zeigt prominent: 📝 Edit Transcript (primary CTA) + Continue without editing (secondary)
- User klickt Edit Transcript →
/runs/{stem}/editöffnet sich - Editor zeigt Segment-Liste, User editiert (oder nicht), klickt Save & Return oder Save & Continue
- Save & Return → schreibt JSON+SRT+TXT, zurück zu Run-Detail, Edit-CTAs bleiben sichtbar
- Save & Continue (oder Continue without editing) → triggert POST
/runs/{stem}/phase/meta/start(bestehende Route) → Meta + Render laufen
3.4 Lifecycle — nachträglich-edit-Flow¶
- User öffnet Run-Detail eines beliebigen Runs mit
transcribe.status = done - Edit-Link sichtbar: Edit Transcript (klein, neben Phase-Indicator)
- Klick → Editor → User editiert + Save
- Save invalidiert:
meta.statusundrender.statuswerden aufpendinggesetzt (falls vorherdone) - Falls upload.status
done→ bleibtdone(Upload betrifft existierendes MP4, nicht Transkript) - Falls upload.status
skipped→ bleibtskipped - Phase-Indicator zeigt Meta/Render als pending → User klickt Phase-Icon → re-startet via bestehende Click-to-restart-Mechanik
3.5 Modulgrenzen¶
transcript_editor.py — pure-Python, kein FastAPI-Import. Testbar standalone.
def load_segments(json_path: str) -> list[dict]:
"""Returns segments with start/end/text/speaker/words/_edited for UI rendering."""
def save_edits(json_path: str, new_texts: list[str]) -> dict:
"""
Updates segment texts, sets _edited flag where changed.
Creates <stem>.whisperx.original.json on first call.
Regenerates SRT + TXT siblings.
Returns dict: {edited_count, total_segments, backup_created: bool}
"""
def regenerate_srt_txt(json_path: str) -> tuple[str, str]:
"""(Re-)writes SRT + TXT from JSON. Returns (srt_path, txt_path).
Same logic as transcribe.py — extracted helper."""
def invalidate_downstream(run_state_path: str) -> list[str]:
"""Sets meta/render phases to pending (if previously done).
Returns list of invalidated phase names."""
def has_been_edited(json_path: str) -> bool:
"""Returns True if any segment has _edited: true."""
webgui/app.py — 2 neue Routes:
@app.get("/runs/{stem}/edit", response_class=HTMLResponse)
async def run_edit(request, stem):
# Lädt Segments via transcript_editor.load_segments
# 404 wenn <stem>.whisperx.json fehlt
# Rendert run_edit.html
@app.post("/runs/{stem}/edit")
async def run_edit_save(request, stem, ...):
# Parsed Form-Data (segment_text_0, segment_text_1, ...)
# transcript_editor.save_edits + invalidate_downstream
# Query-Param ?continue=1 → Redirect zu POST /runs/{stem}/phase/meta/start
# Sonst → Redirect zu /runs/{stem}
webgui/runner.py:spawn_pipeline() — bestehende Signatur bekommt pause_after_transcribe: bool = False-Parameter. Wenn True: append --skip-meta --skip-render --skip-upload an Command.
webgui/settings.py — bekommt Field pause_after_transcribe: bool = False. Persisted in ~/.whisper-pipeline-ui.json.
3.6 UI — Editor-Page (run_edit.html)¶
┌─ Edit Transcript: my-episode ──────────────────────┐
│ ← Back to Run Original backup: ✓ │
│ │
│ Tip: Edit text only. Speaker labels and timecodes │
│ are preserved. Saving invalidates Meta+Render│
│ and they will need to re-run. │
│ │
│ ┌─ [00:03 SPEAKER_00] ──────────────────────────┐ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Hallo und willkommen zu Signal... │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ ┌─ [00:12 SPEAKER_01] ★ edited ─────────────────┐ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Heute reden wir über Autopoiesis. │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────┘ │
│ ... │
│ │
│ [Save & Return] [Save & Continue] [Cancel] │
└─────────────────────────────────────────────────────┘
- Read-only Segment-Header:
[mm:ss SPEAKER_id](+ optional★ editedBadge wenn_edited: true) - Editierbares
<textarea>pro Segment-Text, auto-grow via CSS (field-sizing: content/ JS-Fallback) - Pro Segment ein
<input type="hidden" name="original_text_N">für Server-side Vergleich (Setzt_editednur bei Diff) - Footer-Buttons:
- Save & Return → POST → Redirect zu
/runs/{stem} - Save & Continue → POST mit
?continue=1→ Save + Redirect zu/runs/{stem}/phase/meta/start - Cancel → Browser-Navigate-Back, kein Save
- Reuse: bestehende
base.html,style.css, Kuro Signal Protocol Tokens
3.7 UI — Edit-CTA in run_detail.html¶
Drei sichtbare Zustände, abhängig von run-state.json + Pause-Setting:
A) Run pausiert (Transcribe done, Meta pending, Pause-Flag gesetzt):
┌─ ⏸ Pipeline paused after Transcribe ─┐
│ [📝 Edit Transcript] │
│ [▶ Continue without editing] │
└────────────────────────────────────────┘
B) Run nachträglich editierbar (Transcribe done, kein Pause-State):
C) Run editiert (mind. ein Segment hat _edited: true):
3.8 Settings & Start-Form¶
index.html — neue Checkbox im Advanced/Config-Block:
<label class="checkbox">
<input type="checkbox" name="pause_after_transcribe" {{ "checked" if settings.pause_after_transcribe }}>
Pause after transcribe for editing
</label>
Bei POST /api/runs: Form-Value wird an runner.spawn_pipeline(..., pause_after_transcribe=...) durchgereicht UND in settings.json persistiert.
4 · Testing¶
TDD-Approach¶
Jeder Task wird zuerst als roter Test geschrieben, dann minimal implementiert, dann grün, dann committed.
Test-Module¶
tests/test_transcript_editor.py (~10 Tests):
- test_load_segments_returns_text_speaker_times
- test_save_edits_updates_text
- test_save_edits_sets_edited_flag_when_text_differs
- test_save_edits_does_not_set_edited_flag_when_text_same
- test_save_edits_creates_backup_on_first_call
- test_save_edits_does_not_overwrite_backup_on_second_call
- test_save_edits_regenerates_srt
- test_save_edits_regenerates_txt
- test_invalidate_downstream_resets_meta_render_to_pending
- test_invalidate_downstream_leaves_upload_alone
tests/test_webgui_edit_routes.py (~6 Tests):
- test_get_edit_renders_segments
- test_get_edit_404_when_transcribe_missing
- test_post_edit_save_updates_json
- test_post_edit_save_redirects_to_run_detail
- test_post_edit_save_with_continue_param_triggers_meta_start
- test_post_edit_save_invalidates_downstream_in_runstate
tests/test_pause_after_transcribe.py (~3 Tests):
- test_spawn_pipeline_with_pause_appends_skip_flags
- test_settings_persists_pause_flag
- test_index_form_pause_checkbox_reflects_settings
Fixtures:
- Erweitere tests/fixtures/run-states/: 1 neue Fixture paused-after-transcribe.json (Transcribe done, andere pending)
- Erweitere tests/fixtures/: 1 neues sample-transcript.whisperx.json mit 3 Segments
Regressions-Check¶
Bestehende 62 Tests dürfen nicht brechen.
5 · Konventionen¶
- TemplateResponse: Starlette 1.0+ Signature (
request, name, context) wie im Rest des Projekts. - HTMX: Save-Buttons sind reguläre Form-Submits (nicht hx-post) — Editor ist eine eigene Page mit voller Page-Reload-Semantik. Vereinfacht Form-Encoding bei 100+ Segments.
- Encoding: UTF-8 überall. JSON wird mit
ensure_ascii=Falsegeschrieben. - Branches: Feature-Branch
feat/transcript-editor. Conventional commits. - Pfade:
pipeline_core.resolve_audio_pathPattern — kein hardcoded absolute Path.
6 · Risiken & Mitigationen¶
| Risiko | Mitigation |
|---|---|
| Sehr lange Transkripte (1000+ Segments) brechen Form-POST | V1: kein expliziter Cap; falls Problem → Phase 2 mit lazy-load / pagination |
| User editiert Transkript während Meta läuft | Editor-POST prüft Slot — wenn meta.status == running → 409 mit klarer Message |
| Backup-File wird versehentlich gelöscht | Doku in Editor-Page: "Original backup: ✓ saved"-Banner. Phase 2: Diff-View. |
| Word-Timings sind nach Text-Edit stale → SRT/TXT okay, aber Remotion-Word-Highlight fehl-aligned | V1: Doku-Hint im Editor ("Word-level highlighting in video may be off after edits — fix in Phase 2"). |
| User vergisst Re-Run nach nachträglichem Edit | Phase-Indicator zeigt visuell pending → existing UX kümmert sich darum. |
7 · Phase-2-Roadmap (separate Spec)¶
Nicht in V1, hier als Pointer:
- Word-Level-Edits — einzelne Wörter editieren + Re-Align Word-Timings via WhisperX-Align
- Speaker-Re-Labelling — pro Segment Speaker ändern, optional bulk-rename (
SPEAKER_00→Anna) - Merge/Split-Segments — zwei Segments zusammenführen oder eines in zwei teilen (mit Time-Re-Calc)
- Diff-View — Side-by-side Original vs. Current
- Undo-Stack — Edit-History pro Run, Revert per Segment oder global
Phase 2 baut auf V1-Datenmodell auf (additive Schema-Extensions: words-Re-Align, _speaker_edited-Flag, _split_from/_merged_with-Refs).
8 · Definition of Done — V1¶
- [ ] Alle ~19 neuen Tests grün
- [ ] Alle 62 bestehenden Tests bleiben grün
- [ ] Manueller End-to-End Test: Pipeline mit Pause-Flag → Editor → Save & Continue → Meta + Render laufen mit editiertem Transkript
- [ ] Manueller End-to-End Test: Nachträglicher Edit eines abgeschlossenen Runs → Invalidate Meta + Render → Re-Run via Click-to-restart
- [ ] Backup-File wird angelegt und nicht überschrieben
- [ ]
AGENTS.mdaktualisiert (Editor unter "Wo finde ich was", Phase-2-Liste angepasst) - [ ] Feature-Branch
feat/transcript-editor→ main gemerged (Squash oder regulär, je nach Commit-Anzahl)