Replicate 是一个用 API 运行开源模型的平台:图像、语音、视频、语言模型都有,按运行时间付费。 这篇做
predict.py:调用官方模型(例如 FLUX.1 schnell 文生图),先用同步模式等最多 60 秒,没等到就轮询,成功后马上把输出文件下载到本地,并保存一份元数据 JSON。API 生成的文件 1 小时后会被删除,所以“马上下载”是必须的。
先看清成品
- 用无效 Token 真实调用:两个接口返回的 401 内容不同(一个说 Unauthenticated,一个说 Authentication required),脚本都能识别。
- 对照文档发现两处会让轮询停不下来的问题:创建预测页的示例写的是 "successful",而 HTTP 参考里的状态是 "succeeded";预测生命周期页还有一个 aborted 状态,HTTP 参考没列。两者都已处理。
- 8 项 pytest 通过(mock)。
合规提醒:只用 Replicate 官方 HTTP API 和你自己的 API Token(replicate.com/account/api-tokens),Token 只放服务端,不共享账号。按运行时间计费,价格以 replicate.com/pricing 为准,本文不写价格。每个模型有自己的许可证和使用限制(在模型页面查看),生成内容要遵守 Replicate 服务条款,不生成侵权或违法内容,不冒用真人肖像。
运行说明:我没有 Replicate Token,没有跑过真实预测(也就没有产生费用)。真实跑过的是:用无效 Token 调用官方 API(2026-10-21 21:09–21:40,UTC+8,返回原样贴在文中),以及全部本地代码。正常的预测返回来自我写的 mock,字段照官方 HTTP 参考里的 prediction 对象。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
predict.py |
创建预测、等待、下载输出、写元数据 |
input.json |
示例输入 |
test_predict.py |
8 项测试 |
需要准备哪些环境?
| 项目 | 说明 |
|---|---|
| Replicate 账号 | 注册后在 replicate.com/account/api-tokens 创建 Token(形如 r8_…) |
| Python | 3.9 以上,pip install requests pytest |
| 环境变量 | export REPLICATE_API_TOKEN=r8_... |
关键事实
| 事实 | 来源 |
|---|---|
官方模型用 POST /v1/models/{owner}/{name}/predictions;社区模型用 POST /v1/predictions 加 version |
官方 创建预测 |
请求头 Prefer: wait 进入同步模式,默认最多等 60 秒,wait=n 可设 1–60 秒;没等到就返回未完成的预测,用 urls.get 继续查询 |
同上 + HTTP 参考 |
| 状态:starting、processing、succeeded、failed、canceled;生命周期页另有 aborted(超过期限且尚未开始运行,不计费) | 官方 HTTP 参考、预测生命周期 |
| API 创建的预测,输出文件 1 小时后自动删除;文件域名是 replicate.delivery | 官方 输出文件 |
| 创建预测 600 次/分钟,其他接口 3000 次/分钟;超出返回 429 | 官方 频率限制 |
文件结构
85-replicate-A-src/
├── predict.py
├── input.json
└── test_predict.py
第 1 步:示例输入
{"prompt": "一只橘猫坐在窗台上看雨,水彩画风格", "num_outputs": 1, "aspect_ratio": "1:1", "output_format": "webp"}
第 2 步:脚本
"""用 Replicate HTTP API 跑一次官方模型预测,并在 1 小时过期前把输出文件存到本地。
只用官方端点:
POST /v1/models/{owner}/{name}/predictions (官方模型,带 Prefer: wait)
GET urls.get (没等到结果时轮询)
"""
import argparse, json, os, pathlib, sys, time
import requests
API = "https://api.replicate.com/v1"
UA = "binbho-tutorial/1.0"
# HTTP 参考和 webhook 文档只列了前三个;“预测生命周期”页还有 aborted(超过期限、尚未开始运行)
TERMINAL = {"succeeded", "failed", "canceled", "aborted"}
def session(token):
token = (token or "").strip()
if not token:
raise SystemExit("缺少 REPLICATE_API_TOKEN(replicate.com/account/api-tokens 创建)")
s = requests.Session()
s.headers.update({"Authorization": f"Bearer {token}", "User-Agent": UA})
return s
def check(r):
if r.status_code == 429:
raise RuntimeError(f"429 限流:{r.json().get('detail')}")
if r.status_code >= 400:
try:
d = r.json()
except ValueError:
d = {"detail": r.text[:200]}
raise RuntimeError(f"HTTP {r.status_code} {d.get('title', '')}: {d.get('detail')}")
return r.json()
def create(s, model, inp, wait=60):
r = s.post(f"{API}/models/{model}/predictions", json={"input": inp},
headers={"Prefer": f"wait={wait}"}, timeout=wait + 30)
return check(r)
def poll(s, pred, interval=2, max_wait=600, sleep=time.sleep):
waited = 0
while pred["status"] not in TERMINAL:
if waited >= max_wait:
raise TimeoutError(f"{max_wait}s 未完成:{pred['urls']['web']}")
sleep(interval); waited += interval
pred = check(s.get(pred["urls"]["get"], timeout=30))
return pred
def output_urls(output):
if output is None:
return []
if isinstance(output, str):
return [output] if output.startswith("https://") else []
if isinstance(output, list):
return [u for o in output for u in output_urls(o)]
if isinstance(output, dict):
return [u for o in output.values() for u in output_urls(o)]
return []
def save_outputs(s, pred, outdir):
out = pathlib.Path(outdir); out.mkdir(parents=True, exist_ok=True)
saved = []
for i, url in enumerate(output_urls(pred.get("output"))):
name = f"{pred['id']}_{i}{pathlib.Path(url.split('?')[0]).suffix or '.bin'}"
r = requests.get(url, timeout=120, headers={"User-Agent": UA}) # 文件链接不需要 token
r.raise_for_status()
(out / name).write_bytes(r.content)
saved.append(name)
meta = {k: pred.get(k) for k in ("id", "model", "version", "status", "input", "error", "metrics", "created_at", "completed_at")}
meta["web_url"] = pred.get("urls", {}).get("web")
meta["saved_files"] = saved
(out / f"{pred['id']}.json").write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
return saved
def main(argv=None):
ap = argparse.ArgumentParser()
ap.add_argument("model", help="例如 black-forest-labs/flux-schnell")
ap.add_argument("--input", required=True, help="JSON 字符串或 @文件")
ap.add_argument("--out", default="outputs")
a = ap.parse_args(argv)
inp = json.load(open(a.input[1:], encoding="utf-8")) if a.input.startswith("@") else json.loads(a.input)
s = session(os.environ.get("REPLICATE_API_TOKEN"))
try:
pred = create(s, a.model, inp)
print(f"预测 {pred['id']} 状态 {pred['status']} {pred['urls']['web']}")
pred = poll(s, pred)
except RuntimeError as e:
print(e, file=sys.stderr); return 1
if pred["status"] != "succeeded":
print(f"未成功:{pred['status']} {pred.get('error')}", file=sys.stderr); return 2
files = save_outputs(s, pred, a.out)
print(f"完成,用时 {pred.get('metrics', {}).get('predict_time')}s,已保存:{files or '(无文件输出)'}")
return 0
if __name__ == "__main__":
sys.exit(main())
第 3 步:测试(mock)
"""假 Replicate(我写的 mock,字段按官方 HTTP 参考文档的 prediction 对象)。"""
import json
import pytest
import predict as p
class R:
def __init__(self, code, body=None, content=b""):
self.status_code, self._b, self.content, self.text = code, body, content, json.dumps(body)
def json(self):
return self._b
def raise_for_status(self):
if self.status_code >= 400:
raise RuntimeError(self.status_code)
def pred(status, output=None):
return {"id": "abc", "model": "black-forest-labs/flux-schnell", "version": "v", "status": status,
"input": {"prompt": "x"}, "output": output, "error": None, "metrics": {"predict_time": 1.2},
"urls": {"get": "https://api.replicate.com/v1/predictions/abc", "web": "https://replicate.com/p/abc",
"cancel": "https://api.replicate.com/v1/predictions/abc/cancel"}}
class S:
def __init__(self, gets):
self.gets, self.headers = list(gets), {}
def get(self, url, timeout=None):
return self.gets.pop(0)
def test_poll_until_succeeded():
s = S([R(200, pred("processing")), R(200, pred("succeeded", ["https://replicate.delivery/x/out-0.webp"]))])
assert p.poll(s, pred("starting"), sleep=lambda x: None)["status"] == "succeeded"
def test_successful_is_not_terminal():
# 文档“创建预测”页的示例写的是 "successful",HTTP 参考里的状态是 "succeeded"
assert "successful" not in p.TERMINAL and "succeeded" in p.TERMINAL
def test_poll_timeout():
s = S([R(200, pred("processing"))] * 10)
with pytest.raises(TimeoutError):
p.poll(s, pred("starting"), interval=2, max_wait=4, sleep=lambda x: None)
def test_output_urls_shapes():
assert p.output_urls("https://replicate.delivery/a.png") == ["https://replicate.delivery/a.png"]
assert p.output_urls("Hello Alice") == []
assert p.output_urls(["https://a/1.webp", "https://a/2.webp"]) == ["https://a/1.webp", "https://a/2.webp"]
assert p.output_urls({"audio": "https://a/x.wav", "n": 3}) == ["https://a/x.wav"]
def test_save_outputs(tmp_path, monkeypatch):
monkeypatch.setattr(p.requests, "get", lambda url, timeout=None, headers=None: R(200, content=b"RIFF....WEBP"))
files = p.save_outputs(None, pred("succeeded", ["https://replicate.delivery/x/out-0.webp?sig=1"]), tmp_path)
assert files == ["abc_0.webp"] and (tmp_path / "abc_0.webp").read_bytes().startswith(b"RIFF")
meta = json.loads((tmp_path / "abc.json").read_text(encoding="utf-8"))
assert meta["web_url"] == "https://replicate.com/p/abc" and meta["saved_files"] == ["abc_0.webp"]
def test_errors():
with pytest.raises(RuntimeError, match="401"):
p.check(R(401, {"status": 401, "detail": "Invalid or expired API token.", "title": "Authentication required"}))
with pytest.raises(RuntimeError, match="resets"):
p.check(R(429, {"detail": "Request was throttled. Your rate limit resets in ~30s."}))
def test_token_required():
with pytest.raises(SystemExit):
p.session(" \n")
def test_aborted_stops_polling():
s = S([R(200, pred("aborted"))])
assert p.poll(s, pred("starting"), sleep=lambda x: None)["status"] == "aborted"
第 4 步:运行
# 真实请求(2026-10-21 21:09 UTC+8)
$ curl -s https://api.replicate.com/v1/account -H 'Authorization: Bearer r8_invalid'
{"title":"Unauthenticated","detail":"You did not pass a valid authentication token","status":401} ← HTTP 401
$ curl -s -X POST https://api.replicate.com/v1/models/black-forest-labs/flux-schnell/predictions -H 'Authorization: Bearer r8_invalid' -H 'Content-Type: application/json' -d '{"input":{"prompt":"x"}}'
{"status":401,"detail":"Invalid or expired API token.","title":"Authentication required"} ← HTTP 401
$ REPLICATE_API_TOKEN=r8_invalid_demo python predict.py black-forest-labs/flux-schnell --input @input.json # 21:17 UTC+8
HTTP 401 Authentication required: Invalid or expired API token.
$ echo $?
1
$ python -m pytest -q
8 passed
# 有 Token 时的预期输出格式(我没跑过):
预测 <id> 状态 succeeded https://replicate.com/p/<id>
完成,用时 <秒>s,已保存:['<id>_0.webp']
怎么检查结果?
outputs/<id>.json里有输入、状态、用时和网页链接,方便以后查是哪次生成的。- 在 replicate.com 的 dashboard 能看到这次预测和费用。
- 输入参数以模型页面的 API schema 为准;
input.json里的参数取自 Replicate 官方 llms.txt 中 flux-schnell 的示例。
调试示例
轮询停不下来的两个坑,都来自文档。
"successful" 还是 "succeeded"? 我先读的是“创建预测”页,它的同步模式响应示例是
"status": "successful"。但 HTTP 参考里状态列表是succeeded,webhook 页和输出示例也都是 succeeded。如果照第一个示例写while status != "successful",成功的预测会一直轮询到超时。我以 HTTP 参考为准,并加了测试test_successful_is_not_terminal把这一点固定下来。漏了 aborted。 我最初的终态集合是
{"succeeded", "failed", "canceled"},这是 HTTP 参考和 webhook 文档的写法。后来读“预测生命周期”页,发现还有aborted:预测设置了期限、但还没开始运行就超时,状态就是 aborted。没把它算作终态的话,轮询要等到 max_wait 才结束。已加入终态,并加了测试test_aborted_stops_polling。
另外,两个 401 的返回内容不同(见运行输出),所以脚本只看状态码和 detail 字段,不匹配 title 文本。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 401 | Token 错误或已删除 | 重新创建 Token |
| 422 | 输入参数不符合模型的 schema | 看模型页面的 API 说明 |
| 429 | 请求太快 | 见 C 篇的退避处理 |
| 下载链接打不开 | 超过 1 小时,文件已被删除 | 成功后立即下载(本脚本已这样做) |
模板规则
- 终态以 HTTP 参考为准,并包含 aborted。
- 成功后立即下载输出。
- 保存输入和预测 id。
- 不匹配错误文本,只看状态码。
项目资源
| 用途 | 地址 |
|---|---|
| Replicate 创建预测(官方) | https://replicate.com/docs/topics/predictions/create-a-prediction |
| Replicate HTTP API 参考(官方) | https://replicate.com/docs/reference/http |
| Replicate 预测生命周期(官方) | https://replicate.com/docs/topics/predictions/lifecycle |
| Replicate 定价 | https://replicate.com/pricing |
| Replicate 输出文件(官方) | https://replicate.com/docs/topics/predictions/output-files |
| FLUX.1 schnell 模型页 | https://replicate.com/black-forest-labs/flux-schnell |
| Replicate API Token 页面 | https://replicate.com/account/api-tokens |
先跑一次,看清耗时和费用,再写成批量任务。