引入3d模型

This commit is contained in:
2026-04-20 21:52:35 +08:00
parent ee5cec6de4
commit d6e1797f08
23 changed files with 1890 additions and 288 deletions

View File

@@ -0,0 +1,463 @@
# Mesh-Pipeline 视角变换 · 跨会话实施计划
> **状态**SPEC / 尚未开工
> **最后更新**2026-04-20 15:30
> **负责人**Agent + 用户 @shine
> **适用对象**:任意新会话的 Agent本文档自包含无需额外上下文即可开工
---
## 0 · 上下文Why
### 0.1 起因
当前项目 `art-agent` 已经集成了 Zero123++`jd7h/zero123plusplus`)做单图→多视角变换,但存在固有缺陷:
1. **只输出 6 个固定视角**`azimuth/elevation` 写死在 `config.py`,用户无法指定任意相机位姿
2. **大角度一致性差**Zero123++ 主要训练于小物体,对建筑类规整几何 + 大角度偏转常出现立面错位
3. **无几何中间产物**,纯图对图黑盒映射,无法二次利用
### 0.2 目标
新增一条"Mesh Pipeline"视角变换路径,与现有 `zero123plus` 并列保留,让用户可以:
- 输入 1 张 2D 图(典型场景:带描边卡通风的建筑,如教堂/房屋/塔楼)
- 输出 N 张**保留原画风**、**几何一致**的新视角 2D 图
- 可指定任意 `azimuth/elevation`(至少阶段性支持 Trellis 360° 中的任意角度)
**非目标**:输出 3D mesh 给用户下载mesh 只是中间产物);替代 Zero123++(它作为"轻量路径"继续保留)。
### 0.3 相关历史上下文
| 记录 | 说明 |
|---|---|
| `CL-20260416-1000` | Zero123++ 首次集成 |
| `CL-20260420-1212` | Zero123++ 切图布局修复cols=2/rows=3 |
| `CL-20260420-1501` | 后端 watchfiles 依赖修复uvicorn --reload 生效 |
| `PF-20260420-1212` | Grid 切图布局坑("3×2" 措辞歧义) |
| `PF-20260420-1501` | Windows uvicorn --reload 无 watchfiles 退化坑 |
---
## 1 · 核心决策
### 1.1 三段式流水线
```
[输入: 1 张 2D 图]
【阶段 1】单图 3D 重建 → 生成 textured .glb + 预渲染 360° 视频
【阶段 2】从视频抽帧 / 自定义相机渲染 → 多视角"几何参考图"
【阶段 3】SDXL + ControlNet + IP-Adapter 二次重绘 → 还原原画风
[输出: N 张保留风格、几何一致的 2D 新视角图]
```
### 1.2 模型选型
| 阶段 | 主选 | 备选 / 高级选项 | 理由 |
|---|---|---|---|
| 阶段 1 | **Trellis** (`firtoz/trellis`) | Hunyuan3D-2 (`tencent/hunyuan3d-2`) | Trellis $0.041/30s 且自带 color_videoHunyuan 贵 3×慢 4×但 mesh 更扎实 |
| 阶段 2 | **ffmpeg 从 Trellis `color_video` 抽帧** | pyrender 加载 .glb 任意角度渲染 | Trellis 已送 360° 环绕视频,首版完全省事 |
| 阶段 3 | **`chigozienri/ip_adapter-sdxl-controlnet-depth`** | 自建 SDXL + 多 ControlNet 组合 | 一个 Replicate 模型打包了 SDXL+ControlNet-Depth+IP-Adapter |
### 1.3 为什么不用 Hunyuan3D-2 作默认
- 贵 3 倍($0.12 vs $0.041
- 慢 4 倍127s vs 30s
- **不返回预渲染视频** → 必须额外实现阶段 2pyrender / blender 渲染器)
- 唯一优势mesh 几何质量更高
→ 留作"高质量模式"供用户显式选择,一期不默认启用。
### 1.4 为什么不用 ControlNet-Depth 必须先估深度
`chigozienri/ip_adapter-sdxl-controlnet-depth``controlnet_input` 要求已经是 depth map。两条路
- **路 A**:从 Trellis `normal_video` 抽帧,把 normal 当"结构约束"直接喂(近似方案,不完美但可用)
- **路 B**:从 color_video 抽帧后,额外调用 `chenxwh/depth-anything-v2` 生成 depth map
一期走路 A 简化;若效果不足再升级路 B多一次 API 调用 + $0.003/张)。
---
## 2 · 技术数据清单Schema
### 2.1 Trellis — `firtoz/trellis`
- **版本 ID**`e8f6c45206993f297372f5436b90350817bd9b4a0d52d2a76df50c1c8afa2b3c`
- **成本**$0.041/次(~24 次/$1
- **耗时**~30 秒
- **硬件**Nvidia A100 80GB
- **输入关键字段**
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `images` | `array[uri]` | 必填 | 输入图(支持多图,一期只传 1 张) |
| `seed` | int | 0 | 种子 |
| `randomize_seed` | bool | true | 一期设为 false 便于复现 |
| `texture_size` | int (512-2048) | 1024 | GLB 纹理分辨率 |
| `mesh_simplify` | float (0.9-0.98) | 0.95 | 网格简化,建议保持默认 |
| `generate_color` | bool | **true** | **必开**,用于得到 color_video |
| `generate_normal` | bool | **true** | **一期开启**,便于阶段 3 做结构约束 |
| `generate_model` | bool | **true** | **必开**,否则不输出 .glb |
| `save_gaussian_ply` | bool | false | 不需要 |
| `ss_sampling_steps` | int (1-50) | 12 | 默认即可 |
| `slat_sampling_steps` | int (1-50) | 12 | 默认即可 |
| `ss_guidance_strength` | float (0-10) | 7.5 | 默认即可 |
| `slat_guidance_strength` | float (0-10) | 3 | 默认即可 |
- **输出字段**(⚠️ 实测修订 2026-04-20 16:15版本 `e8f6c452...`
| 字段 | 类型 | 实测行为 |
|---|---|---|
| `model_file` | uri | ✅ .glb 文件(`generate_model=true` 时)|
| `color_video` | uri | ❌ **恒为 null** — 该部署版不单独输出 |
| `normal_video` | uri | ❌ **恒为 null** — 该部署版不单独输出 |
| `combined_video` | uri | ✅ **唯一可用的视频** — color+normal **左右并排** mp4 |
| `gaussian_ply` | uri | ✅ 高斯点云(本项目不用) |
| `no_background_images` | array[uri] | ✅ 去背景的预处理图 |
**实测 schema 踩坑**(已记为 PF-20260420-1600
1. 输入字段必须用 `images`(数组)—— 传 `image`(单数字符串)会 422 `input: images is required`,即使 Replicate README example 里写的是 `image`
2. 尽管 `generate_color=True``generate_normal=True` 都开,`color_video``normal_video` 字段也**恒为 null**,只有 `combined_video` 可用。
3. 因此 Phase 2 抽帧的数据源是 `combined_video`,抽帧后需要**切左半 = color 帧、切右半 = normal 帧**。
### 2.2 Hunyuan3D-2 — `tencent/hunyuan3d-2`(备选)
- **版本 ID**`b1b9449a1277e10402781c5d41eb30c0a0683504fb23fab591ca9dfc2aabe1cb`
- **成本**$0.12/次
- **耗时**~127 秒
- **硬件**Nvidia L40S
- **输入**`image`, `steps` (20-50, default 50), `guidance_scale` (1-20, default 5.5), `seed`, `octree_resolution` (default 256), `remove_background` (default true)
- **输出**`mesh` (uri, .glb) —— **无视频无多视角预览**,必须配套阶段 2 自定义渲染
### 2.3 ControlNet + IP-Adapter — `chigozienri/ip_adapter-sdxl-controlnet-depth`
- **版本 ID**`0436c8702ef52616be5c30948551b3af6a86c821cca9b01f11ac297624fff14c`
- **成本**$0.070/次
- **耗时**~72 秒
- **硬件**Nvidia L40S
- **输入关键字段**
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `image` | uri | 必填 | **IP-Adapter 参考图**(此处传**用户原图**保持风格) |
| `controlnet_input` | uri | 必填 | **Depth / Normal / Lineart 参考图**(此处传**阶段 2 抽出来的 normal 帧** |
| `scale` | float (0-1) | 0.6 | IP-Adapter 权重,建议调到 **0.7-0.8** 强化风格 |
| `controlnet_conditioning_scale` | float (0-1) | 0.6 | 结构约束权重,建议 **0.7-0.9** |
| `prompt` | str | "" | 留空或用 "same building, same art style" |
| `negative_prompt` | str | "" | "blurry, distorted, different style" |
| `num_outputs` | int (1-4) | 1 | 一期 1 |
| `num_inference_steps` | int (1-500) | 30 | 默认即可 |
| `seed` | int | 随机 | 调参阶段固定 |
### 2.4 依赖清单
**一期必装**(首版走 Trellis + ffmpeg 抽帧,最小化依赖):
```
# backend requirements.txt
ffmpeg-python==0.2.0 # 或者直接在后端宿主机装 ffmpeg 可执行文件
```
**二期可选**(若要上 Hunyuan3D-2 + 自定义相机):
```
trimesh==4.5.0
pyrender==0.1.45
PyOpenGL==3.1.7
# Windows 额外需要 OSMesa 或改用 Blender headless二选一
```
**系统依赖**
- `ffmpeg` 可执行文件Windows`choco install ffmpeg` 或手动放 PATH
- 若二期启用Windows 上 OSMesa 编译困难,推荐用 Blender 4.x headless 替代
---
## 3 · 数据流与接口设计
### 3.1 新增后端服务模块
```
art-agent/backend/app/services/
├── view_transform.py # [改造] 管道调度器
├── mesh_generator.py # [新增] 阶段 1调用 Trellis / Hunyuan3D-2
├── video_frame_extractor.py # [新增] 阶段 2轻量ffmpeg 抽帧
├── mesh_renderer.py # [新增, 二期] 阶段 2自由相机pyrender 加载 .glb 渲染任意角度
└── style_restorer.py # [新增] 阶段 3ControlNet + IP-Adapter 重绘
```
### 3.2 管道调度伪代码
```python
# services/view_transform.py
async def transform_view_mesh_pipeline(
image_path: str,
model_id: str = "trellis", # or "hunyuan3d"
azimuths: list[int] = [0, 60, 120, 180, 240, 300],
elevations: list[int] = [0, 0, 0, 0, 0, 0],
preserve_style: bool = True, # 是否走阶段 3
) -> dict:
# === 阶段 1 ===
mesh_result = await mesh_generator.generate(image_path, model_id)
# 返回 {glb_url, color_video_url?, normal_video_url?}
# === 阶段 2 ===
if model_id == "trellis":
# 从视频抽帧Trellis 的 360° 视频约 5s对应 az 0→360°
frames = await video_frame_extractor.extract_by_azimuth(
color_video_url=mesh_result["color_video_url"],
normal_video_url=mesh_result["normal_video_url"],
azimuths=azimuths,
total_rotation_deg=360,
)
else: # hunyuan3d
# 二期:自定义相机渲染
frames = await mesh_renderer.render_views(
glb_path=mesh_result["glb_path"],
azimuths=azimuths,
elevations=elevations,
)
# frames = [{az, el, color_png, normal_png}, ...]
# === 阶段 3 ===
if preserve_style:
final_images = await asyncio.gather(*[
style_restorer.restore(
original_image=image_path,
structure_image=f["normal_png"],
azimuth=f["az"],
elevation=f["el"],
)
for f in frames
])
else:
final_images = [f["color_png"] for f in frames]
return {
"success": True,
"images": [
{"url": img, "azimuth": az, "elevation": el}
for img, (az, el) in zip(final_images, zip(azimuths, elevations))
],
"mesh_url": mesh_result.get("glb_url"),
"model_name": f"Mesh Pipeline ({model_id})",
}
```
### 3.3 config.py 扩展
```python
VIEW_TRANSFORM_MODELS = {
"zero123plus": {...}, # 保留不动
"trellis": {
"id": "trellis",
"name": "Trellis (Mesh Pipeline)",
"provider": "replicate",
"model_id": "firtoz/trellis:e8f6c45206993f297372f5436b90350817bd9b4a0d52d2a76df50c1c8afa2b3c",
"description": "3D 重建 + 任意视角 + 风格还原,建筑友好(~30s + 风格化 ~72s/张)",
"pipeline": "mesh", # 新增字段区分管道类型
"default_params": {
"texture_size": 1024,
"mesh_simplify": 0.95,
"generate_color": True,
"generate_normal": True,
"generate_model": True,
},
},
"hunyuan3d": {
"id": "hunyuan3d",
"name": "Hunyuan3D-2 (高质量)",
"provider": "replicate",
"model_id": "tencent/hunyuan3d-2:b1b9449a1277e10402781c5d41eb30c0a0683504fb23fab591ca9dfc2aabe1cb",
"description": "几何质量最佳(~127s需自定义渲染二期启用",
"pipeline": "mesh",
"enabled": False, # 一期先禁用
},
}
IMAGE_MODELS["ip-adapter-controlnet-depth"] = {
"id": "ip-adapter-controlnet-depth",
"name": "IP-Adapter + ControlNet Depth (内部)",
"provider": "replicate",
"model_id": "chigozienri/ip_adapter-sdxl-controlnet-depth:0436c8702ef52616be5c30948551b3af6a86c821cca9b01f11ac297624fff14c",
"internal": True, # 不在前端展示,仅用于阶段 3
...
}
```
### 3.4 工具层 `tools.py`
`transform_view` 工具参数扩展(向后兼容):
```python
{
"name": "transform_view",
"parameters": {
"image_path": {...},
"model_id": {
"enum": ["zero123plus", "trellis", "hunyuan3d"],
"default": "zero123plus",
},
# 以下为 mesh pipeline 专属参数
"azimuths": {"type": "array", "default": [0, 60, 120, 180, 240, 300]},
"elevations": {"type": "array", "default": [0, 0, 0, 0, 0, 0]},
"preserve_style": {"type": "boolean", "default": True},
},
}
```
### 3.5 前端改造
- 模型下拉增加 "Trellis (Mesh Pipeline)" 和 "Hunyuan3D-2 (高质量)" 选项
- 选中 mesh pipeline 时,额外显示:
- "保留原画风格" 开关(默认开)
- 角度输入:简化为"快速预设6/8/12 视角)" + "自定义 azimuth 数组"(高级)
- 结果区域:除了 N 张图,还展示 `color_video` 可预览
---
## 4 · 施工任务清单(跨会话可独立执行)
### Phase 1Trellis 接入(预计 1.5h
**验收标准**:用 curl / Replicate SDK 能跑通一次 Trellis拿到 .glb + color_video URL下载到本地。
- [x] P1-1 `services/mesh_generator.py` 新建,封装 `generate_with_trellis(image_path)` 函数(调用 Replicate + 下载结果到 `GENERATED_DIR`
- [x] P1-2 `config.py` 新增 `VIEW_TRANSFORM_MODELS["trellis"]` 条目
- [x] P1-3 `services/view_transform.py` 根据 `pipeline == "mesh"` 分流到 `mesh_generator`
- [x] P1-4 `tools.py``transform_view` 增加 `model_id` 参数默认值兼容
- [x] P1-5 本地手动测试:`scripts/test_trellis.py` 可独立运行验证 Trellis → .glb + color_video + normal_video 本地落地(**等用户执行**
### Phase 2ffmpeg 抽帧(预计 1h
**验收标准**:从 color_video.mp4 抽出指定 azimuth 对应的帧,保存为 PNG帧数正确、角度映射正确。
- [x] P2-1 宿主机 ffmpeg 未装,改用 `imageio-ffmpeg==0.5.1`(静态二进制随包,免系统依赖),已写入 `requirements.txt`
- [x] P2-2 `services/video_frame_extractor.py` 新建,实现 `extract_by_azimuth(combined_video_path, azimuths, elevations=None, total_rotation_deg=360)`
- 策略改为"一次抽全帧到 temp dir + 按帧索引挑" —— 比 `-ss` 逐帧抽更稳(不受 keyframe 对齐影响),且 5s 视频成本可忽略
- 抽出后用 PIL 按中线切:左半 → color_png、右半 → normal_png实测 1024×512 → 512×512 方形)
- `ffmpeg + PIL` 是阻塞的,用 `run_in_executor` 丢到线程池避免堵住 async 事件循环
- 临时目录抽完即清理,只保留切好的最终帧
- [x] P2-3 `view_transform._transform_view_mesh` 串接 Stage 1 → Stage 2默认 `azimuths=[0,60,120,180,240,300]`Stage 2 失败不会把整体置为失败Stage 1 的 .glb / video 仍有交付价值,`error` 字段单独带回 Stage 2 错误)
- [x] P2-4 本地测试脚本 `scripts/test_frame_extract.py`**等用户执行**验收)
- Agent 已自测120 帧视频 → az=0/60/120/180/240/300 映射到 frame_idx=0/20/40/60/80/100完全均匀 ✓
- 12 个 color/normal PNG 全部落盘,尺寸 512×512 方形 ✓
### Phase 3风格还原重绘预计 1.5h
**验收标准**6 张视角图通过 IP-Adapter+ControlNet 重绘后,风格与原图一致、几何不变形。
- [x] P3-1 `config.py` 注册 `IMAGE_MODELS["ip-adapter-controlnet-depth"]`,标 `internal=True`,同时改 `get_image_models_list()` 过滤 internal不污染前端下拉
- [x] P3-2 `services/style_restorer.py` 新建,实现 `restore(original_image, structure_image, ...)` + `restore_batch()`(带并发 Semaphore 防 Replicate rate limit
- [x] P3-3 `view_transform.py` 增加阶段 3`preserve_style=True` 时对 Stage 2 的每一帧做重绘;单帧失败自动回退为 Stage 2 color 帧,不阻塞整体
- [ ] P3-4 **等待 Checkpoint 3**:调参轮次需要用户执行 `scripts/test_style_restore.py <原图>`,分别尝试 `scale=0.6/0.7/0.8` × `controlnet_conditioning_scale=0.6/0.7/0.9`,选出最佳组合写入默认参数
### Phase 4前端 + 端到端联调(预计 1h
- [ ] P4-1 `types.ts` 扩展 `ViewTransformModel` 类型
- [ ] P4-2 前端模型下拉新增两个选项
- [ ] P4-3 工作台选中 mesh 管道时显示"保留原画风"开关
- [ ] P4-4 结果面板支持展示视频color_video+ N 张风格化后的视角图
- [ ] P4-5 端到端联调:前端上传一张卡通建筑图 → 选 Trellis → 确认 N 张结果正确显示
### Phase 5Hunyuan3D-2 + 自由相机(**可选,二期**,预计 3-4h
**仅在 Phase 1-4 完成且用户明确要求自由 elevation/远近相机时启动。**
- [ ] P5-1 venv 安装 `trimesh + pyrender + PyOpenGL`Windows 上验证 headless 可用性
- [ ] P5-2 若 pyrender 在 Windows 不可行 → 改用 Blender 4.x headless`blender.exe -b --python render.py`
- [ ] P5-3 `services/mesh_renderer.py` 新建,实现 `render_views(glb_path, azimuths, elevations, distance=2.0, fov=45)`
- [ ] P5-4 启用 `config.py``hunyuan3d` 条目(`enabled=True`
- [ ] P5-5 前端"高级模式"解锁 elevation 滑块
### Phase 6收尾
- [ ] P6-1 `changelog-full.md` / `-recent.md` / `-headlines.md` 三层同步记录
- [ ] P6-2 发现的坑 → `pitfalls.md`
- [ ] P6-3 更新 `art-agent/README.md` 里视角变换段落
---
## 5 · 风险与兜底
| 风险 | 概率 | 影响 | 兜底 |
|---|---|---|---|
| Trellis 对 2D 卡通描边图重建几何偏糊 | 中 | 阶段 3 结构约束失效 | 先用 SDXL+InstantStyle 把原图"半写实化"一版给 Trellis 吃,最后阶段 3 用原 2D 图做 IP-Adapter 还原风格 |
| 从 color_video 抽帧只有水平环绕elevation=0不能仰视/俯视 | 高 | 无法满足"仰视大教堂"等需求 | 二期启用 Phase 5 的 pyrender 自由相机,或改用 Hunyuan3D-2 + 自定义渲染 |
| IP-Adapter+ControlNet 6 次调用并发打满 Replicate rate limit | 低 | 部分帧失败 | 改成串行 + 指数退避 + 失败单帧重试 |
| 总链路太慢(>3 分钟)用户失去耐心 | 中 | 产品体验差 | 前端用 SSE 流式展示中间产物:阶段 1 完成显示视频 → 阶段 3 每张完成立即插入 |
| ffmpeg 未装 | 高Windows | Phase 2 不能跑 | 开工前检查 `ffmpeg -version`,未装先 `choco install ffmpeg` 或用 Python 库 `imageio-ffmpeg` 绑定 |
| Trellis 某次输出 video 是 4s 某次 6s固定 total_rotation_deg=360 可能不准 | 低 | 抽帧角度偏差 | 用 `ffprobe` 动态读取总帧数;如有疑问就固定 24 帧均匀抽样全部保留 |
---
## 6 · 成本与性能预估
**一次完整请求6 视角,保留风格)**
| 阶段 | 耗时 | 成本 |
|---|---|---|
| Trellis 重建 | ~30s | $0.041 |
| ffmpeg 抽 6 帧 | ~2s本地 | 0 |
| ControlNet+IP-Adapter × 6并发 | ~72s | $0.42 |
| **合计** | **~105s** | **~$0.46** |
参考:当前 Zero123++ 一次 ~$0.02~15s。新管道贵约 23×、慢约 7×但产出质量对建筑是跨代提升。
---
## 7 · 跨会话恢复指引
**新会话的 Agent 在开工前务必**
1. 读本文件 **§0-§3**,建立完整技术背景
2.`art-agent/backend/app/services/view_transform.py` 当前实现Zero123++ 的做法作参照)
3.`art-agent/backend/app/config.py``VIEW_TRANSFORM_MODELS` / `IMAGE_MODELS` 当前结构
4.`art-agent/backend/app/agent/tools.py``transform_view` 工具定义
5.**§4 的 Phase 顺序**推进Phase 之间互相独立,完成一个交付一个
6. 每个 Phase 完成后在本文件对应 checkbox 打勾(改 `[ ]``[x]`),并在本节底部追加简短进度 note
**不要一次 Phase 1-6 全做完再交付**——每个 Phase 完成后停下来让用户测试/审阅。
### 进度日志
<!-- 格式YYYY-MM-DD 会话简述 → 完成了 Px-y 的哪些 item -->
- 2026-04-20 · 会话"Zero123++ 切图诊断 + 方案调研" → 完成 §2 全部 schema 查证,写入本 SPEC。尚未开工 Phase 1。
- 2026-04-20 · 会话"按计划实施 Mesh Pipeline" → 完成 Phase 1P1-1 ~ P1-5新增 `mesh_generator.py``config.py` 注册 trellis/hunyuan3d后者 enabled=False`view_transform.py``pipeline` 字段分流、`tools.py``model_id/azimuths/preserve_style` 参数,额外提供 `scripts/test_trellis.py` 供用户手动验收。
- 2026-04-20 16:15 · **Checkpoint 1 通过** → Agent 运行 `scripts/test_trellis.py` 实测发现:
- ✅ Trellis 调用链路完全打通Replicate token / 代理 / httpx 下载 / GENERATED_DIR 落盘均正常
-`.glb` (1.89MB) 下载正常
- ⚠️ 意外发现:该部署版 `color_video` / `normal_video` 字段**恒为 null**,只有 `combined_video` 可用color+normal 左右并排 mp4 ~450KB
- 修订 §2.1 输出字段表为实测版本P2-2 任务改为"从 combined_video 抽帧 + 切左右半",已记 PF-20260420-1600 踩坑
- Phase 1 verdict**functionally green**,可以开 Phase 2
- 2026-04-20 · 会话"继续执行 Phase 2" → 完成 Phase 2 全部 itemP2-1 ~ P2-4
- 放弃系统 ffmpeg改用 `imageio-ffmpeg` 自带二进制22.6MB wheelWin amd64 下已测 v4.2.2 ok
- 新增 `services/video_frame_extractor.py` — 一次全帧抽取 + 按索引挑 + PIL 切左右半的极简实现
- `view_transform.py` 的 mesh 管道调度器现已走到 Stage 2返回的 `images[]``{url, azimuth, elevation, normal_url, frame_idx}`
- 新增 `scripts/test_frame_extract.py` 供用户复用已有 combined_video 做 Checkpoint 2 验收(无需再花钱跑 Trellis
- Agent 自测120 帧 → 6 视角 frame_idx 均匀分布 (0/20/40/60/80/100),所有切图 512×512 方形,逻辑正确
- **等待 Checkpoint 2 用户验收**:跑 `python scripts/test_frame_extract.py` 并目视检查 `generated/view_*_color.png` 6 张图的角度顺序
- 2026-04-20 · 会话"继续执行 Phase 3风格还原" → 完成 P3-1 ~ P3-3
- `config.py` 注册内部模型 `ip-adapter-controlnet-depth``internal=True`),并让 `get_image_models_list()` 自动过滤,不会污染前端下拉;新增 helper `get_style_restore_model_id()`
- 新增 `services/style_restorer.py``restore()` 单帧调用 + `restore_batch()` 带 Semaphore 并发 3 的批量接口;统一处理 FileOutput/list/str 的 Replicate 返回
- `view_transform._transform_view_mesh` 接入 Stage 3`preserve_style=True` 对每一帧做 IP-Adapter+ControlNet 重绘;**单帧失败会自动回退为 Stage 2 color 帧**,不阻塞整体;`images[i]` 新增 `restyled` 布尔和 `color_url`(保留原 color 便于对比)
- 最终返回的 `_stage` 字段从 `stage2` 升级为 `stage3 (N/M restyled)`,便于前端 / 日志快速判断链路完整度
- 新增 `scripts/test_style_restore.py` 供 Checkpoint 3 用户验收:支持 `--single` 冒烟、`--prefix` 指定 normal 帧组、`--scale/--cn-scale` 覆盖参数,复用 Phase 2 已有帧无需再跑 Trellis
- **等待 Checkpoint 3 用户验收**:跑 `python scripts/test_style_restore.py <原图> --single` 做冒烟,通过后再全量 6 帧并调参P3-4
---
## 8 · 开放问题(留给未来决策)
- 前端角度输入 UX要不要做一个"3D 预览转盘"让用户拖动选角度Phase 4+ 再说)
- 是否把 mesh `.glb` 作为资产保存到用户的 gallery涉及存储与用户系统对接
- Hunyuan3D-2 路线的 Windows 渲染器pyrender vs Blender 的选型需要一次实测才能决定
- 是否对同一原图的重复请求加缓存Trellis 结果相对稳定,可按 image_hash 缓存 .glb