HiAPI
博客

教程

如何通过 HiAPI API 调用 hailuo-2.3-fast/image-to-video:curl 与 Python 实战

快速、低成本图生视频档位的第一个可运行请求——从模型 ID 到下载 MP4

2026年11月3日
扁平切面矿物剖面风格的16:9封面图,深青底色配青绿与孔雀石色几何平面。

hailuo-2.3-fast/image-to-video 能为你做什么

hailuo-2.3-fast/image-to-video 是 HiAPI 图生视频产品线中针对速度优化的档位。你给它一张静态画面,外加一段描述所需运动的文本提示词,它就会返回一段从该画面向前动起来的短视频片段。模型 ID 里的“fast”正是重点:这一档位用更低的延迟和更低的单次渲染成本,换取了重型视频模型的部分精细保真度,因此非常适合迭代和批量生产。

当你需要一个说得过去的第一版可运行片段,而不是一张“主视觉”成片时,就选这个模型:分镜预览、社交优先的循环视频、背景运动素材,或流水线中由重型模型负责最终渲染的草稿阶段。如果你的输出需要长时长镜头、复杂的镜头调度,或在许多帧之间严格保持身份一致性,就把这些任务交给更重的档位——同时保持相同的请求结构,这样切换只是改一下配置。

下面所有内容都围绕一个你真正能跑起来的请求展开。目标是让一个 MP4 落到磁盘上,而不是一张成功响应的截图。

请求一览

一次视频渲染需要几秒到几分钟,所以 API 是异步的:你提交一个任务,拿回一个任务标识符,然后轮询该任务,直到它报告完成并把输出交给你。这意味着每个集成都有两个阶段——提交和轮询——且只有提交阶段会因模型而异。

提交请求包含四个由你控制的要素:

  • 端点与方法 — 向 HiAPI 视频生成端点发起一个 POST 请求。
  • 认证头 — 你的密钥,形式为 Authorization: Bearer $HIAPI_API_KEY。把密钥放在环境变量里,绝不要内联进提交的代码。
  • 模型 ID — 字面字符串 hailuo-2.3-fast/image-to-video。正是它选择了档位。
  • 输入 — 一个描述运动的 prompt,以及一张首帧图像。图像以可公开访问的 URL 或 data URI 形式提供,取决于你的账户在该端点接受哪种形式;从 URL 开始最简单。JSON 请求体以 Content-Type: application/json 发送。

把这四个字段放在代码库中的一个地方——一个字典、一个配置文件、一个由环境变量驱动的构建器——这样以后切换到另一个 HiAPI 视频模型只需改一行,而不是重写。响应是一个小的 JSON 信封,其中任务标识符是你唯一需要持久化的字段;其余都是状态元数据。

可用的 curl 请求

这就是完整的提交步骤。把图像 URL 换成你的账户能访问的地址,并先导出你的密钥。

export HIAPI_API_KEY="your-key-here"

curl -sS -X POST "https://api.hiapi.ai/v1/video/generations" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "hailuo-2.3-fast/image-to-video",
    "prompt": "slow push in, gentle breeze moving the fabric, film grain",
    "image": "https://example.com/first-frame.jpg"
  }'

成功响应是包含任务标识符的 JSON,通常形如 {"id": "task_...", "status": "queued"}。信封字段的具体形式可能变化,所以要防御性解析:按键而不是按位置读取任务 ID,并把未知字段视为信息性内容。

如果在调试,加上 -i 查看 HTTP 状态行。401 几乎总是意味着认证头缺失,或者变量展开成了空字符串——在责怪请求体之前先运行 echo $HIAPI_API_KEY。400 通常指向请求体:图像 URL 不可达、缺少 model,或字段名与端点不匹配。

把返回的任务 ID 复制到 shell 变量中,这样后续步骤就无需重新输入:

TASK_ID=$(curl -sS -X POST "https://api.hiapi.ai/v1/video/generations" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"hailuo-2.3-fast/image-to-video","prompt":"slow push in","image":"https://example.com/first-frame.jpg"}' \
  | python -c 'import sys,json; print(json.load(sys.stdin)["id"])')
echo "$TASK_ID"

同样的请求,用 Python 写

Python 版本保持请求体与 curl 逐字节等价,所以当你追查 bug 时,curl 结果和 Python 结果可直接对比。

import os
import requests

API_BASE = "https://api.hiapi.ai/v1"
MODEL = "hailuo-2.3-fast/image-to-video"

def submit_video(prompt: str, image_url: str) -> str:
    resp = requests.post(
        f"{API_BASE}/video/generations",
        headers={"Authorization": f"Bearer {os.environ['HIAPI_API_KEY']}"},
        json={
            "model": MODEL,
            "prompt": prompt,
            "image": image_url,
        },
        timeout=60,
    )
    resp.raise_for_status()
    return resp.json()["id"]

