feat(enhance): discard, tests against a mock ComfyUI, docs (#192)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cameron CordesandClaude Opus 5.5 authored and cameron committed 2026-10-30 19:41:05 +00:00
1 parent 3a21761cfe
commit 97d9301a71
10 files changed
+1165 -48

No files matched your search

+89
View File
@@ -424,6 +424,94 @@ for the `link_versions` binary).
against the current hash; `unresolved` rows are listed, never guessed. `--apply`
takes a `VACUUM INTO` snapshot first.
### Enhance on demand (#192)
An **Enhance** action runs a ComfyUI workflow (a *template*) on one photo; the
user previews the result in the app and **approves** it, and it lands in the
library as a photo version (#191). Nothing reaches the library without an
approval. Code: `src/enhance/` (`config.rs`, `path_map.rs`, `template.rs`,
`comfy.rs`, `service.rs`, `handlers.rs`), table `enhance_jobs`
(`src/database/enhance_dao.rs`). The apps only talk to ImageApi; ComfyUI has no
auth and stays LAN-only, so ImageApi relays previews too.
- **Config: `enhance.toml`** (path from `ENHANCE_CONFIG_PATH`, else `./enhance.toml`;
gitignored; `enhance.example.toml` is the annotated schema and a test parses it).
No secrets live in it. Without `url` (or without the file) Enhance is off:
`GET /enhance/templates` answers `available: false`. A malformed file fails startup;
a *template* that doesn't validate is logged and skipped, the rest load.
Keys: `url`, `timeout_secs`, `probe_ttl_secs`, `poll_secs`, `idle_free_secs`,
`unreachable_grace_secs`, `default_template`, `warn_above_megapixels`,
`[[path_map]]` (`from`, `to`), `[[template]]` (`name`, `label`, `description`,
`file`, `prompt_mode`, `prompt_join`, `output_megapixels`).
- **Templates** are API-format workflow exports, read-only (never edit them from
here). Nodes are found by `_meta.title`: `imageapi:source` (required, gets the
photo path), `imageapi:save` (required, forced to `require_approval = true`),
`imageapi:prompt` (optional; fills the first of `text` / `prompt` / `value`),
`imageapi:reference` (optional). `prompt_mode = "append"`: the user's text is joined
after the template's own prompt with `prompt_join` (default one space); empty text
leaves the prompt as is. `"replace"`: the text *is* the prompt and is required (empty
= 400); it needs a prompt node. With no `reference_path` the reference node **and
every `["<id>", n]` input linking to it** are removed (its leftover export path must
never be used); if ComfyUI then rejects the prompt with `node_errors` the error says
"this template needs a reference photo". Every `seed`, `noise_seed` and `*.seed`
input gets a fresh random value per submit, or a retry would return ComfyUI's cached
result. A stray `SaveImage` is only a startup warning.
- **Path map**: library prefix <-> ComfyUI prefix (`/media/nas/` <-> `Z:\`), longest
prefix wins, separators converted, case-insensitive on the ComfyUI side; used on
submit and to recognise the path `/imageapi/approve` returns. No entries = identity.
A mapped drive only exists in the logged-in session; use UNC if ComfyUI becomes a
service.
- **Jobs** (`enhance_jobs`, keyed by the submitted file's `content_hash`): status
`queued | running | ready | approved | failed | expired | unavailable`. The poller
(`EnhanceService::spawn`, every `poll_secs`, active jobs only) reads `/queue` then
`/history/{id}`: in queue -> queued/running with `queue_position`; finished ->
`ready` (staged results in `pending`) or `failed` (ComfyUI's message); known to
neither -> **`expired`** (ComfyUI restarted and cleared its temp folder). An
unreachable ComfyUI for `unreachable_grace_secs` -> `unavailable`. A preview or
approve that finds the staged result gone also expires the job (HTTP 410).
- **Idle `/free`**: every 30 s, if the queue is empty, none of our jobs is active and
ComfyUI's newest `/history` entry has not changed for `idle_free_secs` (it includes
hand-run workflows), `POST /free {unload_models, free_memory}` is sent once per quiet
spell. "Not changed for" is measured on ImageApi's clock (no cross-host skew); the
first observation after ImageApi starts counts as activity. `idle_free_secs = 0` = off.
- **Approve** (`POST /enhance/jobs/{id}/approve`): ComfyUI's `/imageapi/approve` writes
the file (ImageApi writes nothing under library roots). The returned path is mapped
back, then a **targeted ingest** (`watcher::register_new_files` + `versions::link_new_files`)
registers and links just that file, so it is in `/image/versions` before the response.
Idempotent: an already-saved result returns its version without calling ComfyUI.
- **Output size**: Turbo output is about 1 MP regardless of the original, so
`output_megapixels` / `warn_above_megapixels` let clients warn above ~1.5 MP.
Endpoints (all need a bearer token; errors are `{"error": msg, "code": ...}`; `library`
is accepted as a query or body field like the versions endpoints):
```
GET /enhance/templates
-> {"available": true, "reason"?: "ComfyUI isn't running" | "Enhance isn't configured",
"default": "restore", "warn_above_megapixels": 1.5,
"templates": [{"name","label","description","accepts_prompt",
"prompt_mode": "append"|"replace", "prompt_required",
"accepts_reference","default","output_megapixels",
"warn_above_megapixels"}]}
POST /enhance {"path","template","prompt"?,"reference_path"?,"library"?} -> job
GET /enhance/jobs/{id} -> job
GET /enhance/jobs?path=&library= -> {"jobs": [job, ...]} newest first, discarded hidden
GET /enhance/jobs/{id}/preview/{n} -> the staged PNG (relayed from ComfyUI /view)
POST /enhance/jobs/{id}/approve {"n"} -> a /image/versions `versions[]` entry
POST /enhance/jobs/{id}/discard -> job (hidden from the list; ComfyUI untouched)
job = {"id","status","queue_position"?,"error"?,"path","library_id","template",
"prompt"?,"reference_path"?,"created_at","updated_at",
"pending": [{"n","preview": "/enhance/jobs/{id}/preview/{n}","approved"}],
"saved": [{"n","path","library_id"}]}
```
Error statuses: 400 bad request / empty required prompt / job not ready; 404 unknown
template, photo or job; 410 `expired`; 422 ComfyUI rejected the workflow; 502 upstream
or an approved file outside every library/path_map; 503 `unavailable` (ComfyUI down) or
`disabled`. `pending` is empty unless the job is `ready`/`approved`. Approve answers 202
`{"linked": false, ...}` if the file was saved but couldn't be ingested synchronously.
### File Processing Pipeline
**Thumbnail Generation:**
@@ -632,6 +720,7 @@ GET /image/versions?path=...&library=... (path of the original or any version
{ original:{path,library_id,hash}, versions:[{path,
library_id,hash,kind,source,created_at,references,
width,height}], display_hash })
POST /enhance ... (Enhance on demand; see "Enhance on demand")
PUT /image/versions/display (body { path, display_hash|null, library? }; global choice)
// /photos hides versions behind their original (include_versions=true lists them) and adds
// parallel arrays photo_display_paths / photo_version_counts; memories items, search hits and