Files
YuShiSheJiShi/backend/app/services/ai_generator.py
032c43525a feat(ai): 支持双模型多视角AI设计生图与后台管理系统
- 实现AI多视角设计图生成功能,支持6个可选设计参数配置
- 集成SiliconFlow FLUX.1与火山引擎Seedream 4.5双模型切换
- 构建专业中文转英文prompt系统,提升AI生成质量
- 前端设计预览支持多视角切换与视角指示器展示
- 增加多视角设计图片DesignImage模型关联及存储
- 后端设计服务异步调用AI接口,失败时降级生成mock图
- 新增管理员后台管理路由及完整的权限校验机制
- 实现后台模块:仪表盘、系统配置、用户/品类/设计管理
- 配置数据库系统配置表,支持动态AI配置及热更新
- 增加用户管理员标识字段,管理后台登录鉴权支持
- 更新API接口支持多视角设计参数及后台管理接口
- 优化设计删除逻辑,删除多视角相关图片文件
- 前端新增管理后台页面与路由,布局样式独立分离
- 更新环境变量增加AI模型相关Key与参数配置说明
- 引入httpx异步HTTP客户端用于AI接口调用及图片下载
- README文档完善AI多视角生图与后台管理详细功能与流程说明
2026-03-27 15:29:50 +08:00

145 lines
4.4 KiB
Python
Raw 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 FLUX.1 [dev] 和 火山引擎 Seedream 4.5
"""
import os
import uuid
import logging
import httpx
from typing import Optional
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) -> str:
"""
调用 SiliconFlow FLUX.1 [dev] 生图 API
Returns:
远程图片 URL
"""
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": "black-forest-labs/FLUX.1-dev",
"prompt": prompt,
"image_size": f"{size}x{size}",
"num_inference_steps": 20,
}
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://..."}]}
images = data.get("images", [])
if not images:
raise ValueError("SiliconFlow 返回空图片列表")
return images[0]["url"]
async def _call_seedream(prompt: str, size: int = 1024, ai_config: dict = None) -> str:
"""
调用火山引擎 Seedream 4.5 生图 API
Returns:
远程图片 URL
"""
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-4.5-t2i-250528",
"prompt": prompt,
"size": f"{size}x{size}",
"response_format": "url",
}
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()
# Seedream 响应格式: {"data": [{"url": "https://..."}]}
items = data.get("data", [])
if not items:
raise ValueError("Seedream 返回空图片列表")
return items[0]["url"]
async def generate_image(prompt: str, model: Optional[str] = None) -> str:
"""
统一生图接口,带重试机制
Args:
prompt: 英文提示词
model: 模型名称 (flux-dev / seedream-4.5),为空则使用配置默认值
Returns:
远程图片 URL
Raises:
Exception: 所有重试失败后抛出
"""
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 == "seedream-4.5":
image_url = await _call_seedream(prompt, size, ai_config)
else:
image_url = await _call_siliconflow(prompt, size, ai_config)
logger.info(f"AI 生图成功 (model={model}, attempt={attempt})")
return image_url
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}"