Eigener obs-websocket-v5-Client (das Protokoll ist klein genug, und das Projekt bleibt ohne Abhaengigkeiten). Anmeldung, Wiederverbindung mit wachsender Pause, Ereignisse fuer ReplayBufferSaved und Szenenwechsel. Der Clipper loest zwei Wege gleichzeitig aus: OBS-Wiedergabepuffer fuer die Rohdatei in Aufnahmequalitaet, Twitch-Clip fuer die teilbare URL. Faellt einer aus, ist der andere trotzdem etwas wert. Ein direkt in OBS gedrueckter Hotkey wird ohne unseren Umweg erkannt und als vollwertiger Ausloeser behandelt. Die Pfaduebersetzung Windows -> Container hat eine eigene Datei und eigene Tests - das ist die Stelle, an der die Pipeline am ehesten scheitert. Dazu Warten auf den abgeschlossenen Schreibvorgang, bevor die Datei uebernommen wird. Zum Testvektor der OBS-Anmeldung: die Spezifikation nennt Salt und Challenge, aber kein erwartetes Ergebnis. Der Erwartungswert wurde unabhaengig mit Pythons hashlib aus derselben Beschreibung gerechnet; der Test sagt das im Kommentar, statt einen amtlichen Vektor vorzutaeuschen. 29 Tests gruen. Die Abnahme von S2 braucht ein laufendes OBS und steht aus. Ausserdem: ADR-0006 haelt fest, dass alles unter einem Dach laeuft - eine lokale Weboberflaeche plus Kindprozesse fuer Rechenarbeit. Die Prozessliste in der Architekturdoku ist entsprechend ersetzt. Der Onboarding-Assistent ist als Teil von S5 aufgenommen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013VTGjd92LsDDWF3frd9FTB
289 lines
14 KiB
Markdown
289 lines
14 KiB
Markdown
# 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 | **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**, 29 Tests grün — Abnahme braucht laufendes OBS |
|
||
| S3–S6 | offen |
|
||
|
||
**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](decisions/ADR-0001-monorepo-typescript.md).
|
||
|
||
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`](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](decisions/ADR-0005-zugangsdaten.md).
|
||
|
||
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`](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
|
||
|
||
**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.
|
||
|
||
### 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](decisions/ADR-0006-ein-dach.md)).
|
||
|
||
**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`](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`](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`](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.
|