#Python 与 Node.js 示例
以下示例会创建真实的视频任务。运行前确认模型、参考素材、参数与费用;只验证连接时,先调用
GET /v1/models,无需创建视频。
两个示例均遵循同一流程:只创建一次、立即保存 ID、每隔 5 秒查询、临时错误退避、成功后直接下载 url。
创建失败不自动重试;轮询到期保留 ID,不自动重新生成。
#准备环境变量
示例从进程环境读取配置,不会自动加载 .env。在 Bash 中设置:
export THQ_VIDEO_BASE_URL='https://new.thqllm.com'
read -r -s -p 'THQ Video API Key: ' THQ_VIDEO_API_KEY
export THQ_VIDEO_API_KEY
export THQ_VIDEO_MODEL='MODEL_ID_FROM_V1_MODELS'
export THQ_VIDEO_SECONDS='5'
export THQ_VIDEO_SIZE='16:9'
export THQ_VIDEO_RESOLUTION='720p'将模型 ID 替换为当前 Key 可用的实际 ID,其他参数按模型支持范围修改。
THQ_VIDEO_BASE_URL 使用不含 /v1 的服务根地址。Windows 可在 PowerShell 中使用 $env:变量名 = '值' 设置相同变量。
已有任务时,设置 THQ_VIDEO_TASK_ID 即可跳过创建,只查询和下载该任务:
export THQ_VIDEO_TASK_ID='COPY_THE_RETURNED_TASK_ID'#Python:创建、轮询与下载
安装依赖 python -m pip install requests,将代码保存为 thq-video.py,运行 python thq-video.py。
新任务 ID 会写入当前目录的 thq-video-task-id.txt,结果保存为 output.mp4。
import os
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from pathlib import Path
from urllib.parse import quote, urlparse
import requests
BASE = os.environ.get("THQ_VIDEO_BASE_URL", "https://new.thqllm.com").rstrip("/")
HEADERS = {"Authorization": "Bearer " + os.environ["THQ_VIDEO_API_KEY"]}
TRANSIENT = {429, 500, 502, 503, 504, 522}
task_id = os.environ.get("THQ_VIDEO_TASK_ID")
if not task_id:
payload = {
"model": os.environ["THQ_VIDEO_MODEL"],
"prompt": "雨后的城市街道,镜头缓慢前进,柔和自然光",
"seconds": os.environ["THQ_VIDEO_SECONDS"],
"size": os.environ["THQ_VIDEO_SIZE"],
"resolution": os.environ["THQ_VIDEO_RESOLUTION"],
# "images": ["https://media.example.com/reference.jpg"],
}
# 创建只发送一次;异常时先核对控制台,不能自动重新提交。
response = requests.post(
BASE + "/v1/videos", headers=HEADERS, json=payload, timeout=(15, 180)
)
response.raise_for_status()
task_id = response.json().get("id")
if not isinstance(task_id, str) or not task_id:
raise RuntimeError("创建响应缺少有效 id,请核对控制台任务记录")
print("已保存任务 ID:", task_id, flush=True)
Path("thq-video-task-id.txt").write_text(task_id, encoding="utf-8")
deadline = time.monotonic() + 60 * 60
while time.monotonic() < deadline:
time.sleep(5)
try:
response = requests.get(
BASE + "/v1/videos/" + quote(task_id, safe=""),
headers=HEADERS,
timeout=(15, 60),
)
except (requests.Timeout, requests.ConnectionError):
continue # 只重试查询,使用已保存的 ID。
if response.status_code in TRANSIENT:
value = response.headers.get("Retry-After", "10")
try:
delay = float(value)
except ValueError:
try:
delay = (parsedate_to_datetime(value) - datetime.now(timezone.utc)).total_seconds()
except (TypeError, ValueError, OverflowError):
delay = 10
time.sleep(min(max(5, delay), max(0, deadline - time.monotonic())))
continue
response.raise_for_status()
state = response.json()
status = str(state.get("status", "")).lower()
print("任务状态:", status, "进度:", state.get("progress"), flush=True)
if status in {"failed", "failure", "cancelled", "canceled"}:
raise RuntimeError(str(state.get("error") or state.get("fail_reason") or state))
if status in {"completed", "success", "succeeded"}:
url = state.get("url")
if not isinstance(url, str) or not url:
continue # 结果尚未就绪,继续查同一任务。
parsed = urlparse(url)
if parsed.scheme != "https" or not parsed.netloc:
raise RuntimeError("结果 URL 不是有效 HTTPS 地址,请核对任务响应")
# 媒体下载单独发起,不传 HEADERS,不调用 /content。
with requests.get(url, stream=True, timeout=(15, 180)) as media:
media.raise_for_status()
content_type = media.headers.get("Content-Type", "").lower().split(";")[0].strip()
if not (content_type.startswith("video/") or content_type == "application/octet-stream"):
raise RuntimeError("结果未返回视频文件,Content-Type: " + content_type)
with Path("output.mp4").open("wb") as output:
for chunk in media.iter_content(chunk_size=1024 * 1024):
output.write(chunk)
print("视频已保存为 output.mp4")
break
else:
raise TimeoutError("本地等待已到期,请继续查询原任务: " + task_id)下载发生异常时,保留已保存的 ID,再次设置 THQ_VIDEO_TASK_ID 运行即可查询原任务。
存储服务若返回 application/octet-stream,保存后还应确认文件能够作为视频正常播放。
#Node.js:创建、轮询与下载
使用 Node.js 20 或更高版本,无需额外依赖。保存为 thq-video.mjs,运行 node thq-video.mjs。
import { createWriteStream } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
const base = (process.env.THQ_VIDEO_BASE_URL || 'https://new.thqllm.com').replace(/\/+$/, '');
const key = process.env.THQ_VIDEO_API_KEY;
if (!key) throw new Error('请设置 THQ_VIDEO_API_KEY');
const headers = { Authorization: `Bearer ${key}` };
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const transient = new Set([429, 500, 502, 503, 504, 522]);
let id = process.env.THQ_VIDEO_TASK_ID;
if (!id) {
const {
THQ_VIDEO_MODEL: model,
THQ_VIDEO_SECONDS: seconds,
THQ_VIDEO_SIZE: size,
THQ_VIDEO_RESOLUTION: resolution,
} = process.env;
if (!model || !seconds || !size || !resolution) {
throw new Error('请设置模型、时长、画面比例和清晰度环境变量');
}
// 创建请求只发一次,不为超时或 5xx 配置自动重试。
const response = await fetch(`${base}/v1/videos`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ model, prompt: '雨后的街道,镜头缓慢前进', seconds, size, resolution }),
signal: AbortSignal.timeout(180_000),
});
if (!response.ok) throw new Error(`创建请求 HTTP ${response.status}: ${await response.text()}`);
id = (await response.json()).id;
if (typeof id !== 'string' || !id) throw new Error('响应缺少有效 id,请核对控制台任务记录');
}
console.log('已保存任务 ID:', id);
await writeFile('thq-video-task-id.txt', id, 'utf8');
const deadline = Date.now() + 60 * 60 * 1000;
let resultUrl;
while (Date.now() < deadline) {
await sleep(5000);
let response;
try {
response = await fetch(`${base}/v1/videos/${encodeURIComponent(id)}`, {
headers,
signal: AbortSignal.timeout(60_000),
});
} catch {
continue; // 网络异常只重试查询。
}
if (transient.has(response.status)) {
const value = response.headers.get('Retry-After');
await response.body?.cancel();
const delay =
value && /^\d+$/.test(value)
? Number(value) * 1000
: value
? Date.parse(value) - Date.now()
: 10_000;
await sleep(
Math.min(
Math.max(5000, Number.isFinite(delay) ? delay : 10_000),
Math.max(0, deadline - Date.now()),
),
);
continue;
}
if (!response.ok) throw new Error(`查询 HTTP ${response.status}: ${await response.text()}`);
const state = await response.json();
const status = String(state.status || '').toLowerCase();
console.log('任务状态:', status, '进度:', state.progress);
if (['failed', 'failure', 'cancelled', 'canceled'].includes(status)) {
throw new Error(JSON.stringify(state.error || state.fail_reason || state));
}
if (
['completed', 'success', 'succeeded'].includes(status) &&
typeof state.url === 'string' &&
state.url
) {
resultUrl = state.url;
break;
}
}
if (!resultUrl) throw new Error(`本地等待已到期,请继续查询原任务: ${id}`);
const parsedUrl = new URL(resultUrl);
if (parsedUrl.protocol !== 'https:') throw new Error('结果 URL 必须使用 HTTPS');
// 直接使用返回的 URL;不附带 API Key,不调用 /content。
const media = await fetch(resultUrl, { signal: AbortSignal.timeout(180_000) });
if (!media.ok) throw new Error(`下载 HTTP ${media.status}`);
const contentType = (media.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
if (
(!contentType.startsWith('video/') && contentType !== 'application/octet-stream') ||
!media.body
) {
throw new Error(`结果未返回视频文件,Content-Type: ${contentType}`);
}
await pipeline(Readable.fromWeb(media.body), createWriteStream('output.mp4'));
console.log('视频已保存为 output.mp4');生产应用应把任务 ID 与业务记录关联,提供恢复查询和错误展示。 需要参考图片或音视频时,在创建请求中添加素材字段,不要改变只创建一次与保留 ID 的流程。