Zugangsdaten kommen jetzt aus der App statt aus einer .env: ein Einrichtungs-Assistent auf http://localhost:4792/setup nimmt Client-ID, Secret und OBS-Passwort entgegen, der OAuth-Rueckruf laeuft auf denselben Server. Gespeichert wird verschlueselt (AES-256-GCM) in einem Volume, bewusst nicht in der Datenbank - die wird auf die Windows-Seite gesichert. Siehe ADR-0005. Technikbasis vereinfacht: Node 24 bringt node:sqlite, WebSocket, fetch und die Ausfuehrung von TypeScript mit. Das Projekt hat damit null Laufzeitabhaengigkeiten und keinen Build-Schritt; pnpm-Workspaces und die Paketmanifeste sind entfallen, Importe laufen ueber die imports-Map der Wurzel-package.json. erasableSyntaxOnly verhindert Syntax, die das Type Stripping stillschweigend entfernen wuerde. Siehe Nachtrag 2 in ADR-0001. Enthalten: - packages/core: Config, Layout, Logger mit Secret-Maskierung, Retry, YAML - packages/db: Migrationen, Repositories, alle SQL-Anweisungen an einem Ort - packages/twitch: Secret-Speicher, OAuth, Helix mit Token-Bucket, IRC - apps/ingest: Daemon, Einrichtungsseite, Spike-Erkennung - 20 Tests (IRC-Parser, Spike-Detektor, Datenbankschicht) Der Chat verbindet sich anonym, solange nichts hinterlegt ist - damit laesst sich die Datenaufnahme pruefen, bevor die Twitch-Apps existieren. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013VTGjd92LsDDWF3frd9FTB
252 lines
12 KiB
Markdown
252 lines
12 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–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.
|
||
|
||
### 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 — 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 | 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.
|