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
8.1 KiB
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:
!clipim Chat — funktioniert ab Tag 1, jeder Zuschauer kann auslösen- Hotkey / Streamdeck durch Abdu selbst — bestes Signal überhaupt
- 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).
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:
| 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:
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:
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.
# 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:
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
!clipim Chat, innerhalb von 3 Minuten liegt ein fertigesfinal.mp4vor.- 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).