11 Commits

Author SHA1 Message Date
bvandeusen b7d4998cdb feat(beat): self-gating schedules for ML backfill, centroids, auto-accept
Promotes three previously-manual maintenance tasks to Celery Beat schedules
so the user doesn't have to remember to run them:

- ml.backfill                              daily
- apply_auto_accept_predictions            daily
- recompute_all_centroids                  weekly

Cadences are env-overridable (ML_BACKFILL_EVERY_SECONDS,
AUTO_ACCEPT_EVERY_SECONDS, CENTROIDS_EVERY_SECONDS).

Each task self-gates so a scheduled run is a no-op when there's nothing
to do:

- ml.backfill: already self-gating — its first paginated query returns
  zero rows when no image is missing predictions/embeddings, the loop
  breaks, and the task returns. No code change.

- apply_auto_accept_predictions: adds a fast-path NOT EXISTS query that
  returns immediately when no WD14 prediction at/above the threshold
  exists for an unattached, non-rejected (image, tag) pair. The full
  walk only fires when fresh predictions have landed since the last run.
  Returns {'skipped_no_candidates': True} on the no-op path.

- recompute_all_centroids: tightens the aggregate query to LEFT JOIN
  tag_reference_embedding and skip tags whose stored reference_count
  already matches current image_tags membership count. Without this gate
  the daily-scheduled sweep would re-enqueue a recompute for every
  eligible tag every run, contending with tag_and_embed on the ml queue.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 00:06:09 -04:00
bvandeusen 6488dfff1a feat(maintenance): library-wide apply of auto-accept predictions
Adds a Celery task + Settings button that walks every image and applies
general-category WD14 predictions at or above the auto-accept threshold,
without needing the user to open each modal. Same side effects as the
modal's per-image auto-accept flow (creates kind='user' tag if needed,
attaches to image, writes SuggestionFeedback decision='accepted').

Lazy-imports app.services.tag_suggestions so the maintenance worker
doesn't pay its overhead unless this task fires. Amortizes _config()
and _existing_tag_names() across the loop. Commits per-batch (100
images) to keep transactions short and let later batches see freshly
created Tag rows.

Skips integrity-flagged images (get_suggestions already returns empty
for those). Idempotent — re-running just no-ops on already-applied
images via the existing 'tag not in img.tags' guard.

Trigger: Settings -> Maintenance -> "Apply auto-accepts library-wide"
(confirm-gated since it walks the entire library).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 13:45:34 -04:00
bvandeusen ce560d09a1 feat(integrity): structural verification + supersede-on-replace pipeline
Adds per-image integrity tracking so corrupt files are detected, excluded
from random/showcase/ML/suggestion paths, and recoverable by dropping a
fresh copy in /import — closing the gap that surfaced as the WD14
'6 bytes not processed' OSError.

Schema (migration l26042501)
- image_record.integrity_status: unknown | ok | truncated | unreadable | missing
- image_record.integrity_checked_at: timestamptz
- partial index on status <> 'ok' for cheap report/filter queries

Verifier
- app/services/integrity.py: verify_path() dispatches by extension
- PIL two-stage (verify + load with LOAD_TRUNCATED_IMAGES disabled)
- ffprobe for video, zipfile.testzip for archives
- Truncation-vs-unreadable distinction via PIL message hints

Pipeline
- verify_media_integrity Celery task: per-image, idempotent
- verify_unverified_images sweep: only_unknown by default, skips
  paths in active import tasks
- Hooked into the end of import_media_file (new + archive paths) and
  the supersede branch
- supersede_image() resets status to 'unknown' so the post-supersede
  verify writes a fresh truth
- Supersede-on-replace: a fresh /import/<artist>/<filename> matching
  a flagged-corrupt record routes through _supersede_existing,
  preserving tags/series/embeddings

Exclusions
- /, /api/random-images, tag_and_embed, ml.backfill enqueue, and
  get_suggestions all filter integrity_status IN ('ok', 'unknown') so
  flagged rows don't poison the gallery, ML, or suggestion math.
  'unknown' is treated as healthy so post-migration data stays visible
  until the sweep runs.

UI / report
- Settings -> Maintenance: 'Verify unknown' + 'Force re-verify all'
- GET /api/integrity/failed (paginated list of flagged rows)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 00:16:06 -04:00
bvandeusen 9276b31b99 feat(maintenance): backfill fandom tags onto legacy character attachments
New Celery task sync_character_fandoms_to_images on the maintenance
queue. For every character tag with a non-null fandom_id, runs the
same INSERT ... SELECT ... ON CONFLICT DO NOTHING that set_tag_fandom
already runs inline, attaching the fandom tag to every image that
has the character attached.

Why this exists: migration j26042101 extracted the '(Fandom)' suffix
from pre-refactor character names into tag.fandom_id, but it did NOT
walk image_tags to attach the fandom rows to every image that had
the character. Post-refactor auto-apply (in add_tag /
accept_image_suggestion / set_tag_fandom) only fires on NEW
add/accept events — pre-existing character attachments from before
the migration still show the character pill in the modal but no
fandom pill, because the fandom row was never added to image_tags.

