Skip to content

Refactor AiModelCategory and add Soma Detection model#9477

Open
hotzenklotz wants to merge 22 commits into
masterfrom
soma_modal
Open

Refactor AiModelCategory and add Soma Detection model#9477
hotzenklotz wants to merge 22 commits into
masterfrom
soma_modal

Conversation

@hotzenklotz
Copy link
Copy Markdown
Member

@hotzenklotz hotzenklotz commented Apr 13, 2026

Summary

This PR unifies how AI models are managed across the platform by moving pretrained models into the same database-backed system as custom-trained models. Instead of maintaining hardcoded frontend definitions for built-in models, all models are now loaded from a single API source, simplifying model management and making the system easier to extend.

Key Improvements

  • Unified model architecture

    • Pretrained models (Neuron Segmentation, Mitochondria Detection, Nuclei Detection, Soma Detection) are now stored and served like regular custom models.
    • Frontend now fetches all available models from one endpoint rather than relying on hardcoded entries.
  • Expanded model category support

    • Added missing backend/frontend categories and introduced new categories for:

      • Generic instance models (custom-trained instance segmentation)
      • Somata detection
      • Mitochondria
  • Simplified inference flow

    • Legacy specialized nuclei inference has been consolidated into the standard instance inference pipeline.
    • Historical job compatibility is preserved through deprecated legacy job types.
  • More consistent model metadata

    • Model IDs, categories, permissions, and voxel-size handling are now standardized across pretrained and custom models.
    • Removed legacy frontend-specific logic and hardcoded constants.
  • Improved permissions & visibility

    • Added support for internal/superuser-only models.
    • Pretrained models are globally accessible without needing ownership or organization membership.
  • Frontend cleanup

    • Removed separate pretrained/custom model handling in the UI.
    • Model selector, analysis settings, and related flows now operate on a single shared model type.

User Impact

  • Users see a cleaner, more scalable model selection experience.
  • New pretrained models (such as Soma Detection) can now be added without frontend code changes.
  • Future AI model rollout and maintenance becomes significantly easier and more consistent.

Worker PR

https://github.com/scalableminds/voxelytics/pull/4527

Steps to test:

Fixes

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds three AI model categories, marks nuclei inference as deprecated, introduces isSuperUserOnly and isPretrainedModel on AiModel, refactors frontend model selection and job handling to use DB-backed models and category→job mapping, tightens voxel-size/size-check logic, and includes SQL migrations to update enums and aiModels columns.

Changes

Cohort / File(s) Summary
AI Model category & DB schema
app/models/aimodels/AiModelCategory.scala, schema/evolutions/161-pretrained-ai-models.sql, schema/evolutions/reversions/161-pretrained-ai-models.sql, schema/schema.sql, schema/evolutions/162-aimodel-nullable-user.sql, schema/evolutions/reversions/162-aimodel-nullable-user.sql
Adds enum values em_generic, em_somata, em_mitochondria; adds isSuperUserOnly and isPretrainedModel boolean columns; makes _user nullable via migrations and includes reversion scripts.
AiModel domain & controller
app/models/aimodels/AiModel.scala, app/controllers/AiModelController.scala, app/controllers/InitialDataController.scala
Adds isSuperUserOnly and isPretrainedModel; makes _user optional; changes RunInference params (aiModelId: Option[String]), resolves/stores DB model id for inference; adjusts seeding to insert idempotent pretrained models.
JobCommand & job enums
app/models/job/JobCommand.scala, frontend/javascripts/types/api_types.ts
Moves infer_nuclei to deprecated grouping; frontend API enum replaces INFER_NUCLEI with DEPRECATED_INFER_NUCLEI (same underlying value).
Frontend API & training defaults
frontend/javascripts/admin/api/jobs.ts, frontend/javascripts/viewer/view/ai_jobs/train_ai_model/ai_training_job_context.tsx
Expands APIAiModelCategory with new members; changes default instance-training category from EM_NUCLEIEM_GENERIC.
Frontend job list & UI adjustments
frontend/javascripts/admin/job/job_list_view.tsx, frontend/javascripts/admin/voxelytics/ai_model_list_view.tsx
Switches UI mappings to use deprecated nuclei command; restricts "Manage Access" button to owned, non-pretrained models.
Viewer AI job constants & utilities
frontend/javascripts/viewer/view/ai_jobs/constants.ts, frontend/javascripts/viewer/view/ai_jobs/utils.ts, frontend/javascripts/viewer/view/ai_jobs/credit_information.tsx
Removes nuclei-specific image and MEAN_VX_SIZE; narrows MIN_BBOX_EXTENT and utility unions to exclude nuclei; removes nuclei credit-cost mapping; enforces aiModelId requirement for voxel-size logic.
Run-job contexts & selector
frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_image_segmentation_job_context.tsx, frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_model_selector.tsx, frontend/javascripts/viewer/view/ai_jobs/ai_analysis_settings.tsx
Drops PretrainedModel type and hard-coded pretrained entries; fetches AiModels from API, filters isSuperUserOnly for non-superusers, splits by isPretrainedModel, maps categories (including generic/somata → INFER_INSTANCES, mitochondria → INFER_MITOCHONDRIA), and always derive aiModelId from selected model.
Action bar availability
frontend/javascripts/viewer/view/action_bar_view.tsx
Checks AI availability against INFER_INSTANCES instead of INFER_NUCLEI.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • #9150: Overlaps AiModel data model extensions, controller/service refactors, and DB schema changes.
  • #9304: Overlaps inference/voxel-size resolution and pretrained-model handling across services and frontend utilities.
  • #8902: Related enum changes and deprecation handling for infer_nuclei in JobCommand.

Suggested labels

new feature, refactoring, jobs

Suggested reviewers

  • MichaelBuessemeyer
  • fm3
  • philippotto

Poem

🐇 I hopped through enums, fields, and seed,

