4 Commits

Author SHA1 Message Date
liqupan
309b1318a7 feat: 优化语音速度 2025-12-06 22:42:05 +08:00
liqupan
7fe4b05cf8 feat: 1130 2025-11-30 21:25:23 +08:00
liqupan
792fa980f9 feat:支持语音 2025-11-29 00:18:09 +08:00
6087a3f195 Merge pull request 'feat:mvp' (#1) from feat/lpq/1108 into master
Reviewed-on: #1
2025-11-24 18:46:47 +08:00
11 changed files with 2576 additions and 66 deletions

3
.gitignore vendored
View File

@@ -72,4 +72,5 @@ coverage/
# UniApp specific
unpackage/
dist/
.history/
.history/
purple-energy-visualizer/

269
TEST_MODE_GUIDE.md Normal file
View File

@@ -0,0 +1,269 @@
# 🧪 WebSocket语音流测试模式使用指南
## 概述
测试模式允许你在不使用实际录音功能的情况下测试WebSocket语音流式对话功能。这对于在不支持录音的环境如某些微信小程序开发环境或需要使用特定测试数据的场景非常有用。
## 启用测试模式
`webUI/src/utils/config.js` 中配置:
```javascript
TEST_MODE: {
enabled: true, // 设置为 true 启用测试模式false 使用正常录音
audioDuration: 3, // 测试音频时长(秒)
sampleRate: 16000, // 采样率Hz
channels: 1, // 声道数1=单声道2=立体声)
bitDepth: 16 // 位深度8或16
}
```
## 测试模式特性
### 启用时enabled: true
- ✅ 显示 "🧪 发送测试音频" 按钮
- ✅ 禁用自动录音功能
- ✅ 点击按钮发送假数据到WebSocket
- ✅ 页面提示 "测试模式:点击发送假数据"
### 禁用时enabled: false
- ✅ 隐藏测试按钮
- ✅ 启用正常录音功能
- ✅ 自动模式下自动开始录音
## 使用步骤
1. **启用测试模式**
```javascript
// 在 config.js 中
TEST_MODE: { enabled: true }
```
2. **进入语音模式**
- 点击输入框右侧的 🎧 图标切换到语音模式
3. **发送测试数据**
- 点击 "🧪 发送测试音频" 按钮
- 系统会自动生成假PCM数据并发送到WebSocket服务器
4. **观察结果**
- STT识别结果会显示在聊天界面
- AI回复会实时流式显示
- TTS音频会实时播放
## PCM音频数据格式说明
### 当前生成的测试数据格式
代码会自动生成符合以下规格的PCM数据
```javascript
{
采样率: 16000 Hz, // 16kHz语音常用采样率
位深度: 16 bit, // 16位有符号整数
声道数: 1, // 单声道
字节序: Little Endian, // 小端序
时长: 3秒,
数据大小: 96000 bytes // = 16000 * 3 * 2
}
```
### 测试数据生成原理
代码生成的是一个模拟人声的音频信号:
- **基频**: 200Hz人声范围
- **泛音**: 包含2次和3次谐波让声音更丰富
- **噪声**: 添加轻微随机噪声,模拟呼吸音
- **包络**: 淡入淡出效果,让声音更自然
### 如果需要使用真实音频文件
如果你想使用真实的音频文件进行测试需要先将音频转换为PCM格式
#### 1. 使用FFmpeg转换
```bash
# 从MP3/WAV转换为PCM
ffmpeg -i input.mp3 -f s16le -acodec pcm_s16le -ar 16000 -ac 1 output.pcm
# 参数说明:
# -f s16le: 输出格式为16位小端序PCM
# -acodec pcm_s16le: 使用16位PCM编码
# -ar 16000: 采样率16kHz
# -ac 1: 单声道
```
#### 2. 在代码中使用真实PCM文件
修改 `ChatBox.vue` 中的 `generateTestPCMAudio()` 函数:
```javascript
// 方案A: 直接读取PCM文件
const generateTestPCMAudio = async () => {
// #ifdef MP-WEIXIN
const fs = uni.getFileSystemManager();
return new Promise((resolve, reject) => {
fs.readFile({
filePath: '/static/test_audio.pcm', // 你的PCM文件路径
success: (res) => {
resolve(res.data); // res.data 是 ArrayBuffer
},
fail: reject
});
});
// #endif
};
// 方案B: 从base64字符串加载
const TEST_PCM_BASE64 = "你的base64编码的PCM数据...";
const generateTestPCMAudio = () => {
return uni.base64ToArrayBuffer(TEST_PCM_BASE64);
};
```
#### 3. 获取音频的base64编码
```bash
# Linux/Mac
base64 output.pcm > output_base64.txt
# Windows PowerShell
[Convert]::ToBase64String([IO.File]::ReadAllBytes("output.pcm")) > output_base64.txt
```
然后将base64字符串复制到代码中。
## PCM数据格式要求
### 必须满足的要求
✅ **格式**: 原始PCM数据无文件头
✅ **采样率**: 16000 Hz后端STT服务要求
✅ **位深度**: 16 bit
✅ **声道数**: 1单声道
✅ **字节序**: Little Endian小端序
✅ **编码**: 有符号整数Signed Integer
### 数据大小计算
```
数据大小(字节)= 采样率 × 时长(秒)× 声道数 × (位深度/8)
示例3秒的音频
= 16000 × 3 × 1 × 2
= 96000 bytes
= 93.75 KB
```
## 注意事项
⚠️ **测试数据限制**
- 生成的测试数据是简单的合成音,不包含真实语音内容
- STT识别结果可能为空或乱码取决于STT服务对合成音的处理
- 如需测试真实识别请使用真实录音的PCM数据
⚠️ **WebSocket模式要求**
- 测试模式需要配合WebSocket模式使用
- 确保在 `ChatBox.vue` 中设置 `isWebSocketMode.value = true`
- 确保WebSocket服务器正在运行
⚠️ **性能考虑**
- 较长的音频文件会占用更多内存
- 建议测试音频时长不超过5秒
- 如需长时间测试,可以循环发送短音频
## 调试技巧
### 查看浏览器控制台
测试模式会输出详细日志:
```
[TestMode] 生成测试PCM音频: {
sampleRate: 16000,
duration: "3秒",
channels: 1,
bitDepth: "16位",
dataSize: "96000 bytes",
frequency: "200 Hz"
}
[TestMode] 开始发送测试音频数据
[VoiceMode] 测试音频已通过WebSocket发送
```
### 验证数据格式
在浏览器控制台检查:
```javascript
// 检查生成的数据
const data = generateTestPCMAudio();
console.log('数据类型:', data instanceof ArrayBuffer);
console.log('数据大小:', data.byteLength, 'bytes');
console.log('预期大小:', 16000 * 3 * 2, 'bytes');
```
## 故障排除
### 问题1: 点击按钮无反应
- 检查是否已进入语音模式(点击🎧图标)
- 检查是否启用了WebSocket模式
- 查看浏览器控制台是否有错误
### 问题2: STT识别结果为空
- 这是正常的,合成音不包含真实语音内容
- 使用真实PCM录音文件进行测试
### 问题3: WebSocket连接失败
- 检查后端服务是否运行
- 检查 `config.js` 中的 `WS_BASE_URL` 配置
- 检查网络连接和防火墙设置
### 问题4: 音频无法播放
- 检查返回的TTS音频格式
- 查看浏览器控制台的音频播放日志
- 确认播放权限已授予
## 示例代码
### 完整的测试流程示例
```javascript
// 1. 配置测试模式
// config.js
export const API_CONFIG = {
TEST_MODE: {
enabled: true,
audioDuration: 3,
sampleRate: 16000,
channels: 1,
bitDepth: 16
}
};
// 2. 进入语音模式
toggleVoiceMode(); // 点击🎧图标
// 3. 发送测试数据
sendTestAudio(); // 点击测试按钮
// 4. 观察回调
voiceStreamWs.on('sttResult', (text) => {
console.log('识别结果:', text);
});
voiceStreamWs.on('sentence', (sentence) => {
console.log('完整句子:', sentence);
});
voiceStreamWs.on('audioChunk', (audioData) => {
console.log('收到音频:', audioData.byteLength, 'bytes');
});
```
## 总结
测试模式为你提供了一个便捷的方式来测试WebSocket语音流功能无需实际录音设备。通过生成符合格式的假PCM数据你可以验证整个语音对话流程的正确性。
如有任何问题,请检查浏览器控制台的日志输出,那里会有详细的调试信息。

View File

@@ -54,6 +54,7 @@
"@dcloudio/uni-quickapp-webview": "3.0.0-4060420250429001",
"ant-design-vue": "^4.2.6",
"pinia": "^2.1.7",
"three": "^0.181.2",
"vue": "^3.4.21",
"vue-i18n": "^9.1.9"
},

