做系列短视频的团队,比如品牌吉祥物、固定出镜的虚拟主播、连续几集的小剧情,最头疼的是同一个角色每条视频长得都不一样。纯文生视频每次都会重新"想象"一张脸;用首帧图生视频能锁住长相,但每条视频都要从同一张图、同一个构图开始,换个场景就得先另做一张首帧。
Muse Video 是 Meta 超级智能实验室(Meta Superintelligence Labs)的视频生成模型,生成 4–30 秒、720p 的横屏或竖屏视频,声音和画面一起生成;Muse Image 是同一实验室的图片模型 Muse Image 1.0。2026 年 7 月 7 日 Meta 发布了 Muse Image,同时放出 Muse Video 的早期预览,两者共用同一个预训练底座。Muse Video 除了首帧,还可以带最多 4 张参考图:模型保留图里的人物,但视频不从这些图开始,场景完全由提示词决定,正好对应上面的问题。
本文的做法是:先用 Muse Image 生成一张角色设定图,裁成"头像"和"三视图"两张参考图,再用 Muse Video 的参考图模式,批量生成同一个角色在不同场景里的竖屏短视频。
先说利益关系:文中用的接口是我们自己做的 apimodels.app,它与 Meta 没有隶属关系。
requests 和 Pillow;第四步检查音轨用到 ffprobe(随 FFmpeg 安装)。YOUR_API_KEY 占位。taskId,再用同一个地址带上 task_id 查询。图片走 POST /v1/images/generations,视频走 POST /v1/video/generations,鉴权放在 Authorization: Bearer 头里。Muse 能调的参数比多数模型少,而且不支持的参数不会被悄悄忽略,会在创建任务时直接返回 400、不扣费。动手前先把边界记住,能省掉很多来回。
Muse Video(模型名 muse-video):
negative_prompt、seed,声音也关不掉(传 generate_audio: false 返回 400)。没有运镜参数,运镜写在提示词里。Muse Image(模型名 muse-image):
n 只能是 1),约 250 万像素。尺寸只决定比例:1:1 出 1600×1600,3:2 出 1920×1280,16:9 出 2048×1152,9:16 出 1152×2048。传 1024x1024 也交回 1600×1600;传 3840x2160 这类超过约 2.6MP 的尺寸返回 400,没有 4K。aspect_ratio 可以是 auto(默认,由提示词决定形状),或 1:1、3:2、2:3、4:3、3:4、5:4、4:5、16:9、9:16、2:1、1:2、21:9、9:21。negative_prompt、seed;quality 可以传,但不起作用。output_format 可选 png 或 webp。两个模型都没有 seed,同一个请求每次的结果都不一样。业务上需要可复现输出的,要提前考虑这一点。
角色设定图把同一个角色的头像和全身三视图画在一张图里。同一次生成里的人物天然是同一个人,比分开生成几张、再从里面挑长得一致的要省事。这里用 2:1 的宽幅,左边放头像,右边放三视图(示例):
import time, requests
API_KEY = "YOUR_API_KEY"
H = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
IMG = "https://api.apimodels.app/v1/images/generations"
def wait(url, task_id, interval):
while True:
d = requests.get(url, headers=H, params={"task_id": task_id}).json()["data"]
if d["state"] == "completed":
return d
if d["state"] == "failed":
raise RuntimeError(d.get("failMsg") or "failed") # 失败不扣费
time.sleep(interval)
r = requests.post(IMG, headers=H, json={
"model": "muse-image",
"prompt": (
"Character reference sheet on a plain light grey background. "
"Left third: a head-and-shoulders portrait of a 25-year-old woman with a short black bob, "
"round glasses and a mustard yellow knit cardigan, facing the camera. "
"Right two thirds: a full-body turnaround of the same woman, front, side and back views standing in a row. "
"Even studio lighting, no text, no logos."
),
"aspect_ratio": "2:1",
})
body = r.json()
if body["code"] != 200:
raise SystemExit(body["msg"]) # 参数错误在这里返回,不建任务、不扣费
sheet = wait(IMG, body["data"]["taskId"], 5)
print(sheet["resultUrls"][0], sheet.get("images"))任务完成后,resultUrls[0] 是图片地址,images 里带实际宽高。我们测试时每张约 35–70 秒;只有交付了图片才扣费,每张 $0.02。
习惯用 OpenAI SDK 的话,也可以一次调用直接拿到图:传 size 且不带 callback_url 时,同一个响应里就返回图片,不用轮询。
from openai import OpenAI
client = OpenAI(api_key="YOUR_API_KEY", base_url="https://api.apimodels.app/v1")
img = client.images.generate(model="muse-image", prompt="...", size="2048x1152")
print(img.data[0].url)整张设定图直接当参考图也能用,但头像在宽幅图里只占一小块。我们的做法是把头像和三视图裁开,分成两张传,测试的几组里,人物的脸、发型和衣着在视频里都和参考图一致。下面按左三分之一切开,转成 base64 data URI,这样不用另找地方托管图片(示例):
import base64, io
from PIL import Image
raw = requests.get(sheet["resultUrls"][0]).content
im = Image.open(io.BytesIO(raw)).convert("RGB")
w, h = im.size
cut = w // 3
def to_data_uri(part):
buf = io.BytesIO()
part.save(buf, format="JPEG", quality=92)
return "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode()
refs = [
to_data_uri(im.crop((0, 0, cut, h))), # 参考图 1:头像
to_data_uri(im.crop((cut, 0, w, h))), # 参考图 2:三视图
]模型不会严格按"左三分之一"排版,正式跑批之前打开设定图看一眼切口位置,偏了就调整 cut。
参考图放在 images 数组里(写成 image_urls 或 reference_image_urls 也可以)。提示词开头点明"参考图里的这个人",再写新场景(示例):
VID = "https://api.apimodels.app/v1/video/generations"
scenes = {
"cafe": "sits by the window of a small cafe, wraps both hands around a latte, looks up at the camera and laughs",
"park": "walks through an autumn park, kicks up a few fallen leaves, then turns back toward the camera and waves",
"books": "pulls a book from a shelf in an indie bookstore, flips it open and holds it up to show the cover",
}
tasks = {}
for name, action in scenes.items():
r = requests.post(VID, headers=H, json={
"model": "muse-video",
"prompt": f"The same young woman from the reference images {action}. Handheld vertical shot, natural light.",
"images": refs, # 1–4 张参考图;数组哪怕只有一项,也按参考图处理
"duration": 10,
"aspect_ratio": "9:16", # 720x1280
# "callback_url": "https://your-domain.example/hooks/video",
})
body = r.json()
if body["code"] != 200:
print(name, body["msg"]) # 400 在这里返回,不建任务
continue
tasks[name] = body["data"]["taskId"]
print(tasks)写提示词的几条经验:
Avoid ... 这类句子里的每个词它都会读进去,里面的敏感词同样可能触发内容安全检查。duration 指定。 提示词里写了秒数,模型会照着去控时长,可能和 duration 对不上。视频任务按 pending → processing → completed / failed 推进。10 秒以内的片子通常 1–2 分钟出片,15–30 秒通常 3–7 分钟,每 10–15 秒查一次就够了。成片是 H.264 MP4、24 fps,文件保留 30 天,要长期保存需要自己转存。
import subprocess
for name, tid in tasks.items():
try:
d = wait(VID, tid, 15)
except RuntimeError as e:
print(name, "failed:", e)
continue
url = d["resultUrls"][0]
path = f"{name}.mp4"
with open(path, "wb") as f:
f.write(requests.get(url).content)
# 不是每条都有声音:用 ffprobe 看有没有音频流
out = subprocess.run(
["ffprobe", "-v", "error", "-select_streams", "a",
"-show_entries", "stream=index", "-of", "csv=p=0", path],
capture_output=True, text=True,
).stdout.strip()
print(name, url, "audio" if out else "NO AUDIO")之所以要查音轨:Meta 介绍 Muse Video 的声音和画面一起生成,但我们上线时测试的两条里有一条是无声的。流程里必须有声音的,逐条检查,没有声音就重试或者后期配乐。
批量任务也可以在创建时传 callback_url。任务到达 completed 或 failed 时,你会收到一个 POST;投递失败会在 30 分钟内按退避间隔重试,直到你的接口返回 2xx。回调里的视频地址放在 resultJson 这个字符串字段里,需要再 json.loads 一次。回调地址必须公网可达,填 localhost 或内网地址,创建时就会返回 400。
两种方式价格相同,区别在于视频从哪里开始:
image_url(也可以写成 first_frame_url、image、input_reference)。视频从这张图开始,提示词写接下来怎么动。适合手上已经有一张满意的画面、只想让它动起来。首帧图的横竖方向要和 aspect_ratio 一致。images(也可以写成 image_urls、reference_image_urls)。模型保留人物、主体或风格,但视频不从任何一张图开始。适合同一个角色要出现在很多不同场景里。两种不能放在同一个请求里,同时传会返回 400。最容易踩的坑是字段名:数组字段哪怕只放一张图,也一律按参考图处理;想要首帧效果,必须用单图字段。
Muse Video 每秒 $0.037,按请求里的 duration 计费。成片因为向上取档可能比请求的长,多出来的部分不收费。常用时长的价格:
请求 4 秒,出 5 秒的片、收 $0.148;请求 7 秒,出 8 秒的片、收 $0.259。三种输入方式(文生、首帧、参考图)同价。
本文这套流程:1 张设定图 $0.02,加 3 条 10 秒竖屏 3 × $0.37 = $1.11,合计 $1.13。之后同一个角色每多一条 10 秒视频,再加 $0.37,设定图可以一直复用。
失败的任务不扣费。每一单的实际扣费可以用 GET /v1/records/{task_id} 查,返回里的 credits 是实扣金额。这个查询本身不收费,适合逐单对账。
两个模型的成功率都不是 100%。请求通过参数校验、建好任务之后,仍然可能失败,原因写在 data.failMsg 里:
Avoid 句子里也一样。这类失败原样重试通常没用,要把提示词改得更平实,或者换一张参考图。实践中按"偶尔会失败"来设计流程:临时错误自动重试一两次,内容被拒的单子标记出来、人工改写提示词,不要假设第一次请求一定能拿到视频。
下面这些都在创建任务时返回,不建任务、不扣费:
muse-video supports 16:9 (1280x720) or 9:16 (720x1280) only — set aspect_ratio to 16:9 or 9:16:传了 1:1、1920x1080 等其他比例或尺寸,或者 aspect_ratio 和 size 不一致。muse-video outputs 720p only (resolution: 720p):resolution 传了 1080p 等其他值。去掉它,或者传 720p。duration must be a whole number of seconds from 4 to 30:时长不在 4–30 之间,或者带了小数。muse-video takes either a first frame (image_url) or reference images (images), not both:首帧和参考图同时传了,二选一。muse-video takes at most 4 reference images (images / image_urls):参考图超过 4 张,挑最能代表这个角色的 4 张。muse-video cannot turn sound off: the model decides whether a clip has sound:传了 generate_audio: false。需要无声视频,就在后期去掉音轨。muse-image generates one image per request (got n=2); submit one request per image:图片的 n 大于 1。要多张就并行发多个请求。muse-image does not support masks (mask): describe what to change in prompt instead:Muse Image 没有局部重绘,去掉蒙版,把要改的地方写进提示词。seed。apimodels.app 是一个多模型 API 网关:用一个 API key、一个 OpenAI 兼容端点调用图像、视频、语音和语言模型。在上面的流程里,Muse Image 和 Muse Video 用的是同一个 key、同一套异步任务格式,上文的 wait 函数图片和视频通用。
它在这套流程里做的几件具体的事:
如果一共只做几条视频,在网页上手动生成就够了,没必要专门接接口。
你在做系列短视频时,角色一致性是靠首帧、参考图,还是靠一段固定的人物描述?欢迎在评论区聊聊各自的做法。
接口文档:apimodels.app 的 Muse Video 接口文档;apimodels.app 的 Muse Image 接口文档。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。