12 Commits
Author SHA1 Message Date
dtourolleandClaude Opus 5 23c3155128 Release 0.10.0
Benchmarks / CPU and I/O (per commit) (push) Successful in 11m46s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 1h15m53s
Build and test / Layer separation (push) Successful in 40s
🐳 Android image / Build and push (push) Successful in 8s
Build and test / android-image (push) Successful in 8s
Traceability / Requirement traces (push) Successful in 1m32s
Build and test / Android (aarch64) (push) Successful in 1h3m1s
Three waves of work since 0.9.0.

Culling gained the two instruments FR-CULL-3 asked for and never had:
focus peaking, and a histogram that reads the sensor rather than the
frame about to be displayed. Bursts group themselves. Masks can cover a
whole category rather than one instance.

The catalog now notices when it has been damaged and offers a way back --
restore a backup, or rebuild from the photographs, which were never at
risk. A crash leaves a record. A log survives the process, on a path a
tablet will hand back to adb, which is what makes the Android work
debuggable at all.

The APK compiles its own Java for the first time, so the app can receive
a photograph from another application and hand one back. Memory pressure
is answered in a stated order. A lost library root is reported rather
than reported as an empty library.

There is a benchmark suite now, so §8's promise that a regression fails
the build is a mechanism rather than a sentence. Its first run says the
catalog opens in 70ms against a 2s budget, and that thumbnail throughput
does not obviously reach its target.

Coverage 59.8% -> 69.8%, and it means more than it did: five requirements
that were tagged on code that did not implement them are no longer, and
the tool no longer counts its own test fixtures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 16:52:05 +02:00
dtourolleandClaude Opus 5 7312aceded Get the scene model onto the devices that need it
The decoder can load from a path; nothing yet put a file at one. Four
packaging routes, and one lookup that finds the result.

## Not `include_bytes!`, unlike the instance model

The instance model is 11 MB and compiled in, which was the right call for
it: Android hands the app no filesystem path (ARCH §6.9) and 11 MB is
tolerable. The scene model is 24 MB, and 35 MB of constants in the binary
is paid by every install whether or not the tab is ever opened.

So it follows `models/face/` instead — carried as an APK asset, unpacked
once at first launch into the shared directory a desktop install already
uses, after which every lookup finds it where it finds a desktop user's.
Assets are stored rather than deflated in the APK, so unpacking is a copy
rather than an inflate.

`embedded-scene-model` exists for the desktop build with nowhere else to
read from, and for tests wanting the real graph. Off by default, which is
the asymmetry with `embedded-model` and the reason for a separate
feature.

## Three files, all or none

`scene_model` insists on the graph, its vocabulary and the category
descriptor together, for the reason `face_models` insists on its pair: a
graph alone decodes to 150 anonymous channels. Reporting the set missing
beats starting and failing at the first inference.

## The two model sets are not the same kind of thing

`install_bundled_models` now carries both, and the distinction is worth
keeping in view. Face weights are absent from the repository *by design*
— the InsightFace grant is research-only (docs/faces.md §2) — so a build
carrying none is ordinary. The scene model is committed, so a build
carrying none means a checkout without `git lfs pull`.

Neither is fatal. A photo editor that refuses to start over a missing
grading feature is worse than one that starts without it, so both report
themselves unavailable exactly as face indexing already did.

The LFS-pointer guards apply to the `.onnx` only. The vocabulary and the
descriptor are legitimately a few kilobytes, and a size check that fails
on them would be a guard against the wrong thing.

`scene_model` is exported ahead of the tab that will consume it so the
packaging added here has something to be verified against — assets
written where no lookup looks would be a silent mistake for as long as
the tab took to arrive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 10:48:54 +02:00
dtourolleandClaude Opus 5 a56ac87e2c Build a Flatpak that asks for no filesystem at all
FR-PLAT-LIN-3 says that where DarkRoom is distributed as a Flatpak, filesystem
access uses portals. There was no manifest, so the sandboxed path had never been
exercised and the requirement had never been tested against the code.

The manifest is YAML rather than JSON because it has more to explain than to
declare, and every permission in finish-args carries the argument for itself.
The two that are not obvious:

- --device=dri is not an optimisation. The develop pipeline is compute shaders
  through wgpu with nothing behind it while NFR-R8 is open, so without the
  render node the application starts and cannot develop.
