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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Ruff:
The remaining I001 errors came from ruff treating `alembic` as a first-
party module (because the alembic/ directory exists in the repo root)
rather than third-party. Ran `ruff check --fix` locally — auto-sorted
import groupings to put alembic/sqlalchemy alongside backend.* as first-
party, and trimmed redundant blank lines after a few import blocks.
Frontend:
`npm run check` (vue-tsc --noEmit) was failing because vue-tsc has no
tsconfig.json to read against, and the frontend is pure JS without
JSDoc annotations — vue-tsc had nothing to do. Skipping the step until
we add a tsconfig + convert to TS or add JSDoc annotations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ruff lint surfaced 23 violations across three rules; all addressed:
UP017 (Use datetime.UTC alias):
Replaced 13 sites of datetime.now(timezone.utc) with datetime.now(UTC),
also adjusted from-imports accordingly. UTC is a Python 3.11+ alias for
timezone.utc that ruff's pyupgrade rules prefer.
UP042 (StrEnum):
Replaced `class TagKind(str, Enum)` and `class SkipReason(str, Enum)`
with `class Foo(StrEnum)`. StrEnum was added in Python 3.11 stdlib and
is the modern idiom. Behavior is equivalent for our usage (the .value
attribute, str(member) semantics).
I001 (Import sorting):
Added `known-first-party = ["backend"]` to ruff.toml's [lint.isort] so
ruff groups `backend.*` imports correctly. Without it, ruff treated
them as third-party and demanded a different grouping. The existing
import order is stdlib → third-party → first-party → local relative,
which ruff now accepts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
trigger enqueues a Celery scan_directory task and returns 202 with the
celery task id (the actual scan happens asynchronously; status surfaces
via /api/import/status and /api/system/stats). list_tasks supports
status filter and cursor pagination. retry-failed resets failed tasks
to queued and re-enqueues. clear-completed deletes finished tasks
older than the supplied age (used by the Settings UI's cleanup control).
mode='deep' is explicitly rejected with 400 until FC-2d ships it; the
frontend's UI in FC-2a only sends 'quick'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Settings GET returns the single-row ImportSettings DB record; PATCH does
selective field updates (only known editable fields are applied; unknown
keys silently ignored). System stats returns image/tag/storage counts,
task status histogram, and the active batch summary in one shot for the
Settings overview tab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Thin async blueprint delegating to TagService. Returns 400 with the
TagValidationError message on bad kind/fandom combos so the frontend can
surface the reason. List/add/remove endpoints scoped under
/api/images/<id>/tags follow REST conventions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Thin Quart blueprint that delegates to GalleryService. Cursor-based scroll
returns images + a date_groups summary so the SPA can render year-month
headers without re-grouping client-side. Timeline returns year-month
buckets for the sidebar jump nav. Jump returns a cursor positioned at a
specific year-month so the gallery can scroll to historical periods.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
recover_interrupted_tasks runs every 5 minutes, finds ImportTask rows
stuck in 'processing' for >30 minutes (well above any legitimate import
duration), and re-queues them. cleanup_old_tasks runs daily and deletes
finished tasks older than 7 days so the task table stays an operational
view rather than an archive.
Both thresholds match ImageRepo's precedent. The 30-min stuck threshold
is documented inline so a future reader can adjust it intentionally
rather than mistaking it for a 'magic number'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scan_directory walks ImportSettings.import_scan_path, creates an
ImportBatch, enumerates supported files into ImportTasks, and enqueues
import_media_file per task. import_media_file moves the task through
its state machine (pending → queued → processing → complete/skipped/failed),
updates ImportBatch counters atomically (UPDATE ... SET col = col + 1),
enqueues a thumbnail task on success, and marks the batch complete when
the last task drains.
generate_thumbnail runs on its own queue (thumbnail) so big imports
don't starve thumbnail throughput; failure here is logged and does not
fail the import.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cursor format: base64(iso8601_created_at|image_id). Pagination key is
(created_at DESC, id DESC) so we don't drift when new imports land between
page loads. Timeline groups by date_part(year, month) so the sidebar can
render year-month jump buckets. get_image_with_tags returns full image
detail plus prev/next ids so the modal viewer can navigate without an
extra round-trip per arrow press.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single-file pipeline: validate as image, apply filter rules (min dimensions,
transparency), SHA256 hash dedup, atomic copy to /images/{subdir}/, create
ImageRecord with origin='imported_filesystem' and integrity_status='unknown',
auto-derive top-level folder name as Artist + artist tag.
Importer is intentionally sync (consumed by a Celery worker process); the
async Quart side uses the same ORM through its own async session. The
db_sync fixture in conftest.py was added in Task 3 to support these tests.
Thumbnail generation is NOT inlined; the calling Celery task enqueues a
separate thumbnail task so the import queue keeps moving on big batches.
pHash dedup is FC-2d, not here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generates 400px-max thumbnails: PNG with alpha preserved when source has
transparency, JPEG (q=85, progressive) otherwise. Storage layout is
/images/thumbs/{sha[:3]}/{sha}.{jpg|png} so each prefix bucket holds at
most ~4k files (16^3 buckets across all possible SHAs).
Video path uses ffmpeg's first-frame snapshot at ~5% offset; we have a
1-second floor for short clips. ffmpeg call has a 60s timeout. Video
tests are deferred to FC-2e's integration suite where transcoding is
also wired up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Validates kind/fandom rules at the service layer: fandom_id only allowed
for kind='character', and must reference an existing kind='fandom' tag.
Autocomplete ranking: exact match > prefix match > substring, tie-broken
by image_count descending. Image-tag association uses INSERT ON CONFLICT
DO NOTHING for idempotent re-tagging.
conftest.py adds a transactional AsyncSession fixture; each test rolls
back so they don't pollute each other. Also includes a sync Session
fixture (db_sync) for the Importer tests in Task 5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
slugify() produces ASCII-only lowercase slugs from arbitrary text (used for
artist slugs and tag slugs). paths.derive_subdir/derive_top_level_artist
extract the destination layout and folder→artist convention from an import
source path. hash_suffixed_name builds IR-style 'stem__hash10.ext' filenames.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Tag.kind enum (artist/character/fandom/general/series/archive/post/meta/rating),
Tag.fandom_id FK with CHECK constraint (only valid for kind='character'), and a
kind-aware uniqueness index so the same name can exist across kinds and the same
character name can exist in different fandoms.
Adds ImportBatch + ImportTask state-machine tables for scan tracking, plus a
single-row ImportSettings table (CHECK id=1) holding the importer's filter knobs.
Adds image_record.integrity_status column (defaults to 'unknown'); FC-2e
populates this via the integrity verifier.
Drops the unused tag.namespace column from FC-1 — superseded by kind.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Routes are pre-declared for FC-2/FC-3 task modules (import, ml, thumbnail,
download, scan, maintenance). Queue lanes match the ImageRepo pattern where
beat+maintenance run on a separate worker so long imports don't starve
periodic tasks. Smoke ping task confirms the wiring in eager mode for CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the data model from spec §3 in one go so FC-2/FC-3 don't need
schema-adding migrations of their own. Artist is the unified entity for
both gallery 'artist:' tags and GallerySubscriber Subscriptions
(is_subscription flag). ImageProvenance is many-to-one, enabling the
enrich-on-duplicate rule for downloaded content that pHash-matches an
existing record.
The SigLIP embedding column uses pgvector(1152) for SigLIP-so400m;
swapping models in FC-2 will require a column-width migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Config reads from env vars (12-factor); .env.example documents defaults.
Health endpoint is liveness-only (no DB/Redis touch). Test added for CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>