Skip to content

Batch: PRs #126–#141 in one merge - #137

Merged
davidackerman merged 96 commits into
mainfrom
browser-batch
Oct 19, 2026
Merged

davidackerman merged 96 commits into
mainfrom
browser-batch

Conversation

@davidackerman

@davidackerman davidackerman commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

This merges the sixteen open PRs, #126 to #141, in one go, with their conflicts resolved. It's one review and one merge instead of sixteen merges and the "Update branch" rounds between them.

Merge it with a merge commit, not squash. Each PR's own commits then land on main as they are, and GitHub marks each of the sixteen PRs as merged. All sixteen are based on main, and each one's head commit is in this branch.

What's in it, in the order merged (each is its own merge commit here):

PR What it does
#129 A finetune's Restart no longer fails while its model goes into the browser's viewer (Neuroglancer threw on restoreState with the mouse over it).
#128 A layer drawn again keeps what you set on it (blending, opacity, shader controls), in both viewers, through Submit and in Share links.
#132 A Restart's model replaces the job's previous one in the browser, which is deleted from its storage.
#131 Shift+R over the viewer restarts the finetune, as Restart Training does.
#126 The browser's download buttons save their files (Export Config, the paint volume, the finetuned model).
#127 The browser no longer offers to save an AI-annotate key as a password.
#130 Share carries a model finetuned in the browser, and says what the link leaves behind.
#133 The connect field offers the 20 datasets you connected to last.
#134 An ONNX model type for the server; Export Config's YAML runs as it is.
#135 Run on a region in the browser: a box or the whole volume, saved as OME-Zarr in a folder on your machine. It saves exactly the box, warns when a box would hold a lot of the browser's memory, and can snap the box onto the chunk grid. chunkmirage is pinned to d6fd94a (its Engine.materialize and materializePlan).
#136 The main panel shows the x–y plane, in both viewers. See the note below the table: this branch replaces how.
#138 The server's blockwise runs over bounding boxes write only the boxes.
#139 The Models tab lists the loaded models at the top, each with a ✕ that stops it alone. This works for Hugging Face and bioimage.io models too.
#140 The browser can finetune a 2D model's whole network with TF.js, on Marwan's training graphs, offering the modes each model's manifest declares.
#141 AI-annotate keeps two touching objects apart when the image's pixels are finer than the annotation voxels. Only server models are affected; merged after the cleanup below.

One commit of this branch's own, e421f8f, fixes #136. #136 set the display dimensions to x, y, z over the viewers' z, y, x axes. That broke painting in both viewers: when the two orders differ, the Neuroglancer fork's brush reads the panel's direction in the wrong order, so each stroke stood on edge across the slice and mostly landed in the slices beside it. Both viewers now order their axes x, y, z, as Neuroglancer's own viewers do, and set no display dimensions, so the main panel is still the x–y plane and a stroke lands in the slice shown. Routes and links still use z, y, x. #136's own branch keeps its first version; merging this PR brings the fix.

A second, 74bbd72: whole-network training is offered for 2D models only. Since 2026-10-17 the 3D models' manifests on Hugging Face declare whole-network training too, with 3D training graphs this branch can't build yet. The Finetune tab therefore offered it for 3D models, and the submit failed. It now offers it only for models that read one slice; a 3D model trains its last layer, and the tab says why.

The conflicts were all side by side, not competing, so every resolution keeps both sides:

  • CHANGELOG lines in the same spot.
  • Import lists that each PR extended (main.ts, routes/finetune.ts, two test files).
  • Methods added next to each other in viewer/index.ts.
  • main.ts's boot: the Share call with its catalog, then the Run tab.
  • A CSS block per PR.
  • A sentence each in browser.rst and paragraphs in the web README.

Checks on #126–#140 merged, before the cleanup:

  • npm run check in web/ passes: typecheck, 1548 tests, build.
  • The full Python suite passes: 2238 passed, and 9 skipped for packages the test environment lacks. One test, test_importing_leaves_the_process_alone[blockwise-logging], fails here the same way it does on main in that environment.
  • In headless Chromium, on the branches before they were combined:
    • the main panel reads x across and y down;
    • a region run writes its OME-Zarr box and reopens it as a layer;
    • the memory warning and Snap work;
    • a whole-network finetune runs on WebGPU and a last-layer finetune runs through the new form, each served and downloaded.
  • Each PR's own checks are described in that PR.

Cleanup on top of #126–#140. An audit for redundancy across them found the PRs in decent shape. The commits after the merges remove what it found. The code changes take out slightly more lines than they add.

  • Tests: about 300 fewer lines. They had pinned sentences or constants, or tested the same case twice, in Python and in vitest.
  • Docs: each topic now has one owning doc, and the others point to it.
  • CHANGELOG: the batch's entries are now short, with a "See …" link.
  • Browser code: helpers that the PRs had re-written are now shared: the folder drop and picker, formatting, error messages, view names, and the two training modes' helpers. A stop-serving path and a finetuned-model record are written once. Dead code is gone.
  • Dashboard server:
    • One way to stop a model (launch._stop_jobs).
    • One module for the viewer's keys and their status-bar messages (viewer/keys.py).
    • A finetuned model's training job is looked up by the id its server records, not guessed.
  • Dashboard page:
    • One "models changed" event (lib/events.js).
    • One poll for what is starting on the Models tab.
    • One place that reads and writes the browser's storage (lib/stored.js).
  • Models, viewer and blockwise:
    • The model types share one set of size, axis and channel-name parsers.
    • A Hugging Face repo's files are checked in one place.
    • cellmap_flow view builds its viewer with new_viewer.
    • A bounding box is described once (blockwise/bounding_box.py). The box tool still accepts what it did; a blockwise task's boxes are checked strictly, as before.

What the cleanup changes for a user:

  • Stopping a whole-network run early now says the network, not the head, is kept.
  • A model removed with its ✕ no longer leaves its shader behind for a later layer of the same name. Submit and Cellpose restarts still keep what you set.
  • A model finetuned in a local run shows which model it came from on the Models tab, and its ✕ waits until training ends, as an LSF run's does.
  • The Models tab ticks models that are still starting, not only running ones.
  • If fetching the job logs fails while a Submit is being watched, the page shows the error and tries again. The poll waits while the page is hidden.
  • The training form no longer fails where the browser blocks storage.
  • A browser finetune's log says "1 epoch", not "1 epochs".
  • A browser finetune's submit checks the dataset before the training fields. A restart that asks for the whole network of a model without one says which modes that model offers.

The checks on this head:

  • npm run check in web/ passes: typecheck, 1546 tests, build.
  • Headless on jrc_hela-2: a brush stroke in the browser writes 1,720 voxels, all on the slice shown, and the axes stay x, y, z once the paint layer is added. A server-mode viewer keeps x, y, z and its centre through a layer added from Python. Painting itself wasn't tried in server mode.
  • The full Python suite passes: 2230 passed, 9 skipped. test_importing_leaves_the_process_alone[blockwise-logging] fails as it does on main in that environment.

…rome sends a download link's request past the service worker

Export Config and the Finetune tab's volume and finetuned-model downloads
are <a href="/api/..." download> links, and only the page's service worker
answers /api/. Chrome sends a download link's request straight to the
network: on the deployed site the server has no such file ("File wasn't
available on site"), and Vite's dev server saved its own index.html as
export-config.html. A fetch of the same URL is answered by the worker, so
a click on one of these links is now fetched by the page and the reply
saved from a blob: URL under the name its Content-Disposition gives
(installApiDownloads, web/src/rebase.ts). The links are looked up at the
click, so the Finetune tab's, which the dashboard fills in after the
rebase has run, are taken too, and fetched under the page's directory on
the Pages site.

The new end-to-end test clicks Export Config and reads the saved YAML.
…ser does not offer to save a key as a password

The API key and access token were password fields inside the Finetune
tab's form, and Chrome offered to save the key when the form was
submitted, taking the project field as the user name; autocomplete="off"
does not stop that prompt. The keys are meant to be pasted each time and
kept in the page's memory only. Chrome's password manager offers only for
password fields, so these are now text fields masked by CSS
(-webkit-text-security: disc, .credential-secret), with the attributes the
1Password, LastPass and Bitwarden extensions read to leave a field alone.
Where the browser cannot mask a text field, ai-annotate.js makes them
password fields again: a prompt to save the key is better than the key
shown in the clear.
A Submit, a model's server coming up (in the browser, any model added,
since every served model's layer is set again), a finetune's next
iteration, a good region marked or the paint layer re-added built the
layer anew, and Neuroglancer showed it as built: additive blending,
opacity, resolution, volume rendering and segment colouring were back at
their defaults; only being hidden was kept.

One rule, in cellmap_flow/viewer/layers.py (carried_layer_json,
replace_layer) and web/src/viewer/state.ts (carriedLayer, replaceLayers):
the new layer is the dashboard's, with every property of the old one it
does not set. The dashboard's own (the source, a new chain's shader, the
annotations drawn) win. The properties that belong to the old source's
data (segments shown, equivalences, the channel shown, the shader and its
controls, annotations) are dropped when the source is another one, and a
layer that changes type keeps only being hidden. Sources are compared by
URL, whatever form Neuroglancer wrote them back in.

A finetune's next model is handed the previous one's layer: its place in
the list and its settings (`replacing` on setLayers and serveModels in
the browser, replace_layer(..., replacing=) on the server). The server's
pipeline redraw, model start, finetuned-model, extra-layer, good-regions,
annotated-regions and paint-layer paths all go through replace_layer.

tests/web/test_layer_carry.py holds the two lists of source-bound
properties equal and runs the rule's cases in Python; the vitest suite
has the same cases for the TypeScript, and the pipeline route test checks
blending and opacity through another model coming up and a new chain.
…and reopens on the state if Neuroglancer throws anyway, so a finetune's restart no longer fails as its model goes in

With the pointer over the viewer, Neuroglancer threw on a restored state
that added a layer with its panel open (the paint layer on its Draw tab):
LayerSelectedValues.update, run as the layer list changes while the mouse
is active, read the new user layer before initializationDone had made its
selection state ("Cannot set properties of undefined (setting
'localPositionValid')", "Error restoring property "layout": ... (reading
'generation')"). The viewer was left with the raw alone, blank, and the
throw went up through setLayers into the finetune job, which was marked
FAILED after its model had been trained and kept.

applyState now marks the mouse off the viewer (MouseSelectionState
.setActive(false)) before restoreState, which is synchronous, so the
tracking skips the half-made layers; the next move over a panel marks it
on again. Reproduced in headless Chromium against the fork's bundle: with
the mouse active the restore threw in 3 of 3 fresh pages and left only
the raw layer; marked off first it threw in none, with every layer there
and no error on the next hover. Should Neuroglancer throw anyway, applyState
opens the viewer again on the state in a new iframe, for every change
(refreshLayer had this alone), and says so in the log; nothing it is
asked to show throws to its caller.
…what it leaves behind

Browser finetuning changes only a model's last layer, so a finetuned model
goes in the link as what rebuilds it: model=<name>~<base>~<file>~<out>x<in>~
<weights>, the weights as float32 in URL-safe base64 (528 characters for a
3 x 32 head), in the session's model order. Where the link opens, the base
is downloaded, the weights written into it (withHead), and the model kept
and served as one finetuned there, its record saying it came from a link.
A model of that name already in that browser is never replaced: one with
exactly those weights is reused, otherwise the link's takes the next name
of its series and its layer is renamed in the view. A ref that cannot be
rebuilt is left out, with why, and the rest run. Plain refs and older
links read as before.

