""" Detector de Imágenes IA vs. Reales Pontificia Universidad Javeriana — Técnicas de Aprendizaje de Máquina Proyecto de Aplicación 2 — HuggingFace Spaces """ import os import numpy as np from PIL import Image import tensorflow as tf import gradio as gr # ── Configuración ────────────────────────────────────────────────────────── IMG_SIZE = 64 MODEL_PATH = "mejor_modelo_ai_vs_real.keras" # ── Cargar modelo ────────────────────────────────────────────────────────── try: modelo = tf.keras.models.load_model(MODEL_PATH) MODEL_OK = True except Exception as e: print(f"[ERROR] {e}") modelo = None MODEL_OK = False # ── Predicción ───────────────────────────────────────────────────────────── def predecir(imagen_np): if not MODEL_OK or modelo is None: return {"Error: modelo no disponible": 1.0}, "Modelo no cargado", "" if imagen_np is None: return {"Sin imagen": 1.0}, "Sube una imagen primero", "" img = Image.fromarray(imagen_np.astype("uint8")).convert("RGB") img = img.resize((IMG_SIZE, IMG_SIZE), Image.LANCZOS) arr = np.array(img, dtype=np.float32) / 255.0 arr = np.expand_dims(arr, axis=0) prob_real = float(modelo.predict(arr, verbose=0)[0][0]) prob_ai = 1.0 - prob_real confianza = max(prob_ai, prob_real) * 100 probs = { "IA-Generada": round(prob_ai, 4), "Real": round(prob_real, 4), } if prob_ai >= 0.80: veredicto = f"Imagen generada por IA — confianza {confianza:.1f}%" elif prob_ai >= 0.60: veredicto = f"Probablemente generada por IA — confianza {confianza:.1f}%" elif prob_ai >= 0.40: veredicto = f"Resultado incierto — zona de borde de decisión" elif prob_real >= 0.60: veredicto = f"Probablemente real — confianza {confianza:.1f}%" else: veredicto = f"Imagen real — confianza {confianza:.1f}%" detalle = ( f"P(IA-Generada) = {prob_ai*100:.2f}%\n" f"P(Real) = {prob_real*100:.2f}%\n" f"Confianza = {confianza:.1f}%\n" f"Resolución = {IMG_SIZE}×{IMG_SIZE} px" ) return probs, veredicto, detalle # ── CSS ──────────────────────────────────────────────────────────────────── css = """ @import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=DM+Mono:wght@300;400;500&family=DM+Sans:wght@300;400;500&display=swap'); * { box-sizing: border-box; } body, .gradio-container { font-family: 'DM Sans', sans-serif !important; background: #0a0a0f !important; } gradio-app { background: #0a0a0f !important; } .gradio-container { max-width: 1100px !important; margin: 0 auto !important; padding: 0 24px !important; } /* Header */ .site-header { border-bottom: 1px solid #1e1e2e; padding: 32px 0 28px; margin-bottom: 36px; } .site-header h1 { font-family: 'DM Serif Display', serif; font-size: 2.1rem; color: #f0f0f5; margin: 0 0 6px; letter-spacing: -0.02em; font-weight: 400; } .site-header p { font-family: 'DM Mono', monospace; font-size: 0.72rem; color: #4a4a6a; margin: 0; letter-spacing: 0.08em; text-transform: uppercase; } /* Tabs */ .tab-nav { border-bottom: 1px solid #1e1e2e !important; background: transparent !important; margin-bottom: 28px; } .tab-nav button { font-family: 'DM Mono', monospace !important; font-size: 0.72rem !important; letter-spacing: 0.06em !important; text-transform: uppercase !important; color: #4a4a6a !important; background: transparent !important; border: none !important; border-bottom: 2px solid transparent !important; padding: 10px 18px !important; margin-bottom: -1px !important; transition: color 0.2s, border-color 0.2s !important; } .tab-nav button.selected { color: #c8c8e8 !important; border-bottom-color: #6060c0 !important; } .tab-nav button:hover { color: #a0a0c0 !important; } /* Inputs & outputs */ .gr-input, .gr-output, input, textarea, .gr-box { background: #111120 !important; border: 1px solid #1e1e2e !important; border-radius: 8px !important; color: #d0d0e8 !important; font-family: 'DM Sans', sans-serif !important; } label span, .gr-label { font-family: 'DM Mono', monospace !important; font-size: 0.7rem !important; letter-spacing: 0.07em !important; text-transform: uppercase !important; color: #4a4a6a !important; } /* Button */ button.primary { background: #6060c0 !important; border: none !important; border-radius: 6px !important; font-family: 'DM Mono', monospace !important; font-size: 0.75rem !important; letter-spacing: 0.08em !important; text-transform: uppercase !important; color: #fff !important; padding: 10px 24px !important; transition: background 0.2s !important; cursor: pointer !important; } button.primary:hover { background: #7070d8 !important; } /* Section titles */ .section-title { font-family: 'DM Serif Display', serif; font-size: 1.15rem; color: #c8c8e8; margin: 0 0 16px; font-weight: 400; letter-spacing: -0.01em; } /* Cards */ .card { background: #111120; border: 1px solid #1e1e2e; border-radius: 10px; padding: 20px 24px; margin-bottom: 16px; } /* Tables */ .data-table { width: 100%; border-collapse: collapse; font-size: 0.83rem; color: #c0c0d8; } .data-table th { font-family: 'DM Mono', monospace; font-size: 0.68rem; letter-spacing: 0.07em; text-transform: uppercase; color: #4a4a6a; padding: 8px 12px; text-align: left; border-bottom: 1px solid #1e1e2e; font-weight: 400; } .data-table td { padding: 10px 12px; border-bottom: 1px solid #16162a; vertical-align: top; line-height: 1.5; } .data-table tr:last-child td { border-bottom: none; } .data-table tr:hover td { background: #16162a; } .winner-row td { background: rgba(96, 96, 192, 0.08) !important; } .badge { display: inline-block; font-family: 'DM Mono', monospace; font-size: 0.62rem; letter-spacing: 0.06em; text-transform: uppercase; padding: 2px 8px; border-radius: 4px; background: rgba(96, 96, 192, 0.2); color: #9090d8; border: 1px solid rgba(96, 96, 192, 0.3); margin-left: 8px; vertical-align: middle; } /* Variable grid */ .var-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-top: 4px; } .var-card { background: #0d0d1a; border: 1px solid #1e1e2e; border-radius: 8px; padding: 14px 12px; text-align: center; transition: border-color 0.2s; } .var-card:hover { border-color: #3a3a6a; } .var-val { font-family: 'DM Mono', monospace; font-size: 1.25rem; font-weight: 500; color: #e0e0f5; letter-spacing: -0.01em; } .var-lbl { font-family: 'DM Mono', monospace; font-size: 0.62rem; letter-spacing: 0.07em; text-transform: uppercase; color: #3a3a5a; margin-top: 5px; } .var-card.accent-blue .var-val { color: #7878e0; } .var-card.accent-violet .var-val { color: #a060c8; } .var-card.accent-green .var-val { color: #40b080; } /* Category labels */ .cat-label { font-family: 'DM Mono', monospace; font-size: 0.65rem; letter-spacing: 0.08em; text-transform: uppercase; color: #3a3a5a; margin: 18px 0 8px; } /* Verdict output */ .verdict-box textarea { font-family: 'DM Serif Display', serif !important; font-size: 1.05rem !important; color: #d0d0f0 !important; font-style: italic !important; letter-spacing: 0.01em !important; } /* Info note */ .note { font-family: 'DM Mono', monospace; font-size: 0.7rem; color: #3a3a5a; letter-spacing: 0.04em; margin-top: 8px; } /* Divider */ hr { border: none; border-top: 1px solid #1e1e2e; margin: 28px 0; } footer { display: none !important; } """ # ── HTML blocks ──────────────────────────────────────────────────────────── header_html = """ """ tabla_modelos = """