Surfaced by Settings → Maintenance → 'Sync fandoms to images'.
Safe, additive, idempotent. Run once after the migration to close
the gap; subsequent set-fandom operations continue to maintain the
invariant inline.

Verified locally: seeded a character+fandom pair with only the
character attached to image 1; task added the fandom to image_tags;
final state has both rows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 08:58:51 -04:00
bvandeusen 5b7e033d0c feat(suggestions): auto-accept threshold + retroactive blocklist sweep
Two capabilities that work together to turn the suggestion system from
a 'review every noisy chip' UX into a 'curated tags only, configurable
auto-apply' UX for a solo-user library.

Auto-accept threshold
  Service (tag_suggestions.py):
    - New default auto_accept_general_threshold = 0.95 in _DEFAULTS
    - get_suggestions splits general-category hits at/above the threshold
      into a separate auto_accept_candidates list; the core 'character'/
      'copyright'/'general' keys stay the same shape for existing callers
    - get_bulk_suggestions ignores the new key (no side effects in bulk)
  Route (main.py GET /image/<id>/suggestions):
    - For each candidate: find-or-create Tag(kind='user', name=name), attach
      to image, log SuggestionFeedback(source='wd14'|etc, decision='accepted')
    - Response adds 'auto_accepted: [{id, name, display_name, kind,
      confidence, source}, ...]' so the modal can review + undo
  Config endpoints (main.py):
    GET  /api/suggestions/config/auto-accept-threshold
    POST /api/suggestions/config/auto-accept-threshold {threshold}
    Values > 1.0 effectively disable the feature
  UI (view-modal.js):
    - New renderAutoAccepted block at top of suggestions section with
      green-tinted chips carrying ✕ (undo for this image, logs rejection)
      and ⊘ (blocklist + remove from all images)
    - loadSuggestions refreshes the tag pill list when auto_accepted has
      items so the user sees them in the Tags section too
  Settings:
    - Maintenance tab gains a number input + save button backed by
      /api/suggestions/config/auto-accept-threshold

Retroactive blocklist sweep
  New Celery task app.tasks.maintenance.sweep_blocklisted_tag_from_images
  on the maintenance queue. Enqueued automatically when a name is added
  via POST /api/suggestions/blocklist or appears new in the bulk replace.
  Task body: finds kind='user' Tag matching the name, deletes its
  image_tags rows, deletes the Tag itself. Scope limited to kind='user'
  so deliberate character/fandom/artist tags sharing a blocklisted name
  aren't silently destroyed.

Verification (local dev):
  - Threshold GET/POST round-trip correct (default 0.95, persisted in
    tag_suggestion_config)
  - Image 633 at threshold=0.9: 4 general tags auto-applied, each with
    a SuggestionFeedback row, returned in auto_accepted
  - Blocklist add of 'sweeptestonly' with an attached test tag: Redis
    maintenance queue depth = 1, task body removes 1 image_tags row +
    the Tag row
  - get_bulk_suggestions still works (auto_accept_candidates key skipped)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 17:23:25 -04:00
bvandeusen 934d6ce111 chore: remove sync_character_fandoms maintenance task
tag.fandom_id is now authoritative. set_tag_fandom auto-applies the
fandom tag inline; add_tag already did. There's no drift to sync on
a schedule anymore.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 15:54:08 -04:00
bvandeusen 0f35a0c484 feat: ML tag suggestions, character/fandom integrity, underscores, modal polish
Consolidated merge of feat/tag-suggestions branch. Original 64-commit history
was lost to git-object corruption in a Nextcloud-synced checkout; this single
commit captures the equivalent diff.

Includes:
- pgvector-backed tag suggestion infra (WD14 + SigLIP centroids, ml-worker
  container, Celery tasks, suggestion service, accept/reject endpoints + modal
  UI with green/red chip buttons)
- Character/fandom integrity: title-case normalization on every write path,
  fandom-id backfill, maintenance task + settings button, migrations g26041901
  + h26041901 to canonicalize legacy rows with case-only duplicate merging
- Tag-underscores + modal polish: WD14 name canonicalization at emit + accept
  + add/bulk-add paths, migration i26041901 for legacy-row rename-or-merge
  across character/fandom/NULL kinds, suggestion-accept refresh parity via
  awaited loadTags, persistent chip tint
2026-04-19 19:50:58 -04:00
Bryan Van Deusen 09883960d4 tuning job log views and deep scan archive processing. 2026-02-02 18:48:01 -05:00
Bryan Van Deusen d0fcde38e8 ui polish and importer logging tuning 2026-01-28 09:36:59 -05:00
Bryan Van Deusen 87bcb633f6 added transcoding to mp4 in import to ensure web playback. 2026-01-20 16:10:19 -05:00
Bryan Van Deusen cb3897c0b0 changes to mobile styling in modal view, complete reword of backend worker system 2026-01-20 13:14:13 -05:00