mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-21 22:53:56 +08:00
Merge branch 'main' into manager_build
This commit is contained in:
+3
-2
@@ -107,7 +107,6 @@ celerybeat.pid
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
@@ -151,10 +150,12 @@ main/manager-web/node_modules
|
||||
.config.yaml
|
||||
.secrets.yaml
|
||||
.private_config.yaml
|
||||
.env.development
|
||||
|
||||
# model files
|
||||
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
|
||||
main/xiaozhi-server/models/sherpa-onnx*
|
||||
my_wakeup_words.mp3
|
||||
main/manager-api/.vscode
|
||||
|
||||
# Ignore webpack cache directory
|
||||
main/manager-web/.webpack_cache/
|
||||
@@ -0,0 +1 @@
|
||||
VUE_APP_TITLE=小智-智控台
|
||||
@@ -0,0 +1 @@
|
||||
VUE_APP_API_BASE_URL=https://2662r3426b.vicp.fun
|
||||
@@ -0,0 +1 @@
|
||||
VUE_APP_API_BASE_URL=https://2662r3426b.vicp.fun
|
||||
@@ -0,0 +1 @@
|
||||
registry=https://registry.npmmirror.com/
|
||||
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
['@vue/cli-plugin-babel/preset', {
|
||||
useBuiltIns: 'usage',
|
||||
corejs: 3
|
||||
}]
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-syntax-dynamic-import', // 确保支持动态导入 (Lazy Loading)
|
||||
'@babel/plugin-transform-runtime'
|
||||
]
|
||||
}
|
||||
|
||||
Generated
+1010
-1424
File diff suppressed because it is too large
Load Diff
@@ -4,9 +4,12 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build"
|
||||
"build": "vue-cli-service build",
|
||||
"analyze": "cross-env ANALYZE=true vue-cli-service build"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.41.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"element-ui": "^2.15.14",
|
||||
"flyio": "^0.6.14",
|
||||
"normalize.css": "^8.0.1",
|
||||
@@ -19,16 +22,25 @@
|
||||
"xiaozhi": "file:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.26.10",
|
||||
"@vue/cli-plugin-router": "~5.0.0",
|
||||
"@vue/cli-plugin-vuex": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"compression-webpack-plugin": "^11.1.0",
|
||||
"sass": "^1.32.7",
|
||||
"sass-loader": "^12.0.0",
|
||||
"vue-template-compiler": "^2.6.14"
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
],
|
||||
"sideEffects": [
|
||||
"*.css",
|
||||
"*.scss",
|
||||
"*.vue"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,17 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>小智-智控台</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>
|
||||
<%= process.env.VUE_APP_TITLE %>
|
||||
</title>
|
||||
<% if (htmlWebpackPlugin.options.cdn) { %>
|
||||
<% for (var i in htmlWebpackPlugin.options.cdn.css) { %>
|
||||
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>">
|
||||
<% } %>
|
||||
<% } %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
|
||||
Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<% if (htmlWebpackPlugin.options.cdn) { %>
|
||||
<% for (var i in htmlWebpackPlugin.options.cdn.js) { %>
|
||||
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -28,5 +28,5 @@ nav {
|
||||
}
|
||||
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
<script>
|
||||
</script>
|
||||
@@ -7,7 +7,7 @@ import admin from './module/admin.js'
|
||||
* 如果你想调用8002端口,就用'/xiaozhi-esp32-api/api/v1',请与vue.config.js的devServer配置相结合,方便跨域请求
|
||||
*
|
||||
*/
|
||||
const DEV_API_SERVICE = 'https://2662r3426b.vicp.fun/xiaozhi-esp32-api'
|
||||
// const DEV_API_SERVICE = 'https://2662r3426b.vicp.fun/xiaozhi-esp32-api'
|
||||
// 8002开发完成完成后使用这个
|
||||
// const DEV_API_SERVICE = '/xiaozhi-esp32-api'
|
||||
|
||||
@@ -16,7 +16,7 @@ const DEV_API_SERVICE = 'https://2662r3426b.vicp.fun/xiaozhi-esp32-api'
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getServiceUrl() {
|
||||
return DEV_API_SERVICE
|
||||
return '/xiaozhi-esp32-api'
|
||||
}
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.8 MiB After Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 357 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 MiB After Width: | Height: | Size: 1.9 MiB |
@@ -2,7 +2,7 @@
|
||||
<el-dialog :visible.sync="visible" width="400px" center>
|
||||
<div style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||
<div style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||
<img src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
||||
<img loading="lazy" src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
||||
</div>
|
||||
添加智慧体
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||
<div
|
||||
style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||
<img src="@/assets/login/shield.png" alt="" style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
|
||||
<img loading="lazy" src="@/assets/login/shield.png" alt="" style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
|
||||
</div>
|
||||
修改密码
|
||||
</div>
|
||||
|
||||
@@ -3,22 +3,22 @@
|
||||
<div class="header-container">
|
||||
<!-- 左侧元素 -->
|
||||
<div class="header-left">
|
||||
<img alt="" src="@/assets/xiaozhi-logo.png" class="logo-img"/>
|
||||
<img alt="" src="@/assets/xiaozhi-ai.png" class="brand-img"/>
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" class="logo-img"/>
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" class="brand-img"/>
|
||||
</div>
|
||||
|
||||
<!-- 中间导航菜单 -->
|
||||
<div class="header-center">
|
||||
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/home' }" @click="goHome">
|
||||
<img alt="" src="@/assets/header/roboot.png" :style="{ filter: $route.path === '/home' ? 'brightness(0) invert(1)' : 'None' }"/>
|
||||
<img loading="lazy" alt="" src="@/assets/header/roboot.png" :style="{ filter: $route.path === '/home' ? 'brightness(0) invert(1)' : 'None' }"/>
|
||||
智能体管理
|
||||
</div>
|
||||
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
|
||||
<img alt="" src="@/assets/header/user_management.png" :style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }"/>
|
||||
<img loading="lazy" alt="" src="@/assets/header/user_management.png" :style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }"/>
|
||||
用户管理
|
||||
</div>
|
||||
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }" @click="goModelConfig">
|
||||
<img alt="" src="@/assets/header/model_config.png" :style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }"/>
|
||||
<img loading="lazy" alt="" src="@/assets/header/model_config.png" :style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }"/>
|
||||
模型配置
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,7 +35,7 @@
|
||||
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
||||
</el-input>
|
||||
</div>
|
||||
<img alt="" src="@/assets/home/avatar.png" class="avatar-img"/>
|
||||
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img"/>
|
||||
<el-dropdown trigger="click" class="user-dropdown">
|
||||
<span class="el-dropdown-link">
|
||||
{{ userInfo.mobile || '加载中...' }}<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
|
||||
@@ -5,6 +5,7 @@ import store from './store'
|
||||
import 'normalize.css/normalize.css' // A modern alternative to CSS resets
|
||||
import ElementUI from 'element-ui';
|
||||
import 'element-ui/lib/theme-chalk/index.css';
|
||||
import './styles/global.scss'
|
||||
|
||||
Vue.use(ElementUI);
|
||||
Vue.config.productionTip = false
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// 覆盖 autofill 样式
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
textarea:-webkit-autofill,
|
||||
textarea:-webkit-autofill:hover,
|
||||
textarea:-webkit-autofill:focus,
|
||||
select:-webkit-autofill,
|
||||
select:-webkit-autofill:hover,
|
||||
select:-webkit-autofill:focus {
|
||||
-webkit-box-shadow: 0 0 0px 1000px transparent inset;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
background-color: transparent;
|
||||
}
|
||||
@@ -9,11 +9,11 @@
|
||||
语音设置
|
||||
</el-button>
|
||||
<el-button plain size="small" @click="handleImport" style="background: #7b9de5; color: white;">
|
||||
<img alt="" src="@/assets/model/inner_conf.png">
|
||||
<img loading="lazy" alt="" src="@/assets/model/inner_conf.png">
|
||||
导入配置
|
||||
</el-button>
|
||||
<el-button plain size="small" @click="handleExport" style="background: #71c9d1; color: white;">
|
||||
<img alt="" src="@/assets/model/output_conf.png">
|
||||
<img loading="lazy" alt="" src="@/assets/model/output_conf.png">
|
||||
导出配置
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
<el-header>
|
||||
<div
|
||||
style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
||||
<img alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;"/>
|
||||
<img alt="" src="@/assets/xiaozhi-ai.png" style="width: 70px;height: 13px;"/>
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;"/>
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="width: 70px;height: 13px;"/>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main style="position: relative;">
|
||||
<div class="login-box">
|
||||
<div
|
||||
style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
||||
<img alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;"/>
|
||||
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;"/>
|
||||
<div class="login-text">登录</div>
|
||||
<div class="login-welcome">
|
||||
WELCOME TO LOGIN
|
||||
@@ -20,19 +20,19 @@
|
||||
</div>
|
||||
<div style="padding: 0 30px;">
|
||||
<div class="input-box">
|
||||
<img alt="" class="input-icon" src="@/assets/login/username.png"/>
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png"/>
|
||||
<el-input v-model="form.username" placeholder="请输入用户名"/>
|
||||
</div>
|
||||
<div class="input-box">
|
||||
<img alt="" class="input-icon" src="@/assets/login/password.png"/>
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png"/>
|
||||
<el-input v-model="form.password" placeholder="请输入密码" type="password"/>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||
<img alt="" class="input-icon" src="@/assets/login/shield.png"/>
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png"/>
|
||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
|
||||
</div>
|
||||
<img v-if="captchaUrl"
|
||||
<img loading="lazy" v-if="captchaUrl"
|
||||
:src="captchaUrl"
|
||||
alt="验证码"
|
||||
style="width: 150px; height: 40px; cursor: pointer;"
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<!-- 保持相同的头部 -->
|
||||
<el-header>
|
||||
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
||||
<img alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;"/>
|
||||
<img alt="" src="@/assets/xiaozhi-ai.png" style="width: 70px;height: 13px;"/>
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;"/>
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="width: 70px;height: 13px;"/>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="login-box">
|
||||
<!-- 修改标题部分 -->
|
||||
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
||||
<img alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;"/>
|
||||
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;"/>
|
||||
<div class="login-text">注册</div>
|
||||
<div class="login-welcome">
|
||||
WELCOME TO REGISTER
|
||||
@@ -23,29 +23,29 @@
|
||||
<div style="padding: 0 30px;">
|
||||
<!-- 用户名输入框 -->
|
||||
<div class="input-box">
|
||||
<img alt="" class="input-icon" src="@/assets/login/username.png"/>
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png"/>
|
||||
<el-input v-model="form.username" placeholder="请输入用户名"/>
|
||||
</div>
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<div class="input-box">
|
||||
<img alt="" class="input-icon" src="@/assets/login/password.png"/>
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png"/>
|
||||
<el-input v-model="form.password" placeholder="请输入密码" type="password"/>
|
||||
</div>
|
||||
|
||||
<!-- 新增确认密码 -->
|
||||
<div class="input-box">
|
||||
<img alt="" class="input-icon" src="@/assets/login/password.png"/>
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png"/>
|
||||
<el-input v-model="form.confirmPassword" placeholder="请确认密码" type="password"/>
|
||||
</div>
|
||||
|
||||
<!-- 验证码部分保持相同 -->
|
||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||
<img alt="" class="input-icon" src="@/assets/login/shield.png"/>
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png"/>
|
||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
|
||||
</div>
|
||||
<img v-if="captchaUrl"
|
||||
<img loading="lazy" v-if="captchaUrl"
|
||||
:src="captchaUrl"
|
||||
alt="验证码"
|
||||
style="width: 150px; height: 40px; cursor: pointer;"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
style="padding: 15px 24px;font-weight: 700;font-size: 19px;text-align: left;color: #3d4566;display: flex;gap: 13px;align-items: center;">
|
||||
<div
|
||||
style="width: 37px;height: 37px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
|
||||
<img src="@/assets/home/setting-user.png" alt="" style="width: 19px;height: 19px;"/>
|
||||
<img loading="lazy" src="@/assets/home/setting-user.png" alt="" style="width: 19px;height: 19px;"/>
|
||||
</div>
|
||||
{{ form.agentName }}
|
||||
</div>
|
||||
@@ -83,7 +83,7 @@
|
||||
重制
|
||||
</div>
|
||||
<div class="clear-text">
|
||||
<img src="@/assets/home/red-info.png" alt="" style="width: 19px;height: 19px;"/>
|
||||
<img loading="lazy" src="@/assets/home/red-info.png" alt="" style="width: 19px;height: 19px;"/>
|
||||
保存配置后,需要重启设备,新的配置才会生效。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+121
-132
@@ -18,70 +18,68 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import Recorder from 'opus-recorder';
|
||||
import { OpusDecoder } from 'opus-decoder';
|
||||
|
||||
export default {
|
||||
name: 'TestPage',
|
||||
setup() {
|
||||
const messages = ref([]);
|
||||
const chatContainer = ref(null);
|
||||
const wsStatus = ref('disconnected');
|
||||
const isRecording = ref(false);
|
||||
let ws = null;
|
||||
let recorder = null;
|
||||
let stream = null;
|
||||
let audioContext = null;
|
||||
let sourceNode = null;
|
||||
let audioDecoder = null;
|
||||
let audioBufferQueue = []; // 当前句子的音频缓冲区
|
||||
let playbackQueue = []; // 播放队列,存储待播放的句子
|
||||
let isPlaying = false;
|
||||
|
||||
const connectWebSocket = () => {
|
||||
ws = new WebSocket('ws://192.168.3.97:8000');//修改服务端地址
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = () => {
|
||||
wsStatus.value = 'connected';
|
||||
data() {
|
||||
return {
|
||||
messages: [],
|
||||
wsStatus: 'disconnected',
|
||||
isRecording: false,
|
||||
ws: null,
|
||||
recorder: null,
|
||||
stream: null,
|
||||
audioContext: null,
|
||||
sourceNode: null,
|
||||
audioDecoder: null,
|
||||
audioBufferQueue: [], // 当前句子的音频缓冲区
|
||||
playbackQueue: [], // 播放队列,存储待播放的句子
|
||||
isPlaying: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
connectWebSocket() {
|
||||
this.ws = new WebSocket('ws://192.168.3.97:8000');//修改服务端地址
|
||||
this.ws.binaryType = 'arraybuffer';
|
||||
this.ws.onopen = () => {
|
||||
this.wsStatus = 'connected';
|
||||
console.log('WebSocket 连接成功');
|
||||
ws.send(JSON.stringify({ type: 'auth', 'device-id': 'test-device' }));
|
||||
audioDecoder = new OpusDecoder({ sampleRate: 16000, channels: 1 });
|
||||
this.ws.send(JSON.stringify({ type: 'auth', 'device-id': 'test-device' }));
|
||||
this.audioDecoder = new OpusDecoder({ sampleRate: 16000, channels: 1 });
|
||||
};
|
||||
ws.onmessage = async (event) => {
|
||||
this.ws.onmessage = async (event) => {
|
||||
if (typeof event.data === 'string') {
|
||||
const msg = JSON.parse(event.data);
|
||||
console.log('收到文本消息:', msg);
|
||||
|
||||
if (msg.type === 'stt') {
|
||||
messages.value.push({ role: 'user', content: msg.text });
|
||||
scrollToBottom();
|
||||
this.messages.push({ role: 'user', content: msg.text });
|
||||
this.scrollToBottom();
|
||||
} else if (msg.type === 'llm') {
|
||||
messages.value.push({ role: 'assistant', content: msg.text });
|
||||
scrollToBottom();
|
||||
this.messages.push({ role: 'assistant', content: msg.text });
|
||||
this.scrollToBottom();
|
||||
} else if (msg.type === 'tts') {
|
||||
if (msg.state === 'sentence_start') {
|
||||
// 开始新句子,清空当前缓冲区
|
||||
audioBufferQueue = [];
|
||||
const lastMessage = messages.value[messages.value.length - 1];
|
||||
this.audioBufferQueue = [];
|
||||
const lastMessage = this.messages[this.messages.length - 1];
|
||||
if (!lastMessage || lastMessage.content !== msg.text) {
|
||||
messages.value.push({ role: 'assistant', content: msg.text });
|
||||
scrollToBottom();
|
||||
this.messages.push({ role: 'assistant', content: msg.text });
|
||||
this.scrollToBottom();
|
||||
}
|
||||
} else if (msg.state === 'sentence_end') {
|
||||
// 句子结束,将当前缓冲区加入播放队列
|
||||
if (audioBufferQueue.length > 0) {
|
||||
playbackQueue.push([...audioBufferQueue]);
|
||||
audioBufferQueue = [];
|
||||
playNextInQueue(); // 尝试播放队列中的下一句
|
||||
if (this.audioBufferQueue.length > 0) {
|
||||
this.playbackQueue.push([...this.audioBufferQueue]);
|
||||
this.audioBufferQueue = [];
|
||||
this.playNextInQueue();
|
||||
}
|
||||
} else if (msg.state === 'stop') {
|
||||
console.log('TTS 任务结束');
|
||||
// 确保所有剩余音频播放
|
||||
if (audioBufferQueue.length > 0) {
|
||||
playbackQueue.push([...audioBufferQueue]);
|
||||
audioBufferQueue = [];
|
||||
playNextInQueue();
|
||||
if (this.audioBufferQueue.length > 0) {
|
||||
this.playbackQueue.push([...this.audioBufferQueue]);
|
||||
this.audioBufferQueue = [];
|
||||
this.playNextInQueue();
|
||||
}
|
||||
}
|
||||
} else if (msg.type === 'hello') {
|
||||
@@ -96,18 +94,17 @@ export default {
|
||||
console.log('音频帧前8字节:', frameHead);
|
||||
|
||||
try {
|
||||
const decoded = audioDecoder.decodeFrame(opusFrame);
|
||||
const decoded = this.audioDecoder.decodeFrame(opusFrame);
|
||||
console.log('解码结果:', decoded);
|
||||
if (decoded && decoded.channelData && decoded.channelData[0]) {
|
||||
const pcmData = decoded.channelData[0];
|
||||
if (pcmData.length > 0) {
|
||||
audioBufferQueue.push(pcmData);
|
||||
this.audioBufferQueue.push(pcmData);
|
||||
console.log('解码音频帧,PCM 数据长度:', pcmData.length);
|
||||
// 如果缓冲区达到一定长度(例如 5 帧,180ms),立即播放
|
||||
if (audioBufferQueue.length >= 5 && playbackQueue.length === 0 && !isPlaying) {
|
||||
playbackQueue.push([...audioBufferQueue]);
|
||||
audioBufferQueue = [];
|
||||
playNextInQueue();
|
||||
if (this.audioBufferQueue.length >= 5 && this.playbackQueue.length === 0 && !this.isPlaying) {
|
||||
this.playbackQueue.push([...this.audioBufferQueue]);
|
||||
this.audioBufferQueue = [];
|
||||
this.playNextInQueue();
|
||||
}
|
||||
} else {
|
||||
console.warn('解码成功,但 PCM 数据长度为 0');
|
||||
@@ -120,39 +117,39 @@ export default {
|
||||
}
|
||||
}
|
||||
};
|
||||
ws.onerror = (error) => {
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('WebSocket 错误:', error);
|
||||
wsStatus.value = 'error';
|
||||
this.wsStatus = 'error';
|
||||
};
|
||||
ws.onclose = () => {
|
||||
wsStatus.value = 'disconnected';
|
||||
this.ws.onclose = () => {
|
||||
this.wsStatus = 'disconnected';
|
||||
console.log('WebSocket 断开');
|
||||
setTimeout(connectWebSocket, 1000);
|
||||
setTimeout(this.connectWebSocket, 1000);
|
||||
};
|
||||
};
|
||||
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
if (chatContainer.value) chatContainer.value.scrollTop = chatContainer.value.scrollHeight;
|
||||
},
|
||||
scrollToBottom() {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.chatContainer) {
|
||||
this.$refs.chatContainer.scrollTop = this.$refs.chatContainer.scrollHeight;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const playNextInQueue = async () => {
|
||||
if (isPlaying || playbackQueue.length === 0) {
|
||||
return; // 正在播放或队列为空,等待下次触发
|
||||
},
|
||||
async playNextInQueue() {
|
||||
if (this.isPlaying || this.playbackQueue.length === 0) {
|
||||
return;
|
||||
}
|
||||
isPlaying = true;
|
||||
this.isPlaying = true;
|
||||
|
||||
if (!audioContext || audioContext.state === 'closed') {
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
|
||||
if (!this.audioContext || this.audioContext.state === 'closed') {
|
||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
|
||||
}
|
||||
|
||||
const pcmBuffers = playbackQueue.shift(); // 取出队列中的第一句
|
||||
const pcmBuffers = this.playbackQueue.shift();
|
||||
const totalLength = pcmBuffers.reduce((sum, pcm) => sum + pcm.length, 0);
|
||||
if (totalLength === 0) {
|
||||
console.error('音频缓冲区总长度为 0,无法播放');
|
||||
isPlaying = false;
|
||||
playNextInQueue(); // 尝试播放下一句
|
||||
this.isPlaying = false;
|
||||
this.playNextInQueue();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -163,22 +160,21 @@ export default {
|
||||
offset += pcm.length;
|
||||
}
|
||||
|
||||
const audioBuffer = audioContext.createBuffer(1, totalLength, 16000);
|
||||
const audioBuffer = this.audioContext.createBuffer(1, totalLength, 16000);
|
||||
audioBuffer.getChannelData(0).set(mergedPcm);
|
||||
|
||||
const source = audioContext.createBufferSource();
|
||||
const source = this.audioContext.createBufferSource();
|
||||
source.buffer = audioBuffer;
|
||||
source.connect(audioContext.destination);
|
||||
source.connect(this.audioContext.destination);
|
||||
source.onended = () => {
|
||||
isPlaying = false;
|
||||
this.isPlaying = false;
|
||||
console.log('音频播放结束');
|
||||
playNextInQueue(); // 播放结束后继续下一句
|
||||
this.playNextInQueue();
|
||||
};
|
||||
source.start();
|
||||
console.log('开始播放音频,总长度:', totalLength);
|
||||
};
|
||||
|
||||
const stripOggContainer = (data) => {
|
||||
},
|
||||
stripOggContainer(data) {
|
||||
let arrayBuffer;
|
||||
if (data instanceof ArrayBuffer) {
|
||||
arrayBuffer = data;
|
||||
@@ -227,23 +223,22 @@ export default {
|
||||
}
|
||||
console.log('剥离后找到', frames.length, '个裸 Opus 帧');
|
||||
return frames;
|
||||
};
|
||||
|
||||
const initRecorder = async () => {
|
||||
},
|
||||
async initRecorder() {
|
||||
console.log('开始初始化录音');
|
||||
try {
|
||||
if (stream) {
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
if (this.stream) {
|
||||
this.stream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
console.log('获取麦克风权限成功,stream:', stream);
|
||||
this.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
console.log('获取麦克风权限成功,stream:', this.stream);
|
||||
|
||||
if (!audioContext || audioContext.state === 'closed') {
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
|
||||
if (!this.audioContext || this.audioContext.state === 'closed') {
|
||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
|
||||
}
|
||||
sourceNode = audioContext.createMediaStreamSource(stream);
|
||||
this.sourceNode = this.audioContext.createMediaStreamSource(this.stream);
|
||||
|
||||
recorder = new Recorder({
|
||||
this.recorder = new Recorder({
|
||||
encoderPath: '/encoderWorker.min.js',
|
||||
sampleRate: 16000,
|
||||
numberOfChannels: 1,
|
||||
@@ -252,17 +247,16 @@ export default {
|
||||
encoderFrameSize: 60,
|
||||
encoderBitRate: 24000,
|
||||
monitorGain: 0,
|
||||
sourceNode: sourceNode,
|
||||
sourceNode: this.sourceNode,
|
||||
ogg: false,
|
||||
streamPages: false,
|
||||
maxFramesPerPage: 1,
|
||||
});
|
||||
|
||||
recorder.ondataavailable = (data) => {
|
||||
this.recorder.ondataavailable = (data) => {
|
||||
console.log('录音数据可用:', data.byteLength, '类型:', data.constructor.name);
|
||||
const frames = stripOggContainer(data);
|
||||
const frames = this.stripOggContainer(data);
|
||||
frames.forEach((frame, index) => {
|
||||
const frameView = new DataView(frame);
|
||||
const frameHead = Array.from(new Uint8Array(frame.slice(0, 8)))
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join(' ');
|
||||
@@ -273,8 +267,8 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(frame);
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(frame);
|
||||
console.log('发送裸 Opus 帧:', frame.byteLength);
|
||||
} else {
|
||||
console.warn('WebSocket 未连接,跳过发送');
|
||||
@@ -282,16 +276,16 @@ export default {
|
||||
});
|
||||
};
|
||||
|
||||
recorder.onstart = () => {
|
||||
this.recorder.onstart = () => {
|
||||
console.log('录音已启动');
|
||||
};
|
||||
|
||||
recorder.onstop = () => {
|
||||
this.recorder.onstop = () => {
|
||||
console.log('录音停止');
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
stream = null;
|
||||
sourceNode = null;
|
||||
scrollToBottom();
|
||||
this.stream.getTracks().forEach(track => track.stop());
|
||||
this.stream = null;
|
||||
this.sourceNode = null;
|
||||
this.scrollToBottom();
|
||||
};
|
||||
|
||||
console.log('Recorder 初始化成功');
|
||||
@@ -299,48 +293,43 @@ export default {
|
||||
console.error('初始化录音失败:', err);
|
||||
alert('无法访问麦克风或录音初始化失败,请检查权限');
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRecording = async () => {
|
||||
console.log('点击 toggleRecording,当前状态:', isRecording.value, 'WebSocket 状态:', wsStatus.value);
|
||||
if (!recorder) {
|
||||
await initRecorder();
|
||||
if (!recorder) {
|
||||
},
|
||||
async toggleRecording() {
|
||||
console.log('点击 toggleRecording,当前状态:', this.isRecording, 'WebSocket 状态:', this.wsStatus);
|
||||
if (!this.recorder) {
|
||||
await this.initRecorder();
|
||||
if (!this.recorder) {
|
||||
console.error('recorder 初始化失败');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (isRecording.value) {
|
||||
if (this.isRecording) {
|
||||
console.log('停止录音');
|
||||
recorder.stop();
|
||||
isRecording.value = false;
|
||||
this.recorder.stop();
|
||||
this.isRecording = false;
|
||||
} else {
|
||||
try {
|
||||
console.log('开始录音');
|
||||
await initRecorder();
|
||||
await recorder.start();
|
||||
console.log('录音开始后,状态:', recorder.state);
|
||||
isRecording.value = true;
|
||||
await this.initRecorder();
|
||||
await this.recorder.start();
|
||||
console.log('录音开始后,状态:', this.recorder.state);
|
||||
this.isRecording = true;
|
||||
} catch (err) {
|
||||
console.error('录音启动失败:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
console.log('组件挂载,初始化 WebSocket');
|
||||
connectWebSocket();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (ws) ws.close();
|
||||
if (stream) stream.getTracks().forEach(track => track.stop());
|
||||
if (recorder) recorder.stop();
|
||||
if (audioContext) audioContext.close();
|
||||
if (audioDecoder) audioDecoder.destroy();
|
||||
});
|
||||
|
||||
return { messages, chatContainer, wsStatus, isRecording, toggleRecording };
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
console.log('组件挂载,初始化 WebSocket');
|
||||
this.connectWebSocket();
|
||||
},
|
||||
destroyed() {
|
||||
if (this.ws) this.ws.close();
|
||||
if (this.stream) this.stream.getTracks().forEach(track => track.stop());
|
||||
if (this.recorder) this.recorder.stop();
|
||||
if (this.audioContext) this.audioContext.close();
|
||||
if (this.audioDecoder) this.audioDecoder.destroy();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
+135
-16
@@ -1,24 +1,143 @@
|
||||
const { defineConfig } = require('@vue/cli-service');
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
// TerserPlugin 用于压缩 JavaScript
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
// CompressionPlugin 开启 Gzip 压缩
|
||||
const CompressionPlugin = require('compression-webpack-plugin')
|
||||
// BundleAnalyzerPlugin 用于分析打包后的文件
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
// 引入 path 模块
|
||||
const path = require('path');
|
||||
// 确保加载 .env 文件
|
||||
dotenv.config();
|
||||
|
||||
module.exports = defineConfig({
|
||||
devServer: {
|
||||
// Bug 修复:将代理配置为环境变量中定义的 API 基础 URL
|
||||
port: 8001, // 指定端口为 8001
|
||||
proxy: {
|
||||
'/xiaozhi-esp32-api': {
|
||||
target: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8002', // 后端 API 的基础 URL
|
||||
changeOrigin: true, // 允许跨域
|
||||
// pathRewrite: {
|
||||
// '^/api': '', // 路径重写
|
||||
// },
|
||||
},
|
||||
},
|
||||
client: {
|
||||
overlay: false,
|
||||
productionSourceMap: process.env.NODE_ENV === 'production' ? false : true, // 生产环境不生成 source map
|
||||
devServer: {
|
||||
port: 8001, // 指定端口为 8001
|
||||
proxy: {
|
||||
'/xiaozhi-esp32-api': {
|
||||
target: process.env.VUE_APP_API_BASE_URL, // 后端 API 的基础 URL
|
||||
changeOrigin: true, // 允许跨域
|
||||
pathRewrite: {
|
||||
'^/xiaozhi-esp32-api': '/xiaozhi-esp32-api',
|
||||
},
|
||||
},
|
||||
},
|
||||
client: {
|
||||
overlay: false, // 不显示 webpack 错误覆盖层
|
||||
},
|
||||
},
|
||||
chainWebpack: config => {
|
||||
|
||||
// 修改 HTML 插件配置,动态插入 CDN 链接
|
||||
config.plugin('html')
|
||||
.tap(args => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
args[0].cdn = {
|
||||
css: [
|
||||
'https://cdn.jsdelivr.net/npm/element-ui@2.15.14/lib/theme-chalk/index.css',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css'
|
||||
],
|
||||
js: [
|
||||
'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/vue-router@3.6.5/dist/vue-router.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/vuex@3.6.2/dist/vuex.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/element-ui@2.15.14/lib/index.js',
|
||||
'https://cdn.jsdelivr.net/npm/axios@0.27.2/dist/axios.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/opus-decoder@0.7.7/dist/opus-decoder.min.js'
|
||||
]
|
||||
};
|
||||
}
|
||||
return args;
|
||||
});
|
||||
|
||||
// 代码分割优化
|
||||
config.optimization.splitChunks({
|
||||
chunks: 'all',
|
||||
minSize: 20000,
|
||||
maxSize: 250000,
|
||||
cacheGroups: {
|
||||
vendors: {
|
||||
name: 'chunk-vendors',
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
priority: -10,
|
||||
chunks: 'initial',
|
||||
},
|
||||
common: {
|
||||
name: 'chunk-common',
|
||||
minChunks: 2,
|
||||
priority: -20,
|
||||
chunks: 'initial',
|
||||
reuseExistingChunk: true,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
// 启用优化设置
|
||||
config.optimization.usedExports(true);
|
||||
config.optimization.concatenateModules(true);
|
||||
config.optimization.minimize(true);
|
||||
},
|
||||
configureWebpack: config => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// 开启多线程编译
|
||||
config.optimization = {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
parallel: true,
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: true,
|
||||
drop_debugger: true,
|
||||
pure_funcs: ['console.log']
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
};
|
||||
config.plugins.push(
|
||||
new CompressionPlugin({
|
||||
algorithm: 'gzip',
|
||||
test: /\.(js|css|html|svg)$/,
|
||||
threshold: 20480,
|
||||
minRatio: 0.8
|
||||
})
|
||||
);
|
||||
config.externals = {
|
||||
'vue': 'Vue',
|
||||
'vue-router': 'VueRouter',
|
||||
'vuex': 'Vuex',
|
||||
'element-ui': 'ELEMENT',
|
||||
'axios': 'axios',
|
||||
'opus-decoder': 'OpusDecoder'
|
||||
};
|
||||
if (process.env.ANALYZE === 'true') { // 通过环境变量控制
|
||||
config.plugins.push(
|
||||
new BundleAnalyzerPlugin({
|
||||
analyzerMode: 'server', // 开启本地服务器模式
|
||||
openAnalyzer: true, // 自动打开浏览器
|
||||
analyzerPort: 8888 // 指定端口号
|
||||
})
|
||||
);
|
||||
}
|
||||
config.cache = {
|
||||
type: 'filesystem', // 使用文件系统缓存
|
||||
cacheDirectory: path.resolve(__dirname, '.webpack_cache'), // 自定义缓存目录
|
||||
allowCollectingMemory: true, // 启用内存收集
|
||||
compression: 'gzip', // 启用gzip压缩缓存
|
||||
maxAge: 5184000000, // 缓存有效期为 1个月
|
||||
buildDependencies: {
|
||||
config: [__filename] // 每次配置文件修改时缓存失效
|
||||
}
|
||||
};
|
||||
config.resolve.alias = {
|
||||
'@': path.resolve(__dirname, 'src'), // 让 '@' 代表 'src' 目录
|
||||
'@assets': path.resolve(__dirname, 'src/assets'), // 设置 '@assets' 为 'src/assets' 目录
|
||||
'@components': path.resolve(__dirname, 'src/components'), // 设置 '@components' 为 'src/components' 目录
|
||||
'@views': path.resolve(__dirname, 'src/views'), // 设置 '@views' 为 'src/views' 目录
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@ prompt: |
|
||||
- 讨论感情 → 炫耀程序员男友但抱怨"他只会送键盘当礼物"
|
||||
- 问专业知识 → 先用梗回答,被追问才展示真实理解
|
||||
绝不:
|
||||
- 使用大陆网络流行语
|
||||
- 长篇大论,叽叽歪歪
|
||||
- 长时间严肃对话
|
||||
|
||||
# 使用完声音文件后删除文件(Delete the sound file when you are done using it)
|
||||
|
||||
@@ -3,24 +3,32 @@ import sys
|
||||
from loguru import logger
|
||||
from config.settings import load_config
|
||||
|
||||
SERVER_VERSION = "0.1.16"
|
||||
SERVER_VERSION = "0.1.17"
|
||||
|
||||
|
||||
def setup_logging():
|
||||
"""从配置文件中读取日志配置,并设置日志输出格式和级别"""
|
||||
config = load_config()
|
||||
log_config = config["log"]
|
||||
log_format = log_config.get("log_format", "<green>{time:YYMMDD HH:mm:ss}</green>[{version}_{selected_module}][<light-blue>{extra[tag]}</light-blue>]-<level>{level}</level>-<light-green>{message}</light-green>")
|
||||
log_format_file = log_config.get("log_format_file", "{time:YYYY-MM-DD HH:mm:ss} - {version_{selected_module}} - {name} - {level} - {extra[tag]} - {message}")
|
||||
log_format = log_config.get(
|
||||
"log_format",
|
||||
"<green>{time:YYMMDD HH:mm:ss}</green>[{version}_{selected_module}][<light-blue>{extra[tag]}</light-blue>]-<level>{level}</level>-<light-green>{message}</light-green>",
|
||||
)
|
||||
log_format_file = log_config.get(
|
||||
"log_format_file",
|
||||
"{time:YYYY-MM-DD HH:mm:ss} - {version_{selected_module}} - {name} - {level} - {extra[tag]} - {message}",
|
||||
)
|
||||
|
||||
selected_module = config.get("selected_module")
|
||||
selected_module_str = ''.join([value[0] + value[1] for key, value in selected_module.items()])
|
||||
selected_module_str = "".join(
|
||||
[value[0] + value[1] for key, value in selected_module.items()]
|
||||
)
|
||||
|
||||
log_format = log_format.replace("{version}", SERVER_VERSION)
|
||||
log_format = log_format.replace("{selected_module}", selected_module_str)
|
||||
log_format_file = log_format_file.replace("{version}", SERVER_VERSION)
|
||||
log_format_file = log_format_file.replace("{selected_module}", selected_module_str)
|
||||
|
||||
|
||||
log_level = log_config.get("log_level", "INFO")
|
||||
log_dir = log_config.get("log_dir", "tmp")
|
||||
log_file = log_config.get("log_file", "server.log")
|
||||
|
||||
@@ -195,7 +195,7 @@ class ConnectionHandler:
|
||||
self.logger.bind(tag=TAG).error(f"Connection error: {str(e)}-{stack_trace}")
|
||||
return
|
||||
finally:
|
||||
self._save_and_close(ws)
|
||||
await self._save_and_close(ws)
|
||||
|
||||
async def _save_and_close(self, ws):
|
||||
"""保存记忆并关闭连接"""
|
||||
@@ -253,13 +253,9 @@ class ConnectionHandler:
|
||||
else:
|
||||
# 否则使用主LLM
|
||||
self.intent.set_llm(self.llm)
|
||||
self.logger.bind(tag=TAG).info("意图识别使用主LLM")
|
||||
|
||||
# 记录意图识别LLM初始化耗时
|
||||
intent_llm_init_time = time.time() - intent_llm_init_start
|
||||
self.logger.bind(tag=TAG).info(
|
||||
f"意图识别LLM初始化完成,耗时: {intent_llm_init_time:.4f}秒"
|
||||
)
|
||||
|
||||
"""加载位置信息"""
|
||||
self.client_ip_info = get_ip_info(self.client_ip)
|
||||
|
||||
@@ -45,7 +45,7 @@ async def check_direct_exit(conn, text):
|
||||
|
||||
async def analyze_intent_with_llm(conn, text):
|
||||
"""使用LLM分析用户意图"""
|
||||
if not hasattr(conn, 'intent') or not conn.intent:
|
||||
if not hasattr(conn, "intent") or not conn.intent:
|
||||
logger.bind(tag=TAG).warning("意图识别服务未初始化")
|
||||
return None
|
||||
|
||||
@@ -69,7 +69,9 @@ async def process_intent_result(conn, intent_result, original_text):
|
||||
# 检查是否有function_call
|
||||
if "function_call" in intent_data:
|
||||
# 直接从意图识别获取了function_call
|
||||
logger.bind(tag=TAG).info(f"检测到function_call格式的意图结果: {intent_data['function_call']['name']}")
|
||||
logger.bind(tag=TAG).info(
|
||||
f"检测到function_call格式的意图结果: {intent_data['function_call']['name']}"
|
||||
)
|
||||
function_name = intent_data["function_call"]["name"]
|
||||
if function_name == "continue_chat":
|
||||
return False
|
||||
@@ -83,7 +85,7 @@ async def process_intent_result(conn, intent_result, original_text):
|
||||
function_call_data = {
|
||||
"name": function_name,
|
||||
"id": str(uuid.uuid4().hex),
|
||||
"arguments": function_args
|
||||
"arguments": function_args,
|
||||
}
|
||||
|
||||
await send_stt_message(conn, original_text)
|
||||
@@ -91,16 +93,24 @@ async def process_intent_result(conn, intent_result, original_text):
|
||||
# 使用executor执行函数调用和结果处理
|
||||
def process_function_call():
|
||||
conn.dialogue.put(Message(role="user", content=original_text))
|
||||
result = conn.func_handler.handle_llm_function_call(conn, function_call_data)
|
||||
if result and function_name != 'play_music':
|
||||
result = conn.func_handler.handle_llm_function_call(
|
||||
conn, function_call_data
|
||||
)
|
||||
if result and function_name != "play_music":
|
||||
# 获取当前最新的文本索引
|
||||
text = result.response
|
||||
if text is None:
|
||||
text = result.result
|
||||
if text is not None:
|
||||
text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0
|
||||
text_index = (
|
||||
conn.tts_last_text_index + 1
|
||||
if hasattr(conn, "tts_last_text_index")
|
||||
else 0
|
||||
)
|
||||
conn.recode_first_last_text(text, text_index)
|
||||
future = conn.executor.submit(conn.speak_and_play, text, text_index)
|
||||
future = conn.executor.submit(
|
||||
conn.speak_and_play, text, text_index
|
||||
)
|
||||
conn.llm_finish_task = True
|
||||
conn.tts_queue.put(future)
|
||||
conn.dialogue.put(Message(role="assistant", content=text))
|
||||
@@ -121,10 +131,14 @@ def extract_text_in_brackets(s):
|
||||
:param s: 输入字符串
|
||||
:return: 中括号内的文字,如果不存在则返回空字符串
|
||||
"""
|
||||
left_bracket_index = s.find('[')
|
||||
right_bracket_index = s.find(']')
|
||||
left_bracket_index = s.find("[")
|
||||
right_bracket_index = s.find("]")
|
||||
|
||||
if left_bracket_index != -1 and right_bracket_index != -1 and left_bracket_index < right_bracket_index:
|
||||
return s[left_bracket_index + 1:right_bracket_index]
|
||||
if (
|
||||
left_bracket_index != -1
|
||||
and right_bracket_index != -1
|
||||
and left_bracket_index < right_bracket_index
|
||||
):
|
||||
return s[left_bracket_index + 1 : right_bracket_index]
|
||||
else:
|
||||
return ""
|
||||
|
||||
@@ -12,50 +12,72 @@ class WebSocketServer:
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
self.logger = setup_logging()
|
||||
self._vad, self._asr, self._llm, self._tts, self._memory, self.intent = self._create_processing_instances()
|
||||
self._vad, self._asr, self._llm, self._tts, self._memory, self.intent = (
|
||||
self._create_processing_instances()
|
||||
)
|
||||
self.active_connections = set() # 添加全局连接记录
|
||||
|
||||
def _create_processing_instances(self):
|
||||
memory_cls_name = self.config["selected_module"].get("Memory", "nomem") # 默认使用nomem
|
||||
has_memory_cfg = self.config.get("Memory") and memory_cls_name in self.config["Memory"]
|
||||
memory_cls_name = self.config["selected_module"].get(
|
||||
"Memory", "nomem"
|
||||
) # 默认使用nomem
|
||||
has_memory_cfg = (
|
||||
self.config.get("Memory") and memory_cls_name in self.config["Memory"]
|
||||
)
|
||||
memory_cfg = self.config["Memory"][memory_cls_name] if has_memory_cfg else {}
|
||||
|
||||
"""创建处理模块实例"""
|
||||
return (
|
||||
vad.create_instance(
|
||||
self.config["selected_module"]["VAD"],
|
||||
self.config["VAD"][self.config["selected_module"]["VAD"]]
|
||||
self.config["VAD"][self.config["selected_module"]["VAD"]],
|
||||
),
|
||||
asr.create_instance(
|
||||
self.config["selected_module"]["ASR"]
|
||||
if not 'type' in self.config["ASR"][self.config["selected_module"]["ASR"]]
|
||||
else
|
||||
self.config["ASR"][self.config["selected_module"]["ASR"]]["type"],
|
||||
(
|
||||
self.config["selected_module"]["ASR"]
|
||||
if not "type"
|
||||
in self.config["ASR"][self.config["selected_module"]["ASR"]]
|
||||
else self.config["ASR"][self.config["selected_module"]["ASR"]][
|
||||
"type"
|
||||
]
|
||||
),
|
||||
self.config["ASR"][self.config["selected_module"]["ASR"]],
|
||||
self.config["delete_audio"]
|
||||
self.config["delete_audio"],
|
||||
),
|
||||
llm.create_instance(
|
||||
self.config["selected_module"]["LLM"]
|
||||
if not 'type' in self.config["LLM"][self.config["selected_module"]["LLM"]]
|
||||
else
|
||||
self.config["LLM"][self.config["selected_module"]["LLM"]]['type'],
|
||||
(
|
||||
self.config["selected_module"]["LLM"]
|
||||
if not "type"
|
||||
in self.config["LLM"][self.config["selected_module"]["LLM"]]
|
||||
else self.config["LLM"][self.config["selected_module"]["LLM"]][
|
||||
"type"
|
||||
]
|
||||
),
|
||||
self.config["LLM"][self.config["selected_module"]["LLM"]],
|
||||
),
|
||||
tts.create_instance(
|
||||
self.config["selected_module"]["TTS"]
|
||||
if not 'type' in self.config["TTS"][self.config["selected_module"]["TTS"]]
|
||||
else
|
||||
self.config["TTS"][self.config["selected_module"]["TTS"]]["type"],
|
||||
(
|
||||
self.config["selected_module"]["TTS"]
|
||||
if not "type"
|
||||
in self.config["TTS"][self.config["selected_module"]["TTS"]]
|
||||
else self.config["TTS"][self.config["selected_module"]["TTS"]][
|
||||
"type"
|
||||
]
|
||||
),
|
||||
self.config["TTS"][self.config["selected_module"]["TTS"]],
|
||||
self.config["delete_audio"]
|
||||
self.config["delete_audio"],
|
||||
),
|
||||
memory.create_instance(memory_cls_name, memory_cfg),
|
||||
intent.create_instance(
|
||||
self.config["selected_module"]["Intent"]
|
||||
if not 'type' in self.config["Intent"][self.config["selected_module"]["Intent"]]
|
||||
else
|
||||
self.config["Intent"][self.config["selected_module"]["Intent"]]["type"],
|
||||
self.config["Intent"][self.config["selected_module"]["Intent"]]
|
||||
(
|
||||
self.config["selected_module"]["Intent"]
|
||||
if not "type"
|
||||
in self.config["Intent"][self.config["selected_module"]["Intent"]]
|
||||
else self.config["Intent"][
|
||||
self.config["selected_module"]["Intent"]
|
||||
]["type"]
|
||||
),
|
||||
self.config["Intent"][self.config["selected_module"]["Intent"]],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -64,19 +86,33 @@ class WebSocketServer:
|
||||
host = server_config["ip"]
|
||||
port = server_config["port"]
|
||||
|
||||
self.logger.bind(tag=TAG).info("Server is running at ws://{}:{}", get_local_ip(), port)
|
||||
self.logger.bind(tag=TAG).info("=======上面的地址是websocket协议地址,请勿用浏览器访问=======")
|
||||
async with websockets.serve(
|
||||
self._handle_connection,
|
||||
host,
|
||||
port
|
||||
):
|
||||
self.logger.bind(tag=TAG).info(
|
||||
"Server is running at ws://{}:{}/xiaozhi/v1/", get_local_ip(), port
|
||||
)
|
||||
self.logger.bind(tag=TAG).info(
|
||||
"=======上面的地址是websocket协议地址,请勿用浏览器访问======="
|
||||
)
|
||||
self.logger.bind(tag=TAG).info(
|
||||
"如想测试websocket请用谷歌浏览器打开test目录下的test_page.html"
|
||||
)
|
||||
self.logger.bind(tag=TAG).info(
|
||||
"=============================================================\n"
|
||||
)
|
||||
async with websockets.serve(self._handle_connection, host, port):
|
||||
await asyncio.Future()
|
||||
|
||||
async def _handle_connection(self, websocket):
|
||||
"""处理新连接,每次创建独立的ConnectionHandler"""
|
||||
# 创建ConnectionHandler时传入当前server实例
|
||||
handler = ConnectionHandler(self.config, self._vad, self._asr, self._llm, self._tts, self._memory, self.intent)
|
||||
handler = ConnectionHandler(
|
||||
self.config,
|
||||
self._vad,
|
||||
self._asr,
|
||||
self._llm,
|
||||
self._tts,
|
||||
self._memory,
|
||||
self.intent,
|
||||
)
|
||||
self.active_connections.add(handler)
|
||||
try:
|
||||
await handler.handle_connection(websocket)
|
||||
|
||||
@@ -22,5 +22,5 @@ mem0ai==0.1.62
|
||||
bs4==0.0.2
|
||||
modelscope==1.23.2
|
||||
sherpa_onnx==1.11.0
|
||||
cnlunar==0.2.0
|
||||
mcp==1.4.1
|
||||
cnlunar==0.2.0
|
||||
|
||||
Reference in New Issue
Block a user