一个服务 · 七种能力 · N 个项目

把 AI 生成能力
收成一个接口

调用方传提示词和常用参数,不传模型。模型选择、供应商回退、重试退避、 并发闸、产出校验都在服务里 —— 那些是花过钱才知道的事, 不该让每个项目重踩一遍。

Base https://ai.nofuns.xyz
文生图POST /v1/text2image实测 34s → PNG 1024²
图生图POST /v1/image2image实测 44s → PNG 1254²
文生视频POST /v1/text2video实测 280s → 1.47MB MP4
图生视频POST /v1/image2video实测 180s → 0.09MB MP4
视频生文POST /v1/video2text实测 93s → 313 字
文生 3DPOST /v1/text2model实测 154s → 6.25MB GLB
图生 3DPOST /v1/image2model实测 131s → 11.2MB GLB

01 — 快速开始

三十秒

图片默认同步等,一次请求直接拿到产物地址。

curl -X POST https://ai.nofuns.xyz/v1/text2image \
  -H "Authorization: Bearer $AICAP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"一只戴宇航头盔的橘猫,扁平卡通风格","quality":"fast"}'
# 回来的东西
{
  "job_id": "j_70f06bacbc54",
  "status": "done",
  "artifacts": [{
    "id": "a_4f6a59d9ab40", "kind": "image",
    "mime": "image/png", "bytes": 319426,
    "url": "/v1/files/a_4f6a59d9ab40"
  }],
  "usage": {"calls": 1, "provider": "artsapi",
            "model": "gpt-image-2-low", "seconds": 74.2}
}
# clients/aicap.py 拷进项目就能用,零依赖
from aicap import AiCap

ai = AiCap("https://ai.nofuns.xyz", "tok_xxx")

png = ai.text2image("一只戴宇航头盔的橘猫,扁平卡通风格")[0]
open("cat.png", "wb").write(png)
// clients/aicap.ts 拷进项目就能用,零依赖
import { AiCap } from "./aicap";

const ai = new AiCap("https://ai.nofuns.xyz", process.env.AICAP_TOKEN!);

const [png] = await ai.text2image("一只戴宇航头盔的橘猫,扁平卡通风格");
别放进浏览器 令牌一旦进了前端就是别人拿你的账单出图。这个服务持有所有供应商的 key, SDK 只在服务端用。

三个参数所有能力都吃

参数说明
prompt 必填提示词。你想怎么写就怎么写,服务不改它。
qualityfast / standard(默认)/ hq —— 映射到不同的模型链
model可选,强制指定模型。不推荐:会绕过能力过滤,见「踩过的坑」
别自己猜能力 GET /v1/capabilities 会告诉你每个能力有哪些模型、 哪些是真验证过的、当前并发占用、哪些端点上限还没实测。 服务说得出来的事,不该让调用方去猜。

02 — 鉴权

一个项目一个令牌

