Files
abdulive/docs/07-mvp-plan.md
T
ZeasyAlexandClaude Opus 5 974dfd075b S4: Untertitel aus der Mikrofonspur, plus vorgegebenes OBS-Layout
whisper.cpp laeuft ueber die Tonspur und liefert Tokens; daraus werden
Woerter mit Zeitstempeln (Wortstuecke wie "Sav"+"ina" wieder zusammengesetzt)
und daraus kurze Zeilen im ASS-Format mit wortweiser Hervorhebung. SRT und
Transkript fallen nebenbei ab.

An einem echten deutschen Clip geprueft. Drei Fehler kamen erst dabei heraus:
Zeilen mit einem einzigen Wort bei zoegerndem Sprechen, ein Nachlauf, der in
die naechste Zeile ragte (ASS stapelt gleichzeitige Zeilen uebereinander),
und ein Uebertragsfehler im Zeitformat bei 59,999 s.

Layout wird jetzt VORGEGEBEN statt nachgemessen: ein Vergleich zweier
Aufnahmen zeigte, dass die Facecam von Stream zu Stream anders liegt. Das
Profil "default" beschreibt die Vorgabe (Facecam 480x270 oben links, Chat ab
x=1405, Gameplay-Band 890 breit mittig auf x=960), "bestand-2026-09" haelt
den alten Zustand fuer altes Material fest. Dazu eine Ausrichtungsvorlage als
Bild, die in OBS ueber die Szene gelegt wird, und docs/obs-layout.md.

Befund am echten Clip: im Streambild sind bereits zwei Textebenen eingebrannt
(Spieluntertitel und ein Live-Caption-Overlay). Beides ist im Layout-Dokument
behandelt.

53 Tests gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013VTGjd92LsDDWF3frd9FTB
2026-09-09 10:55:54 +02:00

16 KiB
Raw Blame History

MVP-Paket: „Clip-Knopf"

Ziel in einem Satz: Nach jedem Stream liegen fertige 9:16-Videos mit eingebrannten Untertiteln zur Auswahl bereit — Abdu klickt zwei davon an und lädt sie hoch.

Begründung der Auswahl: 06-hebel-analyse.md. Detailspezifikation: projects/clip-shorts.md.

Warum genau dieser Zuschnitt

Der spürbare Effekt entsteht nicht dadurch, dass ein Clip entsteht — das kann Twitch selbst. Er entsteht dadurch, dass die Strecke von „das war lustig" bis „fertiges Short liegt bereit" von 30 Minuten Handarbeit auf 0 Minuten fällt. Genau diese Strecke ist der MVP. Alles, was nicht auf dieser Strecke liegt, fliegt raus.

Das MVP ist bewusst ein einziger Prozess in einem einzigen Container. Die in 01-architecture.md beschriebene Aufteilung in Ingest, Overlay-Server und Konsumenten kommt erst, wenn es einen zweiten Konsumenten gibt. Vorher ist sie nur Ballast.

Rahmenbedingungen (geklärt)

Punkt Stand
Zielmaschine Abdus Stream-PC, dort wo OBS läuft — alles lokal
OBS-Layout Facecam bündig oben links, 440×330 — eingemessen, siehe unten
GPU keine NVIDIA-Karte → CPU-Encoding mit libx264
Untertitel whisper.cpp (Binary, kein Python) statt faster-whisper
Betrieb Docker Compose, siehe ADR-0004

Fortschritt

Scheibe Stand
S0 Container + Toolchain fertig — Image 258 MB, check-env ohne harte Fehler
Layout eingemessen fertig — config/layout.yaml, vorgezogen aus S3
S1 Einrichtung + Auth + DB + IRC-Ingest fertig — 20 Tests grün, Daemon läuft
S2 OBS-Anbindung + Trigger Code fertig, Abnahme braucht laufendes OBS
S3 9:16-Render fertig — Durchlauf verifiziert
S4 Untertitel fertig — an echtem Clip verifiziert, 53 Tests grün
S5 Onboarding + Sichtung als Nächstes
S6 Erstchatter-Begrüßung offen
OBS-Layout vorgegeben fertig — obs-layout.md

S1 abgenommen am 2026-09-09. Der Daemon läuft, hängt am Chat, die Einrichtung ist unter http://localhost:4792/setup erreichbar.

