diff --git a/alembic/versions/0029_drop_artist_copyright_ml_thresholds.py b/alembic/versions/0029_drop_artist_copyright_ml_thresholds.py
new file mode 100644
index 0000000..e6c044b
--- /dev/null
+++ b/alembic/versions/0029_drop_artist_copyright_ml_thresholds.py
@@ -0,0 +1,71 @@
+"""drop artist + copyright ml thresholds; lower general default to 0.50
+
+Revision ID: 0029
+Revises: 0028
+Create Date: 2026-06-01
+
+Operator-flagged 2026-06-01: the view modal's Suggestions panel hides
+most general-category predictions because the default threshold is
+0.95. Lowering the default to 0.50 (matches character) so general
+suggestions surface more aggressively; the value remains tunable in
+Settings → ML.
+
+Same change retires two ML suggestion categories whose Tag.kind
+surfaces are unused:
+
+- `artist`: retired in FC-2d-vii-c — artist identity is acquisition-
+ derived (image_record.artist_id), never ML-inferred. The threshold
+ column was a leftover from before that retirement.
+- `copyright`: retired 2026-06-01 — the app uses `fandom` for the
+ franchise/copyright concept (per TagsView.vue's doc comment); no
+ Tag rows of kind=copyright exist, and the threshold column never
+ fed anything user-visible.
+
+Both columns are dropped from ml_settings; the existing row's
+suggestion_threshold_general value is bumped from 0.95 to 0.50 iff
+it's still at the old default, so deployed installs pick up the new
+UX without overriding any operator tuning.
+"""
+from typing import Sequence, Union
+
+from alembic import op
+from sqlalchemy import text
+
+revision: str = "0029"
+down_revision: Union[str, None] = "0028"
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ # Bump the general threshold for installs still at the old default.
+ op.execute(text(
+ "UPDATE ml_settings "
+ "SET suggestion_threshold_general = 0.50 "
+ "WHERE id = 1 AND suggestion_threshold_general = 0.95"
+ ))
+ op.drop_column("ml_settings", "suggestion_threshold_artist")
+ op.drop_column("ml_settings", "suggestion_threshold_copyright")
+
+
+def downgrade() -> None:
+ # Restore the columns with their prior defaults. The bump from
+ # 0.95 → 0.50 isn't reversible without remembering whether the
+ # operator had explicitly set 0.95 (unlikely — that was just the
+ # default) so we leave the current general value as-is.
+ from sqlalchemy import Column, Float
+
+ op.add_column(
+ "ml_settings",
+ Column(
+ "suggestion_threshold_artist",
+ Float, nullable=False, server_default="0.30",
+ ),
+ )
+ op.add_column(
+ "ml_settings",
+ Column(
+ "suggestion_threshold_copyright",
+ Float, nullable=False, server_default="0.50",
+ ),
+ )
diff --git a/backend/app/api/ml_admin.py b/backend/app/api/ml_admin.py
index a346175..574e6f5 100644
--- a/backend/app/api/ml_admin.py
+++ b/backend/app/api/ml_admin.py
@@ -9,9 +9,7 @@ ml_admin_bp = Blueprint("ml_admin", __name__, url_prefix="/api/ml")
_EDITABLE = (
- "suggestion_threshold_artist",
"suggestion_threshold_character",
- "suggestion_threshold_copyright",
"suggestion_threshold_general",
"centroid_similarity_threshold",
"min_reference_images",
@@ -28,9 +26,7 @@ async def get_settings():
).scalar_one()
return jsonify(
{
- "suggestion_threshold_artist": s.suggestion_threshold_artist,
"suggestion_threshold_character": s.suggestion_threshold_character,
- "suggestion_threshold_copyright": s.suggestion_threshold_copyright,
"suggestion_threshold_general": s.suggestion_threshold_general,
"centroid_similarity_threshold": s.centroid_similarity_threshold,
"min_reference_images": s.min_reference_images,
diff --git a/backend/app/models/ml_settings.py b/backend/app/models/ml_settings.py
index 5ad33ad..de4e476 100644
--- a/backend/app/models/ml_settings.py
+++ b/backend/app/models/ml_settings.py
@@ -15,17 +15,14 @@ class MLSettings(Base):
__table_args__ = (CheckConstraint("id = 1", name="singleton"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
- suggestion_threshold_artist: Mapped[float] = mapped_column(
- Float, nullable=False, default=0.30
- )
suggestion_threshold_character: Mapped[float] = mapped_column(
Float, nullable=False, default=0.50
)
- suggestion_threshold_copyright: Mapped[float] = mapped_column(
- Float, nullable=False, default=0.50
- )
+ # Default lowered 0.95 → 0.50 on 2026-06-01 — operator-flagged that
+ # 0.95 hid most general suggestions. Operator-tunable via Settings →
+ # ML if too noisy.
suggestion_threshold_general: Mapped[float] = mapped_column(
- Float, nullable=False, default=0.95
+ Float, nullable=False, default=0.50
)
centroid_similarity_threshold: Mapped[float] = mapped_column(
Float, nullable=False, default=0.55
diff --git a/backend/app/services/ml/suggestions.py b/backend/app/services/ml/suggestions.py
index 0b8d805..7c71b0d 100644
--- a/backend/app/services/ml/suggestions.py
+++ b/backend/app/services/ml/suggestions.py
@@ -48,11 +48,11 @@ class SuggestionService:
).scalar_one()
def _threshold_for(self, s: MLSettings, category: str) -> float:
- # 'artist' intentionally absent (FC-2d-vii-c) — falls through to
- # the 1.01 "never surfaces" default like any unsurfaced category.
+ # 'artist' (FC-2d-vii-c) and 'copyright' (2026-06-01) retired;
+ # both fall through to the 1.01 "never surfaces" default like any
+ # unsurfaced category.
return {
"character": s.suggestion_threshold_character,
- "copyright": s.suggestion_threshold_copyright,
"general": s.suggestion_threshold_general,
}.get(category, 1.01)
diff --git a/backend/app/services/ml/tagger.py b/backend/app/services/ml/tagger.py
index 5277b44..ddf17b9 100644
--- a/backend/app/services/ml/tagger.py
+++ b/backend/app/services/ml/tagger.py
@@ -38,10 +38,13 @@ STORE_FLOOR = float(os.environ.get("TAGGER_STORE_FLOOR", "0.05"))
# The categories FC-2b surfaces in the UI. Others (meta/rating/year) are
# still stored but the suggestion service filters them out.
-# FC-2d-vii-c: 'artist' retired — artist identity is acquisition-derived
-# (image_record.artist_id), never ML-inferred. Raw predictions are still
-# stored at STORE_FLOOR but artist never surfaces.
-SURFACED_CATEGORIES = {"character", "copyright", "general"}
+# 'artist' retired in FC-2d-vii-c — artist identity is acquisition-derived
+# (image_record.artist_id), never ML-inferred. 'copyright' retired
+# 2026-06-01 — operator doesn't use the copyright tag-kind; fandom is
+# this app's franchise/series concept (per TagsView.vue's doc comment).
+# Raw predictions for both categories still get stored at STORE_FLOOR but
+# don't surface in suggestions.
+SURFACED_CATEGORIES = {"character", "general"}
# ImageNet preprocessing constants (per Camie v2 onnx_inference.py).
_IMAGENET_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
diff --git a/frontend/src/components/modal/AliasPickerDialog.vue b/frontend/src/components/modal/AliasPickerDialog.vue
index 2552d31..37fae6f 100644
--- a/frontend/src/components/modal/AliasPickerDialog.vue
+++ b/frontend/src/components/modal/AliasPickerDialog.vue
@@ -48,10 +48,11 @@ function onSearch(q) {
if (!query) { results.value = []; return }
loading.value = true
try {
- // Scope the autocomplete to the prediction's category where it maps
- // to a tag kind. 'copyright' has no tag kind; search unscoped there.
- const kind = ['artist', 'character'].includes(props.category)
- ? props.category : null
+ // Scope the autocomplete to the prediction's category where it
+ // maps to a tag kind. Only 'character' surfaces as both a
+ // suggestion category and a tag kind now ('artist' + 'copyright'
+ // retired); other categories search unscoped.
+ const kind = props.category === 'character' ? 'character' : null
const params = { q: query, limit: 20 }
if (kind) params.kind = kind
results.value = await api.get('/api/tags/autocomplete', { params })
diff --git a/frontend/src/components/modal/SuggestionsPanel.vue b/frontend/src/components/modal/SuggestionsPanel.vue
index 0ca5cea..68e8040 100644
--- a/frontend/src/components/modal/SuggestionsPanel.vue
+++ b/frontend/src/components/modal/SuggestionsPanel.vue
@@ -22,7 +22,7 @@
@@ -47,7 +47,10 @@ import AliasPickerDialog from './AliasPickerDialog.vue'
const props = defineProps({ imageId: { type: Number, required: true } })
const store = useSuggestionsStore()
-const peopleCats = ['artist', 'character', 'copyright']
+// 'artist' (FC-2d-vii-c) and 'copyright' (2026-06-01) retired as
+// suggestion categories. Only 'character' remains as a people-style
+// category alongside the general bucket.
+const peopleCats = ['character']
function labelFor(c) { return CATEGORY_LABELS[c] || c }
const isEmpty = computed(() =>
diff --git a/frontend/src/components/modal/TagAutocomplete.vue b/frontend/src/components/modal/TagAutocomplete.vue
index e074488..9767af2 100644
--- a/frontend/src/components/modal/TagAutocomplete.vue
+++ b/frontend/src/components/modal/TagAutocomplete.vue
@@ -1,6 +1,7 @@