InfiniteTalk 源码解析 #5:Wav2Vec2 音频编码:如何把语音变成逐帧 audio embedding

上一篇我们分析了 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_audiovideo_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.pt2.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、参考图片或视频,最终是如何在视频生成管线里汇合的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天天进步2015

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值