diff --git a/.env.example b/.env.example index ffb87ea..624a0d3 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,7 @@ GROK_API_KEY=your_grok_api_key GPT_API_KEY=your_gpt_api_key IMAGE_API_BASE_URL=https://api.slomerex.xyz/v1 + +# Agnes Image 2.1 Flash +AGNES_API_KEY=your_agnes_api_key +AGNES_API_BASE_URL=https://apihub.agnes-ai.com/v1 diff --git a/SKILL.md b/SKILL.md index cc3f69d..ec00378 100644 --- a/SKILL.md +++ b/SKILL.md @@ -13,9 +13,10 @@ description: Generate images via AI text-to-image API (GPT Image / Grok). Use wh | 能力 | 服务端文件 | HTTP 端点 | 模型 | |------|------------|------------|------| -| GPT / Grok 绘图 | `agent-image-t2i.py` | `POST /images/generations` | `gpt-image-2-1K` / `gpt-image-2-2K` / `gpt-image-2-4K` / `grok-imagine-image-lite` | -| 健康检查 | `agent-image-t2i.py` | `GET /health` | — | -| 图片访问 | `agent-image-t2i.py` | `GET /output/{profile_name}/{filename}` | — | +| GPT / Grok 绘图 | `main.py` | `POST /images/generations` | `gpt-image-2-1K` / `gpt-image-2-2K` / `gpt-image-2-4K` / `grok-imagine-image-lite` | +| Agnes 绘图(文生图 / 图生图 / 多图合成) | `main.py` | `POST /agnes/images/generations` | `agnes-image-2.1-flash` | +| 健康检查 | `main.py` | `GET /health` | — | +| 图片访问 | `main.py` | `GET /output/{profile_name}/{filename}` | — | ## GPT 模型说明 @@ -68,6 +69,102 @@ curl -X POST http://localhost:8765/images/generations \ 生成结果中的 `url` 为图片访问地址,例如 `http://localhost:8765/output/{profile_name}/{filename}`。 +## Agnes Image 2.1 Flash + +升级版图像生成模型,针对高信息密度图像和复杂构图做了优化。同时支持**文生图、图生图、多图合成**。 + +### 调用示例 + +文生图(2K, 16:9): + +```bash +curl -X POST http://localhost:8765/agnes/images/generations \ + -H "Content-Type: application/json" \ + -d '{ + "prompt": "a luminous floating city above a misty canyon at sunrise, cinematic realism", + "size": "2K", + "ratio": "16:9", + "profile_name": "profile_name" + }' +``` + +图生图(传入参考图 URL 或 Data URI Base64): + +```bash +curl -X POST http://localhost:8765/agnes/images/generations \ + -H "Content-Type: application/json" \ + -d '{ + "prompt": "turn the scene into a rainy cyberpunk night while preserving the original composition", + "size": "1K", + "ratio": "16:9", + "images": ["https://example.com/input.png"], + "profile_name": "profile_name" + }' +``` + +多图合成(`images` 传多张): + +```bash +curl -X POST http://localhost:8765/agnes/images/generations \ + -H "Content-Type: application/json" \ + -d '{ + "prompt": "combine the two characters into an intense fantasy battle scene, dynamic lighting", + "size": "1K", + "ratio": "16:9", + "images": [ + "https://example.com/character-1.png", + "https://example.com/character-2.png" + ], + "profile_name": "profile_name" + }' +``` + +响应示例: + +```json +{ + "model": "agnes-image-2.1-flash", + "size": "2624x1472", + "ratio": "16:9", + "images": [ + {"filename": "agnes-image-2.1-flash-xxxx.png", "url": "/output/profile_name/agnes-image-2.1-flash-xxxx.png", "bytes": 1234567} + ] +} +``` + +返回的 `size` 是根据 `size` 档位 + `ratio` 解析出的实际像素尺寸,便于拼接访问地址。 + +### 请求参数 + +请求体为 JSON,发送到 `POST /agnes/images/generations`: + +- `profile_name`:**必填**,调用方 agent 的标识,图片会保存到 `output//` +- `prompt`:必填,文本提示词 +- `size`:可选,尺寸档位 `1K` / `2K` / `3K` / `4K`,或精确尺寸如 `1024x1024`;默认 `1K` +- `ratio`:可选,宽高比,与 `size` 档位配合使用;支持 `1:1` / `3:4` / `4:3` / `16:9` / `9:16` / `2:3` / `3:2` / `21:9`;默认 `1:1` +- `images`:可选,图生图或多图合成的输入图像 URL 或 Data URI Base64 列表 + +### 输出尺寸参考 + +| Ratio | 1K | 2K | 3K | 4K | +|--------|-------------|-------------|-------------|-------------| +| `1:1` | `1024x1024` | `2048x2048` | `3072x3072` | `4096x4096` | +| `3:4` | `864x1152` | `1728x2304` | `2592x3456` | `3456x4608` | +| `4:3` | `1152x864` | `2304x1728` | `3456x2592` | `4608x3456` | +| `16:9` | `1312x736` | `2624x1472` | `3936x2208` | `5248x2944` | +| `9:16` | `736x1312` | `1472x2624` | `2208x3936` | `2944x5248` | +| `2:3` | `832x1248` | `1664x2496` | `2496x3744` | `3328x4992` | +| `3:2` | `1248x832` | `2496x1664` | `3744x2496` | `4992x3328` | +| `21:9` | `1568x672` | `3136x1344` | `4704x2016` | `6272x2688` | + +### 使用注意 + +- 需要服务端配置 `AGNES_API_KEY`(默认走 `https://apihub.agnes-ai.com/v1`,可用 `AGNES_API_BASE_URL` 覆盖) +- 单次上游请求超时为 360 秒,建议客户端超时 ≥ 60s +- 上游请求遇到网络错误或 408/429/500/502/503/504 时,最多自动重试 3 次,并使用指数退避 +- 图生图 / 多图合成的参考图必须是可公开访问的 HTTPS URL,否则请用 Data URI Base64 +- 当前定价:免费(`$0 / 张`) + ## 调用建议 - **优先用 Grok**:稳定出图,适合日常使用 diff --git a/docs/agnes-ai-doc.md b/docs/agnes-ai-doc.md new file mode 100644 index 0000000..b1a9089 --- /dev/null +++ b/docs/agnes-ai-doc.md @@ -0,0 +1,554 @@ + +Agnes Image 2.1 Flash - Agnes-Ai Docs
Skip to main content
Agnes Image 2.1 Flash 是 Agnes AI 的升级图像生成模型,支持文生图、图生图和多图合成。相比 2.0 版本,它更适合高信息密度图像、复杂构图和细节丰富的视觉场景。
+

