fix: send product angle refs as image inputs

This commit is contained in:
2026-05-18 07:10:27 +08:00
parent 5fde9f3e22
commit 05283aed52
2 changed files with 35 additions and 56 deletions

View File

@@ -2580,8 +2580,22 @@ def _image_error_status(error: Exception) -> int:
return 503 if ("上游负载饱和" in msg or "HTTP 429" in msg or "saturated" in msg.lower()) else 500
def _prepare_image_edit_bytes(image_path: Path, max_side: int) -> bytes:
import io as _io
from PIL import Image as _PILImage
try:
im = _PILImage.open(image_path)
if max(im.size) > max_side:
im.thumbnail((max_side, max_side), _PILImage.LANCZOS)
buf = _io.BytesIO()
im.convert("RGB").save(buf, format="JPEG", quality=88)
return buf.getvalue()
except Exception:
return image_path.read_bytes()
def _image_edit_call(
image_path: Path,
image_path: Path | list[Path],
prompt: str,
model: str | None = None,
models: list[str] | None = None,
@@ -2592,28 +2606,20 @@ def _image_edit_call(
"""通用 image edit 调用 · 失败重试 + 可选 text fallback。
返回 (image_bytes, effective_mode) where effective_mode in {"edit","text"}。
失败 raise RuntimeError。
输入图自动 resize 到 max_side默认 1024边长后再 base64
输入图自动 resize 到 max_side默认 1024边长后再用 multipart 上传;多参考图使用 image[]
生图模型按产品规则强制使用 gpt-image-2model/models 参数只保留兼容旧调用。"""
import base64 as b64lib
import io as _io
import time as _time
import httpx
from PIL import Image as _PILImage
if not IMAGE_API_KEY:
raise RuntimeError("IMAGE_API_KEY 或 LLM_API_KEY 未配置")
models_cycle = [GPT_IMAGE_MODEL]
model = GPT_IMAGE_MODEL
# 缩到 max_side 内
try:
im = _PILImage.open(image_path)
if max(im.size) > max_side:
im.thumbnail((max_side, max_side), _PILImage.LANCZOS)
buf = _io.BytesIO()
im.convert("RGB").save(buf, format="JPEG", quality=88)
img_bytes_in = buf.getvalue()
except Exception:
# PIL 失败兜底走原文件
img_bytes_in = image_path.read_bytes()
image_paths = image_path if isinstance(image_path, list) else [image_path]
image_paths = [path for path in image_paths if path and path.exists()][:6]
if not image_paths:
raise RuntimeError("image edit reference image missing")
img_bytes_list = [_prepare_image_edit_bytes(path, max_side) for path in image_paths]
plan: list[str] = ["edit"] * max_attempts
if fallback_text:
plan.append("text")
@@ -2636,7 +2642,14 @@ def _image_edit_call(
"Authorization": f"Bearer {IMAGE_API_KEY}",
},
data={"model": current_model, "prompt": prompt, "n": "1"},
files={"image": ("reference.jpg", img_bytes_in, "image/jpeg")},
files=(
{"image": ("reference.jpg", img_bytes_list[0], "image/jpeg")}
if len(img_bytes_list) == 1
else [
("image[]", (f"reference_{idx + 1}.jpg", img_bytes, "image/jpeg"))
for idx, img_bytes in enumerate(img_bytes_list)
]
),
)
r.raise_for_status()
resp_data = r.json()
@@ -5123,29 +5136,6 @@ def analyze_product_views(job_id: str, req: AnalyzeProductViewsReq) -> dict:
return {"items": items, "missing_views": missing}
def _make_product_angle_reference_sheet(paths: list[Path], out_path: Path) -> Path:
thumbs: list[Image.Image] = []
for path in paths[:6]:
try:
img = ImageOps.exif_transpose(Image.open(path)).convert("RGB")
img.thumbnail((520, 520), Image.Resampling.LANCZOS)
cell = Image.new("RGB", (560, 560), (255, 255, 255))
cell.paste(img, ((560 - img.width) // 2, (560 - img.height) // 2))
thumbs.append(cell)
except Exception:
continue
if not thumbs:
raise RuntimeError("no usable product reference images")
cols = 3 if len(thumbs) > 2 else len(thumbs)
rows = (len(thumbs) + cols - 1) // cols
sheet = Image.new("RGB", (cols * 560, rows * 560), (245, 245, 245))
for i, thumb in enumerate(thumbs):
sheet.paste(thumb, ((i % cols) * 560, (i // cols) * 560))
out_path.parent.mkdir(parents=True, exist_ok=True)
sheet.save(out_path, "JPEG", quality=94)
return out_path
@app.post("/jobs/{job_id}/assets/product-angle")
def generate_product_angle_asset(job_id: str, req: GenerateProductAngleAssetReq) -> dict:
if job_id not in JOBS:
@@ -5165,11 +5155,6 @@ def generate_product_angle_asset(job_id: str, req: GenerateProductAngleAssetReq)
if not source_paths:
raise HTTPException(404, "source product image not found")
source_path = source_paths[0]
model_src = source_path
sheet_tmp: Path | None = None
if len(source_paths) > 1:
sheet_tmp = job_dir(job_id) / "tmp" / f"product_angle_refs_{uuid.uuid4().hex[:8]}.jpg"
model_src = _make_product_angle_reference_sheet(source_paths, sheet_tmp)
target_view = (req.target_view or "目标视角").strip()
note = (req.note or "").strip()
source_notes = [re.sub(r"\s+", " ", str(item)).strip()[:180] for item in (req.source_notes or []) if str(item).strip()]
@@ -5181,11 +5166,11 @@ def generate_product_angle_asset(job_id: str, req: GenerateProductAngleAssetReq)
else ""
)
prompt = (
"Use the reference image or reference board as evidence for the same SKG neck-and-shoulder wearable massage product. "
"If a reference board is provided, all panels are the same product from uploaded views; do not output a board, collage, or multiple products. "
"Use all provided reference images as evidence for the same SKG neck-and-shoulder wearable massage product. "
"Each input image is one uploaded view of the same product; do not output a board, collage, or multiple products. "
f"Generate a clean product-only white-background reference image in this missing view: {target_view}. "
+ source_note_clause
"Preserve the exact product identity: white U-shaped wearable neck and shoulder massager that sits around the neck, asymmetric wearer-left and wearer-right details, side buttons, inner metal massage contacts, opening width, material, thickness, curvature, and real shoulder-neck wearing scale. "
+ "Preserve the exact product identity: white U-shaped wearable neck and shoulder massager that sits around the neck, asymmetric wearer-left and wearer-right details, side buttons, inner metal massage contacts, opening width, material, thickness, curvature, and real shoulder-neck wearing scale. "
"Use product coordinates: wearer-left/right are the user's body left/right when worn, top is near chin/upper neck, bottom is near collarbone/shoulders, inner side touches skin, outer side is the shell/buttons. "
"Do not mirror both sides into identical shapes; keep visible left/right asymmetry and believable shoulder-neck wearable proportions. "
"The product should be complete, centered, isolated on pure white, large enough to inspect, with no hands, people, packaging, text, UI, watermark, extra accessories, or scene background. "
@@ -5194,15 +5179,9 @@ def generate_product_angle_asset(job_id: str, req: GenerateProductAngleAssetReq)
)
models = [GPT_IMAGE_MODEL]
try:
img_bytes, _mode = _image_edit_call(model_src, prompt, models=models, fallback_text=False, max_attempts=5, max_side=1600)
img_bytes, _mode = _image_edit_call(source_paths, prompt, models=models, fallback_text=False, max_attempts=5, max_side=1600)
except RuntimeError as e:
raise HTTPException(_image_error_status(e), f"product angle generation failed: {e}")
finally:
if sheet_tmp and sheet_tmp.exists():
try:
sheet_tmp.unlink()
except OSError:
pass
asset_id = f"product_angle_{uuid.uuid4().hex[:10]}"
out_path = job_dir(job_id) / "assets" / f"{asset_id}.jpg"
_normalize_asset_image(img_bytes, out_path, source_path, "1024", "white", square=True, fill_subject=True)