数字人 · combo workflow

OmniHuman 数字人怎么支持 >60s

OmniHuman 单次只能跑 60 秒。超了的音频要切几段分别跑、再拼回来。难点不在切,在拼完之后还能不能跟"没切"一样准。

真实 case task 8bc83ee0 · 2026-06-04 · 讨论+本地验证,未改任何代码/线上

60s
OmniHuman 单次硬上限
71.58s
这条 case 的音频
+1007ms
直接拼 · 漂移
+87ms
裁尾后 · 漂移

问题:超过一分钟就直接挂

combo workflow voiceover_digital_human_pip_subtitle_mai 里,数字人这一棒把整条配音喂给 OmniHuman v1.5。模型 720p 单次最多 60 秒,这条 case 的音频 71.58 秒,于是 skill 直接报 audio_too_long,任务失败。

这不是保守设的值。animate.py:307 严格判 60s、不容 epsilon——是 OmniHuman 本身的天花板。要破只能切。

探索过程:从一个失败任务挖到根因

  1. Mai 那边给到失败任务 8bc83ee0,result 写着 audio_too_long=71.58s。配音是 Tobatsu 内部生成的,不是 Mai 入参。
  2. 只读查 prod 库(magi3),拿到 task_type 和 workspace 路径——workspace 还在机器上。
  3. 翻 workspace:配音阶段其实是逐段生成的——12 段 segment-*-final.mp3 + 一条合并的 final-audio.mp3(71.576s)+ 字符级时间戳。
  4. 对账:每段在合并轨上的起止点,正好压在脚本的时间刻度上(53 / 60 / 66 / 71.53…),段间用静音垫齐。
  5. 挑切点:离 58s 最近的段边界是 53.0s(第 8 段末)→ 切成 A=53s、B=18.6s,都 ≤60。切点落在两段之间的停顿,不切词。
  6. 真跑两段 OmniHuman——结果暴露了真正的坑。
收获:配音本来就是分段的,所以根本不用"切成品音频"。按段分组就行,切点天然干净。

真正的坑:说完话,画面还在空转

两段都中招:OmniHuman 输出的画面比声音长。话说完了,它自己续了一截没有声音的画面——而且不是定格,人还在动。

容器时长画面流声音流多出的空转尾巴
A53.800s53.800s53.000s+0.800s
B18.760s18.760s18.531s+0.229s
audio end frame
53.0s — 声音刚结束,嘴闭上。
tail frame still moving
53.5s — 没声音了,可是手换了姿势、还在眨眼。这截就是要裁掉的尾巴。

它带来三件麻烦:

解法:按声音长度,把尾巴裁掉

尾巴是纯尾部、又没声音,所以按"这段声音多长"一刀切到那里,不会动到任何对口型的内容。一招同时治好时长和接缝。

不裁尾裁到声音末
拼完总时长72.583s(+1007ms)71.663s(+87ms)
接缝空转 0.8s 卡顿说完即切,连贯
剩的 +87ms 来自切成品 mp3 的重编码。改成按段分组、或 PCM 切一次(实测 0.0ms),能压到 30ms 以内,基本等于没切。

改在哪:只动 combo runner 一个函数

skill 不动、agent 不动。逻辑加在编排层 mai-combo-runnerrun_digital_human()——音频超 58s 才走分段,≤58s 老路完全不变。产物还是单个 digital-human.mp4,下游 PIP/字幕完全无感。

配音 71.58s 12 段拼成 按段边界切 @53s · 各 ≤58s chunk A 53s OmniHuman chunk B 18.6s OmniHuman 每段裁尾巴 -t = 该段声音长 concat 单个 .mp4 墨蓝 = 关键新增一步(裁尾),其余沿用现有编排
def run_digital_human(payload, audio_url):
    D = probe_dur(audio_url)
    if D <= 58.0:
        return _omnihuman_once(audio_url, payload)   # ≤60s 老路,零改动
    cuts   = pick_cuts_from_segment_boundaries(58.0)  # 切点落段间停顿
    chunks = split_audio_zero_drift(audio, cuts)      # 按段分组 / PCM,0 漂移
    parts  = []
    for ch in chunks:
        m = _omnihuman_once(ch.url, payload)          # 同 skill,各 ≤60s
        parts.append(trim_video_to(m.video, ch.dur))  # ★ 裁掉无声尾巴
    concat(parts, OUTPUTS/"digital-human/digital-human.mp4")

* split / trim / concat 用已声明的 ffmpeg-media-tools,不加新依赖。

看效果:三条对比

同一张主播图、同一句 prompt。裁过的明显比没裁的连贯;Hedra 是"整条单次跑"的参照(单独提供,见说明)。

OmniHuman 切片 · 裁尾后 — 71.663s,接缝连贯,这是建议落地的版本。
OmniHuman 切片 · 没裁 — 72.583s,接缝处能看到 0.8s 空转。对照用。
参照:Hedra Avatar 整条单次跑,71.577s(+1ms),不用切/裁/拼。若考虑长音频换原生 provider,另议。

等你拍的几个口径

问题建议
安全上限58s(给 strict 60 留 2s 垫)
切点来源配音段边界(现成、干净)
音频切法按段文件分组(段还在盘上,0 漂移)
裁到哪该段配音的时长(下游时间轴对齐)
接缝处理硬拼(姿态重置,已确认可接受)
配套机器版:omnihuman-over60s-handoff.zip(handoff MD + 证据 JSON + 复现脚本),改 combo 的人可直接照着改 run_combo_workflow.py:868

本页仅为可视化解释稿。提案讨论 + 本地验证,未改动任何代码 / skill / workflow / 线上系统。