所有 /v1/* 都要 Authorization: Bearer <令牌>/health 不要(给监控用)。

分开发令牌不是为了好看:N 个项目共用一份供应商额度, 不按项目记账就没法管谁把钱花光了 —— 这是第一天就会遇到的问题。 每个令牌能单独吊销、单独记账(GET /v1/usage)。

# 看自己项目最近 24 小时花了多少
curl -H "Authorization: Bearer $AICAP_TOKEN" \
  https://ai.nofuns.xyz/v1/usage?hours=24

返回里有一列 wasted_but_charged —— 图出了、钱收了、我们用不上 的次数。那是最该被看见的数字,藏起来的话这类浪费永远查不出来。

03 — 图片

文生图 · 图生图

参数说明
image 图生图必填参考图,可传多张(见下面那条旁注)
size默认 1024x1024
n要几张,1~8。每张是独立一次调用,一张不合格不会拖累其他
transparent要真 alpha 通道。服务会替你验
wait默认 true,同步等到出图(上限 180 秒)
# 图生图:多传几张参考图,用 multipart
curl -X POST https://ai.nofuns.xyz/v1/image2image \
  -H "Authorization: Bearer $AICAP_TOKEN" \
  -F "prompt=把这个角色改成绿色,其他全部保持不变" \
  -F "image=@front.png" \
  -F "image=@side.png" \
  -F "image=@back.png" \
  -F "transparent=true"
旁注 · 多传几张,别省 实测过:单张参考图(一张穿蓝衣服的)→ 模型把那件衣服当成了角色特征, 还自己加了口罩和工牌,材质从绒毛变成纸浆。四张一起 → 出来的就是那个角色本身。 一张图只能告诉模型「这个角色在这一张里长什么样」,分不清哪些是角色、 哪些是这一张的布景和衣服;多张一交叉,不变的那部分才是角色。
旁注 · 透明底服务替你验 模型会静默忽略 transparent 返回不透明图,而且照样收钱。 服务会开图查 alpha 通道,不合格就自动换模型重试,并把这次记成「白花钱」。 你拿到的图一定有 alpha。

04 — 视频

文生视频 · 图生视频

视频一律异步 —— 拿 job_id 去轮询。跑 2~10 分钟, 同步等必然被网关掐断,那样你连轮询都没得轮。

参数说明
image 图生视频必填首帧图
last_image尾帧。传和 image 同一张 = 无缝循环
duration秒,默认 5
aspect_ratio默认 16:9
resolution默认 720p
compress默认 true,见下面那条旁注
curl -X POST https://ai.nofuns.xyz/v1/image2video \
  -H "Authorization: Bearer $AICAP_TOKEN" \
  -F "prompt=角色缓慢转头,眨眼,镜头锁死不动" \
  -F "image=@cat.png" \
  -F "last_image=@cat.png" \
  -F "duration=5"

# → {"job_id":"j_xxx","status":"pending"}  然后轮询 /v1/jobs/j_xxx
旁注 · 为什么默认要压 实测一条 960² / 5 秒的片子是 9.0 MB,压到 640² / CRF30 之后 422 KB (小 21 倍),放在卡片上看不出差别。60 条就是 540 MB vs 25 MB —— 托管、传输、 下载全都要为此付钱。真要原片就传 compress=false

05 — 视频理解

视频生文

喂一段视频,问你想问的。默认问「详细描述这段视频」。

参数说明
video 必填mp4 / webm,最大 256 MB
prompt你想问什么。不填就是让它详细描述
frames抽几帧,1~32,默认 8。越多越细也越贵
curl -X POST https://ai.nofuns.xyz/v1/video2text \
  -H "Authorization: Bearer $AICAP_TOKEN" \
  -F "video=@clip.mp4" \
  -F "prompt=这段视频里镜头是怎么动的?主体做了什么动作?" \
  -F "frames=12"

# 结果在 job 的 text 字段里,同时也存成一个 .txt 产物
旁注 · 抽帧不是降级方案 服务用 ffmpeg 均匀抽 N 帧(避开首尾那两帧 —— 首帧常是黑场,尾帧常是渐隐), 再把这组图当成一段连续视频送给视觉模型。这么做不是因为原生视频输入不好, 而是:跨所有视觉模型都能用、便宜得多、帧数可控。 实测 8 帧 + gemini-3-flash,93 秒出 313 字中文描述。

06 — 三维

文生 3D · 图生 3D

两家供应商:Meshy(默认)和 Tripo异步。

参数说明
image 图生3D必填可多张。首图必须是正面,后面依次左 / 后 / 右
providermeshy(默认)/ tripo二选一,不跨家
texture要不要贴图,默认 true
polycount目标面数 100~300000
formatsglb(默认)/ fbx,逗号分隔
quad四边面。必须一起要 fbx,见旁注
hi_poly顺带要减面前的高模(300 万面)
parts分件输出。和贴图互斥
curl -X POST https://ai.nofuns.xyz/v1/image2model \
  -H "Authorization: Bearer $AICAP_TOKEN" \
  -F "image=@front.png" -F "image=@left.png" \
  -F "provider=meshy" -F "quality=standard" \
  -F "formats=glb,fbx" -F "quad=true" -F "hi_poly=true"
旁注 · 两家不互相顶替 Meshy 和 Tripo 是两个不同的产品:拓扑不一样,Meshy 有 300 万面的 pre-remeshed 高模(「高模烘法线到低模」那套流程只有它撑得起来), Tripo 有非人形绑骨和语义分割(Meshy 的绑骨只支持人形两足)。 选定的那家没钱就报错 —— 悄悄换一家给你一个你没要的模型, 而你不会知道,那比报错更糟。
旁注 · quad 必须配 fbx GLB 规范里只有三角形。请求了 quad 却只下 GLB 等于白给 —— 实测拿到 12836 个三角面、0 个四边面。服务会在花钱之前 把这个组合拦下来并告诉你原因,而不是让你等十分钟拿到一个假的四边面模型。

07 — 机制

作业与产物

视频和 3D 一律异步,图片可以同步等。两种回来的形状是一样的。

01 提交POST /v1/…立刻回 job_id,状态 pending
02 轮询GET /v1/jobs/{id}status / step 会告诉你现在打给谁、跑到几 %
03 取件GET /v1/files/{aid}回字节流,带正确的 mime 和文件名
status意思
pending排着,还没有 worker 认领
running在跑。step 里是当前打给谁(如 meshy/meshy-t2)或进度
done成了。artifacts 里是产物,usage 里是这一趟发了几次、谁出的活
failed败了。error 里是每个候选分别为什么失败
旁注 · 报错会说清是谁、为什么 「都失败了」后面什么都没有的报错查不出任何东西。所以 error 里 每一行都是「哪个账号 / 哪个模型 / 回了什么」,例如 tripo/v3.1 HTTP 403 code 2010 余额不足。 密钥会被两层脱敏掉,不会跟着报错泄露出去。

产物默认保留 14 天,过期后取件会回 410 并说明原因 —— 不会给你一个下载到一半断掉的文件。

08 — 客户端

拷走就能用

clients/aicap.pyclients/aicap.ts零依赖, 单文件。轮询、下载、错误处理都封好了。

from aicap import AiCap, AiCapError

ai = AiCap("https://ai.nofuns.xyz", "tok_xxx")

# 图片:一步到位
png  = ai.text2image("扁平卡通的橘猫", quality="fast")[0]
png2 = ai.image2image([("ref.png", png)], "改成绿色")[0]

# 视频 / 3D:会一直等(几分钟),别放在请求处理里同步调
mp4   = ai.image2video(png, "缓慢转头", onProgress=print)
desc  = ai.video2text(mp4, "镜头怎么动的?")
files = ai.image2model([("front.png", png)], provider="meshy")
open("out.glb", "wb").write(files["glb.glb"])

# 要非阻塞就用 submit_* 拿 job_id,自己轮询
job = ai.submit_image2video(png, "缓慢转头")
# … 过一会儿 …
done = ai.wait(job)
import { AiCap } from "./aicap";

const ai = new AiCap("https://ai.nofuns.xyz", process.env.AICAP_TOKEN!);

const [png] = await ai.text2image("扁平卡通的橘猫", { quality: "fast" });
const mp4   = await ai.image2video(png, "缓慢转头");
const desc  = await ai.video2text(mp4, "镜头怎么动的?");
const files = await ai.image2model(
  [{ name: "front.png", data: png }], { provider: "meshy" });

09 — 并发

两层闸

都在 SQLite 里,跨进程共享要么都拿到、要么都不拿。

账号层

限流是按账号算的,不是按 key —— 加 key 不加容量,加账号才加。

artsapi20
artsapi220
4sapi12
meshy10
tripo ×210

端点层

端点必须分开算 —— edits 6 并发就开始劣化, 而 generations 24 并发都没找到顶。

generations24
edits6
chat12 ⚠
model3d10 ⚠
videos4 ⚠

⚠ 标记的三个是猜的或照官方文档配的,我们自己没压测过。 /v1/capabilities 里它们的 measuredfalse —— 没测过的东西不写成已知。

旁注 · 拿不到不是失败 候选链上某一家满了,请求会掉到下一家,不会排队。阻塞的话第二个账号 一次都用不上(请求全堵在第一个的队列上),配了等于白配。 而所有账号都忙时,服务会等 —— 「忙」不消耗重试次数, 否则高峰期会把「慢」直接变成「失败」。

10 — 勘误

踩过的坑

这些都写成了数据,由路由层自动过滤,不靠人记。 列在这里是让你知道服务替你挡掉了什么 —— 以及为什么别绕过它。

  1. 一个总是快速失败的候选,比没有候选更糟。它不但不顶用,还要吃掉一次重试和一次退避。修掉一个这样的候选之后,一趟作业从 11.0 分钟降到 3.4 分钟。
  2. 链必须按端点分。gpt-image-2-low/images/edits 上 8/8 全 400(它不支持这个端点),但在 /images/generations 上是最快最便宜的那个。用一个端点的观测推另一个端点的结论,栽过四次。
  3. 能力不能靠猜。要透明底时 low 5/5 返回不透明图,每次 1.7 秒就返回 —— 那速度根本不是在出图,但上游照样收钱。另一个模型更狠:直接把棋盘格画进像素里当假透明。
  4. 尺寸约束不能一刀切。某个模型要 ≥2048,拿 1024 发它 44 次调用 44 次 400,每张白花 7.4 秒 —— 而且当年被误判成「它不支持透明底」,一个尺寸问题被记成了能力问题。
  5. 「同步端点」也可能返回任务号。gpt-image-2-low/images/generations 回的不是图,是 {"id":"…","status":"pending"}。不轮询的话它每次都被判成失败、每次都掉回贵的那个 —— 配了等于没配
  6. 信封不是内容。Tripo 的响应外层 status:"success"接口调用成功,内层 data.status 才是任务状态。读错的话任务跑到 5% 就被当成完成,然后报「成功了但没有产物」。
  7. 两步的事别只做一步。Meshy 文生 3D 是 preview 出几何 → refine 上贴图,只发第一步交付的是个白模,而调用方要点开才会发现。
  8. 限流按账号算,不按 key 算。12 并发全压 1 个 key 和均摊到 3 个 key 都是 12/12、零 429。⇒ 加 key 不加容量,加账号才加
  9. 并发提高反而更慢。池 8→20 的实测:11.0 分钟 → 13.1 分钟。负反馈:错误率↑ → 退避重试 → 有效吞吐↓。顶着天花板跑,抖动一下就全挂。
  10. 「能连上」不等于「能用」。栽过的每一次,curl 都是 200。所以冒烟检查的不是状态码,是产物本身 —— 图能不能解码、GLB 的魔数对不对、描述是不是空的。

11 — 自检

不用问人

GET /health活着没、配了几个账号、有没有 ffmpeg(不要令牌
GET /v1/capabilities每条链现在实际会打给谁、哪些验证过、并发占用
GET /v1/usage?hours=24你这个项目花了多少,含白花的钱
GET /v1/jobs?limit=50你这个项目最近的作业
GET /docsFastAPI 自动生成的接口文档,能直接试
健康检查会说实话 供应商一个都没配的时候,进程活着但服务是死的 —— 所以 /health 会回 degraded 并列出哪条链降到了第几位、缺谁。 降级路径本来就是设计好的,它会安静地生效 —— 不喊出来的话能安静地跑一年。