InfiniteTalk 源码解析 #4:音频预处理流程:ffmpeg、librosa、响度归一化与 16k 采样

上一篇我们分析了 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.wavsum_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(),看看原始语音数组是如何变成视频生成模型可以使用的条件特征的。