slop-poll / app.py
mbarnig's picture
Update app.py
2155308 verified
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()