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授权下载 - 初始化数据库新增完整表结构与约束,适配新版设计业务逻辑
This commit is contained in:
@@ -18,7 +18,7 @@ class Settings(BaseSettings):
|
||||
SILICONFLOW_BASE_URL: str = "https://api.siliconflow.cn/v1"
|
||||
VOLCENGINE_API_KEY: str = ""
|
||||
VOLCENGINE_BASE_URL: str = "https://ark.cn-beijing.volces.com/api/v3"
|
||||
AI_IMAGE_MODEL: str = "flux-dev" # flux-dev 或 seedream-4.5
|
||||
AI_IMAGE_MODEL: str = "flux-dev" # flux-dev 或 seedream-5.0
|
||||
AI_IMAGE_SIZE: int = 1024
|
||||
|
||||
class Config:
|
||||
|
||||
@@ -35,7 +35,7 @@ app = FastAPI(
|
||||
# 配置 CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:3000"], # 生产环境应限制具体域名
|
||||
allow_origins=["https://c02.wsg.plus", "http://c02.wsg.plus", "http://localhost:3000"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
|
||||
@@ -24,7 +24,7 @@ class Design(Base):
|
||||
size_spec = Column(String(100), nullable=True, comment="尺寸规格")
|
||||
surface_finish = Column(String(50), nullable=True, comment="表面处理")
|
||||
usage_scene = Column(String(50), nullable=True, comment="用途场景")
|
||||
image_url = Column(String(255), nullable=True, comment="设计图URL")
|
||||
image_url = Column(Text, nullable=True, comment="设计图URL")
|
||||
status = Column(String(20), default="generating", comment="状态")
|
||||
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
|
||||
|
||||
@@ -16,7 +16,7 @@ class DesignImage(Base):
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment="图片ID")
|
||||
design_id = Column(BigInteger, ForeignKey("designs.id", ondelete="CASCADE"), nullable=False, comment="关联设计ID")
|
||||
view_name = Column(String(20), nullable=False, comment="视角名称: 效果图/正面图/侧面图/背面图")
|
||||
image_url = Column(String(255), nullable=True, comment="图片URL路径")
|
||||
image_url = Column(Text, nullable=True, comment="图片URL路径")
|
||||
model_used = Column(String(50), nullable=True, comment="使用的AI模型: flux-dev/seedream-4.5")
|
||||
prompt_used = Column(Text, nullable=True, comment="实际使用的英文prompt")
|
||||
sort_order = Column(Integer, default=0, comment="排序")
|
||||
|
||||
@@ -114,7 +114,7 @@ def init_default_configs(
|
||||
("SILICONFLOW_BASE_URL", "https://api.siliconflow.cn/v1", "SiliconFlow 接口地址", "ai", "N"),
|
||||
("VOLCENGINE_API_KEY", "", "火山引擎 API Key", "ai", "Y"),
|
||||
("VOLCENGINE_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3", "火山引擎接口地址", "ai", "N"),
|
||||
("AI_IMAGE_MODEL", "flux-dev", "默认AI生图模型 (flux-dev / seedream-4.5)", "ai", "N"),
|
||||
("AI_IMAGE_MODEL", "flux-dev", "默认AI生图模型 (flux-dev / seedream-5.0)", "ai", "N"),
|
||||
("AI_IMAGE_SIZE", "1024", "AI生图默认尺寸", "ai", "N"),
|
||||
]
|
||||
for key, val, desc, group, secret in defaults:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"""
|
||||
import os
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.responses import FileResponse, RedirectResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import get_db
|
||||
@@ -182,6 +182,7 @@ def download_design(
|
||||
"""
|
||||
下载设计图
|
||||
只能下载自己的设计,非本人设计返回 404
|
||||
支持远程 URL(重定向)和本地文件(兼容历史数据)
|
||||
"""
|
||||
design = design_service.get_design_by_id(
|
||||
db=db,
|
||||
@@ -201,9 +202,12 @@ def download_design(
|
||||
detail="设计图片不存在"
|
||||
)
|
||||
|
||||
# 转换 URL 为文件路径
|
||||
file_path = design.image_url.lstrip("/")
|
||||
# 远程 URL 直接重定向
|
||||
if design.image_url.startswith("http"):
|
||||
return RedirectResponse(url=design.image_url)
|
||||
|
||||
# 本地文件(兼容历史数据)
|
||||
file_path = design.image_url.lstrip("/")
|
||||
if not os.path.exists(file_path):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""
|
||||
AI 生图服务
|
||||
支持双模型:SiliconFlow FLUX.1 [dev] 和 火山引擎 Seedream 4.5
|
||||
支持双模型:SiliconFlow Kolors 和 火山引擎 Seedream 5.0 lite
|
||||
"""
|
||||
import os
|
||||
import uuid
|
||||
import logging
|
||||
import httpx
|
||||
from typing import Optional
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from ..config import settings
|
||||
from .config_service import get_ai_config
|
||||
@@ -19,12 +19,15 @@ REQUEST_TIMEOUT = 90
|
||||
MAX_RETRIES = 3
|
||||
|
||||
|
||||
async def _call_siliconflow(prompt: str, size: int = 1024, ai_config: dict = None) -> str:
|
||||
async def _call_siliconflow(prompt: str, size: int = 1024, ai_config: dict = None, seed: Optional[int] = None) -> Tuple[str, Optional[int]]:
|
||||
"""
|
||||
调用 SiliconFlow FLUX.1 [dev] 生图 API
|
||||
调用 SiliconFlow 生图 API(Kolors 模型)
|
||||
|
||||
Args:
|
||||
seed: 随机种子,传入相同 seed 可保持多视角图片风格一致
|
||||
|
||||
Returns:
|
||||
远程图片 URL
|
||||
(远程图片 URL, 使用的 seed)
|
||||
"""
|
||||
cfg = ai_config or get_ai_config()
|
||||
url = f"{cfg['SILICONFLOW_BASE_URL']}/images/generations"
|
||||
@@ -33,30 +36,38 @@ async def _call_siliconflow(prompt: str, size: int = 1024, ai_config: dict = Non
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
payload = {
|
||||
"model": "black-forest-labs/FLUX.1-dev",
|
||||
"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://..."}]}
|
||||
# SiliconFlow 响应格式: {"images": [{"url": "https://..."}], "seed": 12345}
|
||||
images = data.get("images", [])
|
||||
if not images:
|
||||
raise ValueError("SiliconFlow 返回空图片列表")
|
||||
return images[0]["url"]
|
||||
returned_seed = data.get("seed")
|
||||
return images[0]["url"], returned_seed
|
||||
|
||||
|
||||
async def _call_seedream(prompt: str, size: int = 1024, ai_config: dict = None) -> str:
|
||||
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 4.5 生图 API
|
||||
调用火山引擎 Seedream 5.0 lite 生图 API
|
||||
|
||||
Args:
|
||||
ref_image_url: 参考图 URL,用于多视角一致性(将第一张图作为参考传入后续视角)
|
||||
|
||||
Returns:
|
||||
远程图片 URL
|
||||
(远程图片 URL, seed)
|
||||
"""
|
||||
cfg = ai_config or get_ai_config()
|
||||
url = f"{cfg['VOLCENGINE_BASE_URL']}/images/generations"
|
||||
@@ -65,37 +76,42 @@ async def _call_seedream(prompt: str, size: int = 1024, ai_config: dict = None)
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
payload = {
|
||||
"model": "doubao-seedream-4.5-t2i-250528",
|
||||
"model": "doubao-seedream-5-0-260128",
|
||||
"prompt": prompt,
|
||||
"size": f"{size}x{size}",
|
||||
"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)
|
||||
resp.raise_for_status()
|
||||
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"]
|
||||
return items[0]["url"], seed
|
||||
|
||||
|
||||
async def generate_image(prompt: str, model: Optional[str] = None) -> str:
|
||||
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-4.5),为空则使用配置默认值
|
||||
prompt: 提示词
|
||||
model: 模型名称 (flux-dev / seedream-5.0)
|
||||
seed: 随机种子(SiliconFlow Kolors 支持)
|
||||
ref_image_url: 参考图 URL(Seedream 5.0 支持,用于多视角一致性)
|
||||
|
||||
Returns:
|
||||
远程图片 URL
|
||||
|
||||
Raises:
|
||||
Exception: 所有重试失败后抛出
|
||||
(远程图片 URL, 使用的 seed)
|
||||
"""
|
||||
ai_config = get_ai_config()
|
||||
model = model or ai_config.get("AI_IMAGE_MODEL", "flux-dev")
|
||||
@@ -104,12 +120,12 @@ async def generate_image(prompt: str, model: Optional[str] = None) -> str:
|
||||
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)
|
||||
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 = await _call_siliconflow(prompt, size, ai_config)
|
||||
logger.info(f"AI 生图成功 (model={model}, attempt={attempt})")
|
||||
return image_url
|
||||
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}")
|
||||
|
||||
@@ -19,11 +19,13 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _has_ai_key() -> bool:
|
||||
"""检查是否配置了 AI API Key"""
|
||||
model = settings.AI_IMAGE_MODEL
|
||||
if model == "seedream-4.5":
|
||||
return bool(settings.VOLCENGINE_API_KEY)
|
||||
return bool(settings.SILICONFLOW_API_KEY)
|
||||
"""检查是否配置了 AI API Key(从数据库配置优先读取)"""
|
||||
from .config_service import get_ai_config
|
||||
ai_config = get_ai_config()
|
||||
model = ai_config.get("AI_IMAGE_MODEL", "flux-dev")
|
||||
if model in ("seedream-5.0", "seedream-4.5"):
|
||||
return bool(ai_config.get("VOLCENGINE_API_KEY"))
|
||||
return bool(ai_config.get("SILICONFLOW_API_KEY"))
|
||||
|
||||
|
||||
async def create_design_async(db: Session, user_id: int, design_data: DesignCreate) -> Design:
|
||||
@@ -115,9 +117,19 @@ async def _generate_ai_images(
|
||||
color,
|
||||
design_data: DesignCreate,
|
||||
) -> None:
|
||||
"""使用 AI 模型为每个视角生成图片"""
|
||||
"""使用 AI 模型为每个视角生成图片
|
||||
|
||||
多视角一致性策略:
|
||||
- SiliconFlow Kolors: 通过复用 seed 保持一致
|
||||
- Seedream 5.0 lite: 通过参考图(image参数)保持一致
|
||||
"""
|
||||
views = get_views_for_category(category.name)
|
||||
model = settings.AI_IMAGE_MODEL
|
||||
from .config_service import get_ai_config
|
||||
ai_config = get_ai_config()
|
||||
model = ai_config.get("AI_IMAGE_MODEL", "flux-dev")
|
||||
|
||||
shared_seed = None # Kolors 用: 第一张图的 seed
|
||||
first_remote_url = None # Seedream 用: 第一张图的远程 URL 作为参考图
|
||||
|
||||
for idx, view_name in enumerate(views):
|
||||
# 构建 prompt
|
||||
@@ -136,19 +148,27 @@ async def _generate_ai_images(
|
||||
)
|
||||
|
||||
# 调用 AI 生图
|
||||
remote_url = await ai_generator.generate_image(prompt_text, model)
|
||||
|
||||
# 下载保存到本地
|
||||
save_path = os.path.join(
|
||||
settings.UPLOAD_DIR, "designs", f"{design.id}_{view_name}.png"
|
||||
# 后续视角传入 seed(Kolors)或参考图 URL(Seedream)保持一致性
|
||||
ref_url = first_remote_url if idx > 0 else None
|
||||
remote_url, returned_seed = await ai_generator.generate_image(
|
||||
prompt_text, model, seed=shared_seed, ref_image_url=ref_url
|
||||
)
|
||||
local_url = await ai_generator.download_and_save(remote_url, save_path)
|
||||
|
||||
# 第一张图保存信息供后续视角复用
|
||||
if idx == 0:
|
||||
first_remote_url = remote_url
|
||||
if returned_seed is not None:
|
||||
shared_seed = returned_seed
|
||||
logger.info(f"多视角生图: seed={shared_seed}, ref_url={remote_url[:60]}...")
|
||||
|
||||
# 直接使用远程 URL,不下载到本地(节省服务器存储空间)
|
||||
image_url = remote_url
|
||||
|
||||
# 创建 DesignImage 记录
|
||||
design_image = DesignImage(
|
||||
design_id=design.id,
|
||||
view_name=view_name,
|
||||
image_url=local_url,
|
||||
image_url=image_url,
|
||||
model_used=model,
|
||||
prompt_used=prompt_text,
|
||||
sort_order=idx,
|
||||
@@ -157,7 +177,7 @@ async def _generate_ai_images(
|
||||
|
||||
# 第一张图(效果图)存入 design.image_url 兼容旧逻辑
|
||||
if idx == 0:
|
||||
design.image_url = local_url
|
||||
design.image_url = image_url
|
||||
|
||||
design.status = "completed"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user