模型名称

agnes-image-2.1-flash

API Endpoint

POST /v1/images/generations

核心优化

高信息密度图像、复杂视觉细节和语义对齐。

当前价格

生成图像当前为 $0 / 张
+

概述

+Agnes Image 2.1 Flash 可根据文本提示词生成图像,也可基于输入图像进行转换、重绘和风格化编辑。它支持以图像 URL 或 Base64 数据形式返回结果。 +

高信息密度优化

更适合复杂场景、丰富构图和多层视觉元素。

构图保留

图生图编辑时可尽量保留原始构图和主体布局。
+

核心能力

+

文生图

根据自然语言提示词生成高质量图像。

图生图

根据提示词转换或优化现有图像。

多图合成

使用多张参考图像组合生成新图像。

高信息密度图像

优化细节丰富、布局复杂、视觉元素密集的图像生成效果。

构图保留

编辑输入图像时保留原始构图和主体布局。

灵活尺寸控制

使用 1K2K3K4K 等尺寸档位,并配合支持的宽高比。

URL / Base64 输出

支持图像 URL 或 Base64 数据返回。
+

适用场景

+

创意设计

概念艺术、视觉探索和海报草稿。

营销内容

活动图片、产品视觉和社交媒体创意。

高密度视觉生成

精细场景、复杂环境和丰富构图。

图像转换

风格迁移、场景重打光和背景变换。

产品可视化

产品照片、模型图和商业视觉。

社交媒体素材

封面、横幅、缩略图和帖子图片。
+

API Reference

+

Endpoint

+
+

请求头

+
+

请求参数

+ +

尺寸与宽高比

+为了获得可预期的输出尺寸,建议将 sizeratio 配合使用。 +
    +
  • 推荐 size 值:1K2K3K4K
  • +
  • 支持的 ratio 值:1:13:44:316:99:162:33:221:9
  • +
  • 如果请求 1920x10802560x1440 这类不受原生支持的精确尺寸,服务可能会自动映射到最接近的标准档位和宽高比。
  • +
  • 如果需要生成 1920x10802560x1440 这类常见 16:9 显示素材,建议请求 size: "2K"ratio: "16:9",再在下游裁剪或缩放到最终画布。
  • +
