feat: AI炫图接入gpt-image-2.5云端生图(结构化提示词工程+照片参考合成)
This commit is contained in:
@@ -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="生成成功")
|
||||
@@ -2,6 +2,7 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from huamenchuang.api import (
|
||||
ai_render,
|
||||
auth,
|
||||
business,
|
||||
contracts,
|
||||
@@ -29,6 +30,7 @@ api_router.include_router(quote.router)
|
||||
api_router.include_router(script.router)
|
||||
api_router.include_router(order_format.router)
|
||||
api_router.include_router(finance.router)
|
||||
api_router.include_router(ai_render.router)
|
||||
api_router.include_router(warehouse.router)
|
||||
api_router.include_router(business.router)
|
||||
api_router.include_router(platform.router)
|
||||
|
||||
Reference in New Issue
Block a user