revert: restore original image generation config

This commit is contained in:
2026-05-26 14:02:35 +08:00
parent ffdb60c463
commit bdb7226642
14 changed files with 102 additions and 601 deletions

View File

@@ -132,169 +132,26 @@ IMAGE_SIZE_CHOICES = [
"id": "auto",
"label": "自动",
"value": "auto",
"ratio": "auto",
"width": 0,
"height": 0,
"description": "由图片模型自行决定输出尺寸,生成后显示实际像素",
"description": "由图片模型自行决定输出尺寸",
},
{
"id": "1024x1536",
"label": "竖图 2:3 · 1024×1536",
"label": "竖图 2:3",
"value": "1024x1536",
"ratio": "2:3",
"width": 1024,
"height": 1536,
"description": "适合信息流营销图、人物和产品竖版构图",
},
{
"id": "1536x2304",
"label": "竖图 2:3 · 1536×2304",
"value": "1536x2304",
"ratio": "2:3",
"width": 1536,
"height": 2304,
"description": "适合高精细竖版海报和后期裁切",
},
{
"id": "1088x1920",
"label": "竖屏 9:16 · 1088×1920",
"value": "1088x1920",
"ratio": "9:16",
"width": 1088,
"height": 1920,
"description": "接近 1080p 竖屏,宽度按 16 像素倍数提交",
},
{
"id": "1440x2560",
"label": "竖屏 9:16 · 1440×2560",
"value": "1440x2560",
"ratio": "9:16",
"width": 1440,
"height": 2560,
"description": "适合短视频封面、竖屏高清素材和二次裁切",
},
{
"id": "960x1280",
"label": "竖图 3:4 · 960×1280",
"value": "960x1280",
"ratio": "3:4",
"width": 960,
"height": 1280,
"description": "适合偏人物或产品竖图,文件体积较轻",
},
{
"id": "1536x1920",
"label": "竖图 4:5 · 1536×1920",
"value": "1536x1920",
"ratio": "4:5",
"width": 1536,
"height": 1920,
"description": "适合小红书、社媒封面和产品展示图",
},
{
"id": "1024x1024",
"label": "方图 1:1 · 1024×1024",
"label": "方图 1:1",
"value": "1024x1024",
"ratio": "1:1",
"width": 1024,
"height": 1024,
"description": "适合头像、方形素材和电商图",
},
{
"id": "2048x2048",
"label": "方图 1:1 · 2048×2048",
"value": "2048x2048",
"ratio": "1:1",
"width": 2048,
"height": 2048,
"description": "适合高清方形素材和后期抠图",
},
{
"id": "1536x1024",
"label": "横图 3:2 · 1536×1024",
"label": "横图 3:2",
"value": "1536x1024",
"ratio": "3:2",
"width": 1536,
"height": 1024,
"description": "适合横版封面和详情页配图",
},
{
"id": "2304x1536",
"label": "横图 3:2 · 2304×1536",
"value": "2304x1536",
"ratio": "3:2",
"width": 2304,
"height": 1536,
"description": "适合高清横版主视觉和详情页大图",
},
{
"id": "1280x720",
"label": "横屏 16:9 · 1280×720",
"value": "1280x720",
"ratio": "16:9",
"width": 1280,
"height": 720,
"description": "适合轻量横版封面、网页首屏和视频首帧",
},
{
"id": "2048x1152",
"label": "横屏 16:9 · 2048×1152",
"value": "2048x1152",
"ratio": "16:9",
"width": 2048,
"height": 1152,
"description": "适合高清横版视频封面和大屏展示",
},
]
GEMINI_IMAGE_SIZE_CHOICES = [
{
"id": "auto",
"label": "自动",
"value": "auto",
"ratio": "auto",
"width": 0,
"height": 0,
"description": "由 Gemini 自行决定输出尺寸,生成后显示实际像素",
},
{"id": "1024x1024", "label": "方图 1:1 · 1K · 1024×1024", "value": "1024x1024", "ratio": "1:1", "image_size": "1K", "width": 1024, "height": 1024},
{"id": "848x1264", "label": "竖图 2:3 · 1K · 848×1264", "value": "848x1264", "ratio": "2:3", "image_size": "1K", "width": 848, "height": 1264},
{"id": "1264x848", "label": "横图 3:2 · 1K · 1264×848", "value": "1264x848", "ratio": "3:2", "image_size": "1K", "width": 1264, "height": 848},
{"id": "896x1200", "label": "竖图 3:4 · 1K · 896×1200", "value": "896x1200", "ratio": "3:4", "image_size": "1K", "width": 896, "height": 1200},
{"id": "928x1152", "label": "竖图 4:5 · 1K · 928×1152", "value": "928x1152", "ratio": "4:5", "image_size": "1K", "width": 928, "height": 1152},
{"id": "768x1376", "label": "竖屏 9:16 · 1K · 768×1376", "value": "768x1376", "ratio": "9:16", "image_size": "1K", "width": 768, "height": 1376},
{"id": "1376x768", "label": "横屏 16:9 · 1K · 1376×768", "value": "1376x768", "ratio": "16:9", "image_size": "1K", "width": 1376, "height": 768},
{"id": "2048x2048", "label": "方图 1:1 · 2K · 2048×2048", "value": "2048x2048", "ratio": "1:1", "image_size": "2K", "width": 2048, "height": 2048},
{"id": "1696x2528", "label": "竖图 2:3 · 2K · 1696×2528", "value": "1696x2528", "ratio": "2:3", "image_size": "2K", "width": 1696, "height": 2528},
{"id": "2528x1696", "label": "横图 3:2 · 2K · 2528×1696", "value": "2528x1696", "ratio": "3:2", "image_size": "2K", "width": 2528, "height": 1696},
{"id": "1792x2400", "label": "竖图 3:4 · 2K · 1792×2400", "value": "1792x2400", "ratio": "3:4", "image_size": "2K", "width": 1792, "height": 2400},
{"id": "1856x2304", "label": "竖图 4:5 · 2K · 1856×2304", "value": "1856x2304", "ratio": "4:5", "image_size": "2K", "width": 1856, "height": 2304},
{"id": "1536x2752", "label": "竖屏 9:16 · 2K · 1536×2752", "value": "1536x2752", "ratio": "9:16", "image_size": "2K", "width": 1536, "height": 2752},
{"id": "2752x1536", "label": "横屏 16:9 · 2K · 2752×1536", "value": "2752x1536", "ratio": "16:9", "image_size": "2K", "width": 2752, "height": 1536},
{"id": "4096x4096", "label": "方图 1:1 · 4K · 4096×4096", "value": "4096x4096", "ratio": "1:1", "image_size": "4K", "width": 4096, "height": 4096},
{"id": "3392x5056", "label": "竖图 2:3 · 4K · 3392×5056", "value": "3392x5056", "ratio": "2:3", "image_size": "4K", "width": 3392, "height": 5056},
{"id": "5056x3392", "label": "横图 3:2 · 4K · 5056×3392", "value": "5056x3392", "ratio": "3:2", "image_size": "4K", "width": 5056, "height": 3392},
{"id": "3072x5504", "label": "竖屏 9:16 · 4K · 3072×5504", "value": "3072x5504", "ratio": "9:16", "image_size": "4K", "width": 3072, "height": 5504},
{"id": "5504x3072", "label": "横屏 16:9 · 4K · 5504×3072", "value": "5504x3072", "ratio": "16:9", "image_size": "4K", "width": 5504, "height": 3072},
]
IMAGE_QUALITY_CHOICES = [
{
"id": "low",
"label": "低 · 快速草稿",
"value": "low",
"description": "更快生成,适合批量试方向",
},
{
"id": "medium",
"label": "中 · 常规出图",
"value": "medium",
"description": "速度和质量折中,适合日常迭代",
},
{
"id": "high",
"label": "高 · 最终稿",
"value": "high",
"description": "质量优先,适合定稿和高清素材",
},
]
VIDEO_SIZE_CHOICES = [
{
@@ -597,10 +454,6 @@ class GeneratedImage(BaseModel):
model: str
mode: str = "edit" # "edit"(带参考图) | "text"(纯文字)
url: str # /jobs/{job_id}/frames/{idx}/gen/{id}.jpg
size: str = ""
quality: str = ""
width: int = 0
height: int = 0
selected: bool = False
created_at: float = 0.0
@@ -4714,9 +4567,6 @@ def image_model_options() -> list[dict]:
"model": GPT_IMAGE_MODEL,
"description": "优先 GPT Image 2必要时按后端熔断和兜底策略切到备用图片模型",
"available": bool(IMAGE_API_KEY),
"size_options": IMAGE_SIZE_CHOICES,
"quality_options": IMAGE_QUALITY_CHOICES,
"supports_custom_size": True,
},
{
"id": GPT_IMAGE_MODEL,
@@ -4724,9 +4574,6 @@ def image_model_options() -> list[dict]:
"model": GPT_IMAGE_MODEL,
"description": "主生图模型,适合营销图和参考图重绘",
"available": bool(IMAGE_API_KEY),
"size_options": IMAGE_SIZE_CHOICES,
"quality_options": IMAGE_QUALITY_CHOICES,
"supports_custom_size": True,
},
]
if IMAGE_FALLBACK_ENABLED and IMAGE_FALLBACK_MODEL and IMAGE_FALLBACK_MODEL != GPT_IMAGE_MODEL:
@@ -4734,11 +4581,8 @@ def image_model_options() -> list[dict]:
"id": IMAGE_FALLBACK_MODEL,
"label": "Gemini 图片",
"model": IMAGE_FALLBACK_MODEL,
"description": "备用图片模型,使用 Gemini 官方比例和 1K/2K/4K 固定规格",
"description": "备用图片模型,适合主模型慢或失败时手动选择",
"available": bool(IMAGE_API_KEY),
"size_options": GEMINI_IMAGE_SIZE_CHOICES,
"quality_options": [],
"supports_custom_size": False,
})
return options
@@ -4747,145 +4591,28 @@ def image_size_options() -> list[dict]:
return IMAGE_SIZE_CHOICES
def gemini_image_size_options() -> list[dict]:
return GEMINI_IMAGE_SIZE_CHOICES
def image_quality_options() -> list[dict]:
return IMAGE_QUALITY_CHOICES
def _parse_image_dimensions(value: str) -> tuple[int, int] | None:
normalized = value.strip().lower().replace("×", "x")
m = re.fullmatch(r"(\d{3,4})\s*x\s*(\d{3,4})", normalized)
if not m:
return None
return int(m.group(1)), int(m.group(2))
def _validate_custom_image_size(width: int, height: int, raw: str) -> str:
pixels = width * height
long_edge = max(width, height)
short_edge = min(width, height)
if width % 16 != 0 or height % 16 != 0:
raise HTTPException(400, f"unsupported image size: {raw} (宽高必须都是 16 的倍数,例如 1088x1920)")
if long_edge > 3840:
raise HTTPException(400, f"unsupported image size: {raw} (最长边不能超过 3840px)")
if long_edge / short_edge > 3:
raise HTTPException(400, f"unsupported image size: {raw} (画幅比例不能超过 3:1)")
if pixels < 655_360 or pixels > 8_294_400:
raise HTTPException(400, f"unsupported image size: {raw} (总像素需在 655360 到 8294400 之间)")
return f"{width}x{height}"
def _is_gemini_image_model(model: str | None) -> bool:
normalized = (model or "").strip().lower()
return bool(normalized and normalized.startswith("gemini")) or (
bool(IMAGE_FALLBACK_MODEL) and normalized == IMAGE_FALLBACK_MODEL.lower()
)
def _normalize_image_size(raw: str | None, model: str | None = GPT_IMAGE_MODEL, fallback_to_auto: bool = False) -> str:
def _normalize_image_size(raw: str | None) -> str:
value = (raw or "auto").strip().lower()
gpt_aliases = {
"9:16": "1088x1920",
"9x16": "1088x1920",
"16:9": "1280x720",
"16x9": "1280x720",
"1:1": "1024x1024",
"1x1": "1024x1024",
"2:3": "1024x1536",
"2x3": "1024x1536",
"3:2": "1536x1024",
"3x2": "1536x1024",
"3:4": "960x1280",
"3x4": "960x1280",
"4:5": "1536x1920",
"4x5": "1536x1920",
aliases = {
"vertical": "1024x1536",
"portrait": "1024x1536",
"竖图": "1024x1536",
"竖屏": "1088x1920",
"square": "1024x1024",
"方图": "1024x1024",
"horizontal": "1536x1024",
"landscape": "1536x1024",
"横图": "1536x1024",
"横屏": "1280x720",
}
gemini_aliases = {
"1:1": "1024x1024",
"1x1": "1024x1024",
"2:3": "848x1264",
"2x3": "848x1264",
"3:2": "1264x848",
"3x2": "1264x848",
"3:4": "896x1200",
"3x4": "896x1200",
"4:5": "928x1152",
"4x5": "928x1152",
"9:16": "768x1376",
"9x16": "768x1376",
"16:9": "1376x768",
"16x9": "1376x768",
"竖屏": "768x1376",
"横屏": "1376x768",
"方图": "1024x1024",
}
if _is_gemini_image_model(model):
value = gemini_aliases.get(value, value)
allowed = {str(item["value"]) for item in GEMINI_IMAGE_SIZE_CHOICES}
if value in allowed:
return value
if fallback_to_auto:
return "auto"
raise HTTPException(400, f"unsupported Gemini image size: {raw}")
value = gpt_aliases.get(value, value)
allowed = {str(item["value"]) for item in IMAGE_SIZE_CHOICES}
if value in allowed:
return value
dimensions = _parse_image_dimensions(value)
if dimensions:
return _validate_custom_image_size(dimensions[0], dimensions[1], raw or value)
raise HTTPException(400, f"unsupported image size: {raw}")
def _image_size_payload(raw: str | None, model: str | None = GPT_IMAGE_MODEL, fallback_to_auto: bool = False) -> dict:
size = _normalize_image_size(raw, model, fallback_to_auto=fallback_to_auto)
return {} if size == "auto" else {"size": size}
def _normalize_image_quality(raw: str | None) -> str:
value = (raw or "high").strip().lower()
aliases = {
"standard": "high",
"hd": "high",
"best": "high",
"": "high",
"high-quality": "high",
"normal": "medium",
"regular": "medium",
"": "medium",
"medium-quality": "medium",
"draft": "low",
"fast": "low",
"": "low",
"low-quality": "low",
}
value = aliases.get(value, value)
allowed = {str(item["value"]) for item in IMAGE_QUALITY_CHOICES}
allowed = {str(item["value"]) for item in IMAGE_SIZE_CHOICES}
if value not in allowed:
raise HTTPException(400, f"unsupported image quality: {raw}")
raise HTTPException(400, f"unsupported image size: {raw}")
return value
def _image_quality_payload(raw: str | None, model: str | None) -> dict:
quality = _normalize_image_quality(raw)
return {"quality": quality} if model == GPT_IMAGE_MODEL else {}
def _image_options_payload(size: str | None, quality: str | None, model: str | None) -> dict:
return {**_image_size_payload(size, model, fallback_to_auto=True), **_image_quality_payload(quality, model)}
def _image_size_payload(raw: str | None) -> dict:
size = _normalize_image_size(raw)
return {} if size == "auto" else {"size": size}
def video_duration_options() -> list[int]:
@@ -5047,12 +4774,12 @@ def _image_endpoint(path: str) -> str:
return f"{base}/{path.lstrip('/')}"
def _image_generation_response(prompt: str, model: str, size: str | None = "auto", quality: str | None = "high") -> dict:
def _image_generation_response(prompt: str, model: str, size: str | None = "auto") -> dict:
with ai_http_client(timeout=IMAGE_REQUEST_TIMEOUT_SECONDS) as client:
r = client.post(
_image_endpoint("/images/generations"),
headers={"Authorization": f"Bearer {IMAGE_API_KEY}"},
json={"model": model, "prompt": prompt, "n": 1, **_image_options_payload(size, quality, model)},
json={"model": model, "prompt": prompt, "n": 1, **_image_size_payload(size)},
)
r.raise_for_status()
return r.json()
@@ -6381,8 +6108,6 @@ def health() -> dict:
"image_request_timeout_seconds": IMAGE_REQUEST_TIMEOUT_SECONDS,
"image_options": image_model_options(),
"image_size_options": image_size_options(),
"gemini_image_size_options": gemini_image_size_options(),
"image_quality_options": image_quality_options(),
"ai_proxy_configured": bool(AI_HTTP_PROXY),
"image_fallbacks": _image_fallback_models(),
"image_circuit": _image_circuit_snapshot(),
@@ -6871,8 +6596,7 @@ class GenerateReq(BaseModel):
extra_prompt: str = "" # ✓ 需要的元素(正向)
negative_prompt: str = "" # ✗ 不需要的元素(负向)
model: str = "auto" # auto / gpt-image-2 / gemini-3-pro-image-preview
size: str = "auto" # auto / 1024x1536 / 1088x1920 / custom WxH
quality: str = "high" # low / medium / high
size: str = "auto" # auto / 1024x1536 / 1024x1024 / 1536x1024
mode: str = "edit" # "edit" 带参考图,"text" 纯文字
from_selected: bool = False # True 时优先用 frame.selected 的生成图作 reference迭代否则原关键帧
@@ -6909,10 +6633,7 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job:
if not raw_prompt:
raise HTTPException(400, "prompt required")
full_prompt = _ensure_english(raw_prompt)
requested_model = _normalize_image_model_preference(req.model)
strict_size_model = IMAGE_FALLBACK_MODEL if requested_model == IMAGE_FALLBACK_MODEL else GPT_IMAGE_MODEL
image_size = _normalize_image_size(req.size, strict_size_model)
image_quality = _normalize_image_quality(req.quality)
image_size = _normalize_image_size(req.size)
if not IMAGE_API_KEY:
raise HTTPException(503, "IMAGE_API_KEY 或 LLM_API_KEY 未配置")
@@ -6953,14 +6674,14 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job:
headers={
"Authorization": f"Bearer {IMAGE_API_KEY}",
},
data={"model": current_model, "prompt": full_prompt, "n": "1", **_image_options_payload(image_size, image_quality, current_model)},
data={"model": current_model, "prompt": full_prompt, "n": "1", **_image_size_payload(image_size)},
files={"image": ("reference.jpg", img_bytes_in, "image/jpeg")},
)
r.raise_for_status()
resp_data = r.json()
else:
# text-only
resp_data = _image_generation_response(full_prompt, current_model, image_size, image_quality)
resp_data = _image_generation_response(full_prompt, current_model, image_size)
if resp_data.get("data"):
effective_mode = f"{current_mode}:{current_model}"
@@ -7025,13 +6746,6 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job:
gen_dir.mkdir(parents=True, exist_ok=True)
out_path = gen_dir / f"{idx:03d}_{gen_id}.jpg"
out_path.write_bytes(out_bytes)
actual_width = 0
actual_height = 0
try:
with Image.open(io.BytesIO(out_bytes)) as im:
actual_width, actual_height = im.size
except Exception:
pass
new_gen = GeneratedImage(
id=gen_id,
@@ -7039,10 +6753,6 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job:
model=model,
mode=effective_mode,
url=f"/jobs/{job_id}/frames/{idx}/gen/{gen_id}.jpg",
size=image_size,
quality=image_quality,
width=actual_width,
height=actual_height,
selected=False,
created_at=_time.time(),
)