fix(voice): stabilize single-round flow and sync web UI state
This commit is contained in:
@@ -105,11 +105,13 @@ static const char *s_web_index =
|
||||
" </div>\n"
|
||||
" <div class='small'>Submit prompt only: ESP32-S3 calls DashScope Z-Image, downloads PNG, rasterizes and prints.</div>\n"
|
||||
"\n"
|
||||
" <label>Voice Tap2Talk</label>\n"
|
||||
" <label>Voice 单轮对话</label>\n"
|
||||
" <div class='row'>\n"
|
||||
" <button id='btnVoiceTalk' class='talk'>点击开始对话</button>\n"
|
||||
" <button id='btnVoiceTalk' class='talk'>点击开始单轮对话</button>\n"
|
||||
" </div>\n"
|
||||
" <div class='small'>点击一次开始语音输入,再点击一次结束(自动建立语音会话)。</div>\n"
|
||||
" <div id='voiceState' class='small'>语音状态:初始化中</div>\n"
|
||||
" <div id='voiceErr' class='small' style='color:#b00020;'></div>\n"
|
||||
" <div class='small'>单击开始本轮语音输入;识别结束后自动停止上传,等待语音播报和打印完成。</div>\n"
|
||||
"\n"
|
||||
" <div class='small'>Tip: connect printer first, then submit print jobs.</div>\n"
|
||||
" <pre id='out'>Ready.</pre>\n"
|
||||
@@ -131,9 +133,12 @@ static const char *s_web_index =
|
||||
" const imgInvertEl = document.getElementById('imgInvert');\n"
|
||||
" const imgPromptExtendEl = document.getElementById('imgPromptExtend');\n"
|
||||
" const btnVoiceTalkEl = document.getElementById('btnVoiceTalk');\n"
|
||||
" const voiceStateEl = document.getElementById('voiceState');\n"
|
||||
" const voiceErrEl = document.getElementById('voiceErr');\n"
|
||||
" const thermalPromptSuffix = 'black and white line art, pure white background, high contrast, clean edges, minimal texture, no dark background, no heavy shadows, centered subject, thermal printer friendly';\n"
|
||||
" let voiceTalkHolding = false;\n"
|
||||
" let voiceTalkBusy = false;\n"
|
||||
" let voiceStatusSyncing = false;\n"
|
||||
"\n"
|
||||
" function log(obj) {\n"
|
||||
" if (typeof obj === 'string') {\n"
|
||||
@@ -163,19 +168,95 @@ static const char *s_web_index =
|
||||
" return new Promise((resolve) => setTimeout(resolve, ms));\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" function voiceRoundBusy(st) {\n"
|
||||
" if (!st) return false;\n"
|
||||
" const state = (st.voice_dialog_state || '').toLowerCase();\n"
|
||||
" const voiceBusy = state === 'thinking' || state === 'responding';\n"
|
||||
" const markerBusy = !!st.voice_marker_image_gen_running;\n"
|
||||
" const queueDepth = Number(st.queue_depth || 0);\n"
|
||||
" const printBusy = !!st.printer_busy || queueDepth > 0;\n"
|
||||
" return voiceBusy || markerBusy || printBusy;\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" function voiceStatusText(st) {\n"
|
||||
" if (!st) return '语音状态:未知';\n"
|
||||
" const state = (st.voice_dialog_state || '').toLowerCase();\n"
|
||||
" const queueDepth = Number(st.queue_depth || 0);\n"
|
||||
" if (!st.voice_session_active) {\n"
|
||||
" return '语音状态:会话未启动';\n"
|
||||
" }\n"
|
||||
" if (!st.voice_ws_connected) {\n"
|
||||
" return '语音状态:会话断开,点击开始将自动重建';\n"
|
||||
" }\n"
|
||||
" if (st.voice_tap_active) {\n"
|
||||
" return '语音状态:录音中';\n"
|
||||
" }\n"
|
||||
" if (state === 'thinking' || state === 'responding') {\n"
|
||||
" return '语音状态:语音处理中';\n"
|
||||
" }\n"
|
||||
" if (!!st.voice_marker_image_gen_running) {\n"
|
||||
" return '语音状态:图片生成中';\n"
|
||||
" }\n"
|
||||
" if (!!st.printer_busy || queueDepth > 0) {\n"
|
||||
" return '语音状态:打印处理中';\n"
|
||||
" }\n"
|
||||
" return '语音状态:空闲,可开始下一轮';\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" function voiceErrorText(st) {\n"
|
||||
" if (!st || st.voice_last_error === undefined || st.voice_last_error === null) {\n"
|
||||
" return '';\n"
|
||||
" }\n"
|
||||
" const msg = String(st.voice_last_error).trim();\n"
|
||||
" if (!msg || msg === '-') {\n"
|
||||
" return '';\n"
|
||||
" }\n"
|
||||
" return '最近错误:' + msg;\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" function voiceRoundBusyText(st) {\n"
|
||||
" if (!st) return '处理中,请等待';\n"
|
||||
" const state = (st.voice_dialog_state || '').toLowerCase();\n"
|
||||
" if (state === 'thinking' || state === 'responding') {\n"
|
||||
" return '语音处理中,请等待';\n"
|
||||
" }\n"
|
||||
" if (!!st.voice_marker_image_gen_running) {\n"
|
||||
" return '图片生成中,请等待';\n"
|
||||
" }\n"
|
||||
" const queueDepth = Number(st.queue_depth || 0);\n"
|
||||
" if (!!st.printer_busy || queueDepth > 0) {\n"
|
||||
" return '打印处理中,请等待';\n"
|
||||
" }\n"
|
||||
" return '处理中,请等待';\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" function setVoiceTalkButtonState(active) {\n"
|
||||
" if (active) {\n"
|
||||
" btnVoiceTalkEl.classList.add('active');\n"
|
||||
" btnVoiceTalkEl.textContent = '点击结束对话';\n"
|
||||
" btnVoiceTalkEl.textContent = '点击取消本轮录音';\n"
|
||||
" } else {\n"
|
||||
" btnVoiceTalkEl.classList.remove('active');\n"
|
||||
" btnVoiceTalkEl.textContent = '点击开始对话';\n"
|
||||
" btnVoiceTalkEl.textContent = '点击开始单轮对话';\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" function syncVoiceTalkUi(st) {\n"
|
||||
" const active = !!(st && st.voice_tap_active);\n"
|
||||
" const active = !!(st && st.voice_tap_active && st.voice_ws_connected && st.voice_started);\n"
|
||||
" const busy = voiceRoundBusy(st);\n"
|
||||
" voiceTalkHolding = active;\n"
|
||||
" if (voiceStateEl) {\n"
|
||||
" voiceStateEl.textContent = voiceStatusText(st);\n"
|
||||
" }\n"
|
||||
" if (voiceErrEl) {\n"
|
||||
" voiceErrEl.textContent = voiceErrorText(st);\n"
|
||||
" }\n"
|
||||
" if (busy) {\n"
|
||||
" btnVoiceTalkEl.classList.remove('active');\n"
|
||||
" btnVoiceTalkEl.disabled = true;\n"
|
||||
" btnVoiceTalkEl.textContent = voiceRoundBusyText(st);\n"
|
||||
" return;\n"
|
||||
" }\n"
|
||||
" btnVoiceTalkEl.disabled = false;\n"
|
||||
" setVoiceTalkButtonState(active);\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
@@ -204,6 +285,20 @@ static const char *s_web_index =
|
||||
" throw new Error('voice session is not ready, please retry');\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" async function refreshVoiceStatus() {\n"
|
||||
" if (voiceStatusSyncing) {\n"
|
||||
" return null;\n"
|
||||
" }\n"
|
||||
" voiceStatusSyncing = true;\n"
|
||||
" try {\n"
|
||||
" const st = await callApi('/v1/voice/status', 'GET');\n"
|
||||
" syncVoiceTalkUi(st);\n"
|
||||
" return st;\n"
|
||||
" } finally {\n"
|
||||
" voiceStatusSyncing = false;\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" async function beginVoiceTalk() {\n"
|
||||
" if (voiceTalkBusy) {\n"
|
||||
" const st = await callApi('/v1/voice/status', 'GET');\n"
|
||||
@@ -327,16 +422,23 @@ static const char *s_web_index =
|
||||
" });\n"
|
||||
"\n"
|
||||
" btnVoiceTalkEl.addEventListener('click', () => run(async () => {\n"
|
||||
" const st = await callApi('/v1/voice/status', 'GET');\n"
|
||||
" syncVoiceTalkUi(st);\n"
|
||||
" const st = await refreshVoiceStatus();\n"
|
||||
" if (!st) {\n"
|
||||
" return null;\n"
|
||||
" }\n"
|
||||
" if (voiceRoundBusy(st)) {\n"
|
||||
" return st;\n"
|
||||
" }\n"
|
||||
" if (st && st.voice_tap_active) {\n"
|
||||
" return endVoiceTalk();\n"
|
||||
" }\n"
|
||||
" return beginVoiceTalk();\n"
|
||||
" }));\n"
|
||||
" window.addEventListener('blur', () => run(async () => {\n"
|
||||
" const st = await callApi('/v1/voice/status', 'GET');\n"
|
||||
" syncVoiceTalkUi(st);\n"
|
||||
" const st = await refreshVoiceStatus();\n"
|
||||
" if (!st) {\n"
|
||||
" return null;\n"
|
||||
" }\n"
|
||||
" if (st && st.voice_tap_active) {\n"
|
||||
" return endVoiceTalk();\n"
|
||||
" }\n"
|
||||
@@ -345,10 +447,16 @@ static const char *s_web_index =
|
||||
" setVoiceTalkButtonState(false);\n"
|
||||
" (async () => {\n"
|
||||
" try {\n"
|
||||
" const st = await callApi('/v1/voice/status', 'GET');\n"
|
||||
" syncVoiceTalkUi(st);\n"
|
||||
" await refreshVoiceStatus();\n"
|
||||
" } catch (_) {}\n"
|
||||
" })();\n"
|
||||
" setInterval(() => {\n"
|
||||
" refreshVoiceStatus().catch(() => {\n"
|
||||
" if (voiceStateEl) {\n"
|
||||
" voiceStateEl.textContent = '语音状态:状态获取失败';\n"
|
||||
" }\n"
|
||||
" });\n"
|
||||
" }, 1500);\n"
|
||||
" </script>\n"
|
||||
"</body>\n"
|
||||
"</html>\n";
|
||||
|
||||
@@ -7,12 +7,15 @@
|
||||
static void rest_server_add_voice_status(cJSON *root) {
|
||||
voice_interaction_status_t st = {0};
|
||||
voice_interaction_get_status(&st);
|
||||
printer_runtime_status_t printer = {0};
|
||||
printer_protocol_get_runtime_status(&printer);
|
||||
|
||||
cJSON_AddBoolToObject(root, "voice_ready", st.ready);
|
||||
cJSON_AddBoolToObject(root, "voice_session_active", st.session_active);
|
||||
cJSON_AddBoolToObject(root, "voice_ws_connected", st.ws_connected);
|
||||
cJSON_AddBoolToObject(root, "voice_started", st.started);
|
||||
cJSON_AddBoolToObject(root, "voice_tap_active", st.tap_active);
|
||||
cJSON_AddBoolToObject(root, "voice_marker_image_gen_running", st.marker_image_gen_running);
|
||||
cJSON_AddStringToObject(root,
|
||||
"voice_dialog_state",
|
||||
voice_interaction_dialog_state_str(st.dialog_state));
|
||||
@@ -22,6 +25,8 @@ static void rest_server_add_voice_status(cJSON *root) {
|
||||
cJSON_AddNumberToObject(root, "voice_last_event_ms", (double)st.last_event_ms);
|
||||
cJSON_AddNumberToObject(root, "voice_upstream_packets", (double)st.upstream_packets);
|
||||
cJSON_AddNumberToObject(root, "voice_downstream_packets", (double)st.downstream_packets);
|
||||
cJSON_AddBoolToObject(root, "printer_busy", printer.busy);
|
||||
cJSON_AddNumberToObject(root, "queue_depth", (double)printer.queue_depth);
|
||||
}
|
||||
|
||||
esp_err_t rest_server_voice_status_get(httpd_req_t *req) {
|
||||
|
||||
Reference in New Issue
Block a user