Arquitecturas Comparadas

Modelo Tipo Parámetros Características clave
CNN Scratch Capas Convolucionales + Densas ~800 K 3 bloques Conv → BN → MaxPool → Dropout, cabeza Dense(256)
EfficientNetB0mejor modelo Transfer Learning — ImageNet ~5.3 M Base congelada fase 1 → fine-tuning últimas 20 capas fase 2
ViT Small Vision Transformer ~2.5 M Patch 8×8, 64 patches, 4 bloques Transformer, EMBED_DIM 128
""" tabla_metricas = """

Resultados — Test Set

Actualizar con los valores reales tras ejecutar el notebook en Colab.

Modelo Accuracy Precision Recall F1-Score AUC-ROC
CNN Scratch
EfficientNetB0mejor
ViT Small
""" tabla_hiperparametros = """

Hiperparámetros de Entrenamiento

Parámetro CNN Scratch EfficientNetB0 ViT Small
Optimizador AdamAdamAdam
Learning Rate 1e-3 1e-4 → 1e-5 1e-4
Batch Size 32 32 32
Épocas máx. 30 10 + 10 (fases) 30
Loss Binary Cross-Entropy Binary Cross-Entropy Binary Cross-Entropy
Dropout 0.25 – 0.40 0.30 0.10 – 0.30
EarlyStopping patience=5, monitor=val_loss, restore_best_weights=True
ReduceLROnPlateau factor=0.5, patience=3, min_lr=1e-6
Class weights Balanceado via sklearn.compute_class_weight
""" tabla_dataset = """

