Clock de radio calidad Bloomberg — aplicado 2026-09-07

Objetivo declarado: calidad de radio Bloomberg, pero del otro espectro político. La lectura operativa: el rigor ES la marca, no la ideología. Bloomberg suena a Bloomberg por disciplina de formato (frase corta, atribución en cada claim, consecuencia concreta, reloj consistente, opinión cuarentenada), no por su línea editorial. Las fuentes ya son las otras (Astillero, Ríodoce, AJ English, Democracy Now!, The Intercept, Pie de Página, Mongabay, Brasil de Fato, Doctorow, EFF, Rest of World). Faltaba el oficio.

Aplicado sobre surtido/scripts/radio_parrilla.py + tanda_radio.sh + radio_push.sh + sandbox-sep-6/parrilla/arma_parrilla.sh + asgi.py + proponer.py. Auditoría previa: [CREDENCIAL].md.

1. Se mató el bug que mandó contenido viejo al aire

tanda_radio.sh corría radio_parrilla.py y después arma_parrilla.sh; el segundo hardcodeaba 13 inputs, varios nunca regenerados (b1-mx3, todas las gemelas -c). La edición del 2026-09-06 salió con el bloque tech de las 14:35 a las 19:33.

  • tanda_radio.sh ya no llama a arma_parrilla.sh. Un solo ensamblador.
  • arma_parrilla.sh reescrito: ensambla lo que diga playlist.m3u, sin lista fija.
  • _verificar_frescura() aborta si un segmento noticioso tiene mtime anterior al inicio de la corrida. Probado con 4 casos (corrida real pasa, segmento de 5 h aborta, archivo ausente aborta, cortinilla evergreen exenta).
  • radio_push.sh ya no empuja los globs b1-*/b2-*. Legacy a _viejos-2026-09-06/.
  • Salida renombrada parrilla-7min.mp3 → parrilla.mp3 (el nombre mentía: eran 5:29). asgi.py resuelve ambos y muestra la duración real.

2. Reglas de guion nuevas (todas verificadas contra el preview real)

problema audiblefix
GPT-6/Alibaba y SpaceX leídos en “Internacional”ruteo por prioridad, TECH antes que MUNDO; sin substring suelto
NFL y US Open en bloques de noticias durasdetector de deportes por contenido → cápsula propia
Zenwalk leído dos veces (ES y EN, mismo enlace)dedup por URL canónica; x.com/<h>/status/<id> → el id
”GPT-6 con bucle caliente…” repetido dos veces seguidasanti_eco() recorta del qué pasó las cláusulas que solo parafrasean el titular; si no queda aporte, el bloque pasa a brief
”crucial para la economía digital mexicana”limpiar_boiler(): fuera la relevancia forzada sin dato ni nombre del titular
titular en inglés leído con voz es-USse traduce desde reporte/traducciones.json; si no está, se descarta y se informa
5 bullets diciendo ”, en x.com”atribución = nombre del medio o handle real; fórmula rota (según / de acuerdo con / reporta / informa)
la 3ª noticia de México nunca se oyópresupuesto por bloque: entran las notas que caben, lo demás pasa al siguiente segmento. Nunca se trunca a mitad de frase
”Más de México.” y silencioguarda de bloque vacío: no se emite
”abajo por menos cinco”_marcadores(): “2-1” → “2 a 1”, solo con contexto deportivo (no rompe fechas ni versiones)
emojis al TTS, posts cortados a media palabra_limpiar() + corte en frontera de frase
el pulso eran 9 s de puros conteosahora cita 2 posts reales, descartando saludos y el eco lead/cuerpo

3. Se recuperaron dos bloques que ya existían y se tiraban

parse_preview() solo capturaba viñetas en negrita. Las otras dos secciones del preview se parseaban a 0 items:

  • En 60 segundos → bloque 01-titulares (hard open, estilo Bloomberg: titulares secos antes que nada)
  • PARA SEGUIR → bloque 11-tomorrow

Y el campo Por qué importa, que bullet() descartaba, ahora alimenta los bloques focus. Es análisis ya escrito que la cadena tiraba a la basura.

