前面几篇文章,我们已经分析了 InfiniteTalk 的核心生成机制。
到目前为止,我们已经知道:
InfiniteTalk 会先把音频通过 Wav2Vec2 编码成 audio embedding,然后通过 AudioProjModel 转成 audio context tokens,再在 WanModel 的每个 Transformer block 中通过 audio cross attention 注入音频条件。
这样,语音就能在扩散生成过程中影响视频 latent token,从而控制嘴型、表情、头部动作和身体姿态。
但这还只是解决了一个片段怎么生成的问题。
真正的长视频生成,还要解决另一个难题:
如果视频很长,不能一次性全部生成,应该怎么把多个片段自然接起来?
这一篇我们重点分析 InfiniteTalk 的长视频生成机制,也就是:
streaming
motion_frame
分块生成
片段续接
前后运动上下文传递
对应源码主要在:
wan/multitalk.py
中的:
generate_infinitetalk()
这一篇要回答的问题是:
InfiniteTalk 是如何把一个长音频驱动的视频,拆成多个 clip 生成,又尽量避免片段之间人物动作突然断掉的?
一、为什么长视频不能一次性生成?
在视频扩散模型里,一次生成完整长视频非常困难。
原因主要有三个。
第一,显存压力太大。
视频不是图片。视频多了一个时间维度。
如果一张图片的 latent token 已经很多,那么几十秒、几分钟视频的 token 数量会迅速膨胀。
显存占用大致会受到这些因素影响:
分辨率
帧数
latent 尺寸
Transformer 层数
attention 序列长度
采样步数
batch size
如果想一次生成几百帧、几千帧,显存和计算量都会非常夸张。
第二,时间一致性难以保持。
短视频生成几秒钟,模型还能勉强保持人物身份、背景和动作稳定。
但生成时间越长,越容易出现:
人物身份漂移
脸型变化
背景变化
颜色漂移
动作突然跳变
镜头轨迹不连续
第三,音频和视频对齐更复杂。
长音频需要和长视频逐帧对齐。
如果中间任何一个片段出现时长偏差,后面都可能逐渐音画不同步。
所以长视频生成不能简单地"把 frame_num 调大"。
更可行的方式是:
把长视频拆成多个短片段生成,
每个片段只生成固定长度,
再通过上下文帧让片段之间自然衔接。
这就是 InfiniteTalk 的 streaming 思路。
二、clip 模式和 streaming 模式的区别
在 generate_infinitetalk.py 中,命令行参数里有:
--mode clip
--mode streaming
这两个模式代表两种生成思路。
clip 模式更适合短片段生成。
它可以理解成:
输入参考图像或视频
↓
输入一段音频
↓
生成一个固定长度视频片段
这种方式简单直接,适合几秒钟 demo 或短口播片段。
streaming 模式则面向长视频。
它的思路是:
长音频
↓
按 frame_num 切成多个片段
↓
每次生成一个 clip
↓
把上一段尾部 motion frames 传给下一段
↓
去掉重复续接帧
↓
拼接成完整视频
所以,streaming 不是一次生成无限长视频,而是用循环方式分段生成。
它的核心不是"无限显存",而是"有限长度片段 + 上下文续接"。
三、generate_infinitetalk 中的长视频主变量
在 wan/multitalk.py 的 generate_infinitetalk() 里,有一组变量专门服务于长视频生成。
比较关键的包括:
clip_length = frame_num
is_first_clip = True
arrive_last_frame = False
cur_motion_frames_num = 1
audio_start_idx = 0
audio_end_idx = audio_start_idx + clip_length
gen_video_list = []
这些变量分别表示:
clip_length:每个片段生成多少帧,通常等于 frame_num
is_first_clip:当前是否是第一个片段
arrive_last_frame:是否已经到达最后一个片段
cur_motion_frames_num:当前用于续接的 motion frames 数量
audio_start_idx:当前片段音频 embedding 的起始位置
audio_end_idx:当前片段音频 embedding 的结束位置
gen_video_list:保存每次生成出来的视频片段
这几个变量构成了 streaming 循环的基本状态。
可以把它理解成一个滑动窗口:
第 1 次:
audio_start_idx = 0
audio_end_idx = frame_num
第 2 次:
audio_start_idx += frame_num - motion_frame
audio_end_idx = audio_start_idx + frame_num
第 3 次:
继续往后滑动
为什么不是每次直接加 frame_num?
因为相邻片段之间要保留一段重叠的 motion frames。
这个重叠区域就是片段续接的关键。
四、motion_frame 是什么?
motion_frame 是长视频生成里非常关键的参数。
在入口脚本里,它通过命令行传入:
--motion_frame 25
在 Pipeline 中,它会影响:
cur_motion_frames_num = motion_frame
cond_frame = videos[:, :, -cur_motion_frames_num:]
audio_start_idx += (frame_num - cur_motion_frames_num)
简单说,motion_frame 表示:
每一段生成完成后,从尾部取多少帧作为下一段的运动上下文。
例如:
frame_num = 81
motion_frame = 25
那么第一段生成 81 帧。
下一段不是从第 82 帧开始完全独立生成,而是保留上一段最后 25 帧作为上下文。
所以第二段真正向前推进的长度是:
81 - 25 = 56 帧
这就是为什么更新音频起点时用的是:
audio_start_idx += (frame_num - cur_motion_frames_num)
而不是:
audio_start_idx += frame_num
因为相邻片段之间有重叠区域。
这个重叠区域用于保持动作连续。
五、为什么需要 motion frames?
如果没有 motion frames,长视频会变成这样:
第 1 段独立生成
第 2 段独立生成
第 3 段独立生成
......
每一段都从参考图像或参考帧重新开始。
这样很容易出现:
上一段人物头向左,下一段突然回正
上一段嘴巴刚张开,下一段突然闭嘴
上一段身体正在前倾,下一段突然静止
上一段背景光线偏暗,下一段突然变亮
上一段手的位置在下方,下一段手突然消失
这些问题本质上都是片段之间没有运动上下文。
motion_frame 的作用就是把上一段尾部状态带到下一段。
也就是说,下一段生成时,不是从零开始,而是知道:
上一段最后人物是什么姿态
头部朝向在哪里
嘴型处于什么状态
身体运动趋势是什么
背景和镜头状态是什么
这就是所谓的 motion context。
六、is_first_clip:第一段和后续段的处理不同
源码里有一个变量:
is_first_clip = True
第一段和后续段的处理逻辑不同。
第一段没有上一段可以参考,所以只能使用输入的 cond_image 或参考视频首帧作为条件。
源码中对应逻辑大致是:
if is_first_clip:
latent_motion_frames = self.vae.encode(cond_image)[0]
else:
latent_motion_frames = self.vae.encode(cond_frame)[0]
也就是说:
第一段:
motion frames 来自原始参考图像或参考视频首帧
后续段:
motion frames 来自上一段生成结果的尾部帧 cond_frame
这就是长视频续接的核心区别。
第一段负责启动生成。
后续段负责延续上一段。
七、cond_frame:上一段尾部帧如何传给下一段?
在每轮生成完成后,源码会执行:
cond_frame = videos[:, :, -cur_motion_frames_num:].to(torch.float32).to(self.device)
这行代码的意思是:
从当前生成的视频 videos 中取最后 cur_motion_frames_num 帧,
作为下一轮生成的条件帧。
假设:
motion_frame = 25
那么每次生成完一个 clip,就取最后 25 帧:
当前 clip:
[0, 1, 2, ..., 80]
取尾部:
[56, 57, ..., 80]
作为下一段的 cond_frame
下一段生成时,会把这些帧编码成 latent_motion_frames,再注入到扩散采样初始阶段。
这样下一段就能继承上一段的动作状态。
八、latent_motion_frames:为什么在 latent 空间续接?
后续段中,cond_frame 会通过 VAE 编码成 latent:
latent_motion_frames = self.vae.encode(cond_frame)[0]
然后在采样阶段注入到当前 latent 中。
源码里有两处关键逻辑。
第一处是在采样前,如果不是第一个 clip,会给 motion frames 加噪声:
motion_add_noise = torch.randn_like(latent_motion_frames).contiguous()
add_latent = self.add_noise(latent_motion_frames, motion_add_noise, timesteps[0])
latent[:, :T_m] = add_latent
第二处是在每个扩散步中,持续固定或注入 motion frames:
latent[:, :cur_motion_frames_latent_num] = latent_motion_frames
这说明 InfiniteTalk 不是在像素层面简单拼接帧,而是在 latent 空间中进行运动上下文注入。
这很重要。
因为 WanModel 的扩散采样本来就在 latent 空间里进行。
如果在像素空间硬拼,会出现明显边界或风格不一致。
而在 latent 空间注入 motion frames,可以让下一段在扩散生成过程中自然继承上一段的运动状态。
九、add_noise:为什么要给 motion frames 加噪?
add_noise() 的作用是把已有 latent 加到当前扩散 timestep 对应的噪声水平。
源码中:
def add_noise(self, original_samples, noise, timesteps):
timesteps = timesteps.float() / self.num_timesteps
timesteps = timesteps.view(timesteps.shape + (1,) * (len(noise.shape)-1))
return (1 - timesteps) * original_samples + timesteps * noise
这个函数的逻辑可以理解成:
当前 timestep 越大,噪声越多;
当前 timestep 越小,原始 latent 保留越多。
为什么要这么做?
因为扩散采样不是一次性生成图像,而是从噪声逐步去噪。
如果要把上一段的 motion frames 放进当前片段,就不能直接把干净 latent 塞进一个高噪声阶段,否则它和当前采样状态不匹配。
所以要先把 motion frames 加噪到当前 timestep 的噪声水平。
这样它才能和当前片段的 latent 一起参与去噪过程。
这类似于 img2img 或 video continuation 中的做法:
已有内容
↓
加噪到指定 timestep
↓
再从这个状态继续去噪
这能让 motion frames 既保留上一段信息,又和当前扩散过程兼容。
十、为什么生成后要去掉重复 motion frames?
每个片段之间有重叠区域。
如果直接把所有片段拼起来,重复的 motion frames 会出现两次。
所以源码里会判断:
if is_first_clip:
gen_video_list.append(videos)
else:
gen_video_list.append(videos[:, :, cur_motion_frames_num:])
意思是:
第一段完整保留。
后续段去掉开头的 cur_motion_frames_num 帧。
因为后续段开头这部分是用来续接上一段的运动上下文,不应该重复出现在最终视频里。
举个例子:
第 1 段生成:
0 ~ 80
第 2 段生成时使用第 1 段最后 25 帧作为开头上下文:
56 ~ 136
如果直接拼接:
0 ~ 80 + 56 ~ 136
其中 56 ~ 80 会重复
正确做法:
第 1 段保留 0 ~ 80
第 2 段去掉前 25 帧,只保留 81 ~ 136
所以最终拼接逻辑是:
第一段:完整保留
后续段:去掉开头 motion frames
这就是分块续接中非常关键的一步。
十一、audio_start_idx 和 audio_end_idx:音频窗口如何滑动?
长视频生成不仅要续接画面,还要续接音频。
源码中使用:
audio_start_idx = 0
audio_end_idx = audio_start_idx + clip_length
每轮生成时,根据这两个索引截取当前片段的音频 embedding。
在每轮生成结束后,更新:
audio_start_idx += (frame_num - cur_motion_frames_num)
audio_end_idx = audio_start_idx + clip_length
这和视频片段的重叠逻辑完全对应。
因为视频片段有 motion_frame 重叠,所以音频片段也要按照同样步长滑动。
否则就会出现:
视频重叠了 25 帧
但音频没有重叠
或者:
音频推进太快
视频推进太慢
最终都会导致音画不同步。
所以,audio_start_idx 的更新方式非常重要。
它保证每个生成片段的音频条件与当前视频时间范围对应。
十二、每个片段如何截取 audio embedding?
每轮 while 循环中,会根据 audio_start_idx 和 audio_end_idx 构造音频窗口。
源码中有:
indices = (torch.arange(2 * 2 + 1) - 2) * 1
这会得到类似:
[-2, -1, 0, 1, 2]
然后每一帧都会取一个局部音频窗口:
center_indices = torch.arange(
audio_start_idx,
audio_end_idx,
1,
).unsqueeze(1) + indices.unsqueeze(0)
也就是说,对于当前片段的每个时间位置,都会取前后若干 audio embedding 作为局部上下文。
再通过:
center_indices = torch.clamp(center_indices, min=0, max=full_audio_embs[human_idx].shape[0]-1)
防止索引越界。
最后得到:
audio_emb = full_audio_embs[human_idx][center_indices][None, ...].to(self.device)
这一步说明,每个视频帧并不是只看一个音频点,而是看一个小窗口。
这和前面分析的 audio_window 思路是一致的。
长视频中,音频 embedding 是按片段滑动截取的,同时每个位置又带局部上下文。
十三、arrive_last_frame:什么时候停止循环?
长视频生成是一个 while True 循环。
它需要知道什么时候停。
源码里使用:
arrive_last_frame = False
每轮结束后会判断:
if audio_end_idx >= min(max_frames_num, len(full_audio_embs[0])):
arrive_last_frame = True
也就是说,当当前片段已经覆盖到:
最大生成帧数 max_frames_num
或者:
音频 embedding 的末尾
就准备结束。
然后在下一轮完成后:
if arrive_last_frame:
break
为什么不是一到末尾就立刻停?
因为当前片段可能还需要生成完,才能得到完整的视频尾部。
所以它会标记 arrive_last_frame=True,等当前片段生成完成后再退出。
十四、音频长度不够时为什么要 flip 补齐?
源码中还有一段处理音频尾部的逻辑。
如果 audio_end_idx 超过某个人物的 audio embedding 长度,会计算:
miss_length = audio_end_idx - len(full_audio_embs[human_inx]) + 3
然后执行:
add_audio_emb = torch.flip(full_audio_embs[human_inx][-1*miss_length:], dims=[0])
full_audio_embs[human_inx] = torch.cat([full_audio_embs[human_inx], add_audio_emb], dim=0)
这说明如果最后一个片段需要的音频 embedding 超过了已有长度,源码会从末尾取一段 embedding 翻转后补上。
为什么要这样?
因为最后一个片段仍然需要完整的 clip_length 音频窗口。
如果直接缺失,索引会越界,或者窗口不完整。
用尾部 embedding 反向补齐,是一种工程上的边界处理策略。
它不是为了真实延长音频内容,而是为了保证最后片段的音频窗口形状完整,避免模型输入异常。
最终输出视频会再裁剪到有效长度。
十五、max_frames_num:控制最终生成长度
generate_infinitetalk() 中有参数:
max_frames_num=1000
最终拼接后,源码会执行:
gen_video_samples = torch.cat(gen_video_list, dim=2)[:, :, :int(max_frames_num)]
这说明无论中间生成多少片段,最终都会裁剪到 max_frames_num。
这个参数非常重要。
它相当于长视频生成的上限。
如果你输入音频很长,但只想生成前 1000 帧,就可以通过 max_frames_num 控制。
它也可以防止长音频导致无限循环或生成过长。
在产品化场景中,max_frames_num 可以对应:
免费用户最大生成时长
单次任务最大时长
GPU 队列限制
计费额度限制
十六、gen_video_list:多个片段如何合并?
每次生成的 videos 都会加入:
gen_video_list
最后:
gen_video_samples = torch.cat(gen_video_list, dim=2)
这里的 dim=2 是视频时间维度。
所以拼接方式是:
片段 1 的时间帧
+
片段 2 的时间帧
+
片段 3 的时间帧
注意,前面已经对后续片段去掉了重复 motion frames。
所以最终拼接时不需要再额外处理重叠区域。
完整逻辑是:
每个片段独立生成
↓
后续片段去掉开头 motion frames
↓
按时间维度 cat
↓
裁剪到 max_frames_num
十七、cond_image 为什么每轮都会更新?
在每轮生成结束后,源码还会更新:
cond_image = extract_specific_frames(cond_file_path, audio_start_idx)
也就是说,下一段会从原始条件视频中提取对应时间位置的参考帧。
这点很重要。
长视频生成不只是依赖上一段尾部 motion frames,还会继续从源视频中取稀疏参考帧。
这就对应了 InfiniteTalk 的 sparse-frame video dubbing 思路。
它不是完全脱离原视频自由生成,而是通过稀疏关键帧维持:
人物身份
背景
镜头轨迹
关键姿态
原视频的运动趋势
所以长视频中有两类上下文:
上一段生成结果的 motion frames
原始条件视频的 sparse reference frames
前者帮助片段之间平滑衔接。
后者帮助长期身份、背景和镜头一致。
这两个机制结合起来,才构成长视频生成的核心。
十八、streaming 不是简单拼接,而是"参考帧 + 运动帧"双约束
现在我们可以更准确地理解 InfiniteTalk 的 streaming。
它不是这样:
片段 1 生成
片段 2 生成
片段 3 生成
最后直接拼接
而是:
每个片段:
使用当前时间点的参考帧
使用当前片段的音频 embedding
使用上一段尾部 motion frames
在 latent 空间生成当前片段
片段之间:
保留 motion_frame 重叠
后续片段去掉重复开头
最终按时间拼接
所以,它的长视频稳定性来自两个方向。
第一,参考帧约束长期一致性。
人物是谁
背景是什么
镜头大致怎么走
画面风格是什么
第二,motion frames 约束短期连续性。
上一段尾部是什么姿态
嘴型处于什么状态
头部运动趋势是什么
身体动作是否正在继续
这就是"稀疏参考 + 运动续接"的组合策略。
十九、为什么要在 latent 中持续固定 motion frames?
在采样循环中,源码不只在初始时注入 motion frames,还会在每个 timestep 中设置:
latent[:, :cur_motion_frames_latent_num] = latent_motion_frames
这说明 motion frames 被作为强约束保留下来。
它的作用是:
保证当前片段开头与上一段尾部保持一致
避免采样过程中把续接帧改坏
让后续新生成帧从稳定上下文中延伸出来
如果只在开始时注入一次,后续多步去噪过程中,这些帧可能会逐渐偏离。
持续固定则能增强片段开头的稳定性。
这类似视频续写中的 anchor frames。
开头 motion frames 是锚点。
模型需要基于这些锚点继续生成后面的帧。
二十、motion_frame 取多大合适?
motion_frame 太小,片段之间上下文不足。
可能出现:
动作断裂
头部跳变
嘴型不连续
镜头不平滑
motion_frame 太大,也会有问题。
因为每个片段的有效新增帧数是:
frame_num - motion_frame
如果 motion_frame 太大,每轮推进就很少。
这会导致:
生成效率变低
重复计算增多
同样时长需要更多轮循环
显存和时间成本增加
所以它是一个平衡参数。
可以粗略理解:
motion_frame 小:速度快,但衔接风险更高
motion_frame 大:衔接更稳,但速度更慢
如果默认使用 25 帧,在 25fps 视频中大约是 1 秒上下文。
这比较符合说话视频的短期运动连续性需求。
二十一、frame_num 和 motion_frame 的关系
frame_num 决定每次生成多长。
motion_frame 决定相邻片段重叠多长。
每轮真正新增的帧数是:
effective_new_frames = frame_num - motion_frame
例如:
frame_num = 81
motion_frame = 25
每轮新增:
81 - 25 = 56 帧
如果视频是 25fps,那么每轮新增时长大约是:
56 / 25 ≈ 2.24 秒
也就是说,虽然每次生成 81 帧,但最终视频每轮只向前推进 56 帧。
这就是 streaming 模式的代价:为了衔接自然,需要牺牲一部分生成效率。
但相比片段断裂,这个代价通常是值得的。
二十二、长视频中为什么还会有色彩漂移?
即使用了 motion_frame 和参考帧,长视频仍然可能出现色彩漂移。
原因是每个片段都经历独立扩散采样。
即使参考条件相同,随机噪声、采样误差、音频条件强度、LoRA、量化和步数都会影响最终画面。
InfiniteTalk 源码里也提供了:
color_correction_strength
以及:
match_and_blend_colors()
这说明项目也考虑到了颜色一致性问题。
如果 color_correction_strength > 0,会用原始条件图像作为颜色参考,对生成视频做颜色匹配和混合。
这不是长视频续接的核心机制,但对长序列稳定性有帮助。
尤其是在 image-to-video 长视频中,颜色和光照漂移会比较明显。
二十三、streaming 和 scene_seg 的关系
在第 3 篇我们提到,入口脚本里还有 scene_seg 场景切分逻辑。
它和 streaming 不是一回事。
streaming 是在一个长片段内部按固定窗口逐步生成。
scene_seg 是根据原始视频镜头变化,把视频切成多个场景片段。
可以理解为:
scene_seg:按镜头结构切大段
streaming:在每个大段内部按 frame_num 切小段
如果原视频有明显镜头切换,先做 scene segmentation 会更合理。
否则 streaming 可能跨越镜头切换继续续接,导致前后画面逻辑冲突。
比如上一帧还是室内人物,下一帧原视频已经切到户外场景,如果强行用 motion frames 续接,模型就会很难处理。
所以复杂长视频更适合:
先 scene_seg
再对每个场景内部 streaming
最后拼接各场景结果
二十四、长视频生成最容易出问题的地方
从源码逻辑看,长视频生成有几个高风险点。
1. 音频和视频帧索引错位
如果 audio_start_idx、audio_end_idx 更新不正确,就会导致嘴型和音频逐渐错位。
2. motion_frame 太小
如果重叠帧太少,片段之间可能出现动作跳变。
3. motion_frame 太大
如果重叠帧太多,生成效率会明显下降,甚至可能影响整体时间控制。
4. 参考帧不稳定
如果原视频本身镜头变化很大,稀疏参考帧之间差异过大,模型可能出现跳变。
5. 长视频颜色漂移
多次采样后,颜色和光照可能逐渐偏移,需要颜色校正或更强参考约束。
6. 最后一段音频不足
源码通过 flip 补齐音频 embedding,但如果音频切分过短或边界异常,最后一段仍可能不自然。
二十五、从产品化角度怎么封装 streaming?
如果要基于 InfiniteTalk 做数字人长视频平台,建议把 streaming 抽象成任务生成器。
可以设计成:
class StreamingVideoGenerator:
def split_audio_windows(self):
pass
def prepare_motion_context(self):
pass
def generate_clip(self):
pass
def update_context(self):
pass
def append_clip_without_overlap(self):
pass
def finalize_video(self):
pass
这样比把所有逻辑写在一个大函数里更容易维护。
在产品化中,还可以增加:
每个 clip 的生成进度
每个 clip 的临时文件保存
失败重试
断点续跑
中间结果预览
显存释放
分布式队列调度
长视频生成通常耗时较长,如果中途失败,不应该从头再来。
所以每个 clip 生成后都可以缓存。
例如:
task_id/
clip_000.pt
clip_000.mp4
clip_001.pt
clip_001.mp4
metadata.json
这样失败后可以从最后成功的 clip 继续。
二十六、调参建议:如何减少片段断裂?
如果你运行 streaming 模式时发现片段之间断裂明显,可以尝试下面几个方向。
第一,适当增大 motion_frame。
更多重叠上下文通常能改善动作连续性,但会降低速度。
第二,增加采样步数。
采样步数太低可能导致每段质量不稳定。
第三,降低过强的 audio guide scale。
音频引导太强时,模型可能为了嘴型同步牺牲画面稳定。
第四,使用更稳定的参考视频。
如果源视频本身抖动、模糊、镜头变化大,续接难度会明显增加。
第五,启用颜色校正。
如果主要问题是色彩变化,可以尝试 color_correction_strength。
第六,按场景切分。
如果视频中有镜头切换,不要强行跨镜头 streaming。
二十七、常见问题排查
1. 片段之间人物突然跳变
优先检查:
motion_frame 是否太小
cond_frame 是否正确取上一段尾部
latent_motion_frames 是否成功编码
后续片段是否去掉重复帧
2. 嘴型逐渐和声音错位
优先检查:
audio_start_idx 更新是否正确
frame_num 和 motion_frame 是否匹配
音频 embedding 长度是否正确
最终 video_audio 是否和 cond_audio 来源一致
3. 长视频后半段颜色变了
优先检查:
是否启用 color_correction_strength
参考帧是否稳定
LoRA 是否导致色偏
采样步数是否过低
4. 最后一段生成异常
优先检查:
audio_end_idx 是否超过音频长度
flip 补齐是否触发
max_frames_num 是否设置合理
最终裁剪是否正确
5. 生成速度太慢
优先检查:
motion_frame 是否过大
frame_num 是否过大
分辨率是否过高
是否使用 TeaCache
是否使用低步数 LoRA
是否开启量化或低显存模式
二十八、这一篇的核心结论
InfiniteTalk 的长视频生成不是一次性生成无限帧,而是通过 streaming 循环把长视频拆成多个 clip。
每个 clip 生成固定长度 frame_num。
相邻 clip 之间保留 motion_frame 帧重叠。
上一段生成结果的尾部帧会被保存为 cond_frame,再通过 VAE 编码成 latent_motion_frames,作为下一段生成的运动上下文。
下一段生成时,会在 latent 空间中注入这些 motion frames,并在采样过程中持续固定它们,从而减少片段之间的动作断裂。
音频侧通过 audio_start_idx 和 audio_end_idx 滑动截取 audio embedding,保证每个视频片段对应正确的语音时间范围。
后续片段生成完成后,会去掉开头的重复 motion frames,只保留新增部分,最终通过 torch.cat(..., dim=2) 按时间维度拼接成完整视频。
所以,InfiniteTalk 的长视频机制可以总结为:
长音频滑动窗口
+
视频分块生成
+
上一段尾部 motion frames 续接
+
稀疏参考帧保持身份和背景
+
重复帧裁剪
+
最终时间维度拼接
这就是它能够支持长序列说话视频生成的关键。
下一篇我们会继续分析:
InfiniteTalk 源码解析 #10:低显存运行方案:num_persistent_param_in_dit 与 VRAM 管理源码
前面我们已经看到长视频生成会带来很大的显存压力,下一篇就专门看 InfiniteTalk 是如何通过 offload、VRAM management 和参数常驻控制,让大模型尽可能在有限显存下跑起来。