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 @@
+
-
- // 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();
+