Add AI-art marker detector for the AI-Generated tag (#261) - #263
Merged
Conversation
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>
4 tasks
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What / why
Implements #261 (owner ask: tag known AI artists/generators). Adds a Stage
D-style calculator (
cardpicker.local_detect_ai_art) that scans storedImageEvidenceOCR fields (artist_ocr_name,legal_line_raw_text,collector_line_raw_text) for known AI-generator marker strings and castsCardTagVotevotes for the pre-existing "AI-Generated" tag through theexisting, unmodified vote-consensus machinery
(
tag_consensus/vote_consensus). Never resolves a tag by itself — asingle
VoteSource.OCRvote can never clear the shared human-backed gate(
resolve_weighted_consensus), and "AI-Generated" is additionally markedSENSITIVE(see below), adding a second, independent moderator-co-signgate on top of that.
Code-only: dry-run default,
--writegated, no prod execution (separatelyowner-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.
2024pnotforresaletrademtgenmidjourneycontainsmidjourneyas an exactsubstring 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)
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_MARKERSif bare "Firefly" is actually wanted.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
STANDARDtag(
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 toCard.tagsat import time, bypassing the vote system entirely; untouchedby this PR). It is upgraded to
SENSITIVEhere(
cardpicker.sensitive_tags.SENSITIVE_TAGS), mirroring the existingappropriate-bleedprecedent: a lone machine vote can never resolve anytag 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_tagsupgrades whichever row already exists (created byeither 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-auditcensus (2026-07-15), "AI-Generated" already has 1 genuinely pending
CardTagVotein production. Onceseed_sensitive_tagsis actually runthere, 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-taxonomytable (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, notrelevant here). This PR imports and calls
tag_consensus. resolve_and_persist_tag_votesandpurge_machine_votes. verify_no_machine_only_resolutions(a public function in a non-protectedmanagement command) — it never modifies any protected-core file.
sensitive_tags.py(the one file this PR does edit) is explicitly noton the protected list.
models.pyclasses touched are read-only imports(
Card,CardScanLog,CardTagVote,ImageEvidence,Tag,VotePolarity,VoteSource) — no edits tomodels.pyitself, so the"manual-review" note on
VoteSource/AbstractWeightedVote/CanonicalPrintingMetadata/CardPrintingTagdoesn'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 hostmpcvenv(realPostgres/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)
manage.py seed_sensitive_tags(idempotent — upgrades the existing"AI-Generated" row to
SENSITIVE; also fine to runseed_default_tagsfirst/instead, either order works).manage.py local_detect_ai_art(dry-run by default) over the fullcatalog — review
votes_would_cast/skip_countshit-rate/sampleaudit output.
detailstrings.manage.py local_detect_ai_art --writeto actually cast votes.Purgeable/re-runnable via the existing
purge_machine_votes --run-id <id>(generic — no changes neededthere, it already handles
CardTagVotebyrun_id).Closes #261.