mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-27 09:33:55 +08:00
Merge pull request #2876 from xinnan-tech/fix-digital-human
Fix digital human
This commit is contained in:
@@ -40,9 +40,6 @@ class App {
|
|||||||
// 关闭加载loading
|
// 关闭加载loading
|
||||||
this.setModelLoadingStatus(false);
|
this.setModelLoadingStatus(false);
|
||||||
|
|
||||||
// 绑定页面卸载事件
|
|
||||||
this.bindUnload();
|
|
||||||
|
|
||||||
log('应用初始化完成', 'success');
|
log('应用初始化完成', 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,16 +81,6 @@ class App {
|
|||||||
modelLoading.style.display = isLoading ? 'flex' : 'none';
|
modelLoading.style.display = isLoading ? 'flex' : 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绑定页面卸载事件
|
|
||||||
bindUnload() {
|
|
||||||
window.addEventListener('beforeunload', () => {
|
|
||||||
// 销毁定时器
|
|
||||||
if (this.uiController && this.uiController.wsTimer) {
|
|
||||||
clearInterval(this.uiController.wsTimer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建并启动应用
|
// 创建并启动应用
|
||||||
|
|||||||
@@ -58,10 +58,9 @@ export function saveConfig() {
|
|||||||
|
|
||||||
// 获取配置值
|
// 获取配置值
|
||||||
export function getConfig() {
|
export function getConfig() {
|
||||||
// 从DOM获取值
|
const deviceMac = localStorage.getItem('xz_tester_deviceMac');
|
||||||
const deviceMac = document.getElementById('deviceMac')?.value.trim() || '';
|
const deviceName = localStorage.getItem('xz_tester_deviceName');
|
||||||
const deviceName = document.getElementById('deviceName')?.value.trim() || '';
|
const clientId = localStorage.getItem('xz_tester_clientId');
|
||||||
const clientId = document.getElementById('clientId')?.value.trim() || '';
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deviceId: deviceMac, // 使用MAC地址作为deviceId
|
deviceId: deviceMac, // 使用MAC地址作为deviceId
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { log } from '../../utils/logger.js';
|
import { log } from '../../utils/logger.js';
|
||||||
import { updateScriptStatus } from '../../ui/dom-helper.js'
|
|
||||||
|
|
||||||
|
|
||||||
// 检查Opus库是否已加载
|
// 检查Opus库是否已加载
|
||||||
@@ -15,7 +14,6 @@ export function checkOpusLoaded() {
|
|||||||
// 使用Module.instance对象替换全局Module对象
|
// 使用Module.instance对象替换全局Module对象
|
||||||
window.ModuleInstance = Module.instance;
|
window.ModuleInstance = Module.instance;
|
||||||
log('Opus库加载成功(使用Module.instance)', 'success');
|
log('Opus库加载成功(使用Module.instance)', 'success');
|
||||||
updateScriptStatus('Opus库加载成功', 'success');
|
|
||||||
|
|
||||||
// 3秒后隐藏状态
|
// 3秒后隐藏状态
|
||||||
const statusElement = document.getElementById('scriptStatus');
|
const statusElement = document.getElementById('scriptStatus');
|
||||||
@@ -27,7 +25,6 @@ export function checkOpusLoaded() {
|
|||||||
if (typeof Module._opus_decoder_get_size === 'function') {
|
if (typeof Module._opus_decoder_get_size === 'function') {
|
||||||
window.ModuleInstance = Module;
|
window.ModuleInstance = Module;
|
||||||
log('Opus库加载成功(使用全局Module)', 'success');
|
log('Opus库加载成功(使用全局Module)', 'success');
|
||||||
updateScriptStatus('Opus库加载成功', 'success');
|
|
||||||
|
|
||||||
// 3秒后隐藏状态
|
// 3秒后隐藏状态
|
||||||
const statusElement = document.getElementById('scriptStatus');
|
const statusElement = document.getElementById('scriptStatus');
|
||||||
@@ -38,7 +35,6 @@ export function checkOpusLoaded() {
|
|||||||
throw new Error('Opus解码函数未找到,可能Module结构不正确');
|
throw new Error('Opus解码函数未找到,可能Module结构不正确');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(`Opus库加载失败,请检查libopus.js文件是否存在且正确: ${err.message}`, 'error');
|
log(`Opus库加载失败,请检查libopus.js文件是否存在且正确: ${err.message}`, 'error');
|
||||||
updateScriptStatus('Opus库加载失败,请检查libopus.js文件是否存在且正确', 'error');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { otaStatusStyle } from '../../ui/dom-helper.js';
|
|
||||||
import { log } from '../../utils/logger.js';
|
import { log } from '../../utils/logger.js';
|
||||||
|
|
||||||
// WebSocket 连接
|
// WebSocket 连接
|
||||||
@@ -84,7 +83,7 @@ async function sendOTA(otaUrl, config) {
|
|||||||
},
|
},
|
||||||
ota: { label: 'xiaozhi-web-test' },
|
ota: { label: 'xiaozhi-web-test' },
|
||||||
board: {
|
board: {
|
||||||
type: 'xiaozhi-web-test',
|
type: config.deviceName,
|
||||||
ssid: 'xiaozhi-web-test',
|
ssid: 'xiaozhi-web-test',
|
||||||
rssi: 0,
|
rssi: 0,
|
||||||
channel: 0,
|
channel: 0,
|
||||||
@@ -103,10 +102,8 @@ async function sendOTA(otaUrl, config) {
|
|||||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
||||||
|
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
otaStatusStyle(true)
|
|
||||||
return result; // 返回完整的响应数据
|
return result; // 返回完整的响应数据
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
otaStatusStyle(false)
|
|
||||||
return null; // 失败返回null
|
return null; // 失败返回null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import { getConfig, saveConnectionUrls } from '../../config/manager.js';
|
|||||||
import { getAudioPlayer } from '../audio/player.js';
|
import { getAudioPlayer } from '../audio/player.js';
|
||||||
import { getAudioRecorder } from '../audio/recorder.js';
|
import { getAudioRecorder } from '../audio/recorder.js';
|
||||||
import { getMcpTools, executeMcpTool, setWebSocket as setMcpWebSocket } from '../mcp/tools.js';
|
import { getMcpTools, executeMcpTool, setWebSocket as setMcpWebSocket } from '../mcp/tools.js';
|
||||||
|
import { uiController } from '../../ui/controller.js'
|
||||||
|
|
||||||
// WebSocket处理器类
|
// WebSocket处理器类
|
||||||
export class WebSocketHandler {
|
export class WebSocketHandler {
|
||||||
@@ -17,7 +18,6 @@ export class WebSocketHandler {
|
|||||||
this.onChatMessage = null; // 新增:聊天消息回调
|
this.onChatMessage = null; // 新增:聊天消息回调
|
||||||
this.currentSessionId = null;
|
this.currentSessionId = null;
|
||||||
this.isRemoteSpeaking = false;
|
this.isRemoteSpeaking = false;
|
||||||
this.isEstablishConnection = false; // 新增:是否已建立连接
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送hello握手消息
|
// 发送hello握手消息
|
||||||
@@ -74,7 +74,7 @@ export class WebSocketHandler {
|
|||||||
handleTextMessage(message) {
|
handleTextMessage(message) {
|
||||||
if (message.type === 'hello') {
|
if (message.type === 'hello') {
|
||||||
log(`服务器回应:${JSON.stringify(message, null, 2)}`, 'success');
|
log(`服务器回应:${JSON.stringify(message, null, 2)}`, 'success');
|
||||||
this.isEstablishConnection = true;
|
uiController.startAIChatSession();
|
||||||
} else if (message.type === 'tts') {
|
} else if (message.type === 'tts') {
|
||||||
this.handleTTSMessage(message);
|
this.handleTTSMessage(message);
|
||||||
} else if (message.type === 'audio') {
|
} else if (message.type === 'audio') {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// 背景图加载检测
|
||||||
|
(function() {
|
||||||
|
const backgroundContainer = document.getElementById('backgroundContainer');
|
||||||
|
|
||||||
|
// 提取背景图片URL
|
||||||
|
let bgImageUrl = window.getComputedStyle(backgroundContainer).backgroundImage;
|
||||||
|
const urlMatch = bgImageUrl && bgImageUrl.match(/url\(["']?(.*?)["']?\)/);
|
||||||
|
|
||||||
|
if (!urlMatch || !urlMatch[1]) {
|
||||||
|
console.warn('未提取到有效的背景图片URL');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bgImageUrl = urlMatch[1];
|
||||||
|
|
||||||
|
const bgImage = new Image();
|
||||||
|
bgImage.onerror = function() {
|
||||||
|
console.error('背景图片加载失败:', bgImageUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载成功显示模型加载
|
||||||
|
bgImage.onload = function() {
|
||||||
|
modelLoading.style.display = 'flex';
|
||||||
|
};
|
||||||
|
|
||||||
|
bgImage.src = bgImageUrl;
|
||||||
|
})();
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
// 背景图加载检测
|
|
||||||
const backgroundContainer = document.getElementById('backgroundContainer');
|
|
||||||
// 提取背景图片URL
|
|
||||||
let bgImageUrl = window.getComputedStyle(backgroundContainer).backgroundImage;
|
|
||||||
const urlMatch = bgImageUrl.match(/url\(["']?(.*?)["']?\)/);
|
|
||||||
bgImageUrl = urlMatch[1];
|
|
||||||
const bgImage = new Image();
|
|
||||||
bgImage.src = bgImageUrl;
|
|
||||||
// 加载完成后显示模型加载
|
|
||||||
bgImage.onload = function () {
|
|
||||||
const modelLoading = document.getElementById('modelLoading');
|
|
||||||
if (modelLoading) {
|
|
||||||
modelLoading.style.display = 'flex';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// UI控制模块
|
// UI控制模块
|
||||||
import { loadConfig, saveConfig, getConfig } from '../config/manager.js';
|
import { loadConfig, saveConfig } from '../config/manager.js';
|
||||||
import { getAudioRecorder } from '../core/audio/recorder.js';
|
import { getAudioRecorder } from '../core/audio/recorder.js';
|
||||||
import { getWebSocketHandler } from '../core/network/websocket.js';
|
import { getWebSocketHandler } from '../core/network/websocket.js';
|
||||||
import { getAudioPlayer } from '../core/audio/player.js';
|
import { getAudioPlayer } from '../core/audio/player.js';
|
||||||
@@ -11,7 +11,6 @@ class UIController {
|
|||||||
this.visualizerCanvas = null;
|
this.visualizerCanvas = null;
|
||||||
this.visualizerContext = null;
|
this.visualizerContext = null;
|
||||||
this.audioStatsTimer = null;
|
this.audioStatsTimer = null;
|
||||||
this.wsTimer = null;
|
|
||||||
this.currentBackgroundIndex = 0;
|
this.currentBackgroundIndex = 0;
|
||||||
this.backgroundImages = ['1.png', '2.png', '3.png'];
|
this.backgroundImages = ['1.png', '2.png', '3.png'];
|
||||||
|
|
||||||
@@ -93,6 +92,7 @@ class UIController {
|
|||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
wsHandler.disconnect();
|
wsHandler.disconnect();
|
||||||
this.updateDialButton(false);
|
this.updateDialButton(false);
|
||||||
|
this.addChatMessage('已断开连接,期待下次再见~😉', false);
|
||||||
} else {
|
} else {
|
||||||
// 检查OTA地址是否已填写
|
// 检查OTA地址是否已填写
|
||||||
const otaUrlInput = document.getElementById('otaUrl');
|
const otaUrlInput = document.getElementById('otaUrl');
|
||||||
@@ -152,8 +152,12 @@ class UIController {
|
|||||||
const closeButtons = document.querySelectorAll('.close-btn');
|
const closeButtons = document.querySelectorAll('.close-btn');
|
||||||
closeButtons.forEach(btn => {
|
closeButtons.forEach(btn => {
|
||||||
btn.addEventListener('click', (e) => {
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
const modal = e.target.closest('.modal');
|
const modal = e.target.closest('.modal');
|
||||||
if (modal) {
|
if (modal) {
|
||||||
|
if (modal.id === 'settingsModal') {
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
this.hideModal(modal.id);
|
this.hideModal(modal.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -172,23 +176,19 @@ class UIController {
|
|||||||
modals.forEach(modal => {
|
modals.forEach(modal => {
|
||||||
modal.addEventListener('click', (e) => {
|
modal.addEventListener('click', (e) => {
|
||||||
if (e.target === modal) {
|
if (e.target === modal) {
|
||||||
|
if (modal.id === 'settingsModal') {
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
this.hideModal(modal.id);
|
this.hideModal(modal.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 保存配置按钮
|
|
||||||
const saveConfigBtn = document.getElementById('saveConfigBtn');
|
|
||||||
if (saveConfigBtn) {
|
|
||||||
saveConfigBtn.addEventListener('click', () => {
|
|
||||||
this.saveConfig();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加MCP工具按钮
|
// 添加MCP工具按钮
|
||||||
const addMCPToolBtn = document.getElementById('addMCPToolBtn');
|
const addMCPToolBtn = document.getElementById('addMCPToolBtn');
|
||||||
if (addMCPToolBtn) {
|
if (addMCPToolBtn) {
|
||||||
addMCPToolBtn.addEventListener('click', () => {
|
addMCPToolBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
this.addMCPTool();
|
this.addMCPTool();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -330,47 +330,9 @@ class UIController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存配置
|
// 连接成功后开始对话
|
||||||
saveConfig() {
|
startAIChatSession() {
|
||||||
const config = {
|
this.addChatMessage('连接成功,开始聊天吧~🙂', false);
|
||||||
serverUrl: document.getElementById('serverUrl').value,
|
|
||||||
serverPort: document.getElementById('serverPort').value,
|
|
||||||
audioDevice: document.getElementById('audioDevice').value,
|
|
||||||
audioSampleRate: document.getElementById('audioSampleRate').value,
|
|
||||||
audioChannels: document.getElementById('audioChannels').value
|
|
||||||
};
|
|
||||||
|
|
||||||
saveConfig(config);
|
|
||||||
this.hideModal('settingsModal');
|
|
||||||
|
|
||||||
// 显示保存成功消息
|
|
||||||
this.addChatMessage('配置已保存', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理未绑定设备事件
|
|
||||||
handleUnboundDevice() {
|
|
||||||
const dialBtn = document.getElementById('dialBtn');
|
|
||||||
if (dialBtn) {
|
|
||||||
dialBtn.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 拨号成功后直接开始录音
|
|
||||||
dialAndRecord() {
|
|
||||||
const recordBtn = document.getElementById('recordBtn');
|
|
||||||
const wsHandler = getWebSocketHandler();
|
|
||||||
this.wsTimer = setInterval(() => {
|
|
||||||
if (wsHandler.isConnected() && wsHandler.websocket.onopen) {
|
|
||||||
clearInterval(this.wsTimer);
|
|
||||||
this.wsTimer = null;
|
|
||||||
if (wsHandler.isEstablishConnection) {
|
|
||||||
recordBtn.click();
|
|
||||||
} else {
|
|
||||||
this.handleUnboundDevice();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理连接按钮点击
|
// 处理连接按钮点击
|
||||||
@@ -412,21 +374,12 @@ class UIController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取配置信息
|
|
||||||
const config = getConfig();
|
|
||||||
|
|
||||||
// 导入OTA连接器
|
// 获取WebSocket处理器
|
||||||
const { webSocketConnect } = await import('../core/network/ota-connector.js');
|
const wsHandler = getWebSocketHandler();
|
||||||
|
const isConnected = await wsHandler.connect();
|
||||||
|
|
||||||
// 建立OTA连接
|
if (isConnected) {
|
||||||
const websocket = await webSocketConnect(otaUrl, config);
|
|
||||||
|
|
||||||
if (websocket) {
|
|
||||||
// 获取WebSocket处理器
|
|
||||||
const wsHandler = getWebSocketHandler();
|
|
||||||
|
|
||||||
// 设置WebSocket连接
|
|
||||||
wsHandler.websocket = websocket;
|
|
||||||
|
|
||||||
// 设置连接状态回调
|
// 设置连接状态回调
|
||||||
wsHandler.onConnectionStateChange = (isConnected) => {
|
wsHandler.onConnectionStateChange = (isConnected) => {
|
||||||
@@ -462,16 +415,10 @@ class UIController {
|
|||||||
dialBtn.disabled = false;
|
dialBtn.disabled = false;
|
||||||
dialBtn.querySelector('.btn-text').textContent = '挂断';
|
dialBtn.querySelector('.btn-text').textContent = '挂断';
|
||||||
dialBtn.classList.add('dial-active');
|
dialBtn.classList.add('dial-active');
|
||||||
|
|
||||||
this.dialAndRecord();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hideModal('settingsModal');
|
this.hideModal('settingsModal');
|
||||||
|
|
||||||
// 自动尝试建立WebSocket连接
|
|
||||||
setTimeout(() => {
|
|
||||||
wsHandler.connect();
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('OTA连接失败');
|
throw new Error('OTA连接失败');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
// ota 是否连接成功,修改成对应的样式
|
|
||||||
export function otaStatusStyle(flan) {
|
|
||||||
const otaStatusElement = document.getElementById('otaStatus');
|
|
||||||
if (otaStatusElement) {
|
|
||||||
if (flan) {
|
|
||||||
otaStatusElement.textContent = 'OTA已连接';
|
|
||||||
otaStatusElement.style.color = 'green';
|
|
||||||
} else {
|
|
||||||
otaStatusElement.textContent = 'OTA未连接';
|
|
||||||
otaStatusElement.style.color = 'red';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新Opus库状态显示
|
|
||||||
export function updateScriptStatus(message, type) {
|
|
||||||
const statusElement = document.getElementById('scriptStatus');
|
|
||||||
if (statusElement) {
|
|
||||||
statusElement.textContent = message;
|
|
||||||
statusElement.className = `script-status ${type}`;
|
|
||||||
statusElement.style.display = 'block';
|
|
||||||
statusElement.style.width = 'auto';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
<div class="config-row">
|
<div class="config-row">
|
||||||
<div class="config-item">
|
<div class="config-item">
|
||||||
<label for="deviceMac">设备MAC:</label>
|
<label for="deviceMac">设备MAC:</label>
|
||||||
<input type="text" id="deviceMac" placeholder="device-id" readonly>
|
<input type="text" id="deviceMac" placeholder="device-id">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="config-row">
|
<div class="config-row">
|
||||||
@@ -143,7 +143,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="config-item">
|
<div class="config-item">
|
||||||
<label for="deviceName">设备名称:</label>
|
<label for="deviceName">设备名称:</label>
|
||||||
<input type="text" id="deviceName" value="Web测试设备" placeholder="deviceName">
|
<input type="text" id="deviceName" value="Web测试设备" maxlength="50" placeholder="deviceName">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -236,8 +236,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 背景图加载检测 -->
|
<!-- 背景加载 -->
|
||||||
<script src="js/ui/bg-image-load-detector.js"></script>
|
<script src="js/ui/background-load.js"></script>
|
||||||
|
|
||||||
<!-- PIXI.js 2D渲染引擎 -->
|
<!-- PIXI.js 2D渲染引擎 -->
|
||||||
<script src="js/live2d/pixi.js"></script>
|
<script src="js/live2d/pixi.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user