New somata, mitochondria take the lead.
Pretrained models snug and neat,
Superuser flags keep access neat.
Nuclei bowed out — onward AI feet!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: refactoring AiModelCategory and adding the Soma Detection model, which are the primary objectives of this PR.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the unification of model management, category expansions, and the addition of the Soma Detection model.
Linked Issues check ✅ Passed The PR implements all requirements from issue #9442: adds soma detection support, creates a pretrained soma model with correct category (EM_SOMATA) and job-type mapping (INFER_INSTANCES), updates frontend UI for model selection, and integrates into job workflows.
Out of Scope Changes check ✅ Passed All changes are within scope of unifying model management and adding soma detection. Scope includes refactoring AiModelCategory, consolidating nuclei inference to instances, standardizing model metadata, and cleanup of pretrained/custom separation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch soma_modal

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_model_selector.tsx (1)

72-85: ⚠️ Potential issue | 🟡 Minor

Consider handling additional categories in mapCategoryToJobType.

The switch only handles EM_NEURONS, EM_NUCLEI, EM_GENERIC, and EM_SOMATA. If a custom AiModel from the API has a category like EM_SYNAPSES, EM_NEURON_TYPES, or EM_CELL_ORGANELLES, the default case will throw an error when the user selects it.

🛡️ Suggested defensive handling
 const mapCategoryToJobType = (
   category: APIAiModelCategory,
 ): APIJobCommand.INFER_NEURONS | APIJobCommand.INFER_INSTANCES => {
   switch (category) {
     case APIAiModelCategory.EM_NEURONS:
       return APIJobCommand.INFER_NEURONS;
     case APIAiModelCategory.EM_NUCLEI:
     case APIAiModelCategory.EM_GENERIC:
     case APIAiModelCategory.EM_SOMATA:
+    case APIAiModelCategory.EM_SYNAPSES:
+    case APIAiModelCategory.EM_NEURON_TYPES:
+    case APIAiModelCategory.EM_CELL_ORGANELLES:
       return APIJobCommand.INFER_INSTANCES;
     default:
-      throw new Error(`Unsupported category: ${category}`);
+      // Fallback for any future categories
+      console.warn(`Unknown category: ${category}, defaulting to INFER_INSTANCES`);
+      return APIJobCommand.INFER_INSTANCES;
   }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_model_selector.tsx`
around lines 72 - 85, mapCategoryToJobType currently throws for any
APIAiModelCategory values beyond EM_NEURONS/EM_NUCLEI/EM_GENERIC/EM_SOMATA;
update it to defensively handle new/custom categories (e.g., EM_SYNAPSES,
EM_NEURON_TYPES, EM_CELL_ORGANELLES) by returning a safe default like
APIJobCommand.INFER_INSTANCES instead of throwing, and emit a console/process
warning or logger entry noting the unexpected category; modify the switch in
mapCategoryToJobType to include a catch-all default that returns
APIJobCommand.INFER_INSTANCES and logs the unsupported category value so
unexpected enum values won’t crash the UI.
frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_image_segmentation_job_context.tsx (1)

234-252: ⚠️ Potential issue | 🟡 Minor

Missing customConfiguration in useCallback dependency array.

customConfiguration is used in the callback (lines 202, 214) but is not listed in the dependency array. This could cause the callback to use stale values.

🐛 Proposed fix
   }, [
     areParametersValid,
     selectedModel,
     selectedJobType,
     selectedBoundingBox,
     newDatasetName,
     selectedLayer,
     dataset,
     isViewMode,
     annotationId,
     seedGeneratorDistanceThreshold,
     isEvaluationActive,
     splitMergerEvaluationSettings,
     userBoundingBoxCount,
     taskBoundingBoxes,
     skeletonAnnotation,
     datasetConfiguration,
     dispatch,
+    customConfiguration,
   ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_image_segmentation_job_context.tsx`
around lines 234 - 252, The callback passed to useCallback references
customConfiguration but the dependency array (the array containing
areParametersValid, selectedModel, etc.) omits customConfiguration, risking
stale values; update the useCallback dependency array to include
customConfiguration (or memoize customConfiguration with useMemo if you need a
stable identity) so the callback (the function created by useCallback) is
re-created when customConfiguration changes and lint warnings are resolved.
🧹 Nitpick comments (2)
app/models/aimodels/AiModel.scala (1)

92-109: Consolidate pretrained voxel-size resolution in one path.

inferenceBBoxToTargetMag() now uses findPretrainedModelVoxelSize(), but findModelVoxelSize() below still carries the old boolean neuron-vs-nuclei assumption. Routing that helper through the same ID-based lookup would keep the pretrained model table in one place and avoid the two code paths drifting again when another model is added.

Also applies to: 143-149

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/aimodels/AiModel.scala` around lines 92 - 109,
inferenceBBoxToTargetMag uses findPretrainedModelVoxelSize for pretrained IDs
but findModelVoxelSize still encodes the old boolean neuron-vs-nuclei branching;
update findModelVoxelSize to route pretrainedModelId through
findPretrainedModelVoxelSize (or change its signature to accept Option[String]
and call findPretrainedModelVoxelSize when present) and remove the hardcoded
boolean neuron/vs/nuclei assumption so both code paths share the same ID-based
lookup; update callers (including inferenceBBoxToTargetMag and the code around
the 143-149 region) to pass the pretrainedModelIdOpt into the unified helper and
fall back to existing custom-AI-model voxel-size logic when no pretrained ID is
provided.
frontend/javascripts/admin/job/job_list_view.tsx (1)

151-154: Use job.args.modelId when naming active instance jobs.

Now that nuclei and soma inference run via INFER_INSTANCES, only legacy infer_nuclei jobs keep a specific label here. New nuclei and soma jobs will collapse into the generic instance-segmentation text/filter value, which makes job history harder to understand. Consider deriving the display name/description for INFER_INSTANCES entries from job.args.modelId (for example nuclei_inferral_model vs soma_inferral_model).

Also applies to: 260-264, 441-447

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/javascripts/admin/job/job_list_view.tsx` around lines 151 - 154,
Replace the hardcoded "AI Instance Segmentation" label for
APIJobCommand.INFER_INSTANCES with a derived display name using job.args.modelId
(e.g., `${job.args.modelId}_inferral_model`) and fallback to the generic text
when modelId is missing; update any codepaths that build the job label/display
string and any filters that currently match on the static "AI Instance
Segmentation" value to use the derived name (look for the mapping/labeling logic
around APIJobCommand.INFER_INSTANCES in job_list_view.tsx and the other similar
mappings referenced in the file).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/models/job/JobCommand.scala`:
- Around line 14-21: lowPriorityJobs currently uses values which includes
deprecated commands (globalize_floodfills, train_model, infer_with_model,
infer_nuclei), so change the computation to exclude the legacy set: define a
legacyJobs Set including those deprecated enum members and compute
lowPriorityJobs as values.diff(highPriorityJobs ++ legacyJobs) (reference the
symbols lowPriorityJobs, highPriorityJobs, values and the deprecated enum
entries).

In `@frontend/javascripts/viewer/view/ai_jobs/utils.ts`:
- Around line 63-65: The runtime throw happens because aiModelId can be
undefined when callers run the logic; update the guard conditions so the
query/validator only run when a model is selected: in the component where the
query enabled is computed (the expression currently using selectedBoundingBox &&
selectedJobType && selectedLayer), add aiModelId to that boolean chain so it
becomes selectedBoundingBox && selectedJobType && selectedLayer && aiModelId;
likewise in the validator check (the expression currently using value &&
selectedLayer && selectedJobType), include the selected model variable used
there (selectedModel or aiModelId) so it becomes value && selectedLayer &&
selectedJobType && selectedModel/aiModelId to prevent the throw in the function
that expects aiModelId.

---

Outside diff comments:
In
`@frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_image_segmentation_job_context.tsx`:
- Around line 234-252: The callback passed to useCallback references
customConfiguration but the dependency array (the array containing
areParametersValid, selectedModel, etc.) omits customConfiguration, risking
stale values; update the useCallback dependency array to include
customConfiguration (or memoize customConfiguration with useMemo if you need a
stable identity) so the callback (the function created by useCallback) is
re-created when customConfiguration changes and lint warnings are resolved.

In `@frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_model_selector.tsx`:
- Around line 72-85: mapCategoryToJobType currently throws for any
APIAiModelCategory values beyond EM_NEURONS/EM_NUCLEI/EM_GENERIC/EM_SOMATA;
update it to defensively handle new/custom categories (e.g., EM_SYNAPSES,
EM_NEURON_TYPES, EM_CELL_ORGANELLES) by returning a safe default like
APIJobCommand.INFER_INSTANCES instead of throwing, and emit a console/process
warning or logger entry noting the unexpected category; modify the switch in
mapCategoryToJobType to include a catch-all default that returns
APIJobCommand.INFER_INSTANCES and logs the unsupported category value so
unexpected enum values won’t crash the UI.

---

Nitpick comments:
In `@app/models/aimodels/AiModel.scala`:
- Around line 92-109: inferenceBBoxToTargetMag uses findPretrainedModelVoxelSize
for pretrained IDs but findModelVoxelSize still encodes the old boolean
neuron-vs-nuclei branching; update findModelVoxelSize to route pretrainedModelId
through findPretrainedModelVoxelSize (or change its signature to accept
Option[String] and call findPretrainedModelVoxelSize when present) and remove
the hardcoded boolean neuron/vs/nuclei assumption so both code paths share the
same ID-based lookup; update callers (including inferenceBBoxToTargetMag and the
code around the 143-149 region) to pass the pretrainedModelIdOpt into the
unified helper and fall back to existing custom-AI-model voxel-size logic when
no pretrained ID is provided.

In `@frontend/javascripts/admin/job/job_list_view.tsx`:
- Around line 151-154: Replace the hardcoded "AI Instance Segmentation" label
for APIJobCommand.INFER_INSTANCES with a derived display name using
job.args.modelId (e.g., `${job.args.modelId}_inferral_model`) and fallback to
the generic text when modelId is missing; update any codepaths that build the
job label/display string and any filters that currently match on the static "AI
Instance Segmentation" value to use the derived name (look for the
mapping/labeling logic around APIJobCommand.INFER_INSTANCES in job_list_view.tsx
and the other similar mappings referenced in the file).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f56dd748-2971-4f27-8448-c7ba9418df9f

📥 Commits

Reviewing files that changed from the base of the PR and between 7b8c8d6 and a59c7ea.

⛔ Files ignored due to path filters (1)
  • frontend/assets/images/soma-inferral-example.png is excluded by !**/*.png
📒 Files selected for processing (16)
  • app/controllers/AiModelController.scala
  • app/models/aimodels/AiModel.scala
  • app/models/aimodels/AiModelCategory.scala
  • app/models/job/JobCommand.scala
  • conf/webknossos.latest.routes
  • frontend/javascripts/admin/api/jobs.ts
  • frontend/javascripts/admin/job/job_list_view.tsx
  • frontend/javascripts/types/api_types.ts
  • frontend/javascripts/viewer/view/action_bar_view.tsx
  • frontend/javascripts/viewer/view/ai_jobs/constants.ts
  • frontend/javascripts/viewer/view/ai_jobs/credit_information.tsx
  • frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_analysis_settings.tsx
  • frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_image_segmentation_job_context.tsx
  • frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_model_selector.tsx
  • frontend/javascripts/viewer/view/ai_jobs/train_ai_model/ai_training_job_context.tsx
  • frontend/javascripts/viewer/view/ai_jobs/utils.ts

Comment on lines 14 to 21
val compute_mesh_file, compute_segment_index_file, convert_to_wkw, export_tiff, find_largest_segment_id,
materialize_volume_annotation, render_animation, align_sections, infer_nuclei, infer_neurons, infer_instances,
infer_mitochondria, train_neuron_model, train_instance_model,
materialize_volume_annotation, render_animation, align_sections, infer_neurons, infer_instances, infer_mitochondria,
train_neuron_model, train_instance_model,
// No-longer supported jobs, kept here to be able to display old existing jobs:
globalize_floodfills, train_model, infer_with_model = Value
globalize_floodfills, train_model, infer_with_model, infer_nuclei = Value

val highPriorityJobs: Set[Value] = Set(convert_to_wkw, export_tiff)
val lowPriorityJobs: Set[Value] = values.diff(highPriorityJobs)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Exclude deprecated commands from lowPriorityJobs.

infer_nuclei moved to the legacy bucket, but lowPriorityJobs is still derived from values, so all deprecated commands remain part of the normal runnable set. That undermines the “no-longer supported” split and can leak legacy commands into worker capability or scheduling logic.

Suggested fix
   val compute_mesh_file, compute_segment_index_file, convert_to_wkw, export_tiff, find_largest_segment_id,
   materialize_volume_annotation, render_animation, align_sections, infer_neurons, infer_instances, infer_mitochondria,
   train_neuron_model, train_instance_model,
   // No-longer supported jobs, kept here to be able to display old existing jobs:
   globalize_floodfills, train_model, infer_with_model, infer_nuclei = Value

   val highPriorityJobs: Set[Value] = Set(convert_to_wkw, export_tiff)
-  val lowPriorityJobs: Set[Value] = values.diff(highPriorityJobs)
+  private val deprecatedJobs: Set[Value] =
+    Set(globalize_floodfills, train_model, infer_with_model, infer_nuclei)
+  val lowPriorityJobs: Set[Value] = values.diff(highPriorityJobs ++ deprecatedJobs)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/job/JobCommand.scala` around lines 14 - 21, lowPriorityJobs
currently uses values which includes deprecated commands (globalize_floodfills,
train_model, infer_with_model, infer_nuclei), so change the computation to
exclude the legacy set: define a legacyJobs Set including those deprecated enum
members and compute lowPriorityJobs as values.diff(highPriorityJobs ++
legacyJobs) (reference the symbols lowPriorityJobs, highPriorityJobs, values and
the deprecated enum entries).

