Files
abdulive/docs/handoff.md
T

548 lines
27 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.
# Übergabe
Stand **2026-10-18**, letztes gebautes Paket 0.5.4. Wer hier weitermacht,
liest zuerst diese Seite, dann [`07-mvp-plan.md`](07-mvp-plan.md). Für Abdu
selbst gibt es das [Handbuch](handbuch/handbuch.pdf).
## Wo das Projekt steht
Vier von sieben Scheiben des MVP „Clip-Knopf" sind gebaut. Die Kette vom
Auslöser bis zum fertigen Short steht im Code vollständig. Geprüft ist sie
überall dort, wo es ohne Abdus Stream-PC ging.
| Scheibe | Stand | Nachweis |
|---|---|---|
| S0 Toolchain + Auslieferung | fertig | Paket gebaut und installiert, `check-env` ohne harte Fehler, Updater einmal komplett durchgelaufen |
| S1 Einrichtung, Auth, DB, Chat | fertig | Daemon läuft, Chat verbunden, Secrets verschlüsselt |
| S2 OBS + `!clip` | Code fertig, **nicht abgenommen** | braucht laufendes OBS |
| S3 9:16-Render | fertig | Durchlauf mit künstlicher Aufnahme |
| S4 Untertitel | fertig | Durchlauf mit echtem deutschen Clip, Ränder nachgemessen |
| S5 Onboarding + Sichtung | offen | |
| S6 Erstchatter-Begrüßung | offen | |
288 Tests, alle grün. Was noch fehlt, steht vollständig in
[`offene-punkte.md`](offene-punkte.md).
## Neu: der Dienst läuft zentral
Seit 2026-09-17 läuft ein Teil des Toolkits nicht mehr auf Abdus Rechner,
sondern in einem LXC auf dem Proxmox-Host. Der Schnitt und seine Begründung
stehen in [ADR-0008](decisions/ADR-0008-geteilter-betrieb.md); die Kurzfassung
ist: alles, was nur Internet braucht, gehört dorthin, wo durchgehend Strom
ist.
| | Hub (LXC 141, `192.168.2.15`) | Ingest (Abdus PC) |
|---|---|---|
| Befehl | `abdulive hub` | `abdulive ingest` |
| Chat, Zuschauerzahlen | ja | ja |
| Discord-Knopf, Go-Live-Ping | **ja** | nein |
| Overlay für OBS | **ja** | nein |
| OBS, `!clip`, Rendern | nein | **ja** |
Erreichbar unter `https://abdu.dimension47.de`, hinter dem vorhandenen Nginx
Proxy Manager (Proxy-Eintrag 42, eigenes Let's-Encrypt-Zertifikat, Pufferung
abgeschaltet - ohne das sammelt nginx die Overlay-Ereignisse und liefert sie
gebündelt aus). Ausgerollt wird mit `scripts/deploy-hub.sh`, gestartet von
systemd (`abdulive-hub`). Der Container hat keinen Zugang zum Repo - das
Skript schiebt den Arbeitsstand als Archiv hinüber.
Der Discord-Teil der Zugangsdaten ist übertragen, der Bot hängt am Hub und
meldet beim Start "Rollen-Knöpfe sind scharf". **Abdu hat am 2026-09-23 sein
Konto verbunden** (`abdulive_`, alle 9 Scopes). Damit laufen Helix mit
Nutzer-Token und EventSub. Der Chat liest weiter anonym mit, weil das
Bot-Konto noch nicht verbunden ist.
**Das kostet ADR-0005 seine Hauptbegründung.** "Bindet nur auf 127.0.0.1" gilt
für den Hub nicht mehr. An seine Stelle treten ein Passwort für alles, was ein
Mensch bedient, und ein Schlüssel in der Adresse für die Browser-Sources.
## Der Go-Live-Ping geht raus
War in der letzten Übergabe noch Vorarbeit, ist jetzt gebaut
(`apps/broadcaster`). Ablauf: neuer Stream erkannt, eine Minute warten, Titel
und Kategorie frisch holen, Webhook posten, Rolle `Live-Ping` anpingen.
Der Ping funktioniert nur mit `allowed_mentions` im Nachrichtenkörper. `<@&ID>`
allein färbt die Erwähnung ein und benachrichtigt niemanden - das ist der
Fehler, den man erst nach drei leeren Streams bemerkt, und dagegen steht ein
eigener Test.
Gegen den doppelten Ping stehen drei Dinge, denn wer zweimal in zehn Minuten
angepingt wird, nimmt sich die Rolle wieder ab: die Tabelle `posts` mit einem
eindeutigen Index je Stream, eine Abklingzeit von sechs Stunden, und der
Umstand, dass der Fortsetzungszweig im Polling gar nicht erst auslöst.
Der Hub steht auf `mode: draft` - der Post geht vollständig, aber in
`#technik-log` und ohne Ping. Ein Probelauf ist dort gelandet, der Weg zum
Webhook ist damit nachgewiesen. Zum Scharfschalten: `mode: live` in
`/var/lib/abdulive/config/config.yaml`.
**Ausgelöst hat er noch nie.** Bis zum 2026-09-23 fehlte das
Broadcaster-Token; das ist jetzt da. Der nächste Stream ist der erste echte
Durchlauf, im Entwurfsmodus.
## Das Overlay steht
Browser-Sources unter `apps/overlay-web/public`, ausgeliefert vom
Overlay-Server: Einblendungen, Chat, Kamerarahmen, Statusleiste, Effekte,
Pausenbild, Vorlesen, Liegestütze. Kein Build, kein Framework - OBS lädt die
Dateien direkt.
**Nächste Sitzung: das Overlay wird neu gemacht.** Gestaltung von Grund auf,
nicht nur der Kamerarahmen. Was bleiben sollte, weil es Fehler verhindert, die
man erst im Stream bemerkt: `client.js` (Neuverbinden, Wachhund,
Schnappschuss, `Overlay.esc`), der durchsichtige Hintergrund und die Kontur
aus `theme.css`, `?probe=1` zum Ansehen im Browser. Die Ereignisse auf dem Bus
(`apps/overlay-server/src/bus.ts`) sind die Schnittstelle, an der sich die
neuen Seiten festhalten können.
Die Ereignisse kommen über Server-Sent Events statt WebSocket. Warum, und was
gegen die tote Browser-Source nach einem Neustart getan ist, steht in
[`apps/overlay-server/README.md`](../apps/overlay-server/README.md).
Abos, Raids und Bits kommen über IRC-USERNOTICE herein, das wird jetzt
ausgewertet. **Follows fehlen** - die gibt es nur über EventSub. EventSub
läuft inzwischen, abonniert ist aber nur `channel_points_custom_reward_redemption.add`.
Follows wären ein zweites Abonnement (`channel.follow`, Version 2, braucht
`moderator:read:followers`, der Scope ist schon da).
**Kamerarahmen, Entwurf** (`/kamera`, 520×380 auf 0,0): ein Rahmen rechts und
unten um die Facecam, darunter eine Platte mit Kanal, Kategorie, Zuschauern
und Laufzeit. Soll die Statusleiste ersetzen, die als unschön empfunden wird.
Über der Kamera liegt bewusst nichts, weil genau dieser Bereich für die
Shorts ausgeschnitten wird. Noch nicht ausgerollt, Rückmeldung zur Richtung
steht aus. Ansehen: `node apps/cli/main.ts overlay --demo`, dann
`http://127.0.0.1:4791/kamera?probe=1`.
Zum Ansehen ohne Stream: auf der Übersichtsseite gibt es unter
**Ausprobieren** für jede Einblendung, jede Chat-Variante und jeden Effekt
einen Knopf. Die Ereignisse gehen nur an die Browser-Sources. Dasselbe im
Dauerbetrieb: `abdulive overlay --demo` oder der Knopf „Vorführung starten".
## Kanalpunkte: „Nachricht vorlesen" läuft
Seit 2026-09-23 scharf. Der Weg: EventSub meldet die Einlösung, der Hub
prüft (`apps/hub/src/rewards.ts`), Piper spricht, `/tts` spielt ab.
| | |
|---|---|
| Belohnung | „Nachricht vorlesen", 1000 Punkte, Texteingabe Pflicht |
| Freigabe | keine, wird direkt vorgelesen |
| Schutz | Wortfilter (29 Einträge), 200 Zeichen, 3 je Person und Stunde, „Sofort still" auf der Übersichtsseite |
| Stimme | Piper, `de_DE-thorsten-medium` (mls klang verwaschen) |
**Die Stimme entsteht im Hub, nicht im Browser.** Zuerst lief es über
`speechSynthesis` in der Browser-Quelle. Das hat in OBS gesprochen, aber
amerikanisch: OBS' Browser sah keine deutsche Stimme und die Seite nahm die
erste beliebige. Piper liegt unter `/opt/piper` im LXC (2 Kerne: 200 Zeichen
in 0,7 s), das Stimmenmodell unter `models/piper/`. Beides überlebt jedes
Ausrollen; eingerichtet mit `scripts/install-piper.sh`.
**Die erste Stimme war `de_DE-mls-medium`, Sprecher 0**, und klang im Stream
„wie jemand mit Schlaganfall". Die Phoneme stimmten (`piper --debug`), es lag
am Modell: Hörbuch-Aufnahmen von 236 Laien, Sprecher 0 ist ein beliebiger.
Seit 2026-09-24 läuft `de_DE-thorsten-medium`, ein Sprecher, darum kein
`sprecher` mehr in der Konfiguration. Das mls-Modell liegt noch daneben.
**Die Belohnung muss über die API angelegt sein** (`abdulive rewards:anlegen`).
Nur die App, die eine Belohnung angelegt hat, darf deren Einlösungen
abschließen oder erstatten. Im Dashboard sieht man davon nichts. Titel,
Kosten und Abklingzeit darf man dort ändern; löschen und neu anlegen nicht,
dann ist die ID weg, die in der Hub-Konfiguration steht.
**Der Wortfilter normalisiert vorher**: Leetspeak, Punkte zwischen den
Buchstaben, gezogene Buchstaben, Akzente. Einträge ab 5 Buchstaben werden im
zusammengezogenen Text gesucht, kürzere nur am Wortanfang, sonst stünde
„nazi" in „kann azubi". Die Liste steht nur in der Hub-Konfiguration, nicht
im Repo.
**Der Filter hat eine Stunde lang fast alles abgelehnt.** `www` in der Liste
wurde zusammengezogen zu `w` und traf damit jedes Wort, das mit w anfängt
(„wie", „weg"). Die Punkte gingen zurück, im Log stand nur `Wortfilter`.
Behoben am 2026-09-24: was nach dem Zusammenziehen unter 3 Buchstaben hat,
trifft nur noch als ganzes Wort. **Noch offen:** lange Einträge treffen über
Wortgrenzen hinweg, „Spaß tut gut" fliegt wegen `spast` raus, „eine Gerade"
wegen `neger`. Wer eine Ablehnung nachvollziehen will: der Text steht im
Ereignis `points.redeemed` in der Datenbank.
**Der YAML-Leser konnte keine Listen aus Maps.** `- titel: X` zerfiel in
Zeichenketten, jede Belohnung war ohne Meldung unwirksam - auch das Beispiel
in `config.example.yaml`. Behoben und getestet; die bestehenden Dateien
werden nachweislich gleich gelesen wie vorher.
## Kanalpunkte: Liegestütze
Seit 2026-10-04 scharf. Die Belohnung „5 Liegestütze" gab es auf Twitch schon
(von Hand angelegt, 61 Einlösungen bis dahin). Sie steht jetzt in der
Hub-Konfiguration mit `aktion: liegestuetze`, `anzahl: 5`, zugeordnet über den
Titel. Wird sie auf Twitch umbenannt, zählt der Hub nicht mehr mit.
| | |
|---|---|
| Zähler | `apps/hub/src/liegestuetze.ts`, Tabelle `zaehler` (Migration 003), übersteht Neustarts |
| Offen | Einlösung +5. Auf der Übersicht −1/−5/−10, Setzen, auf 0 |
| Gemacht | Was mit −1/−5/−10 abgezogen wird. Setzen zählt nicht, das ist Korrektur. Eigenes Feld zum Korrigieren |
| Neuer Stream | Gemacht auf 0, offen bleibt. Nicht beim Neustart des Hubs mitten im Stream, nicht bei einer Lücke bis 5 Minuten |
| Overlay | `/liegestuetze`, 420×200. `?verstecken=1`, `?gemacht=0`, `?titel=...` |
**Ungeprüft im Stream.** Getestet mit Probeserver und Browser. Weder eine
echte Einlösung noch das Nullen beim Streamstart ist seit dem Ausrollen
passiert. Der Zähler startete bei 0 offen, die 61 alten Einlösungen sind nicht
übernommen. In OBS ist die Quelle noch nicht eingetragen.
**Die Belohnung ist nicht über die API angelegt.** Der Hub kann ihre
Einlösungen deshalb nicht als erledigt melden und schreibt bei jeder eine
Warnung ins Log. Für das Zählen egal. Eine ältere Belohnung „10 Liegestütze"
(12 Einlösungen bis 2026-09-24) steht nicht in der Konfiguration.
**Nebenbei behoben: die Vorlese-Freigabe auf der Übersicht war tot.**
`POST /tts` landete bei der Overlay-Seite `/tts` statt bei der Freigabe, auf
dem Hub ohne Schlüssel sogar mit 403. „Vorlesen", „Ablehnen" und „Sofort
still" haben damit nie gewirkt. Overlay-Seiten werden jetzt nur bei GET
ausgeliefert. Gegen eine echte Warteschlange nicht nachgeprüft.
## Der Discord-Server steht
Dazugekommen seit der letzten Übergabe, und kein Teil des MVP: der
Community-Server ist eingerichtet, und zwar als Code statt von Hand. Der
Bauplan liegt in [`packages/discord/src/plan.ts`](../packages/discord/src/plan.ts),
aufgetragen wird er mit `abdulive discord:setup`. Wiederholbar: gefunden wird
über Namen und frühere Namen, danach wird angeglichen. Der zweite Lauf ändert
nichts, gelöscht wird nie.
Das ist Vorarbeit für drei Werkzeuge. Die Webhooks für
[Go-Live](projects/go-live-broadcaster.md), [Recap](projects/stream-recap.md)
und [Clips](projects/clip-shorts.md) liegen fertig im verschlüsselten Speicher
unter `goLiveWebhook`, `recapWebhook` und `clipsWebhook`. Wer diese Werkzeuge
baut, muss sich um Discord nicht mehr kümmern, sondern nur noch posten.
Zehn Kanäle, vier Rollen, zwei AutoMod-Regeln. Seit 2026-10-17 dabei:
`🙋│begrüßung` für Discords Beitrittsmeldungen (liefen vorher in
`#allgemein`), und die Foren `🎮│spielvorschläge` und
`👀│reaction-einsendungen`. Beide hatte Abdu als Textkanäle angelegt, sie sind
jetzt Foren mit Tags, die nur Mods setzen, und 👍 als Standard-Reaktion.
Discord wandelt keinen Textkanal in ein Forum um: die beiden leeren
Textkanäle wurden einmalig über die API gelöscht, das Setup-Skript selbst
löscht weiterhin nichts.
Im Spielvorschläge-Forum stehen neun Beiträge, einer je Spiel aus Abdus
Liste, vom Bot angelegt (Titel vorher nachgeschlagen, „involkyr" heißt
Invokyr). In `📢│ankündigungen` hat der Bot das Forum angekündigt, ohne Ping.
Der Bot darf dort trotz Schreibsperre für `@everyone` posten.
**Der Hub ist nicht neu ausgerollt.** Das Setup lief aus einer Kopie im
Container, der laufende Prozess hat noch den Stand davor. Für den Server ist
das egal, für den Knopf auch. Beim nächsten Ausrollen kommt es mit. Der Entwurf und seine
Begründung stehen in [`projects/discord-community.md`](projects/discord-community.md);
die Kurzfassung ist, dass bei 5 bis 30 Zuschauern fünf sichtbare Kanäle besser
sind als zwanzig leere.
Ein Teil davon läuft dauerhaft: der Knopf unter dem Willkommenstext, mit dem
man sich die Rolle `Live-Ping` selbst gibt. Er hängt als Gateway-Verbindung mit
im Prozess und meldet sich als `discordConnected` auf der Statusseite. Seit dem
Umzug in den Hub läuft er durch, auch wenn Abdus Rechner aus ist.
Dazugekommen ist ein vierter Webhook: `technikWebhook` in `#technik-log`. Dort
landet der Go-Live-Post im Entwurfsmodus - dieselbe Nachricht wie im Betrieb,
nur ohne Ping und nur für Mods sichtbar. Wer den Server vor dieser Fassung
eingerichtet hat, muss `abdulive discord:setup` einmal erneut laufen lassen.
## Was jemand wissen muss, bevor er etwas anfasst
**Keine Abhängigkeiten, kein Build.** Node 24 bringt SQLite, WebSocket,
`fetch` und die Ausführung von TypeScript mit. `node apps/cli/main.ts` läuft
direkt. Deshalb gilt: nur *löschbare* TypeScript-Syntax, also keine `enum`,
keine Parameter-Properties im Konstruktor, keine `namespace`.
`erasableSyntaxOnly` in der `tsconfig.json` erzwingt das und hat schon einen
echten Fehler gefangen.
**Alles läuft in einem Prozess** - aber es gibt jetzt zwei davon, Hub und
Ingest (siehe oben). Schwere Arbeit (ffmpeg, whisper) läuft als Kindprozess und
darf abstürzen. Der Chat-Prozess darf nie sterben. Was er nicht mitschreibt,
ist für immer weg.
**Zugangsdaten kommen aus der Weboberfläche**, nicht aus einer Datei. Sie
liegen verschlüsselt neben der Datenbank, aber bewusst nicht *in* ihr. Die
Datenbank wird gesichert und weitergereicht, die Geheimnisse nicht.
**Das Repo ist seit 2026-09-09 öffentlich.** Damit funktioniert der Einzeiler
zur Installation, und der Updater braucht keinen Token. Es heißt aber auch:
nichts Vertrauliches mehr hineinschreiben, auch nicht in die Doku.
**Es gibt zwei Remotes**, beide mit demselben Inhalt: `origin`
(`ZeasyAlex/abdulive`) und `discord` (`ZeasyAlex/abdulive_-discord`). Wer
pusht, pusht auf beide, sonst laufen sie auseinander.
**`deploy-hub.sh` schiebt den Arbeitsstand, nicht den Commit.** Liegt
Unfertiges herum, landet es im LXC. Sauber ausrollen aus einem eigenen
Checkout:
```bash
git worktree add --detach ../abdulive-deploy HEAD
../abdulive-deploy/scripts/deploy-hub.sh
git worktree remove ../abdulive-deploy
```
Die Begründungen stehen in [`decisions/`](decisions/), acht ADRs. Wer eine
davon umwerfen will, liest sie vorher. ADR-0004 (Docker) ist durch ADR-0007
ersetzt; die Analyse darin bleibt lesenswert, weil zwei der vier
beschriebenen Fallen mit dem Container weggefallen sind statt gelöst worden
zu sein.
## Loslegen
Auf Abdus Rechner, eine native Windows-Anwendung:
```powershell
irm https://git.zeasy.dev/ZeasyAlex/abdulive/raw/branch/main/scripts/install.ps1 | iex
```
Der Installer lädt das Paket, prüft die Prüfsumme, holt das Sprachmodell,
läuft den Selbsttest, startet den Dienst und öffnet die Einrichtungsseite.
Ohne Administratorrechte. Ausführlich in [`05-setup.md`](05-setup.md).
Zum Entwickeln reicht ein Checkout und Node 24 oder neuer:
```powershell
node apps/cli/main.ts check-env
node --test "packages/**/*.test.ts" "apps/**/*.test.ts"
node apps/cli/main.ts overlay --demo # Overlay ohne Stream ansehen
node scripts/build-package.ts --version 0.5.5 # baut dist/
```
Den Hub ausrollen (braucht SSH auf den Proxmox-Host):
```bash
scripts/deploy-hub.sh
```
Typprüfung, das Einzige wofür npm gebraucht wird:
```powershell
npm i typescript@5.9 @types/node
npx tsc --noEmit
```
## Ausliefern
`build-package.ts` lädt Node, ffmpeg und whisper.cpp bei ihren Herausgebern,
packt alles nach `dist/` und schreibt `abdulive.json`. Dieses Manifest fragt
`abdulive update` auf Abdus Rechner ab.
Die Veröffentlichung `latest` trägt beide Dateien und wird bei jedem Bau
ersetzt, damit die Adresse gleich bleibt.
`.gitea/workflows/release.yml` macht das beim Tag automatisch. **Die Pipeline
ist noch nie gelaufen**, bisher wurde von Hand veröffentlicht. Der Runner
(`ubuntu-latest`) steht.
## Windows-Fallen, die schon zugeschlagen haben
Alle vier haben Zeit gekostet, und keine sieht im Fehlerfall nach ihrer
Ursache aus. Wer an den PowerShell-Skripten arbeitet, sollte sie kennen.
**stderr beendet das Skript.** Windows PowerShell 5.1 verpackt jede
stderr-Zeile eines nativen Programms in einen `ErrorRecord`. Zusammen mit
`$ErrorActionPreference = 'Stop'` bricht das sofort alles ab. Der Daemon
schreibt im Normalbetrieb Warnungen auf stderr, der Starter hat ihn also bei
seiner ersten Logzeile umgebracht: Aufgabe meldete Code 1, das Protokoll war
leer. Deshalb läuft der Start über `Start-Process` mit getrennten
Umleitungen, und `install.ps1` ruft native Programme über eine Hilfsfunktion
auf, die `ErrorActionPreference` kurz zurückstellt.
**`& $programm; return $LASTEXITCODE` gibt auch die Ausgabe zurück.** Die
Funktion liefert dann ein Array aus allen Ausgabezeilen plus Exit-Code, und
jeder Vergleich damit ist falsch, ohne dass es auffällt. Ein erfolgreiches
`fetch-model` galt so als fehlgeschlagen. Ausgabe mit `| Out-Host`
durchreichen.
**`localhost` kostet zwei Sekunden.** Der Dienst lauscht nur auf IPv4,
Windows probiert bei `localhost` zuerst `::1`. Gemessen: 2066 ms bis zum
Rückfall gegenüber 15 ms bei `127.0.0.1`. Eine Bereitschaftsprüfung mit zwei
Sekunden Zeitlimit läuft dadurch immer ab, obwohl der Dienst längst läuft.
**Windows-Pfade im ffmpeg-Filtergraphen brauchen drei Schritte.** An echtem
ffmpeg 8.1 durchprobiert:
| Form | Ergebnis |
|---|---|
| `ass=filename=C:/x/s.ass` | `No option name near '/x/s.ass'` |
| `ass=filename=C\:/x/s.ass` | ebenso |
| `ass=filename='C:/x/s.ass'` | ebenso |
| `ass=filename='C\:/x/s.ass'` | läuft |
Vorwärtsschrägstriche, escapter Doppelpunkt **und** Anführungszeichen. Je
zwei davon reichen nicht. `filterPath()` in `packages/media/src/ffmpeg.ts`
macht das, mit Tests.
## Untertitel: was gemessen ist
**Die Schrift muss mitgeliefert werden.** Im Container kam DejaVu Sans aus
dem Betriebssystem, Windows hat sie nicht. Fehlt `fontsdir`, setzt libass
wortlos eine Ersatzschrift ein. Gemessen: dieselbe Zeile wird 720 statt 826
Pixel breit, ohne jede Fehlermeldung. Die Dateien liegen unter
`assets/fonts/`.
**Die Zeilenlänge ist eingemessen, nicht geschätzt.** Vorher stand dort fest
`maxChars: 30`. Das sind bei Schriftgröße 76 rund 1085 px, nutzbar sind 940.
Zusammen mit `WrapStyle: 2`, bei dem libass gar nicht umbricht, lief der Text
links und rechts aus dem Bild. Jetzt wird die Grenze aus Breite und
Schriftgröße abgeleitet, Versalien zählen breiter, und einzelne zu lange
Wörter bekommen eine kleinere Schrift, weil libass innerhalb eines Wortes
nicht umbricht. Deutsche Komposita sind regelmäßig breiter als das Bild.
Wer die Schrift wechselt, muss `CHAR_WIDTH_RATIO` in `subtitles.ts` neu
einmessen. Sonst bricht die Zeile wieder an der falschen Stelle.
## Discord: was gemessen ist
Vier Dinge, die nicht in der Dokumentation standen, in der man zuerst sucht.
Alle am echten Server nachgeprüft.
**Kanalnamen lassen sich nur zweimal in zehn Minuten ändern.** Danach verlangt
Discord eine Pause, gemessen wurden 529 Sekunden. Ein Einrichtungslauf, der
brav auf ein 429 wartet, steht dann neun Minuten. Der Client wartet deshalb
höchstens eine Minute, meldet den Kanal als übersprungen und macht weiter.
**Eine Rolle kann nur Rollen unterhalb der eigenen verschieben** - auch mit
Administrator. Discord hängt die Rolle eines frisch eingeladenen Bots ganz
unten ein, also gibt es dort keinen Platz. Der erste Lauf quittierte das mit
`Missing Permissions`, was nach einem fehlenden Recht aussieht und keines ist.
`rolePositions()` rechnet den Platz jetzt aus und sagt, was zu tun ist.
**Discords Onboarding braucht einen größeren Server.**
```
HTTP 400 Cannot enable onboarding, requirements are not met (350000)
sichtbar für @everyone: 5 davon beschreibbar: 3
Discord verlangt: 7 davon beschreibbar: 5
```
Zwei Kanäle anzulegen, damit eine Schwelle passt, hätte den Server leerer
gemacht statt besser. Daher der Knopf.
**Kanalnamen werden normalisiert, aber nicht so weit wie erwartet.**
Textkanäle werden kleingeschrieben und Leerzeichen zu Bindestrichen; Emoji,
Umlaute und das Trennzeichen `│` (U+2502) bleiben unverändert stehen.
`normalizeName()` bildet das ab. Ohne diesen Abgleich geht bei jedem Lauf
dieselbe Umbenennung erneut raus - und läuft nach zwei Läufen in das Limit von
oben.
## Die drei Dinge, die Abdu tun muss
1. ~~**Broadcaster-Konto verbinden.**~~ Erledigt am 2026-09-23. Der Rest
dieses Punkts bleibt als Nachschlag stehen, falls neu verbunden werden
muss.
Die beiden Twitch-Apps (dev.twitch.tv/console/apps) kann auch jemand anders
anlegen, sie hängen nicht am Kanal. Seit dem Umzug in den Hub brauchen sie
als Redirect-URI **`https://abdu.dimension47.de/callback`**; die alte
`http://localhost:4792/callback` bleibt daneben stehen, wenn auf dem
Stream-PC weiter eingerichtet werden soll. Twitch vergleicht zeichengenau.
Die App ist inzwischen hinterlegt. Was nur Abdu kann: auf
`https://abdu.dimension47.de/setup` eingeloggt als er selbst auf „Mit
Twitch verbinden" klicken. Der Vorteil gegenüber vorher: das geht von jedem
Gerät, der Stream-PC muss nicht laufen.
Ob die Redirect-URI in der App richtig eingetragen ist, zeigt sich erst bei
diesem Klick. Twitch prüft sie erst nach dem Login und gibt vorher auch bei
einer falschen URI ein freundliches 302 zurück - von außen ist das nicht
nachweisbar.
2. **OBS einrichten**: obs-websocket an (Port 4455), Wiedergabepuffer 90 s,
Hotkey auf „Wiedergabepuffer speichern", getrennte Audiospuren.
3. **Layout umstellen** nach [`obs-layout.md`](obs-layout.md), danach
`abdulive calibrate` zur Kontrolle.
Das Bot-Konto ist noch nicht verbunden: bis dahin liest das Toolkit den
Chat anonym mit und kann nichts senden.
Ohne Punkt 2 gibt es keine Clips. Ohne Punkt 3 sitzt der Ausschnitt schief.
Dazu eine Kleinigkeit mit Wirkung: das Bot-Konto sollte Moderator sein
(`/mod <botname>`). Ohne das gilt ein Limit von 20 Chat-Nachrichten je 30
Sekunden statt 100, und im Follower-only-Modus kommt der Bot gar nicht durch.
Und drei für Discord, die nicht über die API gehen:
1. **Sich selbst die Rolle `Abdu` geben.** Das Skript legt Rollen an, aber es
hängt sie niemandem um.
2. **Einladungslink** ohne Ablaufdatum erzeugen und ins Twitch-Panel, in die
Kanalinfo und in einen Chat-Befehl eintragen. Ein Server, den im Stream
niemand erwähnt, bleibt leer. Das ist der eine Handgriff, an dem hängt, ob
der Server überhaupt jemanden bekommt.
3. **Tags in den Foren pflegen.** „Wird gespielt", „Gespielt", „Eher nicht"
bzw. „Kommt dran", „Angeschaut" setzen nur Abdu und Mods. Ein Vorschlag,
der sichtbar umgesetzt wird, bringt den nächsten.
## Wo ich weitermachen würde
**S2 an einem laufenden OBS abnehmen.** Das ist die einzige ungeprüfte Stelle
in der ganzen Kette, und alles Weitere hängt daran. Seit es `abdulive sources`
gibt, ist auch die Frage beantwortbar, welche OBS-Quelle fremden Text ins Bild
malt. Das war vorher nicht zu erraten.
**Den Go-Live-Ping scharf schalten.** Er steht auf `draft`. Einmal
`abdulive golive:test --echt` laufen lassen, prüfen ob das Handy klingelt, dann
`mode: live`. Das ist der kleinste verbleibende Handgriff mit der größten
Wirkung.
**Das Overlay neu machen** (nächste Sitzung, siehe oben). Der Kamerarahmen
als Entwurf geht darin auf, die Statusleiste soll ersetzt werden. Danach die
Liegestütze-Quelle in OBS eintragen und im Stream einmal beobachten.
**`apps/recap` bauen.** Angefangen, aber nicht committet: `apps/recap/src/recap.ts`
und Ergänzungen in `packages/db/src/repos.ts` liegen nur im Arbeitsstand
(die Typprüfung meckert dort noch über `url: undefined`). Der Webhook liegt
bereit, der Hub hat die Daten. Seit 2026-10-04 wird aus einem sauberen
Checkout ausgerollt, der halbe Recap liegt also nicht mehr im LXC.
**Danach S5.** Nicht wegen des Codes, sondern weil das Werkzeug ohne
Sichtungsseite für Abdu nicht bedienbar ist. Die Kette funktioniert, aber sie
hat keine Oberfläche, auf der man Clips durchgeht und aussortiert.
Abnahmekriterium steht im Plan: *Ein Fremder kommt von „nichts eingerichtet"
bis zum ersten eigenen Clip, ohne die Dokumentation zu lesen.* Installer und
Handbuch decken davon inzwischen ein gutes Stück ab.
## Womit ich nicht zufrieden bin
**Das Aufräumen im Arbeitsordner fehlt komplett.** Rohdateien sammeln sich
unbegrenzt an, mehrere hundert MB je Clip. Das ist die Lücke, die als Erstes
weh tun wird.
**Die Untertitel sind brauchbar, nicht gut.** `ggml-small` verhört sich
hörbar („Danke" wurde „Dank es"), DejaVu Sans ist unauffällig, und das
fortlaufende Einfärben ist nicht der Stil, der auf Shorts trägt. Sechs
konkrete Verbesserungen stehen in [`offene-punkte.md`](offene-punkte.md).
**Die CI ist geschrieben, aber ungetestet.** Beim ersten Tag zusehen.
**Das Paket ist nicht signiert.** SmartScreen warnt bei jeder Installation.
Solange nur Abdu installiert, reicht ein Klick. Sobald jemand Drittes es
bekommt, nicht mehr.
**Der Discord-Knopf hängt am Daemon** - das war der Punkt aus der letzten
Übergabe und ist mit dem Hub erledigt. Dafür gibt es jetzt einen neuen: der
Bot darf nur an **einer** Stelle laufen. Steht auf dem Stream-PC ein
Discord-Bot-Token im Speicher und läuft dort `ingest`, antworten zwei Prozesse
auf denselben Klick. Auf dem Stream-PC bleibt das Discord-Token leer.
**Der Overlay-Schlüssel steht in der Adresse.** Damit steht er in jedem
OBS-Profil und auf jedem Screenshot der Einstellungen. Neu würfeln geht auf
der Übersichtsseite, aber danach müssen alle Adressen in OBS neu
eingetragen werden.
**Das Overlay hat nie unter Last gelaufen.** Geprüft ist es gegen den
Vorführmodus und im Browser. Wie es sich bei 200 Nachrichten pro Minute
verhält, und was der Emote-Regen beim Encoder kostet, ist offen.
**Der Discord-Bot ist nie unter Last gelaufen.** Getestet sind die Bausteine
und ein Klickweg gegen einen nachgebauten Server; am echten Server ist die
Verbindung nachgewiesen und der Rollenwechsel einmal in beide Richtungen
durchgespielt. Was bei gleichzeitigen Klicks oder nach Tagen Laufzeit
passiert, ist offen.
**Es gibt keine Einstellung für den OBS-Wiedergabepuffer-Ordner.** Im
Container kam der Pfad über `OBS_REPLAY_DIR`, nativ fehlt der Weg. Für die
Clip-Kette ist das egal, weil OBS den Pfad über WebSocket meldet. Aber
`calibrate` findet so keine Aufnahmen, und `check-env` warnt zu Recht.