Skip to content

Make the paint flow visible: Apply at the top, what the ranking sets, and a volume before a model - #155

Merged
davidackerman merged 8 commits into
mainfrom
ux-paint-flow
Oct 22, 2026
Merged

davidackerman merged 8 commits into
mainfrom
ux-paint-flow

Conversation

@davidackerman

Copy link
Copy Markdown
Collaborator

Three things the live try after #152 showed were hidden or missing in the paint-and-finetune flow, in both modes: the chain had to be applied from a button at the bottom of a list, what the ranking installs was invisible, and a paint volume could not exist before a model. Each is written once (shared JS and templates, portable Python) and served by both the Flask dashboard and the browser app.

Nothing applies the Input or Postprocess chain by itself. The one exception is part of an explicit model start, below.

Against main: 53 files, +2,092 −330.

Apply, visible (ux-apply)

  • Submit All at the bottom of the Input and Postprocess lists is now Apply, in a bar that stays at the top of the tab with its title while the list scrolls (one shared partial, _apply_bar.html). It lights up with "N changes not applied" while the lists differ from the chain the backend runs, counting the other tab's edits too ("1 change not applied, in Postprocess"), and goes quiet once applied. Apply, in either tab, sends both lists; Enter in a parameter field still applies. The count is computed in shared/op-chain.js against the live chain.
  • A model's start sets the input normalization it declares, when the chain has none. Server: routes/pipeline.set_declared_input_norm, called from Submit (services/launch.update_run_models) and Add (routes/model_resolve.add_model); browser: serving.ts setDeclaredInputNorm from Submit and Add in routes/models.ts. Both go through the one chain setter on their side, before the layer is drawn, so the layer is drawn through the normalizers. A chain that already has input normalizers is left alone; a model that declares nothing changes nothing. Afterwards nothing changes without Apply.
  • GET /api/models/loaded now also answers the chain (pipeline) and the model whose declaration its input part is (input_norm_from). The page reads it on load, after every model change, and when the ranking changes the chain, so the Input list shows a normalization the backend set, ticked and as applied, with a note naming the model. This also fixes an older gap: the window Use this model stretched never showed in the Input tab, so an Apply there put the old one back.

