Monorepo-Geruest fuer neun Streamer-Tools plus den Ingest-Daemon, der unter sechs davon liegt. Enthaelt: - docs/: Ueberblick, Architektur, Twitch-Integration, Datenmodell, Roadmap, Setup, Hebel-Analyse und den MVP-Plan "Clip-Knopf" - docs/projects/: je eine Spezifikation pro Tool - docs/decisions/: vier ADRs (TS-Monorepo, SQLite, EventSub/IRC, Docker) - docker/: Image mit ffmpeg und whisper.cpp, abhaengigkeitsfreier Entrypoint mit check-env, fetch-model und calibrate - config/layout.yaml: aus einem VOD-Screenshot eingemessener Facecam-Ausschnitt (440x330 oben links, Chat-Overlay ab x=1270) Noch kein Anwendungscode - Scheibe S0 aus docs/07-mvp-plan.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013VTGjd92LsDDWF3frd9FTB
208 lines
9.5 KiB
Markdown
208 lines
9.5 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 | 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/<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 — 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.
|