Files
abdulive/docs/handoff.md
T
ZeasyAlexandClaude Opus 5.5 884e4517d8 Uebergabe: Pult-Freigabe als Abdus Handgriff, Abnahme an echtem OBS
Playwright-Mitschrift aus dem Repo genommen und ausgeschlossen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 16:00:50 +01:00

30 KiB
Raw Blame History

Übergabe

Stand 2026-10-26, letztes gebautes Paket 0.5.7. Wer hier weitermacht, liest zuerst diese Seite, dann 07-mvp-plan.md. Für Abdu selbst gibt es das Handbuch.

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

319 Tests, alle grün. Was noch fehlt, steht vollständig in 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; 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.

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".

Das Pult: Handy als Stream Deck

Seit 2026-10-26 auf dem Hub ausgerollt und im Paket 0.5.7, nicht an echtem OBS geprüft. Auf Abdus Rechner ist nichts auszuführen: das Update kommt über die tägliche Aufgabe, danach meldet sich der PC von selbst, Abdu tippt im Pult einmal „Freigeben“, und die Sound-Quelle legt ingest selbst in OBS an. /pult auf dem Hub ist eine installierbare Web-App (PWA) mit Soundboard, Szenenwechsel, Clip-Knopf, Stummschaltern, Liegestützen, Vorlese-Freigabe und den Testknöpfen. Beschreibung in apps/overlay-web/README.md.

  • Sounds lädt Abdu selbst hoch, am Handy über „+ Sound“. Sie liegen auf dem Hub unter <db>/sounds/, gespielt wird über die Browser-Source abdulive Sounds, die ingest selbst in OBS anlegt.
  • Szenen laufen über die Leitung Stream-PC → Hub. Der PC meldet sich beim ersten Start von selbst, die Freigabe ist ein Tipp im Pult. In OBS muss nur obs-websocket an sein.
  • Restrisiko der Freigabe: Anfragen kann jeder, der die Adresse kennt. Freigeben kann nur, wer im Pult angemeldet ist, und das Pult zeigt Name und Adresse des Rechners. Wer etwas freigibt, das er nicht erwartet, gibt einem Fremden Szenen, Stummschalter und Clip-Knopf - nicht mehr.
  • Geprüft: 319 Tests, Typprüfung, Durchläufe im Browser gegen einen nachgebauten Stream-PC mit falschem OBS (Freigabe, Einrichten der Sound-Quelle, Szenenwechsel, Stummschalten, Hochladen, Abspielen); 8 MB Upload durch den Proxy. Ungeprüft: echtes OBS (vor allem das Anlegen der Quelle), iPhone/Android-Installation.
  • Die Adresse in der Anfragekarte kam bei einer Probe aus dem eigenen Netz als 172.18.0.1 an, der Docker-Brücke des Proxys. Ob bei Abdus echter Verbindung seine Adresse erscheint (x-real-ip/x-forwarded-for vom Proxy), zeigt erst sein erster Versuch. Notfalls hilft der Rechnername.
  • Auf dem Hub stand am 2026-10-26 kurz eine Probe-Anfrage „PRUEFUNG-CLAUDE“. Sie ist nach zehn Minuten von selbst verfallen und wurde nicht freigegeben.

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, 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, Recap und Clips 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; 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:

git worktree add --detach ../abdulive-deploy HEAD
../abdulive-deploy/scripts/deploy-hub.sh
git worktree remove ../abdulive-deploy

Die Begründungen stehen in 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:

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.

Zum Entwickeln reicht ein Checkout und Node 24 oder neuer:

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):

scripts/deploy-hub.sh

Typprüfung, das Einzige wofür npm gebraucht wird:

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. Seit 2026-10-26 läuft sie (0.5.6, danach 0.5.7). Der erste Lauf (v0.5.5) scheiterte daran, dass whisper.cpp seine Windows-Programme nicht mehr an latest hängt, sondern an eigene b…-Veröffentlichungen; githubAsset() sucht jetzt die neueste mit passendem Archiv. Unter v0.5.5 liegt kein Paket.

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.

Was 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, danach abdulive calibrate zur Kontrolle.

  4. Stream-PC im Pult freigeben. Sobald 0.5.7 auf seinem Rechner läuft (tägliche Aufgabe um 05:00, Start bei der Anmeldung), steht unter Szenen „Stream-PC … möchte sich verbinden“. Ein Tipp auf „Freigeben“. Ausführen muss er dafür nichts.

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.

Das Pult an Abdus OBS abnehmen. Nach seiner Freigabe nachsehen, ob die Szene abdulive Ton mit der Quelle abdulive Sounds angelegt und in jede Szene eingehängt ist, ob ein Sound im Stream zu hören ist und ob Szenenwechsel und Stummschalter greifen. Das fällt mit der Abnahme von S2 zusammen, beides braucht nur ein laufendes OBS.

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.

Die CI läuft, aber nur einmal erfolgreich. Beim nächsten Tag kurz hinsehen.

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.