Show rate-limit usage; when it gets tight (or after the Fable window ends) send Fable subagents to Opus instead

<img src="docs/assets/banner.svg" alt="darkroom — Your Lightroom presets, on your own machine" width="100%">
<b>在自己電腦上,用你買的 Lightroom preset 修照片。</b><br> 挑 preset → 調強度 → 微調滑桿 → 即時預覽 → 匯出。不用訂閱,不上傳雲端。
退了 Lightroom 訂閱之後,手上買過的幾百個 XMP preset 就沒地方用了。darkroom 直接讀這些 .xmp,把裡面的 Lightroom 設定(曝光、對比、亮部/陰影、HSL、色彩分級、曲線、漸層遮罩……)用 GPU 在本機重算出來,拖滑桿就能即時看到結果。
讓人省心是這個工具唯一的目標:你只需要專注在擅長的照片編輯,其他的——檔案、路徑、格式、設定——交給 darkroom。 原則很簡單:preset 裡寫的設定一律由程式照算;AI 只做程式做不到的事(看懂畫面、找出位置、重畫內容)。照片原檔和買來的 preset 原檔,darkroom 永遠只讀不寫。
<img src="docs/screenshots/editor-v2.png" alt="darkroom 編輯畫面:左邊 preset 庫、中間預覽與強度、右邊滑桿" width="90%">
<img src="docs/assets/flow.svg" alt="Preset → Strength → Adjust → Preview → Export" width="90%">
| 功能 | 說明 |
|---|---|
| 套 preset | 解析 Lightroom PV2012 系列 XMP(ProcessVersion 6.7/10.0/11.0/15.4),強度 0~200%;Adobe 沒公開的部分(清晰度、紋理、去朦朧、亮部陰影)用公開演算法近似 |
| 即時預覽 | GPU 渲染,1.5MP 全套運算約 15~20 ms;拖滑桿時只算最新一次 |
| 微調滑桿 | 在 preset 之上逐項加減;復原/重做;按住看原圖 |
| 讀檔 | JPEG、PNG、TIFF(含 16-bit)、HEIC/HEIF(iPhone,10-bit、Display P3 轉 sRGB);依 EXIF 自動轉正 |
| A/B 對照 | 編輯前/後拖分隔線對照,隨時「還原成原圖」,也能取回上一份編輯 |
| 裁切與旋轉 | 拖框、鎖比例(原始、1:1、4:5、2:3、16:9、自由…)、拉直、轉 90°、鏡像;縮圖、預覽、匯出都套同一個範圍 |
| 匯出 | JPEG(品質或檔案大小上限)、PNG、TIFF(8/16-bit)、WebP;只縮不放的尺寸;中繼資料全留/只留版權/全移除,可單獨拿掉 GPS;輸出銳利化;匯出預設;嵌入 sRGB,永不覆蓋;24MP 約 0.4 秒/張 |
| preset 庫 | 群組樹、搜尋(含 AI 寫的中英文風格標籤)、最愛、改名、搬移、匯入 .xmp、把目前的修改存成自己的 preset、匯出成 Lightroom 讀得到的 .xmp——整理只改索引,原檔不動 |
| 照片庫 | 資料夾縮圖格、多選;每張照片的修改自動保存(以照片內容對應,搬移改名都不會掉);複製/貼上修改;匯出所選 |
| 設定 | 設定頁(語言 zh-TW/en-US、資料夾、AI、ComfyUI)、改了立即生效;設定可匯出/匯入 |
| 給 AI 代理用 | CLI(--json 固定格式)與 MCP server(38 個 darkroom_* 工具,例如取回上一份編輯 darkroom_edit_restore/CLI edit restore),和 App 共用同一套操作與錯誤訊息 |
偵測不到的功能(CUDA、HEIC、WebP、語意搜尋、ComfyUI…)會自動關掉並說明原因。還沒做的:RAW、AI 修圖建議、AI 遮罩、去雜物/美顏,見 開發文件的路線圖。
安裝檔(建議):到 GitHub Releases 下載 Windows 的 .msi/-setup.exe,或 Linux 的 .deb/.AppImage。第一次開啟時會說明要下載 Python 與 PyTorch(有 NVIDIA 顯示卡約 3 GB),你按同意才下載,之後就直接進編輯畫面。同一頁還有命令列執行檔 darkroom(給 AI 代理或腳本用)。
從原始碼:需要 Python 3.13、Node.js 22+、NVIDIA GPU(CUDA)。或者把 docs/agent-install.md 交給你的 Claude Code/Codex/Cursor,它會一步一步裝好、遇到要你決定的地方先問你。
兩種方式的完整步驟、GPU/CUDA 說明與疑難排解都在 安裝說明。
5 分鐘上手:第一次啟動 → 指定 preset 資料夾 → 修一張 → 匯出,以及 CLI 與 MCP 各一個例子。
其他文件:設定、架構、開發與發版、給 AI 代理的使用手冊、用詞定義。
darkroom 會被你自己和 AI 代理操作,所以這幾條是寫成測試守住的,不是口頭承諾:
safe_write):只建新檔、限定在指定資料夾、拒絕寫進 preset 資料夾、拒絕替換或刪除照片與 .xmp。Sec-Fetch-Site、Content-Type,讀照片路徑的請求要帶專用標頭,HTTP 也不收任意寫入路徑。op://…)或讀環境變數,設定檔裡放金鑰本身會被拒絕。MIT。darkroom 不附任何 preset;你買來的 preset 依它們原本的授權使用。
hooks/register.ts 44 lines1import type { Register, SessionRateLimit } from 'claude-code'
2
3// Temporary mod for the darkroom campaign (2026-10-09): the user asked for Fable subagents until
4// 2026-10-10 02:00 +08:00, then left model choice to the main session; this mod only steps Fable
5// subagents down to Opus 5.5 whenever usage gets tight.
6const TIGHT_PERCENT = 85 // any 5-hour or 7-day window at or past this is "tight"
7const WINDOWS = ['five_hour', 'seven_day']
8
9let limits: SessionRateLimit[] = []
10let warned = ''
11
12const label = (k: string) => (k === 'five_hour' ? '5h' : k === 'seven_day' ? '7d' : k)
13const tightOnes = () => limits.filter(l => WINDOWS.includes(l.kind) && l.percentUsed >= TIGHT_PERCENT)
14const summary = () => limits.filter(l => WINDOWS.includes(l.kind)).map(l => `${label(l.kind)} ${l.percentUsed}%`).join(' · ')
15
16const statusText = () => {
17 const s = summary()
18 return s ? `用量 ${s}${tightOnes().length ? '(緊繃:子代理改 Opus)' : ''}` : undefined
19}
20
21export const register: Register = on => {
22 on('session.start', async ($, e, next) => {
23 const r = await next(e)
24 try { limits = (await $.session.usage()).rateLimits } catch { /* no reading yet */ }
25 $.ui.status(statusText())
26 return r
27 })
28
29 on('session.measure', ($, e, next) => {
30 limits = e.rateLimits
31 $.ui.status(statusText())
32 return next(e)
33 })
34
35 on('tool.call', { tool: 'Agent' }, async ($, e, next) => {
36 if (e.model !== 'fable') return next(e)
37 const tight = tightOnes()
38 if (!tight.length) return next(e)
39 const why = `用量緊繃(${tight.map(l => `${label(l.kind)} ${l.percentUsed}%`).join('、')})`
40 if (warned !== why) { warned = why; $.ui.toast(`usage-guard:${why},子代理改用 Opus 5.5`) }
41 return next({ ...e, model: 'opus' })
42 }).catch(($, e, next) => next(e)) // never block a subagent because this mod failed
43}
44