mosaic-{fast,medium,best}.onnx are the shipped networks re-exported by
darkroom-denoise tools/export_whole.py (22ea648) from the checkpoints
the 1408 files came from: identical to them at 1408 (max |d| = 0), to
torch at 592 x 848, and to tiled inference over the reflected frame in
f64. 42 MB together. The Arch package and the Windows installer carry
them beside the fixed files; the APK leaves them out, since the Hexagon
takes fixed shapes only.
The Windows installer and the Flatpak carried no ONNX Runtime, so they
ran every model on tract's one core; the Arch package left it to an
optional dependency. Each now installs two builds under runtimes/ —
Intel's OpenVINO build and the generic WebGPU one, both with the CPU
provider — fetched by tools/fetch-bundled-runtimes.sh from PyPI wheels
pinned by SHA-256, pruned to the native libraries (81 + 31 MB on Linux,
67 + 42 MB on Windows), licence texts beside them.
darkroom-desktop searches runtimes/openvino and runtimes/webgpu under
each place a package installs to; the engine opens all it finds and
keeps the one that fits the GPU, so a CUDA or ROCm runtime installed
beside them still wins on its vendor's card. On Windows the chosen
runtime's directory goes on PATH, because Intel's build leaves OpenVINO's
DLLs for the loader to find there.
The Windows image gains unzip; the installer smoke test checks both
runtimes landed.
AI Denoise's Apply switch becomes Method: Bilinear, Fast, Medium, Best,
default Best, so an untouched raw writes nothing and develops through the
mixture. `apply` is still read and never written: 0 is Bilinear, 1 keeps
a network already chosen.
- Best is the mixture of a flat and an edge expert with a learned gate;
Medium and Fast are students distilled from it. 2.48 s, 0.79 s and
0.57 s for a 20 MP frame on TensorRT fp16.
- Each network carries its own tile border (256 for the mixture, 192 for
the students) through `dr_denoise::Shipped` and `TileNet::halo`.
- The file is hashed once at open and each network keys its own cached
result; Bilinear keeps the result in memory for the way back.
- Each has an .a16w16 sibling for the Hexagon: 0.00 dB on the 6D gate,
at most 0.11 dB with the noise scaled x0.5 to x4.
- APK BUNDLED 19 -> 23; the PKGBUILD installs all three.
The README said export went "to a folder here or back into the
library", which albums replaced and forbid, and called presets "named
presets" beside a shipped collection of film looks and Lightroom
imports. It now says both, and that a photograph only on the server
opens on its thumbnail with its download's progress. The requirement
count comes from traceability.md's summary: 192, 84% claimed; the
version and release count are left to the release commit.
docs/README.md's manual row gains presets and albums. CONTRIBUTING
said 177 requirements and 826 crates, and the Flatpak manifest 826
crates; the lockfile now holds 849 packages, 822 from the registry.
distribution.md §4, outstanding.md's FR-PLAT-LIN-3 entry, the Flatpak
manifest's comment and the README all said a library was chosen by
typing a path and that nothing in the tree called the FileChooser
portal. Since 6683c14 every folder the desktop asks for is chosen
through rfd's xdg-portal backend, so those sentences were false.
What they now say instead is narrower than "it works in the sandbox":
no Flatpak has been built here, so whether the portal's path opens a
library, holds across a restart and takes a sidecar is unobserved, and
volumes() still cannot see a host card. The chooser also landed in ui/
rather than behind the dr-plat seam distribution.md had proposed, and
both documents say so. FR-PLAT-LIN-3 gets a status note to the same
effect.
The rendered manual was in the repository and nowhere else, so an
installed application still had nothing to open.
Each packager now carries docs/manual/index.html and its pictures, to
where the application will look for them: /usr/share/darkroom/manual on
Arch, manual\ beside darkroom.exe on Windows (where the models already
are, and where dr_plat::system_data_dirs points), and assets/manual in
the APK, stored rather than deflated since a GIF or PNG is already
compressed. The manual is about 27 MB, which the APK and the installer
both grow by; the pictures are 1600x1100 screenshots and short GIFs,
and against an APK that already carries 170 MB of inference runtime and
70 MB of models they are not worth re-encoding for.
The pictures are LFS objects, so each packager refuses a pointer where a
picture should be, as it already does for the models: shipped, a pointer
is a manual of broken images that nothing reports. The Android and
Windows CI legs therefore fetch docs/manual/media, which they excluded
while nothing they built read it, and the installer smoke test checks
that the page and every picture were installed.
docs/ had 26 developer documents flat beside the manual, and the two
audiences are very differently sized: most readers want the manual and
the gesture reference, a few want the register, the designs and the
measurements. The manual and gestures.md stay at the top; everything for
someone changing the code moves to docs/dev/, and the two documents that
name their own successors — the v0.1 milestone and the UI-refinement plan
— go to docs/dev/archive/ rather than being deleted, since both are still
cited. docs/README.md is the index, users first.
Every reference follows: code comments, Cargo manifests, the workflows,
the pre-commit hook, the bench and traceability tools (which locate the
repo root by docs/dev/requirements.md now), packaging, the Docker READMEs,
CLAUDE.md, CONTRIBUTING.md and the README. The matrix links one level
deeper and is regenerated. Links out of the moved documents into the tree
gain a level; a link checker over every Markdown file finds none broken.
`inference::init` listed the models from the user's shared directory
alone, while the app loads them from there or from the package's
`/usr/share/darkroom/models`. On a fresh package install the probe found
"no model to probe with", stayed on the CPU, and compiled nothing. Both
now resolve each file with the same search, `library::shared_model`.
The PKGBUILD names the ONNX Runtime packages as optional dependencies,
since the app loads one from /usr/lib if present.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sargsyan et al., ICCV 2023; MIT code and weights (models/LICENCE.md),
exported by tools/export-migan.sh at a fixed 1×4×512×512 from the
authors' checkpoint — six operator types, 28 MB, in LFS like the rest.
The package installs it beside the scene model and the APK unpacks it
with the others.
2d106det for the eye contours, OCEC for open or closed, SGC for
sunglasses — all three pinned to a batch of one by the same script as
the pair, and installed by every packager so the eyes-open filter works
out of the box. The two classifiers are MIT, code and weights; the
README records their provenance, SGC's undocumented training set, and
the hashes as fetched and as shipped.
The repository declared GPL-3.0-or-later and carried no copy of it;
the Arch package pointed at the system's shared text and nothing else
needed one. The installer does: a licence page needs a file to show,
and the moment before installation is where the terms can still change
a decision. The standard text, at the root where every convention
looks for it, converted to CRLF at packaging time because a Windows
edit control draws a bare LF as nothing.
docs/windows.md specified it; this is §9 steps 1, 2 and 4 run, and the
report in §10. A Debian trixie image with rustup, the MinGW cross
compiler, NSIS and Wine; a build.sh in the shape of the Android one;
a package.sh that stages the executable and the seven models behind
the same LFS-pointer guard every other packager carries, then runs
makensis; and the .nsi itself — per-user, no elevation, an uninstaller
that leaves the library alone.
Measured: the executable links first time once the link flags were
right, imports only Windows system DLLs, prints its version under
Wine, and the installer installs and uninstalls silently under Wine
with the registry key and the models where §5.2 says. What Wine
cannot show is the Start Menu shortcut: CreateShortcut is IShellLink
and does nothing headless.
Four claims in the spec's first draft were wrong and are corrected in
place with the reasoning kept: the whole-archive winpthread flag
breaks the link and was never needed; build scripts need a host gcc;
bookworm's Wine lacks the bcryptprimitives.dll rustc's std imports,
so the image is trixie; and NSIS's default stub is 32-bit, so the
installer says amd64-unicode and needs no i386 Wine.
faces.md §12.3 measured what the cheapest detector costs: the small
faces in every group shot, and a dog embedded a dozen times. Which
trade is right depends on the machine doing the sweep — a desktop left
overnight and a tablet on a battery want different answers — so the
detector is now a per-device setting, Fast / Balanced / Thorough on
the settings page beside the indexing button, persisted with the rest
of the settings file.
A detector is half of a model id. Every face, marker, shard and
calibration is keyed on faces.model_id precisely so that a model change
is a new id and a re-index rather than a silent change under existing
data, and a detector change is a model change: it decides which faces
exist and where the landmarks that align them land. So each choice
names its own pipeline. 500M keeps the bare "w600k_mbf" every existing
library was written under, so an upgrade disturbs nothing; the others
are qualified. Choosing one restarts coverage from zero under the new
id, the sweep re-detects, confirmed names carry across by box overlap,
and the sync shards are keyed by the same id so a peer on another
setting neither adopts nor pollutes them. The library controller
carries the id into the sync the same way it carries the cache budget,
because the sync starts from places that have no settings in reach.
All three shape-fixed exports ship — APK, Arch, Flatpak — since a
tablet has no other way to obtain the one it was not installed with;
the APK grows by twenty megabytes for the choice.
A bug fix release. Nothing here changes what DarkRoom is for since 0.10.0;
it changes how often it does the thing it already claimed to do.
The edits that went missing. A subject or category layer was stored as
identity alone, on the reasoning that the pixels were reproducible by
re-running the model — true, but nothing re-runs one except a photographer
pressing "find subjects". So a reopened photograph rendered without its
local adjustments and then saved that state back, and a batch export wrote
three hundred files without the edits their photographer had made, over a
log warning. The coverage now travels in the sidecar. Export also learned
to write the photograph rather than the canvas, and to carry the
photograph's header when the export is made from develop.
Face indexing was reading previews. It ran against a proxy and then
recorded the result as though it had seen the photograph, so faces smaller
than the proxy could resolve were not missed, they were *concluded absent*.
Indexing now runs on the native render, runs made against proxies too small
to find a face are forgotten rather than trusted, and a sweep that fails
everything says so instead of reporting a clean pass.
Where you were. The photo roll opens on the frame it opened with, develop
returns you to the photograph you were editing, the grid keeps its place
when another screen covers it, and the photographer's position now travels
between devices rather than being rediscovered on each.
Startup. The catalog opens on a worker and the bundled models unpack on
one, so a launch is no longer a page-by-page read on the way to the first
frame; the app says it is starting before there is anything to say it with.
The People rail builds the rows you can see, keeps portraits off the
blocking path, and withholds the empty groups that used to fill it.
Segmentation gained the half it was missing: the colour gate decided what
belonged to a category and had no way to decide where its edge fell, so a
refined sky kept the model's blocky outline no matter how the control was
set. A marker-based watershed now puts each contour onto a real edge, and
the refinement is a per-layer slider.
Coverage 70.4% -> 70.6% (127/180).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>