Beim Bauen hat sich die Technikbasis vereinfacht: Node 24 bringt SQLite, WebSocket, fetch und die Ausführung von TypeScript mit, deshalb hat das Projekt null Laufzeitabhängigkeiten und keinen Build-Schritt. pnpm-Workspaces und die Paketmanifeste sind entfallen. Siehe Nachtrag 2 in ADR-0001.

Nachgewiesen:

  • 20 Tests grün (IRC-Parser, Spike-Detektor, Datenbankschicht)
  • Chat verbindet sich anonym, solange keine Zugangsdaten hinterlegt sind — damit lässt sich die Datenaufnahme prüfen, bevor die Twitch-Apps existieren
  • Zugangsdaten überstehen die Runde durch die Einrichtungsseite, liegen verschlüsselt mit Rechten 0600 im Volume, tauchen weder im HTML noch in der Datenbank noch in einer Volltextsuche im Klartext auf
  • Was noch aussteht: die Strecke IRC → Datenbank im Livebetrieb. Die Teile sind einzeln getestet, die Verdrahtung wird beim ersten echten Stream sichtbar.

S0 abgenommen am 2026-09-09. Im Image: Node 22.23, ffmpeg 5.1.9 mit libx264 und libass, whisper.cpp v1.7.4, Sprachmodell ggml-small geladen. check-env prüft Werkzeuge, Volumes, Layout-Geometrie, OBS-Erreichbarkeit und Zugangsdaten; die verbleibenden Warnungen (OBS nicht erreichbar, noch keine Zugangsdaten hinterlegt) sind auf dieser Entwicklungsmaschine erwartbar.

calibrate wurde mit einem echten 1920×1080-Bild durchgespielt: 1080×1920 heraus, Ausschnitte an der richtigen Stelle. Damit ist der Filtergraph für S3 bereits verifiziert, bevor S3 anfängt.

Das Einmessen der Facecam war eigentlich Teil von S3, ließ sich aber aus dem gelieferten Screenshot sofort erledigen. Dabei kamen zwei Dinge heraus, die das geplante Layout verändert haben — der Chat ist ins Streambild eingebrannt und die Facecam ist 4:3, nicht 16:9. Beides ist in projects/clip-shorts.md dokumentiert und in config/layout.yaml aufgelöst.

Umfang

Drin

  • Trigger: !clip im Chat und OBS-Hotkey (über ReplayBufferSaved)
  • Quelle: OBS-Replay-Buffer, parallel dazu ein Twitch-Clip für die teilbare URL
  • Render: 1080×1920, Facecam oben, Gameplay unten, fester Crop aus Config
  • Untertitel: whisper.cpp deutsch → SRT → ASS mit wortweisem Highlight → eingebrannt
  • Ablage: data/clips/<datum>/<clip-id>/ mit final.mp4, Transkript, Metadaten
  • Sichtung: eine statische HTML-Seite mit Vorschau, Score und zwei Knöpfen
  • Nebenbei: Chat, Zuschauerzahlen und Nutzer werden ab Tag 1 in die DB geschrieben, auch wenn im MVP noch nichts damit passiert

Bewusst draußen

Nicht im MVP Warum
EventSub Für den Clip-Weg reicht IRC plus Helix-Polling. Spart einen ganzen Verbindungstyp mit Reconnect-Logik.
Event-Bus über WebSocket Es gibt nur einen Prozess. Funktionsaufrufe genügen.
Dashboard mit Framework Eine statische HTML-Seite reicht für zehn Videos.
Spike-Trigger scharf Läuft mit und protokolliert, löst aber nichts aus — Schwelle wird erst an echten Daten kalibriert.
Automatischer Upload Die Auswahl ist ohnehin manuell. Kein Gewinn, viel API-Aufwand.
Overlay, Punkte, Recap, Raid Eigene Meilensteine.

Die sieben Scheiben

Jede Scheibe ist einzeln lauffähig und einzeln überprüfbar. Am Ende jeder Scheibe gibt es etwas, das man anschauen kann.

S0 — Container und Toolchain · ~1 Tag

docker/Dockerfile (Node 22, ffmpeg mit libx264 + libass, whisper.cpp), docker-compose.yml mit Volumes und Ports, Modell-Download beim ersten Start in ein eigenes Volume, scripts/check-env als Selbsttest im Container.

