Commit Graph

1024 Commits

Author SHA1 Message Date
Brandon Thomas 7365d06c13 Merge PR #359 2026-10-09 00:17:59 -04:00
Brandon Thomas ab73d3d2e0 Merge PR #423 2026-10-09 00:17:58 -04:00
Brandon Thomas c4c96ecabb
Merge pull request #439 from storytold/claude/integrate-batch-14
Integrate #429, #426, #422, #395, #172, #173, #174, #335, #427
2026-10-09 00:16:54 -04:00
Brandon Thomas cae7bea08e Merge remote-tracking branch 'origin/main' into fix-359
# Conflicts:
#	crates/engine/src/segment/worker.rs
2026-10-08 23:57:36 -04:00
Brandon Thomas 24615cc2d7 Merge PR #335 (clippy fix) 2026-10-08 23:57:23 -04:00
Brandon Thomas d0749ed7a9 HEIC: as_chunks for 16-bit samples (clippy 1.99, caught by the new heif CI step)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 23:57:22 -04:00
Brandon Thomas bb7c46ae3c Remote SAM3 worker: crate lint attributes; document the trust boundary
The worker binary gets the never-crash deny list and forbid(unsafe_code) like
every production crate root. docs/ai-masks.md spells out who can reach the
unauthenticated loopback listener.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 23:55:26 -04:00
Brandon Thomas 7db3b8a1f9 Batch 14: one [features] table in apps/lightcraft (faces + heif), parity summary
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 23:53:54 -04:00
Brandon Thomas a628d5bc38 Merge PR #427
# Conflicts:
#	ROADMAP.md
#	docs/parity.md
2026-10-08 23:46:58 -04:00
Brandon Thomas 42f966eec2 Merge PR #335 2026-10-08 23:46:17 -04:00
Brandon Thomas ca97329ae1 Merge PR #174 2026-10-08 23:45:49 -04:00
Brandon Thomas 3368eb49ee Merge PR #173 2026-10-08 23:45:49 -04:00
Brandon Thomas cbf9935b6e Merge PR #172
# Conflicts:
#	docs/parity.md
2026-10-08 23:45:49 -04:00
Brandon Thomas 4977c648f6 Merge main into formats/heif-heif-decode; CI gates the heif feature
Resolve the docs/parity.md conflict, keep the HEIC rows. `cargo xtask ci` gets
a heif step (clippy + tests for lightcraft-codecs with --features heif), since
the workspace build leaves the feature off. The Cargo.toml comments no longer
say official builds enable it; the web one notes that wasm32 aborts on panic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 23:24:28 -04:00
Brandon Thomas 91d317900f Merge PR #395 2026-10-08 23:22:56 -04:00
sanyamsingh001 56754e5703 LR-BEHAV-HEADLESS: keep CPU snapshots isolated from saved preferences
Co-authored-by: Codex <codex@openai.com>
2026-10-09 13:19:47 +10:00
Brandon Thomas 1a2d2ab975 Merge PR #422 2026-10-08 22:43:10 -04:00
Brandon Thomas 1f30856774 Merge PR #426 2026-10-08 22:43:10 -04:00
Brandon Thomas 05769798f6 Merge PR #429 2026-10-08 22:43:10 -04:00
Daniel Voshart 111cc893ec LR-LIB-PEOPLE: preserve later person navigation on current main
Port the original Escape return and last-person shortcut behavior, keeping current translations and a no-wrap chip. The existing headless regressions and all seven quality gates pass with the hosted fonts on the refreshed recognition dependency chain.

Generated with Codex.
2026-10-08 22:29:31 -04:00
Daniel Voshart 24f87a91a0 LR-PREF-PEOPLE: update pure-Rust recognition and preserve valid model replacements
Keep current upstream UI behavior and the later detector contract checks. Validate candidate models before replacing working weights and records, with a synthetic regression that failed before the fix. Remove the unrelated GPU equivalence-test relaxation. All seven quality gates pass with the pinned hosted fonts.

