Skip to content

Merge #164, #163, #165 and #166 - #167

Merged
davidackerman merged 183 commits into
mainfrom
group-merge-163-166
Oct 28, 2026
Merged

davidackerman merged 183 commits into
mainfrom
group-merge-163-166

Conversation

@davidackerman

@davidackerman davidackerman commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Merges four PRs into main in one go, as #161 did: each under its own "Merge pull request #N" commit, in order, with the conflicts between them resolved here.

  1. Redraw the raw only for a new input normalization, never build a model to list it, and one Processing tab #164 dashboard fixes (Apply, polling, Stop Early, the dashboard log)
  2. Publish finetuned models to Hugging Face from the browser or the server, and open anyone's #163 publishing to Hugging Face, and opening anyone's published model
  3. Finetune one channel, rank and start models as the Models tab does, tick GPU queues, ease the learning rate #165 the Finetune tab: one channel, model choice and ranking, GPU queues, learning-rate schedule
  4. Read a model's output activation from its network, and say what each listed model is #166 a model's activation read from its network, and what each listed model is

Conflicts resolved (each PR's description says the same):

The final tree is the one tested as a whole for the last two days (the local try-both branch, without its local thread-dump helper). Each merge commit typechecks.

Before merging: retarget #165 and #166 to main (each PR's Edit beside its title), so GitHub marks them merged with this one. Merge with a merge commit, not a squash.

Checks: Python 1287 passed, vitest 581, typecheck, schema and export checks, render.

https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW

FinetunedRecord.published: the Hugging Face repository and folder a
model finetuned in the browser was uploaded to, so the publish code can
record it and a share link can name the model there.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
The browser's catalog listed cellmap/onnx-web-models alone. A ref of
another public model repo laid out the same way (a folder holding a
manifest.json and the .onnx files it names, as a model published from
the app is) now resolves on the spot: <owner>/<repo>/<folder>, the repo
alone when it holds one model, hf: before either, or the huggingface.co
URL, as the server's resolver reads them. The repo's models join the
list after cellmap's, and the repo is remembered in localStorage with a
listing cached for a day, so the next visit lists it too; a repo the Hub
no longer shows (401 or 404) is forgotten. A repo that does not exist or
is private, a folder it lacks and a repo of several models named without
its folder are refused with the reason, which Add a model and Submit
show; only the main revision is read.

A published model's manifest names it (published.name); every name in
the list stays unique, the built-in catalog's and the finetuned models'
kept and another repo's model whose name is taken followed by _2.
Serving re-tunes an entry's variant with withVariant, which keeps that
name. A manifest's variant source and published block are kept, so a
published finetune can be finetuned again on its base's file.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
A model sent as a file had no way into the browser app: Add a model's
field takes a path, which means nothing to a page. Open an .onnx file…
(the Models tab, browser only) picks one: an .onnx the app downloaded,
which carries its manifest in metadata_props, or any .onnx with its
manifest.json beside it. The model is kept in this browser as a
finetuned one is (registry and byte cache, FinetunedRecord.from_file),
so it survives a reload and is served, finetuned and published the same
way, named after its published name or its file and kept once however
often it is opened; then it goes through Add a model's own Resolve and
Run. readMetadata moves from the tests into onnx-proto.ts beside
withMetadata.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
Share named every Hugging Face model by its folder, which drops the
repo: a model of another repo opened elsewhere as nothing, or as a
built-in folder of the same name. It is now <owner>/<repo>/<folder>,
and a folder of cellmap/onnx-web-models stays as it was. A finetuned
model published from this browser is named where it was published
(FinetunedRecord.published), whole-network finetunes included, which
links could not carry before; a last-layer finetune is rebuilt on the
first model up its lineage a link can name, its Hugging Face base in
any repo or an ancestor published from here. A model opened from a file
stays out of links until it is published, and Share says so.

Opening such a link resolves the refs through the catalog, so another
repo is looked up and listed there, a model-only link included (served
once a volume is opened); a published ref of a model kept here is that
model, and a Hugging Face model listed under another name has its
layer renamed in the view. Links made before open as they did.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
web/README.md gets a section on Add a model's Hugging Face refs and Open
an .onnx file…, and its link and Share sections say how another repo's
model, a published finetune and a model-only link are named;
docs/source/browser.rst says the same for users; a CHANGELOG bullet
under Added.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
The Hugging Face client the browser's Publish to Hugging Face uses: its
sign-in (oauthLoginUrl, oauthHandleRedirect), whoAmI, createRepo and
uploadFilesWithProgress. 2.17.6 is the newest release at least two weeks
old (2026-10-08); its dependencies are pinned as of then in the lock.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
python-data.json carries cellmap_flow.__version__, which a model published
to Hugging Face from the browser names in its manifest ("cellmap-flow
<version> (browser)"). The freshness test asks for a new export when the
version changes.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
The store answers cross-origin GETs by URL alone, so a request with an
Authorization header (Hugging Face's who-am-I, an upload's short-lived
write token) would be answered with another token's answer, or with a
write token that has expired. The worker now leaves such requests to the
network, as it leaves anything but a GET.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
The routes behind the Training Status card's Publish to Hugging Face,
answered in the page (backend/routes/publish.ts), with src/publish:

- /api/huggingface/credentials: the user's token, pasted or from the Sign
  in popup, checked with whoAmI and kept in the page's memory only; GET
  says who is signed in and gives the popup's URL when there is an OAuth
  app, POST {token} or {forget: true}.
- /api/finetune/model/<name>/publish: POST starts a publish to <user>/<model>
  or a repository typed (the user's or one of their organizations'), GET
  follows it, and answers where a model was published before. Done, it
  gives the repository's page, the ref <owner>/<repo>/<folder>, and the
  links that open it in the app, the model alone and on the open dataset.
- One commit (model.ts, hub.ts): README.md, a model card linking the public
  app; <folder>/manifest.json, the record's manifest with a "published"
  block (the dataset only when asked); <folder>/<file>.onnx. The repository
  is made when it is not there (public unless asked), and a publish to one
  that is is a new commit. Errors say what to do: not signed in, a token
  refused or not allowed to write, an owner that is not the user, the Hub
  out of reach, the file whose upload failed.
- Sign in (oauth.ts, oauth.html): OAuth with PKCE in a popup, scopes openid
  profile contribute-repos, its verifier in the popup's sessionStorage, the
  token handed to the tab over a BroadcastChannel. The client id is empty
  until an OAuth app is registered, and Sign in is not offered meanwhile.

The record keeps where the model went (FinetunedRecord.published). The
routes are in cellmap_flow/schema.py's table as the browser's, their
bodies FinetunePublish and HuggingFaceCredentials; the tests run the
library against a fake Hub.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
…X export

The browser names a model of any Hugging Face repo by its folder, and
accepts hf: before it; the server took hf: as a cellmap-models export
and refused three segments ("not a Hugging Face repo id"). A folder of a
repo can only be an ONNX web export, so hf: with one goes where onnx:
and the bare form already went. hf:<owner>/<repo> is unchanged.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
Beside Download finetuned model on the Training Status card, for the same
model: a button that opens a compact panel (finetune/publish.js) with who
is signed in to Hugging Face, Sign in (a popup, opened in the click so no
blocker stops it, when the app has an OAuth app) or a pasted access token
(masked by CSS and kept from password managers as the AI panel's
credentials are, emptied once the route has taken it), the repository
(by default <user>/<model>), Private, and whether the card names the
dataset. Publish starts the upload and follows its progress; the result
is the Hugging Face page, the model's ref and the two app links, each
with Copy. A model published before shows where it went when the panel
opens. The popup's answer reaches the panel as HUGGINGFACE_SIGN_IN_EVENT.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
ModelManifest.published is contracts.ts's PublishedBlock, declared as the
load side declares it, in place of publish/model.ts's own. A model opened
from a file has no base id: its finetuned_from is what its own manifest's
published block says, or none, and a model trained elsewhere (a link's, a
file's) says so in its card rather than claiming paint in this browser.
The test holds the uploaded variant to the entry's, source and all.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
web/README.md: what is uploaded, the routes, where the token lives, the
CORS checked, how a user makes a write token, and how to register the
OAuth app (name, homepage, scopes, redirect URLs, no secret) and where its
client id goes. docs/source/browser.rst: the panel as the user sees it,
public unless Private is ticked. ARCHITECTURE.md: src/publish. CHANGELOG.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
The browser opens models of any Hugging Face repo and from an .onnx file,
and share links name them by their repo; the server takes
hf:<owner>/<repo>/<folder> too.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
Publish to Hugging Face for a model finetuned in the browser: sign in or
paste a token, then README, manifest and .onnx go to the user's own repo,
with a link to try it in the browser.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
It went in under 0.3.0's Added, the first Added heading on its branch,
where Unreleased had none yet.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
…s link waits for

- The result panel lists each link under its name and Copy, wrapping, in
  place of fields cut off in the sidebar.
- After the commit, the repository is read back: done means Hugging Face
  shows every file at the size sent, and the page says whether the
  repository is public or private (a private one's links open it for no
  one). The two checkboxes are kept from the browser's form restore.
- A link with models and no dataset says the models run on the volume
  you open, rather than showing nothing.
- The dataset checkbox is worded as what it adds to the card.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
…waits for a volume

- A model kept here that was published stands for its copy on the Hub:
  its ref resolves to it, and the Hub's copy is not listed beside it as
  `<name>_2`.
- In the browser mode, a loaded model with no dataset open is "waiting"
  (for a volume) rather than "starting", and the list keeps asking so it
  turns to running once one opens.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
…and sign in with no registered app

- cellmap_flow/portable/publish.py makes the model card, the manifest's
  "published" block, the app links and the file list for both modes; the
  browser's publish route calls it through the Pyodide bridge
  (publish_calls.py), and the TypeScript copy in publish/model.ts is gone.
  It also takes a server's export: several variants, and LoRA merged in.
- Sign in with Hugging Face needs no OAuth app on anyone's account: the
  client id is the URL of a client metadata document (CIMD) the app
  serves. The client id, the scopes and the link parameter names have one
  source, in Python, exported to the browser.
- The code-for-token exchange sends the client id, which @huggingface/hub's
  oauthHandleRedirect leaves out and a public client must send.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
web/.well-known/oauth-cimd is the document at the URL the sign-in uses as
its client id; the Pages workflow copies it beside the app and fails
without it, and a test holds it to the client id and the popup's page.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
… panel

- The server dashboard answers the credentials and publish routes as the
  browser's backend does (cellmap_flow/dashboard/routes/finetune/publish.py),
  uploading with huggingface_hub: the export job's files, then the card and
  manifest from cellmap_flow.portable.publish, in one commit, read back.
- Signing in on the server: a code entered at huggingface.co (OAuth device
  code, the same client id as the browser's popup), the token huggingface_hub
  already has (HF_TOKEN or `hf auth login`), or a pasted one, which
  Remember on this server saves where `hf auth login` does. Every token is
  redacted from the log the dashboard streams.
- The Publish panel shows in both modes, and publish.js follows what the
  credentials answer offers rather than the mode.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
The repository-name check, the owner check, and what a publish says when
Hugging Face refuses its token or the repository are in
cellmap_flow/portable/publish.py (repo_refusal, hub_refusal), which the
server's route calls and the browser's calls through Pyodide; the fixed
sentences (sign in first, a token refused, one that can only read) are
exported to the browser with the client id. Only the out-of-reach and the
Hub's-own-reason wording stays with each mode's HTTP client.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
- cellmap_flow/finetune/export_web.py (`cellmap_flow finetune export-web`)
  builds the finetune's base network as training does, folds the LoRA
  adapter or the full weights in (export_merged.apply_finetune), and exports
  it with cellmap-models' onnx_web at the served block in fp32, fp16 and
  int8 weights, each checked against PyTorch on a raw block of the training
  data normalized as the trainer normalizes it, into the catalog's manifest.
  start_export/export_status run it through the finetune's own launch and
  environment (submit.module_command, taken out of build_command).
- apply_finetune takes the module load_trainable_model builds; unflattened
  transposed convolutions can be rebuilt as real ConvTranspose layers
  (lora_wrapper), which the WebGPU rewrite needs.
- cellmap-models comes from Marwan's onnx-web-export-and-pruning branch, in
  the finetune feature only, until it is released.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
All four trainers stepped at the form's rate throughout. A run now
starts at it and eases down along half a cosine to LEARNING_RATE_FLOOR
(0.1, finetune/options.py, exported to the browser in python-data.json)
of it at its last step: the "big steps, then small steps" a LICONN head
trained at 1e-2 and restarted at a smaller rate got by hand.

The server's AdamW has a LambdaLR (trainer/loop.py learning_rate_factor),
stepped after each optimizer step, over epochs x optimizer steps an
epoch; a step past that count (an OOM's halved batch) stays at the floor,
and a restart's new trainer starts it again. The start line and the
TensorBoard config say the schedule, and train/lr is the rate each step
took. The browser's last layer steps its own Adam at the schedule's rate
(schedule.ts learningRateAt); its whole network in 2D steps an Adam of
its own (full/adam.ts, TF.js's arithmetic), since TF.js's optimizer keeps
the rate it was made with, and in 3D net3d.ts's step takes each step's
rate. Each browser run's start says the schedule too.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
Each Learning Rate field (the server's, the browser's last layer's and
its whole network's) was a select of four to seven rates. It is a field
that takes any number above 0 (1e-4, 0.0003), with the presets in a
select beside it that types the one picked in and shows the one typed.
The typed rate is saved, restored and sent as before; an empty, zero,
negative or non-numeric one is refused in the form, saying why. The
titles say the rate eases down to a tenth by the run's end.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
…greyed out

Closing the review keeps its result waiting, and Annotate stayed disabled
until it was accepted or rejected, with no word of why.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
Every run eased its rate down to a tenth of the form's, with no way back
to the one rate a run used to keep. A run's learning_rate_schedule, a
TrainingOptions setting like the others, is cosine (the default, the
ease down) or constant (every step at the form's rate, as before the
schedule): the submit and restart bodies take it, the job's command line
says it (--learning-rate-schedule), a restart may change it, and the
browser's routes read it into FinetuneOptions.learningRateSchedule. The
server's LambdaLR and the browser's three trainers (learningRateAt) take
it, and each run's start line says which ("1e-4, constant over 40
steps").

The training form has an "Ease down to a tenth over the run" box under
each Learning Rate (the server's, the browser's last layer's and its
whole network's), ticked as the page starts, saved and restored with the
form and sent with submit and restart as the rate's schedule; Restart's
confirm shows it beside the rate.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
Seed from Prediction, All Background and Split Objects acted on a box
centred where the viewer looks, one output patch or Box's size. With
Field of view, beside Box, they cover what the panel shows instead: its
width and height at the zoom on screen, one voxel deep at the slice
shown unless Box gives a depth along the axis the panel looks along. One
slice deep, a wall painted in that slice is enough to split an object.

The panel's plane is the AI annotation's (in 4panel the top-left xy
panel), its zoom the one view_of reads. Its size is the viewer frame's,
which only the page knows (Neuroglancer's own page is of another origin
on the server) and sends as viewer_px, less Neuroglancer's top row and
the side panels the state shows open, and the panel's share of that in
the layout. A view of more than 2048 x 2048 voxels is refused, saying how
far to zoom in, and a 3d layout is refused. One rule for both modes,
portable/label_it.py field_of_view_nm, which the server's routes call
and the browser's through label_it_calls.field_of_view. The buttons'
titles and the log line say the box used; the setting is kept with the
others; Undo is unchanged.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
…s x, y, z axes

Each layout now shows the plane it is named after: the xy panel, and
4panel's top-left, the XY plane.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
Use this model, Fit to Paint and Use these settings set the row's
postprocessing, and since 61b8c5f an affinity model's carried the fitted
threshold too: Sigmoid, then Threshold. AffinityPostprocessor reads that
uint8 0/1 as DefaultPostprocessor's 0-255 and divides by 255, so a
watershed added after it got affinities of at most 0.004, below its 0.5
bias, and no objects came out ("for 2d affs how to run aff on em didn't
seem to work with aff thingy"). An affinity model's row now gives the
sigmoid alone, both modes; its fitted threshold is still the row's, Seed
from Prediction's.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
… modes

A ThresholdPostprocessor before an AffinityPostprocessor leaves the
watershed 0s and 1s and no objects: the review now says so ("warn") and
suggests the chain without the threshold (serving/probe.py
review_postprocess).

The browser gave no postprocessing review at all ("unknown"), though it
runs AffinityPostprocessor. Its advice now reviews an affinity model's
chain as the server's does, from the output class the served model's info
gives: the same two warnings, and the chain into a segmentation offered
as "optional" once its affinities show (models.ts postprocessReview, with
probe.py's suggest_affinity_chain and suggest_postprocess_params ported
beside it and NEIGHBORHOOD_OFFSETS exported through python-data.json). Its
entry's output_class is the served info's, not a second reading of the
manifest's text.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
setup_53_2d and setup_54_2d give three channels (z, y, x), and the browser
serves them a slice a block, where nothing has a neighbour along z. The
mutex watershed builds the z channel no edge there, but counted it in the
mean affinity that tells a fragment's objects from background: where the
model's guess at a z neighbour said "apart", every object was pulled a
third of the way to background, and two 4x4 squares at 0.88 came out as
none. A channel whose offset pairs no voxel in the block is now left out
of the agglomeration and of that mean, both modes (post/segment.py
mutex_watershed, web/src/infer/affinity.ts mutexWatershed); a block thick
enough for every offset, the server's 8 slices a chunk among them,
segments as before, and the parity fixtures are unchanged. In Python this
also keeps an offset longer than its block from mwatershed, which panicked
on one.

The tests run each mode's suggested chains on such a slice: the chain
offered on the ranking's sigmoid (Sigmoid, Default, Affinity) and the one
the threshold warning suggests (Sigmoid, Affinity) find the two squares,
and the chain with the threshold ahead finds none.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
The browser's advice keeps where the output class was read (its network,
else its manifest) and takes #165's affinity verdicts; its unknown review
says which.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
In the browser app the Annotation Crops tab's button answered the number
of painted chunks and drew nothing: the route was a stub. It now draws the
annotated_regions layer as the server's refresh_annotated_regions_layer
does: a local annotation layer of axis-aligned boxes in nm over the
viewer's axes, one per painted chunk of the session's volume (the one it
trains on, as the server draws its manifest's volume alone), from the
corner of the chunk's first voxel, half a voxel before the centre
dataset_offset_nm gives (finetune/volume.ts chunkBoxes, by volumeCornerNm),
labelled painted:<chunk key>. A new layer is added hidden, as the server
adds it; drawn again it keeps what the user set on it, shown or hidden
included (setLayers, carriedLayer), and with nothing painted it is taken
away. It answers the same count. A browser volume has no imported crops
(Load Crops needs a server), so the server's crop boxes have no
counterpart.

The good regions' layer builder draws both layers now, so it is
boxesLayer, and the run_region layer's empty one uses it as before.
schema.py's AVAILABILITY gives the route to both modes (regenerated).

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
… cuts its objects there

David chose to show what was scored: Use this model, Fit to Paint and Use
these settings give an affinity model's layer the sigmoid and the threshold
fitted to the paint again, as a binary model's, both modes (rank_routes.py,
rank.ts; 6515317 gave it the sigmoid alone). Its score calls a painted
pair of neighbours joined above that cut-off, so a threshold there on each
channel shows the decision the score read.

Its objects stay one click away, cut at the same level: the model advice's
optional chain into a segmentation (Sigmoid, Default, Affinity), offered
over the row's Sigmoid and Threshold, takes the threshold's place and gives
AffinityPostprocessor the threshold the ranking fitted for the model as its
bias, 0.5 without a ranking; so does the chain the warning of a threshold
ahead of the watershed suggests. The line says the cut ("joined above 0.076
(fitted to your paint)"). The fit's threshold is kept beside its steps
(Session.rank_fits, the browser's rankFits) for serving/probe.py
review_postprocess and suggest_postprocess_params, and models.ts
postprocessReview and postprocessParams. A suggestion's Apply already makes
the Processing tab's list the steps with their params, the bias among them.

The tests, in both modes: an affinity model's row is Sigmoid then Threshold
at the row's threshold, and the advice over it offers the chain with that
bias; on one slice of two squares whose affinities are about 0.12, the
chain at the fitted 0.076 finds both, at 0.5 neither.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
Once every model's probe has answered, the server's advice stops polling
(the browser's slows to every 30 s), and Fit to Paint and Use these
settings change the chain with no model change: the optional affinity
chain went on cutting at the 0.5 it was offered at before the ranking,
where it now cuts at the threshold fitted. A chain the backend sets itself
(CHAIN_CHANGED_EVENT, which main.js already reads the chain again on) asks
for the advice again, both modes (model-advice.js).

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
The advice's review takes both where the output class was read and the
threshold the ranking fitted.

Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW
Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW

# Conflicts:
#	cellmap_flow/dashboard/routes/model_advice.py
#	cellmap_flow/dashboard/routes/pipeline.py
#	cellmap_flow/dashboard/state.py
#	cellmap_flow/models/configs/base.py
#	docs/source/browser.rst
#	web/src/backend/routes/models.ts
Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW

# Conflicts:
#	CHANGELOG.md
#	cellmap_flow/dashboard/state.py
#	cellmap_flow/dashboard/static/js/dashboard/finetune/training-form.js
#	cellmap_flow/dashboard/templates/_models_tab.html
#	cellmap_flow/finetune/job_manager/submit.py
#	docs/api.md
#	web/build/export_data.py
#	web/src/backend/routes/models.ts
#	web/src/backend/routes/rank.ts
#	web/src/backend/serving.ts
#	web/src/generated/cellmap-flow.schema.json
#	web/src/generated/cellmap-flow.ts
Claude-Session: https://claude.ai/code/session_01EzWMS4pG3tm1ZtvKULfSfW

# Conflicts:
#	CHANGELOG.md
#	web/src/backend/routes/models.ts
#	web/src/backend/routes/rank.ts
#	web/src/backend/serving.ts
#	web/src/finetune/onnx-proto.ts
#	web/src/models/index.ts
#	web/src/models/metadata.ts
@davidackerman
davidackerman marked this pull request as ready for review October 28, 2026 04:28
@davidackerman
davidackerman merged commit 4dbd1e6 into main Oct 28, 2026
7 checks passed
@davidackerman
davidackerman deleted the group-merge-163-166 branch October 28, 2026 04:34
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