|
import gradio as gr |
|
import os, json, random, glob |
|
from typing import List, Dict |
|
from PIL import Image, ImageDraw, ImageFont |
|
|
|
|
|
LABEL_VIRTUEL = "Virtuel" |
|
LABEL_REEL = "Réel" |
|
|
|
|
|
IMAGE_DIR = os.getenv("IMAGE_DIR", "assets") |
|
N_IMAGES = int(os.getenv("N_IMAGES", "24")) |
|
IMG_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif") |
|
|
|
|
|
|
|
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(): |
|
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() |
|
|
|
demo_paths = [] |
|
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) |
|
label = LABEL_VIRTUEL if i % 2 == 0 else LABEL_REEL |
|
text = f"DEMO\nImage {i+1}\nVérité : {label}" |
|
_centered_multiline(d, (0, h//2 - 60), text, font, w) |
|
fname = f"demo_{i+1:02d}_{'ai' if label==LABEL_VIRTUEL else 'human'}.png" |
|
path = os.path.join(IMAGE_DIR, fname) |
|
img.save(path) |
|
demo_paths.append((path, label)) |
|
|
|
|
|
key = [{"file": os.path.basename(p), "label": lab} for p, lab in demo_paths] |
|
with open(os.path.join(IMAGE_DIR, "answer_key.json"), "w", encoding="utf-8") as f: |
|
json.dump(key, f, ensure_ascii=False, indent=2) |
|
|
|
|
|
def load_items() -> List[Dict]: |
|
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] |
|
|
|
|
|
answer_key_path = os.path.join(IMAGE_DIR, "answer_key.json") |
|
label_map = {} |
|
if os.path.exists(answer_key_path): |
|
try: |
|
with open(answer_key_path, "r", encoding="utf-8") as f: |
|
data = json.load(f) |
|
if isinstance(data, dict): |
|
label_map = {k: (LABEL_VIRTUEL if v.lower().startswith(("ia", "ai")) else LABEL_REEL) for k, v in data.items()} |
|
elif isinstance(data, list): |
|
for row in data: |
|
fname = row.get("file") or row.get("name") or row.get("path") |
|
lab = row.get("label") or row.get("truth") |
|
if fname and lab: |
|
lab_low = str(lab).strip().lower() |
|
if lab_low in {"ia", "ai", "artificial", "generated", LABEL_VIRTUEL.lower()}: |
|
lab_norm = LABEL_VIRTUEL |
|
else: |
|
lab_norm = LABEL_REEL |
|
label_map[os.path.basename(fname)] = lab_norm |
|
except Exception as e: |
|
print("[warn] Impossible de lire answer_key.json :", e) |
|
|
|
|
|
ai_markers = ("_ai", "-ai", " ai ", "midjourney", "sdxl", "stable", "gen", "generated", "dalle", "flux") |
|
|
|
items = [] |
|
for p in files: |
|
fname = os.path.basename(p) |
|
truth = label_map.get(fname) |
|
if truth is None: |
|
fn_low = fname.lower() |
|
truth = LABEL_VIRTUEL if any(m in fn_low for m in ai_markers) else LABEL_REEL |
|
items.append({"path": p, "file": fname, "truth": truth}) |
|
|
|
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(f""" |
|
# {LABEL_VIRTUEL} ou {LABEL_REEL} ? |
|
Sélectionnez **{LABEL_VIRTUEL}** ou **{LABEL_REEL}** pour chacune des {N_IMAGES} images, puis cliquez sur **Valider mes {N_IMAGES} 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=[LABEL_VIRTUEL, LABEL_REEL], label="Votre choix", 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(f"✅ Valider mes {N_IMAGES} choix", variant="primary") |
|
|
|
|
|
with gr.Group(visible=False) as result_group: |
|
gr.Markdown("## Résultats") |
|
score_md = gr.Markdown(elem_classes=["score"]) |
|
df = gr.Dataframe( |
|
headers=["#", "Fichier", "Vérité", "Votre réponse", "✓"], |
|
row_count=(N_IMAGES, "fixed"), |
|
interactive=False, |
|
) |
|
with gr.Row(): |
|
gallery_ok = gr.Gallery(label="Réponses correctes", columns=6, height=180) |
|
gallery_ko = gr.Gallery(label="Réponses incorrectes", columns=6, height=180) |
|
with gr.Row(): |
|
btn_again_same = gr.Button("↩️ Rejouer (même ordre)") |
|
btn_again_shuffle = gr.Button("🔁 Rejouer & mélanger") |
|
|
|
|
|
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(value=None), |
|
gr.update(value=None), |
|
gr.update(visible=True), |
|
gr.update(visible=False), |
|
gr.update(value=msg, visible=True), |
|
) |
|
|
|
items = list(state) |
|
rows, ok_imgs, ko_imgs = [], [], [] |
|
ok = 0 |
|
for i, choice in enumerate(answers): |
|
truth = items[i]["truth"] |
|
path = items[i]["path"] |
|
is_ok = (choice == truth) |
|
ok += 1 if is_ok else 0 |
|
rows.append([i + 1, items[i]["file"], truth, choice, "✅" if is_ok else "❌"]) |
|
(ok_imgs if is_ok else ko_imgs).append(path) |
|
score_txt = f"**Score : {ok}/{N_IMAGES} ({round(100 * ok / N_IMAGES)}%)**" |
|
|
|
return ( |
|
gr.update(value=score_txt, visible=True), |
|
gr.update(value=rows), |
|
gr.update(value=ok_imgs), |
|
gr.update(value=ko_imgs), |
|
gr.update(visible=False), |
|
gr.update(visible=True), |
|
gr.update(value="", visible=False), |
|
) |
|
|
|
btn_submit.click( |
|
on_submit, |
|
inputs=[*radio_comps, state_items], |
|
outputs=[score_md, df, gallery_ok, gallery_ko, 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() |
|
|