diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py
index 9785cd7..9e4b2b2 100644
--- a/backend/app/api/admin.py
+++ b/backend/app/api/admin.py
@@ -224,6 +224,27 @@ async def tags_purge_legacy():
return jsonify(result)
+@admin_bp.route("/tags/reset-content", methods=["POST"])
+async def tags_reset_content():
+ """Tier-A: delete ALL general + character tags (the Camie-suggestable
+ content vocabulary) so the operator can re-tag from scratch via
+ auto-suggest. fandom + series tags + series_page ordering are preserved,
+ and image tagger_predictions are untouched so suggestions repopulate.
+ dry-run preview returns per-kind counts + applications + a sample so the
+ UI shows exactly what'll go before the operator confirms (dry_run=false).
+ Irreversible except via DB backup restore."""
+ from ..services.cleanup_service import reset_content_tagging
+
+ body = await request.get_json(silent=True) or {}
+ dry_run = bool(body.get("dry_run", False))
+
+ async with get_session() as session:
+ result = await session.run_sync(
+ lambda sync_sess: reset_content_tagging(sync_sess, dry_run=dry_run)
+ )
+ return jsonify(result)
+
+
@admin_bp.route("/maintenance/db-stats", methods=["GET"])
async def db_stats():
"""Per-table bloat readout (pg_stat_user_tables) for the high-churn tables
diff --git a/backend/app/services/cleanup_service.py b/backend/app/services/cleanup_service.py
index 58aab98..f4ee687 100644
--- a/backend/app/services/cleanup_service.py
+++ b/backend/app/services/cleanup_service.py
@@ -455,6 +455,62 @@ def purge_legacy_tags(session: Session, *, dry_run: bool = False) -> dict:
return result
+# The Camie-suggestable CONTENT vocabulary. "Reset content tagging" wipes
+# these so the operator can re-tag from scratch via auto-suggest. fandom +
+# series (and series_page ordering) are deliberately NOT here — they're kept.
+RESETTABLE_TAG_KINDS = ("general", "character")
+
+
+def reset_content_tagging(session: Session, *, dry_run: bool = False) -> dict:
+ """Count (dry_run) or DELETE every general + character tag so the operator
+ can re-tag from scratch via the Camie auto-suggest.
+
+ PRESERVED: fandom + series tags and their series_page ordering, plus every
+ image's image_record.tagger_predictions (untouched) so suggestions
+ repopulate immediately. CASCADE on image_tag / tag_alias / tag_allowlist /
+ tag_reference_embedding / tag_suggestion_rejection clears each deleted
+ tag's applications + metadata. Tag.fandom_id is SET NULL, so deleting
+ character tags never touches the fandom rows. Irreversible except via DB
+ backup restore.
+
+ Returns:
+ {"by_kind": {"general": N, "character": M},
+ "count": total tags,
+ "applications": image_tag rows that will be / were removed,
+ "sample_names": [first 50],
+ and on live runs "deleted": total}
+ """
+ predicate = Tag.kind.in_(RESETTABLE_TAG_KINDS)
+ rows = session.execute(
+ select(Tag.id, Tag.name, Tag.kind).where(predicate)
+ ).all()
+ by_kind: dict[str, int] = {}
+ for _id, _name, kind in rows:
+ key = kind.value if hasattr(kind, "value") else str(kind)
+ by_kind[key] = by_kind.get(key, 0) + 1
+ # Headline impact: applications (image_tag rows) that vanish via cascade.
+ applications = session.execute(
+ select(func.count())
+ .select_from(image_tag)
+ .where(image_tag.c.tag_id.in_(select(Tag.id).where(predicate)))
+ ).scalar_one()
+ sample = [name for _id, name, _kind in rows[:50]]
+ total = len(rows)
+ result = {
+ "by_kind": by_kind,
+ "count": total,
+ "applications": applications,
+ "sample_names": sample,
+ }
+ if dry_run:
+ return result
+ if total:
+ session.execute(Tag.__table__.delete().where(predicate))
+ session.commit()
+ result["deleted"] = total
+ return result
+
+
# ---------------------------------------------------------------------------
# FC-Cleanup additions (2026-05-26): retroactive audit of import-filter rules.
# ---------------------------------------------------------------------------
diff --git a/frontend/src/components/settings/TagMaintenanceCard.vue b/frontend/src/components/settings/TagMaintenanceCard.vue
index 6ea69c8..db8fc73 100644
--- a/frontend/src/components/settings/TagMaintenanceCard.vue
+++ b/frontend/src/components/settings/TagMaintenanceCard.vue
@@ -89,6 +89,53 @@
@click="onKindCommit"
>Delete {{ kindPreview.count }} legacy tag(s)
+
+
+ Reset content tagging.
+ Deletes every general and character tag and
+ removes them from every image, so you can re-tag from scratch with the
+ auto-suggest. Fandoms and series (with their page order) are
+ kept, and each image's saved predictions are untouched — open
+ an image and its suggestions reappear.
+
+ {{ resetPreview.count }} content tag(s) + + ({{ k }}: {{ n }}) + + across {{ resetPreview.applications }} image + application(s). +
+