from runtime import Args
from typings.gptimage2.gptimage2 import Input, Output
import requests
import base64
import io
import oss2
import uuid
import re
# ╔══════════════════════════════════════════════════════════╗
# ║ API易 线路配置(按需切换) ║
# ╚══════════════════════════════════════════════════════════╝
API_BASE = "https://api.apiyi.com/v1"
# 也可切换为: "https://vip.apiyi.com/v1" 或 "https://b.apiyi.com/v1"
# ╔══════════════════════════════════════════════════════════╗
# ║ 阿里云 OSS 配置(请修改为你的值) ║
# ╚══════════════════════════════════════════════════════════╝
ACCESS_KEY_ID = "" # 填入你的阿里云 Access Key ID
ACCESS_KEY_SECRET = "" # 填入你的阿里云 Access Key Secret
BUCKET_NAME = "" # 填入你的阿里云 OSS Bucket 名称
ENDPOINT = "oss-cn-beijing.aliyuncs.com" # 填入你的 OSS Endpoint
# ╔══════════════════════════════════════════════════════════╗
# ║ 质量超时配置(GPT Image 2 基于 quality 分级) ║
# ╚══════════════════════════════════════════════════════════╝
TIMEOUT = {
"low": 180, # 低质量快速出图(3-8 秒实际耗时)
"medium": 360, # 中等质量(20-40 秒实际耗时,推荐)
"high": 900, # 高质量精细渲染(145-280 秒实际耗时)
}
# ╔══════════════════════════════════════════════════════════╗
# ║ 分辨率 → 尺寸映射表(宽高比 × 分辨率 → W×H) ║
# ╚══════════════════════════════════════════════════════════╝
RESOLUTION_SIZES = {
"1:1": {"1K": "1024x1024", "2K": "2048x2048", "4K": "3840x2160"},
"16:9": {"1K": "1536x1024", "2K": "2048x1152", "4K": "3840x2160"},
"9:16": {"1K": "1024x1536", "2K": "1152x2048", "4K": "2160x3840"},
"4:3": {"1K": "1024x768", "2K": "2048x1536", "4K": "3264x2448"},
"3:2": {"1K": "1536x1024", "2K": "2048x1360", "4K": "3456x2304"},
"3:1": {"1K": "1536x512", "2K": "3072x1024", "4K": "3840x1280"},
"1:3": {"1K": "512x1536", "2K": "1024x3072", "4K": "1280x3840"},
}
# ==============================
# OSS 上传工具
# ==============================
def upload_base64_to_oss(image_base64: str) -> str:
"""
将 base64 图片上传到阿里云 OSS 并返回公网 URL
支持带 data:image/...;base64, 前缀和纯 base64 两种情况
"""
base64_str = re.sub(r"^data:image/[^;]+;base64,", "", image_base64)
image_data = base64.b64decode(base64_str)
image_io = io.BytesIO(image_data)
auth = oss2.Auth(ACCESS_KEY_ID, ACCESS_KEY_SECRET)
bucket = oss2.Bucket(auth, ENDPOINT, BUCKET_NAME)
object_name = f"coze/gptimage2_{uuid.uuid4().hex}.png"
bucket.put_object(object_name, image_io)
return f"https://{BUCKET_NAME}.{ENDPOINT}/{object_name}"
# ==============================
# 工具函数
# ==============================
def get_size(aspect_ratio: str, resolution: str) -> str:
"""根据宽高比和分辨率获取推荐尺寸"""
ratio_map = RESOLUTION_SIZES.get(aspect_ratio, RESOLUTION_SIZES["1:1"])
return ratio_map.get(resolution, ratio_map.get("1K", "1024x1024"))
def guess_mime_from_url(url: str) -> str:
"""根据 URL 后缀猜测 MIME 类型"""
url_lower = url.lower()
if url_lower.endswith(".png"):
return "image/png"
if url_lower.endswith(".jpg") or url_lower.endswith(".jpeg"):
return "image/jpeg"
if url_lower.endswith(".webp"):
return "image/webp"
if url_lower.endswith(".gif"):
return "image/gif"
return "image/png"
# ==============================
# 核心:GPT Image 2 生图 / 编辑
# ==============================
def generate_image(prompt: str, aspect_ratio: str, resolution: str,
quality: str, apikey: str, output_format: str = "png",
moderation: str = "auto", image_urls=None):
"""
GPT Image 2 文生图 / 图生图核心函数
- image_urls 为空:纯文生图 → API易 /v1/images/generations(JSON)
- image_urls 不为空:参考图编辑 → API易 /v1/images/edits(multipart/form-data)
"""
size = get_size(aspect_ratio, resolution)
headers = {
"Authorization": f"Bearer {apikey}",
"Content-Type": "application/json"
}
# ── 分支 1:有参考图 → 图生图(编辑) ──
if image_urls:
return _generate_edit(prompt, size, quality, apikey,
output_format, moderation, image_urls, headers)
# ── 分支 2:无参考图 → 文生图(/v1/images/generations,JSON)──
payload = {
"model": "gpt-image-2",
"prompt": prompt,
"size": size,
}
if quality and quality != "auto":
payload["quality"] = quality
if output_format and output_format != "png":
payload["output_format"] = output_format
if moderation and moderation != "auto":
payload["moderation"] = moderation
timeout_seconds = TIMEOUT.get(quality, 360)
api_url = f"{API_BASE}/images/generations"
try:
response = requests.post(
api_url,
headers=headers,
json=payload,
timeout=timeout_seconds
)
# ── HTTP 错误分发 ──
if response.status_code in (400, 403):
try:
err_body = response.json()
err = err_body.get("error", {})
err_msg = err.get("message", "")
except Exception:
err_msg = response.text
if "moderation" in err_msg.lower() or response.status_code == 403:
return {
"success": False,
"errorType": "MODERATION_BLOCKED",
"error": "❌ 内容安全审核不通过\n"
"您的提示词触发了内容安全策略,"
"请修改提示词后重试(不要用原提示词重试)",
}
return {
"success": False,
"errorType": "BAD_REQUEST",
"error": f"❌ 请求参数错误: {err_msg[:500]}\n"
"常见原因:误传了 input_fidelity 或 background:transparent",
}
if response.status_code == 401:
return {
"success": False,
"errorType": "INVALID_API_KEY",
"error": "❌ API Key 无效\n请检查您的 API易 API 密钥是否正确,"
"或是否已过期。可在 https://api.apiyi.com/token 查看",
}
if response.status_code == 429:
return {
"success": False,
"errorType": "RATE_LIMIT",
"error": "❌ 请求频率超限\nAPI 调用过于频繁,"
"可尝试切换线路或降低并发",
}
if response.status_code in (500, 502, 503):
return {
"success": False,
"errorType": "SERVER_ERROR",
"error": f"❌ 服务端故障(HTTP {response.status_code}),"
"请稍后重试或尝试切换线路",
}
if response.status_code != 200:
return {
"success": False,
"errorType": "HTTP_ERROR",
"error": f"HTTP {response.status_code}: "
f"{(response.text or '')[:500]}",
}
# ── JSON 解析 ──
try:
data = response.json()
except ValueError:
return {
"success": False,
"errorType": "INVALID_JSON",
"error": "响应不是有效 JSON",
}
images = data.get("data", [])
if not isinstance(images, list) or len(images) == 0:
return {
"success": False,
"errorType": "NO_DATA",
"error": "生成失败:未返回图片数据(可能触发了 output filter)",
"response": data,
}
# ── 提取 b64_json(API易 返回纯 base64,无 data:image 前缀)──
image_b64 = images[0].get("b64_json", "")
if not image_b64:
return {
"success": False,
"errorType": "NO_IMAGE_DATA",
"error": "生成失败:b64_json 为空(可能被 content_filter 过滤)",
"response": data,
}
return {"success": True, "image_data": image_b64}
except requests.exceptions.Timeout:
return {
"success": False,
"errorType": "TIMEOUT",
"error": f"图片生成请求超时"
f"(超过 {timeout_seconds} 秒,"
f"当前 quality={quality})\n"
f"建议降低 quality 或 resolution 重试",
}
except Exception as e:
return {
"success": False,
"errorType": "EXCEPTION",
"error": f"图片生成请求失败: {str(e)}",
}
def _generate_edit(prompt: str, size: str, quality: str,
apikey: str, output_format: str, moderation: str,
image_urls: list, headers: dict):
"""
GPT Image 2 图生图(编辑)子函数
调用 API易 /v1/images/edits 端点(multipart/form-data 方式上传参考图)
注意:API易 的 /v1/images/edits 要求 Content-Type: multipart/form-data,
通过 -F "image[]=@file" 方式传图,不支持 JSON base64 data URI。
参考图数量最多 16 张,单张 ≤ 50MB(建议压到 1.5MB 以内)。
"""
# ── 下载参考图到内存 ──
image_files = []
for i, url in enumerate(image_urls):
try:
resp = requests.get(url, timeout=180)
if resp.status_code != 200:
return {
"success": False,
"errorType": "IMAGE_DOWNLOAD_FAILED",
"error": f"图片获取失败({url})HTTP {resp.status_code}",
}
mime = guess_mime_from_url(url)
ext = mime.split("/")[-1] # png / jpeg / webp
if ext == "jpeg":
ext = "jpg"
image_files.append(
("image[]", (f"image{i}.{ext}", io.BytesIO(resp.content), mime))
)
except Exception as e:
return {
"success": False,
"errorType": "IMAGE_DOWNLOAD_FAILED",
"error": f"图片获取失败({url}): {e}",
}
# ── 构造 multipart/form-data 请求(-F 方式)──
form_data = {
"model": "gpt-image-2",
"prompt": prompt,
}
if size:
form_data["size"] = size
if quality and quality != "auto":
form_data["quality"] = quality
if output_format and output_format != "png":
form_data["output_format"] = output_format
if moderation and moderation != "auto":
form_data["moderation"] = moderation
# multipart/form-data 不传 Content-Type(让 requests 自动生成 boundary)
auth_headers = {
"Authorization": headers["Authorization"],
}
timeout_seconds = TIMEOUT.get(quality, 360)
api_url = f"{API_BASE}/images/edits"
try:
response = requests.post(
api_url,
headers=auth_headers,
data=form_data,
files=image_files,
timeout=timeout_seconds
)
# ── 错误处理 ──
if response.status_code in (400, 403):
try:
err = response.json().get("error", {})
err_msg = err.get("message", "")
except Exception:
err_msg = response.text[:500]
if "moderation" in err_msg.lower() or response.status_code == 403:
return {
"success": False,
"errorType": "MODERATION_BLOCKED",
"error": "❌ 内容安全审核不通过",
}
return {
"success": False,
"errorType": "EDIT_FAILED",
"error": f"图片编辑请求参数错误: {err_msg[:500]}\n"
"常见原因:误传了 input_fidelity、background:transparent、"
"超过 16 张参考图或单张超过 50MB",
}
if response.status_code == 401:
return {
"success": False,
"errorType": "INVALID_API_KEY",
"error": "❌ API Key 无效",
}
if response.status_code == 429:
return {
"success": False,
"errorType": "RATE_LIMIT",
"error": "❌ 请求频率超限,可尝试切换线路重试",
}
if response.status_code in (500, 502, 503):
return {
"success": False,
"errorType": "SERVER_ERROR",
"error": f"❌ 服务端故障(HTTP {response.status_code})",
}
if response.status_code != 200:
try:
err = response.json().get("error", {}).get("message", "")
except Exception:
err = response.text[:500]
return {
"success": False,
"errorType": "EDIT_FAILED",
"error": f"图片编辑失败(HTTP {response.status_code}): {err}",
}
# ── 提取图片(b64_json 是纯 base64,无前缀)──
data = response.json()
images = data.get("data", [])
if not images:
return {
"success": False,
"errorType": "NO_DATA",
"error": "编辑结果为空",
}
image_b64 = images[0].get("b64_json", "")
if not image_b64:
return {
"success": False,
"errorType": "NO_IMAGE_DATA",
"error": "编辑结果图片数据为空",
}
return {"success": True, "image_data": image_b64}
except requests.exceptions.Timeout:
return {
"success": False,
"errorType": "TIMEOUT",
"error": f"图片编辑请求超时(超过 {timeout_seconds} 秒)",
}
except Exception as e:
return {
"success": False,
"errorType": "EXCEPTION",
"error": f"图片编辑请求失败: {str(e)}",
}
# ==============================
# Coze Node 入口
# ==============================
def handler(args: Args[Input]) -> Output:
"""
Coze / GPT Image 2(API易 代理)节点入口
- args.input.cleantext: 用户文字提示词
- args.input.fileurls: 参考图 URL 列表(用于图生图)
- args.input.aspect_ratio: 宽高比,如 "1:1" / "16:9" / "9:16"
- args.input.resolution: 分辨率,如 "1K" / "2K" / "4K"
- args.input.quality: 质量等级,如 "low" / "medium" / "high"(默认 auto)
- args.input.moderation: 审核强度,如 "auto" / "low"(默认 auto)
- args.input.output_format: 输出格式,如 "png" / "jpeg" / "webp"(默认 png)
- args.input.apikey: API易 API Key(sk-开头)
"""
API_KEY = args.input.apikey
cleantext = args.input.cleantext or ""
fileurls = args.input.fileurls or []
aspect_ratio = args.input.aspect_ratio or "1:1"
resolution = args.input.resolution or "1K"
quality = getattr(args.input, 'quality', None) or "auto"
output_format = getattr(args.input, 'output_format', None) or "png"
moderation = getattr(args.input, 'moderation', None) or "auto"
prompt = cleantext.strip()
if not prompt:
prompt = "根据参考图片进行合理的编辑与优化。"
# 调用 GPT Image 2 生图 / 编辑
result = generate_image(
prompt=prompt,
aspect_ratio=aspect_ratio,
resolution=resolution,
quality=quality,
apikey=API_KEY,
output_format=output_format,
moderation=moderation,
image_urls=fileurls if fileurls else None
)
if result["success"]:
image_base64 = result["image_data"]
oss_url = upload_base64_to_oss(image_base64)
return {
"analysis": "图片生成成功",
"url": oss_url,
"error": None,
}
else:
return {
"analysis": "图片生成失败",
"url": None,
"error": result.get("error", "未知错误"),
}