MuseTalk推理修复(P0): - 替换_run_inference() stub代码为真实MuseTalk推理逻辑 - 新增音频预处理:任意格式→16kHz mono 16bit WAV - 模型懒加载(VAE+UNet+PE+Whisper),首次推理后复用 - 使用mirror indexing循环帧,消除视频循环边界跳变 - bbox_shift参数可通过请求配置(范围-5~5) - FP16推理支持,节省RTX2060显存 - /health接口增加模型加载状态信息 TTS参数透传修复(P0): - schemas新增style字段(TTSSynthesizeRequest/TTSPreviewRequest/CreateLipsyncJobRequest/AiAvatarTtsPreviewRequest) - schemas新增volume字段(TTSSynthesizeRequest/CreateLipsyncJobRequest) - cosyvoice_service新增STYLE_INSTRUCTION_MAP和build_style_instruction() - submit_synthesize_task新增style/pitch参数支持 - workflow start_synthesis/resynthesize/submit_segments全链路传递style/volume/pitch - tts.py路由/lipsync.py路由/lipsync_service.py全链路透传新参数 - lipsync_tts.py Celery task支持style/volume参数
8.3 KiB
MuseTalk 口型精度问题分析与修复方案
问题总结
运维逐帧分析确认:
- ✅ 音频对齐正常(0ms偏移,相关系数0.99997)
- ❌ 口型只跟音频能量张合,没有音素级精度(发o/u没有圆唇,发m/b/p没有闭唇,静音段嘴巴还张着)
- ❌ 5s循环边界有视觉跳变(帧间差异是正常的3-4倍)
根因分析
根因 1:musetalk_server.py 的 _run_inference() 是 STUB 代码(最严重)
deploy/gpu_worker/musetalk_server.py 第 343-466 行:
# 2. 模拟 MuseTalk 推理:输入原视频帧 + 全量音频,输出音频时长的无声画面。
# TODO: 替换为 MuseTalk 真实推理逻辑。
logger.warning("使用示例推理逻辑,未实际调用 MuseTalk 模型")
当前代码从未调用 MuseTalk 模型。整个推理流程是:
- 从视频提取帧(ffmpeg)
- 生成一段循环原视频的无声画面(ffmpeg
-stream_loop) - 用 ffmpeg
-c:v copy封装音频
根本没有 MuseTalk 推理! 所以"口型只跟能量张合"可能是因为 MuseTalk 的实际推理代码没被调用。
根因 2:音频格式未预处理
gpu_worker.py 把音频下载为 input_audio.bin(第 360 行),直接透传给 musetalk_server.py。
musetalk_server.py 保存为 input_audio.wav(第 527 行)但没有做任何格式转换。
CosyVoice TTS 输出的音频格式:
- 采样率: 22050Hz(
cosyvoice_sample_rate = 22050) - 格式: MP3(
cosyvoice_format = "mp3")
MuseTalk 期望的输入:
- 采样率: 16000Hz
- 声道: mono
- 格式: WAV(16bit PCM)
22050Hz MP3 → 16kHz mono 16bit WAV 的重采样转换完全没有做。
这会导致 MuseTalk 的 whisper feature extractor 拿到错误采样率的音频,提取的 mel 频谱特征频率错位,音素识别完全错误。
根因 3:循环边界无过渡帧
_mux_video_with_audio() 的兜底路径(第 241-286 行)用 ffmpeg -stream_loop -1 直接循环视频,循环边界处:
- 最后一帧 → 第一帧:没有过渡,硬切
- 帧间差异是正常帧的 3-4 倍
修复方案
修复 1:实现真正的 MuseTalk 推理调用
_run_inference() 必须调用真实的 MuseTalk 模型。参考 MuseTalk 官方推理流程:
def _run_inference_real(video_path, audio_path, output_path):
"""真正的 MuseTalk 推理 — 替换 stub。"""
# 1. 音频预处理:转换为 16kHz mono 16bit WAV
preprocessed_audio = audio_path.parent / "audio_16k_mono.wav"
_preprocess_audio(audio_path, preprocessed_audio)
# 2. 调用 MuseTalk 推理
# MuseTalk 代码在 ~/projects/MuseTalk/ 下
import sys
muse_dir = Path.home() / "projects" / "MuseTalk"
if str(muse_dir) not in sys.path:
sys.path.insert(0, str(muse_dir))
from musetalk.utils.utils import get_file_type
from musetalk.whisper.audio2feature import load_audio
# MuseTalk 内部推理 API(具体取决于部署的 MuseTalk 版本)
# 典型调用:
# model = MuseTalkModel(...)
# result = model.infer(video=video_path, audio=preprocessed_audio)
修复 2:音频预处理(必须)
在 musetalk_server.py 中添加 _preprocess_audio() 函数:
def _preprocess_audio(input_path: Path, output_path: Path, target_sr: int = 16000) -> None:
"""将输入音频转换为 MuseTalk 要求的格式:16kHz mono 16bit WAV。
MuseTalk 的 whisper audio2feature 要求 16kHz 采样率,
当前 TTS 输出 22050Hz MP3,不转换会导致 mel 频谱错位、
音素特征提取错误,口型只跟能量不跟音素。
"""
cmd = [
"ffmpeg", "-y",
"-i", str(input_path),
"-ar", str(target_sr), # 重采样到 16kHz
"-ac", "1", # 单声道
"-sample_fmt", "s16", # 16bit PCM
"-f", "wav", # WAV 格式
str(output_path),
]
_run_ffmpeg(cmd, timeout=60)
if not output_path.exists() or output_path.stat().st_size < 100:
raise RuntimeError(f"音频预处理失败: {output_path}")
# 验证输出格式
try:
import wave
with wave.open(str(output_path), 'rb') as wf:
sr = wf.getframerate()
ch = wf.getnchannels()
sw = wf.getsampwidth()
if sr != target_sr or ch != 1 or sw != 2:
logger.warning("音频格式异常: sr=%d ch=%d sw=%d", sr, ch, sw)
except Exception as e:
logger.warning("无法验证 WAV 格式: %s", e)
logger.info("音频预处理完成: %s → %s (%dHz mono 16bit WAV)",
input_path.name, output_path.name, target_sr)
修复 3:循环边界 crossfade
在兜底循环路径中,使用 ffmpeg xfade 滤镜在循环边界处混合:
def _mux_with_loop_crossfade(video_path, audio_path, output_path, crossfade_frames=3):
"""循环视频 + crossfade 过渡,减少循环边界跳变。"""
fps = _get_video_fps(video_path)
crossfade_duration = crossfade_frames / fps # 通常 0.12s (3帧@25fps)
video_duration = _get_media_duration(video_path)
# 使用 tpad 滤镜在视频末尾添加 crossfade
# 方案:用 concat 把 [视频 + 视频前N帧crossfade] 拼起来
cmd = [
"ffmpeg", "-y",
"-stream_loop", "-1",
"-i", str(video_path),
"-i", str(audio_path),
"-filter_complex",
f"[0:v]split=2[v1][v2];"
f"[v2]trim=0:{crossfade_duration},setpts=PTS-STARTPTS+{video_duration - crossfade_duration}/TB[xfade_in];"
f"[v1][xfade_in]xfade=transition=fade:duration={crossfade_duration}:offset={video_duration - crossfade_duration}[looped];"
f"[looped]format=yuv420p[out]",
"-map", "[out]",
"-map", "1:a:0",
"-c:v", encoder,
"-preset", preset,
"-c:a", "aac", "-b:a", "128k",
"-t", f"{audio_duration:.3f}",
str(output_path),
]
但更简单的方案:如果 MuseTalk 正确处理了全量音频(输出时长=音频时长),就不需要兜底循环了。crossfade 只是兜底路径的优化。
修复 4:推理参数
MuseTalk 的关键推理参数:
bbox_shift: 口型区域偏移量,默认 0,范围 [-5, 5]。影响口型位置batch_size: 推理批次大小,RTX2060 6G 显存建议 4-8- 视频帧率:MuseTalk 期望 25fps,当前已正确获取 fps
完整修复优先级
| 优先级 | 修复 | 影响 |
|---|---|---|
| P0 | _run_inference() 调用真实 MuseTalk 模型 |
没有这个其他都没意义 |
| P0 | 音频预处理:22050Hz MP3 → 16kHz mono 16bit WAV | 音素特征正确提取 |
| P1 | 循环边界 crossfade | 减少视觉跳变 |
| P2 | bbox_shift 参数暴露为可配置 | 微调口型位置 |
在 RTX2060 上调试需要什么
-
确认 MuseTalk 推理代码:
# 在 RTX2060 上 ls ~/projects/MuseTalk/ cat ~/projects/MuseTalk/musetalk_server.py # 看实际部署版本 -
验证音频格式:
# 在 musetalk_server.py 的推理前添加日志 ffprobe -v error -show_entries stream=sample_rate,channels,codec_name,bits_per_sample \ -of default /tmp/musetalk_*/input_audio.wav -
提取中间结果对比:
# 对比 TTS 原始音频 vs 预处理后的 16kHz WAV # 用 whisper 提取 mel 特征并可视化 python -c " import whisper model = whisper.load_model('tiny') audio = whisper.load_audio('/tmp/test_audio.wav') # 预处理后的 audio = whisper.pad_or_trim(audio) mel = whisper.log_mel_spectrogram(audio).to(model.device) # 保存 mel 特征供可视化 import numpy as np np.save('/tmp/mel_features.npy', mel.cpu().numpy()) " -
MuseTalk 推理中间帧:
# 在 MuseTalk 推理代码中保存中间帧到 /tmp/debug_frames/ # 对比输入帧 vs 推理后帧 vs 原视频帧
建议的下一步
- 先在 RTX2060 上
cat ~/projects/MuseTalk/musetalk_server.py确认实际部署的代码 - 确认 MuseTalk 是否真的被调用(看日志有没有 "使用示例推理逻辑" 的 warning)
- 如果确认是 stub,需要把真正的 MuseTalk 推理逻辑集成到
_run_inference() - 同时修复音频预处理(P0),这个无论推理代码如何都是必须的