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

+13 -6
View File
@@ -93,24 +93,31 @@ Vollständige Event-Liste: siehe [`03-data-model.md`](03-data-model.md).
| Paket | Inhalt |
|-------|--------|
| `packages/shared` | Typen, Event-Definitionen, Zod-Schemas |
| `packages/core` | Config-Laden, Logger, Event-Bus, Scheduler, Retry |
| `packages/twitch` | OAuth-Token-Store, Helix-Client, EventSub-WS, IRC-Client |
| `packages/shared` | Ereignistypen, ULID |
| `packages/core` | Config, Layout, Logger mit Maskierung, Retry, YAML-Leser |
| `packages/twitch` | Secret-Speicher, OAuth, Helix-Client, IRC-Client |
| `packages/db` | SQLite-Verbindung, Migrationen, Repositories |
Regel: Apps dürfen von Paketen abhängen, Pakete nie von Apps. Apps hängen nicht
voneinander ab — Kommunikation läuft über Bus und DB.
Importiert wird über die `imports`-Map der Wurzel-`package.json`
(`#core/config.ts`, `#db/repos.ts`). Das ist eine Node-Funktion, kein
Build-Werkzeug — es gibt keinen Bundler und keine Pfad-Aliasse im Compiler.
## Technologie
- **TypeScript / Node 22**, pnpm-Workspaces — siehe
- **TypeScript auf Node 24**, ohne Build-Schritt und ohne
Laufzeitabhängigkeiten — Node führt die `.ts`-Dateien direkt aus. Siehe
[ADR-0001](decisions/ADR-0001-monorepo-typescript.md)
- **SQLite** über `better-sqlite3` — siehe [ADR-0002](decisions/ADR-0002-sqlite.md)
- **SQLite** über das eingebaute `node:sqlite` — siehe
[ADR-0002](decisions/ADR-0002-sqlite.md)
- **EventSub über WebSocket**, nicht Webhooks — siehe
[ADR-0003](decisions/ADR-0003-eventsub-websocket.md)
- **ffmpeg** (CLI) für Videoverarbeitung
- **whisper.cpp** (CLI) für Untertitel — kein Python, keine GPU
- **Vite + Preact** für Dashboard und Overlay-Seiten
- **Serverseitiges HTML** ohne Framework für Einrichtung und Sichtungsseite;
für Dashboard und Overlays ab M3 ggf. Vite + Preact
- **Docker Compose** für den Betrieb — siehe [ADR-0004](decisions/ADR-0004-docker.md)
## Fehlerverhalten
+10 -5
View File
@@ -11,9 +11,11 @@ Es werden zwei getrennte Twitch-Apps/Tokens gebraucht:
Trennung, damit Bot-Nachrichten als Bot erkennbar sind und der Broadcaster-
Token nicht im Chat-Client hängt.
Beide Tokens liegen verschlüsselt in `data/db/tokens.json`, werden über Refresh
Tokens automatisch erneuert (`packages/twitch/tokenStore.ts`). Client-Secret
kommt aus `.env`, niemals aus der DB, niemals ins Repo.
Client-IDs, Client-Secrets und beide Tokens liegen verschlüsselt in
`/db/secrets.json` im Docker-Volume und werden über Refresh Tokens automatisch
erneuert (`packages/twitch/secrets.ts`). Eingegeben werden sie im
Einrichtungs-Assistenten unter `http://localhost:4792/setup`, **nicht** in einer
`.env` und nie im Repo — siehe [ADR-0005](decisions/ADR-0005-zugangsdaten.md).
## Scopes
@@ -34,8 +36,11 @@ kommt aus `.env`, niemals aus der DB, niemals ins Repo.
| `analytics:read:games` | Game-Analytics-Report | Broadcaster |
| `bits:read` | Bits-Events | Broadcaster |
Der Autorisierungs-Flow läuft einmalig über `docker compose run --rm --service-ports toolkit auth` → lokaler Redirect auf
`http://localhost:4780/callback`.
Der Autorisierungs-Flow läuft über den Einrichtungs-Assistenten:
`docker compose up -d`, dann `http://localhost:4792/setup` öffnen und je einmal
für Broadcaster- und Bot-Konto auf „Mit Twitch verbinden" klicken. Die
Redirect-URI, die in beiden Twitch-Apps hinterlegt sein muss, lautet
`http://localhost:4792/callback`.
## EventSub (WebSocket)
+29 -11
View File
@@ -34,16 +34,18 @@ Danach `wsl --shutdown` und Docker Desktop neu starten.
Zwei Anwendungen unter https://dev.twitch.tv/console/apps:
1. `abdulive-toolkit` — OAuth Redirect `http://localhost:4780/callback`
1. `abdulive-toolkit` — OAuth Redirect `http://localhost:4792/callback`
2. `abdulive-bot` — gleicher Redirect
Client-ID und Client-Secret beider Apps in `.env` eintragen
(Vorlage: `.env.example`).
Client-ID und Client-Secret **nicht** in eine Datei eintragen, sondern später
im Einrichtungs-Assistenten einfügen (siehe unten). Begründung:
[ADR-0005](decisions/ADR-0005-zugangsdaten.md).
## OBS vorbereiten
1. **obs-websocket aktivieren:** Werkzeuge → WebSocket-Servereinstellungen →
Server aktivieren, Port 4455, Passwort setzen und in `.env` eintragen.
Server aktivieren, Port 4455, Passwort setzen — eingetragen wird es später
im Einrichtungs-Assistenten.
2. **Wiedergabepuffer aktivieren:** Einstellungen → Ausgabe → Wiedergabepuffer,
90 Sekunden. Den Zielordner merken und in `.env` als `OBS_REPLAY_DIR`
eintragen — mit Forward-Slashes, z. B. `C:/Users/abdu/Videos/replay`.
@@ -51,23 +53,39 @@ Client-ID und Client-Secret beider Apps in `.env` eintragen
Das ist der Clip-Knopf. Ein globaler Windows-Hotkey wird bewusst *nicht*
gebaut — aus einem Container ist er nicht abfangbar, und OBS meldet den
gespeicherten Puffer ohnehin über WebSocket.
4. **Browser-Sources** (erst ab Meilenstein M3 relevant): „Browser-Quelle beim
4. **Getrennte Audiospuren** (bereits eingerichtet): Mikrofon auf Spur 2,
Spielton auf Spur 3, Mischung auf Spur 1. Die Untertitel werden aus Spur 2
erzeugt - das ist bei lauten Spielen der Unterschied zwischen brauchbarem
und unbrauchbarem Transkript. Einstellungen -> Ausgabe -> Aufnahme ->
Audiospuren, und im Audiomixer je Quelle die Spurzuordnung pruefen.
5. **Browser-Sources** (erst ab Meilenstein M3 relevant): „Browser-Quelle beim
Szenenwechsel neu laden" **aus**, sonst brechen laufende Effekte ab.
## Erste Einrichtung
```powershell
Copy-Item .env.example .env # Secrets und OBS_REPLAY_DIR eintragen
Copy-Item config\config.example.yaml config\config.yaml
docker compose build
docker compose run --rm toolkit check-env # prueft ffmpeg, whisper, Modell, Volumes
docker compose run --rm --service-ports toolkit auth # Browser oeffnet sich, zweimal bestaetigen
docker compose run --rm toolkit db:migrate
docker compose run --rm toolkit check-env # ffmpeg, whisper, Modell, Volumes, Layout
docker compose run --rm toolkit fetch-model # Sprachmodell, ca. 465 MB
docker compose up -d
```
Der `auth`-Lauf braucht `--service-ports`, damit Port 4780 für den
OAuth-Rücksprung erreichbar ist.
Dann **http://localhost:4792/setup** öffnen. Dort werden nacheinander
eingetragen:
1. Client-ID und Client-Secret der beiden Twitch-Apps
2. „Mit Twitch verbinden" für Broadcaster- und Bot-Konto (OAuth)
3. obs-websocket-Passwort
4. später: Discord-Webhook, Bluesky-Zugang
Alles landet verschlüsselt im Docker-Volume, nicht in einer Datei im Projekt.
> **Diese Seite zeigt Zugangsdaten. Nicht öffnen, während gesendet wird.**
Eine `.env` ist optional und enthält nur noch den Pfad zum Wiedergabepuffer
und das Log-Level (Vorlage: `.env.example`).
## Facecam einmessen
+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
+34 -6
View File
@@ -13,12 +13,40 @@ riecht; Teile sind Browser-Overlays, was TypeScript nahelegt.
Alles in TypeScript auf Node 22, als pnpm-Workspace-Monorepo. ffmpeg und
whisper.cpp werden als **CLI-Prozesse** aufgerufen, nicht über Sprachbindings.
> **Nachtrag 2026-09-09:** Ursprünglich war faster-whisper (Python) vorgesehen.
> Der Check der Zielmaschine ergab Python 3.14, für das `ctranslate2` noch keine
> verlässlichen Wheels liefert. Da ohnehin nur ein Kommandozeilenaufruf
> gebraucht wird, fällt die Wahl auf **whisper.cpp** — eine einzelne Binary
> ohne Laufzeitabhängigkeiten. Damit entfällt Python vollständig aus dem
> Projekt. Siehe [ADR-0004](ADR-0004-docker.md).
> **Nachtrag 1 (2026-09-09):** Ursprünglich war faster-whisper (Python)
> vorgesehen. Der Check der Zielmaschine ergab Python 3.14, für das
> `ctranslate2` noch keine verlässlichen Wheels liefert. Da ohnehin nur ein
> Kommandozeilenaufruf gebraucht wird, fällt die Wahl auf **whisper.cpp** —
> eine einzelne Binary ohne Laufzeitabhängigkeiten. Damit entfällt Python
> vollständig. Siehe [ADR-0004](ADR-0004-docker.md).
> **Nachtrag 2 (2026-09-09, beim Bau von S1):** Der Plan sah pnpm-Workspaces
> mit einem Paketmanifest je Paket und einen `tsc`-Build vor. Beides ist
> weggefallen, weil sich beim Bauen zeigte, dass **Node 24 alles Nötige
> mitbringt**:
>
> | Gebraucht | Statt npm-Paket | Eingebaut |
> |---|---|---|
> | SQLite | `better-sqlite3` (nativ, node-gyp) | `node:sqlite` |
> | WebSocket-Client | `ws` | globales `WebSocket` |
> | HTTP-Aufrufe | `undici`/`axios` | globales `fetch` |
> | TypeScript ausführen | `tsc`-Build, `tsx` | Type Stripping |
> | YAML | `yaml` | eigener Leser, 130 Zeilen |
>
> Das Projekt hat damit **null Laufzeitabhängigkeiten und keinen
> Build-Schritt**. `node docker/entrypoint.ts` läuft direkt. Auflösung der
> paketübergreifenden Importe (`#core/…`, `#db/…`) über die `imports`-Map in
> der Wurzel-`package.json` — ebenfalls eine Node-Funktion, kein Werkzeug.
>
> Konsequenz: Die Paketmanifeste und `pnpm-workspace.yaml` sind gelöscht, es
> gibt eine `package.json`. `typescript` bleibt als einzige
> Entwicklungsabhängigkeit für `tsc --noEmit`.
>
> **Preis:** Nur *löschbare* TypeScript-Syntax ist erlaubt — keine `enum`,
> keine Parameter-Properties, keine `namespace`. Das erzwingt
> `erasableSyntaxOnly: true` in der `tsconfig.json`, und es hat beim ersten
> Durchlauf sofort einen echten Fund gehabt (`constructor(private capacity)`
> im Helix-Client, das zur Laufzeit stillschweigend verschwunden wäre).
## Begründung
+7 -3
View File
@@ -10,7 +10,8 @@ Viewer-Samples pro Monat — wenige MB.
## Entscheidung
SQLite (`better-sqlite3`) im WAL-Modus, eine Datei unter `data/db/`.
SQLite über das eingebaute **`node:sqlite`** im WAL-Modus, eine Datei im
Docker-Volume `abdulive-db`.
## Begründung
@@ -18,8 +19,11 @@ SQLite (`better-sqlite3`) im WAL-Modus, eine Datei unter `data/db/`.
Dateikopie-Vorgang (`VACUUM INTO`).
- WAL erlaubt beliebig viele gleichzeitige Leser neben einem Schreiber. Genau
unsere Topologie: Der Ingest schreibt, alle anderen lesen.
- `better-sqlite3` ist synchron, was den Code im Ingest erheblich vereinfacht —
keine Race Conditions zwischen Event-Verarbeitung und Schreibvorgang.
- `node:sqlite` ist synchron (`DatabaseSync`), was den Code im Ingest erheblich
vereinfacht - keine Race Conditions zwischen Ereignisverarbeitung und
Schreibvorgang.
- Es ist **in Node 24 enthalten**: keine native Abhaengigkeit, kein node-gyp,
keine Prebuild-Suche im Docker-Build. Siehe Nachtrag 2 in ADR-0001.
- Die Datenmenge ist um Größenordnungen von SQLites Grenzen entfernt.
## Konsequenzen
+87
View File
@@ -0,0 +1,87 @@
# ADR-0005: Zugangsdaten kommen aus der App, nicht aus der `.env`
**Status:** akzeptiert · **Datum:** 2026-09-09
## Kontext
Das Toolkit braucht Client-ID und Client-Secret zweier Twitch-Apps, ein
obs-websocket-Passwort und später Discord-/Bluesky-Zugänge. Der erste Entwurf
sah dafür eine `.env`-Datei vor, die von Hand befüllt wird.
Das passt nicht zum Nutzer. Abdu ist Streamer, kein Entwickler. „Kopiere
`.env.example` nach `.env` und trage sieben Werte ein" ist die Stelle, an der
ein Werkzeug liegen bleibt. Und es kommt ein zweites Problem dazu, das für
diesen Anwendungsfall besonders zählt: **Wer eine Klartextdatei mit
Zugangsdaten öffnet, während er sendet, hat sie live gezeigt.**
## Entscheidung
Zugangsdaten werden über einen **Einrichtungs-Assistenten im Browser**
eingegeben, unter `http://localhost:4792/setup`. Gespeichert werden sie
verschlüsselt in `/db/secrets.json` im Docker-Volume.
Die `.env` bleibt bestehen, enthält aber nur noch **optionale** Werte
(Log-Level, Pfad zum Wiedergabepuffer). Ohne `.env` startet alles.
Damit fällt auch Port 4780 weg: Einrichtung, OAuth-Rücksprung und später
Sichtungsseite und Dashboard laufen über **einen** Server auf Port 4792. Die
Redirect-URI in der Twitch-App lautet entsprechend
`http://localhost:4792/callback`.
## Speicherung
| | |
|---|---|
| Ort | `/db/secrets.json`, benanntes Volume `abdulive-db` |
| Verfahren | AES-256-GCM, Schlüssel in `/db/.secret-key` (Modus 0600) |
| Schlüssel | wird beim ersten Start erzeugt, nie ausgegeben, nie geloggt |
| **Nicht** in SQLite | damit das Backup-Skript sie nie mitkopiert |
Die Secrets liegen bewusst **außerhalb der Datenbank**. `scripts/backup.ps1`
zieht die `.db` aus dem Volume nach `data/db/backup/` — also auf die
Windows-Seite, wo sie in einer Cloud-Synchronisation landen kann. Was dort
nichts zu suchen hat, darf gar nicht erst in der Datenbank stehen.
## Was diese Verschlüsselung leistet — und was nicht
Der Schlüssel liegt im selben Volume wie die verschlüsselten Daten. Gegen
jemanden, der Zugriff auf das Volume hat, schützt das **nicht**, und es wird
hier auch nicht so getan.
Wogegen es schützt, und das ist der eigentliche Zweck:
- **Versehentliche Anzeige im Stream.** `cat secrets.json` zeigt Kauderwelsch,
keine Client-Secrets.
- **Verirrte Kopien.** Ein Volume-Export oder ein weitergegebener Ordner
enthält ohne die Schlüsseldatei nichts Verwertbares.
- **Grep-Unfälle.** Secrets tauchen in keiner Volltextsuche und in keinem
versehentlich geteilten Log auf.
Gegen eine Kompromittierung der Maschine schützt es nicht. Das ist bei einem
lokalen Einzelplatzwerkzeug auch nicht das Bedrohungsmodell — wer dort Zugriff
hat, hat auch Zugriff auf OBS, den Browser und die gespeicherten Twitch-Cookies.
## Konsequenzen für die Oberfläche
- Der Einrichtungs-Assistent **zeigt gespeicherte Secrets nie wieder an.**
Nach dem Speichern steht dort `•••••• gespeichert` und ein Knopf *Ersetzen*.
- Über der Seite steht ein Hinweis: **„Diese Seite zeigt Zugangsdaten — nicht
im Stream öffnen."**
- Der Server ist nur lokal erreichbar. **Nicht** dadurch, dass er im Container
auf `127.0.0.1` bindet — das wäre dort die Container-Loopback, die Dockers
Portweiterleitung nie erreicht, der Dienst wäre schlicht tot. Die Abschottung
kommt aus zwei Teilen: der Container bindet auf `0.0.0.0` **innerhalb seines
eigenen Netzwerk-Namensraums**, und Compose veröffentlicht den Port
ausschließlich als `127.0.0.1:4792`. Außerhalb von Docker bindet der Server
weiterhin auf die Loopback-Adresse; gesteuert über `BIND_HOST`, das nur das
Image auf `0.0.0.0` setzt.
- Secrets werden nie geloggt, auch nicht bei Fehlern. Der Logger maskiert
bekannte Schlüsselnamen.
- `check-env` meldet, *ob* Zugangsdaten hinterlegt sind, nie *welche*.
## Preis
Der Einrichtungs-Assistent kostet in S1 ungefähr einen zusätzlichen Tag —
kleiner HTTP-Server, eine Seite, der Secret-Speicher. Dafür entfällt der
gesamte `.env`-Schritt aus der Inbetriebnahme, und das Werkzeug lässt sich
später ohne Texteditor umkonfigurieren.
+1
View File
@@ -6,6 +6,7 @@
| [0002](ADR-0002-sqlite.md) | SQLite statt Postgres | akzeptiert |
| [0003](ADR-0003-eventsub-websocket.md) | EventSub ueber WebSocket, Chat ueber IRC | akzeptiert |
| [0004](ADR-0004-docker.md) | Betrieb in Docker, whisper.cpp statt Python | akzeptiert |
| [0005](ADR-0005-zugangsdaten.md) | Zugangsdaten ueber Einrichtungs-Assistent statt .env | akzeptiert |
Neue Entscheidung: Datei `ADR-<nummer>-<kurzname>.md`, Aufbau Kontext /
Entscheidung / Begruendung / Konsequenzen.
+20 -2
View File
@@ -122,8 +122,26 @@ Bei Quelle = getrennte Vertical-Szene aus OBS entfällt der ganze Filtergraph.
## Untertitel
`whisper.cpp` (`whisper-cli`), Modell `ggml-small` (deutsch, reicht; `medium`
bei Bedarf), auf der extrahierten Audiospur (16 kHz Mono WAV). Kein Python,
keine GPU nötig — auf CPU braucht ein 30-Sekunden-Clip unter einer Minute.
bei Bedarf). Kein Python, keine GPU nötig — auf CPU braucht ein
30-Sekunden-Clip unter einer Minute.
**Transkribiert wird ausschließlich die Mikrofonspur.** OBS nimmt getrennte
Audiospuren auf (Mikro auf Spur 2, Spielton auf Spur 3, Mischung auf Spur 1).
Whisper bekommt nur Spur 2 — bei GTA-Motoren, Schüssen und Spielmusik ist das
der Unterschied zwischen einem brauchbaren und einem unbrauchbaren Transkript.
Ins fertige Short kommt weiterhin die Mischung von Spur 1.
```bash
# Mikrofonspur herausziehen, 16 kHz Mono, so wie whisper.cpp es erwartet
ffmpeg -i raw.mp4 -map 0:a:1 -ac 1 -ar 16000 -c:a pcm_s16le mic.wav
whisper-cli -m /models/ggml-small.bin -l de -oj -osrt -f mic.wav
```
`-map 0:a:1` ist der zweite Audiostrom der Datei — welcher das ist, hängt davon
ab, wie OBS die Spuren schreibt. Die Zuordnung steht in
`config/config.yaml` unter `clipper.audio` und wird beim ersten Clip mit
`ffprobe` gegengeprüft.
Ausgabe als SRT, Umwandlung in ASS mit Karaoke-Timing
(wortweises Highlight — der Stil, der auf Shorts funktioniert), dann einbrennen: