df7bf98ef1
- sync_translations_to_prod.py / sync_audio_to_prod.py / sync_carta_from_prod.py: migran de FEA_PROD_HOST/PASS (CDMON, password) a FEA_PROD_SSH_HOST/PASS + FEA_PROD_DOCKER_CONTAINER (Hetzner/Coolify, auth por clave), con wrapping docker exec y el fix del bug de redirecciones (wc -c/cat) resuelto en el host en vez de dentro del contenedor. - fea_translate_helper.php: subcomando clone_new para clonar en ID local nuevo cuando el ID de prod ya está ocupado localmente. - translate_post.py: --dry-run. - tts_produce.py: --allow-default-voice (voz Nico solo si se permite explícitamente para autores sin voz clonada) + fix voice_for_author. - minimax_tts.py: parámetro speed en t2a/_synth_chunk. - mirror-antiguo/deploy/nginx-mirror.conf: redirects de URLs históricas de catálogo y vista imprimible EFFA. - Importaciones humanas de Pagola (carta 738) + scripts/manifest de la fase A Enrique, y release_issue181_prod.sh / cdmon-retire-fewp1.sh. - .gitignore: excluye docs/backups/ (dumps SQL, mismo motivo que backups/*). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
152 lines
7.9 KiB
Python
152 lines
7.9 KiB
Python
#!/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"<p><strong>{html.escape(date_line)}</strong></p>",
|
|
f"<p><strong>{html.escape(bible)}</strong></p>"]
|
|
+ [f"<p>{html.escape(p)}</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 = '<p> </p>\n<p><span style="color: #ff0000;"><strong>Para unas eucaristías más participativas y actuales</strong></span></p>'
|
|
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'<p><strong><a href="{article_url}">Enrique Martínez Lozano: '
|
|
f'{html.escape(source["title"])}.</a></strong> {html.escape(source["intro"])}</p>')
|
|
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()
|