mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-29 12:23:55 +08:00
@@ -9,7 +9,7 @@ logger = setup_logging()
|
|||||||
class DefaultTTS(TTSProviderBase):
|
class DefaultTTS(TTSProviderBase):
|
||||||
def __init__(self, config, delete_audio_file=True):
|
def __init__(self, config, delete_audio_file=True):
|
||||||
super().__init__(config, delete_audio_file)
|
super().__init__(config, delete_audio_file)
|
||||||
self.output_dir = config.get("output_dir", "output")
|
self.output_dir = config.get("output_dir", "tmp")
|
||||||
if not os.path.exists(self.output_dir):
|
if not os.path.exists(self.output_dir):
|
||||||
os.makedirs(self.output_dir)
|
os.makedirs(self.output_dir)
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,84 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>小智服务器测试页面</title>
|
<title>小智服务器测试页面</title>
|
||||||
<link rel="stylesheet" href="test_page.css">
|
<link rel="stylesheet" href="test_page.css">
|
||||||
|
<style>
|
||||||
|
#fileProtocolWarning {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fileProtocolWarning h2 {
|
||||||
|
color: #ff4d4d;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fileProtocolWarning pre {
|
||||||
|
background-color: green;
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-family: monospace;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fileProtocolWarning button {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 10px 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fileProtocolWarning button:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
// 检测是否使用file://协议打开
|
||||||
|
if (window.location.protocol === 'file:') {
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// 创建警告框
|
||||||
|
const warningDiv = document.createElement('div');
|
||||||
|
warningDiv.id = 'fileProtocolWarning';
|
||||||
|
warningDiv.innerHTML = `
|
||||||
|
<h2>⚠️ 警告:请使用HTTP服务器打开此页面</h2>
|
||||||
|
<p>您当前使用的是本地文件方式打开页面(file://协议),这可能导致页面功能异常。</p>
|
||||||
|
<p>您可以使用nginx映射启动测试页面,也可以请按照以下步骤使用python启动测试http服务:</p>
|
||||||
|
<ol>
|
||||||
|
<li>打开命令行终端</li>
|
||||||
|
<li>命令行进入到 xiaozhi-server/test 目录</li>
|
||||||
|
<li>执行以下命令启动HTTP服务器:</li>
|
||||||
|
</ol>
|
||||||
|
<pre>python -m http.server 8006</pre>
|
||||||
|
<p>然后在浏览器中访问:<strong>http://localhost:8006/test_page.html</strong></p>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(warningDiv);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>小智服务器测试页面</h1>
|
<h1>小智服务器测试页面</h1>
|
||||||
|
|
||||||
<div id="scriptStatus" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);">
|
<div id="scriptStatus" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);">
|
||||||
@@ -57,9 +132,9 @@
|
|||||||
</h2>
|
</h2>
|
||||||
<div class="connection-controls">
|
<div class="connection-controls">
|
||||||
<input type="text" id="otaUrl" value="http://127.0.0.1:8002/xiaozhi/ota/"
|
<input type="text" id="otaUrl" value="http://127.0.0.1:8002/xiaozhi/ota/"
|
||||||
placeholder="OTA服务器地址,如:http://127.0.0.1:8002/xiaozhi/ota/"/>
|
placeholder="OTA服务器地址,如:http://127.0.0.1:8002/xiaozhi/ota/" />
|
||||||
<input type="text" id="serverUrl" value="ws://127.0.0.1:8000/xiaozhi/v1/"
|
<input type="text" id="serverUrl" value="ws://127.0.0.1:8000/xiaozhi/v1/"
|
||||||
placeholder="WebSocket服务器地址,如:ws://127.0.0.1:8000/xiaozhi/v1/"/>
|
placeholder="WebSocket服务器地址,如:ws://127.0.0.1:8000/xiaozhi/v1/" />
|
||||||
<button id="connectButton">连接</button>
|
<button id="connectButton">连接</button>
|
||||||
<button id="authTestButton">测试认证</button>
|
<button id="authTestButton">测试认证</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,16 +170,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Opus解码库 -->
|
<!-- Opus解码库 -->
|
||||||
<script src="libopus.js"></script>
|
<script src="libopus.js"></script>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import {log} from './js/utils/logger.js';
|
import { log } from './js/utils/logger.js';
|
||||||
import {webSocketConnect} from './js/xiaoZhiConnect.js';
|
import { webSocketConnect } from './js/xiaoZhiConnect.js';
|
||||||
import {checkOpusLoaded, initOpusEncoder} from './js/opus.js';
|
import { checkOpusLoaded, initOpusEncoder } from './js/opus.js';
|
||||||
import {addMessage} from './js/document.js'
|
import { addMessage } from './js/document.js'
|
||||||
import BlockingQueue from './js/utils/BlockingQueue.js'
|
import BlockingQueue from './js/utils/BlockingQueue.js'
|
||||||
// 需要加载的脚本列表 - 移除Opus依赖
|
// 需要加载的脚本列表 - 移除Opus依赖
|
||||||
const scriptFiles = [];
|
const scriptFiles = [];
|
||||||
@@ -282,7 +357,7 @@
|
|||||||
// 获取正在播放已解码的PCM队列,单线程:在activeQueue一直更新的状态下不会出现安全问题
|
// 获取正在播放已解码的PCM队列,单线程:在activeQueue一直更新的状态下不会出现安全问题
|
||||||
getQueue: async function (minSamples) {
|
getQueue: async function (minSamples) {
|
||||||
let TepArray = []
|
let TepArray = []
|
||||||
const num = minSamples - this.queue.length > 0 ? minSamples -this.queue.length : 1;
|
const num = minSamples - this.queue.length > 0 ? minSamples - this.queue.length : 1;
|
||||||
// 原子交换 + 清空
|
// 原子交换 + 清空
|
||||||
[TepArray, this.activeQueue] = [await this.activeQueue.dequeue(num), new BlockingQueue()];
|
[TepArray, this.activeQueue] = [await this.activeQueue.dequeue(num), new BlockingQueue()];
|
||||||
this.queue.push(...TepArray)
|
this.queue.push(...TepArray)
|
||||||
@@ -618,7 +693,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建完整的录音blob
|
// 创建完整的录音blob
|
||||||
const blob = new Blob(audioChunks, {type: audioChunks[0].type});
|
const blob = new Blob(audioChunks, { type: audioChunks[0].type });
|
||||||
log(`已创建音频Blob,MIME类型: ${audioChunks[0].type},大小: ${(blob.size / 1024).toFixed(2)} KB`, 'info');
|
log(`已创建音频Blob,MIME类型: ${audioChunks[0].type},大小: ${(blob.size / 1024).toFixed(2)} KB`, 'info');
|
||||||
|
|
||||||
// 保存原始块,以防清空后需要调试
|
// 保存原始块,以防清空后需要调试
|
||||||
@@ -869,7 +944,7 @@
|
|||||||
"tools": [{
|
"tools": [{
|
||||||
"name": "self.get_device_status",
|
"name": "self.get_device_status",
|
||||||
"description": "Provides the real-time information of the device, including the current status of the audio speaker, screen, battery, network, etc.\nUse this tool for: \n1. Answering questions about current condition (e.g. what is the current volume of the audio speaker?)\n2. As the first step to control the device (e.g. turn up / down the volume of the audio speaker, etc.)",
|
"description": "Provides the real-time information of the device, including the current status of the audio speaker, screen, battery, network, etc.\nUse this tool for: \n1. Answering questions about current condition (e.g. what is the current volume of the audio speaker?)\n2. As the first step to control the device (e.g. turn up / down the volume of the audio speaker, etc.)",
|
||||||
"inputSchema": {"type": "object", "properties": {}}
|
"inputSchema": { "type": "object", "properties": {} }
|
||||||
}, {
|
}, {
|
||||||
"name": "self.audio_speaker.set_volume",
|
"name": "self.audio_speaker.set_volume",
|
||||||
"description": "Set the volume of the audio speaker. If the current volume is unknown, you must call `self.get_device_status` tool first and then call this tool.",
|
"description": "Set the volume of the audio speaker. If the current volume is unknown, you must call `self.get_device_status` tool first and then call this tool.",
|
||||||
@@ -903,7 +978,7 @@
|
|||||||
"description": "Set the theme of the screen. The theme can be 'light' or 'dark'.",
|
"description": "Set the theme of the screen. The theme can be 'light' or 'dark'.",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {"theme": {"type": "string"}},
|
"properties": { "theme": { "type": "string" } },
|
||||||
"required": ["theme"]
|
"required": ["theme"]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
@@ -920,7 +995,7 @@
|
|||||||
"payload": {
|
"payload": {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": payload.id,
|
"id": payload.id,
|
||||||
"result": {"content": [{"type": "text", "text": "true"}], "isError": false}
|
"result": { "content": [{ "type": "text", "text": "true" }], "isError": false }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
websocket.send(replay_message);
|
websocket.send(replay_message);
|
||||||
@@ -1352,7 +1427,7 @@
|
|||||||
// 检查是否支持AudioWorklet
|
// 检查是否支持AudioWorklet
|
||||||
if (audioContext.audioWorklet) {
|
if (audioContext.audioWorklet) {
|
||||||
// 注册音频处理器
|
// 注册音频处理器
|
||||||
const blob = new Blob([audioProcessorCode], {type: 'application/javascript'});
|
const blob = new Blob([audioProcessorCode], { type: 'application/javascript' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
await audioContext.audioWorklet.addModule(url);
|
await audioContext.audioWorklet.addModule(url);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
@@ -1369,7 +1444,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
log('使用AudioWorklet处理音频', 'success');
|
log('使用AudioWorklet处理音频', 'success');
|
||||||
return {node: audioProcessor, type: 'worklet'};
|
return { node: audioProcessor, type: 'worklet' };
|
||||||
} else {
|
} else {
|
||||||
// 使用旧版ScriptProcessorNode作为回退方案
|
// 使用旧版ScriptProcessorNode作为回退方案
|
||||||
log('AudioWorklet不可用,使用ScriptProcessorNode作为回退方案', 'warning');
|
log('AudioWorklet不可用,使用ScriptProcessorNode作为回退方案', 'warning');
|
||||||
@@ -1400,7 +1475,7 @@
|
|||||||
scriptProcessor.connect(silent);
|
scriptProcessor.connect(silent);
|
||||||
silent.connect(audioContext.destination);
|
silent.connect(audioContext.destination);
|
||||||
|
|
||||||
return {node: scriptProcessor, type: 'processor'};
|
return { node: scriptProcessor, type: 'processor' };
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`创建音频处理器失败: ${error.message},尝试回退方案`, 'error');
|
log(`创建音频处理器失败: ${error.message},尝试回退方案`, 'error');
|
||||||
@@ -1429,7 +1504,7 @@
|
|||||||
silent.connect(audioContext.destination);
|
silent.connect(audioContext.destination);
|
||||||
|
|
||||||
log('使用ScriptProcessorNode作为回退方案成功', 'warning');
|
log('使用ScriptProcessorNode作为回退方案成功', 'warning');
|
||||||
return {node: scriptProcessor, type: 'processor'};
|
return { node: scriptProcessor, type: 'processor' };
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
log(`回退方案也失败: ${fallbackError.message}`, 'error');
|
log(`回退方案也失败: ${fallbackError.message}`, 'error');
|
||||||
return null;
|
return null;
|
||||||
@@ -1574,7 +1649,7 @@
|
|||||||
|
|
||||||
// 启动音频处理器的录音 - 只有AudioWorklet才需要发送消息
|
// 启动音频处理器的录音 - 只有AudioWorklet才需要发送消息
|
||||||
if (audioProcessorType === 'worklet' && audioProcessor.port) {
|
if (audioProcessorType === 'worklet' && audioProcessor.port) {
|
||||||
audioProcessor.port.postMessage({command: 'start'});
|
audioProcessor.port.postMessage({ command: 'start' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送监听开始消息
|
// 发送监听开始消息
|
||||||
@@ -1631,7 +1706,7 @@
|
|||||||
if (audioProcessor) {
|
if (audioProcessor) {
|
||||||
// 只有AudioWorklet才需要发送停止消息
|
// 只有AudioWorklet才需要发送停止消息
|
||||||
if (audioProcessorType === 'worklet' && audioProcessor.port) {
|
if (audioProcessorType === 'worklet' && audioProcessor.port) {
|
||||||
audioProcessor.port.postMessage({command: 'stop'});
|
audioProcessor.port.postMessage({ command: 'stop' });
|
||||||
}
|
}
|
||||||
|
|
||||||
audioProcessor.disconnect();
|
audioProcessor.disconnect();
|
||||||
@@ -1734,7 +1809,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
initApp();
|
initApp();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user