Files
abdulive/docs/projects/clip-shorts.md
T
ZeasyAlexandClaude Opus 5 d07c18441b 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
2026-09-09 09:56:55 +02:00

194 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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). 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:
```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).