从一张照片到20秒口播视频:AI 首尾帧链式生成全流程拆解

在这里插入图片描述

手把手教你用 RunningHub API 实现多段视频的无缝衔接,让 AI 生成的口播视频像一条拍完的。

前言

做短视频口播内容,最头疼的不是写文案,而是「人」。请演员成本高,自己出镜又没时间,能不能让 AI 替你出镜?

今天这篇文章,我把完整的 「照片 → 改装 → 口播视频 → 多段衔接」 工作流拆开来讲。重点讲一个很多人忽略的技巧:首尾帧链式生成——用上一段的尾帧作为下一段的参考图,让多段视频之间画面连贯、不再「跳切」。

整个流程用到的核心工具就一个:RunningHub 的 ComfyUI 云端工作流 API,图生图用 Kera2Edit,图生视频用 AnimateDiff + MiniMax H3。


整体流程一览

在这里插入图片描述

看起来很清晰对吧?接下来每一步都详细讲。


第一步:图生图编辑 —— 给照片「换装」

场景

在这里插入图片描述

手头有一张带迷彩妆的照片,但我们要的是普通休闲装风格。直接用这张图去生成视频,出来的还是迷彩妆,不符合口播场景。所以第一步先把图「改」了。

用的工作流

Kera2Edit(RunningHub App ID: 2088926295186034689),一个专门做图生图局部重绘和风格迁移的 ComfyUI 工作流。

API 调用

1) 上传本地图片到 RunningHub



curl -X POST 'https://www.runninghub.cn/openapi/v2/media/upload/binary' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -F 'file=@./original_photo.jpg'

返回的 data.fileName(形如 openapi/xxxxx.jpg)就是后续提交任务时用的图片标识。

2) 提交图生图任务

KREA-2-EDIT-One-image-V2 单图编辑工作流【短剧必备】 
下载地址:https://www.runninghub.cn/post/2088923554007048194/?inviteCode=oga1ahgc

curl -X POST 'https://www.runninghub.cn/openapi/v2/run/ai-app/2088926295186034689' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -d '{
    "nodeInfoList": [
      {
        "nodeId": "160",
        "fieldName": "text",
        "fieldValue": "Remove all camouflage face paint, replace with clean natural face..."
      },
      {
        "nodeId": "104",
        "fieldName": "image",
        "fieldValue": "openapi/xxxxx.jpg"
      }
    ],
    "instanceType": "default",
    "usePersonalQueue": "false"
  }'

关键节点说明:

  • nodeId: 160, fieldName: text → 编辑提示词,告诉模型要改什么
  • nodeId: 104, fieldName: image → 参考图,用上传后返回的 fileName

3) 轮询等待结果

curl -X POST 'https://www.runninghub.cn/openapi/v2/query' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -d '{"taskId": "你的taskId"}'

状态会经历 QUEUED → RUNNING → SUCCESS,图生图一般 60-90秒 出结果,消耗约 14-17 RH 币。

提示词技巧

写图生图提示词时,要同时说清楚三件事:

要素示例
改什么Remove camouflage face paint, change military outfit
改成什么Clean natural face, t-shirt and jeans
保留什么Keep facial features, pose, background unchanged

漏掉第三条,AI 可能会连人脸一起「重绘」掉,那就不是同一个人了。


第二步:图生视频 —— 让照片「开口说话」

用的工作流

AnimateDiff + MiniMax H3(App ID: 2088844222551121921),图生视频,支持角色对话、表情变化、自然动作,单条生成约 10 秒视频。

提示词格式:6 段式结构

这个工作流对提示词格式有要求,推荐使用 6 段式 Full-Reference 格式:

subject_definitions:
<Subject 1> is 人物描述 in <Picture 1>, 外貌特征.

summary:
[reference generation] 概括视频内容和参考关系.

retention_analysis:
<Subject 1> (appears in [Shot 1]): fully_preserved - 要保留的特征.

summarized_description:
[Shot 1] 镜头描述. <Subject 1> (S1) 动作描述.
S1 says, <d>[Chinese] 台词内容</d>

overall_soundscape:
环境音描述.

non_diegetic_music:
背景音乐描述.

⚠️ 重要避坑:不要在提示词中引用 <Audio 1>。该工作流的音频节点未连接,引用会直接报错 prompt media tag validation failed。如需配音,后期用 edge-tts + ffmpeg 叠加。

实际调用

MiniMax H3稳定加速版(全能参考4步加速版本)​ 
工作流下载地址:https://www.runninghub.cn/post/2088836712364601345/?inviteCode=oga1ahgc
curl -X POST 'https://www.runninghub.cn/openapi/v2/run/ai-app/2088844222551121921' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -d @task1.json

关键节点说明:

  • nodeId: 138, fieldName: value → 6 段式提示词
  • nodeId: 137, fieldName: image → 参考图(可以是图生图的输出,也可以是尾帧)

图生视频一般 4-5 分钟出结果,消耗约 49-52 RH 币。


