af7b5c95e9
Four coupled operator-asked changes to the view modal (Scribe plan #509): 1. **Autofocus tag entry on modal open** — TagAutocomplete grabs focus in onMounted/nextTick so the caret is in the input the moment the modal renders. No click needed to start typing. 2. **General suggestions expanded by default** — SuggestionsPanel's general-category group now mounts with `:default-open="true"`. Operator can collapse if too noisy, but the v1 frame shows them. 3. **Lower general threshold default 0.95 → 0.50** — MLSettings. suggestion_threshold_general default matches character. Alembic 0029 also bumps the existing singleton row's value if it's still at the old 0.95. Operator can re-tune from Settings → ML. 4. **Retire `copyright` + `artist` as ML suggestion categories** — neither feeds a Tag.kind (`artist` retired in FC-2d-vii-c, never really existed as a copyright tag-kind). They were surfaced in the suggestions pipeline + threshold settings UI but had no follow- through. Drop from SURFACED_CATEGORIES, suggestions._threshold_for, ml_admin GET/PATCH allowlist, MLSettings columns (alembic 0029 drops the two columns), frontend CATEGORY_ORDER + CATEGORY_LABELS, SuggestionsPanel.peopleCats, AliasPickerDialog kind-check, and MLThresholdSliders rows. Out of scope (intentional): `tag_kind` Postgres enum still includes `artist` for historic Tag row queryability (per the model comment); no operator pain reported, no enum-shrink needed. Tests: - test_surfaced_categories asserts {character, general}, excludes artist + copyright. - test_threshold_for_artist_is_unsurfaced extended to cover copyright. - test_get_and_patch_settings asserts new 0.50 default and the absent artist + copyright keys in the GET payload.
71 lines
2.1 KiB
Python
71 lines
2.1 KiB
Python
"""ML admin API: settings, backfill trigger, centroid recompute trigger."""
|
|
|
|
from quart import Blueprint, jsonify, request
|
|
|
|
from ..extensions import get_session
|
|
from ..models import MLSettings
|
|
|
|
ml_admin_bp = Blueprint("ml_admin", __name__, url_prefix="/api/ml")
|
|
|
|
|
|
_EDITABLE = (
|
|
"suggestion_threshold_character",
|
|
"suggestion_threshold_general",
|
|
"centroid_similarity_threshold",
|
|
"min_reference_images",
|
|
)
|
|
|
|
|
|
@ml_admin_bp.route("/settings", methods=["GET"])
|
|
async def get_settings():
|
|
from sqlalchemy import select
|
|
|
|
async with get_session() as session:
|
|
s = (
|
|
await session.execute(select(MLSettings).where(MLSettings.id == 1))
|
|
).scalar_one()
|
|
return jsonify(
|
|
{
|
|
"suggestion_threshold_character": s.suggestion_threshold_character,
|
|
"suggestion_threshold_general": s.suggestion_threshold_general,
|
|
"centroid_similarity_threshold": s.centroid_similarity_threshold,
|
|
"min_reference_images": s.min_reference_images,
|
|
"tagger_model_version": s.tagger_model_version,
|
|
"embedder_model_version": s.embedder_model_version,
|
|
}
|
|
)
|
|
|
|
|
|
@ml_admin_bp.route("/settings", methods=["PATCH"])
|
|
async def patch_settings():
|
|
from sqlalchemy import select
|
|
|
|
body = await request.get_json()
|
|
if not isinstance(body, dict):
|
|
return jsonify({"error": "body must be an object"}), 400
|
|
async with get_session() as session:
|
|
s = (
|
|
await session.execute(select(MLSettings).where(MLSettings.id == 1))
|
|
).scalar_one()
|
|
for field in _EDITABLE:
|
|
if field in body:
|
|
setattr(s, field, body[field])
|
|
await session.commit()
|
|
return await get_settings()
|
|
|
|
|
|
@ml_admin_bp.route("/backfill", methods=["POST"])
|
|
async def trigger_backfill():
|
|
from ..tasks.ml import backfill
|
|
|
|
r = backfill.delay()
|
|
return jsonify({"celery_task_id": r.id}), 202
|
|
|
|
|
|
@ml_admin_bp.route("/recompute-centroids", methods=["POST"])
|
|
async def trigger_recompute():
|
|
from ..tasks.ml import recompute_centroids
|
|
|
|
r = recompute_centroids.delay()
|
|
return jsonify({"celery_task_id": r.id}), 202
|