Comment thread frontend/javascripts/viewer/view/ai_jobs/utils.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/controllers/AiModelController.scala`:
- Around line 291-292: resolveAiModel currently accepts any known pretrained id
causing cross-route mismatches; before computing effectivePretrainedModelId in
AiModelController, validate that the supplied or derived pretrained model id is
allowed for the current inference route. Add route-aware validation (e.g., a
small whitelist/map keyed by route type used by the instance/neuron endpoints)
and reject pretrained ids that don’t match that whitelist (return a validation
error or None) instead of silently accepting them; update the resolution logic
that sets effectivePretrainedModelId (and the other similar spots you mentioned
around lines 349-350 and 462-473) to call this route-aware check or pass the
route into resolveAiModel so resolveAiModel enforces allowed pretrained ids per
route.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cdb374fb-8785-4fed-9c7c-677c616cfca1

📥 Commits

Reviewing files that changed from the base of the PR and between a59c7ea and a89cadb.

📒 Files selected for processing (3)
  • app/controllers/AiModelController.scala
  • conf/webknossos.latest.routes
  • frontend/javascripts/types/api_types.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • conf/webknossos.latest.routes
  • frontend/javascripts/types/api_types.ts

Comment thread app/controllers/AiModelController.scala Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/models/aimodels/AiModel.scala (1)

205-225: ⚠️ Potential issue | 🟠 Major

readAccessQ ignores isSuperUserOnly.

OR isPretrainedModel = TRUE makes every pretrained row readable to every authenticated user, so listAiModels will expose models that the new isSuperUserOnly flag is supposed to hide. Please enforce that flag server-side instead of relying on frontend filtering.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/aimodels/AiModel.scala` around lines 205 - 225, readAccessQ
currently lets any authenticated user see pretrained models via "OR
isPretrainedModel = TRUE", ignoring the new isSuperUserOnly flag; change the
query in readAccessQ to additionally require that either isSuperUserOnly IS NOT
TRUE or the requesting user is a superuser. Concretely, add a clause like "AND
(isSuperUserOnly IS NOT TRUE OR EXISTS (SELECT 1 FROM webknossos.users_ u WHERE
u._id = $requestingUserId AND u.isSuperUser = TRUE))" to the model visibility
logic in readAccessQ so pretrained models marked isSuperUserOnly=true are only
returned when the requesting user is a superuser.
♻️ Duplicate comments (1)
app/controllers/AiModelController.scala (1)

