A busy GPU (llama-swap holding the 3090) made ffmpeg's CUDA setup fail
with CUDA_ERROR_OUT_OF_MEMORY, exit 187. The playlist generator took
that for a bad file and wrote playlist.unsupported, so the video never
played; preview clips were marked failed the same way.
is_gpu_init_failure() recognises CUDA/NVENC setup errors in ffmpeg's
stderr. When an NVENC run fails that way, the playlist and the preview
clip rerun once on libx264. A file that also fails on the CPU still gets
the sentinel. The HLS command and the timed run are pulled out of the
handler (hls_command, run_ffmpeg) so both attempts share them.
Fixes#215
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8090 is held by a long-running service on the dev box, so the e2e server
(and the app's drive-app / Detox, which read E2E_PORT too) move to 8093.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /image/favorites had no ORDER BY, so SQLite returned rows in index
order (by path). Order by id descending so the app's Favorites view
(FileViewer #73) shows what was just added on top.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /enhance/jobs without a path now answers the jobs manager (FVR #99):
every photo's queued / running / unavailable jobs, plus ready / failed /
expired ones touched in the last 3 days, newest first, discarded and
approved hidden, at most 100. DB only; the poller keeps rows current.
An optional status=a,b filter applies to both forms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Derived clips (#208) aren't counted as versions, so a photo whose only
derived file is a clip looked like a plain photo, and clients never fetched
/image/versions for it. Summaries now carry clip_count: /photos adds the
parallel array photo_clip_counts, records (memories, search, /photos/exif)
carry clip_count. All skipped when zero, so other responses are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Groundwork for Enhance "Animate" (#208): a video made from a photo.
- Version linking reads the XMP link from MP4 / MOV too (top-level uuid box
with the XMP UUID, mdat seeked over), at ingest and on Enhance approve.
- New kind `animated`, a derived clip (CLIP_KINDS): hidden behind the photo
like a version, but not counted in photo_version_counts / version_count,
never a display choice (400 clip_not_displayable), and listed as `clips`
in GET /image/versions. Approve returns the clip entry.
- Templates whose imageapi:save is ImageApiSaveVideoVersion report
output "video" in /enhance/templates (no enhance.toml change). Previews
already relay ComfyUI's content type (video/mp4).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SeedVR2VideoUpscaler (the new Sharpen / Rescue templates) rejects seeds
above 2^32 - 1 at validation, and the 52-bit seeds made every job fail with
"Value ... bigger than max of 4294967295: seed". 32 bits is still plenty of
variety for the Fibo templates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves every remaining hand-built error response onto ApiError: ~300
sites across faces, image, video, files, memories, versions,
duplicates, favorites, libraries, auth, clip/unified search, prefs,
reels, tts, insight chat, persona chat, knowledge and personas.
- 500s: the body was the error's text (anyhow / diesel / {:?}); now
ApiError::internal logs it and sends a generic message.
- Each 4xx/502/503 has a stable code and a message written for users;
statuses are unchanged. Shared helpers per module (faces_unavailable,
person_not_found, unknown_library, insight_not_found, ...).
- Pre-format top-level fields kept where clients read them: upload 409
duplicate_of / content_hash / library_name.
- PersonaChatError converts into ApiError in one place.
- Multipart upload errors use the format (error::multipart_config).
- Guard test: no non-test code builds HttpResponse::<error>() by hand.
Closes#202
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- error::ApiError (ResponseError): status + stable snake_case code +
display-safe message + optional details. From<anyhow>/From<diesel> and
into_http_internal_err become a 500 "internal" whose cause is logged,
never sent.
- JSON body, query and path parse failures, the Claims 401 and unknown
routes now answer in the format instead of actix's plain text.
- tags handlers return ApiResult with codes (invalid_tag_name,
tag_not_found, tag_name_taken, no_files, too_many_files,
nothing_to_change, tag_added_and_removed). The rename 409 keeps
existing_tag at the top level for older clients.
- Enhance's error_response builds through ApiError; its codes are
unchanged, and an internal error no longer sends its message.
- duplicates: eight 500s sent format!("{:?}", e) to the client.
Refs #202
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- POST /image/tags/bulk { file_names, add: [names], remove: [ids] }: one
transaction over many photos; names match case-insensitively and are
created when missing; 400 if a tag is both added and removed.
- Adding a tag a photo already has is a no-op (was a 500 on
UNIQUE(rel_path, tag_id)).
- Adding a tag no photo carries reuses it (the lookup only saw tags in
use, so create_tag hit idx_tags_name_nocase: 500). update_tags could
likewise skip such ids silently.
- Removing a tag matches the name case-insensitively and deletes it from
every path sharing the photo's content, as reads union them; it used to
answer 200 and leave the tag on the other copy.
- update_tags returns errors instead of panicking with the TagDao lock
held, which poisoned it for every later tag request.
Closes#200
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Parse execution_start vs the end event from /history, store it when the
poller moves a job to ready/failed (never overwritten with null), and
return it as "run_ms" on every job endpoint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The daily-summary cleaner hard-coded two contacts' names in its opening
phrases; a pattern now strips "Today, <Name> and I discussed" for any
contact (with a test). Video path tests use a neutral folder name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
link_versions only ran the ComfyUI LoadImage match on PNGs inside an
aiedits/ folder, so early outputs saved to a sibling folder (e.g.
edited/) or beside their original were never proposed. Run it for any
PNG whose prompt names a loaded image (still review-only), and search
the output's own folder before its parent when it isn't in aiedits/.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A version no longer hides behind an original that duplicates resolve
demoted (visible_version_clause, hidden_version_rows).
- Recording a link drops the version's faces, CLIP data and clip failures
(covers link-at-ingest and link_versions --apply).
- The display file must live in the original's library; otherwise the
original shows, and PUT /image/versions/display answers 400.
- link_new_files looks up hashes in chunks of 500 and logs errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixtures gain an XMP-linked version (renamed file), a name-linked version
and an edit with a reference photo; e2e.sh counts with include_versions and
runs check_versions.py. CLAUDE.md documents the feature.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dry run by default: prints a TSV of proposed links. XMP (PNG DerivedFrom,
incl. files already ingested) and file-name links are exact and apply
without review; ComfyUI prompt LoadImage inputs, Upscayl-style names and
nearest pHash are heuristic and apply only from a reviewed TSV, checked
against current hashes. Unresolvable cases are listed, not guessed. --apply
takes a WAL-safe VACUUM INTO snapshot first and aborts if it fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
XMP (PNG iTXt DerivedFrom, native chunk reader) then file-name convention
links at ingest; unlinked name candidates are retried each tick. Versions
are hidden from /photos (SQL for the recursive listing, hidden-row set for
the walk and tag branches), /memories, reels, /photos/exif, the GPS summary,
unified search and the media gauges while their original is reachable in the
view; include_versions=true lists them. /photos gains the parallel arrays
photo_display_paths / photo_version_counts, memories / search hits /
/photos/exif rows gain display_path / version_count, all skipped when empty.
GET /image/versions and PUT /image/versions/display (library optional).
Tags and favorites on a version path bind to the original; faces, CLIP and
duplicate grouping skip version hashes; insight generation redirects to the
original. Orphan GC and the back-ref refresh cover the new tables.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
photo_versions links a version's content_hash to its primary original's;
photo_version_references keeps provenance-only reference photos;
photo_version_display holds the global display choice. VersionDao covers
linking (chains flatten to the root), the display choice, per-original
summaries for /photos, the hidden-row set for non-SQL listings, and the
copy of the original's date/GPS/camera onto version rows. Also the SQL
predicate listings use to hide a version while its original is reachable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README, CLAUDE.md and .env.example now point at llm.toml for LLM settings
(OPENROUTER_API_KEY stays in env) and describe the legacy env override
window, the discovered picker and the startup cross-check. ADR-0003 records
the move and amends ADR-0002's deployment-config home; CONTEXT.md's
Deployment config / Hard default entries follow. Source comments and the
"set LLAMA_SWAP_URL" error strings name the llm.toml keys.
Also fixes README's AGENTIC_MAX_ITERATIONS default (10 -> 12, what the
code uses).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After AppState is built, main checks the config against llama-swap's
/v1/models in the background and logs one warning per problem: a role
model (chat, vision, embedding, unified_search, tts) that isn't served or
lacks what the role needs (vision, kind: embed / tts), an embed model
whose metadata dim differs from embedding.dim, picker include/exclude ids
llama-swap doesn't serve, and models with no metadata. Warn-only: llama-swap
may still be starting. Chat-side roles are only checked under
backend = "llamacpp"; TTS always is.
Against the deployed .env this reports exactly the three stale
LLAMA_SWAP_ALLOWED_MODELS ids.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>