API¶
The UI talks to the backend through named commands with JSON arguments
(Command API). In the desktop app they go
through the api Tauri command; in development, through
POST /api/commands/{name} (Running the UI in a Browser). Types
are generated into ui/src/lib/api/generated/.
Errors are { "code": ..., "message": ... } with code one of
invalid_arguments, unknown_command, not_found and
internal. Messages are
written to be shown to users.
Commands¶
app_infoNo arguments.
{ "name": "Picturesque", "version": "0.1.0" }.get_settings,update_settingsSee Settings.
template_presetsNo arguments. The built-in templates:
[{ "id", "label", "template" }].preview_template{ "template": "..." }.{ "error": null, "examples": [...] }: either the reason the template is invalid, or the paths it produces for three sample photos (RAW+JPEG with a place, a phone photo without location, an undated scan).query_photosA
PhotoQuery; every field is optional and filters combine:{ "from": "2026-10-01", "to": "2026-10-31", "country_code": "PT", "city": "Lisbon", "camera": { "make": "Canon", "model": "Canon EOS R5" }, "lens": "RF50mm F1.2 L USM", "flags": ["pick", "unflagged"], "min_rating": 3, "sort": "captured_desc", "limit": 200, "cursor": null }
sortiscaptured_desc(default),captured_asc,imported_descorquality_desc(combined score, best first; photos not analyzed last). With the capture-date sorts, photos without a capture date come last. Returns{ "photos": [PhotoSummary], "next_cursor": "...", "total": 1234 }. Passnext_cursorback ascursorfor the next page; it isnullon the last page. Paging is keyset-based, so later pages are as fast as the first.limitis 1–1000.cameramatches every spelling of the camera with the same display name (Canon/EOS R5andCanon/Canon EOS R5).min_sharpness,min_exposureandmin_noise(0–1) keep photos whose technical score is at least that (Quality scores); photos not analyzed yet never match.min_aesthetic(0–1) does the same for the aesthetic score,hide_closed_eyes: truehides photos where someone important has closed eyes, andpeoplekeeps only photos with (true) or without (false) faces.auto_picked: truekeeps auto-picked photos (Auto-pick); withflags, photos matching either.best_only: truehides photos that are in a series but not its best photo.photo_ids(at most 1000) limits the result to those photos.collapse_series: truelists one photo per series: the user’s chosen best photo if it matches the other filters, else the best-ranked matching one. Series listed inexpanded_series(byseries_id) are shown whole. Facets (photo_facets) take the same fields, so day counts match the list.A
PhotoSummaryhasid,captured_at,day(YYYY-MM-DD, for grouping),camera,lens,city,country,flag,rating, displayedwidth/height,has_raw,file_name,series_id(the first photo of its series, ornull),best_in_series,series_size(0 when in no series) andquality(the combined score, Combined score;nulluntil analyzed),aesthetic(0–1 ornull),eyes_closed(true/false,nullwhen undecided),faces(count) andkeeper(suggested by auto-pick; the photo shows as auto-picked while itsflagisunflagged). Facets includeauto_picked, the number of auto-picked photos.get_photo{ "photo_id": 7 }. APhotoDetail: capture time and UTC offset, camera, lens,iso,aperture(f/2.8),shutter(1/250 s),focal_length(35 mm), GPS,place, displayed size,flag,rating,files(path, format, size) andquality(sharpness,exposure,noise,overall,highlights_clipped,shadows_clipped;nulluntil analyzed, see Quality scores) andaesthetic(score0–1 and the predictor’srating, about 1–10;nulluntil the CLIP model analyzed the photo, see Aesthetic score) andfaces(largest first; each withx,y,width,heightas fractions of the photo, detectorscore,eyes_closed—true,falseornullwhen undecided —eye_opennessandsharpness0–1;nulluntil the face models analyzed the photo, see Faces and closed eyes),score, the combined quality 0–1 (Combined score), andreasons: why it scored so, most important first, each{ "tone": "good" | "bad" | "info", "text": "Eyes closed: the score is halved" }. Unknown photos arenot_found.set_decisions{ "changes": [{ "photo_id": 7, "flag": "pick" }, { "photo_id": 8, "rating": 4 }] }. Each change sets a photo’sflag(unflagged,pick,reject),rating(0–5), or both; omitted fields stay as they are. Returns each photo’s resulting{ "photo_id", "flag", "rating" }. All changes are validated first: an unknown photo (not_found) or a rating above 5 (invalid_arguments) changes nothing.photo_facetsOptional
{ "filter": PhotoQuery }. With a filter,total,first_day,last_dayanddayscount only matching photos (they drive the timeline layout); the option lists below always cover the whole library. The values present in the library with photo counts:days(newest first),cameras(with display labels; spellings of one camera are one entry, keyed by the most used),lenses,places,flags,ratings(counts for 0–5 stars), plustotal,first_dayandlast_day.library_statsNo arguments. Keepers per camera and lens and focal lengths per camera (Statistics):
{ "all": { "photos": 812, "picks": 120, "auto_picked": 64, "rejects": 230 }, "keeper_rate": 0.23, "cameras": [{ "label": "Canon EOS R6", "camera": { "make": "Canon", "model": "Canon EOS R6" }, "lens": null, "counts": { "photos": 500, "picks": 90, "auto_picked": 40, "rejects": 150 }, "keeper_rate": 0.26, "few_photos": false, "focal_lengths": [{ "value": 35, "count": 210 }, { "value": 85, "count": 120 }] }], "lenses": [{ "label": "RF35mm F1.8", "camera": null, "lens": "RF35mm F1.8", "...": "..." }], "min_photos": 20 }
A keeper is a picked or auto-picked photo;
keeper_rateis keepers per photo, 0–1.camerasandlensescome most successful first: by keeper rate, then by photos, with gear of fewer thanmin_photosphotos (few_photos) last.focal_lengths(cameras only) counts photos per focal length in whole millimetres, most used first.reanalyze_photosOptional
{ "photo_ids": [7, 8] }; without it, every photo. Marks the photos’ analysis results stale so background analysis runs again (Analyzing photos again), the given photos first. Stale results stay in use until replaced. Returns{ "photos": 2 }: how many of the photos exist (unknown ids are ignored).ai_statusNo arguments. Where local AI models run (Local AI):
{ "available": ["webgpu", "cpu"], "choice": "auto", "provider": "webgpu", "problem": null }
availablelists the providers of this build, preferred first.provideris the one a tiny test model actually ran on (nullif even the CPU failed);problemsays why the chosen provider was not used. The check runs once per choice.start_export{ "destination": "/home/me/Export", "query": { "flags": ["pick"] }, "format": "jpeg", "naming": "{date}_{seq}.{ext}", "include_raw": true, "include_sidecars": true, "max_edge": 2048, "quality": 90 }; onlydestinationis required (default: the picks, originals, original names). Starts an export in the background (Exporting) and returns anExportStatus:{ "state": "running", "destination": "...", "done": 0, "total": 12, "files": 0, "failed": [] }(stateisidle,running,completedorcancelled;failedlists{ "photo_id", "error" }). Invalid options, or an export already running, areinvalid_arguments.export_status/cancel_exportNo arguments. The current
ExportStatus; cancelling stops after the photo being exported.plan_reject_moveNo arguments.
{ "photos": 12, "files": 30, "bytes": 912345678, "undo_batch": 3, "undo_photos": 8 }: rejected photos not moved yet, and the latest move that can be undone (Moving Rejects).move_rejectsNo arguments. Moves every rejected photo’s files into
_Rejected. Returns{ "batch": 4, "moved": 11, "failed": [{ "photo_id": 7, "error": "..." }] }; failed photos stayed where they were.undo_reject_move{ "batch": 4 }. Moves the latest batch back; another batch isinvalid_arguments. Returns the same shape asmove_rejects.similar_photos{ "photo_id": 7, "limit": 50 }(limit1–500, default 50). The photos that look most like this one, most similar first, by CLIP embedding (Image embeddings):[{ "photo_id": 12, "similarity": 0.93 }]. Empty until the photo has an embedding; unknown photos arenot_found.search_photos{ "text": "dog on a beach", "limit": 200 }(limit1–1000, default 200). Photos matching the description, best first, by CLIP (Text search):[{ "photo_id": 12, "similarity": 0.23 }]. Photos below a similarity of 0.17 are left out. Empty text, or a missing CLIP model (the message says which), isinvalid_arguments.list_modelsNo arguments. The downloadable AI models (Local AI), each a
ModelView:{ "id": "clip-image", "name": "CLIP ViT-L/14 image encoder (OpenAI)", "purpose": "...", "version": 1, "license": "MIT", "license_url": "https://...", "homepage": "https://...", "size": 1216297719, "state": "downloading", "downloaded": 12000000, "error": null }
stateisnot_installed,downloading,installedorfailed(witherror). Never uses the network.install_model/cancel_model/remove_model{ "id": "clip-image" }. Start a download in the background, stop it, or delete the model’s files; each returns theModelView. Removing a model while it downloads isinvalid_arguments; unknown models arenot_found.analysis_statusNo arguments.
{ "analyzed": 120, "total": 300, "running": true, "paused": null }: photos with every analysis done, all photos, whether workers are busy, and why analysis waits (a full disk), if it does (ADR-0011: Resumable Background Analysis).prioritize_analysis{ "photo_ids": [7, 8, 9] }. Analyzes these photos next; the library sends the photos on screen. Unknown or finished photos are ignored. Returns theanalysis_status.photo_series{ "photo_id": 7 }. The photo’s series:{ "photo_id": 7, "series_id": 5, "photo_ids": [5, 6, 7], "edit": null, "best_photo_id": 6, "best_chosen": false, "ranking": [{ "photo_id": 6, "score": 0.97 }, { "photo_id": 5, "score": 0.81 }, { "photo_id": 7, "score": null }] }
series_idis the first photo’s id (null, with empty lists, when the photo is in no series);editis the user’s correction before this photo;rankinglists members best first with scores (nulluntil analyzed);best_chosensays whether the user chose the best photo. Unknown photos arenot_found.edit_series{ "photo_id": 7, "edit": "split" }. Sets the correction before a photo:split(it starts a new series),join(it joins the series of the photo before it) ornull(automatic). Series are recomputed immediately; returns the photo’sphoto_series(Series).choose_best{ "photo_id": 6, "best": true }. Makes the photo the best of its series (the latest choice wins);"best": falsewithdraws the choice. Returns the photo’sphoto_series.
list_import_sourcesNo arguments. Every
ImportSource:id,path,watch,scanned_atandscan_error.add_import_source{ "path": "/absolute/folder", "watch": true }(watchdefaults totrue). Stores the folder and scans it right away; returns theImportSource. Adding a stored folder again returns it unchanged. Fails if the path is relative, cannot be read, or is inside or contains the library folder.remove_import_source{ "id": 1 }. Forgets the folder and drops its waiting photos; imported photos stay. Returns the remaining sources.set_import_source_watch{ "id": 1, "watch": false }. Returns theImportSource.scan_import_sources{ "id": 1 }scans one source; withoutid, all of them. Returns thePipelineStatusat once; the scan runs in the background.pipeline_statusNo arguments. The
PipelineStatus: number of photosqueued,imported,duplicatesandfailed; theactivephoto (job_id,file_name,stage) ornull; whether a scan is running; andpaused(why importing waits, for example no library folder) ornull.pipeline_jobs{ "filter": "all", "limit": 200, "before": 1234 }(all optional). Photos in the pipeline, newest first, asJobView:id,file_name,source_path,files,stage(queued,reading,copying,imported,duplicate,failed),error,failed_stage(readingorcopying, for failed photos),photo_id,updated_at, and for imported photos whether thethumbnailis ready andanalysisprogress as[done, total].filterisall,active,imported,duplicateorfailed; page withbefore(the lastid).pipeline_log{ "min_level": "info", "limit": 200, "before": 5678 }(all optional). Log entries newest first:id,at,level(info,warn,error),job_id,photo_idandmessage.retry_failed_imports{ "ids": [3, 4] }queues those failed photos again; withoutids, all failed photos. Returns thePipelineStatus.
Images¶
Photo images are served as JPEG files, not JSON, so they can be used
directly as <img src>:
Where |
URL |
|---|---|
Desktop app |
|
Development server |
|
size is grid (longest edge 512 px), preview (2048 px) or
full (native size, for 100% zoom).
Images are generated on first request and cached
(Data Locations). Responses carry an ETag; a request with a
matching If-None-Match gets 304 Not Modified. An unknown photo is
404, a malformed path 400.
Events¶
Events are JSON objects with a type field. In the desktop app they
arrive as the Tauri event app-event; in development, as server-sent
events from GET /api/events.
pipeline_progressA
PipelineStatus(seepipeline_status), sent at most four times a second while the pipeline works, and whenever a photo finishes or a scan starts or ends.analysis_progressThe
analysis_status, sent after each analyzed photo.series_changed{ "series": 12, "photos": 57 }: series membership changed (after analysis, an import, an edit or a settings change).export_progressAn
ExportStatus(seestart_export), sent about five times a second during an export and when it ends.model_progressA
ModelView(seelist_models), sent about four times a second while a model downloads, and when a download ends or a model is removed.
Example:
{ "type": "pipeline_progress", "queued": 280, "imported": 120,
"duplicates": 3, "failed": 1, "scanning": false, "paused": null,
"active": { "job_id": 124, "file_name": "IMG_0124.CR3", "stage": "copying" } }