284-286: ⚠️ Potential issue | 🟠 Major

Keep raw pretrained ids route- and access-scoped.

The findPretrainedModelVoxelSize(id).isDefined branch still treats any known pretrained token as valid on every inference route. That means /infer_instances can accept neuron models, /infer_neurons can accept nuclei/soma models, and raw ids also bypass any DB-backed flags like isSuperUserOnly because no AiModel is loaded. Resolve pretrained ids against route-specific, access-checked metadata instead of accepting the string on its own.

Also applies to: 346-348, 463-473

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/AiModelController.scala` around lines 284 - 286, The code
currently accepts raw pretrained IDs (via effectivePretrainedModelId) when
aiModelOpt is empty, allowing route-mismatched or access-restricted pretrained
tokens; instead, validate any pretrainedModelId against route-specific and
access-checked metadata rather than trusting the string. Update the logic around
resolveAiModel and effectivePretrainedModelId so that when pretrainedModelIdOpt
is defined but aiModelOpt.isEmpty you resolve and authorize that pretrained ID
using the same route-scoped checks you use for DB-backed AiModel (e.g., call the
route-specific resolver/authorizer or a helper that maps a pretrained id ->
AiModel metadata and verifies isSuperUserOnly and model-type compatibility via
findPretrainedModelVoxelSize), and only set effectivePretrainedModelId if that
validation succeeds; apply the same pattern used in resolveAiModel for the other
affected blocks referenced (lines ~346-348 and ~463-473).
🧹 Nitpick comments (2)
frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_model_selector.tsx (2)

65-70: Silent early return when category is missing may confuse users.

If a model somehow lacks a category, clicking it does nothing with no feedback. Consider logging a warning or showing an error toast to aid debugging.

♻️ Optional: Add user feedback for invalid model selection
   const onSelectModel = (model: AiModel) => {
-    if (!model.category) return;
+    if (!model.category) {
+      console.warn("Selected model has no category:", model);
+      return;
+    }
     const jobType = mapCategoryToJobType(model.category);
     setSelectedModel(model);
     setSelectedJobType(jobType);
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_model_selector.tsx`
around lines 65 - 70, The onSelectModel handler currently returns silently if
model.category is missing; update onSelectModel (involving AiModel,
mapCategoryToJobType, setSelectedModel, setSelectedJobType) to surface feedback:
when model.category is falsy, log a warning (e.g., console.warn with the model
id/name) and trigger a user-facing notification/toast indicating invalid model
selection, otherwise proceed to compute jobType and call setSelectedModel and
setSelectedJobType as before.