File diff suppressed because it is too large Load Diff

BIN
src/static/output.pcm Normal file

Binary file not shown.

View File

@@ -565,6 +565,29 @@ export const voiceAPI = {
authHeader = loginStatus.token.startsWith('Bearer ') ? loginStatus.token : 'Bearer ' + loginStatus.token;
}
// 构建formData支持更多参数
const formData = {
modelId: options.modelId || null,
templateId: options.templateId || null,
voiceStyle: options.voiceStyle || 'default'
};
// 添加可选参数
if (options.sessionId) {
formData.sessionId = options.sessionId;
}
if (options.ttsConfigId) {
formData.ttsConfigId = options.ttsConfigId;
}
if (options.sttConfigId) {
formData.sttConfigId = options.sttConfigId;
}
if (options.useFunctionCall !== undefined) {
formData.useFunctionCall = options.useFunctionCall;
}
console.log('语音对话参数:', formData);
return new Promise((resolve) => {
uni.uploadFile({
url: getApiUrl(API_CONFIG.ENDPOINTS.VOICE_CHAT),
@@ -573,11 +596,7 @@ export const voiceAPI = {
header: authHeader ? {
'Authorization': authHeader
} : {},
formData: {
modelId: options.modelId || null,
templateId: options.templateId || null,
voiceStyle: options.voiceStyle || 'default'
},
formData: formData,
success: (res) => {
console.log('语音对话上传成功:', res);
@@ -586,41 +605,46 @@ export const voiceAPI = {
console.log('语音对话响应数据:', data);
if (data.code === 200) {
// 根据后端实际返回结构提取字段
// 返回完整的data对象包括sttResult、llmResult、ttsResult
const responseData = data.data || {};
// 兼容旧格式:提取关键字段
let aiResponse = null;
let userText = null;
let audioUrl = null;
let audioBase64 = null;
// 从 data.llmResult.response 提取AI回复
if (data.data && data.data.llmResult && data.data.llmResult.response) {
aiResponse = data.data.llmResult.response;
if (responseData.llmResult && responseData.llmResult.response) {
aiResponse = responseData.llmResult.response;
}
// 从 data.sttResult.text 提取用户文本(语音转文字)
if (data.data && data.data.sttResult && data.data.sttResult.text) {
userText = data.data.sttResult.text;
if (responseData.sttResult && responseData.sttResult.text) {
userText = responseData.sttResult.text;
}
// 从 data.ttsResult.audioPath 提取音频路径
if (data.data && data.data.ttsResult && data.data.ttsResult.audioPath) {
audioUrl = data.data.ttsResult.audioPath;
// 从 data.ttsResult 提取音频
if (responseData.ttsResult) {
audioUrl = responseData.ttsResult.audioPath;
audioBase64 = responseData.ttsResult.audioBase64;
}
// 备用字段提取(保持向后兼容)
if (!aiResponse) {
if (data.response && typeof data.response === 'string') {
aiResponse = data.response;
} else if (data.data && data.data.response) {
aiResponse = data.data.response;
} else if (responseData.response) {
aiResponse = responseData.response;
}
}
if (!userText) {
userText = data.userText || data.data?.userText || data.data?.text || data.data?.user_text || data.data?.recognizedText || data.data?.transcription;
userText = data.userText || responseData.userText || responseData.text || responseData.user_text || responseData.recognizedText || responseData.transcription;
}
if (!audioUrl) {
audioUrl = data.audioPath || data.audioUrl || data.data?.audioUrl || data.data?.url || data.data?.audio_url || data.data?.speechUrl || data.data?.ttsUrl || data.data?.audioPath;
if (!audioUrl && !audioBase64) {
audioUrl = data.audioPath || data.audioUrl || responseData.audioUrl || responseData.url || responseData.audio_url || responseData.speechUrl || responseData.ttsUrl || responseData.audioPath;
}
// 清理AI回复文本
@@ -631,9 +655,16 @@ export const voiceAPI = {
resolve({
success: true,
data: {
// 兼容旧接口
userText: userText,
aiResponse: cleanedAiResponse,
audioUrl: audioUrl
audioUrl: audioUrl,
// 新增完整数据结构
sttResult: responseData.sttResult || { text: userText },
llmResult: responseData.llmResult || { response: cleanedAiResponse, inputText: userText },
ttsResult: responseData.ttsResult || { audioPath: audioUrl, audioBase64: audioBase64 },
sessionId: responseData.sessionId || null,
timestamp: responseData.timestamp || Date.now()
}
});
} else {

View File

@@ -1,11 +1,32 @@
// API配置统一管理
export const API_CONFIG = {
// 基础API地址
BASE_URL: 'https://api.aixsy.com.cn',
BASE_URL: 'http://192.168.3.13:8091',
// WebSocket地址
WS_BASE_URL: 'ws://192.168.3.13:8091',
// 其他服务地址(如果需要)
WEB_URL: 'https://www.aixsy.com.cn',
// 🧪 测试模式配置
TEST_MODE: {
// 是否启用测试模式true: 显示测试按钮,禁用录音; false: 正常录音模式)
enabled: false,
// 📝 测试音频数据base64编码的PCM数据
// 格式要求:
// - 采样率: 16000 Hz
// - 位深度: 16 bit (有符号整数)
// - 声道数: 1 (单声道)
// - 字节序: Little Endian (小端序)
// - 无文件头纯PCM数据
testAudioBase64: '', // 👈 在这里填入你的base64编码的PCM数据
// 或者使用文件路径优先使用base64
testAudioPath: 'src/static/output.pcm', // 例如: '/static/test_audio.pcm'
},
// API端点
ENDPOINTS: {
// 登录相关
@@ -44,6 +65,11 @@ export const API_CONFIG = {
CONFIG_TTS: '/app/config/tts'
},
// WebSocket端点
WS_ENDPOINTS: {
VOICE_STREAM: '/ws/voice-stream'
},
// 请求超时时间(毫秒)
TIMEOUT: 30000,
@@ -61,3 +87,8 @@ export const getWebUrl = (endpoint) => {
return API_CONFIG.WEB_URL + endpoint;
};
// 导出WebSocket URL构建函数
export const getWsUrl = (endpoint) => {
return API_CONFIG.WS_BASE_URL + endpoint;
};

View File

@@ -0,0 +1,310 @@
/**
* 语音流式对话 WebSocket 管理模块
*/
class VoiceStreamWebSocket {
constructor(url) {
this.url = url
this.ws = null
this.isConnected = false
this.reconnectAttempts = 0
this.maxReconnectAttempts = 5
this.reconnectDelay = 2000
this.manualClose = false // 标记是否为主动关闭
// 事件监听器
this.listeners = {
onConnected: null,
onDisconnected: null,
onSttResult: null,
onLlmToken: null,
onSentence: null,
onAudioChunk: null,
onComplete: null,
onError: null
}
}
/**
* 建立连接
* @param {String} sessionId - 聊天会话ID用于历史记录查询和保存与文字对话的sessionId保持一致
* @param {Number} templateId - 模板ID
* @param {String} token - 认证令牌(可选)
* @param {String} userId - 用户ID可选
*/
connect(sessionId = null, templateId = null, token = null, userId = null) {
return new Promise((resolve, reject) => {
try {
// 重置主动关闭标志
this.manualClose = false
// 构建连接URL
let wsUrl = this.url
const params = []
if (sessionId) params.push(`sessionId=${sessionId}`)
if (templateId) params.push(`templateId=${templateId}`)
if (token) params.push(`token=${token}`)
if (userId) params.push(`userId=${userId}`)
if (params.length > 0) {
wsUrl += '?' + params.join('&')
}
console.log('[VoiceStreamWS] 正在连接:', wsUrl)
// 创建WebSocket连接
this.ws = uni.connectSocket({
url: wsUrl,
success: () => {
console.log('[VoiceStreamWS] WebSocket创建成功')
},
fail: (err) => {
console.error('[VoiceStreamWS] WebSocket创建失败:', err)
reject(err)
}
})
// 连接打开
this.ws.onOpen(() => {
console.log('[VoiceStreamWS] 连接已建立')
this.isConnected = true
this.reconnectAttempts = 0
if (this.listeners.onConnected) {
this.listeners.onConnected()
}
resolve()
})
// 接收消息
this.ws.onMessage((res) => {
this.handleMessage(res)
})
// 连接错误
this.ws.onError((err) => {
console.error('[VoiceStreamWS] 连接错误:', err)
this.isConnected = false
if (this.listeners.onError) {
this.listeners.onError('WebSocket连接错误')
}
})
// 连接关闭
this.ws.onClose(() => {
console.log('[VoiceStreamWS] 连接已关闭')
this.isConnected = false
if (this.listeners.onDisconnected) {
this.listeners.onDisconnected()
}
// 只有非主动关闭才尝试重连
if (!this.manualClose) {
console.log('[VoiceStreamWS] 检测到异常断开,将尝试自动重连')
this.tryReconnect()
} else {
console.log('[VoiceStreamWS] 主动关闭连接,不进行重连')
}
})
} catch (err) {
console.error('[VoiceStreamWS] 连接异常:', err)
reject(err)
}
})
}
/**
* 处理接收到的消息
*/
handleMessage(res) {
try {
// 二进制消息(音频数据)
if (res.data instanceof ArrayBuffer) {
console.log('[VoiceStreamWS] 收到音频数据:', res.data.byteLength, 'bytes')
if (this.listeners.onAudioChunk) {
this.listeners.onAudioChunk(res.data)
}
return
}
// 文本消息JSON格式
const message = JSON.parse(res.data)
console.log('[VoiceStreamWS] 收到消息:', message.type)
switch (message.type) {
case 'connected':
console.log('[VoiceStreamWS] 服务器确认连接')
break
case 'stt_result':
// STT识别结果
if (this.listeners.onSttResult) {
this.listeners.onSttResult(message.message)
}
break
case 'llm_token':
// LLM输出token
if (this.listeners.onLlmToken) {
this.listeners.onLlmToken(message.message)
}
break
case 'sentence':
// 完整句子
if (this.listeners.onSentence) {
this.listeners.onSentence(message.message)
}
break
case 'complete':
// 对话完成
if (this.listeners.onComplete) {
this.listeners.onComplete()
}
break
case 'error':
// 错误
console.error('[VoiceStreamWS] 服务器错误:', message.message)
if (this.listeners.onError) {
this.listeners.onError(message.message)
}
break
case 'pong':
// 心跳响应
console.log('[VoiceStreamWS] 心跳响应')
break
default:
console.warn('[VoiceStreamWS] 未知消息类型:', message.type)
}
} catch (err) {
console.error('[VoiceStreamWS] 处理消息失败:', err)
}
}
/**
* 发送音频数据
*/
sendAudio(audioData) {
if (!this.isConnected || !this.ws) {
console.error('[VoiceStreamWS] 未连接,无法发送音频')
return false
}
try {
this.ws.send({
data: audioData,
success: () => {
console.log('[VoiceStreamWS] 音频数据发送成功:', audioData.byteLength, 'bytes')
},
fail: (err) => {
console.error('[VoiceStreamWS] 音频数据发送失败:', err)
if (this.listeners.onError) {
this.listeners.onError('发送音频失败')
}
}
})
return true
} catch (err) {
console.error('[VoiceStreamWS] 发送音频异常:', err)
return false
}
}
/**
* 发送文本消息
*/
sendMessage(type, data = null) {
if (!this.isConnected || !this.ws) {
console.error('[VoiceStreamWS] 未连接,无法发送消息')
return false
}
try {
const message = {
type,
timestamp: Date.now()
}
if (data) {
message.data = data
}
this.ws.send({
data: JSON.stringify(message),
success: () => {
console.log('[VoiceStreamWS] 消息发送成功:', type)
},
fail: (err) => {
console.error('[VoiceStreamWS] 消息发送失败:', err)
}
})
return true
} catch (err) {
console.error('[VoiceStreamWS] 发送消息异常:', err)
return false
}
}
/**
* 取消当前对话(打断)
*/
cancel() {
return this.sendMessage('cancel')
}
/**
* 发送心跳
*/
ping() {
return this.sendMessage('ping')
}
/**
* 尝试重连
*/
tryReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.log('[VoiceStreamWS] 达到最大重连次数,停止重连')
return
}
this.reconnectAttempts++
console.log(`[VoiceStreamWS] 尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
setTimeout(() => {
if (!this.isConnected) {
this.connect().catch(err => {
console.error('[VoiceStreamWS] 重连失败:', err)
})
}
}, this.reconnectDelay)
}
/**
* 关闭连接
*/
close() {
if (this.ws) {
console.log('[VoiceStreamWS] 主动关闭连接')
this.manualClose = true // 标记为主动关闭
this.isConnected = false
this.reconnectAttempts = 0 // 重置重连计数
this.ws.close()
this.ws = null
}
}
/**
* 设置事件监听器
*/
on(event, callback) {
if (this.listeners.hasOwnProperty(`on${event.charAt(0).toUpperCase()}${event.slice(1)}`)) {
this.listeners[`on${event.charAt(0).toUpperCase()}${event.slice(1)}`] = callback
}
}
}
export default VoiceStreamWebSocket

View File

@@ -0,0 +1,262 @@
# 📝 如何准备测试音频数据
## 你需要提供的数据格式
### PCM格式要求重要
```
✅ 采样率: 16000 Hz
✅ 位深度: 16 bit (有符号整数Signed Integer)
✅ 声道数: 1 (单声道Mono)
✅ 字节序: Little Endian (小端序)
✅ 格式: 纯PCM数据无任何文件头不是WAV
```
### 数据大小计算
```
数据大小bytes= 采样率 × 时长(秒)× 2
示例:
- 1秒音频 = 16000 × 1 × 2 = 32,000 bytes ≈ 31.25 KB
- 3秒音频 = 16000 × 3 × 2 = 96,000 bytes ≈ 93.75 KB
- 5秒音频 = 16000 × 5 × 2 = 160,000 bytes ≈ 156.25 KB
```
## 方式一:使用 FFmpeg 转换(推荐)
### 1. 从MP3/WAV/M4A等格式转换
```bash
ffmpeg -i input.mp3 -f s16le -acodec pcm_s16le -ar 16000 -ac 1 output.pcm
```
**参数说明:**
- `-i input.mp3`: 输入文件(可以是任何音频格式)
- `-f s16le`: 输出格式为16位小端序PCM
- `-acodec pcm_s16le`: 使用16位PCM编码
- `-ar 16000`: 采样率16000 Hz
- `-ac 1`: 单声道
### 2. 验证生成的PCM文件
```bash
# 查看文件大小
ls -lh output.pcm
# 计算时长(秒)= 文件大小bytes/ 32000
# 例如96000 bytes / 32000 = 3 秒
```
### 3. 转换为base64
```bash
# Linux/Mac
base64 output.pcm > output_base64.txt
# 或者一行输出(适合小文件)
base64 output.pcm | tr -d '\n' > output_base64.txt
# Windows PowerShell
[Convert]::ToBase64String([IO.File]::ReadAllBytes("output.pcm")) > output_base64.txt
```
## 方式二:在线录音并导出
### 1. 使用Audacity免费开源
1. 打开Audacity
2. 点击红色按钮录音
3. 录制3-5秒的测试语音说点什么都可以
4. 点击停止
5. **设置项目采样率**:左下角设置为 `16000 Hz`
6. **转换为单声道**:轨道 → 混音 → 混音立体声为单声道
7. **导出**
- 文件 → 导出 → 导出音频
- 文件类型选择:`其他未压缩文件`
- 头部:`RAW (header-less)`
- 编码:`Signed 16-bit PCM`
- 保存为 `test.pcm`
### 2. 使用Python脚本录音
```python
import pyaudio
import wave
import struct
# 配置
RATE = 16000
CHANNELS = 1
FORMAT = pyaudio.paInt16
RECORD_SECONDS = 3
# 录音
audio = pyaudio.PyAudio()
stream = audio.open(format=FORMAT, channels=CHANNELS,
rate=RATE, input=True,
frames_per_buffer=1024)
print("录音中... (3秒)")
frames = []
for i in range(0, int(RATE / 1024 * RECORD_SECONDS)):
data = stream.read(1024)
frames.append(data)
print("录音完成")
stream.stop_stream()
stream.close()
audio.terminate()
# 保存为PCM
with open('output.pcm', 'wb') as f:
f.write(b''.join(frames))
print("已保存为 output.pcm")
```
## 如何使用生成的数据
### 选项A使用base64推荐适合小文件
1. **转换为base64**(见上面的命令)
2. **打开** `webUI/src/utils/config.js`
3. **填入base64数据**
```javascript
TEST_MODE: {
enabled: true,
testAudioBase64: 'AAEAAgADAAQABQAG...', // 👈 粘贴你的base64字符串
testAudioPath: '',
}
```
**完整示例:**
```javascript
TEST_MODE: {
enabled: true,
testAudioBase64: 'AAEAAgADAAQABQAGAAcACA...(很长的base64字符串)...==',
testAudioPath: '',
}
```
### 选项B使用文件路径适合大文件
1. **将PCM文件放入项目**
```
webUI/src/static/test_audio.pcm
```
2. **配置路径**
```javascript
TEST_MODE: {
enabled: true,
testAudioBase64: '', // 留空
testAudioPath: '/static/test_audio.pcm', // 👈 文件路径
}
```
## 快速测试数据示例
### 生成一个简单的测试文件Python
```python
import struct
# 生成3秒的简单正弦波200Hz
sample_rate = 16000
duration = 3
frequency = 200
with open('test.pcm', 'wb') as f:
for i in range(sample_rate * duration):
t = i / sample_rate
# 正弦波振幅8000
sample = int(8000 * (2 * 3.14159 * frequency * t) ** 0.5)
sample = max(-32768, min(32767, sample))
f.write(struct.pack('<h', sample)) # 小端序有符号16位
print("生成完成: test.pcm")
```
## 推荐的测试内容
建议你录制以下内容之一3-5秒
1. **简单问候**
- "你好"
- "今天天气怎么样"
- "给我讲个笑话"
2. **简单指令**
- "帮我查询一下"
- "打开设置"
- "我要了解一下"
3. **随意说话**
- 随便说点什么都可以
- 主要是测试流程是否正常
## 验证数据是否正确
### 检查文件大小
```bash
# 应该接近这个大小
# 3秒 = 96,000 bytes
# 5秒 = 160,000 bytes
ls -lh your_file.pcm
```
### 使用FFmpeg播放测试
```bash
# 如果能听到声音,说明格式正确
ffplay -f s16le -ar 16000 -ac 1 output.pcm
```
### 检查base64长度
```bash
# base64后的大小约为原始数据的 1.37 倍
# 96,000 bytes → 约 131,000 字符的base64
wc -c output_base64.txt
```
## 常见问题
### Q: 我的base64太长了怎么办
A: 建议音频不要超过5秒或者使用文件路径方式选项B
### Q: 如何确认生成的数据格式正确?
A:
1. 用ffplay播放测试
2. 检查文件大小是否符合公式
3. 查看控制台日志,看是否有格式错误
### Q: 为什么必须是16000Hz
A: 后端的STT服务要求16000Hz采样率
### Q: 可以用WAV文件吗
A: 不行必须是纯PCM数据不能有WAV文件头
### Q: 我应该说什么内容?
A: 随便说什么都可以,主要是测试流程。如果想测试识别准确度,建议说清晰的普通话
## 下一步
准备好数据后:
1. 填入 `config.js` 的 `testAudioBase64` 或 `testAudioPath`
2. 设置 `enabled: true`
3. 运行项目,进入语音模式
4. 点击 "🧪 发送测试音频" 按钮
5. 观察控制台日志和结果
---
**需要帮助?** 检查浏览器控制台的 `[TestMode]` 日志输出。

View File

@@ -0,0 +1,168 @@
# 🚀 测试数据快速生成命令
## 一键生成(最快方式)
### 方式1: 从现有音频转换
```bash
# 假设你有一个 input.mp3 文件
ffmpeg -i input.mp3 -f s16le -acodec pcm_s16le -ar 16000 -ac 1 test.pcm
# 转为base64一行无换行
base64 test.pcm | tr -d '\n' > test_base64.txt
# 复制 test_base64.txt 的内容,粘贴到 config.js 的 testAudioBase64
```
### 方式2: 快速录音Mac
```bash
# 录制3秒音频并自动转换
rec -r 16000 -c 1 -b 16 test.pcm trim 0 3
# 转为base64
base64 test.pcm | tr -d '\n' > test_base64.txt
```
### 方式3: 生成测试音频Python
```python
# 保存为 generate_test.py
import struct
import base64
sample_rate = 16000
duration = 3
data = bytearray()
for i in range(sample_rate * duration):
# 简单正弦波
import math
sample = int(8000 * math.sin(2 * math.pi * 200 * i / sample_rate))
data.extend(struct.pack('<h', sample))
# 保存PCM
with open('test.pcm', 'wb') as f:
f.write(data)
# 输出base64
b64 = base64.b64encode(data).decode()
print(f"生成完成!大小: {len(data)} bytes")
print(f"Base64长度: {len(b64)} 字符")
print(f"\n请复制以下内容到 config.js:")
print(f"testAudioBase64: '{b64[:100]}...(太长已截断请从test_base64.txt读取)'")
with open('test_base64.txt', 'w') as f:
f.write(b64)
```
运行:
```bash
python generate_test.py
```
## 配置到项目
### 打开配置文件
```bash
# 打开编辑器
code webUI/src/utils/config.js
# 或
vim webUI/src/utils/config.js
```
### 填入数据
```javascript
TEST_MODE: {
enabled: true,
// 方式A: 粘贴base64适合小文件<200KB
testAudioBase64: '此处粘贴你的base64字符串',
// 方式B: 使用文件路径(适合大文件)
testAudioPath: '/static/test_audio.pcm',
}
```
## 验证数据
### 播放测试PCM
```bash
# Mac/Linux
ffplay -f s16le -ar 16000 -ac 1 test.pcm
# 或使用aplay (Linux)
aplay -f S16_LE -r 16000 -c 1 test.pcm
```
### 检查大小
```bash
# 应该接近: 时长(秒) × 32000 bytes
ls -lh test.pcm
```
## 格式速查
```
格式: PCM
采样率: 16000 Hz
位深度: 16 bit (Signed)
声道: 1 (Mono)
字节序: Little Endian
公式: 时长(秒) × 32000 = 文件大小(bytes)
```
## 示例:完整流程
```bash
# 1. 录制或转换音频
ffmpeg -i my_voice.mp3 -f s16le -acodec pcm_s16le -ar 16000 -ac 1 test.pcm
# 2. 验证(能听到声音就对了)
ffplay -f s16le -ar 16000 -ac 1 test.pcm
# 3. 转base64无换行
base64 test.pcm | tr -d '\n' > test_base64.txt
# 4. 复制内容
cat test_base64.txt | pbcopy # Mac
# 或手动打开 test_base64.txt 复制
# 5. 粘贴到 config.js
# testAudioBase64: '刚才复制的内容'
# 完成!
```
## Windows 用户
### 使用 PowerShell
```powershell
# 转base64
$bytes = [System.IO.File]::ReadAllBytes("test.pcm")
$base64 = [System.Convert]::ToBase64String($bytes)
$base64 | Out-File -Encoding ascii test_base64.txt
# 打开查看
notepad test_base64.txt
```
## 快速测试内容建议
录制这些内容任选一个3秒即可
- "你好"
- "今天天气怎么样"
- "给我讲个笑话"
- "帮我查询一下"
- 随便说点什么
---
**准备好后**,在项目中点击 🎧 进入语音模式,然后点击 "🧪 发送测试音频" 按钮测试!