feat: 试用过程中的细节优化
This commit is contained in:
@@ -10,6 +10,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import io
|
||||
import logging
|
||||
import mimetypes
|
||||
import re
|
||||
@@ -17,6 +18,7 @@ from dataclasses import dataclass
|
||||
from typing import Awaitable, Callable
|
||||
|
||||
import httpx
|
||||
from PIL import Image, ImageOps
|
||||
|
||||
from config import get_settings
|
||||
from services.prompts import build_context, build_prompt, type_name
|
||||
@@ -84,8 +86,8 @@ def _image_size(provider: str, ratio: str, is_wan: bool = True, model: str = "")
|
||||
# gpt-image 自定义尺寸约束:16 的倍数、长短边比 ≤ 3:1(1536x2048 合法)
|
||||
return "1536x2048" if ratio == "3:4" else "2048x2048"
|
||||
if provider == "pcore":
|
||||
# pcore 的 size 是枚举(默认 1024x1024),不接受自定义尺寸;用 gpt-image 枚举:竖版 1024x1536(近似 3:4)
|
||||
return "1024x1536" if ratio == "3:4" else "1024x1024"
|
||||
# 竖图必须与 Ozon 的 3:4 规格一致。1024x1536 实际是 2:3,会使成图明显偏瘦高。
|
||||
return "1152x1536" if ratio == "3:4" else "1024x1024"
|
||||
# tongyi:万象与千问的 size 语法相同(* 分隔),档位不同
|
||||
# wan2.6 系列总像素限制在 [1280², 1440²],wan2.7 的 1536*2048/2048*2048 会超限
|
||||
if model.startswith("wan2.6"):
|
||||
@@ -141,6 +143,75 @@ async def _resolve_ref(url: str) -> str:
|
||||
return _bytes_to_data_uri(data, mime)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OutpaintCanvas:
|
||||
"""用于受限扩图的预处理结果。
|
||||
|
||||
``image_data_url`` 与 ``mask_data_url`` 尺寸严格一致:蒙版不透明的区域是原图,
|
||||
透明区域才允许供应商生成。最终仍会把 ``original`` 覆盖回结果,防止供应商在
|
||||
蒙版区域有轻微重绘。
|
||||
"""
|
||||
|
||||
image_data_url: str
|
||||
mask_data_url: str
|
||||
original: Image.Image
|
||||
box: tuple[int, int, int, int]
|
||||
size: tuple[int, int]
|
||||
|
||||
def restore_original(self, generated: bytes) -> bytes:
|
||||
"""恢复原图区域,确保输出中原图像素不受模型影响。"""
|
||||
with Image.open(io.BytesIO(generated)) as result:
|
||||
result = ImageOps.exif_transpose(result).convert("RGBA")
|
||||
if result.size != self.size:
|
||||
# 供应商偶发不遵从 size 时仍输出固定 3:4;随后会精确覆盖原图区域。
|
||||
result = ImageOps.fit(result, self.size, method=Image.Resampling.LANCZOS)
|
||||
result.alpha_composite(self.original, self.box[:2])
|
||||
out = io.BytesIO()
|
||||
result.save(out, format="PNG")
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def _png_data_url(image: Image.Image) -> str:
|
||||
out = io.BytesIO()
|
||||
image.save(out, format="PNG")
|
||||
return _bytes_to_data_uri(out.getvalue(), "image/png")
|
||||
|
||||
|
||||
async def prepare_outpaint_canvas(url: str, size: str) -> OutpaintCanvas:
|
||||
"""把单张参考图置于目标画布,并生成仅开放新增边缘的 PNG alpha 蒙版。"""
|
||||
try:
|
||||
target_width, target_height = (int(value) for value in size.lower().split("x", 1))
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError(f"仅扩图需要合法的宽高尺寸,收到: {size}") from exc
|
||||
if target_width <= 0 or target_height <= 0:
|
||||
raise ValueError(f"仅扩图需要正数宽高尺寸,收到: {size}")
|
||||
|
||||
data, _ = await _resolve_ref_bytes(url)
|
||||
with Image.open(io.BytesIO(data)) as opened:
|
||||
source = ImageOps.exif_transpose(opened).convert("RGBA")
|
||||
if source.width <= 0 or source.height <= 0:
|
||||
raise ValueError("参考底图尺寸无效")
|
||||
scale = min(target_width / source.width, target_height / source.height)
|
||||
source_width = max(1, round(source.width * scale))
|
||||
source_height = max(1, round(source.height * scale))
|
||||
original = source.resize((source_width, source_height), Image.Resampling.LANCZOS)
|
||||
|
||||
x = (target_width - original.width) // 2
|
||||
y = (target_height - original.height) // 2
|
||||
canvas = Image.new("RGBA", (target_width, target_height), (0, 0, 0, 0))
|
||||
canvas.alpha_composite(original, (x, y))
|
||||
# OpenAI/GPT Image 兼容蒙版约定:透明像素是可编辑区域;原图区域不透明。
|
||||
mask = Image.new("RGBA", (target_width, target_height), (0, 0, 0, 0))
|
||||
mask.paste((255, 255, 255, 255), (x, y, x + original.width, y + original.height))
|
||||
return OutpaintCanvas(
|
||||
image_data_url=_png_data_url(canvas),
|
||||
mask_data_url=_png_data_url(mask),
|
||||
original=original,
|
||||
box=(x, y, x + original.width, y + original.height),
|
||||
size=(target_width, target_height),
|
||||
)
|
||||
|
||||
|
||||
# ── Provider:豆包 Seedream(火山方舟)────────────────────────────────────
|
||||
|
||||
async def generate_doubao(prompt: str, ref_images: list[str], size: str = "2048x2048", model: str | None = None) -> bytes:
|
||||
@@ -562,6 +633,7 @@ async def advance_generation(
|
||||
model: str,
|
||||
provider_task_id: str | None = None,
|
||||
provider_task_kind: str | None = None,
|
||||
edit_mask: str | None = None,
|
||||
) -> GenerationAdvance:
|
||||
"""推进一次图片生成,不在本函数内无限轮询。
|
||||
|
||||
@@ -569,6 +641,8 @@ async def advance_generation(
|
||||
能继续外部任务,且在等待期间可以领取其他任务。豆包和同步千问仍在一次调用内完成。
|
||||
"""
|
||||
settings = get_settings()
|
||||
if edit_mask and provider != "pcore":
|
||||
raise RuntimeError("带蒙版的仅扩图当前仅支持 Pcore gpt-image-2")
|
||||
if provider == "doubao":
|
||||
return GenerationAdvance(data=await generate_doubao(prompt, ref_images, size=size, model=model))
|
||||
|
||||
@@ -650,6 +724,10 @@ async def advance_generation(
|
||||
body: dict = {"model": model, "prompt": prompt, "n": 1, "size": size, "async": True}
|
||||
if ref_images:
|
||||
body["images"] = [{"image_url": await _resolve_ref(item)} for item in ref_images]
|
||||
if edit_mask:
|
||||
# Pcore 的图片参数使用 JSON {image_url};蒙版沿用相同的 OpenAI 兼容对象形式。
|
||||
# 透明区域允许编辑,不透明区域保留。若供应商轻微偏移,Worker 会二次覆盖原图。
|
||||
body["mask"] = {"image_url": edit_mask}
|
||||
response = await client.post(
|
||||
f"{base}/v1/images/{kind}", headers={**headers, "Content-Type": "application/json"}, json=body,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user