Docker faellt weg. Das Projekt hat keine npm-Abhaengigkeiten und Node fuehrt TypeScript direkt aus - der Container schrieb nur noch drei Programme fest, und dafuer braucht es keine Virtualisierung, sondern einen Ordner. Der Anlass war, dass ADR-0004 zwar `docker compose pull` versprach, es aber nie ein veroeffentlichtes Image gab. Wer installieren wollte, musste whisper.cpp aus dem Quelltext uebersetzen. Plattformschicht (packages/core/src/platform.ts): - Programme werden neben der Anwendung gesucht, nicht im PATH - Daten unter %LOCALAPPDATA%, damit Installation und Update ohne Administratorrechte auskommen - Schriften mitgeliefert: ohne fontsdir setzt libass wortlos eine Ersatzschrift ein - gemessen 720 statt 826 Pixel Zeilenbreite Windows-Eigenheiten, an echtem ffmpeg 8.1 durchprobiert: - Pfade im Filtergraphen brauchen alle drei Schritte (Vorwaertsschraegstriche, escapter Doppelpunkt, Anfuehrungszeichen); je zwei davon reichen nicht - `nice` gibt es nicht, ersetzt durch os.setPriority Auslieferung: - scripts/build-package.ts baut das Paket, laeuft auch unter Linux - install.ps1 ohne Administratorrechte, Autostart ueber den Aufgabenplaner - update legt neue Fassungen bereit; eingespielt wird beim naechsten Start, nie mitten im Stream - ZIP lesen und schreiben ohne Abhaengigkeiten (packages/core/src/zip.ts) Kein `irm ... | iex`-Einzeiler: git.zeasy.dev antwortete instanzweit mit einer Umleitung auf /user/login, und `iex` fuehrte die Anmeldeseite als PowerShell aus. Der Updater verfolgt Umleitungen jetzt nicht und hat keine erfundene Vorgabeadresse mehr. Ausserdem: backup.ps1 war an zwei Stellen dokumentiert, aber nie geschrieben. ADR-0007 ersetzt ADR-0004. 84 Tests, Typpruefung sauber. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhVSfMFdWnXKCv5JfLnArZ
194 lines
8.1 KiB
Markdown
194 lines
8.1 KiB
Markdown
# 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
|
||
abdulive 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).
|