feat: 强化多视角图片一致性 + 修复下载逻辑 + 技术文档

- 新增品类专属背面/侧面描述(BACK_VIEW_HINTS/SIDE_VIEW_HINTS)
- 强化一致性前缀策略,按视角定制相机位置描述
- 更新视角映射提示词为纯摄影术语
- 修复前端下载逻辑:改用fetch直接下载当前视角图片
- HTTPS改HTTP修复外网URL访问
- 新增多视角一致性与3D视频生成技术文档
This commit is contained in:
2026-03-28 19:51:08 +08:00
parent 1d94ec114a
commit 2ef126e445
8 changed files with 942 additions and 286 deletions

View File

@@ -0,0 +1,539 @@
# 多视角图片一致性 & 3D/视频生成 技术文档
## 一、系统概述
玉宗珠宝 AI 设计系统支持根据用户选择的品类、颜色、工艺等参数,自动生成多视角设计图(效果图、正面图、侧面图、背面图),并基于这些图片进一步生成 3D 模型和 360 度旋转展示视频。
**核心挑战**AI 生图模型(如 Seedream 5.0 lite的"参考图"参数本质上是**风格参考**,而非"同一物体不同角度"的语义理解。因此需要通过多层提示词策略来约束 AI 保持多视角图片的一致性。
---
## 二、整体架构
```
用户提交设计请求
┌──────────────────────────────────────────────────────────────┐
│ design_service.py - create_design_async() │
│ │
│ 1. 获取品类视角列表prompt_builder.get_views_for_category
│ 2. 循环每个视角: │
│ ├── prompt_builder.build_prompt() → 构建英文提示词 │
│ ├── 添加一致性前缀(相机位置描述) │
│ ├── ai_generator.generate_image() → 调用AI生图API │
│ ├── 下载图片到本地持久化 │
│ └── 创建 DesignImage 记录 │
│ 3. 第一张图的 seed/URL 传给后续视角复用 │
└──────────────────────────────────────────────────────────────┘
▼ 用户手动触发
┌──────────────┐ ┌──────────────────┐
│ 生成3D模型 │ │ 生成旋转视频 │
│ ai_3d_ │ │ ai_video_ │
│ generator.py │ │ generator_ │
│ │ │ kling.py │
│ 腾讯混元3D │ │ 可灵AI多图参考 │
│ 专业版 │ │ 生视频 │
└──────────────┘ └──────────────────┘
```
---
## 三、涉及文件清单
| 文件路径 | 职责 |
|---------|------|
| `backend/app/services/design_service.py` | 设计生成主流程,多视角循环、一致性策略协调 |
| `backend/app/services/prompt_builder.py` | 提示词构建器,品类视角配置、品类专属背面/侧面描述 |
| `backend/app/services/ai_generator.py` | AI 生图服务,支持 SiliconFlow Kolors 和 Seedream 双模型 |
| `backend/app/services/ai_3d_generator.py` | 腾讯混元3D 模型生成服务多视角→3D |
| `backend/app/services/ai_video_generator_kling.py` | 可灵 AI 多图参考生视频服务 |
| `backend/app/routers/designs.py` | REST API 路由,收集多视角图片调用 3D/视频服务 |
| `backend/app/models/design_image.py` | DesignImage 数据模型 |
| `backend/app/services/config_service.py` | 配置服务AI Key 等从数据库优先读取 |
| `init_data.sql` | 数据库初始化,包含 prompt_mappings 视角映射 |
| `frontend/src/components/DesignPreview.vue` | 前端多视角展示与下载 |
---
## 四、品类视角配置
不同品类生成的视角数量和种类不同,定义在 `prompt_builder.py``CATEGORY_VIEWS` 字典中:
| 品类 | 视角列表 | 视角数 |
|------|---------|-------|
| 牌子 | 效果图、正面图、背面图 | 3 |
| 珠子 | 效果图、正面图 | 2 |
| 手把件 | 效果图、正面图、侧面图、背面图 | 4 |
| 雕刻件 | 效果图、正面图、侧面图、背面图 | 4 |
| 摆件 | 效果图、正面图、侧面图、背面图 | 4 |
| 随形 | 效果图、正面图、侧面图、背面图 | 4 |
| 手镯 | 效果图、正面图、侧面图 | 3 |
| 戒指 | 效果图、正面图、侧面图 | 3 |
| 耳钉/耳饰/手链/项链/表带 | 效果图、正面图 | 2 |
**生成顺序**:严格按列表顺序,第一张始终是"效果图"45度英雄镜头后续视角依次生成。
---
## 五、多视角图片一致性策略(四层保障)
### 第一层AI 模型级参考机制
位置:`design_service.py``_generate_ai_images()` 第 134-183 行
**两种模型的不同一致性方式:**
| 模型 | 一致性机制 | 参数 | 说明 |
|------|-----------|------|------|
| SiliconFlow Kolors | seed 复用 | `seed` | 第一张图返回的 seed 值传给后续视角,确保随机过程相同 |
| Seedream 5.0 lite | 参考图 | `ref_image_url``image: [url]` | 第一张图的远程 URL 作为风格参考传入后续视角 |
**代码流程:**
```python
shared_seed = None # Kolors 用: 第一张图的 seed
first_remote_url = None # Seedream 用: 第一张图的远程 URL
for idx, view_name in enumerate(views):
# 后续视角传入 seed 或参考图 URL
ref_url = first_remote_url if idx > 0 else None
remote_url, returned_seed = await ai_generator.generate_image(
final_prompt, model, seed=shared_seed, ref_image_url=ref_url
)
# 第一张图保存信息供后续复用
if idx == 0:
first_remote_url = remote_url
if returned_seed is not None:
shared_seed = returned_seed
```
**关键限制**Seedream 的 `image` 参数本质上是**风格参考**AI 不能直接理解为"同一个物体的不同角度",所以需要第二、三、四层提示词来补充约束。
---
### 第二层:一致性前缀(相机位置描述)
位置:`design_service.py` 第 159-172 行
从第 2 张图起idx > 0在提示词**最前面**拼接一段一致性约束前缀,按视角名称定制相机位置描述:
```python
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",
}
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
```
**设计思路**
- 强调"EXACT SAME"和"IDENTICAL"强制 AI 保持物体一致
- 用"相机移动"的摄影术语描述视角变化,而非"生成另一张图"
- 明确"物体不动,只有相机位置改变"
---
### 第三层:品类专属视角描述
位置:`prompt_builder.py` 第 35-65 行 & 第 197-201 行
不同品类的背面特征差异极大,需要品类专属描述来避免 AI 产生错误理解。
**背面描述BACK_VIEW_HINTS**
| 品类 | 背面特征 | 核心约束 |
|------|---------|---------|
| **牌子** | 光滑平面,可能有题款,**绝对没有雕刻图案** | "Do NOT mirror or duplicate the front carving on the back" |
| **手把件** | 同一件三维雕塑的背面,自然延续 | "natural continuation of the carving from the rear angle" |
| **雕刻件** | 三维雕塑的背面,雕刻自然延续 | "not a separate or different design" |
| **摆件** | 三维陈列品的背面 | "form and carving continue naturally" |
| **随形** | 天然石料表面的背面 | "may show more of the raw jade texture" |
**侧面描述SIDE_VIEW_HINTS**
| 品类 | 侧面特征 |
|------|---------|
| **牌子** | 薄片轮廓5-10mm 厚度 |
**解决的核心问题**
牌子品类出现"双面佛"效果 —— 背面也有雕刻图案。真实的玉雕牌子背面是光滑平面。通过 `BACK_VIEW_HINTS["牌子"]` 强制约束 AI
> "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."
**应用时机**:在 `build_prompt()` 构建提示词时,根据视角名称和品类名称自动追加:
```python
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])
```
---
### 第四层:数据库视角映射提示词
位置:`prompt_mappings`mapping_type='view'
每个视角有标准化的英文摄影术语描述,存储在数据库中,支持后台热更新:
| 视角 | 英文描述 |
|------|---------|
| 效果图 | `three-quarter view at 45-degree angle, hero shot showing the complete jade artwork with depth and dimension, single object on pure white background` |
| 正面图 | `front view, camera positioned directly in front of the object facing it straight-on, showing only the front carved surface, single object on pure white background` |
| 侧面图 | `side view, camera positioned at exactly 90 degrees to the left of the object, showing the edge profile and thickness, single object on pure white background` |
| 背面图 | `back view, camera positioned directly behind the object at 180 degrees, showing the reverse side, single object on pure white background` |
**设计特点**
- 使用纯**摄影术语**camera positioned, facing, straight-on
- 精确的**角度度数**45°、90°、180°
- 统一的**纯白背景**要求pure white background
- 强调**单一物体**single object避免生成多个物体
---
### 四层策略的组合效果
以"牌子"品类的背面图为例,最终提示词结构:
```
[第二层:一致性前缀]
Photograph the EXACT SAME jade object from the reference image,
moving the camera 180 degrees to see the reverse/back side of the object.
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.
[常规提示词内容]
Chinese Hetian nephrite jade pendant plaque,
natural Hetian nephrite jade with warm luster,
featuring dragon-phoenix auspicious pattern,
relief carving with raised design emerging from surface,
...
[第四层:视角映射]
back view, camera positioned directly behind the object at 180 degrees,
showing the reverse side, single object on pure white background,
[第三层:品类专属背面描述]
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.
[质量后缀]
professional jewelry product photography, studio lighting setup,
pure white background, ultra-detailed, sharp focus, 8K resolution,
photorealistic rendering, high-end commercial quality
```
加上 [第一层] Seedream API 的 `image` 参数传入第一张效果图作为风格参考。
---
## 六、3D 模型生成
### 6.1 技术方案
- **服务**腾讯混元3D 专业版Hunyuan3D Pro
- **API 认证**TC3-HMAC-SHA256 签名
- **输入**:主图 + 多视角图片
- **输出**ZIP 包(包含 GLB/OBJ/FBX/STL 多种格式)
### 6.2 多视角图片收集
位置:`designs.py``generate_3d_model()` 第 329-339 行
```python
# 收集所有多视角图片 URL 和视角名称
image_urls = []
view_names = []
if design.images:
for img in sorted(design.images, key=lambda x: x.sort_order):
if img.image_url:
image_urls.append(img.image_url)
view_names.append(img.view_name or "效果图")
```
### 6.3 视角映射
位置:`ai_3d_generator.py` 第 121-126 行
混元3D API 的 `MultiViewImages` 参数支持指定每张图的视角类型:
```python
_VIEW_NAME_MAP = {
"侧面图": "left",
"背面图": "back",
}
```
**主图选择逻辑**:正面图优先 → 效果图次之 → 否则取第一张
```python
for url, name in zip(image_urls, view_names):
if name == "正面图" and not main_url:
main_url = url # 正面图作为主图
elif name in _VIEW_NAME_MAP:
multi_views.append({ # 侧面/背面作为辅助视角
"ViewType": _VIEW_NAME_MAP[name],
"ViewImageUrl": url
})
elif not main_url:
main_url = url # 效果图或其他作为备选主图
```
### 6.4 URL 转换
位置:`ai_3d_generator.py``_to_public_url()` 第 129-138 行
第三方 API 需要外网可访问的完整 URL本地存储路径/uploads/xxx需要拼接域名
```python
def _to_public_url(url: str) -> str:
if url and url.startswith("/uploads/"):
base_domain = get_config_value("SITE_DOMAIN", "http://c02.wsg.plus")
return f"{base_domain}{url}"
return url
```
> **注意**:服务器仅支持 HTTP不支持 HTTPSHTTPS 会 ERR_CONNECTION_REFUSED
### 6.5 生成流程
```
收集多视角图片 → 转换为外网URL → 选择主图/辅助视角
→ 提交 SubmitHunyuanTo3DProJob (ImageUrl + MultiViewImages)
→ 轮询 QueryHunyuanTo3DProJob (每5秒最多120次≈10分钟)
→ 下载 ZIP 解压提取 .glb → 保存到 /uploads/models/
```
### 6.6 结果存储
3D 模型 URL 保存到**第一张 DesignImage** 的 `model_3d_url` 字段。同时保留 ZIP 包(包含 GLB/OBJ/FBX/STL供用户完整下载。
---
## 七、视频生成
### 7.1 技术方案
- **服务**:可灵 AIKling多图参考生视频
- **API 端点**`https://api-beijing.klingai.com/v1/videos/multi-image2video`
- **API 认证**JWTHS256Access Key + Secret Key 签发30 分钟有效期)
- **输入**1-4 张参考图片 + 提示词
- **输出**MP4 视频
### 7.2 多视角图片收集
位置:`designs.py``generate_video()` 第 273-280 行
```python
# 收集多视角图片 URL
image_urls = []
if design.images:
for img in sorted(design.images, key=lambda x: x.sort_order):
if img.image_url:
image_urls.append(img.image_url)
if not image_urls and design.image_url:
image_urls.append(design.image_url) # 兼容旧数据:只有 design.image_url
```
### 7.3 核心优势
可灵 AI 的多图参考生视频 API **原生支持传入 1-4 张参考图**AI 会理解为同一物体的多个角度参考,生成单品旋转展示视频。这是选择可灵 AI 的核心原因。
### 7.4 视频提示词
默认提示词(可通过数据库 `VIDEO_PROMPT` 配置覆盖):
```
精美玉雕工艺品在专业珠宝摄影棚内展示,
纯白色背景,柔和的珠宝摄影灯光,
玉石作品放在旋转展台上缓慢平稳地旋转360度
展示正面、侧面、背面全貌,
展现玉石温润的质感、细腻的雕刻纹理和通透的光泽,
电影级画质,微距细节感,平稳流畅的转台旋转
```
### 7.5 API 参数
```python
payload = {
"model_name": "kling-v1-6", # 可灵 v1.6 模型
"image_list": image_list, # [{"image": "url"}, ...]最多4张
"prompt": prompt, # 视频提示词
"mode": "pro", # 高品质模式Pro
"duration": "5", # 视频时长5秒或10秒
"aspect_ratio": "1:1", # 正方形比例
}
```
### 7.6 生成流程
```
收集多视角图片(最多4张) → 转换为外网URL → 构建image_list
→ 提交 POST /v1/videos/multi-image2video
→ 轮询 GET /v1/videos/multi-image2video/{task_id} (每5秒最多120次)
→ 下载 MP4 → 保存到 /uploads/videos/
```
### 7.7 JWT 认证细节
```python
headers = {"alg": "HS256", "typ": "JWT"}
payload = {
"iss": access_key, # 签发者 = Access Key
"exp": now + 1800, # 30 分钟过期
"nbf": now - 5, # 允许 5 秒时钟偏差
"iat": now, # 签发时间(必须包含!)
}
token = jwt.encode(payload, secret_key, algorithm="HS256", headers=headers)
```
> **踩坑记录**`headers` 中的 `typ` 和 `payload` 中的 `iat` 字段缺一不可,否则返回 401 认证失败。
---
## 八、图片持久化存储
### 8.1 问题背景
第三方 AI 服务(火山引擎 Seedream生成的图片 URL 是**临时链接**,一段时间后会过期失效(返回 NoSuchKey 错误)。
### 8.2 解决方案
生成图片后立即下载到本地 `uploads/designs/` 目录持久化存储:
```python
async def _download_image_to_local(remote_url, design_id, idx):
filename = f"{design_id}_{idx}_{uuid.uuid4().hex[:8]}.png"
local_path = os.path.join(designs_dir, filename)
# 下载远程图片
async with httpx.AsyncClient(timeout=60, follow_redirects=True) as client:
resp = await client.get(remote_url)
with open(local_path, "wb") as f:
f.write(resp.content)
return f"/uploads/designs/{filename}"
```
### 8.3 URL 格式
- **本地存储路径**`/uploads/designs/{design_id}_{idx}_{随机8位}.png`
- **外网访问 URL**`http://c02.wsg.plus/uploads/designs/{design_id}_{idx}_{随机8位}.png`
---
## 九、前端展示与下载
### 9.1 多视角切换
前端 `DesignPreview.vue` 通过 Tab 栏展示多视角图片,用户点击不同视角 Tab 切换显示。
### 9.2 图片下载
前端直接通过 `fetch()` 下载当前视角图片(不走后端 download 接口),确保下载的是当前展示的视角图片:
```typescript
const handleDownload = async () => {
const imgUrl = currentImageUrl.value // 当前视角的图片URL
const res = await fetch(imgUrl)
const blob = await res.blob()
// 创建 <a> 标签触发下载
_downloadBlob(blob, downloadFilename.value)
}
```
> **踩坑记录**:之前使用后端 `/api/designs/{id}/download` 接口,该接口只返回 `design.image_url`(第一张效果图),导致所有视角下载的都是效果图。
---
## 十、数据模型
### DesignImage 表
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Integer | 主键 |
| design_id | Integer | 关联设计 ID |
| view_name | String(20) | 视角名称:效果图/正面图/侧面图/背面图 |
| image_url | String(500) | 图片 URL本地路径或远程链接 |
| model_used | String(50) | AI 模型标识seedream-5.0 / kolors |
| prompt_used | Text | 实际使用的英文提示词 |
| sort_order | Integer | 视角排序0=效果图, 1=正面图, 2=侧面图, 3=背面图) |
| model_3d_url | String(500) | 对应的 3D 模型 URL |
### Design 表(关键字段)
| 字段 | 说明 |
|------|------|
| image_url | 第一张效果图 URL兼容旧逻辑 |
| video_url | 生成的视频 URL |
| images | 关联的 DesignImage 列表 |
---
## 十一、API 接口
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/designs/generate` | 创建设计AI 多视角生图 |
| GET | `/api/designs` | 分页查询设计历史 |
| GET | `/api/designs/{id}` | 设计详情(含 images 数组) |
| DELETE | `/api/designs/{id}` | 删除设计及所有图片 |
| GET | `/api/designs/{id}/download` | 下载效果图(兼容旧逻辑) |
| POST | `/api/designs/{id}/generate-video` | 生成旋转视频 |
| POST | `/api/designs/{id}/generate-3d` | 生成 3D 模型 |
---
## 十二、成本估算
| 服务 | 单价 | 说明 |
|------|------|------|
| Seedream 5.0 lite 生图 | ≈ 0.22 元/张 | 4 个视角 ≈ 0.88 元 |
| 腾讯混元3D 专业版 | 按量计费 | 单次 3D 生成 |
| 可灵 AI 视频 Pro 5s | ≈ 7 元/次 | Pro 模式5 秒时长 |
**单次完整设计成本**4视角品类≈ 0.88(生图)+ 混元3D + 7视频
---
## 十三、已知问题与踩坑记录
| 问题 | 原因 | 解决方案 |
|------|------|---------|
| 背面图出现"双面佛" | Seedream 参考图是风格参考AI 把正面图案复制到背面 | 品类专属 BACK_VIEW_HINTS + 一致性前缀 |
| 图片过期 404 (NoSuchKey) | 火山引擎临时链接过期 | 生成后立即下载到本地持久化 |
| HTTPS 连接被拒绝 | 服务器未配置 HTTPS | `_to_public_url` 使用 `http://` |
| 下载所有视角都是效果图 | 后端 download 接口只返回 image_url | 前端改用 fetch 直接下载当前视角 URL |
| 可灵 AI 401 认证失败 | JWT 缺少 typ header 和 iat 字段 | 显式传入 headers={typ:JWT} 和 iat |
| 混元3D 字段名错误 | ViewImage 应为 ViewImageUrl | 使用正确的 ViewImageUrl 字段 |
| 前端更新不生效 | 只上传了 .vue 源文件 | 前端是编译型项目,需上传编译后的 dist/ |
---
## 十四、配置项
以下配置通过数据库 `system_configs` 表管理,支持后台热更新:
| 配置键 | 说明 | 默认值 |
|-------|------|--------|
| `VOLCENGINE_API_KEY` | 火山引擎 Seedream API Key | - |
| `SILICONFLOW_API_KEY` | SiliconFlow Kolors API Key | - |
| `AI_IMAGE_MODEL` | 生图模型选择 | `flux-dev` |
| `AI_IMAGE_SIZE` | 图片尺寸 | `1024` |
| `TENCENT_SECRET_ID` | 腾讯云 SecretId混元3D | - |
| `TENCENT_SECRET_KEY` | 腾讯云 SecretKey混元3D | - |
| `KLING_ACCESS_KEY` | 可灵 AI Access Key | - |
| `KLING_SECRET_KEY` | 可灵 AI Secret Key | - |
| `SITE_DOMAIN` | 站点域名外网URL拼接 | `http://c02.wsg.plus` |
| `VIDEO_PROMPT` | 视频生成提示词 | 内置默认值 |
| `VIDEO_DURATION` | 视频时长(秒) | `5` |