第三步(核心):首尾帧链式生成

为什么需要这一步?

如果你直接用同一张静态参考图分别生成两段视频,虽然人物一样,但每段视频的起始姿态、光线、背景位置都会不同。拼接在一起时会出现明显的「跳切」——上一个镜头人往左看,下一个镜头人突然往右看,观感非常割裂。

解决思路

第1段视频最后一帧的画面状态
    ↓
作为第2段视频的输入参考图
    ↓
第2段从这个姿态「接着演」

这样两段视频之间的人物姿态、表情、光线角度都是连续的,拼接后观感就像一镜到底。

具体操作

1) 用 ffmpeg 提取第1段视频的最后一帧

# 方法一:用 -sseof 参数从末尾往前seek
ffmpeg -sseof -1 -i scene1.mp4 \
  -vframes 1 -q:v 2 \
  last_frame.jpg -y

# 方法二:已知总帧数,直接定位(更精确)
ffmpeg -i scene1.mp4 \
  -vf "select=eq(n\,242)" \
  -vframes 1 last_frame.jpg -y

-sseof -1 表示从文件末尾往前偏移 1 秒,-vframes 1 只取 1 帧。-q:v 2 保证输出质量。

2) 上传尾帧到 RunningHub

curl -X POST 'https://www.runninghub.cn/openapi/v2/media/upload/binary' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -F 'file=@./last_frame.jpg'

3) 用尾帧作为参考图提交第2段视频

和第2步完全一样的 API 调用,唯一区别是把 nodeId: 137image 字段值换成尾帧的 fileName:

{
  "nodeId": "137",
  "fieldName": "image",
  "fieldValue": "openapi/尾帧的fileName.jpg"
}

提示词中可以加一句 seamlessly continuing from the previous shot 强调连贯性。

效果对比

方式衔接效果说明
同一张参考图❌ 跳切明显两段起始姿态不同,拼接后有断裂感
尾帧链式传递✅ 自然过渡第2段从第1段结束的画面状态开始,连贯

第四步:视频拼接(可选)

两段视频都生成后,可以用 ffmpeg 无损拼接:

# 创建文件列表
echo "file 'scene1.mp4'" > filelist.txt
echo "file 'scene2.mp4'" >> filelist.txt

# concat 拼接
ffmpeg -f concat -safe 0 -i filelist.txt \
  -c copy output_final.mp4 -y

-c copy 表示不重新编码,直接拷贝流,速度极快且无画质损失。前提是两段视频的编码参数(分辨率、帧率、编码格式)一致——使用同一个工作流生成的视频天然满足这个条件。


第五步:TTS 配音叠加(可选)

RunningHub 的图生视频工作流自带的音频效果有限。如果你想要清晰的中文人声配音,建议用 edge-tts 单独生成音频,再叠加到视频上:

# 生成 TTS 音频
edge-tts --voice zh-CN-YunxiNeural \
  --text "以前我以为长大能叱咤风云,长大之后才搞明白,我只能风吹雨打" \
  --write-media audio1.mp3

# 叠加到视频(替换原始音频)
ffmpeg -i scene1.mp4 -i audio1.mp3 \
  -c:v copy -c:a aac -map 0:v:0 -map 1:a:0 \
  scene1_with_voice.mp4 -y

如果音频和视频时长不完全匹配,可以用 -shortest 参数自动截取:

ffmpeg -i scene1.mp4 -i audio1.mp3 \
  -c:v copy -c:a aac -map 0:v:0 -map 1:a:0 \
  -shortest scene1_final.mp4 -y

完整流程代码(Python 脚本)

把整个流程串起来,核心代码长这样:

import requests
import subprocess
import time

API_KEY = "你的RunningHub API Key"
BASE_URL = "https://www.runninghub.cn/openapi/v2"
HEADERS = {
    "Content-Type": "application/json",
    "Authorization": f"Bearer {API_KEY}"
}

# ---- 工具函数 ----

def upload_image(path):
    """上传图片到 RunningHub"""
    resp = requests.post(
        f"{BASE_URL}/media/upload/binary",
        headers={"Authorization": f"Bearer {API_KEY}"},
        files={"file": open(path, "rb")}
    )
    return resp.json()["data"]["fileName"]


def submit_task(app_id, node_info):
    """提交 ComfyUI 工作流任务"""
    resp = requests.post(
        f"{BASE_URL}/run/ai-app/{app_id}",
        headers=HEADERS,
        json={
            "nodeInfoList": node_info,
            "instanceType": "default",
            "usePersonalQueue": "false"
        }
    )
    return resp.json()["taskId"]


def poll_result(task_id, interval=30, timeout=600):
    """轮询任务结果,返回下载 URL"""
    start = time.time()
    while time.time() - start < timeout:
        resp = requests.post(
            f"{BASE_URL}/query",
            headers=HEADERS,
            json={"taskId": task_id}
        )
        data = resp.json()
        if data["status"] == "SUCCESS":
            return data["results"][0]["url"]
        elif data["status"] == "FAILED":
            raise Exception(f"Task failed: {data}")
        time.sleep(interval)
    raise TimeoutError("Task polling timeout")