这里有三个重要习惯。当变量缺失时,os.environ[...](而不是 .get)会大声失败,这正是你想要的失败方式。显式的 timeout 能阻止挂起的套接字无限期占用工作进程。而 raise_for_status() 会把 4xx 响应体变成可记录的异常,而不是下一行因 ID 缺失而静默抛出的 KeyError。

这个函数刻意只返回任务 ID。提交和等待是分离的关切;把它们混在一起,轮询循环就会变得难以测试。注意模型 ID 只出现一次,位于模块级——这是你迁移到另一个 HiAPI 视频模型时唯一需要改动的地方。

轮询任务,直到视频就绪

状态检查是向任务资源发起的 GET 请求。排队中或运行中的任务返回非终态状态;已完成的任务返回终态状态,并附带输出。

import time

def wait_for_video(task_id: str, timeout: float = 600.0) -> dict:
    deadline = time.monotonic() + timeout
    delay = 2.0
    url = f"{API_BASE}/video/generations/{task_id}"
    while True:
        r = requests.get(
            url,
            headers={"Authorization": f"Bearer {os.environ['HIAPI_API_KEY']}"},
            timeout=30,
        )
        r.raise_for_status()
        task = r.json()

        if task.get("status") == "succeeded":
            return task
        if task.get("status") in {"failed", "cancelled"}:
            raise RuntimeError(f"task {task_id} ended: {task}")

        if time.monotonic() >= deadline:
            raise TimeoutError(f"task {task_id} still {task.get('status')} after {timeout}s")

        time.sleep(delay)
        delay = min(delay * 1.5, 15.0)

这是一个重试循环,而不是固定睡眠。退避从两秒开始,逐步增长到十五秒的上限,这样短渲染能很快完成,长渲染也不会猛击端点。截止时间在睡眠前检查,所以总墙钟时间保持在时限内,而不会超出一个间隔。用 time.monotonic() 而不是 time.time(),这样时钟调整不会延长循环。

把 failed 和 cancelled 与“继续等待”区别对待:它们是终态但不成功。发生这种情况时记录整个任务对象——这是你唯一能获得的诊断信息,仅凭一个 ID 复现失败非常痛苦。把未知状态字符串视为非终态并继续轮询,这样新的中间状态就不会让你的工作进程崩溃。

下载与处理输出

完成的任务携带一个或多个输出 URL。获取字节并把它们写到你能控制的地方。

def download_video(task: dict, dest: str) -> str:
    url = task["output"][0]["url"] if isinstance(task.get("output"), list) else task["output"]["url"]
    with requests.get(url, stream=True, timeout=120) as r:
        r.raise_for_status()
        with open(dest, "wb") as f:
            for chunk in r.iter_content(chunk_size=1 << 20):
                f.write(chunk)
    return dest

流式下载,而不是调用 .content;一个视频可能大到整块缓存在内存中对于处理多个任务的工作进程来说很浪费。输出 URL 的具体形态可能因模型档位而略有不同,所以在索引前先探测是否为列表——上面的分支覆盖了两种情况。

重要规则:把返回的 URL 当作短时效句柄,而不是存储。任务一成功,就把文件复制到你自己的桶或本地路径,并从那里提供服务。把 URL 持久化到数据库、几天后再去获取,就是你得到 403 和空仪表盘的原因。对于对象存储,同样的流式字节循环可以直接喂给你的客户端上传方法——如果你的 SDK 接受类文件对象,就不需要中间临时文件。

实用技巧与常见陷阱

  • 用认证头,而不是查询参数。 密钥放在 Authorization: Bearer ... 中。粘贴进 URL 的密钥就是访问日志里的密钥。
  • 图像可达性比图像正确性更重要。 提交时的 400 远更常见的是 API 无法抓取的 URL——localhost、私有桶、防盗链主机、会过期的链接——而不是提示词有问题。在调试其他任何东西之前,先用全新的浏览器会话测试该 URL。
  • 请求体的内容类型。 用 Content-Type: application/json 发送 JSON。用 curl 提交时若忘记 -H,服务器可能根本不会解析你的请求体。
  • 轮询过紧。 没有睡眠的 while True 会让你被限流,也不会让渲染更快完成。退避加截止时间就是全部诀窍。
  • 哪里都没有超时。 在提交、轮询和下载调用上都设置超时。缺少超时会把一个慢速提供商变成你自己系统中的卡死队列。
  • 提示词描述运动,而不是描述画面。 描述帧与帧之间变化的东西——镜头运动、主体动作、环境元素——并保持简短。重述静态图像毫无增益。
  • 一份配置,多个模型。 把模型 ID、提示词和图像输入放在同一个载荷构建器中。从这一快速档位迁移到更重的档位就只需改一个字符串。

端到端验证设置的最快方式是:运行 curl 提交,把任务 ID 粘贴进轮询循环,并确认一个 MP4 落到磁盘上——一旦这一步跑通,后面就都是打包工作了。

立即开始构建持久 AI 产物

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

阅读 API 文档