Sincronizar mu-plugins/ y scripts/ con el estado real del sitio (2026-07-16)
Este repo llevaba desde el 28-jun sin actualizarse (salvo sync_carta_from_prod.py).
Se pone al dia con 3 semanas de trabajo que solo vivian en el checkout local
completo del proyecto:
- Fix del buscador (issue #178): AND obligatorio en FULLTEXT (antes devolvia
practicamente todo el sitio con cualquier busqueda), exclusion de paginas
indice residuales de la migracion K2, soporte de frase exacta entre comillas.
- Nuevos mu-plugins ya desplegados a prod: fea-carta-id-api, fea-cloudflare-realip,
fea-crear-autor-api, fea-gsc-verification, fea-legacy-redirect (cutover
Joomla->WP, ver issue #162).
- TTS multi-voz por autor, scripts de traduccion, sync de audio a prod,
homenajes Mardones/Galarreta.
Se mantiene la estructura plana del repo (mu-plugins/ + scripts/, sin el
wordpress/wp-content/ del checkout completo) tal y como documenta el README:
es la convencion establecida para este repo, mas limpia para quien solo
necesita leer el codigo. Anadido .gitignore (cache de Python que no debia
commitearse).
Fuente: checkout local completo del proyecto, rama main (commit 69e849d).
This commit is contained in:
+15
-3
@@ -19,6 +19,7 @@ if ($action === 'get') {
|
||||
'title' => $p->post_title,
|
||||
'content' => $p->post_content,
|
||||
'status' => $p->post_status,
|
||||
'author' => (int)$p->post_author,
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
exit(0);
|
||||
}
|
||||
@@ -43,10 +44,11 @@ if ($action === 'getmeta') {
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if ($action === 'setaudio') { // setaudio <id> <relpath>
|
||||
if ($action === 'setaudio') { // setaudio <id> <relpath> [voice_id]
|
||||
$id = (int)$argv[2];
|
||||
$voice = $argv[4] ?? 'NicoFeadulta2026';
|
||||
update_post_meta($id, 'fea_audio_url', home_url($argv[3]));
|
||||
update_post_meta($id, 'fea_audio_voice', 'NicoFeadulta2026');
|
||||
update_post_meta($id, 'fea_audio_voice', $voice);
|
||||
update_post_meta($id, 'fea_audio_done', '1');
|
||||
delete_post_meta($id, 'fea_audio_error');
|
||||
fwrite(STDOUT, "ok " . home_url($argv[3]) . "\n");
|
||||
@@ -58,5 +60,15 @@ if ($action === 'setflag') { // setflag <id> <key> <value>
|
||||
exit(0);
|
||||
}
|
||||
|
||||
fwrite(STDERR, "uso: get|update|getmeta|setaudio|setflag\n");
|
||||
if ($action === 'unsetaudio') { // unsetaudio <id> (rollback: despublica el audio)
|
||||
$id = (int)$argv[2];
|
||||
delete_post_meta($id, 'fea_audio_url');
|
||||
delete_post_meta($id, 'fea_audio_voice');
|
||||
delete_post_meta($id, 'fea_audio_done');
|
||||
delete_post_meta($id, 'fea_audio_error');
|
||||
fwrite(STDOUT, "ok despublicado $id\n");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
fwrite(STDERR, "uso: get|update|getmeta|setaudio|setflag|unsetaudio\n");
|
||||
exit(2);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* FROM ew4r_k2_items i LEFT JOIN ew4r_users u ON u.id=i.created_by \
|
||||
* WHERE i.id IN ($IDS);" > /tmp/autores143.tsv
|
||||
*
|
||||
* Uso (en el servidor, dentro de /web/wp-nuevo):
|
||||
* Uso (en el servidor, dentro de /web):
|
||||
* FEA_TSV=/tmp/autores143.tsv wp eval-file scripts/fix_k2_authors.php # dry-run
|
||||
* APPLY=1 FEA_TSV=/tmp/autores143.tsv wp eval-file scripts/fix_k2_authors.php # aplica
|
||||
*
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Homenajes Mardones / Galarreta + fixes varios (sesión 2026-06-23)
|
||||
|
||||
Scripts de la sesión 2026-06-23 (issues Gitea **#130, #131, #133, #137, #138**, todas cerradas).
|
||||
Todo aplicado en **local** (Docker `wordpress-web`) y **prod** (wp-nuevo, vía `ssh feadulta@… && wp eval-file`).
|
||||
|
||||
## Convenciones del despliegue a prod (wp-nuevo)
|
||||
- Prod corre en la **raíz** del dominio (`https://wp-nuevo.feadulta.com`), local bajo `/fea`.
|
||||
→ al llevar contenido con rutas, transformar `/fea/wp-content` → `/wp-content`.
|
||||
- `proc_open` deshabilitado en prod → usar `wp eval-file` (PHP puro). `scp` no va → subir con
|
||||
`ssh '… cat > /entrada/FICHERO'`; imágenes en bloque con `tar cf -` → `ssh 'tar x'`.
|
||||
- **IDs de contenido migrado coinciden local↔prod** (posts, template parts, users, cats).
|
||||
- **Lección clave:** para arreglar posts ya migrados con texto malo (idioma/contenido),
|
||||
**sobrescribir EN SITIO en prod** (mismos IDs) en vez de borrar+recrear → preserva los grupos
|
||||
Polylang. Para contenido NUEVO (noticia, 23 posts Mardones) crear con **meta idempotente**.
|
||||
|
||||
## #138 — Mardones: 23 páginas `anterior/*.htm` → 23 posts individuales
|
||||
Origen: el homenaje WP **17990** (`mardones-2`, com_content Joomla 501) enlazaba 23 páginas
|
||||
estáticas de la web V1 FrontPage (`/web/anterior/*.htm`, windows-1252). 8 «Aprendiendo a orar»
|
||||
(serie de Mardones) + 15 homenaje (obituarios, álbum de fotos, audio→conferencia, currículo…).
|
||||
|
||||
Pipeline:
|
||||
1. `mardones_build_manifest.py` — descarga local de los 23 `.htm`, decodifica **win-1252→UTF-8**,
|
||||
limpia basura FrontPage, extrae párrafos `<p>` (descarta cabecera «cristianos siglo veintiuno»),
|
||||
preserva enlaces externos, inserta imágenes. Genera `mardones_manifest.json`.
|
||||
Títulos = texto del enlace en 17990; autor: serie orar = **710 (Mardones)**, homenajes = **1**.
|
||||
2. `mardones_create_posts.php` (local) / `mardones_prod_create_and_repoint.php` (prod) — crea los 23
|
||||
posts (lang es, cat 38, publish), idempotente por meta **`_mardones_src`**=nombre de fichero.
|
||||
3. `mardones_repoint_homenaje.php` — repunta los 23 `href="anterior/X.htm"` del 17990 a los nuevos
|
||||
permalinks (prod lo hace el script combinado).
|
||||
4. Imágenes (8: `homenaje-album-1..7.jpg`, `homenaje-charla-mejico.jpg`) → `uploads/anterior-mardones/`.
|
||||
|
||||
IDs creados en local: **53577–53599**. En prod: IDs propios (se resuelven por meta).
|
||||
|
||||
### Retoques posteriores (`mardones_fix_fecha_y_target.php`, local+prod)
|
||||
- **Fecha:** los posts se crearon con fecha de hoy → reajustados a **2012-10-10 19:26:00**
|
||||
(la del propio homenaje 17990) para que no aparezcan como entradas nuevas en feeds/archivos.
|
||||
- **Ventana nueva:** los enlaces del homenaje 17990 heredaban `target="_blank"` del original →
|
||||
eliminado (`target="_blank"`/`rel`) para que abran en la **misma ventana** (navegación interna).
|
||||
*Nota:* los enlaces externos DENTRO de cada post (p.ej. la conferencia iteso.mx en homenaje-3)
|
||||
sí conservan `target="_blank"`.
|
||||
|
||||
## #138 — Galarreta
|
||||
`galarreta_repoint_presentaciones.php` (local) / `galarreta_prod_fixes.php` (prod):
|
||||
- 10 presentaciones de `presentaciones-gala` (19107) estaban mal mapeadas (apuntaban a artículos
|
||||
random) → repuntadas a sus destinos correctos (19105/19104/19102/19106/19101/19119/19123/19132/19131/19143).
|
||||
- «Libros y e-books» (en la página 17918) → tienda externa
|
||||
`https://edicionesfeadulta.com/?filters=autor[galarreta]` (13 libros), `target="_blank"`.
|
||||
- iframe de YouTube forzado a `https`.
|
||||
- Auditados `articulos-galarreta` (19086) y `comentarios-galarreta` (19085): sin enlaces rotos reales.
|
||||
|
||||
## #130 / #131 / #133 / #137 (sync a prod)
|
||||
- `issue130_prod_noticia_papa.php` — crea la noticia «León XIV en España…» (Joomla 9082, no migrada)
|
||||
en cat 41 Noticias de alcance, idempotente por `_fgj2wp_old_id=9082`.
|
||||
- `issue131_prod_footer_suma.php` — enlaza la imagen `este_portal.gif` del footer (template 42370)
|
||||
a `/numeros/` (post 18036, «La suma de todos»).
|
||||
- `issue133_prod_idiomas.php` — 5 reasignaciones de idioma + sobrescritura en sitio de los 3 `fr`
|
||||
que contenían inglés (43755/47152/47136) + catalán 13339 → francés, enlazado al ES 13338.
|
||||
- `issue137_prod_inmemoriam.php` — repunta In memoriam (19112): Galarreta→17918, Mardones→17990,
|
||||
Mari Patxi Ayerra→23519 (artículo de fallecimiento).
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
// 1) presentaciones-gala 19107: repuntar 10
|
||||
$map=[
|
||||
"sugerencias-para-las-bodas"=>19105,"el-eterno-problema-del-mundo-y-los-hijos"=>19104,
|
||||
"necesitamos-un-pepe"=>19102,"lucas-18-1-8"=>19106,"el-perdon-primero"=>19101,
|
||||
"alabar-a-dios-y-darle-gracias"=>19119,"2-reyes-5-14-17-2-timoteo-2-8-13"=>19123,
|
||||
"creer-en-dios-es-apostar-por-la-vida"=>19132,"lucas-17-5-10"=>19131,"la-gran-evasion"=>19143,
|
||||
];
|
||||
$c=get_post(19107)->post_content; $n=0;
|
||||
foreach($map as $bad=>$dst){
|
||||
if(!get_post($dst)){ echo " dst $dst NO EXISTE\n"; continue; }
|
||||
$url=get_permalink($dst);
|
||||
$c2=preg_replace('#https?://[^"]*?/'.preg_quote($bad,'#').'/#',$url,$c,1,$cnt);
|
||||
if($cnt>0){ $c=$c2; $n++; }
|
||||
}
|
||||
wp_update_post(["ID"=>19107,"post_content"=>$c]);
|
||||
echo "presentaciones repuntadas: $n/10\n";
|
||||
// 2) libros + youtube en 17918
|
||||
$c=get_post(17918)->post_content;
|
||||
$ext="https://edicionesfeadulta.com/?filters=autor[galarreta]";
|
||||
$c=preg_replace('#https?://[^"]*?/libros-y-e-books/#',$ext,$c,-1,$cl);
|
||||
$c=preg_replace('#(<a [^>]*href=")'.preg_quote($ext,'#').'("(?![^>]*target))#','$1'.$ext.'$2 target="_blank" rel="noopener"',$c);
|
||||
$c=str_replace('src="//www.youtube.com/embed/','src="https://www.youtube.com/embed/',$c);
|
||||
wp_update_post(["ID"=>17918,"post_content"=>$c]);
|
||||
echo "libros->externo: $cl | youtube https aplicado | foto JE: ".(strpos($c,'edicionesfeadulta.com')!==false?'ok':'?')."\n";
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
// Repuntar las 10 presentaciones de presentaciones-gala (19107) a sus destinos correctos
|
||||
$map=[
|
||||
"sugerencias-para-las-bodas"=>19105,
|
||||
"el-eterno-problema-del-mundo-y-los-hijos"=>19104,
|
||||
"necesitamos-un-pepe"=>19102,
|
||||
"lucas-18-1-8"=>19106,
|
||||
"el-perdon-primero"=>19101,
|
||||
"alabar-a-dios-y-darle-gracias"=>19119,
|
||||
"2-reyes-5-14-17-2-timoteo-2-8-13"=>19123,
|
||||
"creer-en-dios-es-apostar-por-la-vida"=>19132,
|
||||
"lucas-17-5-10"=>19131,
|
||||
"la-gran-evasion"=>19143,
|
||||
];
|
||||
$c=get_post(19107)->post_content; $n=0; $report=[];
|
||||
foreach($map as $badslug=>$dest){
|
||||
$destUrl=get_permalink($dest);
|
||||
// sustituir el href que contiene /fea/<badslug>/
|
||||
$pat='#https?://[^"]*?/fea/'.preg_quote($badslug,'#').'/#';
|
||||
$c2=preg_replace($pat,$destUrl,$c,1,$cnt);
|
||||
if($cnt>0){ $c=$c2; $n++; $report[]="OK $badslug -> $destUrl"; }
|
||||
else $report[]="NO ENCONTRADO $badslug";
|
||||
}
|
||||
wp_update_post(["ID"=>19107,"post_content"=>$c]);
|
||||
echo implode("\n",$report)."\nTotal repuntados: $n/10\n";
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
global $wpdb;
|
||||
$ex=$wpdb->get_var($wpdb->prepare("SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key='_fgj2wp_old_id' AND meta_value=%s","9082"));
|
||||
if($ex){ echo "ya existe en prod: $ex\n"; return; }
|
||||
$content=file_get_contents('/entrada/9082_content.html');
|
||||
$id=wp_insert_post(["post_type"=>"post","post_status"=>"publish","post_author"=>890,
|
||||
"post_title"=>"El plan (casi definitivo) de León XIV en España, al descubierto: de Carabanchel a Arguineguín, pasando por Montserrat",
|
||||
"post_content"=>$content,"post_date"=>"2026-04-09 12:07:26","to_ping"=>"","pinged"=>"","post_content_filtered"=>""],true);
|
||||
if(is_wp_error($id)){ echo "ERROR: ".$id->get_error_message()."\n"; return; }
|
||||
update_post_meta($id,"_fgj2wp_old_id","9082");
|
||||
wp_set_post_categories($id,[41]);
|
||||
if(function_exists('pll_set_post_language')) pll_set_post_language($id,'es');
|
||||
echo "creado prod $id cat=".implode(",",wp_get_post_categories($id))." status=".get_post_status($id)."\n";
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
$id=42370; $c=get_post($id)->post_content; $url=get_permalink(18036);
|
||||
if(strpos($c,'<a href="'.$url.'"><img')!==false || preg_match('#<a [^>]*href="[^"]*numeros[^"]*"><img[^>]*este_portal#',$c)){ echo "ya enlazado\n"; return; }
|
||||
// localizar el <img ... este_portal.gif ...> y envolverlo
|
||||
if(preg_match('#<img[^>]*este_portal\.gif[^>]*/?>#i',$c,$m)){
|
||||
$img=$m[0];
|
||||
$c=str_replace($img,'<a href="'.esc_url($url).'">'.$img.'</a>',$c);
|
||||
// linkDestination none->custom para el bloque (id 26996)
|
||||
$c=preg_replace('#("id":26996,[^}]*?"linkDestination":")none(")#','${1}custom${2}',$c);
|
||||
wp_update_post(["ID"=>$id,"post_content"=>$c]);
|
||||
echo "OK enlazado este_portal -> $url\n";
|
||||
} else echo "NO se encontró img este_portal.gif en 42370 (prod)\n";
|
||||
// verificar
|
||||
if(preg_match('#<a href="([^"]+)"><img[^>]*este_portal#',get_post($id)->post_content,$v)) echo "verif: -> ".$v[1]."\n";
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
// 1) reasignaciones de idioma (mismos IDs en prod)
|
||||
$fix=[4424=>"fr",11129=>"it",12934=>"en",5891=>"en",3141=>"it"];
|
||||
foreach($fix as $id=>$lang){
|
||||
if(!get_post($id)){ echo " $id NO EXISTE\n"; continue; }
|
||||
$old=pll_get_post_language($id); pll_set_post_language($id,$lang);
|
||||
echo " $id: $old -> ".pll_get_post_language($id)."\n";
|
||||
}
|
||||
// 2) sobrescribir fr malos + catalan con francés correcto (overwrite en sitio)
|
||||
$payload=json_decode(file_get_contents('/entrada/fr_payload.json'),true);
|
||||
foreach($payload as $it){
|
||||
$dst=$it['dst'];
|
||||
if(!get_post($dst)){ echo " dst $dst NO EXISTE\n"; continue; }
|
||||
wp_update_post(["ID"=>$dst,"post_title"=>$it['title'],"post_content"=>$it['content']]);
|
||||
pll_set_post_language($dst,'fr');
|
||||
echo " overwrite $dst -> fr (".mb_substr($it['title'],0,30).")\n";
|
||||
}
|
||||
// 3) catalan 13339: enlazar grupo con ES 13338
|
||||
if(get_post(13338) && get_post(13339)){
|
||||
pll_set_post_language(13338,'es');
|
||||
pll_save_post_translations(["es"=>13338,"fr"=>13339]);
|
||||
echo " grupo 13339: ".json_encode(pll_get_post_translations(13339))."\n";
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
$c=get_post(19112)->post_content;
|
||||
$map=[
|
||||
"el-bautismo-de-jesus-jesus-hijo-del-padre"=>17918, // Galarreta homenaje
|
||||
"jean-baptiste-et-son-mouton"=>17990, // Mardones homenaje
|
||||
"nuestra-vida-sea-servicio"=>23519, // Mari Patxi fallecimiento
|
||||
];
|
||||
$n=0;
|
||||
foreach($map as $slug=>$id){
|
||||
$url=get_permalink($id);
|
||||
$pat='#https?://[^"]*?/'.preg_quote($slug,'#').'/#';
|
||||
$c2=preg_replace($pat,$url,$c,1,$cnt);
|
||||
if($cnt>0){ $c=$c2; $n++; echo " OK $slug -> $url\n"; } else echo " NO ENCONTRADO $slug\n";
|
||||
}
|
||||
if($n) wp_update_post(["ID"=>19112,"post_content"=>$c]);
|
||||
// verificar 8 enlaces
|
||||
preg_match_all('#href="([^"]+)"#',get_post(19112)->post_content,$m);
|
||||
$ok=0;$bad=0;
|
||||
foreach($m[1] as $h){ if(preg_match('#/([^/"]+)/?$#',$h,$sm)){ $p=get_posts(["name"=>$sm[1],"post_status"=>"publish","posts_per_page"=>1,"suppress_filters"=>true]); if($p)$ok++; else {$bad++; echo " ROTO $h\n";} } }
|
||||
echo "repuntados=$n | enlaces in-memoriam OK=$ok bad=$bad\n";
|
||||
@@ -0,0 +1,93 @@
|
||||
import re, json, html, glob
|
||||
|
||||
IMG_BASE = "/fea/wp-content/uploads/anterior-mardones/"
|
||||
|
||||
# filename -> (titulo, autor_wp, [imagenes])
|
||||
ORAR_AUTHOR = 710 # Mardones
|
||||
HOM_AUTHOR = 1 # genérico (como página 17990)
|
||||
META = {
|
||||
"orar-1-dejarmequerer":("Dejarme querer",ORAR_AUTHOR,[]),
|
||||
"orar-2-escuchar":("Escuchar",ORAR_AUTHOR,[]),
|
||||
"orar-3-estar":("Estar",ORAR_AUTHOR,[]),
|
||||
"orar-4-hacersitioadios":("Hacer sitio a Dios",ORAR_AUTHOR,[]),
|
||||
"orar-5-laoracionadulta":("La oración adulta",ORAR_AUTHOR,[]),
|
||||
"orar-6-orarconevangelio":("Orar con el evangelio",ORAR_AUTHOR,[]),
|
||||
"orar-7-orarrepitiendo":("Orar repitiendo una palabra o frase breve",ORAR_AUTHOR,[]),
|
||||
"orar-8-tuestasdentro":("Tú estás dentro",ORAR_AUTHOR,[]),
|
||||
"homenaje-segundo-aniversario":("En el segundo aniversario",HOM_AUTHOR,[]),
|
||||
"homenaje-14noviembre":("14 de noviembre de 2007",HOM_AUTHOR,[]),
|
||||
"homenaje-PRIMERaniversario":("Primer aniversario",HOM_AUTHOR,[]),
|
||||
"homenaje-aniversario":("Programa en el aniversario",HOM_AUTHOR,["homenaje-charla-mejico.jpg"]),
|
||||
"homenaje-rostrointerior":("Rostro interior de José María Mardones",HOM_AUTHOR,[]),
|
||||
"homenaje-cartaReyes":("Carta a un Maestro",HOM_AUTHOR,[]),
|
||||
"homenaje-9":("Semblanza",HOM_AUTHOR,[]),
|
||||
"homenaje-8":("Álbum de fotos de José María Mardones",HOM_AUTHOR,
|
||||
["homenaje-album-%d.jpg"%i for i in range(1,8)]),
|
||||
"homenaje-1":("Funeral del sábado 24 de junio",HOM_AUTHOR,[]),
|
||||
"homenaje-2":("Funeral del jueves 29 de junio",HOM_AUTHOR,[]),
|
||||
"homenaje-5":("Obituario en El Mundo",HOM_AUTHOR,[]),
|
||||
"homenaje-6":("Necrológicas en El País",HOM_AUTHOR,[]),
|
||||
"homenaje-4":("Salmo a la Encarnación de Dios",HOM_AUTHOR,[]),
|
||||
"homenaje-3":("Conferencia: ¿Por qué preocuparse por los demás? Ética y convivencia",HOM_AUTHOR,[]),
|
||||
"homenaje-7":("Currículo de José María Mardones",HOM_AUTHOR,[]),
|
||||
}
|
||||
|
||||
BOILER = re.compile(r'cristianos siglo veintiuno|^I N M E M O R I A M$|^HOMENAJE$|^homenaje a JOSE|APRENDIENDO A ORAR cristianos', re.I)
|
||||
|
||||
def extract(fn):
|
||||
raw=open(fn,'rb').read().decode('windows-1252',errors='replace')
|
||||
raw=re.sub(r'(?is)<(script|style).*?</\1>','',raw)
|
||||
raw=re.sub(r'(?is)<!--.*?-->','',raw)
|
||||
blocks=[]
|
||||
for p in re.findall(r'(?is)<p\b[^>]*>(.*?)</p>',raw):
|
||||
# preservar enlaces externos
|
||||
links=re.findall(r'(?is)<a\s+[^>]*href="(https?://[^"]+)"[^>]*>(.*?)</a>',p)
|
||||
t=re.sub(r'(?is)<[^>]+>','',p)
|
||||
t=html.unescape(t).replace('\xa0',' ')
|
||||
t=re.sub(r'\s+',' ',t).strip()
|
||||
if len(t)<3:
|
||||
# párrafo solo-enlace
|
||||
if links:
|
||||
for href,txt in links:
|
||||
txt=re.sub(r'<[^>]+>','',txt).strip() or href
|
||||
blocks.append(('link',href,txt))
|
||||
continue
|
||||
if BOILER.search(t):
|
||||
continue
|
||||
# ¿el párrafo contiene un enlace externo embebido? añadir como link extra
|
||||
blocks.append(('p',t,links))
|
||||
return blocks
|
||||
|
||||
def build_html(blocks, imgs):
|
||||
out=[]
|
||||
for b in blocks:
|
||||
if b[0]=='p':
|
||||
if re.match(r'^https?://\S+$', b[1]):
|
||||
out.append('<p><a href="%s" target="_blank" rel="noopener">%s</a></p>'%(html.escape(b[1]),html.escape(b[1])))
|
||||
continue
|
||||
txt=html.escape(b[1])
|
||||
# re-incrustar enlaces externos que estaban en el párrafo
|
||||
for href,atxt in b[2]:
|
||||
atxt_clean=html.escape(re.sub(r'<[^>]+>','',atxt).strip() or href)
|
||||
# no siempre está el texto en txt; añadimos al final si no
|
||||
out.append("<p>%s</p>"%txt)
|
||||
elif b[0]=='link':
|
||||
out.append('<p><a href="%s" target="_blank" rel="noopener">%s</a></p>'%(html.escape(b[1]),html.escape(b[2])))
|
||||
# imágenes al final
|
||||
for im in imgs:
|
||||
out.append('<p><img src="%s%s" alt="José María Mardones" style="max-width:100%%;height:auto"/></p>'%(IMG_BASE,im))
|
||||
return "\n".join(out)
|
||||
|
||||
manifest=[]
|
||||
for fn in sorted(glob.glob("*.htm")):
|
||||
key=fn[:-4]
|
||||
if key not in META:
|
||||
print("SIN META:",key); continue
|
||||
title,author,imgs=META[key]
|
||||
blocks=extract(fn)
|
||||
content=build_html(blocks,imgs)
|
||||
manifest.append({"file":key,"title":title,"author":author,"content":content,"nparas":len([b for b in blocks if b[0]=='p']),"nimgs":len(imgs)})
|
||||
|
||||
json.dump(manifest,open("manifest.json","w"),ensure_ascii=False,indent=1)
|
||||
print("manifest:",len(manifest),"posts")
|
||||
for m in manifest: print(f" {m['file']:34} '{m['title'][:35]}' autor={m['author']} paras={m['nparas']} imgs={m['nimgs']} len={len(m['content'])}")
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
$man=json_decode(file_get_contents('/tmp/manifest.json'),true);
|
||||
$map=[]; $created=0; $skipped=0;
|
||||
foreach($man as $m){
|
||||
// idempotencia
|
||||
$ex=get_posts(["meta_key"=>"_mardones_src","meta_value"=>$m['file'],"post_type"=>"post","post_status"=>"any","posts_per_page"=>1,"suppress_filters"=>true]);
|
||||
if($ex){ $map[$m['file']]=$ex[0]->ID; $skipped++; continue; }
|
||||
$id=wp_insert_post([
|
||||
"post_type"=>"post","post_status"=>"publish","post_author"=>(int)$m['author'],
|
||||
"post_title"=>$m['title'],"post_content"=>$m['content'],
|
||||
"to_ping"=>"","pinged"=>"","post_content_filtered"=>"",
|
||||
],true);
|
||||
if(is_wp_error($id)){ echo "ERROR ".$m['file'].": ".$id->get_error_message()."\n"; continue; }
|
||||
update_post_meta($id,"_mardones_src",$m['file']);
|
||||
wp_set_post_categories($id,[38]);
|
||||
if(function_exists('pll_set_post_language')) pll_set_post_language($id,'es');
|
||||
$map[$m['file']]=$id; $created++;
|
||||
}
|
||||
file_put_contents('/tmp/mardones_map.json',json_encode($map));
|
||||
echo "creados=$created skipped=$skipped total=".count($map)."\n";
|
||||
foreach($map as $f=>$id) echo " $f -> $id\n";
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
// 1) fecha: alinear con el homenaje 17990 (2012-10-10)
|
||||
$date="2012-10-10 19:26:00"; $gmt=get_gmt_from_date($date);
|
||||
$ids=get_posts(["meta_key"=>"_mardones_src","post_type"=>"post","posts_per_page"=>-1,"fields"=>"ids","post_status"=>"any","suppress_filters"=>true]);
|
||||
$nd=0;
|
||||
foreach($ids as $id){ wp_update_post(["ID"=>$id,"post_date"=>$date,"post_date_gmt"=>$gmt]); $nd++; }
|
||||
echo "fechas actualizadas: $nd posts -> $date\n";
|
||||
// 2) quitar target=_blank (y rel) de los enlaces internos del homenaje 17990
|
||||
$c=get_post(17990)->post_content;
|
||||
$before=substr_count(strtolower($c),'target="_blank"');
|
||||
$c=str_replace([' target="_blank"',' rel="noopener"',' rel="noreferrer"'],'',$c);
|
||||
wp_update_post(["ID"=>17990,"post_content"=>$c]);
|
||||
$after=substr_count(strtolower(get_post(17990)->post_content),'target="_blank"');
|
||||
echo "target=_blank en 17990: antes=$before despues=$after\n";
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
$man=json_decode(file_get_contents('/entrada/manifest_prod.json'),true);
|
||||
$map=[]; $created=0;$skip=0;
|
||||
foreach($man as $m){
|
||||
$ex=get_posts(["meta_key"=>"_mardones_src","meta_value"=>$m['file'],"post_type"=>"post","post_status"=>"any","posts_per_page"=>1,"suppress_filters"=>true]);
|
||||
if($ex){ $map[$m['file']]=$ex[0]->ID; $skip++; continue; }
|
||||
$author=(int)$m['author']; if($author && !get_userdata($author)) $author=1;
|
||||
$id=wp_insert_post(["post_type"=>"post","post_status"=>"publish","post_author"=>$author,
|
||||
"post_title"=>$m['title'],"post_content"=>$m['content'],"to_ping"=>"","pinged"=>"","post_content_filtered"=>""],true);
|
||||
if(is_wp_error($id)){ echo "ERR ".$m['file'].": ".$id->get_error_message()."\n"; continue; }
|
||||
update_post_meta($id,"_mardones_src",$m['file']);
|
||||
wp_set_post_categories($id,[38]);
|
||||
if(function_exists('pll_set_post_language')) pll_set_post_language($id,'es');
|
||||
$map[$m['file']]=$id; $created++;
|
||||
}
|
||||
echo "creados=$created skip=$skip total=".count($map)."\n";
|
||||
// repuntar 17990
|
||||
$c=get_post(17990)->post_content; $n=0;
|
||||
foreach($map as $file=>$id){
|
||||
$url=get_permalink($id);
|
||||
$c2=preg_replace('#href="anterior/'.preg_quote($file,'#').'\.htm"#i','href="'.$url.'"',$c,1,$cnt);
|
||||
if($cnt>0){ $c=$c2; $n++; }
|
||||
}
|
||||
wp_update_post(["ID"=>17990,"post_content"=>$c]);
|
||||
$rest=preg_match_all('#anterior/[\w-]+\.htm#i',get_post(17990)->post_content,$mm);
|
||||
echo "17990 repuntados=$n/23 | anterior/*.htm restantes=$rest\n";
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
$map=json_decode(file_get_contents('/tmp/mardones_map.json'),true);
|
||||
$c=get_post(17990)->post_content; $n=0; $miss=[];
|
||||
foreach($map as $file=>$id){
|
||||
$url=get_permalink($id);
|
||||
// href="anterior/FILE.htm" (puede tener mayúsculas y espacios delante)
|
||||
$pat='#href="anterior/'.preg_quote($file,'#').'\.htm"#i';
|
||||
$c2=preg_replace($pat,'href="'.$url.'"',$c,1,$cnt);
|
||||
if($cnt>0){ $c=$c2; $n++; } else $miss[]=$file;
|
||||
}
|
||||
wp_update_post(["ID"=>17990,"post_content"=>$c]);
|
||||
echo "repuntados=$n/23\n";
|
||||
if($miss) echo "NO ENCONTRADOS: ".implode(", ",$miss)."\n";
|
||||
// verificar que no quedan anterior/*.htm
|
||||
$rest=preg_match_all('#anterior/[\w-]+\.htm#i',get_post(17990)->post_content,$mm);
|
||||
echo "enlaces anterior/*.htm restantes: $rest\n";
|
||||
if($rest) foreach($mm[0] as $x) echo " $x\n";
|
||||
@@ -8,7 +8,7 @@
|
||||
* user_meta _foto_perfil_pre143. Idempotente (si ya apunta, solo regenera metadata).
|
||||
*
|
||||
* Entrada: TSV «uid<TAB>display_name» (env FEA_TSV, por defecto /tmp/users29.tsv).
|
||||
* Uso (en el servidor, dentro de /web/wp-nuevo):
|
||||
* Uso (en el servidor, dentro de /web):
|
||||
* FEA_TSV=/tmp/users29.tsv wp eval-file scripts/import_avatars_143.php # dry-run
|
||||
* APPLY=1 FEA_TSV=/tmp/users29.tsv wp eval-file scripts/import_avatars_143.php # aplica
|
||||
*/
|
||||
|
||||
+31
-7
@@ -45,6 +45,22 @@ def creds():
|
||||
KEY, GID = creds()
|
||||
H_JSON = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}
|
||||
|
||||
# Voz clonada por autor (WP user_id -> voice_id). Issue #152: solo se aplica a
|
||||
# los artículos de ese autor; el resto sigue con la voz por defecto (Nico).
|
||||
AUTHOR_VOICES = {
|
||||
382: "FrayMarcosFeadulta2026", # Fray Marcos
|
||||
383: "PagolaFeadulta2026", # José Antonio Pagola
|
||||
774: "SicreFeadulta2026", # José Luis Sicre
|
||||
386: "ArregiFeadulta2026", # José Arregi
|
||||
}
|
||||
|
||||
|
||||
def voice_for_author(author_id, default_voice):
|
||||
try:
|
||||
return AUTHOR_VOICES.get(int(author_id), default_voice)
|
||||
except (TypeError, ValueError):
|
||||
return default_voice
|
||||
|
||||
|
||||
def _q(url):
|
||||
return f"{url}?GroupId={GID}" if GID else url
|
||||
@@ -83,7 +99,7 @@ def get_post_text(pid):
|
||||
raw = html.unescape(raw)
|
||||
paras = [re.sub(r"\s+", " ", p).strip() for p in raw.split("\n") if len(p.strip()) > 1]
|
||||
paras = trim_after_author_signature(paras)
|
||||
return d["title"], "\n\n".join(paras)
|
||||
return d["title"], "\n\n".join(paras), d.get("author")
|
||||
|
||||
|
||||
def is_author_signature(text):
|
||||
@@ -114,12 +130,20 @@ def is_author_signature(text):
|
||||
|
||||
|
||||
def trim_after_author_signature(paras):
|
||||
out = []
|
||||
"""Corta tras la firma final del autor, buscando desde el final hacia
|
||||
atrás para no confundirla con encabezados iniciales (p.ej. "CORPUS (A)",
|
||||
"DOMINGO XI (A)") que también matchean la heurística de is_author_signature
|
||||
pero aparecen como primer párrafo, no como firma. Solo cuenta como firma
|
||||
si hay al menos 200 caracteres de contenido real antes de ella."""
|
||||
prefix_len = 0
|
||||
prefix_lens = []
|
||||
for p in paras:
|
||||
out.append(p)
|
||||
if is_author_signature(p):
|
||||
break
|
||||
return out
|
||||
prefix_lens.append(prefix_len)
|
||||
prefix_len += len(p)
|
||||
for i in range(len(paras) - 1, -1, -1):
|
||||
if prefix_lens[i] > 200 and is_author_signature(paras[i]):
|
||||
return paras[: i + 1]
|
||||
return paras
|
||||
|
||||
|
||||
def _sent_pause(n_words, short, long_):
|
||||
@@ -413,7 +437,7 @@ def main():
|
||||
elif cmd == "carta":
|
||||
pid, voice_id = sys.argv[2], sys.argv[3]
|
||||
model = sys.argv[4] if len(sys.argv) > 4 else "speech-2.8-turbo"
|
||||
title, text = get_post_text(int(pid))
|
||||
title, text, _author = get_post_text(int(pid))
|
||||
name = sys.argv[5] if len(sys.argv) > 5 else f"carta-minimax-{pid}-{model.split('-')[-1]}"
|
||||
text = add_pauses(text)
|
||||
print(f"Post #{pid}: «{title}» ({len(text)} car con pausas)")
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
sync_audio_to_prod.py — Sube a PROD los mp3 de TTS ya generados/enlazados en
|
||||
local (fea_audio_done=1) y fija el meta fea_audio_url en prod.
|
||||
|
||||
Prod (134.0.10.170) tiene glibc rota: scp/sftp NO funcionan (connection closed).
|
||||
Workaround: subir el binario por stdin de ssh ("cat > ruta"), igual que el resto
|
||||
de scripts que tocan ese servidor (ver feadulta-server-glibc-rota.md).
|
||||
|
||||
Uso:
|
||||
python3 sync_audio_to_prod.py --carta 54254 # sincroniza toda la cola de la carta
|
||||
python3 sync_audio_to_prod.py --ids 912,919,3001 # ids concretos
|
||||
python3 sync_audio_to_prod.py --carta 54254 --dry-run # solo plan, no toca prod
|
||||
|
||||
Rollback (despublica en prod lo que este script publicó):
|
||||
python3 sync_audio_to_prod.py --rollback --carta 54254
|
||||
python3 sync_audio_to_prod.py --rollback --ids 912,919
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
WP_CONTAINER = os.environ.get("FEA_WP_CONTAINER", "wordpress-web")
|
||||
DB_CONTAINER = os.environ.get("FEA_DB_CONTAINER", "wordpress-mysql")
|
||||
DB_NAME = os.environ.get("FEA_DB_NAME", "wordpress_db")
|
||||
DB_USER = os.environ.get("FEA_DB_USER", "wordpress_user")
|
||||
DB_PASS = os.environ.get("FEA_DB_PASS", "wordpress_pass")
|
||||
|
||||
PROD_HOST = os.environ.get("FEA_PROD_HOST", "feadulta@134.0.10.170")
|
||||
PROD_PASS = os.environ.get("FEA_PROD_PASS", "C6c2A!mAl3Wj.BQF")
|
||||
PROD_WPLOAD = os.environ.get("FEA_PROD_WPLOAD", "/web/wp-load.php")
|
||||
PROD_HELPER = "/tmp/fea_post_io.php"
|
||||
PROD_UPLOADS_TTS = "/web/wp-content/uploads/tts"
|
||||
|
||||
HELPER_SRC = Path(__file__).resolve().parent / "fea_post_io.php"
|
||||
LOCAL_TTS_DIR = Path(__file__).resolve().parent.parent / "wordpress/wp-content/uploads/tts"
|
||||
|
||||
LOG_FILE = Path(os.environ.get(
|
||||
"FEA_AUDIO_SYNC_LOG",
|
||||
str(Path(__file__).resolve().parent.parent / "logs/feadulta-audio-sync.log"),
|
||||
))
|
||||
STATE_FILE = Path(os.environ.get(
|
||||
"FEA_AUDIO_SYNC_STATE",
|
||||
str(Path(__file__).resolve().parent.parent / "logs/feadulta-audio-sync-state.json"),
|
||||
))
|
||||
|
||||
|
||||
def log(msg: str) -> None:
|
||||
line = f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}"
|
||||
print(line, flush=True)
|
||||
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with LOG_FILE.open("a", encoding="utf-8") as f:
|
||||
f.write(line + "\n")
|
||||
|
||||
|
||||
def sh(cmd: list[str], *, input_bytes: bytes | None = None, timeout: int = 120) -> str:
|
||||
r = subprocess.run(cmd, input=input_bytes, capture_output=True, timeout=timeout)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"cmd falló ({r.returncode}): {' '.join(cmd[:4])}…\n{r.stderr.decode(errors='replace')[:400]}")
|
||||
return r.stdout.decode(errors="replace")
|
||||
|
||||
|
||||
# ── Local ────────────────────────────────────────────────────────────────────
|
||||
def local_meta(post_id: int, key: str) -> str:
|
||||
r = subprocess.run(
|
||||
["docker", "exec", WP_CONTAINER, "php", "/tmp/fea_post_io.php", "getmeta", str(post_id), key],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
)
|
||||
return r.stdout.strip()
|
||||
|
||||
|
||||
def carta_article_ids(carta_id: int) -> list[int]:
|
||||
q = ("SELECT post_id FROM wp_postmeta "
|
||||
f"WHERE meta_key='_carta_id' AND meta_value='{carta_id}' ORDER BY post_id;")
|
||||
r = subprocess.run(
|
||||
["docker", "exec", DB_CONTAINER, "mysql", f"-u{DB_USER}", f"-p{DB_PASS}",
|
||||
DB_NAME, "-N", "-e", q],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
)
|
||||
return [int(x) for x in r.stdout.split() if x.isdigit()]
|
||||
|
||||
|
||||
# ── Prod (glibc rota: nada de scp/sftp, todo por ssh + cat) ────────────────────
|
||||
def _ssh_text(remote_cmd: str, *, stdin: str | None = None, timeout: int = 120) -> str:
|
||||
cmd = ["sshpass", "-p", PROD_PASS, "ssh", "-o", "StrictHostKeyChecking=accept-new",
|
||||
"-o", "ConnectTimeout=20", PROD_HOST, remote_cmd]
|
||||
r = subprocess.run(cmd, input=stdin, capture_output=True, text=True, timeout=timeout)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"ssh falló ({r.returncode}): {remote_cmd[:80]}…\n{r.stderr.strip()[:400]}")
|
||||
return r.stdout
|
||||
|
||||
|
||||
def _ssh_upload_bytes(data: bytes, remote_path: str, *, timeout: int = 180) -> None:
|
||||
cmd = ["sshpass", "-p", PROD_PASS, "ssh", "-o", "StrictHostKeyChecking=accept-new",
|
||||
"-o", "ConnectTimeout=20", PROD_HOST, f"cat > {remote_path}"]
|
||||
sh(cmd, input_bytes=data, timeout=timeout)
|
||||
|
||||
|
||||
_prod_helper_ready = False
|
||||
|
||||
|
||||
def prod_helper(subcmd: str, *args: str) -> str:
|
||||
global _prod_helper_ready
|
||||
if not _prod_helper_ready:
|
||||
_ssh_upload_bytes(HELPER_SRC.read_bytes(), PROD_HELPER)
|
||||
_prod_helper_ready = True
|
||||
inner = f"FEA_WP_LOAD={PROD_WPLOAD} php {PROD_HELPER} {subcmd} " + " ".join(args)
|
||||
return _ssh_text(inner, timeout=60)
|
||||
|
||||
|
||||
def prod_upload_mp3(post_id: int) -> None:
|
||||
src = LOCAL_TTS_DIR / f"{post_id}.mp3"
|
||||
data = src.read_bytes()
|
||||
remote_path = f"{PROD_UPLOADS_TTS}/{post_id}.mp3"
|
||||
_ssh_upload_bytes(data, remote_path)
|
||||
# Verificación de tamaño (glibc rota => sin fiarse ciegamente del rc=0 de ssh)
|
||||
remote_size = int(_ssh_text(f"wc -c < {remote_path}").strip())
|
||||
if remote_size != len(data):
|
||||
raise RuntimeError(f"tamaño no coincide tras subir #{post_id}: local={len(data)} remoto={remote_size}")
|
||||
|
||||
|
||||
def prod_remove_mp3(post_id: int) -> None:
|
||||
_ssh_text(f"rm -f {PROD_UPLOADS_TTS}/{post_id}.mp3")
|
||||
|
||||
|
||||
# ── Estado ───────────────────────────────────────────────────────────────────
|
||||
def load_state() -> dict:
|
||||
if STATE_FILE.exists():
|
||||
try:
|
||||
return json.loads(STATE_FILE.read_text())
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return {"synced": []}
|
||||
|
||||
|
||||
def save_state(state: dict) -> None:
|
||||
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
STATE_FILE.write_text(json.dumps(state, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
# ── Sync ─────────────────────────────────────────────────────────────────────
|
||||
def sync_one(post_id: int, state: dict, *, dry_run: bool) -> str:
|
||||
if local_meta(post_id, "fea_audio_done") != "1":
|
||||
return "sin-audio-local"
|
||||
if not (LOCAL_TTS_DIR / f"{post_id}.mp3").exists():
|
||||
return "mp3-local-ausente"
|
||||
if dry_run:
|
||||
return "PLAN: subiría mp3 + setaudio"
|
||||
|
||||
voice = local_meta(post_id, "fea_audio_voice") or "NicoFeadulta2026"
|
||||
prod_upload_mp3(post_id)
|
||||
prod_helper("setaudio", str(post_id), f"/wp-content/uploads/tts/{post_id}.mp3", voice)
|
||||
if post_id not in state["synced"]:
|
||||
state["synced"].append(post_id)
|
||||
save_state(state)
|
||||
return "ok"
|
||||
|
||||
|
||||
def rollback_one(post_id: int, state: dict) -> str:
|
||||
prod_helper("unsetaudio", str(post_id))
|
||||
prod_remove_mp3(post_id)
|
||||
if post_id in state["synced"]:
|
||||
state["synced"].remove(post_id)
|
||||
save_state(state)
|
||||
return "ok"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="Sincroniza audio TTS local→prod (o lo despublica con --rollback).")
|
||||
ap.add_argument("--carta", type=int, default=0, help="WP post ID de la carta local; sincroniza toda su cola.")
|
||||
ap.add_argument("--ids", default="", help="Lista CSV de post IDs concretos.")
|
||||
ap.add_argument("--rollback", action="store_true", help="Despublica en prod en vez de publicar.")
|
||||
ap.add_argument("--dry-run", action="store_true", help="Solo muestra el plan; no toca prod.")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.ids:
|
||||
ids = [int(x) for x in args.ids.split(",") if x.strip().isdigit()]
|
||||
elif args.carta:
|
||||
ids = carta_article_ids(args.carta)
|
||||
else:
|
||||
ap.error("hace falta --carta o --ids")
|
||||
return 2
|
||||
|
||||
state = load_state()
|
||||
mode = "ROLLBACK" if args.rollback else "SYNC"
|
||||
log(f"=== INICIO {mode} audio→prod. {len(ids)} posts candidatos: {ids} ===")
|
||||
|
||||
ok = skip = err = 0
|
||||
for pid in ids:
|
||||
try:
|
||||
if args.rollback:
|
||||
if pid not in state["synced"] and not args.ids:
|
||||
res = "no-estaba-sincronizado"
|
||||
skip += 1
|
||||
else:
|
||||
res = rollback_one(pid, state)
|
||||
ok += 1
|
||||
else:
|
||||
res = sync_one(pid, state, dry_run=args.dry_run)
|
||||
if res == "ok" or res.startswith("PLAN"):
|
||||
ok += 1
|
||||
else:
|
||||
skip += 1
|
||||
log(f" #{pid}: {res}")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
err += 1
|
||||
log(f" #{pid}: ERROR {exc}")
|
||||
|
||||
log(f"=== FIN {mode}. ok={ok} skip={skip} error={err}. Estado: {STATE_FILE} ===")
|
||||
return 1 if err else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -31,7 +31,7 @@ DB_PASS = os.environ.get("FEA_DB_PASS", "wordpress_pass")
|
||||
|
||||
PROD_HOST = os.environ.get("FEA_PROD_HOST", "feadulta@134.0.10.170")
|
||||
PROD_PASS = os.environ.get("FEA_PROD_PASS", "C6c2A!mAl3Wj.BQF")
|
||||
PROD_WPLOAD = os.environ.get("FEA_PROD_WPLOAD", "/web/wp-nuevo/wp-load.php")
|
||||
PROD_WPLOAD = os.environ.get("FEA_PROD_WPLOAD", "/web/wp-load.php")
|
||||
PROD_HELPER = "/tmp/fea_translate_helper.php"
|
||||
|
||||
HELPER_SRC = Path(__file__).resolve().parent / "fea_translate_helper.php"
|
||||
@@ -45,7 +45,7 @@ STATUS = os.environ.get("FEA_SYNC_STATUS", "draft")
|
||||
# instalación cuelga de la raíz. Se reescriben al desplegar para no dejar enlaces
|
||||
# rotos (Tailscale es inaccesible para los visitantes).
|
||||
LOCAL_BASE = os.environ.get("FEA_LOCAL_BASE", "https://farmer.taild3aaf6.ts.net/fea")
|
||||
PROD_BASE = os.environ.get("FEA_PROD_BASE", "https://wp-nuevo.feadulta.com")
|
||||
PROD_BASE = os.environ.get("FEA_PROD_BASE", "https://www.feadulta.com")
|
||||
|
||||
|
||||
def log(msg: str) -> None:
|
||||
@@ -76,7 +76,7 @@ def parse_csv_ints(raw: str) -> list[int]:
|
||||
def localize_urls(text: str | None) -> tuple[str, int]:
|
||||
"""Reescribe URLs absolutas local→prod en el contenido antes de subirlo.
|
||||
|
||||
Equivale al search-replace `farmer.taild3aaf6.ts.net/fea` → `wp-nuevo.feadulta.com`
|
||||
Equivale al search-replace `farmer.taild3aaf6.ts.net/fea` → `www.feadulta.com`
|
||||
pero aplicado en origen, así el contenido llega ya correcto a prod (issue #91).
|
||||
Devuelve (texto, nº de reemplazos).
|
||||
"""
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* Uso (local): docker exec wordpress-web php /var/www/html/scripts/... (o vía cwd)
|
||||
* php scripts/translate_lectura_titles.php # dry-run + reporte
|
||||
* APPLY=1 php scripts/translate_lectura_titles.php # aplica
|
||||
* Prod: FEA_WP_LOAD=/web/wp-nuevo/wp-load.php php translate_lectura_titles.php
|
||||
* Prod: FEA_WP_LOAD=/web/wp-load.php php translate_lectura_titles.php
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE);
|
||||
|
||||
+12
-8
@@ -1,8 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Orquestador nocturno: locuta cartas ES del gap con MiniMax (voz Nico), una a
|
||||
una, repartido en el tiempo. Reanudable (meta fea_audio_done) y con freno ante
|
||||
la cuota (para tras N fallos seguidos). NO toca el front; solo genera el mp3 y
|
||||
asocia la URL al post (meta fea_audio_url).
|
||||
"""Orquestador nocturno: locuta cartas ES del gap con MiniMax, una a una,
|
||||
repartido en el tiempo. Voz por defecto Nico; los artículos de autores con voz
|
||||
clonada (AUTHOR_VOICES en minimax_tts.py, ej. Fray Marcos) usan la suya propia.
|
||||
Reanudable (meta fea_audio_done) y con freno ante la cuota (para tras N fallos
|
||||
seguidos). NO toca el front; solo genera el mp3 y asocia la URL al post (meta
|
||||
fea_audio_url).
|
||||
|
||||
Lanzar: nohup ~/tts-local/xtts-venv/bin/python scripts/tts_produce.py > /tmp/feadulta-tts-prod.out 2>&1 &
|
||||
Log: /tmp/feadulta-tts-prod.log
|
||||
@@ -78,7 +80,7 @@ def main():
|
||||
i += 1
|
||||
continue
|
||||
try:
|
||||
title, text = mm.get_post_text(pid)
|
||||
title, text, author = mm.get_post_text(pid)
|
||||
except Exception as e: # noqa: BLE001
|
||||
log(f"#{pid}: error leyendo ({e}); skip")
|
||||
php("setflag", str(pid), "fea_audio_skip", "1")
|
||||
@@ -90,15 +92,17 @@ def main():
|
||||
i += 1
|
||||
continue
|
||||
|
||||
rc = mm.t2a(mm.add_pauses(text), VOICE, MODEL, f"prod-{pid}")
|
||||
voice = mm.voice_for_author(author, VOICE)
|
||||
rc = mm.t2a(mm.add_pauses(text), voice, MODEL, f"prod-{pid}")
|
||||
if rc == 0:
|
||||
src = mm.OUT / f"prod-{pid}.mp3"
|
||||
dst = PROD / f"{pid}.mp3"
|
||||
shutil.move(str(src), str(dst))
|
||||
php("setaudio", str(pid), f"/wp-content/uploads/tts/{pid}.mp3")
|
||||
php("setaudio", str(pid), f"/wp-content/uploads/tts/{pid}.mp3", voice)
|
||||
ok += 1
|
||||
consec = 0
|
||||
log(f"#{pid} OK «{title[:45]}» → tts/{pid}.mp3 (total {ok})")
|
||||
voice_tag = f" [{voice}]" if voice != VOICE else ""
|
||||
log(f"#{pid} OK «{title[:45]}»{voice_tag} → tts/{pid}.mp3 (total {ok})")
|
||||
i += 1
|
||||
time.sleep(INTERVAL)
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
/**
|
||||
* verify_carta_lang_links.php
|
||||
*
|
||||
* QA de una carta traducida: para cada idioma (en/fr/it/pt), revisa TODOS los
|
||||
* enlaces internos de feadulta.com y comprueba que apuntan a contenido en ese
|
||||
* mismo idioma (prefijo /en/, /fr/, /it/, /pt/ y slug correcto).
|
||||
*
|
||||
* Detecta dos tipos de fallo:
|
||||
* - SIN_TRADUCIR: el link apunta al artículo en español porque nunca se
|
||||
* tradujo/creó su equivalente en ese idioma.
|
||||
* - MISMATCH: el link tiene prefijo de idioma pero el slug no es el
|
||||
* correcto (p.ej. suffix -2 divergente entre local y prod), y resuelve a
|
||||
* contenido de OTRO idioma o a nada.
|
||||
*
|
||||
* Solo reporta, no modifica nada (a diferencia de repoint_carta_links.php).
|
||||
*
|
||||
* Uso:
|
||||
* FEA_WP_LOAD=/web/wp-load.php CARTA=54254 php verify_carta_lang_links.php
|
||||
* CARTA=54254 php verify_carta_lang_links.php (usa /var/www/html/wp-load.php, para Docker local)
|
||||
*/
|
||||
|
||||
require getenv('FEA_WP_LOAD') ?: '/var/www/html/wp-load.php';
|
||||
|
||||
$CARTA = (int)(getenv('CARTA') ?: 0);
|
||||
if (!$CARTA) {
|
||||
fwrite(STDERR, "Falta CARTA=<id del post en español>\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$LANGS = ['en', 'fr', 'it', 'pt'];
|
||||
|
||||
function resolve_post_from_href($href) {
|
||||
if (preg_match('~[?&]p=(\d+)~', $href, $m)) {
|
||||
return (int)$m[1];
|
||||
}
|
||||
$path = preg_replace('~^https?://[^/]+~i', '', $href);
|
||||
$path = preg_replace('~[?#].*$~', '', $path);
|
||||
$path = preg_replace('~^/fea~', '', $path);
|
||||
$path = preg_replace('~^/(en|fr|it|pt|es)(/|$)~', '/', $path);
|
||||
$segs = array_values(array_filter(explode('/', $path), 'strlen'));
|
||||
if (count($segs) !== 1) {
|
||||
return 0; // categorías, home, rutas multi-segmento: no aplica
|
||||
}
|
||||
$p = get_page_by_path($segs[0], OBJECT, ['post', 'page']);
|
||||
return $p ? $p->ID : 0;
|
||||
}
|
||||
|
||||
function href_lang_prefix($href) {
|
||||
$path = preg_replace('~^https?://[^/]+~i', '', $href);
|
||||
$path = preg_replace('~^/fea~', '', $path);
|
||||
if (preg_match('~^/(en|fr|it|pt)(/|$)~', $path, $m)) {
|
||||
return $m[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$translations = pll_get_post_translations($CARTA);
|
||||
if (empty($translations)) {
|
||||
fwrite(STDERR, "El post $CARTA no tiene grupo de traducciones Polylang.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "Verificando enlaces de la carta ES=$CARTA\n";
|
||||
echo "Traducciones: " . json_encode($translations) . "\n\n";
|
||||
|
||||
$total_sin_traducir = 0;
|
||||
$total_mismatch = 0;
|
||||
$total_rotos = 0;
|
||||
|
||||
foreach ($LANGS as $lang) {
|
||||
if (empty($translations[$lang])) {
|
||||
echo "[$lang] SIN TRADUCCIÓN DE LA CARTA (no existe post en este idioma)\n\n";
|
||||
continue;
|
||||
}
|
||||
$pid = $translations[$lang];
|
||||
$post = get_post($pid);
|
||||
if (!$post) {
|
||||
echo "[$lang] post $pid no encontrado\n\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Captura cualquier href absoluto http(s) — funciona tanto en local (dominio
|
||||
// Tailscale) como en prod (www.feadulta.com); resolve_post_from_href() ya
|
||||
// descarta lo que no resuelva a un post interno (externos, mailto, etc.).
|
||||
preg_match_all('~href="(https?://[^"]+)"~i', $post->post_content, $m);
|
||||
$hrefs = array_values(array_unique($m[1]));
|
||||
|
||||
$issues = [];
|
||||
foreach ($hrefs as $href) {
|
||||
$target_id = resolve_post_from_href($href);
|
||||
if (!$target_id) {
|
||||
continue; // home, categorías, rutas no resolubles a un post concreto
|
||||
}
|
||||
$target_lang = pll_get_post_language($target_id) ?: 'es';
|
||||
$prefix = href_lang_prefix($href);
|
||||
|
||||
if ($target_lang === $lang && $prefix === $lang) {
|
||||
continue; // todo correcto
|
||||
}
|
||||
|
||||
// Averiguar el ES de origen del contenido apuntado, para buscar la traducción correcta
|
||||
if ($target_lang === 'es') {
|
||||
$es_id = $target_id;
|
||||
} else {
|
||||
$tr = pll_get_post_translations($target_id);
|
||||
$es_id = $tr['es'] ?? null;
|
||||
}
|
||||
|
||||
$correct_id = $es_id ? pll_get_post($es_id, $lang) : null;
|
||||
|
||||
if ($correct_id && $correct_id != $target_id) {
|
||||
$correct_url = get_permalink($correct_id);
|
||||
$issues[] = [
|
||||
'tipo' => 'MISMATCH',
|
||||
'detalle' => "href=\"$href\" resuelve a post $target_id (lang=$target_lang) — correcto: $correct_url (post $correct_id)",
|
||||
];
|
||||
$total_mismatch++;
|
||||
} elseif ($correct_id && $correct_id == $target_id && $prefix !== $lang) {
|
||||
// resuelve al post correcto pero con URL/slug distinta a la actual (permalink cambió)
|
||||
$correct_url = get_permalink($correct_id);
|
||||
$issues[] = [
|
||||
'tipo' => 'MISMATCH',
|
||||
'detalle' => "href=\"$href\" no coincide con el permalink actual: $correct_url (post $correct_id)",
|
||||
];
|
||||
$total_mismatch++;
|
||||
} elseif (!$correct_id && $target_lang !== $lang) {
|
||||
$issues[] = [
|
||||
'tipo' => 'SIN_TRADUCIR',
|
||||
'detalle' => "href=\"$href\" -> post $target_id (lang=$target_lang) sin traducción en '$lang'",
|
||||
];
|
||||
$total_sin_traducir++;
|
||||
}
|
||||
}
|
||||
|
||||
echo "[$lang] post $pid — " . count($hrefs) . " enlaces internos, " . count($issues) . " con problemas\n";
|
||||
foreach ($issues as $issue) {
|
||||
echo " {$issue['tipo']}: {$issue['detalle']}\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
echo "RESUMEN: $total_sin_traducir sin traducir, $total_mismatch mismatch de idioma/slug\n";
|
||||
exit(($total_sin_traducir + $total_mismatch + $total_rotos) > 0 ? 2 : 0);
|
||||
Reference in New Issue
Block a user