HiAPI
博客

教程

Happyhorse 图生视频 API:可用的 curl 和 Python 示例(hiapi)

2026年11月3日
Happyhorse 图生视频 API:可用的 curl 和 Python 示例(hiapi)

你想从自己的代码里把一张静态图片变成一段连贯的短视频。hiapi 上的 happyhorse-1.1/image-to-video 模型正是做这件事的:传入一个图片 URL 和一个可选的动态提示词,几秒钟内就会返回 720p 或 1080p 的 MP4。这份指南是最短的可用路径——真实的请求体、真实的错误信息,curl 和 Python 都有——这样你今天就能上线,而不用在字段名上折腾。

开始前你需要准备什么

  • 一个 hiapi API key。从 hiapi 控制台 获取——它看起来像 sk-...,放在 Authorization: Bearer ... 请求头里。
  • 一个可访问的图片 URL(公网 HTTPS)。本地文件不行;模型会自己去拉取这个 URL。
  • 用文字表达的运动意图(可选但推荐)。动词越清晰,运动效果越好。

模型按视频秒数计价,价格列在 hiapi 定价页;其余模型目录在 hiapi models——在提高批量之前,两处都看一下。

最小的可用请求(curl)

hiapi 上的每个视频都走异步的 /v1/tasks 端点:你先 POST 一个创建请求,拿回一个 taskId,然后要么轮询 GET /v1/tasks/{taskId},要么在完成时接收回调。

创建任务:

curl -X POST "https://api.hiapi.ai/v1/tasks" \
  -H "Authorization: Bearer sk-your-key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "happyhorse-1.1/image-to-video",
    "input": {
      "image_urls": ["https://example.com/your-still.jpg"],
      "prompt": "the cat slowly turns its head toward the camera and blinks",
      "resolution": "1080p",
      "duration": 5
    }
  }'

成功的响应长这样:

{
  "code": 200,
  "data": { "taskId": "tk-hiapi-01KW3D..." },
  "message": "success"
}

现在轮询结果(每 5 秒一次足够——大多数片段两分钟内完成):

curl "https://api.hiapi.ai/v1/tasks/tk-hiapi-01KW3D..." \
  -H "Authorization: Bearer sk-your-key"

运行期间你会得到 "status": "handling"。完成时你会得到:

{
  "code": 200,
  "data": {
    "status": "success",
    "model": "happyhorse-1.1/image-to-video",
    "output": [
      {
        "type": "video",
        "url": "https://temp.hiapi.ai/.../01KW....mp4",
        "expireAt": 1783131245
      }
    ],
    "taskId": "tk-hiapi-01KW3D..."
  }
}

那个 url 就是你要下载的——而且你应该立刻下载,因为 expireAt 是一个 Unix 时间戳,标记临时存储何时会删除它。

Python:轮询直到成功

这是一个完整、可运行的脚本——填入你的 key 和图片 URL 即可。

import os
import time
import requests

API = "https://api.hiapi.ai/v1/tasks"
TOKEN = os.environ["HIAPI_KEY"]  # sk-...

def create_video(image_url: str, prompt: str, duration: int = 5,
                 resolution: str = "1080p") -> str:
    """Submit an image-to-video task. Returns the taskId."""
    r = requests.post(
        API,
        headers={"Authorization": f"Bearer {TOKEN}",
                 "Content-Type": "application/json"},
        json={
            "model": "happyhorse-1.1/image-to-video",
            "input": {
                "image_urls": [image_url],          # exactly one URL
                "prompt": prompt,                   # optional
                "resolution": resolution,           # "720p" or "1080p"
                "duration": duration,               # int, 3..15
            },
        },
        timeout=60,
    )
    body = r.json()
    if body.get("code") != 200:
        raise RuntimeError(f"create failed: {body}")
    return body["data"]["taskId"]

def wait_for_video(task_id: str, timeout_s: int = 600,
                   poll_s: int = 5) -> str:
    """Block until the task succeeds, fails, or times out. Returns the MP4 URL."""
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        r = requests.get(
            f"{API}/{task_id}",
            headers={"Authorization": f"Bearer {TOKEN}"},
            timeout=30,
        )
        task = r.json().get("data") or {}
        status = task.get("status")
        if status == "success":
            return task["output"][0]["url"]
        if status == "fail":
            raise RuntimeError(f"task failed: {task.get('error')}")
        time.sleep(poll_s)
    raise TimeoutError(f"task {task_id} did not finish in {timeout_s}s")

def download(url: str, path: str) -> None:
    with requests.get(url, stream=True, timeout=120) as r:
        r.raise_for_status()
        with open(path, "wb") as f:
            for chunk in r.iter_content(1 << 16):
                f.write(chunk)

if __name__ == "__main__":
    task_id = create_video(
        image_url="https://example.com/your-still.jpg",
        prompt="the dancer spins once and ends in a pose",
        duration=5,
        resolution="1080p",
    )
    print("submitted:", task_id)
    mp4_url = wait_for_video(task_id)
    download(mp4_url, "out.mp4")
    print("saved out.mp4")

整个东西约 50 行,就是全部生产可用的接口面——没有 SDK,没有后台队列,没有流式协议。用 HIAPI_KEY=sk-... python video.py 运行它。

