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:
commit
d09e2b945a
98 files changed
+3273
No files matched your search
@@ -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).
|
||||
Reference in new issue
Block a user