feat(explore): cluster-consensus tag-gaps service + route (#94a)
Cluster C, milestone #94. BulkTagService.tag_gaps(image_ids, threshold) finds tags applied to >= threshold fraction of a visual neighbour set but not all of it (the '7 of 10 share Miku; these 3 don't' signal). Each gap carries the laggard image ids minus any TagSuggestionRejection rows, so apply-to-cluster never re-proposes a tag a neighbour dismissed. 100%-common tags and <2-image sets are excluded. New POST /api/images/cluster/tag-gaps. Tests: consensus found / common excluded / missing ids; rejected laggard excluded from missing; tag dropped when all laggards rejected; <2 images empty; route shape + bad input. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XCUHUGQLrBrkgyk1t49kpX
This commit is contained in:
@@ -320,6 +320,27 @@ async def common_tags():
|
||||
return jsonify({"tags": tags})
|
||||
|
||||
|
||||
@tags_bp.route("/images/cluster/tag-gaps", methods=["POST"])
|
||||
async def cluster_tag_gaps():
|
||||
"""Cluster-consensus tag gaps for a visual neighbour set (#94 Explore):
|
||||
tags on >= threshold of the images but not all. Each gap carries the
|
||||
laggard image ids (minus rejections) for apply-to-cluster."""
|
||||
body = await request.get_json()
|
||||
ids, err = _parse_bulk_ids(body)
|
||||
if err:
|
||||
return err
|
||||
try:
|
||||
threshold = float(body.get("threshold", 0.6))
|
||||
except (TypeError, ValueError):
|
||||
threshold = 0.6
|
||||
threshold = min(1.0, max(0.0, threshold))
|
||||
async with get_session() as session:
|
||||
gaps = await BulkTagService(session).tag_gaps(ids, threshold)
|
||||
return jsonify(
|
||||
{"gaps": gaps, "total": len(set(ids)), "threshold": threshold}
|
||||
)
|
||||
|
||||
|
||||
@tags_bp.route("/images/bulk/tags", methods=["POST"])
|
||||
async def bulk_add_tag():
|
||||
body = await request.get_json()
|
||||
|
||||
Reference in New Issue
Block a user