From 709bc12b1c169372ec6ba04cf634e216fbc12566 Mon Sep 17 00:00:00 2001 From: JianYu Zheng <2375294554@qq.com> Date: Mon, 11 Aug 2025 14:24:06 +0800 Subject: [PATCH 01/10] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E6=8B=86?= =?UTF-8?q?=E5=88=86test=5Fpage.html,css=E6=8F=90=E5=8F=96=E5=87=BA?= =?UTF-8?q?=E6=9D=A5=EF=BC=8C=E5=87=8F=E5=B0=91=E5=8D=95=E4=B8=AA=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=A4=A7=E5=B0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/test/test_page.css | 425 +++++++++++++++++++++++ main/xiaozhi-server/test/test_page.html | 429 +----------------------- 2 files changed, 426 insertions(+), 428 deletions(-) create mode 100644 main/xiaozhi-server/test/test_page.css diff --git a/main/xiaozhi-server/test/test_page.css b/main/xiaozhi-server/test/test_page.css new file mode 100644 index 00000000..c70c9706 --- /dev/null +++ b/main/xiaozhi-server/test/test_page.css @@ -0,0 +1,425 @@ +body { + font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; + margin: 0; + padding: 20px; + background-color: #f5f5f5; +} + +.container { + max-width: 1000px; + margin: 0 auto; + background-color: white; + border-radius: 10px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + padding: 10px 20px 10px 20px; +} + +h1 { + color: #333; + text-align: center; + margin-bottom: 30px; +} + +.section { + margin-bottom: 20px; + padding: 15px; + border-radius: 8px; + background-color: #f9f9f9; +} + +.section h2 { + margin-top: 0; + color: #444; + font-size: 18px; + display: flex; + align-items: center; + gap: 10px; + padding: 10px 0; +} + +.section h2 .toggle-button { + margin-left: auto; + padding: 4px 12px; + font-size: 12px; + border-radius: 4px; + cursor: pointer; + height: 28px; + line-height: 20px; +} + +.device-info { + display: flex; + align-items: center; + gap: 20px; + margin-left: 20px; + padding: 0 15px; + background-color: #f9f9f9; + border-radius: 4px; + height: 28px; + line-height: 28px; +} + +.device-info span { + color: #666; + font-size: 13px; +} + +.device-info strong { + color: #333; + font-weight: 500; +} + +.config-panel { + display: none; + transition: all 0.3s ease; + margin-top: 5px; + padding: 5px 0; +} + +.config-panel.expanded { + display: block; +} + +.control-panel { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 10px; +} + +button { + padding: 8px 15px; + border: none; + border-radius: 5px; + background-color: #4285f4; + color: white; + cursor: pointer; + transition: background-color 0.2s; +} + +button:hover { + background-color: #3367d6; +} + +button:disabled { + background-color: #cccccc; + cursor: not-allowed; +} + +#serverUrl, +#otaUrl { + flex-grow: 1; + padding: 8px; + border: 1px solid #ddd; + border-radius: 5px; +} + +.message-input { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +#messageInput { + flex-grow: 1; + padding: 8px; + border: 1px solid #ddd; + border-radius: 5px; +} + +#nfcCardId { + flex-grow: 1; + padding: 8px; + border: 1px solid #ddd; + border-radius: 5px; +} + +.conversation { + max-height: 300px; + overflow-y: auto; + border: 1px solid #ddd; + border-radius: 5px; + padding: 10px; + background-color: white; + flex: 1; + margin-right: 10px; +} + +.message { + margin-bottom: 10px; + padding: 8px 12px; + border-radius: 8px; + max-width: 80%; +} + +.user { + background-color: #e2f2ff; + margin-left: auto; + margin-right: 10px; + text-align: right; +} + +.server { + background-color: #f0f0f0; + margin-right: auto; + margin-left: 10px; +} + +.status { + color: #666; + font-style: italic; + font-size: 14px; + margin: 0; + padding: 0; +} + +.audio-controls { + display: flex; + justify-content: center; + gap: 10px; + margin-top: 20px; +} + +.audio-visualizer { + height: 60px; + width: 100%; + margin-top: 10px; + border: 1px solid #ddd; + border-radius: 5px; + background-color: #fafafa; +} + +.record-button { + background-color: #db4437; +} + +.record-button:hover { + background-color: #c53929; +} + +.record-button.recording { + animation: pulse 1.5s infinite; +} + +@keyframes pulse { + 0% { + background-color: #db4437; + } + + 50% { + background-color: #ff6659; + } + + 100% { + background-color: #db4437; + } +} + +#logContainer { + margin-top: 0; + padding: 10px; + background-color: #f0f0f0; + border-radius: 5px; + font-family: monospace; + height: 300px; + overflow-y: auto; + flex: 1; + margin-left: 10px; +} + +.log-entry { + margin: 5px 0; + font-size: 12px; +} + +.log-info { + color: #333; +} + +.log-error { + color: #db4437; +} + +.log-success { + color: #0f9d58; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translate(-50%, -60%); + } + + to { + opacity: 1; + transform: translate(-50%, -50%); + } +} + +.script-status { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 5px; +} + +.script-loaded { + background-color: #0f9d58; +} + +.script-loading { + background-color: #f4b400; +} + +.script-error { + background-color: #db4437; +} + +.script-list { + margin: 10px 0; + padding: 10px; + background-color: #f9f9f9; + border-radius: 5px; + font-family: monospace; + font-size: 11px; +} + +#scriptStatus.success { + background-color: #e6f4ea; + color: #0f9d58; + border-left: 4px solid #0f9d58; +} + +#scriptStatus.error { + background-color: #fce8e6; + color: #db4437; + border-left: 4px solid #db4437; +} + +#scriptStatus.warning { + background-color: #fef7e0; + color: #f4b400; + border-left: 4px solid #f4b400; +} + +/* 标签页样式 */ +.tabs { + display: flex; + margin-bottom: 20px; + border-bottom: 2px solid #e0e0e0; +} + +.tab { + padding: 10px 20px; + cursor: pointer; + border: none; + background: none; + font-size: 16px; + color: #666; + position: relative; + transition: all 0.3s ease; +} + +.tab:hover { + color: #4285f4; +} + +.tab.active { + color: #4285f4; + font-weight: bold; +} + +.tab.active::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 100%; + height: 2px; + background-color: #4285f4; +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +.flex-container { + display: flex; + gap: 20px; + margin-top: 10px; +} + +.config-item { + display: flex; + align-items: center; + margin-bottom: 8px; + width: 100%; +} + +.config-item label { + width: 100px; + text-align: right; + margin-right: 10px; + color: #666; +} + +.config-item input { + flex-grow: 1; + padding: 6px; + border: 1px solid #ddd; + border-radius: 5px; +} + +.control-panel { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 10px; +} + +.connection-controls { + display: flex; + gap: 10px; + align-items: center; + width: 100%; +} + +.connection-controls input { + flex: 1; + padding: 8px; + border: 1px solid #ddd; + border-radius: 5px; + min-width: 200px; +} + +.connection-controls button { + white-space: nowrap; + padding: 8px 15px; +} + +.connection-status { + display: flex; + align-items: center; + gap: 20px; + margin-left: 20px; + padding: 0 15px; + background-color: #f9f9f9; + border-radius: 4px; + height: 28px; + line-height: 28px; +} + +.connection-status span { + color: #666; + font-size: 13px; +} + +.connection-status .status { + color: #333; + font-weight: 500; +} \ No newline at end of file diff --git a/main/xiaozhi-server/test/test_page.html b/main/xiaozhi-server/test/test_page.html index 567332b8..ec9a5f4b 100644 --- a/main/xiaozhi-server/test/test_page.html +++ b/main/xiaozhi-server/test/test_page.html @@ -5,435 +5,8 @@ 小智服务器测试页面 - + -

小智服务器测试页面

From 76516336afa302d7136c87782f9d963cf50dfafb Mon Sep 17 00:00:00 2001 From: JianYu Zheng <2375294554@qq.com> Date: Mon, 11 Aug 2025 16:20:46 +0800 Subject: [PATCH 02/10] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E6=8B=86?= =?UTF-8?q?=E5=88=86test=5Fpage.html,=E6=8A=8A=E6=97=A5=E5=BF=97=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E6=8F=90=E5=8F=96=E5=87=BA=E6=9D=A5=EF=BC=8C=E5=87=8F?= =?UTF-8?q?=E5=B0=91=E5=8D=95=E4=B8=AA=E6=96=87=E4=BB=B6=E5=A4=A7=E5=B0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/test/js/utils/logger.js | 34 ++++++++++++++++++ main/xiaozhi-server/test/test_page.html | 38 ++------------------- 2 files changed, 36 insertions(+), 36 deletions(-) create mode 100644 main/xiaozhi-server/test/js/utils/logger.js diff --git a/main/xiaozhi-server/test/js/utils/logger.js b/main/xiaozhi-server/test/js/utils/logger.js new file mode 100644 index 00000000..38e43150 --- /dev/null +++ b/main/xiaozhi-server/test/js/utils/logger.js @@ -0,0 +1,34 @@ +const logContainer = document.getElementById('logContainer'); +export function log(message, type = 'info') { + // 将消息按换行符分割成多行 + const lines = message.split('\n'); + const now = new Date(); + // const timestamp = `[${now.toLocaleTimeString()}] `; + const timestamp = `[${now.toLocaleTimeString()}.${now.getMilliseconds().toString().padStart(3, '0')}] `; + // 为每一行创建日志条目 + lines.forEach((line, index) => { + const logEntry = document.createElement('div'); + logEntry.className = `log-entry log-${type}`; + // 如果是第一条日志,显示时间戳 + const prefix = index === 0 ? timestamp : ' '.repeat(timestamp.length); + logEntry.textContent = `${prefix}${line}`; + // logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; + // logEntry.style 保留起始的空格 + logEntry.style.whiteSpace = 'pre'; + if (type === 'error') { + logEntry.style.color = 'red'; + } else if (type === 'debug') { + logEntry.style.color = 'gray'; + return; + } else if (type === 'warning') { + logEntry.style.color = 'orange'; + } else if (type === 'success') { + logEntry.style.color = 'green'; + } else { + logEntry.style.color = 'black'; + } + logContainer.appendChild(logEntry); + }); + + logContainer.scrollTop = logContainer.scrollHeight; +} \ No newline at end of file diff --git a/main/xiaozhi-server/test/test_page.html b/main/xiaozhi-server/test/test_page.html index ec9a5f4b..8eaad778 100644 --- a/main/xiaozhi-server/test/test_page.html +++ b/main/xiaozhi-server/test/test_page.html @@ -99,7 +99,8 @@ - From a9d116df2f74218f5e398f33476930980722f2fd Mon Sep 17 00:00:00 2001 From: JianYu Zheng <2375294554@qq.com> Date: Wed, 13 Aug 2025 14:25:04 +0800 Subject: [PATCH 07/10] =?UTF-8?q?=E4=BC=98=E5=8C=96:=E6=8B=86=E5=88=86test?= =?UTF-8?q?=5Fpage.html=E4=B8=80=E9=83=A8=E5=88=86=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/test/js/document.js | 10 ++ main/xiaozhi-server/test/js/opus.js | 143 +++++++++++++++++++++ main/xiaozhi-server/test/test_page.html | 163 ++---------------------- 3 files changed, 163 insertions(+), 153 deletions(-) diff --git a/main/xiaozhi-server/test/js/document.js b/main/xiaozhi-server/test/js/document.js index eb23247e..6fb5b222 100644 --- a/main/xiaozhi-server/test/js/document.js +++ b/main/xiaozhi-server/test/js/document.js @@ -6,6 +6,7 @@ const messageInput = document.getElementById('messageInput'); const sendTextButton = document.getElementById('sendTextButton'); const recordButton = document.getElementById('recordButton'); const stopButton = document.getElementById('stopButton'); +// 会话记录 const conversationDiv = document.getElementById('conversation'); const logContainer = document.getElementById('logContainer'); let visualizerCanvas = document.getElementById('audioVisualizer'); @@ -37,3 +38,12 @@ export function updateScriptStatus(message, type) { } } +// 添加消息到会话记录 +export function addMessage(text, isUser = false) { + const messageDiv = document.createElement('div'); + messageDiv.className = `message ${isUser ? 'user' : 'server'}`; + messageDiv.textContent = text; + conversationDiv.appendChild(messageDiv); + conversationDiv.scrollTop = conversationDiv.scrollHeight; +} + diff --git a/main/xiaozhi-server/test/js/opus.js b/main/xiaozhi-server/test/js/opus.js index 9259b9de..67e7ec30 100644 --- a/main/xiaozhi-server/test/js/opus.js +++ b/main/xiaozhi-server/test/js/opus.js @@ -40,4 +40,147 @@ export function checkOpusLoaded() { log(`Opus库加载失败,请检查libopus.js文件是否存在且正确: ${err.message}`, 'error'); updateScriptStatus('Opus库加载失败,请检查libopus.js文件是否存在且正确', 'error'); } +} + + +// 创建一个Opus编码器 +let opusEncoder = null; +export function initOpusEncoder() { + try { + if (opusEncoder) { + return opusEncoder; // 已经初始化过 + } + + if (!window.ModuleInstance) { + log('无法创建Opus编码器:ModuleInstance不可用', 'error'); + return; + } + + // 初始化一个Opus编码器 + const mod = window.ModuleInstance; + const sampleRate = 16000; // 16kHz采样率 + const channels = 1; // 单声道 + const application = 2048; // OPUS_APPLICATION_VOIP = 2048 + + // 创建编码器 + opusEncoder = { + channels: channels, + sampleRate: sampleRate, + frameSize: 960, // 60ms @ 16kHz = 60 * 16 = 960 samples + maxPacketSize: 4000, // 最大包大小 + module: mod, + + // 初始化编码器 + init: function () { + try { + // 获取编码器大小 + const encoderSize = mod._opus_encoder_get_size(this.channels); + log(`Opus编码器大小: ${encoderSize}字节`, 'info'); + + // 分配内存 + this.encoderPtr = mod._malloc(encoderSize); + if (!this.encoderPtr) { + throw new Error("无法分配编码器内存"); + } + + // 初始化编码器 + const err = mod._opus_encoder_init( + this.encoderPtr, + this.sampleRate, + this.channels, + application + ); + + if (err < 0) { + throw new Error(`Opus编码器初始化失败: ${err}`); + } + + // 设置位率 (16kbps) + mod._opus_encoder_ctl(this.encoderPtr, 4002, 16000); // OPUS_SET_BITRATE + + // 设置复杂度 (0-10, 越高质量越好但CPU使用越多) + mod._opus_encoder_ctl(this.encoderPtr, 4010, 5); // OPUS_SET_COMPLEXITY + + // 设置使用DTX (不传输静音帧) + mod._opus_encoder_ctl(this.encoderPtr, 4016, 1); // OPUS_SET_DTX + + log("Opus编码器初始化成功", 'success'); + return true; + } catch (error) { + if (this.encoderPtr) { + mod._free(this.encoderPtr); + this.encoderPtr = null; + } + log(`Opus编码器初始化失败: ${error.message}`, 'error'); + return false; + } + }, + + // 编码PCM数据为Opus + encode: function (pcmData) { + if (!this.encoderPtr) { + if (!this.init()) { + return null; + } + } + + try { + const mod = this.module; + + // 为PCM数据分配内存 + const pcmPtr = mod._malloc(pcmData.length * 2); // 2字节/int16 + + // 将PCM数据复制到HEAP + for (let i = 0; i < pcmData.length; i++) { + mod.HEAP16[(pcmPtr >> 1) + i] = pcmData[i]; + } + + // 为输出分配内存 + const outPtr = mod._malloc(this.maxPacketSize); + + // 进行编码 + const encodedLen = mod._opus_encode( + this.encoderPtr, + pcmPtr, + this.frameSize, + outPtr, + this.maxPacketSize + ); + + if (encodedLen < 0) { + throw new Error(`Opus编码失败: ${encodedLen}`); + } + + // 复制编码后的数据 + const opusData = new Uint8Array(encodedLen); + for (let i = 0; i < encodedLen; i++) { + opusData[i] = mod.HEAPU8[outPtr + i]; + } + + // 释放内存 + mod._free(pcmPtr); + mod._free(outPtr); + + return opusData; + } catch (error) { + log(`Opus编码出错: ${error.message}`, 'error'); + return null; + } + }, + + // 销毁编码器 + destroy: function () { + if (this.encoderPtr) { + this.module._free(this.encoderPtr); + this.encoderPtr = null; + } + } + }; + + opusEncoder.init(); + return opusEncoder; + } catch (error) { + log(`创建Opus编码器失败: ${error.message}`, 'error'); + return false; + } } \ No newline at end of file diff --git a/main/xiaozhi-server/test/test_page.html b/main/xiaozhi-server/test/test_page.html index 03898608..c95ce98d 100644 --- a/main/xiaozhi-server/test/test_page.html +++ b/main/xiaozhi-server/test/test_page.html @@ -102,7 +102,8 @@ +
+

+ 连接信息 + + OTA: ota未连接 + WS: ws未连接 + +

+
+ + + + +
+
- - // DOM元素 - const connectButton = document.getElementById('connectButton'); - const serverUrlInput = document.getElementById('serverUrl'); - const connectionStatus = document.getElementById('connectionStatus'); - const messageInput = document.getElementById('messageInput'); - const sendTextButton = document.getElementById('sendTextButton'); - const recordButton = document.getElementById('recordButton'); - const stopButton = document.getElementById('stopButton'); - const conversationDiv = document.getElementById('conversation'); - const logContainer = document.getElementById('logContainer'); + + // 编码并发送剩余的数据 + encodeAndSendOpus(); + + // 发送一个空的消息作为结束标志(模拟接收到空音频数据的情况) + if (websocket && websocket.readyState === WebSocket.OPEN) { + // 使用空的Uint8Array发送最后一个空帧 + const emptyOpusFrame = new Uint8Array(0); + websocket.send(emptyOpusFrame); + + // 发送监听结束消息 + const stopMessage = { + type: 'listen', + mode: 'manual', + state: 'stop' + }; + + websocket.send(JSON.stringify(stopMessage)); + log('已发送录音停止信号', 'info'); + } + + // 重置UI + recordButton.textContent = '开始录音'; + recordButton.classList.remove('recording'); + recordButton.disabled = false; + + log('停止PCM直接录音', 'success'); + return true; + } catch (error) { + log(`直接录音停止错误: ${error.message}`, 'error'); + return false; + } + } + + async function handleBinaryMessage(data) { + try { + let arrayBuffer; + // 根据数据类型进行处理 + if (data instanceof ArrayBuffer) { + arrayBuffer = data; + log(`收到ArrayBuffer音频数据,大小: ${data.byteLength}字节`, 'debug'); + } else if (data instanceof Blob) { + // 如果是Blob类型,转换为ArrayBuffer + arrayBuffer = await data.arrayBuffer(); + log(`收到Blob音频数据,大小: ${arrayBuffer.byteLength}字节`, 'debug'); + } else { + log(`收到未知类型的二进制数据: ${typeof data}`, 'warning'); + return; + } + // 创建Uint8Array用于处理 + const opusData = new Uint8Array(arrayBuffer); + if (opusData.length > 0) { + // 将数据添加到缓冲队列 + queue.enqueue(opusData); + } else { + log('收到空音频数据帧,可能是结束标志', 'warning'); + // 如果正在播放,发送结束信号 + if (isAudioPlaying && streamingContext) { + streamingContext.endOfStream = true; + } + } + } catch (error) { + log(`处理二进制消息出错: ${error.message}`, 'error'); + } + } + + // 获取配置值 + function getConfig() { + const deviceMac = document.getElementById('deviceMac').value.trim(); + return { + deviceId: deviceMac, // 使用MAC地址作为deviceId + deviceName: document.getElementById('deviceName').value.trim(), + deviceMac: deviceMac, + clientId: document.getElementById('clientId').value.trim(), + token: document.getElementById('token').value.trim() + }; + } + + initApp(); + \ No newline at end of file From 94662e17bab4ebded8432d3ff83654296fdd0bae Mon Sep 17 00:00:00 2001 From: JianYu Zheng <2375294554@qq.com> Date: Fri, 15 Aug 2025 17:33:02 +0800 Subject: [PATCH 10/10] =?UTF-8?q?=E4=BC=98=E5=8C=96:=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E6=9C=80=E7=9F=AD=E6=98=AF0.3=E7=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/test/test_page.html | 43 ++----------------------- 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/main/xiaozhi-server/test/test_page.html b/main/xiaozhi-server/test/test_page.html index 1bd8a36a..2dc37fe4 100644 --- a/main/xiaozhi-server/test/test_page.html +++ b/main/xiaozhi-server/test/test_page.html @@ -331,8 +331,8 @@ // 开始播放音频 startPlaying: async function () { while (true) { - // 如果累积了至少0.2秒的音频,开始播放 - const minSamples = SAMPLE_RATE * MIN_AUDIO_DURATION; + // 如果累积了至少0.3秒的音频,开始播放 + const minSamples = SAMPLE_RATE * MIN_AUDIO_DURATION * 3; if (!this.playing && this.queue.length < minSamples) { await this.getQueue(minSamples) } @@ -373,45 +373,6 @@ } await this.getQueue(minSamples) } - // // 播放结束后的处理 - // this.source.onended = () => { - // this.source = null; - // this.playing = false; - // - // // 使用setTimeout避免递归调用 - // setTimeout(() => { - // // 如果队列中还有数据,继续播放 - // if (this.queue.length > 0) { - // this.startPlaying(); - // } else if (audioBufferQueue.length > 0) { - // // 缓冲区有新数据,进行解码 - // const frames = [...audioBufferQueue]; - // audioBufferQueue = []; - // this.decodeOpusFrames(frames); - // } else if (this.endOfStream) { - // // 流已结束且没有更多数据 - // log("音频播放完成", 'info'); - // isAudioPlaying = false; - // this.endOfStream = false; - // streamingContext = null; - // } else { - // // 等待更多数据 - // setTimeout(() => { - // // 如果仍然没有新数据,但有更多的包到达 - // if (this.queue.length === 0 && audioBufferQueue.length > 0) { - // const frames = [...audioBufferQueue]; - // audioBufferQueue = []; - // this.decodeOpusFrames(frames); - // } else if (this.queue.length === 0 && audioBufferQueue.length === 0) { - // // 真的没有更多数据了 - // log("音频播放完成 (超时)", 'info'); - // isAudioPlaying = false; - // streamingContext = null; - // } - // }, 500); // 500ms超时 - // } - // }, 10); // 10ms延迟,避免立即递归 - // }; } }; }