From 729f6eb47eb3021107a8025fb3e86268f460654c Mon Sep 17 00:00:00 2001 From: vrubelroman Date: Wed, 1 Jul 2026 17:39:33 +0000 Subject: [PATCH] Hold viewer message until AI reply is ready, when requested Previously the viewer's message displayed immediately and the AI reply showed up whenever DeepSeek happened to respond, landing with an awkward gap. Now, when the AI checkbox is checked, both texts are fetched/prepared before anything is broadcast, so the original message and the AI reply queue back-to-back on the overlay. Falls back to just showing the original message if the AI call fails. Behavior without the checkbox is unchanged (still immediate). Co-Authored-By: Claude Sonnet 5 --- ai-prompt.txt | 3 +- server.js | 78 ++++++++++++++++++++++++--------------------------- 2 files changed, 39 insertions(+), 42 deletions(-) diff --git a/ai-prompt.txt b/ai-prompt.txt index 670d42f..7cf7056 100644 --- a/ai-prompt.txt +++ b/ai-prompt.txt @@ -2,4 +2,5 @@ адресованное стримеру. Отвечай коротко (1-3 предложения), живо и по делу — твой ответ будет прочитан вслух на стриме и показан на экране, поэтому не пиши длинных текстов, списков или markdown-разметки. -При этом старайся шутить. +При этом старайся шутить. +Не допускать матных слов. diff --git a/server.js b/server.js index f62837e..3654318 100644 --- a/server.js +++ b/server.js @@ -182,6 +182,22 @@ function synthesizeSpeech(text, voice) { }); } +function broadcastMessage(room, id, text, from, durationMs, ttsText, voice) { + io.to(room).emit('display_message', { id, text, from, at: Date.now(), durationMs }); + + synthesizeSpeech(ttsText, voice) + .then((audioBuffer) => { + io.to(room).emit('display_audio', { + id, + audio: audioBuffer.toString('base64'), + mimeType: 'audio/mpeg', + }); + }) + .catch((err) => { + console.error('[tts] synthesis failed:', err); + }); +} + function getAiSystemPrompt() { try { const content = fs.readFileSync(AI_PROMPT_PATH, 'utf8').trim(); @@ -264,59 +280,39 @@ io.on('connection', (socket) => { const messageId = crypto.randomUUID(); const room = `streamer-${streamer.id}`; + const durationMs = (streamer.display_duration_seconds || 10) * 1000; console.log(`[broadcast] streamer=${streamer.id} id=${messageId} from="${displayName}" "${message}"`); - io.to(room).emit('display_message', { - id: messageId, - text: message, - from: displayName, - at: Date.now(), - durationMs: (streamer.display_duration_seconds || 10) * 1000, - }); - // Voice is generated after the fact so the text shows up immediately - // instead of waiting on the round trip to the TTS service. The overlay - // queues messages and matches this to the right one by id, since it may - // arrive before that message's turn to display. - synthesizeSpeech(`Сообщение от ${displayName}. ${message}`, streamer.tts_voice || DEFAULT_TTS_VOICE) - .then((audioBuffer) => { - io.to(room).emit('display_audio', { - id: messageId, - audio: audioBuffer.toString('base64'), - mimeType: 'audio/mpeg', - }); - }) - .catch((err) => { - console.error('[tts] synthesis failed:', err); - }); + const broadcastOriginal = () => { + broadcastMessage( + room, messageId, message, displayName, durationMs, + `Сообщение от ${displayName}. ${message}`, + streamer.tts_voice || DEFAULT_TTS_VOICE + ); + }; if (aiReply && DEEPSEEK_API_KEY) { + // Wait for the AI reply text before showing anything, so the viewer's + // message and the AI reply land in the overlay queue back-to-back + // instead of the reply trailing in later with an awkward gap. getAiReply(message) .then((replyText) => { + broadcastOriginal(); + const aiMessageId = crypto.randomUUID(); console.log(`[ai] streamer=${streamer.id} id=${aiMessageId} reply="${replyText}"`); - io.to(room).emit('display_message', { - id: aiMessageId, - text: replyText, - from: '🤖 ИИ', - at: Date.now(), - durationMs: (streamer.display_duration_seconds || 10) * 1000, - }); - - synthesizeSpeech(`Ответ от ИИ. ${replyText}`, AI_VOICE) - .then((audioBuffer) => { - io.to(room).emit('display_audio', { - id: aiMessageId, - audio: audioBuffer.toString('base64'), - mimeType: 'audio/mpeg', - }); - }) - .catch((err) => { - console.error('[tts] AI reply synthesis failed:', err); - }); + broadcastMessage( + room, aiMessageId, replyText, '🤖 ИИ', durationMs, + `Ответ от ИИ. ${replyText}`, + AI_VOICE + ); }) .catch((err) => { console.error('[ai] DeepSeek reply failed:', err); + broadcastOriginal(); }); + } else { + broadcastOriginal(); } }); });