51-60: Query key should include isSuperUser for cache correctness.

The queryFn filters models based on isSuperUser, but this value isn't included in the queryKey. If the user's super user status were to change (e.g., in a different tab), the cached data would be stale.

♻️ Proposed fix
   const { data: allModels = [], isLoading } = useQueryWithErrorHandling(
     {
-      queryKey: ["aiModels"],
+      queryKey: ["aiModels", isSuperUser],
       queryFn: async () => {
         const models = await getAiModels();
         return models.filter((aiModel) => (isSuperUser ? true : !aiModel.isSuperUserOnly));
       },
     },
     "Could not load model list.",
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_model_selector.tsx`
around lines 51 - 60, The query key for useQueryWithErrorHandling must include
the isSuperUser value so cached results reflect the user's privilege; update the
queryKey in the useQueryWithErrorHandling call (currently ["aiModels"]) to
include isSuperUser (e.g., ["aiModels", isSuperUser]) so the queryFn (which
calls getAiModels and filters by isSuperUser) will be re-run when that flag
changes and avoid stale allModels cache.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/controllers/AiModelController.scala`:
- Around line 298-306: The JSON payload currently sets "model_organization_id"
by passing an Option
(aiModelOpt.filterNot(_.isPretrainedModel).map(_._organization)) directly into
commandArgs, which serializes None as null; change commandArgs construction to
build a base Json.obj with the required fields and then conditionally merge in
the model_organization_id only when the Option is defined (e.g., compute val
modelOrgOpt = aiModelOpt.filterNot(_.isPretrainedModel).map(_._organization) and
do baseObj ++ modelOrgOpt.map(o => Json.obj("model_organization_id" ->
o)).getOrElse(Json.obj())), and apply the same conditional merge in the second
location (the other commandArgs around lines 367-375) so pretrained runs omit
the key entirely.

In `@app/models/aimodels/AiModel.scala`:
- Around line 103-105: The code is using the mutable user-facing AiModel.name to
lookup pretrained metadata; instead use the immutable pretrained identifier
field (pretrainedModelId) instead. Update the match branches that call
findPretrainedModelVoxelSize (including the case that currently uses
aiModel.name and the other occurrences around the noted blocks 149-155 and
160-161) to pass aiModel.pretrainedModelId or the model’s immutable pretrained
identifier, and adjust the related error message text to mention that identifier
instead of aiModel.name. Ensure any use of pretrainedModelIdOpt remains
consistent with this immutable id.

---

Outside diff comments:
In `@app/models/aimodels/AiModel.scala`:
- Around line 205-225: readAccessQ currently lets any authenticated user see
pretrained models via "OR isPretrainedModel = TRUE", ignoring the new
isSuperUserOnly flag; change the query in readAccessQ to additionally require
that either isSuperUserOnly IS NOT TRUE or the requesting user is a superuser.
Concretely, add a clause like "AND (isSuperUserOnly IS NOT TRUE OR EXISTS
(SELECT 1 FROM webknossos.users_ u WHERE u._id = $requestingUserId AND
u.isSuperUser = TRUE))" to the model visibility logic in readAccessQ so
pretrained models marked isSuperUserOnly=true are only returned when the
requesting user is a superuser.

---

Duplicate comments:
In `@app/controllers/AiModelController.scala`:
- Around line 284-286: The code currently accepts raw pretrained IDs (via
effectivePretrainedModelId) when aiModelOpt is empty, allowing route-mismatched
or access-restricted pretrained tokens; instead, validate any pretrainedModelId
against route-specific and access-checked metadata rather than trusting the
string. Update the logic around resolveAiModel and effectivePretrainedModelId so
that when pretrainedModelIdOpt is defined but aiModelOpt.isEmpty you resolve and
authorize that pretrained ID using the same route-scoped checks you use for
DB-backed AiModel (e.g., call the route-specific resolver/authorizer or a helper
that maps a pretrained id -> AiModel metadata and verifies isSuperUserOnly and
model-type compatibility via findPretrainedModelVoxelSize), and only set
effectivePretrainedModelId if that validation succeeds; apply the same pattern
used in resolveAiModel for the other affected blocks referenced (lines ~346-348
and ~463-473).

---

Nitpick comments:
In `@frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_model_selector.tsx`:
- Around line 65-70: The onSelectModel handler currently returns silently if
model.category is missing; update onSelectModel (involving AiModel,
mapCategoryToJobType, setSelectedModel, setSelectedJobType) to surface feedback:
when model.category is falsy, log a warning (e.g., console.warn with the model
id/name) and trigger a user-facing notification/toast indicating invalid model
selection, otherwise proceed to compute jobType and call setSelectedModel and
setSelectedJobType as before.
- Around line 51-60: The query key for useQueryWithErrorHandling must include
the isSuperUser value so cached results reflect the user's privilege; update the
queryKey in the useQueryWithErrorHandling call (currently ["aiModels"]) to
include isSuperUser (e.g., ["aiModels", isSuperUser]) so the queryFn (which
calls getAiModels and filters by isSuperUser) will be re-run when that flag
changes and avoid stale allModels cache.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2d23b33e-cefd-4a76-9411-e8ad17c8b9a1

📥 Commits

Reviewing files that changed from the base of the PR and between a89cadb and 2cc587d.

📒 Files selected for processing (12)
  • app/controllers/AiModelController.scala
  • app/controllers/InitialDataController.scala
  • app/models/aimodels/AiModel.scala
  • app/models/aimodels/AiModelCategory.scala
  • frontend/javascripts/admin/api/jobs.ts
  • frontend/javascripts/types/api_types.ts
  • frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_analysis_settings.tsx
  • frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_image_segmentation_job_context.tsx
  • frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_model_selector.tsx
  • schema/evolutions/161-pretrained-ai-models.sql
  • schema/evolutions/reversions/161-pretrained-ai-models.sql
  • schema/schema.sql
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/models/aimodels/AiModelCategory.scala
  • frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_analysis_settings.tsx

