HiAPI
博客

教程

在 HiAPI 上使用 Veo 3.1 Lite 图生视频:curl 与 Python 教程

2026年11月3日
午夜蓝底上铜色精细平行刻线由密到疏流动,抽象呼应图像转为视频的教程封面图。

veo-3.1-lite/image-to-video 能做什么

veo-3.1-lite/image-to-video 是一个首帧转视频模型。你给它一张静态图片,它就把这一帧动画化,生成一段短视频。你提供的图片不是风格参考,也不是模糊的灵感来源——它就是输出视频的字面开头帧,因此构图、主体位置和取景会直接延续到运动的最初几秒。

这一设计选择决定了你该如何看待输入。一张光线充足、清晰、裁剪得当的图片,会生成一段从你预期的位置开始的视频。而一张模糊或取景奇怪的图片,生成的视频开头也会模糊、取景奇怪,并且随着模型生成运动而进一步漂移。用好这个模型的大部分功夫都发生在你写任何代码之前。

在输出侧,模型返回的视频带有原生音频。这正是它区别于旧式图生视频流水线的地方——旧流程先生成无声画面,再另行拼接一条独立音轨。而在这里,音频作为同一任务的一部分生成,因此环境声与视觉场景保持连贯——无需手动混流,音效与其对应的运动之间也不会出现时间错位。

生成是异步的。你提交一个任务,拿到一个标识符,然后轮询直到任务完成。这是一个刻意的架构选择:视频生成耗时远超典型的 HTTP 请求超时,因此 API 将提交与完成解耦。本教程中的每个示例都遵循这种“先提交、后轮询”的形式。

简而言之:输入一张图,输出一段带音频的视频,通过异步任务交付。记住这个心智模型,API 的其余部分就都很直接了。

发送第一个请求之前

首次调用要成功,需要准备好三件事。

一个 API 密钥

对 HiAPI.ai 的每个请求都用 API 密钥进行身份认证。在账户仪表盘中创建一个,然后从环境变量加载,而不是直接粘贴到源文件里:

export HIAPI_API_KEY="your_api_key_here"

如果某个密钥曾被提交到仓库,请立即轮换。密钥替换成本很低,泄露代价却很高。

一张可访问的首帧图片

模型需要自行抓取图片,因此图片必须能通过 URL 访问——公共对象存储链接、CDN 路径,或任何不需要交互式认证的 HTTP 端点。带长有效期的签名 URL 没问题;需要登录会话才能访问的 URL 则不行。使用 PNG 或 JPEG 之类的常见格式,并选择合理的分辨率:一张清晰、尺寸适中的图片能给模型提供干净的细节,同时不浪费带宽。

如果你的源图片位于内部服务之后,请先上传到对象存储,再传入生成的 URL。这通常是首次尝试失败最常见的原因。

端点路径

该模型以 veo-3.1-lite/image-to-video 标识。把这个字符串当作随任务发送的模型标识符,而不要硬编码到十几个地方——把它放进一个常量,因为你之后很可能要在不同模型变体之间切换。

导出好密钥、拿到可抓取的图片 URL、并掌握模型标识符之后,你就准备好提交了。

一个可用的 curl 请求

提交调用会创建一个生成任务。一个完整、可运行的示例如下:

curl -X POST "https://api.hiapi.ai/v1/video/generations" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "veo-3.1-lite/image-to-video",
    "image": "https://your-bucket.example.com/first-frame.jpg",
    "prompt": "晨光在场景中缓缓移动,镜头缓慢推进",
    "duration": 5
  }'

逐项来看:

  • URL 是 HiAPI 上的生成端点。所有模型流量都经过这一个接口,因此之后切换模型只需改 model 字段,而不是改端点。
  • Authorization 头携带你的持有者令牌。$HIAPI_API_KEY 这种写法从环境变量读取,因此密钥永远不会出现在 shell 历史记录中。
  • model 标识 veo-3.1-lite/image-to-video。
  • image 是首帧 URL。正是这个字段让本次调用成为图生视频而非文生视频。
  • prompt 是可选的,但强烈推荐。它描述你想要的运动——镜头运动、光照变化、主体动作——而不是对静态画面的描述,因为模型已经能看到那张图。
  • duration 请求的视频时长(秒),受模型支持范围限制。

响应是一个包含 id 的任务对象。把它保存下来:

{
  "id": "job_abc123",
  "status": "queued",
  "model": "veo-3.1-lite/image-to-video"
}

状态为 queued 或 processing 是预期结果——你还没有拿到视频,只拿到了一个承诺。下一节介绍如何把承诺变成文件。

同样的调用用 Python 写

这个 curl 调用可以直接用 requests 翻译成 Python。下面这个版本把提交封装成一个小函数,方便在流水线中复用:

import os
import requests

API_KEY = os.environ["HIAPI_API_KEY"]
BASE_URL = "https://api.hiapi.ai/v1"
HEADERS = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}

def submit_video_job(image_url: str, prompt: str, duration: int = 5) -> str:
    payload = {
        "model": "veo-3.1-lite/image-to-video",
        "image": image_url,
        "prompt": prompt,
        "duration": duration,
    }
    response = requests.post(
        f"{BASE_URL}/video/generations",
        headers=HEADERS,
        json=payload,
        timeout=30,
    )
    response.raise_for_status()
    data = response.json()
    print(f"Submitted job {data['id']} ({data['status']})")
    return data["id"]

