feat(trip-cluster): master/detail review routes + templates + clusterReview JS
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
from flask import Blueprint
|
||||
from flask import Blueprint, current_app, render_template
|
||||
from photoflow.core import Store
|
||||
|
||||
bp = Blueprint("nav", __name__)
|
||||
|
||||
|
||||
def _store():
|
||||
cfg = current_app.config["APP_CONFIG"]
|
||||
return Store(cfg.db_path).connect()
|
||||
|
||||
|
||||
@bp.route("/health")
|
||||
def health():
|
||||
return "ok"
|
||||
@@ -10,4 +16,9 @@ def health():
|
||||
|
||||
@bp.route("/")
|
||||
def index():
|
||||
return "trip-cluster"
|
||||
s = _store()
|
||||
clusters = s.clusters_by_attention()
|
||||
stats = {"assets": len(s.all_assets()), "clusters": len(clusters),
|
||||
"pending": sum(1 for c in clusters if c.status == "pending")}
|
||||
s.close()
|
||||
return render_template("review.html", clusters=clusters, stats=stats)
|
||||
|
||||
@@ -1,3 +1,129 @@
|
||||
from flask import Blueprint
|
||||
from flask import Blueprint, current_app, render_template, request, jsonify, abort
|
||||
from photoflow.core import Store
|
||||
from app import review
|
||||
|
||||
bp = Blueprint("review", __name__)
|
||||
|
||||
|
||||
def _store():
|
||||
cfg = current_app.config["APP_CONFIG"]
|
||||
return Store(cfg.db_path).connect()
|
||||
|
||||
|
||||
def _client():
|
||||
factory = current_app.config.get("IMMICH_FACTORY")
|
||||
if factory:
|
||||
return factory()
|
||||
from photoflow.immich import ImmichClient
|
||||
cfg = current_app.config["APP_CONFIG"]
|
||||
return ImmichClient(cfg.immich_url, cfg.immich_api_key)
|
||||
|
||||
|
||||
def _body():
|
||||
return request.get_json(silent=True) or {}
|
||||
|
||||
|
||||
@bp.route("/cluster/<int:cid>")
|
||||
def detail(cid):
|
||||
s = _store()
|
||||
c = s.get_cluster(cid)
|
||||
if c is None:
|
||||
s.close()
|
||||
abort(404)
|
||||
members = s.cluster_members(cid)
|
||||
prev_id, next_id = s.chronological_neighbors(cid)
|
||||
s.close()
|
||||
return render_template("_detail.html", c=c, members=members,
|
||||
prev_id=prev_id, next_id=next_id)
|
||||
|
||||
|
||||
@bp.route("/cluster/<int:cid>/approve", methods=["POST"])
|
||||
def approve(cid):
|
||||
s = _store()
|
||||
review.approve(s, cid, _body().get("name"))
|
||||
c = s.get_cluster(cid)
|
||||
s.close()
|
||||
return jsonify({"ok": True, "status": c.status, "name": c.decided_name})
|
||||
|
||||
|
||||
@bp.route("/cluster/<int:cid>/non-trip", methods=["POST"])
|
||||
def non_trip(cid):
|
||||
s = _store()
|
||||
review.mark_non_trip(s, cid)
|
||||
s.close()
|
||||
return jsonify({"ok": True, "status": "non_trip"})
|
||||
|
||||
|
||||
@bp.route("/cluster/<int:cid>/skip", methods=["POST"])
|
||||
def skip(cid):
|
||||
s = _store()
|
||||
review.skip(s, cid)
|
||||
s.close()
|
||||
return jsonify({"ok": True, "status": "skipped"})
|
||||
|
||||
|
||||
@bp.route("/cluster/<int:cid>/split", methods=["POST"])
|
||||
def split(cid):
|
||||
s = _store()
|
||||
boundary = _body().get("boundary_asset_id")
|
||||
if not boundary:
|
||||
s.close()
|
||||
return jsonify({"error": "boundary_asset_id required"}), 400
|
||||
try:
|
||||
id1, id2 = review.split(s, cid, boundary)
|
||||
except ValueError as e:
|
||||
s.close()
|
||||
return jsonify({"error": str(e)}), 400
|
||||
s.close()
|
||||
return jsonify({"ok": True, "ids": [id1, id2]})
|
||||
|
||||
|
||||
@bp.route("/cluster/<int:cid>/merge", methods=["POST"])
|
||||
def merge(cid):
|
||||
s = _store()
|
||||
other = _body().get("other_id")
|
||||
if other is None:
|
||||
s.close()
|
||||
return jsonify({"error": "other_id required"}), 400
|
||||
new_id = review.merge(s, cid, int(other))
|
||||
s.close()
|
||||
return jsonify({"ok": True, "id": new_id})
|
||||
|
||||
|
||||
@bp.route("/cluster/<int:cid>/member", methods=["POST"])
|
||||
def member(cid):
|
||||
s = _store()
|
||||
body = _body()
|
||||
review.set_member(s, cid, body["asset_id"], bool(body.get("included")))
|
||||
s.close()
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
@bp.route("/approve-high-confidence", methods=["POST"])
|
||||
def approve_high_confidence():
|
||||
s = _store()
|
||||
threshold = float(_body().get("threshold", 0.75))
|
||||
n = review.approve_high_confidence(s, threshold)
|
||||
s.close()
|
||||
return jsonify({"approved": n})
|
||||
|
||||
|
||||
@bp.route("/cluster/<int:cid>/apply", methods=["POST"])
|
||||
def apply_one(cid):
|
||||
# Lazy import: app.writeback is owned by a later task and may be absent
|
||||
# at app-startup; importing here keeps the blueprint importable regardless.
|
||||
from app.writeback import apply_cluster
|
||||
s = _store()
|
||||
res = apply_cluster(_client(), s, cid)
|
||||
s.close()
|
||||
return jsonify(res)
|
||||
|
||||
|
||||
@bp.route("/apply-all", methods=["POST"])
|
||||
def apply_everything():
|
||||
# Lazy import: see apply_one above.
|
||||
from app.writeback import apply_all
|
||||
s = _store()
|
||||
results = apply_all(_client(), s)
|
||||
s.close()
|
||||
return jsonify({"results": results})
|
||||
|
||||
Reference in New Issue
Block a user