项目基于musetalk 唇动视频生成模型+websocket构成,通过TTS文本转语音、ASR语音转文本、websocket通信来实现实时对话。整体架构图如下:下面总结一下碰到的问题:
(1)getUserMedia 未定义
一般是一种浏览器无法访问媒体设备权限问题,即部署的时候通过http访问,浏览器在非安全连接(HTTP)下会限制媒体设备访问
解决方法1:nginx转发的时候配置为localhost, 但需要你有一个可以操作的浏览器用于访问
如果你只有一个带显卡的机箱怎么办,只能通过外部访问主机的ip地址
要么配置https访问,要么给浏览器开启这个地址下的访问媒体设备权限
方法:chrome 在地址栏输入
chrome://flags/#unsafely-treat-insecure-origin-as-secure
然后添加地址,重启浏览器
firefox浏览器:
Firefox:
地址栏输入
about:config
搜索
media.devices.insecure.enabled
设置为true
搜索
media.navigator.permission.disabled
设置为true
(2)audiocontext上下文不匹配
这个问题由于浏览器有默认的音频输入频率,而如果在前端指定了输入音频上下文的频率(如16000hz),但系统中的声音输入设备无法输入16000hz的音频,就会导致这样的问题
解决方法是:切换前端定义的音频输入频率,即audiocontext指定为输入设备能支持的输入音频频率 然后在需要使用其他频率处理的时候(如语音转文本)使用ffmpeg进行重采样,示例如下
/**
* 重采样音频
* @param fileName
* @return
*/
private String convertTo16000Hz(String fileName) {
try {
// 输出文件路径
String outputFileName = fileName.replace(".pcm", "_16000.pcm");
// 构建 FFmpeg 命令
String command = String.format("ffmpeg -f s16le -ar 48000 -ac 1 -i %s -ar 16000 -f s16le %s", fileName, outputFileName);
// 执行命令
Process process = Runtime.getRuntime().exec(command);
// 获取 FFmpeg 的标准输出和标准错误流
StringBuilder output = new StringBuilder();
StringBuilder error = new StringBuilder();
// 读取标准输出流
new Thread(() -> {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
// 读取标准错误流
new Thread(() -> {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String line;
while ((line = reader.readLine()) != null) {
error.append(line).append("\n");
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
// 等待 FFmpeg 命令执行完毕
int exitCode = process.waitFor();
if (exitCode == 0) {
System.out.println("音频转换成功:" + outputFileName);
System.out.println("FFmpeg 输出: " + output.toString());
return outputFileName; // 返回转换后的文件路径
} else {
System.err.println("FFmpeg 转换音频失败,错误输出: " + error.toString());
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
(3) 同时发送多个音频【多情绪的拆分】导致显卡爆显存,需要一段一段推理,一段音频发送后,推理完成再发送下一段音频
方案1:
1.定义一个boolean变量标识,数字人端发送一个带有completed的json,像这样{"status": "completed"}
2.后端通过onmessage()接到这个消息解析出completed则把标识变成true,代表已经推理一段完成,
3.定义一个循环while(标识为false) 执行循环,直到标识为true的时候跳出【跳出循环的时机应该是接到completed标识的时候】
问题:
在发送了第一个json消息,推理第一段语音的视频帧后,Java端再也接受不到任何数字人端发送的消息,且线程一直卡死在循环中,死循环打印日志【等待中】
原来的大概代码:
private final AtomicBoolean isCompleted = new AtomicBoolean(false);
while (!client.isCompleted()) {
if (System.currentTimeMillis() - startTime > 30000) { // 30秒超时
throw new RuntimeException("处理超时");
}
// 检查连接状态
if (!client.isOpen()) {
throw new RuntimeException("WebSocket连接已断开");
}
Thread.sleep(100); // 添加短暂休眠,避免CPU占用过高
System.out.println("等待数字人生成图片中... 当前完成状态: " + client.isCompleted() + ", 连接状态: " + client.getReadyState());
}
这样的问题是,while循环为死循环处于忙等待状态
(1)cpu线程一直处理循环的打印日志任务,无法进入空闲处理接受到带有完成标识的消息
(2)导致无法跳出循环,循环仍在不断检查状态,死循环占用一个完整核心的全部资源
(3)主线程持续运行不释放CPU操作系统无法调度其他线程执行
解决方法是用阻塞队列,或者new countdownlatch(1)
实际两边 服务 发消息的时候,存在隐式多线程(主线程+WebSocket IO线程)所以可以用latch来控制消息发送的线程等待