#!/usr/bin/env python3 """ fix_titles.py Fixes wrong/contaminated/untranslated titles for translated WordPress posts. Translates only the title via Jan (fast, ~5s each). """ import pymysql import json import urllib.request import sys import time JAN_URL = "http://172.19.128.1:1337/v1/chat/completions" JAN_MODEL = "gemma-3-12b-it-Q4_K_M" DB_HOST = "172.18.0.2" DB_PORT = 3306 DB_NAME = "wordpress_db" DB_USER = "wordpress_user" DB_PASS = "wordpress_pass" TARGET_LANGS = {"en": "English", "fr": "French", "it": "Italian", "pt": "Portuguese"} # All posts needing title fix: post_id -> (lang, spanish_id, spanish_title) FIXES = { 43151: ("en", 42523, "LA TENTACIÓN"), 43281: ("fr", 42523, "LA TENTACIÓN"), 43150: ("en", 42524, "CUANDO NOS LEEMOS EN CLAVE DE CARENCIA"), 43280: ("fr", 42524, "CUANDO NOS LEEMOS EN CLAVE DE CARENCIA"), 43278: ("fr", 42525, "SE TRATA DE BUSCAR LO MEJOR PARA MÍ, AUNQUE ME CUESTE"), 43270: ("it", 42526, "PARA SER TENTADO"), 43269: ("pt", 42526, "PARA SER TENTADO"), 43143: ("en", 42531, "LA MAYOR TENTACIÓN HUMANA"), 43263: ("fr", 42531, "LA MAYOR TENTACIÓN HUMANA"), 43261: ("it", 42531, "LA MAYOR TENTACIÓN HUMANA"), 43256: ("fr", 42532, "MIÉRCOLES DE CENIZA"), 43260: ("it", 42532, "MIÉRCOLES DE CENIZA"), 43141: ("en", 42533, "1º DOMINGO DE CUARESMA"), 43259: ("it", 42533, "1º DOMINGO DE CUARESMA"), 43251: ("pt", 42533, "1º DOMINGO DE CUARESMA"), 43137: ("en", 42538, "ADÁN, EVA Y JESÚS FRENTE A LA TENTACIÓN"), 43240: ("fr", 42538, "ADÁN, EVA Y JESÚS FRENTE A LA TENTACIÓN"), 43236: ("pt", 42538, "ADÁN, EVA Y JESÚS FRENTE A LA TENTACIÓN"), 43135: ("en", 42544, "LO PROVISIONAL Y LO DEFINITIVO"), 43234: ("fr", 42544, "LO PROVISIONAL Y LO DEFINITIVO"), 43228: ("pt", 42544, "LO PROVISIONAL Y LO DEFINITIVO"), 43134: ("en", 42545, "2º DOMINGO DE CUARESMA"), 43232: ("fr", 42545, "2º DOMINGO DE CUARESMA"), 43226: ("pt", 42545, "2º DOMINGO DE CUARESMA"), 43225: ("pt", 42546, "POR LA RENUNCIA AL TRIUNFO"), 43132: ("en", 42547, "LO DIVINO ES NUESTRA ESENCIA"), 43233: ("it", 42547, "LO DIVINO ES NUESTRA ESENCIA"), 43131: ("en", 42548, "¡QUÉ BUENO ES QUE ESTEMOS AQUÍ!"), 43223: ("fr", 42548, "¡QUÉ BUENO ES QUE ESTEMOS AQUÍ!"), 43230: ("it", 42548, "¡QUÉ BUENO ES QUE ESTEMOS AQUÍ!"), 43216: ("pt", 42549, "¿A QUÉ TRANSFIGURACIÓN NOS ESTAMOS REFIRIENDO?"), 43129: ("en", 42555, "CUANDO NOS LEEMOS EN CLAVE DE PLENITUD"), 43211: ("fr", 42555, "CUANDO NOS LEEMOS EN CLAVE DE PLENITUD"), 43221: ("it", 42555, "CUANDO NOS LEEMOS EN CLAVE DE PLENITUD"), 43212: ("pt", 42555, "CUANDO NOS LEEMOS EN CLAVE DE PLENITUD"), 43128: ("en", 42556, "CUARESMA: CREER EN EL EVANGELIO"), 43208: ("fr", 42556, "CUARESMA: CREER EN EL EVANGELIO"), 43127: ("en", 42557, "LA CUARESMA COMO PEDAGOGÍA EN EL TIEMPO"), 43206: ("fr", 42557, "LA CUARESMA COMO PEDAGOGÍA EN EL TIEMPO"), 43217: ("it", 42557, "LA CUARESMA COMO PEDAGOGÍA EN EL TIEMPO"), 43205: ("pt", 42557, "LA CUARESMA COMO PEDAGOGÍA EN EL TIEMPO"), 43126: ("en", 42558, "¡NO TENEMOS UN DIOS VENGATIVO!"), 43124: ("en", 42560, 'CARLOS AGUIAR: "LA SINODALIDAD HA VENIDO A LA IGLESIA PARA QUEDARSE"'), 43123: ("en", 42561, "¿HERENCIA CRISTIANA?"), 43196: ("fr", 42561, "¿HERENCIA CRISTIANA?"), 43194: ("pt", 42561, "¿HERENCIA CRISTIANA?"), 43122: ("en", 42562, 'EL PAPA ADVIERTE A LOS CURAS DE LA "PANDEMIA" DEL CLERICALISMO'), 43120: ("en", 42564, "MOISÉS, LA SAMARITANA Y EL BORRACHO"), 43187: ("pt", 42564, "MOISÉS, LA SAMARITANA Y EL BORRACHO"), 43119: ("en", 42565, "EL FINAL DE LA BÚSQUEDA"), 43182: ("pt", 42565, "EL FINAL DE LA BÚSQUEDA"), 43174: ("fr", 42568, "EN EL POZO DE LA DIGNIDAD LIBERADA"), 43183: ("it", 42568, "EN EL POZO DE LA DIGNIDAD LIBERADA"), 43115: ("en", 42569, "PALABRA Y EUCARISTÍA"), 43171: ("fr", 42569, "PALABRA Y EUCARISTÍA"), 43172: ("pt", 42569, "PALABRA Y EUCARISTÍA"), 43167: ("fr", 42570, 'MABEL RUIZ: "LA TRADICIÓN HA UTILIZADO A LAS MUJERES PARA QUE SEAN SILENCIADAS"'), 43169: ("pt", 42570, 'MABEL RUIZ: "LA TRADICIÓN HA UTILIZADO A LAS MUJERES PARA QUE SEAN SILENCIADAS"'), 43113: ("en", 42571, 'LEÓN XIV, ANTE EL ATAQUE DE EEUU E ISRAEL CONTRA IRÁN: "HAY QUE DETENERLO"'), 43166: ("pt", 42571, 'LEÓN XIV, ANTE EL ATAQUE DE EEUU E ISRAEL CONTRA IRÁN: "HAY QUE DETENERLO"'), 43111: ("en", 42573, 'VICARIO GENERAL DE MOSCÚ: "LA GUERRA EN UCRANIA DEBE TERMINAR"'), 43104: ("pt", 42573, 'VICARIO GENERAL DE MOSCÚ: "LA GUERRA EN UCRANIA DEBE TERMINAR"'), 43163: ("pt", 42574, "SERVIR ES UNA FORMA DE LIDERAR"), 43156: ("pt", 42576, 'DIARMAID MACCULLOCH, HISTORIADOR: "NO EXISTE UNA ENSEÑANZA UNIFORME SOBRE SEXUALIDAD"'), 43155: ("pt", 42577, "3º DOMINGO DE CUARESMA"), } # Orphaned posts to delete (no Polylang link to any Spanish original) ORPHANS_TO_DELETE = [42581, 43130, 43235] def translate_title(spanish_title, lang_code, lang_name): payload = json.dumps({ "model": JAN_MODEL, "messages": [ {"role": "user", "content": f"Translate this title from Spanish to {lang_name}. Return ONLY the translated title in ALL CAPS, nothing else: {spanish_title}"} ], "temperature": 0.2, "max_tokens": 100, }).encode("utf-8") req = urllib.request.Request( JAN_URL, data=payload, headers={"Content-Type": "application/json", "Authorization": "Bearer dummy"}, method="POST" ) with urllib.request.urlopen(req, timeout=30) as r: result = json.loads(r.read()) return result["choices"][0]["message"]["content"].strip().strip('"').strip("'") def get_db(): return pymysql.connect( host=DB_HOST, port=DB_PORT, user=DB_USER, password=DB_PASS, database=DB_NAME, charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor ) def main(): db = get_db() c = db.cursor() # Delete orphans first print("Deleting orphaned posts...") for orphan_id in ORPHANS_TO_DELETE: cmd = f"docker exec wordpress-web wp post delete {orphan_id} --force --allow-root" import subprocess result = subprocess.run(cmd.split(), capture_output=True, text=True) print(f" Deleted {orphan_id}: {result.stdout.strip() or result.stderr.strip()}") print(f"\nFixing {len(FIXES)} titles...\n") done = 0 errors = 0 # Group by Spanish title to batch translate same title to multiple langs by_spanish = {} for post_id, (lang, sp_id, sp_title) in FIXES.items(): by_spanish.setdefault((sp_id, sp_title), []).append((post_id, lang)) translated_cache = {} # (sp_id, lang) -> translated_title for (sp_id, sp_title), targets in by_spanish.items(): print(f"ES:{sp_id} — {sp_title[:50]}") for post_id, lang in targets: lang_name = TARGET_LANGS[lang] cache_key = (sp_id, lang) if cache_key not in translated_cache: try: t0 = time.time() new_title = translate_title(sp_title, lang, lang_name) elapsed = time.time() - t0 translated_cache[cache_key] = new_title print(f" [{lang}] {new_title[:60]} ({elapsed:.0f}s)") except Exception as e: print(f" [{lang}] ERROR translating: {e}") errors += 1 continue new_title = translated_cache[cache_key] # Update the post title c.execute("UPDATE wp_posts SET post_title=%s WHERE ID=%s", (new_title, post_id)) db.commit() print(f" [{lang}] Updated {post_id}: {new_title[:60]}") done += 1 db.close() print(f"\nDone: {done} fixed, {errors} errors") if __name__ == "__main__": main()