+
1920x10802560x1440 是标准显示器分辨率,但不是该图像模型的原生输出尺寸。不支持的精确尺寸可能会被标准化,例如映射为 16:9 的 1K 输出尺寸 1312x736
+

输出尺寸参考

+ +

示例:16:9 的 2K 输出

+
+该请求会返回 16:9 的 2K 档位输出尺寸:2624x1472 +

重要说明

+
请勿在请求体顶层放置 response_format。需要 URL 输出时,请使用 extra_body.response_format: "url";图生图 Base64 输出请使用 extra_body.response_format: "b64_json"
+

文生图

必填参数为 modelpromptsize

尺寸 + 宽高比

使用 2K 等档位式 size,并配合 16:9ratio

图生图

需要在 extra_body.image 中提供输入图像 URL 或 Data URI Base64。

多图合成

extra_body.image 中传入多张参考图像。

Base64 输出

文生图可使用 return_base64: true;图生图请使用 extra_body.response_format: "b64_json"

无需 tags

图生图不需要传递 tags: ["img2img"]
+

请求示例

+
返回路径:data[0].url
+

响应格式

+
+

推荐提示词结构

+
请说明每张参考图的角色,以及最终图像应如何组合这些参考信息。
请清晰描述视觉层次结构,包括主要主体、背景环境、重要次要细节、风格、光照和构图约束。
+

常见错误与故障排除

+
错误写法:
正确写法:
图生图不需要传递 tags: ["img2img"]。只需在 extra_body.image 中提供输入图像。
请使用公共 HTTPS 图像 URL,并确认不需要登录、cookie 或私有请求头;如果无法公开访问,请使用 Data URI Base64。
根据提示词复杂度、图像尺寸和服务器负载情况,图像生成可能需要数秒到几十秒。客户端超时时间建议设置为 60s - 360s
图生图和多图合成生成时,extra_body.image 为必填项。
+

定价

+ +

接入检查清单

