wan-i2v-2 / api_client.py
IdlecloudX's picture
Unify job API result URLs and restore ranged downloads
71912ab verified
Raw History Blame Contribute Delete
4.53 kB
"""七个空间共用的可恢复任务客户端;仅 URL 和请求 JSON 随空间变化。"""
import argparse
import json
import os
import time
import uuid
from pathlib import Path
import httpx
def main():
"""提交或恢复任务,轮询终态后通过 result_url 保存结果。
Returns:
下载成功后结束;状态文件保留幂等键和任务 ID,便于断线后继续。
"""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--url", default="https://idlecloudx-moss-image-base.hf.space")
parser.add_argument("--request", type=Path, required=True)
parser.add_argument("--state", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--timeout", type=int, default=900)
args = parser.parse_args()
api_key = os.environ.get("JOB_API_KEY")
if not api_key:
raise SystemExit("请设置 JOB_API_KEY;HF_TOKEN 可选,游客无需设置。")
payload = json.loads(args.request.read_text(encoding="utf-8-sig"))
base_url = args.url.rstrip("/")
if args.state.exists():
state = json.loads(args.state.read_text(encoding="utf-8"))
if state["payload"] != payload or state["base_url"] != base_url:
raise SystemExit("状态文件属于另一请求,请为新任务使用新的 --state 路径。")
else:
state = {"base_url": base_url, "payload": payload, "idempotency_key": uuid.uuid4().hex}
# 先保存幂等键,提交响应丢失时仍能安全恢复同一个任务。
args.state.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
headers = {"X-API-Key": api_key, "Idempotency-Key": state["idempotency_key"]}
hf_token = os.environ.get("HF_TOKEN", "").strip()
if hf_token:
headers["Authorization"] = f"Bearer {hf_token}"
deadline = time.monotonic() + args.timeout
# 下载只使用 URL 自带的令牌,不把任务密钥或 HF_TOKEN 发送到结果地址。
with httpx.Client(headers=headers, timeout=30) as client, httpx.Client(timeout=120) as downloader:
while time.monotonic() < deadline:
downloading = "result_url" in state
try:
if downloading:
response = downloader.get(state["result_url"])
elif "job_id" not in state:
response = client.post(f"{base_url}/api/jobs", json=payload)
else:
response = client.get(f"{base_url}/api/jobs/{state['job_id']}")
is_json = "application/json" in response.headers.get("content-type", "")
# 业务失败是终态;只对限流、重启或传输故障重试。
if not downloading and is_json and response.status_code == 500:
failure = response.json()
if failure.get("status") == "failed":
raise SystemExit(json.dumps(failure["error"], ensure_ascii=False))
if response.status_code in {408, 429, 500, 502, 503, 504}:
time.sleep(min(30, max(2, int(response.headers.get("Retry-After", "5")))))
continue
response.raise_for_status()
# HF 启动等待页可能以 HTTP 200 返回 HTML。
if "text/html" in response.headers.get("content-type", ""):
time.sleep(5)
continue
except httpx.TransportError:
time.sleep(3)
continue
if downloading:
args.output.write_bytes(response.content)
print(f"已保存:{args.output.resolve()}")
return
body = response.json()
if "job_id" not in state:
state["job_id"] = body["job_id"]
print(f"任务已提交:{state['job_id']}", flush=True)
elif body["status"] in {"queued", "running"}:
time.sleep(int(response.headers.get("Retry-After", "2")))
continue
elif body["status"] == "failed":
raise SystemExit(json.dumps(body["error"], ensure_ascii=False))
else:
state["result_url"] = body["result_url"]
args.state.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
raise SystemExit("等待超时;用相同命令和状态文件继续,不会重复生成。")
if __name__ == "__main__":
main()