Auditoría de parrilla + clock de 15 min — 2026-09-07
Auditoría con evidencia de sandbox-sep-6/parrilla/ + surtido/scripts/radio_parrilla.py,
corrida real del cron tanda_radio.sh de 2026-09-06 19:30.
1. El error grave: contenido stale al aire
tanda_radio.sh:11-12 corre dos ensambladores en secuencia:
.venv/bin/python scripts/radio_parrilla.py # genera b1-*, b2-* frescos + parrilla-7min.mp3
bash .../parrilla/arma_parrilla.sh # SOBREESCRIBE parrilla-7min.mp3
arma_parrilla.sh hardcodea inputs que radio_parrilla.py nunca regenera:
| archivo | mtime | estado |
|---|---|---|
b1-mx/int/mx2/int2.mp3 | 19:33 | fresco |
b1-mx3.mp3 | 14:34 | STALE — nadie lo genera |
b2-tech1-c … cierre-id-c.mp3 | 14:35 | STALE — todo el bloque 2 |
b2-tech1/2/3.mp3 | 19:33 | fresco pero descartado |
Verificación por duración (no por suposición):
parrilla-7min.mp3 real = 329.74s
simulado radio_parrilla.py = 311.66s (delta 18.08)
simulado arma_parrilla.sh = 329.78s (delta 0.04) ← GANADOR
Conclusión: la edición al aire es bloque 1 de las 19:33 + bloque 2 completo de las 14:35.
Nueve llamadas a Google TTS de las 19:33 se tiraron a la basura. radio_push.sh subió los 34
archivos al Linode, stale incluidos.
Fix: una sola fuente de verdad. Borrar arma_parrilla.sh de tanda_radio.sh, o reescribirlo
para que lea playlist.m3u (que sí genera radio_parrilla.py).
Corolario: playlist.m3u (orden de radio_parrilla.py) no coincide con
parrilla-7min.mp3 (orden de arma_parrilla.sh). Quien consuma el .m3u oye otro programa.
E INTEGRACION.md documenta un tercer orden (13 segmentos con b1-mx3/b2-tech4).
2. Errores editoriales audibles (confirmados parseando el preview real)
Fuente: reporte/2026-09-06-preview.md. Secciones: MUNDO 2, EEUU 1, BILATERAL/FRONTERA 1,
MÉXICO 4, CIENCIA·TECH·MEDIOAMBIENTE 1, TECNOLOGÍA · CHINA Y EL MUNDO 5.
2.1 Fuga tech → internacional (substring match)
pick(secs,"MUNDO",...) hace if name in k. La sección "TECNOLOGÍA · CHINA Y EL MUNDO"
contiene "MUNDO" → sus 5 items entran al bloque Internacional.
lo que se oyó en "Internacional":
- Hebrón (MUNDO) ✓
- ultraderecha alemana ✓
- GPT-6 / Alibaba ✗ TECH
- SpaceX / empresa china ✗ TECH
- Zheng Qinwen US Open ✗ DEPORTE
Fix: match por orden explícito + marcar secciones consumidas, nunca substring.
2.2 Duplicado por URL distinta de título
Zenwalk aparece 2× con el mismo URL (phoronix.com/news/Zenwalk-2026-Current), una en
español y otra en inglés. pick() deduplica por t[0][:40] → no los atrapa. Se lee dos veces.
Fix: dedup por URL normalizada (quitar query/fragment/trailing slash), no por prefijo de título.
2.3 Eco título == “Qué pasó”
En los items de QbitAI / TechNode / Zhihu / Phoronix-EN el campo Qué pasó es una copia literal
del título. bullet() hace f"{title}. {que}" → la misma frase dos veces seguidas:
“GPT-6 con Transformer de bucle caliente, Alibaba ya lo ha planeado. GPT-6 con Transformer de bucle caliente, Alibaba ya lo ha planeado, en qbitai.com.”
Fix: si similitud(title, que) alta → usar solo porque. Afectó a 4 de 14 bullets hoy.
2.4 Truncación silenciosa a 620 caracteres
cap(normalizar(text), 620) corta a mitad de frase y pega un ”.“. Medido en el log de la corrida:
| bloque | texto crudo | emitido | pérdida |
|---|---|---|---|
| b1-mx | 759c | 610c | 3ª noticia de México nunca se oyó |
| b1-int2 | 740c | 619c | truncado |
| b2-tech1 | 686c | 616c | truncado |
Fix: presupuesto por bloque — agregar bullets uno a uno mientras quepan; si sobran, abrir
b1-mx3/b2-tech4 de verdad (generados, no stale). Nunca cortar a ciegas.
2.5 Bloques raquíticos / degenerados
Slices sin guarda: b1-mx2 = mx[3:] salió con 1 noticia (17s: “Más de México.” + una frase).
b2-tech3 = tech[5:] → 1 noticia (18s). Si un día el preview trae ≤3 items de México, el
segmento dice “Más de México.” y se calla. Fix: si el slice sale vacío, no emitir el segmento.
2.6 Inglés leído con voz es-US
"Better Models Aren't Enough: China's AI Race Turns to Cost, Products and Paying Users" entra
al bloque tech y se sintetiza con es-US-Neural2-C. El propio preview advierte: “Titulares
chinos traducidos por LLM local — revisar antes de enviar.”
Ya existe traducciones.py + reporte/traducciones.json (22KB, regenerado 19:32) y no se usa
en la cadena de radio. Fix: detectar idioma → traducir o descartar.
2.7 Deportes sin sección
NFL/Djokovic/béisbol cayó en EEUU → bloque Internacional. US Open/Zheng Qinwen cayó en
TECNOLOGÍA · CHINA Y EL MUNDO → bloque Tech. Fix: sección DEPORTES propia, o filtro.
2.8 Atribución degenerada
dominio() sobre x.com/<handle>/status/... devuelve siempre x.com. Hoy 5 bullets dijeron
”, en x.com” — muletilla, y pierde la fuente real (Julio Astillero, Ríodoce, AJ English).
Peor: zurl.co/FHXbC (acortador) se citó como fuente de un video de PeriodistasU.
Fix: para x.com/twitter.com usar el handle; lista negra de acortadores → fallback a src.
2.9 Pulso = 9 segundos de puros números
b2-pulso lee “Muestra de 46 publicaciones: 27 neutras, 6 negativas y 13 positivas, balance
0.152”. Cero contenido, y pulso.json trae posts reales con texto (Jornada, bluesky).
Además desincronizado: pulso.json dice muestra 46 (13/27/6) y el preview.md dice 61 posts
(39% neg / 41% neu / 20% pos). Dos corridas distintas conviviendo.
2.10 Dos secciones del preview que se tiran enteras
parse_preview() solo captura bullets con - **título** — fuente. Estas dos secciones usan
viñetas sin negrita → se parsean a 0 items:
En 60 segundos(6 bullets) = el bloque Titulares / Daily Recap gratisPARA SEGUIR(4 bullets) = el bloque Tomorrow gratis
Es contenido ya escrito por el LLM que nadie lee. Fix: segundo regex para viñeta simple.
2.11 fallback-loop.mp3 es copia idéntica
cp parrilla-7min.mp3 fallback-loop.mp3. En Liquidsoap/AzuraCast, cuando cae la fuente loopeas
el mismo audio bit a bit: el oyente escucha las mismas 14 noticias en ciclo infinito.
Ese es exactamente el anti-patrón de la rotación (ver §4).
2.12 Menores
- Cron one-shot
30 19 6 9 *no se autodesinstaló:crontab: Operation not permitted. Sigue vivo. _concataplicaatempo=0.96a todo menos al último archivo, incluida la cortinilla 8-bit.playlist.m3uES usa rutas absolutas;playlist-en.m3uusa relativas.
3. De la parrilla de 60 min propuesta: qué sirve y qué no
Aritmética de la oferta (medida, no estimada)
retrieval por ronda: feed.json 173 + feed-tech.json 105 = 278 items crudos
destilado a preview: 14 bullets únicos
audio resultante: 5:29 (329.74s verificados)
banco sonoro total: 6:00 (3:00 son debates, resto IDs de 2-15s)
60 minutos de radio = 7,000–9,000 palabras ≈ 150–180 bullets. Hoy produces 14. Factor 11×. Una ronda de retrieval no da para una hora: da para cinco minutos y medio.
Sirve, y ya tienes la fuente de datos
| bloque propuesto | fuente real que ya existe | estado |
|---|---|---|
| Headlines / Daily Recap | sección En 60 segundos | se tira (§2.10) |
| Brief World / National / Tech | b1-int / b1-mx / b2-tech | existe, mal ruteado (§2.1) |
| Focus (“why this matters”) | campo Por qué importa | bullet() lo descarta; solo lo usa el debate |
| Signals | pulso.json con posts reales | solo lees conteos (§2.9) |
| Tomorrow | sección PARA SEGUIR | se tira (§2.10) |
| Midshow ID / Outro | cierre-id, intro-8bit, tun-tun, stingers | existe |
| separar news de analysis | que vs porque ya vienen separados | gratis |
El campo Por qué importa es el hallazgo más caro: análisis ya escrito que la cadena tira.
No sirve hoy
- The Big Question / Closing Commentary — requieren generación editorial (LLM), no retrieval. Es el paso a Groq/Ollama que aún no está cableado.
- World→Business / Tech Radar — no hay fuentes de economía/mercados ni de releases separadas; “tech” es un solo bucket.
- 60 min corridos — ver aritmética arriba.
- Orpheus local — la cadena actual es Google Cloud TTS (
project-323ffb8d-90a3-4bc3-971) con voces ya calibradas:es-US-News-D,es-US-Neural2-C, más Neural2-B / es-ES-Standard-E/F para el debate. Orpheus es inglés-first y tu estación es español (decidido en2026-09-06-radio-en-optin.md:/radioespañol, EN opt-in en/radio/en). Es otro proyecto; no lo mezcles con arreglar la parrilla. - La identidad en inglés de la propuesta (World news. National perspective.) — el formato sirve, el idioma no.
4. La unidad no es la hora, es el clock de 15 minutos
Radio real no programa 60 minutos de una pieza: programa un hot clock de :00/:15/:30/:45 y lo repite con variación. Eso resuelve de raíz “el retrieval solo da para una ronda”.
Clock de 15:00 — construible hoy con datos que ya tienes
:00 ID + En 60 segundos (titulares) 1:00 ← §2.10, ya escrito
:01 NACIONAL brief (3) 2:30 ← b1-mx
:04 NACIONAL focus (1, Por qué importa) 1:30 ← campo ya existe
:05 MUNDO brief (3) 2:00 ← b1-int con routing arreglado
:07 MUNDO focus (1) 1:00
:08 cortinilla 8-bit 0:15 ← descanso-8bit.mp3
:08 TECH headlines (3) 2:00 ← b2-tech1
:10 TECH focus (1) 1:30
:12 PULSO / Signals (2-3 posts reales) 1:00 ← §2.9
:13 PARA SEGUIR / Tomorrow (3) 1:00 ← §2.10, ya escrito
:14 debate o recap 1:00
:15 ID de cierre 0:15 ← cierre-id
= 15:00 clavado, sin una sola fuente nueva.
Tres palancas para crecer la oferta, en orden de costo
- Rendir más la misma ronda (gratis):
Por qué importa+En 60 segundos+PARA SEGUIR- pulso con posts reales = +3:00 → 8:30 por ronda.
- Destilar más bullets (barato): el preview recorta por cuotas de auditoría (México 4/9, EEUU 3/9, independientes 7/9, grandes 2/9). Subir a 30–40 bullets de los 278 crudos = ~18–20 min por ronda. Aquí ya alcanza un clock de 15 sin repetir nada.
- Más rondas (costo real): retrieval cada 3–4 h en vez de una tanda nocturna.
Rotación: cómo repetir sin que se note
El problema no es repetir, es repetir idéntico. fallback-loop.mp3 = cp es el anti-patrón.
estado.jsoncon fingerprint (URL normalizada) + timestamp de cada item emitido.- Cada pasada del clock toma los N bullets más frescos no emitidos en las últimas X horas.
- Si la ronda se agota antes del siguiente fetch, degradación en 3 niveles, nunca silencio:
- Otro ángulo — mismo item, bloque Focus en vez de Brief.
queyporqueya vienen separados: dos lecturas legítimas de la misma noticia. - Otra voz — las gemelas
-c(News-D ↔ Neural2-C) ya existen por rotación anti-saturación. - Banco evergreen — debates anteriores, stingers.
- Otro ángulo — mismo item, bloque Focus en vez de Brief.
- Caducidad 12–18 h. Un item muere y no vuelve aunque no haya nada más. Mejor cortinilla que noticia rancia.
5. Orden de fixes (scope mínimo que cambia lo que se oye hoy)
tanda_radio.sh— eliminar la doble ensamblada. Una fuente de verdad. (crítico, 1 línea)pick()— secciones por orden explícito + consumidas, no substring. (crítico)- dedup por URL normalizada. (crítico)
bullet()— anti-eco title/que + usarporqueen bloques Focus. (crítico)parse_preview()— segundo regex para viñeta simple → recuperaEn 60 segundosyPARA SEGUIR.cap()— presupuesto por bloque en vez de truncar.- guarda de slice vacío + filtro de idioma +
dominio()con handles de x.com. fallback-loop.mp3— que no sea copia idéntica.
Los 1–4 solos ya eliminan: tech en el bloque internacional, el Zenwalk doble, el eco de los titulares chinos, la noticia de México perdida por truncamiento y el bloque 2 de hace cinco horas.