# Pitfall Journal 开发过程中踩过的坑,按时间倒序排列。 Agent 进入 Debug mode 或遇到运行时错误时自动检索匹配。 --- ### [PF-20260420-1600] firtoz/trellis Replicate 部署版 schema 与 README 及官方 API 文档不一致 - **症状**: (1) 按 Replicate README example 用 `input={"image": uri}` 传单数 image → `422 Input validation failed: input: images is required`。改成 `images: [uri]` 后模型能跑,产出 model_file/combined_video/gaussian_ply/no_background_images。(2) 尽管 `generate_color=True` + `generate_normal=True` 都开,输出字典里 `color_video` 和 `normal_video` 字段**恒为 null**,只有 `combined_video` 这个"color 和 normal 左右并排的 mp4"才真正可用。 - **根因**: `firtoz/trellis:e8f6c45206993f297372f5436b90350817bd9b4a0d52d2a76df50c1c8afa2b3c` 这个 Replicate 部署版的实际输入/输出 schema 与 firtoz 在 Replicate README 里贴的 example、以及 microsoft/TRELLIS 官方仓库的 Python API 都不一样——输入字段是 `images`(数组)而不是 `image`(字符串),输出字段虽然声明了 `color_video` 和 `normal_video` 但该部署版恒给 null,只填 `combined_video`。很可能是部署者在包装层做了合并(省带宽/对象存储开销)但没同步 README。 - **解法**: (1) 输入字段固定传 `images: [uri]` 数组,即使只有 1 张图;(2) Phase 2 抽帧的数据源从 `combined_video` 切——每一帧的左半 = color、右半 = normal,用 PIL crop 即可;(3) 不要相信 Replicate README 上的 Python example,永远以 **该版本 ID 的 OpenAPI schema** 为准(虽然 firtoz/trellis 这个 README 也看不到精确 schema,所以只能实测)。 - **防御**: 接 Replicate 新模型前先用 1 美元额度做一次最小输入测试,打印 `output` 的原始 dict 结构 + 每个 key 的值类型;不要假设 README 的 example 与实际部署版本一致。`mesh_generator.py` 里已经做了"把 raw_output_keys 和每个字段的 None 警告都打印"的防御日志。 - **关联**: Replicate, firtoz/trellis, Mesh Pipeline, view_transform, Trellis, 3D 重建, schema, OpenAPI, mesh_generator.py ### [PF-20260420-1501] Windows uvicorn --reload 缺 watchfiles 退化为轮询,代码改动不被热重载 - **症状**: 代码已修好(git diff / Read 看源文件都是新版),但后端运行时行为仍是旧代码。具体表现:CL-20260420-1212 修好 Zero123++ 切图后(`cols=2/rows=3` + 自动纠错),前端再触发一次转视角,切出的 view 图仍是 213×480(= 640/3 × 960/2,旧的 `cols=3,rows=2` 逻辑),而独立用当前代码调用 `_split_grid_image` 能正确切出 320×320。进程启动时间 11:55,改动时间 12:12,观察时间 15:00,期间未 reload 过任何改动。 - **根因**: uvicorn `--reload` 在未安装 `watchfiles` 或 `watchdog` 时 fallback 到 `StatReload`(基于轮询)。Windows 文件系统事件语义本就弱,加上深层子目录(`app/services/`)+ 新建文件(`view_transform.py` 在仓库里是新增 untracked 文件),几个因素叠加让 StatReload 漏通知/响应极慢,表现为"启动后改什么代码都不会重载"。venv 里 `watchfiles` 和 `watchdog` 双双 MISSING 是核心证据。 - **解法**: (1) `pip install watchfiles==0.24.0`(uvicorn 官方首选 reloader);(2) 写入 `requirements.txt` 和 `requirements-lock.txt`,避免其他设备重装环境时再次踩坑;(3) **kill 当前 uvicorn 进程重启一次**——新进程才会用上 WatchFiles reloader,并同步加载最新代码。仅装依赖不重启不会生效,因为当前进程已经加载了 StatReload。 - **防御**: Windows 上只要 uvicorn 带 `--reload`,`watchfiles` 就是硬依赖,不是可选项。启动后首屏日志如果看到 `WatchFiles detected changes` 或 `Started reloader process [xxx] using WatchFiles` 才算正常;如果只有 `Started reloader process [xxx] using StatReload` 就是退化状态。可以额外传 `--reload-dir app` 显式指定监控范围更保险。任何"改了代码不生效"的诡异现象,第一时间先确认进程是否已经重载(看日志里的文件变更提醒,或对比进程启动时间 vs 文件 mtime),再排查代码逻辑。 - **关联**: uvicorn, --reload, watchfiles, StatReload, FastAPI, Windows, 热重载, requirements.txt, view_transform.py, Zero123++ ### [PF-20260420-1212] Zero123++ 输出 grid 布局被文档措辞"3×2"误导 - **症状**: Zero123++ 视角变换后切出的 6 张图严重错位,每张子图呈窄长条(宽:高 约 1:2),画面里上下拼着两个教堂,边缘大量留白。拼接图原图视觉正常。 - **根因**: Zero123++ 官方 README 写 "output is a 3×2 grid of 6 views",但 `jd7h/zero123plusplus` 这个 Replicate 部署版本实际输出的是 **2 列 × 3 行**(高图,如 640×960),而 config.py 里按"3 列 × 2 行"配置了 `grid_layout`。切图时 `cell_w = 640//3 = 213, cell_h = 960//2 = 480`,每个切片横跨了真实 grid 中两行的一部分,于是每张图都拼着两个视角的局部。 - **解法**: (1) `config.py` 的 `VIEW_TRANSFORM_MODELS["zero123plus"].grid_layout` 改为 `{"cols": 2, "rows": 3}`;(2) `view_transform._split_grid_image` 加入"按实际图像长宽比 vs 配置 ratio 自动纠正"——假定每个单视角为正方形,真实 aspect ratio 应等于 `cols/rows`,若配置 ratio 与实际差异 >20% 就自动交换 cols/rows 并打 warning。 - **防御**: 任何涉及"grid 拼接图切分"的场景,不要完全相信模型文档的"A×B"措辞(A 是列还是行常有歧义),必须用**实际图像长宽比**做健壮性检查;最稳妥的做法是让 split 函数本身能根据单视角正方形假设自动推断布局,不信任硬编码。更激进的方案是直接用 PIL 探测内容块边界(但对有微量白边的模型不稳定)。 - **关联**: Zero123++, Replicate, jd7h/zero123plusplus, grid 切图, PIL, view_transform.py, config.py, Novel View Synthesis ### [PF-20260412-1530] Python load_dotenv 默认不覆盖已有环境变量 + 模块级变量在 import 时固化 - **症状**: 修改 .env 切换 API 供应商后,后端仍报旧供应商的错误(如 DeepSeek 报 "Model Not Exist",实际发送的模型名是 OpenAI 的 gpt-4o-mini) - **根因**: 两层叠加:(1) `load_dotenv()` 默认 `override=False`,如果进程/系统中已有同名环境变量,.env 中的新值不会生效;(2) `config.py` 使用模块级常量 `LLM_MODEL = os.getenv(...)` 在 import 时就求值固化,而 `main.py` 的 import 链在 `load_dotenv()` 之前就触发了 config.py 的加载 - **解法**: (1) `load_dotenv(override=True)` 并提到所有业务 import 之前;(2) config.py 改为函数式 `get_llm_model()`,运行时才读取 - **防御**: 任何 Python 项目使用 dotenv 时,始终 `override=True` + 放在文件最顶部(仅在 `import os` 和 `from dotenv import load_dotenv` 之后);配置值用函数或属性包装,不要用模块级常量 - **关联**: Python, dotenv, FastAPI, uvicorn, 环境变量, 配置管理, OpenAI SDK