#!/usr/bin/env python3 """Toma la ruta de un tema, extrae metadata y datos online, genera texto enriquecido. Uso: ./enriquecer-locucion.py /ruta/al/tema.mp3 Salida (stdout): texto enriquecido listo para locución Estructura de la locución: 1. Opener: una línea aleatoria, alternando entre curiosidades.txt y frases-motivacionales.txt (nunca se repite el mismo banco dos veces seguidas). 2. Tema: título/artista + info enriquecida (año/género/país/dato Wikipedia). 3. Cierre opcional: una línea de identificaciones.txt (con {radio} sustituido por el nombre real de la emisora, leído de gradio.config) — aparece según el porcentaje configurado en locutor.config (50% por defecto). """ import json, os, random, re, requests, sys try: import mutagen except ImportError: mutagen = None DATA_DIR = os.environ.get("GR_LOCUTOR_DIR", os.path.expanduser("~/.gradio/data/locutor-ia")) CACHE_FILE = os.path.expanduser("~/.gradio/data/tmp/locuciones_cache.json") GRADIO_CONFIG_FILE = os.path.expanduser("~/.gradio/data/tmp/gradio.config") LOCUTOR_CONFIG_FILE = os.path.join(DATA_DIR, "locutor.config") ALTERNANCIA_FILE = os.path.expanduser("~/.gradio/data/tmp/locutor_ultimo_banco") MB_USER_AGENT = "GRadio-LocutorIA/1.0" TIMEOUT = 8 def log(msg): print(f"[enriquecer] {msg}", file=sys.stderr, flush=True) def station_name(): """Lee el nombre de la radio desde gradio.config (línea 3), igual que GRP. Default 'G Radio' si no existe el archivo o la línea está vacía.""" try: with open(GRADIO_CONFIG_FILE) as f: lines = f.read().splitlines() if len(lines) >= 3 and lines[2].strip(): return lines[2].strip() except FileNotFoundError: pass return "G Radio" def porcentaje_identificacion(): """Lee porcentaje_identificacion de locutor.config (clave=valor). Default 50.""" try: with open(LOCUTOR_CONFIG_FILE) as f: for line in f: if line.strip().startswith("porcentaje_identificacion"): return int(line.split("=", 1)[1].strip()) except (FileNotFoundError, ValueError, IndexError): pass return 50 def leer_banco(nombre_archivo): path = os.path.join(DATA_DIR, nombre_archivo) try: with open(path, encoding="utf-8") as f: return [l.strip() for l in f if l.strip()] except FileNotFoundError: return [] def elegir_opener(): """Alterna entre curiosidades.txt y frases-motivacionales.txt: nunca repite el mismo banco dos veces seguidas (se persiste cuál tocó última vez).""" bancos = {"curiosidades": "curiosidades.txt", "frases": "frases-motivacionales.txt"} ultimo = "" try: with open(ALTERNANCIA_FILE) as f: ultimo = f.read().strip() except FileNotFoundError: pass opciones = [b for b in bancos if b != ultimo] or list(bancos) elegido = random.choice(opciones) lineas = leer_banco(bancos[elegido]) if not lineas: # Fallback: probar el otro banco si el elegido está vacío/falta otro = [b for b in bancos if b != elegido] if otro: lineas = leer_banco(bancos[otro[0]]) elegido = otro[0] if not lineas: return "" try: with open(ALTERNANCIA_FILE, "w") as f: f.write(elegido) except OSError: pass return random.choice(lineas) def elegir_identificacion(): lineas = leer_banco("identificaciones.txt") if not lineas: return f"Estás en sintonía con {station_name()}." return random.choice(lineas).replace("{radio}", station_name()) def extract_metadata(song_path): title = artist = album = year = genre = None if mutagen: try: tags = mutagen.File(song_path, easy=True) if tags: title = tags.get("title", [None])[0] artist = tags.get("artist", [None])[0] album = tags.get("album", [None])[0] year = tags.get("date", [None])[0] genre = tags.get("genre", [None])[0] if title and artist: return title.strip(), artist.strip(), album, year, genre except Exception as e: log(f"mutagen error: {e}") name = os.path.splitext(os.path.basename(song_path))[0] name = re.sub(r"\(.*?\)", "", name) name = re.sub(r"www\..*", "", name) name = name.strip() artist = title = None if " - " in name: parts = name.split(" - ", 1) artist = parts[0].strip() title = parts[1].strip() else: title = name return title, artist, album, year, genre def load_cache(): try: with open(CACHE_FILE) as f: return json.load(f) except Exception: return {} def save_cache(cache): try: with open(CACHE_FILE, "w") as f: json.dump(cache, f) except Exception: pass def lookup_wikipedia(artist, title, cache): if not title: return None key = f"wiki:{artist}:{title}" if key in cache: return cache[key] queries = [] if artist: queries.append(f'"{artist}" "{title}" canción') queries.append(f'"{artist}" "{title}" música') queries.append(f'"{artist}" músico') else: queries.append(f'"{title}" canción') WIKI = "https://es.wikipedia.org/w/api.php" for q in queries: try: r = requests.get(WIKI, params={ "action": "query", "list": "search", "srsearch": q, "format": "json", "srlimit": 3, }, timeout=TIMEOUT, headers={"User-Agent": "GRadio-LocutorIA/1.0 (radio-automation)"}, ) data = r.json() pages = data.get("query", {}).get("search", []) if not pages: continue for page in pages: page_title = page["title"] r2 = requests.get( WIKI, params={ "action": "query", "prop": "extracts", "exintro": True, "explaintext": True, "titles": page_title, "format": "json", "exsentences": 3, }, timeout=TIMEOUT, headers={"User-Agent": "GRadio-LocutorIA/1.0 (radio-automation)"}, ) pg_data = r2.json().get("query", {}).get("pages", {}) for pid, pg in pg_data.items(): if pid == "-1" or "extract" not in pg: continue extract = pg["extract"].strip() sentences = re.split(r"(?<=[.!?])\s+", extract) for s in sentences: s = s.strip() if 40 <= len(s) <= 200: cache[key] = s save_cache(cache) return s except Exception as e: log(f"wiki error ({q[:30]}...): {e}") cache[key] = None save_cache(cache) return None def lookup_musicbrainz(artist, title, cache): if not artist or not title: return None key = f"mb:{artist}:{title}" if key in cache: return cache[key] try: r = requests.get( "https://musicbrainz.org/ws/2/recording/", params={ "query": f'artist:"{artist}" AND recording:"{title}"', "fmt": "json", "limit": 1, }, timeout=TIMEOUT + 2, headers={"User-Agent": MB_USER_AGENT}, ) data = r.json() recs = data.get("recordings", []) if not recs: cache[key] = None save_cache(cache) return None rec = recs[0] info = {} if "releases" in rec and rec["releases"]: rel = rec["releases"][0] date = rel.get("date", "") info["year"] = date[:4] if date and len(date) >= 4 else "" info["country"] = rel.get("country", "") if "artist-credit" in rec: ac = rec["artist-credit"][0] if isinstance(ac, dict) and "artist" in ac: info["artist_type"] = ac["artist"].get("type", "") cache[key] = info if info else None save_cache(cache) return info except Exception as e: log(f"musicbrainz error: {e}") cache[key] = None save_cache(cache) return None _PAISES = { "US": "Estados Unidos", "GB": "Reino Unido", "UK": "Reino Unido", "ES": "España", "MX": "México", "AR": "Argentina", "CO": "Colombia", "CL": "Chile", "PE": "Perú", "CU": "Cuba", "PR": "Puerto Rico", "DE": "Alemania", "FR": "Francia", "IT": "Italia", "BR": "Brasil", "CA": "Canadá", "AU": "Australia", "JP": "Japón", "MY": "Malasia", "SE": "Suecia", "NL": "Países Bajos", "IE": "Irlanda", "NO": "Noruega", "DK": "Dinamarca", "PT": "Portugal", "RU": "Rusia", } def generate_text(artist, title, album, year, genre, wiki_fact, mb_info): anyo = mb_info.get("year", "") if mb_info and isinstance(mb_info, dict) else "" nombre_pais = "" pa_mb = mb_info.get("country", "") if mb_info and isinstance(mb_info, dict) else "" if pa_mb: nombre_pais = _PAISES.get(pa_mb, pa_mb) # --- 1. Opener (alternando curiosidades.txt / frases-motivacionales.txt) --- opener = elegir_opener() # --- 2. Tema --- if artist and title: tema = f"Escuchas {title} de {artist}" elif title: tema = f"Escuchas {title}" else: tema = "" # --- 3. Info enriquecida --- infos = [] if anyo and anyo not in ("?", ""): infos.append(f"grabada en {anyo}") if genre and genre not in ("?", ""): infos.append(f"un tema de {genre}") if nombre_pais: infos.append(f"lanzada en {nombre_pais}") info_str = ", ".join(infos) if infos else "" wiki_str = "" if wiki_fact: fact = wiki_fact.strip() if len(fact) > 160: fact = fact[:157] + "..." wiki_str = fact if tema and info_str: cuerpo = f"{tema}, {info_str}." elif tema: cuerpo = f"{tema}." elif info_str: cuerpo = info_str else: cuerpo = "" if wiki_str: cuerpo = f"{cuerpo} {wiki_str}" if cuerpo else wiki_str # --- 4. Cierre opcional (según porcentaje_identificacion de locutor.config) --- cierre = "" if random.randint(1, 100) <= porcentaje_identificacion(): cierre = elegir_identificacion() oraciones = [o for o in (opener, cuerpo, cierre) if o] return " ".join(oraciones) def main(): if len(sys.argv) < 2: log("Uso: enriquecer-locucion.py /ruta/al/tema.mp3") sys.exit(1) song_path = os.path.abspath(sys.argv[1]) if not os.path.exists(song_path): log(f"no existe: {song_path}") sys.exit(1) title, artist, album, year, genre = extract_metadata(song_path) log(f"metadata: artist={artist} title={title} album={album} year={year} genre={genre}") cache = load_cache() wiki_fact = lookup_wikipedia(artist or title, title, cache) mb_info = lookup_musicbrainz(artist or title, title, cache) text = generate_text(artist, title, album, year, genre, wiki_fact, mb_info) log(f"wiki={'sí' if wiki_fact else 'no'} mb={'sí' if mb_info else 'no'}") log(f"texto ({len(text)} chars): {text[:100]}...") print(text) if __name__ == "__main__": main()