import gradio as gr import os, json, random, glob from typing import List, Dict from PIL import Image, ImageDraw, ImageFont # --- Paramètres généraux --- IMAGE_DIR = os.getenv("IMAGE_DIR", "assets") # Dossier des images N_IMAGES = int(os.getenv("N_IMAGES", "24")) # Nombre d’images attendues IMG_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif") # --- Sondage : émotions disponibles --- EMOTIONS = ["joie", "tristesse", "peur", "colère", "surprise", "dégoût", "indifférence"] # --- Utilitaires --- def _centered_multiline(draw: ImageDraw.ImageDraw, xy, text: str, font: ImageFont.ImageFont, img_w: int): """Dessine un texte multi-lignes centré horizontalement autour de (xy[1]) en Y.""" x, y = xy line_h = int(font.size * 1.2) for i, line in enumerate(text.split("\n")): bbox = draw.textbbox((0, 0), line, font=font) w = bbox[2] - bbox[0] draw.text(((img_w - w) // 2, y + i * line_h), line, fill=(0, 0, 0), font=font) def generate_demo_assets(): """Crée un petit jeu de données de démo si le dossier est vide (sans vérité terrain).""" os.makedirs(IMAGE_DIR, exist_ok=True) files = [p for p in glob.glob(os.path.join(IMAGE_DIR, "*")) if p.lower().endswith(IMG_EXTS)] if len(files) >= N_IMAGES: return print("[setup] Génération d’un jeu de données de démonstration…") w, h = 640, 640 try: font = ImageFont.truetype("DejaVuSans-Bold.ttf", 36) except Exception: font = ImageFont.load_default() for i in range(N_IMAGES): bg = (random.randint(160, 240), random.randint(160, 240), random.randint(160, 240)) img = Image.new("RGB", (w, h), bg) d = ImageDraw.Draw(img) text = f"DEMO\nImage {i+1}" _centered_multiline(d, (0, h//2 - 40), text, font, w) fname = f"demo_{i+1:02d}.png" path = os.path.join(IMAGE_DIR, fname) img.save(path) def load_items() -> List[Dict]: """Charge simplement les chemins d’images (sans 'truth').""" os.makedirs(IMAGE_DIR, exist_ok=True) generate_demo_assets() # crée un dataset de démo si le dossier est vide files = [p for p in glob.glob(os.path.join(IMAGE_DIR, "*")) if p.lower().endswith(IMG_EXTS)] files.sort() if len(files) < N_IMAGES: raise RuntimeError(f"Il faut au moins {N_IMAGES} images dans '{IMAGE_DIR}'. Trouvé : {len(files)}.") files = files[:N_IMAGES] items = [{"path": p, "file": os.path.basename(p)} for p in files] return items ITEMS = load_items() # --- Construction de l’UI --- COLS = 3 # 24 images → 8 lignes def build_interface(items: List[Dict]): with gr.Blocks(theme=gr.themes.Soft(), css=""" .quiz-grid .gr-image {max-height: 220px} .score {font-size: 1.2rem; font-weight: 700} .center-button {display: flex; justify-content: center; margin-top: 0.5rem;} .warn-msg {text-align: center; color: #b91c1c; font-weight: 600;} """) as demo: gr.Markdown(""" # Sondage d'émotions 😃😢😱😡😮🤢😐 Pour chaque image, sélectionnez **l'émotion ressentie** parmi : **joie, tristesse, peur, colère, surprise, dégoût, indifférence**, puis cliquez sur **Valider mes 24 choix**. """) state_items = gr.State(items) # --- Zone du sondage --- with gr.Group(visible=True) as quiz_group: with gr.Row(): btn_shuffle = gr.Button("🔀 Mélanger l’ordre") btn_reset = gr.Button("♻️ Réinitialiser les choix") image_comps: List[gr.Image] = [] radio_comps: List[gr.Radio] = [] rows = (N_IMAGES + COLS - 1) // COLS idx = 0 with gr.Column(elem_classes=["quiz-grid"]): for r in range(rows): with gr.Row(): for c in range(COLS): if idx >= N_IMAGES: break with gr.Column(): img = gr.Image(value=items[idx]["path"], label=f"Image {idx+1}", interactive=False) image_comps.append(img) radio = gr.Radio( choices=EMOTIONS, label="Votre ressenti", value=None ) radio_comps.append(radio) idx += 1 # Message d'avertissement placé juste au-dessus du bouton warn_md = gr.Markdown("", visible=False, elem_classes=["warn-msg"]) # Bouton Valider centré with gr.Row(elem_classes=["center-button"]): btn_submit = gr.Button("✅ Valider mes 24 choix", variant="primary") # --- Zone des résultats --- with gr.Group(visible=False) as result_group: gr.Markdown("## Résultats du sondage") # Résumé (répartition des réponses) result_md = gr.Markdown(elem_classes=["score"]) # Détail par image df = gr.Dataframe( headers=["#", "Fichier", "Votre réponse"], row_count=(N_IMAGES, "fixed"), interactive=False, ) with gr.Row(): btn_again_same = gr.Button("↩️ Rejouer (même ordre)") btn_again_shuffle = gr.Button("🔁 Rejouer & mélanger") # --- Callbacks --- def on_shuffle(state): items = list(state) random.shuffle(items) img_updates = [gr.update(value=items[i]["path"], label=f"Image {i+1}") for i in range(N_IMAGES)] radio_updates = [gr.update(value=None) for _ in range(N_IMAGES)] warn_upd = gr.update(value="", visible=False) return [*img_updates, *radio_updates, warn_upd, items] btn_shuffle.click( on_shuffle, inputs=[state_items], outputs=[*image_comps, *radio_comps, warn_md, state_items], ) def on_reset(): radio_updates = [gr.update(value=None) for _ in range(N_IMAGES)] warn_update = gr.update(value="", visible=False) return [*radio_updates, warn_update] btn_reset.click( on_reset, inputs=None, outputs=[*radio_comps, warn_md], ) def on_submit(*args): # args = [r1, r2, ..., rN, state] state = args[-1] answers = list(args[:-1]) # Vérif : toutes les réponses sont renseignées if any(a is None for a in answers): missing = sum(1 for a in answers if a is None) msg = f"❗ Merci de répondre aux **{missing}** image(s) restante(s) avant de valider." return ( gr.update(value="", visible=False), # result_md gr.update(value=None), # df gr.update(visible=True), # quiz_group gr.update(visible=False), # result_group gr.update(value=msg, visible=True), # warn_md (au-dessus du bouton) ) # Répartition des réponses counts = {e: 0 for e in EMOTIONS} for choice in answers: counts[choice] += 1 lines = [f"- **{emo.capitalize()}** : {counts[emo]}" for emo in EMOTIONS] summary = "### Répartition des réponses\n" + "\n".join(lines) # Détail pour le tableau items = list(state) rows = [] for i, choice in enumerate(answers): rows.append([i + 1, items[i]["file"], choice]) return ( gr.update(value=summary, visible=True), # result_md gr.update(value=rows), # df gr.update(visible=False), # quiz_group gr.update(visible=True), # result_group gr.update(value="", visible=False), # warn_md (on le cache) ) btn_submit.click( on_submit, inputs=[*radio_comps, state_items], outputs=[result_md, df, quiz_group, result_group, warn_md], scroll_to_output=True, # ancre automatique vers les résultats ) def restart(state, do_shuffle: bool): items = list(state) if do_shuffle: random.shuffle(items) img_updates = [gr.update(value=items[i]["path"], label=f"Image {i+1}") for i in range(N_IMAGES)] radio_updates = [gr.update(value=None) for _ in range(N_IMAGES)] # on réaffiche le quiz et on masque les résultats return [*img_updates, *radio_updates, gr.update(visible=True), gr.update(visible=False), items] btn_again_same.click( lambda state: restart(state, False), inputs=[state_items], outputs=[*image_comps, *radio_comps, quiz_group, result_group, state_items], ) btn_again_shuffle.click( lambda state: restart(state, True), inputs=[state_items], outputs=[*image_comps, *radio_comps, quiz_group, result_group, state_items], ) return demo demo = build_interface(ITEMS) if __name__ == "__main__": demo.launch()