feat: AI炫图接入gpt-image-2.5云端生图(结构化提示词工程+照片参考合成)

This commit is contained in:
HHY
2026-09-25 04:15:45 -07:00
parent 710b3521a2
commit 6726a9fa3a
12 changed files with 496 additions and 48 deletions
+107
View File
@@ -0,0 +1,107 @@
"""AI 炫图路由:gpt-image 生图代理(Key 仅存后端),提示词由 drawing_data 结构化构建。
- quick 模式: /v1/images/generations 纯提示词生成
- photo 模式: /v1/images/edits 以用户照片为参考图,把窗型合成进照片
"""
import base64
import json
import httpx
from fastapi import APIRouter
from pydantic import BaseModel, Field
from huamenchuang.core.config import settings
from huamenchuang.core.deps import CurrentUser, DbSession
from huamenchuang.core.response import fail, ok
from huamenchuang.services.ai_prompt import build_prompt, describe_window, pick_size
router = APIRouter(prefix="/ai-render", tags=["AI 炫图"])
class AiRenderBody(BaseModel):
template_id: int | None = None
drawing_data: str | dict | None = None
mode: str = Field(default="quick", pattern="^(quick|photo)$")
style: str = Field(default="modern", pattern="^(modern|chinese|european|nordic)$")
time: str = Field(default="day", pattern="^(day|dusk|night)$")
# quick: indoor/outdoor/studio; photo: render/install/erase/indoor/villa/fence/blank
scene: str = "indoor"
photo_b64: str | None = Field(default=None, description="照片模式参考图(base64, 不带 dataURL 前缀)")
async def _resolve_drawing(db, body: AiRenderBody) -> str | dict | None:
if body.drawing_data:
return body.drawing_data
if body.template_id:
from sqlalchemy import select
from huamenchuang.models.windoor_template import WindoorTemplate
t = (await db.execute(select(WindoorTemplate).where(
WindoorTemplate.id == body.template_id))).scalar_one_or_none()
return t.drawing_data if t else None
return None
@router.post("/prompt")
async def preview_prompt(body: AiRenderBody, user: CurrentUser, db: DbSession):
"""提示词预览(前端可展开查看优化后的完整提示词)。"""
drawing = await _resolve_drawing(db, body)
if drawing is None:
return fail("未找到图纸数据")
try:
info = describe_window(drawing)
prompt = build_prompt(drawing, body.style, body.time, body.scene)
except Exception:
return fail("图纸数据解析失败")
return ok(data={"features": info, "prompt": prompt,
"size": pick_size(info["width_mm"], info["height_mm"])})
@router.post("/generate")
async def generate(body: AiRenderBody, user: CurrentUser, db: DbSession):
"""云端 AI 生图(gpt-image):quick=生成 / photo=参考图编辑。"""
if not settings.ai_api_key:
return fail("AI 生图服务未配置(HMC_AI_API_KEY)")
drawing = await _resolve_drawing(db, body)
if drawing is None:
return fail("未找到图纸数据")
if body.mode == "photo" and not body.photo_b64:
return fail("照片模式需要上传参考照片")
try:
info = describe_window(drawing)
prompt = build_prompt(drawing, body.style, body.time, body.scene)
size = pick_size(info["width_mm"], info["height_mm"])
except Exception:
return fail("图纸数据解析失败")
base = settings.ai_base_url.rstrip("/")
headers = {"Authorization": f"Bearer {settings.ai_api_key}"}
timeout = httpx.Timeout(settings.ai_timeout, connect=15)
try:
async with httpx.AsyncClient(timeout=timeout) as client:
if body.mode == "photo":
img_bytes = base64.b64decode(body.photo_b64.split(",")[-1])
resp = await client.post(
f"{base}/v1/images/edits", headers=headers,
data={"model": settings.ai_model, "prompt": prompt, "size": size, "n": "1"},
files={"image[]": ("photo.png", img_bytes, "image/png")})
else:
resp = await client.post(
f"{base}/v1/images/generations", headers=headers,
json={"model": settings.ai_model, "prompt": prompt, "n": 1, "size": size})
resp.raise_for_status()
data = resp.json()
item = (data.get("data") or [{}])[0]
b64 = item.get("b64_json")
if not b64:
return fail(f"生图服务返回异常: {str(data)[:200]}")
except httpx.TimeoutException:
return fail("AI 生图超时,请稍后重试")
except httpx.HTTPStatusError as e:
detail = e.response.text[:200] if e.response is not None else ""
return fail(f"生图服务错误({e.response.status_code if e.response else '?'}): {detail}")
except Exception as e:
return fail(f"生图失败: {str(e)[:150]}")
return ok(data={"image_b64": b64, "prompt": prompt, "size": size,
"features": {"width_mm": info["width_mm"], "height_mm": info["height_mm"],
"summary": info["summary"]}},
message="生成成功")