Struktur, Doku und S0-Toolchain fuer das abdulive-Toolkit

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
This commit is contained in:
ZeasyAlexandClaude Opus 5 committed 2026-09-09 08:56:09 +02:00
commit d09e2b945a
98 files changed
+3273

No files matched your search

+175
View File
@@ -0,0 +1,175 @@
# 1 — Clip-to-Shorts-Pipeline (`apps/clipper`)
**Wirkrichtung:** Reichweite. Der einzige Kanal mit nichtlinearer Reichweite für
einen Kanal dieser Größe.
## Der ehrliche Engpass
Nicht das Zuschneiden ist schwer, sondern **die Auswahl**. Ein automatisch
erzeugter, mittelmäßiger Clip pro Stream bringt nichts; drei gute pro Woche
bringen viel. Die Pipeline ist deshalb so gebaut, dass sie **Kandidaten
produziert und rankt**, nicht dass sie blind veröffentlicht. Abdu sieht eine
Liste fertig gerenderter 9:16-Videos und wählt aus. Das dauert zwei Minuten
statt zwei Stunden — genau das ist der Hebel.
Zweiter ehrlicher Punkt: Bei 10 Zuschauern sind Chat-Spikes ein **schwaches
Signal**. Deshalb ist die Reihenfolge der Trigger:
1. **`!clip` im Chat** — funktioniert ab Tag 1, jeder Zuschauer kann auslösen
2. **Hotkey / Streamdeck** durch Abdu selbst — bestes Signal überhaupt
3. **Chat-Spike** — wird von Anfang an mitgeschrieben und protokolliert, aber
erst scharf geschaltet, wenn genug Material für eine Kalibrierung da ist
## Zwei Quellwege
### Weg A: OBS-Replay-Buffer (Primärweg)
Replay-Buffer in OBS auf 90 s. Trigger → `SaveReplayBuffer` über obs-websocket
→ MP4 in voller Aufnahmequalität landet lokal.
**Vorteile:** Originalqualität statt Twitch-Transcode. Kein API-Delay. Kein
Rate Limit. Und — der eigentliche Gewinn — man kann in OBS eine **zweite
Aufnahmespur oder eine dedizierte Vertical-Szene** einrichten, in der Facecam
und Gameplay bereits sauber getrennt sind. Dann entfällt das fehleranfällige
Zurückschneiden aus dem komposite Bild komplett.
**Nachteil:** Braucht Festplattenplatz und eine korrekte OBS-Konfiguration.
### Weg B: Twitch-Clip über Helix (Zweitweg)
`POST /helix/clips` → warten → `GET /helix/clips?id=` → MP4 über die aus
`thumbnail_url` abgeleitete URL (siehe
[`../02-twitch-integration.md`](../02-twitch-integration.md)).
**Wird trotzdem gebraucht,** weil ein Twitch-Clip eine teilbare URL hat, im
Kanal auffindbar ist und von Zuschauern weiterverbreitet werden kann. Deshalb:
**beide Wege gleichzeitig auslösen.** Twitch-Clip für die Plattform, OBS-Datei
für das Short.
## Layout und ffmpeg
Zielformat: 1080×1920, H.264, höchstens 60 s.
### Eingemessen am 2026-09-09
Aus einem VOD-Screenshot (GTA 5, Szene „Gameplay") ergaben sich diese Werte,
festgehalten in [`config/layout.yaml`](../../config/layout.yaml):
| Element | Lage im Streambild 1920×1080 |
|---|---|
| Facecam | bündig oben links, **440×330**, Seitenverhältnis 4:3 |
| Chat-Overlay | ins Bild eingebrannt, beginnt bei **x = 1270** |
| Minimap | unten links, endet bei x ≈ 360 |
| Handy-/Kontakt-Overlay | rechts unten, ab x ≈ 1540 |
Die Kanten der Facecam sind über die gesamte Höhe bzw. Breite konstant
(rechte Kante x = 439 bei y = 120/200/280/360; untere Kante y = 391 bei
x = 40/140/240/340/420) — ein sauberes Rechteck, also ein fester Crop.
**Zwei Befunde, die das ursprünglich geplante Layout verändert haben:**
**1. Der Chat ist ins Streambild eingebrannt.** Ein zentrierter Gameplay-Crop
hätte mitten durch die Chat-Zeilen geschnitten — abgeschnittene Buchstaben am
rechten Rand. Das nutzbare Band liegt zwischen Facecam (endet bei 440) und Chat
(beginnt bei 1270), also **828 px breit**. Das ist schmaler als die 888 px, die
für ein zentrales Drittel nötig wären, und legt damit die Bandaufteilung fest.
**2. Die Facecam ist 4:3, das Zielband ist deutlich breiter.** Ein 4:3-Bild auf
ein 16:9-Band zu skalieren zieht das Gesicht um ein Drittel in die Breite.
Deshalb wird aus der Cam ein **Streifen mit der Zielproportion** geschnitten
(440×208 ab y = 100), nicht das ganze Bild verzerrt. Der Versatz y = 100 sitzt
so, dass Kopf und Kinn vollständig drin sind und oben etwas Luft bleibt.
Daraus folgt die Aufteilung **512 px Cam + 1408 px Gameplay = 1920**:
```bash
ffmpeg -i in.mp4 -filter_complex "
[0:v]crop=440:208:0:100,scale=1080:512,setsar=1[cam];
[0:v]crop=828:1080:440:0,scale=1080:1408,setsar=1[game];
[cam][game]vstack=inputs=2[v]
" -map "[v]" -map 0:a \
-c:v libx264 -crf 20 -preset veryfast -pix_fmt yuv420p \
-c:a aac -b:a 160k -movflags +faststart out.mp4
```
Nachmessen und Vorschau rendern:
```powershell
docker compose run --rm toolkit calibrate
```
Das legt drei Bilder in `data/clips/_calibration/` ab: das Einzelbild, dasselbe
Bild mit eingezeichneten Ausschnitten, und die fertige 9:16-Vorschau.
Für andere Spiele lässt sich pro Kategorie ein eigenes Profil in
`layout.yaml` hinterlegen.
### Was das Layout verbessern würde
Der Crop skaliert die Facecam von 440 px auf 1080 px Breite — Faktor 2,45. Das
ist sichtbar weich. **Die eigentliche Lösung liegt in OBS, nicht in ffmpeg:**
eine zweite Aufnahmespur oder eine dedizierte Vertical-Szene, in der Cam und
Gameplay in voller Auflösung nebeneinander vorliegen. Dann entfällt sowohl der
Qualitätsverlust als auch der ganze Filtergraph. Das ist eine Aufgabe für die
Zeit nach dem MVP — der Crop-Weg funktioniert vorher gut genug.
Kleinigkeit mit großer Wirkung: **GTA-Untertitel im Spiel ausschalten.** Sie
laufen über die volle Bildbreite und werden vom Gameplay-Crop rechts
abgeschnitten. Die eigenen Whisper-Untertitel liegen ohnehin darüber.
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.
Ausgabe als SRT, Umwandlung in ASS mit Karaoke-Timing
(wortweises Highlight — der Stil, der auf Shorts funktioniert), dann einbrennen:
```bash
ffmpeg -i out.mp4 -vf "ass=subs.ass" -c:a copy -c:v libx264 -crf 20 final.mp4
```
Der Untertitel-Stil (Font, Größe, Position, Highlight-Farbe) liegt in
`config/subtitle-style.ass`. Sicherheitsabstand nach unten mindestens 320 px,
sonst verdeckt die TikTok-UI den Text.
## Ranking der Kandidaten
`score` aus: Spike-Höhe, Anzahl unterschiedlicher Chatter im Fenster,
Emote-Dichte, Lautstärkespitze im Audio (`ebur128`), Trigger-Art
(Hotkey > `!clip` > Spike), Menge erkannter Sprache im Transkript.
## Ausgabe
```
data/clips/<stream-datum>/<clip-id>/
raw.mp4 Quelle (OBS oder Twitch)
vertical.mp4 9:16 ohne Untertitel
final.mp4 9:16 mit eingebrannten Untertiteln <- Upload-Kandidat
subs.srt / subs.ass
transcript.txt
meta.json Score, Trigger, Chat-Ausschnitt, Titelvorschläge
thumb.jpg
```
Plus ein Eintrag in `clips` mit `status='rendered'`. Das Dashboard zeigt die
Kandidaten des letzten Streams, sortiert nach Score, mit Vorschau und den
Knöpfen *Übernehmen* / *Verwerfen*.
## Ausdrücklich nicht in Ausbaustufe 1
Automatischer Upload zu TikTok/YouTube/Reels. Die Upload-APIs sind aufwendig
(die TikTok Content Posting API braucht eine App-Review), und der Nutzen ist
gering, solange die Auswahl ohnehin manuell erfolgt. Ausbaustufe 2.
## Abnahmekriterien
- `!clip` im Chat, innerhalb von 3 Minuten liegt ein fertiges `final.mp4` vor.
- Untertitel sind zeitlich um weniger als 300 ms versetzt und bei normaler
Sprechlautstärke deutsch korrekt.
- Facecam-Crop sitzt bei drei verschiedenen OBS-Szenen richtig.
- Ein Clip läuft durch die Pipeline, während der Stream weiterläuft, ohne dass
die Encoding-Last Frames im Stream kostet (`-preset veryfast`, niedrige
Prozesspriorität, bei Bedarf NVENC).