生产环境:用回调代替轮询

轮询对于一次性任务和测试没问题。对于服务器负载,你希望平台在片段就绪时回调你——省掉循环,无需额外工作即可扩展,而且请求流会立即返回。

在请求体的根层级(不是 input 内部)添加一个 callback 对象:

curl -X POST "https://api.hiapi.ai/v1/tasks" \
  -H "Authorization: Bearer sk-your-key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "happyhorse-1.1/image-to-video",
    "input": {
      "image_urls": ["https://example.com/still.jpg"],
      "prompt": "slow zoom out from the subject",
      "resolution": "1080p",
      "duration": 6
    },
    "callback": {
      "url": "https://your-app.example.com/webhooks/hiapi",
      "when": "final"
    }
  }'

当任务到达终态时,hiapi 会把你本可以轮询到的那个任务对象(status、output[].url、error 等)POST 到你的 URL。when: "final" 意味着你只会收到成功或失败的回调——没有进度噪音。

几条能省下真实调试时间的规则:

  • 让 webhook 幂等。 同一个 taskId 可能到达两次(网络重试是真实存在的)。处理前先在数据库里查一下这个任务。
  • 在处理函数内重新托管 MP4。 hiapi 给你的 URL 指向临时存储,带有一个 expireAt 字段;在第一次收到时就把它流式存入你自己的 bucket,否则你会丢失它。
  • 快速确认。 快速返回 2xx,然后异步做重活。慢 webhook 有重试风险。
  • 用轮询作为兜底。 把 taskId 包进一个延迟任务,10 分钟后再轮询,以防 webhook 丢失。如果你一直遇到这个问题,查看 hiapi 任务回调可能不触发的原因 了解常见原因。

你会遇到的错误信息

这些是 API 返回的确切字符串;在你的代码里匹配它们比猜测更可靠。

情况 HTTP 响应体形态
错误或已撤销的 key 401 {"error":{"code":"permission_denied","message":"This API key cannot use the selected model...","type":"hiapi_error"}}
缺少必填字段 400 {"code":400,"error_code":"INVALID_REQUEST","message":"invalid input: image_urls: missing required field \"image_urls\""}
resolution 无效 400 ... resolution: value must be one of '720p', '1080p'
时长超出范围 400 ... duration: minimum: got 2, want 3 / duration: maximum: got 30, want 15
image_urls 长度错误 400 ... image_urls: maxItems: got 2, want 1(或 minItems: got 0, want 1)
根层级或 input 内有未知字段 400 ... additional properties 'identity_preserving' not allowed
提交后任务本身失败 200(任务 GET)且 data.status == "fail" 以及 data.error.{code,message}

认证和验证错误意味着你不应该重试;其他所有情况(平台的 5xx、轮询途中的网络超时、带有可重试代码的暂时性 task fail)都适合做小幅退避重试。

调优输出

真正有影响的旋钮有四个——其他任何东西(seed、aspect_ratio、fps、cfg_scale、negative_prompt)目前都会作为未知字段被拒绝。请留在这个集合内:

  • image_urls——一个 HTTPS URL。输出的宽高比和构图跟随这张图片。
  • prompt——简短的动态描述("she smiles and tilts her head")。电影感的、以动词驱动的提示词胜过形容词列表。
  • resolution——"720p" 用于原型制作和缩略图,"1080p" 用于交付。
  • duration——整数秒,从 3 到 15。更长的片段花费更多时间和金钱;选择能讲清故事的最短时长。

需要起始帧和结束帧的项目,请参阅 happyhorse-1.1 reference-to-video。不需要源图片的纯文生视频,happyhorse-1.1 text-to-video 是姊妹篇。在 hiapi models catalogue 或 博客的配方栏目 浏览其余内容。

FAQ

我能否传两张图片来控制首帧和尾帧? 在 happyhorse-1.1/image-to-video 上不行——image_urls 最多一个元素(maxItems: 1)。如果你需要这个,使用 happyhorse-1.1/reference-to-video,或者用 happyhorse-1.1 reference-to-video 构建。

我实际拿回的 MP4 是什么分辨率? 输出遵循你输入图片的宽高比和你设置的 resolution 标志。"1080p" 给出两者中更高的那一个;"720p" 对测试来说更快更便宜。

为什么我的 key 在其他模型上能用,却得到 401 permission_denied? 有些模型是按 key 门控的。在 hiapi 控制台 查看模型页面——如果 happyhorse-1.1/image-to-video 不在你的允许列表里,生成一个新 key 或带上错误里的 request_id 联系支持。

有流式/SSE 响应吗? 没有。每个视频模型都使用本文中的异步任务模式。根据你是否控制服务器来选择轮询或回调。

我在哪里看到每个片段的实际成本? 在 hiapi 定价页。成本按视频秒数计,所以你传入的 duration 直接决定花费——把开发/测试片段保持在 duration: 3 以节省预算。

输出 URL 一天后就不能用了。 这是预期行为——该 URL 是临时存储,响应包含一个 expireAt Unix 时间戳。始终在任务成功的那一刻下载 MP4(或通过你自己的 CDN 流式传输)。

立即开始构建持久 AI 产物

获取 API 密钥,通过一个基于任务(task)的 API 调用图像、视频和音频模型。

阅读 API 文档