4. El clock (14 segmentos + cortinilla + debate)

00 ID + hora al aire · 01 titulares · 02 NACIONAL brief · 03 NACIONAL focus
04 MUNDO brief · 05 MUNDO focus · 06 TECH headlines · 07 TECH focus
08 TECH radar (condicional) · 08b DEPORTES · [cortinilla 8-bit]
09 time check · 10 señales/pulso · 11 mañana · 12 resumen · 13 ID cierre · debate (4 voces)

Separación brief ≠ focus en todo el reloj: el brief da titular + fuente, el focus agrega contexto y consecuencia. Es lo que distingue radio de un RSS leído en voz alta.

5. Audio

loudnorm=I=-20:TP=-2.0:LRA=7 + aresample=48000:async=1 por entrada. Los MP3 de gcloud vienen a 24 kHz y las cortinillas a 48; sin el resample el concat tiraba non monotonically increasing dts y Liquidsoap/Icecast calculan mal la duración. Salida 48 kHz mono 96 kb/s. Medido: parrilla -20.5 LUFS, fallback -20.3 LUFS.

El spread entre segmentos ya era bueno antes (-19.2 a -20.7 LUFS, 1.5 LU). No era un problema; se fijó de todos modos para que no se mueva cuando entre música.

6. fallback-loop.mp3 dejó de ser cp de la parrilla

Era copia idéntica: si caía la fuente en vivo, el oyente escuchaba las mismas 14 noticias en ciclo infinito. Ahora rota la parte atemporal del reloj (focus + señales + mañana + resumen) entre cortinillas. Verificado por md5: distintos.

7. Fix aguas arriba

proponer.py PROMPT_SISTEMA (el de español) no tenía la regla anti-boilerplate que el PROMPT_SISTEMA_TECH sí tenía. Agregada: consecuencia concreta y verificable, prohibido cerrar con relevancia genérica, y que_paso no debe repetir el titular. Así el próximo preview ya viene limpio y limpiar_boiler() pasa a ser red de seguridad, no el filtro principal.

Verificación

  • radio_parrilla.py --dry imprime el guion completo con tiempos y lo descartado, sin gastar TTS.
  • Corrida real: 14 segmentos, parrilla.mp3 334.13 s, decodifica sin errores.
  • arma_parrilla.sh por separado: 16 segmentos, 334 s — ambas rutas coinciden.
  • asgi.py con TestClient: /radio 200 con secciones Nacional / Internacional / Tech / Reloj, player “Al aire — parrilla 5.6 min”, /radio/en 200, los 4 /radio/audio/* 200, cero referencias residuales a parrilla-7min.
  • bash -n en los 4 scripts, py_compile en los 4 módulos.

Lo que NO se hizo (deliberadamente)

  • Orpheus local. La cadena es Google Cloud TTS con voces ya calibradas, y la estación es en español (Orpheus es inglés-first). Es otro proyecto.
  • Los 60 minutos. Una ronda da 5.6 min. La unidad es el clock, no la hora.
  • The Big Question / Closing Commentary. Requieren generación editorial (LLM), no retrieval. Sigue sin cablearse.
  • Quitar la cortinilla 8-bit. Choca con el acabado Bloomberg, pero es identidad existente (“donde la nostalgia debate con el futuro”). Decisión de gusto, no técnica.

Siguiente palanca real

El cuello no es el retrieval (278 items por ronda) sino la destilación: el preview entrega ~14 bullets por las cuotas de auditoría (México ≥40%, EEUU ≥40%, independientes ≥40%, grandes ≤35%, máx 2 por medio). Subir a 30–40 bullets llevaría el clock a ~15–20 min sin repetir nada. Eso es un cambio en proponer.py, no en la cadena de radio.

Pendiente conocido (aguas arriba, no es de radio)

pulso.json trae “creador de chetos de Call of Duty” — mistranslation de cheats (trampas). En México “cheto” es delator. Se oyó al aire en el bloque de señales. La traducción de posts del pulso no pasa por el mismo control que los titulares.