Comment thread app/controllers/AiModelController.scala
Comment thread app/models/aimodels/AiModel.scala Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
app/controllers/AiModelController.scala (1)

293-305: ⚠️ Potential issue | 🟠 Major

Omit model_organization_id instead of serializing null.

aiModelOpt.filterNot(_.isPretrainedModel).map(_._organization) still goes through Json.obj, so pretrained runs emit "model_organization_id": null rather than omitting the key. If the worker distinguishes absent from null, this still takes the wrong code path. Build the base payload first and merge model_organization_id only when it exists.

In Play JSON, when building Json.obj with an Option field whose value is None, is the field omitted or serialized as null?

Also applies to: 356-373

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/AiModelController.scala` around lines 293 - 305, The Json.obj
construction in AiModelController (the commandArgs payload) is currently
serializing model_organization_id as null when the model is pretrained; instead,
build the base Json object first (e.g., basePayload or commandArgsBase) using
the existing keys, then detect the organization with
aiModelOpt.filterNot(_.isPretrainedModel).map(_._organization) and only
merge/add the "model_organization_id" key into the Json object when that Option
is defined; apply the same pattern to the other payload block around lines
356-373 so optional fields are omitted rather than serialized as null.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/controllers/AiModelController.scala`:
- Around line 283-285: The aiModel resolution logic (the Fox.runOptional block
using ObjectId.fromString and aiModelDAO.findOne) needs to accept pretrained
string ids like "soma_inferral_model" / "nuclei_inferral_model": implement a
resolver function (e.g., resolveAiModelByIdOrPretrained) that takes the incoming
aiModelId string, returns the seeded AiModel from aiModelDAO when the string
matches known pretrained keys, or falls back to ObjectId.fromString(...) ->
aiModelDAO.findOne for DB ids; replace the existing
Fox.runOptional(request.body.aiModelId)(id =>
ObjectId.fromString(id).flatMap(oid => aiModelDAO.findOne(oid))) usages with
Fox.runOptional(request.body.aiModelId)(resolveAiModelByIdOrPretrained) (also
update the other similar blocks referenced), and ensure you still persist the
AiInference._aiModel as the resolved DB AiModel id while keeping the
worker-facing model_id string unchanged.

In `@app/models/aimodels/AiModel.scala`:
- Line 228: The DAO read filter currently allows pretrained models through
because of "isPretrainedModel = TRUE", which bypasses the isSuperUserOnly
restriction; update the aiModelDAO.findOne/read predicate to include the
isSuperUserOnly visibility rule (e.g., only return rows where isSuperUserOnly =
FALSE for non-superusers) so DAO-level reads respect model visibility, or
alternatively enforce a superuser check immediately after aiModelDAO.findOne in
the inference path (the request handlers that resolve models for
submission/inference) and reject non-superusers when the resolved
AiModel.isSuperUserOnly is true; change either the DAO query logic or add an
explicit gate in the model-resolution code paths (where models are fetched for
inference) to fix the bypass.

In `@schema/evolutions/reversions/162-aimodel-nullable-user.sql`:
- Around line 5-10: The rollback SQL currently only deletes pretrained models
but leaves the seeded sample_ai_model row with _user = NULL, causing the ALTER
COLUMN _user SET NOT NULL to fail; modify the migration to either (a) update the
seeded sample row in webknossos.aiModels to set a valid _user before running
ALTER TABLE (targeting the row where name = 'sample_ai_model' or seeded by
InitialDataController), or (b) add a single UPDATE that backfills every NULL
_user in webknossos.aiModels to a valid user id (or a system-owner id) before
executing ALTER TABLE, ensuring you still remove pretrained rows
(isPretrainedModel = TRUE) as currently done.

---

Duplicate comments:
In `@app/controllers/AiModelController.scala`:
- Around line 293-305: The Json.obj construction in AiModelController (the
commandArgs payload) is currently serializing model_organization_id as null when
the model is pretrained; instead, build the base Json object first (e.g.,
basePayload or commandArgsBase) using the existing keys, then detect the
organization with aiModelOpt.filterNot(_.isPretrainedModel).map(_._organization)
and only merge/add the "model_organization_id" key into the Json object when
that Option is defined; apply the same pattern to the other payload block around
lines 356-373 so optional fields are omitted rather than serialized as null.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0aa72f47-596d-418a-aee3-7438a9af307b

📥 Commits

Reviewing files that changed from the base of the PR and between 2cc587d and 19e7516.

📒 Files selected for processing (6)
  • app/controllers/AiModelController.scala
  • app/controllers/InitialDataController.scala
  • app/models/aimodels/AiModel.scala
  • schema/evolutions/162-aimodel-nullable-user.sql
  • schema/evolutions/reversions/162-aimodel-nullable-user.sql
  • schema/schema.sql

Comment thread app/controllers/AiModelController.scala
FROM webknossos.users_
WHERE _id = $requestingUserId
))
OR isPretrainedModel = TRUE
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

isSuperUserOnly is bypassed by the DAO read filter.

