| | import gradio as gr |
| | import os, json, random, glob |
| | from typing import List, Dict |
| | from PIL import Image, ImageDraw, ImageFont |
| |
|
| | |
| | IMAGE_DIR = os.getenv("IMAGE_DIR", "assets") |
| | N_IMAGES = int(os.getenv("N_IMAGES", "24")) |
| | IMG_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif") |
| |
|
| | |
| | EMOTIONS = ["joie", "tristesse", "peur", "colère", "surprise", "dégoût", "indifférence"] |
| |
|
| | |
| |
|
| | 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() |
| |
|
| | 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() |
| |
|
| | |
| | COLS = 3 |
| |
|
| | 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) |
| |
|
| | |
| | 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 |
| |
|
| | |
| | warn_md = gr.Markdown("", visible=False, elem_classes=["warn-msg"]) |
| |
|
| | |
| | with gr.Row(elem_classes=["center-button"]): |
| | btn_submit = gr.Button("✅ Valider mes 24 choix", variant="primary") |
| |
|
| | |
| | with gr.Group(visible=False) as result_group: |
| | gr.Markdown("## Résultats du sondage") |
| | |
| | result_md = gr.Markdown(elem_classes=["score"]) |
| | |
| | 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") |
| |
|
| | |
| | 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): |
| | |
| | state = args[-1] |
| | answers = list(args[:-1]) |
| |
|
| | |
| | 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), |
| | gr.update(value=None), |
| | gr.update(visible=True), |
| | gr.update(visible=False), |
| | gr.update(value=msg, visible=True), |
| | ) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | 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), |
| | gr.update(value=rows), |
| | gr.update(visible=False), |
| | gr.update(visible=True), |
| | gr.update(value="", visible=False), |
| | ) |
| |
|
| | btn_submit.click( |
| | on_submit, |
| | inputs=[*radio_comps, state_items], |
| | outputs=[result_md, df, quiz_group, result_group, warn_md], |
| | scroll_to_output=True, |
| | ) |
| |
|
| | 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)] |
| | |
| | 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() |
| |
|