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,63 @@
|
||||
# Ingest-Daemon (`apps/ingest`)
|
||||
|
||||
> Kein eigenes "Tool" im Sinne der Wunschliste, sondern das Fundament, auf dem
|
||||
> sechs der neun Tools stehen. Wird zuerst gebaut.
|
||||
|
||||
## Aufgabe
|
||||
|
||||
Ein Prozess, der dauerhaft läuft und alle Datenquellen in einen einheitlichen
|
||||
Event-Strom übersetzt: schreibt in SQLite, publiziert auf dem Bus.
|
||||
|
||||
## Quellen
|
||||
|
||||
| Quelle | Verbindung | liefert |
|
||||
|---|---|---|
|
||||
| Twitch IRC | `wss://irc-ws.chat.twitch.tv:443` | Chat-Nachrichten mit Tags |
|
||||
| Twitch EventSub | `wss://eventsub.wss.twitch.tv/ws` | Follows, Subs, Raids, Kategorie, Online/Offline |
|
||||
| Twitch Helix | REST, Polling 60 s | Viewerzahl, Chatter-Liste (5 min) |
|
||||
| obs-websocket | `ws://127.0.0.1:4455` | Szenenwechsel, Replay-Buffer-Events |
|
||||
|
||||
## Was der Ingest selbst berechnet
|
||||
|
||||
**Stream-Sessions.** `stream.online` legt eine `streams`-Zeile an,
|
||||
`stream.offline` schließt sie. Wenn der Daemon mitten im Stream startet, wird
|
||||
die laufende Session über `GET /helix/streams` rekonstruiert. Kurze
|
||||
Unterbrechungen (< 5 min) werden als *dieselbe* Session behandelt — sonst
|
||||
zerfallen Streams bei jedem Verbindungsabbruch in Fragmente.
|
||||
|
||||
**Chat-Spikes.** Gleitendes Fenster über die Nachrichtenrate:
|
||||
|
||||
```
|
||||
rate(t) = Nachrichten in den letzten 15 s
|
||||
baseline(t) = Median von rate über die letzten 10 min
|
||||
spike = rate >= max(baseline * 3, baseline + 4)
|
||||
&& Zeit seit letztem Spike > 60 s
|
||||
```
|
||||
|
||||
Zusätzlich gewichtet: Lach-Emotes (`KEKW`, `LUL`, `OMEGALUL`, Kanal-Emotes),
|
||||
Ausrufe (`?!`, Großbuchstaben-Anteil), das Wort `clip`. Ergebnis ist ein
|
||||
`chat.spike`-Event mit `score`. Wer darauf reagiert, entscheidet der Konsument.
|
||||
|
||||
> Bei 10 Zuschauern ist die Baseline klein und die Statistik dünn. Der Detektor
|
||||
> ist deshalb bewusst konservativ und protokolliert *alle* Kandidaten mit Score,
|
||||
> auch die unter der Schwelle — so lässt sich die Schwelle nachträglich am
|
||||
> echten Material kalibrieren, statt sie zu raten.
|
||||
|
||||
**Anwesenheit.** Aus `GET /helix/chat/chatters` (alle 5 min) plus jeder
|
||||
Nachricht wird `user_streams` fortgeschrieben: Ersterscheinung, letzte
|
||||
Aktivität, Minuten. Daraus fällt `streams_present` für jeden User ab.
|
||||
|
||||
## Schnittstellen nach außen
|
||||
|
||||
- **WS `ws://127.0.0.1:4790`** — Broadcast aller Events als JSON-Zeilen.
|
||||
Ein Client kann beim Connect `{"since":"<iso-ts>"}` senden und bekommt die
|
||||
verpassten Events aus der DB nachgeliefert.
|
||||
- **HTTP `GET /health`** — Status aller vier Verbindungen, für das Dashboard.
|
||||
|
||||
## Abnahmekriterien
|
||||
|
||||
- Läuft 7 Tage ohne Neustart durch, inklusive Twitch-Reconnects.
|
||||
- Nach einem erzwungenen Prozess-Kill mitten im Stream: Neustart rekonstruiert
|
||||
die laufende Session, keine doppelten `streams`-Zeilen, keine doppelten Events.
|
||||
- Jede Chat-Nachricht eines Test-Streams ist in `chat_messages` (Abgleich gegen
|
||||
ein parallel mitgeschriebenes Log).
|
||||
@@ -0,0 +1,17 @@
|
||||
# Spezifikationen
|
||||
|
||||
Ein Dokument pro Tool. Aufbau jeweils: Wirkrichtung, Ablauf, technische
|
||||
Entscheidungen, Fallstricke, Abnahmekriterien.
|
||||
|
||||
| # | Dokument | App |
|
||||
|---|---|---|
|
||||
| — | [Ingest-Daemon](00-ingest.md) | `apps/ingest` |
|
||||
| 1 | [Clip-to-Shorts](clip-shorts.md) | `apps/clipper` |
|
||||
| 2 | [Go-Live-Broadcaster](go-live-broadcaster.md) | `apps/broadcaster` |
|
||||
| 3 | [Raid- und Netzwerk-Tool](raid-network.md) | `apps/raid-scout` |
|
||||
| 4 | [Analytics-Dashboard](analytics.md) | `apps/dashboard` |
|
||||
| 5 | [VOD-Kapitel](vod-chapters.md) | `apps/vod-chapters` |
|
||||
| 6 | [Community-Gedaechtnis](community-memory.md) | `apps/community` |
|
||||
| 7 | [Chat steuert Dinge](chat-control.md) | `apps/overlay-server`, `apps/overlay-web` |
|
||||
| 8 | [Punktesystem und Shop](loyalty-shop.md) | `apps/loyalty` |
|
||||
| 9 | [Stream-Recap](stream-recap.md) | `apps/recap` |
|
||||
@@ -0,0 +1,68 @@
|
||||
# 4 — Analytics-Dashboard (`apps/dashboard`)
|
||||
|
||||
**Wirkrichtung:** Steuerung. Beantwortet die Frage "was funktioniert
|
||||
eigentlich", die Twitch selbst nicht beantwortet.
|
||||
|
||||
Läuft auf `http://localhost:4792`. Vite + Preact + uPlot, liest direkt aus
|
||||
SQLite. Keine Authentifizierung, weil nur lokal erreichbar.
|
||||
|
||||
## Seiten
|
||||
|
||||
### Stream-Zeitachse (die wichtigste)
|
||||
|
||||
Eine gemeinsame X-Achse (Stream-Laufzeit), darauf übereinander:
|
||||
|
||||
- Viewerzahl (Linie, aus `viewer_samples`)
|
||||
- Chat-Nachrichten pro Minute (Balken)
|
||||
- unterschiedliche Chatter pro 5 min (Linie)
|
||||
- Marker: Kategoriewechsel, Titelwechsel, Raids rein/raus, Clips, Follows, Subs
|
||||
|
||||
Damit sieht man direkt: An welcher Stelle kamen Leute rein, an welcher gingen
|
||||
sie, und war der Chat da tot oder wach. **Zusammenhang von Viewerzahl und
|
||||
Chat-Aktivität ist die Kernfrage** — 15 Zuschauer mit stillem Chat sind
|
||||
weniger wert als 8 mit lebendigem.
|
||||
|
||||
### Abgangs-Analyse
|
||||
|
||||
Aus `viewer_samples`: Ableitung über die Zeit, die stärksten Abfälle markiert
|
||||
und mit dem gleichzeitigen `stream_segments`-Eintrag und den Chat-Nachrichten
|
||||
der Minute davor angereichert. Antwort auf "wann gehen die Leute".
|
||||
|
||||
Wichtige Einschränkung, die im UI stehen muss: `viewer_samples` ist eine
|
||||
Momentaufnahme der Gesamtzahl, keine Ein-/Austrittsliste. Twitch liefert das
|
||||
nicht. Ein Abfall von 12 auf 8 kann vier Abgänge oder sechs Abgänge plus zwei
|
||||
Zugänge bedeuten. Für Chatter ist es exakt (über `user_streams`), für stille
|
||||
Zuschauer geschätzt.
|
||||
|
||||
### Titel- und Kategorie-Performance
|
||||
|
||||
Aggregation über `stream_segments`: pro Titel/Kategorie die durchschnittliche
|
||||
Viewerzahl, der Nettozufluss während des Segments, Chat-Rate, gewonnene
|
||||
Follower pro Stunde. Sortierbar. Mit Hinweis auf die Stichprobengröße —
|
||||
bei drei Streams pro Titel ist jede Zahl Rauschen, und das Dashboard sagt das,
|
||||
statt eine Rangliste vorzutäuschen.
|
||||
|
||||
### Chatter: neu vs. wiederkehrend
|
||||
|
||||
Pro Stream ein Stapelbalken: Erstchatter / wiederkehrend (1–4 Streams) /
|
||||
Stammgast (5+). Dazu eine Kohorten-Tabelle: Von den Erstchattern aus Stream N,
|
||||
wie viele waren in N+1, N+2, N+3 wieder da. Das ist die Zahl, die tatsächlich
|
||||
über Wachstum entscheidet.
|
||||
|
||||
### Betriebsstatus
|
||||
|
||||
`GET /health` vom Ingest, letzte Fehler aus `logs/`, Größe der DB,
|
||||
Clip-Warteschlange.
|
||||
|
||||
## Clip-Freigabe
|
||||
|
||||
Zusätzlich hostet das Dashboard die Freigabe-Ansicht für die Clip-Pipeline
|
||||
(siehe [`clip-shorts.md`](clip-shorts.md)): Kandidatenliste mit
|
||||
Videovorschau, Score, Chat-Ausschnitt und den Knöpfen *Übernehmen* /
|
||||
*Verwerfen*.
|
||||
|
||||
## Abnahmekriterien
|
||||
|
||||
- Die Zeitachse eines abgeschlossenen Streams lädt in unter 2 s.
|
||||
- Alle Marker liegen zeitlich korrekt (Abgleich gegen das VOD).
|
||||
- Die Kohorten-Tabelle stimmt mit einer manuellen SQL-Auszählung überein.
|
||||
@@ -0,0 +1,95 @@
|
||||
# 7 — Chat steuert Dinge (`apps/overlay-server` + `apps/overlay-web`)
|
||||
|
||||
**Wirkrichtung:** Bindung. Aus Zuschauen wird Mitmachen.
|
||||
|
||||
## Aufbau
|
||||
|
||||
```
|
||||
Chat-Command ──► overlay-server (4791) ──► WS ──► Browser-Source in OBS
|
||||
│
|
||||
├──► obs-websocket (Szenen, Filter, Quellen, Hotkeys)
|
||||
├──► Home Assistant / WLED (Licht)
|
||||
└──► Helix (Umfragen, Titel)
|
||||
```
|
||||
|
||||
`overlay-server` ist ein Fastify-Server, der drei Dinge tut: Overlay-Seiten
|
||||
ausliefern, den WS-Kanal zu den Browser-Sources halten, und Commands vom
|
||||
Event-Bus in Aktionen übersetzen.
|
||||
|
||||
## Overlay-Seiten (Browser-Sources in OBS)
|
||||
|
||||
| Pfad | Zweck | Größe |
|
||||
|---|---|---|
|
||||
| `/overlay/alerts` | Follow/Sub/Raid-Einblendungen | 1920x1080 |
|
||||
| `/overlay/effects` | Emote-Regen, Soundboard-Feedback, Konfetti | 1920x1080 |
|
||||
| `/overlay/community` | Chatter-Hinweis (nur für Abdu) | 480x220 |
|
||||
| `/overlay/poll` | laufende Umfrage | 480x600 |
|
||||
| `/overlay/game` | Minigame-Anzeige | 1920x1080 |
|
||||
| `/overlay/raid-panel` | Raid-Ziele am Streamende | 900x600 |
|
||||
|
||||
Alle Seiten teilen sich einen WS-Client (`packages/shared/overlay-client.ts`)
|
||||
mit automatischem Reconnect — eine Browser-Source, die nach einem Serverneustart
|
||||
tot bleibt, ist der häufigste Fehler bei solchen Overlays.
|
||||
|
||||
## Command-Registry
|
||||
|
||||
Commands liegen deklarativ in `config/commands.yaml`:
|
||||
|
||||
```yaml
|
||||
- trigger: "!sound"
|
||||
args: [name]
|
||||
cooldown: { global: 20, user: 120 }
|
||||
permission: everyone
|
||||
cost: 50 # Punkte, optional
|
||||
action: { type: sound, file: "sounds/{name}.mp3" }
|
||||
|
||||
- trigger: "!licht"
|
||||
args: [farbe]
|
||||
cooldown: { global: 60 }
|
||||
permission: subscriber
|
||||
action: { type: wled, preset: "{farbe}" }
|
||||
```
|
||||
|
||||
Aktionstypen: `sound`, `overlay-effect`, `obs-hotkey`, `obs-source-toggle`,
|
||||
`obs-filter`, `wled`, `helix-poll`, `set-title`, `script`.
|
||||
|
||||
**Cooldowns und Rechte sind nicht optional.** Ein Soundboard ohne Cooldown ist
|
||||
nach zwei Streams abgeschaltet. Default: 20 s global, 120 s pro User,
|
||||
Sounds nur für Follower.
|
||||
|
||||
## Minispiele
|
||||
|
||||
Ausbaustufe 2. Kandidaten, die bei kleinem Chat funktionieren (wichtig: sie
|
||||
dürfen mit 3 Mitspielern genauso gut sein wie mit 30):
|
||||
|
||||
- **Tipprunde** — `!tipp <zahl>`, Abdu löst auf, Punkte für die Nächsten
|
||||
- **Kollektive Entscheidung** — Helix-Umfrage mit echter Konsequenz im Spiel
|
||||
- **Boss-Bingo** — Karte im Overlay, Chat ruft Felder aus
|
||||
|
||||
## Umfragen mit Konsequenz
|
||||
|
||||
`!umfrage` startet eine Helix-Poll (`POST /helix/polls`). Bei `poll.end` führt
|
||||
der Server die hinterlegte Aktion zur Gewinneroption aus — Szenenwechsel,
|
||||
Schwierigkeitsgrad, nächstes Spiel. Der Unterschied zu einer normalen Umfrage
|
||||
ist, dass das Ergebnis automatisch etwas tut, statt nur angezeigt zu werden.
|
||||
|
||||
## Sicherheit
|
||||
|
||||
Alles, was den Stream sichtbar verändert oder Geräte im Haus schaltet, ist
|
||||
missbrauchbar. Deshalb:
|
||||
|
||||
- `overlay-server` bindet nur auf `127.0.0.1`.
|
||||
- Der Panik-Command `!aus` (Mods) schaltet alle Effekte für 10 min ab.
|
||||
- Aktionen mit `requires_approval: true` erscheinen erst in einer Warteschlange
|
||||
im Overlay-Panel und laufen nach Abdus Klick.
|
||||
- Kein Command darf beliebigen Text auf dem Bildschirm anzeigen. TTS und freie
|
||||
Textanzeigen sind bewusst nicht Teil von Ausbaustufe 1.
|
||||
|
||||
## Abnahmekriterien
|
||||
|
||||
- `!sound huch` spielt innerhalb von 300 ms den Sound ab.
|
||||
- Cooldown greift und wird im Chat sichtbar quittiert (einmal, nicht bei jedem
|
||||
Versuch).
|
||||
- OBS-Neustart oder Server-Neustart: Browser-Source verbindet sich innerhalb von
|
||||
5 s automatisch neu.
|
||||
- `!aus` stoppt sofort alle laufenden Effekte.
|
||||
@@ -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).
|
||||
@@ -0,0 +1,76 @@
|
||||
# 6 — Community-Gedächtnis (`apps/community`)
|
||||
|
||||
**Wirkrichtung:** Bindung. Bei 10 Zuschauern der Unterschied zwischen
|
||||
"Zuschauer" und "Community".
|
||||
|
||||
Läuft als Modul im `overlay-server`, nicht als eigener Prozess — es braucht
|
||||
dieselbe Chat-Verbindung und denselben WS-Kanal zum Overlay.
|
||||
|
||||
## Datenlage
|
||||
|
||||
Alles Nötige liegt bereits in `users` und `user_streams` (siehe
|
||||
[`../03-data-model.md`](../03-data-model.md)). Dieses Tool ist überwiegend
|
||||
**Auswertung und Ausgabe**, kaum neue Datenerfassung — deshalb ist es billig,
|
||||
sobald der Ingest steht.
|
||||
|
||||
## Die drei Ausgaben
|
||||
|
||||
### 1. Chat-Begrüßung
|
||||
|
||||
Ausgelöst durch die erste Nachricht eines Users im laufenden Stream:
|
||||
|
||||
| Fall | Bedingung | Beispiel |
|
||||
|---|---|---|
|
||||
| **Erstchatter** | `first-msg`-Tag oder `users.message_count = 0` | "Oh, @name schreibt zum ersten Mal — willkommen!" |
|
||||
| **Rückkehrer** | letzter Besuch > 21 Tage her | "@name, lange nicht gesehen." |
|
||||
| **Meilenstein** | `streams_chatted` in {5, 10, 25, 50, 100} | "Stream Nummer 25 für @name." |
|
||||
| **Stammgast** | `streams_chatted >= 5` | individuelle Begrüßung aus `users.greeting` |
|
||||
| sonst | — | nichts |
|
||||
|
||||
Regeln, damit es nicht nervt: höchstens eine Begrüßung alle 20 s, höchstens
|
||||
zehn pro Stream, `muted_greeting` respektiert, und **keine Begrüßung, wenn Abdu
|
||||
in den letzten 15 s selbst gesprochen hat** (der Ingest weiß über OBS bzw. über
|
||||
eine Audiopegel-Schwelle, ob gerade geredet wird) — sonst redet der Bot dem
|
||||
Streamer ins Wort.
|
||||
|
||||
### 2. Overlay-Hinweis für Abdu
|
||||
|
||||
Kleines, nur für Abdu sichtbares Panel (eigene Browser-Source in der
|
||||
Studio-Ansicht oder zweiter Monitor). Wenn jemand schreibt:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ maxi_2k Stream 15 · Sub 3M │
|
||||
│ zuletzt: vor 2 Tagen │
|
||||
│ Notiz: studiert Maschinenbau, │
|
||||
│ spielt selbst Elden Ring │
|
||||
│ erste Nachricht: 04.03.2026 │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Das ist der eigentliche Wert.** Nicht der Bot, der etwas schreibt, sondern
|
||||
Abdu, der weiß, mit wem er redet. Ein Bot, der "willkommen zurück" sagt, ist
|
||||
Deko; ein Streamer, der "na, wie war die Klausur" sagt, bindet Menschen.
|
||||
|
||||
### 3. Notizen
|
||||
|
||||
Chat-Command für Abdu und Mods: `!notiz <name> <text>` hängt an `users.notes` an.
|
||||
Im Dashboard editierbar. Das ist die einzige Datenquelle, die Handarbeit
|
||||
braucht, und die mit dem höchsten Nutzen pro Aufwand.
|
||||
|
||||
## Datenschutz
|
||||
|
||||
Die DB enthält personenbezogene Daten (Chatverläufe, Notizen). Sie liegt lokal,
|
||||
wird nicht geteilt und nicht in ein Repo committet (`data/` ist in `.gitignore`).
|
||||
`!vergissmich` löscht auf Wunsch alle Daten eines Users bis auf die anonyme
|
||||
Aggregatstatistik. Notizen enthalten nichts, was man der Person nicht ins
|
||||
Gesicht sagen würde.
|
||||
|
||||
## Abnahmekriterien
|
||||
|
||||
- Ein Testkonto, das zum ersten Mal schreibt, wird als Erstchatter erkannt und
|
||||
genau einmal begrüßt.
|
||||
- Nach einem Namenswechsel des Testkontos bleibt die Historie erhalten
|
||||
(Verknüpfung über `user_id`, nicht `login`).
|
||||
- Das Overlay-Panel zeigt den Hinweis innerhalb von 500 ms nach der Nachricht.
|
||||
- Zwanzig Nachrichten in fünf Sekunden erzeugen höchstens eine Begrüßung.
|
||||
@@ -0,0 +1,61 @@
|
||||
# 2 — Go-Live-Broadcaster (`apps/broadcaster`)
|
||||
|
||||
**Wirkrichtung:** Reichweite. Billigster Hebel im ganzen Repo — Leute, die schon
|
||||
folgen, aber die Twitch-Benachrichtigung nicht sehen.
|
||||
|
||||
## Ablauf
|
||||
|
||||
```
|
||||
stream.online
|
||||
-> 60 s warten (Titel/Kategorie/Thumbnail sind erst dann gesetzt)
|
||||
-> GET /helix/channels (Titel, Kategorie)
|
||||
-> OBS GetSourceScreenshot (Programm-Ausgabe, 1280x720)
|
||||
-> Text aus Vorlage rendern
|
||||
-> pro Plattform: Entwurf anlegen oder direkt senden
|
||||
```
|
||||
|
||||
## Plattformen
|
||||
|
||||
| Plattform | Weg | Aufwand | Auto-Post |
|
||||
|---|---|---|---|
|
||||
| **Discord** | Webhook, Embed mit Bild + `@everyone`/Rolle | trivial | ja |
|
||||
| **Bluesky** | AT-Protocol, `com.atproto.repo.createRecord` mit Blob-Upload | klein | ja |
|
||||
| **Mastodon** | `POST /api/v1/statuses` mit Media | klein | ja |
|
||||
| **X/Twitter** | API v2, kostenpflichtig ab Basic-Tier | mittel | optional |
|
||||
| **Instagram Story** | **kein offizieller Weg** — siehe unten | groß | nein |
|
||||
|
||||
### Instagram: die Wahrheit
|
||||
|
||||
Die Instagram Graph API kann Stories nur für **Business-/Creator-Konten**
|
||||
veröffentlichen, die mit einer Facebook-Seite verknüpft sind, und braucht eine
|
||||
App-Review mit `instagram_content_publish`. Für einen kleinen Kanal ist das
|
||||
unverhältnismäßig, und inoffizielle Wege riskieren die Kontosperre.
|
||||
|
||||
**Lösung in Ausbaustufe 1:** Der Broadcaster erzeugt ein fertiges
|
||||
**1080x1920-Story-Bild** (Screenshot + Titel + Kategorie + "LIVE"-Badge) und
|
||||
legt es in `data/posts/<datum>/story.png` ab, plus eine Push-Nachricht an Abdus
|
||||
Handy (Discord-DM oder ntfy.sh). Hochladen dauert dann fünf Sekunden manuell.
|
||||
Das ist 90 % des Nutzens für 5 % des Aufwands.
|
||||
|
||||
## Sicherung gegen Fehlposts
|
||||
|
||||
- **Ein Post pro Stream.** `posts`-Tabelle wird auf `stream_id` geprüft.
|
||||
- **Cooldown 6 h.** Wenn Twitch nach einem Verbindungsabbruch ein zweites
|
||||
`stream.online` schickt, wird nicht erneut gepostet.
|
||||
- **Kill-Switch** in `config/config.yaml`: `broadcaster.enabled: false`.
|
||||
- Beim ersten Einrichten läuft alles im Modus `draft` — Posts landen nur in der
|
||||
DB und in einem privaten Discord-Kanal zur Kontrolle.
|
||||
|
||||
## Vorlagen
|
||||
|
||||
`config/templates/golive.*.md`, mit Platzhaltern `{{title}}`, `{{category}}`,
|
||||
`{{url}}`, `{{weekday}}`. Mehrere Varianten pro Plattform, zufällig gewählt —
|
||||
identische Posts jede Woche werden von Algorithmen und Menschen gleichermaßen
|
||||
ignoriert.
|
||||
|
||||
## Abnahmekriterien
|
||||
|
||||
- Stream-Start erzeugt innerhalb von 90 s einen Discord-Post mit korrektem
|
||||
Titel, Kategorie und aktuellem Screenshot.
|
||||
- Ein simulierter doppelter `stream.online` erzeugt keinen zweiten Post.
|
||||
- Story-Bild ist 1080x1920, Text ist auf dem Handy lesbar.
|
||||
@@ -0,0 +1,71 @@
|
||||
# 8 — Punktesystem mit Bedeutung (`apps/loyalty`)
|
||||
|
||||
**Wirkrichtung:** Bindung. Läuft als Modul im `overlay-server`.
|
||||
|
||||
## Warum nicht Channel Points?
|
||||
|
||||
Twitch-Channel-Points funktionieren, haben aber zwei Grenzen: Man kann die
|
||||
Vergabelogik nicht anpassen (keine Boni für Verhalten, das man belohnen will),
|
||||
und Einlösungen sind an das Twitch-UI gebunden. Eigene Punkte laufen daneben.
|
||||
|
||||
**Beides parallel betreiben.** Channel Points bleiben für simple Sachen,
|
||||
eigene Punkte für alles, was Logik braucht. `channel.channel_points_custom_reward_redemption.add`
|
||||
wird ohnehin schon ingested, also können beide Systeme dieselben Aktionen
|
||||
auslösen.
|
||||
|
||||
## Vergabe
|
||||
|
||||
| Anlass | Punkte |
|
||||
|---|---|
|
||||
| pro 10 Minuten Anwesenheit | 10 |
|
||||
| ...davon aktiv im Chat | +5 |
|
||||
| erste Nachricht des Streams | 25 |
|
||||
| Anwesenheit in Streamminute 1–10 | +50 (belohnt pünktliches Erscheinen) |
|
||||
| Follow | 200 (einmalig) |
|
||||
| Sub / Resub | 500 |
|
||||
| Raid rein | 300 an den Raider |
|
||||
| Clip, der übernommen wird | 250 an den Auslöser |
|
||||
|
||||
Anwesenheit kommt aus `user_streams.minutes` (Helix-Chatter-Liste alle 5 min),
|
||||
nicht aus dem Chat allein — sonst bekommen stille Zuschauer nie etwas.
|
||||
|
||||
## Shop
|
||||
|
||||
Der Punkt der Sache ist, dass Einlösungen **wirklich etwas tun**:
|
||||
|
||||
| Artikel | Kosten | Aktion | Freigabe |
|
||||
|---|---|---|---|
|
||||
| Streamtitel für 10 min | 5.000 | `PATCH /helix/channels`, Timer setzt zurück | ja |
|
||||
| Emote-Regen 30 s | 800 | Overlay-Effekt | nein |
|
||||
| Sound aus dem Board | 300 | Overlay + Audio | nein |
|
||||
| Challenge für Abdu | 3.000 | Karte im Overlay, Abdu bestätigt | ja |
|
||||
| Nächstes Spiel bestimmen | 10.000 | Eintrag in die Warteschlange | ja |
|
||||
| Facecam-Filter 60 s | 1.500 | obs-websocket Filter an | nein |
|
||||
|
||||
`requires_approval` ist bei allem gesetzt, was den Stream nach außen verändert
|
||||
(Titel!) oder Abdu zu etwas verpflichtet. Titeländerung zusätzlich abgesichert:
|
||||
Wortfilter, Längenbegrenzung, automatische Rückkehr zum Originaltitel per
|
||||
Timer, und der Originaltitel wird vorher in `stream_segments` gesichert.
|
||||
|
||||
## Rückerstattung
|
||||
|
||||
Wenn eine Aktion fehlschlägt (OBS nicht erreichbar, Helix-Fehler, Abdu lehnt
|
||||
ab), werden die Punkte automatisch zurückgebucht und der User im Chat
|
||||
informiert. `redemptions.status = 'refunded'`.
|
||||
|
||||
## Bilanz-Integrität
|
||||
|
||||
Punkte werden nie direkt auf `users.points` geschrieben, sondern als Zeilen in
|
||||
einer `points_ledger`-Tabelle geführt; `users.points` ist ein
|
||||
Materialized-Summenfeld, das über einen Trigger aktualisiert wird. So ist jede
|
||||
Punktzahl nachvollziehbar und ein Fehler in der Vergabelogik reparierbar, ohne
|
||||
dass jemandem grundlos Punkte fehlen.
|
||||
|
||||
## Abnahmekriterien
|
||||
|
||||
- Ein Zuschauer, der 30 min anwesend ist und dreimal schreibt, hat exakt die
|
||||
erwartete Punktzahl.
|
||||
- Titel-Einlösung ändert den Titel und setzt ihn nach 10 min zurück, auch wenn
|
||||
der Server dazwischen neu startet (Timer wird persistiert).
|
||||
- Fehlgeschlagene Einlösung bucht zurück.
|
||||
- `points_ledger`-Summe stimmt mit `users.points` überein.
|
||||
@@ -0,0 +1,64 @@
|
||||
# 3 — Raid- und Netzwerk-Tool (`apps/raid-scout`)
|
||||
|
||||
**Wirkrichtung:** Reichweite über Menschen statt über Algorithmen. In dieser
|
||||
Kanalgröße die zuverlässigste Quelle für neue Stammzuschauer.
|
||||
|
||||
## Kandidaten finden
|
||||
|
||||
`GET /helix/streams?game_id=<aktuelle Kategorie>&first=100&language=de`,
|
||||
seitenweise bis die Viewerzahl unter die Untergrenze fällt (Twitch sortiert
|
||||
absteigend, also von hinten interessant). Filter:
|
||||
|
||||
- Viewer zwischen `min` und `max` (Default 5–30, konfigurierbar)
|
||||
- Sprache `de`
|
||||
- Uptime mindestens 30 min und höchstens 5 h — wer gerade erst angefangen hat,
|
||||
verliert durch einen Raid nichts, wer gleich aufhört, kann nichts damit
|
||||
anfangen
|
||||
- nicht auf der Sperrliste (`config/raid-blocklist.yaml`)
|
||||
|
||||
## Bewertung
|
||||
|
||||
```
|
||||
score = 0.35 * viewerNaehe (Ziel: aehnliche Groesse wie wir)
|
||||
+ 0.25 * kategorieUeberlapp (gleiche Kategorie > gleiche Sparte)
|
||||
+ 0.20 * beziehung (schon mal Kontakt gehabt? zurueckgeraidet?)
|
||||
+ 0.10 * frische (lange nicht mehr geraidet)
|
||||
+ 0.10 * uptimeFenster
|
||||
```
|
||||
|
||||
`beziehung` kommt aus der `raids`-Tabelle: Wer uns geraidet hat und noch keinen
|
||||
Raid zurückbekommen hat, steht ganz oben. **Das ist der eigentliche Kern des
|
||||
Tools** — nicht das Finden von Fremden, sondern das Nicht-Vergessen von
|
||||
Leuten, die schon Kontakt aufgenommen haben.
|
||||
|
||||
## Oberfläche
|
||||
|
||||
- **Chat-Command `!raidziel`** (nur Mods/Broadcaster): Bot postet die Top 3 mit
|
||||
Login, Viewerzahl und Kategorie in den Chat.
|
||||
- **Overlay-Panel**, das Abdu am Streamende einblenden kann: Top 5 als Karten,
|
||||
Klick kopiert `/raid <login>` in die Zwischenablage. Bewusst **kein
|
||||
automatisches Auslösen** — ein Raid ist eine soziale Geste, die einen
|
||||
Menschen braucht.
|
||||
- **Dashboard-Seite** mit der vollständigen Raid-Historie.
|
||||
|
||||
## Netzwerk-Gedächtnis
|
||||
|
||||
Für jeden Kanal, mit dem es je Kontakt gab:
|
||||
|
||||
| Feld | Quelle |
|
||||
|---|---|
|
||||
| Raids rein / raus, mit Datum und Viewerzahl | `channel.raid` EventSub |
|
||||
| erwidert ja/nein | berechnet |
|
||||
| Notizen | manuell im Dashboard |
|
||||
| letzter Kontakt | berechnet |
|
||||
|
||||
Wöchentliche Erinnerung im Dashboard: "Diese 3 haben dich geraidet, du sie noch
|
||||
nicht."
|
||||
|
||||
## Abnahmekriterien
|
||||
|
||||
- `!raidziel` liefert im laufenden Stream innerhalb von 3 s drei plausible
|
||||
Kanäle in der aktuellen Kategorie.
|
||||
- Ein eingehender Raid landet automatisch in `raids` mit `direction='in'`.
|
||||
- Ein ausgehender Raid (manuell in Twitch ausgelöst) wird ebenfalls erfasst.
|
||||
- Kanäle auf der Sperrliste tauchen nie auf.
|
||||
@@ -0,0 +1,67 @@
|
||||
# 9 — Stream-Recap (`apps/recap`)
|
||||
|
||||
**Wirkrichtung:** Bindung. Gibt Leuten einen Grund, im Discord zu sein, und
|
||||
hält den Kanal zwischen zwei Streams präsent.
|
||||
|
||||
## Ablauf
|
||||
|
||||
`stream.offline` → 10 Minuten warten (VOD-Verarbeitung, letzte Clips rendern)
|
||||
→ Zahlen aus der DB ziehen → Discord-Post → optional Bluesky-Kurzfassung.
|
||||
|
||||
## Inhalt
|
||||
|
||||
```
|
||||
🔴 Stream vom 09.09. · 3 h 42 min · Elden Ring, Just Chatting
|
||||
|
||||
Peak 17 Zuschauer · Schnitt 11
|
||||
23 Leute im Chat, davon 4 zum ersten Mal
|
||||
+6 Follower
|
||||
|
||||
Top-Chatter
|
||||
1. maxi_2k 84 Nachrichten
|
||||
2. lenaaa 61
|
||||
3. der_olli 47
|
||||
|
||||
Willkommen im Chat 👋
|
||||
@neuer1 @neuer2 @neuer3 @neuer4
|
||||
|
||||
Beste Momente
|
||||
▸ "Malenia Versuch 41" clips.twitch.tv/...
|
||||
▸ "Der Hund im Hintergrund" clips.twitch.tv/...
|
||||
|
||||
VOD-Kapitel
|
||||
00:00 Start
|
||||
00:14 Elden Ring
|
||||
01:32 Pause
|
||||
...
|
||||
|
||||
Nächster Stream: Mittwoch 20 Uhr
|
||||
```
|
||||
|
||||
## Regeln, damit es gelesen wird
|
||||
|
||||
- **Namen vor Zahlen.** Der Teil, der Leute in den Discord bringt, ist die
|
||||
Liste mit ihren Namen darin — nicht die Peak-Viewer-Zahl. Erstchatter werden
|
||||
namentlich begrüßt, Top-Chatter genannt.
|
||||
- **Keine schlechten Streams schönreden, aber auch nicht ausstellen.** Wenn Peak
|
||||
unter der Hälfte des Schnitts der letzten fünf Streams liegt, entfallen die
|
||||
Viewer-Zahlen und der Post konzentriert sich auf Chat und Momente. Öffentlich
|
||||
schwache Zahlen zu posten schadet mehr, als es nützt; im Dashboard stehen sie
|
||||
trotzdem.
|
||||
- **Kein Post ohne Inhalt.** Streams unter 20 Minuten oder ohne Chat-Aktivität
|
||||
erzeugen keinen Recap.
|
||||
- **Clips einbetten, nicht verlinken.** Discord bettet Twitch-Clips ab; die
|
||||
besten zwei nach `clips.score`.
|
||||
|
||||
## Vergleich zur Vorwoche
|
||||
|
||||
Zweite Zeile, klein: "Chat +18 % ggü. letzten 4 Streams". Nur bei mindestens
|
||||
vier Vergleichsstreams, sonst weggelassen — Prozentangaben auf Basis von zwei
|
||||
Datenpunkten sind Unsinn.
|
||||
|
||||
## Abnahmekriterien
|
||||
|
||||
- Der Post erscheint innerhalb von 15 min nach Streamende.
|
||||
- Alle Zahlen stimmen mit dem Dashboard überein.
|
||||
- Ein Stream ohne Chat erzeugt keinen Post.
|
||||
- Clip-Einbettungen funktionieren in Discord (nicht nur als nackte URL).
|
||||
@@ -0,0 +1,54 @@
|
||||
# 5 — VOD-Kapitel (`apps/vod-chapters`)
|
||||
|
||||
**Wirkrichtung:** Steuerung und ein wenig Reichweite (YouTube-Zweitverwertung).
|
||||
|
||||
## Was aufgezeichnet wird
|
||||
|
||||
Während des Streams sammelt der Ingest ohnehin schon:
|
||||
|
||||
| Signal | Quelle | wird Kapitel? |
|
||||
|---|---|---|
|
||||
| Kategoriewechsel | EventSub `channel.update` | immer |
|
||||
| Titelwechsel | EventSub `channel.update` | wenn Kategorie gleich bleibt |
|
||||
| OBS-Szenenwechsel | obs-websocket | nur bei Szenen aus der Whitelist |
|
||||
| Stream-Marker | `POST /helix/streams/markers` | immer |
|
||||
| erstellter Clip | `clips` | als Kapitel-Kandidat |
|
||||
|
||||
OBS-Szenen sind das rauschendste Signal — ein Wechsel auf "BRB" und zurück sind
|
||||
zwei Wechsel, die kein Kapitel sind. Deshalb Whitelist in `config/obs-scenes.yaml`
|
||||
plus Mindestabstand von 5 Minuten zwischen Kapiteln; kürzere Segmente werden mit
|
||||
dem vorherigen verschmolzen.
|
||||
|
||||
## Ausgabe
|
||||
|
||||
Nach `stream.offline`: VOD über `GET /helix/videos?user_id=&type=archive&first=1`
|
||||
holen, Zeitstempel relativ zu `streams.started_at` umrechnen, Text erzeugen:
|
||||
|
||||
```
|
||||
00:00 Start & Begrüßung
|
||||
00:14 Elden Ring — Malenia, Versuch 40
|
||||
01:32 Pause
|
||||
01:47 Elden Ring — sie ist tot
|
||||
02:55 Just Chatting — Community-Fragen
|
||||
```
|
||||
|
||||
Format wählbar: Twitch-VOD-Beschreibung, YouTube-Kapitel (identisch, muss aber
|
||||
bei `00:00` beginnen und mindestens drei Einträge mit je mindestens 10 s haben,
|
||||
sonst zeigt YouTube keine Kapitel an), Discord-Nachricht.
|
||||
|
||||
Twitch erlaubt kein Setzen der VOD-Beschreibung über die API — der Text landet
|
||||
deshalb in `data/vods/<datum>/chapters.txt` und wird per Push-Nachricht
|
||||
zugestellt, plus als Block im Recap-Post. Einfügen dauert zehn Sekunden.
|
||||
|
||||
## Kapitel-Titel
|
||||
|
||||
Der Rohtitel ist der Twitch-Titel bzw. der Kategoriename. Optional (Ausbaustufe 2)
|
||||
eine Verdichtung aus dem Chat: die häufigsten Substantive im Segment als
|
||||
Zusatz. Erst sinnvoll, wenn genug Chat-Volumen da ist.
|
||||
|
||||
## Abnahmekriterien
|
||||
|
||||
- Ein Testtream mit zwei Kategoriewechseln und drei Szenenwechseln erzeugt
|
||||
genau die erwarteten Kapitel.
|
||||
- Zeitstempel weichen vom VOD um weniger als 5 s ab.
|
||||
- Kein Kapitel unter 5 Minuten Länge.
|
||||
Reference in new issue
Block a user