Fertig, wenn: docker compose run --rm toolkit check-env meldet ffmpeg, whisper.cpp, Modell und die Schreibrechte auf allen Volumes als OK.

S1 — Einrichtung, Auth und Datenaufnahme · ~3 Tage

Einrichtungs-Assistent auf http://localhost:4792/setup: Client-ID und -Secret werden dort eingegeben, nicht in eine Datei. Verschlüsselter Secret-Speicher im Volume, OAuth-Flow mit Rücksprung auf denselben Port, Token-Refresh. Siehe ADR-0005.

Dazu Helix-Client mit Rate-Limit-Bucket, DB-Migrationen für streams, users, user_streams, chat_messages, viewer_samples, clips, ein IRC-Client, der jede Nachricht schreibt, und Stream-Sessions über Helix-Polling im Minutentakt.

Der Assistent war ursprünglich nicht eingeplant und kostet etwa einen Tag mehr. Dafür entfällt der .env-Schritt aus der Inbetriebnahme komplett.

Fertig, wenn: Nach einem echten Stream stehen alle Nachrichten in chat_messages und eine korrekte Zeile in streams.

Diese Scheibe ist der Teil ohne sichtbaren Effekt — und trotzdem der, den man nicht verschieben darf. Was hier nicht mitgeschrieben wird, ist für immer weg.

S2 — OBS-Anbindung und Trigger · ~1 Tag

obs-websocket v5 über host.docker.internal:4455, Reconnect-Schleife. SaveReplayBuffer auslösen, auf ReplayBufferSaved warten, Dateipfad vom Windows-Pfad auf den Container-Mount abbilden. Trigger: !clip im Chat (Cooldown 60 s global, 300 s pro Nutzer, nur Follower) und der OBS-Hotkey. Parallel POST /helix/clips, Ergebnis-ID in clips nachtragen. Bestätigung im Chat, genau eine pro Auslösung.

Fertig, wenn: !clip im laufenden Stream erzeugt binnen 5 s eine raw.mp4 im Ablageordner und eine Chat-Rückmeldung.

Stand 2026-09-09: Code steht und ist getestet, soweit es ohne OBS geht — obs-websocket-Client mit Anmeldung und Wiederverbindung, Pfadübersetzung Windows → Container, Abklingzeiten, Twitch-Clip parallel, Übernahme der Rohdatei mit Warten auf fertigen Schreibvorgang. Der OBS-Hotkey wird ohne unseren Umweg erkannt und als vollwertiger Auslöser behandelt.

Die Abnahme steht aus: Auf dieser Entwicklungsmaschine läuft kein OBS. Was geprüft ist: Anmeldeverfahren (gegen eine unabhängige Zweitimplementierung), Pfadübersetzung in allen vier Fällen, Warten auf die fertige Datei. Was ungeprüft bleibt: das tatsächliche Gespräch mit einem laufenden OBS.

S3 — 9:16-Render · ~1,5 Tage

calibrate-Kommando: zieht einen Frame aus einer Replay-Datei, schreibt ihn als PNG raus, nimmt die vier Crop-Werte entgegen und legt sie in config/layout.yaml ab. Render-Schritt mit dem Filtergraph aus projects/clip-shorts.md, Kopie der Rohdatei ins Arbeitsvolume vor der Verarbeitung.

Fertig, wenn: Aus einer Replay-Datei entsteht eine vertical.mp4 mit korrekt sitzender Facecam oben und Gameplay unten.

S3 abgenommen am 2026-09-09. Verifiziert an einer künstlichen Aufnahme, die eine OBS-Aufzeichnung nachbildet: 1920×1080, 70 s, drei getrennte Tonspuren (Mischung, Mikrofon, Spiel). Ergebnis 1080×1920, 45 s, Naht bei y = 512 an der richtigen Stelle, Rechenzeit 27,9 s bei nice 15 — also gut zwei Drittel der Echtzeit auf einer CPU ohne Grafikbeschleunigung.

Dabei aufgefallen und korrigiert: S2 hat die Rohdatei in den Windows-Bind-Mount kopiert und dort verarbeitet — genau das, was ADR-0004 Punkt 2 verbietet. Jetzt liegt die Rohdatei im Arbeits-Volume, und auf die Windows-Seite kommt nur das fertige, kleine Ergebnis.

Neu dazu: docker compose run --rm toolkit render <datei> rendert eine Rohdatei von Hand — gedacht zum Nachrendern, wenn sich das Layout ändert.

