教程
Happyhorse 1.1 文生视频 API:可运行的 curl 与 Python 教程
本页将带你走完用 happyhorse-1.1/text-to-video 生成视频的最小请求,说明每个输入字段实际接受什么值,以及两种等待结果的方式(轮询和回调)。以下所有内容均已对线上 API 实测,请求结构和错误消息均为原文照录。
你将构建什么
一个函数:接收文本提示词,返回可下载的 MP4 URL。模型为 happyhorse-1.1/text-to-video,通过统一的 /v1/tasks 端点提供服务。你提交任务,立即拿回一个 taskId,然后要么轮询直到完成,要么让 hiapi 把最终结果 POST 到你的 webhook。
前置条件:
- 一个 hiapi API key。新账户赠送 1 美元免费额度——在 API Keys 页面获取。
- 一种发起 HTTPS 请求的方式。无需安装 SDK。
不要把 key 放进客户端代码,也不要提交到 git。从环境变量读取:
export HIAPI_API_KEY="sk-..."
最小可用请求
唯一必填的输入字段是 prompt。其他所有字段都有默认值。
curl
curl -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "happyhorse-1.1/text-to-video",
"input": {
"prompt": "a calm ocean wave at sunset, slow motion, golden hour"
}
}'
响应:
{
"code": 200,
"data": { "taskId": "tk-hiapi-01KW23VDG2EZ20J7RQH8S67MNC" },
"message": "success"
}
这个 taskId 就是你操作该任务的凭据。请妥善保存。
Python(仅用标准库)
import os
import urllib.request
import json
API_KEY = os.environ["HIAPI_API_KEY"]
def submit_task(prompt: str, **input_overrides) -> str:
body = {
"model": "happyhorse-1.1/text-to-video",
"input": {"prompt": prompt, **input_overrides},
}
req = urllib.request.Request(
"https://api.hiapi.ai/v1/tasks",
method="POST",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
data=json.dumps(body).encode("utf-8"),
)
with urllib.request.urlopen(req) as resp:
payload = json.loads(resp.read())
if payload.get("code") != 200:
raise RuntimeError(payload)
return payload["data"]["taskId"]
task_id = submit_task("a calm ocean wave at sunset, slow motion, golden hour")
print(task_id) # e.g. tk-hiapi-01KW23VDG2EZ20J7RQH8S67MNC
完整参数表
参数结构小而严格——未知字段会被 400 拒绝,而不是被静默丢弃。以下是 API 实际接受的 input 内字段,直接取自校验错误信息:
| 字段 | 类型 | 是否必填 | 允许值 | 默认行为 |
|---|---|---|---|---|
prompt |
string | 是 | 任何描述场景的非空文本 | — |
aspect_ratio |
string | 否 | 16:9, 9:16, 3:4, 4:3, 4:5, 5:4, 1:1, 9:21, 21:9 |
模型默认值 |
resolution |
string | 否 | 720p, 1080p |
模型默认值 |
duration |
integer | 否 | 3 到 15(秒) | 模型默认值 |
看起来理所当然、但目前 不被 接受的字段:size、audio、seed、negative_prompt。发送其中任何一个都会返回 additional properties '<field>' not allowed。如果你用过其他 hiapi 模型——qwen-image-2.0 使用 size,z-image 使用 aspect_ratio——请注意 happyhorse-1.1/text-to-video 属于宽高比一族,而非显式尺寸一族。
一个用上所有支持参数的请求:
curl -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "happyhorse-1.1/text-to-video",
"input": {
"prompt": "a vintage paper plane gliding above neon Tokyo at night, cinematic, 24fps",
"aspect_ratio": "16:9",
"resolution": "1080p",
"duration": 8
}
}'
关于成本与时长
该模型按生成视频的秒数计费——当前费率见 定价页面。duration 是成本杠杆,resolution 不是。从 5 秒提到 15 秒会让费用翻三倍;而在撰写本文时,从 720p 换成 1080p 则不会。开发期间,默认使用 duration: 3 和较低分辨率;只在最终确认的提示词上才升到 1080p。
获取结果:轮询
取回输出最简单的方式是按 id GET 任务,直到 status 进入终态。
import time
import urllib.request
import json
def get_task(task_id: str) -> dict:
req = urllib.request.Request(
f"https://api.hiapi.ai/v1/tasks/{task_id}",
headers={"Authorization": f"Bearer {API_KEY}"},
)
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())["data"]
def wait_for_task(task_id: str, timeout_s: int = 600, poll_interval_s: int = 10) -> dict:
deadline = time.time() + timeout_s
while time.time() < deadline:
task = get_task(task_id)
status = task["status"]
if status == "success":
return task
if status == "fail":
raise RuntimeError(f"task failed: {task}")
time.sleep(poll_interval_s)
raise TimeoutError(f"task {task_id} still {status} after {timeout_s}s")
task = wait_for_task(submit_task("a quiet coffee shop on a rainy afternoon"))
video_url = task["output"][0]["url"]
print(video_url)
几个重要细节:
status依次经历queued → handling → archiving → success(或fail)。在success或fail之前的任何状态都不应视为终态。- 生成时间随
duration和当前队列深度增长。5 秒 720p 任务通常几分钟内完成;1080p 15 秒需要预留更多时间。 output[].url存于临时存储。请立即下载字节并自行重新托管;不要只存 URL 再提供给最终用户——它会过期。
import urllib.request
def download(video_url: str, out_path: str) -> None:
with urllib.request.urlopen(video_url) as resp, open(out_path, "wb") as f:
f.write(resp.read())
download(video_url, "ocean.mp4")
获取结果:回调(生产模式)
轮询适合脚本和 notebook,但在真实后端中,你不会希望一个 worker 为每个视频被占用几分钟。提交时传入一个 callback 对象,hiapi 会把完成的任务体恰好一次 POST 到你的 URL。
curl -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "happyhorse-1.1/text-to-video",
"input": {
"prompt": "an astronaut planting a flag on a glass beach",
"aspect_ratio": "16:9",
"resolution": "1080p",
"duration": 6
},
"callback": {
"url": "https://example.com/hooks/hiapi",
"when": "final"
}
}'
目前仅支持 when: "final"——发送其他值会得到:
{ "code": 400, "error_code": "INVALID_REQUEST",
"message": "invalid callback.when: only 'final' is supported" }
设计健壮的接收端
三条规则保证回调路径安全:
- 幂等性。 按
taskId持久化。如果你两次收到同一个回调——最终一定会,因为重试——重复处理必须是无操作。 - 把确认与处理解耦。 一旦把 payload 入队,就从 webhook 返回
200 OK。实际的下载 / 重传 / 写数据库放在后台任务中。慢 webhook 会招致超时和重复投递。 - 回退到轮询。 把你提交的每个
taskId连同created_at存入自己的数据库。运行一个清扫器,对任何超过约 30 分钟且尚无终态的行轮询/v1/tasks/{id}。回调是快路径,不是唯一路径。
一个最小的 FastAPI 接收端:
from fastapi import FastAPI, Request
app = FastAPI()
@app.post("/hooks/hiapi")
async def hiapi_callback(req: Request):
body = await req.json()
task = body.get("data", body) # callback wraps the same shape as GET
task_id = task["taskId"]
if task["status"] == "success":
url = task["output"][0]["url"]
# enqueue, don't download here
await queue.put({"task_id": task_id, "url": url})
return {"ok": True}
你实际会遇到的错误
错误信封的形状始终一致——{code, error_code, message, data}——因此你可以只写一个处理器。针对该模型值得特殊处理的几种:
| HTTP | error_code |
触发条件 | 修复方法 |
|---|---|---|---|
| 400 | INVALID_REQUEST |
未知字段、枚举值错误、duration 超出 3–15 |
查看 message,它会指出字段——例如 aspect_ratio: value must be one of '16:9'... |
| 401 | — | key 缺失或错误,或 permission_denied |
重新检查 Authorization: Bearer sk-... 请求头 |
| 402 | — | 余额不足以覆盖预估成本 | 在 hiapi.ai/en/pricing 充值后重试 |
| 429 | — | 你在猛打该端点 | 退避并使用抖动重试 |
一次真实的校验失败如下所示——可复制作为你的测试夹具:
{
"code": 400,
"data": null,
"error_code": "INVALID_REQUEST",
"message": "invalid input: aspect_ratio: value must be one of '16:9', '9:16', '3:4', '4:3', '4:5', '5:4', '1:1', '9:21', '21:9'; resolution: value must be one of '720p', '1080p'; duration: maximum: got 99, want 15"
}
注意每个字段都在同一个响应中报告。能本地校验就本地校验,但做不到时就依赖它。
hiapi 上的相关内容
- happyhorse-1.1/text-to-video 模型页面 —— 演练场、完整参数 UI 和实时定价
- happyhorse-1.1/image-to-video —— 同一模型家族,改为让静态图像动起来
- veo-3.1-fast/text-to-video 和 seedance-2.0 —— 同一
/v1/tasksAPI 上的替代文生视频模型 - 统一任务 API 参考 ——
POST /v1/tasks、GET /v1/tasks/:id、回调和错误信封的权威来源 - 浏览所有模型 和 定价
常见问题
happyhorse-1.1/text-to-video 会生成音频吗?
通过任务 API 不会——请求结构目前没有 audio 字段,发送它会返回 additional properties 'audio' not allowed。输出是无声 MP4。在下游叠加配乐。
竖屏 / 短视频可用哪些宽高比?
对于 TikTok 和 Reels 风格输出,9:16 是标准选择。API 还接受 4:5(Instagram 信息流竖屏)和 9:21(超长竖屏),如果你需要更窄的画幅。
最短和最长时长是多少?
duration 是 3 到 15 秒(含端点)的整数。发送 0 会看到 duration: minimum: got 0, want 3;发送 99 会看到 duration: maximum: got 99, want 15。选择能捕捉所需运动的最小值——时长就是你付费的部分。
能否传 seed 以获得可复现的输出?
该模型目前不支持。请求 {"input": {"prompt": "...", "seed": 42}} 会失败并返回 additional properties 'seed' not allowed。如果可复现性重要,记录完整请求体并用相同提示词重试;你会得到相似——但非逐像素相同——的结果。
支持 negative_prompt 吗?
不支持。通过在 prompt 本身添加约束来引导输出(例如 "...clean composition, no text, no watermarks")。
输出 URL 有效期多长?
默认套餐下任务的 storage 字段为 temp,output[].url 是短期有效的签名链接。任务一到 success 就下载文件并重新托管。不要存下原始 URL 以后再提供。
开发期间如何控制花费?
两个省钱习惯:锁定提示词前保持 duration: 3,分镜确认前以 resolution: "720p" 提交。只有最终渲染才升到 1080p 和更长时长。
轮询还是回调——该用哪个?
两者都用,按此顺序。先轮询,因为少一个活动部件。当你第一次拥有无法承受让 worker 阻塞在 wait_for_task 上的后端时,就转向回调。无论如何都保留轮询作为回退清扫器;回调是尽力而为,不是保证送达。
为什么我有额度调用却返回 402? API 会估算任务成本上限——时长 × 每秒价格——并预先预留。如果余额无法覆盖该估算,就立即返回 402。充值、重试,成功后只扣实际成本。