if __name__ == "__main__":
    job_id = submit_video_job(
        image_url="https://your-bucket.example.com/first-frame.jpg",
        prompt="晨光在场景中缓缓移动,镜头缓慢推进",
    )

有几个细节值得保留:

  • raise_for_status() 会把非 2xx 响应尽早变成异常。没有它,认证错误或校验错误看起来就像是一次成功但返回了意外响应体的调用。
  • 30 秒超时只适用于提交。提交应当很快返回;长时间的工作发生在服务端。如果提交本身就超时了,问题出在连通性或请求负载大小上,而不是生成速度。
  • 返回任务 id而不是整个响应,能让函数保持可组合——由调用方决定如何处理这个任务。

至此,这个函数可以不做修改地放进队列 worker、Flask 端点或笔记本单元格中。

轮询异步任务

视频生成需要数分钟,而不是数毫秒。以接近人类节奏的间隔轮询任务状态端点,并在任务到达终态时立即停止:

import time
import requests

def wait_for_video(job_id: str, timeout: int = 600, interval: int = 10) -> dict:
    deadline = time.time() + timeout
    url = f"{BASE_URL}/video/generations/{job_id}"
    while time.time() < deadline:
        response = requests.get(url, headers=HEADERS, timeout=30)
        response.raise_for_status()
        data = response.json()
        status = data["status"]
        print(f"Job {job_id}: {status}")
        if status == "completed":
            return data
        if status == "failed":
            raise RuntimeError(f"Generation failed: {data}")
        time.sleep(interval)
    raise TimeoutError(f"Job {job_id} did not finish within {timeout}s")

三个实践能让它在生产环境中可靠运行:

  1. 指数退避。从短间隔开始逐步增大,并设置一个合理的上限。首次轮询通常还是返回 queued,所以在最初几秒猛打端点毫无收益。
  2. 硬性超时。始终对总等待时间设上限。卡在 processing 的任务应当表现为可告警的错误,而不是无限循环。
  3. 持久化任务 id。在开始轮询前把它写入数据库。如果进程在等待中途崩溃,重启后可以继续轮询,而不必重新提交并为同一份工作付两次费。

只把 completed 和 failed 视为结束循环的状态。其他一切——queued、processing——都意味着继续等待。

再提醒一点:如果你在 Web 服务器上运行这段代码,不要在请求处理器内轮询。提交、把任务 id 返回给客户端,让后台 worker 完成等待。否则一次缓慢的生成会占用连接数分钟。

下载输出视频

任务完成时,状态响应中会包含成品视频的 URL。以二进制数据下载,并写入磁盘,使用合适的扩展名:

def download_video(job: dict, path: str = "output.mp4") -> str:
    video_url = job["output"]["url"]
    response = requests.get(video_url, headers=HEADERS, timeout=120, stream=True)
    response.raise_for_status()
    with open(path, "wb") as f:
        for chunk in response.iter_content(chunk_size=1 << 20):
            f.write(chunk)
    print(f"Saved {path}")
    return path

有几件事要做对:

  • 以二进制模式写入。用 "w" 而不是 "wb" 打开会静默损坏文件;下载看似成功,视频却无法播放。
  • 流式处理大文件。上面的 stream=True 加分块复制无论视频大小都能保持内存平稳。
  • 计算校验和,如果你要大规模归档输出。这只需一行代码,却能在截断的下载到达用户之前将其捕获。

关于音频:返回的视频已经包含其原生音轨。没有单独的音频文件需要获取,也没有混流步骤。如果你在下游重新封装文件——例如更改容器格式——请把音频流一并带上。常见错误是运行一个默认剥离音频的转换,然后纳闷音频去哪了。

最后,输出 URL 通常是有时效的。请尽快下载并保存到你自己的存储桶中,而不是存下 URL 几天后再去抓取;那时链接可能已经过期。

常见失败点

以下列出了在排查图生视频调用问题时耗费时间最多的错误:

图片 URL 不可访问

这是遥遥领先的最常见失败原因。模型在服务端抓取你的图片,因此在你已登录浏览器中可用的 URL 说明不了任何问题。提交任务之前,用一个不带认证的普通请求测试它。签名 URL 的有效期应显著长于生成时间。

看似模型错误的认证错误

缺失或格式错误的 Authorization 头通常会返回明确的 401,但偶尔开发者把初始化代码包在会吞掉状态码的逻辑里,然后报告“模型故障”。调试请求负载之前,先检查原始状态码。

轮询过于频繁或完全不轮询

每 100 毫秒轮询一次浪费配额,还可能触发限流。只轮询一次、看到 processing 就放弃,则意味着任务成功却无人知晓。请遵循上面的退避加超时模式。

把提交当作完成

提交成功返回的是任务,不是视频。从提交响应中读取 url 字段的代码什么也找不到。只有完成状态响应才携带输出。

提示词在描述图片

像“一只猫坐在窗台上”这样的提示词只是复述模型已经看到的内容。描述运动的提示词——“雨开始下时镜头缓缓拉远”——能产生更好的效果。描述变化,而非内容。

崩溃时丢失任务 id

如果进程在提交与完成之间死亡,未持久化的任务 id 意味着工作无法恢复。持久化它,并从 worker 异步轮询。小小的设计选择能避免最漫长的调试过程。

立即开始构建持久 AI 产物

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

阅读 API 文档