bc6d43d3f2
Hygiene follow-up to the Camie retirement (#1189) — these were left inert to bound that change; nothing reads them now. Migration 0068 drops: - ml_settings: tagger_store_floor, tagger_model_version, suggestion_threshold_ character/general (already dead pre-retirement — scoring uses per-head thresholds), video_min_tag_frames (only the deleted video-prediction aggregator used it). - image_record: tagger_model_version (no writer), centroid_scores (dead JSON cache, no reader). Also: ml_admin _EDITABLE/GET/_validate pruned (dropped the store-floor invariant + video_min_tag_frames check); MLThresholdSliders trimmed to a video-embedding card (interval + max frames only); importer no longer resets the dropped cols; download_models drops the Camie fetch; stale CASCADE comments in cleanup_service no longer name the removed tables. Tests updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
113 lines
4.0 KiB
Python
113 lines
4.0 KiB
Python
"""ML admin API: settings + backfill 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 = (
|
|
"video_frame_interval_seconds",
|
|
"video_max_frames",
|
|
"head_min_positives",
|
|
"head_auto_apply_precision",
|
|
"head_auto_apply_enabled",
|
|
"head_auto_apply_min_positives",
|
|
"ccip_match_threshold",
|
|
"ccip_auto_apply_enabled",
|
|
"ccip_auto_apply_threshold",
|
|
"embedder_model_name",
|
|
"embedder_model_version",
|
|
)
|
|
|
|
|
|
@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(
|
|
{
|
|
"video_frame_interval_seconds": s.video_frame_interval_seconds,
|
|
"video_max_frames": s.video_max_frames,
|
|
"embedder_model_version": s.embedder_model_version,
|
|
"head_min_positives": s.head_min_positives,
|
|
"head_auto_apply_precision": s.head_auto_apply_precision,
|
|
"head_auto_apply_enabled": s.head_auto_apply_enabled,
|
|
"head_auto_apply_min_positives": s.head_auto_apply_min_positives,
|
|
"ccip_match_threshold": s.ccip_match_threshold,
|
|
"ccip_auto_apply_enabled": s.ccip_auto_apply_enabled,
|
|
"ccip_auto_apply_threshold": s.ccip_auto_apply_threshold,
|
|
"embedder_model_name": s.embedder_model_name,
|
|
}
|
|
)
|
|
|
|
|
|
@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()
|
|
|
|
# Merge the patch over current values, then validate the result as a
|
|
# whole — the store-floor invariant couples three fields, so they
|
|
# can't be checked one at a time.
|
|
proposed = {f: getattr(s, f) for f in _EDITABLE}
|
|
for field in _EDITABLE:
|
|
if field in body:
|
|
proposed[field] = body[field]
|
|
|
|
err = _validate(proposed)
|
|
if err is not None:
|
|
return jsonify({"error": err}), 400
|
|
|
|
for field in _EDITABLE:
|
|
setattr(s, field, proposed[field])
|
|
await session.commit()
|
|
return await get_settings()
|
|
|
|
|
|
def _validate(p: dict) -> str | None:
|
|
"""Returns an error string if the proposed settings are invalid, else None."""
|
|
# Video embedding (#747).
|
|
if p["video_frame_interval_seconds"] <= 0:
|
|
return "video_frame_interval_seconds must be > 0"
|
|
if p["video_max_frames"] < 1:
|
|
return "video_max_frames must be >= 1"
|
|
# Head training (#114).
|
|
if int(p["head_min_positives"]) < 1:
|
|
return "head_min_positives must be >= 1"
|
|
if not (0.5 <= float(p["head_auto_apply_precision"]) <= 0.999):
|
|
return "head_auto_apply_precision must be between 0.5 and 0.999"
|
|
if int(p["head_auto_apply_min_positives"]) < 1:
|
|
return "head_auto_apply_min_positives must be >= 1"
|
|
if not (0.5 <= float(p["ccip_match_threshold"]) <= 0.999):
|
|
return "ccip_match_threshold must be between 0.5 and 0.999"
|
|
if not (0.5 <= float(p["ccip_auto_apply_threshold"]) <= 0.999):
|
|
return "ccip_auto_apply_threshold must be between 0.5 and 0.999"
|
|
# Embedder model swap (#1190): both must be non-empty. Changing them means a
|
|
# different embedding space — the operator must re-embed + retrain after.
|
|
for key in ("embedder_model_name", "embedder_model_version"):
|
|
if not str(p[key]).strip():
|
|
return f"{key} must not be empty"
|
|
return None
|
|
|
|
|
|
@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
|