- --talk-name=org.freedesktop.secrets rather than the Secret portal. These are
  different things and the requirement's wording invites the wrong one: the
  portal hands an app a master key for a store it keeps itself, whereas the
  session's secret daemon is what keeps the Nextcloud app password visible to
  secret-tool and Seahorse, and therefore individually revocable by the user.
  FR-NC-2's degraded mode is what happens if nothing answers, inside the sandbox
  exactly as outside it.

There is no --filesystem= line, and that absence is the substance rather than an
oversight. It leaves the document-portal path working — a photograph opened from
a file manager arrives in argv under /run/user/$UID/doc and opens with no code
change — and leaves library selection broken, because dr-sync-folder takes a
typed absolute path and nothing in the tree calls the FileChooser portal.
--filesystem=host would fix that and is precisely what the requirement forbids;
--filesystem=xdg-pictures would fix it by not testing the design, in a smaller
directory. docs/distribution.md §4 records what actually closes the gap and the
flatpak override to use in the meantime.

The runtime version is chosen from what the binary needs rather than from what
is newest. ldd on a release build names fontconfig, freetype, expat, libpng,
zlib, brotli and bzip2 and nothing more: wgpu dlopens libvulkan.so.1, and x11rb
and wayland-client speak the wire protocols in Rust rather than binding libxcb
or libwayland. So what the runtime must supply at runtime is a Vulkan loader and
an ICD, which is the GL extension's job, and freedesktop 25.08 carries a
rust-stable extension at 1.98.0 — comfortably above the workspace's 1.92
minimum. That extension is not rustup, so rust-toolchain.toml's pin is ignored
here; the manifest says why that is correct rather than a violation of
CONTRIBUTING.md, since the pin exists to make fmt and clippy agree and neither
runs in a packaging build.

Like the PKGBUILD, it builds from the local checkout, so a build is of what you
are working on. That needs network for cargo, which Flathub forbids — the
comment says what a submission there would need instead and why generating
30,000 lines of vendored sources buys nothing yet. The LFS pointer check is
carried over from the PKGBUILD for the same reason it exists there: a dir source
copies a 130-byte pointer in without complaint, and the failure would land on a
user's machine rather than the packager's.

Not verified: nothing has been built. flatpak-builder is not installed here and
the machine is under a build embargo. The manifest parses, its keys are the ones
flatpak-builder reads, and the desktop entry and metainfo it installs both
validate — but no Flatpak has been produced from it and no permission has been
observed to be sufficient.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 20:18:56 +02:00
dtourolleandClaude Opus 5 47a40d1afa Describe the application once, in a file every channel installs
A desktop entry gives a software centre a name and a one-line Comment, and
nothing else — no description, no licence, no age rating, no statement of what
hardware the interface was laid out for. GNOME Software and Discover both show
an application with no metainfo as an unexplained icon, and both packages this
repository produces were in that position.

packaging/paris.tourolle.darkroom.metainfo.xml is the AppStream component, and
it is installed by the PKGBUILD as well as by the Flatpak manifest because the
description, the licence fields and the OARS rating are facts about the
application rather than about how it was packaged. Writing it twice is how the
two packages start disagreeing.

Two things in it are easy to get wrong and are commented in place. The two
licence fields differ on purpose: metadata_license covers the file itself and
has to permit the unconditional redistribution and reformatting a catalogue
does, which GPLv3 does not, so it is CC0-1.0; project_license is the
application's own and reads GPL-3.0-or-later to match D8. And the component id
is not a fifth name but the same string as the desktop basename, the Flatpak
application id, and the app_id dr_ui::run sets — a rename that misses one costs
the icon or the association, and neither failure announces itself.

D15's decision that the target devices are a tablet and a desktop is stated as
a display_length requirement rather than left implicit, so a software centre
does not offer this on hardware where the photograph and the parameter panel
cannot both be on screen.

Validates clean under appstream-util validate-relax; appstreamcli --pedantic
reports only that the gitea URLs are unreachable from a machine that cannot
see that host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 20:18:35 +02:00
dtourolleandClaude Opus 5 bbd26f05f9 Release 0.9.0
Build and test / Desktop (Linux) (push) Successful in 2h8m7s
Build and test / Layer separation (push) Successful in 37s
🐳 Android image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 1s
Traceability / Requirement traces (push) Successful in 1m42s
Build and test / Android (aarch64) (push) Successful in 1h4m36s
Thirty-five commits since 0.8.0, and two of them are the reason this is a
minor rather than a patch.

