feat(bulk): consensus ML suggestions + frontend polish
Bulk editor now loads consensus ML suggestions across the current selection via a new POST /api/suggestions/bulk endpoint, powered by get_bulk_suggestions() in tag_suggestions.py. A tag is surfaced only if it was suggested for or already applied to >= 80% of the selection; coverage counts include images that already have the tag so a near- universal tag isn't penalized for dropping out of suggestion lists. Accept-only chips (no reject) match the rest of the suggestions UX. Frontend polish in the same commit: - Showcase session dedup (exclude seen ids, reset on second lap) and aspect-ratio-aware placeholder heights for better column balancing - Modal touch-swipe navigation on the image wrapper, auto-focus of the tag input on desktop (>=768px), and autocomplete flip-up when the dropdown would cover the Suggestions section - Settings template inline styles replaced with utility classes - Inline series-editor script extracted to gallery-series-editor.js Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+30
@@ -95,6 +95,8 @@ def random_images_api():
|
||||
'thumb_url': url_for('main.serve_image', filename=thumb_path),
|
||||
'full_url': url_for('main.serve_image', filename=full_path),
|
||||
'is_video': is_video,
|
||||
'width': img.width,
|
||||
'height': img.height,
|
||||
'date': (img.taken_at or img.imported_at).strftime('%Y-%m-%d') if (img.taken_at or img.imported_at) else '',
|
||||
'tags': [{'name': t.name, 'kind': t.kind} for t in img.tags]
|
||||
})
|
||||
@@ -714,6 +716,34 @@ def get_image_suggestions(image_id):
|
||||
return jsonify({'ok': True, 'suggestions': suggestions})
|
||||
|
||||
|
||||
@main.post("/api/suggestions/bulk")
|
||||
def get_bulk_suggestions_api():
|
||||
"""Consensus ML suggestions across a selection of images.
|
||||
|
||||
Body: {"image_ids": [...], "threshold": 0.8}
|
||||
Returns grouped suggestions where each tag was suggested for or already
|
||||
applied to at least `threshold` fraction of the selected images.
|
||||
"""
|
||||
from app.services.tag_suggestions import get_bulk_suggestions
|
||||
payload = request.get_json(force=True, silent=True) or {}
|
||||
image_ids = payload.get('image_ids') or []
|
||||
try:
|
||||
image_ids = [int(x) for x in image_ids]
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({'ok': False, 'error': 'invalid_image_ids'}), 400
|
||||
if not image_ids:
|
||||
return jsonify({'ok': False, 'error': 'no_image_ids'}), 400
|
||||
|
||||
try:
|
||||
threshold = float(payload.get('threshold', 0.8))
|
||||
except (TypeError, ValueError):
|
||||
threshold = 0.8
|
||||
threshold = max(0.0, min(1.0, threshold))
|
||||
|
||||
suggestions = get_bulk_suggestions(image_ids, threshold=threshold)
|
||||
return jsonify({'ok': True, 'suggestions': suggestions, 'evaluated': len(image_ids), 'threshold': threshold})
|
||||
|
||||
|
||||
@main.post("/image/<int:image_id>/suggestions/accept")
|
||||
def accept_image_suggestion(image_id):
|
||||
from app.models import SuggestionFeedback
|
||||
|
||||
Reference in New Issue
Block a user