一个跑在用户本地的「摄影工作区 Agent」,用 MiniMax M3 多模态帮摄影师做一键调色 / 一键筛片 / 回答摄影问题。数据不离开电脑。
这一版核心做了三件事:修 cull bug + 多会话模式 + AI 调色优化 + 新加"AI 看图"功能。
相比 v0.2.2:解决"修图色彩怪异"问题(M3 之前会让原图氛围丢失),并新增"AI 看图"功能 — 上传单图,Agent 给出 5 维评价 + 问题清单 + 修图建议。
之前 cull 完 frontend 拿到任务详情时,photos[].output_path 全是 null,只能看 keep/quality 状态,看不到产出文件在哪。根因是 upsert_photo 调 output_path=... 时 dest 还没赋值(UnboundLocalError)。v0.2.3 改完:
- 复制 → catalog 顺序对调,
output_path真的写进 SQLite TaskProgress.tsx加 cull 照片列表 + ✅/❌/failed 状态 + 每张照片的output_path链接(可预览/打开)- "复制路径" 按钮(
navigator.clipboard.writeText(outputFolder)),在 web 模式下方便拿到结果目录 - 任务级
output_folder字段(整组的输出目录)同步到前端
之前只有一个 messages 列表,新建任务要 Ctrl+A → Del。v0.2.3 改成左侧栏对话列表:
- 每个对话的 messages 完全独立(
Conversationinterface +activeConvId切换),互不串 - 新建 / 删除 / 切换 / 清空当前对话
- localStorage 持久化(
phc.conversations.v1+phc.activeConv.v1),刷新页面不丢 - 自动迁移旧 key (
phc.messages.v1→ 第一个 conv 的 messages) - 每个 conv cap 200 条 messages,总数 cap 30 convs(超出自动 trim)
- 后端
/chat当前是单轮(M3 chat 端点无 multi-turn support),所以"独立上下文"指前端 messages 隔离,不会跨对话续聊——这点在 chat panel 文档里会写明
之前调色效果不理想的根因 — M3 没拿到足够上下文就开始下参数,所以经常瞎给:明明偏色了却给"自然还原",照片欠曝却不动 exposure,HSL 一拉就给橙色 +30 把肤色搞怪。v0.2.3 从三方面系统修:核心是"AI 自己看图说话",不是套预设滤镜。
重要设计变更:之前一版试过加 7 种"风格 preset"按钮(自然/明亮/电影/暖色/冷色/胶片/黑白),但这其实就是把市面滤镜换个名字让 AI 套——AI 该做的是读懂这张图,不是给一张"看起来差不多"的滤镜。v0.2.3 砍掉所有 preset 按钮,只留一个"用自然语言告诉 AI 你想做什么"的输入框,所有调色方向由 AI 基于客观分析自己决定。
之前 v0.2.2 的 bug:M3 看到"整体偏冷"就给"加暖 +5~8",把舞台 LED 冷蓝 / 逆光暖橙 / 夜景霓虹的氛围都"中和"了 — 修完图反而失去原图设计感。
根因:
- prompt 没说"原图不一定是错的",M3 默认所有偏色都是"待修问题"
- M3 输出 JSON 时配合
response_format=json_object会和 prompt 里"先 prose 再json"的要求冲突,直接返回{}(2 token)放弃
修法:
- "原图不一定是错的"写进 prompt 最前面,并明确教 M3 哪些场景该保留(舞台/逆光/夜景/阴天/影棚/街拍),哪些是真问题(欠曝 0.7 档以上 / 高光 clip > 3% / 严重偏色)
- 加
no_op: bool字段 — M3 判断"原图主基调就该这样"就no_op=true,其他参数全 0,color_grade.py短路直接返回原图拷贝 - 加
preserve: ["..."]字段 — M3 显式列出"该保留的色彩/光感/氛围"(算法层会尊重) - 删 prose 要求,改成"只输出 JSON",文字说明都进 JSON 字段(避免 json_object 冲突)
真 M3 跑出来的效果:
- 逆光弹琴图:M3 正确判断"主基调必须保留,暖就是灵魂",WB 不动 0,只 shadows +20 救主体,vib +5 提血色。结果:逆光氛围完整保留,主体从"过暗"变成"可读"
- 舞台舞者图:M3 正确判断"冷青 LED 是氛围核心",WB 只动 +8(不是 +30),主调放在 shadows +35 救脸部。结果:冷蓝 LED 还在,主体面部提亮,没"拉暖杀掉整个氛围"
- 强制 M3 看到 EXIF + 客观分析后,自己判断这张图属于什么场景、有什么问题、保留什么
scene/diagnosis/notes都要写真实观察(不是套话),notes 必须说"我看到 X,所以动了 Y"- 禁止套固定套路:不是所有夜景都该冷调,不是所有"看着闷"都该 +contrast,不是所有"逆光"都该提亮
- 参数范围给"sweet spot 提示"(人像保守 / 风景大胆 / 演出/夜景针对当前光),但是参考不是模板
- 每张调色前同步读 EXIF(品牌/型号/ISO/光圈/快门/原 WB/拍摄时间),写进 prompt
- 客观分析:平均亮度 / 直方图 8 段分布 / 平均饱和度 / 阴影/高光 clip 比例 / 估计色温(K) / 肤色像素占比
- M3 看到"肤色占比 18%"就知道保护肤色;看到"色温 ~7500K + 偏冷光"就知道这是阴天/夜景,可能保留戏剧性而不是压
点 👍 / 👎 会被 catalog 记录(photos.feedback),下次调色前取最近 10 条,告诉 M3 "用户喜欢什么 / 不喜欢什么"。例:"👍 的最近 3 张:contrast=20, temp=+15, sat=10" → M3 后续输出会更靠近这个调子。
- HSL mask 改 cosine falloff(原来硬阈值,现在 ±0.11 hue 距离内 cos 曲线渐变),无 banding
- HSL 强度按 saturation 衰减 — 灰区(低 s)不会被乱加色,避免 noise
- 对比度改 5 控制点 + 5-tap Gaussian 平滑 LUT(原来线性 S-curve 有硬拐点)
- highlights / shadows 用 smoothstep,过渡更柔
- 黑白 luminance mask 改 Rec.709(0.2126 R + 0.7152 G + 0.0722 B),更接近人眼
- 新加 split toning — highlights / shadows 独立加色,XMP sidecar 写全 Lightroom 字段
- 调参范围收窄(wb ±15 而不是 ±100,sat ±10 而不是 ±30),让 mock/弱模型也不会"一拉到底"
之前只写基本 11 个字段,Lightroom 导入丢 split tone / 部分 HSL。v0.2.3 加上 crs:SplitToningHighlightHue/Saturation + crs:SplitToningShadowHue/Saturation,Lightroom 直接看到。
v0.2.3 修完"色彩怪异"问题后,顺带加了独立功能 — 用户上传单图,Agent 全面评价,给修图建议。
功能定位:
- 一键修图(grade):上传一批图 → M3 给每张调色参数 → 系统 apply → 输出 JPEG/XMP。直接改图。
- AI 看图(analyze):上传单图 → M3 给 5 维评价 + 问题清单 + 修图建议(JSON)。不直接改图,用户拿到报告自己看(可以自己手动精修,或者拿建议喂回 grade 任务)。
怎么用:
- 左侧栏点 "AI 看图" tab(眼睛图标)
- 拖拽 / 点选上传 JPG / PNG(最大 30MB)
- 等 5-15 秒,M3 返回报告
报告长啥样:
- 头部:
category(人像/街拍/演出/夜景/...) + 综合分(1-5 星) - 5 维评分: 构图 / 光线 / 色彩 / 主体 / 技术 各 1-5 星
- 👍 做得好的 (
strengths): 3-5 条具体点 ⚠️ 问题清单 (issues): 每条带type/severity(minor/moderate/major) /description/fixable(修图能救 vs 拍摄问题)- 🛠️ 修图建议 (
suggestions): 每条带category/action/priority(high/medium/low) - 专项总评:
composition_notes/lighting_notes/color_notes三个段 - 🔒 最该保留的 (
preserved): 1 句话 — 这张图最不能动的部分,后期所有调整都围绕这个 - 总结 (
summary): 1 句
真 M3 跑出来的效果(逆光弹琴图,5 维评分):
- 构图 3 / 光线 4 / 色彩 4 / 主体 3 / 技术 3,综合 3
- 5 条 strengths(具体到"发丝上的金色轮廓光非常漂亮,是整张图最出彩的细节")
- 6 条 issues(1 major: 谱架挡脸 / 1 moderate: 脸欠曝 0.5-1 档 / 4 minor: 上方留白多、右侧观众头、锐度偏软、电缆乱)
- 8 条 suggestions(3 high: 阴影 +40-60 + 橙色 HSL + 径向蒙版提亮 / 2 medium: 裁切 + 锐化 / 3 low: 修脏 + WB + 拍摄建议)
- preserved: "发丝上的金色逆光轮廓和户外黄昏的温暖氛围 — 这是这张图最不能动的灵魂"
API: POST /analyze/photo 接收 multipart file 字段,返回 {analyze_id, photo_path, report}。
这一版核心解决两个真在用的痛点 + 顺手清理了若干历史包袱。
- 修图不再"静默失败" — 之前修图/筛片过程的所有日志(
Grading xxx.jpg/Wrote XMP sidecar/M3 returned invalid JSON等)只在磁盘sidecar.log里能看到,UI 上不显示。v0.2.0 之后 sidecar 启动时主动验证 M3 key(M3Client.verify_key),OpenRouter 走/auth/key不耗 credits,其他 provider 走 1-token chat。401 / 余额 0 / 网络错误都会在 cmd 启动瞬间打到屏幕,不用等用户开了 UI 看到"修图全失败"才发现 key 配错。后端m3_client.chat/stream_chat捕获 401 抛M3 API key 无效或被拒绝 (401): <err>友好提示,前端 chat 把它写到消息流末尾,修图每张照片显示photo_failed+ 具体原因 - 后端活动从 UI 拿掉,改到 cmd — 之前左边栏的「后端活动」tab 接 SSE 拿日志,但 (a) EventSource 频繁 mount/unmount 会导致"刷新一次就不显示";(b) 用户切到 logs tab 一次切走再切回,recentLogs state 丢失,什么也看不到。v0.2.0 砍掉这个 tab,改由
start.py把 sidecar 的 stdout/stderr 全部继承给启动脚本,所有后端日志实时显示在你运行的 cmd 窗口里。需要查历史可以tail -f workspace/.logs/sidecar.log - "文件传不上去"修复 —
/upload端点之前对f.size is None的流式上传会漏检 payload size,大文件夹上传可能突然 413 但 cmd 看不到原因。v0.2.0 把 pre-check 改成"size 缺失时按 50MB/张估算"兜底,所有失败路径(超大文件 / payload too large / 全部被 reject / 中途写盘失败)都打logger.warning,cmd 里直接看到具体哪一步出问题 - m3_client 清理 — 删了
_iter_stream_chat里一坨continue之后永远不会执行的死代码、修了import asyncio放文件末尾的丑问题、M3 plugin header 改成只在非 OpenRouter 时发(避免被原样转发给上游 provider 触发 4xx) - mock 模式筛片修好 —
build_cull_prompt第一句"请判断这张照片是否应该保留"不含 cull/screen/废片 等关键字,旧 mock fallback 返回的 dict 没有keep字段,导致 mock 模式下筛片全留。v0.2.0 加"判断这张照片 / 保留"作为更宽泛的兜底关键字 - 版本号 —
Sidebar左下角显示v0.2.0,与backend/pyproject.toml/frontend/package.json同步
- M3 key 启动时验证 — 见上文 v0.2.0 说明,这一版做了第一版但只发了 warning,UI 仍是裸的。后端
_unwrap拿不到这个信息,所以修图/chat 看着像"网络挂了" - M3 401 友好提示 — 同上,后端抛
RuntimeError("M3 API key 无效或被拒绝 (401): ..."),前端 chat 把 message 写到消息流末尾 - 详细排查指南 — 见下方「常见问题 → M3 报 401 / User not found」
- 上传入口 — 前端加「上传文件 / 上传文件夹」按钮,选完自动落盘到
workspace/input/<时间>-uploaded/,只接受 arw/cr2/cr3/nef/dng/jpg/jpeg/png,其他自动过滤 + 计数 - 修图识别修复 — 之前"应用无法识别工作区里的照片"是因为
iterdir()不递归 + 散文件被忽略;现在list_input_folders返回loose_files,grade/cull_photos递归扫描并跳过系统目录 - 修图/筛片独立页面 — 从聊天框上方移出来,和「智能对话」平级放在 Sidebar(v0.2.0 把「后端活动」tab 拿掉)
- 进度条 bug 修复 — SSE race condition:前端 EventSource 建立前,后端已发的
task_started和早期事件丢失。现在后端每个 task 维护 event buffer,SSE 建立时先 replay;同时start_grade/cull等前端订阅(最多 3 秒)再开跑 - 刷新持久化 — 聊天记录、任务进度、当前 view 全部 localStorage 持久化,刷新页面不丢;Sidebar 加「清空历史」按钮
- 完整测试 — 新增 23 个上传相关单测,后端 52/52 通过
完整变更(从 v0.1.0 累计):新文件(upload/, views/, usePersistedState, WorkspacePanel, UploadButton) + 重构(App.tsx, Sidebar.tsx, api.ts) + 修复(tools.py _iter_photos, main.py SSE buffer + pending_start, v0.2.0 删 LogPanel/SSE/前端日志总线、删 m3_client 死代码、修 mock cull prompt 关键字、upload 端点加强日志和大小预检)。
Windows:解压后双击 启动.bat,等 1-2 分钟,看到「应用已就绪」后浏览器开 http://localhost:1420。
macOS / Linux:
chmod +x start.sh && ./start.sh
python3 stop.py # 停
python3 diagnose.py # 诊断,生成 diagnose.txt启动脚本会自动装 Python venv + Node 依赖、配 pip / pnpm 镜像源、起 sidecar + Vite。不动 MSVC,不动 Rust 编译。
┌──────────────────────────────────────────────────────┐
│ React 前端 (Vite :1420) │
│ 智能对话 / 一键修图 / 一键筛片 │
│ (后端日志从 v0.2.0 改到启动 cmd 实时显示) │
└────────────────────┬─────────────────────────────────┘
│ HTTP + SSE
▼
┌──────────────────────────────────────────────────────┐
│ Python Sidecar (FastAPI :8765) │
│ 任务调度 · 图像处理(rawpy / opencv) · M3 调用 │
│ 工作区管理(input / output / .tasks / .logs) │
└────────────────────┬─────────────────────────────────┘
│ HTTPS
▼
┌──────────────────────────────────────────────────────┐
│ M3 (MiniMax-M3) via OpenRouter │
│ 图像质量评分 · 构图分析 · 摄影知识问答 │
└──────────────────────────────────────────────────────┘
v0.2.3+ 计划接入 Tauri 2 (Rust) 桌面壳,代码在 src-tauri/ 已就位(v0.2.0 暂不启用,需要 MSVC Build Tools)。
- 一键修图 (grade) — 批量调色,自动色温/曝光/对比度
- 一键筛片 (cull) — 智能去重,质量评分,保留/剔除建议
- 摄影问答 (chat) — 问「什么是光圈优先」之类,SSE 流式响应
- 后端活动(cmd) — sidecar 的所有 stdout/stderr 实时显示在启动 cmd 里(替代了 v0.1.x 时的"后端活动" UI 面板)。文件日志同步写到
workspace/.logs/sidecar.log方便tail -f - 照片反馈 — 用户对结果点赞/踩,后续风格学习用
- 本地工作区 —
workspace/input/放照片,workspace/output/存结果 - M3 离线模式 — 不配 key 自动进 mock,本地开发无障碍
| 层 | 技术 |
|---|---|
| 前端 | React 18 + Vite 5 + TypeScript + Tailwind 3 + lucide-react |
| 后端 | Python 3.10+ + FastAPI + uvicorn |
| 图像 | rawpy + opencv + Pillow |
| LLM | MiniMax M3 (openai-compatible, 走 OpenRouter) |
| 桌面壳 (预留) | Tauri 2 + Rust + WebView2 |
| 构建 | pnpm 11 + Vite |
photographer-copilot/
├── 启动.bat / 停止.bat / 诊断.bat # Windows 一键脚本
├── start.sh / start.py / stop.py # macOS / Linux / 跨平台
├── diagnose.py # 诊断脚本
├── .env / .env.example # M3 配置(env 不进 git)
├── package.json + pnpm-lock.yaml # 根 workspace
│
├── backend/ # Python FastAPI sidecar
│ ├── agent/ (main.py / tools.py / m3_client.py / prompts.py)
│ ├── db/ (SQLite catalog)
│ ├── image/ (color_grade.py / raw_processor.py)
│ ├── tests/ (pytest 4 模块)
│ └── requirements.txt + pyproject.toml
│
├── frontend/ # React 前端
│ ├── src/
│ │ ├── App.tsx + main.tsx
│ │ ├── components/ (7 个组件: Sidebar / ChatBox / WorkspacePanel / UploadButton / TaskProgress / ToolCallBlock / MiniMarkdown)
│ │ ├── views/ (GradeView / CullView)
│ │ ├── lib/api.ts (HTTP 封装,无前端日志总线)
│ │ └── styles/ (Apple 毛玻璃基础类)
│ └── vite.config.ts + tailwind.config.js
│
├── src-tauri/ # Tauri 桌面壳 (v0.2.3+ 启用,需要 MSVC Build Tools)
│ ├── src/ (5 个 Rust 文件)
│ ├── capabilities/ + icons/
│ └── tauri.conf.json + Cargo.toml
│
├── docs/ARCHITECTURE.md # 架构文档
├── examples/sample_jpegs/ # 8 张场景示例图
├── scripts/smoke_test.py
├── knowledge_base/ # M3 提示词预设(v0.2.3+ 充实)
└── samples/ # 演示数据
- 双击
启动.bat(或./start.sh) 起应用 - 浏览器开
http://localhost:1420 - 把照片文件夹丢进
workspace/input/<随便起名>/ - 前端选文件夹 → 点「一键修图」或「一键筛片」
- 等任务跑完,在 TaskProgress 面板看结果
- 对结果反馈(赞/踩),数据会进风格学习
- 聊天问摄影问题,SSE 流式响应
.env(从 .env.example 复制):
M3_BASE_URL=https://openrouter.ai/api/v1
M3_API_KEY=sk-or-v1-...你的 key...
M3_MODEL=minimax/minimax-m3
LOG_LEVEL=INFO不配 M3_API_KEY 会进 mock 模式(返回假数据,适合离线开发)。
python start.py # 完整流程(默认)
python start.py --check # 只检查环境
python start.py --install # 只装依赖
python start.py --desktop # 启用 Tauri 桌面壳(需 MSVC Build Tools)后端日志:v0.2.0 起直接显示在启动 cmd 里(sidecar_proc 的 stdout/stderr 继承给父进程),同时同步写到 workspace/.logs/sidecar.log(可 tail -f 查历史)。v0.1.x 时的前端「后端活动」tab 已被移除,见 v0.2.0 升级要点
前端日志:浏览器 DevTools console([api] POST /upload ✗ HTTP ... 这类)
症状:启动后 chat 第一条消息就报 错误: M3 API key 无效或被拒绝 (401): ...,修图每张照片 photo_failed,任务最终 0/N 成功。
排查步骤(从最快到最慢):
-
看 sidecar 启动日志 —
workspace/.logs/sidecar.log开头几行会有M3 key 验证失败或M3 key 验证通过,看错误信息:401 Unauthorized — key 无效或被撤销→ key 在 OpenRouter 端被删了,去 https://openrouter.ai/keys 重新生成401 No cookie auth credentials set/User not found→ key 复制错了,前后多了空格 / 多了换行网络错误: ...→ 电脑没网,或 OpenRouter 临时挂了,看 https://status.openrouter.ai
-
手动验证 key — 在终端跑:
curl -s https://openrouter.ai/api/v1/auth/key \ -H "Authorization: Bearer $(grep M3_API_KEY .env | cut -d= -f2)" | python3 -m json.tool
应该看到
{"data": {"limit_remaining": 4.99, ...}}。User not found/401就说明 key 废了。 -
检查 .env — 注意:
M3_API_KEY值不要带引号(写成M3_API_KEY="sk-..."也会被读成字面值,key 里多了双引号)M3_BASE_URL必须带/v1结尾(https://openrouter.ai/api/v1,不是https://openrouter.ai/api)M3_MODEL必须和 OpenRouter 上真实存在的 model id 一致(当前 v0.2.0 默认minimax/minimax-m3,去 https://openrouter.ai/models 搜 "minimax" 看最新列表)
-
余额 —
limit_remaining: 0时 OpenRouter 会返回 402,不是 401,错误信息会说 "insufficient credits",去 https://openrouter.ai/credits 充值。 -
还是不行 — 把
workspace/.logs/sidecar.log开头 20 行 +curl /auth/key的输出贴给开发者。
症状:点了「一键修图」,进度条一直转,30 秒后还显示 0%。
排查:这通常是 SSE / 拉取双重路径里有一个挂了,v0.1.1+ 加了 5 秒一次的兜底轮询,正常情况下 30 秒内会强制收尾。如果一直不动:
- 看启动 cmd 窗口里有没有
httpx: HTTP Request: POST ... "401"或timeout— 网络问题 - 看启动 cmd 窗口里有没有
Grading xxx.jpg— 在跑,等 - v0.1.x 时的前端「后端活动」tab 已被 v0.2.0 移除,改看启动 cmd 窗口(或
tail -f workspace/.logs/sidecar.log)
症状:点上传按钮后,前端提示错误或一直没反应。
排查(v0.2.0 起所有失败都会在 cmd 留痕,这是最快的诊断入口):
- 看启动 cmd 窗口 — 找
Upload start/Upload done/Upload rejected: .../Skip oversized file: .../Upload partial-fail: ...这类行,直接看到底是哪一步失败 - 常见原因:
- 选了一堆 .txt / .DS_Store / 视频等被 reject(
Upload partial-fail: ...readme.txt reason: unsupported type: .txt)。v0.2.0 只接受 arw/cr2/cr3/nef/dng/jpg/jpeg/png - 文件超大(单张 > 500 MB 或总 > 2 GB),会被 pre-check 拦下
- 文件名里有特殊字符 / Windows 反斜杠(被
_sanitize_rel_path当 invalid path 拒绝)
- 选了一堆 .txt / .DS_Store / 视频等被 reject(
- cmd 里没看到
Upload start行 — 说明 multipart 根本没到后端,要么浏览器 CORS 失败要么 sidecar 死了。看 cmd 有没有CORS相关报错,或tail -f workspace/.logs/sidecar.log看 sidecar 是不是挂了
v0.2.0 起,启动脚本会把 sidecar 的 stdout 全部继承,所以最简单就是看启动 cmd 窗口。但如果你在另一个终端想看历史:
# Windows (PowerShell)
Get-Content workspace\.logs\sidecar.log -Wait
# macOS / Linux
tail -f workspace/.logs/sidecar.log文件 log 包含所有子模块(backend.agent.m3_client / backend.image.color_grade / backend.db.catalog 等)的 logger 输出,带行号和模块名。
原因:上一次 start.py 没正常退出,或者手动跑了 python3 -m backend.agent.main。
处理:
- Windows:
netstat -ano | findstr :8765→ 找到 PID →taskkill /F /PID <pid> - macOS/Linux:
lsof -i :8765→ 找到 PID →kill -9 <pid>
或者直接 python3 stop.py,脚本会清掉所有 v0.2.x 启动时登记的 PID。