diff --git a/.gitignore b/.gitignore index 967c352..53b1940 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,10 @@ joomla-php83/ backups/* !backups/README.md +# Backups pesados (GBs) bajo docs/ — mismo motivo que backups/*, ruta distinta +docs/backups/* +!docs/backups/README.md + # Capturas de pantalla capturas/ diff --git a/docs/releases/enrique-2026-07-25-prod-dry-run.md b/docs/releases/enrique-2026-07-25-prod-dry-run.md new file mode 100644 index 0000000..2aab853 --- /dev/null +++ b/docs/releases/enrique-2026-07-25-prod-dry-run.md @@ -0,0 +1,71 @@ +# Release dry-run — Enrique Martínez Lozano / carta «Hacia el corazón» + +**Estado:** preparado localmente; **no ejecutado en producción**. + +## Validación local completada + +- Comentario ES: `#55041` — `El tesoro está ya en nosotros` + - Categorías: `Comentarios al evangelio` + `Feadulta`. + - Audio: `tts/55041.mp3`, 2,221,101 bytes, voz `NicoFeadulta2026`. + - QA Haiku: aprobado tras correcciones objetivas EN/FR/IT. +- Traducciones del comentario: EN `#55047`, FR `#55048`, IT `#55049`, PT `#55050`. +- Carta local ES: `#55042` — `Hacia el corazón` (slug técnico de preview conservado). + - Traducciones: EN `#55051`, FR `#55052`, IT `#55053`, PT `#55054`. + - QA Haiku: contenido aprobado tras correcciones; 24 enlaces con destino traducido se repuntaron. Los 12 enlaces externos que quedan son fallback ES: no existe post destino traducido en local (verificado por `post_title` exacto y Polylang). +- Render comprobado en Tailscale: + - Comentario ES: título correcto, categorías correctas y reproductor HTML5 visible. + - Carta FR: título correcto; Enrique aparece tras Fray Marcos y antes de Pagola. + +## Evidencia de producción (sólo lectura server-side) + +- Carta real: `#54914`, título `Hacia el corazón`, estado `publish`. +- Hash actual de contenido prod: `5e7eb1b880bfc6311edb78ac8b6e81639f5b4502282a1b182c610b1d01308b69`. +- Hash candidato local: `6fdb4929fcb5b49b7f2dc1deb7c6f5be70983af866d44c5d5c27d2866c09418c`. +- La carta candidata lleva `fea_phase_a_source_prod_id=54914`; por tanto debe **actualizar #54914**, no crearse una carta nueva. + +## Dry-runs que pasaron + +```bash +# Crea solamente el grupo nuevo del comentario (ES + EN/FR/IT/PT): 5 posts draft +python3 scripts/sync_translations_to_prod.py --ids 55041 --dry-run + +# Sube y asocia solamente el MP3 de Enrique +python3 scripts/sync_audio_to_prod.py --ids 55041 --dry-run +``` + +Ambos devolvieron `ok/error=1/0` (audio) y un grupo Polylang de cinco posts (comentario). + +## Guardarraíl crítico + +**NO ejecutar:** + +```bash +python3 scripts/sync_translations_to_prod.py --ids 55041,55042 +``` + +El dry-run demostró que clonaría `#55042` como una segunda carta ES. No actualiza la carta real `#54914`. + +## Release que debe ejecutar Rafa (producción) + +1. Backup de producción y snapshot server-side de `#54914`. +2. Crear el grupo nuevo del comentario desde `#55041` (ES + EN/FR/IT/PT), inicialmente en `draft`; anotar los IDs reales de producción. +3. Subir `tts/55041.mp3` y asociarlo al ID ES real creado en producción. +4. Actualizar **solamente** título/contenido de la carta real `#54914` con el candidato local `#55042`; mantener título `Hacia el corazón`, autor, fecha y estado existentes. +5. Crear EN/FR/IT/PT de la carta contra el origen real `#54914` y enlazarlas en Polylang. No clonar otra ES. +6. Releer por servidor los IDs creados y `#54914`; comprobar: + - Enrique sólo está en `Evangelio y comentarios al Evangelio`, tras Fray Marcos y antes de Pagola. + - No aparece en `Artículos seleccionados`. + - Cada carta enlaza a Enrique en su mismo idioma. + - El reproductor del ES carga `tts/55041.mp3`. +7. Sólo tras esas lecturas, promover los drafts de producción a `publish`. + +## Rollback + +- Restaurar el backup de producción previo. +- Alternativamente, restaurar el contenido/hash previo de `#54914` y despublicar el nuevo grupo + audio de Enrique. + +## Backups locales relacionados + +- `docs/backups/enrique-translations-20260725T004921Z/` +- `docs/backups/enrique-carta-fr-final-20260725T012649Z/` +- `docs/backups/enrique-pre-tts-20260725T013021Z/` diff --git a/import_avatars_62.php b/import_avatars_62.php new file mode 100644 index 0000000..ebee76a --- /dev/null +++ b/import_avatars_62.php @@ -0,0 +1,50 @@ + dry run (no escribe nada). + */ +require_once ABSPATH . 'wp-admin/includes/image.php'; + +$apply = getenv('APPLY') === '1'; +$mfile = getenv('MANIFEST') ?: 'manifest_62.json'; +$updir = wp_get_upload_dir(); +$manifest = json_decode(file_get_contents($updir['basedir'] . '/avatares/' . $mfile), true); + +$done = $skip = $err = 0; +foreach ($manifest as $row) { + list($uid, $name, $orig, $kind, $old_attach) = $row; + if (!in_array($kind, ['initials','trim','logo','qs'], true)) { $skip++; continue; } + + $rel = "avatares/autores/autor-{$uid}.png"; + $abs = $updir['basedir'] . '/' . $rel; + if (!file_exists($abs)) { echo "MISSING file uid=$uid\n"; $err++; continue; } + + // si foto_perfil ya apunta a este fichero, solo se ha sobrescrito el PNG: no crear attachment nuevo + $cur = (int) get_user_meta($uid, 'foto_perfil', true); + if ($cur && get_post_meta($cur, '_wp_attached_file', true) === $rel) { + if ($apply) wp_update_attachment_metadata($cur, wp_generate_attachment_metadata($cur, $abs)); + $skip++; continue; + } + + if (!$apply) { $done++; continue; } + + // guardar foto_perfil antiguo una sola vez (idempotente) + if (get_user_meta($uid, '_foto_perfil_pre62', true) === '') { + update_user_meta($uid, '_foto_perfil_pre62', $old_attach); + } + + $attach = [ + 'post_mime_type' => 'image/png', + 'post_title' => "Avatar {$name}", + 'post_status' => 'inherit', + 'guid' => $updir['baseurl'] . '/' . $rel, + ]; + $aid = wp_insert_attachment($attach, $abs, 0, true); + if (is_wp_error($aid)) { echo "ERR insert uid=$uid: ".$aid->get_error_message()."\n"; $err++; continue; } + wp_update_attachment_metadata($aid, wp_generate_attachment_metadata($aid, $abs)); + update_user_meta($uid, 'foto_perfil', $aid); + $done++; +} +echo ($apply ? "APLICADO" : "DRY-RUN") . ": procesados=$done omitidos=$skip errores=$err\n"; diff --git a/imports/carta738/pagola-human/9r_2_eng_Pagola.docx b/imports/carta738/pagola-human/9r_2_eng_Pagola.docx new file mode 100644 index 0000000..c91111d Binary files /dev/null and b/imports/carta738/pagola-human/9r_2_eng_Pagola.docx differ diff --git a/imports/carta738/pagola-human/9r_3_fr_Pagola.docx b/imports/carta738/pagola-human/9r_3_fr_Pagola.docx new file mode 100644 index 0000000..768af91 Binary files /dev/null and b/imports/carta738/pagola-human/9r_3_fr_Pagola.docx differ diff --git a/imports/carta738/pagola-human/9r_4_it_Pagola.docx b/imports/carta738/pagola-human/9r_4_it_Pagola.docx new file mode 100644 index 0000000..9124354 Binary files /dev/null and b/imports/carta738/pagola-human/9r_4_it_Pagola.docx differ diff --git a/imports/carta738/pagola-human/9r_5_pt_Pagola.docx b/imports/carta738/pagola-human/9r_5_pt_Pagola.docx new file mode 100644 index 0000000..d5fee9a Binary files /dev/null and b/imports/carta738/pagola-human/9r_5_pt_Pagola.docx differ diff --git a/manifest_62.json b/manifest_62.json new file mode 100644 index 0000000..4e3fa19 --- /dev/null +++ b/manifest_62.json @@ -0,0 +1 @@ +[["382", "Fray Marcos", "autores/382_avatar.png", "skip", "42395"], ["383", "José Antonio Pagola", "autores/383_avatar.png", "skip", "42396"], ["384", "Enrique Martínez Lozano", "autores/384_avatar.png", "skip", "42397"], ["385", "José Enrique Galarreta", "autores/385_avatar.png", "skip", "42398"], ["386", "José Arregi", "autores/386_avatar.png", "skip", "42399"], ["387", "Eloy Roy", "autores/387_avatar.png", "skip", "42400"], ["388", "Dolores Aleixandre", "autores/388_avatar.png", "skip", "42401"], ["389", "Vicente Martínez", "autores/389_avatar.png", "skip", "42402"], ["390", "Sandra Hojman", "autores/390_avatar.png", "skip", "42403"], ["391", "Julián Mellado", "autores/391_avatar.png", "skip", "42404"], ["392", "Matilde Gastalver", "autores/392_avatar.png", "skip", "42405"], ["393", "Koldo Aldai", "autores/393_avatar.png", "skip", "42406"], ["394", "Marta Salazar", "autores/394_avatar.png", "skip", "42407"], ["395", "Florentino Ulibarri", "autores/395_avatar.png", "skip", "42408"], ["396", "Rafael Calvo Beca", "autores/396_avatar.png", "skip", "42409"], ["405", "Faustino Vilabrille", "autores/405_avatar.png", "skip", "42410"], ["407", "Víctor Daniel Blanco", "autores/407_avatar.png", "skip", "42411"], ["423", "Mari Patxi Ayerra", "autores/423_avatar.png", "skip", "42412"], ["468", "José Sánchez Luque", "autores/468_avatar.png", "skip", "42413"], ["746", "Vicky Irigaray", "autores/746_avatar.png", "skip", "42414"], ["774", "José Luis Sicre", "autores/774_avatar.png", "skip", "42415"], ["842", "Yolanda Chávez", "autores/842_avatar.png", "skip", "42416"], ["948", "Inma Calvo Torrejón", "autores/948_avatar.png", "skip", "42417"], ["1048", "Inma Calvo", "autores/1048_avatar.png", "skip", "42418"], ["1010", "Ínigo García Blanco", "autores/1010_avatar.png", "skip", "42419"], ["775", "Miguel A. Munárriz Casajús", "quienes_somos/avatars/col_munarriz.png", "skip", "42599"], ["1129", "Rosario Ramos", "quienes_somos/avatars/col_rosario_ramos.png", "skip", "42600"], ["583", "Fidel Aizpurúa", "quienes_somos/avatars/col_fidel_aizpurua.png", "skip", "42601"], ["890", "Fe Adulta", "2026/02/cropped-logo_fa.jpg", "skip", "43901"], ["1", "Fe Adulta", "2026/02/cropped-logo_fa.jpg", "skip", "43901"], ["1540", "Ediciones Feadulta", "2026/02/cropped-logo_fa.jpg", "skip", "43901"], ["411", "Gabriel Mª Otalora", "autores/joomla/col_otalora.png", "trim", "44250"], ["472", "Leonardo Boff", "autores/joomla/col_boff.png", "trim", "44251"], ["436", "José María Castillo", "autores/joomla/col_castillo.png", "trim", "44252"], ["429", "Jesús Bastante", "autores/joomla/col_bastante.png", "trim", "44253"], ["863", "Magdalena Bennasar", "autores/joomla/col_bennasar.png", "trim", "44254"], ["464", "Juan José Tamayo", "autores/joomla/col_tamayo.png", "trim", "44255"], ["658", "Gonzalo Haya", "autores/joomla/col_haya.png", "trim", "44256"], ["937", "Juan Zapatero", "autores/joomla/col_zapatero.png", "initials", "44257"], ["521", "Miguel Ángel Mesa", "autores/joomla/col_mesa.png", "trim", "44258"], ["481", "José Ignacio González Faus", "autores/joomla/col_faus.png", "trim", "44259"], ["485", "Xabier Pikaza", "autores/joomla/col_pikaza.png", "trim", "44260"], ["1260", "José Lorenzo", "autores/joomla/col_lorenzo.png", "initials", "44261"], ["954", "Marifé Ramos González", "autores/joomla/col_marife.png", "trim", "44262"], ["917", "Antonio Aradillas", "autores/joomla/col_aradillas.png", "initials", "44263"], ["508", "Juan Masiá", "autores/joomla/col_masia.png", "trim", "44264"], ["861", "Carmen Notario", "autores/joomla/col_notario.png", "trim", "44265"], ["1253", "Mª Luisa Paret García", "autores/joomla/col_paret.png", "trim", "44266"], ["1138", "Guadalupe Labrador", "autores/joomla/col_labrador.png", "trim", "44267"], ["993", "África de la Cruz Tomé", "autores/joomla/col_cruz-tome.png", "trim", "44268"], ["1031", "Paula Depalma", "autores/joomla/col_depalma.png", "trim", "44269"], ["988", "Pepa Torres", "autores/joomla/col_pepa_torres.png", "trim", "44270"], ["1004", "Carmiña Navia Velasco", "autores/joomla/col_navia.png", "initials", "44271"], ["1055", "Leandro Sequeiros", "autores/joomla/col_sequeiros.png", "initials", "44272"], ["811", "Luis Miguel Modino", "autores/joomla/col_modino.png", "trim", "44273"], ["1383", "Jesús Lozano Pino", "autores/joomla/col_pino.png", "initials", "44274"], ["1256", "Jaume Patuel Puig", "autores/joomla/col_patuel.png", "initials", "44275"], ["1029", "Jesús Martínez Gordo", "autores/joomla/col_jesus-mart.png", "initials", "44276"], ["1058", "Consuelo Vélez", "autores/joomla/col_velez.png", "initials", "44277"], ["1145", "Ramón Hernández Martín", "autores/joomla/col_ramonhnz.png", "initials", "44278"], ["501", "Pedro M. Lamet", "autores/joomla/col_lamet.png", "trim", "44279"], ["816", "Varios autores", "autores/joomla/col_varios-autores.png", "initials", "44280"], ["522", "Carlos F. Barberá", "autores/joomla/col_barbera.png", "trim", "44281"], ["724", "Rogelio Cárdenas", "autores/joomla/col_cardenas.png", "trim", "44282"], ["519", "Frei Betto", "autores/joomla/col_betto.png", "trim", "44283"], ["483", "Benjamín Forcano", "autores/joomla/col_forcano.png", "trim", "44284"], ["761", "Fernando Bermúdez López", "autores/joomla/col_bermudez.png", "trim", "44285"], ["794", "José María Alvarez", "autores/joomla/col_jmalvarez.png", "trim", "44286"], ["781", "Santiago Agrelo", "autores/joomla/col_agrelo.png", "trim", "44287"], ["982", "Cameron Doody", "autores/joomla/col_doody.png", "initials", "44288"], ["1362", "Guillermo Jesús Kowalski", "autores/joomla/col_kowalski.png", "initials", "44289"], ["1288", "Joan Planellas", "autores/joomla/col_planellas.png", "initials", "44290"], ["1157", "José Antonio Vázquez Mosquera", "autores/joomla/col_javazquez.png", "initials", "44291"], ["1212", "Mariano Delgado", "autores/joomla/col_mar_delgado.png", "initials", "44292"], ["1473", "Joseba Kamiruaga Mieza", "autores/joomla/col_joseba.png", "initials", "44293"], ["416", "Luís García Orso", "autores/joomla/col_orso.png", "trim", "44294"], ["631", "Evaristo Villar", "autores/joomla/col_evvillar.png", "trim", "44295"], ["432", "Juan Antonio Estrada", "autores/joomla/col_estrada.png", "trim", "44296"], ["703", "José María García-Mauriño", "autores/joomla/col_maurino.png", "trim", "44297"], ["821", "Jesús Espeja", "autores/joomla/col_espeja.png", "trim", "44298"], ["1289", "Víctor Martell", "autores/joomla/col_martell.png", "initials", "44299"], ["403", "Andrés Torres Queiruga", "autores/joomla/col_queiruga.png", "trim", "44300"], ["445", "Susana Merino", "autores/joomla/col_merino.png", "trim", "44301"], ["489", "Ima Sanchís", "autores/joomla/col_ima.png", "trim", "44302"], ["507", "Juan G. Bedoya", "autores/joomla/col_bedoya.png", "trim", "44303"], ["1425", "Pedro Miguel Ansó Esarte", "autores/joomla/col_anso.png", "initials", "44304"], ["1544", "Alfonso Núñez Chávez", "autores/joomla/col_alfonso-nunez.png", "initials", "44305"], ["419", "María Teresa Sánchez Carmona", "autores/joomla/col_carmona.png", "trim", "44306"], ["438", "Quiero ver", "autores/joomla/col_quierover.png", "initials", "44307"], ["446", "Martín Gelabert Ballester", "autores/joomla/col_gelabert.png", "trim", "44308"], ["462", "José Ignacio Calleja", "autores/joomla/col_calleja.png", "initials", "44309"], ["853", "Eduardo de la Serna", "autores/joomla/col_serna.png", "trim", "44310"], ["963", "Marta García", "autores/joomla/col_Marta_Garcia.png", "trim", "44311"], ["994", "Pedro Aranda Astudillo", "autores/joomla/col_aranda.png", "initials", "44312"], ["1068", "Tomás Muro", "autores/joomla/col_muro.png", "initials", "44313"], ["1086", "Stefano Cartabia", "autores/joomla/col_cartabia.png", "initials", "44314"], ["1262", "Jordi Pacheco", "autores/joomla/col_pacheco.png", "initials", "44315"], ["520", "Juan Cejudo", "autores/joomla/col_cejudo.png", "initials", "44316"], ["603", "Juan Yzuel", "autores/joomla/col_yzuel.png", "initials", "44317"], ["921", "José Luis Pinilla", "autores/joomla/col_pinilla.png", "initials", "44318"], ["960", "José Luis Ferrando Lada", "autores/joomla/col_ferrando.png", "initials", "44319"], ["1026", "Santiago Villamayor", "autores/joomla/col_villamayor.png", "initials", "44320"], ["592", "Félix Placer Ugarte", "autores/joomla/col_placer.png", "initials", "44321"], ["905", "Rufo González", "autores/joomla/col_rufo.png", "initials", "44322"], ["942", "Carmen Herrero", "autores/joomla/col_cmherrero.png", "initials", "44323"], ["1079", "César Caro", "autores/joomla/col_caro.png", "initials", "44324"], ["1107", "Juan de Burgos Román", "autores/joomla/col_burgos.png", "initials", "44325"], ["1115", "Antonio Spadaro", "autores/joomla/col_spadaro.png", "initials", "44326"], ["1223", "Julián Bedoya Cardona", "autores/joomla/col_julian-bedoya.png", "initials", "44327"], ["461", "Juan José Aguirre", "autores/joomla/col_aguirre.png", "trim", "44328"], ["728", "Pablo Ordaz", "autores/joomla/col_ordaz.png", "trim", "44329"], ["757", "Andrea Tornielli", "autores/joomla/col_andrea.png", "trim", "44330"], ["848", "Carolina Abarca", "autores/joomla/col_abarca.png", "trim", "44331"], ["1051", "José Carlos Enríquez", "autores/joomla/col_enriquez.png", "initials", "44332"], ["1287", "Jairo Alberto Franco Uribe", "autores/joomla/col_franco-uribe.png", "initials", "44333"], ["1326", "Benedetta Capelli", "autores/joomla/col_capelli.png", "initials", "44334"], ["510", "Felipe Arizmendi Esquivel", "autores/joomla/col_esquivel.png", "trim", "44335"], ["523", "Mariá Corbí", "autores/joomla/col_corbi.png", "trim", "44336"], ["898", "Deme Orte", "autores/joomla/col_orte.png", "initials", "44337"], ["1092", "Sor Lucía Caram", "autores/joomla/col_caram.png", "initials", "44338"], ["1257", "Vicente Luis García Corres", "autores/joomla/col_corres.png", "initials", "44339"], ["1335", "Paola Calderón", "autores/joomla/col_calderon.png", "initials", "44340"], ["1562", "Rafael Narbona", "autores/joomla/col_narbona.png", "initials", "44341"], ["435", "Javier Melloni", "autores/joomla/col_melloni.png", "trim", "44342"], ["476", "Patricia Paz", "autores/joomla/col_paz.png", "initials", "44343"], ["722", "John P. Meier", "autores/joomla/col_meier.png", "trim", "44344"], ["849", "Peio Sánchez Rodríguez", "autores/joomla/col_peio.png", "trim", "44345"], ["888", "Josep Miquel Bausset", "autores/joomla/col_bausset.png", "initials", "44346"], ["923", "Bernardo Pérez Andreu", "autores/joomla/col_perez_andreu.png", "initials", "44347"], ["974", "Javier Elzo", "autores/joomla/col_elzo.png", "initials", "44348"], ["1274", "Hermann Rodríguez Osorio", "autores/joomla/col_hermann.png", "initials", "44349"], ["1286", "Marco Enrique Salas Laure", "autores/joomla/col_salas-laure.png", "initials", "44350"], ["1358", "Víctor Ricardo Moreno Holguín", "autores/joomla/col_victor-r-moreno.png", "initials", "44351"], ["1409", "Pedro Lanzas", "autores/joomla/col_lanzas.png", "initials", "44352"], ["1431", "Jean Charles Putzolu", "autores/joomla/col_putzolu.png", "initials", "44353"], ["1516", "José Vicente Martínez", "autores/joomla/col_jose-vicente.png", "initials", "44354"], ["1520", "Fray Jesús Díaz Sariego", "autores/joomla/col_sariego.png", "initials", "44355"], ["1547", "José Antonio Satué", "autores/joomla/col_satue.png", "initials", "44356"], ["404", "José Luís Vilanova Alonso", "autores/joomla/col_vilanova.png", "trim", "44357"], ["433", "Rafael Díaz Arias", "autores/joomla/col_arias.png", "trim", "44358"], ["487", "Tomás Maza Ruiz", "autores/joomla/col_maza.png", "trim", "44359"], ["516", "Ricardo Gayol", "autores/joomla/col_gayol.png", "initials", "44360"], ["709", "Patxi Loidi", "autores/joomla/col_loidi.png", "initials", "44361"], ["721", "Nicolás Castellanos Franco", "autores/joomla/col_castellanos.png", "trim", "44362"], ["759", "Carlos Ayala Ramírez", "autores/joomla/col_ayalaramirez.png", "trim", "44363"], ["841", "Francisco Bautista", "autores/joomla/col_bautista.png", "trim", "44364"], ["970", "José Antonio Revuelta", "autores/joomla/col_jarevuelta.png", "initials", "44365"], ["1091", "Antonio Moreno de la Fuente", "autores/joomla/col_moreno-fuente.png", "initials", "44366"], ["1105", "Fernando Jiménez", "autores/joomla/col_fer-jim.png", "initials", "44367"], ["1122", "Victorino Pérez Prieto", "autores/joomla/col_vict-perez.png", "initials", "44368"], ["1163", "José Rafael Ruz Villamil", "autores/joomla/col_ruz.png", "initials", "44369"], ["1173", "Pablo Heras Alonso", "autores/joomla/col_heras.png", "initials", "44370"], ["1240", "Felisa Elizondo", "autores/joomla/col_elizondo.png", "initials", "44371"], ["1301", "José Miguel Martínez Castelló", "autores/joomla/col_castello.png", "initials", "44372"], ["1311", "Juan Cuatrecasas Asua", "autores/joomla/col_cuatrecasas.png", "initials", "44373"], ["1467", "Xavier Pete", "autores/joomla/col_pete.png", "initials", "44374"], ["1560", "Adele Howard", "autores/joomla/col_adele-howard.png", "initials", "44375"], ["1561", "Bruno Desideria", "autores/joomla/col_desideria.png", "initials", "44376"], ["424", "LCWR", "autores/joomla/col_lcwr.png", "initials", "44377"], ["655", "Antonio Duato", "autores/joomla/col_duato.png", "trim", "44378"], ["690", "Esteban Tabares", "autores/joomla/col_tabares.png", "initials", "44379"], ["782", "Mercedes Nasarre", "autores/joomla/col_nasarre.png", "initials", "44380"], ["889", "José Mª Rivas Conde", "autores/joomla/col_rivas_conde.png", "initials", "44381"], ["892", "Antonio Gil de Zúñiga", "autores/joomla/col_gil_zuniga.png", "initials", "44382"], ["908", "Juan Jauregui", "autores/joomla/col_jnjauregui.png", "initials", "44383"], ["914", "José Manuel Bernal", "autores/joomla/col_bernal.png", "initials", "44384"], ["941", "Pedro Zabala", "autores/joomla/col_zabala.png", "initials", "44385"], ["953", "Álvaro de Juana", "autores/joomla/col_juana.png", "initials", "44386"], ["976", "Lucía López Alonso", "autores/joomla/col_lopez_alonso.png", "initials", "44387"], ["986", "Cristina Plaza", "autores/joomla/col_plaza_cris.png", "initials", "44388"], ["995", "Juan José Omella", "autores/joomla/col_omella.png", "initials", "44389"], ["1205", "Michael P. Moore", "autores/joomla/col_moore.png", "initials", "44390"], ["1228", "Ángel Gutiérrez Sanz", "autores/joomla/col_angel-gutierrez.png", "initials", "44391"], ["1247", "Jesús Herrero E.", "autores/joomla/col_jesus_herrero.png", "initials", "44392"], ["1251", "José Carlos Bermejo", "autores/joomla/col_bermejo.png", "initials", "44393"], ["1306", "Amelia Hidalgo Jiménez", "autores/joomla/col_amelia_hidalgo.png", "initials", "44394"], ["1315", "Toño Casado", "autores/joomla/col_casado.png", "initials", "44395"], ["1390", "Francisca Abad Martín", "autores/joomla/col_abad.png", "initials", "44396"], ["1417", "Alicia Torres", "autores/joomla/col_alicia_torres.png", "initials", "44397"], ["1471", "Mario J. Paredes", "autores/joomla/col_paredes.png", "initials", "44398"], ["415", "Asun Gutiérrez", "autores/joomla/col_asun.png", "trim", "44399"], ["477", "Pilar Rahola", "autores/joomla/col_rahola.png", "initials", "44400"], ["514", "Eduardo Galeano", "autores/joomla/col_galeano.png", "trim", "44401"], ["618", "Víctor M. Amela", "autores/joomla/col_amela.png", "initials", "44402"], ["680", "Pope Godoy", "autores/joomla/col_pope.png", "trim", "44403"], ["716", "Ángeles Caso", "autores/joomla/col_caso.png", "trim", "44404"], ["719", "Luís Carlos Saiz Fernández", "autores/joomla/col_saiz.png", "trim", "44405"], ["727", "Ken Wilber", "autores/joomla/col_wilber.png", "trim", "44406"], ["804", "John Shelby Spong", "autores/joomla/col_spong.png", "trim", "44407"], ["825", "José Ignacio Spuche", "autores/joomla/col_spuche.png", "trim", "44408"], ["859", "Darío Menor", "autores/joomla/col_menor.png", "initials", "44409"], ["916", "Lucía Gayón", "autores/joomla/col_gayon.png", "initials", "44410"], ["927", "Pablo D'Ors", "autores/joomla/col_dors.png", "initials", "44411"], ["938", "Luis Ángel Aguilar", "autores/joomla/col_aguilar.png", "initials", "44412"], ["968", "Jeff Foster", "autores/joomla/col_foster.png", "initials", "44413"], ["1063", "Beto Vargas", "autores/joomla/col_vargas.png", "initials", "44414"], ["1064", "Nicolás Pons", "autores/joomla/col_pons.png", "initials", "44415"], ["1099", "Salvador Santos", "autores/joomla/col_salvsantos.png", "initials", "44416"], ["1113", "Anna Seguí", "autores/joomla/col_segui.png", "initials", "44417"], ["1123", "Javier Sánchez", "autores/joomla/col_javi_sanchez.png", "initials", "44418"], ["1139", "Yolanda Salamanca Pinilla", "autores/joomla/col_salamanca.png", "initials", "44419"], ["1158", "César Rollán Sánchez", "autores/joomla/col_rollan.png", "initials", "44420"], ["1176", "Andrés Felipe Rojas Saavedra", "autores/joomla/col_rojas.png", "initials", "44421"], ["1186", "Jesús Peláez", "autores/joomla/col_pelaez.png", "initials", "44422"], ["1217", "Jesús Sastre", "autores/joomla/col_sastre.png", "initials", "44423"], ["1220", "Julio Puente López", "autores/joomla/col_puente.png", "initials", "44424"], ["1239", "Antoni Ferret", "autores/joomla/col_ferret.png", "initials", "44425"], ["1313", "Alberto Ares", "autores/joomla/col_ares.png", "initials", "44426"], ["1332", "Jesús Donaire", "autores/joomla/col_donaire.png", "initials", "44427"], ["1348", "Ángel Aznárez", "autores/joomla/col_aznarez.png", "initials", "44428"], ["1376", "José Mª Rojo G.", "autores/joomla/col_rojo.png", "initials", "44429"], ["1395", "Julio Guerrero Moreno", "autores/joomla/col_guerrero.png", "initials", "44430"], ["1412", "José María Vigil", "autores/joomla/col_jm_vigil.png", "initials", "44431"], ["1415", "Rodrigo Guerra López", "autores/joomla/col_rodri_guerra.png", "initials", "44432"], ["1423", "Ana Bou", "autores/joomla/col_bou.png", "initials", "44433"], ["1440", "Macario Ofilada Mina", "autores/joomla/col_ofilada.png", "initials", "44434"], ["1458", "José Melero Pérez", "autores/joomla/col_melero.png", "initials", "44435"], ["1459", "ADN Celam", "autores/joomla/col_adncelam.png", "logo", "44436"], ["1462", "Josep Vicent Martínez", "autores/joomla/col_josep_vicent.png", "initials", "44437"], ["1475", "José Chamorro", "autores/joomla/col_chamorro.png", "initials", "44438"], ["1495", "Luz Marina Medina", "autores/joomla/col_luz-marina.png", "initials", "44439"], ["1509", "Luz Estela (Lucha) Castro", "autores/joomla/col_estela.png", "initials", "44440"], ["1511", "Avelino Seco Muñoz", "autores/joomla/col_avelino.png", "initials", "44441"], ["1529", "Manuel Fraijó", "autores/joomla/col_fraijo.png", "initials", "44442"], ["402", "José Luís Caravias", "autores/joomla/col_caravias.png", "trim", "44443"], ["420", "Salvador Capote", "autores/joomla/col_capote.png", "initials", "44444"], ["554", "Celso Alcaina", "autores/joomla/col_alcaina.png", "initials", "44445"], ["556", "Esteban Velázquez", "autores/joomla/col_velazquez.png", "trim", "44446"], ["597", "Félix Azurmendi", "autores/joomla/col_azurmendi.png", "initials", "44447"], ["608", "José Antonio Marina", "autores/joomla/col_marina.png", "trim", "44448"], ["675", "Ignacio Ramonet", "autores/joomla/col_ramonet.png", "initials", "44449"], ["713", "Vicente Simón", "autores/joomla/col_simon.png", "trim", "44450"], ["742", "Marciano Vidal", "autores/joomla/col_marciano.png", "trim", "44451"], ["824", "Joan Chittister", "autores/joomla/col_chittister.png", "trim", "44452"], ["873", "Manuel Ansede", "autores/joomla/col_ansede.png", "initials", "44453"], ["875", "César Cervera", "autores/joomla/col_cervera.png", "initials", "44454"], ["882", "Antonio Esquivias", "autores/joomla/col_esquivias.png", "initials", "44455"], ["883", "Francisco Margallo", "autores/joomla/col_margallo.png", "initials", "44456"], ["891", "Kuczynski", "autores/joomla/col_kuczynski.png", "initials", "44457"], ["922", "José Aldunate", "autores/joomla/col_aldunate.png", "initials", "44458"], ["945", "Fernando Torres Pérez", "autores/joomla/col_torres-perez.png", "initials", "44459"], ["946", "Ignacio Villota Elejalde", "autores/joomla/col_villota.png", "initials", "44460"], ["949", "Blanca Arregui", "autores/joomla/col_arregui_blanca.png", "initials", "44461"], ["955", "Pablo Moñino", "autores/joomla/col_monino.png", "initials", "44462"], ["957", "Martín Pérez Millán", "autores/joomla/col_millan.png", "initials", "44463"], ["958", "Francisco Rafael de Pascual", "autores/joomla/col_depascual.png", "initials", "44464"], ["959", "José Olivero", "autores/joomla/col_olivero.png", "trim", "44465"], ["961", "Juan P. Espinosa Arce", "autores/joomla/col_espinosa.png", "initials", "44466"], ["964", "Maria López Vigil", "autores/joomla/col_maria_vigil.png", "initials", "44467"], ["977", "Olga Larrazábal", "autores/joomla/col_larrazabal.png", "initials", "44468"], ["978", "Mónica Cavallé", "autores/joomla/col_cavalle.png", "initials", "44469"], ["983", "Francisco Massó", "autores/joomla/col_masso.png", "initials", "44470"], ["989", "Honorio Cadarso", "autores/joomla/col_cadarso.png", "initials", "44471"], ["1001", "Jaime Escobar", "autores/joomla/col_escobar.png", "initials", "44472"], ["1006", "Gilberto Urrutia", "autores/joomla/col_urrutia.png", "initials", "44473"], ["1030", "Jaime Rubio Hancock", "autores/joomla/col_rubio-hanc.png", "initials", "44474"], ["1037", "Eduardo Hornaert", "autores/joomla/col_hornaert.png", "initials", "44475"], ["1043", "Guillermo Oroz Aragón", "autores/joomla/col_oroz.png", "initials", "44476"], ["1076", "Pepe Mallo", "autores/joomla/col_mallo.png", "initials", "44477"], ["1080", "Manuel Mandianes", "autores/joomla/col_mandianes.png", "initials", "44478"], ["1088", "Grupo cristiano de Reflexión y Acción", "autores/joomla/col_reflex.png", "initials", "44479"], ["1126", "Luis Romasanta", "autores/joomla/col_romasanta.png", "initials", "44480"], ["1128", "Juan Alemany", "autores/joomla/col_alemany.png", "initials", "44481"], ["1133", "Juan Ignacio Cortés", "autores/joomla/col_jicortes.png", "initials", "44482"], ["1147", "Abraham Canales", "autores/joomla/col_canales.png", "initials", "44483"], ["1148", "José Carlos Rodríguez", "autores/joomla/col_jcrodri.png", "initials", "44484"], ["1150", "Yuval Noah Harari", "autores/joomla/col_yuval.png", "initials", "44485"], ["1151", "Yaiza Martínez", "autores/joomla/col_yaiza.png", "initials", "44486"], ["1154", "Pedro Langa", "autores/joomla/col_langa.png", "initials", "44487"], ["1156", "Manuel Cubías", "autores/joomla/col_cubias.png", "initials", "44488"], ["1166", "Sara Lumbreras", "autores/joomla/col_lumbreras.png", "initials", "44489"], ["1180", "Fernando Vidal", "autores/joomla/col_vidal_fer.png", "initials", "44490"], ["1221", "Baltasar Bueno", "autores/joomla/col_baltasar.png", "initials", "44491"], ["1222", "Renato Martínez", "autores/joomla/col_renato.png", "initials", "44492"], ["1252", "Julia Serrano Fernández", "autores/joomla/col_julia-serrano.png", "initials", "44493"], ["1261", "Manuel de Jesús", "autores/joomla/col_de-jesus.png", "initials", "44494"], ["1268", "José Luis Sánchez", "autores/joomla/col_jl-sanchez.png", "initials", "44495"], ["1269", "Manlio Fabio Altamirano", "autores/joomla/col_manlio.png", "initials", "44496"], ["1273", "Emilce Cuda", "autores/joomla/col_emilce.png", "initials", "44497"], ["1281", "Bernardo Baldeón", "autores/joomla/col_baldeon.png", "initials", "44498"], ["1310", "Antonio Piñero", "autores/joomla/col_pinero.png", "initials", "44499"], ["1323", "Grupo laico de Algorta", "autores/joomla/col_algorta.png", "initials", "44500"], ["1327", "Alberto Roselli", "autores/joomla/col_roselli.png", "initials", "44501"], ["1338", "Gabriel Cifermann", "autores/joomla/col_cifermann.png", "initials", "44502"], ["1342", "Brenda García", "autores/joomla/col_brenda.png", "initials", "44503"], ["1365", "Salvatore Cernuzio", "autores/joomla/col_cernuzio.png", "initials", "44504"], ["1392", "Aníbal Pastor N.", "autores/joomla/col_pastor.png", "initials", "44505"], ["1416", "Linda Bordoni", "autores/joomla/col_bordoni.png", "initials", "44506"], ["1424", "Luis Manuel Romero Sánchez", "autores/joomla/col_lmromero.png", "initials", "44507"], ["1434", "Michele Raviart", "autores/joomla/col_raviart.png", "initials", "44508"], ["1445", "Luis Van de Velde", "autores/joomla/col_velde.png", "initials", "44509"], ["1453", "Xiskya Valladares", "autores/joomla/col_valladares.png", "initials", "44510"], ["1461", "Fray Alfredo Quintero Campoy", "autores/joomla/col_fray-alfredo.png", "initials", "44511"], ["1463", "Ángel Morillo", "autores/joomla/col_morillo.png", "initials", "44512"], ["1464", "Sebastián Cózar Gavira", "autores/joomla/col_cozar.png", "initials", "44513"], ["1474", "Mariano Álvarez Valenzuela", "autores/joomla/col_mariano-alvarez.png", "initials", "44514"], ["1479", "Ricardo Mauti", "autores/joomla/col_mauti.png", "initials", "44515"], ["1487", "Alessandro Gisotti", "autores/joomla/col_gisotti.png", "initials", "44516"], ["1497", "Virginia Bonard", "autores/joomla/col_bonard.png", "initials", "44517"], ["1526", "Florencio Roselló", "autores/joomla/col_rosello.png", "initials", "44518"], ["1557", "Reinaldo Nann", "autores/joomla/col_nann.png", "initials", "44519"], ["401", "Emma Torralba", "autores/joomla/col_torralba.png", "initials", "44520"], ["437", "José Miguel Izquierdo", "autores/joomla/col_izquierdo.png", "trim", "44521"], ["558", "Carlo Maria Martini", "autores/joomla/col_martini.png", "trim", "44522"], ["559", "Luís Cano", "autores/joomla/col_cano.png", "initials", "44523"], ["560", "Pere Codina Mas", "autores/joomla/col_codina.png", "trim", "44524"], ["712", "Ángel Arnaiz Quintana", "autores/joomla/col_arnaiz.png", "trim", "44525"], ["723", "Pablo Jáuregui", "autores/joomla/col_jauregui.png", "trim", "44526"], ["726", "Vicenc Alujas", "autores/joomla/col_alujas.png", "trim", "44527"], ["734", "Enric González", "autores/joomla/col_enric.png", "trim", "44528"], ["735", "Sergio Bergman", "autores/joomla/col_sergio.png", "trim", "44529"], ["736", "Eileen Markey", "autores/joomla/col_eilen.png", "trim", "44530"], ["738", "José Rodríguez Carballo", "autores/joomla/col_carballo.png", "trim", "44531"], ["739", "José María Arnaiz", "autores/joomla/col_jmarnaiz.png", "trim", "44532"], ["745", "Ángeles Conde", "autores/joomla/col_conde.png", "trim", "44533"], ["750", "Francisco Asensi", "autores/joomla/col_asensi.png", "trim", "44534"], ["751", "Francisco Ayala", "autores/joomla/col_ayala.png", "trim", "44535"], ["752", "Alfredo Leuco", "autores/joomla/col_leuco.png", "trim", "44536"], ["753", "Jesús Hidalgo", "autores/joomla/col_hidalgo.png", "trim", "44537"], ["754", "Fernando Lázaro", "autores/joomla/col_lazaro.png", "trim", "44538"], ["758", "Ismael Bárcenas Orozco", "autores/joomla/col_barcenas.png", "trim", "44539"], ["762", "Salvador Sostres", "autores/joomla/col_sostres.png", "trim", "44540"], ["763", "Javier Marín", "autores/joomla/col_marin.png", "trim", "44541"], ["776", "Carmen Almansa", "autores/joomla/col_almansa.png", "trim", "44542"], ["783", "Adolfo Ranero", "autores/joomla/col_ranero.png", "initials", "44543"], ["789", "Rafael Argullol", "autores/joomla/col_argullol.png", "trim", "44544"], ["797", "Susana Hidalgo", "autores/joomla/col_sushidalgo.png", "trim", "44545"], ["805", "Philippe Ariño", "autores/joomla/col_arino.png", "trim", "44546"], ["806", "Muhammad Yunus", "autores/joomla/col_yunus.png", "trim", "44547"], ["819", "Mads Gilbert", "autores/joomla/col_mads.png", "trim", "44548"], ["820", "Amanda Céspedes", "autores/joomla/col_cespedes.png", "trim", "44549"], ["823", "Padre Ángel García", "autores/joomla/col_padre-angel.png", "trim", "44550"], ["826", "Daniel Jarreta", "autores/joomla/col_jarreta.png", "trim", "44551"], ["828", "Francisco Puértolas", "autores/joomla/col_puertolas.png", "trim", "44552"], ["829", "Desmond Tutu", "autores/joomla/col_tutu.png", "trim", "44553"], ["830", "Anniken Jørgensen", "autores/joomla/col_anniken.png", "trim", "44554"], ["840", "José Mujica", "autores/joomla/col_mujica.png", "trim", "44555"], ["850", "Jorge Serrano", "autores/joomla/col_serrano.png", "trim", "44556"], ["852", "Andrés Aberasturi", "autores/joomla/col_aberasturi.png", "trim", "44557"], ["857", "Nani Vall-llossera", "autores/joomla/col_nani.png", "trim", "44558"], ["858", "Nestor Nongo", "autores/joomla/col_nongo.png", "initials", "44559"], ["860", "Ricardo Blázquez", "autores/joomla/col_blazquez.png", "trim", "44560"], ["862", "Juan José Arnáiz", "autores/joomla/col_jjarnaiz.png", "trim", "44561"], ["864", "Javier Montserrat", "autores/joomla/col_montserrat.png", "initials", "44562"], ["865", "Henrique Mariño", "autores/joomla/col_marino.png", "initials", "44563"], ["866", "Mª Carmen Nieto León", "autores/joomla/col_nieto_leon.png", "initials", "44564"], ["869", "Jesús Díaz Gómez", "autores/joomla/col_diaz_gomez.png", "initials", "44565"], ["871", "Pepe Baena", "autores/joomla/col_baena.png", "initials", "44566"], ["872", "Clara Janés", "autores/joomla/col_janes.png", "initials", "44567"], ["874", "Cristina Fontenele", "autores/joomla/col_fontenele.png", "initials", "44568"], ["879", "José Alegre", "autores/joomla/col_alegre.png", "initials", "44569"], ["886", "Claude Benoit", "autores/joomla/col_benoit.png", "initials", "44570"], ["887", "F. Brune", "autores/joomla/col_brune.png", "initials", "44571"], ["894", "Gabriel García de Oro", "autores/joomla/col_garcia_oro.png", "initials", "44572"], ["895", "Ángel García-Zamorano", "autores/joomla/col_zamorano.png", "initials", "44573"], ["896", "Mariana Caplan", "autores/joomla/col_caplan.png", "initials", "44574"], ["897", "Willigis Jäger", "autores/joomla/col_jager.png", "trim", "44575"], ["899", "Carmen Charo", "autores/joomla/col_charo.png", "trim", "44576"], ["900", "Lluís Bassets", "autores/joomla/col_bassets.png", "initials", "44577"], ["902", "María Pérez", "autores/joomla/col_perez.png", "initials", "44578"], ["903", "Carlos Sánchez Mato", "autores/joomla/col_mato.png", "initials", "44579"], ["904", "Juan Manuel de Prada", "autores/joomla/col_prada.png", "initials", "44580"], ["906", "Emiliano Calle Moreno", "autores/joomla/col_calle.png", "initials", "44581"], ["907", "Juan Cruz", "autores/joomla/col_cruz.png", "initials", "44582"], ["909", "Roberto Alifano", "autores/joomla/col_alifano.png", "initials", "44583"], ["910", "Tere Cortés", "autores/joomla/col_cortes.png", "initials", "44584"], ["915", "José Agustín Cabré", "autores/joomla/col_cabre.png", "initials", "44585"], ["918", "Bernard Trémel", "autores/joomla/col_tremel.png", "initials", "44586"], ["919", "Daniel Gomis", "autores/joomla/col_gomis.png", "initials", "44587"], ["920", "Javier Salas", "autores/joomla/col_salas.png", "initials", "44588"], ["925", "J. V. Echagüe", "autores/joomla/col_echague.png", "initials", "44589"], ["928", "Walter Kasper", "autores/joomla/col_kasper.png", "initials", "44590"], ["931", "Pedro Kalmbach", "autores/joomla/col_kalmbach.png", "initials", "44591"], ["934", "Eduardo Arens", "autores/joomla/col_arens.png", "initials", "44592"], ["935", "José María Toro", "autores/joomla/col_toro.png", "initials", "44593"], ["936", "Nacho Dueñas", "autores/joomla/col_duenas.png", "initials", "44594"], ["943", "David Jou", "autores/joomla/col_jou.png", "trim", "44595"], ["944", "José de Segovia Barrón", "autores/joomla/col_segovia.png", "initials", "44596"], ["947", "Raúl Juárez", "autores/joomla/col_juarez.png", "initials", "44597"], ["950", "Xavier Guix", "autores/joomla/col_guix.png", "initials", "44598"], ["951", "Francesc Mateu i Hosta", "autores/joomla/col_mateu.png", "initials", "44599"], ["952", "Jorge Alcalde", "autores/joomla/col_alcalde.png", "initials", "44600"], ["956", "Samir Khalil Samir", "autores/joomla/col_samir.png", "initials", "44601"], ["965", "Adolfo Nicolás", "autores/joomla/col_adolfo.png", "initials", "44602"], ["971", "Bárbara Torres", "autores/joomla/col_barbara_t.png", "initials", "44603"], ["972", "José Manuel Caamaño", "autores/joomla/col_caamano.png", "initials", "44604"], ["973", "Raquel Simón Justo", "autores/joomla/col_simon-justo.png", "initials", "44605"], ["975", "Gabriel Asenjo", "autores/joomla/col_asenjo.png", "initials", "44606"], ["980", "Carmen Bellver", "autores/joomla/col_bellver.png", "initials", "44607"], ["984", "Fabián García", "autores/joomla/col_fab-garcia.png", "initials", "44608"], ["985", "Natalia Martín Cantero", "autores/joomla/col_martin_cantero.png", "initials", "44609"], ["987", "Román Espadas", "autores/joomla/col_espadas.png", "initials", "44610"], ["990", "Sor María de Gracia", "autores/joomla/col_sor-maria-gracia.png", "initials", "44611"], ["991", "Javier Velasco Arias", "autores/joomla/col_velasco-arias.png", "initials", "44612"], ["992", "Fran Otero", "autores/joomla/col_otero.png", "initials", "44613"], ["996", "Elena Cué", "autores/joomla/col_cue.png", "initials", "44614"], ["998", "Tomás Trigo", "autores/joomla/col_trigo.png", "initials", "44615"], ["999", "Isidoro García Gómez", "autores/joomla/col_isidoro.png", "initials", "44616"], ["1000", "Roberto Savio", "autores/joomla/col_savio.png", "initials", "44617"], ["1002", "Eduard Punset", "autores/joomla/col_punset.png", "initials", "44618"], ["1003", "Rupert Spira", "autores/joomla/col_spira.png", "initials", "44619"], ["1005", "María Mikhailova", "autores/joomla/col_mikhailova.png", "initials", "44620"], ["1007", "Laura Serrano-Conde", "autores/joomla/col_serrano-conde.png", "initials", "44621"], ["1008", "José Luis Perera López", "autores/joomla/col_perera.png", "initials", "44622"], ["1009", "Javier Lozano", "autores/joomla/col_javier_lozano.png", "initials", "44623"], ["1011", "Xavier Casanovas", "autores/joomla/col_casanovas.png", "initials", "44624"], ["1012", "Vicente Gallego", "autores/joomla/col_gallego.png", "initials", "44625"], ["1013", "Santi Torres", "autores/joomla/col_santi_torres.png", "initials", "44626"], ["1014", "José Cristo Rey", "autores/joomla/col_cristo-rey.png", "initials", "44627"], ["1015", "Óscar Mateos", "autores/joomla/col_mateos.png", "initials", "44628"], ["1016", "Vicente Jiménez Zamora", "autores/joomla/col_vic_jimenez.png", "initials", "44629"], ["1017", "Carlos López", "autores/joomla/col_carlos_lopez.png", "initials", "44630"], ["1018", "Victoria Juliani", "autores/joomla/col_juliani.png", "initials", "44631"], ["1019", "Fernando Trías de Bes", "autores/joomla/col_triasdebes.png", "initials", "44632"], ["1020", "Aurelio Lepe", "autores/joomla/col_lepe.png", "initials", "44633"], ["1021", "Miryam Castro", "autores/joomla/col_castro_mir.png", "initials", "44634"], ["1022", "Ángel Vilariño", "autores/joomla/col_vilarino.png", "initials", "44635"], ["1023", "Christoph Strack", "autores/joomla/col_strack.png", "initials", "44636"], ["1024", "Comunidad de Nazaret", "autores/joomla/col_nazaret.png", "initials", "44637"], ["1025", "Augusto Zamora", "autores/joomla/col_zamora.png", "initials", "44638"], ["1027", "Carles Capdevila", "autores/joomla/col_capdevila.png", "initials", "44639"], ["1028", "George R. Porta", "autores/joomla/col_porta.png", "initials", "44640"], ["1032", "Ana Cabirta", "autores/joomla/col_cabirta.png", "initials", "44641"], ["1034", "J. Jesús Lemus", "autores/joomla/col_lemus.png", "initials", "44642"], ["1035", "Enrique Delgado", "autores/joomla/col_delgado.png", "initials", "44643"], ["1038", "Cristina Sánchez", "autores/joomla/col_cris_sanchez.png", "initials", "44644"], ["1039", "Asunción Poudereux", "autores/joomla/col_poudereux.png", "initials", "44645"], ["1040", "M. Arrizabalaga", "autores/joomla/col_arrizabalaga.png", "initials", "44646"], ["1041", "Josean Villalabeitia", "autores/joomla/col_villalabeitia.png", "initials", "44647"], ["1042", "Joan Carrera", "autores/joomla/col_carrera.png", "initials", "44648"], ["1045", "Irene Meca Mateo", "autores/joomla/col_meca.png", "initials", "44649"], ["1046", "Juan José Mateo", "autores/joomla/col_jjmateo.png", "initials", "44650"], ["1050", "Miguel d’Escoto", "autores/joomla/col_descoto.png", "trim", "44651"], ["1052", "Alfredo Sepúlveda", "autores/joomla/col_sepulveda.png", "initials", "44652"], ["1053", "Jesús Sanz", "autores/joomla/col_sanz.png", "initials", "44653"], ["1056", "Gustavo Valls", "autores/joomla/col_valls.png", "initials", "44654"], ["1057", "Ricardo Benjumea", "autores/joomla/col_benjumea.png", "initials", "44655"], ["1059", "Eubilio Rodríguez Aguado", "autores/joomla/col_eubilio.png", "initials", "44656"], ["1060", "Alejandro Córdoba", "autores/joomla/col_cordoba.png", "initials", "44657"], ["1061", "Dr. Norberto Levy", "autores/joomla/col_levy.png", "initials", "44658"], ["1062", "Santiago Panizo", "autores/joomla/col_panizo.png", "initials", "44659"], ["1066", "Giancarlo Pani", "autores/joomla/col_pani.png", "initials", "44660"], ["1067", "Mussa’ab Bashir", "autores/joomla/col_bashir.png", "initials", "44661"], ["1071", "Nabil Antaki", "autores/joomla/col_nabil.png", "initials", "44662"], ["1074", "Iosu Perales", "autores/joomla/col_perales.png", "initials", "44663"], ["1077", "Carmen Barba Pérez", "autores/joomla/col_barba.png", "trim", "44664"], ["1078", "Roberto José Castillo Telles", "autores/joomla/col_rob_castillo.png", "initials", "44665"], ["1081", "Julio A. Ruiz", "autores/joomla/col_aruiz.png", "initials", "44666"], ["1082", "Alex Roig", "autores/joomla/col_roig.png", "initials", "44667"], ["1083", "Meijer y Geesink", "autores/joomla/col_mei.png", "initials", "44668"], ["1084", "Fernando Fuentes", "autores/joomla/col_fuentes.png", "initials", "44669"], ["1085", "Felipe Portales", "autores/joomla/col_portales.png", "initials", "44670"], ["1089", "Jean Klein", "autores/joomla/col_klein.png", "initials", "44671"], ["1090", "Camilo Barrionuevo", "autores/joomla/col_barrionuevo.png", "initials", "44672"], ["1094", "Gregorio Fernández", "autores/joomla/col_greg-fer.png", "initials", "44673"], ["1095", "Olegario González de Cardedal", "autores/joomla/col_olegario.png", "initials", "44674"], ["1096", "Andrés Opazo", "autores/joomla/col_opazo.png", "initials", "44675"], ["1097", "Luis Badilla", "autores/joomla/col_badilla.png", "initials", "44676"], ["1098", "Lucía Ramón Carbonell", "autores/joomla/col_carbonell.png", "initials", "44677"], ["1100", "Manuel Vicent", "autores/joomla/col_manvicent.png", "initials", "44678"], ["1101", "Karla Huerta", "autores/joomla/col_huerta.png", "initials", "44679"], ["1102", "Celso Morga", "autores/joomla/col_morga.png", "initials", "44680"], ["1104", "Carlos E. Cué", "autores/joomla/col_carlos-cue.png", "initials", "44681"], ["1106", "Fernando García de Cortázar", "autores/joomla/col_fer-gar.png", "initials", "44682"], ["1108", "Javier Barros del Villar", "autores/joomla/col_barros-villar.png", "initials", "44683"], ["1110", "Alex Zanotelli", "autores/joomla/col_zanotelli.png", "initials", "44684"], ["1111", "Ángela Cañal", "autores/joomla/col_canal.png", "initials", "44685"], ["1114", "José Manuel Barreda", "autores/joomla/col_barreda.png", "initials", "44686"], ["1116", "Raquel Muñoz", "autores/joomla/col_raquel_munoz.png", "initials", "44687"], ["1118", "Andrés A. Fernández", "autores/joomla/col_andres_fer.png", "initials", "44688"], ["1119", "Ramón Alario", "autores/joomla/col_alario.png", "initials", "44689"], ["1120", "Miren Etxezarreta", "autores/joomla/col_miren.png", "initials", "44690"], ["1121", "Mary E. Hunt", "autores/joomla/col_hunt.png", "initials", "44691"], ["1124", "Ricardo Olmedo", "autores/joomla/col_olmedo.png", "initials", "44692"], ["1127", "Claudio Ibáñez", "autores/joomla/col_ibanez.png", "initials", "44693"], ["1131", "Gemma Morató", "autores/joomla/col_morato.png", "initials", "44694"], ["1132", "Lourdes Fernández Manzano", "autores/joomla/col_lourdes.png", "initials", "44695"], ["1135", "José María Rodríguez Olaizola", "autores/joomla/col_jmrodriguez.png", "initials", "44696"], ["1136", "María Landi", "autores/joomla/col_landi.png", "initials", "44697"], ["1137", "Lucetta Scaraffia", "autores/joomla/col_lucetta.png", "initials", "44698"], ["1140", "Salvador García Bardón", "autores/joomla/col_salva_garcia.png", "initials", "44699"], ["1141", "Juan García Caselles", "autores/joomla/col_juan_garcia.png", "initials", "44700"], ["1143", "Lidia Rodríguez", "autores/joomla/col_lidia_rod.png", "initials", "44701"], ["1144", "María Teresa Compte", "autores/joomla/col_compte.png", "initials", "44702"], ["1146", "Jesús Fernández González", "autores/joomla/col_jesusfnz.png", "initials", "44703"], ["1149", "Lluís Amiguet", "autores/joomla/col_amiguet.png", "initials", "44704"], ["1152", "Carmen Bernabé Ubieta", "autores/joomla/col_bernabe.png", "initials", "44705"], ["1153", "Robson Sávio Reis Souza", "autores/joomla/col_robson.png", "initials", "44706"], ["1155", "Juan Carlos Claret", "autores/joomla/col_claret.png", "initials", "44707"], ["1159", "Charlotta Smeds", "autores/joomla/col_smeds.png", "initials", "44708"], ["1160", "Federico Mayor", "autores/joomla/col_mayor.png", "initials", "44709"], ["1161", "Jesús L. Sotillo", "autores/joomla/col_sotillo.png", "initials", "44710"], ["1164", "Susana Oñoro", "autores/joomla/col_onoro.png", "initials", "44711"], ["1165", "Robert Adams", "autores/joomla/col_adams.png", "initials", "44712"], ["1167", "Alfonso Pérez Ranchal", "autores/joomla/col_ranchal.png", "initials", "44713"], ["1168", "Joseph Corcó", "autores/joomla/col_corco.png", "initials", "44714"], ["1169", "Antonio Carrascosa Mendieta", "autores/joomla/col_carrascosa.png", "initials", "44715"], ["1170", "James Alison", "autores/joomla/col_alison.png", "initials", "44716"], ["1171", "Ricardo de Querol", "autores/joomla/col_querol.png", "initials", "44717"], ["1174", "Antonio Mendoza", "autores/joomla/col_ant_mendoza.png", "initials", "44718"], ["1175", "A. Moreno", "autores/joomla/col_amoreno.png", "initials", "44719"], ["1177", "Maricel Mena López", "autores/joomla/col_maricel.png", "initials", "44720"], ["1178", "Alfredo Marcos", "autores/joomla/col_alfredo_marcos.png", "initials", "44721"], ["1179", "Grup Cristià del Dissabte", "autores/joomla/col_dissabte.png", "initials", "44722"], ["1181", "Rafael Aguirre", "autores/joomla/col_aguirre_rafa.png", "initials", "44723"], ["1182", "Carolina Narváez", "autores/joomla/col_narvaez.png", "initials", "44724"], ["1187", "Paloma García", "autores/joomla/col_paloma.png", "initials", "44725"], ["1188", "Adriana Masotti", "autores/joomla/col_masotti.png", "initials", "44726"], ["1189", "Ana Torres Menárguez", "autores/joomla/col_anatorres.png", "initials", "44727"], ["1190", "Pedro Castelao", "autores/joomla/col_castelao.png", "initials", "44728"], ["1191", "Maciej Klein", "autores/joomla/col_maciej.png", "initials", "44729"], ["1192", "Elsa Fernández-Santos", "autores/joomla/col_elsa.png", "initials", "44730"], ["1193", "Amelia Valcárcel", "autores/joomla/col_amelia.png", "initials", "44731"], ["1195", "Techu Baragaño", "autores/joomla/col_techu.png", "initials", "44732"], ["1196", "Nicola Graziani", "autores/joomla/col_graziani.png", "initials", "44733"], ["1197", "Rodrigo Ponce de León", "autores/joomla/col_ponce.png", "initials", "44734"], ["1198", "Hanzel J. Zúñiga Valerio", "autores/joomla/col_hanzel.png", "initials", "44735"], ["1200", "Irene López Alonso", "autores/joomla/col_ire_lopez.png", "initials", "44736"], ["1201", "Máximo García Ruiz", "autores/joomla/col_maximo.png", "initials", "44737"], ["1202", "Federico Piana", "autores/joomla/col_piana.png", "initials", "44738"], ["1204", "José Luis Jiménez", "autores/joomla/col_jljimenez.png", "initials", "44739"], ["1206", "César Henriquez", "autores/joomla/col_cesar_henriquez.png", "initials", "44740"], ["1207", "Benjamín Zorrilla", "autores/joomla/col_zorrilla.png", "initials", "44741"], ["1208", "Íñigo Domínguez", "autores/joomla/col_i_dominguez.png", "initials", "44742"], ["1209", "Alberto Agrazal", "autores/joomla/col_agrazal.png", "initials", "44743"], ["1210", "Félix Badía", "autores/joomla/col_fbadia.png", "initials", "44744"], ["1211", "Raúl Berzosa", "autores/joomla/col_berzosa.png", "initials", "44745"], ["1213", "Carlos Martínez", "autores/joomla/col_carlos_mtnz.png", "initials", "44746"], ["1214", "Grupo Erasmo", "autores/joomla/col_erasmo.png", "initials", "44747"], ["1216", "Massimiliano Menichetti", "autores/joomla/col_menichetti.png", "initials", "44748"], ["1218", "Alan Watts", "autores/joomla/col_alan.png", "initials", "44749"], ["1219", "Ines Schaberger", "autores/joomla/col_schaberger.png", "initials", "44750"], ["1224", "Dumar Espinosa", "autores/joomla/col_dumar.png", "initials", "44751"], ["1225", "Mª del Rosario Gómez Serrano", "autores/joomla/col_rosario-gomez.png", "initials", "44752"], ["1226", "Berta González De Vega", "autores/joomla/col_berta.png", "initials", "44753"], ["1227", "José Luis Rocha", "autores/joomla/col_rocha.png", "initials", "44754"], ["1229", "Christina Moreira Vázquez", "autores/joomla/col_moreira.png", "initials", "44755"], ["1230", "Joseph Moingt", "autores/joomla/col_moingt.png", "initials", "44756"], ["1231", "Comunidad", "autores/joomla/col_accion_solidaria.png", "initials", "44757"], ["1232", "James Clear", "autores/joomla/col_clear.png", "initials", "44758"], ["1233", "Cristina Galafate", "autores/joomla/col_galafate.png", "initials", "44759"], ["1234", "Isabel Corpas de Posada", "autores/joomla/col_corpas.png", "initials", "44760"], ["1235", "Georgina Zerega", "autores/joomla/col_zerega.png", "initials", "44761"], ["1236", "Mireia Rourera", "autores/joomla/col_rourera.png", "initials", "44762"], ["1238", "Magdalena Tsanis", "autores/joomla/col_tsanis.png", "initials", "44763"], ["1241", "Mercedes Arbaiza", "autores/joomla/col_arbaiza.png", "initials", "44764"], ["1242", "Ilia Delio", "autores/joomla/col_delio.png", "initials", "44765"], ["1243", "J.M. Ferreira Cunquero", "autores/joomla/col_ferreira.png", "initials", "44766"], ["1244", "Miguel Muñoz", "autores/joomla/col_munoz.png", "initials", "44767"], ["1245", "Agustín Ortega", "autores/joomla/col_ortega.png", "initials", "44768"], ["1246", "Maribel Serrano", "autores/joomla/col_maribel.png", "initials", "44769"], ["1248", "Meritxell Rigol", "autores/joomla/col_rigol.png", "initials", "44770"], ["1250", "Ventura Puigdomenech", "autores/joomla/col_ventura.png", "initials", "44771"], ["1254", "Miguel Ángel Herrero", "autores/joomla/col_maherrero.png", "initials", "44772"], ["1255", "Francisco Maya", "autores/joomla/col_maya.png", "initials", "44773"], ["1258", "Julio Ciges", "autores/joomla/col_ciges.png", "initials", "44774"], ["1259", "Gloria Sáenz", "autores/joomla/col_saenz.png", "initials", "44775"], ["1263", "Mª del Carmen Navarro García", "autores/joomla/col_carmen_navarro.png", "initials", "44776"], ["1264", "Ignacio de Magdala", "autores/joomla/col_magdala.png", "initials", "44777"], ["1265", "María José Capellín", "autores/joomla/col_capellin.png", "initials", "44778"], ["1266", "Lorena R. de la Torre", "autores/joomla/col_de-la-torre.png", "initials", "44779"], ["1267", "Carmen Rodas Pérez", "autores/joomla/col_carmen-rodas.png", "initials", "44780"], ["1271", "Juan Sánchez Núñez", "autores/joomla/col_juan-sanchez.png", "initials", "44781"], ["1272", "Víctor Hernández Ramírez", "autores/joomla/col_victor-hernandez.png", "initials", "44782"], ["1275", "Corina Mora", "autores/joomla/col_corina.png", "initials", "44783"], ["1277", "Eduardo Martínez de la Fe", "autores/joomla/col_eduardo-martinez.png", "initials", "44784"], ["1279", "Bernardino Zanella", "autores/joomla/col_zanella.png", "initials", "44785"], ["1280", "Javier Sampedro", "autores/joomla/col_sampedro.png", "initials", "44786"], ["1282", "Robert Mickens", "autores/joomla/col_mickens.png", "initials", "44787"], ["1283", "Santiago Gorgas", "autores/joomla/col_gorgas.png", "initials", "44788"], ["1284", "Jaime Richart", "autores/joomla/col_richart.png", "initials", "44789"], ["1285", "María Victoria Olavarrieta", "autores/joomla/col_olavarrieta.png", "initials", "44790"], ["1290", "Josefina (Pinita) Gurdián", "autores/joomla/col_pinita.png", "initials", "44791"], ["1291", "Juan Bosco Monroy Campero", "autores/joomla/col_bosco.png", "initials", "44792"], ["1292", "Javier Recio Huetos", "autores/joomla/col_recio.png", "initials", "44793"], ["1293", "Fanny Magdelaine", "autores/joomla/col_fanny.png", "initials", "44794"], ["1294", "María José Encina Muñoz", "autores/joomla/col_encina.png", "initials", "44795"], ["1295", "Michael Sean Winters", "autores/joomla/col_sean.png", "initials", "44796"], ["1297", "Manuel Rodríguez Díez", "autores/joomla/col_manuel-rodriguez.png", "initials", "44797"], ["1298", "José María Vera", "autores/joomla/col_vera.png", "initials", "44798"], ["1299", "Agustín Moreno Fernández", "autores/joomla/col_agustin_moreno.png", "initials", "44799"], ["1300", "Montse Santolino", "autores/joomla/col_santolino.png", "initials", "44800"], ["1302", "Sergi Marcè Mestres", "autores/joomla/col_marce.png", "initials", "44801"], ["1303", "Silvia Torralba", "autores/joomla/col_silvia-torralba.png", "initials", "44802"], ["1304", "Isabel C. Ruiz Fortes", "autores/joomla/col_fortes.png", "initials", "44803"], ["1305", "Luis Ángel de las Heras", "autores/joomla/col_delasheras.png", "initials", "44804"], ["1307", "Víctor Manuel Fernández", "autores/joomla/col_victor_manuel.png", "initials", "44805"], ["1308", "Pasqualina Curcio", "autores/joomla/col_curcio.png", "initials", "44806"], ["1309", "Marcel Uwineza", "autores/joomla/col_uwineza.png", "initials", "44807"], ["1314", "Renardo Schlegelmilch", "autores/joomla/col_renardo.png", "initials", "44808"], ["1316", "Bernard Ménard", "autores/joomla/col_menard.png", "initials", "44809"], ["1317", "Araceli Ruiz", "autores/joomla/col_araceli.png", "initials", "44810"], ["1318", "Óscar Fortín", "autores/joomla/col_fortin.png", "initials", "44811"], ["1319", "Eugenio Gómez Segura", "autores/joomla/col_eugenio.png", "initials", "44812"], ["1321", "Loup Besmond de Senneville", "autores/joomla/col_loup.png", "initials", "44813"], ["1322", "John Crothers", "autores/joomla/col_crothers.png", "initials", "44814"], ["1325", "Isabella Piro", "autores/joomla/col_piro.png", "initials", "44815"], ["1328", "Alberto Morlanes", "autores/joomla/col_morlanes.png", "initials", "44816"], ["1329", "Ximena Pascutti", "autores/joomla/col_ximena.png", "initials", "44817"], ["1330", "Alba Urrutia", "autores/joomla/col_alba_urrutia.png", "initials", "44818"], ["1331", "Daniel Almagro", "autores/joomla/col_almagro.png", "initials", "44819"], ["1334", "Massimo Faggioli", "autores/joomla/col_faggioli.png", "initials", "44820"], ["1336", "Amedeo Lomonaco", "autores/joomla/col_lomonaco.png", "initials", "44821"], ["1339", "Luis Andrés Henao", "autores/joomla/col_henao.png", "initials", "44822"], ["1340", "Sebastián Sansón Ferrari", "autores/joomla/col_sanson.png", "initials", "44823"], ["1343", "Jesús Riaza", "autores/joomla/col_riaza.png", "initials", "44824"], ["1344", "José Manuel Coviella C.", "autores/joomla/col_coviella.png", "initials", "44825"], ["1345", "Patricio Lynch", "autores/joomla/col_lynch.png", "initials", "44826"], ["1346", "P. Luis Gerardo Moro", "autores/joomla/col_moro.png", "initials", "44827"], ["1347", "Comunidad de aprendizaje “Evangelio en pijama”", "autores/joomla/col_aprendizaje.png", "initials", "44828"], ["1351", "Victòria Molins", "autores/joomla/col_molins.png", "initials", "44829"], ["1352", "Edgar Pérez Ruiz", "autores/joomla/col_edgar_perez.png", "initials", "44830"], ["1353", "Antonella Palermo", "autores/joomla/col_palermo.png", "initials", "44831"], ["1354", "Christopher Wells", "autores/joomla/col_wells.png", "initials", "44832"], ["1355", "Josean Manzanos", "autores/joomla/col_manzanos.png", "initials", "44833"], ["1356", "David E. DeCosse", "autores/joomla/col_decosse.png", "initials", "44834"], ["1357", "Susi Pérez-Bustos Leal", "autores/joomla/col_susi.png", "initials", "44835"], ["1359", "P. Alberto Reyes", "autores/joomla/col_alberto_reyes.png", "initials", "44836"], ["1360", "Charles Taylor", "autores/joomla/col_taylor.png", "initials", "44837"], ["1361", "José María Aguirre Oraa", "autores/joomla/col_jm_aguirre.png", "initials", "44838"], ["1363", "Maarten Boudry", "autores/joomla/col_boudry.png", "initials", "44839"], ["1364", "Teresa Muñoz Guillén", "autores/joomla/col_teresa_munoz.png", "initials", "44840"], ["1366", "Isaac Enrique Pérez", "autores/joomla/col_isaac.png", "initials", "44841"], ["1368", "Marisa Vidal Collazo", "autores/joomla/col_marisa.png", "initials", "44842"], ["1369", "Nazarin Armanian", "autores/joomla/col_nazarin.png", "initials", "44843"], ["1370", "Pierluigi Mele", "autores/joomla/col_pierluigi.png", "initials", "44844"], ["1371", "Adrián J. Taranzano", "autores/joomla/col_taranzano.png", "initials", "44845"], ["1372", "Paqui Puente Durández", "autores/joomla/col_paqui.png", "initials", "44846"], ["1373", "Hna. Michele da Silva", "autores/joomla/col_michele.png", "initials", "44847"], ["1374", "Comunidades de Base de Andalucía", "autores/joomla/col_base-andalucia.png", "initials", "44848"], ["1375", "Charles Mercier", "autores/joomla/col_mercier.png", "initials", "44849"], ["1377", "Alessandro de Carolis", "autores/joomla/col_carolis.png", "initials", "44850"], ["1378", "Michael Morwood", "autores/joomla/col_morwood.png", "initials", "44851"], ["1379", "Luis Fernando González Gaviria", "autores/joomla/col_gaviria.png", "initials", "44852"], ["1380", "María José Amor", "autores/joomla/col_amor.png", "initials", "44853"], ["1382", "Claudia Fanti", "autores/joomla/col_fanti.png", "initials", "44854"], ["1384", "Raphael Rauch", "autores/joomla/col_rauch.png", "initials", "44855"], ["1385", "Kiko Llaneras", "autores/joomla/col_llaneras.png", "initials", "44856"], ["1386", "Miguel Ángel Malavia", "autores/joomla/col_malavia.png", "initials", "44857"], ["1387", "Óscar de Baltodano", "autores/joomla/col_baltodano.png", "initials", "44858"], ["1388", "Roberto Fermín Bertossi", "autores/joomla/col_bertossi.png", "initials", "44859"], ["1391", "Christopher Cordero", "autores/joomla/col_cordero.png", "initials", "44860"], ["1393", "James Martin", "autores/joomla/col_james.png", "initials", "44861"], ["1394", "Cristina Monsegur", "autores/joomla/col_monsegur.png", "initials", "44862"], ["1397", "Natalia Faria", "autores/joomla/col_faria.png", "initials", "44863"], ["1399", "Adela Perez del Viso", "autores/joomla/col_adela.png", "initials", "44864"], ["1401", "Francesco Antonio Grana", "autores/joomla/col_grana.png", "initials", "44865"], ["1404", "Tiziana Campisi", "autores/joomla/col_campisi.png", "initials", "44866"], ["1405", "Elías Pérez Esteban", "autores/joomla/col_elias.png", "initials", "44867"], ["1406", "Enrique de Amo", "autores/joomla/col_de-amo.png", "initials", "44868"], ["1407", "Patricia Ynestroza", "autores/joomla/col_ynestroza.png", "initials", "44869"], ["1410", "Mónica Isabel Quintero", "autores/joomla/col_quintero.png", "initials", "44870"], ["1411", "Luis López-Yarto", "autores/joomla/col_yarto.png", "initials", "44871"], ["1413", "Marco Guerra", "autores/joomla/col_guerra.png", "initials", "44872"], ["1414", "Alessandro Di Bussolo", "autores/joomla/col_bussolo.png", "initials", "44873"], ["1418", "Patricia Noya", "autores/joomla/col_noya.png", "initials", "44874"], ["1419", "María Gómez", "autores/joomla/col_maria_gomez.png", "initials", "44875"], ["1420", "Comunidad de Cristianos de Base de Gijón", "autores/joomla/col_base_gijon.png", "initials", "44876"], ["1422", "Lucía Alegrete", "autores/joomla/col_alegrete.png", "initials", "44877"], ["1426", "Francisco García", "autores/joomla/col_francisco_garcia.png", "initials", "44878"], ["1427", "Yongey Mingyur Rinpoche", "autores/joomla/col_yongey.png", "initials", "44879"], ["1429", "Anselmo Borges", "autores/joomla/col_borges.png", "initials", "44880"], ["1430", "Fernando Chomali", "autores/joomla/col_chomali.png", "initials", "44881"], ["1432", "Antonio Osuna Fernández-Largo", "autores/joomla/col_osuna.png", "initials", "44882"], ["1433", "Guadalupe Romero", "autores/joomla/col_guadalupe.png", "initials", "44883"], ["1435", "Paqui Esteban", "autores/joomla/col_paqui_esteban.png", "initials", "44884"], ["1436", "Manuel Ossa", "autores/joomla/col_ossa.png", "initials", "44885"], ["1437", "Jaime Bustamante Gómez", "autores/joomla/col_jaime_bustamante.png", "initials", "44886"], ["1438", "Ángel Fernández Artime", "autores/joomla/col_angel_fernandez.png", "initials", "44887"], ["1439", "Jesús Rojano", "autores/joomla/col_rojano.png", "initials", "44888"], ["1441", "Gilles Castelnau", "autores/joomla/col_castelnau.png", "initials", "44889"], ["1442", "Elvira Pérez", "autores/joomla/col_elvira.png", "initials", "44890"], ["1443", "Javier Fariñas Martín", "autores/joomla/col_farinas.png", "initials", "44891"], ["1444", "Nazareth Castellanos", "autores/joomla/col_nazareth.png", "initials", "44892"], ["1446", "Enrique Ciro Bianchi", "autores/joomla/col_ciro.png", "initials", "44893"], ["1447", "Eloy Mealla", "autores/joomla/col_mealla.png", "initials", "44894"], ["1449", "Tete Hernández", "autores/joomla/col_tete.png", "initials", "44895"], ["1450", "Volker Türk", "autores/joomla/col_volker.png", "initials", "44896"], ["1454", "Mari Carmen y Fernando Bermúdez", "autores/joomla/col_mcfbermudez.png", "initials", "44897"], ["1455", "Silvio Báez", "autores/joomla/col_baez.png", "initials", "44898"], ["1456", "Pablo Cirujeda", "autores/joomla/col_cirujeda.png", "initials", "44899"], ["1457", "Leandro Gaitán", "autores/joomla/col_gaitan.png", "initials", "44900"], ["1460", "Archicompostela", "autores/joomla/col_archicompostela.jpg", "initials", "44901"], ["1465", "Janet Forbes", "autores/joomla/col_forbes.png", "initials", "44902"], ["1466", "Mateo González Alonso", "autores/joomla/col_mateo-gonzalez.png", "initials", "44903"], ["1468", "José Luis Celada", "autores/joomla/col_celada.png", "initials", "44904"], ["1469", "Joaquín Sánchez", "autores/joomla/col_joaquin-sanchez.png", "initials", "44905"], ["1472", "Rafael Luciani", "autores/joomla/col_luciani.png", "initials", "44906"], ["1476", "Juan Currais Porrúa", "autores/joomla/col_currais.png", "initials", "44907"], ["1477", "Infomadrid / B. Aragoneses", "autores/joomla/col_infomadrid-aragoneses.png", "initials", "44908"], ["1478", "Marie-Lucile Kubacki", "autores/joomla/col_lucile.png", "initials", "44909"], ["1481", "Martín Burgos", "autores/joomla/col_martin-burgos.png", "initials", "44910"], ["1482", "Leopoldina Reis Simões", "autores/joomla/col_reis.png", "initials", "44911"], ["1483", "Joachim Negel", "autores/joomla/col_negel.png", "initials", "44912"], ["1484", "Joyce Meyer", "autores/joomla/col_meyer.png", "initials", "44913"], ["1485", "Francesco Strazzari", "autores/joomla/col_strazzari.png", "initials", "44914"], ["1486", "Douglas Sepulchre", "autores/joomla/col_sepulchre.png", "initials", "44915"], ["1488", "Antonio Zugasti", "autores/joomla/col_zugasti.png", "initials", "44916"], ["1489", "Carlos F. Chamorro", "autores/joomla/col_carlos_chamorro.png", "initials", "44917"], ["1490", "Llucià Pou Sabaté", "autores/joomla/col_pou.png", "initials", "44918"], ["1491", "José María Marín Sevilla", "autores/joomla/col_marin-sevilla.png", "initials", "44919"], ["1492", "Julio Pernús", "autores/joomla/col_pernus.png", "initials", "44920"], ["1493", "Pedro Simón", "autores/joomla/col_pedro-simon.png", "initials", "44921"], ["1498", "Pere Montagut", "autores/joomla/col_montagut.png", "initials", "44922"], ["1499", "Francesca Merlo", "autores/joomla/col_merlo.png", "initials", "44923"], ["1501", "Rose Marie Barandiaran", "autores/joomla/col_barandiaran.png", "initials", "44924"], ["1502", "Blanca Albaida", "autores/joomla/col_albaida.png", "initials", "44925"], ["1504", "Monseñor Oscar Egea", "autores/joomla/col_egea.png", "initials", "44926"], ["1506", "Silvia Cáceres Frisancho", "autores/joomla/col_caceres.png", "initials", "44927"], ["1507", "Roberto Cetera", "autores/joomla/col_cetera.png", "initials", "44928"], ["1508", "Rubén Cruz", "autores/joomla/col_ruben-cruz.png", "initials", "44929"], ["1513", "Viqui Molins", "autores/joomla/col_viqui-molins.png", "initials", "44930"], ["1514", "Francisco Javier Bustillo", "autores/joomla/col_bustillo.png", "initials", "44931"], ["1515", "Santiago Tedeschi", "autores/joomla/col_tedeschi.png", "initials", "44932"], ["1517", "Manolo Navarro", "autores/joomla/col_navarro.png", "initials", "44933"], ["1518", "Vanesa Gourhand", "autores/joomla/col_gourhand.png", "initials", "44934"], ["1519", "Luis Español Bouché", "autores/joomla/col_espanol.png", "initials", "44935"], ["1521", "Gloria Leiva Rivera", "autores/joomla/col_leiva.png", "initials", "44936"], ["1522", "Silvia Rozas Barrero", "autores/joomla/col_rozas.png", "initials", "44937"], ["1523", "Javier Gil Quintana", "autores/joomla/col_gil-quintana.png", "initials", "44938"], ["1524", "Fray Roberto de la Iglesia", "autores/joomla/col_fray-roberto.png", "initials", "44939"], ["1527", "Jaime Flaquer", "autores/joomla/col_flaquer.png", "initials", "44940"], ["1528", "Natalia Velasco", "autores/joomla/col_natalia_velasco.png", "initials", "44941"], ["1530", "Antonio J. Mialdea", "autores/joomla/col_mialdea.png", "initials", "44942"], ["1532", "Gabriela Castellanos Llanos", "autores/joomla/col_gabriela.png", "initials", "44943"], ["1533", "Paloma Pérez Muniáin", "autores/joomla/col_paloma-perez.png", "initials", "44944"], ["1534", "Natalia Chientaroli", "autores/joomla/col_chientaroli.png", "initials", "44945"], ["1535", "Paula Bistagnino", "autores/joomla/col_bistagnino.png", "initials", "44946"], ["1536", "André Luiz Bordignon", "autores/joomla/col_bordignon.png", "initials", "44947"], ["1537", "Ángel Sixto Rossi", "autores/joomla/col_sixto.png", "initials", "44948"], ["1539", "José Bautista", "autores/joomla/col_jose_bautista.png", "initials", "44949"], ["1541", "Robert Ageneau", "autores/joomla/col_ageneau.png", "initials", "44950"], ["1543", "Jonás Fernández", "autores/joomla/col_jonas-fernandez.png", "initials", "44951"], ["1546", "Merche Saiz", "autores/joomla/col_merche.png", "initials", "44952"], ["1550", "Juan Manuel Ribeiro", "autores/joomla/col_ribeiro.png", "initials", "44953"], ["1551", "Roberto Paglialonga", "autores/joomla/col_paglialonga.png", "initials", "44954"], ["1552", "Cesare Baldi", "autores/joomla/col_baldi.png", "initials", "44955"], ["1553", "Miguel Ángel Gullón Pérez", "autores/joomla/col_gullon.png", "initials", "44956"], ["1554", "Susana Acosta", "autores/joomla/col_acosta.png", "initials", "44957"], ["1555", "Faustino Castaño", "autores/joomla/col_faustino-castano.png", "initials", "44958"], ["1558", "Mabel Ruiz Ruiz", "autores/joomla/col_mabel.png", "initials", "44959"], ["1559", "Víctor M. Ruano", "autores/joomla/col_ruano.png", "initials", "44960"], ["466", "Gerardo Villar", "autores/joomla/col_barros-villar.png", "initials", "44683"], ["406", "José Manuel Vidal", "autores/joomla/col_vidal_fer.png", "initials", "44490"], ["474", "Mari Paz López Santos", "autores/joomla/col_bennasar-santos.png", "trim", "44961"], ["569", "Carmen Soto", "autores/joomla/col_Carme_Soto.png", "trim", "44962"], ["979", "Inma Eibe", "autores/joomla/col_cruz_eibe.png", "trim", "44963"], ["801", "Pedro", "autores/joomla/col_pedro-simon.png", "initials", "44921"], ["814", "Reyes", "autores/joomla/col_alberto_reyes.png", "initials", "44836"], ["1142", "Hernán Reyes Alcaide", "autores/joomla/col_hernan_reyes.png", "initials", "44964"], ["470", "Ana Sofía Pérez-Bustamante", "autores/joomla/col_jaime_bustamante.png", "initials", "44886"], ["504", "Emilia Robles", "autores/joomla/col_emi_robles.png", "trim", "44965"], ["1109", "Gregorio Delgado del Río", "autores/joomla/col_delgado-delrio.png", "initials", "44966"], ["555", "Javier Domínguez", "autores/joomla/col_dominguez_pb.png", "initials", "44967"], ["765", "Juan Rubio", "autores/joomla/col_rubio-hanc.png", "initials", "44474"], ["442", "Valerio Gigante", "autores/joomla/col_v_gigante.png", "trim", "44968"], ["544", "Yolanda Plaza Ruíz", "autores/joomla/col_plaza_cris.png", "initials", "44388"], ["578", "Juan Martín Velasco", "autores/joomla/col_natalia_velasco.png", "initials", "44941"], ["585", "Carlos Prieto", "autores/joomla/col_prieto-moreno.png", "initials", "44969"], ["601", "Eusebio Losada", "autores/joomla/col_miguel-s-losada.png", "initials", "44970"], ["664", "A. González", "autores/joomla/col_mateo-gonzalez.png", "initials", "44903"], ["665", "María y José Ignacio López Vigil", "autores/joomla/col_jm_vigil.png", "initials", "44431"], ["682", "Salvador Mendoza", "autores/joomla/col_ant_mendoza.png", "initials", "44718"], ["851", "Jonás", "autores/joomla/col_jonas-fernandez.png", "initials", "44951"], ["1093", "Gonzalo Brito", "autores/joomla/col_brito-gon.png", "initials", "44971"], ["1215", "David López Royo", "autores/joomla/col_lopez_royo.png", "initials", "44972"], ["1494", "Ignacio Álvarez-Ossorio Alvariño", "autores/joomla/col_alvarez-ossorio.png", "initials", "44973"]] \ No newline at end of file diff --git a/mirror-antiguo/deploy/nginx-mirror.conf b/mirror-antiguo/deploy/nginx-mirror.conf index 11d8944..02b5847 100644 --- a/mirror-antiguo/deploy/nginx-mirror.conf +++ b/mirror-antiguo/deploy/nginx-mirror.conf @@ -13,6 +13,21 @@ server { # application/octet-stream y el navegador se los descarga en vez de mostrarlos. default_type text/html; + # URLs históricas de catálogo: el catálogo vigente vive en Ediciones Fe Adulta. + # 302 primero: evita cachear un destino externo de forma irreversible durante el soak. + location ~ ^/(?:es/)?catalogo-de-libros-feadulta(?:/|\.html)?$ { + return 302 https://edicionesfeadulta.com/; + } + + # La vista imprimible Joomla de esta página EFFA no existe en el archivo estático. + # Conserva la página normal del archivo; sólo la variante ?print=1 va al contenido WP vigente. + location = /es/effa/95-secc5cat/3304-seccion5col00.html { + if ($arg_print = 1) { + return 302 https://www.feadulta.com/seccion5col00-2/; + } + try_files $uri $uri.html $uri/index.html =404; + } + location / { try_files $uri $uri.html $uri/index.html =404; } diff --git a/mirror-antiguo/stopped-containers.txt.hecho-20260730T014132Z b/mirror-antiguo/stopped-containers.txt.hecho-20260730T014132Z new file mode 100644 index 0000000..363aa3c --- /dev/null +++ b/mirror-antiguo/stopped-containers.txt.hecho-20260730T014132Z @@ -0,0 +1,10 @@ +portfolio-tracker +yt-api-bridge +wordpress-web +joomla-web-php83 +jellyfin +triptyk-local-wordpress-1 +triptyk-local-db-1 +yt-summaries +joomla-web +wordpress-mysql diff --git a/scripts/cdmon-retire-fewp1.sh b/scripts/cdmon-retire-fewp1.sh new file mode 100755 index 0000000..f0ef809 --- /dev/null +++ b/scripts/cdmon-retire-fewp1.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Retira exclusivamente el esquema vacío fewp1 de CDMON. +# Por defecto solo hace dry-run. La eliminación real exige --apply y el token exacto. +set -euo pipefail + +mode="dry-run" +if [[ "${1:-}" == "--apply" ]]; then + mode="apply" +fi + +if [[ "$mode" == "apply" && "${CONFIRM_DROP_FEWP1:-}" != "DROP_FEWP1" ]]; then + echo "Refusing apply: export CONFIRM_DROP_FEWP1=DROP_FEWP1 first." >&2 + exit 2 +fi + +set -a +source "$HOME/.hermes/profiles/feadulta/.env" +set +a + +php='global $wpdb; +$count=(int)$wpdb->get_var("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=\"fewp1\""); +if ($count !== 0) { fwrite(STDERR, "ABORT: fewp1 has ".$count." tables; it is not empty.\n"); exit(3); } +echo "PASS: fewp1 exists and has 0 tables.\n";' + +if [[ "$mode" == "apply" ]]; then + php+=' $ok=$wpdb->query("DROP DATABASE `fewp1`"); +if ($ok === false) { fwrite(STDERR, "ABORT: DROP DATABASE failed.\n"); exit(4); } +$remaining=(int)$wpdb->get_var("SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name=\"fewp1\""); +if ($remaining !== 0) { fwrite(STDERR, "ABORT: fewp1 remains after DROP.\n"); exit(5); } +echo "APPLIED: fewp1 removed; verified absent.\n";' +else + php+=' echo "DRY-RUN: would execute DROP DATABASE `fewp1`; no production change made.\n";' +fi + +printf -v quoted_php '%q' "$php" +sshpass -p "$FEA_PROD_SSH_PASS" ssh -o StrictHostKeyChecking=accept-new "$FEA_PROD_SSH_HOST" \ + "cd /web && wp eval $quoted_php" diff --git a/scripts/fea_translate_helper.php b/scripts/fea_translate_helper.php index dd35ec3..b12b1ed 100644 --- a/scripts/fea_translate_helper.php +++ b/scripts/fea_translate_helper.php @@ -262,6 +262,47 @@ switch ($cmd) { break; } + case 'clone_new': { + // Clona en un ID local NUEVO. Se usa al importar desde prod cuando el + // mismo ID ya está ocupado por contenido local distinto. + $lang = (string) ($argv[2] ?? ''); + $status = (string) ($argv[3] ?? ''); + if ($lang === '') { + fwrite(STDERR, "uso: clone_new \n"); exit(6); + } + $payload = json_decode(file_get_contents('php://stdin'), true); + if (!is_array($payload) || empty($payload['title'])) { + fwrite(STDERR, "payload inválido por stdin\n"); exit(4); + } + $postarr = [ + 'post_title' => wp_slash($payload['title']), + 'post_content' => wp_slash($payload['content'] ?? ''), + 'post_excerpt' => wp_slash($payload['excerpt'] ?? ''), + 'post_status' => $status ?: ($payload['status'] ?? 'draft'), + 'post_type' => $payload['type'] ?? 'post', + 'post_author' => (int) ($payload['author'] ?? 1), + 'post_date' => $payload['date'] ?? current_time('mysql'), + 'post_date_gmt'=> $payload['date_gmt'] ?? current_time('mysql', true), + 'post_name' => $payload['slug'] ?? '', + 'to_ping' => '', + 'pinged' => '', + ]; + $new_id = wp_insert_post($postarr, true); + if (is_wp_error($new_id)) { fwrite(STDERR, $new_id->get_error_message() . "\n"); exit(5); } + pll_set_post_language($new_id, $lang); + $cats = []; + foreach ((array) ($payload['cat_slugs'] ?? []) as $slug) { + $term = get_term_by('slug', (string) $slug, 'category'); + if ($term && !is_wp_error($term)) $cats[] = (int) $term->term_id; + } + if (!$cats) $cats = array_values(array_unique(array_map('intval', (array) ($payload['cats'] ?? [])))); + wp_set_post_categories($new_id, $cats); + set_meta_payload($new_id, normalize_meta_input($payload)); + clean_post_cache($new_id); + echo $new_id; + break; + } + case 'carta_sections': { // Descubre el cluster de una carta parseando sus propios enlaces internos // (misma lógica que pinta la portada, fea-carta-portada.php). No depende diff --git a/scripts/import_human_pagola_translations.py b/scripts/import_human_pagola_translations.py new file mode 100644 index 0000000..0481892 --- /dev/null +++ b/scripts/import_human_pagola_translations.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Importa las traducciones editoriales humanas adjuntas a un issue de carta. + +Por defecto solo valida y muestra el plan. --apply-local crea borradores en el +WordPress Docker local; nunca publica ni toca producción. +""" +from __future__ import annotations + +import argparse +import hashlib +import html +import json +import re +import subprocess +import tempfile +import zipfile +from pathlib import Path +from xml.etree import ElementTree as ET + +ROOT = Path(__file__).resolve().parent +HELPER = ROOT / "fea_translate_helper.php" +CONTAINER = "wordpress-web" +SOURCE_ID = 55465 # Pagola, Carta 738 +LANG_BY_NAME = {"2_eng": "en", "3_fr": "fr", "4_it": "it", "5_pt": "pt"} +TRANSLATOR_LABELS = ("Translator:", "Traducteur:", "Traduzzione:", "Tradutor:") + + +def docx_lines(path: Path) -> list[str]: + ns = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"} + with zipfile.ZipFile(path) as zf: + root = ET.fromstring(zf.read("word/document.xml")) + return [ + "".join(t.text or "" for t in p.findall(".//w:t", ns)).strip() + for p in root.findall(".//w:p", ns) + if "".join(t.text or "" for t in p.findall(".//w:t", ns)).strip() + ] + + +def value_after(lines: list[str], label: str) -> str: + try: + return lines[lines.index(label) + 1] + except (ValueError, IndexError) as exc: + raise ValueError(f"Falta {label!r}") from exc + + +def parse_doc(path: Path) -> dict: + lines = docx_lines(path) + title = value_after(lines, "Título:") + excerpt = value_after(lines, "Entradilla:") + author = value_after(lines, "Autor:") + start = lines.index("Cuerpo:") + 1 + end = next((i for i in range(start, len(lines)) + if any(lines[i].startswith(label) for label in TRANSLATOR_LABELS)), len(lines)) + body = lines[start:end] + if not body: + raise ValueError("Cuerpo vacío") + translator = "" + if end < len(lines): + label = next(label for label in TRANSLATOR_LABELS if lines[end].startswith(label)) + translator = lines[end][len(label):].strip() + if not translator and end + 1 < len(lines): + translator = lines[end + 1] + # La fuente española incluye el crédito/origen como último párrafo. + body.append('Publicado en: https://www.gruposdejesus.com') + content = "\n".join( + f"

{line if line.startswith('Publicado en: " for line in body + ) + return { + "title": title, + "excerpt": excerpt, + "author": author, + "translator": translator, + "content": content, + "source_sha256": hashlib.sha256(path.read_bytes()).hexdigest(), + } + + +def run(cmd: list[str], stdin: str | None = None) -> str: + result = subprocess.run(cmd, input=stdin, text=True, capture_output=True) + if result.returncode: + raise RuntimeError(f"rc={result.returncode}: {result.stderr.strip()} {result.stdout.strip()}") + return result.stdout.strip() + + +def helper(*args: str, stdin: str | None = None) -> str: + return run(["docker", "exec", "-i", CONTAINER, "php", "/tmp/fea_translate_helper.php", *args], stdin) + + +def lang_for(path: Path) -> str: + for marker, lang in LANG_BY_NAME.items(): + if marker in path.name: + return lang + raise ValueError(f"No reconozco idioma en {path.name}") + + +def apply_meta(post_id: int, doc: Path, parsed: dict) -> None: + payload = json.dumps({"id": post_id, "doc": doc.name, "sha": parsed["source_sha256"], + "translator": parsed["translator"]}, ensure_ascii=False) + php = r'''$p=json_decode(base64_decode(getenv('PAYLOAD')),true); update_post_meta($p['id'],'traduccion_automatica','0'); update_post_meta($p['id'],'traduccion_modelo','editorial-humana'); update_post_meta($p['id'],'traduccion_fuente_doc',$p['doc']); update_post_meta($p['id'],'traduccion_fuente_sha256',$p['sha']); update_post_meta($p['id'],'traduccion_editor',$p['translator']); echo 'ok';''' + import base64 + encoded = base64.b64encode(payload.encode()).decode() + run(["docker", "exec", "-i", "-e", f"PAYLOAD={encoded}", CONTAINER, + "wp", "--allow-root", "eval", php]) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--dir", type=Path, required=True) + ap.add_argument("--apply-local", action="store_true") + args = ap.parse_args() + docs = sorted(args.dir.glob("*.docx")) + if len(docs) != 4: + raise SystemExit(f"Se esperaban 4 DOCX, encontrados {len(docs)}") + run(["docker", "cp", str(HELPER), f"{CONTAINER}:/tmp/fea_translate_helper.php"]) + for doc in docs: + lang, parsed = lang_for(doc), parse_doc(doc) + existing = helper("exists", str(SOURCE_ID), lang) + plan = {"lang": lang, "doc": doc.name, "existing": int(existing or "0"), + "title": parsed["title"], "excerpt_chars": len(parsed["excerpt"]), + "content_chars": len(parsed["content"]), "translator": parsed["translator"]} + print(json.dumps(plan, ensure_ascii=False)) + if not args.apply_local: + continue + if int(existing or "0"): + raise RuntimeError(f"{lang} ya existe como #{existing}; no sobrescribo") + payload = json.dumps({"title": parsed["title"], "excerpt": parsed["excerpt"], + "content": parsed["content"], "model": "editorial-humana"}, ensure_ascii=False) + post_id = int(helper("create", str(SOURCE_ID), lang, "draft", stdin=payload)) + apply_meta(post_id, doc, parsed) + print(json.dumps({"created": post_id, "lang": lang, "status": "draft"})) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/minimax_tts.py b/scripts/minimax_tts.py index 24124d5..08e864e 100644 --- a/scripts/minimax_tts.py +++ b/scripts/minimax_tts.py @@ -361,12 +361,12 @@ def _split_for_tts(text, limit=CHAR_LIMIT): return chunks -def _synth_chunk(text, voice_id, model): +def _synth_chunk(text, voice_id, model, speed=1.0): """Una petición t2a. Devuelve (audio_bytes|None, rc, usage_chars).""" body = { "model": model, "text": text, - "voice_setting": {"voice_id": voice_id, "speed": 1.0, "vol": 1.0, "pitch": 0}, + "voice_setting": {"voice_id": voice_id, "speed": speed, "vol": 1.0, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "mp3", "channel": 1}, "language_boost": "Spanish", } @@ -381,13 +381,15 @@ def _synth_chunk(text, voice_id, model): return bytes.fromhex(audio_hex), 0, usage -def t2a(text, voice_id, model, name): +def t2a(text, voice_id, model, name, speed=1.0): + if not 0.5 <= float(speed) <= 2.0: + raise ValueError(f"speed fuera de rango: {speed} (permitido 0.5–2.0)") chunks = _split_for_tts(text) - print(f"Sintetizando {len(text)} car con {model} / {voice_id} " + print(f"Sintetizando {len(text)} car con {model} / {voice_id} a {float(speed):.2f}× " f"({len(chunks)} petición/es)…", flush=True) raw = OUT / f"{name}.raw.mp3" if len(chunks) == 1: - audio, rc, _ = _synth_chunk(chunks[0], voice_id, model) + audio, rc, _ = _synth_chunk(chunks[0], voice_id, model, speed) if audio is None: return rc raw.write_bytes(audio) @@ -398,7 +400,7 @@ def t2a(text, voice_id, model, name): import os as _os, time as _t _t.sleep(int(_os.environ.get("FEA_CHUNK_PAUSE", "35"))) # respetar TPM de MiniMax print(f" trozo {k + 1}/{len(chunks)} ({len(ch)} car)…", flush=True) - audio, rc, _ = _synth_chunk(ch, voice_id, model) + audio, rc, _ = _synth_chunk(ch, voice_id, model, speed) if audio is None: for p in parts: p.unlink(missing_ok=True) diff --git a/scripts/phase_a_enrique_preview.py b/scripts/phase_a_enrique_preview.py new file mode 100644 index 0000000..7f8cb3d --- /dev/null +++ b/scripts/phase_a_enrique_preview.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Fase A local: borrador de Enrique + copia draft de la carta para revisión. + +Por defecto no escribe WordPress. --apply-local sólo modifica el WordPress Docker local. +Nunca publica ni toca producción. +""" +import argparse +import base64 +import hashlib +import html +import json +import os +import re +import subprocess +import sys +import zipfile +from pathlib import Path +from xml.etree import ElementTree as ET + +ROOT = Path(__file__).resolve().parent.parent +DEFAULT_DOC = Path("/home/rafa/.hermes/cache/documents/doc_7909db597c74_7r_Enrique_Mart_nez_Lozano_tesoro_esta_ya_en_nosotros.docx") +CARTA_PROD_ID = 54914 +AUTHOR_ID = 384 # Enrique Martínez Lozano, verificado localmente. +CATEGORIES = [1650, 71] # Artículos + Feadulta +SLUG = "el-tesoro-esta-ya-en-nosotros" + + +def docx_text(path: Path) -> list[str]: + ns = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"} + with zipfile.ZipFile(path) as zf: + root = ET.fromstring(zf.read("word/document.xml")) + out = [] + for p in root.findall(".//w:p", ns): + text = "".join(t.text or "" for t in p.findall(".//w:t", ns)).strip() + if text: + out.append(text) + return out + + +def parse_source(path: Path) -> dict: + lines = docx_text(path) + def after(label: str) -> str: + return lines[lines.index(label) + 1] + title = after("Título:") + author = after("Autor:") + assert title == "EL TESORO ESTÁ YA EN NOSOTROS", title + assert author == "ENRIQUE MARTÍNEZ LOZANO", author + body_start = lines.index("Cuerpo:") + 1 + body = lines[body_start:] + # Última firma del boletín no es contenido del post: el autor va en WP. + if body[-1].startswith("ENRIQUE MARTÍNEZ LOZANO"): + body = body[:-1] + date_line, bible, *paragraphs = body + intro = lines[lines.index("Entradilla:") + 1] + content = "\n".join( + [f"

{html.escape(date_line)}

", + f"

{html.escape(bible)}

"] + + [f"

{html.escape(p)}

" for p in paragraphs] + ) + return {"title": title.lower().capitalize(), "author": author.title(), "intro": intro, + "content": content, "source_sha256": hashlib.sha256(path.read_bytes()).hexdigest()} + + +def prod_card() -> dict: + sys.path.insert(0, str(ROOT / "scripts")) + import sync_translations_to_prod as sync + return json.loads(sync.prod_helper("read_full", str(CARTA_PROD_ID))) + + +def carta_candidate(card: dict, anchor: str) -> str: + marker = '

 

\n

Para unas eucaristías más participativas y actuales

' + assert card["content"].count(marker) == 1, "Marcador de sección no único/no encontrado" + return card["content"].replace(marker, anchor + "\n" + marker, 1) + + +def run_wp(payload: dict, apply: bool) -> dict: + encoded = base64.b64encode(json.dumps(payload, ensure_ascii=False).encode()).decode() + php = r''' +$p=json_decode(base64_decode(getenv('FEA_PAYLOAD')), true); +$apply=getenv('FEA_APPLY') === '1'; +$existing=get_page_by_path($p['slug'], OBJECT, 'post'); +$author=get_user_by('id',(int)$p['author_id']); +$cats=array_map('intval',$p['categories']); +$cat_ok=count(array_filter($cats, fn($id)=>term_exists($id,'category')))===count($cats); +$preview=get_posts(['post_type'=>'post','post_status'=>'any','meta_key'=>'fea_phase_a_source_hash','meta_value'=>$p['source_sha256'],'numberposts'=>1]); +$out=['dry_run'=>!$apply,'existing_slug'=>$existing?['id'=>$existing->ID,'status'=>$existing->post_status]:null, + 'author'=>$author?['id'=>$author->ID,'name'=>$author->display_name]:null,'categories_ok'=>$cat_ok, + 'preview_existing'=>$preview?['id'=>$preview[0]->ID,'status'=>$preview[0]->post_status]:null, + 'candidate_anchor'=>$p['anchor']]; +if (!$author || !$cat_ok) { $out['error']='Autor o categorías no válidos'; echo wp_json_encode($out,JSON_UNESCAPED_UNICODE); exit(3); } +if ($existing || $preview) { $out['error']='Ya existe un borrador/slug para esta Fase A; no se duplica'; echo wp_json_encode($out,JSON_UNESCAPED_UNICODE); exit(4); } +if (!$apply) { echo wp_json_encode($out,JSON_UNESCAPED_UNICODE); exit; } +$article_id=wp_insert_post(['post_type'=>'post','post_status'=>'draft','post_author'=>(int)$p['author_id'], + 'post_title'=>$p['article_title'],'post_name'=>$p['slug'],'post_excerpt'=>$p['intro'], + 'post_content'=>$p['article_content'],'post_category'=>$cats],true); +if (is_wp_error($article_id)) { $out['error']=$article_id->get_error_message(); echo wp_json_encode($out,JSON_UNESCAPED_UNICODE); exit(5); } +if (function_exists('pll_set_post_language')) pll_set_post_language($article_id,'es'); +update_post_meta($article_id,'fea_phase_a_source_hash',$p['source_sha256']); +update_post_meta($article_id,'fea_phase_a_source_doc','7r_Enrique Martínez Lozano_tesoro_esta_ya_en_nosotros.docx'); +update_post_meta($article_id,'fea_phase_a_status','preview-only'); +$preview_id=wp_insert_post(['post_type'=>'post','post_status'=>'draft','post_author'=>(int)$p['card_author_id'], + 'post_title'=>'PREVIEW — Hacia el corazón (+ Enrique Martínez Lozano)','post_excerpt'=>'Copia local de validación; no publicar.', + 'post_content'=>$p['candidate_card_content']],true); +if (is_wp_error($preview_id)) { wp_delete_post($article_id,true); $out['error']=$preview_id->get_error_message(); echo wp_json_encode($out,JSON_UNESCAPED_UNICODE); exit(6); } +if (function_exists('pll_set_post_language')) pll_set_post_language($preview_id,'es'); +update_post_meta($preview_id,'fea_phase_a_source_hash',$p['source_sha256']); +update_post_meta($preview_id,'fea_phase_a_source_prod_id',(int)$p['carta_prod_id']); +update_post_meta($preview_id,'fea_phase_a_preview_article_id',$article_id); +update_post_meta($preview_id,'fea_phase_a_status','preview-only'); +$out['article']=['id'=>$article_id,'status'=>'draft','slug'=>get_post_field('post_name',$article_id),'author'=>get_the_author_meta('display_name',(int)$p['author_id'])]; +$out['preview_card']=['id'=>$preview_id,'status'=>'draft','source_prod_id'=>(int)$p['carta_prod_id']]; +echo wp_json_encode($out,JSON_UNESCAPED_UNICODE); +''' + cmd = [ + "docker", "exec", "-i", "-e", f"FEA_PAYLOAD={encoded}", + "-e", f"FEA_APPLY={'1' if apply else '0'}", "wordpress-web", + "wp", "--allow-root", "eval", php, + ] + result = subprocess.run(cmd, text=True, capture_output=True, check=False) + if result.returncode: + raise RuntimeError(f"WP local rc={result.returncode}: {result.stderr}\n{result.stdout}") + return json.loads(result.stdout) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--doc", type=Path, default=DEFAULT_DOC) + ap.add_argument("--apply-local", action="store_true") + ap.add_argument("--output", type=Path, required=True) + args = ap.parse_args() + source = parse_source(args.doc) + card = prod_card() # lectura server-side únicamente. + article_url = "http://localhost:8080/" + SLUG + "/" + anchor = (f'

Enrique Martínez Lozano: ' + f'{html.escape(source["title"])}. {html.escape(source["intro"])}

') + payload = { + "source_sha256": source["source_sha256"], "article_title": source["title"], + "article_content": source["content"], "intro": source["intro"], "slug": SLUG, + "author_id": AUTHOR_ID, "categories": CATEGORIES, "anchor": anchor, + "candidate_card_content": carta_candidate(card, anchor), "card_author_id": card["author"], + "carta_prod_id": CARTA_PROD_ID, + } + result = run_wp(payload, args.apply_local) + report = {"source": source, "prod_card": {"id": card["id"], "title": card["title"], "status": card["status"]}, + "result": result, "mode": "apply-local" if args.apply_local else "dry-run"} + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + print(json.dumps(report, ensure_ascii=False, indent=2)) + +if __name__ == "__main__": + main() diff --git a/scripts/release_issue181_prod.sh b/scripts/release_issue181_prod.sh new file mode 100755 index 0000000..e56690a --- /dev/null +++ b/scripts/release_issue181_prod.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +# Release Fase 1+2, issue #181: 68 traducciones publicadas + 16 MP3 TTS. +# Por defecto es sólo dry-run. --apply requiere confirmación explícita de Rafa. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +MODE="dry-run" +if [[ "${1:-}" == "--apply" ]]; then + MODE="apply" +elif [[ "${1:-}" == "--verify" ]]; then + MODE="verify" +elif [[ "${1:-}" != "" && "${1:-}" != "--dry-run" ]]; then + echo "Uso: $0 [--dry-run|--verify|--apply]" >&2 + exit 2 +fi + +# shellcheck disable=SC1091 +source ~/.hermes/profiles/feadulta/.env +: "${FEA_PROD_WPLOAD:=/web/wp-load.php}" +if [[ "$FEA_PROD_WPLOAD" != "/web/wp-load.php" ]]; then + echo "ABORT: FEA_PROD_WPLOAD debe ser /web/wp-load.php (recibido: $FEA_PROD_WPLOAD)" >&2 + exit 2 +fi + +RELEASE_DIR="logs/release-issue-181" +mkdir -p "$RELEASE_DIR/backups" +STAMP="$(date -u +%Y%m%dT%H%M%SZ)" +ORIGINS=(54875 54902 54903 54883 54884 54885 54886 54866 54867 54868 54869 54870 54871 54872 54873 54880 54914) +AUDIO_IDS=(54875 54902 54903 54883 54884 54885 54886 54866 54867 54868 54869 54870 54871 54872 54873 54880) + +if [[ "$MODE" == "apply" && "${FEA_RELEASE_CONFIRM:-}" != "PUBLISH_ISSUE_181" ]]; then + echo "ABORT: para escribir en producción usa:" >&2 + echo " FEA_RELEASE_CONFIRM=PUBLISH_ISSUE_181 $0 --apply" >&2 + exit 2 +fi + +preflight() { + python3 - "${ORIGINS[@]}" <<'PY' +import json, sys +sys.path.insert(0, 'scripts') +import sync_translations_to_prod as sync +ids = [int(x) for x in sys.argv[1:]] +rows = [] +for pid in ids: + d = json.loads(sync.prod_helper('read_full', str(pid))) + rows.append({'id': pid, 'status': d['status'], 'lang': d['lang'], 'translations': d.get('translations', {})}) +assert len(rows) == 17 +assert all(r['status'] == 'publish' and r['lang'] == 'es' for r in rows), rows +# Los 16 artículos no deben tener traducciones aún; la carta tampoco antes del primer release. +assert all(r['translations'] == {'es': r['id']} for r in rows), rows +print(json.dumps(rows, ensure_ascii=False, indent=2)) +print('preflight=PASS sources=17') +PY +} + +backup_prod_state() { + python3 - "$RELEASE_DIR/backups/prod-before-$STAMP.json" "${ORIGINS[@]}" -- "${AUDIO_IDS[@]}" <<'PY' +import json, sys +out = sys.argv[1] +sep = sys.argv.index('--') +origins = [int(x) for x in sys.argv[2:sep]] +audio_ids = [int(x) for x in sys.argv[sep+1:]] +sys.path.insert(0, 'scripts') +import sync_translations_to_prod as translations +import sync_audio_to_prod as audio +payload = { + 'origins': {str(pid): json.loads(translations.prod_helper('read_full', str(pid))) for pid in origins}, + 'audio_before': { + str(pid): { + 'url': audio.prod_helper('getmeta', str(pid), 'fea_audio_url').strip(), + 'voice': audio.prod_helper('getmeta', str(pid), 'fea_audio_voice').strip(), + 'done': audio.prod_helper('getmeta', str(pid), 'fea_audio_done').strip(), + } for pid in audio_ids + }, +} +with open(out, 'w', encoding='utf-8') as fh: + json.dump(payload, fh, ensure_ascii=False, indent=2) +print(out) +PY +} + +verify_release() { + python3 - "${ORIGINS[@]}" -- "${AUDIO_IDS[@]}" <<'PY' +import json, sys, time +sep = sys.argv.index('--') +origins = [int(x) for x in sys.argv[1:sep]] +audio_ids = [int(x) for x in sys.argv[sep+1:]] +sys.path.insert(0, 'scripts') +import sync_translations_to_prod as translations +import sync_audio_to_prod as audio + +def read_post(pid): + """Lectura server-side resiliente ante una respuesta SSH/PHP vacía transitoria.""" + last = '' + for attempt in range(1, 4): + raw = translations.prod_helper('read_full', str(pid)).strip() + if raw: + try: + return json.loads(raw) + except json.JSONDecodeError as exc: + last = f'JSON inválido intento {attempt}: {exc}; prefijo={raw[:160]!r}' + else: + last = f'respuesta vacía intento {attempt}' + time.sleep(attempt) + raise AssertionError(f'No se pudo leer post #{pid}: {last}') + +for pid in origins: + d = read_post(pid) + group = d.get('translations', {}) + assert set(group) == {'es', 'en', 'fr', 'it', 'pt'}, (pid, group) + for lang, tid in group.items(): + td = read_post(tid) + assert td['lang'] == lang and td['status'] == 'publish', (pid, lang, tid, td['lang'], td['status']) +for pid in audio_ids: + url = audio.prod_helper('getmeta', str(pid), 'fea_audio_url').strip() + done = audio.prod_helper('getmeta', str(pid), 'fea_audio_done').strip() + assert url.endswith(f'/wp-content/uploads/tts/{pid}.mp3') and done == '1', (pid, url, done) +print('verification=PASS groups=17 translations=68 audio=16') +PY +} + +if [[ "$MODE" == "verify" ]]; then + echo "== Verificación server-side posterior (solo lectura) ==" + verify_release | tee "$RELEASE_DIR/verification-$STAMP.txt" + echo "VERIFICACIÓN ISSUE #181 COMPLETADA" + exit 0 +fi + +echo "== Preflight server-side ($MODE): 17 fuentes ES ==" +preflight | tee "$RELEASE_DIR/preflight-$MODE-$STAMP.json" + +if [[ "$MODE" == "dry-run" ]]; then + : > "$RELEASE_DIR/translations-dry-run-$STAMP.log" + for origin in "${ORIGINS[@]}"; do + FEA_SYNC_STATUS=publish \ + FEA_SYNC_LOG="$RELEASE_DIR/translations-dry-run-$STAMP.log" \ + FEA_SYNC_STATE="$RELEASE_DIR/translations-$origin-state.json" \ + python3 scripts/sync_translations_to_prod.py --origin "$origin" --dry-run + done + FEA_AUDIO_SYNC_LOG="$RELEASE_DIR/audio-dry-run-$STAMP.log" \ + FEA_AUDIO_SYNC_STATE="$RELEASE_DIR/audio-state.json" \ + python3 scripts/sync_audio_to_prod.py --ids "$(IFS=,; echo "${AUDIO_IDS[*]}")" --dry-run | tee "$RELEASE_DIR/audio-dry-run-$STAMP.log" + echo "DRY-RUN terminado: 68 traducciones publicables + 16 audios planificados." + exit 0 +fi + +echo "== Backup server-side previo ==" +backup_prod_state | tee "$RELEASE_DIR/backup-path-$STAMP.txt" + +echo "== Subiendo 68 traducciones como publish ==" +for origin in "${ORIGINS[@]}"; do + FEA_SYNC_STATUS=publish \ + FEA_SYNC_LOG="$RELEASE_DIR/translations-apply-$STAMP.log" \ + FEA_SYNC_STATE="$RELEASE_DIR/translations-$origin-state.json" \ + python3 scripts/sync_translations_to_prod.py --origin "$origin" +done + +echo "== Subiendo 16 audios ==" +FEA_AUDIO_SYNC_LOG="$RELEASE_DIR/audio-apply-$STAMP.log" \ +FEA_AUDIO_SYNC_STATE="$RELEASE_DIR/audio-state.json" \ +python3 scripts/sync_audio_to_prod.py --ids "$(IFS=,; echo "${AUDIO_IDS[*]}")" + +echo "== Verificación server-side posterior ==" +verify_release | tee "$RELEASE_DIR/verification-$STAMP.txt" +echo "RELEASE ISSUE #181 COMPLETADO" diff --git a/scripts/sync_audio_to_prod.py b/scripts/sync_audio_to_prod.py index 211856f..b3dc01f 100644 --- a/scripts/sync_audio_to_prod.py +++ b/scripts/sync_audio_to_prod.py @@ -3,9 +3,10 @@ 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). +Prod vive en Hetzner (Coolify/Docker) desde el cutover de agosto 2026. Subida +del binario por stdin de ssh ("cat > ruta" dentro del contenedor vía +`docker exec -i`), sin depender de scp/sftp — mismo patrón que +sync_translations_to_prod.py y sync_carta_from_prod.py. Uso: python3 sync_audio_to_prod.py --carta 54254 # sincroniza toda la cola de la carta @@ -19,8 +20,10 @@ Rollback (despublica en prod lo que este script publicó): from __future__ import annotations import argparse +import base64 import json import os +import shlex import subprocess import time from pathlib import Path @@ -31,13 +34,20 @@ 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" +PROD_HOST = os.environ.get("FEA_PROD_SSH_HOST", "") +PROD_PASS = os.environ.get("FEA_PROD_SSH_PASS", "") +PROD_WPLOAD = os.environ.get("FEA_PROD_WPLOAD", "/var/www/html/wp-load.php") +# Desde el cutover a Hetzner, WordPress vive dentro de Coolify/Docker. Si se +# define, el helper y la subida de mp3 se ejecutan dentro del contenedor. +# Espejo de sync_translations_to_prod.py / sync_carta_from_prod.py. +PROD_DOCKER_CONTAINER = os.environ.get("FEA_PROD_DOCKER_CONTAINER", "") +PROD_UPLOADS_TTS = os.environ.get("FEA_PROD_UPLOADS_TTS", "/var/www/html/wp-content/uploads/tts") HELPER_SRC = Path(__file__).resolve().parent / "fea_post_io.php" +# Se evalúa en memoria con `php -r`: no se copia un helper temporal a prod. +HELPER_EVAL_CODE = "eval(base64_decode(" + repr( + base64.b64encode(HELPER_SRC.read_text(encoding="utf-8").removeprefix(" str: return r.stdout.strip() +def prod_id_for_local(local_id: int) -> int: + """Resolve a remapped local post to its original production ID. + + ``sync_carta_from_prod.py --remap-conflicts`` keeps this mapping in + ``fea_prod_source_id``. Audio files remain named with the local ID, but + remote paths and production post meta must use the source ID. + """ + source_id = local_meta(local_id, "fea_prod_source_id") + return int(source_id) if source_id.isdigit() else local_id + + 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;") @@ -85,47 +106,80 @@ def carta_article_ids(carta_id: int) -> list[int]: return [int(x) for x in r.stdout.split() if x.isdigit()] -# ── Prod (glibc rota: nada de scp/sftp, todo por ssh + cat) ──────────────────── +# ── Prod (Hetzner/Docker: todo por ssh, mp3 por stdin de "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] + # El Hetzner nuevo usa auth por clave (ed25519 claude-code@feadulta); sshpass + # solo se antepone si hay contraseña configurada (servidor viejo CDMON). + if PROD_PASS: + cmd = ["sshpass", "-p", PROD_PASS, "ssh", "-o", "StrictHostKeyChecking=accept-new", + "-o", "ConnectTimeout=20", PROD_HOST, remote_cmd] + else: + cmd = ["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 _remote_wrap(inner_cmd: str) -> str: + """Envuelve un comando para que corra dentro del contenedor Docker de prod + si FEA_PROD_DOCKER_CONTAINER está definido; si no, corre en el host tal cual. + + Importante: el wrapping (incluidas redirecciones como `< ruta`) debe quedar + DENTRO de la shell del contenedor (`sh -c '...'`), porque `docker exec + CONTENEDOR cmd < ruta` resuelve esa redirección en el filesystem del HOST, + no dentro del contenedor. + """ + if PROD_DOCKER_CONTAINER: + return f"docker exec -i {shlex.quote(PROD_DOCKER_CONTAINER)} sh -c {shlex.quote(inner_cmd)}" + return inner_cmd + + 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}"] + remote_cmd = _remote_wrap(f"cat > {remote_path}") + if PROD_PASS: + cmd = ["sshpass", "-p", PROD_PASS, "ssh", "-o", "StrictHostKeyChecking=accept-new", + "-o", "ConnectTimeout=20", PROD_HOST, remote_cmd] + else: + cmd = ["ssh", "-o", "StrictHostKeyChecking=accept-new", + "-o", "ConnectTimeout=20", PROD_HOST, remote_cmd] 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) + """Ejecuta el helper PHP en memoria; no deja ficheros temporales en prod.""" + argv = " ".join(shlex.quote(x) for x in (subcmd, *args)) + inner = ( + f"FEA_WP_LOAD={shlex.quote(PROD_WPLOAD)} " + f"php -r {shlex.quote(HELPER_EVAL_CODE)} {argv}" + ) + if PROD_DOCKER_CONTAINER: + remote_cmd = ( + f"docker exec -i -e FEA_WP_LOAD={shlex.quote(PROD_WPLOAD)} " + f"{shlex.quote(PROD_DOCKER_CONTAINER)} php -r {shlex.quote(HELPER_EVAL_CODE)} {argv}" + ) + else: + remote_cmd = inner + return _ssh_text(remote_cmd, timeout=60) -def prod_upload_mp3(post_id: int) -> None: - src = LOCAL_TTS_DIR / f"{post_id}.mp3" +def prod_upload_mp3(local_id: int, prod_id: int) -> None: + src = LOCAL_TTS_DIR / f"{local_id}.mp3" data = src.read_bytes() - remote_path = f"{PROD_UPLOADS_TTS}/{post_id}.mp3" + remote_path = f"{PROD_UPLOADS_TTS}/{prod_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()) + # Verificación de tamaño: no fiarse ciegamente del rc=0 de ssh. `wc -c` debe + # correr (y resolver la redirección) DENTRO del contenedor — ver _remote_wrap. + remote_size = int(_ssh_text(_remote_wrap(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}") + raise RuntimeError( + f"tamaño no coincide tras subir prod#{prod_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") + _ssh_text(_remote_wrap(f"rm -f {PROD_UPLOADS_TTS}/{post_id}.mp3")) # ── Estado ─────────────────────────────────────────────────────────────────── @@ -149,25 +203,27 @@ def sync_one(post_id: int, state: dict, *, dry_run: bool) -> str: return "sin-audio-local" if not (LOCAL_TTS_DIR / f"{post_id}.mp3").exists(): return "mp3-local-ausente" + prod_id = prod_id_for_local(post_id) if dry_run: - return "PLAN: subiría mp3 + setaudio" + return f"PLAN: local#{post_id}.mp3 → prod#{prod_id}.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) + prod_upload_mp3(post_id, prod_id) + prod_helper("setaudio", str(prod_id), f"/wp-content/uploads/tts/{prod_id}.mp3", voice) + if prod_id not in state["synced"]: + state["synced"].append(prod_id) save_state(state) - return "ok" + return f"ok prod#{prod_id}" 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) + prod_id = prod_id_for_local(post_id) + prod_helper("unsetaudio", str(prod_id)) + prod_remove_mp3(prod_id) + if prod_id in state["synced"]: + state["synced"].remove(prod_id) save_state(state) - return "ok" + return f"ok prod#{prod_id}" def main() -> int: @@ -194,7 +250,7 @@ def main() -> int: for pid in ids: try: if args.rollback: - if pid not in state["synced"] and not args.ids: + if prod_id_for_local(pid) not in state["synced"] and not args.ids: res = "no-estaba-sincronizado" skip += 1 else: diff --git a/scripts/sync_carta_from_prod.py b/scripts/sync_carta_from_prod.py index 6b814e7..d1d1760 100644 --- a/scripts/sync_carta_from_prod.py +++ b/scripts/sync_carta_from_prod.py @@ -23,6 +23,8 @@ el mismo fea_translate_helper.php sin tocar su lógica, solo invertido Uso: python3 sync_carta_from_prod.py --carta 54495 --dry-run python3 sync_carta_from_prod.py --carta 54495 + python3 sync_carta_from_prod.py --ids 54875,54902,54903 --dry-run + python3 sync_carta_from_prod.py --ids 54875,54902,54903 Tras esto, el resto del ciclo ya existente no cambia: translate_post.py --carta 54495 --langs en,fr,it,pt --status draft @@ -34,8 +36,10 @@ Tras esto, el resto del ciclo ya existente no cambia: from __future__ import annotations import argparse +import base64 import json import os +import shlex import subprocess import time from pathlib import Path @@ -44,7 +48,11 @@ WP_CONTAINER = os.environ.get("FEA_WP_CONTAINER", "wordpress-web") PROD_HOST = os.environ.get("FEA_PROD_SSH_HOST", "") PROD_PASS = os.environ.get("FEA_PROD_SSH_PASS", "") -PROD_WPLOAD = os.environ.get("FEA_PROD_WPLOAD", "/web/wp-load.php") +PROD_WPLOAD = os.environ.get("FEA_PROD_WPLOAD", "/var/www/html/wp-load.php") +# Desde el cutover a Hetzner, WordPress vive dentro de Coolify/Docker. Si se +# define, el helper se ejecuta en memoria dentro del contenedor; no se escribe +# ningún fichero temporal en prod. +PROD_DOCKER_CONTAINER = os.environ.get("FEA_PROD_DOCKER_CONTAINER", "") PROD_HELPER = "/tmp/fea_translate_helper.php" HELPER_SRC = Path(__file__).resolve().parent / "fea_translate_helper.php" @@ -74,22 +82,35 @@ def sh(cmd: list[str], *, stdin: str | None = None, timeout: int = 120) -> str: # ── Prod (origen, solo lectura) ───────────────────────────────────────────── -_prod_ready = False - - def _ssh(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] + # El Hetzner nuevo usa auth por clave (ed25519 claude-code@feadulta); sshpass + # solo se antepone si hay contraseña configurada (servidor viejo CDMON). + if PROD_PASS: + cmd = ["sshpass", "-p", PROD_PASS, "ssh", "-o", "StrictHostKeyChecking=accept-new", + "-o", "ConnectTimeout=20", PROD_HOST, remote_cmd] + else: + cmd = ["ssh", "-o", "StrictHostKeyChecking=accept-new", + "-o", "ConnectTimeout=20", PROD_HOST, remote_cmd] return sh(cmd, stdin=stdin, timeout=timeout) -def prod_helper(subcmd: str, *args: str, stdin: str | None = None) -> str: - global _prod_ready - if not _prod_ready: - _ssh(f"cat > {PROD_HELPER}", stdin=HELPER_SRC.read_text(encoding="utf-8")) - _prod_ready = True - inner = f"FEA_WP_LOAD={PROD_WPLOAD} php {PROD_HELPER} {subcmd} " + " ".join(args) - return _ssh(inner, stdin=stdin, timeout=180) +def prod_helper(subcmd: str, *args: str) -> str: + """Run the helper in prod memory; never upload a temporary file to prod.""" + helper_php = HELPER_SRC.read_text(encoding="utf-8").replace(" dict: @@ -110,6 +131,17 @@ def seed_ids_from_carta(carta_id: int) -> list[int]: return sorted(ids) +def seed_ids_from_csv(raw_ids: str) -> list[int]: + """Parse a deliberate prod→local subset for Mixbot --parte1 (#181).""" + try: + ids = sorted({int(part.strip()) for part in raw_ids.split(",") if part.strip()}) + except ValueError as exc: + raise ValueError("--ids debe ser una lista CSV de IDs numéricos") from exc + if not ids or any(pid <= 0 for pid in ids): + raise ValueError("--ids debe contener al menos un ID positivo") + return ids + + def collect_related_posts(seed_ids: list[int]) -> tuple[dict[int, dict], list[dict[str, int]]]: posts: dict[int, dict] = {} groups: dict[tuple[tuple[str, int], ...], dict[str, int]] = {} @@ -153,7 +185,11 @@ def local_read_safe(post_id: int) -> dict | None: return None -def local_clone(post: dict) -> int: +def local_clone(post: dict, *, preserve_id: bool = True) -> int: + # Conserva el ID remoto como trazabilidad incluso cuando haya que crear un + # ID local nuevo por una colisión entre entornos. + meta = dict(post.get("meta", {})) + meta["fea_prod_source_id"] = [str(post["id"])] payload = { "title": post["title"], "content": post.get("content", ""), @@ -166,10 +202,14 @@ def local_clone(post: dict) -> int: "status": post.get("status"), "cats": post.get("cats", []), "cat_slugs": post.get("cat_slugs", []), - "meta": post.get("meta", {}), + "meta": meta, } - out = local_helper("clone", str(post["id"]), post.get("lang") or "es", STATUS, - stdin=json.dumps(payload)).strip() + if preserve_id: + out = local_helper("clone", str(post["id"]), post.get("lang") or "es", STATUS, + stdin=json.dumps(payload)).strip() + else: + out = local_helper("clone_new", post.get("lang") or "es", STATUS, + stdin=json.dumps(payload)).strip() return int(out) @@ -179,13 +219,9 @@ def local_save_group(group: dict[str, int]) -> dict[str, int]: # ── Main ───────────────────────────────────────────────────────────────────── -def sync_carta(carta_id: int, *, dry_run: bool) -> int: - seed_ids = seed_ids_from_carta(carta_id) - log(f"Cluster descubierto desde fea_parse_carta_sections(prod#{carta_id}): " - f"{len(seed_ids)} post(s) -> {seed_ids}") - if not seed_ids or seed_ids == [carta_id]: - log(" ⚠️ El parser no resolvió ningún artículo enlazado (¿carta sin publicar aún, " - "o secciones sin encabezados reconocibles?). Revisa antes de continuar.") +def sync_posts(seed_ids: list[int], *, dry_run: bool, source_label: str, + remap_conflicts: bool = False) -> int: + log(f"Lote {source_label}: {len(seed_ids)} post(s) -> {seed_ids}") posts, groups = collect_related_posts(seed_ids) @@ -196,51 +232,82 @@ def sync_carta(carta_id: int, *, dry_run: bool) -> int: conflicts.append((pid, existing.get("title", ""), p.get("title", ""))) if conflicts: - log(f" ⚠️ {len(conflicts)} CONFLICTO(S): el ID ya existe en local con OTRO contenido " - f"y va a ser SOBRESCRITO:") + log(f" ⚠️ {len(conflicts)} CONFLICTO(S): el ID ya existe en local con OTRO contenido:") for pid, old_title, new_title in conflicts: - log(f" #{pid}: local actual «{old_title[:50]}» -> prod «{new_title[:50]}»") + log(f" #{pid}: local actual «{old_title[:50]}» | prod «{new_title[:50]}»") + if not dry_run and not remap_conflicts: + raise RuntimeError( + "Importación cancelada: usar --remap-conflicts para crear IDs locales nuevos; " + "nunca se sobrescriben posts locales por un choque de IDs entre entornos." + ) if dry_run: for pid, p in posts.items(): - log(f" PULL prod#{pid} [{p.get('lang','?')}] status={p.get('status')} " + action = "REMAPPING to new local ID" if pid in {c[0] for c in conflicts} else "PULL" + log(f" {action} prod#{pid} [{p.get('lang','?')}] status={p.get('status')} " f"slug={p.get('slug','')} «{p.get('title','')[:50]}»") for group in groups: log(f" GROUP {group}") log("DRY-RUN: nada escrito en local.") return 0 + conflict_ids = {pid for pid, _, _ in conflicts} + id_map: dict[int, int] = {} for pid, p in posts.items(): - new_id = local_clone(p) + new_id = local_clone(p, preserve_id=pid not in conflict_ids) + id_map[pid] = new_id if new_id != pid: - log(f" ⚠️ prod#{pid} se clonó como local#{new_id} — ID NO preservado, revisar a mano.") + log(f" remap prod#{pid} -> local#{new_id} [{p.get('lang','?')}] «{p['title'][:45]}»") else: log(f" clone prod#{pid} -> local#{new_id} [{p.get('lang','?')}] «{p['title'][:45]}»") for group in groups: - if len(group) < 2: + local_group = {lang: id_map.get(pid, pid) for lang, pid in group.items()} + if len(local_group) < 2: continue - saved = local_save_group(group) + saved = local_save_group(local_group) log(f" group enlazado en local {saved}") - log(f"FIN sync prod->local. carta={carta_id} posts={len(posts)} conflictos_previos={len(conflicts)}") + log(f"FIN sync prod->local. fuente={source_label} posts={len(posts)} conflictos_previos={len(conflicts)}") return 0 +def sync_carta(carta_id: int, *, dry_run: bool, remap_conflicts: bool = False) -> int: + seed_ids = seed_ids_from_carta(carta_id) + if not seed_ids or seed_ids == [carta_id]: + log(" ⚠️ El parser no resolvió ningún artículo enlazado (¿carta sin publicar aún, " + "o secciones sin encabezados reconocibles?). Revisa antes de continuar.") + return sync_posts(seed_ids, dry_run=dry_run, source_label=f"carta prod#{carta_id}", + remap_conflicts=remap_conflicts) + + +def sync_ids(raw_ids: str, *, dry_run: bool, remap_conflicts: bool = False) -> int: + return sync_posts(seed_ids_from_csv(raw_ids), dry_run=dry_run, + source_label="IDs explícitos de Mixbot --parte1", + remap_conflicts=remap_conflicts) + + def main() -> int: ap = argparse.ArgumentParser( description="Copia una carta (y su cluster de artículos) de PROD a LOCAL preservando IDs.") - ap.add_argument("--carta", type=int, required=True, help="ID del post ES de la carta en PROD.") + group = ap.add_mutually_exclusive_group(required=True) + group.add_argument("--carta", type=int, help="ID del post ES de la carta en PROD.") + group.add_argument("--ids", help="Lista CSV explícita de posts ES para Mixbot --parte1 (#181).") ap.add_argument("--dry-run", action="store_true", help="Solo muestra el plan; no escribe en local.") + ap.add_argument("--remap-conflicts", action="store_true", + help="Ante IDs ocupados localmente, crea IDs nuevos y guarda fea_prod_source_id; nunca sobrescribe.") args = ap.parse_args() - if not PROD_HOST or not PROD_PASS: + if not PROD_HOST: raise SystemExit( - "Faltan FEA_PROD_SSH_HOST / FEA_PROD_SSH_PASS en el entorno.\n" + "Falta FEA_PROD_SSH_HOST en el entorno (FEA_PROD_SSH_PASS es opcional: " + "el Hetzner nuevo usa auth por clave).\n" "Antes de ejecutar: source ~/.hermes/profiles/feadulta/.env" ) - return sync_carta(args.carta, dry_run=args.dry_run) + if args.carta: + return sync_carta(args.carta, dry_run=args.dry_run, remap_conflicts=args.remap_conflicts) + return sync_ids(args.ids, dry_run=args.dry_run, remap_conflicts=args.remap_conflicts) if __name__ == "__main__": diff --git a/scripts/sync_translations_to_prod.py b/scripts/sync_translations_to_prod.py index 82b948b..48cf742 100644 --- a/scripts/sync_translations_to_prod.py +++ b/scripts/sync_translations_to_prod.py @@ -16,8 +16,10 @@ coincidencia local↔prod cuando prod va por detrás. from __future__ import annotations import argparse +import base64 import json import os +import shlex import subprocess import time from pathlib import Path @@ -29,12 +31,20 @@ 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_translate_helper.php" +PROD_HOST = os.environ.get("FEA_PROD_SSH_HOST", "") +PROD_PASS = os.environ.get("FEA_PROD_SSH_PASS", "") +PROD_WPLOAD = os.environ.get("FEA_PROD_WPLOAD", "/var/www/html/wp-load.php") +# Desde el cutover a Hetzner, WordPress vive dentro de Coolify/Docker. Si se +# define, el helper se ejecuta en memoria dentro del contenedor; no se escribe +# ningún fichero temporal en prod. Espejo de sync_carta_from_prod.py. +PROD_DOCKER_CONTAINER = os.environ.get("FEA_PROD_DOCKER_CONTAINER", "") HELPER_SRC = Path(__file__).resolve().parent / "fea_translate_helper.php" +# El helper se ejecuta en memoria con `php -r` en prod. Así no se copia ningún +# fichero a /tmp remoto y stdin queda disponible para el payload JSON. +HELPER_EVAL_CODE = "eval(base64_decode(" + repr( + base64.b64encode(HELPER_SRC.read_text(encoding="utf-8").removeprefix(" dict: def local_translation_pairs() -> list[tuple[int, int]]: - q = ("SELECT post_id, meta_value FROM wp_postmeta " - "WHERE meta_key='traduccion_origen' ORDER BY CAST(meta_value AS UNSIGNED), post_id;") + """Devuelve (traducción_local, origen_ES_en_prod). + + Normalmente ambos entornos compartían IDs. Para importaciones remapeadas, + la carta ES local guarda `fea_prod_source_id`; esa referencia remota tiene + prioridad y evita enlazar una traducción con un post distinto de prod. + """ + q = ( + "SELECT tr.post_id, COALESCE(NULLIF(src.meta_value,''), tr.meta_value) " + "FROM wp_postmeta tr " + "LEFT JOIN wp_postmeta src ON src.post_id=CAST(tr.meta_value AS UNSIGNED) " + "AND src.meta_key='fea_prod_source_id' " + "WHERE tr.meta_key='traduccion_origen' " + "ORDER BY CAST(COALESCE(NULLIF(src.meta_value,''), tr.meta_value) AS UNSIGNED), tr.post_id;" + ) out = sh(["docker", "exec", DB_CONTAINER, "mysql", f"-u{DB_USER}", f"-p{DB_PASS}", DB_NAME, "-N", "-e", q]) pairs = [] @@ -155,21 +177,31 @@ def collect_related_posts(seed_ids: list[int]) -> tuple[dict[int, dict], list[di # ── Prod ───────────────────────────────────────────────────────────────────── -_prod_ready = False - - def _ssh(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] + # El Hetzner nuevo usa auth por clave (ed25519 claude-code@feadulta); sshpass + # solo se antepone si hay contraseña configurada (servidor viejo CDMON). + if PROD_PASS: + cmd = ["sshpass", "-p", PROD_PASS, "ssh", "-o", "StrictHostKeyChecking=accept-new", + "-o", "ConnectTimeout=20", PROD_HOST, remote_cmd] + else: + cmd = ["ssh", "-o", "StrictHostKeyChecking=accept-new", + "-o", "ConnectTimeout=20", PROD_HOST, remote_cmd] return sh(cmd, stdin=stdin, timeout=timeout) def prod_helper(subcmd: str, *args: str, stdin: str | None = None) -> str: - global _prod_ready - if not _prod_ready: - _ssh(f"cat > {PROD_HELPER}", stdin=HELPER_SRC.read_text(encoding="utf-8")) - _prod_ready = True - inner = f"FEA_WP_LOAD={PROD_WPLOAD} php {PROD_HELPER} {subcmd} " + " ".join(args) + """Ejecuta el helper PHP en memoria; no crea ficheros temporales en prod.""" + argv = " ".join(shlex.quote(x) for x in (subcmd, *args)) + if PROD_DOCKER_CONTAINER: + inner = ( + f"docker exec -i -e FEA_WP_LOAD={shlex.quote(PROD_WPLOAD)} " + f"{shlex.quote(PROD_DOCKER_CONTAINER)} php -r {shlex.quote(HELPER_EVAL_CODE)} {argv}" + ) + else: + inner = ( + f"FEA_WP_LOAD={shlex.quote(PROD_WPLOAD)} " + f"php -r {shlex.quote(HELPER_EVAL_CODE)} {argv}" + ) return _ssh(inner, stdin=stdin, timeout=180) @@ -256,7 +288,12 @@ def deploy_fixed_ids(seed_ids: list[int], *, keep_existing: set[int], dry_run: b # ── Main legado ────────────────────────────────────────────────────────────── -def legacy_sync(limit: int, origin: int) -> int: +def legacy_sync(limit: int, origin: int, *, dry_run: bool = False) -> int: + """Sincroniza por origen ES+idioma, sin reutilizar IDs locales en prod. + + Este es el modo seguro si producción ha avanzado y sus IDs ya pueden + colisionar con traducciones creadas en el entorno local. + """ state = load_state() pairs = local_translation_pairs() if origin: @@ -280,6 +317,10 @@ def legacy_sync(limit: int, origin: int) -> int: if key in state["done"]: n_skip += 1 continue + if dry_run: + log(f" PLAN {key}: crearía traducción [{lang}] «{t['title'][:45]}»") + n_ok += 1 + continue try: new_id = prod_create(src_origin, lang, t["title"], t["content"]) state["done"][key] = new_id @@ -292,8 +333,10 @@ def legacy_sync(limit: int, origin: int) -> int: n_err += 1 log(f" {key} ERROR: {exc}") - save_state(state) - log(f"FIN sync legado. nuevos={n_ok} saltados={n_skip} errores={n_err}. Estado: {STATE_FILE}") + if not dry_run: + save_state(state) + mode = "DRY-RUN" if dry_run else "SYNC" + log(f"FIN sync legado {mode}. nuevos/plan={n_ok} saltados={n_skip} errores={n_err}. Estado: {STATE_FILE}") log("Recuerda en prod: ejecutar remap_translation_cats.php si alguna quedó sin categoría traducida.") return 0 @@ -318,7 +361,7 @@ def main() -> int: keep_existing = set(parse_csv_ints(args.keep_existing)) return deploy_fixed_ids(seed_ids, keep_existing=keep_existing, dry_run=args.dry_run) - return legacy_sync(args.limit, args.origin) + return legacy_sync(args.limit, args.origin, dry_run=args.dry_run) if __name__ == "__main__": diff --git a/scripts/translate_post.py b/scripts/translate_post.py index 4fe17b6..622aeae 100644 --- a/scripts/translate_post.py +++ b/scripts/translate_post.py @@ -271,7 +271,8 @@ def save_state(state: dict) -> None: # ── Orquestación ───────────────────────────────────────────────────────────── -def process_post(post_id: int, langs: list[str], status: str, force: bool, state: dict) -> None: +def process_post(post_id: int, langs: list[str], status: str, force: bool, state: dict, + *, dry_run: bool = False) -> None: src = read_post(post_id) if src.get("lang") and src["lang"] != "es": log(f"#{post_id} no es ES (lang={src['lang']}) — saltado") @@ -286,8 +287,14 @@ def process_post(post_id: int, langs: list[str], status: str, force: bool, state state["done"][key] = existing continue if existing and force: + if dry_run: + log(f" {lang}: PLAN regenerar traducción previa #{existing}") + continue php_helper("unlink", str(post_id), lang) log(f" {lang}: --force, eliminada traducción previa #{existing}") + if dry_run: + log(f" {lang}: PLAN traducir con {ENGINE} y crear como {status}") + continue try: t0 = time.time() title = translate_text(src["title"], lang, is_title=True) @@ -312,6 +319,7 @@ def main() -> int: ap.add_argument("--langs", default="en,fr,it,pt", help="Idiomas destino separados por coma.") ap.add_argument("--status", default="draft", choices=["draft", "publish"], help="Estado de la traducción.") ap.add_argument("--force", action="store_true", help="Regenera aunque ya exista la traducción.") + ap.add_argument("--dry-run", action="store_true", help="Muestra el plan sin llamar al LLM ni escribir en WordPress/estado.") args = ap.parse_args() langs = [l.strip() for l in args.langs.split(",") if l.strip() in LANG_NAMES] @@ -329,9 +337,11 @@ def main() -> int: state = load_state() for pid in ids: - process_post(pid, langs, args.status, args.force, state) - save_state(state) - log(f"FIN. {len(state['done'])} traducciones registradas, " + process_post(pid, langs, args.status, args.force, state, dry_run=args.dry_run) + if not args.dry_run: + save_state(state) + mode = "DRY-RUN" if args.dry_run else "FIN" + log(f"{mode}. {len(state['done'])} traducciones registradas, " f"{len(state.get('errors', {}))} errores. Estado: {STATE_FILE}") return 0 diff --git a/scripts/tts_produce.py b/scripts/tts_produce.py index 934b02c..1cee82b 100644 --- a/scripts/tts_produce.py +++ b/scripts/tts_produce.py @@ -29,7 +29,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import minimax_tts as mm # get_post_text, add_pauses, t2a, OUT import translate_post as tp # carta_article_ids -VOICE = "NicoFeadulta2026" +VOICE = os.environ.get("FEA_TTS_VOICE", "NicoFeadulta2026") MODEL = "speech-2.8-hd" CONTAINER = "wordpress-web" PROD = Path(__file__).resolve().parent.parent / "wordpress/wp-content/uploads/tts" @@ -103,6 +103,8 @@ def main(): help="para tras N audios OK en esta ejecución (0 = sin tope)") parser.add_argument("--dry-run", action="store_true", help="imprime la cola y sale, sin sintetizar ni gastar cuota") + parser.add_argument("--allow-default-voice", action="store_true", + help="permite Nico para autores sin voz digitalizada (desactivado por defecto)") args = parser.parse_args() if args.ids: os.environ["FEA_TTS_IDS"] = args.ids @@ -149,7 +151,15 @@ def main(): i += 1 continue - voice = mm.voice_for_author(author, VOICE) + cloned_voice = mm.voice_for_author(author, "") + if cloned_voice: + voice = cloned_voice + elif args.allow_default_voice: + voice = VOICE + else: + log(f"#{pid}: autor sin voz digitalizada (author_id={author}); omitido") + i += 1 + continue rc = mm.t2a(mm.add_pauses(text), voice, MODEL, f"prod-{pid}") if rc == 0: src = mm.OUT / f"prod-{pid}.mp3"