This makes every pretrained row readable via aiModelDAO.findOne, including the nuclei/soma models seeded as superuser-only. The inference endpoints use that lookup directly, so a non-superuser can still submit jobs against those models by id. Keep the DAO predicate aligned with the visibility rules, or enforce the superuser gate before using the resolved model for inference.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/aimodels/AiModel.scala` at line 228, The DAO read filter currently
allows pretrained models through because of "isPretrainedModel = TRUE", which
bypasses the isSuperUserOnly restriction; update the aiModelDAO.findOne/read
predicate to include the isSuperUserOnly visibility rule (e.g., only return rows
where isSuperUserOnly = FALSE for non-superusers) so DAO-level reads respect
model visibility, or alternatively enforce a superuser check immediately after
aiModelDAO.findOne in the inference path (the request handlers that resolve
models for submission/inference) and reject non-superusers when the resolved
AiModel.isSuperUserOnly is true; change either the DAO query logic or add an
explicit gate in the model-resolution code paths (where models are fetched for
inference) to fix the bypass.

Comment on lines +5 to +10
-- Remove pretrained models that have no user (they will be re-inserted by InitialDataController after rollback)
DELETE FROM webknossos.aiModels WHERE isPretrainedModel = TRUE;

ALTER TABLE webknossos.aiModels DROP CONSTRAINT _user_objectId;
ALTER TABLE webknossos.aiModels ALTER COLUMN _user SET NOT NULL;
ALTER TABLE webknossos.aiModels ADD CONSTRAINT _user_objectId CHECK (_user ~ '^[0-9a-f]{24}$');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Rollback still fails on the seeded sample model.

This only removes pretrained rows before restoring _user to NOT NULL, but InitialDataController now seeds sample_ai_model with _user = NULL and isPretrainedModel = FALSE. Rolling back a fresh 162 install will still fail on that row. Either keep the sample model owned, or clear/backfill every null _user row before SET NOT NULL.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@schema/evolutions/reversions/162-aimodel-nullable-user.sql` around lines 5 -
10, The rollback SQL currently only deletes pretrained models but leaves the
seeded sample_ai_model row with _user = NULL, causing the ALTER COLUMN _user SET
NOT NULL to fail; modify the migration to either (a) update the seeded sample
row in webknossos.aiModels to set a valid _user before running ALTER TABLE
(targeting the row where name = 'sample_ai_model' or seeded by
InitialDataController), or (b) add a single UPDATE that backfills every NULL
_user in webknossos.aiModels to a valid user id (or a system-owner id) before
executing ALTER TABLE, ensuring you still remove pretrained rows
(isPretrainedModel = TRUE) as currently done.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
app/models/aimodels/AiModel.scala (1)

149-149: 💤 Low value

Typo in variable name: PretrainedMitchondriaVoxelSize.

The variable name has a typo - "Mitchondria" should be "Mitochondria".

✏️ Suggested fix
-  private lazy val PretrainedMitchondriaVoxelSize = VoxelSize(Vec3Double(7.96, 7.96, 31.2), LengthUnit.nanometer)
+  private lazy val PretrainedMitochondriaVoxelSize = VoxelSize(Vec3Double(7.96, 7.96, 31.2), LengthUnit.nanometer)

Also update the reference on line 156:

-      case "576500000000000000000002" => Some(PretrainedMitchondriaVoxelSize)
+      case "576500000000000000000002" => Some(PretrainedMitochondriaVoxelSize)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/aimodels/AiModel.scala` at line 149, Rename the misspelled
identifier PretrainedMitchondriaVoxelSize to PretrainedMitochondriaVoxelSize
across the file (and any references, e.g., the usage referenced near the
variable declaration) to correct "Mitchondria" → "Mitochondria"; update the
constant declaration name and all places that reference
PretrainedMitchondriaVoxelSize (such as the subsequent initialization/usage) so
identifiers remain consistent and compile.
frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_model_selector.tsx (2)

65-70: 💤 Low value

Silent early return when model has no category.

If model.category is falsy, the function returns early without any user feedback. While this shouldn't happen for properly configured models, it could lead to confusing UX where clicking a model does nothing.

Consider logging a warning or showing a toast notification:

💡 Suggested improvement
 const onSelectModel = (model: AiModel) => {
-  if (!model.category) return;
+  if (!model.category) {
+    console.warn(`Model ${model.name} has no category defined`);
+    return;
+  }
   const jobType = mapCategoryToJobType(model.category);
   setSelectedModel(model);
   setSelectedJobType(jobType);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_model_selector.tsx`
around lines 65 - 70, The onSelectModel handler currently returns silently if
model.category is falsy, causing no feedback; update onSelectModel to handle
that branch by emitting a clear warning or user-facing notification instead of
returning silently — e.g., call a toast/notification helper or console.warn with
contextual info mentioning the model (use model.id or model.name if available),
and avoid setting selected state when category is missing; ensure
mapCategoryToJobType, setSelectedModel and setSelectedJobType are only called
when a valid category exists.

185-187: 💤 Low value

Tag label doesn't reflect all category types.

The tag only distinguishes between EM_NEURONS ("NEURONS") and everything else ("INSTANCES"), but EM_MITOCHONDRIA models would more accurately be labeled "MITOCHONDRIA". Consider extending the logic to show more descriptive tags for each category.

💡 Suggested improvement
-                    <Tag>
-                      {item.category === APIAiModelCategory.EM_NEURONS ? "NEURONS" : "INSTANCES"}
-                    </Tag>
+                    <Tag>
+                      {item.category === APIAiModelCategory.EM_NEURONS
+                        ? "NEURONS"
+                        : item.category === APIAiModelCategory.EM_MITOCHONDRIA
+                          ? "MITOCHONDRIA"
+                          : "INSTANCES"}
+                    </Tag>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_model_selector.tsx`
around lines 185 - 187, The Tag rendering currently maps only
APIAiModelCategory.EM_NEURONS to "NEURONS" and defaults all other
APIAiModelCategory values to "INSTANCES"; update the render logic in
ai_model_selector.tsx (where Tag is used and item.category is read) to map each
enum value to a descriptive label (e.g., APIAiModelCategory.EM_NEURONS =>
"NEURONS", APIAiModelCategory.EM_MITOCHONDRIA => "MITOCHONDRIA", etc.)—replace
the ternary with a small switch/lookup keyed by APIAiModelCategory so new
categories show accurate tags and are easy to extend.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/models/aimodels/AiModel.scala`:
- Line 149: Rename the misspelled identifier PretrainedMitchondriaVoxelSize to
PretrainedMitochondriaVoxelSize across the file (and any references, e.g., the
usage referenced near the variable declaration) to correct "Mitchondria" →
"Mitochondria"; update the constant declaration name and all places that
reference PretrainedMitchondriaVoxelSize (such as the subsequent
initialization/usage) so identifiers remain consistent and compile.

