# Rhuan API 接入文档

Rhuan 是独立 AI Photography Model。输入同一个人的参考照片、约定场景 `scene_id`
和少量参数即可获得成片。所有客户使用相同接口。

公共协议版本：**v0.4**。在线文档：[rhuan.app/docs](https://rhuan.app/docs)。

| 接入项 | 值 |
| --- | --- |
| Base URL | `https://rhuan.app/v1` |
| Model | `rhuan-photo-1` |
| 认证 | `Authorization: Bearer rhuan_sk_…` |
| 生图入口 | `POST /v1/images/edits`，`multipart/form-data` |
| 返回 | 同步 `created + data[].b64_json` |

API key 由管理员交付，只在创建时显示一次。放在服务端环境变量或 secret 管理工具，
避免嵌入公开网页、提交 Git 或放入 URL。撤销后立即失效。

v0.5 运营后台可创建场景和 key；本公共协议仍为 v0.4，模型名和参数不变。
`scene_id` 创建后不可改；管理员可更新图片或停用场景。停用、未知与未授权场景
均返回同样的 `404 scene_not_found`。后台登录 cookie 不能替代 Bearer key。

## 开始接入

1. 向管理员获取 API key，以及这把 key 已获授权的 `scene_id`。
2. 准备同一个人的 1–4 张 PNG、JPEG 或 WEBP 照片，把清晰正面照放在第一张。
3. 按下方 Python 或 curl 示例上传；将示例场景 ID 换为管理员提供的 ID。
4. 将响应的 `b64_json` 解码成图片；保留 `X-Request-ID`，用于查询请求结果或排障。

普通 Images API 的场景发布与 Key 授权是两个步骤。仅 Publish 场景不会自动让
现有 Key 获得此接口的权限。ID 和授权规则区分大小写，例如 `GD_001`
与 `gd_001` 不同。遇到 `404 scene_not_found`，请让管理员同时核对场景状态和 Key 权限。

## Python SDK

安装 `openai` 和用于识别成片格式的 `pillow`。已用 **OpenAI Python SDK 3.36.1**
的真实本地 HTTP 验证空 prompt、多文件、布尔扩展字段和响应解析。

```python
import base64
import os
from io import BytesIO
from pathlib import Path

from openai import OpenAI
from PIL import Image

client = OpenAI(
    api_key=os.environ["RHUAN_API_KEY"],
    base_url="https://rhuan.app/v1",
    timeout=900,
    max_retries=0,
)

with open("person_front.jpg", "rb") as front, open("person_side.jpg", "rb") as side:
    result = client.images.edit(
        model="rhuan-photo-1",
        image=[front, side],
        prompt="",
        n=2,
        quality="high",
        size="auto",
        response_format="b64_json",
        extra_body={
            "scene_id": "lvpai_yulong_001",  # 替换为已获授权的场景 ID
            "allow_outfit_change": True,
            "aspect_ratio": "9:16",
        },
    )

for index, item in enumerate(result.data, 1):
    raw = base64.b64decode(item.b64_json)
    with Image.open(BytesIO(raw)) as photo:
        extension = {"PNG": "png", "JPEG": "jpg", "WEBP": "webp"}[photo.format]
    Path(f"output_{index}.{extension}").write_bytes(raw)
```

当前公共上传不标注角色，以第一张为主要身份参考，其余
原图保留用于追溯，不保证全部送入生成请求。请把最清晰的正面照片放在第一张。

单图可用 `image=front`。`prompt=""` 会正常编码为空字段，不需要自由提示词。
示例按实际格式保存原始图片字节，不做重新编码。`max_retries=0` 避免 SDK 自动重试
造成重复生成；本版没有幂等去重。超时、断线或部分失败后先凭 request ID 核查结果。

## curl

假设 `RHUAN_API_KEY` 已通过安全方式放入环境变量：

```bash
curl --fail-with-body --max-time 900 \
  https://rhuan.app/v1/images/edits \
  -H "Authorization: Bearer ${RHUAN_API_KEY}" \
  -F 'model=rhuan-photo-1' \
  -F 'image[]=@person_front.jpg' \
  -F 'image[]=@person_side.jpg' \
  -F 'prompt=' \
  -F 'scene_id=lvpai_yulong_001' \
  -F 'n=2' \
  -F 'quality=high' \
  -F 'size=auto' \
  -F 'response_format=b64_json' \
  -F 'allow_outfit_change=true' \
  -F 'aspect_ratio=9:16' \
  -o response.json
```

支持重复 `image` 或 `image[]` 文件字段。由 curl/SDK 自动生成 Content-Type boundary，
不要手填。只接受直接上传文件，不接受图片 URL、base64 JSON 或服务器路径。

## 模型接口

```python
print(client.models.list())
print(client.models.retrieve("rhuan-photo-1"))
```

`GET /v1/models` 返回：

```json
{
  "object": "list",
  "data": [
    {"id": "rhuan-photo-1", "object": "model", "created": 1792627200, "owned_by": "rhuan"}
  ]
}
```

全部 `/v1` 实际业务请求需要 key；合法 CORS 预检 OPTIONS 不需要 key。`POST /v1/images/generations` 返回 400，提示使用带人物图
的 edits 接口。

## 参数

| 字段 | 默认 / 限制 | 含义 |
| --- | --- | --- |
| `model` | 必填，`rhuan-photo-1` | 唯一公开模型 |
| `image` / `image[]` | 必填，1–4 个文件 | 同一个人的照片；最好正面照放第一张，无需标角色 |
| `scene_id` | 必填 | 提前注册且当前 key 获授权的稳定 ID |
| `prompt` | 缺省、空字符串或固定 `rhuan` | 不用于选场景，不支持自由提示词 |
| `n` | 默认 1，整数 1–6 | 期望成片数；场景素材不足时报错 |
| `quality` | 默认 `high`；`low` / `medium` / `high` | 稳定质量选项 |
| `aspect_ratio` | 默认 `9:16` | `1:1`、`3:2`、`2:3`、`4:3`、`3:4`、`16:9`、`9:16`、`21:9`、`auto` |
| `allow_outfit_change` | 默认 false | false 尽量保留参考服装；true 允许按场景换装 |
| `size` | 仅 `auto` | 不承诺精确像素；如 `1024x1792` 返回 `unsupported_parameter` |
| `response_format` | 默认且仅 `b64_json` | base64 图片字节 |

不支持 `resolution`、URL 输出、mask、stream、output_format 或其他未列出的控制字段，
也不会静默降级。尺寸与比例不同，不会把精确尺寸偷换为某个比例。

`scene_id` 不是路径。运营先注册场景包，再授权 ID 或通配范围，例如 `lvpai_*`。
没有公共场景枚举、创建或上传接口；请使用双方约定的 ID。n 张场景沿用已有抽样方式，
服务端记录实际选中素材。示例中的 `lvpai_yulong_001` 仅作占位，不代表该 ID 已向你的 Key 授权。

## 响应与错误

```json
{"created": 1792627200, "data": [{"b64_json": "..."}, {"b64_json": "..."}]}
```

200 必须恰好返回 n 张图。请求四张而只成功三张时返回 502；内部产物保留供调查。
响应头 `x-request-id` 用于排障；联系运营时提供该 ID，不提供 key。

```json
{
  "error": {
    "message": "scene_id is required",
    "type": "invalid_request_error",
    "param": "scene_id",
    "code": "missing_scene_id"
  }
}
```

| HTTP | code / 含义 | 处理 |
| --- | --- | --- |
| 400 | `unsupported_parameter` / `invalid_parameter` / `missing_scene_id` / `model_not_found` / `invalid_request` / `upload_timeout` / `unsupported_endpoint` | 修正请求；上传超时可在确认未生成后重传 |
| 401 | `invalid_api_key` | 检查 Bearer key 是否有效或已撤销 |
| 404 | `scene_not_found` / `not_found` | 核对 ID；无权限、停用与未知场景响应相同 |
| 413 | `request_too_large` | 减少上传体积 |
| 422 | `invalid_image` | 使用有效 PNG/JPEG/WEBP |
| 429 | `rate_limit_exceeded` | 返回 `Retry-After: 5`；等待现有生成结束再提交 |
| 500 | `internal_error` | 携 request ID 联系运营 |
| 502 | `upstream_generation_error` | 生成未完整成功，先核查 |
| 504 | `generation_timeout` | 已开始任务可能继续执行，先核查再重试 |

## 限制与数据保存

- 每张 PNG/JPEG/WEBP 最多 8 MiB、20,000,000 像素；总请求最多 34 MiB，含 multipart。
  不支持动态图，恶意文件名、重复标量和嵌套扩展字段会被拒绝。
- 当前单进程公共请求最多并行 2 个，每个 key 最多 1 个，每请求最多 6 张并发。
  这是并发保护，不是计费或配额系统。
- 应用上传总时限 30 秒；生成等待最多 780 秒，SDK 建议 900 秒。HTTP 超时后，实际任务结束才释放并发额度。
- 人物图不加入人物库。输入、成片、prompt、metadata 保存在本次私有 job 工作目录
  中，不通过公开 URL 或免登录 Web 历史提供访问。
- 本版保留请求数据用于追溯，**没有自动过期删除**。账本保存状态与数量，不保存
  明文 key 或图片；需删除数据时联系运营确认范围。
- 不提供客户场景管理、注册或账单接口。

## Browser Client / CORS

官方Demo为 **https://demo.rhuan.app**，使用同一公开模型。v0.10无需登录，所有访客
共享服务器人物照片与生成历史，人物/场景/参数的选择只保存在各自浏览器。页面提供
共享Key编辑和用量入口，运行凭据保存于服务器且只显示掩码；普通使用无需自带Key。
支持全部可用已发布场景、首图缩略图、逐图评分与默认关闭的低分重做。
Demo单图上限20 MiB，普通Images API仍为8 MiB并执行原Key场景权限。
第三方服务端仍将自己的Key保存在secret配置中，通过下方SDK/curl调用
`/v1/images/edits`。保留的`/v1/demo`扩展仍需Bearer；官方网页不再从浏览器调用它。

浏览器跨域白名单唯一值是 `https://demo.rhuan.app`。无 Origin 的 SDK/server-to-server
请求保持现有行为；其他、空或重复 Origin 被拒绝，不要求 SDK 添加 Origin/Referer。
Admin 与旧 `/api/*` 不增加 CORS。

无需 key 的 Images 兼容接口预检支持：

- `OPTIONS /v1/models`，请求方法 GET。
- `OPTIONS /v1/images/edits`，请求方法 POST。

Demo 扩展另对白名单中的场景/缩略图/批次/成片 GET 和批次 POST 提供精确预检，
实际请求均重新验证 Bearer Key；Origin 不授予访问权，历史与成片按 Key 隔离。

验证 Origin、Access-Control-Request-Method 与 Access-Control-Request-Headers；请求头
仅允许 Authorization、Content-Type（大小写不敏感）。合法预检返回 204，并包含：

```http
Access-Control-Allow-Origin: https://demo.rhuan.app
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 600
Vary: Origin
```

不返回通配 Origin 或 Allow-Credentials。合法来源的成功和错误响应均带允许头，
`Access-Control-Expose-Headers` 使浏览器可读 `X-Request-ID` 与 `Retry-After`；预检
不计入业务 usage。网关返回的 413/502/504 同样只对允许来源添加 CORS。

multipart由FormData自动生成boundary；官方Demo不发送Cookie，不自动重试生成请求。
429 显示 Retry-After；504/网络中断应视为结果未知，服务器任务可能继续，联系运营
核查后再决定重新提交，避免重复付费。当前不提供 `/v1/scenes`。
