引入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

@@ -4,6 +4,91 @@
## 记录
### [CL-20260420-1800] 2026-04-20 18:00 — Mesh Pipeline Phase 3 风格还原接入IP-Adapter + ControlNet
- **tags**: 视角变换, Mesh Pipeline, 风格还原, IP-Adapter, ControlNet, Replicate, style_restorer, 后端, services, config, Phase 3
- **affected_files**:
- art-agent/backend/app/config.py
- art-agent/backend/app/services/style_restorer.py (新增)
- art-agent/backend/app/services/view_transform.py
- art-agent/backend/scripts/test_style_restore.py (新增)
- docs/art-agent/VIEW-TRANSFORM-MESH-PIPELINE-PLAN.md
- **what**: 按 SPEC §4 Phase 3 落地 Mesh Pipeline 的阶段 3 — 对 Stage 2 抽出的 normal 帧做 IP-Adapter + ControlNet 重绘,用用户原图做风格参考,输出保留原画风且视角正确的最终图。
- **why**: Phase 2 已能稳定产出 color/normal 6 视角帧,但 Trellis 的 color 帧是"半写实"纹理风与用户上传的卡通描边原图风格差异明显。Phase 3 用 `chigozienri/ip_adapter-sdxl-controlnet-depth`(版本 `0436c870...`$0.07/张 ~72sL40S`image` 字段传原图承担 IP-Adapter 风格参考,`controlnet_input` 传 normal 帧作为结构约束的 depth 近似(按 SPEC §1.4 一期走"路 A"简化方案,效果不足再升级为 `depth-anything-v2` 真 depth
- **decisions**:
1. 新模型以 `internal=True` 注册进 `IMAGE_MODELS`(而非 `VIEW_TRANSFORM_MODELS`),理由:它是"普通文生/图生图"模型,只是用在内部流水线;同步改 `get_image_models_list()` 过滤 `internal`,避免它出现在前端生图模型下拉
2. 暴露 `get_style_restore_model_id()` helper 让 service 层不硬编码短 ID
3. `restore_batch()``asyncio.Semaphore(3)` 控制并发而非 `asyncio.gather` 全冲 —— SPEC §5 风险表提示过 Replicate rate limit 风险
4. 默认参数选 `scale=0.75` / `controlnet_conditioning_scale=0.8`SPEC §2.3 建议范围 0.7-0.8 / 0.7-0.9 的中位偏上),最终值由 P3-4 调参确认
5. **单帧失败不阻塞整体**Stage 3 任一帧失败时自动回退为 Stage 2 的 color 帧返回,前端至少能看到东西;并在该 image 上挂 `restyle_error` 字段暴露失败原因。这一兜底 + "Stage 2 失败不影响 Stage 1 交付" 形成一致的"阶段独立可交付"设计
- **notes**:
- 成本:一次完整 6 视角请求 ~$0.46Trellis $0.041 + 6 × IP-Adapter $0.07 = $0.461),耗时 ~105s30s 重建 + 并发 3 两轮 × 72s ≈ 75s 风格化)
- `images[i]` 新结构:新增 `color_url`(保留 Stage 2 原图便于前端对比/切换)、`restyled` 布尔、可选 `restyle_error`;返回体 `_stage``stage2` 升级为 `stage3 (N/M restyled)`
- `_note` 字段去掉了,错误通过 `error` 字段聚合(拼接 Stage 2 + Stage 3 错误);`_preserve_style_requested` 保留供调试
- Checkpoint 3 脚本 `scripts/test_style_restore.py` 支持 `--single` 冒烟(~$0.07)和全量(~$0.42),以及 `--scale/--cn-scale` 临时覆盖参数,便于 P3-4 调参;默认自动挑 generated/ 下最新一组 `view_<prefix>_az*_el0_normal.png` 作为结构参考
- **P3-4 未完成**等用户用真实原图跑调参P4 前端联调将在 Checkpoint 3 通过后开启
- **source_chat**: [继续执行 Mesh Pipeline Phase 3](85c3e2f0-ab1f-48c3-9f12-a1b2c3d4e5f6)
### [CL-20260420-1700] 2026-04-20 17:00 — Mesh Pipeline Phase 2 抽帧 + combined_video 左右切分落地
- **tags**: 视角变换, Mesh Pipeline, Trellis, combined_video, ffmpeg, imageio-ffmpeg, PIL, 抽帧, 后端, services, Phase 2
- **affected_files**:
- art-agent/backend/app/services/video_frame_extractor.py (新增)
- art-agent/backend/app/services/view_transform.py
- art-agent/backend/requirements.txt
- art-agent/backend/scripts/test_frame_extract.py (新增)
- docs/art-agent/VIEW-TRANSFORM-MESH-PIPELINE-PLAN.md
- **what**: 按 SPEC §4 Phase 2 清单落地。(1) 确认宿主机 `ffmpeg` 未安装,改用 `imageio-ffmpeg==0.5.1` 自带的 Windows 静态二进制(`ffmpeg-win64-v4.2.2.exe`,随 wheel 一起打包 22.6MB免系统依赖venv 已装,`requirements.txt` 同步写入。(2) 新增 `services/video_frame_extractor.py` 实现 `extract_by_azimuth(combined_video_path, azimuths, elevations=None, total_rotation_deg=360)`:核心策略采用"一次性把 combined_video 所有帧抽到 `generated/_frames_<id>/` 临时目录 + 按 `frame_idx = round(az / total_rotation_deg * total_frames) % total_frames` 挑帧 + PIL 按中线切左半→color、右半→normal 保存到 `generated/view_<batch>_az{az}_el{el}_{color|normal}.png`",比逐帧用 `-ss` 抽更稳(不受 keyframe 对齐影响5s 视频整体成本可忽略;用完即清理临时目录,只保留切好的最终帧。(3) ffmpeg + PIL 是阻塞操作,用 `asyncio.get_running_loop().run_in_executor(None, _do_extract, ...)` 丢到默认线程池跑,避免堵住 FastAPI 事件循环。(4) `services/view_transform.py``_transform_view_mesh` 在 Stage 1 成功后串接 Stage 2——默认 `azimuths=[0,60,120,180,240,300]`(从模块级常量 `DEFAULT_MESH_AZIMUTHS` 读取),`elevations` 缺省或长度不足补 0、超长截断保证与 azimuths 对齐;调用 `video_frame_extractor.extract_by_azimuth(combined_video_path=stage1["combined_video_path"], ...)` 得到 `frames[{az, el, frame_idx, color_png, normal_png}]`;把 `images` 填为 `[{url: color_png, azimuth, elevation, normal_url: normal_png, frame_idx}]`Stage 2 失败**不**把整体置为失败Stage 1 的 mesh/video 仍有交付价值),单独在返回 dict 的 `error` 字段带回 Stage 2 错误信息,`success` 同时要求 stage1 成功 + Stage 2 无 error。(5) 新增 `scripts/test_frame_extract.py` 供用户 Checkpoint 2 验收:默认取 `generated/` 下最新的 `trellis_combined_*.mp4` 做输入(**不需要再花钱跑 Trellis**),调用 `video_frame_extractor.extract_by_azimuth` 抽 6 视角,打印每帧的 `color_png/normal_png` URL + 本地文件大小,并提示用户目视检查。(6) SPEC 文档 P2-1 ~ P2-4 全部打勾、追加本次会话的进度 note含 Agent 自测结论)。
- **why**: SPEC §4 "每个 Phase 完成后停下来让用户测试/审阅"Phase 2 是用户能看到"可见结果"的第一步——Phase 1 只能交付一个 .glb + 一个 mp4 视频用户很难一眼判断是否正确Phase 2 交付 6 张方位角明确的方形图后,用户可以直接目视环绕教堂一周。抽帧方案故意选"一次抽全 + 按索引挑"而不是"每帧一次 -ss 快进",是因为:(a) combined_video 约 5s/120 帧,总 PIL+ffmpeg 开销 ~1-2s比 6 次 -ss (每次 ffmpeg 启动 ~300-500ms) 更快;(b) `-ss` 在 GOP 长的 mp4 上会对齐到最近 keyframe取帧索引可能不精确(c) 代码简单、debug 时看临时目录文件序号就能对应帧。Stage 2 失败不牵连 Stage 1是考虑到"抽帧失败"通常意味着 ffmpeg 或视频本身异常,但用户可能仍想拿 .glb 做手动检视——与其整体 fail不如降级返回部分产物 + 明确 error。测试脚本用已有 combined_video 是故意为之Checkpoint 2 的重点是验证抽帧+切图的正确性,不是再验证一遍 Trellis没理由让用户多花 $0.041。
- **decisions**: (1) **ffmpeg 方案**:首选 `imageio-ffmpeg` 而不是让用户 `choco install ffmpeg`,因为 SPEC §5 风险栏就列了 "Windows ffmpeg 未装" 是高概率风险imageio-ffmpeg 的 pip wheel 自带 Win64 二进制是零摩擦方案;代价是 wheel 22.6MB + 每次 Python 启动查找二进制(成本可忽略)。(2) **返回 schema**:给 mesh 管道的 `images[i]` 加了 `normal_url``frame_idx` 字段而不是单独的 `frames` 列表——这样前端不用改 schema 就能显示(`url` 仍是最终要展示的彩色图),同时 Phase 3 可以直接读 `normal_url` 作为 `controlnet_input`。(3) **elevation 字段保留但忽略**combined_video 本身是水平平视一圈,`elevation != 0` 在 Phase 2 无意义(所有帧 el 都是 0但保留字段为 Phase 5 自由相机路径预留;当前传 `elevations=[30,30,...]` 也不会报错,只是结果图的 el 字段被原样记录、帧内容仍是水平视角(有心理预期即可,未来 renderer 路径会真正使用)。(4) **临时目录策略**:抽帧到 `generated/_frames_<8位hex>/` 而不是系统临时目录,是因为后者在 Windows 跨盘符时可能慢;前者失败时也留现场方便 debug。完成后的清理用 `try/except OSError` 兜底,即使 PIL 占着句柄也不会报错退出。(5) **DEFAULT_MESH_AZIMUTHS 提到模块级**:下 Phase 4 前端会暴露"快速预设 6/8/12 视角"选项,这里先把默认值拎出来便于配置扩展;目前命名为 6 视角Phase 4 再考虑增加 8/12。
- **notes**: (1) **Stage 2 路径依赖 stage1 的 `combined_video_path` 键名**(非 `color_video_path`)——这是 Trellis 部署版 `e8f6c452...` 的实测事实PF-20260420-1600不要以为 Phase 1 的字段 `color_video_path` 有值;现在该字段仍然返回但恒为 `None``_transform_view_mesh` 里优先读 `combined_video_path`。(2) **前端目前仍不会自动展示 mesh 管道结果** ——`loop.py` 只在 `result.get("images")` 非空时发 `image_result` 事件Phase 2 让 images 有值了,所以前端**应该**能看到 6 张;但 Phase 4 才正式端到端联调Phase 2 的建议验收入口仍是 `scripts/test_frame_extract.py` 而不是 UI。(3) **normal 帧的 url 在前端目前是"隐藏字段"**——`images[i].normal_url` 前端 types.ts 里没这个字段会被丢弃Phase 3 接入时 view_transform 会改为把重绘后的图填到 `url`normal 帧继续作为中间产物留在磁盘上。(4) **预留的 `preserve_style` 参数当前不生效**,返回 dict 的 `_preserve_style_requested` 字段记录了用户意图——Phase 3 会根据这个开关分流True 走 ControlNet 重绘、False 直出 color 帧)。(5) **Agent 自测结果2026-04-20 17:05**`test_frame_extract.py``trellis_combined_290da12de954.mp4` 运行,抽出 120 帧az=[0,60,120,180,240,300] 分别映射到 frame_idx=[0,20,40,60,80,100](每 60° = 20 帧完全均匀12 个 PNG 全部落盘,单张尺寸 512×512 方形(说明原 combined_video 是 1024×512 左右并排)。自测**不能替代用户目视检查** "6 张 color 图是否按 0°→300° 顺序环绕建筑一圈"Checkpoint 2 必须由用户拍板。(6) 用户验收流程:(a) 在 `art-agent/backend` 激活 venv(b) 跑 `python scripts/test_frame_extract.py`(默认取 `generated/` 最新 trellis_combined也可 `python scripts/test_frame_extract.py generated/trellis_combined_<hash>.mp4` 指定);(c) 终端应打印 6 个 color_png + 6 个 normal_png 均 `OK`,每个 size > 0(d) 打开 `generated/view_*_az0_el0_color.png ~ view_*_az300_el0_color.png` 6 张图,验证是同一建筑从 6 个均匀角度绕一圈的视角;(e) normal 图是蓝紫调法线图,可抽 1-2 张抽查。通过后开 Phase 3。
- **source_chat**: 本次会话 "继续执行 Phase 2"
### [CL-20260420-1600] 2026-04-20 16:00 — Mesh Pipeline Phase 1 接入 Trellis视角变换第二条管道落地阶段 1
- **tags**: 视角变换, Mesh Pipeline, Trellis, Replicate, 后端, services, config, tools, Phase 1
- **affected_files**:
- art-agent/backend/app/services/mesh_generator.py (新增)
- art-agent/backend/app/services/view_transform.py
- art-agent/backend/app/config.py
- art-agent/backend/app/agent/tools.py
- art-agent/backend/scripts/test_trellis.py (新增)
- docs/art-agent/VIEW-TRANSFORM-MESH-PIPELINE-PLAN.md
- **what**: 按 SPEC §4 Phase 1 清单落地。(1) 新增 `services/mesh_generator.py`,封装 `generate_with_trellis(image_path, params=None)` 和统一入口 `generate_mesh(image_path, model_id)`,调用 `firtoz/trellis:e8f6c452...` 并把 `model_file/color_video/normal_video/combined_video` 挨个 httpx 下载到 `GENERATED_DIR`,以 `trellis_*_<uuid>.{glb,mp4}` 命名,返回 `/generated/...` 本地 URL。(2) `config.py``VIEW_TRANSFORM_MODELS` 新增 `trellis`pipeline=mesh, enabled=True`hunyuan3d`pipeline=mesh, enabled=False 占位),给原 `zero123plus` 条目补 `pipeline=grid``get_view_transform_models_list` 改为只返回 enabled 条目 + 带上 pipeline 字段。(3) `services/view_transform.py` 改为按 `pipeline` 字段分流——`grid` 走原 Zero123++ 逻辑(逻辑未动,仅输出字段补齐 `pipeline/mesh_url/color_video/normal_video``mesh` 走新增 `_transform_view_mesh()`:当前阶段只跑 mesh_generator 返回 `.glb + color_video + normal_video` 本地 URL`images` 列表留空,附 `_stage="stage1"``_note` 标记 Phase 2/3 未接入。(4) `tools.py``transform_view` 工具定义扩三个参数 `model_id` (enum: zero123plus/trellis, default zero123plus) / `azimuths` / `preserve_style`,执行侧把这些参数透传给 `transform_view()`。(5) 新增 `backend/scripts/test_trellis.py` 作为 Phase 1 独立验收脚本——默认用 uploads/ 下最新 png、支持命令行指定路径、自动加载 .env、调用 `mesh_generator.generate_with_trellis` 后检查本地文件大小。(6) SPEC 文档 P1-1 ~ P1-5 打勾并追加进度 note。
- **why**: SPEC 明确要求"按 Phase 顺序推进,每个 Phase 完成后停下来让用户测试/审阅"Phase 1 是整条 Mesh Pipeline 的地基——验证 Replicate Trellis 调用通道是否打通、文件下载策略是否正确、与现有 grid 管道并存不互相干扰。把 mesh_generator 独立成模块是为后续 Phase 2/3/5 解耦Phase 2 只需读 color_video_path/normal_video_pathPhase 3 只需消费 images 列表Phase 5 切到 Hunyuan3D-2 时只需替换 generate_mesh 的分支。测试脚本独立走 asyncio 是因为 Phase 1 还没接到工具链,用户需要一个"最小能跑"的入口先确认 API 本身可用SPEC P1-5 也是这么要求的)。
- **decisions**: (1) `view_transform` 返回结构升级为同时带 `grid_image/mesh_url/color_video/normal_video/pipeline` 字段,两种管道缺省位补 `None`——比"两种完全不同的返回 schema"更利于前端和 loop.py 后续统一处理。(2) Phase 1 当前故意让 mesh 管道的 `images` 为空 + 带 `_note` 说明,而不是硬拼一个"只返回视频"的假结果——这样 Phase 2 接入时 frontend 的 "没有图片显示" 现象会被替换为 "6 张图片显示",语义更清晰。(3) hunyuan3d 提前注册但 `enabled=False`,避免用户在前端误选到二期才启用的路径;`get_view_transform_models_list` 同步过滤 enabled。(4) `mesh_generator._to_url` 做了 `FileOutput → str` 的显式转换兜底,因为 Replicate SDK 的字典输出里每个字段可能是 FileOutput 对象而不是裸字符串 URL——这点在 image_gen.py 的 `ReplicateProvider._download` 已经吃过亏,这里主动规避。(5) `_download_asset` 的 httpx 超时放到 300s因为 color_video ~3-5MB 在国内网络可能慢;复用项目既有的 HTTPS_PROXY 注入模式。
- **notes**: 本次改动**完全不触碰 Zero123++ 现有流程**——grid 管道所有行为保持 byte-level 等价,只是返回字典多了几个 None 字段,`loop.py` 和前端已有代码没事。但还**未修改 loop.py 的 SSE 事件处理**:当前 loop.py 只在 `tool_name == "transform_view" and result.get("images")` 时发 `image_result`mesh 管道 Phase 1 的 `images=[]` 会让 loop.py 不发事件,前端看到空结果——这**是预期行为**Phase 1 的测试入口是 `scripts/test_trellis.py` 而不是前端。Phase 2 接入后 images 有值loop.py 无需改动即可工作。用户验收流程:(a) 在 `art-agent/backend` 激活 venv(b) 跑 `python scripts/test_trellis.py`(默认取 uploads/ 最新 png`python scripts/test_trellis.py uploads/<hash>.png` 指定图;(c) 等 ~30-60s 看终端打印 `glb_path/color_video_path/normal_video_path` 均为 `/generated/trellis_*...` + 本地大小 > 0(d) 可选:去 `backend/generated/` 目录用系统播放器看 color_video.mp4 是否为 360° 环绕教堂视频。验收通过后我再开 Phase 2ffmpeg 抽帧)。
- **source_chat**: 本次会话 "按计划实施 Mesh Pipeline"
### [CL-20260420-1530] 2026-04-20 15:30 — 写入 Mesh-Pipeline 视角变换方案 SPEC跨会话实施计划
- **tags**: 视角变换, Mesh Pipeline, Trellis, Hunyuan3D-2, ControlNet, IP-Adapter, SPEC, 方案设计, 建筑, 跨会话
- **affected_files**:
- docs/art-agent/VIEW-TRANSFORM-MESH-PIPELINE-PLAN.md
- **what**: 新增一份完整的实施计划 SPEC规划 Zero123++ 之外的第二条视角变换路径——Mesh Pipeline。三段式流水线(1) Trellis/Hunyuan3D-2 单图生 mesh + 360° color_video(2) ffmpeg 从 color_video 抽指定 azimuth 的帧(一期方案,免 pyrender(3) `chigozienri/ip_adapter-sdxl-controlnet-depth` 做 SDXL+ControlNet-Depth+IP-Adapter 二次重绘,用原图保持卡通描边风格。文档含:三个 Replicate 模型的精确版本 ID + OpenAPI schema、config.py 扩展设计、伪代码管道、6 个 Phase 的可独立执行任务清单(每个 Phase 有独立验收标准)、风险兜底、成本预估(~$0.46/~105s、跨会话恢复指引。
- **why**: 用户反馈 Zero123++ 只能出 6 个固定视角 + 建筑大角度偏转一致性差。用户选定重流水线P1-P3 全做),明确要求"保留 2D 卡通风格"+"自包含跨会话执行"。调研中发现Trellis ($0.041/30s 自带 color_video) 比 Hunyuan3D-2 ($0.12/127s 仅 mesh) 快 4× 便宜 3×一期走 Trellis 还能省掉自定义渲染模块。把 Hunyuan3D-2 路线拆进 Phase 5二期可选工程量从 ~9h 降到 ~5h。
- **decisions**: 主选 Trellis 而非 Hunyuan3D-2——以便一期完全跳过 pyrender/blender 渲染器(用 ffmpeg 抽帧替代)。阶段 3 的 ControlNet 结构约束用 normal_video 抽帧Trellis 送的),不先走 depth-anything-v2省一次 API 调用不完美但足够。Hunyuan3D-2 + pyrender 自由相机留作 Phase 5 可选。不在一期解决 elevation 仰俯视角Trellis color_video 只给水平环绕),那是 Phase 5 的事。
- **notes**: 文档位置 `docs/art-agent/VIEW-TRANSFORM-MESH-PIPELINE-PLAN.md`。下一个会话开工时 Agent 需先读 §0-§3 建立背景,再按 §4 Phase 顺序推进,每 Phase 交付后停下来让用户验证。文档底部有"进度日志"章节,每个 Phase 完成后在 checkbox 打勾 + 追加 note。未修改任何业务代码纯 SPEC 文档。
- **source_chat**: 本次会话排查 Zero123++ 切图错位 meta-bug + 后续方案调研
### [CL-20260420-1501] 2026-04-20 15:01 — 后端增加 watchfiles 依赖,修复 Windows uvicorn --reload 失效
- **tags**: uvicorn, watchfiles, 热重载, Windows, FastAPI, requirements, 依赖管理, reload, Zero123++
- **affected_files**:
- art-agent/backend/requirements.txt
- art-agent/backend/requirements-lock.txt
- **what**: 在 `requirements.txt``requirements-lock.txt` 中显式加入 `watchfiles==0.24.0`,并在当前 venv 中安装。这让 uvicorn `--reload` 使用 WatchFiles 作为 reloader 后端,而不是无依赖时的 StatReload 轮询回退。
- **why**: 用户反馈 Zero123++ 切图错位——但 CL-20260420-1212 里做的 `cols=2/rows=3` 修复和 `_split_grid_image` 自动纠错逻辑在磁盘上明明是新版,独立调用也能切出正确的 320×320。进一步排查发现后端 uvicorn 进程是 11:55 启动、从未 reload而 venv 里 `watchfiles`/`watchdog` 均未安装——StatReload 在 Windows + 深层子目录 + 新建文件 (`view_transform.py` 是 untracked 新增) 的叠加场景下几乎完全失效,所以代码改了但运行时还是老版本。这一层 meta-bug 如果不治本,以后每次改后端都要手动重启。
- **decisions**: 选择 `watchfiles`uvicorn 官方推荐、跨平台 Rust 实现、依赖最轻)而不是 `watchdog`(历史包袱更重)。版本锁 0.24.0 与 Python 3.12 兼容良好且 uvicorn 0.44.0 默认识别。未添加 `--reload-dir app` 参数到 project-launcher Skill 的启动命令里——先靠装依赖覆盖主要场景,如果未来仍有监听遗漏再显式限定目录。
- **notes**: **仅装依赖不重启当前 uvicorn 进程不会生效**,因为旧进程已经绑定到 StatReload。需要 kill PID 28172 并重启一次后端,之后所有代码改动才会自动 reload。启动日志里应该能看到 `Started reloader process [xxx] using WatchFiles`,如果仍是 `StatReload` 说明 watchfiles 没正确安装到当前 venv。踩坑已记录 PF-20260420-1501。
- **source_chat**: 本次会话排查 Zero123++ 切图错位 meta-bug
### [CL-20260420-1212] 2026-04-20 12:12 — 修复 Zero123++ 视角切图布局反转导致的图像错位
- **tags**: Zero123++, 视角变换, 切图, bug修复, grid_layout, PIL, Replicate, view_transform
- **affected_files**:
- art-agent/backend/app/config.py
- art-agent/backend/app/services/view_transform.py
- **what**: 修复 `transform_view` 输出的 6 张视角图严重错位的问题。`config.py` 里的 `VIEW_TRANSFORM_MODELS["zero123plus"].grid_layout``{"cols": 3, "rows": 2}` 改为 `{"cols": 2, "rows": 3}`,与 `jd7h/zero123plusplus` Replicate 实际输出(高图布局)一致。同时在 `view_transform._split_grid_image` 加入"按实际图像长宽比自动纠正 cols/rows"的健壮性保护:假定每个单视角为正方形,若配置的 `cols/rows` 与图像实际 `w/h` 差异超过 20%,自动交换 cols/rows 并记录 warning。
- **why**: 用户反馈视角变换后的切图全部错位(窄长条、上下拼两个物体)。根因是 Zero123++ 官方 README 写 "3×2 grid" 但实际 Replicate 部署版输出的是 2 列 × 3 行高图config 里按"3 列 × 2 行"配置cell_w/cell_h 算反了。加上动态纠错是为了未来换其他 view transform 模型时不再重复踩这个坑。
- **decisions**: 选择"修 config + 加运行时自动纠错"而非仅修 config。纯改 config 能解决眼前问题但无法防御未来模型切换;加动态纠错的代价只是一段 if 判断+warning 日志,换取对"布局文档歧义"场景的整体免疫。放弃了更激进的"用 PIL 探测内容块边界"方案,因为对有微量白边/噪声的模型不稳定。
- **notes**: uvicorn 以 `--reload` 方式运行,改完自动重载,无需重启服务。用户需要在前端重新触发一次视角变换来验证切图是否正确。对应踩坑记录已写入 `.cursor/pitfalls/pitfalls.md` (PF-20260420-1212)。
- **source_chat**: 本次会话启动项目并调试 Zero123++ 切图
### [CL-20260417-1510] 2026-04-17 15:10 — Cloudflare Quick Tunnel 脚本修复PS5.1 编码 + 本地 cloudflared
- **tags**: 内网穿透, cloudflared, PowerShell, start-tunnel, UTF-8, gitignore
- **affected_files**: