Apunta los scripts de sync prod al Hetzner nuevo y suma trabajo pendiente

- 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>
This commit is contained in:
2026-08-05 07:09:29 -04:00
parent 159080f0c8
commit df7bf98ef1
21 changed files with 980 additions and 110 deletions
+41
View File
@@ -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 <lang> <status>\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