语音对话

This commit is contained in:
admin
2026-02-25 11:16:30 +08:00
parent e9ac05592c
commit 9d060f9f40
5 changed files with 612 additions and 58 deletions

View File

@@ -30,6 +30,8 @@ static const char *s_web_index =
" button { border:0; border-radius:8px; padding:8px 12px; cursor:pointer; background:#0b66d0; color:#fff; }\n"
" button.alt { background:#5f6f82; }\n"
" button.warn { background:#cf2f2f; }\n"
" button.talk { background:#198754; touch-action:none; user-select:none; }\n"
" button.talk.active { background:#d68910; }\n"
" pre { background:#0f1720; color:#d6e2f5; border-radius:8px; padding:10px; overflow:auto; min-height:120px; }\n"
" .small { font-size: 12px; color:#6a778a; }\n"
" label { font-size:13px; color:#44566c; display:block; margin: 10px 0 4px 0; }\n"
@@ -102,6 +104,12 @@ 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"
" <div class='row'>\n"
" <button id='btnVoiceTalk' class='talk'>点击开始对话</button>\n"
" </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"
" </div>\n"
@@ -121,7 +129,10 @@ static const char *s_web_index =
" const imgScaleToWidthEl = document.getElementById('imgScaleToWidth');\n"
" const imgInvertEl = document.getElementById('imgInvert');\n"
" const imgPromptExtendEl = document.getElementById('imgPromptExtend');\n"
" const btnVoiceTalkEl = document.getElementById('btnVoiceTalk');\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"
"\n"
" function log(obj) {\n"
" if (typeof obj === 'string') {\n"
@@ -147,6 +158,97 @@ static const char *s_web_index =
" return n;\n"
" }\n"
"\n"
" function sleep(ms) {\n"
" return new Promise((resolve) => setTimeout(resolve, ms));\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);\n"
" voiceTalkHolding = active;\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)) {\n"
" return st;\n"
" }\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)) {\n"
" return st;\n"
" }\n"
" }\n"
"\n"
" throw new Error('voice session is not ready, please retry');\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"
"\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"
"\n"
" voiceTalkBusy = true;\n"
" try {\n"
" let st = await callApi('/v1/voice/status', 'GET');\n"
" syncVoiceTalkUi(st);\n"
" if (!(st && st.voice_tap_active)) {\n"
" return st;\n"
" }\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"
" function buildImagePromptRequest() {\n"
" const prompt = imagePromptEl.value.trim();\n"
" if (!prompt) {\n"
@@ -222,6 +324,30 @@ static const char *s_web_index =
" const req = buildImagePromptRequest();\n"
" return callApi('/v1/print/image', 'POST', req);\n"
" });\n"
"\n"
" btnVoiceTalkEl.addEventListener('click', () => run(async () => {\n"
" const st = await callApi('/v1/voice/status', 'GET');\n"
" syncVoiceTalkUi(st);\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"
" if (st && st.voice_tap_active) {\n"
" return endVoiceTalk();\n"
" }\n"
" return st;\n"
" }));\n"
" setVoiceTalkButtonState(false);\n"
" (async () => {\n"
" try {\n"
" const st = await callApi('/v1/voice/status', 'GET');\n"
" syncVoiceTalkUi(st);\n"
" } catch (_) {}\n"
" })();\n"
" </script>\n"
"</body>\n"
"</html>\n";