通过 Interactions API 接入视频模型
视频示例
本示例为所选模型 inferway/minimax-h3-768p 的 Interactions API 流程:后台创建、轮询、下载。
前置条件:bash、curl、jq;Python 示例只用标准库;TypeScript 示例需要带 fetch 的 Node。先设置 INFERWAY_API_KEY。上次没等完的任务,带上同一个 INFERWAY_IDEMPOTENCY_KEY 重跑就能接着等;最长等多久由 INFERWAY_MAX_WAIT_SECONDS 决定,默认 7200 秒(2 小时)。
- 创建:向 /interactions 发送顶层 JSON 体(op=create,mode=background,duration_seconds 与 prompt),并带上持久的 Idempotency-Key。
- 轮询:拿返回的 interaction_id 用 op=get 每 15 秒查一次。只要任务还在排队或生成,就一直等,每次会显示前面还有几单、或者已经生成了多少秒;遇到 failed、cancelled、expired、recovery_required 就停下报错。
- 下载:成功且 result.download_url 非空后,用一次不带 API 头部的独立 HTTPS 请求下载,不跟随重定向。
等到上限时,任务多半还在排队或生成,并没有取消,成片后才扣费,所以不要重新下单。记下 interaction ID 和 Idempotency-Key,用 INFERWAY_IDEMPOTENCY_KEY=<key> 重跑,接回的还是原来那个任务。下载链接过期了,再调一次 get 就能拿到新的。
隐私层级:retained_7d。视频产物按所选模型的策略保存,并非聊天那样的零保留。
#!/usr/bin/env bash
# Inferway video generation via the Interactions API.
# Prerequisites: bash, curl, jq.
# Required env: INFERWAY_API_KEY.
# Optional env: INFERWAY_IDEMPOTENCY_KEY — resume a previous create with this
# key; otherwise a fresh key is generated.
# Optional env: INFERWAY_MAX_WAIT_SECONDS — total seconds to wait on a
# queued/running task (default 7200 = 2 hours, positive integer).
# On success the video is saved to ./inferway-video.mp4.
set -u
API_BASE="https://api.inferway.ai/v1"
MODEL="inferway/minimax-h3-768p"
PROMPT="A drone shot along a coastline at dawn."
DURATION_SECONDS=5
SAVE_PATH=inferway-video.mp4
workdir=$(mktemp -d) || exit 1
cleanup() { rm -rf "$workdir"; }
trap cleanup EXIT HUP INT TERM
if [ -z "${INFERWAY_API_KEY:-}" ]; then
echo "error: INFERWAY_API_KEY is required" >&2
exit 1
fi
# The total wait budget, validated BEFORE the first request: a typo must not
# turn into a run that waits on the default budget.
max_wait_raw=$(printf '%s' "${INFERWAY_MAX_WAIT_SECONDS-}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
max_wait=7200
if [ -n "$max_wait_raw" ]; then
case "$max_wait_raw" in
*[!0-9]*)
echo "error: INFERWAY_MAX_WAIT_SECONDS must be a positive integer" >&2
exit 1
;;
esac
if [ -z "$(printf '%s' "$max_wait_raw" | sed 's/^0*//')" ]; then
echo "error: INFERWAY_MAX_WAIT_SECONDS must be a positive integer" >&2
exit 1
fi
max_wait=$max_wait_raw
fi
idempotency_key="${INFERWAY_IDEMPOTENCY_KEY:-}"
if [ -z "$idempotency_key" ]; then
if command -v uuidgen >/dev/null 2>&1; then
idempotency_key=$(uuidgen | tr 'A-Z' 'a-z') || exit 1
elif command -v uuid >/dev/null 2>&1; then
idempotency_key=$(uuid | tr -d '-') || exit 1
else
idempotency_key="iv-$(date +%s%N)-$$"
fi
fi
echo "Idempotency-Key: $idempotency_key"
interactions_url="$API_BASE/interactions"
response_headers=$workdir/headers
response_body=$workdir/body
create_body_file=$workdir/create-body.json
jq -cn \
--arg model "$MODEL" \
--argjson durationSeconds "$DURATION_SECONDS" \
--arg prompt "$PROMPT" \
'{op: "create", model: $model, mode: "background", duration_seconds: $durationSeconds, prompt: $prompt}' \
> "$create_body_file" || exit 1
attempt=0
while :; do
attempt=$((attempt + 1))
rc=0
curl -sS --connect-timeout 30 --max-time 30 \
-D "$response_headers" \
--data-binary @"$create_body_file" \
-H "Authorization: Bearer $INFERWAY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $idempotency_key" \
"$interactions_url" > "$response_body" 2> "$workdir/curl-err" || rc=$?
if [ "$rc" -ne 0 ]; then
if [ "$attempt" -ge 5 ]; then
echo "error: create request failed on the 5th attempt: $(cat "$workdir/curl-err")" >&2
exit 1
fi
sleep $((1 << (attempt - 1)))
continue
fi
status=$(head -n 1 "$response_headers" | tr -d '\r' | awk '{print $2}')
case "$status" in
200|202)
id=$(jq -er '.id | select(type == "string" and length > 0)' "$response_body" 2>/dev/null) || {
echo "error: accepted response has no usable id" >&2
exit 1
}
echo "task id: $id"
break
;;
429|5[0-9][0-9])
if jq -e '((.error? // {}) | (type == "object") and .retryable == false) or (.retryable? == false)' "$response_body" >/dev/null 2>&1; then
echo "error: server reported a non-retryable failure (status $status)" >&2
jq -c . "$response_body" >&2 || true
exit 1
fi
if [ "$attempt" -ge 5 ]; then
echo "error: create request failed with status $status on the 5th attempt" >&2
exit 1
fi
retry_after=$(sed -n '1,/^$/s/^Retry-After:[[:space:]]*//Ip' "$response_headers" | head -n 1 | tr -d '\r' | tr -d '[:space:]')
if printf '%s' "${retry_after:-}" | grep -Eq '^[0-9]+$'; then
if [ "$retry_after" -gt 30 ]; then
echo "error: server asked to wait $retry_after s (ceiling 30 s); resume later with:" >&2
echo " INFERWAY_IDEMPOTENCY_KEY=$idempotency_key" >&2
exit 1
else
sleep "$retry_after"
fi
else
sleep $((1 << (attempt - 1)))
fi
continue
;;
*)
if jq -e '((.error? // {}) | (type == "object") and .retryable == false) or (.retryable? == false)' "$response_body" >/dev/null 2>&1; then
echo "error: server reported a non-retryable failure (status $status)" >&2
else
echo "error: create request failed with status $status" >&2
fi
jq -c . "$response_body" >&2 || true
exit 1
;;
esac
done
# --- Wait on the queue: poll while the task is queued or running ------------
# Video is generated serially, so a task's queue wait is the sum of the
# generation seconds estimated from recent observed runs of every order ahead
# of it (see docs/interactions-queue-contract-v1.md,
# queue.estimated_wait_seconds). A fixed poll count therefore gives up on a
# perfectly healthy task, and a customer shown an error re-submits the same job
# and is charged twice. So this loop waits for as long as the task says queued
# or running, reports the queue position every round, and is bounded only in
# total by INFERWAY_MAX_WAIT_SECONDS (default 7200 s = 2 h). Failures of the
# status check itself are retried too; only a non-429 4xx answer stops the run
# at once.
download_url=""
waited=0
transient=0
poll_delay=15
stop_waiting() {
echo "Stopped waiting. The task was NOT cancelled: it stays in the queue and is charged only if it succeeds." >&2
echo "Do not submit it again. To resume, re-run this script with:" >&2
echo " INFERWAY_IDEMPOTENCY_KEY=$idempotency_key" >&2
exit 1
}
poll_delay_for() {
# 429 and 503 may name their own delay: honor a whole number of seconds up
# to 60; anything else (fraction, negative, > 60, absent) waits 15 s.
if [ "$1" != "429" ] && [ "$1" != "503" ]; then
echo 15
return
fi
retry_after=$(sed -n '1,/^$/s/^Retry-After:[[:space:]]*//Ip' "$response_headers" | head -n 1 | tr -d '\r' | tr -d '[:space:]')
if printf '%s' "${retry_after:-}" | grep -Eq '^[0-9]+$' && [ "$retry_after" -le 60 ]; then
echo "$retry_after"
else
echo 15
fi
}
while :; do
: > "$response_headers"
rc=0
curl -sS --connect-timeout 30 --max-time 30 \
-D "$response_headers" \
--data-binary "$(jq -cn --arg id "$id" '{op: "get", interaction_id: $id}')" \
-H "Authorization: Bearer $INFERWAY_API_KEY" \
-H "Content-Type: application/json" \
"$interactions_url" > "$response_body" 2> "$workdir/curl-err" || rc=$?
status=$(head -n 1 "$response_headers" | tr -d '\r' | awk '{print $2}')
poll_delay=15
hard_stop=""
if [ "$rc" -ne 0 ]; then
# Network error or timeout: the next round gets to try again.
transient=$((transient + 1))
elif [[ ! "$status" =~ ^2[0-9][0-9]$ ]]; then
case "$status" in
429|5[0-9][0-9])
transient=$((transient + 1))
poll_delay=$(poll_delay_for "$status")
;;
*)
hard_stop="status=$status"
;;
esac
elif ! jq -e . "$response_body" >/dev/null 2>&1 || [ -z "$(jq -r '.state // empty' "$response_body")" ]; then
transient=$((transient + 1))
else
transient=0
state=$(jq -r '.state' "$response_body")
case "$state" in
failed|cancelled|expired|recovery_required)
echo "error: task $id ended in state '$state'" >&2
jq -r 'if ((.failure | type) == "object") and (.failure | has("charged")) then "charged: \(.failure.charged)" else empty end' "$response_body" >&2
exit 1
;;
queued)
jq -r 'if ((.queue | type) == "object")
and ((.queue.position | type) == "number")
and (.queue.position == (.queue.position | floor))
and (.queue.position >= 1)
and ((.queue.estimated_wait_seconds | type) == "number")
and (.queue.estimated_wait_seconds >= 0)
then "queued: position \(.queue.position), about \([1, ((.queue.estimated_wait_seconds / 60) | ceil)] | max) min to start"
else "queued" end' "$response_body"
;;
running)
jq -r 'if ((.progress | type) == "object")
and ((.progress.elapsed_seconds | type) == "number")
and (.progress.elapsed_seconds >= 0)
and ((.progress.typical_seconds | type) == "number")
and (.progress.typical_seconds >= 1)
then "running: \(.progress.elapsed_seconds)s elapsed (typically \(.progress.typical_seconds)s)"
else "running" end' "$response_body"
;;
succeeded)
candidate=$(jq -er '.result? | select(type == "object") | .download_url? | select(type == "string" and length > 0)' "$response_body" 2>/dev/null) || candidate=""
if [ -n "$candidate" ]; then
download_url="$candidate"
break
fi
;;
esac
fi
if [ -n "$hard_stop" ]; then
echo "polling stopped ($hard_stop); the job may still be running." >&2
echo "resume later with:" >&2
echo " task id: $id" >&2
echo " INFERWAY_IDEMPOTENCY_KEY=$idempotency_key" >&2
exit 1
fi
if [ "$transient" -ge 8 ]; then
echo "error: the status check failed 8 times in a row" >&2
stop_waiting
fi
if [ "$((waited + poll_delay))" -gt "$max_wait" ]; then
stop_waiting
fi
sleep "$poll_delay"
waited=$((waited + poll_delay))
done
case "$download_url" in
https://*) ;;
*)
echo "error: refusing to download a non-HTTPS URL" >&2
exit 1
;;
esac
# --- Download: separate request, no API headers, no redirects ---------------
rc=0
curl -sS --connect-timeout 30 --max-time 30 \
-D "$response_headers" -o "$workdir/video" \
"$download_url" || rc=$?
if [ "$rc" -ne 0 ]; then
echo "error: download failed (network/timeout)" >&2
exit 1
fi
status=$(head -n 1 "$response_headers" | tr -d '\r' | awk '{print $2}')
case "$status" in
2??) ;;
3??)
echo "error: download redirected (status $status); refusing to follow redirects" >&2
exit 1
;;
*)
echo "error: download failed with status $status" >&2
exit 1
;;
esac
mv "$workdir/video" "$SAVE_PATH" || exit 1
echo "SAVED $SAVE_PATH"
用图片生成视频
两种用法:给一张首帧(可以再给一张尾帧),成片从首帧出发、落到尾帧;或者给 1–3 张参考图,成片参考这些图里的人物或场景。两者不能混用。用哪一种由你填了哪些图决定——不用单独传模式,mode 仍然是 background。
- 申请上传凭证:向 /interactions 发送 op=prepare_upload,写明 purpose(first_frame、last_frame 或 ref_image)、content_type 和 content_length(图片字节数),换回 upload_url——它是相对路径,拼到自己的 API 根地址后面用。每张图申请一次;凭证 15 分钟内有效,只能用一次。
- 上传原图:把图片的原始字节 PUT 到 upload_url,带上与申请时相同的 Authorization: Bearer 和你声明的 Content-Type。请求体就是原始字节,不是 multipart,也不是 base64;成功后返回 upload_id。
- 创建任务:把 upload_id 填进 first_frame(给了尾帧就再填 last_frame),或者填进 ref_image_1 到 ref_image_3,其余参数与文生视频完全相同。拿到 interaction id 之后,轮询与下载沿用上一节的示例。
图片限 JPEG、PNG、WebP,单张不超过 10 MiB。
计费与文生视频相同:按所选时长计价,下单时冻结额度,成片后才扣,失败不收费。
一个模型此刻开放哪些模式,看 GET /v1/models 里它的 available_modes(始终以 t2v 开头);不在列表里的模式会在下单时拒绝,发生在冻结额度之前,不产生费用。
前置条件与上一节相同:INFERWAY_API_KEY,外加 bash/curl/jq、Python 标准库,或带 fetch 的 Node。示例到拿到 interaction id 为止;之后的轮询与上一节相同:每 15 秒查一次,排队时会显示前面还有几单,最长等 2 小时,可用 INFERWAY_MAX_WAIT_SECONDS 调整。
示例代码加载中…