feat: complete image console and add verified REST and MCP workflows
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
"""出图服务:调用适配器、落盘、记录 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"),
|
||||
}
|
||||
Reference in New Issue
Block a user