**Storage became pluggable.** A folder backend now sits beside the
Nextcloud one behind the same seam, proved by a test rather than by a
trait, and the launch screen offers three routes into a library instead
of one.

**Faces gained a confidence that means something.** A suggestion is
scored against the people the user has actually named, the curve it came
from is stated rather than implied, and a regroup runs roughly three
times faster — the similarity scan and the merge engine both use the
machine's own SIMD kernel now, measured on the tablet where the NEON path
is the one that runs.

Alongside those, the framing tools got the quality-of-life pass this
release is named for: a crop can be held to a ratio, a straighten crops
away the corners it exposed, an export can be pinned to an exact
resolution with the common panel sizes offered as buttons, and dragging
the crop rectangle no longer tracks the pointer at half speed.

`pkgrel` returns to 1: a new `pkgver` is a new archive name, so there is
nothing left for a release number to disambiguate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 13:53:19 +02:00
dtourolleandClaude Opus 5 e997be8c72 Say which version this is: 0.8.0
Build and test / Desktop (Linux) (push) Successful in 2h14m44s
Build and test / Layer separation (push) Successful in 54s
🐳 Android image / Build and push (push) Successful in 3s
Build and test / android-image (push) Successful in 3s
Traceability / Requirement traces (push) Successful in 1m45s
Build and test / Android (aarch64) (push) Failing after 53m50s
A hundred commits since 0.7.0, and the face subsystem in them is not a set of
fixes — it is the difference between a feature that was shipped and one that
works.

Clustering finished at all for the first time: the old agglomeration rescanned
every live pair and recomputed average link from scratch after every merge, and
on a real library it did not return. A sparse above-threshold graph, connected
components and Lance-Williams sums put 1,813 faces at 0.28s, and the merge
threshold moved to 0.80 because the old 0.90 was measured — not guessed — to
leave a third of the library ungrouped.

Faces are no longer indexed at any size or any sharpness. Both floors were
measured over the real library with `face_index --quality`: 32 source pixels
across the aligned crop, and a contrast-invariant sharpness that catches the
large-but-blurred face whose confident, wrong embedding used to weld two people
together.

The crop is cut once and kept, so the People screen is no longer a derivative of
a thumbnail cache entitled to evict anything at any moment. The screen itself
became usable: the faces wrap into a grid instead of running off the edge, the
header fits a phone, a group can be set aside, and a person's photographs are a
button away — as a union or an intersection of several people.

And face sync now reaches the other device. Re-indexed images re-export, shards
written before the crop and index-time columns are repaired rather than failing
every insert, people and the user's judgements about them cross the wire at all,
and the pass says what it is doing while it does it.

The Android versionCode follows without being restated — package.sh packs
MAJOR*10000 + MINOR*100 + PATCH, so 0.8.0 is 800, above the 700 already on
devices and therefore an upgrade rather than a refusal. `pkgrel` returns to 1,
since this is a new version rather than a rebuild of the last one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 22:24:58 +02:00
dtourolleandClaude Opus 5 16c17c349d Bump pkgrel so makepkg rebuilds instead of reusing the modelless archive
`makepkg -si` reported "A package has already been built, installing existing
package" and installed 0.7.0-1 — the archive from before the models were added,
so the install still had no model and the app still said so. The version had not
changed because the application had not changed; only what the package contains
did, which is precisely what pkgrel exists to signal.

Verified: 0.7.0-2 carries both models at /usr/share/darkroom/models/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 18:14:56 +02:00
dtourolleandClaude Opus 5 2d95807542 Package the models on every platform, not just the phone
The Android bundling landed the weights under that platform's asset directory,
which was the wrong home the moment a second packager wanted them. `makepkg -si`
produced a desktop install with no model at all — the same "no face model is
installed" the phone used to show, for the same reason: nothing put the files
anywhere the app looks.

So `models/face/` at the root is the one copy, and both packagers read it:
assemble-apk.sh bundles it as APK assets, and the PKGBUILD installs it to
/usr/share/darkroom/models. Both refuse an LFS pointer rather than shipping a
130-byte file that fails inside the graph loader on a user's machine.

`face_models` now searches three places, most specific first: the account's own
directory, the shared user directory, then $XDG_DATA_DIRS. So a packaged pair is
found automatically and a pair the user placed by hand still outranks it — which
is what keeps a deliberate choice of weights from being overridden by an
upgrade.

