# 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_video,Hunyuan 贵 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) - **不返回预渲染视频** → 必须额外实现阶段 2(pyrender / 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 # [新增] 阶段 3:ControlNet + 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 1:Trellis 接入(预计 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 2:ffmpeg 抽帧(预计 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 5:Hunyuan3D-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 完成后停下来让用户测试/审阅。 ### 进度日志 - 2026-04-20 · 会话"Zero123++ 切图诊断 + 方案调研" → 完成 §2 全部 schema 查证,写入本 SPEC。尚未开工 Phase 1。 - 2026-04-20 · 会话"按计划实施 Mesh Pipeline" → 完成 Phase 1(P1-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 全部 item(P2-1 ~ P2-4): - 放弃系统 ffmpeg,改用 `imageio-ffmpeg` 自带二进制(22.6MB wheel,Win 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)