S1: Einrichtungs-Assistent, Twitch-Auth, Datenbank und Chat-Aufnahme

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
This commit is contained in:
ZeasyAlexandClaude Opus 5 committed 2026-09-09 09:56:55 +02:00
1 parent 4ceb61f095
commit d07c18441b
82 files changed
+2775 -838

No files matched your search

+57 -31
View File
@@ -36,14 +36,34 @@ gibt. Vorher ist sie nur Ballast.
|---|---|
| S0 Container + Toolchain | **fertig** — Image 258 MB, `check-env` ohne harte Fehler |
| Layout eingemessen | **fertig** — `config/layout.yaml`, vorgezogen aus S3 |
| S1 Auth + DB + IRC-Ingest | als Nächstes |
| 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. `check-env` prüft Werkzeuge,
Volumes, Layout-Geometrie, OBS-Erreichbarkeit und Zugangsdaten; die fünf
verbleibenden Warnungen (Sprachmodell, OBS, drei `.env`-Schlüssel) sind auf
dieser Entwicklungsmaschine erwartbar.
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
@@ -56,13 +76,6 @@ 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.
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
@@ -101,13 +114,19 @@ 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
### S1 — Einrichtung, Auth und Datenaufnahme · ~3 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.
**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`.
@@ -140,10 +159,12 @@ 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.
**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
@@ -174,12 +195,13 @@ merken — deshalb ist sie trotzdem drin.
| Woche | Scheiben | Ergebnis |
|---|---|---|
| 1 | S0, S1 | Läuft im Container, schreibt jeden Stream mit |
| 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 neun Arbeitstage. Realistisch drei Kalenderwochen, wenn nebenher noch
etwas anderes läuft.
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
@@ -200,7 +222,7 @@ Ein echter Stream, ohne Eingriff von außen:
| 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. |
| 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 |
@@ -208,13 +230,17 @@ Ein echter Stream, ohne Eingriff von außen:
## Was Abdu vorher tun muss
1. Docker Desktop installieren, WSL2-Backend aktivieren
2. Zwei Twitch-Apps anlegen (siehe [`05-setup.md`](05-setup.md))
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. **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)
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