What the ranking sets (ux-rank)

  • The rank card is closed by default and remembers its state in this browser.
  • Every ranking row shows the threshold (as a probability, 0.5 unless a better cut-off was found) and the input window the model read (its declared range, or the stretched 1st–99th percentiles when the stretch won). Both modes build the row in one portable function, portable/rank.score_row, which also makes the keep-the-stretch decision the two routes used to make separately.
  • After Use this model, a "set by ranking" badge marks Seed from Prediction's threshold until the field holds a different value, and a line in the Finetune tab says what was set. Use this model now always sets the threshold, 0.5 included; it used to leave a hand-typed value in place when the best cut-off was the model's own.
  • Fit to Paint, beside Seed from Prediction: scores the model that seeds (the Seed box's prediction source) with the existing POST /api/finetune/rank/score and installs its window and threshold without a new volume, through a new light route POST /api/finetune/rank/window that applies rank.windowed and the existing chain setter in each mode. rank/use installs its window through the same helper. One scoring function, no new maths.
  • docs/finetuning.md Which model to start from explains the cut-off and the stretch in plain words, mirroring docs/source/browser.rst, and both describe Fit to Paint.

A volume before a model (ux-volume)

  • With no model loaded, New Volume shows a Voxel size (nm) field: the raw's levels to pick from, any size typed (one number, or z, y, x), or blank for the level the viewer shows when you click (the server reads the viewer's zoom; the browser decodes it from the viewer URL; the finest level when neither knows). The volume lies on the raw's own grid at that size, in 64-voxel chunks of uint8 labels, with no model recorded (model_name null) and no training manifest. Planned once in portable/volume.plan_paint_volume and shown_level; CreateVolume takes an optional voxel_size and its model becomes optional. A voxel size together with a model is refused.
  • Which model to start from ranks the models on its paint (only the running ones start ticked; none is marked as the volume's), scoring on the volume's own grid, and Use this model copies the paint onto the chosen model's grid as before.
  • Start Finetuning on such a volume is refused in both modes with one sentence pointing at Which model to start from → Use this model (portable/volume.training_refusal, paint_first in the answer), instead of the trainer's misleading "no manifest" message.
  • Seed from Prediction and Fit to Paint work on it with the model picked under Prediction from, or the only one served; the seed used to refuse a volume that named no model, in both modes.
  • The raw's levels come with the existing GET /api/finetune/models answer as raw_voxel_sizes (cached per dataset on the server, since the page polls it). Docs in docs/finetuning.md and docs/source/browser.rst.

The chunk shape is always 64³ rather than the level's own chunk shape: the browser's dataset info carries no chunk shapes, and the two modes must plan the same volume.

Behaviour changes to review

  1. Use this model always sets the seed threshold (was: only when the cut-off differed from 0).
  2. Starting a model that declares normalizers into a chain with none sets them for every model running (the chain is shared), then nothing moves without Apply.
  3. The bottom Submit All buttons are gone; the Apply bar is the one control per tab.
  4. CreateVolume without a model is no longer a 400 "No models loaded"; it plans a paint-first volume. A model's volume is planned as before.
  5. A seed on a volume that names no model is refused only when several models are served and none is picked.

Left for later

  • The Input tab could carry the "set by ranking" mark too; today only the Seed box does.
  • The Finetune tab's "what was set" line is a record of the moment, not live state: it is not kept across reloads and does not notice later chain changes.
  • The model-advice banner's own "Apply: X → Y" buttons tick the list and let the bar light up; the shared word may confuse.
  • Comments in index_page.py, model-advice.js and pipeline-builder/state.js still say "Submit All".

Checks

On the merged branch, once: Python 1,216 passed (plus the known import-hygiene row), vitest 58 files / 524 tests, typecheck, gen:check, export_data --check and ruff (the three pre-existing findings) all clean. The Apply bar was driven in a real page by its agent (count, lighting, note, Apply, Enter, stickiness); the rest is route-level tests in both modes. Not tried live on a running dashboard yet.

Branches merged with merge commits: ux-rank, ux-apply, ux-volume. Background: the UX follow-up planned after #152 (consolidation_review/UX_FOLLOWUP_BRIEFS.md).

https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW

Every row of Which model to start from gives its threshold (the cut-off
as a probability, 0.5 at the model's own boundary) and its input window
(the normalizer's own range, or the raw stretched when that won), where
they were prose only when they differed. The row is built once, in
portable/rank.py score_row, which both routes answer as it is.

Use this model now always sets Seed from Prediction's threshold, marked
"set by ranking" beside the field until it is changed, and a line under
Label it's buttons says what was set. Fit to Paint, beside Seed from
Prediction, scores the model Prediction from names with the same score
route and sets its threshold and, when the stretch won, its window, with
no new volume: POST /api/finetune/rank/window (both modes) makes the
window the session's input normalization alone, and Use shares it.

The rank card starts closed, lists the models only while open, and stays
as it was left.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
…el's declared normalization on its start

Submit All sat at the bottom of each list, so a model started from the
Models tab saw raw voxel values until the user scrolled down and found it.
Each tab now opens with one bar (templates/_apply_bar.html): the tab's
title and Apply, which stays at the top of the dashboard column as the
list scrolls. It lights up and says how many changes are not applied while
the lists differ from the chain the backend runs (shared/op-chain.js
chainChanges, against the chain last applied or shown; in either tab, as
Apply sends both lists), and goes quiet once they are applied. Nothing
else sends the lists; Enter in a parameter field still applies.

A model started with no input normalizers in the chain sets the ones it
declares (an ONNX export's manifest), as part of its start and before its
layer is drawn, through the chain's one setter on each side:
routes/pipeline.py set_declared_input_norm (services/launch.py
update_run_models calls it), and web/src/backend/serving.ts
setDeclaredInputNorm (Submit and Add in routes/models.ts). A chain with
input normalizers is left alone, and a model that declares none changes
nothing. GET /api/models/loaded answers the chain too, and the model whose
declaration its input part is; the page reads it after every model change,
so the Input list shows what a start set, ticked, with a note naming the
model (and a chain Use this model stretched, which it did not show).

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
…int's window in the Input tab

The Models tab's Add started a model without the step Submit's start has:
with no input normalizers in the chain, the ones the model declares are set
before its layer is drawn (routes.pipeline.set_declared_input_norm); the
browser's Add already did this. And Fit to Paint, which stretches the raw's
window with no model change, now tells the page (CHAIN_CHANGED_EVENT) so the
Input tab's list shows the normalizer as it runs and a later Apply keeps it.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
…en voxel size

With no model loaded, the Finetune tab's New Volume shows a voxel-size field
offering the raw's levels (the models answer's raw_voxel_sizes) and taking any
size typed; blank is the level the viewer shows. The volume lies on the raw's
grid at that size, in 64-voxel chunks of uint8 labels, with model_name null.
The plan is portable/volume.py's (plan_paint_volume, shown_level), which the
server and the browser both run. Which model to start from ranks the models
on its paint and Use this model carries the paint onto the chosen model's
grid; a training job on the volume itself is refused in both modes with
training_refusal's one sentence (paint_first in the answer).

CreateVolume's model_name is optional and it gains voxel_size; the generated
contract is regenerated.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
A volume made before a model was chosen names no model, and Seed from
Prediction refused it in both modes even with a model picked under
Prediction from. The pick, or the only server running, now seeds it; with
several running and none picked the seed is refused, not the volume. The
rank card's status line no longer says "painted for null" on such a volume.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
@davidackerman
davidackerman marked this pull request as ready for review October 22, 2026 13:39
@davidackerman
davidackerman merged commit 5888699 into main Oct 22, 2026
7 checks passed
@davidackerman
davidackerman deleted the ux-paint-flow branch October 22, 2026 13:41
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