Files
YuShiSheJiShi/backend/app/services/ai_generator.py
bb84747917 feat(ai): 升级AI生图模型及多视角一致性支持
- 将默认AI生图模型升级为flux-dev及seedream-5.0版本
- SiliconFlow模型由FLUX.1-dev切换为Kolors,优化调用参数和返回值
- 火山引擎Seedream升级至5.0 lite版本,支持多视角参考图传入
- 设计图片字段由字符串改为Text扩展URL长度限制
- 设计图下载支持远程URL重定向和本地文件兼容
- 生成AI图片时多视角保持风格一致,SiliconFlow复用seed,Seedream传参考图
- 后台配置界面更改模型名称及价格显示,新增API Key状态检测
- 前端照片下载从链接改为按钮,远程文件新窗口打开
- 设计相关接口支持较长请求超时,下载走API路径无/api前缀
- 前端页面兼容驼峰与下划线格式URL参数识别
- 用户中心设计图下载支持本地文件Token授权下载
- 初始化数据库新增完整表结构与约束,适配新版设计业务逻辑
2026-03-27 17:39:01 +08:00

161 lines
5.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
AI 生图服务
支持双模型SiliconFlow Kolors 和 火山引擎 Seedream 5.0 lite
"""
import os
import uuid
import logging
import httpx
from typing import Optional, Tuple
from ..config import settings
from .config_service import get_ai_config
logger = logging.getLogger(__name__)
# 超时设置(秒)
REQUEST_TIMEOUT = 90
# 最大重试次数
MAX_RETRIES = 3
async def _call_siliconflow(prompt: str, size: int = 1024, ai_config: dict = None, seed: Optional[int] = None) -> Tuple[str, Optional[int]]:
"""
调用 SiliconFlow 生图 APIKolors 模型)
Args:
seed: 随机种子,传入相同 seed 可保持多视角图片风格一致
Returns:
(远程图片 URL, 使用的 seed)
"""
cfg = ai_config or get_ai_config()
url = f"{cfg['SILICONFLOW_BASE_URL']}/images/generations"
headers = {
"Authorization": f"Bearer {cfg['SILICONFLOW_API_KEY']}",
"Content-Type": "application/json",
}
payload = {
"model": "Kwai-Kolors/Kolors",
"prompt": prompt,
"image_size": f"{size}x{size}",
"batch_size": 1,
"num_inference_steps": 20,
"guidance_scale": 7.5,
}
if seed is not None:
payload["seed"] = seed
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
resp = await client.post(url, json=payload, headers=headers)
resp.raise_for_status()
data = resp.json()
# SiliconFlow 响应格式: {"images": [{"url": "https://..."}], "seed": 12345}
images = data.get("images", [])
if not images:
raise ValueError("SiliconFlow 返回空图片列表")
returned_seed = data.get("seed")
return images[0]["url"], returned_seed
async def _call_seedream(prompt: str, size: int = 1024, ai_config: dict = None, seed: Optional[int] = None, ref_image_url: Optional[str] = None) -> Tuple[str, Optional[int]]:
"""
调用火山引擎 Seedream 5.0 lite 生图 API
Args:
ref_image_url: 参考图 URL用于多视角一致性将第一张图作为参考传入后续视角
Returns:
(远程图片 URL, seed)
"""
cfg = ai_config or get_ai_config()
url = f"{cfg['VOLCENGINE_BASE_URL']}/images/generations"
headers = {
"Authorization": f"Bearer {cfg['VOLCENGINE_API_KEY']}",
"Content-Type": "application/json",
}
payload = {
"model": "doubao-seedream-5-0-260128",
"prompt": prompt,
"size": "2K",
"response_format": "url",
"watermark": False,
}
# 传入参考图保持多视角一致性API 要求数组格式)
if ref_image_url:
payload["image"] = [ref_image_url]
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
resp = await client.post(url, json=payload, headers=headers)
if resp.status_code != 200:
logger.error(f"Seedream API 错误: status={resp.status_code}, body={resp.text[:500]}")
resp.raise_for_status()
data = resp.json()
# Seedream 响应格式: {"data": [{"url": "https://..."}]}
items = data.get("data", [])
if not items:
raise ValueError("Seedream 返回空图片列表")
return items[0]["url"], seed
async def generate_image(prompt: str, model: Optional[str] = None, seed: Optional[int] = None, ref_image_url: Optional[str] = None) -> Tuple[str, Optional[int]]:
"""
统一生图接口,带重试机制
Args:
prompt: 提示词
model: 模型名称 (flux-dev / seedream-5.0)
seed: 随机种子SiliconFlow Kolors 支持)
ref_image_url: 参考图 URLSeedream 5.0 支持,用于多视角一致性)
Returns:
(远程图片 URL, 使用的 seed)
"""
ai_config = get_ai_config()
model = model or ai_config.get("AI_IMAGE_MODEL", "flux-dev")
size = ai_config.get("AI_IMAGE_SIZE", 1024)
last_error: Optional[Exception] = None
for attempt in range(1, MAX_RETRIES + 1):
try:
if model in ("seedream-5.0", "seedream-4.5"):
image_url, returned_seed = await _call_seedream(prompt, size, ai_config, seed, ref_image_url)
else:
image_url, returned_seed = await _call_siliconflow(prompt, size, ai_config, seed)
logger.info(f"AI 生图成功 (model={model}, seed={returned_seed}, attempt={attempt})")
return image_url, returned_seed
except Exception as e:
last_error = e
logger.warning(f"AI 生图失败 (model={model}, attempt={attempt}/{MAX_RETRIES}): {e}")
if attempt < MAX_RETRIES:
import asyncio
await asyncio.sleep(2 * attempt) # 指数退避
raise RuntimeError(f"AI 生图在 {MAX_RETRIES} 次重试后仍然失败: {last_error}")
async def download_and_save(image_url: str, save_path: str) -> str:
"""
下载远程图片并保存到本地
Args:
image_url: 远程图片 URL
save_path: 本地保存路径(如 uploads/designs/1001_效果图.png
Returns:
本地文件相对路径(以 / 开头,如 /uploads/designs/1001_效果图.png
"""
# 确保目录存在
os.makedirs(os.path.dirname(save_path), exist_ok=True)
async with httpx.AsyncClient(timeout=60, follow_redirects=True) as client:
resp = await client.get(image_url)
resp.raise_for_status()
with open(save_path, "wb") as f:
f.write(resp.content)
logger.info(f"图片已下载保存: {save_path}")
return f"/{save_path}"