Paint does not travel. Share still copies the link, and a notice under the
header names each paint volume in the view, says it stays in this browser
and points to Download paint (zarr); it also names the finetuned models
rebuilt where the link opens. The page that opens a link drops the paint
layers whose volume it does not hold (the sender's own other tab keeps
them) and the layers of models it does not run, and says which and why in
the same notice.

Checked in headless Chromium: a finetuned setup_53_2d and a painted chunk;
the link opened in a fresh profile served the rebuilt -ft1 beside its base
and left the paint layer out with the notice; in a second tab of the
sender's browser it kept the paint and reused the model.
…es with the form's values, without the confirm

Retraining after more paint meant leaving the viewer for the Finetune tab
and its Restart Training button. Shift+R, pressed over the viewer or on
the dashboard page outside a field, now does the button's work: the job on
the Training Status card restarted with the Training form's current
values, only while the card offers Restart (can_restart), without the
button's confirm. Otherwise it says why and does nothing; it never starts
a job, and a second press while a restart is on its way is ignored. The
button and the key share restartWithForm in job-monitor.js.

Server mode: the press is Neuroglancer's, in the Python process
(routes/finetune/restart_key.py, bound once per viewer as Shift+G is, from
the jobs and status routes). It is counted against the job the card shows,
the status answer carries the count (restart_requests), and the page,
seeing it go up on its next poll (every 3 s), restarts with the form's
values; the status bar says what happened, or that nothing did within 15 s.
Browser mode: the page binds the key (ctx.viewer.bindKey) and tells the
dashboard at once (cellmap-flow:restart-finetune). Neuroglancer gives
every Shift+letter to a layer tool; no layer here has an R tool.
setLayers takes both: the layer handed over (replacing) and the label applyState logs should Neuroglancer throw.
…, which is removed from the browser

Each Restart made a new model (-ft2, -ft3, ...) and kept every earlier
iteration in the Models tab and in IndexedDB, 3.9 to 43 MB each. The
restart's model already took the previous one's place in serving and in
the viewer; now, once it is served, the previous one is removed from the
catalog too, record and bytes, so a job keeps its latest model only, as
the server serves only a job's latest iteration. Only the job's own model
goes (servedEntryId, set by this job alone), never its base, and a model
that cannot be removed is logged and stays. A new Start Finetuning still
makes a new model beside the others.
…st, in the path field's dropdown

The field had only the browser's own form history, which shows a few
entries, and only those that start with what is typed. connect.js now
keeps each dataset that opened (after /api/set-data answers, before the
reload) at the front of a list of up to 20 in this browser's
localStorage, and fills the field's datalist from it; the field's own
autocomplete is off so the two do not mix. A folder picked on this
machine (the browser mode's local/<key>/ URLs) is left out, since it
opens through Browse and its permission prompt. Storage that cannot be
read or written leaves the field as it was. Both modes, one template.

web/test/e2e/recent-datasets.spec.ts connects to a public dataset and
checks a new tab's panel offers it first.
…hole volume, saved as OME-Zarr in a folder on the machine

A browser-only Run tab. Tick the served models (all by default); draw a
box in the viewer (Draw a box adds a run_region annotation layer whose
tool is Neuroglancer's bounding box, two Ctrl+clicks at opposite corners;
a legacy annotation tool, which toolBindings cannot take), or pick Whole
volume, or type the offset and size in nm. The tab says each model's box
in voxels and chunks at the level it is served at and warns of a run of
more than 2,000 chunks. Choose folder... picks a folder with the File
System Access API (kept in IndexedDB, its write permission asked again in
the Run click after a reload). Run writes each model, one after another,
to <folder>/<model>.zarr through chunkmirage's Engine.materialize (the
layer's own view at its one level: what the viewer shows, postprocessing
applied), as zarr v2 with OME-Zarr 0.4 metadata, gzip or none
(Compression: None for Fiji, which reads no gzip), empty chunks left out,
with progress, time left and Cancel; the viewer's own chunks go first.
A <model>.zarr already there is replaced only after a confirm. The group
attributes record the model, variant, chain, dataset, region and time
under cellmap_flow. Show the saved predictions in the viewer opens each
zarr as a "<model> (saved)" layer read back through the service worker's
local/ route. Without File System Access (Firefox, Safari) a run of up to
256 MB is offered as a zip.

