feat: M1 foundation packages + trip-cluster app #1

Merged
m038 merged 19 commits from feat/m1-foundation-trip-cluster into master 2026-06-27 19:10:50 +02:00
2 changed files with 100 additions and 0 deletions
Showing only changes of commit a9b443cd53 - Show all commits
+41
View File
@@ -0,0 +1,41 @@
import datetime
def _now() -> str:
return datetime.datetime.now(datetime.timezone.utc).isoformat()
def approve(store, cluster_id, name=None) -> None:
c = store.get_cluster(cluster_id)
decided = (name or "").strip() or c.suggested_name
store.update_cluster(cluster_id, status="approved",
decided_name=decided, reviewed_at=_now())
def mark_non_trip(store, cluster_id) -> None:
store.update_cluster(cluster_id, status="non_trip", reviewed_at=_now())
def skip(store, cluster_id) -> None:
store.update_cluster(cluster_id, status="skipped", reviewed_at=_now())
def split(store, cluster_id, boundary_immich_id):
return store.split_cluster(cluster_id, boundary_immich_id)
def merge(store, cluster_id_a, cluster_id_b):
return store.merge_clusters(cluster_id_a, cluster_id_b)
def set_member(store, cluster_id, immich_id, included: bool) -> None:
store.set_member_inclusion(cluster_id, immich_id, included)
def approve_high_confidence(store, threshold: float = 0.75) -> int:
count = 0
for c in store.all_clusters():
if c.status == "pending" and c.kind_guess == "trip" and c.confidence >= threshold:
approve(store, c.id)
count += 1
return count
+59
View File
@@ -0,0 +1,59 @@
from photoflow.core import Store
from photoflow.core.models import Asset, Cluster, ClusterMember
from app import review
def _store(tmp_path):
s = Store(str(tmp_path / "t.db")).connect()
for i, t in [("a", "2019-06-01"), ("b", "2019-06-02"), ("c", "2019-06-03")]:
s.upsert_asset(Asset(immich_id=i, taken_at=t))
return s
def _cluster(s, name="Trip", conf=0.9, kind="trip", ids=("a", "b")):
return s.insert_cluster(
Cluster(start_at="2019-06-01", end_at="2019-06-03", suggested_name=name,
confidence=conf, kind_guess=kind, status="pending"),
[ClusterMember(cluster_id=0, immich_id=i) for i in ids])
def test_approve_uses_suggested_when_no_name(tmp_path):
s = _store(tmp_path)
cid = _cluster(s, name="Venice")
review.approve(s, cid)
c = s.get_cluster(cid)
assert c.status == "approved" and c.decided_name == "Venice" and c.reviewed_at
review.approve(s, cid, name="Venezia")
assert s.get_cluster(cid).decided_name == "Venezia"
s.close()
def test_non_trip_skip(tmp_path):
s = _store(tmp_path)
cid = _cluster(s)
review.mark_non_trip(s, cid)
assert s.get_cluster(cid).status == "non_trip"
c2 = _cluster(s)
review.skip(s, c2)
assert s.get_cluster(c2).status == "skipped"
s.close()
def test_set_member(tmp_path):
s = _store(tmp_path)
cid = _cluster(s, ids=("a", "b"))
review.set_member(s, cid, "b", False)
inc = {m.immich_id: m.included for _, m in s.cluster_members(cid)}
assert inc["b"] is False
s.close()
def test_approve_high_confidence_only_trips(tmp_path):
s = _store(tmp_path)
hi = _cluster(s, conf=0.9, kind="trip")
_cluster(s, conf=0.3, kind="trip") # too low
_cluster(s, conf=0.9, kind="everyday") # everyday excluded
n = review.approve_high_confidence(s, threshold=0.75)
assert n == 1
assert s.get_cluster(hi).status == "approved"
s.close()