f946918cd2
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
120 lines
4.0 KiB
Python
120 lines
4.0 KiB
Python
"""Suggestions API: per-image ranked suggestions + accept/alias/dismiss."""
|
|
|
|
from quart import Blueprint, jsonify, request
|
|
|
|
from ..extensions import get_session
|
|
from ..services.ml.allowlist import AllowlistService
|
|
from ..services.ml.suggestions import SuggestionService
|
|
|
|
suggestions_bp = Blueprint("suggestions", __name__, url_prefix="/api")
|
|
|
|
|
|
@suggestions_bp.route("/images/<int:image_id>/suggestions", methods=["GET"])
|
|
async def get_suggestions(image_id: int):
|
|
async with get_session() as session:
|
|
sl = await SuggestionService(session).for_image(image_id)
|
|
return jsonify(
|
|
{
|
|
"by_category": {
|
|
cat: [
|
|
{
|
|
"canonical_tag_id": s.canonical_tag_id,
|
|
"display_name": s.display_name,
|
|
"category": s.category,
|
|
"score": round(s.score, 4),
|
|
"source": s.source,
|
|
"creates_new_tag": s.creates_new_tag,
|
|
}
|
|
for s in items
|
|
]
|
|
for cat, items in sl.by_category.items()
|
|
}
|
|
}
|
|
)
|
|
|
|
|
|
@suggestions_bp.route(
|
|
"/images/<int:image_id>/suggestions/accept", methods=["POST"]
|
|
)
|
|
async def accept_suggestion(image_id: int):
|
|
body = await request.get_json()
|
|
if not body or "tag_id" not in body:
|
|
return jsonify({"error": "tag_id required"}), 400
|
|
tag_id = body["tag_id"]
|
|
async with get_session() as session:
|
|
newly_added = await AllowlistService(session).accept(image_id, tag_id)
|
|
await session.commit()
|
|
if newly_added:
|
|
from ..tasks.ml import apply_allowlist_tags
|
|
|
|
apply_allowlist_tags.delay(tag_id=tag_id)
|
|
return "", 204
|
|
|
|
|
|
@suggestions_bp.route(
|
|
"/images/<int:image_id>/suggestions/alias", methods=["POST"]
|
|
)
|
|
async def alias_suggestion(image_id: int):
|
|
body = await request.get_json()
|
|
required = {"alias_string", "alias_category", "canonical_tag_id"}
|
|
if not body or not required.issubset(body):
|
|
return jsonify({"error": f"required: {sorted(required)}"}), 400
|
|
async with get_session() as session:
|
|
newly_added = await AllowlistService(session).add_alias_and_accept(
|
|
image_id,
|
|
body["alias_string"],
|
|
body["alias_category"],
|
|
body["canonical_tag_id"],
|
|
)
|
|
await session.commit()
|
|
if newly_added:
|
|
from ..tasks.ml import apply_allowlist_tags
|
|
|
|
apply_allowlist_tags.delay(tag_id=body["canonical_tag_id"])
|
|
return "", 204
|
|
|
|
|
|
@suggestions_bp.route(
|
|
"/images/<int:image_id>/suggestions/dismiss", methods=["POST"]
|
|
)
|
|
async def dismiss_suggestion(image_id: int):
|
|
body = await request.get_json()
|
|
if not body or "tag_id" not in body:
|
|
return jsonify({"error": "tag_id required"}), 400
|
|
async with get_session() as session:
|
|
await AllowlistService(session).dismiss(image_id, body["tag_id"])
|
|
await session.commit()
|
|
return "", 204
|
|
|
|
|
|
@suggestions_bp.route("/suggestions/bulk", methods=["POST"])
|
|
async def bulk_suggestions():
|
|
body = await request.get_json()
|
|
if not body or "image_ids" not in body:
|
|
return jsonify({"error": "image_ids required"}), 400
|
|
raw = body["image_ids"]
|
|
if not isinstance(raw, list) or not raw:
|
|
return jsonify({"error": "image_ids must be a non-empty list"}), 400
|
|
try:
|
|
ids = [int(x) for x in raw]
|
|
except (TypeError, ValueError):
|
|
return jsonify({"error": "image_ids must be integers"}), 400
|
|
if len(ids) > 200:
|
|
return jsonify({"error": "selection too large (max 200)"}), 400
|
|
try:
|
|
threshold = float(body.get("threshold", 0.8))
|
|
except (TypeError, ValueError):
|
|
threshold = 0.8
|
|
threshold = min(1.0, max(0.0, threshold))
|
|
async with get_session() as session:
|
|
suggestions = await SuggestionService(session).for_selection(
|
|
ids, threshold=threshold
|
|
)
|
|
return jsonify(
|
|
{
|
|
"suggestions": suggestions,
|
|
"evaluated": len(ids),
|
|
"threshold": threshold,
|
|
}
|
|
)
|