做 AI 视频的人常卡在同一个地方:文案能改写,但"这条片子到底是怎么拍出来的"说不清。镜头怎么走、光从哪来、色调偏冷还是偏暖、运动幅度多大——这些描述不出来,就没法让模型复现。
视频反推提示词这个接口解决的就是这一步:给一条短视频链接,返回一段可直接喂给视频生成模型的结构化提示词。
本文按 /doc/60 讲清楚三件事:它和"文案提取"的区别、计费里的预扣机制怎么绕开、以及创建、轮询、回调三套代码怎么写。
一、先分清两个容易混的接口
平台上有两个从视频里"取东西"的接口,很多人接错了才发现方向不对:
| 短视频文案提取 | 视频反推提示词 | |
|---|---|---|
| 路径 | /api/video/text |
/api/video/prompt |
| 输入 | 视频/音频链接 | 短视频分享链接或直链 |
| 输出 | 口播文案(文字内容) | 画面、镜头、光影描述(生成用提示词) |
| 用途 | 改写、二创脚本、知识库 | 复现画面、镜头语言拆解、跨模型迁移 |
| 计费 | 固定 10 点/次 | 按时长,10 点/分钟向上取整 |
一句话区分:一个取"说了什么",一个取"怎么拍的"。 做完整二创时两个都要——文案决定内容,提示词决定画面。
二、六个真正用得上的场景
| 场景 | 怎么用 | 产出 |
|---|---|---|
| 爆款复现 | 反推高赞视频的提示词,换成自己的产品主体重新生成 | 同款镜头语言的原创视频 |
| 镜头语言拆解 | 批量反推同类账号,统计高频镜头与光位描述 | 可复用的拍摄/生成模板库 |
| 跨模型迁移 | 把某平台验证过的效果,转成另一家视频模型的提示词 | 降低换模型的试错成本 |
| 素材库打标 | 给已有素材批量生成描述,做语义检索 | 可按"逆光""手持跟拍"检索的素材库 |
| 培训与教学 | 用真实视频反推,对照讲解镜头设计 | 可量化的教学案例 |
| 生成质量对齐 | 先反推参考片,再和自己的生成结果做提示词差异对比 | 定位"差在哪一层描述" |
一个实操建议:别把反推结果直接拿去生成。 它描述的是"原片有什么",而你要的是"我要什么"。正确做法是把结果拆成三段——主体、镜头运动、光影色调,保留镜头与光影,替换主体,这样既借了结构又不会撞车。
三、接口说明
| 项 | 值 |
|---|---|
| 地址 | https://xiaoapis.com/api/video/prompt |
| 方法 | 仅 POST(表单或 JSON 均可,参数放请求体) |
| 鉴权 | 请求参数 key |
| 模式 | 异步:创建返回 task_id,回调或轮询取结果 |
| 单条上限 | 10 分钟(600 秒) |
| 频率 | 10 QPS;免费/测试用户同档,每日 10000 次 |
| 查询计费 | 不扣费 |
3.1 请求参数
| 名称 | 必填 | 类型 | 说明 |
|---|---|---|---|
key |
是 | string | 平台 API 密钥 |
videoUrl |
创建时必填 | string | 分享链接、带链接的口令,或视频直链;兼容 url、content、link |
maxDuration |
否 | int | 最多处理多少秒,默认 600,最大 600;预扣按这个值算 |
notify |
否 | string | 完成回调地址,POST JSON;兼容 callbackUrl、callBackUrl |
task_id |
查询时必填 | string | 创建返回的任务号;兼容 taskId |
action |
否 | string | create(默认)或 query |
两个细节值得注意:
- 带链接的口令也支持。 不必自己先把口令解析成 URL,直接整段丢进
videoUrl即可——这点比同类接口省一步。 - 只传
task_id时按查询处理。 也就是说查询不强制带action,但显式传action="query"更清晰,避免以后加参数时语义漂移。
3.2 返回字段
创建返回:
| 名称 | 类型 | 说明 |
|---|---|---|
data.task_id |
string | 任务号,形如 vp_xxx |
data.status |
int | 0 处理中、1 成功、2 失败 |
data.schedule |
string | PROCESSING / SUCCESS / FAIL,与 status 同义 |
data.max_duration |
int | 本次最长处理秒数 |
data.power_prepaid |
number | 预扣点数 |
data.power_cost |
number | 预扣点数,完成后可能退回差额 |
查询返回:
| 名称 | 类型 | 说明 |
|---|---|---|
data.status |
int | 0 / 1 / 2 |
data.content |
string | 反推的提示词,成功时有值 |
data.duration |
int | 识别到的秒数;识别不到为 0 |
data.power_cost |
number | 处理中是预扣,结束后是实际扣点 |
data.fail_reason |
string | 失败原因 |
status 是 0/1/2 的数字,不是字符串。 判断成功要写 status == 1,别写 status == "success"——这是接这套接口最常见的低级错误,而且不会报错,只会一直"处理中"。
3.3 计费:预扣 + 多退少补
按实际时长结算:
| 视频时长 | 扣点 |
|---|---|
| ≤ 60 秒 | 10 点 |
| 61–120 秒 | 20 点 |
| 121–180 秒 | 30 点 |
| 600 秒(10 分钟上限) | 100 点 |
规则里有三条要记牢:
- 提交时按
maxDuration预扣。 不传就是默认 600 → 预扣 100 点。账户点数不足 100,创建会直接失败(code=400),哪怕你这条视频只有 15 秒。 - 完成后多退少补。 直链能读到时长时按真实时长结算,差额退回。
- 失败退回全部预扣;识别不到时长按 10 点结算。
所以第一条工程建议很具体:调用时一定显式传 maxDuration,并尽量贴近真实时长。 这既是省钱,也是让小账户能跑起来的前提。
四、代码一:创建 + 轮询
依赖只有 requests。轮询间隔按文档建议的 3~5 秒取 4 秒。
import os
import time
import requests
ENDPOINT = "https://xiaoapis.com/api/video/prompt"
API_KEY = os.environ["XIAOAPIS_API_KEY"]
# status 是数字:0 处理中 / 1 成功 / 2 失败
PROCESSING, SUCCESS, FAILED = 0, 1, 2
class PromptApiError(RuntimeError):
pass
def _post(payload: dict) -> dict:
body = {
"key": API_KEY, **payload}
response = requests.post(ENDPOINT, json=body, timeout=(10, 60))
try:
result = response.json()
except ValueError:
raise PromptApiError(f"HTTP {response.status_code}:{response.text[:200]}")
if result.get("code") != 200:
# 400 参数错误 / 点数不足 / 任务失败;403 认证失败;500 繁忙且不扣费
raise PromptApiError(f"[{result.get('code')}] {result.get('msg')}")
return result.get("data") or {
}
def infer_prompt(
video_url: str,
*,
max_duration: int = 60,
interval: float = 4.0,
total_timeout: float = 900.0,
) -> dict:
"""创建任务并轮询到终态,返回含 content / duration / power_cost 的 data。
max_duration 直接影响预扣点数:默认 600 会预扣 100 点,
短视频场景务必显式传小值。
"""
if not 1 <= max_duration <= 600:
raise ValueError("maxDuration 取值 1~600")
created = _post({
"videoUrl": video_url, "maxDuration": max_duration})
task_id = created.get("task_id")
if not task_id:
raise PromptApiError(f"创建未返回 task_id:{created}")
print(f"task_id={task_id} 预扣={created.get('power_prepaid')} 点")
deadline = time.monotonic() + total_timeout
while True:
if created.get("status") == SUCCESS:
return created
if created.get("status") == FAILED:
raise PromptApiError(f"任务失败:{created.get('fail_reason')}")
if time.monotonic() > deadline:
raise TimeoutError(f"task_id={task_id} 超过 {total_timeout}s 未完成")
time.sleep(interval)
# 查询不扣费;显式传 action 更清晰
created = _post({
"action": "query", "task_id": task_id})
if __name__ == "__main__":
data = infer_prompt("https://v.douyin.com/xxxxxxx/", max_duration=60)
print(f"时长:{data.get('duration')}s 实扣:{data.get('power_cost')} 点")
print(data.get("content"))
三个设计点:
- 默认
max_duration=60是刻意的。 大部分二创素材在一分钟内,按 60 秒预扣只要 10 点;默认 600 会占住 100 点。 content只在成功时有值。 不要在status=0时去读它,会拿到None。- 超时给 15 分钟。 10 分钟视频的反推不可能很快,超时设短了会把正常任务判死。
五、代码二:回调接收
如果服务已经有公网入口,走回调比轮询更省资源。注意文档写的是"只推一次"——所以回调必须落库,丢了就靠轮询兜底。
import pathlib
from fastapi import FastAPI, Request
app = FastAPI()
RESULT_DIR = pathlib.Path("./prompts")
# 生产环境换成数据库,task_id 建唯一索引
DONE: dict[str, dict] = {
}
@app.post("/callback/prompt")
async def prompt_callback(request: Request) -> dict:
body = await request.json()
data = body.get("data") or {
}
task_id = data.get("task_id")
if not task_id:
return {
"ok": True}
# 幂等:只推一次,但仍按重复推送处理更安全
if task_id in DONE:
return {
"ok": True}
if body.get("code") != 200:
DONE[task_id] = {
"status": "failed", "reason": data.get("fail_reason")}
return {
"ok": True}
RESULT_DIR.mkdir(parents=True, exist_ok=True)
target = RESULT_DIR / f"{task_id}.txt"
target.write_text(data.get("content") or "", encoding="utf-8")
DONE[task_id] = {
"status": "success",
"duration": data.get("duration"),
"power_cost": data.get("power_cost"),
"path": str(target),
}
return {
"ok": True}
回调里有两点和轮询不同:
- 回调的
code是 200 或 400,语义是"任务成功/失败",与 HTTP 状态码不是一回事。400 表示任务失败,不要当成参数错误去改请求。 - 回调只推一次,失败不重投。 所以"回调 + 定时扫表"这套组合是必需的,不能只依赖回调。
六、提示词拿到之后怎么用
content 是一段描述性文本,直接喂给生成模型往往效果一般。建议做三步加工:
- 拆结构。 把文本按"主体 / 镜头运动 / 光影色调 / 场景氛围"四段拆开,便于只换其中一段。
- 替换主体。 保留镜头与光影,把原片主体换成自己的产品。这是"借结构不撞车"的关键。
- 对齐目标模型。 不同视频模型对提示词的偏好不同:有的吃长句描述,有的吃关键词堆叠。同一段反推结果,需要按目标模型重排语序与删减冗余。
一个常被忽略的用法:把反推结果当"对照组"。 你自己写的提示词生成效果不理想时,把参考片反推一遍,逐段对比差在哪——很多时候问题不在文案,而在"镜头运动"这一段根本没写。
七、六个容易踩的坑
- 不传
maxDuration就预扣 100 点。 小账户可能直接创建失败(code=400,提示点数不足),误以为是接口坏了。 - 把
status当字符串判断。 它是0/1/2的数字,写成"success"会永远等不到结果。 - 用 GET 查询。 这个接口只接受 POST,查询也一样。
- 以为有
content就是成功。 个别失败场景可能有残留字段,判断条件应是status == 1。 - 忽略
duration=0。 识别不到时长时按 10 点结算,做成本核算时这类记录要单独标出来,否则均值会失真。 - 只靠回调。 回调只推一次,服务重启窗口内推送就丢了。必须留轮询兜底。
八、常见问题
Q1:它输出的是中文还是英文提示词?
文档没有约定输出语言,也没有语言参数。建议先用一条视频实测确认;如果要做多语言发布,拿到结果后自行翻译,别假设它跟随输入语言。
Q2:能处理超过 10 分钟的视频吗?
不能。单条上限 600 秒。长视频请先切片,只反推你要复现的那一段——反推整条长片既贵又没意义。
Q3:传口令行不行?
行。videoUrl 支持"带链接的口令",不用自己先解析成 URL。
Q4:预扣 100 点,但我这条只有 15 秒,会退吗?
会。完成后按实际时长结算,差额退回,15 秒最终扣 10 点。但预扣阶段账户必须够 100 点,否则创建就失败——这就是要显式传 maxDuration 的原因。
Q5:查询会不会重复扣费?
不会。文档明确查询不扣费,可以放心轮询,但仍要遵守 10 QPS。
Q6:和"短视频文案提取"能串起来用吗?
可以,而且推荐。文案提取拿口播内容、反推提示词拿画面描述,两者拼起来就是一份完整的二创简报:内容 + 镜头。
结语
这个接口的价值不在"生成",在"翻译"——把一条成片翻译成模型能听懂的语言。
落地顺序建议:先用一条 30 秒以内的公开视频跑通创建与轮询(maxDuration=60,成本 10 点),确认 status 与 content 的行为;再试一条 3 分钟视频,看轮询节奏和实际扣点;最后才接批量与回调。别一上来就默认参数跑,100 点一次的预扣会在你没反应过来时把余额吃掉。