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>
44 lines
1.5 KiB
TypeScript
44 lines
1.5 KiB
TypeScript
// Fertig gesprochene Nachrichten, bis die Browser-Quelle sie abholt.
|
|
//
|
|
// Warum nicht direkt im Ereignis: eine WAV von 200 Zeichen hat gut 500 KB.
|
|
// Der Ereignisstrom geht an alle Browser-Sources, auch an Chat und
|
|
// Statusleiste - die sollen das nicht mitschleppen. Also geht nur die ID
|
|
// hinaus, und `/tts` holt sich den Ton einzeln.
|
|
//
|
|
// Bewusst im Arbeitsspeicher und kurzlebig: niemand braucht eine vorgelesene
|
|
// Nachricht von gestern, und fremder Text soll nicht auf der Platte liegen.
|
|
|
|
export type AudioAblage = {
|
|
ablegen: (id: string, wav: Buffer) => void;
|
|
holen: (id: string) => Buffer | undefined;
|
|
leeren: () => void;
|
|
};
|
|
|
|
export function createAudioAblage(opts: { maximal?: number; haltbarMs?: number } = {}): AudioAblage {
|
|
const maximal = opts.maximal ?? 20;
|
|
const haltbarMs = opts.haltbarMs ?? 10 * 60_000;
|
|
// Map haelt die Einfuegereihenfolge; das Aelteste steht vorn.
|
|
const ablage = new Map<string, { wav: Buffer; seit: number }>();
|
|
|
|
const aufraeumen = (jetzt: number): void => {
|
|
for (const [id, e] of ablage) if (jetzt - e.seit > haltbarMs) ablage.delete(id);
|
|
while (ablage.size > maximal) {
|
|
const aeltestes = ablage.keys().next().value;
|
|
if (aeltestes === undefined) break;
|
|
ablage.delete(aeltestes);
|
|
}
|
|
};
|
|
|
|
return {
|
|
ablegen(id, wav) {
|
|
ablage.set(id, { wav, seit: Date.now() });
|
|
aufraeumen(Date.now());
|
|
},
|
|
holen(id) {
|
|
aufraeumen(Date.now());
|
|
return ablage.get(id)?.wav;
|
|
},
|
|
leeren: () => ablage.clear(),
|
|
};
|
|
}
|