#!/usr/bin/env python3 """ Convierte los ficheros *_audio.md de todos los bloques a MP3. Uso: python3 scripts/temas_a_audio.py # procesa todos los bloques python3 scripts/temas_a_audio.py 2 # solo bloque2 python3 scripts/temas_a_audio.py 2 3 # bloque2 y bloque3 python3 scripts/temas_a_audio.py src/.../tema2_audio.md # fichero concreto Dependencias: pip install edge-tts Requisitos: ffmpeg instalado en el sistema Fuente: src/main/resources/temas/bloqueX/*_audio.md Destino: src/main/resources/static/audios/bloqueX/*.mp3 """ import asyncio import edge_tts import re import os import sys from pathlib import Path BASE_DIR = Path(__file__).parent.parent TEMAS_DIR = BASE_DIR / "src" / "main" / "resources" / "temas" AUDIOS_DIR = BASE_DIR / "src" / "main" / "resources" / "static" / "audios" VOZ = "es-ES-AlvaroNeural" def limpiar_markdown(texto: str) -> str: texto = re.sub(r'```.*?```', ' [código] ', texto, flags=re.DOTALL) texto = re.sub(r'\|.*?\|', '', texto) texto = re.sub(r'[#*_~`>]', '', texto) return ' '.join(texto.split()) def extraer_titulo(path_md: Path) -> str: """Usa el primer encabezado ## del fichero como título del MP3.""" for linea in path_md.read_text(encoding="utf-8").splitlines(): linea = linea.strip() if linea.startswith('## '): return linea[3:].strip() return path_md.stem.replace('_', ' ').capitalize() async def convertir(path_md: Path, mp3_dir: Path) -> None: mp3_dir.mkdir(parents=True, exist_ok=True) final_output = mp3_dir / path_md.with_suffix('.mp3').name temp_output = mp3_dir / (path_md.stem + '.temp.mp3') if final_output.exists() and final_output.stat().st_mtime >= path_md.stat().st_mtime: print(f"⏭️ Sin cambios, omitiendo: {path_md.name}") return titulo = extraer_titulo(path_md) titulo_escaped = titulo.replace('"', '\\"') texto = path_md.read_text(encoding="utf-8") texto_limpio = limpiar_markdown(texto) print(f"🔊 Generando: {path_md.name} ({len(texto_limpio)} caracteres)...") comunicar = edge_tts.Communicate(texto_limpio, VOZ) await comunicar.save(str(temp_output)) comando = ( f'ffmpeg -i "{temp_output}" -codec:a libmp3lame -b:a 192k -ar 44100 ' f'-metadata title="{titulo_escaped}" -id3v2_version 3 -write_id3v1 1 ' f'-y "{final_output}" > /dev/null 2>&1' ) os.system(comando) if temp_output.exists(): temp_output.unlink() print(f"✅ Listo: {final_output.relative_to(BASE_DIR)}") def resolver_archivos(args: list[str]) -> list[tuple[Path, Path]]: """ Devuelve lista de (path_md, mp3_dir). - Sin args: todos los bloques. - Args numéricos: solo esos bloques (ej. '2 3'). - Args con ruta de fichero: ese fichero concreto. """ resultado = [] if not args: bloques = range(1, 5) else: bloques_num = [] ficheros_directos = [] for a in args: if re.fullmatch(r'\d', a): bloques_num.append(int(a)) else: ficheros_directos.append(Path(a)) for path_md in ficheros_directos: match = re.search(r'bloque(\d+)', str(path_md)) bloque_num = match.group(1) if match else 'misc' resultado.append((path_md, AUDIOS_DIR / f"bloque{bloque_num}")) bloques = bloques_num for n in bloques: md_dir = TEMAS_DIR / f"bloque{n}" mp3_dir = AUDIOS_DIR / f"bloque{n}" if not md_dir.exists(): print(f"⚠️ No existe: {md_dir}") continue for path_md in sorted(md_dir.glob('*_audio.md')): resultado.append((path_md, mp3_dir)) return resultado async def main() -> None: archivos = resolver_archivos(sys.argv[1:]) if not archivos: print("No se encontraron ficheros *_audio.md.") return print(f"📂 Procesando {len(archivos)} fichero(s)...\n") for path_md, mp3_dir in archivos: await convertir(path_md, mp3_dir) print("\n✨ Proceso completado.") if __name__ == "__main__": asyncio.run(main())