feat(lecturas): lote masivo de lecturas bíblicas vía índice del leccionario
build_lectionary_index.py construye un índice por referencia bíblica descargando un ciclo litúrgico de evangelizo.ws (combina idiomas por reading_code, robusto a fiestas trasladadas por país). lecturas_apply.py casa las lecturas ES por referencia y apply_lecturas_wp.php crea+asocia las traducciones en Polylang. 440/501 lecturas cubiertas y publicadas. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* apply_lecturas_wp.php — Crea las traducciones de lecturas bíblicas casadas por
|
||||||
|
* referencia (lecturas_apply.py) y las asocia en Polylang. Idempotente.
|
||||||
|
*
|
||||||
|
* Ejecutar dentro del contenedor:
|
||||||
|
* docker cp /tmp/lecturas_creadas.json wordpress-web:/tmp/
|
||||||
|
* docker exec wordpress-web wp eval-file /tmp/apply_lecturas_wp.php [publish]
|
||||||
|
*/
|
||||||
|
$status = (isset($argv[1]) && $argv[1] === 'publish') ? 'publish' : 'draft';
|
||||||
|
$data = json_decode(file_get_contents('/tmp/lecturas_creadas.json'), true);
|
||||||
|
$created = 0; $posts_done = 0; $skipped = 0;
|
||||||
|
foreach ($data as $row) {
|
||||||
|
$es_id = (int) $row['es_id'];
|
||||||
|
$es = get_post($es_id);
|
||||||
|
if (!$es) { $skipped++; continue; }
|
||||||
|
$existing = function_exists('pll_get_post_translations') ? pll_get_post_translations($es_id) : ['es' => $es_id];
|
||||||
|
$group = $existing;
|
||||||
|
$es_cats = wp_get_post_categories($es_id);
|
||||||
|
foreach (['en', 'fr', 'it', 'pt'] as $L) {
|
||||||
|
if (!empty($existing[$L])) { $group[$L] = $existing[$L]; continue; }
|
||||||
|
if (empty($row['langs'][$L])) continue;
|
||||||
|
$id = wp_insert_post([
|
||||||
|
'post_title' => $es->post_title, // referencia bíblica (igual en todos)
|
||||||
|
'post_content' => $row['langs'][$L],
|
||||||
|
'post_status' => $status,
|
||||||
|
'post_type' => 'post',
|
||||||
|
'comment_status' => 'closed',
|
||||||
|
], true);
|
||||||
|
if (is_wp_error($id)) continue;
|
||||||
|
pll_set_post_language($id, $L);
|
||||||
|
$cats = [];
|
||||||
|
foreach ($es_cats as $c) { $t = pll_get_term($c, $L); if ($t) $cats[] = $t; }
|
||||||
|
if ($cats) wp_set_post_categories($id, $cats);
|
||||||
|
$group[$L] = $id;
|
||||||
|
$created++;
|
||||||
|
}
|
||||||
|
if (function_exists('pll_save_post_translations')) pll_save_post_translations($group);
|
||||||
|
$posts_done++;
|
||||||
|
}
|
||||||
|
echo "posts ES procesados: $posts_done | traducciones creadas: $created | status=$status | skip=$skipped\n";
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
build_lectionary_index.py — Descarga el leccionario de evangelizo.ws para un rango de
|
||||||
|
fechas (un ciclo litúrgico completo cubre todas las lecturas) en es/en/fr/it/pt y
|
||||||
|
construye un índice POR REFERENCIA bíblica, para casar lecturas sin depender de fechas.
|
||||||
|
|
||||||
|
Salida: /tmp/lectionary_index.json { "LIBRO|cap|vers": {es,en,fr,it,pt: html} }
|
||||||
|
Cache por día/idioma en /tmp/evangelizo_cache (resumible).
|
||||||
|
|
||||||
|
Uso: python3 build_lectionary_index.py 2023-01-01 2025-12-31
|
||||||
|
"""
|
||||||
|
import sys, os, re, json, time, html, unicodedata, urllib.request
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
LANGS = {"SP": "es", "AM": "en", "FR": "fr", "IT": "it", "PT": "pt"}
|
||||||
|
CACHE = "/tmp/evangelizo_cache"
|
||||||
|
os.makedirs(CACHE, exist_ok=True)
|
||||||
|
INDEX = "/tmp/lectionary_index.json"
|
||||||
|
|
||||||
|
|
||||||
|
def norm_book(full_title):
|
||||||
|
# "Libro de Jeremías" / "Carta de san Pablo a los Romanos" -> "JEREMIAS"/"ROMANOS"
|
||||||
|
s = unicodedata.normalize("NFKD", full_title).encode("ascii", "ignore").decode().upper()
|
||||||
|
s = re.sub(r"[^A-Z0-9 ]", " ", s)
|
||||||
|
toks = [t for t in s.split() if t]
|
||||||
|
return toks[-1] if toks else ""
|
||||||
|
|
||||||
|
|
||||||
|
def clean(raw):
|
||||||
|
raw = html.unescape(raw or "")
|
||||||
|
raw = re.sub(r"\[\[[^\]]+\]\]", "", raw)
|
||||||
|
paras = [p.strip() for p in raw.split("\n") if p.strip()]
|
||||||
|
return "".join(f"<p>{p}</p>\n" for p in paras)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(date_s, lang_code):
|
||||||
|
fp = os.path.join(CACHE, f"{date_s}_{lang_code}.json")
|
||||||
|
if os.path.exists(fp):
|
||||||
|
try:
|
||||||
|
return json.load(open(fp))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
url = f"https://publication.evangelizo.ws/{lang_code}/days/{date_s}"
|
||||||
|
for a in range(3):
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": "fea-lect/1.0"})
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as r:
|
||||||
|
data = json.load(r)
|
||||||
|
out = []
|
||||||
|
for rd in data.get("data", {}).get("readings", []) or []:
|
||||||
|
out.append({
|
||||||
|
"code": rd.get("reading_code", ""),
|
||||||
|
"ref": (rd.get("reference_displayed") or "").strip().rstrip("."),
|
||||||
|
"book": (rd.get("book") or {}).get("full_title", ""),
|
||||||
|
"text": clean(rd.get("text", "")),
|
||||||
|
})
|
||||||
|
json.dump(out, open(fp, "w"), ensure_ascii=False)
|
||||||
|
time.sleep(0.15)
|
||||||
|
return out
|
||||||
|
except Exception:
|
||||||
|
if a == 2:
|
||||||
|
json.dump([], open(fp, "w"))
|
||||||
|
return []
|
||||||
|
time.sleep(1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def key_from(book_full, ref):
|
||||||
|
m = re.match(r"(\d{1,3})\s*,\s*(\d{1,3})", ref)
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
return f"{norm_book(book_full)}|{int(m.group(1))}|{int(m.group(2))}"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
d0 = date.fromisoformat(sys.argv[1])
|
||||||
|
d1 = date.fromisoformat(sys.argv[2])
|
||||||
|
days = (d1 - d0).days + 1
|
||||||
|
|
||||||
|
# Las fiestas trasladadas caen en fechas distintas por país/idioma → NO se puede
|
||||||
|
# casar dentro del mismo día. Indexamos por reading_code (estable entre idiomas)
|
||||||
|
# acumulando el texto de cada idioma desde CUALQUIER día donde aparezca.
|
||||||
|
code_text = {wl: {} for wl in LANGS.values()} # lang -> {code: text}
|
||||||
|
code_book = {} # code -> norm_book (del SP)
|
||||||
|
cur, n = d0, 0
|
||||||
|
while cur <= d1:
|
||||||
|
ds = cur.isoformat()
|
||||||
|
for lc, wl in LANGS.items():
|
||||||
|
for rd in fetch(ds, lc):
|
||||||
|
code = rd["code"]
|
||||||
|
if not code:
|
||||||
|
continue
|
||||||
|
if rd["text"] and code not in code_text[wl]:
|
||||||
|
code_text[wl][code] = rd["text"]
|
||||||
|
if wl == "es" and code not in code_book:
|
||||||
|
nb = norm_book(rd["book"])
|
||||||
|
m = re.search(r"(\d{1,3})\s*,\s*(\d{1,3})", code)
|
||||||
|
if nb and m:
|
||||||
|
code_book[code] = f"{nb}|{int(m.group(1))}|{int(m.group(2))}"
|
||||||
|
n += 1
|
||||||
|
if n % 60 == 0:
|
||||||
|
print(f" {n}/{days} días codes_es={len(code_text['es'])}", flush=True)
|
||||||
|
cur += timedelta(days=1)
|
||||||
|
|
||||||
|
# combinar: para cada code con clave-ES y texto en los 4 idiomas
|
||||||
|
index = {}
|
||||||
|
for code, key in code_book.items():
|
||||||
|
if key in index:
|
||||||
|
continue
|
||||||
|
entry = {}
|
||||||
|
ok = True
|
||||||
|
for wl in ("es", "en", "fr", "it", "pt"):
|
||||||
|
t = code_text[wl].get(code)
|
||||||
|
if not t:
|
||||||
|
ok = (wl == "es") and ok # es siempre presente; faltar otro descarta
|
||||||
|
if wl != "es":
|
||||||
|
ok = False
|
||||||
|
break
|
||||||
|
entry[wl] = t
|
||||||
|
if ok and all(l in entry for l in ("en", "fr", "it", "pt")):
|
||||||
|
index[key] = entry
|
||||||
|
json.dump(index, open(INDEX, "w"), ensure_ascii=False)
|
||||||
|
print(f"FIN. {n} días. codes_es={len(code_book)} → índice {len(index)} referencias en {INDEX}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
lecturas_apply.py — Casa las lecturas ES sin traducir contra el índice del leccionario
|
||||||
|
(build_lectionary_index.py) POR REFERENCIA bíblica y vuelca las traducciones a crear.
|
||||||
|
|
||||||
|
Entrada: /tmp/lectionary_index.json , /tmp/lecturas_todo.json
|
||||||
|
Salida: /tmp/lecturas_creadas.json (para que un wp eval cree+asocie+publique)
|
||||||
|
/tmp/lecturas_skip.json
|
||||||
|
|
||||||
|
Uso: python3 lecturas_apply.py [--limit N]
|
||||||
|
"""
|
||||||
|
import sys, re, json, unicodedata
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
# Alias de nombre de libro: feadulta -> token usado por evangelizo (último token full_title ES)
|
||||||
|
ALIAS = {
|
||||||
|
"HECHOS": "APOSTOLES", "HCH": "APOSTOLES",
|
||||||
|
"CANTAR": "CANTARES",
|
||||||
|
"APOC": "APOCALIPSIS", "AP": "APOCALIPSIS",
|
||||||
|
"QOHELET": "ECLESIASTES",
|
||||||
|
# abreviaturas litúrgicas
|
||||||
|
"MT": "MATEO", "MC": "MARCOS", "LC": "LUCAS", "JN": "JUAN",
|
||||||
|
"RM": "ROMANOS", "GA": "GALATAS", "EF": "EFESIOS", "FLP": "FILIPENSES",
|
||||||
|
"COL": "COLOSENSES", "HB": "HEBREOS", "ST": "SANTIAGO",
|
||||||
|
"IS": "ISAIAS", "JR": "JEREMIAS", "EZ": "EZEQUIEL", "GN": "GENESIS",
|
||||||
|
"EX": "EXODO", "DT": "DEUTERONOMIO", "SAL": "SALMOS", "PR": "PROVERBIOS",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def norm(s):
|
||||||
|
s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode().upper()
|
||||||
|
return re.sub(r"[^A-Z]", "", s) # solo letras → descarta el número del libro
|
||||||
|
|
||||||
|
|
||||||
|
def title_keys(title):
|
||||||
|
keys = []
|
||||||
|
for part in re.split(r"\s*/\s*", title):
|
||||||
|
m = re.search(r"([0-9]?\s*[A-Za-zÀ-ÿ][A-Za-zÀ-ÿ.\s]+?)\s+(\d{1,3})\s*,\s*(\d{1,3})", part)
|
||||||
|
if not m:
|
||||||
|
return None # parte no parseable → no casar el post entero
|
||||||
|
book = norm(m.group(1))
|
||||||
|
book = ALIAS.get(book, book)
|
||||||
|
keys.append(f"{book}|{int(m.group(2))}|{int(m.group(3))}")
|
||||||
|
return keys or None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
limit = 0
|
||||||
|
if "--limit" in sys.argv:
|
||||||
|
limit = int(sys.argv[sys.argv.index("--limit") + 1])
|
||||||
|
idx = json.load(open("/tmp/lectionary_index.json"))
|
||||||
|
todo = json.load(open("/tmp/lecturas_todo.json"))
|
||||||
|
if limit:
|
||||||
|
todo = todo[:limit]
|
||||||
|
|
||||||
|
creadas, skip = [], []
|
||||||
|
for t in todo:
|
||||||
|
keys = title_keys(t["title"])
|
||||||
|
if not keys:
|
||||||
|
skip.append({**t, "why": "título no parseable"})
|
||||||
|
continue
|
||||||
|
if not all(k in idx for k in keys):
|
||||||
|
missing = [k for k in keys if k not in idx]
|
||||||
|
skip.append({**t, "why": "ref no en índice", "missing": missing})
|
||||||
|
continue
|
||||||
|
langs = {}
|
||||||
|
for wl in ("en", "fr", "it", "pt"):
|
||||||
|
langs[wl] = "".join(idx[k][wl] for k in keys)
|
||||||
|
creadas.append({"es_id": t["id"], "title": t["title"], "langs": langs})
|
||||||
|
|
||||||
|
json.dump(creadas, open("/tmp/lecturas_creadas.json", "w"), ensure_ascii=False)
|
||||||
|
json.dump(skip, open("/tmp/lecturas_skip.json", "w"), ensure_ascii=False)
|
||||||
|
print(f"CASADAS: {len(creadas)} / {len(todo)} SKIP: {len(skip)}")
|
||||||
|
print("motivos skip:", dict(Counter(s["why"] for s in skip)))
|
||||||
|
# muestra de refs que faltan (para ampliar alias/rango)
|
||||||
|
missing = Counter()
|
||||||
|
for s in skip:
|
||||||
|
for k in s.get("missing", []):
|
||||||
|
missing[k.split("|")[0]] += 1
|
||||||
|
print("libros con más misses:", dict(missing.most_common(12)))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Lecturas sin traducir tras lote evangelizo (61) — ref no hallada en ciclo 2023-2025
|
||||||
|
1199 MATEO 27, 11-54 ref no en índice
|
||||||
|
52 JUAN 18, 1-40 / JUAN 19, 1-42 ref no en índice
|
||||||
|
286 DEUTERONOMIO, 2-3 y 14-16 / 1 CORINTIOS 10, 16-17 título no parseable
|
||||||
|
2988 JUAN 18, 28-40 ref no en índice
|
||||||
|
3478 LUCAS 19, 28-44 ref no en índice
|
||||||
|
2687 MATEO 21, 1-11 ref no en índice
|
||||||
|
5532 SANTIAGO 2, 1-9 ref no en índice
|
||||||
|
5539 PUNTOS DE REFLEXIÓN (Mt 23, 8-24) ref no en índice
|
||||||
|
5778 HECHOS 1, 12-14 ref no en índice
|
||||||
|
5842 1 PEDRO 1, 8 ref no en índice
|
||||||
|
5866 HECHOS 10, 37-39 ref no en índice
|
||||||
|
6000 GÁLATAS 5, 19-20, 22-23 ref no en índice
|
||||||
|
6015 ROMANOS 8, 15-17 ref no en índice
|
||||||
|
6087 SABIDURÍA 11, 23-26 y 12, 1-2 ref no en índice
|
||||||
|
57 MARCOS 16, 1-7 ref no en índice
|
||||||
|
4843 ÉXODO 15, 22 a 16, 35 ref no en índice
|
||||||
|
4846 HECHOS 2, 14-36 ref no en índice
|
||||||
|
6163 1 JUAN 4, 20-21 y 3, 16-18 ref no en índice
|
||||||
|
7661 1 REYES 17, 17-24 ref no en índice
|
||||||
|
7662 GÁLATAS 1, 11-19 ref no en índice
|
||||||
|
7704 ZACARÍAS 12, 10-11 y 13, 1 ref no en índice
|
||||||
|
7705 GÁLATAS 3, 26-29 ref no en índice
|
||||||
|
8152 MALAQUÍAS 4, 1-2 ref no en índice
|
||||||
|
4675 ISAÍAS 49, 14-15 ref no en índice
|
||||||
|
10527 SAMUEL 27, 4-7 ref no en índice
|
||||||
|
13022 HECHOS 4,33;5,12.27-33;12,2 ref no en índice
|
||||||
|
13289 DEUTERONOMIO 6, 1-9 ref no en índice
|
||||||
|
2217 LUCAS 3, 1-6 ref no en índice
|
||||||
|
7070 BARUC 5, 1-9 ref no en índice
|
||||||
|
7071 FILIPENSES 1, 3-11 ref no en índice
|
||||||
|
1996 LUCAS 4, 21-30 ref no en índice
|
||||||
|
7266 JEREMÍAS 1, 4-19 ref no en índice
|
||||||
|
7958 TIMOTEO 1, 12-17 ref no en índice
|
||||||
|
8105 SABIDURÍA 11, 23 - 12, 2 ref no en índice
|
||||||
|
8106 2 TESALONICENSES 1, 11 - 2, 2 ref no en índice
|
||||||
|
8131 2 TESALONICENSES 2, 15 - 3, 5 ref no en índice
|
||||||
|
11260 MALAQUIAS 3,19-20a ref no en índice
|
||||||
|
8195 ROMANOS 13, 11-14 ref no en índice
|
||||||
|
4701 GÉNESIS 2, 7-9 y 3, 1-7 ref no en índice
|
||||||
|
4932 1 PEDRO 2, 20-25 ref no en índice
|
||||||
|
5218 ROMANOS 8, 35-39 ref no en índice
|
||||||
|
9039 ECLESIÁSTICO 27, 33 a 28, 9 ref no en índice
|
||||||
|
5549 SABIDURÍA 6, 13-17 ref no en índice
|
||||||
|
5550 1 TESALONICENSES 4, 12-17 ref no en índice
|
||||||
|
5640 ISAÍAS 63, 16 a 64, 8 ref no en índice
|
||||||
|
5657 2 PEDRO 3, 8-14 ref no en índice
|
||||||
|
5954 LEVÍTICO 13. 1-2, 44-46 título no parseable
|
||||||
|
71 MARCOS 15, 1-39 ref no en índice
|
||||||
|
15823 SEGUNDO ‘ANUNCIO PASCUAL’: LO FEMENINO DEL ALMA DISPONE A LA EXPERIENCIA PASCUAL LC 24, 6-10 ref no en índice
|
||||||
|
6302 HECHOS 10, 25-48 ref no en índice
|
||||||
|
6644 1 REYES 19, 4-8 ref no en índice
|
||||||
|
10026 SANTIAGO 2, 1-5 ref no en índice
|
||||||
|
6818 NÚMEROS 11, 25-29 ref no en índice
|
||||||
|
6896 ISAÍAS 53, 10-11 ref no en índice
|
||||||
|
2849 MARCOS 10, 46-52 ref no en índice
|
||||||
|
16393 DEUTERONOMIO 6, 2-6 ref no en índice
|
||||||
|
6970 1 REYES 17, 10-16 ref no en índice
|
||||||
|
2001 LUCAS 1, 1-4 / LUCAS 4, 14-21 ref no en índice
|
||||||
|
7746 ISAÍAS 66, 10-14 ref no en índice
|
||||||
|
1648 LUCAS 17, 5-10 ref no en índice
|
||||||
|
17511 “NO NOS DEJES CAER EN LA TENTACIÓN” (MT 6, 13) ref no en índice
|
||||||
Reference in New Issue
Block a user