Let streamers name their AI assistant; fix overlay TTS audio overlap
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 30s

Streamer dashboard gains an editable AI assistant name (default
"Альтушка Ирина"), stored per-streamer and used for the overlay label,
TTS intro, and DeepSeek system prompt. Also fixes a bug where a long
message's TTS audio could still be playing when the next queued
message started its own audio — the overlay queue now waits for the
current audio to actually finish (not just the configured display
duration) before advancing.
This commit is contained in:
vrubelroman 2026-07-08 17:28:36 +00:00
parent fec77e2737
commit ec6d8c4cfe
6 changed files with 106 additions and 22 deletions

View file

@ -45,9 +45,15 @@ function playNotificationSound() {
}
}
function playSpeech(base64Audio, mimeType) {
function playSpeech(base64Audio, mimeType, onEnded) {
const audio = new Audio(`data:${mimeType};base64,${base64Audio}`);
audio.play().catch((err) => console.error('Could not play TTS audio:', err));
const done = () => { if (onEnded) onEnded(); };
audio.addEventListener('ended', done);
audio.addEventListener('error', done);
audio.play().catch((err) => {
console.error('Could not play TTS audio:', err);
done();
});
}
function advanceQueue() {
@ -62,14 +68,37 @@ function advanceQueue() {
box.classList.add('visible');
playNotificationSound();
if (current.audio) {
playSpeech(current.audio.data, current.audio.mimeType);
}
setTimeout(() => {
// The message stays up for at least durationMs (streamer's configured
// display time), but if its TTS audio is still playing when that timer
// fires (e.g. a long question read aloud), we must not start the next
// message's audio on top of it — so we also wait for the audio to
// actually finish before advancing.
let minTimeElapsed = false;
let audioFinished = !current.audio;
const maybeAdvance = () => {
if (!minTimeElapsed || !audioFinished) return;
box.classList.remove('visible');
current = null;
setTimeout(advanceQueue, FADE_MS);
};
current.startAudio = (audio) => {
audioFinished = false;
playSpeech(audio.data, audio.mimeType, () => {
audioFinished = true;
maybeAdvance();
});
};
if (current.audio) {
current.startAudio(current.audio);
}
setTimeout(() => {
minTimeElapsed = true;
maybeAdvance();
}, current.durationMs || 10000);
}
@ -87,7 +116,7 @@ socket.on('display_message', (payload) => {
socket.on('display_audio', (payload) => {
const audio = { data: payload.audio, mimeType: payload.mimeType };
if (current && current.id === payload.id) {
playSpeech(audio.data, audio.mimeType);
current.startAudio(audio);
return;
}
const queued = queue.find((item) => item.id === payload.id);