S4 — Untertitel · ~1,5 Tage

Nur die Mikrofonspur extrahieren (OBS nimmt getrennt auf: Mikro Spur 2, Spielton Spur 3, Mischung Spur 1) als 16 kHz Mono WAV, whisper.cpp mit ggml-small und --language de, Ausgabe als SRT mit Wort-Zeitstempeln, Umwandlung in ASS mit wortweisem Highlight, Stil aus config/subtitle-style.ass, Einbrennen. Sicherheitsabstand nach unten mindestens 320 px. Ins Video kommt weiterhin die Mischung von Spur 1.

Fertig, wenn: final.mp4 hat lesbare, zeitlich passende deutsche Untertitel, und der Text wird auf dem Handy von der TikTok-Oberfläche nicht verdeckt.

S4 abgenommen am 2026-09-09, geprüft an einem echten 30-Sekunden-Clip mit deutscher Sprache. 41 Wörter in 37 s Rechenzeit auf der CPU, Transkript inhaltlich korrekt, Untertitel sitzen zeitlich und sind lesbar.

Drei Fehler kamen erst am echten Material heraus:

  • Ein-Wort-Zeilen. Bei zögerndem Sprechen zerfiel der Text in Zeilen mit einem einzigen Wort („Dank" / „es," / „Savina,"). Die Obergrenze von 2,2 s je Zeile war zu eng; jetzt 3,2 s plus ein Nachverdichten, das allein stehende Wörter an den Nachbarn hängt. 17 Zeilen wurden zu 10.
  • Überlappende Zeilen. Der Nachlauf von 0,25 s ragte in die nächste Zeile. ASS stapelt gleichzeitig sichtbare Zeilen übereinander — das sieht kaputt aus. Das Ende wird jetzt am Beginn der Folgezeile abgeschnitten.
  • Zeitformat. Bei 59,999 s rundeten die Hundertstel auf 100 und der Übertrag lief nicht in die Minuten: 0:00:60.00 statt 0:01:00.00. Gefunden durch einen Test, nicht durch Zusehen.

Befund fürs Streamsetup: Im Quellmaterial lagen bereits zwei Textebenen (Spieluntertitel und ein Live-Caption-Overlay mit Wort-Markierung). Beide sind fest eingebrannt. Behandelt in obs-layout.md.

S5 — Oberfläche: Onboarding und Sichtung · ~2 Tage

Erststart-Assistent. Die heutige Einrichtungsseite ist ein Formularblock — das reicht für mich, nicht für Abdu. Der Assistent führt durch sechs Schritte und prüft jeden davon live, statt zu behaupten, es sei erledigt:

Schritt Wird geprüft
1. Kanal bestätigen Kanal existiert (Helix)
2. Twitch-Apps anlegen Redirect-URI zum Kopieren, Anleitung
3. Verbinden Token gültig, Scopes vollständig
4. OBS erreichbar, Wiedergabepuffer aktiv, Hotkey belegt
5. Facecam einmessen Vorschau direkt in der Seite
6. Fertig „So löst du einen Clip aus"

Warum diese Scheibe hinten liegt: Die Prüfungen sind nur so viel wert, wie es zu prüfen gibt. Ein OBS-Schritt, der behauptet „alles gut", ohne den Wiedergabepuffer wirklich abzufragen, ist schlimmer als kein Schritt. Deshalb erweitert jede Scheibe ab S2 den Assistenten um ihren eigenen Schritt, und S5 macht daraus einen zusammenhängenden Ablauf mit Texten, Reihenfolge und der Startseite danach.

Danach wird aus der Einrichtungsseite eine Einstellungsseite, und die Startseite zeigt den laufenden Stream und die anstehenden Entscheidungen — nicht eine Liste von Werkzeugen (siehe ADR-0006).

Sichtung. Auf derselben Oberfläche eine Seite mit allen Kandidaten des letzten Streams: Videovorschau, Punktzahl, Auslöser, der Chat-Ausschnitt aus dem Clip-Fenster, Transkript, und die Knöpfe Übernehmen / Verwerfen. Übernommene Clips wandern nach data/clips/_upload/.

Fertig, wenn: Ein Fremder kommt von „nichts eingerichtet" bis zum ersten eigenen Clip, ohne die Dokumentation zu lesen. Und: nach einem Stream lässt sich in unter zwei Minuten entscheiden, welche Clips hochgeladen werden.

S6 — Erstchatter-Begrüßung · ~0,5 Tage · optional

Der IRC-Client aus S1 liegt ohnehin da, das first-msg-Tag ebenso. Eine Begrüßung für Erstchatter, eine für Rückkehrer nach mehr als 21 Tagen, mit Cooldown und Obergrenze pro Stream. Details in projects/community-memory.md.

Diese Scheibe steht am Ende, weil sie unabhängig ist und notfalls entfällt. Sie kostet fast nichts und ist das Einzige im MVP, das die Zuschauer direkt merken — deshalb ist sie trotzdem drin.

Zeitplan

Woche Scheiben Ergebnis
1 S0, S1 Einrichtung im Browser, schreibt jeden Stream mit
2 S2, S3 Knopfdruck erzeugt ein fertiges 9:16-Video
3 S4, S5, S6 Untertitel, Sichtungsseite, Begrüßung

Rund zehn Arbeitstage (S0 erledigt, S1 durch den Einrichtungs-Assistenten einen Tag teurer). Realistisch drei Kalenderwochen, wenn nebenher noch etwas anderes läuft.

Definition of Done für das gesamte MVP

Ein echter Stream, ohne Eingriff von außen:

  1. Container läuft von Streambeginn bis -ende durch, auch über einen Twitch-Reconnect hinweg.
  2. Fünfmal ausgelöst (dreimal !clip, zweimal Hotkey) ergibt fünf final.mp4 mit sitzendem Crop und brauchbaren Untertiteln.
  3. Kein Auslöser bleibt unbestätigt, kein Clip geht verloren.
  4. Die Sichtungsseite zeigt alle fünf, sortiert nach Score.
  5. Das Encoding kostet den Stream keine sichtbaren Frames (Kontrolle über die OBS-Statistik: keine übersprungenen Frames wegen Encoder-Überlast).
  6. Chat und Zuschauerzahlen des Streams sind vollständig in der DB.

Risiken

Risiko Wahrscheinlichkeit Gegenmaßnahme
Encoding kostet Frames im Stream mittel -preset veryfast, CPU-Limit im Compose-File, Schalter renderWhileLive: false — dann läuft alles nach Streamende
whisper.cpp versteht Spielgeräusche statt Sprache mittel gelöst OBS nimmt getrennte Spuren auf; transkribiert wird nur das Mikrofon (Spur 2).
Twitch ändert die Ableitung der Clip-MP4-URL niedrig Betrifft nur den Zweitweg; die Rohdatei kommt aus OBS. Gekapselt in einer Datei.
Replay-Buffer-Pfad-Abbildung Windows ↔ Container mittel In S2 als Erstes lösen, mit einem Testlauf vor allem anderen
Docker Desktop belastet den Stream-PC mittel .wslconfig mit Speicher- und CPU-Grenze; wenn es stört, ist ein Betrieb ohne Container jederzeit möglich, weil nur ffmpeg und whisper.cpp fehlen

Was Abdu vorher tun muss

  1. Docker Desktop installieren, WSL2-Backend aktivieren
  2. Zwei Twitch-Apps anlegen, Redirect-URI http://localhost:4792/callback (siehe 05-setup.md). Client-ID und Secret nicht irgendwo eintragen — die kommen in den Einrichtungs-Assistenten.
  3. In OBS: obs-websocket aktivieren (Port 4455, Passwort), Wiedergabepuffer auf 90 Sekunden, Hotkey auf „Wiedergabepuffer speichern" legen
  4. Screenshot der laufenden Szene schicken — erledigt, Crop ist eingemessen und steht in config/layout.yaml
  5. Getrennte Audiospuren klären — erledigt, OBS nimmt getrennt auf; Whisper bekommt nur die Mikrofonspur
  6. In GTA die spieleigenen Untertitel ausschalten (werden vom Ausschnitt rechts abgeschnitten, und die eigenen liegen ohnehin darüber)

Danach

Wenn das MVP steht, ist der teuerste Teil erledigt: Auth, DB, Chat-Anbindung und Container laufen. Die nächsten Meilensteine aus 04-roadmap.md setzen darauf auf und werden dadurch erheblich billiger — das Analytics-Dashboard wertet dann bereits mehrere Wochen echter Daten aus, statt bei null anzufangen.