Files
ZeasyAlexandClaude Opus 5 5f6301268a Native Windows-Auslieferung statt Container
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
2026-09-09 17:39:26 +02:00

194 lines
8.1 KiB
Markdown
Raw Permalink 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
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).