133 lines
4.1 KiB
Python
133 lines
4.1 KiB
Python
#!/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())
|