Repository navigation
Batch: PRs #126–#141 in one merge - #137
Merged
Merged
Conversation
…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.
…SciComp/chunkmirage#39), which Run on a region calls
…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
…user set on it, in both viewers
…ser replaces the job's previous model
…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
…onnected to before
…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
…adds its layer, then selects it)
…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.
…mit in new_viewer itself
…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
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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):
restoreStatewith the mouse over it).Engine.materializeandmaterializePlan).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:
main.ts,routes/finetune.ts, two test files).viewer/index.ts.main.ts's boot: the Share call with its catalog, then the Run tab.browser.rstand paragraphs in the web README.Checks on #126–#140 merged, before the cleanup:
npm run checkin web/ passes: typecheck, 1548 tests, build.test_importing_leaves_the_process_alone[blockwise-logging], fails here the same way it does on main in that environment.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.
launch._stop_jobs).viewer/keys.py).lib/events.js).lib/stored.js).cellmap_flow viewbuilds its viewer withnew_viewer.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:
The checks on this head:
npm run checkin web/ passes: typecheck, 1546 tests, build.test_importing_leaves_the_process_alone[blockwise-logging]fails as it does on main in that environment.