Repository navigation
AI Edit: an AI retoucher that looks at Lightroom's render and corrects it - #481
Merged
Merged
Conversation
Research into writing crs: XMP for AI Edit (docs/wiki/Dev-AI-Edit-XMP-Findings.md) settled most questions from real files: the crs: key space is the getDevelopSettings() table, semantic AI masks carry no geometry, and crop and geometric masks live in the uncropped sensor-oriented frame. Four questions decide the architecture and only Lightroom can answer them, so this adds a developer task (Help > Developer: Run Develop-Settings Experiments...) that runs them in a test catalog: - E13 (read-only): runtime orientation of dimensions/croppedDimensions and the develop setting `orientation`. It fits the crop model and says "ambiguous" when sensor and display frames tie. - E1: does applyDevelopSettings accept `Temp` (what AI Edit writes today) or only Temperature / IncrementalTemperature? Each variant runs on a fresh virtual copy. - E2: can applyDevelopSettings add a digest-less AI mask, and is it computed on its own or only after updateAISettings()? - E4: plugin preset lifecycle. Covers same-name adds, file location and content, merge vs replace against a seeded gradient, re-apply idempotency, and amount with and without SupportsAmount. All writes go to "LrG Exp ..." virtual copies. The report is Markdown plus JSON, with verdicts, manual checks and raw readbacks. Failures are recorded and shown, never counted as results. The pure helpers are covered by busted. Docs-Reviewed: Credits.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds what the step-1 plan needs from the experiments, before the task is
run in a test catalog:
- E1: white-balance mode only ({WhiteBalance="Daylight"/"Auto"}, and
"Auto" on non-raw files). Each copy starts from a distinctive Custom
white balance, and a short poll catches a late recompute. A flattened
Auto reads as "flattened to Custom".
- E11 (new): Look transfer via applyDevelopSettings on raw files, in
three forms: the preset form of a stub (Stubbed=true), a bare stub, and
a full Look. Installed presets never carry Look.Parameters, so the full
Look comes from another selected photo, from the E11a readback, or from
the photo's own Look after a prepare step; presets are the last resort.
Camera-restricted Looks are only used on the same camera model.
- E13: full top-level readback of getDevelopSettings(), for the raw and
non-raw defaults. It records whether the photo has develop edits
(hasAdjustments search, editCount, lastEditTime).
- E2: timing of the updateAISettings() call and of the time until the
mask is ready, measured from the call itself. A lock before the update
marks the time as a lower bound.
Pure helpers are in DevelopExperiments.lua with busted specs (388 pass).
All branches were exercised through a mock SDK harness (15 scenarios).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…xtures
PR 0 of the step-1 plan for the native Lightroom develop model
(lrg-develop).
- server-rs/scripts/develop_registry/: a stdlib-only uv project with the
tools the research used, cleaned up as CLIs:
- parse_xmp.py: crs: RDF parser;
- scan_corpus.py, sample.py, structform.py: inventory and sampling;
- gen.py + spec.py: registry generator. It seeds table.rs in PR 1a and
reads no ExifTool data;
- dump_training.py: optional 'lance' group;
- extract_fixtures.py: the fixture extractor.
No private paths are hard-coded, and every tool refuses to write
output inside the repo; only the extractor may write, and only to the
fixture folder.
- server-rs/testdata/develop/lua/: 17 getDevelopSettings() fixtures plus a
manifest. They cover every shape the 1a Lua reader needs: all mask
kinds, intersect/subtract, Look variants, LensBlur/FilterList in both
forms, PV 11.0 and 15.4, and a top-level []. They are derived from the
maintainer's own edits and scrubbed for a public repo:
- only develop settings, no metadata;
- synthetic ids, digests and GUIDs;
- generic mask names;
- synthetic lens and camera strings;
- Look.Parameters replaced by a registry-key-only stub, and non-Adobe
profiles stubbed;
- no lookup-table blobs;
- GrainSeed neutralised.
- The extractor's hygiene check runs as a pre-commit hook and in the
lint-format CI job, so fixtures with private data or Adobe content fail
the build. Staging plus the check means a failed run never touches the
committed fixtures.
Docs-Reviewed: Dev-Build-Environment-Setup.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… reader
PR 1a of the step-1 plan: the native Lightroom develop model as a leaf
crate. It has no image, network or async dependencies, so
`cargo test -p lrg-develop` takes seconds.
- Registry: 562 crs: keys plus 6 pattern families, one KeySpec each:
- type, range in stored units, UI scale, number format;
- raw and non-raw defaults (non-raw Unverified until the E1 readback);
- policy class (LEARN, LEARN†, PHOTO, COMPUTED, NEVER, META, UNKNOWN);
- frame scope, file kind, presence and min_pv.
table.rs is generated from scripts/develop_registry/spec.py by
gen_table_rs.py, byte-reproducible. A JSON snapshot turns registry edits
into readable diffs (LRG_BLESS=1). ui.rs and coerce.rs are the only
places for UI scaling and int/real conversion. `Temp` is deliberately
not a key.
- Model:
- DevelopSettings keeps a private invariant, with values keyed by
registry order.
- Finite rejects NaN and infinities.
- Corrections and mask components carry typed tools: semantic AI masks
incl. people parts and landscape classes, linear, radial with Angle 0,
luminance range, or opaque.
- Look is taken whole.
- White-balance types derive the family from the keys.
- The policy filter checks nested structs at their own registry rows,
drops global META values for presets and photo targets, and lets only
portable camera profiles into presets.
- Lua reader: reads getDevelopSettings() blobs as JSON.lua encodes them:
- an empty table anywhere means the key is absent;
- int/real and bool/0-1 are normalised;
- unknown keys pass through losslessly with warnings;
- the file kind comes from the white-balance keys;
- PV < 6.7 is flagged;
- output order is deterministic whether or not serde_json's
preserve_order is on.
- Tests (104 unit + integration):
- all generated and 7 new hand_*.json fixtures read with 0 unknown keys;
- a Rust fixture-hygiene test mirrors extract_fixtures.py, and both
share 12 crafted self-test cases;
- a local-only sweep over the maintainer's 1,294 training rows passes
with 0 errors and 0 warnings.
- Shared golden gate: LOCAL_ONLY families (`training-dump`). `all` now
leaves out data that can never be in CI; naming such a family still
requires it.
- .gitattributes keeps server-rs/testdata/develop LF for the byte-exact
snapshot on Windows runners.
- Docs: new Dev-Develop-Model.md; crate lists in CLAUDE.md, AGENTS.md and
server-rs/README.md; the regenerated Dev-Server-README.md.
No behaviour change in any other crate: nothing consumes lrg-develop yet
(PR 1d is the first consumer).
Docs-Reviewed: Credits.md
Docs-Reviewed: Dev-Project-README.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR 1d of the step-1 plan, and the first consumer of lrg-develop.
AI Edit read and wrote the develop key "Temp", which Lightroom never uses.
It appears in 0 of 17,814 catalog rows. White balance was therefore never
learned from training examples, and the averaged Tint was applied as-is to
every file, which is wrong for non-raw files.
Backend:
- Training reads getDevelopSettings() blobs through lrg_develop::lua.
The white balance of each example becomes a typed WbSetting (mode,
family, numbers only for Custom).
- "[]" is accepted.
- is_raw follows the white-balance key family, and a contradiction is
logged.
- A canonical_version is stored. Older rows are re-read from their blob,
for the at most 20 candidates of a request, with no write-back.
- style_engine::blend_white_balance votes over the whole candidate pool,
weighted by score:
- As Shot: the photo keeps its own white balance.
- Auto or a named mode: a note, nothing applied (EMIT_NAMED_WB_MODES
stays off until experiment E1).
- Custom: Kelvin/Tint from up to 3 Custom examples of the photo's own
family, only if there are at least 2 and they are within 500 K.
- Raw examples never give a JPEG target Kelvin values; the user gets a
note instead.
- The generic blend renormalises per key and rounds with registry
precision. temperature/tint left it.
- The style response carries edit.white_balance at recipe level. There is
no global.temperature/tint any more, and the LLM alias
global.white_balance is untouched.
- Warnings are a Vec. Responses carry a top-level `warnings` array plus
the legacy joined `warning` for installed plugins. Warning texts are
constant per cause, so they merge across photos. Examples whose blob
cannot be learned are reported, not dropped silently.
- The LLM path: TEMPERATURE_KEYS became the whole white-balance family,
the prompt key names are fixed, and the local exposure range is +-4 with
a drift test against the registry.
Plugin:
- DevelopEditManager.whiteBalanceFamily() decides by the photo's own
settings, so a DNG converted from a JPEG counts as non-raw.
- edit.white_balance maps to Temperature/Tint or IncrementalTemperature/
IncrementalTint, each with WhiteBalance="Custom". The old
global.temperature/tint form maps the same way.
- The style request sends is_raw from that family.
- AI Edit and Save-as-Training collect `warnings` per run: deduplicated,
counted, and the most frequent shown first. The review dialog lists why
white balance was not transferred.
- No "Temp" literal is left. tests/no_temp_key.rs guards this, and it
runs in the unfiltered format-lint-rust CI job. The develop experiments
keep writing Temp on purpose.
Docs: Dev-Backend-API, Help-AI-Edit (incl. the release note text),
Help-Train-From-Edits, Dev-Develop-Model and Dev-AI-Edit-XMP-Findings
are updated.
Docs-Reviewed: Credits.md
Docs-Reviewed: Dev-Server-README.md
Docs-Reviewed: Help-Analyze-and-Index.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lies
PR 1f of the step-1 plan. The style engine blended 20 keys, but the
installed plugin already knows how to apply many more. 1f adds 45 recipe
aliases in the lrg-develop registry (65 in total). This needs no plugin
update and no change to the wire format.
- Detail: sharpen radius/detail/masking, luminance NR detail/contrast,
colour NR detail/smoothness.
- Effects: post-crop vignette midpoint/roundness/feather/highlights, grain
size/roughness.
- Colour mixer: hsl.<colour>.{hue,saturation,luminance} for all 8 colours.
- Colour grading: shadows/highlights hue and saturation, plus balance.
Midtones, global, luminance and blending are left out, because the
installed plugin warns about them.
- Parametric curve: the three region splits, blended as one group so their
order holds. If rounding collides, a constant note is sent instead.
Toning hues use a circular mean (350 and 10 give 0). Saturation is the
length of the score-weighted mean colour vector, so tints that disagree
fade towards neutral instead of blending into a colour no example used.
The run summary says when a zone was toned down. Black-and-white examples
contribute no toning or colour-mixer values. Blend warnings reach the
response's `warnings`.
CANONICAL_VERSION is 3, so stored examples are re-read from their blob.
Guardrails are unchanged: the frame budget covers only tone and curve
strength, which the new keys do not affect (reasoning in Dev-Backend-API).
These values now replace the photo's own mixer, grading, splits and
detail/effects settings. Earlier versions left them alone; Help-AI-Edit
says so and points to the virtual-copy option.
Tests: 9 blend unit tests; the real Lua fixtures blended end to end; a
contract test for every new field; a busted spec run against this
branch's plugin and against origin/main's; and a local-only check over the
1,294 training rows (every key present, 0 blend notes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… file Answers whether AI Edit can use a throwaway plugin preset per photo: write it, apply it, delete it. The SDK has no call to import an .xmp preset or to delete a preset. The programmatic equivalent is addDevelopPresetForPlugin + applyDevelopPreset(updateAI) + deleting the file that preset:getFile() returns. E4f creates "LrGenius Experiment E4f" (a contrast value plus a subject mask), applies it to a fresh copy and reads it back. It then deletes the preset's file, under a pure path-safety check: absolute path, no relative components, .xmp/.lrtemplate only, file name matching the preset, and a file inside the Plugin Develop Presets folder or next to the other E4 preset files. After the delete it records: - whether the copy keeps its settings and its computed mask; - whether Lightroom still lists the preset, and whether its settings can still be read; - what applying the deleted preset object to a second copy does; - whether re-adding the same name recreates the file, and with which content. Before creating its preset, E4f records the presets of that name already listed, which answers the after-restart question on a second run. A manual check names the plugin-preset folder to inspect after a restart. Verdicts are VIABLE / NOT viable (qualified: within this session; for the settings only while the mask is pending) or inconclusive when the preset never took effect or the delete was refused. The outcome always appears in the final dialog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR 1c of the step-1 plan. lrg_develop::xmp::parse reads Camera Raw /
Lightroom XMP (sidecars, develop presets, profiles) into the same
DevelopSettings model the Lua reader produces. Classification, typing,
the white-balance family and process-version handling are shared through
a crate-private reader module.
- Forms: attribute and element form, property elements with attributes,
nested rdf:Description, parseType="Resource", rdf:Seq, rdf:Alt.
x-default goes into the model; rdf:Bag, other languages and qualifiers
are kept whole and counted as kept_whole.
- crss: snapshots are skipped and counted.
- crs:Preset/Parameters and Look/Parameters stay opaque.
- The namespace is matched by URI.
- Top-level names resolve at global level first, then header
(Baseline).
- Kinds: Sidecar, Preset (with a typed PresetHeader), Profile,
NotDevelop.
- Errors: hard only for broken XML, size, encoding, a missing rdf:RDF and
limits; everything else is a ParseWarning with a path. Duplicates keep
their first occurrence.
- Untrusted input:
- a 4 MiB cap;
- DTDs refused;
- a non-recursive depth pre-scan before roxmltree runs (a deeply nested
file would otherwise abort the process);
- node, attribute, namespace and URI-length limits;
- namespace URIs shared via Arc, so a small file cannot amplify into
gigabytes.
- Fixtures: 22 self-authored XMP files under
server-rs/testdata/develop/xmp/ covering every form. An equivalence
test shows the XMP and Lua readers build equal models for equal
content. The hygiene checks (Python and Rust, kept in step) now also
parse the XMP fixtures.
- Local-only sweeps (gate families lrc-presets, xmp-corpus, acr-presets
in LOCAL_ONLY):
- the LrC 15 bundle: 1,045 files with 0 errors and 0 unknown keys (only
Adobe's own duplicate crs:Group in 6 profiles);
- the maintainer's sidecar corpus: 10,814 sidecars with 0 errors and 0
warnings;
- Camera Raw's user folder: clean.
The sweeps print counts only.
- roxmltree (MIT/Apache-2.0) added; Credits.md, Dev-Develop-Model.md,
CLAUDE.md/AGENTS.md goldens paragraph and server-rs/README.md updated.
Docs-Reviewed: Dev-Build-Environment-Setup.md
Docs-Reviewed: Dev-Project-README.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # AGENTS.md # CLAUDE.md
# Conflicts: # server-rs/crates/lrg-api/tests/contract.rs # server-rs/crates/lrg-providers/Cargo.toml
Merging main brought provider-level edit warnings (Gemini's thinking fallback, an OpenAI-compatible server ignoring the answer format, Claude's fallback). generate_edit_recipe_for_photo appends the style-training note to that same slot on a new line, and the `warnings` list was built from the slot as a single entry, so both notes reached the plugin as one text that its tally could not group with the same note on other photos. Split the slot into one entry per line where /v1/edit/recipe and the LLM fallback of /v1/edit/style build the list. Docs-Reviewed: Dev-Backend-API.md Docs-Reviewed: Help-AI-Edit.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fnt8Bs3rCjBvABRokgmoM
# Conflicts: # AGENTS.md # CLAUDE.md
lrg-develop can now write a DevelopSettings model back out as a crs: develop preset, and build the masks AI Edit will need without going through Lightroom's UI. - xmp::write (WriteMode::Preset with a PresetSpec header, file-kind and process-version gating) plus xmp::format for value spelling. The writer reports every value it drops in Written::skipped; nothing is lost silently. Opaque (kept-whole) values never reach a preset, the Look keeps only Name/UUID/Amount/Stubbed, Temperature/Tint next to a non-Custom white balance mode are dropped with their own reason. - CompatibleVersion is derived from the features used (TARGET_ENGINE 18.5); rows not yet confirmed in Lightroom are marked unverified. - build: semantic (subject, sky, background, landscape, people parts), linear/radial gradients and luminance ranges, combined with add, subtract, intersect and add-inverted the way Lightroom writes them (range Invert mirrors MaskInverted; duplicate roles are refused). - A test-roundtrip feature (self dev-dependency, never in the server build) exercises write -> parse -> compare. Local sweeps (LOCAL_ONLY, private inputs never committed): training dump, the Lightroom preset bundle, the Camera Raw presets and the XMP corpus all round-trip with 0 differences against the filtered settings; builder output matches Adobe's adaptive presets exactly. Three hand-test presets (Subject+Sky, Landscape+People, Combinations+Background) are committed as byte goldens; their import into Lightroom is still open. Docs-Reviewed: Dev-Develop-Model.md Docs-Reviewed: Dev-AI-Edit-XMP-Findings.md Docs-Reviewed: Credits.md Docs-Reviewed: Dev-Server-README.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de/develop-xmpw-1b # Conflicts: # AGENTS.md # CLAUDE.md # server-rs/Cargo.lock
lrg-develop can now write a DevelopSettings model as the Lua table
photo:applyDevelopSettings takes, sent as JSON and decoded by the
plugin's JSON.lua. Nothing calls it yet; that is step 2.
- lua::to_lua_value(settings, LuaMode::Apply(PhotoContext), options)
returns the table, every dropped value as Skipped, and the call flags
(updateAISettings, flattenAutoNow). check_wire guards the JSON.lua
hazards: no null, no empty containers (MaskGroupBasedCorrections = {}
would delete every mask), no mixed or sparse tables. Temp and
ProcessVersion are never written, white balance always as a family,
computed mask fields and digests never.
- LuaOptions::PROVISIONAL holds every choice the experiments decide
(mask enum encoding, flag encoding, panel switches, mask and local
form, WB forms, AI update, Look form, preset amount flags), each with
the experiment that settles it and the evidence for its default.
- 18 wire goldens in testdata/develop/wire/ plus key_classes.txt, both
generated from the registry (LRG_BLESS=1); not frozen until E1/E2/E4.
plugin/spec/native_wire_format_spec.lua decodes them with JSON.lua and
type-checks every value; lua-tests.yml now runs on wire/ changes.
- Round trip 2 (model -> Lua -> model) is lossless and round trip 3
(Lua -> model -> XMP preset -> model -> Lua) agrees after quantisation
over the fixtures, the goldens under every option flip, and locally
over the training dump, the Lightroom bundle, Camera Raw presets and
the sidecar corpus.
- Registry: new unknown-policy row Glow (seen in the corpus).
Docs-Reviewed: Dev-Develop-Model.md
Docs-Reviewed: Dev-AI-Edit-XMP-Findings.md
Docs-Reviewed: Dev-Server-README.md
Docs-Reviewed: Dev-Backend-API.md
Docs-Reviewed: Help-AI-Edit.md
Docs-Reviewed: Help-Train-From-Edits.md
Docs-Reviewed: Credits.md
Docs-Reviewed: README.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…esets-1a845a # Conflicts: # docs/doc-sources.toml
…a' into claude/develop-registry-pr0
…o claude/develop-model-1a # Conflicts: # AGENTS.md # CLAUDE.md
…aude/develop-wb-1d
…e/develop-style-1f
…aude/develop-xmp-1c # Conflicts: # AGENTS.md # CLAUDE.md
…de/develop-xmpw-1b # Conflicts: # AGENTS.md # CLAUDE.md
…registry One run on Lightroom Classic 15.6 (2026-10-03), from the Library module, on one non-raw TIFF virtual copy without develop edits; no raw file, and the one people-part mask found nothing. - LuaOptions::EVIDENCE: one typed row per option (settled / supported / open, experiments, scope, what is still open); a test requires a row per field and pins the first run's statuses. Settled in that scope: mask_enum_as, mask_form and local_form for subject/sky/background, ai_update (E2b vs E2c), wb_mode_only and flatten_auto_now on non-raw (E1f/E1g), preset_amount_flags (E4e: the amount has no effect). Supported only: wb_custom_with_numbers (E1b/E1d) and panel_switches. Open: int_flag_as, look_form, people parts, every raw-side question. - panel_switches defaults to MaskOnly, the form E2 applied (None was never applied); ten wire goldens gain the mask switch. - A mode alone is never written for a non-raw photo, in the writer and in the style engine: the Auto/named-mode note for JPEG/TIFF targets now says the mode does not apply to non-raw files. - Registry: non-raw defaults from E13's readback, committed scrubbed as testdata/develop/defaults/non_raw_lrc15.6.json (key -> value plus Lightroom version, process version, file kind) and read by gen_table_rs.py; 145 rows filled, HDRMaxValue held back (one photo), NoDefault rows kept, nothing marked absent. spec.py now mirrors table.rs, so the generator reproduces it byte for byte. Effect: removed values equal to a non-raw default are no longer reported for non-raw sources (tested). - Findings: dated results section; step 2 applies AI masks with applyDevelopSettings + updateAISettings(), not plugin presets (they replace the photo's masks, their updateAI flag left the mask pending in the Library module, the amount is ignored). Docs-Reviewed: Dev-Develop-Model.md Docs-Reviewed: Dev-AI-Edit-XMP-Findings.md Docs-Reviewed: Dev-Backend-API.md Docs-Reviewed: Help-AI-Edit.md Docs-Reviewed: Help-Train-From-Edits.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ude/develop-luaw-1g # Conflicts: # AGENTS.md # CLAUDE.md
catalog:createVirtualCopies() files a copy of a virtual copy under that copy's master, as Photo > Create Virtual Copy does. AI Edit compared the new copy's master with the source photo itself, so with "Apply the edit to a new virtual copy" every source that was itself a virtual copy failed with "Lightroom copied a different photo", after the copy had already been created. - Util.isVirtualCopyOf: pure check. For a virtual-copy source the new copy's master must equal the source's master; for a master source it must equal the source. A copy of a different master is never accepted. - Check again, inside the write gate and right before copying, that the source alone is selected: waiting for write access can take up to the gate's timeout, and a changed selection would copy another photo. - Count every "AI Edit" copy a failed attempt leaves behind (failed check, more than one copy returned, recipe not saved, recipe not applied) and say once in the run summary how many to remove with Photo > Remove Photo; the SDK cannot delete photos. - Log catalog-local ids and isVirtualCopy when the check fails. - Help-AI-Edit: virtual-copy sources work; stray copies after a failure. Docs-Reviewed: Dev-Plugin-README.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # docs/wiki/Dev-Server-README.md # server-rs/README.md
# Conflicts: # docs/wiki/Dev-Backend-API.md # docs/wiki/Dev-Server-README.md # plugin/LrGeniusAI.lrdevplugin/APISearchIndex.lua # plugin/LrGeniusAI.lrdevplugin/DevelopEditManager.lua # plugin/LrGeniusAI.lrdevplugin/TaskAiEditPhotos.lua # plugin/LrGeniusAI.lrdevplugin/TaskTrainFromEdits.lua # plugin/LrGeniusAI.lrdevplugin/Util.lua # server-rs/Cargo.toml # server-rs/README.md # server-rs/crates/lrg-api/Cargo.toml # server-rs/crates/lrg-api/src/routes/edit.rs # server-rs/crates/lrg-api/src/routes/style_edit.rs # server-rs/crates/lrg-api/tests/contract.rs
# Conflicts: # docs/wiki/Dev-Server-README.md # server-rs/README.md
# Conflicts: # docs/wiki/Dev-Server-README.md # server-rs/README.md
# Conflicts: # docs/wiki/Dev-Server-README.md # server-rs/README.md
…i-edit-xmp # Conflicts: # plugin/LrGeniusAI.lrdevplugin/TaskAiEditPhotos.lua # plugin/LrGeniusAI.lrdevplugin/Util.lua
Raw runs on LrC 15.6 (E1, E2, E4, E11, E13): E11b shows a complete Look with Parameters is applied while a stub sets none, so look_form defaults to Full; a Look without Parameters is reported as Look.Parameters. The evidence rows, the wire goldens with a Look and the wiki carry the results. Docs-Reviewed: Dev-AI-Edit-XMP-Findings.md Docs-Reviewed: Dev-Develop-Model.md Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013a7JYahnNttgNMVgEDLLDJ
…M loop Brings lrg-develop (key registry, typed model, Lua/XMP readers and writers, mask builders), the style engine's typed white balance and the develop-settings experiments onto current main, as the base for applying LLM edits directly through applyDevelopSettings + updateAISettings. Conflicts: Info.lua keeps main's Help-menu entries and adds the experiments; Util.lua keeps both sides' helpers; style_edit.rs takes the branch's warning list with main's guardrail result (the face readers and its own warning), and drops the temperature sanitising the branch's typed white balance made unnecessary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HaMMvswyTLENR7Gdygjt41
…s it
The single-shot LLM edit guessed absolute slider values for a file it had
never seen rendered, and the style engine never looks at the photo at all.
The retoucher works the way a person at the Develop panel does: a vision
model edits, Lightroom applies and renders the edit, the model sees the
render next to the original with measurements of its tones and corrects,
until it finishes or runs out of rounds (default 3, at most 5).
Backend
- lrg-api edit_loop + routes/edit_session: POST /v1/edit/sessions, POST
/v1/edit/sessions/{id}/render, DELETE /v1/edit/sessions/{id}. The
conversation runs on lrg_providers::chat::ChatModel, so every provider the
agent chat runs on works, local engines included. Each edit passes the
existing guardrails, becomes DevelopSettings through
lrg_develop::build::recipe and an applyDevelopSettings table through the
Lua writer — its first production caller.
- lrg-providers edit_loop: the system prompt, looks, the apply_edit/finish
tools with a schema built from the develop registry (Lightroom's own
ranges, the photo's white-balance family), opening and render texts.
- lrg-develop build::recipe: recipe -> settings (aliases, colour-grading
midtones, black-and-white mix, white balance per family, AI masks for
subject, sky, background, ten people parts and seven landscape classes
with sync ids derived from the target) and a photo's starting values.
- lrg-imaging tone: luminance percentiles, histogram bands, clipping, the
colour of the neutral tones, midtone saturation — what the model is told
to trust over its eye.
- lrg-analysis style_engine::white_balance_into_global, so the
photographer's style can be round 0.
Plugin
- EditLoop.lua: applies each round from the photo's starting point (keys an
earlier round set go back; masks merge by CorrectionSyncID, the photo's
own kept raw), computes AI masks with updateAISettings (LrC 15.3+),
renders at 1536 px, reports masks that found nothing, reverts on discard.
- TaskAiEditPhotos: two editors, AI retoucher (default) and My style only
(the style engine as before, now runStyleEdits). Model, look, own
instructions, edits per photo, start from my style, AI masks; review is a
Keep / Discard / Keep all dialog on the finished edit.
- persistEditRecipe returns whether it recorded, and both editors report a
failure as a warning.
Live-tested against the dev binary over HTTP with a scripted
OpenAI-compatible server (start, two renders, back to round 2, persist);
not yet in Lightroom or with a real model.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HaMMvswyTLENR7Gdygjt41
…ay what they show Found in the live test in Lightroom (LrC 16.0, test catalog): - Started from the photographer's style, the model never saw the style's values, and since every edit is complete from the starting point, whatever it did not repeat — the colour mix, the grading, the detail settings — fell back to the photo's own. Edits are now merged onto the style (`merge_onto`): the model sends what it changes, the style keeps the rest, and round 0's text lists the style's values. - A small local model (Gemma 4 E4B on MLX) dropped the values it meant to keep in later rounds. Every render now comes with the values of the edit it shows (`recipe_text`), so the model has the numbers in front of it. The live run's evidence goes into Dev-AI-Edit-XMP-Findings: people-part (face skin, iris) and landscape (vegetation) masks computed through applyDevelopSettings + updateAISettings, own masks passed back raw kept, same-sync-id corrections replaced in place, and an empty MaskGroupBasedCorrections removing the loop's masks on discard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HaMMvswyTLENR7Gdygjt41
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
AI Edit gets an AI retoucher: a vision model edits, Lightroom applies and renders the edit, the model looks at the render next to the original with measurements of its tones, and corrects — round by round, the way a person works at the Develop panel. The style engine stays as My style only (offline, no model), and can be the retoucher's starting point.
Why
Both existing paths stop short of good edits. The single-shot LLM edit (
/v1/edit/recipe, unreachable from the plugin since #274) guessed absolute slider values for a file and profile it had never seen rendered — what vision models are worst at. The style engine never looks at the photo at all; only the guardrails do. Vision models are good at the step in between: judging a picture and saying what is wrong with it. The loop gives them that step.What the branch contains
1. Merge of
feature/ai-edit-xmp(a3726ea):lrg-develop(key registry, typed develop model, Lua/XMP readers and writers, mask builders), the style engine's typed white balance and the develop-settings experiments, onto currentmain. Conflicts resolved inInfo.lua,Util.lua,style_edit.rs(the branch's warning list with main's guardrail result),Cargo.toml/lock, the golden-tests workflow. The retoucher is the first production caller oflrg-develop's Lua writer, applying throughapplyDevelopSettings+updateAISettingsas experiment E2 settled.2. The retoucher (
1f46a97, and930b7f2from the live test):lrg-apiedit_loop.rs,routes/edit_session.rsPOST /v1/edit/sessions,POST /v1/edit/sessions/{id}/render,DELETE /v1/edit/sessions/{id}. The backend holds the conversation (in memory, 30 min idle expiry); the plugin drives it. Runs onlrg_providers::chat::ChatModel, so every provider the agent chat runs on works, local engines included. Every edit passes the existing guardrailslrg-providersedit_loop.rsapply_edit/finishtools with a schema generated from the develop registry (Lightroom's own ranges, the photo's white-balance family), the opening and render textslrg-developbuild/recipe.rsDevelopSettings: recipe aliases, colour-grading midtones/luminances/blending, black-and-white mix, white balance per family (a value on the other scale is dropped, never clamped), AI masks for subject, sky, background, ten people parts and seven landscape classes with sync ids derived from the target. Andstarting_values, the way backlrg-imagingtone.rslrg-analysisstyle_enginewhite_balance_into_global, so the photographer's style can be round 0EditLoop.luaCorrectionSyncID, the photo's own masks passed back raw), computes AI masks, renders at 1536 px, reports a mask that found nothing, reverts on discardTaskAiEditPhotos.luarunStyleEditsunchangedEvery edit is the complete edit measured from the starting point, so rounds are idempotent and the model can go back to an earlier, better round (
best_round).Errors surface (reviewed against Errors must surface): what did not reach the photo (clamped values, unknown mask targets, masks beyond six, keys the process version lacks, the style's white balance from the other file kind), a kept round's mask that found nothing, a failed record of the edit in the catalog or the index, edits the model sent that Lightroom could not use (named as the cause instead of "no tool calling") — all reach the review dialog and the run summary.
persistEditRecipenow returns whether it recorded; both editors report a failure.Test plan
cargo fmt,cargo clippy --workspace --all-targets(zero warnings),cargo test --workspace(1534 passed). New: 6 end-to-end loop tests on a scripted model (edit → render → refine → finish withbest_round; style as round 0 with both images; nudge then refuse a model without tool calls; edit budget; JPEG offset white balance; unusable edits named as the cause), recipe builder (incl. writing an apply table with a mask), tone measurement, tool schemabusted(10 new specs for composing rounds, merging masks by sync id, revert, mask states; the one failure,aesthetic_refine_spec.lua, predates this branch — Lua 5.5 prints1.0)luacheck,stylua,scripts/check-docs.py, pre-commit hooksopenai_compatibleadapter: start, two renders with Lightroom notes, a guardrail cap, back to round 2, the recipe stored on the photo's record, 404 after the end,DELETEEditLoop.runviaeval_lua), seven CR3 photos:930b7f2(edits now merge onto the style instead of dropping its values)Dev-AI-Edit-XMP-Findings.mdcatalog_photo), virtual copiesKnown behaviour
/v1/edit/stylestores the recipe it generates.🤖 Generated with Claude Code
https://claude.ai/code/session_01HaMMvswyTLENR7Gdygjt41