| """ |
| app.py – Clasificador de huellas (EfficientNet / ResNet) + ASR español |
| """ |
|
|
| import gradio as gr |
| import numpy as np |
| import tensorflow as tf |
| import torch |
| import torch.nn as nn |
| import torchvision.transforms as T |
| import torchvision.models as tv_models |
| import librosa |
| import soundfile as sf |
| import os |
|
|
| |
| IMG_SIZE = 224 |
| DEVICE = "cuda" if torch.cuda.is_available() else "cpu" |
|
|
| FINGERS_EN = ["index", "little", "middle", "ring", "thumb"] |
| FINGERS_ES = ["índice", "meñique", "medio", "anular", "pulgar"] |
| FINGER_MAP_ES = dict(zip(FINGERS_EN, FINGERS_ES)) |
| HANDS_ES = ["izquierda", "derecha"] |
|
|
| RESNET_CLASSES = [ |
| "Left_index", "Left_little", "Left_middle", "Left_ring", "Left_thumb", |
| "Right_index", "Right_little", "Right_middle", "Right_ring", "Right_thumb", |
| ] |
|
|
| TORCH_MEAN = [0.485, 0.456, 0.406] |
| TORCH_STD = [0.229, 0.224, 0.225] |
| torch_tfms = T.Compose([ |
| T.Grayscale(num_output_channels=3), |
| T.Resize((IMG_SIZE, IMG_SIZE)), |
| T.ToTensor(), |
| T.Normalize(TORCH_MEAN, TORCH_STD), |
| ]) |
|
|
| |
| FRAME_LENGTH = 256 |
| FRAME_STEP = 160 |
| FFT_LENGTH = 384 |
| TARGET_SR = 16_000 |
|
|
| CHARS = [c for c in "abcdefghijklmnopqrstuvwxyzáéíóúüñ'?! "] |
| char_to_num = tf.keras.layers.StringLookup(vocabulary=CHARS, oov_token="") |
| num_to_char = tf.keras.layers.StringLookup( |
| vocabulary=char_to_num.get_vocabulary(), oov_token="", invert=True |
| ) |
|
|
| |
| _models_cache = {} |
|
|
| def _load_efficientnet(): |
| return tf.keras.models.load_model("models/fingerprint_model_EfficientNet.keras") |
|
|
| def _load_resnet(): |
| model = tv_models.resnet18(weights=tv_models.ResNet18_Weights.IMAGENET1K_V1) |
| model.fc = nn.Linear(model.fc.in_features, len(RESNET_CLASSES)) |
| model.load_state_dict(torch.load("models/resnet.pt", map_location=DEVICE)) |
| model.eval().to(DEVICE) |
| return model |
|
|
| def CTCLoss(y_true, y_pred): |
| b = tf.cast(tf.shape(y_true)[0], dtype="int64") |
| t = tf.cast(tf.shape(y_pred)[1], dtype="int64") |
| l = tf.cast(tf.shape(y_true)[1], dtype="int64") |
| t = t * tf.ones(shape=(b, 1), dtype="int64") |
| l = l * tf.ones(shape=(b, 1), dtype="int64") |
| return tf.keras.backend.ctc_batch_cost(y_true, y_pred, t, l) |
|
|
| def _load_asr(): |
| return tf.keras.models.load_model("models/audio.keras", |
| custom_objects={"CTCLoss": CTCLoss}) |
|
|
| def _get_model(name): |
| if name not in _models_cache: |
| _models_cache[name] = ( |
| _load_efficientnet() if name == "EfficientNet" else |
| _load_resnet() if name == "ResNet" else |
| _load_asr() if name == "ASR" else None |
| ) |
| return _models_cache[name] |
|
|
| |
| def classify_fingerprint(image, model_name): |
| if image is None: |
| return "⚠️ Sube una imagen primero." |
|
|
| |
| if model_name == "EfficientNet": |
| model = _get_model("EfficientNet") |
| img = image.convert("RGB").resize((IMG_SIZE, IMG_SIZE)) |
| arr = tf.keras.applications.efficientnet.preprocess_input( |
| tf.keras.utils.img_to_array(img) |
| )[None, ...] |
| preds = model.predict(arr, verbose=0) |
| finger_probs = np.squeeze(preds[0] if isinstance(preds, (list, tuple)) else preds) |
| idx = int(finger_probs.argmax()) |
| finger_es = FINGERS_ES[idx] |
| conf = finger_probs[idx] |
| hand_es = "N/A" |
| if isinstance(preds, (list, tuple)) and len(preds) > 1: |
| hand_es = HANDS_ES[int(np.squeeze(preds[1]).argmax())] |
| return f"Dedo: {finger_es}\nMano: {hand_es}\nConfianza: {conf:.2%}" |
|
|
| |
| if model_name == "ResNet": |
| model = _get_model("ResNet") |
| tensor = torch_tfms(image).unsqueeze(0).to(DEVICE) |
| with torch.no_grad(): |
| probs = torch.softmax(model(tensor), 1)[0].cpu().numpy() |
| idx = int(probs.argmax()) |
| conf = probs[idx] |
| hand_en, finger_en = RESNET_CLASSES[idx].split('_') |
| return ( |
| f"Dedo: {FINGER_MAP_ES[finger_en]}\n" |
| f"Mano: {HANDS_ES[0] if hand_en=='Left' else HANDS_ES[1]}\n" |
| f"Confianza: {conf:.2%}" |
| ) |
|
|
| return "🔧 Modelo no reconocido" |
|
|
| |
| def _load_audio_16k(path): |
| audio, sr = sf.read(path) |
| if audio.ndim > 1: |
| audio = audio.mean(axis=1) |
| if sr != TARGET_SR: |
| audio = librosa.resample(audio, orig_sr=sr, target_sr=TARGET_SR) |
| return audio.astype("float32") |
|
|
| def _make_spectrogram(path): |
| audio = _load_audio_16k(path) |
| spec = np.abs(librosa.stft(audio, |
| n_fft=FFT_LENGTH, |
| hop_length=FRAME_STEP, |
| win_length=FRAME_LENGTH)) ** 0.5 |
| spec = spec.T |
|
|
| |
| means = spec.mean(axis=1, keepdims=True) |
| stds = spec.std(axis=1, keepdims=True) + 1e-10 |
| return ((spec - means) / stds).astype("float32") |
|
|
| def _decode_predictions(pred): |
| decoded, _ = tf.keras.backend.ctc_decode( |
| pred, input_length=np.ones(pred.shape[0]) * pred.shape[1], greedy=True |
| ) |
| seq = decoded[0][0] |
| return tf.strings.reduce_join(num_to_char(seq)).numpy().decode("utf-8").strip() |
|
|
| def transcribe_audio(audio_path): |
| if not audio_path or not os.path.exists(audio_path): |
| return "⚠️ Sube o graba un audio primero." |
| model = _get_model("ASR") |
| spec = _make_spectrogram(audio_path) |
| pred = model.predict(spec[None, ...], verbose=0) |
| text = _decode_predictions(pred) |
| return text if text else "(vacío)" |
|
|
| |
| with gr.Blocks() as demo: |
| gr.Markdown("# 🤖 Aplicación Multimodal con Deep Learning") |
|
|
| with gr.Tab("📷 Clasificación de Imágenes"): |
| image_input = gr.Image(type="pil", label="📤 Imagen de entrada") |
| image_model = gr.Dropdown(["ResNet", "EfficientNet"], value="ResNet", |
| label="Selecciona el modelo") |
| image_output = gr.Textbox(label="📈 Resultado") |
| gr.Button("Clasificar Imagen").click( |
| classify_fingerprint, |
| inputs=[image_input, image_model], |
| outputs=image_output, |
| ) |
|
|
| with gr.Tab("🎙️ Reconocimiento de Voz"): |
| audio_input = gr.Audio(type="filepath", label="🎧 Audio de entrada") |
| audio_output = gr.Textbox(label="📝 Texto transcrito") |
| gr.Button("Transcribir Audio").click( |
| transcribe_audio, |
| inputs=audio_input, |
| outputs=audio_output, |
| ) |
|
|
| demo.launch() |
|
|