Raid-Scout, Auswertung, VOD-Kapitel, Community-Gedaechtnis, Punkteshop
und Recap samt Platzhaltern, Spezifikationen und Konfiguration. Der Hub
schickt am Chat zusaetzlich die Sendezeit mit, fuer die Uhrzeit im Pult.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Der Hub loest die Abzeichen aus dem IRC-Tag ueber Helix (global und
Kanal) zu Bildern auf und schickt sie mit jeder Chatzeile mit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ingest wuerfelt beim ersten Start einen Schluessel und meldet sich beim
Hub; der antwortet 202, bis im Pult jemand auf Freigeben tippt
- nach dem Verbinden schickt der Hub die Adresse von /sounds, ingest legt
Szene "abdulive Ton" mit der Browser-Source an und haengt sie in jede Szene
- fern:verbinden bleibt als Weg von Hand
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- /pult als installierbare Web-App (Manifest, Service Worker, Symbole)
- Soundboard: Sounds vom Handy hochladen, bearbeiten, im Stream spielen
ueber die neue Browser-Source /sounds
- Szenen, Clip und Stummschalter: der Stream-PC waehlt sich mit eigenem
Schluessel beim Hub ein (fern:verbinden), Befehle warten auf OBS
- Browser-Sources melden, welche Seite sie sind
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Herunterzaehlen mit -1/-5/-10 zaehlt als gemacht, Setzen bleibt eine
Korrektur und zaehlt nicht. Der Stand "gemacht" beginnt mit jedem neuen
Stream bei 0 (nicht beim Neustart des Hubs mitten im Stream), steht im
Overlay unter der offenen Zahl und laesst sich auf der Uebersicht
korrigieren.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Neue Aktion `liegestuetze` fuer Belohnungen: jede Einloesung zaehlt um
`anzahl` hoch (Vorgabe 5). Der Stand liegt in der Datenbank und
uebersteht einen Neustart. Browser-Source /liegestuetze zeigt ihn, die
Uebersichtsseite hat Knoepfe zum Herunterzaehlen und Setzen.
Nebenbei: POST /tts landete bei der Overlay-Seite statt bei der
Freigabe. Seiten werden jetzt nur noch bei GET ausgeliefert.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
"www" in der Blockliste wurde beim Zusammenziehen gezogener Buchstaben zu
"w" und traf als kurzer Eintrag jedes Wort, das mit w anfaengt. Im Stream
flog damit fast jede Nachricht raus ("wie", "weg"), die Punkte gingen
zurueck. Was nach dem Normalisieren unter 3 Buchstaben hat, trifft jetzt
nur noch als ganzes Wort.
de_DE-mls-medium, Sprecher 0, klang verwaschen. Die Phoneme stimmten, es
lag am Modell. Vorgabe ist jetzt de_DE-thorsten-medium; im Hub schon so
eingestellt.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Rahmen rechts und unten um die Facecam, darunter eine Platte mit Kanal,
Kategorie, Zuschauern und Laufzeit. Soll die Statusleiste abloesen.
Ueber der Kamera liegt nichts: genau 0,0 480x270 wird fuer die Shorts
ausgeschnitten, das Gameplay ab x=515. Der Rahmen nutzt nur den Streifen
dazwischen und den Platz darunter. Bei Abo, Raid und Bits glueht er kurz
in der Farbe des Ereignisses.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
In OBS' Browser-Quelle kam `speechSynthesis` amerikanisch heraus: der
eingebaute Browser sah keine deutsche Stimme, und die Seite nahm die erste
beliebige. Welche Stimmen es gibt, haengt am Rechner, der streamt.
Jetzt spricht der Hub mit Piper (lokal im LXC, 200 Zeichen in 0,7 s). Die
WAV liegt kurz im Speicher, `/tts` holt sie unter /tts/audio/<id> mit dem
Overlay-Schluessel und spielt sie ueber <audio> ab - das laeuft sicher ueber
OBS' Tonspur. Der Browser springt nur noch mit einer deutschen Stimme ein,
sonst wird nur angezeigt. Stimme und Sprecher stehen in rewards.tts.
Dazu:
- `abdulive rewards:anlegen`: Belohnung ueber die API anlegen. Nur die App,
die sie angelegt hat, darf Einloesungen abschliessen oder erstatten.
- Einloesungen werden nach der Aktion als FULFILLED markiert, statt in
Twitchs Warteschlange liegen zu bleiben.
- Wortfilter normalisiert vorher: Leetspeak, Punkte dazwischen, gezogene
Buchstaben, Akzente. Kurze Eintraege nur am Wortanfang.
- Der Testknopf "Vorlesen" geht durch Piper und prueft so die echte Stimme.
- scripts/install-piper.sh richtet Piper und Stimme im LXC ein.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Stand 2026-09-17. Der Dienst laeuft seitdem zentral, der Go-Live-Ping geht
raus, und das Overlay steht.
- Neuer Abschnitt "Neu: der Dienst laeuft zentral" mit der Tabelle, was wo
laeuft, und dem Hinweis, dass ADR-0005 damit seine Hauptbegruendung
verliert.
- "Der Go-Live-Ping geht raus": Ablauf, der Fallstrick mit `allowed_mentions`,
und die drei Sperren gegen den doppelten Ping. Dazu ehrlich, dass er noch
nie von allein ausgeloest hat.
- "Das Overlay steht": fuenf Seiten, SSE statt WebSocket, und dass Follows
fehlen.
- Der Discord-Knopf haengt nicht mehr am Daemon - dafuer ein neuer offener
Punkt: der Bot darf nur an einer Stelle laufen, sonst antworten zwei
Prozesse auf denselben Klick.
- Redirect-URI der Twitch-App auf die oeffentliche Adresse umgestellt, mit
dem Hinweis, dass Twitch sie erst nach dem Login prueft und ein Test von
aussen darueber nichts sagt.
- 162 -> 264 Tests.
config.example.yaml dokumentiert broadcaster, rewards, overlay und publicUrl.
apps/hub bekommt ein README mit dem Schnitt zwischen Hub und Ingest.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
EventSub ueber WebSocket wurde in ADR-0003 beschlossen und nie gebaut. Jetzt
gebaut, weil Kanalpunkt-Einloesungen ueber IRC nur ankommen, wenn die
Belohnung eine Texteingabe verlangt - und selbst dann ohne Einloese-ID. Ohne
die ID lassen sich Punkte nicht zurueckgeben, wenn eine Aktion scheitert.
Zwei Eigenheiten des Protokolls, die im Code stehen:
- Ein Abonnement muss innerhalb von 10 Sekunden nach dem Willkommensgruss
stehen, sonst schliesst Twitch wieder.
- Bei `session_reconnect` wird erst die neue Verbindung aufgebaut und dann die
alte geschlossen. Andersherum faellt genau dazwischen eine Einloesung unter
den Tisch.
Belohnungen werden ueber ihren Titel zugeordnet, wahlweise ueber die stabile
ID; liegt beides vor, gewinnt die ID. Eine unbekannte Belohnung ist kein
Fehler - es darf welche geben, die das Toolkit nichts angehen.
Zum Vorlesen, und warum es eine Warteschlange hat: chat-control.md schliesst
TTS fuer Ausbaustufe 1 aus, und der Grund gilt. Kanalpunkte sammeln sich beim
Zugucken von allein an und sind damit kaum eine Huerde; ohne Freigabe hat man
eine Vorlesefunktion fuer Fremde im eigenen Stream. Vorgabe ist deshalb die
Freigabe von Hand, dazu Laengengrenze, Wortfilter, Drossel je Person und
Stunde und ein Panikschalter. Zu langer Text wird abgelehnt statt gekuerzt -
ein halb vorgelesener Satz ist schlimmer als gar keiner, weil niemand weiss,
ob der Rest Absicht war.
Gesprochen wird ueber `speechSynthesis`, im Browser eingebaut: kein
Cloud-Dienst, keine Kosten, kein Zuschauertext, der das Haus verlaesst. Ob
OBS' Browser-Quelle das kann, ist offen - CEF wird teils ohne Sprachausgabe
gebaut. Die Seite prueft es beim Start und blendet einen roten Hinweis ein,
statt still zu bleiben.
Kanalpunkte haben eine eigene Einblendung. Zwischendurch stand hier die des
Abos, womit eine eingeloeste Belohnung als "Neues Abo" im Bild gelandet waere -
eine Falschmeldung, die in der Aufzeichnung bleibt. Ein Test haelt das fest.
`rewards.enabled` ist aus, bis jemand bewusst Belohnungen eintraegt. Eine
Aktion, die niemand erwartet, ist im Stream schlimmer als gar keine.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wann ist ein Stream ein *neuer* Stream? Die Entscheidung stand doppelt in Hub
und Ingest und war ungetestet - dabei haengt genau an ihr, ob der Go-Live-Ping
zum richtigen Zeitpunkt und nur einmal rausgeht.
Jetzt rein in stream-state.ts, mit zehn Tests. Darunter die Faelle, die im
Betrieb selten sind und dann mitten im Stream zuschlagen:
- Neue Twitch-ID nach kurzem Aussetzer ist eine Fortsetzung, kein zweiter
Ping. Twitch vergibt bei einem Verbindungsabbruch eine neue ID.
- Eine einzelne leere Antwort beendet den Stream nicht. Wer daraufhin die
Sitzung schliesst, hat beim naechsten Abruf einen "neuen" Stream.
- Ein Durchlauf ueber 90 Minuten mit zwei Aussetzern, im Minutentakt
abgefragt, ergibt genau einen Start und genau ein Ende.
Dazu App-Token fuer Helix: `GET /users` und `GET /streams` kommen ohne
Nutzer-Token aus. Das ist der Unterschied zwischen "laeuft" und "wartet auf
einen Klick" - ohne diese Unterscheidung stand der ganze Dienst still, bis
der Broadcaster sein Konto verbindet. Endpunkte, die etwas tun oder Privates
lesen, verlangen weiterhin ein Nutzer-Token, und `appOk` sagt je Aufruf, was
gilt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Einblendungen, Chat, Statusleiste, Effekte, Pausenbild. Kein Build, kein
Framework - OBS laedt die Dateien direkt, und jede Zwischenstufe waere ein
Ding, das mitten im Stream kaputtgehen kann.
Server-Sent Events statt WebSocket, entgegen der Skizze in chat-control.md:
Node bringt einen WebSocket-*Client* mit, aber keinen Server, `EventSource`
verbindet sich von allein neu, und die Richtung stimmt - das Overlay hoert
nur zu. Gegen die tote Browser-Source nach einem Neustart stehen drei Dinge:
der eingebaute Reconnect, ein Herzschlag als echtes Ereignis (ein
SSE-Kommentar haelt die Verbindung offen, ist im Browser aber unsichtbar),
und ein Zustands-Schnappschuss beim Verbinden.
Abos, Raids, Bits und Loeschungen kommen ueber IRC-USERNOTICE und CLEARCHAT
herein; das wurde bisher weggeworfen. Follows fehlen weiter, die gibt es nur
ueber EventSub.
GIFs im Chat: Twitch schickt dafuer einen Tag `gifs`, den wir nicht kannten.
gifs=0-22|Pnd7qLFuGCOntZcNKB|https://media0.giphy.com/media/.../giphy.gif
PRIVMSG #abdulive_ :[Idk GIF by Respective]
Der Nachrichtentext ist nur ein Platzhalter, der Bereich markiert ihn.
Ausgewertet wird er jetzt; ohne das stand woertlich "[Idk GIF by Respective]"
im Bild. Gefunden wurde der Tag durch einen Rohmitschnitt - in 3094
Nachrichten aus acht grossen Kanaelen war kein einziges GIF, weil die Adresse
eben nicht im Text steht.
Zwei Fehler, die beim Durchklicken im Browser aufgefallen sind:
- Die Aufraeumfunktion im Chat zaehlte Zeilen, die erst nach der
Ausblend-Animation aus dem DOM verschwinden, und lief in eine
Endlosschleife. In OBS waere die Quelle beim vierzehnten Chatbeitrag
eingefroren.
- Der Bereichsparser der GIFs war zu nachsichtig: `-1-4` zerfaellt beim
Trennen in ['', '1', '4'], und `Number('')` ist 0 - der Bereich saehe
gueltig aus und wuerde den falschen Textteil ersetzen.
Auf der Uebersichtsseite steht fuer jede Einblendung, jede Chat-Variante und
jeden Effekt ein Knopf. Ohne den liesse sich eine Raid-Einblendung nur
pruefen, indem man auf einen echten Raid wartet. Was einen Zustand setzt, ist
waehrend eines Streams gesperrt; "Stand zuruecksetzen" ausdruecklich nicht,
sonst kaeme man aus einem erfundenen Zustand nicht mehr heraus.
build-package.ts nimmt jetzt auch html/css/js mit - sonst liefert das Paket
404 statt Overlay.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Zwei Dinge, die Reichweite bringen sollen, hingen bisher daran, dass jemand
vor seinem Rechner sitzt: der Discord-Rollen-Knopf und die Go-Live-
Ankuendigung. Beide brauchen nur Internet.
Der Schnitt verlaeuft entlang einer Frage: braucht die Aufgabe eine Datei
oder ein Programm, das nur auf dem Stream-PC existiert?
Hub (LXC) Ingest (Abdus PC)
--------- -----------------
Chat, Zuschauerzahlen OBS-Wiedergabepuffer
Discord: Knopf und Go-Live !clip, Rendern, Untertitel
Overlay-Seiten fuer OBS
Beide schreiben in ihre eigene Datenbank. Eine gemeinsame waere eine
Netzwerkfreigabe fuer SQLite - der zuverlaessigste bekannte Weg zu einer
beschaedigten Datei.
Das kostet ADR-0005 seine Hauptbegruendung: "bindet nur auf 127.0.0.1" gilt
fuer den Hub nicht mehr. An die Stelle treten ein Passwort mit signiertem
Sitzungs-Cookie fuer alles, was ein Mensch bedient, und ein zufaelliger
Schluessel in der Adresse fuer die Browser-Sources. Der Schluessel ist kein
schoenes Verfahren - er steht in jedem OBS-Profil -, aber eine Browser-Source
kann kein Formular ausfuellen und keinen Login durchhalten.
`publicUrl` haengt der OAuth-Rueckruf daran. Twitch vergleicht die
Redirect-URI zeichengenau, und ein fehlender Schraegstrich sieht im Fehlertext
aus wie ein falsches Client-Secret.
Die Routen der Einrichtungsseite sind aus ingest/server.ts herausgeloest,
weil sie jetzt an zwei Servern haengen.
Begruendung und Konsequenzen in ADR-0008.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Der billigste Hebel im Repo, und bis jetzt nur Vorarbeit: die Webhooks lagen
seit der Discord-Einrichtung bereit, gepostet hat nie jemand.
Ablauf: neuer Stream erkannt, eine Minute warten (Twitch setzt Titel und
Kategorie erst kurz nach dem Start, wer sofort postet kuendigt den Titel des
letzten Streams an), Daten frisch holen, Webhook posten, Rolle Live-Ping
anpingen.
Der Ping funktioniert nur mit `allowed_mentions` im Nachrichtenkoerper. `<@&ID>`
allein faerbt die Erwaehnung lila ein und benachrichtigt niemanden - der
Fehler, den man erst nach drei leeren Streams bemerkt. Dagegen steht ein
eigener Test.
Gegen den doppelten Post stehen drei Dinge, denn wer zweimal in zehn Minuten
angepingt wird, nimmt sich die Rolle wieder ab und ist damit fuer immer stumm:
- Tabelle `posts` mit eindeutigem Index je Stream und Plattform. Nur
*gesendete* Posts sperren; ein fehlgeschlagener Versuch darf wiederholt
werden, sonst kostet ein Netzwerkaussetzer die ganze Ankuendigung.
- Abklingzeit von sechs Stunden, gegen die neue Stream-ID nach einem
Verbindungsabbruch.
- Ausgeloest wird nur beim erkannten Neustart, nie im Fortsetzungszweig.
`stream_id` ist bewusst kein Fremdschluessel: ein Post ist die Quittung fuer
etwas, das den Rechner verlassen hat, und muss auch dann schreibbar sein,
wenn es die Sitzung nicht gibt - beim Probelauf etwa.
Dazu ein vierter Webhook in #technik-log. Der Entwurfsmodus postet dieselbe
Nachricht dorthin, ohne Ping und nur fuer Mods sichtbar. So sieht man das
Ergebnis, bevor es der Server sieht.
Die Rollenaufloesung aus selfroles.ts liegt jetzt in roles.ts, weil der Ping
sie auch braucht: ein Webhook kann eine Rolle nicht ueber ihren Namen
anpingen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Live-Ping nuetzt nur, wenn man sie sich selbst geben kann. Discords
Onboarding waere der vorgesehene Weg, laesst sich hier aber nicht
einschalten - gemessen, nicht vermutet:
HTTP 400 Cannot enable onboarding, requirements are not met (350000)
sichtbar fuer @everyone: 5 davon beschreibbar: 3
Discord verlangt: 7 davon beschreibbar: 5
Zwei Kanaele anzulegen, damit eine Schwelle passt, macht den Server leerer
statt besser. Stattdessen ein Knopf unter dem Willkommenstext: ein Druck
holt die Rolle, der naechste gibt sie ab, die Antwort sieht nur, wer
geklickt hat.
Der Knopf braucht eine dauerhafte Verbindung, weil Discord Klicks ans
Gateway oder an eine oeffentliche HTTPS-Adresse liefert und der Stream-PC
keine hat:
- gateway.ts WebSocket mit Herzschlag, Fortsetzen und Wiederverbinden.
Bei 4004 und Verwandten wird aufgegeben statt im Kreis zu
laufen - dagegen hilft nur eine Aenderung an der App.
intents: 0, der Bot sieht weder Nachrichten noch Mitglieder.
- selfroles.ts Knopfleiste und Rollenwechsel.
- bot.ts beides zusammen, laeuft im Ingest-Daemon mit. Ohne
hinterlegtes Token entfaellt er wortlos.
Neuer Befehl `abdulive discord:bot` zum Ausprobieren; im Normalbetrieb
haengt der Bot im Daemon und meldet sich als discordConnected auf der
Statusseite.
Die Knopfleiste gehoert zum Soll-Zustand des Willkommenstextes: fehlt sie,
wird sie nachgereicht, ohne die Nachricht neu zu posten.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Der Server ist kein Tool aus der Neunerliste, aber Broadcaster, Recap und
Clipper posten dorthin. Deshalb steht die Struktur als Code im Repo und
nicht nur in einer Oberflaeche.
packages/discord:
- permissions.ts Rechtebits als BigInt, das Feld ist 64 Bit breit
- api.ts REST-Client: Warteschlange, Rate Limits, lesbare Fehler
- plan.ts Rollen, Kanaele, Rechte, AutoMod, Willkommenstext
- apply.ts traegt den Bauplan auf einen bestehenden Server auf
Neuer Befehl `abdulive discord:setup [--server ID] [--vorschau]`.
Zugeschnitten auf 5 bis 30 gleichzeitige Zuschauer: fuenf sichtbare Kanaele
statt zwanzig, und eine selbst vergebene Rolle Live-Ping statt eines
Rundrufs an alle bei jedem Stream.
Wiederholbar statt einmalig: gefunden wird ueber Namen und frueheren Namen,
danach wird angeglichen. Der zweite Lauf aendert nichts. Geloescht wird nie,
und von Hand gesetzte Rechte-Ausnahmen bleiben stehen.
Drei Dinge, die am echten Server nachgemessen und nicht angenommen wurden:
- Discord schreibt Textkanaele klein und macht aus Leerzeichen Bindestriche;
Emoji, Umlaute und U+2502 bleiben. normalizeName() bildet das ab, sonst
geht bei jedem Lauf dieselbe Umbenennung erneut raus.
- Kanalnamen lassen sich nur zweimal je zehn Minuten aendern, danach
verlangt Discord bis zu neun Minuten Pause. Der Lauf wartet hoechstens
eine Minute und meldet den Rest.
- Eine Rolle kann nur Rollen unterhalb der eigenen verschieben, auch mit
Administrator. rolePositions() rechnet den Platz unter der Bot-Rolle aus,
statt blind 3, 2, 1 zu verlangen.
Zugangsdaten laufen ueber den verschluesselten Speicher (ADR-0005): der
Bot-Token wird verdeckt abgefragt, die drei Webhook-URLs werden nie
ausgegeben und nie geloggt.
Im Quelltext bleibt es bei ae/oe/ue, alles was ein Mitglied auf Discord
liest steht in richtigem Deutsch. Ein Test haelt das fest.
Begruendung, Fallstricke und die Schritte von Hand:
docs/projects/discord-community.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Das Repo ist jetzt oeffentlich, die Pakete haengen an der Veroeffentlichung
`latest`. Damit funktioniert `abdulive update` ohne Token.
- config.example.yaml traegt die gepruefte Update-Adresse
- .gitea/workflows/release.yml baut beim Tag und ersetzt `latest`
- Byte Order Mark: Notepad und `Set-Content -Encoding utf8` schreiben eines,
und ohne Behandlung hiess der erste Schluessel in der config.yaml
"\ufeffchannel" - die Einstellung waere stillschweigend ignoriert worden
- Der YAML-Leser hatte gar keine Tests; er liest die Konfiguration, an der
alles haengt. Jetzt acht.
Der Updater ist einmal komplett durchgelaufen: 0.4.0 erkannt, 119 MB geladen,
Pruefsumme geprueft, bereitgelegt, von launcher.ps1 getauscht.
93 Tests, Typpruefung sauber.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhVSfMFdWnXKCv5JfLnArZ
Docker faellt weg. Das Projekt hat keine npm-Abhaengigkeiten und Node fuehrt
TypeScript direkt aus - der Container schrieb nur noch drei Programme fest,
und dafuer braucht es keine Virtualisierung, sondern einen Ordner.
Der Anlass war, dass ADR-0004 zwar `docker compose pull` versprach, es aber
nie ein veroeffentlichtes Image gab. Wer installieren wollte, musste
whisper.cpp aus dem Quelltext uebersetzen.
Plattformschicht (packages/core/src/platform.ts):
- Programme werden neben der Anwendung gesucht, nicht im PATH
- Daten unter %LOCALAPPDATA%, damit Installation und Update ohne
Administratorrechte auskommen
- Schriften mitgeliefert: ohne fontsdir setzt libass wortlos eine
Ersatzschrift ein - gemessen 720 statt 826 Pixel Zeilenbreite
Windows-Eigenheiten, an echtem ffmpeg 8.1 durchprobiert:
- Pfade im Filtergraphen brauchen alle drei Schritte (Vorwaertsschraegstriche,
escapter Doppelpunkt, Anfuehrungszeichen); je zwei davon reichen nicht
- `nice` gibt es nicht, ersetzt durch os.setPriority
Auslieferung:
- scripts/build-package.ts baut das Paket, laeuft auch unter Linux
- install.ps1 ohne Administratorrechte, Autostart ueber den Aufgabenplaner
- update legt neue Fassungen bereit; eingespielt wird beim naechsten Start,
nie mitten im Stream
- ZIP lesen und schreiben ohne Abhaengigkeiten (packages/core/src/zip.ts)
Kein `irm ... | iex`-Einzeiler: git.zeasy.dev antwortete instanzweit mit einer
Umleitung auf /user/login, und `iex` fuehrte die Anmeldeseite als PowerShell
aus. Der Updater verfolgt Umleitungen jetzt nicht und hat keine erfundene
Vorgabeadresse mehr.
Ausserdem: backup.ps1 war an zwei Stellen dokumentiert, aber nie geschrieben.
ADR-0007 ersetzt ADR-0004. 84 Tests, Typpruefung sauber.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhVSfMFdWnXKCv5JfLnArZ
Aus der Rohdatei entsteht das fertige Hochformat. Der Filtergraph liegt jetzt
in packages/media und wird von calibrate und vom Render-Schritt gemeinsam
benutzt - die Zahlen darin sind eingemessen, sie gehoeren an genau eine Stelle.
Eine Aufgabe nach der anderen, und waehrend gesendet wird standardmaessig
gar nicht (renderWhileLive: false). ffmpeg laeuft unter nice, mit Zeitlimit,
und die letzten Zeilen von stderr landen in der Fehlermeldung statt im Nichts.
Aus dem 90-Sekunden-Puffer werden die letzten 45 Sekunden genommen (-sseof vor
-i, sonst wird die ganze Datei dekodiert und danach verworfen).
Dabei einen eigenen Fehler aus S2 korrigiert: die Rohdatei wurde in den
Windows-Bind-Mount kopiert und dort verarbeitet - genau das, was ADR-0004
Punkt 2 verbietet. Jetzt liegt sie im Arbeits-Volume, auf die Windows-Seite
kommt nur das fertige, kleine Ergebnis.
Verifiziert an einer kuenstlichen Aufnahme mit drei getrennten Tonspuren:
1920x1080/70 s hinein, 1080x1920/45 s heraus, Naht an der richtigen Stelle,
27,9 s Rechenzeit unter nice 15. 39 Tests gruen.
Neuer Befehl `render <datei>` zum Nachrendern nach Layout-Aenderungen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013VTGjd92LsDDWF3frd9FTB
Eigener obs-websocket-v5-Client (das Protokoll ist klein genug, und das
Projekt bleibt ohne Abhaengigkeiten). Anmeldung, Wiederverbindung mit
wachsender Pause, Ereignisse fuer ReplayBufferSaved und Szenenwechsel.
Der Clipper loest zwei Wege gleichzeitig aus: OBS-Wiedergabepuffer fuer die
Rohdatei in Aufnahmequalitaet, Twitch-Clip fuer die teilbare URL. Faellt
einer aus, ist der andere trotzdem etwas wert. Ein direkt in OBS gedrueckter
Hotkey wird ohne unseren Umweg erkannt und als vollwertiger Ausloeser
behandelt.
Die Pfaduebersetzung Windows -> Container hat eine eigene Datei und eigene
Tests - das ist die Stelle, an der die Pipeline am ehesten scheitert. Dazu
Warten auf den abgeschlossenen Schreibvorgang, bevor die Datei uebernommen
wird.
Zum Testvektor der OBS-Anmeldung: die Spezifikation nennt Salt und Challenge,
aber kein erwartetes Ergebnis. Der Erwartungswert wurde unabhaengig mit
Pythons hashlib aus derselben Beschreibung gerechnet; der Test sagt das im
Kommentar, statt einen amtlichen Vektor vorzutaeuschen.
29 Tests gruen. Die Abnahme von S2 braucht ein laufendes OBS und steht aus.
Ausserdem: ADR-0006 haelt fest, dass alles unter einem Dach laeuft - eine
lokale Weboberflaeche plus Kindprozesse fuer Rechenarbeit. Die Prozessliste
in der Architekturdoku ist entsprechend ersetzt. Der Onboarding-Assistent
ist als Teil von S5 aufgenommen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013VTGjd92LsDDWF3frd9FTB
Zugangsdaten kommen jetzt aus der App statt aus einer .env: ein
Einrichtungs-Assistent auf http://localhost:4792/setup nimmt Client-ID,
Secret und OBS-Passwort entgegen, der OAuth-Rueckruf laeuft auf denselben
Server. Gespeichert wird verschlueselt (AES-256-GCM) in einem Volume,
bewusst nicht in der Datenbank - die wird auf die Windows-Seite gesichert.
Siehe ADR-0005.
Technikbasis vereinfacht: Node 24 bringt node:sqlite, WebSocket, fetch und
die Ausfuehrung von TypeScript mit. Das Projekt hat damit null
Laufzeitabhaengigkeiten und keinen Build-Schritt; pnpm-Workspaces und die
Paketmanifeste sind entfallen, Importe laufen ueber die imports-Map der
Wurzel-package.json. erasableSyntaxOnly verhindert Syntax, die das Type
Stripping stillschweigend entfernen wuerde. Siehe Nachtrag 2 in ADR-0001.
Enthalten:
- packages/core: Config, Layout, Logger mit Secret-Maskierung, Retry, YAML
- packages/db: Migrationen, Repositories, alle SQL-Anweisungen an einem Ort
- packages/twitch: Secret-Speicher, OAuth, Helix mit Token-Bucket, IRC
- apps/ingest: Daemon, Einrichtungsseite, Spike-Erkennung
- 20 Tests (IRC-Parser, Spike-Detektor, Datenbankschicht)
Der Chat verbindet sich anonym, solange nichts hinterlegt ist - damit laesst
sich die Datenaufnahme pruefen, bevor die Twitch-Apps existieren.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013VTGjd92LsDDWF3frd9FTB
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