feat(voice): migrate to push2talk and serialize websocket text sends
This commit is contained in:
@@ -21,29 +21,22 @@ static const char *s_web_index =
|
||||
" .box { max-width: 760px; margin: 0 auto; background:#fff; border:1px solid #dfe6ee; border-radius:12px; padding:16px; }\n"
|
||||
" h1 { margin: 0 0 8px 0; font-size: 22px; }\n"
|
||||
" p { margin: 6px 0 12px 0; color:#4b5d73; }\n"
|
||||
" .row { display:flex; gap:8px; flex-wrap:wrap; margin: 8px 0; }\n"
|
||||
" button { border:0; border-radius:8px; padding:10px 14px; cursor:pointer; background:#198754; color:#fff; font: inherit; min-width:220px; }\n"
|
||||
" button.active { background:#d68910; }\n"
|
||||
" button:disabled { background:#5f6f82; cursor:default; }\n"
|
||||
" .hint { font-size: 13px; color:#29583f; background:#e7f4ec; border:1px solid #cbe4d5; border-radius:8px; padding:10px; margin: 8px 0 12px 0; }\n"
|
||||
" .small { font-size: 13px; color:#44566c; }\n"
|
||||
" </style>\n"
|
||||
"</head>\n"
|
||||
"<body>\n"
|
||||
" <div class=\"box\">\n"
|
||||
" <h1>TQ Voice Console</h1>\n"
|
||||
" <p>ESP32-S3 本地语音控制页。用于单轮语音对话(Tap2Talk)。</p>\n"
|
||||
" <p>ESP32-S3 本地语音控制页。已切换为 MIC_KEY(IO14)触发单轮语音对话。</p>\n"
|
||||
"\n"
|
||||
" <div class=\"row\">\n"
|
||||
" <button id=\"btnVoiceTalk\">点击开始单轮对话</button>\n"
|
||||
" </div>\n"
|
||||
" <div class=\"hint\">按住机身 MIC_KEY 说话,松开后自动进入“语音交互→生图→显示→处理→打印”。</div>\n"
|
||||
" <div id=\"voiceState\" class=\"small\">语音状态:初始化中</div>\n"
|
||||
" </div>\n"
|
||||
"\n"
|
||||
" <script>\n"
|
||||
" const btnVoiceTalkEl = document.getElementById(\"btnVoiceTalk\");\n"
|
||||
" const voiceStateEl = document.getElementById(\"voiceState\");\n"
|
||||
"\n"
|
||||
" let voiceTalkBusy = false;\n"
|
||||
" let voiceStatusSyncing = false;\n"
|
||||
"\n"
|
||||
" function headers(withJson) {\n"
|
||||
@@ -52,10 +45,6 @@ static const char *s_web_index =
|
||||
" return h;\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" function sleep(ms) {\n"
|
||||
" return new Promise((resolve) => setTimeout(resolve, ms));\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" async function callApi(path, method, body) {\n"
|
||||
" const init = { method, headers: headers(body !== undefined) };\n"
|
||||
" if (body !== undefined) init.body = JSON.stringify(body);\n"
|
||||
@@ -69,27 +58,17 @@ static const char *s_web_index =
|
||||
" return data;\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) return \"语音状态:会话未启动\";\n"
|
||||
" if (!st.voice_ws_connected) return \"语音状态:会话断开,点击开始将自动重建\";\n"
|
||||
" if (!st.voice_ws_connected) return \"语音状态:会话断开,按 MIC_KEY 将自动重建\";\n"
|
||||
" if (st.voice_tap_active) return \"语音状态:录音中\";\n"
|
||||
" if (state === \"thinking\" || state === \"responding\") return \"语音状态:语音处理中\";\n"
|
||||
" if (!!st.voice_marker_image_gen_running) return \"语音状态:图片生成中\";\n"
|
||||
" if (!!st.printer_busy || queueDepth > 0) return \"语音状态:打印处理中\";\n"
|
||||
" return \"语音状态:空闲,可开始下一轮\";\n"
|
||||
" return \"语音状态:空闲,等待 MIC_KEY 触发\";\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" function voiceErrorText(st) {\n"
|
||||
@@ -99,138 +78,21 @@ static const char *s_web_index =
|
||||
" 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\") return \"语音处理中,请等待\";\n"
|
||||
" if (!!st.voice_marker_image_gen_running) return \"图片生成中,请等待\";\n"
|
||||
" const queueDepth = Number(st.queue_depth || 0);\n"
|
||||
" if (!!st.printer_busy || queueDepth > 0) return \"打印处理中,请等待\";\n"
|
||||
" return \"处理中,请等待\";\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" function setVoiceTalkButtonState(active) {\n"
|
||||
" if (active) {\n"
|
||||
" btnVoiceTalkEl.classList.add(\"active\");\n"
|
||||
" btnVoiceTalkEl.textContent = \"点击取消本轮录音\";\n"
|
||||
" } else {\n"
|
||||
" btnVoiceTalkEl.classList.remove(\"active\");\n"
|
||||
" btnVoiceTalkEl.textContent = \"点击开始单轮对话\";\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" function syncVoiceTalkUi(st) {\n"
|
||||
" const active = !!(st && st.voice_tap_active && st.voice_ws_connected && st.voice_started);\n"
|
||||
" const busy = voiceRoundBusy(st);\n"
|
||||
" let status = voiceStatusText(st);\n"
|
||||
" const err = voiceErrorText(st);\n"
|
||||
" if (err) status += \";\" + err;\n"
|
||||
" if (voiceStateEl) voiceStateEl.textContent = status;\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"
|
||||
" function voiceReady(st) {\n"
|
||||
" return !!(st && st.voice_session_active && st.voice_ws_connected && st.voice_started);\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" async function ensureVoiceSessionReady() {\n"
|
||||
" let st = await callApi(\"/v1/voice/status\", \"GET\");\n"
|
||||
" syncVoiceTalkUi(st);\n"
|
||||
" if (voiceReady(st)) return st;\n"
|
||||
"\n"
|
||||
" await callApi(\"/v1/voice/session/start\", \"POST\", {});\n"
|
||||
" const deadline = Date.now() + 10000;\n"
|
||||
" while (Date.now() < deadline) {\n"
|
||||
" await sleep(250);\n"
|
||||
" st = await callApi(\"/v1/voice/status\", \"GET\");\n"
|
||||
" syncVoiceTalkUi(st);\n"
|
||||
" if (voiceReady(st)) return st;\n"
|
||||
" }\n"
|
||||
" throw new Error(\"voice session is not ready, please retry\");\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" async function refreshVoiceStatus() {\n"
|
||||
" if (voiceStatusSyncing) return null;\n"
|
||||
" voiceStatusSyncing = true;\n"
|
||||
" try {\n"
|
||||
" const st = await callApi(\"/v1/voice/status\", \"GET\");\n"
|
||||
" syncVoiceTalkUi(st);\n"
|
||||
" let status = voiceStatusText(st);\n"
|
||||
" const err = voiceErrorText(st);\n"
|
||||
" if (err) status += \";\" + err;\n"
|
||||
" if (voiceStateEl) voiceStateEl.textContent = status;\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"
|
||||
" syncVoiceTalkUi(st);\n"
|
||||
" return st;\n"
|
||||
" }\n"
|
||||
" voiceTalkBusy = true;\n"
|
||||
" try {\n"
|
||||
" let st = await ensureVoiceSessionReady();\n"
|
||||
" if (st && st.voice_tap_active) {\n"
|
||||
" syncVoiceTalkUi(st);\n"
|
||||
" return st;\n"
|
||||
" }\n"
|
||||
" await callApi(\"/v1/voice/tap/start\", \"POST\", {});\n"
|
||||
" st = await callApi(\"/v1/voice/status\", \"GET\");\n"
|
||||
" syncVoiceTalkUi(st);\n"
|
||||
" return st;\n"
|
||||
" } finally {\n"
|
||||
" voiceTalkBusy = false;\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" async function endVoiceTalk() {\n"
|
||||
" if (voiceTalkBusy) {\n"
|
||||
" const st = await callApi(\"/v1/voice/status\", \"GET\");\n"
|
||||
" syncVoiceTalkUi(st);\n"
|
||||
" return st;\n"
|
||||
" }\n"
|
||||
" voiceTalkBusy = true;\n"
|
||||
" try {\n"
|
||||
" let st = await callApi(\"/v1/voice/status\", \"GET\");\n"
|
||||
" syncVoiceTalkUi(st);\n"
|
||||
" if (!(st && st.voice_tap_active)) return st;\n"
|
||||
" await callApi(\"/v1/voice/tap/cancel\", \"POST\", {});\n"
|
||||
" st = await callApi(\"/v1/voice/status\", \"GET\");\n"
|
||||
" syncVoiceTalkUi(st);\n"
|
||||
" return st;\n"
|
||||
" } finally {\n"
|
||||
" voiceTalkBusy = false;\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" btnVoiceTalkEl.addEventListener(\"click\", async () => {\n"
|
||||
" try {\n"
|
||||
" const st = await refreshVoiceStatus();\n"
|
||||
" if (!st) return;\n"
|
||||
" if (voiceRoundBusy(st)) return;\n"
|
||||
" if (st && st.voice_tap_active) {\n"
|
||||
" await endVoiceTalk();\n"
|
||||
" } else {\n"
|
||||
" await beginVoiceTalk();\n"
|
||||
" }\n"
|
||||
" } catch (e) {\n"
|
||||
" let msg = \"语音状态:请求失败\";\n"
|
||||
" if (e && e.data && typeof e.data === \"object\" && e.data.error) {\n"
|
||||
" msg += \";\" + String(e.data.error);\n"
|
||||
" }\n"
|
||||
" voiceStateEl.textContent = msg;\n"
|
||||
" }\n"
|
||||
" });\n"
|
||||
"\n"
|
||||
" setVoiceTalkButtonState(false);\n"
|
||||
" (async () => {\n"
|
||||
" try { await refreshVoiceStatus(); } catch (_) {}\n"
|
||||
" })();\n"
|
||||
|
||||
@@ -204,7 +204,7 @@ esp_err_t rest_server_voice_tap_cancel_post(httpd_req_t *req) {
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "ok", true);
|
||||
cJSON_AddStringToObject(root, "message", "tap canceled");
|
||||
cJSON_AddStringToObject(root, "message", "tap stopped");
|
||||
rest_server_add_voice_status(root);
|
||||
|
||||
esp_err_t err = rest_server_send_json(req, "200 OK", root);
|
||||
|
||||
Reference in New Issue
Block a user