diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..a6c79ee Binary files /dev/null and b/.DS_Store differ diff --git a/.qoder/.DS_Store b/.qoder/.DS_Store new file mode 100644 index 0000000..3d46949 Binary files /dev/null and b/.qoder/.DS_Store differ diff --git a/backend/.DS_Store b/backend/.DS_Store new file mode 100644 index 0000000..ed6cb78 Binary files /dev/null and b/backend/.DS_Store differ diff --git a/backend/app/config.py b/backend/app/config.py index ae7f4f2..32f8486 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -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: diff --git a/backend/app/main.py b/backend/app/main.py index b60e260..d55a0bd 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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=["*"], diff --git a/backend/app/models/design.py b/backend/app/models/design.py index 892dc0d..5511ebe 100644 --- a/backend/app/models/design.py +++ b/backend/app/models/design.py @@ -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="更新时间") diff --git a/backend/app/models/design_image.py b/backend/app/models/design_image.py index c9a9192..e4ca468 100644 --- a/backend/app/models/design_image.py +++ b/backend/app/models/design_image.py @@ -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="排序") diff --git a/backend/app/routers/admin.py b/backend/app/routers/admin.py index 18effc5..a8ca0a5 100644 --- a/backend/app/routers/admin.py +++ b/backend/app/routers/admin.py @@ -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: diff --git a/backend/app/routers/designs.py b/backend/app/routers/designs.py index d58fdb4..6736fb2 100644 --- a/backend/app/routers/designs.py +++ b/backend/app/routers/designs.py @@ -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, diff --git a/backend/app/services/ai_generator.py b/backend/app/services/ai_generator.py index 491de50..b61f2ee 100644 --- a/backend/app/services/ai_generator.py +++ b/backend/app/services/ai_generator.py @@ -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}") diff --git a/backend/app/services/design_service.py b/backend/app/services/design_service.py index 73c8e45..f922d36 100644 --- a/backend/app/services/design_service.py +++ b/backend/app/services/design_service.py @@ -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" diff --git a/backend/uploads/designs/1.png b/backend/uploads/designs/1.png deleted file mode 100644 index 52c5499..0000000 Binary files a/backend/uploads/designs/1.png and /dev/null differ diff --git a/frontend/src/api/admin.ts b/frontend/src/api/admin.ts index c8f26c0..18c8dfd 100644 --- a/frontend/src/api/admin.ts +++ b/frontend/src/api/admin.ts @@ -7,7 +7,7 @@ export const getDashboard = () => request.get('/admin/dashboard') export const getConfigs = (group?: string) => request.get('/admin/configs', { params: group ? { group } : {} }) -export const updateConfigs = (configs: Record) => +export const updateConfigs = (_configs: Record) => request.post('/admin/configs', null, { // PUT 方法 }) diff --git a/frontend/src/api/design.ts b/frontend/src/api/design.ts index 5586f87..a8a1757 100644 --- a/frontend/src/api/design.ts +++ b/frontend/src/api/design.ts @@ -70,9 +70,9 @@ export function getDesignApi(id: number) { return request.get(`/designs/${id}`) } -// 生成设计 +// 生成设计(AI生图需要较长时间,超时设为5分钟) export function generateDesignApi(data: GenerateDesignParams) { - return request.post('/designs/generate', data) + return request.post('/designs/generate', data, { timeout: 300000 }) } // 删除设计 @@ -80,7 +80,7 @@ export function deleteDesignApi(id: number) { return request.delete(`/designs/${id}`) } -// 获取设计下载 URL +// 获取设计下载 URL(相对于 baseURL /api) export function getDesignDownloadUrl(id: number) { - return `/api/designs/${id}/download` + return `/designs/${id}/download` } diff --git a/frontend/src/components/DesignPreview.vue b/frontend/src/components/DesignPreview.vue index a29f297..822e8f6 100644 --- a/frontend/src/components/DesignPreview.vue +++ b/frontend/src/components/DesignPreview.vue @@ -85,14 +85,14 @@
- - 下载设计图 - + {{ downloading ? '下载中...' : '下载设计图' }} +