#!/usr/bin/env python3 """ Herramienta para organizar infografías y medios según la estructura definida. Uso: python scripts/organizar_infografias.py Permite: - Seleccionar archivos de imágenes - Identificar a qué infografía corresponde cada una - Mover y renombrar automáticamente según la estructura """ import os import shutil import re from pathlib import Path from typing import Dict, List, Tuple, Optional # Mapeo de infografías según LISTADO_COMPLETO_MEDIOS_FALTANTES.md INFORGRAFIAS = { # BLOQUE 0: FUNDAMENTOS "diagrama-seleccion-dispositivo-oxigeno": { "bloque": "bloque-0-fundamentos", "nombre_archivo": "diagrama-seleccion-dispositivo-oxigeno", "descripcion": "Diagrama de Selección de Dispositivo de Oxigenoterapia", "prioridad": "Alta" }, "tabla-rangos-fio2": { "bloque": "bloque-0-fundamentos", "nombre_archivo": "tabla-rangos-fio2", "descripcion": "Tabla Visual de Rangos de FiO2", "prioridad": "Alta" }, "guia-colocacion-dispositivos-oxigeno": { "bloque": "bloque-0-fundamentos", "nombre_archivo": "guia-colocacion-dispositivos-oxigeno", "descripcion": "Guía de Colocación de Dispositivos de Oxigenoterapia", "prioridad": "Media" }, # BLOQUE 2: MATERIAL E INMOVILIZACIÓN "componentes-sistema-inmovilizacion": { "bloque": "bloque-2-inmovilizacion", "nombre_archivo": "componentes-sistema-inmovilizacion", "descripcion": "Componentes del Sistema de Inmovilización", "prioridad": "Alta" }, "seleccion-talla-collarín": { "bloque": "bloque-2-inmovilizacion", "nombre_archivo": "seleccion-talla-collarín", "descripcion": "Selección de Talla de Collarín Cervical", "prioridad": "Alta" }, "colocacion-collarín-paso": { "bloque": "bloque-2-inmovilizacion", "nombre_archivo": "colocacion-collarín-paso", "descripcion": "Colocación de Collarín Paso a Paso", "prioridad": "Alta", "es_serie": True, "pasos": 6 }, "verificaciones-post-colocacion": { "bloque": "bloque-2-inmovilizacion", "nombre_archivo": "verificaciones-post-colocacion", "descripcion": "Verificaciones Post-Colocación de Collarín", "prioridad": "Media" }, "errores-frecuentes-collarín": { "bloque": "bloque-2-inmovilizacion", "nombre_archivo": "errores-frecuentes-collarín", "descripcion": "Errores Frecuentes en Colocación de Collarín", "prioridad": "Media" }, "posicion-tes-inmovilizacion-manual": { "bloque": "bloque-2-inmovilizacion", "nombre_archivo": "posicion-tes-inmovilizacion-manual", "descripcion": "Posición del TES en Inmovilización Manual", "prioridad": "Alta" }, "tecnica-sujecion-manual": { "bloque": "bloque-2-inmovilizacion", "nombre_archivo": "tecnica-sujecion-manual", "descripcion": "Técnica de Sujeción Manual", "prioridad": "Alta" }, "situaciones-requieren-inmovilizacion": { "bloque": "bloque-2-inmovilizacion", "nombre_archivo": "situaciones-requieren-inmovilizacion", "descripcion": "Situaciones que Requieren Inmovilización", "prioridad": "Media" }, "secuencia-transicion-inmovilizacion": { "bloque": "bloque-2-inmovilizacion", "nombre_archivo": "secuencia-transicion-inmovilizacion", "descripcion": "Secuencia de Transición en Inmovilización", "prioridad": "Alta" }, "coordinacion-equipo-inmovilizacion": { "bloque": "bloque-2-inmovilizacion", "nombre_archivo": "coordinacion-equipo-inmovilizacion", "descripcion": "Coordinación del Equipo en Inmovilización", "prioridad": "Media" }, "componentes-tablero-espinal": { "bloque": "bloque-2-inmovilizacion", "nombre_archivo": "componentes-tablero-espinal", "descripcion": "Componentes del Tablero Espinal", "prioridad": "Alta" }, "colocacion-tablero-espinal-paso": { "bloque": "bloque-2-inmovilizacion", "nombre_archivo": "colocacion-tablero-espinal-paso", "descripcion": "Colocación de Tablero Espinal Paso a Paso", "prioridad": "Alta", "es_serie": True, "pasos": 5 }, "componentes-colchon-vacio": { "bloque": "bloque-2-inmovilizacion", "nombre_archivo": "componentes-colchon-vacio", "descripcion": "Componentes del Colchón de Vacío", "prioridad": "Alta" }, "colocacion-colchon-vacio-paso": { "bloque": "bloque-2-inmovilizacion", "nombre_archivo": "colocacion-colchon-vacio-paso", "descripcion": "Colocación de Colchón de Vacío Paso a Paso", "prioridad": "Alta", "es_serie": True, "pasos": 10 }, "componentes-camilla-cuchara": { "bloque": "bloque-2-inmovilizacion", "nombre_archivo": "componentes-camilla-cuchara", "descripcion": "Componentes de la Camilla Cuchara", "prioridad": "Media" }, # BLOQUE 3: MATERIAL SANITARIO "configuracion-maxima-fio2": { "bloque": "bloque-3-material-sanitario", "nombre_archivo": "configuracion-maxima-fio2", "descripcion": "Configuración para Máxima FiO2", "prioridad": "Media" }, # BLOQUE 7: CONDUCCIÓN "configuracion-gps": { "bloque": "bloque-7-conduccion", "nombre_archivo": "configuracion-gps", "descripcion": "Configuración de GPS Antes de Salir", "prioridad": "Baja" }, # BLOQUE 12: MARCO LEGAL "diagrama-decisiones-eticas": { "bloque": "bloque-12-marco-legal", "nombre_archivo": "diagrama-decisiones-eticas", "descripcion": "Toma de Decisiones Éticas en Urgencias", "prioridad": "Media" }, } BASE_DIR = Path(__file__).parent.parent ASSETS_DIR = BASE_DIR / "public" / "assets" / "infografias" def normalizar_nombre(texto: str) -> str: """Normaliza un texto para usarlo como nombre de archivo.""" # Convertir a minúsculas texto = texto.lower() # Reemplazar espacios y caracteres especiales por guiones texto = re.sub(r'[^\w\s-]', '', texto) texto = re.sub(r'[-\s]+', '-', texto) # Eliminar guiones al inicio y final return texto.strip('-') def buscar_infografia_por_palabras_clave(nombre_archivo: str) -> List[Tuple[str, Dict]]: """Busca infografías que coincidan con palabras clave del nombre del archivo.""" nombre_lower = nombre_lower = nombre_archivo.lower() coincidencias = [] for key, info in INFORGRAFIAS.items(): # Buscar coincidencias en el nombre del archivo palabras_clave = [ "collarín", "collarin", "collar", "tablero", "colchon", "colchón", "camilla", "oxigeno", "oxígeno", "fio2", "gps", "eticas", "éticas", "inmovilizacion", "inmovilización", "componentes", "seleccion", "selección", "colocacion", "colocación", "configuracion", "configuración" ] score = 0 for palabra in palabras_clave: if palabra in nombre_lower and palabra in key: score += 1 if score > 0: coincidencias.append((key, info, score)) # Ordenar por score descendente coincidencias.sort(key=lambda x: x[2], reverse=True) return [(key, info) for key, info, _ in coincidencias] def mostrar_menu_infografias(coincidencias: List[Tuple[str, Dict]]) -> Optional[str]: """Muestra un menú de infografías y permite seleccionar una.""" if not coincidencias: print("\n❌ No se encontraron coincidencias automáticas.") print("\nInfografías disponibles:") for i, (key, info) in enumerate(INFORGRAFIAS.items(), 1): print(f" {i}. {info['descripcion']} ({info['bloque']})") try: seleccion = input("\nSelecciona el número de la infografía (o 'n' para cancelar): ").strip() if seleccion.lower() == 'n': return None idx = int(seleccion) - 1 if 0 <= idx < len(INFORGRAFIAS): return list(INFORGRAFIAS.keys())[idx] except (ValueError, IndexError): return None else: print("\n✅ Coincidencias encontradas:") for i, (key, info) in enumerate(coincidencias[:5], 1): print(f" {i}. {info['descripcion']} ({info['bloque']})") if len(coincidencias) > 5: print(f" ... y {len(coincidencias) - 5} más") try: seleccion = input("\nSelecciona el número (o 'n' para ver todas): ").strip() if seleccion.lower() == 'n': return mostrar_menu_infografias([]) idx = int(seleccion) - 1 if 0 <= idx < len(coincidencias): return coincidencias[idx][0] except (ValueError, IndexError): return None return None def obtener_numero_paso(nombre_archivo: str) -> Optional[int]: """Intenta extraer el número de paso del nombre del archivo.""" # Buscar patrones como: paso1, paso-1, paso_1, 1, etc. patrones = [ r'paso[_\s-]?(\d+)', r'step[_\s-]?(\d+)', r'(\d+)', ] for patron in patrones: match = re.search(patron, nombre_archivo.lower()) if match: return int(match.group(1)) return None def organizar_archivo(archivo_origen: Path, infografia_key: str, numero_paso: Optional[int] = None) -> bool: """Organiza un archivo moviéndolo a la ubicación correcta.""" if infografia_key not in INFORGRAFIAS: print(f"❌ Error: Infografía '{infografia_key}' no encontrada") return False info = INFORGRAFIAS[infografia_key] bloque_dir = ASSETS_DIR / info["bloque"] # Crear directorio si no existe bloque_dir.mkdir(parents=True, exist_ok=True) # Determinar nombre del archivo destino extension = archivo_origen.suffix.lower() if info.get("es_serie") and numero_paso: nombre_destino = f"{info['nombre_archivo']}-{numero_paso}{extension}" else: nombre_destino = f"{info['nombre_archivo']}{extension}" archivo_destino = bloque_dir / nombre_destino # Si el archivo ya existe, preguntar if archivo_destino.exists(): respuesta = input(f"⚠️ El archivo {nombre_destino} ya existe. ¿Sobrescribir? (s/n): ").strip().lower() if respuesta != 's': print(f"⏭️ Saltando {archivo_origen.name}") return False try: # Copiar archivo (no mover, por seguridad) shutil.copy2(archivo_origen, archivo_destino) print(f"✅ Organizado: {archivo_origen.name} → {archivo_destino.relative_to(BASE_DIR)}") return True except Exception as e: print(f"❌ Error al organizar {archivo_origen.name}: {e}") return False def main(): """Función principal del script.""" print("=" * 70) print("🖼️ ORGANIZADOR DE INFOGRAFÍAS Y MEDIOS") print("=" * 70) print("\nEsta herramienta organiza automáticamente las infografías") print("según la estructura definida en LISTADO_COMPLETO_MEDIOS_FALTANTES.md\n") # Verificar que existe el directorio de assets if not ASSETS_DIR.exists(): print(f"❌ Error: No existe el directorio {ASSETS_DIR}") print(" Ejecuta primero: mkdir -p public/assets/infografias/...") return # Solicitar archivos print("📁 Ingresa las rutas de los archivos de imágenes a organizar.") print(" (Puedes ingresar múltiples archivos separados por comas)") print(" (O presiona Enter para seleccionar archivos del directorio actual)\n") entrada = input("Archivos: ").strip() if not entrada: # Buscar archivos de imagen en el directorio actual archivos = [] extensiones = ['.svg', '.png', '.jpg', '.jpeg', '.webp'] for ext in extensiones: archivos.extend(Path('.').glob(f'*{ext}')) archivos.extend(Path('.').glob(f'*{ext.upper()}')) if not archivos: print("❌ No se encontraron archivos de imagen en el directorio actual") return print(f"\n📋 Archivos encontrados ({len(archivos)}):") for i, archivo in enumerate(archivos, 1): print(f" {i}. {archivo.name}") seleccion = input("\n¿Procesar todos? (s/n): ").strip().lower() if seleccion != 's': return archivos_a_procesar = archivos else: # Procesar archivos ingresados rutas = [r.strip() for r in entrada.split(',')] archivos_a_procesar = [] for ruta in rutas: archivo = Path(ruta) if archivo.exists() and archivo.is_file(): archivos_a_procesar.append(archivo) else: print(f"⚠️ Archivo no encontrado: {ruta}") if not archivos_a_procesar: print("❌ No hay archivos para procesar") return print(f"\n🔄 Procesando {len(archivos_a_procesar)} archivo(s)...\n") organizados = 0 for archivo in archivos_a_procesar: print(f"\n📄 Procesando: {archivo.name}") # Buscar coincidencias automáticas coincidencias = buscar_infografia_por_palabras_clave(archivo.name) # Seleccionar infografía infografia_key = mostrar_menu_infografias(coincidencias) if not infografia_key: print(f"⏭️ Saltando {archivo.name}") continue # Detectar si es parte de una serie numero_paso = None info = INFORGRAFIAS[infografia_key] if info.get("es_serie"): numero_paso = obtener_numero_paso(archivo.name) if not numero_paso: respuesta = input(f"¿Es parte de una serie de pasos? (s/n): ").strip().lower() if respuesta == 's': try: numero_paso = int(input(f"¿Qué número de paso? (1-{info.get('pasos', 10)}): ")) except ValueError: numero_paso = None # Organizar archivo if organizar_archivo(archivo, infografia_key, numero_paso): organizados += 1 print("\n" + "=" * 70) print(f"✅ Proceso completado: {organizados}/{len(archivos_a_procesar)} archivos organizados") print("=" * 70) if __name__ == "__main__": main()