YWW
YWW
发布于 2025-09-05 / 15 阅读
0
0

实时数字人项目总结

项目基于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​​:

  1. 地址栏输入 about:config

  2. 搜索 media.devices.insecure.enabled 设置为 true

  3. 搜索 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来控制消息发送的线程等待




评论