# 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`](06-hebel-analyse.md). Detailspezifikation: [`projects/clip-shorts.md`](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`](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](decisions/ADR-0004-docker.md) | ## Fortschritt | Scheibe | Stand | |---|---| | S0 Container + Toolchain | in Arbeit | | Layout eingemessen | **fertig** — `config/layout.yaml`, vorgezogen aus S3 | | S1–S6 | offen | 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`](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///` 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 — Auth und Datenaufnahme · ~2 Tage OAuth-Flow (`auth`-Kommando, Callback auf Port 4780), verschlüsselter Token-Store mit automatischem Refresh, Helix-Client mit Rate-Limit-Bucket, DB-Migrationen für `streams`, `users`, `user_streams`, `chat_messages`, `viewer_samples`, `clips`. IRC-Client, der jede Nachricht schreibt. Stream-Sessions über Helix-Polling im Minutentakt. **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. ### 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`](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. ### S4 — Untertitel · ~1,5 Tage Audiospur extrahieren (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. **Fertig, wenn:** `final.mp4` hat lesbare, zeitlich passende deutsche Untertitel, und der Text wird auf dem Handy von der TikTok-Oberfläche nicht verdeckt. ### S5 — Sichtung · ~1 Tag Ein kleiner HTTP-Server auf Port 4792 liefert eine Seite mit allen Kandidaten des letzten Streams: Videovorschau, Score, 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:** 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`](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 | Läuft im Container, schreibt jeden Stream mit | | 2 | S2, S3 | Knopfdruck erzeugt ein fertiges 9:16-Video | | 3 | S4, S5, S6 | Untertitel, Sichtungsseite, Begrüßung | Rund neun Arbeitstage. 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 | Falls OBS getrennte Audiospuren aufnimmt, nur die Mikrofonspur transkribieren. Sonst Rauschgatter vorschalten. | | 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 (siehe [`05-setup.md`](05-setup.md)) 3. In OBS: obs-websocket aktivieren (Port 4455, Passwort), Wiedergabepuffer auf 90 Sekunden, Hotkey auf „Wiedergabepuffer speichern" legen 4. **Einen Screenshot der laufenden Szene** schicken — daraus wird der Facecam-Crop eingemessen 5. Klären, ob OBS Mikrofon und Spiel auf getrennte Audiospuren aufnimmt (macht die Untertitel deutlich besser) ## 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`](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.