Keep upstream tests and the fixed SAM worker, refresh the workspace version and parity summary, and route the model picker through the case-insensitive native filter helper. Carry the shared translated font-symbol correction; validation includes the pinned hosted fonts and all seven CI gates.
Generated with Codex.
The model list marked the YuNet detector as bundled with LightCraft and installed, but no model ships
inside LightCraft (the weights are opt-in downloads or files the user adds). Drop the notion: a
model is installed when its folder is, it can be removed like any other, and the licence notice no
longer says it is bundled.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An off-by-default "Recognise faces" toggle and the face model list in Settings,
with a one-drop install: pick a .onnx (Add a model file…) or drop one on the
window. The engine inspects it (recognised by hash, or described from its
shape), a dialog shows its licence, whether commercial use is allowed, what it
was trained on and what LightCraft had to assume, and Install stays disabled
until "I accept" is ticked. "Get…" opens a model's own page in the browser:
LightCraft never fetches a model itself. A file it cannot use says why.
The models folder is shared by the app, the CLI and the MCP server
(`engine::config`, `$LIGHTCRAFT_FACE_MODELS` overrides `<config>/models`), so a
model installed from one is there for the others. A session has none until a host
asks (`with_default_face_models`), which keeps tests hermetic.
Nothing runs a model yet, and the tab says so. Headless tests cover the whole
flow (the dialog, refusing until accepted, installing, a file it cannot use).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
`faces.models.list / inspect / install / remove / select` and `faces.enable`, all
reachable from the UI, CLI, control channel and MCP. Models live in a folder the
host provides (`Session::face_models_dir`): one subfolder each with the model, its
manifest and a record of what the user accepted, plus settings.json.
- install needs `acknowledged: true` (the user has been shown the licence), copies
the file under a .part name, checks the copy's SHA-256 against the original, then
renames; a failure leaves nothing behind.
- ids are validated before they become folder names (and a manifest that disagrees
with its folder is ignored); remove only ever deletes a folder that has a model
manifest. A manifest id can no longer be `.`, `..` or start with a dot.
- inspect describes any .onnx without writing anything; unusable files are an
answer ("unsupported" with the reason), not a crash.
- the list says plainly that running a model is not built in yet.
`faces::synthetic` makes a minimal valid ONNX for tests (and the later self-check).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
New L1 crate `lightcraft-faces`, the base of a one-drop model install:
- `ModelManifest` (role, licence with a commercial yes / no / unknown flag and a
plain-language notice, provenance, input preprocessing, output, thresholds)
with strict validation: sizes capped, numbers finite and sane, https-only
addresses, bounded text. A manifest from any source is untrusted.
- `known`: models recognised by SHA-256 (YuNet, SFace, AuraFace v1), with what is
and is not known about their licences and training data.
- `onnx`: a streaming, hostile-input-safe protobuf probe of a model's graph
inputs and outputs. It reads a 248 MB model in about 4 ms and never loads the
weights; truncation at every length and random corruption never panic.
- `suggest`: known hash -> its manifest; an unknown model shaped like a face
embedder -> a draft manifest with the assumptions spelled out; anything else
-> a plain reason it is not supported yet.
- `hash`: streamed SHA-256.
Checked against the real model files with an opt-in test (LC_FACE_MODELS). It
found an old exporter listing every weight as an input, now handled. Running a
model (tract) is not part of this crate yet.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
a_window_job_crosses_the_wire_and_renders_the_same_window compared the worker's
CPU render with the engine job's render bit for bit. On a machine with a GPU the
engine job renders there, so the test failed by 1 LSB (on main too); without a
GPU (CI) both run on the CPU. It now checks both sizes and a maximum channel
difference of 3, the bound docs/gpu-pipeline.md documents.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- With no window (a mask or point-colour overlay, Visualize Spots, soft
proofing, a window refused for a far read) the loupe renders the whole
frame at the size it is drawn. While a slider drags that meant the
photo's every pixel each draft frame at 1:1 and beyond (24 MP per tick
where 2.5 MP was rendered before #351; mask overlays are on by
default). A drag now drafts at most the canvas size, as the windowed
path does; at rest the zoomed frame is rendered as before.
- The Preview size hint in German, Spanish and Russian; the replaced
'Largest long edge…' text removed from those catalogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
The desktop app keeps logs/lightcraft.log in its settings folder (#286)
and a failed start names it (#370), but a user still had to find the
folder by hand. The host now hands the UI its log file (Services
.log_file) and Help ▸ Open Log Folder (app.openLogFolder) reveals it
through the existing reveal service: Finder and Explorer select the
file, Linux opens the folder. Off where no log is kept (the web,
--memory, LIGHTCRAFT_NO_PREFS). Translated in de, es, ja, pt-br, ru,
zh-hans and zh-hant; parity row MENU-HELP-LOGFOLDER; What's New and
README note it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With Shader Cache Size disabled in NVIDIA Control Panel, the driver walks
the whole system drive inside D3D12CreateDevice (traced by the reporter
to NvMemMapStoragex.dll!TotalDiskUsage), before the window's first
frame; Driver Default fixes it (confirmed in #374, PhotoCraft too). The
launch-argument walk found on the way was fixed in #394.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
The desktop showed rfd's synchronous dialogs on the UI thread, so the
window stopped answering the compositor while one was up: GNOME marked
LightCraft "not responding", the File menu stayed drawn and the control
channel went quiet. Services.picker lets the host start a dialog
elsewhere and answer through a channel; a command that needs a path
asks for one and returns, and runs again with the answer when the
dialog closes (pick.rs). Import Photos, Import from Folder, Open
Library, Import Lightroom Catalog, Find Missing Photos, the preset and
curve-preset imports and exports, Auto-Tag from Tracklog and Locate
Missing File use it; hosts without a picker (the web, tests) keep the
synchronous dialogs. ui.inspect lists the waiting commands as
fileDialogs. Same cause as storytold/photocraft#574.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A full-size load asks the decoder for the largest box (`SourceLevel::Full` is
`usize::MAX`, cast to `u32::MAX`). `jpeg::decode` then computed `2 * u32::MAX` to
decide on DCT scaling: an overflow panic in debug builds (caught, so every
full-size JPEG load failed with "decoder panicked"), and a wrap that happened to
give the right answer in release. The comparison saturates now, and the engine
converts its `usize` edge with `u32::try_from` instead of a truncating `as`.
Found decoding camera JPEGs (no JFIF/EXIF header) at full size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lightroom Classic reads `ToneCurvePV2012Red/Green/Blue` only together with
`ToneCurvePV2012`: a packet with a channel curve and no master curve leaves the
photo unchanged there (rendered on a chart: the channel curve alone has no
effect, with an identity master curve it applies). Imports now do the same.
The preset-packet test helper uses made-up as-shot values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
XMP presets usually say LensProfileEnable=0, which turned optics.lens_profile
off and with it the DNG-embedded WarpRectilinear / FixVignetteRadial
corrections (iPhone ProRAW and other files that carry them). Lightroom
applies a file's built-in corrections whatever that switch says (our
inference), and LightCraft's profile corrections are only those embedded
ones, so crs import now carries over only an enabling switch. The Enable
Lens Corrections checkbox and the amount sliders keep working as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
XMP presets carry fields that describe the preset or the photo rather than
an adjustment (Cluster, SupportsAmount2, RequiresRGBTables, SortName,
Description, CropConstrainToWarp, OverrideLookVignette, AsShotTemperature /
AsShotTint). preset.import listed them as settings that couldn't be carried
over; so did an HDREditMode of 0 and PointColors slots that are all -1.
They are now treated as bookkeeping; an HDR edit, a used Point Color slot and
unknown adjustments are still reported.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clippy 1.98's manual_chunks lint fails `cargo xtask ci` on main at psd.rs:170/191/201
(chunks_exact with a constant size). The fixed-size chunks also drop the per-sample
indexing. No behaviour change; the codecs tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CR2 and PEF decode without a camera colour matrix, like ARW, NEF, RW2,
RAF and CR3, but camera_preview::file_local_look (and the catalog's
relative white balance) left them out, so they rendered with the neutral
fallback matrix: the camera preview showed first, then a flat,
desaturated render replaced it. They now use the same guarded fit and
relative WB; a fit that fails the gates keeps the fallback, so a file can
only stay as it was or move closer to its camera JPEG. Not measured on
the corpus here (raw.pixls.us is blocked from this environment).
RENDER_CACHE_VERSION 15 re-renders cached CR2/PEF thumbnails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
egui keeps a popup inside the window, so a menu taller than the room
under the bar (Photo, on a short window) was slid up over the bar: the
titles vanished under it, and the pointer left on the title rested on
the first row and opened its submenu by itself. Every level now gets
the room it has below the bar (a submenu: from its row down, or up to
the bar when egui opens it upward) and scrolls inside it, with wheel,
drag and ▲/▼ strips that scroll while the pointer rests on them, as in
PhotoCraft's menu bar (storytold/photocraft#337). Menu levels and
submenu rows are registered for the control channel and tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A review measured 1 GB of GPU stages for a Before/After at 1:1 on a 2800 px
canvas against a 400 MB budget, with nothing bounding it. The open photo's window
and before caches are now cut (the before side first) past four times the budget,
the whole-frame view's never; idle caches still go at the budget. The one shared
device copy of a big original is reported (sharedSourceBytes in the renderer's
memory report) and a misleading comment about it is corrected.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
At first open of Before/After the two window jobs were asked for together, each
decoded its own 24 MP copy of the original (a review saw two decodes and three
uploads for one photo) and the shared device copy was swapped between them. The
Before's window now waits until the After's job has handed the original to the
session (region::defer_before_window, Session media has_source); later windows
of both share the one copy.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>