from __future__ import annotations import logging import os from pathlib import Path from typing import List, Optional import gradio as gr from dotenv import load_dotenv from db import ( configure_database, create_score, ensure_user, get_global_top, get_image_top, get_user_recent, init_db, normalize_username, scores_to_rows, session_scope, validate_username, ) from model import ClipScorer, ImageEntry, load_image_entries load_dotenv() logging.basicConfig(level=logging.INFO) logger = logging.getLogger("app") DATABASE_URL = os.getenv("DATABASE_URL") if not DATABASE_URL: raise RuntimeError("DATABASE_URL ist nicht gesetzt. Bitte in den Space-Secrets hinterlegen.") configure_database(DATABASE_URL) init_db() IMAGE_ENTRIES: List[ImageEntry] = [] SCORER: Optional[ClipScorer] = None EMBEDDING_ERROR: Optional[str] = None try: IMAGE_ENTRIES = load_image_entries(Path("images.csv")) except Exception as exc: # noqa: BLE001 EMBEDDING_ERROR = f"images.csv konnte nicht geladen werden: {exc}" logger.exception("Fehler beim Laden der images.csv", exc_info=exc) if EMBEDDING_ERROR is None and IMAGE_ENTRIES: try: SCORER = ClipScorer() SCORER.load_precomputed_embeddings(IMAGE_ENTRIES) except Exception as exc: # noqa: BLE001 EMBEDDING_ERROR = ( "Embeddings konnten nicht geladen werden. Bitte precompute_embeddings.py ausführen." f"\nFehler: {exc}" ) logger.exception("Fehler beim Laden der Embeddings", exc_info=exc) APP_READY = EMBEDDING_ERROR is None HELP_TEXT = ( "Beschreibe das angezeigte Bild in 3 bis 500 Zeichen. " "Die KI vergleicht deine Beschreibung mit der (theoretisch) perfekten Prompt für das angezeigte Bild. " "Der Score reicht von 0 (gar nicht passend) bis 1000 (perfekte Übereinstimmung)." ) LEADERBOARD_HEADERS = [ "Platz", "Benutzername", "Bild-ID", "Score", "Ähnlichkeit", "Text", "Zeitstempel", ] def fetch_global_rows() -> List[List[object]]: with session_scope() as session: scores = get_global_top(session) return scores_to_rows(scores, include_rank=True) def fetch_image_rows(image_id: str) -> List[List[object]]: if not image_id: return [] with session_scope() as session: scores = get_image_top(session, image_id) return scores_to_rows(scores, include_rank=True) def fetch_user_rows(username: str) -> List[List[object]]: if not username: return [] canonical = normalize_username(username) with session_scope() as session: scores = get_user_recent(session, canonical) return scores_to_rows(scores, include_rank=True) def handle_score(username: str, text: str, image_index: int | None): if not APP_READY or SCORER is None or not IMAGE_ENTRIES: raise gr.Error( "Embeddings sind nicht verfügbar. Bitte vor dem Start precompute_embeddings.py ausführen." ) username_clean = (username or "").strip() if not validate_username(username_clean): raise gr.Error("Ungültiger Benutzername. Erlaubt sind 3-20 Zeichen aus A-Z, a-z, 0-9, _.-") text_clean = (text or "").strip() if len(text_clean) < 3: raise gr.Error("Bitte gib mindestens 3 Zeichen Text ein.") if len(text_clean) > 500: raise gr.Error("Der Beschreibungstext darf höchstens 500 Zeichen enthalten.") if image_index is None: image_index = 0 if image_index < 0 or image_index >= len(IMAGE_ENTRIES): image_index = 0 entry = IMAGE_ENTRIES[image_index] similarity, score = SCORER.score_text_for_image(text_clean, entry.image_id) with session_scope() as session: user = ensure_user(session, username_clean) create_score( session, user=user, image_id=entry.image_id, score_value=score, similarity=similarity, text=text_clean, ) global_rows = fetch_global_rows() image_rows = fetch_image_rows(entry.image_id) user_rows = fetch_user_rows(username_clean) gr.Info("Score gespeichert!") return ( gr.update(value=score), gr.update(value=round(similarity, 4)), gr.update(value=global_rows), gr.update(value=image_rows), gr.update(value=user_rows), ) def handle_next_image(current_index: int | None): if not IMAGE_ENTRIES: raise gr.Error("Keine Bilder konfiguriert.") if current_index is None: current_index = 0 new_index = (current_index + 1) % len(IMAGE_ENTRIES) entry = IMAGE_ENTRIES[new_index] image_rows = fetch_image_rows(entry.image_id) return ( new_index, gr.update(value=entry.image_url), gr.update(value=f"**Bild-ID:** {entry.image_id}"), gr.update(value=entry.image_id), gr.update(value=image_rows), ) def handle_image_dropdown(image_id: str): rows = fetch_image_rows(image_id) return gr.update(value=rows) def handle_username_change(username: str): if not username: return gr.update(value=[]) username_clean = username.strip() if not validate_username(username_clean): gr.Warning("Benutzername ungültig. Zeige keine Ergebnisse.") return gr.update(value=[]) rows = fetch_user_rows(username_clean) return gr.update(value=rows) def build_interface() -> gr.Blocks: status_message = "" if EMBEDDING_ERROR: status_message = f"⚠️ {EMBEDDING_ERROR}" elif not IMAGE_ENTRIES: status_message = "⚠️ Keine Bilder konfiguriert." else: status_message = "✅ Bereit zum Scoren!" initial_index = 0 if IMAGE_ENTRIES else None initial_entry = IMAGE_ENTRIES[0] if IMAGE_ENTRIES else None global_rows = fetch_global_rows() if APP_READY else [] image_rows = fetch_image_rows(initial_entry.image_id) if initial_entry else [] image_choices = [entry.image_id for entry in IMAGE_ENTRIES] with gr.Blocks(title="KI Prompt Challenge", theme=gr.themes.Soft()) as demo: gr.Markdown("# KI Prompt Challenge") gr.Markdown(HELP_TEXT) gr.Markdown(status_message) image_state = gr.State(initial_index) with gr.Row(): with gr.Column(scale=3): image_component = gr.Image( value=initial_entry.image_url if initial_entry else None, label="Bild", show_download_button=False, ) image_info = gr.Markdown( f"**Bild-ID:** {initial_entry.image_id}" if initial_entry else "Kein Bild geladen." ) next_button = gr.Button( "Nächstes Bild", variant="secondary", interactive=bool(IMAGE_ENTRIES), ) with gr.Column(scale=2): username_input = gr.Textbox( label="Benutzername", placeholder="3-20 Zeichen (A-Z, a-z, 0-9, _.-)", ) text_input = gr.Textbox( label="Beschreibungstext", placeholder="Was siehst du auf dem Bild?", lines=5, ) score_button = gr.Button( "Scoren", variant="primary", interactive=APP_READY and bool(IMAGE_ENTRIES), ) score_output = gr.Number(label="Score", value=0, precision=0) similarity_output = gr.Number(label="Ähnlichkeit", value=0.0, precision=4) gr.Markdown("### Leaderboard") with gr.Tabs(): with gr.Tab("Top 50"): global_df = gr.Dataframe( headers=LEADERBOARD_HEADERS, value=global_rows, datatype=[ "number", "str", "str", "number", "number", "str", "str", ], interactive=False, wrap=True, ) with gr.Tab("Dieses Bild Top 50"): image_dropdown = gr.Dropdown( choices=image_choices, value=initial_entry.image_id if initial_entry else None, label="Bild auswählen", interactive=bool(image_choices), ) image_df = gr.Dataframe( headers=LEADERBOARD_HEADERS, value=image_rows, datatype=[ "number", "str", "str", "number", "number", "str", "str", ], interactive=False, wrap=True, ) with gr.Tab("Meine letzten 50"): user_df = gr.Dataframe( headers=LEADERBOARD_HEADERS, value=[], datatype=[ "number", "str", "str", "number", "number", "str", "str", ], interactive=False, wrap=True, ) next_button.click( handle_next_image, inputs=[image_state], outputs=[image_state, image_component, image_info, image_dropdown, image_df], ) score_button.click( handle_score, inputs=[username_input, text_input, image_state], outputs=[score_output, similarity_output, global_df, image_df, user_df], ) image_dropdown.change(handle_image_dropdown, inputs=[image_dropdown], outputs=[image_df]) username_input.change(handle_username_change, inputs=[username_input], outputs=[user_df]) return demo demo = build_interface() if __name__ == "__main__": demo.launch()