视频生成开放接口 · 对接文档
测试环境 快速联调信息
以下为测试渠道信息,仅用于对接自测;正式渠道请以我方单独下发为准。
测试请求地址
https://mfapi.mailxiuyun.com
复制
测试渠道 ID
CH_26954D42995D444C
复制
测试证书(RSA 公钥)
1
开始之前
接入前我方会向你提供:
- 请求地址;
- 渠道号(也叫渠道标识),形如
CH_xxxxxxxx,是你在系统中的唯一身份; - RSA 公钥证书(一个
.pem文件),用于对每一次请求做签名。
接入测试请使用上方提供的凭证进行联调,对接成功后切入生产环境之前再向我方索取正式参数,测试参数不可用于生产。
切生产环境之前,还需准备以下两点:
- IP加白:把你方生产端IP发给我们加白,测试环境无需IP白名单,生产环境必须加白IP,如有负载均衡,请上报全部IP
- 充值视频时长余量:切生产环境之前需联系我方加余量,否则无法调用。
2
请求鉴权
除健康检查外,所有接口都需要在请求头里带上三个字段。每次请求都要重新计算,不能复用。
| 请求头 | 说明 |
|---|---|
X-Channel-Id | 你的渠道号,例如 CH_xxxxxxxx |
X-Timestamp | 当前时间的 Unix 时间戳,单位为秒 |
X-Channel-Sign | 签名,见下方生成方式 |
另外提交任务时需要带 Content-Type: application/json。
签名怎么算
规则很简单,一句话概括:用我方给你的公钥,加密「渠道号 + 时间戳」这串文本,再做 Base64 编码,就是签名。
- 取当前时间戳(秒),比如
1752480000,这个值同时用于X-Timestamp。 - 把渠道号和时间戳直接拼接成一个字符串,中间没有任何分隔符。例如渠道号
CH_abc123+ 时间戳1752480000,拼出来就是CH_abc1231752480000。 - 用公钥对这个字符串做 RSA 加密,加密方案为 RSA/ECB/PKCS1Padding(即 PKCS#1 v1.5)。
- 对加密结果做 Base64 编码,得到的字符串放进
X-Channel-Sign。
⚠️
三个要点:① 时间戳有效窗口默认 ±300 秒,请确保调用服务器时间准确(建议开启 NTP 同步),否则会被判为过期;② 拼接顺序固定「渠道号在前、时间戳在后」,顺序错了签名过不了;③ 每次请求都要用当前时间戳重新生成签名,签名不可缓存复用。
Python 示例(pycryptodome)
pythonimport time, base64
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
CHANNEL_ID = "CH_abc123" # 我方提供的渠道号
PUBLIC_KEY_PATH = "channel_public.pem" # 我方提供的公钥证书路径
def build_auth_headers():
ts = str(int(time.time()))
plain = (CHANNEL_ID + ts).encode("utf-8")
with open(PUBLIC_KEY_PATH, "rb") as f:
public_key = RSA.import_key(f.read())
cipher = PKCS1_v1_5.new(public_key)
sign = base64.b64encode(cipher.encrypt(plain)).decode("utf-8")
return {
"X-Channel-Id": CHANNEL_ID,
"X-Timestamp": ts,
"X-Channel-Sign": sign,
}
💡
依赖安装:
pip install pycryptodomeJava 示例
javaimport javax.crypto.Cipher;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class ChannelSign {
// 我方提供的渠道号与公钥证书路径
static final String CHANNEL_ID = "CH_abc123";
static final String PUBLIC_KEY_PATH = "channel_public.pem";
// 生成鉴权请求头:X-Channel-Id / X-Timestamp / X-Channel-Sign
public static Map<String, String> authHeaders() throws Exception {
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String sign = sign(CHANNEL_ID, timestamp, PUBLIC_KEY_PATH);
Map<String, String> headers = new HashMap<>();
headers.put("X-Channel-Id", CHANNEL_ID);
headers.put("X-Timestamp", timestamp);
headers.put("X-Channel-Sign", sign);
return headers;
}
// 用公钥加密「渠道号 + 时间戳」,Base64 后即为签名
public static String sign(String channelId, String timestamp, String publicKeyPath) throws Exception {
String pem = new String(Files.readAllBytes(Paths.get(publicKeyPath)));
String base64 = pem.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
byte[] keyBytes = Base64.getDecoder().decode(base64);
PublicKey publicKey = KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(keyBytes));
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encrypted = cipher.doFinal((channelId + timestamp).getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(encrypted);
}
}
3
接口一:提交视频生成任务
POSThttps://<接口域名>/ttv/jobs
提交成功后接口立即返回任务 ID,视频在后台生成。请求体为 JSON
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
text | string | 是 | 视频文案内容,100~5000 字 |
rate | number | 是 | 语速倍率,仅支持 0.8 / 1.0 / 1.2 / 1.5 四档 |
subtitle | boolean | 是 | 是否显示字幕,true 开启 / false 关闭 |
bgmMode | string | 是 | 背景音乐开关:llm 开启(系统智能选曲)/ none 关闭 |
bgmVolume | number | 是 | 背景音乐音量倍率,取值 0~1.0,推荐 0.7(即 70%) |
voice | string | 是 | 音色代号,取值见下方「开放音色」表 |
clientJobId | string | 否 | 你自己的业务单号,用于幂等:相同值重复提交返回同一任务,不会重复出片 |
- 语速请严格使用四个挡位之一,传其他值可能不生效。
- 背景音乐音量
0.7表示压到原音量的 70%,日常推荐 0.7 左右,你也可以按需要设一个自己的默认值。
开放音色
voice 参数请从下表选择,直接填「音色代号」列的值即可。
A 组 代号为数字
| 内部音色名 | 音色代号 |
|---|---|
| 沉稳青叔 男 | 603006 |
| 情感姐姐 女 | 602003 |
| 青年解说 男 | 502005 |
| 温柔女声 女 | 502001 |
| 知心暖男 男 | 603005 |
| 亲和女声 女 | 602005 |
| 随和沉稳 男 | 603003 |
| 活力女声 女 | 502003 |
| 阳光男孩 男 | 502006 |
| 客服姐姐 女 | 502004 |
| 阅读男声 男 | 101004 |
| 新闻女声 女 | 101011 |
| 磁性男声 男 | 101021 |
| 自然轻快 男 | 101030 |
B 组 代号为英文
| 内部音色名 | 音色代号 |
|---|---|
| 温柔女声 女 | Serena |
| 阳光男孩 男 | Ethan |
| 暴躁女声 女 | Vivian |
| 乖巧女声 女 | Mia |
| 新闻主持 男 | Neil |
| 讲解讲师 女 | Elias |
| 粤语阿强 男 | Rocky |
| 四川辣妹 女 | Sunny |
请求示例
json{
"text": "近日,我国新能源产业再传捷报。据最新数据显示,今年上半年新能源汽车销量同比增长超过35%。",
"rate": 1.0,
"subtitle": true,
"bgmMode": "llm",
"bgmVolume": 0.7,
"voice": "603006",
"clientJobId": "biz-20260714-0001"
}
响应示例
json{
"code": 0,
"ok": true,
"msg": "操作成功",
"data": {
"jobId": "ttv-3f9a1c2b-1752480000",
"status": "queued",
"clientJobId": "biz-20260714-0001"
}
}
data.jobId 是这条任务的唯一标识,后续查询进度都以它为准,请务必保存。status 为 queued 表示已进入排队。
4
接口二:查询任务状态
GEThttps://<接口域名>/ttv/jobs/{jobId}
把提交时拿到的 jobId 拼到路径上,请求头同样带上三件套鉴权字段。
响应字段
json{
"code": 0,
"ok": true,
"data": {
"jobId": "ttv-3f9a1c2b-1752480000",
"clientJobId": "biz-20260714-0001",
"status": "succeeded",
"progress": "完成",
"progressPct": 100,
"ossUrl": "https://oss.example.com/output/xxx.mp4",
"title": "视频标题",
"duration": 25.6,
"error": null,
"createdAt": 1752480000,
"finishedAt": 1752480180
}
}
| 字段 | 说明 |
|---|---|
status | 任务状态,取值见下 |
progress | 当前阶段的中文描述,如「配音」「合成」 |
progressPct | 进度百分比,0~100 |
ossUrl | 成片下载地址,仅在 status=succeeded 时有值,链接有效期5分钟 |
title | 系统自动生成的视频标题 |
duration | 成片时长(秒) |
error | 失败原因,仅在 status=failed 时有值 |
createdAt / finishedAt | 创建、完成时间(Unix 秒) |
状态说明
| status | 含义 |
|---|---|
| queued | 排队中 |
| running | 生成中 |
| uploading | 上传成片中 |
| succeeded | 已完成,可从 ossUrl 下载 |
| failed | 失败,原因见 error |
正常流转为:queued → running → uploading → succeeded;中途出错则转为 failed。
轮询建议
- 任务提交后,请由你方主动轮询查询接口获取进度,我方不会主动推送结果。
- 轮询不要过于频繁,建议每 10 秒查询一次,直到状态变为
succeeded或failed;请求过密可能触发限流。 - 一条视频的生成通常在数十秒到十几分钟之间,与文案长度、素材匹配等因素相关。
- 查询进度是获取任务结果的唯一方式,请在提交后持续轮询,直至拿到终态。
视频地址说明
- 视频生成成功后,链接地址有效期为5分钟。
- 请在视频生成成功后5分钟内通过我方提供的视频地址下载视频到你方资产库,超时视频将丢失。
5
错误处理
鉴权或参数有问题时,接口会返回对应的 HTTP 状态码,响应体形如:
json{ "code": 401, "ok": false, "msg": "RSA签名验证失败", "data": null }
| HTTP | 提示 | 排查方向 |
|---|---|---|
| 401 | 缺少渠道标识 / 渠道不存在 / 渠道已禁用 | 检查 X-Channel-Id 是否正确、渠道是否启用 |
| 401 | 缺少时间戳 / 时间戳已过期 | 检查服务器时间,确保与标准时间偏差在 300 秒内 |
| 401 | 缺少签名 / RSA签名验证失败 / RSA解密失败 | 检查签名的拼接顺序、加密方案、公钥是否用对 |
| 403 | IP不在白名单 | 用白名单内的服务器 IP 调用,或联系我方调整 |
| 403 | 余量小于10分钟,请及时充值 | 视频时长余量不足,联系我方充值 |
✅
业务成功时
code 为 0、ok 为 true;判断成功与否请以 ok 字段为准。6
完整调用示例(Python)
下面是一段可直接参考的端到端示例:提交任务,然后轮询到出片完成。
pythonimport time, base64, json, requests
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
BASE_URL = "https://<接口域名>"
CHANNEL_ID = "CH_abc123"
PUBLIC_KEY_PATH = "channel_public.pem"
with open(PUBLIC_KEY_PATH, "rb") as f:
PUBLIC_KEY = RSA.import_key(f.read())
def auth_headers():
ts = str(int(time.time()))
cipher = PKCS1_v1_5.new(PUBLIC_KEY)
sign = base64.b64encode(cipher.encrypt((CHANNEL_ID + ts).encode())).decode()
return {
"X-Channel-Id": CHANNEL_ID,
"X-Timestamp": ts,
"X-Channel-Sign": sign,
}
# 1. 提交任务
body = {
"text": "近日,我国新能源产业再传捷报,上半年新能源汽车销量同比增长超过35%。",
"rate": 1.0,
"subtitle": True,
"bgmMode": "llm",
"bgmVolume": 0.7,
"voice": "603006",
"clientJobId": "biz-20260714-0001",
}
headers = auth_headers()
headers["Content-Type"] = "application/json"
resp = requests.post(f"{BASE_URL}/ttv/jobs", data=json.dumps(body), headers=headers, timeout=30)
job_id = resp.json()["data"]["jobId"]
print("任务已提交:", job_id)
# 2. 主动轮询进度(建议间隔 10 秒,不要过于频繁)
while True:
time.sleep(10)
r = requests.get(f"{BASE_URL}/ttv/jobs/{job_id}", headers=auth_headers(), timeout=30)
data = r.json()["data"]
print(f"[{data['progressPct']}%] {data['status']} - {data.get('progress')}")
if data["status"] == "succeeded":
print("出片完成,下载地址:", data["ossUrl"])
break
if data["status"] == "failed":
print("出片失败:", data.get("error"))
break