quito comentarios
This commit is contained in:
parent
68a5528f2f
commit
937da3f90b
8 changed files with 48 additions and 496 deletions
|
|
@ -1,16 +0,0 @@
|
||||||
id,nombre
|
|
||||||
1,Ciencia
|
|
||||||
2,Cultura
|
|
||||||
3,Deportes
|
|
||||||
4,Economía
|
|
||||||
5,Educación
|
|
||||||
6,Entretenimiento
|
|
||||||
7,Internacional
|
|
||||||
8,Medio Ambiente
|
|
||||||
9,Moda
|
|
||||||
10,Opinión
|
|
||||||
11,Política
|
|
||||||
12,Salud
|
|
||||||
13,Sociedad
|
|
||||||
14,Tecnología
|
|
||||||
15,Viajes
|
|
||||||
|
|
|
@ -31,30 +31,41 @@ EVENT_BATCH_IDS = int(os.environ.get("EVENT_BATCH_IDS", "200"))
|
||||||
EVENT_SLEEP_IDLE = float(os.environ.get("EVENT_SLEEP_IDLE", "5.0"))
|
EVENT_SLEEP_IDLE = float(os.environ.get("EVENT_SLEEP_IDLE", "5.0"))
|
||||||
EVENT_DIST_THRESHOLD = float(os.environ.get("EVENT_DIST_THRESHOLD", "0.25"))
|
EVENT_DIST_THRESHOLD = float(os.environ.get("EVENT_DIST_THRESHOLD", "0.25"))
|
||||||
|
|
||||||
|
EMB_MODEL = os.environ.get(
|
||||||
|
"EMB_MODEL",
|
||||||
|
"sentence-transformers/paraphrase-multilingual-mpnet-base-v2",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_conn():
|
def get_conn():
|
||||||
return psycopg2.connect(**DB)
|
return psycopg2.connect(**DB)
|
||||||
|
|
||||||
|
|
||||||
def ensure_schema(conn):
|
def ensure_schema(conn):
|
||||||
|
"""
|
||||||
|
Asegura que la tabla de eventos y las columnas necesarias existen.
|
||||||
|
Aquí se asume el esquema original de eventos con centroid JSONB.
|
||||||
|
"""
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS eventos (
|
CREATE TABLE IF NOT EXISTS eventos (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
creado_en TIMESTAMP NOT NULL DEFAULT NOW(),
|
creado_en TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
actualizado_en TIMESTAMP NOT NULL DEFAULT NOW(),
|
actualizado_en TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
centroid JSONB NOT NULL,
|
centroid JSONB NOT NULL,
|
||||||
total_traducciones INTEGER NOT NULL DEFAULT 1
|
total_traducciones INTEGER NOT NULL DEFAULT 1
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
ALTER TABLE traducciones
|
ALTER TABLE traducciones
|
||||||
ADD COLUMN IF NOT EXISTS evento_id INTEGER REFERENCES eventos(id);
|
ADD COLUMN IF NOT EXISTS evento_id INTEGER REFERENCES eventos(id);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
CREATE INDEX IF NOT EXISTS idx_traducciones_evento
|
CREATE INDEX IF NOT EXISTS idx_traducciones_evento
|
||||||
|
|
@ -67,6 +78,7 @@ def ensure_schema(conn):
|
||||||
ON traducciones(evento_id, noticia_id);
|
ON traducciones(evento_id, noticia_id);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
CREATE OR REPLACE FUNCTION actualizar_evento_modificado()
|
CREATE OR REPLACE FUNCTION actualizar_evento_modificado()
|
||||||
|
|
@ -91,43 +103,55 @@ def ensure_schema(conn):
|
||||||
|
|
||||||
|
|
||||||
def fetch_pending_traducciones(conn) -> List[int]:
|
def fetch_pending_traducciones(conn) -> List[int]:
|
||||||
|
"""
|
||||||
|
Traducciones con status 'done', sin evento asignado
|
||||||
|
y que ya tienen embedding en traduccion_embeddings para EMB_MODEL.
|
||||||
|
"""
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT t.id
|
SELECT t.id
|
||||||
FROM traducciones t
|
FROM traducciones t
|
||||||
JOIN embeddings e ON e.traduccion_id = t.id
|
JOIN traduccion_embeddings e
|
||||||
|
ON e.traduccion_id = t.id
|
||||||
|
AND e.model = %s
|
||||||
WHERE t.status = 'done'
|
WHERE t.status = 'done'
|
||||||
AND t.evento_id IS NULL
|
AND t.evento_id IS NULL
|
||||||
AND t.lang_to = ANY(%s)
|
AND t.lang_to = ANY(%s)
|
||||||
ORDER BY t.id DESC
|
ORDER BY t.id DESC
|
||||||
LIMIT %s;
|
LIMIT %s;
|
||||||
""",
|
""",
|
||||||
(EVENT_LANGS, EVENT_BATCH_IDS),
|
(EMB_MODEL, EVENT_LANGS, EVENT_BATCH_IDS),
|
||||||
)
|
)
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
return [r[0] for r in rows]
|
return [r[0] for r in rows]
|
||||||
|
|
||||||
|
|
||||||
def fetch_embeddings_for(conn, tr_ids: List[int]) -> Dict[int, np.ndarray]:
|
def fetch_embeddings_for(conn, tr_ids: List[int]) -> Dict[int, np.ndarray]:
|
||||||
|
"""
|
||||||
|
Devuelve un diccionario {traduccion_id: vector_numpy}
|
||||||
|
leyendo de traduccion_embeddings.embedding para el EMB_MODEL.
|
||||||
|
"""
|
||||||
if not tr_ids:
|
if not tr_ids:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT traduccion_id, vec
|
SELECT traduccion_id, embedding
|
||||||
FROM embeddings
|
FROM traduccion_embeddings
|
||||||
WHERE traduccion_id = ANY(%s);
|
WHERE traduccion_id = ANY(%s)
|
||||||
|
AND model = %s;
|
||||||
""",
|
""",
|
||||||
(tr_ids,),
|
(tr_ids, EMB_MODEL),
|
||||||
)
|
)
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
|
|
||||||
out: Dict[int, np.ndarray] = {}
|
out: Dict[int, np.ndarray] = {}
|
||||||
for tr_id, vec in rows:
|
for tr_id, emb in rows:
|
||||||
if not vec:
|
if not emb:
|
||||||
continue
|
continue
|
||||||
arr = np.array([float(x or 0.0) for x in vec], dtype="float32")
|
arr = np.array([float(x or 0.0) for x in emb], dtype="float32")
|
||||||
if arr.size == 0:
|
if arr.size == 0:
|
||||||
continue
|
continue
|
||||||
out[int(tr_id)] = arr
|
out[int(tr_id)] = arr
|
||||||
|
|
@ -135,6 +159,9 @@ def fetch_embeddings_for(conn, tr_ids: List[int]) -> Dict[int, np.ndarray]:
|
||||||
|
|
||||||
|
|
||||||
def fetch_centroids(conn) -> List[Dict[str, Any]]:
|
def fetch_centroids(conn) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Carga todos los centroides actuales desde eventos.
|
||||||
|
"""
|
||||||
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
|
|
@ -180,6 +207,10 @@ def assign_to_event(
|
||||||
vec: np.ndarray,
|
vec: np.ndarray,
|
||||||
centroids: List[Dict[str, Any]],
|
centroids: List[Dict[str, Any]],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
Asigna una traducción a un evento existente (si distancia <= umbral)
|
||||||
|
o crea un evento nuevo con este vector como centroide.
|
||||||
|
"""
|
||||||
from psycopg2.extras import Json
|
from psycopg2.extras import Json
|
||||||
|
|
||||||
if vec is None or vec.size == 0:
|
if vec is None or vec.size == 0:
|
||||||
|
|
@ -260,11 +291,12 @@ def assign_to_event(
|
||||||
def main():
|
def main():
|
||||||
log.info(
|
log.info(
|
||||||
"Iniciando cluster_worker eventos "
|
"Iniciando cluster_worker eventos "
|
||||||
"(EVENT_LANGS=%s, BATCH_IDS=%s, DIST_THRESHOLD=%.3f, SLEEP=%.1fs)",
|
"(EVENT_LANGS=%s, BATCH_IDS=%s, DIST_THRESHOLD=%.3f, SLEEP=%.1fs, EMB_MODEL=%s)",
|
||||||
",".join(EVENT_LANGS),
|
",".join(EVENT_LANGS),
|
||||||
EVENT_BATCH_IDS,
|
EVENT_BATCH_IDS,
|
||||||
EVENT_DIST_THRESHOLD,
|
EVENT_DIST_THRESHOLD,
|
||||||
EVENT_SLEEP_IDLE,
|
EVENT_SLEEP_IDLE,
|
||||||
|
EMB_MODEL,
|
||||||
)
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
id,nombre
|
|
||||||
1,África
|
|
||||||
2,América
|
|
||||||
3,Asia
|
|
||||||
4,Europa
|
|
||||||
5,Oceanía
|
|
||||||
6,Antártida
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ import nltk
|
||||||
import logging
|
import logging
|
||||||
import ssl
|
import ssl
|
||||||
|
|
||||||
# Soluciona problemas de certificado SSL en algunas configuraciones de sistema al descargar
|
|
||||||
try:
|
try:
|
||||||
_create_unverified_https_context = ssl._create_unverified_context
|
_create_unverified_https_context = ssl._create_unverified_context
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
|
@ -12,31 +11,22 @@ else:
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
|
||||||
|
|
||||||
# Lista de paquetes de NLTK que newspaper3k puede necesitar.
|
|
||||||
# 'punkt' y 'punkt_tab' son para tokenización, 'stopwords' para el resumen.
|
|
||||||
PACKAGES = ['punkt', 'punkt_tab', 'stopwords']
|
PACKAGES = ['punkt', 'punkt_tab', 'stopwords']
|
||||||
|
|
||||||
|
|
||||||
def download_nltk_data():
|
def download_nltk_data():
|
||||||
"""
|
|
||||||
Descarga los paquetes de NLTK necesarios para newspaper3k.
|
|
||||||
"""
|
|
||||||
for package in PACKAGES:
|
for package in PACKAGES:
|
||||||
try:
|
try:
|
||||||
logging.info(f"Verificando si el paquete '{package}' de NLTK está disponible...")
|
logging.info(f"Verificando si el paquete '{package}' de NLTK está disponible...")
|
||||||
# Determina la ruta correcta para la verificación
|
|
||||||
if package.startswith('punkt'):
|
if package.startswith('punkt'):
|
||||||
path = f'tokenizers/{package}'
|
path = f'tokenizers/{package}'
|
||||||
else:
|
else:
|
||||||
path = f'corpora/{package}'
|
path = f'corpora/{package}'
|
||||||
|
|
||||||
nltk.data.find(path)
|
nltk.data.find(path)
|
||||||
logging.info(f"El paquete '{package}' ya está descargado.")
|
logging.info(f"El paquete '{package}' ya está descargado.")
|
||||||
|
|
||||||
except LookupError:
|
except LookupError:
|
||||||
logging.info(f"El paquete '{package}' no se encontró. Iniciando descarga...")
|
logging.info(f"El paquete '{package}' no se encontró. Iniciando descarga...")
|
||||||
try:
|
try:
|
||||||
# El parámetro quiet=True evita el diálogo interactivo
|
|
||||||
nltk.download(package, quiet=True)
|
nltk.download(package, quiet=True)
|
||||||
logging.info(f"Paquete '{package}' descargado con éxito.")
|
logging.info(f"Paquete '{package}' descargado con éxito.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -44,6 +34,7 @@ def download_nltk_data():
|
||||||
import sys
|
import sys
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
download_nltk_data()
|
download_nltk_data()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import torch
|
||||||
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] %(levelname)s: %(message)s')
|
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] %(levelname)s: %(message)s')
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
# ---------- Configuración DB ----------
|
|
||||||
DB = dict(
|
DB = dict(
|
||||||
host=os.environ.get("DB_HOST", "localhost"),
|
host=os.environ.get("DB_HOST", "localhost"),
|
||||||
port=int(os.environ.get("DB_PORT", 5432)),
|
port=int(os.environ.get("DB_PORT", 5432)),
|
||||||
|
|
@ -22,31 +21,22 @@ DB = dict(
|
||||||
password=os.environ.get("DB_PASS", "x"),
|
password=os.environ.get("DB_PASS", "x"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------- Parámetros de worker ----------
|
|
||||||
# Modelo por defecto: multilingüe, bueno para muchas lenguas
|
|
||||||
EMB_MODEL = os.environ.get(
|
EMB_MODEL = os.environ.get(
|
||||||
"EMB_MODEL",
|
"EMB_MODEL",
|
||||||
"sentence-transformers/paraphrase-multilingual-mpnet-base-v2",
|
"sentence-transformers/paraphrase-multilingual-mpnet-base-v2",
|
||||||
)
|
)
|
||||||
EMB_BATCH = int(os.environ.get("EMB_BATCH", "128"))
|
EMB_BATCH = int(os.environ.get("EMB_BATCH", "128"))
|
||||||
SLEEP_IDLE = float(os.environ.get("EMB_SLEEP_IDLE", "5.0"))
|
SLEEP_IDLE = float(os.environ.get("EMB_SLEEP_IDLE", "5.0"))
|
||||||
# Filtrado por idiomas destino (coma-separado). Por defecto sólo 'es'
|
|
||||||
EMB_LANGS = [s.strip() for s in os.environ.get("EMB_LANGS", "es").split(",") if s.strip()]
|
EMB_LANGS = [s.strip() for s in os.environ.get("EMB_LANGS", "es").split(",") if s.strip()]
|
||||||
|
|
||||||
# DEVICE_ENV: 'auto' | 'cpu' | 'cuda'
|
|
||||||
DEVICE_ENV = os.environ.get("DEVICE", "auto").lower()
|
DEVICE_ENV = os.environ.get("DEVICE", "auto").lower()
|
||||||
|
|
||||||
# Límite por iteración (para no tragar toda la tabla de golpe)
|
|
||||||
EMB_LIMIT = int(os.environ.get("EMB_LIMIT", "1000"))
|
EMB_LIMIT = int(os.environ.get("EMB_LIMIT", "1000"))
|
||||||
|
|
||||||
|
|
||||||
# ---------- Utilidades ----------
|
|
||||||
def get_conn():
|
def get_conn():
|
||||||
return psycopg2.connect(**DB)
|
return psycopg2.connect(**DB)
|
||||||
|
|
||||||
|
|
||||||
def ensure_schema(conn):
|
def ensure_schema(conn):
|
||||||
"""Crea la tabla de embeddings para traducciones si no existe."""
|
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
|
|
@ -61,21 +51,12 @@ def ensure_schema(conn):
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
# Alineado con init-db/08-embeddings.sql
|
cur.execute("CREATE INDEX IF NOT EXISTS idx_tr_emb_model ON traduccion_embeddings(model);")
|
||||||
cur.execute(
|
cur.execute("CREATE INDEX IF NOT EXISTS idx_tr_emb_traduccion_id ON traduccion_embeddings(traduccion_id);")
|
||||||
"CREATE INDEX IF NOT EXISTS idx_tr_emb_model ON traduccion_embeddings(model);"
|
|
||||||
)
|
|
||||||
cur.execute(
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_tr_emb_traduccion_id ON traduccion_embeddings(traduccion_id);"
|
|
||||||
)
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
def fetch_batch_pending(conn) -> List[psycopg2.extras.DictRow]:
|
def fetch_batch_pending(conn) -> List[psycopg2.extras.DictRow]:
|
||||||
"""
|
|
||||||
Devuelve un lote de traducciones 'done' del/los idioma(s) objetivo
|
|
||||||
que no tienen embedding aún para el EMB_MODEL indicado.
|
|
||||||
"""
|
|
||||||
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
|
|
@ -96,15 +77,10 @@ def fetch_batch_pending(conn) -> List[psycopg2.extras.DictRow]:
|
||||||
""",
|
""",
|
||||||
(EMB_MODEL, EMB_LANGS, EMB_LIMIT),
|
(EMB_MODEL, EMB_LANGS, EMB_LIMIT),
|
||||||
)
|
)
|
||||||
rows = cur.fetchall()
|
return cur.fetchall()
|
||||||
return rows
|
|
||||||
|
|
||||||
|
|
||||||
def texts_from_rows(rows: List[psycopg2.extras.DictRow]) -> List[str]:
|
def texts_from_rows(rows: List[psycopg2.extras.DictRow]) -> List[str]:
|
||||||
"""
|
|
||||||
Compone el texto a vectorizar por cada traducción:
|
|
||||||
'titulo_trad' + '\n' + 'resumen_trad'. Si alguno falta, usa lo disponible.
|
|
||||||
"""
|
|
||||||
texts: List[str] = []
|
texts: List[str] = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
title = (r["titulo_trad"] or "").strip()
|
title = (r["titulo_trad"] or "").strip()
|
||||||
|
|
@ -117,15 +93,11 @@ def texts_from_rows(rows: List[psycopg2.extras.DictRow]) -> List[str]:
|
||||||
|
|
||||||
|
|
||||||
def upsert_embeddings(conn, rows, embs: np.ndarray, model_name: str):
|
def upsert_embeddings(conn, rows, embs: np.ndarray, model_name: str):
|
||||||
"""
|
|
||||||
Inserta/actualiza embeddings por traducción en lote (batch insert).
|
|
||||||
"""
|
|
||||||
if embs.size == 0 or not rows:
|
if embs.size == 0 or not rows:
|
||||||
return
|
return
|
||||||
|
|
||||||
dim = int(embs.shape[1])
|
dim = int(embs.shape[1])
|
||||||
|
|
||||||
# Preparamos los datos para execute_values
|
|
||||||
data = [
|
data = [
|
||||||
(
|
(
|
||||||
int(r["traduccion_id"]),
|
int(r["traduccion_id"]),
|
||||||
|
|
@ -152,7 +124,6 @@ def upsert_embeddings(conn, rows, embs: np.ndarray, model_name: str):
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
# ---------- Main loop ----------
|
|
||||||
def main():
|
def main():
|
||||||
log.info("Arrancando embeddings_worker para TRADUCCIONES")
|
log.info("Arrancando embeddings_worker para TRADUCCIONES")
|
||||||
log.info(
|
log.info(
|
||||||
|
|
|
||||||
223
install.sh
223
install.sh
|
|
@ -1,223 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
APP_NAME="rss"
|
|
||||||
DB_NAME="rss"
|
|
||||||
DB_USER="rss"
|
|
||||||
APP_USER="x"
|
|
||||||
APP_DIR=$(pwd)
|
|
||||||
PYTHON_ENV="$APP_DIR/venv"
|
|
||||||
WSGI_APP_ENTRY="app:app"
|
|
||||||
WEB_PORT=8000
|
|
||||||
|
|
||||||
echo "🟢 Paso 0: Verificaciones y confirmación de seguridad"
|
|
||||||
if [[ $EUID -ne 0 ]]; then
|
|
||||||
echo "❌ Este script debe ser ejecutado como root (usa sudo)."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "------------------------------------------------------------------"
|
|
||||||
echo "⚠️ ADVERTENCIA: Este script realizará las siguientes acciones DESTRUCTIVAS:"
|
|
||||||
echo " - Eliminará TODOS los servicios systemd que empiecen por '$APP_NAME'."
|
|
||||||
echo " - Eliminará PERMANENTEMENTE la base de datos '$DB_NAME'."
|
|
||||||
echo " - Eliminará PERMANENTEMENTE el usuario de base de datos '$DB_USER'."
|
|
||||||
echo "------------------------------------------------------------------"
|
|
||||||
read -p "Estás seguro de que quieres continuar? (escribe 'si' para confirmar): " CONFIRM
|
|
||||||
if [ "$CONFIRM" != "si" ]; then
|
|
||||||
echo "Operación cancelada por el usuario."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
read -sp "🔑 Introduce la contraseña para el usuario de la base de datos '$DB_USER' (se creará de nuevo): " DB_PASS
|
|
||||||
echo
|
|
||||||
if [ -z "$DB_PASS" ]; then
|
|
||||||
echo "❌ La contraseña no puede estar vacía. Abortando."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "🧹 Paso 0.5: Limpiando instalación anterior..."
|
|
||||||
echo " -> Buscando y eliminando servicios systemd antiguos..."
|
|
||||||
for service in $(systemctl list-unit-files | grep "^$APP_NAME" | cut -d' ' -f1); do
|
|
||||||
echo " -> Deteniendo y deshabilitando $service"
|
|
||||||
systemctl stop "$service" || true
|
|
||||||
systemctl disable "$service" || true
|
|
||||||
done
|
|
||||||
rm -f /etc/systemd/system/$APP_NAME*
|
|
||||||
systemctl daemon-reload
|
|
||||||
echo " -> Servicios systemd limpiados."
|
|
||||||
|
|
||||||
echo "🟢 Paso 1: Instalando dependencias del sistema..."
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y wget ca-certificates postgresql postgresql-contrib python3-venv python3-pip python3-dev libpq-dev gunicorn
|
|
||||||
|
|
||||||
echo "🔥 Paso 2: Eliminando y recreando la base de datos y el usuario..."
|
|
||||||
## CORRECCIÓN: Se exporta PGPASSWORD para evitar exponer la contraseña en la línea de comandos
|
|
||||||
## y se agrupa con la del paso 4 para mayor eficiencia.
|
|
||||||
export PGPASSWORD="$DB_PASS"
|
|
||||||
|
|
||||||
sudo -u postgres psql -c "DROP DATABASE IF EXISTS $DB_NAME;"
|
|
||||||
sudo -u postgres psql -c "DROP USER IF EXISTS $DB_USER;"
|
|
||||||
echo " -> Entidades de BD anteriores eliminadas."
|
|
||||||
sudo -u postgres psql -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';"
|
|
||||||
sudo -u postgres psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;"
|
|
||||||
echo "✅ Base de datos y usuario recreados con éxito."
|
|
||||||
|
|
||||||
echo "🐍 Paso 3: Configurando el entorno de la aplicación..."
|
|
||||||
if ! id "$APP_USER" &>/dev/null; then
|
|
||||||
echo "👤 Creando usuario del sistema '$APP_USER'..."
|
|
||||||
useradd -m -s /bin/bash "$APP_USER"
|
|
||||||
else
|
|
||||||
echo "✅ Usuario del sistema '$APP_USER' ya existe."
|
|
||||||
fi
|
|
||||||
chown -R "$APP_USER":"$APP_USER" "$APP_DIR"
|
|
||||||
|
|
||||||
sudo -u "$APP_USER" bash <<EOF
|
|
||||||
set -e
|
|
||||||
cd "$APP_DIR"
|
|
||||||
echo " -> Creando entorno virtual en $PYTHON_ENV"
|
|
||||||
rm -rf "$PYTHON_ENV"
|
|
||||||
python3 -m venv "$PYTHON_ENV"
|
|
||||||
echo " -> Instalando dependencias desde requirements.txt..."
|
|
||||||
"$PYTHON_ENV/bin/python" -m pip install --upgrade pip
|
|
||||||
if [ -f "requirements.txt" ]; then
|
|
||||||
"$PYTHON_ENV/bin/python" -m pip install -r "requirements.txt"
|
|
||||||
else
|
|
||||||
echo "⚠️ ADVERTENCIA: No se encontró requirements.txt."
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "🧠 Paso 3.5: Descargando modelos de lenguaje para Newspaper3k..."
|
|
||||||
if [ -f "download_models.py" ]; then
|
|
||||||
sudo -u "$APP_USER" "$PYTHON_ENV/bin/python" "$APP_DIR/download_models.py"
|
|
||||||
echo "✅ Modelos NLP verificados/descargados."
|
|
||||||
else
|
|
||||||
echo "⚠️ ADVERTENCIA: No se encontró download_models.py. El scraping de URLs puede fallar."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "📐 Paso 4: Creando esquema de BD y sembrando datos..."
|
|
||||||
# La variable PGPASSWORD ya está exportada desde el paso 2
|
|
||||||
|
|
||||||
psql -U "$DB_USER" -h localhost -d "$DB_NAME" <<SQL
|
|
||||||
CREATE TABLE IF NOT EXISTS continentes (id SERIAL PRIMARY KEY, nombre VARCHAR(50) NOT NULL UNIQUE);
|
|
||||||
CREATE TABLE IF NOT EXISTS categorias (id SERIAL PRIMARY KEY, nombre VARCHAR(100) NOT NULL UNIQUE);
|
|
||||||
CREATE TABLE IF NOT EXISTS paises (id SERIAL PRIMARY KEY, nombre VARCHAR(100) NOT NULL UNIQUE, continente_id INTEGER REFERENCES continentes(id) ON DELETE SET NULL);
|
|
||||||
CREATE TABLE IF NOT EXISTS feeds (id SERIAL PRIMARY KEY, nombre VARCHAR(255), descripcion TEXT, url TEXT NOT NULL UNIQUE, categoria_id INTEGER REFERENCES categorias(id) ON DELETE SET NULL, pais_id INTEGER REFERENCES paises(id) ON DELETE SET NULL, idioma CHAR(2), activo BOOLEAN DEFAULT TRUE, fallos INTEGER DEFAULT 0, last_etag TEXT, last_modified TEXT);
|
|
||||||
CREATE TABLE IF NOT EXISTS fuentes_url (id SERIAL PRIMARY KEY, nombre VARCHAR(255) NOT NULL, url TEXT NOT NULL UNIQUE, categoria_id INTEGER REFERENCES categorias(id) ON DELETE SET NULL, pais_id INTEGER REFERENCES paises(id) ON DELETE SET NULL, idioma CHAR(2) DEFAULT 'es');
|
|
||||||
CREATE TABLE IF NOT EXISTS noticias (id VARCHAR(32) PRIMARY KEY, titulo TEXT, resumen TEXT, url TEXT NOT NULL UNIQUE, fecha TIMESTAMP, imagen_url TEXT, fuente_nombre VARCHAR(255), categoria_id INTEGER REFERENCES categorias(id) ON DELETE SET NULL, pais_id INTEGER REFERENCES paises(id) ON DELETE SET NULL, tsv tsvector);
|
|
||||||
ALTER TABLE noticias ADD COLUMN IF NOT EXISTS tsv tsvector;
|
|
||||||
CREATE OR REPLACE FUNCTION noticias_tsv_trigger() RETURNS trigger AS \$\$ BEGIN new.tsv := setweight(to_tsvector('spanish', coalesce(new.titulo,'')), 'A') || setweight(to_tsvector('spanish', coalesce(new.resumen,'')), 'B'); return new; END \$\$ LANGUAGE plpgsql;
|
|
||||||
DROP TRIGGER IF EXISTS tsvectorupdate ON noticias;
|
|
||||||
CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE ON noticias FOR EACH ROW EXECUTE PROCEDURE noticias_tsv_trigger();
|
|
||||||
CREATE INDEX IF NOT EXISTS noticias_tsv_idx ON noticias USING gin(tsv);
|
|
||||||
SQL
|
|
||||||
|
|
||||||
echo " -> Buscando archivos .sql para sembrar datos..."
|
|
||||||
if [ -f "continentes.sql" ]; then
|
|
||||||
echo " -> Cargando continentes.sql..."
|
|
||||||
psql -U "$DB_USER" -h localhost -d "$DB_NAME" -f "continentes.sql"
|
|
||||||
fi
|
|
||||||
if [ -f "categorias.sql" ]; then
|
|
||||||
echo " -> Cargando categorias.sql..."
|
|
||||||
psql -U "$DB_USER" -h localhost -d "$DB_NAME" -f "categorias.sql"
|
|
||||||
fi
|
|
||||||
if [ -f "paises.sql" ]; then
|
|
||||||
echo " -> Cargando paises.sql..."
|
|
||||||
psql -U "$DB_USER" -h localhost -d "$DB_NAME" -f "paises.sql"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " -> Actualizando contadores de secuencias de la base de datos..."
|
|
||||||
psql -U "$DB_USER" -h localhost -d "$DB_NAME" <<SQL
|
|
||||||
SELECT setval('categorias_id_seq', (SELECT MAX(id) FROM categorias), true) WHERE (SELECT MAX(id) FROM categorias) IS NOT NULL;
|
|
||||||
SELECT setval('continentes_id_seq', (SELECT MAX(id) FROM continentes), true) WHERE (SELECT MAX(id) FROM continentes) IS NOT NULL;
|
|
||||||
SELECT setval('paises_id_seq', (SELECT MAX(id) FROM paises), true) WHERE (SELECT MAX(id) FROM paises) IS NOT NULL;
|
|
||||||
SQL
|
|
||||||
|
|
||||||
unset PGPASSWORD
|
|
||||||
echo "✅ Esquema de base de datos y datos iniciales configurados."
|
|
||||||
|
|
||||||
## CORRECCIÓN: Se elimina el Paso 5, ya no es necesario crear el worker.py dinámicamente.
|
|
||||||
|
|
||||||
echo "⚙️ Paso 6: Creando nuevos archivos de servicio systemd..."
|
|
||||||
cat <<EOF > /etc/systemd/system/$APP_NAME.service
|
|
||||||
[Unit]
|
|
||||||
Description=Gunicorn instance to serve $APP_NAME
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
User=$APP_USER
|
|
||||||
Group=$APP_USER
|
|
||||||
WorkingDirectory=$APP_DIR
|
|
||||||
Environment="PATH=$PYTHON_ENV/bin"
|
|
||||||
Environment="SECRET_KEY=$(python3 -c 'import os; print(os.urandom(24).hex())')"
|
|
||||||
Environment="DB_HOST=localhost"
|
|
||||||
Environment="DB_PORT=5432"
|
|
||||||
Environment="DB_NAME=$DB_NAME"
|
|
||||||
Environment="DB_USER=$DB_USER"
|
|
||||||
Environment="DB_PASS=$DB_PASS"
|
|
||||||
ExecStart=$PYTHON_ENV/bin/gunicorn --workers 3 --bind 0.0.0.0:$WEB_PORT --timeout 120 $WSGI_APP_ENTRY
|
|
||||||
Restart=always
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
## CORRECCIÓN: Se elimina la creación de rss-worker.service y rss-worker.timer.
|
|
||||||
## Se añade el nuevo servicio para el planificador persistente scheduler.py.
|
|
||||||
cat <<EOF > /etc/systemd/system/$APP_NAME-scheduler.service
|
|
||||||
[Unit]
|
|
||||||
Description=$APP_NAME Scheduler Worker
|
|
||||||
After=postgresql.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=$APP_USER
|
|
||||||
Group=$APP_USER
|
|
||||||
WorkingDirectory=$APP_DIR
|
|
||||||
Environment="PATH=$PYTHON_ENV/bin"
|
|
||||||
Environment="SECRET_KEY=$(python3 -c 'import os; print(os.urandom(24).hex())')"
|
|
||||||
Environment="DB_HOST=localhost"
|
|
||||||
Environment="DB_PORT=5432"
|
|
||||||
Environment="DB_NAME=$DB_NAME"
|
|
||||||
Environment="DB_USER=$DB_USER"
|
|
||||||
Environment="DB_PASS=$DB_PASS"
|
|
||||||
ExecStart=$PYTHON_ENV/bin/python $APP_DIR/scheduler.py
|
|
||||||
Restart=always
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "✅ Archivos de servicio creados."
|
|
||||||
|
|
||||||
echo "🚀 Paso 7: Recargando, habilitando, arrancando servicios y configurando firewall..."
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl enable $APP_NAME.service
|
|
||||||
systemctl start $APP_NAME.service
|
|
||||||
|
|
||||||
## CORRECCIÓN: Se habilitan e inician el nuevo servicio del planificador.
|
|
||||||
systemctl enable $APP_NAME-scheduler.service
|
|
||||||
systemctl start $APP_NAME-scheduler.service
|
|
||||||
|
|
||||||
if command -v ufw &> /dev/null && ufw status | grep -q 'Status: active'; then
|
|
||||||
echo " -> Firewall UFW detectado. Abriendo puerto $WEB_PORT..."
|
|
||||||
ufw allow $WEB_PORT/tcp
|
|
||||||
ufw reload
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🎉 ¡REINSTALACIÓN COMPLETADA!"
|
|
||||||
echo "--------------------------------"
|
|
||||||
echo ""
|
|
||||||
echo "✅ La aplicación web está ahora accesible en:"
|
|
||||||
echo " http://<IP_DE_TU_SERVIDOR>:$WEB_PORT"
|
|
||||||
echo ""
|
|
||||||
echo "Puedes verificar el estado de los servicios con:"
|
|
||||||
echo "sudo systemctl status $APP_NAME.service"
|
|
||||||
## CORRECCIÓN: Se actualiza el mensaje para reflejar el nuevo nombre del servicio del worker.
|
|
||||||
echo "sudo systemctl status $APP_NAME-scheduler.service"
|
|
||||||
echo ""
|
|
||||||
echo "Para ver los logs de la aplicación web:"
|
|
||||||
echo "sudo journalctl -u $APP_NAME.service -f"
|
|
||||||
echo "Para ver los logs del planificador de noticias:"
|
|
||||||
echo "sudo journalctl -u $APP_NAME-scheduler.service -f"
|
|
||||||
196
paises.csv
196
paises.csv
|
|
@ -1,196 +0,0 @@
|
||||||
id,nombre,continente_id
|
|
||||||
1,Afganistán,3
|
|
||||||
2,Albania,4
|
|
||||||
3,Alemania,4
|
|
||||||
4,Andorra,4
|
|
||||||
5,Angola,1
|
|
||||||
6,Antigua y Barbuda,2
|
|
||||||
7,Arabia Saudita,3
|
|
||||||
8,Argelia,1
|
|
||||||
9,Argentina,2
|
|
||||||
10,Armenia,3
|
|
||||||
11,Australia,5
|
|
||||||
12,Austria,4
|
|
||||||
13,Azerbaiyán,3
|
|
||||||
14,Bahamas,2
|
|
||||||
15,Bangladés,3
|
|
||||||
16,Barbados,2
|
|
||||||
17,Baréin,3
|
|
||||||
19,Belice,2
|
|
||||||
20,Benín,1
|
|
||||||
21,Bielorrusia,4
|
|
||||||
22,Birmania,3
|
|
||||||
23,Bolivia,2
|
|
||||||
24,Bosnia y Herzegovina,4
|
|
||||||
25,Botsuana,1
|
|
||||||
26,Brasil,2
|
|
||||||
27,Brunéi,3
|
|
||||||
28,Bulgaria,4
|
|
||||||
29,Burkina Faso,1
|
|
||||||
30,Burundi,1
|
|
||||||
31,Bután,3
|
|
||||||
18,Bélgica,4
|
|
||||||
32,Cabo Verde,1
|
|
||||||
33,Camboya,3
|
|
||||||
34,Camerún,1
|
|
||||||
35,Canadá,2
|
|
||||||
36,Catar,3
|
|
||||||
37,Chad,1
|
|
||||||
38,Chile,2
|
|
||||||
39,China,3
|
|
||||||
40,Chipre,3
|
|
||||||
41,Colombia,2
|
|
||||||
42,Comoras,1
|
|
||||||
43,Corea del Norte,3
|
|
||||||
44,Corea del Sur,3
|
|
||||||
46,Costa Rica,2
|
|
||||||
45,Costa de Marfil,1
|
|
||||||
47,Croacia,4
|
|
||||||
48,Cuba,2
|
|
||||||
49,Dinamarca,4
|
|
||||||
50,Dominica,2
|
|
||||||
51,Ecuador,2
|
|
||||||
52,Egipto,1
|
|
||||||
53,El Salvador,2
|
|
||||||
54,Emiratos Árabes Unidos,3
|
|
||||||
55,Eritrea,1
|
|
||||||
56,Eslovaquia,4
|
|
||||||
57,Eslovenia,4
|
|
||||||
58,España,4
|
|
||||||
59,Estados Unidos,2
|
|
||||||
60,Estonia,4
|
|
||||||
61,Esuatini,1
|
|
||||||
62,Etiopía,1
|
|
||||||
63,Filipinas,3
|
|
||||||
64,Finlandia,4
|
|
||||||
65,Fiyi,5
|
|
||||||
66,Francia,4
|
|
||||||
67,Gabón,1
|
|
||||||
68,Gambia,1
|
|
||||||
69,Georgia,3
|
|
||||||
70,Ghana,1
|
|
||||||
71,Granada,2
|
|
||||||
72,Grecia,4
|
|
||||||
73,Guatemala,2
|
|
||||||
74,Guinea,1
|
|
||||||
76,Guinea Ecuatorial,1
|
|
||||||
75,Guinea-Bisáu,1
|
|
||||||
77,Guyana,2
|
|
||||||
78,Haití,2
|
|
||||||
79,Honduras,2
|
|
||||||
80,Hungría,4
|
|
||||||
81,India,3
|
|
||||||
82,Indonesia,3
|
|
||||||
83,Irak,3
|
|
||||||
85,Irlanda,4
|
|
||||||
84,Irán,3
|
|
||||||
86,Islandia,4
|
|
||||||
87,Islas Marshall,5
|
|
||||||
88,Islas Salomón,5
|
|
||||||
89,Israel,3
|
|
||||||
90,Italia,4
|
|
||||||
91,Jamaica,2
|
|
||||||
92,Japón,3
|
|
||||||
93,Jordania,3
|
|
||||||
94,Kazajistán,3
|
|
||||||
95,Kenia,1
|
|
||||||
96,Kirguistán,3
|
|
||||||
97,Kiribati,5
|
|
||||||
98,Kuwait,3
|
|
||||||
99,Laos,3
|
|
||||||
100,Lesoto,1
|
|
||||||
101,Letonia,4
|
|
||||||
103,Liberia,1
|
|
||||||
104,Libia,1
|
|
||||||
105,Liechtenstein,4
|
|
||||||
106,Lituania,4
|
|
||||||
107,Luxemburgo,4
|
|
||||||
102,Líbano,3
|
|
||||||
108,Macedonia del Norte,4
|
|
||||||
109,Madagascar,1
|
|
||||||
110,Malasia,3
|
|
||||||
111,Malaui,1
|
|
||||||
112,Maldivas,3
|
|
||||||
114,Malta,4
|
|
||||||
113,Malí,1
|
|
||||||
115,Marruecos,1
|
|
||||||
116,Mauricio,1
|
|
||||||
117,Mauritania,1
|
|
||||||
119,Micronesia,5
|
|
||||||
120,Moldavia,4
|
|
||||||
122,Mongolia,3
|
|
||||||
123,Montenegro,4
|
|
||||||
124,Mozambique,1
|
|
||||||
118,México,2
|
|
||||||
121,Mónaco,4
|
|
||||||
125,Namibia,1
|
|
||||||
126,Nauru,5
|
|
||||||
127,Nepal,3
|
|
||||||
128,Nicaragua,2
|
|
||||||
130,Nigeria,1
|
|
||||||
131,Noruega,4
|
|
||||||
132,Nueva Zelanda,5
|
|
||||||
129,Níger,1
|
|
||||||
133,Omán,3
|
|
||||||
135,Pakistán,3
|
|
||||||
136,Palaos,5
|
|
||||||
137,Palestina,3
|
|
||||||
138,Panamá,2
|
|
||||||
139,Papúa Nueva Guinea,5
|
|
||||||
140,Paraguay,2
|
|
||||||
134,Países Bajos,4
|
|
||||||
141,Perú,2
|
|
||||||
142,Polonia,4
|
|
||||||
143,Portugal,4
|
|
||||||
144,Reino Unido,4
|
|
||||||
145,República Centroafricana,1
|
|
||||||
146,República Checa,4
|
|
||||||
148,República Democrática del Congo,1
|
|
||||||
149,República Dominicana,2
|
|
||||||
147,República del Congo,1
|
|
||||||
150,Ruanda,1
|
|
||||||
151,Rumanía,4
|
|
||||||
152,Rusia,3
|
|
||||||
153,Samoa,5
|
|
||||||
154,San Cristóbal y Nieves,2
|
|
||||||
155,San Marino,4
|
|
||||||
156,San Vicente y las Granadinas,2
|
|
||||||
157,Santa Lucía,2
|
|
||||||
158,Santo Tomé y Príncipe,1
|
|
||||||
159,Senegal,1
|
|
||||||
160,Serbia,4
|
|
||||||
161,Seychelles,1
|
|
||||||
162,Sierra Leona,1
|
|
||||||
163,Singapur,3
|
|
||||||
164,Siria,3
|
|
||||||
165,Somalia,1
|
|
||||||
166,Sri Lanka,3
|
|
||||||
167,Sudáfrica,1
|
|
||||||
168,Sudán,1
|
|
||||||
169,Sudán del Sur,1
|
|
||||||
170,Suecia,4
|
|
||||||
171,Suiza,4
|
|
||||||
172,Surinam,2
|
|
||||||
173,Tailandia,3
|
|
||||||
174,Tanzania,1
|
|
||||||
175,Tayikistán,3
|
|
||||||
176,Timor Oriental,3
|
|
||||||
177,Togo,1
|
|
||||||
178,Tonga,5
|
|
||||||
179,Trinidad y Tobago,2
|
|
||||||
181,Turkmenistán,3
|
|
||||||
182,Turquía,3
|
|
||||||
183,Tuvalu,5
|
|
||||||
180,Túnez,1
|
|
||||||
184,Ucrania,4
|
|
||||||
185,Uganda,1
|
|
||||||
186,Uruguay,2
|
|
||||||
187,Uzbekistán,3
|
|
||||||
188,Vanuatu,5
|
|
||||||
189,Vaticano,4
|
|
||||||
190,Venezuela,2
|
|
||||||
191,Vietnam,3
|
|
||||||
192,Yemen,3
|
|
||||||
193,Yibuti,1
|
|
||||||
194,Zambia,1
|
|
||||||
195,Zimbabue,1
|
|
||||||
|
Loading…
Add table
Add a link
Reference in a new issue