Skip to content

Add AI-art marker detector for the AI-Generated tag (#261) - #263

Merged
WilfordGrimley merged 2 commits into
masterfrom
feat/ai-artist-tagging
Jul 21, 2026
Merged

Add AI-art marker detector for the AI-Generated tag (#261)#263
WilfordGrimley merged 2 commits into
masterfrom
feat/ai-artist-tagging

Conversation

@WilfordGrimley

Copy link
Copy Markdown

What / why

Implements #261 (owner ask: tag known AI artists/generators). Adds a Stage
D-style calculator (cardpicker.local_detect_ai_art) that scans stored
ImageEvidence OCR fields (artist_ocr_name, legal_line_raw_text,
collector_line_raw_text) for known AI-generator marker strings and casts
CardTagVote votes for the pre-existing "AI-Generated" tag through the
existing, unmodified vote-consensus machinery
(tag_consensus/vote_consensus). Never resolves a tag by itself — a
single VoteSource.OCR vote can never clear the shared human-backed gate
(resolve_weighted_consensus), and "AI-Generated" is additionally marked
SENSITIVE (see below), adding a second, independent moderator-co-sign
gate on top of that.

Code-only: dry-run default, --write gated, no prod execution (separately
owner-gated per the runbook below).

Marker list (final form)

Midjourney, DALL-E, Stable Diffusion, SDXL, Gemini, Imagen, Adobe
Firefly
, Leonardo AI, NightCafe, Bing Image Creator, "AI art", "AI
generated". Matching is OCR-tolerant: exact substring match on
lowercase-and-strip-non-alphanumeric-normalized text first (already
sufficient for every real sample in the issue — e.g.
2024pnotforresaletrademtgenmidjourney contains midjourney as an exact
substring once normalized), falling back to a cheap sliding-window
single-character-substitution-tolerant match for markers ≥8 normalized
chars. Documented limitation: only substitutions are tolerated, not
insertions/deletions (a real edit-distance scan would cost more than any
observed OCR noise needs).

Generator-site/tool URLs (CardConjurer.com etc.) are excluded per the
owner amendment — they identify a rendering tool usable with ordinary
human-drawn art, not AI provenance.

Deviations from spec (both flagged for override)

  1. "Firefly" → "Adobe Firefly": the owner's list said bare "Firefly".
    Substituted the full product name because a bare, common English
    word/plausible human-artist pseudonym appearing in the artist-credit
    OCR line carries the same false-positive risk class the owner already
    flagged for generator-site exclusion — "Adobe Firefly" keeps the same
    detection power with materially less collision risk. Easy one-line
    revert in AI_GENERATOR_MARKERS if bare "Firefly" is actually wanted.
  2. "Leonardo AI" (not bare "Leonardo"): this one was already in the
    owner's own list this way — noted here only because it's the same
    principle as Add NotMPC ordering tab and PringlePrints listing #1, applied consistently, not because it's a change I
    made.

No other deviations.

Sensitive-tag decision (and why)

"AI-Generated" already existed as a STANDARD tag
(cardpicker.default_tags.DEFAULT_TAGS, seeded for an unrelated,
orthogonal reason — filename-bracket matching, e.g. an uploaded file
literally named "... [Midjourney].png", applies it directly to
Card.tags at import time, bypassing the vote system entirely; untouched
by this PR). It is upgraded to SENSITIVE here
(cardpicker.sensitive_tags.SENSITIVE_TAGS), mirroring the existing
appropriate-bleed precedent: a lone machine vote can never resolve any
tag regardless of moderation class (the shared human-backed gate), but a
confident crowd consensus on this one specifically still needs a
privileged (moderator) co-sign before it goes live. Reasoning: publicly
labelling a real human artist's work as AI-generated is a reputational
harm serious enough to warrant the same "machine can flag it, a human
must confirm it" treatment as NSFW/low-res/incorrect-info, not just the
ordinary crowd threshold — the false-positive failure mode this feature
has to guard hardest against.

seed_sensitive_tags upgrades whichever row already exists (created by
either seeding path, in either order), so the gate applies regardless of
seeding order in an existing deployment.

Runbook note: per docs/features/printing-tags.md's own DPI-tag-audit
census (2026-07-15), "AI-Generated" already has 1 genuinely pending
CardTagVote in production. Once seed_sensitive_tags is actually run
there, that pre-existing vote becomes subject to the new privileged
co-sign gate for the first time — worth knowing before running the
seeding step, not a surprise to discover after.

Docs updated in place: docs/features/moderation.md's sensitive-taxonomy
table (now 5 rows) + a new paragraph explaining the upgrade and the
detector, matching that doc's existing per-tag paragraph convention.

PROTECTED CORE check (docs/upstreaming/license-provenance.md §2)

Confirmed the protected-core file list before touching anything
consensus-adjacent: vote_consensus.py, printing_consensus.py,
tag_consensus.py, artist_consensus.py, local_phash.py,
local_fallback.py (+ the federation-hash-tool/decrypt-tool pair, not
relevant here). This PR imports and calls tag_consensus. resolve_and_persist_tag_votes and purge_machine_votes. verify_no_machine_only_resolutions (a public function in a non-protected
management command) — it never modifies any protected-core file.
sensitive_tags.py (the one file this PR does edit) is explicitly not
on the protected list. models.py classes touched are read-only imports
(Card, CardScanLog, CardTagVote, ImageEvidence, Tag,
VotePolarity, VoteSource) — no edits to models.py itself, so the
"manual-review" note on VoteSource/AbstractWeightedVote/
CanonicalPrintingMetadata/CardPrintingTag doesn't apply.

Tests

27 new tests in cardpicker/tests/test_local_detect_ai_art.py:
marker matching (exact real-sample fixtures, fuzzy single-substitution
tolerance + its two-substitution limitation, short-marker fuzzy
exclusion, CardConjurer-URL non-match), the pure per-card calculator
(single/multi-field confidence tiers), and the batch runner (dry-run
counts, write + never-resolves-alone + gate-check-reuse, no-hit scan-log,
identity-skip idempotence, no-evidence and incomplete-evidence
rescannability, stale-content-hash rejection, no-content-hash skip,
unseeded-tag error, multi-card selectivity).

Full local run: 146 passed (new suite + test_sensitive_tags,
test_default_tags, test_tag_votes, test_purge_machine_votes,
test_local_calculate_verdicts) against the host mpcvenv (real
Postgres/ES testcontainers, no mocks). pre-commit run (ruff, isort,
black, mypy, prettier) clean on every changed file.

Gated runbook (prod execution NOT done here — separately owner-gated)

  1. manage.py seed_sensitive_tags (idempotent — upgrades the existing
    "AI-Generated" row to SENSITIVE; also fine to run
    seed_default_tags first/instead, either order works).
  2. manage.py local_detect_ai_art (dry-run by default) over the full
    catalog — review votes_would_cast / skip_counts hit-rate/sample
    audit output.
  3. Owner reviews hit counts and sampled detail strings.
  4. manage.py local_detect_ai_art --write to actually cast votes.
    Purgeable/re-runnable via the existing
    purge_machine_votes --run-id <id> (generic — no changes needed
    there, it already handles CardTagVote by run_id).

Closes #261.

WilfordGrimley and others added 2 commits July 21, 2026 10:16
Stage D-style calculator (cardpicker.local_detect_ai_art) scans stored
ImageEvidence OCR fields for known AI-generator markers and casts
CardTagVote votes for "AI-Generated" via the existing vote-consensus
machinery - never resolves alone, and the tag is upgraded to SENSITIVE
(moderator co-sign required) given the reputational risk of a false
positive against a human artist. Generator-site URLs (CardConjurer etc.)
are deliberately excluded from the marker list per owner amendment.

dry-run-default local_detect_ai_art management command, --write gated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@WilfordGrimley
WilfordGrimley merged commit 87b39de into master Jul 21, 2026
7 checks passed
WilfordGrimley added a commit that referenced this pull request Jul 21, 2026
…ial revert) (#292)

Ordinary crowd consensus is fine for AI-Generated per owner decision;
seed_sensitive_tags now syncs the downgrade on instances that ran the
prior seed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tag AI-generated card art via known-generator markers in stored OCR evidence

1 participant