Generated with Codex.
2026-10-08 22:29:31 -04:00
Daniel Voshart 20a801d6d2 Faces: YuNet face detection and one-click model downloads, with nothing bundled
Photo > Detect Faces runs the YuNet detector (a small pure-Rust interpreter over the ONNX graph, no
runtime dependency) and adds the faces it finds as unnamed face regions in one undo step. The detector
and the recognition models are opt-in downloads: Settings > Faces shows each model's licence and
training-data note, Download stays disabled until the terms are accepted, and the file is fetched over
https with lightcraft-fetch from the model's own repository at a commit-pinned address, checked against
its recorded size and SHA-256 and then installed. Detect Faces offers the detector's download the first
time it is used. No model file is part of the repository (the real-model tests read LC_YUNET_MODEL; two
ignored tests download from the real hosts).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 22:29:30 -04:00
Daniel Voshart 03b665e7a3 LR-PREF-PEOPLE: refresh the generated parity summary
Generated with Codex.
2026-10-08 22:29:29 -04:00
Daniel Voshart 6e8db91673 LR-PREF-PEOPLE: refresh model manager for current platform helpers
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.
2026-10-08 21:59:02 -04:00
Daniel Voshart 7845d515e8 Faces: nothing is bundled, so YuNet is not reported as installed
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>
2026-10-08 21:59:02 -04:00
Daniel Voshart cb34bb4842 Faces: Settings > Faces tab and the add-a-model licence dialog
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>
2026-10-08 21:59:01 -04:00
Daniel Voshart 7ca7113b32 Faces: model commands - list, inspect, install behind the licence, select, remove, on/off
`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>
2026-10-08 21:59:01 -04:00
Daniel Voshart 39de0e6a1c Faces: model manifests, known-model table and an ONNX probe (no runtime yet)
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>
2026-10-08 21:59:00 -04:00
Exalted5373 3c3b527b7f Web: the window wire test allows the CPU/GPU difference
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>
2026-10-08 18:33:43 -07:00
radialmonster 3d9f8492da LR-IMP-FORMATS: Coolpix NRW black level from the NRW data block
In NRW data version 0104 the u32 at byte 0x20 is the black level in sample units: 200 where the darkest samples of the B700, P950, P1000 and P1100 sit, 0 for the A1000 whose samples reach 0. Used when the maker note has no 0x003d.
2026-10-08 21:26:26 -04:00
radialmonster a12f1697a1 LR-IMP-FORMATS: decode 12-bit raws packed in 32-bit little-endian words (Fujifilm RAF flag 0xf006, Coolpix NRW data block)
Raw-IFD flag 0xf006 = 1 marks RAF samples packed MSB-first into 32-bit little-endian words at any depth; 12-bit files with it (FinePix S1, X-A10, SL1000) were read as an LSB-first stream. Nikon uncompressed 12-bit strips of exactly 1.5 bytes per pixel in a file whose maker note has an NRW data block (COOLPIX B700, P950, P1000, P1100) use the same words in file byte order; without the block (E5700, E8400, E8800) they stay an MSB-first byte stream. Shared unpack_words32_msb and Packing::Words32Msb. RENDER_CACHE_VERSION 15 to 16.
2026-10-08 21:26:26 -04:00
Exalted5373 0ac5cf4430 LR-EDIT-COLOR-TEMP: camera-space white balance for raws with a colour matrix
A custom white balance on a raw with a colour matrix (DNGs, and other raws
without a file-local look) re-develops the pixels for the chosen white
through the file's own colour model: lightcraft_raw::color::rebalance
re-evaluates the camera transform at the new white and maps the as-shot
development onto it, so a neutral lit by that white renders neutral and
saturated colours move as the camera's matrices say (DNG spec ch. 6, as
Lightroom does). The decoder passes the colour tags and the white the
pixels were developed for in SourceInfo::camera_color; other photos, and
relative white balances, keep the Bradford adaptation. The eyedropper and
Auto invert the same model (color::neutral_white), so a picked grey card
renders neutral.

The Tint sign is main's (#349): unchanged here. Render cache version 15.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:18:54 -07:00
Exalted5373 ce7b8f511e LR-IMP-FORMATS: read DNG BaselineSharpness and keep it in DNG exports
ColorData::baseline_sharpness holds the file's BaselineSharpness (raw IFD
or IFD 0; zero, negative, non-finite or above 16 is ignored) and the DNG
writer writes it back, so a conversion or smart preview keeps it. Merge
outputs leave it unset. Rendering doesn't use it yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:18:54 -07:00
Exalted5373 ed59d78e83 LR-MASK-SKY: AI masks use DNG semantic mattes (iPhone ProRAW sky matte)
Read DNG 1.6 semantic masks (SemanticName, SemanticInstanceID,
MaskSubArea; any coding the tile/strip reader decodes, e.g. ProRAW's
lossy-JPEG matte) with lightcraft_raw::semantic_masks, map them onto the
developed image, and carry Apple's sky / portrait / skin / hair / teeth /
glasses mattes in SourceInfo::mattes (SourceInfo is no longer Copy).
Sky, Subject, Background and People components sample them through
orientation, warp (Warp::frame, as geometry.rs) and crop like the image,
so preview = export, instead of the heuristics; files without mattes
render as before. Malformed mask IFDs are skipped, and only the first
MAX_MASKS mask IFDs are decoded, so a file full of them costs bounded
work. The render cache version is bumped once, in the white-balance commit.

The corpus test reads corpus/raw/dng-apple-iphone12pro-proraw.dng, the
name `cargo xtask corpus` downloads it under.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:18:54 -07:00
Exalted5373 a28dc4dbdf LR-PROF-CAMERACOLOR: read and keep DNG ProfileGainTableMap (not rendered)
Parse ProfileGainTableMap (DNG 1.6) and ProfileGainTableMap2 (DNG 1.7):
both versions, DNG 1.7 precedence (version 2 in IFD 0, then the raw IFD;
version 1 in the raw IFD, then IFD 0), u8/u16/f16/f32 gains, gamma.
Malformed tags (wrong size, zero or huge grids, non-finite or negative
gains, bad spacing or gamma) are ignored, never allocated for.

Byte order: the specification gives this tag no byte order of its own,
unlike opcode lists, so the payload is read in the file's byte order with
a big-endian fallback; a wrong-order read can't pass the size checks.
The module docs quote the DNG 1.6 "Byte Order" rule.

The map is kept in ColorData, written back by the DNG writer (version 1,
or version 2 when its gamma isn't 1) and dropped from merge outputs, whose
geometry no longer matches the capture's active area. It is not applied
to the render: Lightroom Classic renders Apple ProRAW without it.
Supersedes the parser of #273.

The CC0 iPhone 12 Pro ProRAW from raw.pixls.us joins the corpus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:18:16 -07:00
Exalted5373 1ed80c65bf LR-IMP-FORMATS: decode JPEG XL compressed DNG tiles (DNG 1.7)
Compression 52546 tiles and strips decode through jxl-oxide (new
default-on `jxl` feature of lightcraft-raw): bare codestreams and
ISO-BMFF containers, 1 or 3 planes, 8-16-bit integers scaled back
exactly from the codestream's own bit depth, 16-bit floats. Edge tiles
may be coded at their nominal or in-image size. Headers are checked
against the IFD in header-only probing too. A single-chunk JPEG XL
preview IFD (RGB or grey) is offered as the embedded preview.

The decoder's memory is bounded by the tile: 16 bytes per sample (three
times that for a lossy grey tile, which the decoder expands to three
channels) plus 64 KiB; jxl-oxide 0.12 was measured at <= 10.3 bytes per
sample for lossless and <= 13.5 per colour sample for lossy tiles. A
codestream needing more (e.g. extra channels) is an error.

Lossless tiles are checked bit-exact on synthetic CFA and LinearRaw
files (zune-jpegxl as a test-only encoder); lossy tiles are unverified
on a real file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:18:16 -07:00
BFlat b1883231bd
Merge pull request #424 from storytold/claude/integrate-batch-13
Integrate #403, #411, #412, #414, #415, #416
2026-10-09 08:15:07 +07:00
Nathaniel Ledford 733846b326
Docs: sidebar auto-scroll while dragging
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 21:02:07 -04:00
Nathaniel Ledford 8ff45f5237
Sidebar: dragging albums or photos near its top or bottom edge scrolls it
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 21:02:07 -04:00
Nathaniel Ledford 5fefe91640
Docs: top-level drop is on the Albums header
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 21:02:07 -04:00
Nathaniel Ledford 24b80b4ee3
Albums: the Albums header is the way back to the top level
It replaces the strip under the tree, which could be scrolled out of view
and shifted the rows when it appeared; the header is always visible and
nothing moves.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 21:02:07 -04:00
Nathaniel Ledford 922ea25d88
Docs: album tree, drag and hand order (parity, what's new)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 21:02:07 -04:00
Nathaniel Ledford 085c25b10b
Albums: drop beside a row to place it by hand; Sort A–Z goes back to by name
Dropping on the edge of a row places the album there (album.reorder) with
a line showing where; a folder's middle still means inside. A closed folder
opens only when the drop would go inside it. Sort Contents A–Z and Sort
Albums A–Z (album.sort) appear once a folder is ordered by hand.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 21:02:07 -04:00
Nathaniel Ledford 8036f98aca
Catalog: tidy a test assertion
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 21:02:07 -04:00
Nathaniel Ledford 34a4b61f7e
Albums: one-pass children map, before-itself is a no-op, more order tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 21:02:07 -04:00
Nathaniel Ledford 2c5cb708bb
Engine: album.reorder and album.sort
Place an album or folder before a sibling of its kind (also in another
folder), or last of its kind, in one undo step; album.sort goes back to
by name for one folder.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 21:02:07 -04:00
Nathaniel Ledford 39f5f114de
Catalog: albums can be ordered by hand (format version 3)
Album.order and Op::SetAlbumOrder; album_children lists folders first, then
by place, then by name. Moving an album drops its place and undo restores it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 21:02:07 -04:00
Nathaniel Ledford 67aed0b4f4
Albums: a drag ends with the main button, Esc cancels it, hidden rows are no targets
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 21:02:07 -04:00
Nathaniel Ledford 63335e2b0a
Parity: albums and folders move by dragging onto a folder
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 21:02:07 -04:00
Nathaniel Ledford aaae39fae0
Albums: drag an album or folder onto a folder to move it
A folder under the pointer is outlined when the drop is allowed and opens
after resting there; a drop zone under the tree takes an album back to the
top level. The move is album.move, so it is one undo step.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 21:02:07 -04:00