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:
1 parent
4ceb61f095
commit
d07c18441b
82 files changed
+2775
-838
No files matched your search
+13
-6
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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:
|
||||
|
||||
|
||||
Reference in new issue
Block a user