上一篇我们分析了 InfiniteTalk 的命令行推理入口 generate_infinitetalk.py。
这个入口文件负责把命令行参数、输入 JSON、模型路径、音频、视频、Pipeline 和最终 MP4 输出串起来。
在整个流程里,音频是非常关键的一环。
因为 InfiniteTalk 不是普通的 image-to-video,也不是简单的嘴部替换工具。它的核心任务是:
让视频人物根据输入音频重新生成说话状态。
这意味着音频不是一个附属文件,不是最后才合成到视频里的背景声音,而是驱动视频生成的核心条件。
所以,在音频进入 Wav2Vec2 编码器之前,源码会先做一系列预处理:
视频文件中抽取音频
↓
读取音频波形
↓
统一采样率到 16k
↓
做响度归一化
↓
单人或多人音频组织
↓
生成最终合成用音轨
↓
送入 Wav2Vec2 提取 audio embedding
这一篇我们就重点分析 InfiniteTalk 的音频预处理流程,包括:
-
为什么要用 FFmpeg;
-
为什么要用 librosa;
-
为什么要做响度归一化;
-
为什么统一成 16k 采样;
-
单人音频和双人音频有什么区别;
-
音频预处理和后续 Wav2Vec2 embedding 有什么关系。
一、为什么音频预处理这么重要?
很多人第一次看音频驱动视频生成项目时,会低估音频预处理的重要性。
直觉上可能会觉得:用户上传一段 wav 或 mp3,模型直接读取就行了。
但真实情况不是这样。
原始音频可能有很多不确定因素:
有的音频是 44.1kHz
有的音频是 48kHz
有的是单声道
有的是双声道
有的是从视频里提取出来的
有的音量很小
有的音量过大
有的前后有很长静音
有的是 mp3、aac、wav、m4a 等不同格式
如果不先统一处理,后面的音频编码器就很难得到稳定的特征。
对于 InfiniteTalk 这种模型来说,音频特征会直接影响人物嘴型、表情、头部动作和身体姿态。如果音频输入不稳定,生成结果就可能出现:
嘴型对不上声音
人物开口时间延迟
说话节奏不自然
静音片段也在乱动
多人对话时角色错位
长视频中音画逐渐不同步
所以,音频预处理不是一个边角功能,而是整个音频驱动视频生成链路的基础。
二、源码里的音频处理主线
在 generate_infinitetalk.py 里,音频相关的核心函数主要有几个:
def loudness_norm(audio_array, sr=16000, lufs=-23):
...
def audio_prepare_single(audio_path, sample_rate=16000):
...
def audio_prepare_multi(left_path, right_path, audio_type, sample_rate=16000):
...
def extract_audio_from_video(filename, sample_rate):
...
def get_embedding(speech_array, wav2vec_feature_extractor, audio_encoder, sr=16000, device='cpu'):
...
这几个函数可以按职责分成三层。
第一层是音频读取层:
extract_audio_from_video()
audio_prepare_single()
它负责从视频或音频文件中拿到语音波形。
第二层是音频规整层:
loudness_norm()
audio_prepare_multi()
它负责统一音量、组织单人或双人音频。
第三层是音频特征层:
get_embedding()
它负责把处理后的音频送入 Wav2Vec2,得到后续视频模型能使用的 audio embedding。
本文主要讲前两层,下一篇会重点进入 Wav2Vec2 音频编码。
三、audio_prepare_single:单人音频预处理入口
先看单人音频处理函数。
它的逻辑非常清晰:
def audio_prepare_single(audio_path, sample_rate=16000):
ext = os.path.splitext(audio_path)[1].lower()
if ext in ['.mp4', '.mov', '.avi', '.mkv']:
human_speech_array = extract_audio_from_video(audio_path, sample_rate)
return human_speech_array
else:
human_speech_array, sr = librosa.load(audio_path, sr=sample_rate)
human_speech_array = loudness_norm(human_speech_array, sr)
return human_speech_array
它首先判断输入文件扩展名。
如果输入是:
.mp4
.mov
.avi
.mkv
就说明用户传入的不是纯音频,而是视频文件。
这种情况下,代码会调用:
extract_audio_from_video(audio_path, sample_rate)
先把视频里的音频抽取出来。
如果输入不是视频文件,就直接用:
librosa.load(audio_path, sr=sample_rate)
读取音频,并统一采样率为 sample_rate,默认是 16000。
然后调用:
loudness_norm(human_speech_array, sr)
做响度归一化。
所以,audio_prepare_single() 的职责可以概括成一句话:
无论输入是视频还是音频,都把它变成 16k 采样率、响度规整后的 speech array。
这一步输出的不是文件路径,而是一个音频数组。
后续 get_embedding() 会继续使用这个数组提取音频特征。
四、为什么要支持从视频中抽取音频?
InfiniteTalk 的输入场景里,音频不一定总是一个独立 wav 文件。
比如用户可能直接上传一个原始视频,希望模型基于这个视频中的声音进行处理。
这种情况下,音频被封装在视频容器里。
视频文件本身可能包含:
视频流:画面
音频流:声音
字幕流:可选
元数据:编码信息、时长、帧率等
如果模型要使用其中的声音,就必须先把音频流抽取出来。
这就是 extract_audio_from_video() 的作用。
它大致执行这样的 FFmpeg 命令:
ffmpeg -y -i input.mp4 -vn -acodec pcm_s16le -ar 16000 -ac 2 output.wav
这里几个参数值得解释一下。
-y 表示如果输出文件已存在,就直接覆盖。
-i input.mp4 表示输入文件。
-vn 表示不要视频流,只处理音频。
-acodec pcm_s16le 表示输出为 16-bit PCM WAV。
-ar 16000 表示音频采样率设置为 16000 Hz。
-ac 2 表示输出双声道。
最后输出一个临时 wav 文件。
也就是说,FFmpeg 在这里承担的是格式解封装和音频转码的工作。
这一步非常适合交给 FFmpeg,因为 FFmpeg 对视频和音频容器格式的支持非常成熟。无论输入是 mp4、mov、avi 还是 mkv,FFmpeg 都比手写 Python 解析可靠得多。
五、为什么抽取后还要用 librosa.load?
在 extract_audio_from_video() 里,FFmpeg 已经把视频中的音频抽成 wav 文件了,而且还设置了 -ar 16000。
那为什么后面还要再执行:
human_speech_array, sr = librosa.load(raw_audio_path, sr=sample_rate)
原因是,FFmpeg 负责生成中间 wav 文件,而 librosa.load() 负责把 wav 文件读取成 Python 里的浮点数组。
模型不能直接处理 wav 文件路径,它需要的是可以喂给特征提取器的数组。
librosa.load() 做了几件事:
读取音频文件
转换成 numpy array
按指定 sr 重采样
通常返回浮点波形数据
所以这里可以理解成两个阶段:
FFmpeg:从视频容器中抽音频,并转成标准 wav
librosa:把 wav 文件读进 Python,变成模型可处理的数组
对于非视频音频文件,则不需要 FFmpeg,直接用 librosa 读取即可。
这也是为什么 audio_prepare_single() 里要先判断文件扩展名。
六、为什么统一到 16k 采样率?
源码里多处出现了 16000:
def loudness_norm(audio_array, sr=16000, lufs=-23):
...
def audio_prepare_single(audio_path, sample_rate=16000):
...
def audio_prepare_multi(left_path, right_path, audio_type, sample_rate=16000):
...
def get_embedding(..., sr=16000, ...):
...
FFmpeg 抽取音频时也指定:
-ar 16000
保存最终中间音频时,也经常使用:
sf.write(sum_audio, human_speech, 16000)
这说明 16k 是整个音频链路的标准采样率。
原因主要有三点。
第一,语音识别和语音表征模型经常使用 16k 音频。
InfiniteTalk 后面使用 Wav2Vec2 提取音频特征,而 Wav2Vec2 这类语音模型通常围绕 16k 语音建模。统一成 16k,可以减少采样率不一致带来的特征偏差。
第二,16k 对人声信息已经足够。
人声主要信息集中在较低频段。对于驱动嘴型、节奏、发音变化来说,16k 通常已经能覆盖足够多的语音特征。
第三,16k 能降低计算量。
如果用 44.1k 或 48k,音频序列长度会大幅增加,后续特征提取成本也会增加。对于视频生成这种本来就很重的任务来说,没有必要在音频侧浪费太多计算。
所以,统一到 16k 是一个工程折中:
保留足够语音信息
+
降低计算成本
+
匹配 Wav2Vec2 输入习惯
+
保证不同来源音频格式一致
七、响度归一化:loudness_norm 做了什么?
接下来重点看:
def loudness_norm(audio_array, sr=16000, lufs=-23):
meter = pyln.Meter(sr)
loudness = meter.integrated_loudness(audio_array)
if abs(loudness) > 100:
return audio_array
normalized_audio = pyln.normalize.loudness(audio_array, loudness, lufs)
return normalized_audio
这个函数的目标是把音频响度归一化到指定 LUFS,默认是:
-23 LUFS
LUFS 可以简单理解为一种更接近人耳感知的响度单位。
普通的音量峰值不能很好代表"听起来有多响"。比如两个音频峰值一样,但一个整体很压缩,一个动态很大,听感会不同。
pyloudnorm 提供了基于响度标准的测量和归一化能力。
源码里先创建:
meter = pyln.Meter(sr)
然后计算整段音频响度:
loudness = meter.integrated_loudness(audio_array)
如果响度绝对值特别异常,比如:
if abs(loudness) > 100:
return audio_array
就直接返回原音频,避免对异常值做归一化导致更严重的问题。
否则执行:
normalized_audio = pyln.normalize.loudness(audio_array, loudness, lufs)
把音频规整到目标响度。
八、为什么响度归一化会影响生成质量?
在音频驱动视频生成里,响度不只是"听起来大小"的问题。
音频会进入 Wav2Vec2 编码器,变成 audio embedding。这个 embedding 会继续影响视频生成。
如果输入音频音量太小,语音特征可能不够明显,模型可能无法稳定捕捉发音边界和节奏变化。
如果输入音频音量太大,可能出现削波、失真,导致特征异常。
对于生成结果来说,可能表现为:
嘴巴开合幅度不稳定
语音开始和结束位置不准
某些音节被弱化
人物动作节奏怪异
静音处出现不必要的嘴动
响度归一化的意义,就是尽量让不同来源音频处在一个相对一致的输入范围里。
这样 Wav2Vec2 提取出来的特征更稳定,后面的视频生成模型也更容易学习到可靠的音频条件。
九、audio_prepare_multi:双人音频如何组织?
InfiniteTalk 不只支持单人,也支持双人说话。
双人音频处理函数是:
def audio_prepare_multi(left_path, right_path, audio_type, sample_rate=16000):
...
这里有三个关键输入:
left_path:第一个人物音频
right_path:第二个人物音频
audio_type:音频组织方式
源码里会先判断左右音频是否存在。
如果两个人的音频都存在,就分别调用:
audio_prepare_single(left_path)
audio_prepare_single(right_path)
如果其中一个是 'None',就用另一个音频长度创建零数组,代表该人物在这个时间段不说话。
这很重要。
因为双人任务里,模型需要知道:
person1 什么时候说话
person2 什么时候说话
谁在当前时间段是静音
如果不显式构造静音数组,角色和音频时间轴就容易错位。
十、audio_type='para':并行说话模式
当:
audio_type == 'para'
源码逻辑是:
new_human_speech1 = human_speech_array1
new_human_speech2 = human_speech_array2
也就是说,两个人的音频保持原始时间轴。
这种模式适合两个人可能同时说话、或者两段音频本身已经在时间上对齐的场景。
可以理解为:
person1:从自己的音频时间轴开始说
person2:从自己的音频时间轴开始说
最终音轨:两路相加
这种模式的重点是"并行"。
如果两个人的语音本来就是对齐好的,para 模式更自然。
十一、audio_type='add':顺序拼接模式
当:
audio_type == 'add'
源码逻辑大致是:
new_human_speech1 = np.concatenate([
human_speech_array1,
np.zeros(human_speech_array2.shape[0])
])
new_human_speech2 = np.concatenate([
np.zeros(human_speech_array1.shape[0]),
human_speech_array2
])
这表示:
第一段时间:person1 说话,person2 静音
第二段时间:person1 静音,person2 说话
最后:
sum_human_speechs = new_human_speech1 + new_human_speech2
把两路音频加起来,得到最终合成用音轨。
这种模式适合"你一句我一句"的对话场景。
比如:
person1:你好,今天我们讲 InfiniteTalk。
person2:好的,我们先从音频预处理开始。
如果用 add 模式,系统会把 person1 的音频放前面,person2 的音频接在后面,并用零数组保证两个角色的音频条件在时间维度上对齐。
十二、为什么多人音频要拆成两路 embedding?
在双人模式下,源码后面会分别做:
audio_embedding_1 = get_embedding(new_human_speech1, wav2vec_feature_extractor, audio_encoder)
audio_embedding_2 = get_embedding(new_human_speech2, wav2vec_feature_extractor, audio_encoder)
然后保存为:
1.pt
2.pt
最终写入:
cond_audio['person1'] = emb1_path
cond_audio['person2'] = emb2_path
也就是说,模型拿到的不是一个混合音频 embedding,而是两个人各自的音频 embedding。
这对多人说话非常关键。
因为如果只把两个人的声音混成一路,模型很难判断当前应该让谁张嘴、谁保持静止。
拆成两路之后,模型至少能在条件层面知道:
person1 的音频条件是什么
person2 的音频条件是什么
当前时间谁在说话
当前时间谁是静音
这也是 InfiniteTalk 支持多人说话的重要基础。
十三、最终音轨和模型条件不是一回事
这里有一个很容易混淆的点:
cond_audio 和 video_audio 不是同一个东西。
在源码中,音频会走两条路线。
第一条路线是模型条件路线:
原始音频
↓
audio_prepare_single / audio_prepare_multi
↓
get_embedding
↓
保存为 1.pt / 2.pt
↓
input_clip['cond_audio']
↓
作为模型生成条件
第二条路线是最终合成路线:
原始音频
↓
audio_prepare_single / audio_prepare_multi
↓
保存为 sum.wav 或 sum_all.wav
↓
input_clip['video_audio']
↓
FFmpeg 合成到最终 MP4
也就是说:
cond_audio:给模型看的音频特征
video_audio:给观众听的最终声音
这两条路线使用同一段音频作为来源,但目的完全不同。
如果你做二次开发,一定要分清这两个字段。
不要以为 cond_audio 是最终视频里的音频,也不要以为 video_audio 会直接参与模型生成。
十四、get_embedding 前的最后一步:音频长度和视频帧对齐
虽然这一篇重点是预处理,但必须简单提一下 get_embedding()。
因为前面所有音频预处理,最终都是为了这一步服务。
在 get_embedding() 里,源码会计算:
audio_duration = len(speech_array) / sr
video_length = audio_duration * 25
这里默认视频帧率是 25fps。
也就是说,如果音频是 4 秒,那么视频长度大约对应:
4 × 25 = 100 帧
这说明音频 embedding 不是一个静态全局向量,而是和时间强相关的序列特征。
模型后续需要知道:
第 0.1 秒的音频状态是什么
第 0.5 秒的音频状态是什么
第 1.0 秒的音频状态是什么
第 2.0 秒的音频状态是什么
这样才能让嘴型和动作随时间变化。
所以,16k 采样、响度归一化、静音补齐、双人音频对齐,本质上都是为了让音频时间轴更稳定,让后续 embedding 更容易和视频帧对齐。
十五、为什么音频预处理会影响"嘴型同步"?
很多人调模型时,会第一时间怀疑视频模型或 attention 机制,但忽略音频输入本身。
实际上,嘴型同步问题很可能来自音频预处理。
比如:
1. 采样率不一致
如果音频采样率没有统一,Wav2Vec2 的输入长度和真实音频时长可能产生偏差。
后果是:
音频特征时间轴错位
嘴型提前或延迟
长视频中音画逐渐不同步
2. 音量过小
如果声音太小,模型可能捕捉不到清晰的发音边界。
后果是:
嘴巴动得不明显
开口幅度偏小
某些短音节被忽略
3. 音量过大或失真
如果音频过载,Wav2Vec2 提取到的特征可能不稳定。
后果是:
嘴型抖动
动作夸张
静音处也产生异常动作
4. 双人音频没有对齐
如果双人音频时间轴没有对齐,模型可能不知道该让谁说话。
后果是:
person1 的声音对应到 person2 嘴上
两个人同时乱动
对话顺序错乱
所以,音频预处理是排查生成问题时非常重要的一环。
十六、从工程角度看,当前实现有什么可优化点?
从源码来看,当前音频预处理逻辑清晰,但如果要做产品化,还可以继续优化。
1. 支持更多音频格式判断
当前 audio_prepare_single() 主要通过视频扩展名判断是否需要 FFmpeg。
但实际用户可能上传:
.mp3
.m4a
.aac
.flac
.webm
虽然 librosa 可能能读取一部分音频格式,但生产环境中更稳妥的方式是统一使用 FFmpeg 转成标准 wav,再交给 librosa 或 soundfile 读取。
这样兼容性会更好。
2. 临时文件命名要避免冲突
extract_audio_from_video() 里会根据输入文件名生成临时 wav。
如果多个任务并发运行,或者两个用户上传同名文件,可能发生文件冲突。
产品化时建议使用:
task_id
uuid
时间戳
独立临时目录
来管理中间音频。
比如:
/tmp/infinitetalk_tasks/{task_id}/raw_audio.wav
这样更安全。
3. 增加静音裁剪或静音检测
当前主要做响度归一化,但如果用户音频前后有长静音,生成视频时可能出现无意义的长时间停顿。
产品化时可以增加:
前后静音裁剪
静音段检测
最小时长限制
最大时长限制
异常音频提示
不过这类处理要谨慎,因为有些场景本来就需要停顿,比如自然口播、对话停顿、情绪停顿。
4. 增加音频质量检查
可以在进入模型前检查:
音频是否为空
音频时长是否过短
音频时长是否超过限制
是否存在 NaN
是否存在削波
响度是否异常
采样率是否正确
这样比等到模型生成失败后再排查要高效得多。
5. 缓存 audio embedding
音频预处理之后会进入 Wav2Vec2 提取 embedding。
如果同一段音频被多次使用,可以缓存 .pt 文件。
比如同一段口播文案配不同人物、不同背景时,就没必要每次重新提取 Wav2Vec2 特征。
十七、二次开发时可以怎么封装音频模块?
如果你准备基于 InfiniteTalk 做一个自己的数字人生成平台,可以把音频处理独立封装成一个模块。
例如:
class AudioProcessor:
def prepare_single(self, audio_or_video_path):
pass
def prepare_multi(self, person1_path, person2_path, audio_type):
pass
def normalize_loudness(self, audio_array, sr=16000):
pass
def extract_from_video(self, video_path):
pass
def save_sum_audio(self, audio_array, task_dir):
pass
def build_embedding(self, audio_array):
pass
这样做有几个好处:
generate 入口更简洁
音频逻辑更容易测试
多人音频更容易扩展
TTS 和本地音频可以统一
后续替换音频编码器更方便
原始源码是研究和推理脚本风格,适合理解主流程。
如果要做 SaaS 或批量任务系统,就应该把音频处理、模型推理、文件保存、任务状态更新拆成更清晰的模块。
十八、常见问题排查
如果你运行 InfiniteTalk 时发现音频相关问题,可以按下面顺序排查。
1. 音频是否能正常读取?
先确认输入路径是否正确,文件是否存在,librosa 或 FFmpeg 是否能读取。
2. FFmpeg 是否安装?
如果输入是视频文件,必须依赖 FFmpeg 抽取音频。没有安装 FFmpeg,视频输入就会失败。
3. 输出 wav 是否正常?
可以检查 audio_save_dir 下生成的 sum.wav 或 sum_all.wav,确认能否正常播放。
4. 音频是否是 16k?
如果你自己改了处理流程,要确认最终送入 Wav2Vec2 的音频是 16k。
5. 是否生成了 1.pt / 2.pt?
如果 .pt 文件没有生成,说明 audio embedding 提取环节失败。
6. 双人模式 audio_type 是否正确?
如果是两个人顺序对话,用 add 更合适;如果两路音频已经按时间对齐,用 para 更合适。
7. 最终视频有声音但嘴型不对?
这说明 video_audio 合成成功,但 cond_audio 的 embedding 可能有问题。要重点检查音频预处理和 .pt 文件。
8. 嘴型对了但最终没声音?
这说明模型条件可能正常,但最终 save_video_ffmpeg() 合成音频时出了问题。要检查 video_audio 路径和 FFmpeg。
十九、这一篇的核心结论
InfiniteTalk 的音频预处理流程可以概括成一句话:
把各种来源的音频统一整理成稳定、标准、可编码、可对齐的 16k speech array。
更具体地说:
audio_prepare_single() 负责处理单人音频输入。
如果输入是视频文件,会先用 FFmpeg 抽取音频。
如果输入是普通音频文件,则直接用 librosa 读取。
所有音频都会统一到 16k 采样率。
loudness_norm() 会使用 LUFS 做响度归一化,让不同来源音频处在相对稳定的响度范围内。
audio_prepare_multi() 负责双人音频组织,支持 para 并行模式和 add 顺序拼接模式。
音频在流程里分成两条路线:
cond_audio:提取 embedding,作为模型生成条件
video_audio:保存 wav,最后合成进 MP4
而所有这些预处理的最终目标,都是为了让 Wav2Vec2 能提取稳定的 audio embedding,并让这个 embedding 和视频帧在时间轴上对齐。
下一篇我们会继续深入:
InfiniteTalk 源码解析 #5:Wav2Vec2 音频编码:如何把语音变成逐帧 audio embedding
那一篇会重点分析 custom_init() 和 get_embedding(),看看原始语音数组是如何变成视频生成模型可以使用的条件特征的。