diff --git a/static/voice-demo/README.md b/static/voice-demo/README.md index 8832eb8..ed38697 100644 --- a/static/voice-demo/README.md +++ b/static/voice-demo/README.md @@ -51,7 +51,7 @@ examples/webpage/ 2. Open the demo page served by the same process: ```text - http://127.0.0.1:8000/demo/ + http://127.0.0.1:8000/voice-demo/ ``` The default websocket URL is derived from the page host @@ -63,7 +63,7 @@ examples/webpage/ ```json "server": { "serve_webpage": true, - "webpage_mount": "/demo" + "webpage_mount": "/voice-demo" } ``` diff --git a/static/voice-demo/app.js b/static/voice-demo/app.js index dc23c3a..ea6010a 100644 --- a/static/voice-demo/app.js +++ b/static/voice-demo/app.js @@ -23,6 +23,19 @@ const WS_LOG_GROUP_KEYS = { TEXT_DELTA: "recv:response.text.delta", AUDIO_SEND: "send:input.audio", }; +const CAMERA_DONE_TEXT = "【拍摄完成】"; +const CAMERA_STATE_PROMPTS = { + 2000: "请对准车辆碰撞部位拍摄照片。", + 2001: "请对准车辆碰撞部位拍摄照片。", + 2002: "请对准被撞物品拍摄照片。", + 2003: "请切换摄像头对准本人拍摄一张正面照片。", + 2010: "请对准第一辆车碰撞部位拍摄。", + 2011: "请对准第一辆车碰撞部位拍摄。", + 2012: "请对准第二辆车碰撞部位拍摄。", + 2013: "请对准第二方车辆侧后方,看清车牌拍摄。", + 2014: "请拍摄另一方驾驶人的正面照片。", + 2015: "请切换前置摄像头对准本人拍摄一张正面照片。", +}; function defaultWsUrl() { const scheme = location.protocol === "https:" ? "wss:" : "ws:"; @@ -34,6 +47,7 @@ const els = { connectBtn: document.getElementById("connect-btn"), statusDot: document.getElementById("status-dot"), statusText: document.getElementById("status-text"), + conversation: document.getElementById("conversation"), chatLog: document.getElementById("chat-log"), micBtn: document.getElementById("mic-btn"), micSelect: document.getElementById("mic-select"), @@ -42,6 +56,10 @@ const els = { botIndicator: document.getElementById("bot-indicator"), stateIndicator: document.getElementById("state-indicator"), stateLabel: document.getElementById("state-label"), + cameraDrawer: document.getElementById("camera-drawer"), + cameraState: document.getElementById("camera-state"), + cameraQuestion: document.getElementById("camera-question"), + cameraDoneBtn: document.getElementById("camera-done-btn"), clearBtn: document.getElementById("clear-btn"), clearWsLogBtn: document.getElementById("clear-ws-log-btn"), wsLog: document.getElementById("ws-log"), @@ -75,6 +93,7 @@ const state = { // Chat state. currentAssistantBubble: null, assistantState: "", + cameraState: "", // VU meter smoothing. meterLevel: 0, @@ -121,6 +140,7 @@ function setMicSelectEnabled() { function setComposerEnabled(enabled) { els.textInput.disabled = !enabled; els.sendBtn.disabled = !enabled || els.textInput.value.trim().length === 0; + setCameraButtonEnabled(); } function setBotIndicator(active) { @@ -134,6 +154,37 @@ function setAssistantState(value) { els.stateIndicator.classList.toggle("is-active", Boolean(text)); els.stateLabel.textContent = label ? `State ${label}` : "State -"; els.stateIndicator.title = label ? `Assistant state: ${text}` : "Assistant state"; + syncCameraDrawer(text); +} + +function setCameraButtonEnabled() { + if (!els.cameraDoneBtn) return; + els.cameraDoneBtn.disabled = + !state.connected || !state.cameraState || + !state.ws || state.ws.readyState !== WebSocket.OPEN; +} + +function syncCameraDrawer(value) { + const prompt = CAMERA_STATE_PROMPTS[value]; + const open = Boolean(prompt); + state.cameraState = open ? value : ""; + els.cameraDrawer.classList.toggle("is-open", open); + els.conversation.classList.toggle("has-camera", open); + els.cameraDrawer.setAttribute("aria-hidden", open ? "false" : "true"); + if (open) { + els.cameraState.textContent = `State ${value}`; + els.cameraQuestion.textContent = prompt; + } else { + els.cameraState.textContent = "State -"; + els.cameraQuestion.textContent = ""; + } + setCameraButtonEnabled(); +} + +function updateCameraQuestion(text) { + const value = typeof text === "string" ? text.trim() : ""; + if (!state.cameraState || !value) return; + els.cameraQuestion.textContent = value; } function addBubble(role, text) { @@ -761,6 +812,7 @@ function handleAssistantFinal(text, interrupted) { if (interrupted) { state.currentAssistantBubble.classList.add("bubble--interrupted"); } + updateCameraQuestion(text); state.currentAssistantBubble = null; scrollChatToBottom(); } @@ -883,6 +935,7 @@ async function connect() { wsSend(JSON.stringify(startMessage)); addBubble("system", "Session started."); setComposerEnabled(true); + setCameraButtonEnabled(); els.textInput.focus(); }); @@ -928,6 +981,7 @@ async function connect() { setMicButton(); setMicSelectEnabled(); setComposerEnabled(false); + setCameraButtonEnabled(); setBotIndicator(false); finalizeWsLogGroup(); addWsLog( @@ -1019,6 +1073,11 @@ els.clearWsLogBtn.addEventListener("click", () => { clearWsLog(); }); +els.cameraDoneBtn.addEventListener("click", () => { + if (!state.cameraState) return; + sendText(CAMERA_DONE_TEXT); +}); + function autosizeTextarea() { const ta = els.textInput; ta.style.height = "auto"; diff --git a/static/voice-demo/index.html b/static/voice-demo/index.html index 85b1469..0d0f76a 100644 --- a/static/voice-demo/index.html +++ b/static/voice-demo/index.html @@ -38,17 +38,55 @@
Connect to the engine, enable your mic, and start talking.
-
- Audio is streamed as PCM16 mono @ 16 kHz over
- /ws-product.
-
Connect to the engine, enable your mic, and start talking.
+
+ Audio is streamed as PCM16 mono @ 16 kHz over
+ /ws-product.
+