Dataset

Fuente Parveshiiii/AI-vs-Real — HuggingFace Datasets
Total imágenes 13 999
Distribución ~75% IA-Generada / ~25% Real — desbalanceado
División 80% train+val / 20% test — estratificado
Resolución entrada 64×64 px — normalizado [0, 1]
Data augmentation Flip horizontal, rotación ±10%, zoom ±10%
Pipeline tf.data.Dataset con prefetch y augmentation en GPU
""" variables_html = """

Variables del Sistema

Generales

64×64
IMG_SIZE
32
BATCH_SIZE
42
SEED
80 / 20
Train / Test
1e-3
LR — CNN
1e-4 → 1e-5
LR — EfficientNet
1e-4
LR — ViT
0.50
Umbral decisión

Vision Transformer

8×8
PATCH_SIZE
64
NUM_PATCHES
128
EMBED_DIM
4
NUM_HEADS
4
NUM_BLOCKS
256
FF_DIM
0.10
DROPOUT_RATE

Callbacks

5
EarlyStopping patience
0.5
ReduceLR factor
3
ReduceLR patience
1e-6
ReduceLR min_lr
""" sobre_html = """

Sobre el Proyecto

Problema Clasificación binaria supervisada: imagen IA-Generada vs. imagen Real
Motivación Los generadores de imagen por IA producen imágenes indistinguibles para el ojo humano, generando riesgos de desinformación, deepfakes y fraude digital
Autores Javier Felipe Aldana Jaramillo · Integrante 2 · Integrante 3 · Integrante 4
Curso Técnicas de Aprendizaje de Máquina — PUJ Bogotá · 2026
""" # ── Interfaz ─────────────────────────────────────────────────────────────── with gr.Blocks( title="Detector IA vs Real", css=css, ) as demo: gr.HTML(header_html) with gr.Tabs(): # Tab 1 — Predictor with gr.Tab("Predictor"): with gr.Row(equal_height=True): with gr.Column(scale=1): img_input = gr.Image( label="Imagen de entrada", type="numpy", height=300, ) btn = gr.Button("Analizar", variant="primary") with gr.Column(scale=1): out_label = gr.Label(label="Probabilidad por clase", num_top_classes=2) out_veredicto = gr.Textbox( label="Veredicto", interactive=False, lines=1, elem_classes=["verdict-box"], ) out_detalle = gr.Textbox( label="Detalle técnico", interactive=False, lines=4, ) gr.HTML(variables_html) btn.click(fn=predecir, inputs=[img_input], outputs=[out_label, out_veredicto, out_detalle]) img_input.change(fn=predecir, inputs=[img_input], outputs=[out_label, out_veredicto, out_detalle]) # Tab 2 — Modelos with gr.Tab("Modelos"): gr.HTML(tabla_modelos) gr.HTML(tabla_metricas) # Tab 3 — Hiperparámetros with gr.Tab("Hiperparámetros"): gr.HTML(tabla_hiperparametros) # Tab 4 — Dataset with gr.Tab("Dataset"): gr.HTML(tabla_dataset) # Tab 5 — Proyecto with gr.Tab("Proyecto"): gr.HTML(sobre_html) if __name__ == "__main__": demo.launch()