In `@frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_model_selector.tsx`:
- Around line 65-70: The onSelectModel handler currently returns silently if
model.category is falsy, causing no feedback; update onSelectModel to handle
that branch by emitting a clear warning or user-facing notification instead of
returning silently — e.g., call a toast/notification helper or console.warn with
contextual info mentioning the model (use model.id or model.name if available),
and avoid setting selected state when category is missing; ensure
mapCategoryToJobType, setSelectedModel and setSelectedJobType are only called
when a valid category exists.
- Around line 185-187: The Tag rendering currently maps only
APIAiModelCategory.EM_NEURONS to "NEURONS" and defaults all other
APIAiModelCategory values to "INSTANCES"; update the render logic in
ai_model_selector.tsx (where Tag is used and item.category is read) to map each
enum value to a descriptive label (e.g., APIAiModelCategory.EM_NEURONS =>
"NEURONS", APIAiModelCategory.EM_MITOCHONDRIA => "MITOCHONDRIA", etc.)—replace
the ternary with a small switch/lookup keyed by APIAiModelCategory so new
categories show accurate tags and are easy to extend.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dc281518-8efe-4fb3-b10a-6194df96ccd0

📥 Commits

Reviewing files that changed from the base of the PR and between 19e7516 and e3fec7e.

📒 Files selected for processing (6)
  • app/controllers/AiModelController.scala
  • app/controllers/InitialDataController.scala
  • app/models/aimodels/AiModel.scala
  • frontend/javascripts/admin/voxelytics/ai_model_list_view.tsx
  • frontend/javascripts/types/api_types.ts
  • frontend/javascripts/viewer/view/ai_jobs/run_ai_model/ai_model_selector.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/controllers/InitialDataController.scala

@hotzenklotz hotzenklotz requested a review from fm3 April 30, 2026 14:12
Copy link
Copy Markdown
Member

@fm3 fm3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice stuff! Testing the happy path went well. I had a look at the backend code and already like the unification. I think we can go a little further in the cleanup. I added some comments and questions below, please have a look :-)

I think it would be great if someone from the frotnend team could have a look at the frontend changes.

The PR description doesn’t include the usual checkboxes, which I think are useful. For example, the migration guide entries are still missing.

Some(AiModelCategory.em_somata),
isSuperUserOnly = true,
isPretrainedModel = true
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be helpful to add the parameter names for all, not just the boolean parameters here. Otherwise all the Nones and List.empty are confusing.

case "576500000000000000000001" => Some(PretrainedNeuronModelVoxelSize)
case "576500000000000000000002" => Some(PretrainedMitchondriaVoxelSize)
case "576500000000000000000003" => Some(PretrainedNucleiModelVoxelSize)
case "576500000000000000000004" => Some(PretrainedSomataModelVoxelSize)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These strings should not be duplicated here and in InitialDataController. Maybe we could define them here as public variables, inject the AiModelService there and use them from it?

voxelSize <- dataStoreClient.getEffectiveAiModelVoxelSize(modelPath)
} yield voxelSize
case None =>
Fox.failure("Expected an AI model")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this function ever called with aiModelOpt=None? If not, it should just take aiModel: AiModel, then we don’t need the match and can just use if(aiModel.isPretrainedModel)

FROM webknossos.users_
WHERE _id = $requestingUserId
))
OR isPretrainedModel = TRUE
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
OR isPretrainedModel = TRUE
-- The pretrained models should be visible by all users of all orgas
OR isPretrainedModel

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What’s the idea behind isSuperUserOnly? Should they be hidden from non-superusers? Then this query should be adapted to reflect that. Currently it looks like the backend does not check this bool at all, neither in sql nor scala.

trainingAnnotations: AiModelTrainingAnnotationSpecification[];
name: string;
aiModelCategory: APIAiModelCategory.EM_NUCLEI;
aiModelCategory: APIAiModelCategory.EM_GENERIC;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So generic is always instance? Should the name maybe reflect that?

do $$ begin if (select schemaVersion from webknossos.releaseInformation) <> 161 then raise exception 'Previous schema version mismatch'; end if; end; $$ language plpgsql;

-- Remove pretrained models inserted by application startup
DELETE FROM webknossos.aiModels WHERE isPretrainedModel = TRUE;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
DELETE FROM webknossos.aiModels WHERE isPretrainedModel = TRUE;
DELETE FROM webknossos.aiModels WHERE isPretrainedModel;

_ <- organizationService.assertNoOrganizationsPresent
_ <- insertRootFolder()
_ <- insertOrganization()
_ <- insertPretrainedAiModels()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want this in production and not only in dev, this needs to be moved above assertInitialDataEnabled. Maybe while we’re here you could add a comment at assertInitialDataEnabled saying something like All insert calls below this assertion are only executed in the dev setup (where initialDataEnabled is true)!

Comment thread schema/schema.sql
modified TIMESTAMPTZ NOT NULL DEFAULT NOW(),
isDeleted BOOLEAN NOT NULL DEFAULT FALSE,
isSuperUserOnly BOOLEAN NOT NULL DEFAULT FALSE,
isPretrainedModel BOOLEAN NOT NULL DEFAULT FALSE,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
isPretrainedModel BOOLEAN NOT NULL DEFAULT FALSE,
isPretrained BOOLEAN NOT NULL DEFAULT FALSE,

We’re already in the model context here, so I’d just call it isPretrained. This propagates to scala and typescript as well.

private lazy val PretrainedSomataModelVoxelSize = VoxelSize(Vec3Double(179.84, 179.84, 224.0), LengthUnit.nanometer)

def findModelVoxelSize(aiModelOpt: Option[AiModel], usePretrainedNeuronModel: Boolean, dataStore: DataStore)(
def findPretrainedModelVoxelSize(pretrainedModelId: ObjectId): Option[VoxelSize] =
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def findPretrainedModelVoxelSize(pretrainedModelId: ObjectId): Option[VoxelSize] =
private def findPretrainedModelVoxelSize(pretrainedModelId: ObjectId): Option[VoxelSize] =


case class RunInferenceParameters(datasetId: ObjectId,
aiModelId: Option[ObjectId],
aiModelId: Option[String],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly you moved away from non-objectId-strings again, right? So this could be changed back to ObjectId?

Also, does it still need to be optional? In my understanding a model id is now always passed? This could then also simplify runInstanceModelInference and runNeuronModelInference quite a bit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add AI model for soma detection

2 participants