Files
hejyou_content_creation/app.py
2026-04-23 22:10:45 +02:00

816 lines
29 KiB
Python

from pathlib import Path
from datetime import datetime
from uuid import uuid4
import json
from flask import Flask, send_from_directory, request, jsonify
from flask_cors import CORS
from PIL import Image
import ollama
BASE_DIR = Path(__file__).resolve().parent
PICTURES_DIR = BASE_DIR / "pictures"
OBJECTS_DIR = BASE_DIR / "objects_image"
SENTENCE_DIR = BASE_DIR / "sentence_object"
PROMPTS_DIR = BASE_DIR / "prompts"
app = Flask(
__name__,
template_folder=str(BASE_DIR / "templates"),
static_folder=str(BASE_DIR / "static"),
)
CORS(app)
def read_prompt(filepath: Path, fallback: str) -> str:
"""
Hilfsfunktion, um Prompt-Dateien zu lesen.
"""
try:
if filepath.exists():
return filepath.read_text(encoding="utf-8").strip()
except Exception:
pass
return fallback.strip()
@app.route("/api/images", methods=["GET"])
def list_images():
"""
Gibt alle Bilder im pictures-Ordner zurück.
Query-Parameter: ?mode=draw (Standard) oder ?mode=generate
draw → Bilder ohne _saved-Suffix
generate → Bilder mit _saved-Suffix
"""
mode = request.args.get("mode", "draw")
PICTURES_DIR.mkdir(parents=True, exist_ok=True)
if mode == "generate":
image_paths = sorted(
[f for f in PICTURES_DIR.iterdir() if f.is_file() and f.stem.endswith("_saved")],
key=lambda p: p.stat().st_mtime,
)
else:
image_paths = sorted(
[f for f in PICTURES_DIR.iterdir() if f.is_file() and not f.stem.endswith("_saved")],
key=lambda p: p.stat().st_mtime,
)
return jsonify({"images": [p.name for p in image_paths]})
REACT_BUILD_DIR = BASE_DIR / "static" / "react"
def _serve_spa():
"""Liefert die React SPA index.html für alle Frontend-Routen."""
return send_from_directory(REACT_BUILD_DIR, "index.html")
@app.route("/")
@app.route("/draw")
@app.route("/generate")
def serve_spa():
return _serve_spa()
@app.route("/assets/<path:filename>")
def serve_react_assets(filename: str):
"""Liefert JS/CSS-Assets aus dem Vite-Build."""
return send_from_directory(REACT_BUILD_DIR / "assets", filename)
@app.route("/pictures/<path:filename>")
def serve_picture(filename: str):
# Statisches Ausliefern der Originalbilder
return send_from_directory(PICTURES_DIR, filename)
@app.route("/objects_image/<path:filename>")
def serve_object_image(filename: str):
# Ausliefern der gespeicherten Ausschnitt-Bilder
return send_from_directory(OBJECTS_DIR, filename)
@app.route("/api/objects", methods=["GET"])
def list_objects_for_image():
"""
Gibt alle gespeicherten Objekte (Ausschnitte) zu einem Quellbild zurück.
Query-Parameter:
?filename=testbild.png
"""
filename = request.args.get("filename")
if not filename:
return jsonify({"error": "Missing filename query parameter"}), 400
# Für *_saved-Bilder auch Objekte berücksichtigen, deren source_filename
# noch auf den ursprünglichen Namen ohne "_saved" zeigt (Kompatibilität)
base_filename = filename
legacy_filename = None
if base_filename.endswith(".png") or base_filename.endswith(".jpg") or base_filename.endswith(".jpeg") or base_filename.endswith(".webp"):
stem = base_filename.rsplit(".", 1)[0]
suffix = base_filename.rsplit(".", 1)[1]
if stem.endswith("_saved"):
legacy_filename = f"{stem[:-6]}.{suffix}"
OBJECTS_DIR.mkdir(parents=True, exist_ok=True)
objects = []
for meta_file in OBJECTS_DIR.glob("*.txt"):
try:
meta = json.loads(meta_file.read_text(encoding="utf-8"))
except Exception:
continue
src_name = meta.get("source_filename")
if src_name != filename and (legacy_filename is None or src_name != legacy_filename):
continue
objects.append(
{
"id": meta.get("id"),
"image_file": meta.get("image_file"),
"title_de": meta.get("title_de", ""),
"position_de": meta.get("position_de", ""),
"action_de": meta.get("action_de", ""),
"condition_de": meta.get("condition_de", ""),
# Von KI erzeugte Details (optional)
"label_en": meta.get("label_en"),
"label_de": meta.get("label_de"),
"label_se": meta.get("label_se"),
"color_en": meta.get("color_en"),
"adjective_en": meta.get("adjective_en"),
"action_verb_en": meta.get("action_verb_en"),
"preposition_en": meta.get("preposition_en"),
"relative_position_en": meta.get("relative_position_en"),
"season_en": meta.get("season_en"),
"mode": meta.get("mode"),
"bbox": meta.get("bbox"),
"polygon": meta.get("polygon"),
"hierarchy": meta.get("hierarchy", 1),
"parent_id": meta.get("parent_id"),
"created_at": meta.get("created_at"),
}
)
# Älteste zuerst sortieren (1 = ältestes Objekt)
objects.sort(key=lambda o: o.get("created_at") or "")
# Laufende Nummer je Bild (1..n) nach obiger Sortierung
for idx, obj in enumerate(objects, start=1):
obj["index"] = idx
return jsonify({"objects": objects})
@app.route("/api/object/<obj_id>/hierarchy", methods=["POST"])
def update_object_hierarchy(obj_id: str):
"""
Aktualisiert die Hierarchie (1, 2 oder 3) eines gespeicherten Objekts.
"""
data = request.get_json(force=True, silent=True) or {}
try:
hierarchy = int(data.get("hierarchy"))
except (TypeError, ValueError):
return jsonify({"error": "Invalid hierarchy"}), 400
if hierarchy not in (1, 2, 3):
return jsonify({"error": "Hierarchy must be 1, 2 or 3"}), 400
meta_path = OBJECTS_DIR / f"{obj_id}.txt"
if not meta_path.exists():
return jsonify({"error": "Object not found"}), 404
try:
meta = json.loads(meta_path.read_text(encoding="utf-8"))
except Exception:
return jsonify({"error": "Could not read meta file"}), 500
meta["hierarchy"] = hierarchy
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
return jsonify({"id": obj_id, "hierarchy": hierarchy})
@app.route("/api/object/<obj_id>", methods=["POST"])
def update_object_meta(obj_id: str):
"""
Aktualisiert Text-Metadaten (title_de, position_de, action_de, condition_de)
eines gespeicherten Objekts.
"""
data = request.get_json(force=True, silent=True) or {}
meta_path = OBJECTS_DIR / f"{obj_id}.txt"
if not meta_path.exists():
return jsonify({"error": "Object not found"}), 404
try:
meta = json.loads(meta_path.read_text(encoding="utf-8"))
except Exception:
return jsonify({"error": "Could not read meta file"}), 500
for key in ("title_de", "position_de", "action_de", "condition_de"):
if key in data:
meta[key] = data.get(key, "")
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
return jsonify({k: meta.get(k) for k in ("id", "title_de", "position_de", "action_de", "condition_de")})
@app.route("/api/object/<obj_id>/parent", methods=["POST"])
def update_object_parent(obj_id: str):
"""
Aktualisiert die Parent-Relation eines Objekts.
Erwartet JSON: { "parent_id": "<uuid>" | null }
"""
data = request.get_json(force=True, silent=True) or {}
parent_id = data.get("parent_id")
meta_path = OBJECTS_DIR / f"{obj_id}.txt"
if not meta_path.exists():
return jsonify({"error": "Object not found"}), 404
try:
meta = json.loads(meta_path.read_text(encoding="utf-8"))
except Exception:
return jsonify({"error": "Could not read meta file"}), 500
# Optional: prüfen, ob parent_id existiert und zum selben Bild gehört
if parent_id:
parent_meta_path = OBJECTS_DIR / f"{parent_id}.txt"
if not parent_meta_path.exists():
return jsonify({"error": "Parent object not found"}), 400
try:
parent_meta = json.loads(parent_meta_path.read_text(encoding="utf-8"))
except Exception:
return jsonify({"error": "Could not read parent meta file"}), 500
if parent_meta.get("source_filename") != meta.get("source_filename"):
return jsonify({"error": "Parent must belong to same source image"}), 400
meta["parent_id"] = parent_id or None
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
return jsonify({"id": obj_id, "parent_id": meta.get("parent_id")})
@app.route("/api/object/<obj_id>/generate_details", methods=["POST"])
def generate_object_details(obj_id: str):
"""
Erzeugt mit Llama 3.2 Vision zusätzliche englische Metadaten zu einem Objekt
auf Basis des ausgeschnittenen Objektbildes + vorhandener deutscher Felder.
"""
meta_path = OBJECTS_DIR / f"{obj_id}.txt"
if not meta_path.exists():
return jsonify({"error": "Object not found"}), 404
try:
meta = json.loads(meta_path.read_text(encoding="utf-8"))
except Exception:
return jsonify({"error": "Could not read meta file"}), 500
image_file = meta.get("image_file")
if not image_file:
return jsonify({"error": "Object has no image_file"}), 400
image_path = OBJECTS_DIR / image_file
if not image_path.exists():
return jsonify({"error": "Object image not found"}), 404
# Prompt laden
prompt_template = read_prompt(
PROMPTS_DIR / "create_details.txt",
"Analyze this object and return one JSON object with the requested fields.",
)
title_de = meta.get("title_de", "") or ""
action_de = meta.get("action_de", "") or ""
condition_de = meta.get("condition_de", "") or ""
# Platzhalter im Prompt ersetzen (einfache .format-Verwendung)
try:
prompt = prompt_template.format(
title_de=title_de,
action_de=action_de,
condition_de=condition_de,
)
except Exception:
# Falls das Format fehlschlägt, einfach Rohprompt + Kontext anhängen
prompt = (
f"{prompt_template}\n\n"
f"Titel (Deutsch): {title_de}\n"
f"Status (Deutsch): {action_de}\n"
f"Zustand (Deutsch): {condition_de}\n"
)
# Bild als Bytes laden
try:
img_bytes = image_path.read_bytes()
except Exception:
return jsonify({"error": "Could not read object image"}), 500
try:
response = ollama.chat(
model="llama3.2-vision",
messages=[{"role": "user", "content": prompt, "images": [img_bytes]}],
format="json",
options={"temperature": 0},
)
except Exception as e:
return jsonify({"error": f"LLM request failed: {e}"}), 500
content = response.get("message", {}).get("content")
if not content:
return jsonify({"error": "Empty response from LLM"}), 500
try:
ai_data = json.loads(content)
except Exception:
# Fallback: versuchen, das erste JSON-Objekt zu extrahieren
try:
start = content.index("{")
end = content.rindex("}") + 1
ai_data = json.loads(content[start:end])
except Exception:
return jsonify({"error": "Could not parse JSON from LLM response"}), 500
# Debug-Ausgabe des rohen AI-Outputs
try:
print(f"[generate_details] Raw AI data for {obj_id}: {json.dumps(ai_data, ensure_ascii=False)}")
except Exception:
print(f"[generate_details] Raw AI data for {obj_id} (non-serializable): {ai_data!r}")
# Erwartete Felder übernehmen (gemäß neuem Prompt)
fields = [
"label_en",
"label_de",
"label_se",
"color_en",
"adjective_en",
"action_verb_en",
"preposition_en",
"relative_position_en",
"season_en",
]
for key in fields:
if key in ai_data:
meta[key] = ai_data.get(key)
# Metadatei zurückschreiben
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
# Nur die neuen Felder zurückgeben (None als leeren String konvertieren)
result = {key: (meta.get(key) or "") for key in fields}
try:
print(f"[generate_details] Stored meta for {obj_id}: {json.dumps(result, ensure_ascii=False)}")
except Exception:
print(f"[generate_details] Stored meta for {obj_id}: {result!r}")
return jsonify(result)
def load_sentences_for_object(obj_id: str):
"""
Lädt alle gespeicherten Sätze für ein Objekt aus sentence_object/<obj_id>.txt.
Rückgabe: Liste von Dicts mit mindestens question_en, answer_en, created_at.
"""
SENTENCE_DIR.mkdir(parents=True, exist_ok=True)
sentence_path = SENTENCE_DIR / f"{obj_id}.txt"
if not sentence_path.exists():
return []
try:
data = json.loads(sentence_path.read_text(encoding="utf-8"))
if isinstance(data, list):
return data
if isinstance(data, dict):
# Fallback: einzelnes Objekt in Liste wrappen
return [data]
return []
except Exception:
return []
@app.route("/api/object/<obj_id>/sentences", methods=["GET"])
def get_object_sentences(obj_id: str):
"""
Gibt alle bisher erzeugten KI-Sätze zu einem Objekt zurück.
"""
sentences = load_sentences_for_object(obj_id)
return jsonify({"object_id": obj_id, "sentences": sentences})
@app.route("/api/object/<obj_id>/generate_sentence", methods=["POST"])
def generate_object_sentence(obj_id: str):
"""
Erzeugt mit Llama 3.1 einen neuen englischen Frage-Antwort-Satz
zu einem Objekt basierend auf den KI-Details und bisherigen Sätzen.
Speichert alle Sätze in sentence_object/<obj_id>.txt.
"""
# Objekt-Metadaten laden (inkl. KI-Details)
meta_path = OBJECTS_DIR / f"{obj_id}.txt"
if not meta_path.exists():
return jsonify({"error": "Object not found"}), 404
try:
meta = json.loads(meta_path.read_text(encoding="utf-8"))
except Exception:
return jsonify({"error": "Could not read meta file"}), 500
# Relevante Felder für den Satz (KI-Details)
details = {
"label_en": meta.get("label_en"),
"label_de": meta.get("label_de"),
"label_se": meta.get("label_se"),
"color_en": meta.get("color_en"),
"adjective_en": meta.get("adjective_en"),
"action_verb_en": meta.get("action_verb_en"),
"preposition_en": meta.get("preposition_en"),
"relative_position_en": meta.get("relative_position_en"),
"season_en": meta.get("season_en"),
"title_de": meta.get("title_de"),
"position_de": meta.get("position_de"),
"action_de": meta.get("action_de"),
"condition_de": meta.get("condition_de"),
}
# Bisherige Sätze laden
previous_sentences = load_sentences_for_object(obj_id)
# Prompt laden
prompt_template = read_prompt(
PROMPTS_DIR / "create_sentence.txt",
"Create one new English question_en and answer_en as JSON, based on the object details and avoiding previous sentences.",
)
# Vollständigen Prompt zusammensetzen
try:
details_json = json.dumps(details, ensure_ascii=False, indent=2)
prev_json = json.dumps(previous_sentences, ensure_ascii=False, indent=2)
except Exception:
details_json = str(details)
prev_json = str(previous_sentences)
prompt = (
f"{prompt_template}\n\n"
f"OBJECT_DETAILS_JSON:\n{details_json}\n\n"
f"PREVIOUS_SENTENCES_JSON:\n{prev_json}\n"
)
# LLM-Aufruf (nur Text, kein Bild nötig)
try:
response = ollama.chat(
model="llama3.1",
messages=[{"role": "user", "content": prompt}],
format="json",
options={"temperature": 0.2},
)
except Exception as e:
return jsonify({"error": f"LLM request failed: {e}"}), 500
content = response.get("message", {}).get("content")
if not content:
return jsonify({"error": "Empty response from LLM"}), 500
try:
ai_data = json.loads(content)
except Exception:
# Fallback: erstes JSON-Objekt extrahieren
try:
start = content.index("{")
end = content.rindex("}") + 1
ai_data = json.loads(content[start:end])
except Exception:
return jsonify({"error": "Could not parse JSON from LLM response"}), 500
question_simple = (ai_data.get("question_simple_en") or "").strip()
answer_simple = (ai_data.get("answer_simple_en") or "").strip()
question_advanced = (ai_data.get("question_advanced_en") or "").strip()
answer_advanced = (ai_data.get("answer_advanced_en") or "").strip()
if not question_simple or not answer_simple or not question_advanced or not answer_advanced:
return jsonify({"error": "LLM response missing required fields (question_simple_en, answer_simple_en, question_advanced_en, answer_advanced_en)"}), 500
# Neuen Satz mit Timestamp anreichern
entry = {
"object_id": obj_id,
"created_at": datetime.now().isoformat(),
"question_simple_en": question_simple,
"answer_simple_en": answer_simple,
"question_advanced_en": question_advanced,
"answer_advanced_en": answer_advanced,
}
sentences = previous_sentences + [entry]
# In Datei speichern
SENTENCE_DIR.mkdir(parents=True, exist_ok=True)
sentence_path = SENTENCE_DIR / f"{obj_id}.txt"
sentence_path.write_text(json.dumps(sentences, ensure_ascii=False, indent=2), encoding="utf-8")
return jsonify({"object_id": obj_id, "sentence": entry, "count": len(sentences)})
@app.route("/api/image/save", methods=["POST"])
def save_image():
"""
Markiert ein Bild und alle zugehörigen Objekte als 'gespeichert',
indem der Dateiname um '_saved' erweitert wird und die Metadaten
der Objekte angepasst werden.
"""
data = request.get_json(force=True, silent=True) or {}
filename = data.get("filename")
if not filename:
return jsonify({"error": "Missing filename"}), 400
src_path = PICTURES_DIR / filename
if not src_path.exists():
return jsonify({"error": "Image not found"}), 404
if src_path.stem.endswith("_saved"):
return jsonify({"error": "Image already saved"}), 400
new_name = f"{src_path.stem}_saved{src_path.suffix}"
dst_path = PICTURES_DIR / new_name
# Bild umbenennen
src_path.rename(dst_path)
# Zugehörige Objekte aktualisieren
OBJECTS_DIR.mkdir(parents=True, exist_ok=True)
for meta_file in OBJECTS_DIR.glob("*.txt"):
try:
meta = json.loads(meta_file.read_text(encoding="utf-8"))
except Exception:
continue
if meta.get("source_filename") != filename:
continue
meta["source_filename"] = new_name
meta_file.write_text(
json.dumps(meta, ensure_ascii=False, indent=2),
encoding="utf-8",
)
return jsonify({"old_name": filename, "new_name": new_name})
@app.route("/api/crop", methods=["POST"])
def crop_image():
"""
Erwartet JSON:
{
"filename": "bild.jpg",
"selections": [
{
"number": 1,
"mode": "rect" | "polygon",
"bbox": {"x": 10, "y": 20, "width": 200, "height": 150}, // für rect
"polygon": [{"x": 10, "y": 20}, ...] // für polygon
},
...
],
"title_de": "...",
"position_de": "...",
"action_de": "...",
"condition_de": "..."
}
ODER (Legacy-Format für Kompatibilität):
{
"filename": "bild.jpg",
"mode": "rect" | "polygon",
"x": 10, "y": 20, "width": 200, "height": 150, // für rect
"polygon": [{"x": 10, "y": 20}, ...] // für polygon
}
"""
data = request.get_json(force=True, silent=True) or {}
if "filename" not in data:
return jsonify({"error": "Missing filename"}), 400
src_path = PICTURES_DIR / data["filename"]
if src_path.stem.endswith("_saved"):
return jsonify({"error": "Cannot crop on saved image"}), 400
if not src_path.exists():
return jsonify({"error": "Source image not found"}), 404
OBJECTS_DIR.mkdir(parents=True, exist_ok=True)
# UUID für Bild + Metadaten
obj_id = uuid4().hex
timestamp = datetime.now().isoformat()
out_image_name = f"{obj_id}{src_path.suffix}"
out_image_path = OBJECTS_DIR / out_image_name
out_meta_path = OBJECTS_DIR / f"{obj_id}.txt"
# Prüfen, ob neues Format (selections) oder Legacy-Format
selections = data.get("selections")
if selections and isinstance(selections, list) and len(selections) > 0:
# Neues Format: mehrere Auswahlen
processed_selections = []
with Image.open(src_path) as img:
img_w, img_h = img.size
for sel in selections:
sel_mode = sel.get("mode")
if sel_mode == "rect":
bbox = sel.get("bbox")
if not bbox:
continue
try:
x = int(bbox.get("x", 0))
y = int(bbox.get("y", 0))
w = int(bbox.get("width", 0))
h = int(bbox.get("height", 0))
except (ValueError, TypeError):
continue
if w <= 0 or h <= 0:
continue
x2 = min(x + w, img_w)
y2 = min(y + h, img_h)
x1 = max(0, x)
y1 = max(0, y)
if x1 >= x2 or y1 >= y2:
continue
processed_selections.append({
"number": sel.get("number", len(processed_selections) + 1),
"mode": "rect",
"bbox": {"x": x1, "y": y1, "width": x2 - x1, "height": y2 - y1},
})
elif sel_mode == "polygon":
polygon = sel.get("polygon")
if not isinstance(polygon, list) or len(polygon) < 3:
continue
try:
xs = [int(p.get("x", 0)) for p in polygon]
ys = [int(p.get("y", 0)) for p in polygon]
except (KeyError, TypeError, ValueError):
continue
min_x = max(min(xs), 0)
min_y = max(min(ys), 0)
max_x = min(max(xs), img_w)
max_y = min(max(ys), img_h)
if min_x >= max_x or min_y >= max_y:
continue
processed_selections.append({
"number": sel.get("number", len(processed_selections) + 1),
"mode": "polygon",
"polygon": polygon,
"bbox": {"x": min_x, "y": min_y, "width": max_x - min_x, "height": max_y - min_y},
})
if not processed_selections:
return jsonify({"error": "No valid selections provided"}), 400
# Erstes Bild aus erster Auswahl erstellen
first_sel = processed_selections[0]
try:
with Image.open(src_path) as img:
if first_sel["mode"] == "rect":
bbox = first_sel["bbox"]
x1 = bbox["x"]
y1 = bbox["y"]
x2 = x1 + bbox["width"]
y2 = y1 + bbox["height"]
cropped = img.crop((x1, y1, x2, y2))
else: # polygon - verwende Bounding-Box für das Bild
bbox = first_sel["bbox"]
x1 = bbox["x"]
y1 = bbox["y"]
x2 = x1 + bbox["width"]
y2 = y1 + bbox["height"]
cropped = img.crop((x1, y1, x2, y2))
# Bild speichern (Format beibehalten)
img_format = img.format or "PNG"
cropped.save(out_image_path, format=img_format)
print(f"[crop_image] Bild gespeichert: {out_image_path} (Größe: {cropped.size}, Format: {img_format})")
except Exception as e:
print(f"[crop_image] Fehler beim Erstellen des Bildes: {e}")
return jsonify({"error": f"Failed to create image: {e}"}), 500
# Metadaten mit allen Auswahlen speichern
meta = {
"id": obj_id,
"created_at": timestamp,
"source_filename": data["filename"],
"mode": processed_selections[0]["mode"], # Legacy-Feld
"bbox": processed_selections[0]["bbox"], # Legacy-Feld
"image_file": out_image_name,
"hierarchy": 1,
"parent_id": None,
"title_de": data.get("title_de", ""),
"position_de": data.get("position_de", ""),
"action_de": data.get("action_de", ""),
"condition_de": data.get("condition_de", ""),
"selections": processed_selections, # Neue Feld: alle Auswahlen nummeriert
}
if processed_selections[0]["mode"] == "polygon":
meta["polygon"] = processed_selections[0]["polygon"] # Legacy-Feld
else:
# Legacy-Format: einzelne Auswahl (für Rückwärtskompatibilität)
mode = data.get("mode", "rect")
if mode not in {"rect", "polygon"}:
return jsonify({"error": "Invalid mode"}), 400
with Image.open(src_path) as img:
img_w, img_h = img.size
if mode == "rect":
try:
x = int(data["x"])
y = int(data["y"])
w = int(data["width"])
h = int(data["height"])
except (ValueError, TypeError, KeyError):
return jsonify({"error": "Invalid rectangle coordinates"}), 400
if w <= 0 or h <= 0:
return jsonify({"error": "Width and height must be positive"}), 400
x2 = min(x + w, img_w)
y2 = min(y + h, img_h)
x1 = max(0, x)
y1 = max(0, y)
if x1 >= x2 or y1 >= y2:
return jsonify({"error": "Crop area outside of image"}), 400
cropped = img.crop((x1, y1, x2, y2))
bbox = {"x": x1, "y": y1, "width": x2 - x1, "height": y2 - y1}
else: # polygon
polygon = data.get("polygon") or []
if not isinstance(polygon, list) or len(polygon) < 3:
return jsonify({"error": "Polygon must have at least 3 points"}), 400
try:
xs = [int(p["x"]) for p in polygon]
ys = [int(p["y"]) for p in polygon]
except (KeyError, TypeError, ValueError):
return jsonify({"error": "Invalid polygon coordinates"}), 400
min_x = max(min(xs), 0)
min_y = max(min(ys), 0)
max_x = min(max(xs), img_w)
max_y = min(max(ys), img_h)
if min_x >= max_x or min_y >= max_y:
return jsonify({"error": "Polygon bounding box outside of image"}), 400
cropped = img.crop((min_x, min_y, max_x, max_y))
bbox = {
"x": min_x,
"y": min_y,
"width": max_x - min_x,
"height": max_y - min_y,
}
cropped.save(out_image_path)
# Metadaten als JSON in .txt speichern
meta = {
"id": obj_id,
"created_at": timestamp,
"source_filename": data["filename"],
"mode": mode,
"bbox": bbox,
"image_file": out_image_name,
"hierarchy": 1,
"parent_id": None,
"title_de": data.get("title_de", ""),
"position_de": data.get("position_de", ""),
"action_de": data.get("action_de", ""),
"condition_de": data.get("condition_de", ""),
}
if mode == "polygon":
meta["polygon"] = data.get("polygon")
out_meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
return jsonify(
{
"id": obj_id,
"image_file": out_image_name,
"meta_file": out_meta_path.name,
}
)
if __name__ == "__main__":
# Du kannst den Port hier anpassen, z.B. 8000 oder 5000.
# Für externen Zugriff später einfach in Nginx/Reverse Proxy weiterleiten.
app.run(host="0.0.0.0", port=8000, debug=True)