$XDG_DATA_DIRS rather than a hard-coded /usr/share: that is the variable a
distribution, a prefix install or a Nix-style store already sets to say where
its data went, and its documented default is exactly the two paths that would
otherwise have been hard-coded. Empty on Android, which has no such directories
— there the APK's copy is unpacked into the shared user directory instead,
because an asset inside a package is not a path anything can read from.

Verified: the APK still carries both models at assets/models/, the PKGBUILD
parses and installs from the new path, 467 tests pass.

Includes the pkgver 0.6.0 → 0.7.0 bump that was already sitting uncommitted in
the working tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 18:05:23 +02:00
dtourolleandClaude Opus 5 a3785ba55d Say which version this is: 0.6.0
Build and test / Desktop (Linux) (push) Failing after 2m24s
Build and test / Layer separation (push) Successful in 26s
🐳 Android image / Build and push (push) Successful in 8s
Build and test / android-image (push) Successful in 7s
Traceability / Requirement traces (push) Successful in 1m5s
Build and test / Android (aarch64) (push) Failing after 4s
Set by tools/set-version.sh, which is the only thing that should. The
workspace, the pacman package and — through Cargo.toml at link time — the
APK all state 0.6.0, so a bug report naming a version names one commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 19:59:30 +02:00
dtourolleandClaude Opus 5 f4c7afb74c Say which version this is: 0.5.0
Build and test / Desktop (Linux) (push) Failing after 3m8s
Build and test / Layer separation (push) Successful in 1m32s
Traceability / Requirement traces (push) Failing after 2m56s
🐳 Android image / Build and push (push) Successful in 24m43s
Build and test / android-image (push) Successful in 24m45s
Build and test / Android (aarch64) (push) Failing after 6s
Set by tools/set-version.sh, which is the only thing that should. The
workspace, the pacman package and — through Cargo.toml at link time — the
APK all state 0.5.0, so a bug report naming a version names one commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 11:48:03 +02:00
dtourolleandClaude Opus 5 5a0a9719eb Set the version once, in one place, for every artefact
The workspace read 0.1.0 for two releases, so every desktop binary reported a
version two releases stale. The APK was worse: `AndroidManifest.xml` states no
version at all, so a device showed `versionName=null` and `versionCode=0` while
the library inside the APK knew exactly what it was. A version edited by hand
in several files is a version that is wrong in at least one of them.

`tools/set-version.sh` is now the only thing that sets one. It takes the
version from the latest git tag, or is told, and writes the two files that must
state it before anything is built: the workspace `Cargo.toml`, from which every
crate inherits, and `packaging/PKGBUILD`, which pacman reads before a build
exists. It refreshes `Cargo.lock`, because members appear there by version and
CI builds `--locked`. `--commit` commits the result.

Android is not in that list on purpose. `package.sh` reads the version out of
`Cargo.toml` and hands it to `aapt2 link`, so the APK cannot drift from the
binary it contains — there is no third file to forget. `versionCode` has to be
one increasing integer, which a semantic version is not, so it is packed as
MAJOR*10000 + MINOR*100 + PATCH: ordered the way Android requires, and readable
at a glance.

A version that is not MAJOR.MINOR.PATCH is refused rather than coerced. It is
a contract with whoever reads a bug report, and silently turning "0.4" into
something else is worse than being asked to type it again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 11:09:03 +02:00
dtourolleandClaude Opus 5 e25f0ad2c6 Let the launcher find an icon for the window
The icon was already embedded and had been all along: `app.slint` sets it and
`build.rs` compiles it into the binary with `EmbedFiles`. That is not what a
Wayland compositor looks at. It ignores a client-set icon entirely, matches
the surface's `app_id` against installed `.desktop` files, and takes the icon
from the one whose basename agrees. Nothing set an app id and no desktop entry
existed, so GNOME had nothing to resolve and drew the placeholder.

Both halves are needed and neither substitutes for the other: the embedded
icon is what X11 and the window itself use, and the desktop entry is what the
overview, the dash and alt-tab use.

The app id, the `.desktop` basename and the installed icon's filename are one
string in three places — `paris.tourolle.darkroom`, the same reverse-DNS name
the Android manifest already uses, because it is one application. Change one
without the others and the icon disappears again with nothing logged, so each
file says so where the string appears.

The PKGBUILD builds from the checkout rather than a release tarball, so
`makepkg -si` installs what is being worked on. Install verified by inspection
of the built package: binary, desktop entry, and the icon under
hicolor/256x256 by the name the entry asks for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 10:26:04 +02:00