Commit Graph

98 Commits

Author SHA1 Message Date
bvandeusen 13be9085b5 fix(integration): asyncio auto-mode, per-test truncation, artist images IN-subquery 2026-05-15 22:23:32 -04:00
bvandeusen 65a055408b ci(integration): reach services by bridge IP via docker socket, no host ports 2026-05-15 22:18:01 -04:00
bvandeusen cd8d0bd606 ci(integration): publish service ports, connect via localhost (host runner) 2026-05-15 22:02:43 -04:00
bvandeusen 827a4180a9 ci(integration): add pgvector+redis integration job; amend lint-only policy 2026-05-15 21:46:10 -04:00
bvandeusen 5faa19234d fix(fc2c-i): assert / redirect via navigation, not resolve() 2026-05-15 21:29:52 -04:00
bvandeusen 5a4663fa3d fix(fc2c-i): router falls back to memory history when no window (vitest) 2026-05-15 21:27:28 -04:00
bvandeusen 85d8b4b150 fix(fc2c-i): vitest needs plugin-vue to transform SFCs in router.spec 2026-05-15 21:15:04 -04:00
bvandeusen 4c1cabe4ee feat(fc2c-i): rich artist page with sparkline and FC-4 stubs 2026-05-15 21:14:07 -04:00
bvandeusen a984149ee2 feat(fc2c-i): tag directory view with previews, filter, search 2026-05-15 21:13:26 -04:00
bvandeusen c17c4b1667 feat(fc2c-i): showcase view with shortest-column masonry 2026-05-15 21:12:53 -04:00
bvandeusen 861e8baeb8 feat(fc2c-i): pinia stores for showcase, tag directory, artist 2026-05-15 21:12:19 -04:00
bvandeusen d166871be8 feat(fc2c-i): gallery accepts ?tag_id deep-link 2026-05-15 21:11:27 -04:00
bvandeusen 798d1a7fbc feat(fc2c-i): front-door redirect, /gallery move, discovery routes 2026-05-15 21:11:06 -04:00
bvandeusen 61f9401fde feat(fc2c-i): usePolyMasonry shortest-column distribution 2026-05-15 21:10:21 -04:00
bvandeusen 6bc7689f6e build(fc2c-i): add Vitest harness and run it in CI 2026-05-15 21:09:52 -04:00
bvandeusen 681d7777f3 fix(build): install CPU-only torch in ml image (drops ~5.6GB CUDA layer) 2026-05-15 21:04:26 -04:00
bvandeusen a4c7d44780 fix(fc2c-i): ruff I001 one-per-line import in test_artist_service 2026-05-15 16:27:55 -04:00
bvandeusen e43c2a0dd0 feat(fc2c-i): artist overview + paged images endpoints 2026-05-15 15:52:02 -04:00
bvandeusen 8484cb9aaa feat(fc2c-i): tag directory service and /api/tags/directory 2026-05-15 15:50:41 -04:00
bvandeusen a74b313596 feat(fc2c-i): showcase random-sample service and endpoint 2026-05-15 15:49:25 -04:00
bvandeusen a638c469e0 feat(fc2c-i): migration 0004 enables tsm_system_rows 2026-05-15 15:48:29 -04:00
bvandeusen f7f75fcac6 refactor(fc2c-i): sweep blueprints + ml tasks onto shared get_session (covers tasks/ml.py, a survey gap) 2026-05-15 15:48:02 -04:00
bvandeusen fc33c23dd5 refactor(fc2c-i): single shared async engine with lifecycle dispose 2026-05-15 13:53:42 -04:00
bvandeusen 0a4eb0bdc0 fix(fc2b): bare CHECK constraint names (naming-convention double-prefix)
Base.metadata's convention applies ck_%(table_name)s_%(constraint_name)s.
ml_settings and tag_allowlist passed already-prefixed names
(ck_ml_settings_singleton / ck_tag_allowlist_confidence_range), so the
ORM-side names came out doubled (ck_ml_settings_ck_ml_settings_singleton
etc.) and the migration-0003 smoke tests failed.

Same class of bug fixed in FC-2a for ImportSettings — should have applied
that lesson here. Bare names ('singleton', 'confidence_range') let the
convention produce the final names that match migration 0003's literal
DDL. Migration unchanged; only the model __table_args__.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:05:41 -04:00
bvandeusen 4ebe779b7c fix(fc2b): lazy-import onnxruntime in tagger (CI collection failure)
onnxruntime is in requirements-ml.txt only (deliberately kept out of the
lean web image and CI). The top-level `import onnxruntime` broke pytest
collection of test_ml_tagger / test_ml_suggestions / test_tasks_ml even
though those are pure-logic/integration-marked, because collection
imports the module.

