"""出图服务:调用适配器、落盘、记录 generations。""" from __future__ import annotations import json import math import re import time import uuid from datetime import datetime from typing import Any from pydantic import ValidationError from .. import config, db from ..providers import ProviderError, get_adapter from ..providers.agnes import size_to_tier_and_ratio from ..schemas import GenerateIn from . import providers as provider_service WEB_PROFILE = "user" _SAFE = re.compile(r"[^A-Za-z0-9._-]+") def _safe_model(model: str) -> str: return _SAFE.sub("-", model).strip("-") or "image" def record_generation( *, provider_id: str, provider_name: str, model: str, profile_name: str, prompt: str, size: str, ratio: str, n: int, status: str, error: str, files: list[str], duration_ms: int, ) -> str: gid = "gen_" + uuid.uuid4().hex[:12] with db.session() as conn: conn.execute( "INSERT INTO generations (id,provider_id,provider_name,model,profile_name,prompt,size," "ratio,n,status,error,files,duration_ms,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)", ( gid, provider_id, provider_name, model, profile_name, prompt, size, ratio, n, status, error, json.dumps(files, ensure_ascii=False), duration_ms, datetime.now().strftime("%Y-%m-%d %H:%M:%S"), ), ) return gid def list_generations() -> list[dict[str, Any]]: with db.session() as conn: rows = conn.execute("SELECT * FROM generations ORDER BY created_at DESC, rowid DESC").fetchall() out: list[dict[str, Any]] = [] for row in rows: try: files = json.loads(row["files"] or "[]") except ValueError: files = [] out.append( { "id": row["id"], "provider_id": row["provider_id"], "provider_name": row["provider_name"], "model": row["model"], "profile_name": row["profile_name"], "prompt": row["prompt"], "size": row["size"], "ratio": row["ratio"], "n": row["n"], "status": row["status"], "error": row["error"], "files": files, "duration_ms": row["duration_ms"], "created_at": row["created_at"], } ) return out def generate( *, provider_id: str, model: str, prompt: str, size: str = "1024x1024", ratio: str = "", n: int = 1, images: list[str] | None = None, profile_name: str | None = None, ) -> dict[str, Any]: try: payload = GenerateIn(provider_id=provider_id, model=model, prompt=prompt, size=size, ratio=ratio, n=n, images=images or [], profile_name=profile_name) except ValidationError as exc: raise ProviderError(str(exc), status=400) from exc prompt = payload.prompt size = payload.size profile = payload.profile_name or WEB_PROFILE cfg = provider_service.provider_config(provider_id, require_enabled=True) selected = provider_service.get_model(f"{provider_id}:{model}") if not selected["enabled"]: raise ProviderError("模型已停用", status=400) provider_name = cfg.name adapter = get_adapter(cfg) if images and not adapter.capabilities.supports_image_input: raise ProviderError("该供应商不支持参考图输入", status=400) if ratio and cfg.adapter != "agnes": raise ProviderError("该供应商不支持 ratio,请使用 WxH 尺寸", status=400) if cfg.adapter == "agnes": _, ratio = size_to_tier_and_ratio(size, ratio) elif "x" in size: width, height = map(int, size.split("x")) divisor = math.gcd(width, height) ratio = f"{width // divisor}:{height // divisor}" started = time.monotonic() files: list[str] = [] folder = config.OUTPUT_DIR / profile if not folder.resolve().is_relative_to(config.OUTPUT_DIR.resolve()): raise ProviderError("画廊路径超出输出目录", status=400) images_out: list[dict[str, Any]] = [] try: results = adapter.generate(model, prompt, size, ratio, n, images or []) if not results: raise ProviderError("上游未返回图片") folder.mkdir(parents=True, exist_ok=True) stamp = uuid.uuid4().hex[:8] for idx, item in enumerate(results): if item.url and not item.data: item = adapter.download(item.url) if not item.data: raise ProviderError("上游返回空图片") suffix = item.filename if item.filename in config.IMAGE_SUFFIXES else ".png" filename = f"{_safe_model(model)}-{stamp}{'' if len(results) == 1 else f'-{idx + 1}'}{suffix}" path = folder / filename files.append(filename) path.write_bytes(item.data) images_out.append({"filename": filename, "url": f"/output/{profile}/{filename}", "bytes": path.stat().st_size}) except (ProviderError, OSError) as exc: for filename in files: (folder / filename).unlink(missing_ok=True) message = exc.message if isinstance(exc, ProviderError) else "保存图片失败" duration = int((time.monotonic() - started) * 1000) record_generation( provider_id=provider_id, provider_name=provider_name, model=model, profile_name=profile, prompt=prompt, size=size, ratio=ratio, n=n, status="failed", error=message, files=[], duration_ms=duration, ) raise ProviderError(message, status=exc.status if isinstance(exc, ProviderError) else 500) from exc duration = int((time.monotonic() - started) * 1000) gid = record_generation( provider_id=provider_id, provider_name=provider_name, model=model, profile_name=profile, prompt=prompt, size=size, ratio=ratio or "1:1", n=n, status="success", error="", files=files, duration_ms=duration, ) return { "id": gid, "provider": provider_id, "provider_name": provider_name, "model": model, "profile_name": profile, "size": size, "ratio": ratio or "1:1", "images": images_out, "output_dir": f"output/{profile}/", "created_at": datetime.now().strftime("%Y-%m-%d %H:%M"), }