Nothing of the walk or the zarr writing is here: it is the engine's.
web/chunkmirage.lock still names 9eaf308, which has no materialize; it is
bumped once chunkmirage has the method.

Checked in headless Chromium: the box drawn with two Ctrl+clicks fills
the fields; a 4-chunk box of setup_53_2d written to a folder (the
origin's private file system standing in for the picker) in 15 s; the
result opened by zarr-python 2.18 (scale 16 nm, translation 4 nm, the
level's full shape, values in [0, 1], zero outside the box, provenance
in the attributes) and by the viewer as the saved layer.
…ls finetuned in the browser served with ONNX Runtime, and Export Config's YAML runs as it is

`type: onnx` (cellmap_flow/models/configs/onnx.py) serves an .onnx file
(`path:`) or a cellmap/onnx-web-models folder (`folder:`, with repo,
revision and file or variant), feeding the shapes the browser feeds:
float32 [1,1,z,y,x], or float16 when the graph's input is, one slice per
call for the 2D students. Its geometry, channels, output kind and input
normalization come from a cellmap-flow manifest: the Hub's for a folder,
and for a file the one embedded in its metadata_props
(cellmap_flow.manifest), which the browser now writes into the .onnx
Download finetuned model saves, so the file alone is enough; a file
without one takes the geometry from the YAML. The manifest's input_norm
is offered as the dashboard's suggested normalization (the model-advice
hook's declared input norm). Not finetunable: LoRA cannot train through an
ONNX Runtime session.

It runs in a new pixi environment `onnx` (onnxruntime-gpu, CUDA 13, on
Linux; onnxruntime on the Mac), solved on its own so the default
environment's packages do not move; the lock adds that environment and
changes no other. A relative `path:` is read against the YAML's folder.
Add a model and `cellmap_flow add` resolve an .onnx path, `onnx:<ref>`,
org/repo/folder, a Hub URL into it, and a bare onnx-web-models folder.
The browser's Export Config writes `type: onnx` entries, the Hub models by
the file the page served and finetuned ones by `path: <name>.onnx`, the
downloaded file, so `cellmap_flow yaml` runs the export after adding a
charge group and queue.

Checked on CPU against real Hub exports (setup_53_2d fp16 and fp32,
setup_54): the served geometry was right and the forward matched a direct
ONNX Runtime session. The CUDA provider is unverified (this workstation's
driver is CUDA 12.2), and the onnx environment was solved, not installed.
…y, z

Both viewers name their dimensions z, y, x, and Neuroglancer's "xy" panel
shows the first two display dimensions, so it showed z across and y down.
They now set displayDimensions to x, y, z (bootstrap.display_dimensions,
layers.ts displayDimensionsFor), so that panel is the x–y plane, x across and
y down, looking along z; positions and every route's z, y, x order are
unchanged. A 2D model computes one z-slice at a time, so a z–y panel asked
for a block per z position on screen: on jrc_hela-2 the browser's panel
asked for 997 of setup_53_2d's chunks in 25 s, and now asks for 12.

AI-assisted annotation lays its image out as the viewer's panel draws the
plane. That layout now follows the viewer's display dimensions
(geometry.screen_axes / screenAxes), which each plan keeps; a plan staged
before has none and is laid out z, y, x as before.
…ls as its model goes into the browser's viewer
…etune, as Restart Training does

# Conflicts:
#	docs/source/browser.rst
… an AI-annotate key as a password

# Conflicts:
#	CHANGELOG.md
…browser, and says what it leaves behind

# Conflicts:
#	web/src/main.ts
…Export Config's YAML runs as it is

# Conflicts:
#	web/README.md
#	web/src/backend/routes/finetune.ts
… whole volume, saved as OME-Zarr in a folder on the machine

# Conflicts:
#	web/src/browser.css
#	web/src/main.ts
…wers

# Conflicts:
#	CHANGELOG.md
#	web/src/viewer/index.ts
#	web/test/unit/viewer-layers.test.ts
#	web/test/unit/viewer-state.test.ts
… be dropped on it

Typing in the region's fields redraws the run_region layer's box to match
once typing pauses (400 ms; at once on change), as a box drawn fills the
fields: withBox writes the region into the layer's own coordinate space
(coordsFromNm, pointerNm backwards) as its one box, and a box read back a
little off (Neuroglancer keeps corners as 32-bit floats) is not taken as a
new one (sameRegion).

A folder dropped on the Output folder block is used as the picker's would
be, the browser asking to allow writing to it at Run. The note says Chrome
refuses Downloads and the home folder themselves ("it contains system
files"): a folder made inside one works.
…ize, a folder can be dropped

# Conflicts:
#	web/test/unit/viewer-state.test.ts
…play dimensions back after restoreState

Neuroglancer restores a state's displayDimensions against its coordinate
space as it is at that moment, and restoreState empties that space while the
layers come back, so the display dimensions fell back to z, y, x on every
setLayers (Draw a box, Submit, a finetune's model): the main panel turned
back to z–y. The controller now puts them back once the space names them
again (keepDisplayDimensions, a frame or so later in Chromium). The server
mode's viewer keeps them through a layer change from Python (checked).
… in the browser

# Conflicts:
#	web/src/viewer/index.ts
…er/keys.py

Shift+R (restart_key) had copied Shift+G's (ai_annotate) status-bar
message and its bind-once-per-viewer, and the Review tab's t key bound its
action a third way. viewer.keys now has say(), say_for() (restart_key's
show-then-clear) and bind_key(), which remembers the viewers it bound
itself, weakly, so the session's restart_key["bound_for"] and
ai_annotate["binding_registered_for"] are gone. A new viewer is still bound
the first time a caller asks after it. Each feature keeps its own status
key, so their messages do not overwrite each other's.
…'s shader

Submit's unticked models, the Loaded models list's ✕ and a launch withdrawn
while it was starting each killed, dropped the layer and filtered the
session's jobs in a copy of their own. _stop_jobs does it once: it kills
unless a finetune job owns the server, drops the layers whether or not
there is a viewer, and forgets the jobs by identity. The withdrawn launch
leaves the layers alone: the one of its name is a later launch's.

Fixed: the ✕ deleted the layer but kept the session's shader and shader
controls for its name, which /api/viewer/remove-layer drops, so a later
layer of the name came back with them. Both now go through
Session.forget_layer, Submit's unticked models' too.
…ecords

training_job_of guessed the finetune job from the session's jobs: the
newest whose latest model was the one served, else one finetuned from its
base in the same LSF job, which a local run (job id "local") cannot match.
add_finetuned_layer, the one place such a server's job is made, holds the
FinetuneJob, so it now records its id as training_job_id, and
training_job_of asks the job manager for that id.
…ellpose panel's models once

The Loaded models list (_loaded_entry) re-wrote index_page's test for a
Cellpose model the panel lists as its own, and the hf_id and zoo_id it gives
each model repeated how the index page found the Hugging Face and zoo rows
to tick. is_panel_cellpose() is that test, used by both, and the page now
ticks the rows of loaded_models()' hf_id and zoo_id. Those include models
still starting, which the page left unticked until their job came up.
launch.py's two {name: config} and {name: catalog group} maps are
configs_by_name() and _catalog_groups().
…t when Submit stops it

_stop_jobs forgot the session's shader and shader controls for every layer
it dropped, so a model unticked and ticked again, or a Cellpose model
Submit restarted with a new voxel size or slice linking, was drawn again
without what the user had set on it. Only remove_model forgets them now.
…r helper

The comments and docstrings added since 8badcf1 say only what the code
and names do not: no lists of callers, no restated lines, no history, and
each point made once. viewer.keys' timer is now the one restart_key's
pending press uses too, so its copy goes and the tests replace one timer.
_stop_jobs and bind_key are shorter: bind_key drops a lock its race note
already says it does not need.
…ounces and the advice and finetune picker hear

The Models tab announces on Submit, Run and ✕, as the Finetune tab already
did for its job's model, and asks its own list again on hearing it; the
onModelsSubmitted callback is gone. The model-advice banner and the
finetune model picker listen, the picker quietly and keeping its pick
while it asks; its Refresh stays for models served from elsewhere.
lib/models-changed.js becomes lib/events.js, which also names Shift+R's
RESTART_EVENT (the string web/src sends, unchanged).
…ist and Job Logs both

The list's poll while a model starts and Job Logs' watch after a Submit
were two 5 s polls over the same minutes. One poll now asks for each while
it has something starting, and stops once neither has; the watch's
requests share the one-at-a-time guard Follow and the button use. It waits
while the page is hidden, as the list's did. The Cellpose panel's
"N running" is written by the Loaded models list alone, on load as after a
Submit or a remove, not also from the page's data.
…ze options once

restartSummary's three hand-written strings, which repeated the learning
rate, epochs, smoothing and balance rows, are now a [label, key, fallback]
table per mode (server, the browser's head, its whole network); the one
change in what it says is that augment's value is aligned like the rest.
The Finetune tab writes the Batch Size options in one loop with the default
picked by mode, and the browser's Tile Size and Voxels per Step columns
share one browser-only block. Both pages render as before.
Six places read and wrote localStorage each with its own try/catch: the
recent datasets, the patch tools' open sections, Label it's seed
settings, the training form, the Load Crops path and the Resume root.
lib/stored.js's readJSON and writeJSON (and readText and writeText, for
the two keys that hold a plain string) do it once: a read that fails gives
the fallback, and a write that fails keeps nothing. The keys, what they
hold and the fallbacks are as they were; the training form no longer
throws where the storage is blocked.
…s starting

Comments that listed callers, restated the code or repeated a point made
elsewhere are gone. The Models tab's poll for what is starting runs for
the page's life and asks only while the Loaded models list or a Submit's
watch has something starting, so it needs no start or stop; the watch uses
refreshJobLogs and its one-at-a-time guard. lib/stored.js and lib/events.js
are shorter, and the picker's quiet loads key off lastLoadedCount.
…onfig base

The onnx type had its own copies of the bioimage type's _sizes, _per_axis
and _as_given, and of the fly type's channel split; they are now one each
in configs/base.py, with the onnx type's bounds (one or three sizes, each
at least 1) as keyword arguments. The fly type's _voxel_size was base's
too. ModelConfig.declared_input_norm() returns None, so the model advice
calls it on any model rather than guarding the onnx type's.
_onnx_checked repeated _hf_checked's offline, no-such-repo and
Hub-unreachable handling and its notes; both now take the files, or the
note to add, from _hub_files. The onnx type's default repo is resolve.py's
ONNX_REPO, which it imports, rather than a second constant of its own.
…er displays in one place

The view command built its own viewer (the defaults, z, y, x at 8 nm, the
raw layer), so the x, y, z display dimensions had to be added there as well
as in new_viewer; it now calls new_viewer with the same scales, and its
viewer's state is unchanged. viewer/ holds DISPLAY_DIMENSIONS and
display_dimensions_for_view, which bootstrap and the AI annotation's
geometry import. The geometry normalizes display dimensions where they
come in (from a viewer, to a new plan and from a saved one), and lays a
plane out with them as given.
The blockwise processor checked {offset, shape} by hand, and the dashboard's
BoundingBox described the same box but defaulted the shape to 1, 1, 1 and
took sides of 0. BoundingBox is now blockwise/bounding_box.py's, which
imports no Flask: the shape is required, every side above 0. The processor
checks a task's boxes with it, strictly, so a "8" is still refused, and
still raises ConfigError naming the box. output_arrays, kept only for the
tests, is gone; they read outputs[0].arrays.
The billing test of cellmap_flow view replaced viewer.raw.get_raw_layer,
which the command called itself before it built its viewer with
new_viewer; new_viewer gets it through viewer.layers.raw_layer.
…peless box

BoundingBox is again what /api/bbx-generator took: a missing shape is
1, 1, 1 and a flat box is drawn. TaskBoundingBox, a subclass that requires
a shape with every side above 0, is what _check_bounding_boxes checks a
task YAML's boxes against, strictly.
…an its voxels

Accept shrank the model's mask to the write box's voxels by nearest
neighbour and only then cut it into 4-connected objects. On voxels
coarser than the image's pixels (8 nm pixels into 16 nm voxels) each
voxel takes every second pixel, so the one-pixel black line the prompt
asks for between touching objects could fall between the pixels taken,
and the two objects were written as one id.

mask_to_write_shape now labels the mask at the image's resolution and
shrinks the object ids, numbered again in the box's raster order;
labels_for_box takes those objects instead of a bool mask. The staging
keeps the objects, and the preview's overlay and the mask fraction read
them as > 0. The browser's maskToWriteShape and labelsForBox do the
same, and the parity fixture gains a one-pixel line on coarser voxels.
Where the image is as fine as the voxels or coarser, as for every
browser-catalog model, the labels are what they were.
# Conflicts:
#	CHANGELOG.md
#	cellmap_flow/ai_annotate/pipeline.py
#	cellmap_flow/ai_annotate/staging.py
#	web/src/ai-annotate/plane.ts
#	web/src/backend/ai-annotate-job.ts
@davidackerman davidackerman changed the title Batch: PRs #126–#140 in one merge Batch: PRs #126–#141 in one merge Oct 17, 2026
…ensions

#136 set the display dimensions to x, y, z over a z, y, x coordinate space,
so that the "xy" panel showed the XY plane. The painting fork's brush and
flood fill read a panel's normal, which the panel gives in display order, as
if it were in the coordinate space's order: the XY panel's z normal was read
as x, and every stamp stood on edge across the slice shown, most of it in
the slices beside. Both viewers now order the coordinate space itself x, y, z
(viewer.VIEWER_AXES; bootstrap.viewer_space, layers.ts dimensionsOf), as
Neuroglancer's own viewers do, and set no display dimensions: the two orders
are the same, and the "xy" panel is the XY plane looking along z.

- viewer_position_and_scales returns z, y, x by name, so a good region is
  centred where the viewer looks; AI-annotate's run reads the state's
  position by name (_zyx_nm). Every route still takes and gives z, y, x.
- The browser holds Neuroglancer's coordinate space through each live
  restoreState (layerSpecification.coordinateSpaceCombiner.retain). The
  restore drops every layer first, and with none bound Neuroglancer emptied
  the space and took it again from the new layers, in their z, y, x order
  and at whichever voxel size loaded first, keeping the position's numbers.
  That is why #136 needed keepDisplayDimensions, which goes.

Checked headless on jrc_hela-2: a radius-6 brush stroke in the browser
writes 1,720 voxels, all on the slice shown, and the order stays x, y, z
after the paint layer is added. A server-mode viewer keeps x, y, z and its
centre through a layer added from Python.
…ilds 3D graphs

Since 2026-10-17 (cellmap/onnx-web-models 3331842ec2) the 3D models'
manifests declare whole-network training too, with NDHWC training graphs,
PyTorch-named params and structured weight maps. finetune/full builds the
2D students' NHWC graphs only, so the Finetune tab offered a 3D model's
whole network and its submit then failed. finetuneModes now offers it for a
model that reads one slice; a 3D model keeps the last layer, and asking for
its whole network says the browser does not train a 3D model's yet.
@davidackerman
davidackerman marked this pull request as ready for review October 19, 2026 14:39
@davidackerman
davidackerman merged commit 512fc62 into main Oct 19, 2026
7 checks passed
@davidackerman
davidackerman deleted the browser-batch branch October 19, 2026 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant