feat: 强化多视角图片一致性 + 修复下载逻辑 + 技术文档
- 新增品类专属背面/侧面描述(BACK_VIEW_HINTS/SIDE_VIEW_HINTS) - 强化一致性前缀策略,按视角定制相机位置描述 - 更新视角映射提示词为纯摄影术语 - 修复前端下载逻辑:改用fetch直接下载当前视角图片 - HTTPS改HTTP修复外网URL访问 - 新增多视角一致性与3D视频生成技术文档
This commit is contained in:
@@ -126,6 +126,18 @@ _VIEW_NAME_MAP = {
|
||||
}
|
||||
|
||||
|
||||
def _to_public_url(url: str) -> str:
|
||||
"""将本地路径转换为外网可访问的完整 URL
|
||||
|
||||
第三方API(如混元3D、可灵AI)需要外网可访问的图片URL,
|
||||
本地存储路径(/uploads/xxx)需要拼接域名。
|
||||
"""
|
||||
if url and url.startswith("/uploads/"):
|
||||
base_domain = get_config_value("SITE_DOMAIN", "http://c02.wsg.plus")
|
||||
return f"{base_domain}{url}"
|
||||
return url
|
||||
|
||||
|
||||
async def generate_3d_model(image_urls: list, view_names: Optional[list] = None) -> str:
|
||||
"""
|
||||
调用腾讯混元3D 专业版 API 将图片生成 3D 模型
|
||||
@@ -146,6 +158,9 @@ async def generate_3d_model(image_urls: list, view_names: Optional[list] = None)
|
||||
if not view_names:
|
||||
view_names = ["效果图"] + ["未知"] * (len(image_urls) - 1)
|
||||
|
||||
# 将本地路径转换为外网可访问URL(第三方API需要完整URL)
|
||||
image_urls = [_to_public_url(u) for u in image_urls]
|
||||
|
||||
# 选择主图(正面图优先,其次效果图,否则第一张)
|
||||
main_url = None
|
||||
multi_views = []
|
||||
|
||||
@@ -108,6 +108,10 @@ async def generate_video(
|
||||
|
||||
logger.info(f"可灵视频生成,传入图片数量: {len(image_urls)}")
|
||||
|
||||
# 将本地路径转换为外网可访问URL(可灵API需要完整URL)
|
||||
from .ai_3d_generator import _to_public_url
|
||||
image_urls = [_to_public_url(u) for u in image_urls]
|
||||
|
||||
# 可灵最多支持 4 张参考图
|
||||
if len(image_urls) > 4:
|
||||
image_urls = image_urls[:4]
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
处理设计相关的业务逻辑,支持 AI 多视角生图 + mock 降级
|
||||
"""
|
||||
import os
|
||||
import uuid
|
||||
import logging
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import httpx
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc
|
||||
|
||||
@@ -150,8 +153,26 @@ async def _generate_ai_images(
|
||||
# 调用 AI 生图
|
||||
# 后续视角传入 seed(Kolors)或参考图 URL(Seedream)保持一致性
|
||||
ref_url = first_remote_url if idx > 0 else None
|
||||
|
||||
# 后续视角在提示词前加一致性约束前缀(根据视角名称定制)
|
||||
final_prompt = prompt_text
|
||||
if idx > 0:
|
||||
# 根据视角名称生成更具体的一致性前缀
|
||||
view_angle_map = {
|
||||
"正面图": "moving the camera to face the object directly from the front",
|
||||
"侧面图": "moving the camera 90 degrees to the left side of the object",
|
||||
"背面图": "moving the camera 180 degrees to see the reverse/back side of the object",
|
||||
}
|
||||
angle_desc = view_angle_map.get(view_name, "changing the camera angle")
|
||||
consistency_prefix = (
|
||||
f"Photograph the EXACT SAME jade object from the reference image, {angle_desc}. "
|
||||
"The object does NOT move or change - only the camera position changes. "
|
||||
"The shape, size, color, material texture, and all physical features must remain IDENTICAL. "
|
||||
)
|
||||
final_prompt = consistency_prefix + prompt_text
|
||||
|
||||
remote_url, returned_seed = await ai_generator.generate_image(
|
||||
prompt_text, model, seed=shared_seed, ref_image_url=ref_url
|
||||
final_prompt, model, seed=shared_seed, ref_image_url=ref_url
|
||||
)
|
||||
|
||||
# 第一张图保存信息供后续视角复用
|
||||
@@ -161,8 +182,9 @@ async def _generate_ai_images(
|
||||
shared_seed = returned_seed
|
||||
logger.info(f"多视角生图: seed={shared_seed}, ref_url={remote_url[:60]}...")
|
||||
|
||||
# 直接使用远程 URL,不下载到本地(节省服务器存储空间)
|
||||
image_url = remote_url
|
||||
# 下载到本地持久化存储(远程URL是临时链接,会过期失效)
|
||||
image_url = await _download_image_to_local(remote_url, design.id, idx)
|
||||
logger.info(f"视角[{view_name}] 已下载到本地: {image_url}")
|
||||
|
||||
# 创建 DesignImage 记录
|
||||
design_image = DesignImage(
|
||||
@@ -182,6 +204,34 @@ async def _generate_ai_images(
|
||||
design.status = "completed"
|
||||
|
||||
|
||||
async def _download_image_to_local(remote_url: str, design_id: int, idx: int) -> str:
|
||||
"""
|
||||
下载远程 AI 生成的图片到本地 uploads/designs/ 目录
|
||||
第三方AI服务生成的图片URL是临时链接,会过期失效,必须下载到本地持久化
|
||||
|
||||
Returns:
|
||||
本地图片 URL 路径,如 /uploads/designs/123_0_xxxx.png
|
||||
"""
|
||||
designs_dir = os.path.join(settings.UPLOAD_DIR, "designs")
|
||||
os.makedirs(designs_dir, exist_ok=True)
|
||||
|
||||
filename = f"{design_id}_{idx}_{uuid.uuid4().hex[:8]}.png"
|
||||
local_path = os.path.join(designs_dir, filename)
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=60, follow_redirects=True) as client:
|
||||
resp = await client.get(remote_url)
|
||||
resp.raise_for_status()
|
||||
with open(local_path, "wb") as f:
|
||||
f.write(resp.content)
|
||||
logger.info(f"图片下载完成: {len(resp.content)} 字节 -> {local_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"图片下载失败,回退使用远程URL: {e}")
|
||||
return remote_url # 下载失败时回退使用远程URL
|
||||
|
||||
return f"/uploads/designs/{filename}"
|
||||
|
||||
|
||||
def _generate_mock_fallback(
|
||||
db: Session,
|
||||
design: Design,
|
||||
|
||||
@@ -29,6 +29,41 @@ CATEGORY_VIEWS: Dict[str, List[str]] = {
|
||||
"随形": ["效果图", "正面图", "侧面图", "背面图"],
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# 品类专属背面描述(不同品类的背面特征差异很大)
|
||||
# ============================================================
|
||||
BACK_VIEW_HINTS: Dict[str, str] = {
|
||||
"牌子": (
|
||||
"IMPORTANT: The reverse/back side of a jade pendant plaque is traditionally a smooth, flat, polished surface. "
|
||||
"It may have a brief inscription or seal mark, but it must NOT have any carved figure, face, or decorative relief pattern. "
|
||||
"The back is plain and minimalist. Do NOT mirror or duplicate the front carving on the back."
|
||||
),
|
||||
"手把件": (
|
||||
"The back of this hand-held jade piece continues the same sculptural form as the front. "
|
||||
"It is part of the same three-dimensional object, showing the natural continuation of the carving from the rear angle."
|
||||
),
|
||||
"雕刻件": (
|
||||
"The back of this carved jade piece shows the rear of the same three-dimensional sculpture. "
|
||||
"The carving continues around the object naturally, not a separate or different design."
|
||||
),
|
||||
"摆件": (
|
||||
"The back of this jade display piece shows the rear of the same three-dimensional artwork. "
|
||||
"The form and carving continue naturally around the object."
|
||||
),
|
||||
"随形": (
|
||||
"The back of this free-form jade piece shows the natural stone surface from the rear. "
|
||||
"The organic shape continues naturally, the back may show more of the raw jade texture."
|
||||
),
|
||||
}
|
||||
|
||||
# 品类专属侧面描述
|
||||
SIDE_VIEW_HINTS: Dict[str, str] = {
|
||||
"牌子": (
|
||||
"The side/edge view of a jade pendant plaque shows its thin, flat profile. "
|
||||
"The plaque is typically 5-10mm thick, showing the edge thickness and any subtle edge carving."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _load_mappings(mapping_type: str) -> Dict[str, str]:
|
||||
"""从数据库加载指定类型的映射字典"""
|
||||
@@ -159,7 +194,13 @@ def build_prompt(
|
||||
view_desc = view_map.get(view_name, "three-quarter view")
|
||||
parts.append(view_desc)
|
||||
|
||||
# 12. 质量后缀
|
||||
# 12. 品类专属视角描述(背面/侧面特征)
|
||||
if view_name == "背面图" and category_name in BACK_VIEW_HINTS:
|
||||
parts.append(BACK_VIEW_HINTS[category_name])
|
||||
elif view_name == "侧面图" and category_name in SIDE_VIEW_HINTS:
|
||||
parts.append(SIDE_VIEW_HINTS[category_name])
|
||||
|
||||
# 13. 质量后缀
|
||||
parts.append(quality_suffix)
|
||||
|
||||
return ", ".join(parts)
|
||||
|
||||
Reference in New Issue
Block a user