tobatsu / skills / shared

digital-human-omnihuman

一张主播图 + 一段配音 → 对口型数字人视频. 跑 fal 上的 ByteDance OmniHuman v1.5; 输入是视频自动抽人声.
v0 · contract locked
入口 scripts/animate.py · 最长 1500s · 不走 Rendi · provider fal.bytedance.omnihuman.v1.5

01 · WHAT IT DOES

一句话

给我 audio_urlimage_url, 我还你一段会说话的数字人 mp4. 如果 audio_url 是视频, 我先抽人声.

不解析自然语言 — agent 已经把意图翻成这 4 个字段了. 不生成主播图 — 形象怎么来不归我管.

重活全在 fal SaaS, 本机不跑视频编解码; 跟 subtitle / pip 走 Rendi 的套路不同.

02 · INPUT CONTRACT

入参 (4 个字段, 全在 payload.json)

audio_url
音频或视频的公网 URL. skill 自己 ffprobe 探测: 有视频流 → 自动抽人声; 只有音频 → 直接用. agent 不用关心是哪种.
image_url
主播图的公网 URL. 推荐 1:1 (对下游 PIP 圆形/方形浮窗最友好), 其他比例也接受. 内容由上游决定: 现成图 / 上游生成 / hook 风格迁移都行, 不归这个 skill.
prompt
OmniHuman 行为引导文本. 语气 / 表情 / 肢体怎么演. agent 看场景自己写, 不传走空串. 例: "语气活泼, 用手比划".
user_brief
审计描述. 不参与逻辑分流, 只进日志方便事后追. 跟 subtitle skill 同套路.

03 · ADAPTIVE INPUT

audio_url 是音频还是视频, skill 自己分

不开 isolate_voice 开关 — ffprobe 看一眼就知道. 视频自动调 ElevenLabs 抽人声, 抽完还校验时长 + 响度.

分支 A · 音频

只有 audio 流

  • ffprobe → 只见 audio 流
  • 不调 ElevenLabs
  • 校验时长 ≤ 60s
  • 直接喂 OmniHuman
分支 B · 视频

有 video 流

  • ffprobe → 见 video 流
  • 调 ElevenLabs audio-isolation
  • 下隔离后的音频, ffprobe + ffmpeg volumedetect
  • 校验时长 ≥ 1s, mean_volume > -50 dBFS
  • 校验时长 ≤ 60s, 喂 OmniHuman

视频里压根没人声 (纯 BGM) → 隔离后会拿到时长极短或近静音的结果, 走 voice_extraction_failed.

04 · INTERNAL RULES

这些不开字段, skill 自己定

设计上不让 agent 调; 都是工程默认值, 改要发新 skill 修订.

OmniHuman 分辨率
720p
≤ 60s 通吃; 1080p 卡 30s 不够用
turbo_mode
false
求稳; 实测画质差异不明显
mask_url
不传
默认单人主播图场景
主播图尺寸
不变换
image_url 给啥用啥, skill 不缩放

05 · FAILURE MODES

8 种 hard-fail · 失败立即 tobatsu-agent fail

每条 fail 前先写 outputs/.logs/animate-error.json (reason / fal_request_ids_so_far / created_at), 再 tobatsu-agent fail --summary.

reason触发场景
invalid_payloadaudio_url 或 image_url 缺 / payload.json 损坏 / 字段类型错
download_failed任一 URL 3 次重试都拉不到
image_unreadableimage_url 内容不是有效图片 (Pillow open 抛错)
audio_unreadableaudio_url ffprobe 跑挂 / 文件无音频流
audio_too_long隔离后 / 直接用的音频 > 60s (OmniHuman 720p 上限)
voice_extraction_failed输入是视频, ElevenLabs 成功返回, 但音频时长 < 1s 或 mean_volume < -50 dBFS
provider_failedfal 接口报错 (audio-isolation 或 OmniHuman 任一)
upload_failedtobatsu-agent output upload 3 次重试都挂

06 · RUNTIME FLOW

端到端跑一遍

读 payload → 探输入 → (可能抽人声) → 探图 → 跑 OmniHuman → 下载 → 上传 → 写 result.json.

1

读 payload + 校验

检查 audio_url / image_url 都在, 类型对, JSON 合法. 不合法 → invalid_payload.

2

下载 audio_url, ffprobe 探测

拿 streams + duration. 看是有 video 流还是只有 audio.

# 关键判断
streams = ffprobe.streams
input_kind = "video" if any(s.codec_type == "video") else "audio"
3

视频分支 → 抽人声 (仅当 input_kind = video)

调 fal ElevenLabs audio-isolation, 下隔离结果, ffprobe 校验时长, ffmpeg volumedetect 校验响度.

fal_call("fal-ai/elevenlabs/audio-isolation", {video_url: audio_url})
# 校验
duration >= 1.0s # else voice_extraction_failed
mean_volume > -50 dBFS # else voice_extraction_failed
4

下载 image_url + Pillow 校验

Pillow open + verify, 不是图片 → image_unreadable.

5

调 OmniHuman v1.5

image_url + 干净 audio_url + prompt + resolution=720p + turbo=false. fal_client.submit() 拿 request_id, get() 等结果. 约 60s 音频 ≈ 13 分钟. 失败 → provider_failed.

6

下载结果视频 + 探尺寸

3 次重试; 探完拿 width / height / duration.

7

tobatsu-agent output upload → R2

label = digital-human.mp4. 3 次重试. 拿到 CDN URL.

8

写 result.json + done

包含 fal_request_ids (omnihuman / 可能的 audio_isolation), output URL, 尺寸 / 时长, input_audio_kind, audio_isolated. 跑 tobatsu-agent done.

07 · PIPELINE POSITION

跟其他 skill 的关系

这个 skill 是 pipeline 中间一环, 单一职责. 形象怎么来、字幕怎么烧、怎么拼接都不归它. 它只管"图 + 音 → 数字人".

hook 视频 / 用户描述
上游: 形象生成 / 改图 / 风格迁移
image_url
源视频 .mp4 或 配音 .mp3
audio_url
digital-human-omnihuman
digital-human.mp4 (含音)
↓ 可选下游
pip-compose-rendi (pip_volume=0)
subtitle-burn-rendi (烧字幕)
concat (hook + 数字人段)

音频去重提醒: 数字人输出永远带 OmniHuman 生成的音轨. 下游做 PIP / concat 时, 如果源视频已经有这段人声 (典型: input_kind=video 那条路, 源视频被原样保留参与拼接), 必须把数字人那一路静音 — 否则同一句话出现两次. PIP 那边 pip_volume=0; concat 那边选主音轨.

08 · KNOWN BOUNDARIES

第一版不做

音频 > 60s — 不切片, 直接 fail. 后续可加切片+拼接逻辑.

1080p 分辨率 — 第一版只开 720p (≤60s). 1080p 卡 30s 不够用, 留作 v1+.

多人物主播图 — 不暴露 mask_url, 默认单人. 多人场景由上游图层处理.

多语种校验 — OmniHuman 自己处理, skill 不预校验.