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多视角生图与后台管理详细功能与流程说明
This commit is contained in:
144
backend/app/services/ai_generator.py
Normal file
144
backend/app/services/ai_generator.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
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}"
|
||||
Reference in New Issue
Block a user