上一篇我们分析了 InfiniteTalk 的音频预处理流程。
在进入模型之前,音频会先经历几步处理:
视频抽音频
↓
librosa 读取
↓
统一到 16k 采样率
↓
响度归一化
↓
单人或双人音频整理
这些步骤的目标是把各种来源的音频统一成稳定的 speech array。
但 speech array 还不能直接驱动视频生成模型。
它只是一个一维波形数组,本质上是连续的振幅数值。视频生成模型无法直接从这些原始采样点里理解“当前音节是什么”“嘴巴应该张多大”“人物什么时候该停顿”。
所以 InfiniteTalk 还需要一个关键步骤:
把原始语音波形转换成模型可以使用的音频特征。
在源码中,这一步主要由 Wav2Vec2 完成。
本文就重点分析:
-
InfiniteTalk 为什么使用 Wav2Vec2;
-
custom_init()做了什么; -
get_embedding()如何把音频变成 embedding; -
为什么要计算
video_length = audio_duration * 25; -
hidden_states[1:]代表什么; -
rearrange(audio_emb, "b s d -> s b d")为什么重要; -
.pt文件在整个推理链路里扮演什么角色; -
单人和双人音频 embedding 有什么差异。
一、为什么音频不能直接喂给视频模型?
首先要明确一个问题:
音频文件里的波形数据,并不是视频生成模型想要的输入形式。
比如一段 16k 采样率的音频,1 秒钟就有 16000 个采样点。
如果音频是 10 秒,就有 160000 个采样点。
这些采样点表示的是空气振动的幅度变化,但它们并不直接等于语义、发音、节奏和口型。
对于 InfiniteTalk 来说,真正有用的信息包括:
当前是否有人在说话
发音开始和结束在哪里
音节节奏如何变化
元音和辅音的变化趋势
停顿位置在哪里
语气强弱如何变化
多人音频中谁在说话
这些信息隐藏在原始音频波形里,需要通过语音模型提取出来。
Wav2Vec2 的作用,就是把原始音频转换成更高层的语音表征。
简单理解:
原始音频波形:一串采样点
Wav2Vec2 输出:一串语音特征向量
这些语音特征向量后面会作为条件输入,参与视频生成过程。
所以,Wav2Vec2 在 InfiniteTalk 里相当于一个“语音理解前端”。
二、InfiniteTalk 中音频编码的整体链路
先把音频编码流程放在整体链路里看:
audio_prepare_single / audio_prepare_multi
↓
得到 16k speech_array
↓
custom_init 加载 Wav2Vec2FeatureExtractor 和 Wav2Vec2Model
↓
get_embedding 提取 audio embedding
↓
torch.save 保存为 1.pt / 2.pt
↓
写入 input_clip['cond_audio']
↓
传给 InfiniteTalkPipeline
↓
作为音频条件控制视频生成
在源码里,和 Wav2Vec2 直接相关的主要是两个函数:
def custom_init(device, wav2vec):
...
def get_embedding(speech_array, wav2vec_feature_extractor, audio_encoder, sr=16000, device='cpu'):
...
其中:
custom_init() 负责加载模型。
get_embedding() 负责真正提取音频特征。
这两个函数虽然代码不长,但它们连接了音频世界和视频生成世界,是 InfiniteTalk 的关键桥梁。
三、custom_init:初始化 Wav2Vec2
先看 custom_init() 的核心逻辑:
def custom_init(device, wav2vec):
audio_encoder = Wav2Vec2Model.from_pretrained(
wav2vec,
local_files_only=True
).to(device)
audio_encoder.feature_extractor._freeze_parameters()
wav2vec_feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(
wav2vec,
local_files_only=True
)
return wav2vec_feature_extractor, audio_encoder
这个函数做了三件事:
1. 从本地目录加载 Wav2Vec2Model
2. 冻结 Wav2Vec2 的 feature_extractor 参数
3. 从本地目录加载 Wav2Vec2FeatureExtractor
最后返回两个对象:
wav2vec_feature_extractor
audio_encoder
这两个对象名字很像,但职责不一样。
四、Wav2Vec2FeatureExtractor 负责什么?
Wav2Vec2FeatureExtractor 可以理解成输入适配器。
它负责把原始 speech array 整理成 Wav2Vec2 模型需要的输入格式。
在 get_embedding() 中,对它的调用是:
audio_feature = np.squeeze(
wav2vec_feature_extractor(
speech_array,
sampling_rate=sr
).input_values
)
这里输入的是:
speech_array:librosa 读取并预处理后的音频波形
sampling_rate:采样率,默认 16000
输出的是:
input_values
也就是 Wav2Vec2 可以接受的输入张量数据。
为什么不能直接把 speech array 转成 torch tensor 就喂给模型?
因为特征提取器通常还会负责:
检查采样率
整理 batch 维度
做输入归一化或格式适配
生成模型期望的 input_values
所以,Wav2Vec2FeatureExtractor 更像是“把原始音频整理成模型输入”的一层封装。
五、Wav2Vec2Model 负责什么?
audio_encoder 是真正的 Wav2Vec2 模型。
在源码中,它来自:
from src.audio_analysis.wav2vec2 import Wav2Vec2Model
然后通过:
Wav2Vec2Model.from_pretrained(wav2vec, local_files_only=True)
从本地目录加载。
注意这里的 local_files_only=True。
这意味着模型不会运行时联网下载,而是要求你提前准备好 Wav2Vec2 权重目录。
这也是为什么命令行参数里有:
--wav2vec_dir
如果这个路径配置不对,音频编码阶段就会失败。
从工程角度看,audio_encoder 的作用是:
输入:音频 input_values
输出:多层 hidden states
这些 hidden states 后面会被整理成 audio embedding。
六、为什么要 freeze feature_extractor?
custom_init() 里有一行:
audio_encoder.feature_extractor._freeze_parameters()
这表示冻结 Wav2Vec2 的 feature extractor 参数。
在推理阶段,模型只是做特征提取,不需要训练。
冻结参数有几个意义:
避免误更新参数
减少训练相关开销
明确当前流程只是 inference
保证音频编码器行为稳定
虽然在纯推理场景下本来也不会反向传播,但显式冻结 feature extractor 可以表达一个设计意图:
Wav2Vec2 在这里不是被训练的模块,而是作为固定音频特征提取器使用。
InfiniteTalk 的视频生成模型会使用它提取出的语音特征,但不会在这个脚本里更新 Wav2Vec2。
七、get_embedding:音频编码的核心函数
接下来进入最关键的函数:
def get_embedding(
speech_array,
wav2vec_feature_extractor,
audio_encoder,
sr=16000,
device='cpu'
):
...
这个函数输入的是:
speech_array:预处理后的音频数组
wav2vec_feature_extractor:Wav2Vec2 输入特征处理器
audio_encoder:Wav2Vec2 模型
sr:采样率,默认 16000
device:运行设备,默认 cpu
输出的是:
audio_emb
也就是后续视频生成模型要使用的音频条件。
源码逻辑可以拆成六步:
1. 计算音频时长
2. 根据 25fps 估算视频长度
3. 用 feature_extractor 处理音频
4. 转成 torch tensor 并增加 batch 维度
5. 调用 Wav2Vec2Model 得到 hidden states
6. 整理 hidden states 维度,得到 audio embedding
下面逐步看。
八、第一步:计算 audio_duration
源码中先计算:
audio_duration = len(speech_array) / sr
这里的逻辑很简单。
speech_array 是音频采样点数组。
sr 是采样率,默认 16000。
所以:
音频时长 = 采样点数量 / 每秒采样点数量
比如一段音频有 160000 个采样点,采样率是 16000,那么:
160000 / 16000 = 10 秒
这个时长后面会用于估算视频帧长度。
九、第二步:把音频时长映射到视频帧数
接着源码计算:
video_length = audio_duration * 25
注释里写得很明确:
# Assume the video fps is 25
也就是说,InfiniteTalk 在这里默认按 25fps 来对齐音频和视频。
如果音频是 4 秒,那么对应视频长度大约是:
4 × 25 = 100 帧
如果音频是 10 秒,对应视频长度大约是:
10 × 25 = 250 帧
这个 video_length 后面会传给 Wav2Vec2Model:
embeddings = audio_encoder(
audio_feature,
seq_len=int(video_length),
output_hidden_states=True
)
这里非常关键。
它说明 InfiniteTalk 提取音频 embedding 时,不只是想得到一段音频的全局特征,而是希望得到和视频帧长度匹配的时间序列特征。
换句话说,audio embedding 需要服务于视频帧生成。
视频生成模型后续需要知道:
第 1 帧附近的语音状态
第 20 帧附近的语音状态
第 50 帧附近的语音状态
第 100 帧附近的语音状态
所以这里要把音频时长和视频帧数关联起来。
这就是标题里“逐帧 audio embedding”的来源。
严格说,它不是每个原始音频采样点对应一帧,而是通过 Wav2Vec2 和 seq_len 让音频特征序列对齐到视频生成所需的时间尺度。
十、第三步:FeatureExtractor 处理 speech_array
接着代码调用:
audio_feature = np.squeeze(
wav2vec_feature_extractor(
speech_array,
sampling_rate=sr
).input_values
)
这里做了两件事。
第一,把 speech_array 交给 wav2vec_feature_extractor。
第二,用 np.squeeze() 去掉多余维度。
处理后的 audio_feature 仍然是 numpy 数据,还不能直接进入 PyTorch 模型。
所以后面会继续转 tensor。
十一、第四步:转成 torch tensor 并增加 batch 维度
源码接着写:
audio_feature = torch.from_numpy(audio_feature).float().to(device=device)
audio_feature = audio_feature.unsqueeze(0)
第一行把 numpy array 转成 float tensor,并移动到指定设备。
第二行增加 batch 维度。
如果原始 audio_feature 形状类似:
[T]
unsqueeze(0) 后就变成:
[1, T]
这里的 1 表示 batch size。
模型通常要求输入有 batch 维度,即使一次只处理一段音频,也要把它包装成 batch。
所以这一步是非常常见的模型输入格式整理。
十二、第五步:调用 audio_encoder 得到 hidden states
真正的音频编码发生在这里:
with torch.no_grad():
embeddings = audio_encoder(
audio_feature,
seq_len=int(video_length),
output_hidden_states=True
)
这里有三个关键信息。
第一,使用了 torch.no_grad()。
这说明当前是推理过程,不需要计算梯度。
这样可以减少显存或内存开销,也能提高推理效率。
第二,传入了 seq_len=int(video_length)。
这说明音频编码器输出的特征序列会受到视频长度约束。
第三,设置了:
output_hidden_states=True
这意味着模型不只是返回最后一层输出,还会返回中间多层 hidden states。
后面源码正是使用这些 hidden states 来构造 audio embedding。
十三、为什么要使用 hidden_states?
在 Wav2Vec2 这类深度语音模型里,不同层的 hidden states 可能包含不同层次的信息。
较浅层可能更接近声学特征,比如音高、能量、局部频谱变化。
较深层可能更接近语音内容、发音结构和上下文信息。
对于音频驱动视频生成来说,只用最后一层未必最合适。
因为嘴型和表情不只依赖“语义内容”,也依赖声音的节奏、发音边界、音素变化和局部声学细节。
所以源码使用:
embeddings.hidden_states[1:]
也就是去掉第 0 层后,保留后续多层 hidden states。
这样相当于把不同层级的语音特征都交给后续模型使用。
可以理解为:
浅层特征:更偏声学和局部变化
中层特征:更偏发音和音素结构
深层特征:更偏上下文和语音表示
把这些层堆叠起来,可以为视频生成模型提供更丰富的音频条件。
十四、为什么跳过 hidden_states[0]?
源码中使用的是:
embeddings.hidden_states[1:]
而不是:
embeddings.hidden_states
这意味着第 0 层被跳过了。
通常可以理解为,第 0 层更接近模型最初的输入表示,还没有经过足够深的语音上下文建模。
后续层经过 Transformer 或类似结构处理后,特征表达能力更强。
所以跳过第 0 层,是为了保留更有语音语义和时序表达能力的 hidden states。
从二次开发角度看,这也是一个可以实验的点:
只用最后一层会怎样?
使用全部 hidden states 会怎样?
只用中间几层会怎样?
不同层对嘴型同步和动作自然度有什么影响?
不过在原始源码中,它选择了 hidden_states[1:]。
我们先按照源码理解即可。
十五、第六步:stack 多层 hidden states
接下来是:
audio_emb = torch.stack(embeddings.hidden_states[1:], dim=1).squeeze(0)
这行代码很重要。
假设 hidden_states[1:] 里有多层特征,每一层形状大致类似:
[batch, seq, dim]
torch.stack(..., dim=1) 会把这些层堆叠到新的维度上。
堆叠后大致变成:
[batch, layers, seq, dim]
然后 .squeeze(0) 去掉 batch 维度,得到:
[layers, seq, dim]
也就是说,audio_emb 此时包含三个核心维度:
layers:来自 Wav2Vec2 的不同层
seq:时间序列长度
dim:每个时间点的特征维度
这一步的意义是:
把 Wav2Vec2 多层 hidden states 合并成一个多层音频条件张量。
它不是单层特征,也不是一个全局向量,而是一个包含层级信息和时间信息的音频表示。
十六、第七步:rearrange 调整维度
紧接着源码执行:
audio_emb = rearrange(audio_emb, "b s d -> s b d")
这里变量名虽然写的是 b s d,但结合前面的 stack,实际可以理解为:
原始维度:层数 × 时间 × 特征维度
调整后:时间 × 层数 × 特征维度
也就是说,它把时间维度放到了第一位。
调整前:
[layers, seq, dim]
调整后:
[seq, layers, dim]
为什么要这么做?
因为后续视频生成模型更关心“每个时间点对应的音频条件”。
视频是按时间帧生成的,音频条件也应该以时间为主轴。
把 seq 放在第一维,可以更方便地按时间步对齐视频帧。
简单理解:
audio_emb[0]:第 0 个时间位置的多层语音特征
audio_emb[1]:第 1 个时间位置的多层语音特征
audio_emb[2]:第 2 个时间位置的多层语音特征
...
这样后续模型在处理视频时间维度时,就可以更自然地取到对应音频特征。
十七、第八步:detach 并放回 CPU
最后源码执行:
audio_emb = audio_emb.cpu().detach()
return audio_emb
这里做了两件事。
第一,.detach() 让张量从计算图中分离出来。
因为推理阶段不需要梯度。
第二,.cpu() 把张量放回 CPU。
这也符合前面整体工程策略:尽量减少 GPU 显存占用。
InfiniteTalk 后续会把 audio embedding 保存为 .pt 文件:
torch.save(audio_embedding, emb_path)
既然要保存到磁盘,就没有必要继续放在 GPU 上。
所以 get_embedding() 最终返回的是一个 CPU tensor。
十八、audio embedding 的形状可以怎么理解?
虽然具体维度取决于 Wav2Vec2 配置和源码实现,但从逻辑上可以把 audio_emb 理解成:
[时间位置, Wav2Vec2层级, 特征维度]
它表达的是:
在某一个时间点,语音模型不同层抽取到的声音特征是什么。
这和普通语音分类任务不一样。
语音分类可能只需要一个全局向量,表示这段音频整体属于哪个类别。
但 InfiniteTalk 需要的是时间连续的特征,因为视频中人物每一帧的嘴型和动作都要跟着语音变化。
所以这里的 audio embedding 必须保留时间维度。
如果时间维度丢了,就无法做精细的嘴型同步。
十九、单人场景:生成 1.pt
在单人音频场景中,源码逻辑大致是:
human_speech = audio_prepare_single(items[1])
audio_embedding = get_embedding(
human_speech,
wav2vec_feature_extractor,
audio_encoder
)
emb_path = os.path.join(args.audio_save_dir, '1.pt')
torch.save(audio_embedding, emb_path)
cond_audio['person1'] = emb_path
这里的流程非常清楚:
单人原始音频
↓
audio_prepare_single
↓
get_embedding
↓
保存为 1.pt
↓
cond_audio['person1'] 指向 1.pt
后续传给 pipeline 的不是 wav,而是:
1.pt
也就是说,1.pt 才是模型真正使用的音频条件文件。
原始 wav 只是音频来源。
二十、双人场景:生成 1.pt 和 2.pt
在双人场景中,源码逻辑是:
new_human_speech1, new_human_speech2, sum_human_speechs = audio_prepare_multi(...)
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
)
torch.save(audio_embedding_1, emb1_path)
torch.save(audio_embedding_2, emb2_path)
cond_audio['person1'] = emb1_path
cond_audio['person2'] = emb2_path
这说明双人场景不是把两个人声音混成一个 embedding。
它会分别生成:
person1 → 1.pt
person2 → 2.pt
这样模型后续才能区分:
哪个音频条件对应第一个人物
哪个音频条件对应第二个人物
当前时间段是谁在说话
另一个人是否应该保持静音
这对于多人对话非常关键。
如果只生成一个混合 embedding,模型就很难知道应该让谁动嘴。
二十一、audio embedding 和最终音轨的区别
这里必须再强调一次:
cond_audio 和 video_audio 是两条不同路线。
模型条件路线:
speech_array
↓
Wav2Vec2
↓
audio_embedding
↓
1.pt / 2.pt
↓
cond_audio
↓
驱动视频生成
最终音轨路线:
speech_array
↓
sum.wav / sum_all.wav
↓
video_audio
↓
FFmpeg
↓
合成到最终 MP4
所以:
1.pt / 2.pt:给模型“看”的声音特征
sum.wav / sum_all.wav:给观众“听”的最终声音
如果生成视频嘴型对了,但最终视频没有声音,问题可能在 video_audio 或 FFmpeg 合成。
如果最终视频有声音,但嘴型不对,问题可能在 cond_audio 或 Wav2Vec2 embedding。
排查问题时要分清这两条链路。
二十二、为什么 audio embedding 要保存成 .pt 文件?
源码没有直接把 audio_embedding tensor 放进 input_clip,而是保存为 .pt 文件,再把路径放进去:
torch.save(audio_embedding, emb_path)
cond_audio['person1'] = emb_path
这种设计有几个好处。
1. 降低内存占用
视频生成模型本身非常吃显存和内存。
把 audio embedding 保存到磁盘,可以让 pipeline 在需要时再读取,而不是在入口脚本里长期持有大量中间 tensor。
2. 方便缓存
如果同一段音频要反复生成不同视频,可以复用 .pt 文件,避免重复跑 Wav2Vec2。
比如:
同一段口播音频 + 不同人物形象
同一段角色台词 + 不同视频背景
同一段课程讲解 + 不同画面模板
这些场景都可以复用 audio embedding。
3. 方便调试
如果生成效果异常,可以检查:
音频 wav 是否正常
1.pt / 2.pt 是否生成
pt 文件维度是否符合预期
双人场景是否两个 embedding 都存在
如果 .pt 文件没生成,说明问题在音频编码之前。
如果 .pt 文件生成正常,但视频不对,就继续查 pipeline 和 attention。
4. 方便分布式或任务队列
在服务化架构里,音频编码和视频生成可以拆成两个阶段。
例如:
音频 worker:负责生成 .pt
视频 worker:读取 .pt 生成视频
这样可以更灵活地调度资源。
二十三、v_length:音频 embedding 长度如何影响生成?
源码中还有一行:
v_length = audio_embedding.shape[0]
或者双人时:
v_length = audio_embedding_1.shape[0]
虽然在当前片段里这个变量没有展开使用太多,但它表达了一个关键信息:
audio_embedding.shape[0] 是时间维度长度。
也就是说,audio embedding 的第一维代表音频条件的时间长度。
这和前面的 rearrange(audio_emb, "b s d -> s b d") 对应。
如果 audio embedding 的时间长度和视频帧长度不匹配,就可能出现音画不同步。
所以在调试时,可以重点看:
音频时长是多少?
video_length 计算出来是多少?
audio_embedding.shape[0] 是多少?
生成视频帧数是多少?
最终音频长度是多少?
这些信息能帮助判断同步问题是不是来自音频编码阶段。
二十四、Wav2Vec2 embedding 为什么能控制嘴型?
严格来说,Wav2Vec2 本身并不会直接输出“嘴巴张开多少”。
它输出的是语音特征。
真正把语音特征转成嘴型、表情、头部动作的是后面的 InfiniteTalk 视频生成模型。
可以这样理解:
Wav2Vec2:把声音变成语音特征
InfiniteTalk:根据语音特征生成视频动作
Wav2Vec2 提供的信息包括发音节奏、声音变化、语音结构等。
视频模型在训练或设计中学习到:
某些语音特征对应嘴巴张开
某些语音特征对应闭嘴
某些节奏对应头部微动
某些停顿对应动作减弱
某些说话强度对应表情变化
所以 Wav2Vec2 不是直接控制嘴巴,而是提供音频条件。
后面的 audio cross attention 或相关条件注入机制,才是真正把语音条件作用到视频生成里的关键。
这也是后续第 8 篇会重点分析的内容。
二十五、为什么不是直接做语音识别文本?
有人可能会问:
既然要理解语音,为什么不先把语音转成文本,再让模型根据文本生成嘴型?
原因是,文本丢失了太多声音细节。
比如同一句话:
“我知道了。”
可以有很多种说法:
平静地说
激动地说
犹豫地说
低声说
快速说
拖长音说
带停顿地说
如果只看文本,这些差异都没了。
但音频里包含:
语速
停顿
重音
音高变化
情绪强度
发音持续时间
音节边界
这些信息对人物视频生成非常重要。
所以 InfiniteTalk 不是把语音转文本后再控制视频,而是直接用语音表征作为条件。
这能保留更多和嘴型、动作、节奏相关的信息。
二十六、为什么不只用音量曲线?
也有人会想到一种简单方法:
用音频音量大小控制嘴巴开合。
音量大,嘴巴张大;音量小,嘴巴闭上。
这种方法可以做非常粗糙的说话动画,但远远不够。
因为嘴型不只由音量决定。
比如:
“a”
“o”
“m”
“f”
“shi”
这些发音对应的嘴型完全不同。
有些音量不大,但嘴型变化明显。
有些音量很大,但嘴型不一定张得特别大。
所以音量曲线只能提供说话强弱,不能提供足够的发音结构。
Wav2Vec2 这类模型能提取更丰富的语音特征,比简单音量曲线更适合驱动视频生成。
二十七、从二次开发角度看,可以怎么优化?
如果你想基于 InfiniteTalk 做自己的产品,Wav2Vec2 音频编码这里有几个可优化方向。
1. 缓存 embedding
同一段音频只需要编码一次。
可以根据音频文件 hash、采样率、预处理参数生成缓存 key。
例如:
cache_key = sha256(audio_file + sr + normalize_params)
如果缓存命中,就直接读取 .pt,不用重新跑 Wav2Vec2。
2. 增加 embedding 维度检查
生成 .pt 后可以检查:
是否为 torch.Tensor
是否存在 NaN
shape 是否为空
时间维度是否大于 0
双人 embedding 时间长度是否一致
这些检查能提前发现音频异常。
3. 把音频编码拆成独立服务
视频生成非常耗 GPU,而 Wav2Vec2 音频编码相对轻一些。
可以把音频编码拆成单独 worker:
任务提交
↓
音频预处理 worker
↓
Wav2Vec2 embedding worker
↓
视频生成 worker
这样可以提高系统吞吐。
4. 尝试替换音频编码器
Wav2Vec2 是一种选择,但不是唯一选择。
如果要做更强的多语言、情绪、唱歌、方言或跨语言配音,可以尝试其他语音表征模型。
不过替换不是简单改一行代码。
因为后续视频模型可能已经适配了当前 audio embedding 的形状和语义分布。
如果换编码器,需要同时适配:
embedding 维度
时间长度
层数结构
归一化方式
后续模型输入接口
训练或微调策略
所以普通二次开发不建议一开始就换音频编码器。
5. 记录音频与 embedding 元数据
每次生成 .pt 时,可以额外保存一个 JSON:
{
"audio_path": "xxx.wav",
"sample_rate": 16000,
"duration": 12.4,
"fps": 25,
"video_length": 310,
"embedding_shape": [310, 12, 768]
}
这样排查问题会方便很多。
尤其是长视频任务,音频长度、视频帧数和 embedding 时间维度非常容易出错。
二十八、常见问题排查
如果你在 InfiniteTalk 中遇到音频编码相关问题,可以按下面思路排查。
1. wav2vec_dir 是否正确?
custom_init() 使用 local_files_only=True,所以本地必须已经有 Wav2Vec2 权重。
如果路径错误,模型加载会失败。
2. speech_array 是否为空?
如果音频读取失败,speech_array 可能为空或长度异常。
这会直接导致 embedding 提取失败。
3. 采样率是否是 16k?
虽然前面音频预处理已经统一为 16k,但如果你修改过流程,必须确认传入 get_embedding() 的音频和 sr 一致。
4. hidden_states 是否为空?
源码里有判断:
if len(embeddings) == 0:
print("Fail to extract audio embedding")
return None
如果出现这个提示,说明 Wav2Vec2 没有正常返回 embedding。
5. 1.pt / 2.pt 是否生成?
如果 .pt 文件没有生成,说明问题出在音频编码阶段。
如果 .pt 文件生成了,但视频不对,再继续查 pipeline。
6. 双人模式两个 embedding 是否对齐?
双人对话中,audio_prepare_multi() 会通过静音补齐来对齐两路音频。
如果你自己改了双人音频逻辑,要确认 1.pt 和 2.pt 时间维度是否一致。
否则可能出现角色说话错位。
7. 最终音频和 embedding 是否来自同一段语音?
如果 cond_audio 用的是 A 音频生成的 embedding,但 video_audio 合成的是 B 音频,最终视频就会出现嘴型和声音不一致。
所以生成任务里一定要保证:
用于 embedding 的音频
和
最终合成的音频
来自同一个处理流程。
二十九、这一篇的核心结论
InfiniteTalk 中的 Wav2Vec2 音频编码,可以概括成一句话:
把 16k speech array 转换成按视频时间轴组织的多层 audio embedding。
更具体地说:
custom_init() 负责加载 Wav2Vec2FeatureExtractor 和 Wav2Vec2Model,并冻结 feature extractor。
get_embedding() 负责把 speech array 转成 Wav2Vec2 输入,计算音频时长,并按 25fps 估算对应视频长度。
Wav2Vec2 输出 hidden states 后,源码使用 hidden_states[1:] 保留多层语音特征。
然后通过 torch.stack() 把多层特征堆叠起来,再通过 rearrange() 把时间维度调整到第一维。
最终得到的 audio embedding 可以理解为:
[时间位置, Wav2Vec2层级, 特征维度]
它会被保存成:
1.pt
2.pt
然后写入:
input_clip['cond_audio']
作为 InfiniteTalkPipeline 生成视频时的音频条件。
到这里,音频已经完成了从“声音文件”到“模型条件”的转换。
下一篇我们会继续进入 Pipeline 初始化部分,分析:
InfiniteTalk 源码解析 #6:InfiniteTalkPipeline 初始化:T5、CLIP、VAE、WanModel 如何串起来
从下一篇开始,我们会看到音频 embedding、文本 prompt、参考图片或视频,最终是如何在视频生成管线里汇合的。

251

被折叠的 条评论
为什么被折叠?



