Skip to content

Vision Analysis

Vision analysis turns an uploaded image into structured JSON: a description, scene, detected objects, text/logos, vehicles, safety flags, and inferred context (time of day, country, source camera, weather). It runs entirely locally through an Ollama vision model (VLM). Routes live in backend/src/routes/vision.ts, mounted at /api/vision.

This is distinct from the in-chat “send the companion a photo” path: that attaches images directly to the companion’s chat model and is not part of this subsystem.

Model

The VLM is resolved by getVisionModel() (backend/src/lib/models.ts); the default catalog vision model is gemma3:4b. GET /api/vision/status reports { available, model } by checking the model against ollamaList() (matching the exact name or the base tag prefix).

Multi-pass inference

Rather than one prompt, POST /api/vision/analyze runs several focused passes in parallel via Promise.all, each with its own prompt and JSON schema, at low temperature (0.1, num_ctx: 4096). Each pass is a single ollamaChat() call with the image as a base64 attachment and a JSON schema passed as the structured-output format. runPass() parses the response, falling back to a {...} regex extraction if the model wraps the JSON.

PassPrompt constSchemaOutput
ContextPROMPT_CONTEXTSCHEMA_CONTEXTdescription, scene, inference (timeOfDay, country, sourceType, sourceBrand, weather, summary)
ObjectsPROMPT_OBJECTSSCHEMA_OBJECTSobjects[] (label, confidence, area)
VehiclesPROMPT_VEHICLESSCHEMA_VEHICLESvehicles[] (type, brand, model, plate, plateState, color, area)
TextPROMPT_TEXTSCHEMA_TEXTtext[] (value, language, type, area)
SafetyPROMPT_SAFETYSCHEMA_SAFETYsafety[] (hazard, context, assessment, reason, area)

area is constrained to a 3×3 screen-region enum (top-leftbottom-right). The context and safety passes always run; the objects, vehicles, and text passes run only when requested (or when no tasks are specified, i.e. “run all”). Known task tokens (AnalysisTask): description, scene, objects, text, vehicles, language.

Safety cross-reference

After the passes complete, the merge step scans objects[] for weapon and fire terms (WEAPON_TERMS, FIRE_TERMS) and promotes any that the dedicated safety pass missed into safety[] (weapons → critical, fire → concerning), so a hazard flagged by object detection is never silently dropped. Assessment levels are normal | concerning | critical.

Storage

analysisResults (backend/src/db/schema.ts):

ColumnNotes
idUUID; source image saved at data/analysis/{id}.{png|jpg}
userIdowner (cascade delete)
pathsource image path
resultJSON of the merged AnalysisResult
modelVLM used
tasksJSON string[] of requested tasks
statebuilding | ready | failed
errorfailure message
createdAttimestamp

A building row is inserted before inference; it flips to ready (with result) on success or failed (with error) on any pass throwing.

Routes (/api/vision)

All require auth and scope to the calling user.

Method + pathPurpose
GET /statusVLM availability + model name
POST /analyzeMultipart (image file + optional tasks JSON); runs the passes, persists, returns { id, result, model, state }
GET /historyRecent ready results (limit ≤ 50)
GET /results/:idA single result with state/error
GET /artifacts/:idServe the stored source image (immutable cache)
DELETE /artifacts/:idDelete the row and source image

Where it surfaces

  • Imaging page (frontend/src/pages/ImagingPage.tsx): the Recognize tab drives POST /api/vision/analyze through the useImageAnalyze hook, letting the user pick which passes to run (objects, text, vehicles, etc.) and rendering the structured result, including safety flags.
  • The VLM is also reused outside this subsystem by image generation’s mid-flight POST /api/image/preview-check, which runs the same vision model (forced to CPU, num_gpu: 0) against an in-progress preview frame.

The generation queue exposes a separate vision slot type (alongside chat and image) so VLM passes are rate-limited independently of image generation.