def extract_last_frame(video_path, output_path):
    """用 ffmpeg 提取视频最后一帧"""
    subprocess.run([
        "ffmpeg", "-sseof", "-1", "-i", video_path,
        "-vframes", "1", "-q:v", "2", output_path, "-y"
    ], check=True, capture_output=True)


# ---- 主流程 ----

def main():
    # 1. 图生图:换装
    print("📷 Step 1: 图生图编辑...")
    original_fname = upload_image("./original.jpg")
    edit_task = submit_task(
        "2088926295186034689",  # Kera2Edit
        [
            {"nodeId": "160", "fieldName": "text",
             "fieldValue": "Remove camouflage...keep face..."},
            {"nodeId": "104", "fieldName": "image",
             "fieldValue": original_fname}
        ]
    )
    edit_url = poll_result(edit_task)
    # 下载编辑后的图片...

    # 2. 生成第1段视频
    print("🎬 Step 2: 生成第1段视频...")
    edited_fname = upload_image("./edited_photo.jpg")
    v1_task = submit_task(
        "2088844222551121921",  # AnimateDiff + MiniMax H3
        [
            {"nodeId": "138", "fieldName": "value",
             "fieldValue": "subject_definitions:..."},
            {"nodeId": "137", "fieldName": "image",
             "fieldValue": edited_fname}
        ]
    )
    v1_url = poll_result(v1_task)
    # 下载第1段视频...

    # 3. 提取尾帧 → 生成第2段视频(核心!)
    print("🔗 Step 3: 提取尾帧,链式生成第2段...")
    extract_last_frame("./scene1.mp4", "./last_frame.jpg")
    lf_fname = upload_image("./last_frame.jpg")
    v2_task = submit_task(
        "2088844222551121921",
        [
            {"nodeId": "138", "fieldName": "value",
             "fieldValue": "subject_definitions:...(续)"},
            {"nodeId": "137", "fieldName": "image",
             "fieldValue": lf_fname}  # 用尾帧!
        ]
    )
    v2_url = poll_result(v2_task)

    # 4. 拼接
    print("✂️ Step 4: 拼接视频...")
    with open("filelist.txt", "w") as f:
        f.write("file 'scene1.mp4'\n")
        f.write("file 'scene2.mp4'\n")
    subprocess.run([
        "ffmpeg", "-f", "concat", "-safe", "0",
        "-i", "filelist.txt", "-c", "copy",
        "output.mp4", "-y"
    ], check=True)

    print("🎉 Done!")


if __name__ == "__main__":
    main()

费用一览

环节工作流耗时RH 币消耗
图片上传-~2秒免费
图生图Kera2Edit60-90秒14-17 币
图生视频 ×2AnimateDiff + MiniMax H34-5分钟/段49-52 币/段
总计约12分钟约120 币

常见问题

Q: 两段视频的分辨率不一样怎么办?

同一个工作流生成的视频分辨率是固定的(本案例为 1152×640),不会有这个问题。如果混用不同工作流,需要在拼接前用 ffmpeg 统一分辨率:

ffmpeg -i input.mp4 -vf scale=1152:640 -c:a copy output.mp4 -y

Q: 尾帧提取出来是黑帧怎么办?

可能是因为视频末尾有黑场。用 -sseof -0.5 从末尾 0.5 秒处提取,避开黑场:

ffmpeg -sseof -0.5 -i scene1.mp4 -vframes 1 last_frame.jpg -y

Q: 并发提交被限流怎么办?

RunningHub 对同一 API Key 有并发限制(错误码 421)。解决方案:串行提交,等上一段完成后再提交下一段。正好我们的链式流程天然是串行的——必须等第1段完成才能提取尾帧。

Q: 提示词中能用英文吗?

可以。MiniMax H3 引擎对中英文都有不错的理解力。6 段式结构中的画面描述用英文、台词用中文(放在 <d>[Chinese]...</d> 标签中)是推荐的混合用法。


总结

整条链路的核心就三句话:

  1. 图生图解决「人不对」的问题——先把照片改成你想要的风格
  2. 图生视频解决「不动」的问题——让静态照片变成有表情有动作的视频
  3. 尾帧传递解决「不连贯」的问题——用上段结尾做下段开头,实现无缝衔接

这套流程不局限于口播视频。任何需要多段连续 AI 视频的场景——剧情短片、产品展示、教程录制——都可以复用这个「首尾帧链式生成」的思路。

关键就在那一帧:提取、上传、作为下一段的起点。一个小操作,换来的是观感上的质的飞跃。


本文基于 RunningHub ComfyUI 云端工作流实战总结,工作流 App ID 均为当前可用版本。API 文档详见 RunningHub 开放平台。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