feat(annotate): Words-Frame – Safe Words mit Level per Bild speichern

- Rechte Sidebar in zwei Frames aufgeteilt: Objects (bisherig) + Words (neu)
- Words-Frame: Wörter + Level (1–100) per Bild anlegen, dedupliziert via words_pictures Junction
- Pending-Words in Primary-Farbe mit inline Level-Edit, gespeicherte Words in neutralem Grau
- Save-Button speichert alle pending Words nach Directus (status=draft, title_de, level, picture-Link)
- Automatisches Laden der Bild-Words bei Bildwechsel
- Backend: GET/POST /api/directus/pictures/<pic_id>/words (words_pictures Junction, _find_or_create_word)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tim Leikauf
2026-05-06 21:05:19 +02:00
parent 20e8176cab
commit 84186110e7
4 changed files with 256 additions and 4 deletions

57
app.py
View File

@@ -113,6 +113,63 @@ def directus_object(obj_id):
return jsonify(data), status
@app.route("/api/directus/pictures/<pic_id>/words", methods=["GET", "POST"])
def directus_picture_words(pic_id):
"""Proxy: Safe-Words eines Bildes laden (GET) oder speichern (POST)."""
token = request.headers.get("Authorization", "")
if request.method == "GET":
junc, _ = _directus(
"GET",
f"/items/words_pictures?filter[pictures_id][_eq]={pic_id}&fields=id,words_id&limit=500",
token,
)
w_ids = [e["words_id"] for e in (junc.get("data") or []) if e.get("words_id")]
if not w_ids:
return jsonify({"data": []})
ids_param = urllib.parse.quote(",".join(w_ids), safe="")
w_data, _ = _directus(
"GET",
f"/items/words?filter[id][_in]={ids_param}&filter[status][_neq]=archived&fields=id,title_de,level,status&limit=500",
token,
)
junc_by_word = {e["words_id"]: e["id"] for e in (junc.get("data") or [])}
items = [
{
"id": junc_by_word.get(w["id"], ""),
"word_id": w["id"],
"title_de": w["title_de"],
"level": w.get("level") or 50,
"status": w.get("status", ""),
}
for w in (w_data.get("data") or [])
]
return jsonify({"data": items})
else: # POST
body = request.get_json(force=True, silent=True) or {}
words = body.get("words", [])
_ensure_junction("words_pictures", "words_id", "pictures_id", token)
saved = 0
for entry in words:
title_de = (entry.get("title_de") or "").strip()
level = int(entry.get("level") or 50)
if not title_de:
continue
try:
wid, _ = _find_or_create_word(title_de, level, token)
_ensure_link(
"words_pictures",
{"words_id": wid, "pictures_id": pic_id},
{"words_id": wid, "pictures_id": pic_id},
token,
)
saved += 1
except Exception as e:
print(f"[picture_words] error for '{title_de}': {e}")
return jsonify({"ok": True, "saved": saved})
@app.route("/api/images", methods=["GET"])
def list_images():
"""