Mirrors the embedder's lazy-torch pattern: onnxruntime is imported inside
Tagger.load(), placed AFTER the file-existence checks so
test_load_raises_when_model_missing still gets RuntimeError (not
ModuleNotFoundError) in onnxruntime-less environments. self._session
annotation dropped to a comment to avoid an eval-time ort reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:15:24 -04:00
bvandeusen 4623551bf6 fix(fc2b): ruff import sort in test_ml_tagger.py
ruff isort orders Tagger before TagPrediction (its case-insensitive
tiebreak). One-char autofix; ruff check now clean across backend/tests/alembic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:07:38 -04:00
bvandeusen cd7721fb03 feat(fc2b): Settings Maintenance tab
MaintenancePanel hosts: backfill + centroid recompute trigger cards,
the five suggestion-threshold sliders (autosave on slider release),
the allowlist table (inline editable min_confidence, delete), and the
alias table (mapping display, delete). Wired as a third Settings tab,
ML settings loaded lazily when the tab opens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:57:42 -04:00
bvandeusen 6cf82970a1 feat(fc2b): ml / allowlist / aliases Pinia stores
Thin wrappers over the FC-2b API: ml (settings GET/PATCH, backfill +
recompute triggers), allowlist (list, threshold patch, remove with
optimistic local update), aliases (list, remove with optimistic update).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:56:45 -04:00
bvandeusen 2a21ede912 feat(fc2b): wire SuggestionsPanel + rename into TagPanel; reject-on-remove
TagPanel now renders SuggestionsPanel below the current-tags area and
adds a per-chip kebab → Rename… (TagRenameDialog). modal store's
removeTag now also POSTs suggestions/dismiss after the delete so removing
an auto-applied tag records a per-image rejection and the allowlist
maintenance sweep won't re-apply it (closes the remove→re-apply loop
identified in the spec).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:56:22 -04:00
bvandeusen 5ae2154e88 feat(fc2b): AliasPickerDialog + TagRenameDialog
AliasPicker: debounced autocomplete scoped to the prediction's category
(artist/character map to tag kinds; copyright searches unscoped),
returns the chosen canonical tag id. RenameDialog: PATCH /api/tags/<id>,
surfaces 409 collisions with the FC-2c merge hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:55:40 -04:00
bvandeusen a8cc6a27dc feat(fc2b): suggestions store + modal panel components
Store: load per-image suggestions, accept (creates the tag first for
raw/creates_new_tag suggestions, then accepts by id), aliasAccept,
dismiss (client-side hide for raw tags), success toasts folded in.
Panel: people/sources groups always open, General collapsed by default;
alias picker dialog wired; shimmer skeleton while loading. SuggestionItem
shows score %, +new badge for raw tags, kebab menu. AliasPickerDialog
referenced (lands in Task 15).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:55:10 -04:00
bvandeusen aa4cc7c629 feat(fc2b): add tag rename (PATCH /api/tags/<id>) + /api/ml admin
Rename returns 409 on collision (frontend shows FC-2c merge hint), 400
on empty. ml_admin: GET/PATCH settings (thresholds + min_reference;
model versions read-only here), POST backfill / recompute-centroids
returning 202 + celery task id. Tests integration-marked, eager Celery.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:54:12 -04:00
bvandeusen 46338a1f2e feat(fc2b): add /api/allowlist and /api/aliases endpoints
Allowlist: list-all, get-one (404 if not listed), PATCH threshold
(range-validated), DELETE. Aliases: list-all (with canonical name),
create (idempotent, 201), DELETE by (string, category). Tests
integration-marked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:46:38 -04:00
bvandeusen 07961dfe04 feat(fc2b): add /api/images/<id>/suggestions endpoints
GET (ranked, category-grouped), accept (→ apply + allowlist; enqueues
retro-apply when newly added), alias (create alias + accept canonical),
dismiss (per-image rejection). Thin blueprint over SuggestionService +
AllowlistService. Tests marked integration, eager Celery.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:45:55 -04:00
bvandeusen b68a382b60 feat(fc2b): importer enqueues tag_and_embed + ml-worker model self-heal
import_media_file now enqueues tag_and_embed alongside generate_thumbnail
after a successful import. scripts/download_models.py snapshots Camie +
SigLIP into /models, idempotent (skips when present). The ml-worker
entrypoint runs it before starting the Celery worker so a fresh /models
volume self-heals on first boot. Downloader tests are pure-logic (no
network in CI).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:45:09 -04:00
bvandeusen bb48845268 fix(fc2b): add ML beat schedule entries (missed in Task 9 commit)
The celery_app.py beat-schedule edit failed with a stale-read error in
the Task 9 commit, so the ML daily jobs weren't registered. Adds
ml-backfill-daily, recompute-centroids-daily, apply-allowlist-sweep-daily.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:44:12 -04:00
bvandeusen 3e6cc8fffa feat(fc2b): add allowlist-apply + centroid recompute tasks + beat
apply_allowlist_tags: 4 modes (tag-only / image-only / both / full sweep),
matches a tag to a prediction either by direct name or via alias
(name, category) resolution, gates on per-tag min_confidence, skips
applied/rejected, applies source='ml_auto'. recompute_centroid /
recompute_centroids: async-bridged calls into CentroidService, delta-gated.
Beat: daily backfill, daily centroid recompute, daily allowlist sweep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:43:39 -04:00
bvandeusen ac7e0d13bc feat(fc2b): add tag_and_embed + backfill Celery tasks
tag_and_embed: Camie + SigLIP on one image (video → 10-frame sample,
max-pool tags, mean-pool embeddings), stores predictions/embedding with
model versions, then enqueues per-image allowlist apply. backfill:
keyset-paginated discovery of images missing predictions/embeddings for
the current model versions (restart-safe). apply_allowlist_tags stub
included so .delay() resolves between commits (filled in Task 9).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:40:27 -04:00
bvandeusen ed92548c0f feat(fc2b): add AllowlistService + TagService.rename
AllowlistService: accept (apply ml_accepted + add to allowlist + clear
rejection; returns whether newly-added so API can kick retro-apply),
add_alias_and_accept, dismiss, reject_applied_tag (remove + record
rejection so the allowlist won't re-apply), threshold update, remove,
list_all.

TagService.rename: refuses on (name, kind, fandom_id) collision with a
message pointing at FC-2c merge. Tests marked integration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:39:33 -04:00
bvandeusen 7860b86a13 feat(fc2b): add SuggestionService — alias-resolved, threshold-filtered, ranked
The read path: load tagger_predictions, drop unsurfaced categories
(rating/meta/year), apply per-category thresholds, batch-resolve aliases,
skip applied + rejected, augment with centroid hits above the similarity
threshold, merge duplicate signals (take max score, mark source 'both'),
group by category, sort by score DESC. Tests marked integration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:38:33 -04:00
bvandeusen dfa67d6437 feat(fc2b): add CentroidService — per-tag SigLIP centroids + similarity
recompute_for_tag (mean of member embeddings, eligible-kind + min-refs
gated, upsert), list_drifted (the delta-gate: member-count mismatch OR
missing OR wrong model version), find_similar_tags (pgvector cosine
distance, similarity = 1 - distance). Tests marked integration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:37:53 -04:00
bvandeusen 03c6a61673 feat(fc2b): add AliasService — (name, category) -> canonical tag
resolve() / resolve_many() (batch, used by the suggestion read path),
idempotent create, remove, list_all. Category-scoped so 'naruto' as
character vs copyright map to different canonicals. Tests marked
integration (real DB).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:37:13 -04:00
bvandeusen 696c17fe29 feat(fc2b): add SigLIP embedder wrapper
Direct port of ImageRepo's siglip.py. Lazy torch/transformers import so
the web container can import the module (for enqueue logic) without the
torch cost. EMBED_DIM=1152 asserted against the schema's Vector(1152)
columns. Real inference runs in the local integration suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:36:17 -04:00
bvandeusen 41fa26ed95 feat(fc2b): add Camie tagger ONNX wrapper
CPU-only, lazy-loaded, process-singleton ONNX session. Parses Camie's
string-category selected_tags.csv (vs WD14's integer Danbooru ids).
STORE_FLOOR (0.05) keeps the stored predictions JSON compact;
SURFACED_CATEGORIES gates which categories the suggestion UI shows
(meta/rating/year stored but never surfaced).

Inference itself isn't unit-tested (1GB model not in CI); the missing-
model error path and pure-logic surface are. Full inference runs in the
local integration suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:35:58 -04:00
bvandeusen 906804140c feat(fc2b): schema migration 0003 — ML pipeline tables
Renames image_record.wd14_* -> tagger_* (we're on Camie now, not WD14).
Adds tag_allowlist (auto-apply opt-in, per-tag confidence),
tag_suggestion_rejection (per-image dismissals), tag_alias (composite
(string, category) -> canonical tag, resolved at read time),
tag_reference_embedding (per-tag SigLIP centroids), and the ml_settings
singleton (per-category + centroid thresholds, model version pins).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:35:22 -04:00
bvandeusen aa9705882f fix: use npm install in Dockerfile frontend-builder stage (no lockfile)
Same fix as the CI workflow — `npm ci` requires package-lock.json and we
don't track one. Missed this in the earlier CI fix. Local `docker compose
up` builds now succeed past the frontend stage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:01:29 -04:00
bvandeusen 4aff9c557d chore: docker compose ergonomics — rename dev override, bake dev defaults
- Renamed docker-compose.dev.yml → docker-compose.override.yml so Docker
  Compose auto-merges it. `docker compose up` (no -f) now Just Works for
  local development.

- Removed the `env_file: .env` requirement from every service in the base
  file. Operators no longer need to create a .env to bring the stack up.

- Baked sane dev defaults directly into docker-compose.yml via
  ${VAR:-default} interpolation:
    DB_USER=fabledcurator
    DB_PASSWORD=fabledcurator_dev
    DB_NAME=fabledcurator
    SECRET_KEY=dev_secret_key_not_for_production_change_me
    LOG_LEVEL=INFO (overridden to DEBUG by the dev override)

  Defaults are insecure but explicitly named so. For production, override
  via shell env vars or a .env file at the project root.

- README quick-start simplified to a single `docker compose up -d`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 18:27:25 -04:00
bvandeusen 23d2fb24ac fix(fc2a): three unit-test failures — async fixture decorator, name convention, lazy celery includes
test_health.py:
  @pytest.fixture on an `async def` function is rejected by pytest-asyncio
  1.x strict mode. Switched to @pytest_asyncio.fixture.

backend/app/models/import_settings.py:
  My constraint name was 'ck_import_settings_singleton' and Base.metadata's
  naming convention applies 'ck_<table>_<name>' on top, so the final ORM
  name was 'ck_import_settings_ck_import_settings_singleton' (double prefix).
  The migration creates the DB constraint as 'ck_import_settings_singleton'
  via raw alembic, so they didn't match. Fix: bare name 'singleton' in the
  model → convention produces 'ck_import_settings_singleton', matching the
  migration's literal name.

tests/test_tasks_register.py:
  Celery's include=[...] parameter on the constructor is lazy — task
  modules aren't imported until a worker boots. The test only imported
  the Celery instance, so the @celery.task decorators in scan.py /
  import_file.py / thumbnail.py never ran. Fix: explicit `import` of
  those modules for side-effect at the top of the test file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:40:47 -04:00
bvandeusen 531ab6243e fix(fc2a): move pytestmark below imports in test_importer.py (E402)
Misplaced the integration marker between import groups in the previous
commit — I only read the top 12 lines so I missed the 3 more imports
below. ruff flagged it correctly as E402.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:34:46 -04:00
bvandeusen 22bc24b6b6 fix(fc2a): align CI with FabledRulebook — lint + short unit tests only
The CI failure resolving 'postgres' hostname was the symptom; the cause is
that the workflow violated FabledRulebook/forgejo.md's "CI philosophy —
lint + short unit tests only" rule. Integration tests against a real
Postgres are supposed to run locally via docker-compose, not in CI.

Changes:
- Marked 8 DB-dependent test files with @pytest.mark.integration:
  test_tag_service, test_importer, test_gallery_service, test_api_gallery,
  test_api_tags, test_api_settings, test_api_import_admin, test_maintenance.
- CI workflow drops the postgres/redis service containers and the alembic
  upgrade smoke step entirely.
- Pytest invocation in CI changes to `pytest -v -m "not integration"`.
- Added pytest marker registration to pyproject.toml.
- DB_PASSWORD and SECRET_KEY env vars retained because config.py reads
  them at import time even though unit tests don't actually use them
  (set to placeholder values).

What CI now runs:
- ruff check
- pytest on the 6 unit test files: test_slug, test_paths,
  test_migration_0002, test_thumbnailer, test_celery_smoke,
  test_tasks_register.
- npm install + npm run build

What CI no longer runs:
- alembic upgrade (no live DB)
- the 8 integration test files (these run locally via docker-compose)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:23:07 -04:00