+
使用 agnes-image-2.1-flash 作为模型名称。
+
使用 https://apihub.agnes-ai.com/v1/images/generations 作为 API 端点。
+
文生图请求必须包含 modelpromptsize
+
为获得可预期的输出尺寸,建议使用 1K2Ksize 档位,并配合 ratio
+
图生图和多图合成请求需要在 extra_body.image 中提供输入图像。
+
请勿将 response_format 放在顶层,也不要传递 tags: ["img2img"]
+ + \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..2fd3ccc --- /dev/null +++ b/main.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""FastAPI 入口:AI 图片生成服务。""" + +from dotenv import load_dotenv + +load_dotenv() + +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel, Field, model_validator + +from utils.mm_api_t2i import OUTPUT_DIR, PROVIDERS, generate_images +from utils.agnes_image import VALID_RATIOS, generate_images as agnes_generate_images + +app = FastAPI(title="AI Image T2I API", version="1.0.0") +OUTPUT_DIR.mkdir(parents=True, exist_ok=True) +app.mount("/output", StaticFiles(directory=OUTPUT_DIR), name="output") + + +class ImageGenerationRequest(BaseModel): + prompt: str = Field(min_length=1, description="图片提示词") + provider: str = Field(default="grok", pattern="^(grok|gpt)$", description="模型提供方") + model: str | None = Field(default=None, description="覆盖提供方默认模型") + size: str = Field(default="1024x1024", description="图片尺寸") + n: int = Field(default=1, ge=1, le=10, description="生成数量") + profile_name: str = Field(pattern=r"^[\w-]+$", description="调用方标识(必填),图片会保存到 output// 下") + + @model_validator(mode="before") + @classmethod + def check_profile_name(cls, data): + if isinstance(data, dict) and not data.get("profile_name"): + raise ValueError("缺少 profile_name 参数,请在请求中带上你的 agent/profile 名称") + return data + + +class GeneratedImage(BaseModel): + filename: str + url: str + bytes: int + + +class ImageGenerationResponse(BaseModel): + provider: str + model: str + images: list[GeneratedImage] + + +@app.get("/health") +def health_check(): + return {"status": "ok"} + + +@app.post("/images/generations", response_model=ImageGenerationResponse) +def generate_images_endpoint(request: ImageGenerationRequest): + result = generate_images( + provider=request.provider, + model=request.model, + prompt=request.prompt, + size=request.size, + n=request.n, + profile_name=request.profile_name, + ) + return ImageGenerationResponse(**result) + + +class AgnesImageGenerationRequest(BaseModel): + prompt: str = Field(min_length=1, description="图片提示词") + size: str = Field(default="1K", description="输出尺寸档位: 1K / 2K / 3K / 4K,也支持精确尺寸如 1024x1024") + ratio: str | None = Field(default=None, description=f"宽高比,支持 {sorted(VALID_RATIOS)};与 size 档位配合使用") + images: list[str] | None = Field(default=None, description="图生图 / 多图合成的输入图像 URL 或 Data URI Base64") + profile_name: str = Field(pattern=r"^[\w-]+$", description="调用方标识(必填),图片会保存到 output// 下") + + @model_validator(mode="before") + @classmethod + def check_profile_name(cls, data): + if isinstance(data, dict) and not data.get("profile_name"): + raise ValueError("缺少 profile_name 参数,请在请求中带上你的 agent/profile 名称") + return data + + +class AgnesImageGenerationResponse(BaseModel): + model: str + size: str + ratio: str + images: list[GeneratedImage] + + +@app.post("/agnes/images/generations", response_model=AgnesImageGenerationResponse) +def agnes_generate_images_endpoint(request: AgnesImageGenerationRequest): + result = agnes_generate_images( + prompt=request.prompt, + size=request.size, + ratio=request.ratio, + images=request.images, + profile_name=request.profile_name, + ) + return AgnesImageGenerationResponse(**result) + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/agnes_image.py b/utils/agnes_image.py new file mode 100644 index 0000000..dae1ab7 --- /dev/null +++ b/utils/agnes_image.py @@ -0,0 +1,140 @@ +"""Agnes Image 2.1 Flash 文生图 / 图生图 / 多图合成的核心逻辑。""" + +import base64 +import json +import os +import time +import urllib.error +import urllib.request +from pathlib import Path +from uuid import uuid4 + +from fastapi import HTTPException + +API_BASE_URL = os.getenv("AGNES_API_BASE_URL", "https://apihub.agnes-ai.com/v1") +OUTPUT_DIR = Path("output") +MAX_RETRIES = 3 +RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504} +DEFAULT_MODEL = "agnes-image-2.1-flash" +VALID_RATIOS = {"1:1", "3:4", "4:3", "16:9", "9:16", "2:3", "3:2", "21:9"} +RATIO_DIMENSIONS = { + "1:1": {"1K": "1024x1024", "2K": "2048x2048", "3K": "3072x3072", "4K": "4096x4096"}, + "3:4": {"1K": "864x1152", "2K": "1728x2304", "3K": "2592x3456", "4K": "3456x4608"}, + "4:3": {"1K": "1152x864", "2K": "2304x1728", "3K": "3456x2592", "4K": "4608x3456"}, + "16:9": {"1K": "1312x736", "2K": "2624x1472", "3K": "3936x2208", "4K": "5248x2944"}, + "9:16": {"1K": "736x1312", "2K": "1472x2624", "3K": "2208x3936", "4K": "2944x5248"}, + "2:3": {"1K": "832x1248", "2K": "1664x2496", "3K": "2496x3744", "4K": "3328x4992"}, + "3:2": {"1K": "1248x832", "2K": "2496x1664", "3K": "3744x2496", "4K": "4992x3328"}, + "21:9": {"1K": "1568x672", "2K": "3136x1344", "3K": "4704x2016", "4K": "6272x2688"}, +} + + +def _resolve_size(size: str, ratio: str | None) -> str: + """把 size 档位 (1K/2K/3K/4K) 配合 ratio 解析成实际像素尺寸,方便保存文件。""" + if ratio and ratio not in VALID_RATIOS: + raise HTTPException(status_code=400, detail=f"不支持的宽高比: {ratio}") + if size in {"1K", "2K", "3K", "4K"}: + if not ratio: + ratio = "1:1" + return RATIO_DIMENSIONS[ratio][size] + return size + + +def _build_payload(*, prompt: str, size: str, ratio: str | None, images: list[str] | None) -> dict: + """组装请求体。注意 response_format 必须放在 extra_body 内。""" + payload: dict = { + "model": DEFAULT_MODEL, + "prompt": prompt, + "size": size, + } + extra_body: dict = {} + if ratio: + payload["ratio"] = ratio + if images: + extra_body["image"] = images + if extra_body: + payload["extra_body"] = extra_body + return payload + + +def _post_with_retry(url: str, headers: dict, payload: dict) -> dict: + """调用 Agnes Image 接口,并对可恢复的上游错误进行指数退避重试。""" + data = json.dumps(payload).encode("utf-8") + for attempt in range(1, MAX_RETRIES + 1): + request = urllib.request.Request(url, data=data, headers=headers, method="POST") + try: + with urllib.request.urlopen(request, timeout=360) as response: + return json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as e: + if e.code not in RETRYABLE_STATUS_CODES or attempt == MAX_RETRIES: + body = e.read().decode("utf-8", errors="replace") + raise HTTPException(status_code=502, detail=f"Agnes 图片服务错误: HTTP {e.code} {body}") from e + except (urllib.error.URLError, TimeoutError) as e: + if attempt == MAX_RETRIES: + raise HTTPException(status_code=504, detail="Agnes 图片服务连接或请求超时") from e + time.sleep(2 ** (attempt - 1)) + + +def _download_image(url: str) -> bytes: + """从 URL 或 data URI 下载图片。""" + if url.startswith("data:image/"): + try: + _, encoded = url.split(",", 1) + return base64.b64decode(encoded, validate=True) + except (ValueError, TypeError) as e: + raise HTTPException(status_code=502, detail="图片 data URL 格式无效") from e + + request = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) + for attempt in range(1, MAX_RETRIES + 1): + try: + with urllib.request.urlopen(request, timeout=120) as response: + return response.read() + except urllib.error.HTTPError as e: + if e.code not in RETRYABLE_STATUS_CODES or attempt == MAX_RETRIES: + raise HTTPException(status_code=502, detail=f"图片 URL 下载失败: HTTP {e.code}") from e + except (urllib.error.URLError, TimeoutError) as e: + if attempt == MAX_RETRIES: + raise HTTPException(status_code=504, detail="图片 URL 下载超时或网络连接失败") from e + time.sleep(2 ** (attempt - 1)) + + +def generate_images( + *, + prompt: str, + size: str, + ratio: str | None, + images: list[str] | None, + profile_name: str, +): + """生成图片并保存到本地,返回结果字典。支持文生图 / 图生图 / 多图合成。""" + api_key = os.getenv("AGNES_API_KEY") + if not api_key: + raise HTTPException(status_code=500, detail="未配置环境变量 AGNES_API_KEY") + + resolved_size = _resolve_size(size, ratio) + payload = _build_payload(prompt=prompt, size=size, ratio=ratio, images=images) + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + response = _post_with_retry(f"{API_BASE_URL}/images/generations", headers, payload) + + profile_dir = OUTPUT_DIR / profile_name + profile_dir.mkdir(parents=True, exist_ok=True) + saved = [] + for item in response.get("data") or []: + try: + if item.get("b64_json"): + raw = base64.b64decode(item["b64_json"], validate=True) + elif item.get("url"): + raw = _download_image(item["url"]) + else: + raise HTTPException(status_code=502, detail="Agnes 图片服务未返回 b64_json 或 url") + except ValueError as e: + raise HTTPException(status_code=502, detail="Agnes Base64 图片数据格式无效") from e + + filename = f"{DEFAULT_MODEL}-{uuid4().hex}.png" + (profile_dir / filename).write_bytes(raw) + saved.append({"filename": filename, "url": f"/output/{profile_name}/{filename}", "bytes": len(raw)}) + + return {"model": DEFAULT_MODEL, "size": resolved_size, "ratio": ratio or "1:1", "images": saved} diff --git a/agent-image-t2i.py b/utils/mm_api_t2i.py similarity index 55% rename from agent-image-t2i.py rename to utils/mm_api_t2i.py index 95e9dcc..3010107 100644 --- a/agent-image-t2i.py +++ b/utils/mm_api_t2i.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python3 -"""通过 FastAPI 提供 GPT Image 和 Grok Image 文生图服务。""" +"""GPT Image 和 Grok Image 文生图的核心逻辑。""" import base64 import os @@ -9,18 +8,14 @@ import urllib.request from pathlib import Path from uuid import uuid4 -from dotenv import load_dotenv -from fastapi import FastAPI, HTTPException -from fastapi.staticfiles import StaticFiles +from fastapi import HTTPException from openai import APIConnectionError, APIStatusError, APITimeoutError, OpenAI -from pydantic import BaseModel, Field, model_validator - -load_dotenv() API_BASE_URL = os.getenv("IMAGE_API_BASE_URL", "https://api.slomerex.xyz/v1") OUTPUT_DIR = Path("output") MAX_RETRIES = 3 RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504} + PROVIDERS = { "grok": { "api_key_env": "GROK_API_KEY", @@ -32,40 +27,8 @@ PROVIDERS = { }, } -app = FastAPI(title="AI Image T2I API", version="1.0.0") -OUTPUT_DIR.mkdir(parents=True, exist_ok=True) -app.mount("/output", StaticFiles(directory=OUTPUT_DIR), name="output") - - -class ImageGenerationRequest(BaseModel): - prompt: str = Field(min_length=1, description="图片提示词") - provider: str = Field(default="grok", pattern="^(grok|gpt)$", description="模型提供方") - model: str | None = Field(default=None, description="覆盖提供方默认模型") - size: str = Field(default="1024x1024", description="图片尺寸") - n: int = Field(default=1, ge=1, le=10, description="生成数量") - profile_name: str = Field(pattern=r"^[\w-]+$", description="调用方标识(必填),图片会保存到 output// 下") - - @model_validator(mode="before") - @classmethod - def check_profile_name(cls, data): - if isinstance(data, dict) and not data.get("profile_name"): - raise ValueError("缺少 profile_name 参数,请在请求中带上你的 agent/profile 名称") - return data - - -class GeneratedImage(BaseModel): - filename: str - url: str - bytes: int - -class ImageGenerationResponse(BaseModel): - provider: str - model: str - images: list[GeneratedImage] - - -def generate_with_retry(client: OpenAI, *, model: str, prompt: str, size: str, n: int): +def _generate_with_retry(client: OpenAI, *, model: str, prompt: str, size: str, n: int): """调用图片接口,并对可恢复的上游错误进行指数退避重试。""" for attempt in range(1, MAX_RETRIES + 1): try: @@ -80,7 +43,8 @@ def generate_with_retry(client: OpenAI, *, model: str, prompt: str, size: str, n time.sleep(2 ** (attempt - 1)) -def download_image(url: str) -> bytes: +def _download_image(url: str) -> bytes: + """从 URL 或 data URI 下载图片。""" if url.startswith("data:image/"): try: _, encoded = url.split(",", 1) @@ -102,29 +66,18 @@ def download_image(url: str) -> bytes: time.sleep(2 ** (attempt - 1)) -@app.get("/health") -def health_check(): - return {"status": "ok"} - - -@app.post("/images/generations", response_model=ImageGenerationResponse) -def generate_images(request: ImageGenerationRequest): - config = PROVIDERS[request.provider] +def generate_images(*, provider: str, model: str | None, prompt: str, size: str, n: int, profile_name: str): + """生成图片并保存到本地,返回结果字典。""" + config = PROVIDERS[provider] api_key = os.getenv(config["api_key_env"]) if not api_key: raise HTTPException(status_code=500, detail=f"未配置环境变量 {config['api_key_env']}") - model = request.model or config["model"] - profile_dir = OUTPUT_DIR / request.profile_name + resolved_model = model or config["model"] + profile_dir = OUTPUT_DIR / profile_name profile_dir.mkdir(parents=True, exist_ok=True) client = OpenAI(base_url=API_BASE_URL, api_key=api_key, timeout=120.0, max_retries=0) - response = generate_with_retry( - client, - model=model, - prompt=request.prompt, - size=request.size, - n=request.n, - ) + response = _generate_with_retry(client, model=resolved_model, prompt=prompt, size=size, n=n) images = [] for item in response.data: @@ -132,20 +85,14 @@ def generate_images(request: ImageGenerationRequest): if item.b64_json: raw = base64.b64decode(item.b64_json, validate=True) elif item.url: - raw = download_image(item.url) + raw = _download_image(item.url) else: raise HTTPException(status_code=502, detail="上游图片服务未返回 b64_json 或 url") except ValueError as e: raise HTTPException(status_code=502, detail="上游 Base64 图片数据格式无效") from e - filename = f"{model}-{uuid4().hex}.png" + filename = f"{resolved_model}-{uuid4().hex}.png" (profile_dir / filename).write_bytes(raw) - images.append(GeneratedImage(filename=filename, url=f"/output/{request.profile_name}/{filename}", bytes=len(raw))) - - return ImageGenerationResponse(provider=request.provider, model=model, images=images) - - -if __name__ == "__main__": - import uvicorn + images.append({"filename": filename, "url": f"/output/{profile_name}/{filename}", "bytes": len(raw)}) - uvicorn.run(app, host="0.0.0.0", port=8000) + return {"provider": provider, "model": resolved_model, "images": images}