mejora en el modelo de traduccion
This commit is contained in:
parent
be56de7dd4
commit
ecd7a3cdf8
21 changed files with 3708 additions and 322 deletions
39
.docker-compose-hooks.sh
Executable file
39
.docker-compose-hooks.sh
Executable file
|
|
@ -0,0 +1,39 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Hook para docker compose - Se ejecuta automáticamente
|
||||||
|
# Copia este archivo al inicio de tu .bashrc o .bash_profile
|
||||||
|
|
||||||
|
# Función de validación automática
|
||||||
|
validate_credentials() {
|
||||||
|
# Verificar .env
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo "🔑 Generando credenciales..."
|
||||||
|
./generate_secure_credentials.sh --force
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar contraseñas vacías
|
||||||
|
for var in POSTGRES_PASSWORD REDIS_PASSWORD DB_PASS; do
|
||||||
|
val=$(grep "^${var}=" .env | cut -d'=' -f2 | tr -d ' ')
|
||||||
|
if [ -z "$val" ] || [ "$val" = "change_*" ]; then
|
||||||
|
echo "🔑 Generando credenciales para $var..."
|
||||||
|
./generate_secure_credentials.sh --force
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alias para docker compose con validación automática
|
||||||
|
alias docker="docker"
|
||||||
|
alias "docker compose"="docker compose --env-file .env"
|
||||||
|
|
||||||
|
# Función helper: docker compose up con validación
|
||||||
|
docker_compose_up() {
|
||||||
|
echo "🔐 Validando credenciales..."
|
||||||
|
validate_credentials
|
||||||
|
|
||||||
|
echo "🚀 Iniciando servicios..."
|
||||||
|
docker compose up -d
|
||||||
|
}
|
||||||
|
|
||||||
|
# Exportar alias
|
||||||
|
alias docker_compose_up="docker_compose_up"
|
||||||
37
.docker-compose-validate.sh
Executable file
37
.docker-compose-validate.sh
Executable file
|
|
@ -0,0 +1,37 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Validador automático de credenciales para docker compose
|
||||||
|
# Se ejecuta automáticamente antes de iniciar los servicios
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colores
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
echo -e "${BLUE}🔐 Validando credenciales antes de iniciar...${NC}"
|
||||||
|
|
||||||
|
# Verificar archivo .env
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo -e "${RED}❌ ERROR: Archivo .env no encontrado${NC}"
|
||||||
|
echo -e "${YELLOW}💡 Generando credenciales...${NC}"
|
||||||
|
./generate_secure_credentials.sh --force
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar credenciales CRÍTICAS
|
||||||
|
for var in POSTGRES_PASSWORD REDIS_PASSWORD DB_PASS; do
|
||||||
|
value=$(grep "^${var}=" .env 2>/dev/null | cut -d'=' -f2 | tr -d ' ')
|
||||||
|
|
||||||
|
if [ -z "$value" ] || [ "$value" = "change_this_to_a_long_random_string" ] || [ "$value" = "change_this_password" ]; then
|
||||||
|
echo -e "${RED}❌ ERROR: $var está vacía o tiene valor por defecto${NC}"
|
||||||
|
echo -e "${YELLOW}💡 Generando credenciales seguras...${NC}"
|
||||||
|
./generate_secure_credentials.sh --force
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ Todas las credenciales están definidas${NC}"
|
||||||
|
exit 0
|
||||||
5
.env.backup.20260403_232109
Normal file
5
.env.backup.20260403_232109
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
POSTGRES_PASSWORD=test_pass_123456
|
||||||
|
REDIS_PASSWORD=test_redis_123456
|
||||||
|
DB_PASS=test_pass_123456
|
||||||
|
SECRET_KEY=test_secret_key
|
||||||
|
GRAFANA_PASSWORD=test_grafana_123456
|
||||||
5
.env.backup.20260403_232115
Normal file
5
.env.backup.20260403_232115
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
POSTGRES_PASSWORD=test_pass_123456
|
||||||
|
REDIS_PASSWORD=test_redis_123456
|
||||||
|
DB_PASS=test_pass_123456
|
||||||
|
SECRET_KEY=test_secret_key
|
||||||
|
GRAFANA_PASSWORD=test_grafana_123456
|
||||||
92
.env.backup.20260403_232957
Normal file
92
.env.backup.20260403_232957
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
# ==================================================================================
|
||||||
|
# CONFIGURACIÓN SEGURA - Generado automáticamente
|
||||||
|
# Fecha: 2026-04-03 23:21:15
|
||||||
|
# ==================================================================================
|
||||||
|
#
|
||||||
|
# IMPORTANTE:
|
||||||
|
# - NO compartas este archivo
|
||||||
|
# - Guarda las credenciales en un gestor de contraseñas
|
||||||
|
# - Añade .env al .gitignore
|
||||||
|
#
|
||||||
|
# ==================================================================================
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# DATABASE CONFIGURATION - PostgreSQL
|
||||||
|
# ==================================================================================
|
||||||
|
POSTGRES_DB=rss
|
||||||
|
POSTGRES_USER=rss
|
||||||
|
POSTGRES_PASSWORD=npti6eFCYsAv71LcJKasnpEAhtHgoD4a
|
||||||
|
|
||||||
|
DB_NAME=rss
|
||||||
|
DB_USER=rss
|
||||||
|
DB_PASS=npti6eFCYsAv71LcJKasnpEAhtHgoD4a
|
||||||
|
DB_HOST=db
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_WRITE_HOST=db
|
||||||
|
DB_READ_HOST=db-replica
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# REDIS CONFIGURATION - Con autenticación
|
||||||
|
# ==================================================================================
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=pmBJtSBMiYGptknv5pXa163kUR946V25
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# APPLICATION SECRETS
|
||||||
|
# ==================================================================================
|
||||||
|
SECRET_KEY=4a60ebda58f0716b846de1758e63e209e33070d0dbf8ac3c4ecd0c216e6bb4aa
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# MONITORING - Grafana
|
||||||
|
# ==================================================================================
|
||||||
|
GRAFANA_PASSWORD=oTkxxwKCNB25r0unvtNi8AQt
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# EXTERNAL SERVICES
|
||||||
|
# ==================================================================================
|
||||||
|
ALLTALK_URL=http://host.docker.internal:7851
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# AI MODELS & WORKERS
|
||||||
|
# ==================================================================================
|
||||||
|
RSS_MAX_WORKERS=3
|
||||||
|
TARGET_LANGS=es
|
||||||
|
TRANSLATOR_BATCH=128
|
||||||
|
ENQUEUE=300
|
||||||
|
|
||||||
|
# RSS Ingestor Configuration
|
||||||
|
RSS_POKE_INTERVAL_MIN=15
|
||||||
|
RSS_MAX_FAILURES=10
|
||||||
|
RSS_FEED_TIMEOUT=60
|
||||||
|
|
||||||
|
# URL Feed Discovery Worker
|
||||||
|
URL_DISCOVERY_INTERVAL_MIN=15
|
||||||
|
URL_DISCOVERY_BATCH_SIZE=10
|
||||||
|
MAX_FEEDS_PER_URL=5
|
||||||
|
|
||||||
|
# CTranslate2 / AI Model Paths
|
||||||
|
CT2_MODEL_PATH=/app/models/nllb-ct2
|
||||||
|
CT2_DEVICE=cuda
|
||||||
|
CT2_COMPUTE_TYPE=int8_float16
|
||||||
|
UNIVERSAL_MODEL=facebook/nllb-200-distilled-600M
|
||||||
|
|
||||||
|
# Embeddings
|
||||||
|
EMB_MODEL=sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
|
||||||
|
EMB_BATCH=64
|
||||||
|
EMB_DEVICE=cuda
|
||||||
|
|
||||||
|
# NER
|
||||||
|
NER_LANG=es
|
||||||
|
NER_BATCH=64
|
||||||
|
|
||||||
|
# Flask / Gunicorn
|
||||||
|
GUNICORN_WORKERS=8
|
||||||
|
FLASK_DEBUG=0
|
||||||
|
|
||||||
|
# Qdrant Configuration
|
||||||
|
QDRANT_HOST=qdrant
|
||||||
|
QDRANT_PORT=6333
|
||||||
|
QDRANT_COLLECTION_NAME=news_vectors
|
||||||
|
QDRANT_BATCH_SIZE=100
|
||||||
|
QDRANT_SLEEP_IDLE=30
|
||||||
92
.env.backup.20260403_233004
Normal file
92
.env.backup.20260403_233004
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
# ==================================================================================
|
||||||
|
# CONFIGURACIÓN SEGURA - Generado automáticamente
|
||||||
|
# Fecha: 2026-04-03 23:21:15
|
||||||
|
# ==================================================================================
|
||||||
|
#
|
||||||
|
# IMPORTANTE:
|
||||||
|
# - NO compartas este archivo
|
||||||
|
# - Guarda las credenciales en un gestor de contraseñas
|
||||||
|
# - Añade .env al .gitignore
|
||||||
|
#
|
||||||
|
# ==================================================================================
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# DATABASE CONFIGURATION - PostgreSQL
|
||||||
|
# ==================================================================================
|
||||||
|
POSTGRES_DB=rss
|
||||||
|
POSTGRES_USER=rss
|
||||||
|
POSTGRES_PASSWORD=npti6eFCYsAv71LcJKasnpEAhtHgoD4a
|
||||||
|
|
||||||
|
DB_NAME=rss
|
||||||
|
DB_USER=rss
|
||||||
|
DB_PASS=npti6eFCYsAv71LcJKasnpEAhtHgoD4a
|
||||||
|
DB_HOST=db
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_WRITE_HOST=db
|
||||||
|
DB_READ_HOST=db-replica
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# REDIS CONFIGURATION - Con autenticación
|
||||||
|
# ==================================================================================
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=pmBJtSBMiYGptknv5pXa163kUR946V25
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# APPLICATION SECRETS
|
||||||
|
# ==================================================================================
|
||||||
|
SECRET_KEY=4a60ebda58f0716b846de1758e63e209e33070d0dbf8ac3c4ecd0c216e6bb4aa
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# MONITORING - Grafana
|
||||||
|
# ==================================================================================
|
||||||
|
GRAFANA_PASSWORD=oTkxxwKCNB25r0unvtNi8AQt
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# EXTERNAL SERVICES
|
||||||
|
# ==================================================================================
|
||||||
|
ALLTALK_URL=http://host.docker.internal:7851
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# AI MODELS & WORKERS
|
||||||
|
# ==================================================================================
|
||||||
|
RSS_MAX_WORKERS=3
|
||||||
|
TARGET_LANGS=es
|
||||||
|
TRANSLATOR_BATCH=128
|
||||||
|
ENQUEUE=300
|
||||||
|
|
||||||
|
# RSS Ingestor Configuration
|
||||||
|
RSS_POKE_INTERVAL_MIN=15
|
||||||
|
RSS_MAX_FAILURES=10
|
||||||
|
RSS_FEED_TIMEOUT=60
|
||||||
|
|
||||||
|
# URL Feed Discovery Worker
|
||||||
|
URL_DISCOVERY_INTERVAL_MIN=15
|
||||||
|
URL_DISCOVERY_BATCH_SIZE=10
|
||||||
|
MAX_FEEDS_PER_URL=5
|
||||||
|
|
||||||
|
# CTranslate2 / AI Model Paths
|
||||||
|
CT2_MODEL_PATH=/app/models/nllb-ct2
|
||||||
|
CT2_DEVICE=cuda
|
||||||
|
CT2_COMPUTE_TYPE=int8_float16
|
||||||
|
UNIVERSAL_MODEL=facebook/nllb-200-distilled-600M
|
||||||
|
|
||||||
|
# Embeddings
|
||||||
|
EMB_MODEL=sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
|
||||||
|
EMB_BATCH=64
|
||||||
|
EMB_DEVICE=cuda
|
||||||
|
|
||||||
|
# NER
|
||||||
|
NER_LANG=es
|
||||||
|
NER_BATCH=64
|
||||||
|
|
||||||
|
# Flask / Gunicorn
|
||||||
|
GUNICORN_WORKERS=8
|
||||||
|
FLASK_DEBUG=0
|
||||||
|
|
||||||
|
# Qdrant Configuration
|
||||||
|
QDRANT_HOST=qdrant
|
||||||
|
QDRANT_PORT=6333
|
||||||
|
QDRANT_COLLECTION_NAME=news_vectors
|
||||||
|
QDRANT_BATCH_SIZE=100
|
||||||
|
QDRANT_SLEEP_IDLE=30
|
||||||
92
.env.backup.20260403_233140
Normal file
92
.env.backup.20260403_233140
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
# ==================================================================================
|
||||||
|
# CONFIGURACIÓN SEGURA - Generado automáticamente
|
||||||
|
# Fecha: 2026-04-03 23:30:04
|
||||||
|
# ==================================================================================
|
||||||
|
#
|
||||||
|
# IMPORTANTE:
|
||||||
|
# - NO compartas este archivo
|
||||||
|
# - Guarda las credenciales en un gestor de contraseñas
|
||||||
|
# - Añade .env al .gitignore
|
||||||
|
#
|
||||||
|
# ==================================================================================
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# DATABASE CONFIGURATION - PostgreSQL
|
||||||
|
# ==================================================================================
|
||||||
|
POSTGRES_DB=rss
|
||||||
|
POSTGRES_USER=rss
|
||||||
|
POSTGRES_PASSWORD=SI84OnqRT40FtdAeQro3eSJULKzFJ06F
|
||||||
|
|
||||||
|
DB_NAME=rss
|
||||||
|
DB_USER=rss
|
||||||
|
DB_PASS=SI84OnqRT40FtdAeQro3eSJULKzFJ06F
|
||||||
|
DB_HOST=db
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_WRITE_HOST=db
|
||||||
|
DB_READ_HOST=db-replica
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# REDIS CONFIGURATION - Con autenticación
|
||||||
|
# ==================================================================================
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=MvdWZlO1YCZ3QxE761bbvEd2qx44dO9j
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# APPLICATION SECRETS
|
||||||
|
# ==================================================================================
|
||||||
|
SECRET_KEY=68d5b0297d44e88f5ab83720e0bdf8eeef2ceeccd72b8e46b7f1b47531571780
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# MONITORING - Grafana
|
||||||
|
# ==================================================================================
|
||||||
|
GRAFANA_PASSWORD=BVWprtQiW2KDmaDR8zZ2Br83
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# EXTERNAL SERVICES
|
||||||
|
# ==================================================================================
|
||||||
|
ALLTALK_URL=http://host.docker.internal:7851
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# AI MODELS & WORKERS
|
||||||
|
# ==================================================================================
|
||||||
|
RSS_MAX_WORKERS=3
|
||||||
|
TARGET_LANGS=es
|
||||||
|
TRANSLATOR_BATCH=128
|
||||||
|
ENQUEUE=300
|
||||||
|
|
||||||
|
# RSS Ingestor Configuration
|
||||||
|
RSS_POKE_INTERVAL_MIN=15
|
||||||
|
RSS_MAX_FAILURES=10
|
||||||
|
RSS_FEED_TIMEOUT=60
|
||||||
|
|
||||||
|
# URL Feed Discovery Worker
|
||||||
|
URL_DISCOVERY_INTERVAL_MIN=15
|
||||||
|
URL_DISCOVERY_BATCH_SIZE=10
|
||||||
|
MAX_FEEDS_PER_URL=5
|
||||||
|
|
||||||
|
# CTranslate2 / AI Model Paths
|
||||||
|
CT2_MODEL_PATH=/app/models/nllb-ct2
|
||||||
|
CT2_DEVICE=cuda
|
||||||
|
CT2_COMPUTE_TYPE=int8_float16
|
||||||
|
UNIVERSAL_MODEL=facebook/nllb-200-distilled-600M
|
||||||
|
|
||||||
|
# Embeddings
|
||||||
|
EMB_MODEL=sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
|
||||||
|
EMB_BATCH=64
|
||||||
|
EMB_DEVICE=cuda
|
||||||
|
|
||||||
|
# NER
|
||||||
|
NER_LANG=es
|
||||||
|
NER_BATCH=64
|
||||||
|
|
||||||
|
# Flask / Gunicorn
|
||||||
|
GUNICORN_WORKERS=8
|
||||||
|
FLASK_DEBUG=0
|
||||||
|
|
||||||
|
# Qdrant Configuration
|
||||||
|
QDRANT_HOST=qdrant
|
||||||
|
QDRANT_PORT=6333
|
||||||
|
QDRANT_COLLECTION_NAME=news_vectors
|
||||||
|
QDRANT_BATCH_SIZE=100
|
||||||
|
QDRANT_SLEEP_IDLE=30
|
||||||
92
.env.backup.20260404_000245
Normal file
92
.env.backup.20260404_000245
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
# ==================================================================================
|
||||||
|
# CONFIGURACIÓN SEGURA - Generado automáticamente
|
||||||
|
# Fecha: 2026-04-03 23:31:40
|
||||||
|
# ==================================================================================
|
||||||
|
#
|
||||||
|
# IMPORTANTE:
|
||||||
|
# - NO compartas este archivo
|
||||||
|
# - Guarda las credenciales en un gestor de contraseñas
|
||||||
|
# - Añade .env al .gitignore
|
||||||
|
#
|
||||||
|
# ==================================================================================
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# DATABASE CONFIGURATION - PostgreSQL
|
||||||
|
# ==================================================================================
|
||||||
|
POSTGRES_DB=rss
|
||||||
|
POSTGRES_USER=rss
|
||||||
|
POSTGRES_PASSWORD=S5xwsI9HKjdaBWLpCkqp0mA0DJYZerpD
|
||||||
|
|
||||||
|
DB_NAME=rss
|
||||||
|
DB_USER=rss
|
||||||
|
DB_PASS=S5xwsI9HKjdaBWLpCkqp0mA0DJYZerpD
|
||||||
|
DB_HOST=db
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_WRITE_HOST=db
|
||||||
|
DB_READ_HOST=db-replica
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# REDIS CONFIGURATION - Con autenticación
|
||||||
|
# ==================================================================================
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=cVF79QuJulPO5auEhA2nNqv7mqAruzYh
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# APPLICATION SECRETS
|
||||||
|
# ==================================================================================
|
||||||
|
SECRET_KEY=2eb7c221245bb8d896f83255188de614e59fae6b8dfbf16eee8a23c60dde4ffa
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# MONITORING - Grafana
|
||||||
|
# ==================================================================================
|
||||||
|
GRAFANA_PASSWORD=R4J61V6OGLTCesrITwEvUPTm
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# EXTERNAL SERVICES
|
||||||
|
# ==================================================================================
|
||||||
|
ALLTALK_URL=http://host.docker.internal:7851
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# AI MODELS & WORKERS
|
||||||
|
# ==================================================================================
|
||||||
|
RSS_MAX_WORKERS=3
|
||||||
|
TARGET_LANGS=es
|
||||||
|
TRANSLATOR_BATCH=128
|
||||||
|
ENQUEUE=300
|
||||||
|
|
||||||
|
# RSS Ingestor Configuration
|
||||||
|
RSS_POKE_INTERVAL_MIN=15
|
||||||
|
RSS_MAX_FAILURES=10
|
||||||
|
RSS_FEED_TIMEOUT=60
|
||||||
|
|
||||||
|
# URL Feed Discovery Worker
|
||||||
|
URL_DISCOVERY_INTERVAL_MIN=15
|
||||||
|
URL_DISCOVERY_BATCH_SIZE=10
|
||||||
|
MAX_FEEDS_PER_URL=5
|
||||||
|
|
||||||
|
# CTranslate2 / AI Model Paths
|
||||||
|
CT2_MODEL_PATH=/app/models/nllb-ct2
|
||||||
|
CT2_DEVICE=cuda
|
||||||
|
CT2_COMPUTE_TYPE=int8_float16
|
||||||
|
UNIVERSAL_MODEL=facebook/nllb-200-distilled-600M
|
||||||
|
|
||||||
|
# Embeddings
|
||||||
|
EMB_MODEL=sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
|
||||||
|
EMB_BATCH=64
|
||||||
|
EMB_DEVICE=cuda
|
||||||
|
|
||||||
|
# NER
|
||||||
|
NER_LANG=es
|
||||||
|
NER_BATCH=64
|
||||||
|
|
||||||
|
# Flask / Gunicorn
|
||||||
|
GUNICORN_WORKERS=8
|
||||||
|
FLASK_DEBUG=0
|
||||||
|
|
||||||
|
# Qdrant Configuration
|
||||||
|
QDRANT_HOST=qdrant
|
||||||
|
QDRANT_PORT=6333
|
||||||
|
QDRANT_COLLECTION_NAME=news_vectors
|
||||||
|
QDRANT_BATCH_SIZE=100
|
||||||
|
QDRANT_SLEEP_IDLE=30
|
||||||
143
.gitignore
vendored
143
.gitignore
vendored
|
|
@ -1,80 +1,119 @@
|
||||||
|
# ==================================================================================
|
||||||
|
# Python
|
||||||
|
# ==================================================================================
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.py[cod]
|
||||||
*.pyo
|
*$py.class
|
||||||
*.pyd
|
|
||||||
.Python
|
|
||||||
*.so
|
*.so
|
||||||
*.egg
|
.Python
|
||||||
*.egg-info/
|
|
||||||
dist/
|
|
||||||
build/
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
venv/
|
venv/
|
||||||
.env
|
env/
|
||||||
|
ENV/
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# IDEs
|
||||||
|
# ==================================================================================
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
.DS_Store
|
.DS_Store
|
||||||
pgdata/
|
Thumbs.db
|
||||||
pgdata-replica/
|
|
||||||
pgdata.failed_restore/
|
|
||||||
pgdata-replica.old.*/
|
|
||||||
redis-data/
|
|
||||||
hf_cache/
|
|
||||||
models/nllb-ct2/
|
|
||||||
qdrant_storage/
|
|
||||||
*.log
|
|
||||||
*.db
|
|
||||||
*.sqlite
|
|
||||||
*.sqlite3
|
|
||||||
data/
|
|
||||||
*.mp4
|
|
||||||
*.mp3
|
|
||||||
*.wav
|
|
||||||
*.srt
|
|
||||||
*.tar.gz
|
|
||||||
*.zip
|
|
||||||
*.bak
|
|
||||||
*.old
|
|
||||||
|
|
||||||
|
|
||||||
# ==================================================================================
|
# ==================================================================================
|
||||||
# SECURITY FILES - NEVER COMMIT THESE
|
# SECURITY - NEVER COMMIT
|
||||||
# ==================================================================================
|
# ==================================================================================
|
||||||
# Environment files with credentials
|
|
||||||
.env
|
.env
|
||||||
.env.backup*
|
.env.backup
|
||||||
|
.env.backup2
|
||||||
.env.generated
|
.env.generated
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|
||||||
# Database backups
|
# Credentials
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.crt
|
||||||
|
credentials.json
|
||||||
|
service-account.json
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# RSS2 Data & Models (downloaded at runtime)
|
||||||
|
# ==================================================================================
|
||||||
|
# ML Models - downloaded/converted by workers
|
||||||
|
models/
|
||||||
|
models/nllb-ct2/
|
||||||
|
models/nllb-ct2-1.3b/
|
||||||
|
|
||||||
|
# Wikipedia images - downloaded by wiki_worker
|
||||||
|
data/wiki_images/
|
||||||
|
data/backups/
|
||||||
|
|
||||||
|
# Database storage (PostgreSQL)
|
||||||
|
data/pgdata/
|
||||||
|
pgdata/
|
||||||
|
pgdata-replica/
|
||||||
|
pgdata.failed_restore/
|
||||||
|
pgdata-replica.old.*/
|
||||||
|
|
||||||
|
# Redis data
|
||||||
|
data/redis-data/
|
||||||
|
redis-data/
|
||||||
|
|
||||||
|
# Qdrant vector storage
|
||||||
|
data/qdrant_storage/
|
||||||
|
qdrant_storage/
|
||||||
|
|
||||||
|
# HuggingFace cache (downloaded models)
|
||||||
|
hf_cache/
|
||||||
|
|
||||||
|
# Monitoring data
|
||||||
|
monitoring/data/
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# Backup files
|
||||||
|
# ==================================================================================
|
||||||
*.sql
|
*.sql
|
||||||
!init-db/*.sql
|
!init-db/*.sql
|
||||||
!migrations/*.sql
|
!migrations/*.sql
|
||||||
backup_*.sql
|
backup_*.sql
|
||||||
|
|
||||||
# Redis backups
|
|
||||||
*.rdb
|
*.rdb
|
||||||
redis_backup_*.rdb
|
redis_backup_*.rdb
|
||||||
|
|
||||||
# Qdrant backups
|
|
||||||
qdrant_backup_*.tar.gz
|
qdrant_backup_*.tar.gz
|
||||||
|
|
||||||
# Docker compose with real credentials (if you create variations)
|
# ==================================================================================
|
||||||
|
# Temporary files
|
||||||
|
# ==================================================================================
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
*.tmp
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# Docker
|
||||||
|
# ==================================================================================
|
||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
|
|
||||||
# Large Language Models
|
# ==================================================================================
|
||||||
models/llm/
|
# Keep directory structure (optional - uncomment if needed)
|
||||||
|
# models/.gitkeep
|
||||||
# User Uploads
|
# data/.gitkeep
|
||||||
static/uploads/
|
|
||||||
|
|
||||||
# Celery/Workers
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# System/IDE
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
408
AGENTS.md
Normal file
408
AGENTS.md
Normal file
|
|
@ -0,0 +1,408 @@
|
||||||
|
# AGENTS.md - RSS2 Development Guide
|
||||||
|
|
||||||
|
## 🛠️ Build & Test Commands
|
||||||
|
|
||||||
|
### Backend (Go)
|
||||||
|
```bash
|
||||||
|
cd backend && go mod tidy
|
||||||
|
cd backend && go build -o ../bin/server ./cmd/server
|
||||||
|
cd rss-ingestor-go && go build -o ../bin/rss-ingestor .
|
||||||
|
|
||||||
|
# Single test
|
||||||
|
cd backend && go test ./internal/handlers -v -run TestLogin
|
||||||
|
cd backend && go test ./internal/auth -v -run TestGenerateToken
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
```bash
|
||||||
|
cd frontend && npm install
|
||||||
|
cd frontend && npm run dev # Development
|
||||||
|
cd frontend && npm run build # Production build
|
||||||
|
cd frontend && npm test # Run tests
|
||||||
|
cd frontend && npm run test:ui # UI mode
|
||||||
|
```
|
||||||
|
|
||||||
|
### Makefile
|
||||||
|
```bash
|
||||||
|
make build # Build all binaries
|
||||||
|
make clean # Remove binaries
|
||||||
|
make docker-build # Build Docker images
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
```
|
||||||
|
rss2/
|
||||||
|
├── backend/ # Go API server + Workers
|
||||||
|
│ ├── cmd/
|
||||||
|
│ │ ├── server/main.go # API REST principal (Gin)
|
||||||
|
│ │ ├── wiki_worker/main.go # Wikipedia integration
|
||||||
|
│ │ ├── qdrant/main.go # Vector indexing worker
|
||||||
|
│ │ ├── related/main.go # Related news worker
|
||||||
|
│ │ ├── topics/main.go # Country/topic matcher
|
||||||
|
│ │ ├── scraper/main.go # Deep scraping worker
|
||||||
|
│ │ ├── discovery/main.go # RSS feed discovery
|
||||||
|
│ │ └── topics/main.go # Topic matching
|
||||||
|
│ └── internal/
|
||||||
|
│ ├── handlers/ # HTTP endpoints
|
||||||
|
│ ├── models/ # Data models
|
||||||
|
│ ├── auth/ # JWT authentication
|
||||||
|
│ ├── middleware/ # CORS, Auth middleware
|
||||||
|
│ ├── services/ # ML services (Translate, Embeddings, NER, Semantic Search)
|
||||||
|
│ ├── db/ # PostgreSQL connection
|
||||||
|
│ ├── cache/ # Redis connection
|
||||||
|
│ └── config/ # Configuration loading
|
||||||
|
├── frontend/ # React + TypeScript + Vite
|
||||||
|
│ └── src/
|
||||||
|
│ ├── pages/ # Home, News, Search, Admin, Feeds, Stats
|
||||||
|
│ ├── components/ # Layout, UI components
|
||||||
|
│ └── services/ # API client
|
||||||
|
├── workers/ # Python workers
|
||||||
|
│ ├── ctranslator_worker.py # NLLB-200 translation (CTranslate2)
|
||||||
|
│ ├── ner_worker.py # Spacy NER + Topic extraction
|
||||||
|
│ ├── embeddings_worker.py # Sentence transformers embeddings
|
||||||
|
│ ├── cluster_worker.py # News clustering
|
||||||
|
│ ├── langdetect_worker.py # Language detection
|
||||||
|
│ ├── llm_categorizer_worker.py # Ollama LLM categorization
|
||||||
|
│ ├── simple_categorizer_worker.py
|
||||||
|
│ ├── simple_translator.py
|
||||||
|
│ ├── simple_translator_worker.py
|
||||||
|
│ ├── translation_worker.py
|
||||||
|
│ ├── translation_scheduler.py
|
||||||
|
│ └── remote_translator_worker.py # Remote GPU worker via WebSocket
|
||||||
|
├── data/ # PostgreSQL, Redis, Qdrant data
|
||||||
|
├── models/ # ML models (nllb-ct2)
|
||||||
|
├── hf_cache/ # HuggingFace cache
|
||||||
|
├── init-db/ # SQL migrations
|
||||||
|
├── monitoring/ # Prometheus + Grafana config
|
||||||
|
├── rss-ingestor-go/ # RSS crawler (Go)
|
||||||
|
└── docker-compose.yml # Full stack orchestration
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Servicios del Sistema
|
||||||
|
|
||||||
|
### Capa de Acceso y API (Puerto 8888)
|
||||||
|
| Servicio | Tecnología | Descripción |
|
||||||
|
|---------|------------|-------------|
|
||||||
|
| **nginx** | Nginx Alpine | Gateway y Proxy Inverso |
|
||||||
|
| **rss2_frontend** | React + Vite | Interfaz web responsiva |
|
||||||
|
| **backend-go** | Go + Gin | API REST principal |
|
||||||
|
|
||||||
|
### Ingesta y Descubrimiento (Go)
|
||||||
|
| Servicio | Tecnología | Descripción |
|
||||||
|
|---------|------------|-------------|
|
||||||
|
| **rss-ingestor-go** | Go | Crawler RSS de alto rendimiento |
|
||||||
|
| **scraper** | Go | Scraper profundo con sanitización HTML |
|
||||||
|
| **discovery** | Go | Agente de descubrimiento de feeds RSS |
|
||||||
|
|
||||||
|
### Procesamiento de Datos e IA
|
||||||
|
| Servicio | Tecnología | Descripción |
|
||||||
|
|---------|------------|-------------|
|
||||||
|
| **translator** | NLLB-200 (CPU) | Traducción neuronal CTranslate2 |
|
||||||
|
| **translator-gpu** | NLLB-200 (GPU) | Traducción acelerada CUDA |
|
||||||
|
| **remote-translator** | WebSocket | Worker GPU remoto |
|
||||||
|
| **embeddings** | S-Transformers | Generación de vectores semánticos |
|
||||||
|
| **ner** | Spacy + BERT | Reconocimiento de entidades (PER, ORG, LOC) |
|
||||||
|
| **llm-categorizer** | Ollama/Mistral | Clasificación con modelos de lenguaje |
|
||||||
|
| **wiki-worker** | Go | Integración Wikipedia + thumbnails |
|
||||||
|
| **topics** | Go | Matcher de países y temas |
|
||||||
|
| **related** | Go | Detección de noticias relacionadas |
|
||||||
|
| **qdrant-worker** | Go | Vectorización + búsqueda semántica |
|
||||||
|
| **cluster** | Python | Agrupación de noticias |
|
||||||
|
| **langdetect** | Python | Detección de idioma |
|
||||||
|
| **translation-scheduler** | Python | Creador de tareas de traducción |
|
||||||
|
|
||||||
|
### Capa de Almacenamiento
|
||||||
|
| Servicio | Tecnología | Descripción |
|
||||||
|
|---------|------------|-------------|
|
||||||
|
| **db** | PostgreSQL 18 | Base de datos relacional |
|
||||||
|
| **qdrant** | Qdrant | Base de datos vectorial |
|
||||||
|
| **redis** | Redis 7 | Cache y colas de mensajes |
|
||||||
|
|
||||||
|
### Monitoreo
|
||||||
|
| Servicio | Tecnología | Descripción |
|
||||||
|
|---------|------------|-------------|
|
||||||
|
| **prometheus** | Prometheus | Métricas del sistema |
|
||||||
|
| **grafana** | Grafana | Dashboard (puerto 3001) |
|
||||||
|
| **cadvisor** | cAdvisor | Monitoreo Docker |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Code Style Guidelines
|
||||||
|
|
||||||
|
### Backend (Go)
|
||||||
|
|
||||||
|
#### Imports
|
||||||
|
```go
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/rss2/backend/internal/auth"
|
||||||
|
"github.com/rss2/backend/internal/models"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
**Order:** Standard → Third-party → Local packages
|
||||||
|
**Use:** `goimports` to auto-format
|
||||||
|
|
||||||
|
#### Naming
|
||||||
|
- Packages: lowercase (`handlers`, `services`)
|
||||||
|
- Functions/Methods: camelCase (`GetNews`, `CreateUser`)
|
||||||
|
- Variables: camelCase (`userId`, `newsList`)
|
||||||
|
- Constants: UPPER_SNAKE_CASE (`MaxPageSize`)
|
||||||
|
- Types: PascalCase (`NewsResponse`, `User`)
|
||||||
|
|
||||||
|
#### Error Handling
|
||||||
|
```go
|
||||||
|
func CreateResource(c *gin.Context) {
|
||||||
|
var req CreateRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||||
|
Error: "Invalid request",
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Always return errors** | **Use `models.ErrorResponse`**
|
||||||
|
|
||||||
|
#### Database
|
||||||
|
```go
|
||||||
|
err := db.GetPool().QueryRow(ctx, "SELECT * FROM users WHERE id = $1", id).Scan(&user)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, models.ErrorResponse{Error: "User not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Use context** | **Named parameters ($1, $2)**
|
||||||
|
|
||||||
|
#### Tests
|
||||||
|
```go
|
||||||
|
func TestLoginInvalidRequest(t *testing.T) {
|
||||||
|
router := gin.New()
|
||||||
|
router.POST("/auth/login", Login)
|
||||||
|
body := []byte(`{}`)
|
||||||
|
req, _ := http.NewRequest("POST", "/auth/login", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Errorf("expected 400, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Use `gin.TestMode`** | **Test error paths**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Frontend (TypeScript + React)
|
||||||
|
|
||||||
|
#### Imports
|
||||||
|
```tsx
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { Routes, Route } from 'react-router-dom'
|
||||||
|
import { Layout } from './components/layout/Layout'
|
||||||
|
import { api } from './services/api'
|
||||||
|
```
|
||||||
|
**Order:** React → Router → Components → Services → Utils
|
||||||
|
|
||||||
|
#### Components
|
||||||
|
```tsx
|
||||||
|
function NewsList() {
|
||||||
|
const [news, setNews] = useState<News[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => { fetchNews(); }, [])
|
||||||
|
|
||||||
|
const fetchNews = async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.get('/news')
|
||||||
|
setNews(res.data)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="news-list">
|
||||||
|
{loading && <Spinner />}
|
||||||
|
{error && <ErrorBanner message={error} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Types
|
||||||
|
```tsx
|
||||||
|
interface News {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
summary: string
|
||||||
|
url: string
|
||||||
|
publishedAt: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Styling
|
||||||
|
- Use Tailwind CSS utility classes
|
||||||
|
- Avoid inline styles
|
||||||
|
- Use `clsx` for conditional classes
|
||||||
|
|
||||||
|
#### TypeScript Rules
|
||||||
|
- Use `strict: true` mode
|
||||||
|
- Avoid `any` - define interfaces
|
||||||
|
- Use type guards for narrowing
|
||||||
|
- Prefer optional chaining `?.`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Security Guidelines
|
||||||
|
|
||||||
|
1. **Never commit secrets** - Use `.env` in `.gitignore`
|
||||||
|
2. **Validate all inputs** - Use `go-playground/validator`
|
||||||
|
3. **Prepared statements** - Prevent SQL injection
|
||||||
|
4. **Rate limiting** - On sensitive endpoints
|
||||||
|
5. **HTTPS only** - Enforce in production
|
||||||
|
|
||||||
|
### Variables de Entorno Críticas
|
||||||
|
```bash
|
||||||
|
POSTGRES_PASSWORD # Contraseña PostgreSQL
|
||||||
|
REDIS_PASSWORD # Contraseña Redis
|
||||||
|
DB_PASS # Contraseña para workers
|
||||||
|
SECRET_KEY # Key JWT
|
||||||
|
GRAFANA_PASSWORD # Dashboard password
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing Best Practices
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- Test error cases and edge cases
|
||||||
|
- Use `gin.TestMode` for HTTP tests
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- Test component rendering
|
||||||
|
- Test API integration
|
||||||
|
- Test error states
|
||||||
|
- Use vitest
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generar credenciales seguras
|
||||||
|
./pre-deploy.sh --generate
|
||||||
|
|
||||||
|
# Validar y desplegar
|
||||||
|
./pre-deploy.sh
|
||||||
|
|
||||||
|
# O manualmente
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Escalar workers de traducción
|
||||||
|
docker compose up -d --scale translator-gpu=4
|
||||||
|
```
|
||||||
|
|
||||||
|
### Escalado de Workers GPU
|
||||||
|
```bash
|
||||||
|
# 1 worker GPU (8GB+ VRAM)
|
||||||
|
docker compose up -d --scale translator-gpu=1
|
||||||
|
|
||||||
|
# 2 workers GPU (16GB+ VRAM)
|
||||||
|
docker compose up -d --scale translator-gpu=2
|
||||||
|
|
||||||
|
# 4 workers GPU (32GB+ VRAM)
|
||||||
|
docker compose up -d --scale translator-gpu=4
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Configuración de Workers
|
||||||
|
|
||||||
|
### Environment Variables Principales
|
||||||
|
|
||||||
|
| Variable | Descripción | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `DB_HOST` | Host PostgreSQL | localhost |
|
||||||
|
| `DB_PORT` | Puerto PostgreSQL | 5432 |
|
||||||
|
| `DB_NAME` | Nombre base de datos | rss |
|
||||||
|
| `DB_USER` | Usuario PostgreSQL | rss |
|
||||||
|
| `DB_PASS` | Contraseña PostgreSQL | - |
|
||||||
|
| `TARGET_LANGS` | Idiomas destino | es |
|
||||||
|
| `TRANSLATOR_BATCH` | Tamaño de batch | 32 |
|
||||||
|
| `CT2_DEVICE` | Dispositivo (cpu/cuda) | cpu |
|
||||||
|
| `CT2_COMPUTE_TYPE` | Tipo (int8/float16) | int8 |
|
||||||
|
| `NER_BATCH` | Batch NER | 64 |
|
||||||
|
| `EMB_BATCH` | Batch embeddings | 64 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Endpoints de API Principales
|
||||||
|
|
||||||
|
### News
|
||||||
|
- `GET /api/news` - Listar noticias (paginado, filtros)
|
||||||
|
- `GET /api/news/:id` - Ver noticia con entidades
|
||||||
|
- `DELETE /api/news/:id` - Eliminar noticia (admin)
|
||||||
|
|
||||||
|
### Feeds
|
||||||
|
- `GET /api/feeds` - Listar feeds
|
||||||
|
- `POST /api/feeds` - Crear feed (auth)
|
||||||
|
- `PUT /api/feeds/:id` - Actualizar feed (auth)
|
||||||
|
- `DELETE /api/feeds/:id` - Eliminar feed (auth)
|
||||||
|
|
||||||
|
### Search
|
||||||
|
- `GET /api/search?q=...` - Búsqueda texto
|
||||||
|
- `GET /api/search?q=...&semantic=true` - Búsqueda semántica
|
||||||
|
|
||||||
|
### Entities
|
||||||
|
- `GET /api/entities?tipo=persona` - Listar entidades (PER, ORG, LOC)
|
||||||
|
|
||||||
|
### Admin
|
||||||
|
- `GET /api/admin/backup` - Backup SQL completo
|
||||||
|
- `GET /api/admin/backup/news` - Backup noticias (ZIP)
|
||||||
|
- `GET /api/admin/users` - Listar usuarios
|
||||||
|
- `POST /api/admin/workers/start` - Iniciar workers traducción
|
||||||
|
- `POST /api/admin/workers/stop` - Detener workers traducción
|
||||||
|
- `GET /api/admin/workers/status` - Estado de workers
|
||||||
|
|
||||||
|
### Auth
|
||||||
|
- `POST /api/auth/login` - Iniciar sesión
|
||||||
|
- `POST /api/auth/register` - Registrarse
|
||||||
|
- `GET /api/auth/me` - Usuario actual (auth)
|
||||||
|
|
||||||
|
### Stats
|
||||||
|
- `GET /api/stats` - Estadísticas globales
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Capabilities del Sistema
|
||||||
|
|
||||||
|
1. **Enriquecimiento Wikipedia**: Detecta personas/orgs, descarga biografías e imágenes
|
||||||
|
2. **Categorización LLM**: Clasificación con Mistral-7B vía Ollama
|
||||||
|
3. **Búsqueda Semántica**: Qdrant vector search con mxbai-embed-large
|
||||||
|
4. **Traducción Neuronal**: NLLB-200 (600M-1.3B params) CPU/GPU
|
||||||
|
5. **NER**: Spacy es_core_news_lg para entidades nombradas
|
||||||
|
6. **Noticias Relacionadas**: Similitud coseno entre embeddings
|
||||||
|
7. **Detección de Idioma**: langdetect
|
||||||
|
8. **Clustering**: Agrupación automática de noticias
|
||||||
|
9. **WebSocket Workers**: Workers GPU remotos conectados por WS
|
||||||
|
10. **Backup Automático**: pg_dump con compresión ZIP
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 Documentación Adicional
|
||||||
|
|
||||||
|
- [README.md](../README.md) - Guía de despliegue completo
|
||||||
|
- [DEPLOY.md](./DEPLOY.md) - Instrucciones de producción
|
||||||
|
- [SECURITY_GUIDE.md](./SECURITY_GUIDE.md) - Guía de seguridad
|
||||||
|
- [QUICKSTART_LLM.md](./QUICKSTART_LLM.md) - Configuración LLM
|
||||||
|
- [remote-worker.md](./remote-worker.md) - Workers remotos
|
||||||
385
DEPLOY.md
385
DEPLOY.md
|
|
@ -1,54 +1,353 @@
|
||||||
# Deployment Guide
|
# 🚀 Guía de Despliegue RSS2
|
||||||
|
|
||||||
This guide describes how to deploy the application to a new server.
|
## ⚡ Despliegue Rápido (5 minutos)
|
||||||
|
|
||||||
## Prerequisites
|
### Paso 1: Clonar el Proyecto
|
||||||
|
|
||||||
* **Linux Server** (Ubuntu 22.04+ recommended)
|
```bash
|
||||||
* **NVIDIA GPU**: Required for translation, embeddings, and NER services.
|
git clone https://github.com/tu-usuario/rss2.git
|
||||||
* **NVIDIA Container Toolkit**: Must be installed to allow Docker to access the GPU.
|
cd rss2
|
||||||
* **Docker** & **Docker Compose**: Latest versions.
|
```
|
||||||
* **Git**: To clone the repository.
|
|
||||||
* **External Service**: An instance of [AllTalk](https://github.com/erew123/alltalk_tts) running externally or on the host (port 7851 by default).
|
|
||||||
|
|
||||||
## Deployment Steps
|
### Paso 2: Generar Credenciales Seguras
|
||||||
|
|
||||||
1. **Clone the Repository**
|
```bash
|
||||||
```bash
|
./pre-deploy.sh --generate
|
||||||
git clone <your-repo-url>
|
```
|
||||||
cd <your-repo-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Configure Environment Variables**
|
**Salida esperada:**
|
||||||
Copy the example configuration file:
|
```
|
||||||
```bash
|
🔑 Generando credenciales seguras...
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
Edit `.env` and set secure passwords and configuration:
|
|
||||||
```bash
|
|
||||||
nano .env
|
|
||||||
```
|
|
||||||
* Change `POSTGRES_PASSWORD` and `DB_PASS` to a strong unique password.
|
|
||||||
* Change `SECRET_KEY` to a long random string.
|
|
||||||
* Verify `ALLTALK_URL` points to your AllTalk instance (default assumes host machine access).
|
|
||||||
|
|
||||||
3. **Start the Services**
|
⚠️ IMPORTANTE: Guarda estas credenciales en un lugar seguro
|
||||||
Run the following command to build and start the application:
|
|
||||||
```bash
|
|
||||||
docker compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Database Initialization**
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
The database will automatically initialize on the first run using the scripts in `init-db/`. This may take a few minutes. Check logs with:
|
POSTGRES_PASSWORD: S5xwsI9HKjdaBWLpCkqp0mA0DJYZerpD
|
||||||
```bash
|
REDIS_PASSWORD: cVF79QuJulPO5auEhA2nNqv7mqAruzYh
|
||||||
docker compose logs -f db
|
SECRET_KEY: 2eb7c221245bb8d896f83255188de614e59fae6b8dfbf16eee8a23c60dde4ffa
|
||||||
```
|
GRAFANA_PASSWORD: R4J61V6OGLTCesrITwEvUPTm
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
```
|
||||||
|
|
||||||
5. **Verify Deployment**
|
**⚠️ IMPORTANTE:** Copia estas contraseñas y guárdalas en un gestor de contraseñas. ¡Nunca se volverán a mostrar!
|
||||||
Access the application at `http://<your-server-ip>:8001`.
|
|
||||||
|
|
||||||
## Important Notes
|
### Paso 3: Validar y Desplegar
|
||||||
|
|
||||||
* **Models**: The application mounts `./models` and `./hf_cache` to persist AI models. On the first run, it will attempt to download necessary models (NLLB, BERT, etc.), which requires significant bandwidth and time.
|
```bash
|
||||||
* **Data Persistence**: Database data is stored in `./pgdata` (mapped in docker-compose). Ensure this directory is backed up.
|
./pre-deploy.sh
|
||||||
* **Security**: Ensure port 5432 (Postgres) and 6379 (Redis) are firewall-protected and not exposed to the public internet unless intended (Docker maps them to the host network).
|
```
|
||||||
|
|
||||||
|
**El script te preguntará:**
|
||||||
|
```
|
||||||
|
¿Deseas desplegar ahora?
|
||||||
|
1) Desplegar automáticamente (recomendado)
|
||||||
|
2) Solo validar y salir (modo manual)
|
||||||
|
Elige una opción:
|
||||||
|
```
|
||||||
|
|
||||||
|
**Opción A: Despliegue automático (Opción 1)**
|
||||||
|
- Elige `1` para que el script haga todo automáticamente
|
||||||
|
- Espera a que termine la construcción de las imágenes
|
||||||
|
- Espera a que inicien los servicios
|
||||||
|
|
||||||
|
**Opción B: Despliegue manual (Opción 2)**
|
||||||
|
- Elige `2` para validar solo
|
||||||
|
- Luego ejecuta manualmente:
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paso 4: Verificar Despliegue
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verificar estado de los servicios
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# Deberías ver algo como:
|
||||||
|
# Name Status
|
||||||
|
# rss2_db Up (healthy)
|
||||||
|
# rss2_redis Up (healthy)
|
||||||
|
# rss2_backend_go Up (running)
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paso 5: Acceder a la Aplicación
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Aplicación web
|
||||||
|
http://localhost:8888
|
||||||
|
|
||||||
|
# API (prueba con curl)
|
||||||
|
curl http://localhost:8888/api/feeds
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Seguridad
|
||||||
|
|
||||||
|
### ¿Por qué es importante?
|
||||||
|
|
||||||
|
**Sin seguridad (❌ MAL):**
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
WARN: The "DB_PASS" variable is not set. Defaulting to a blank string.
|
||||||
|
# Las contraseñas están vacías ¡PELIGRO!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Con seguridad (✅ BIEN):**
|
||||||
|
```bash
|
||||||
|
./pre-deploy.sh --generate
|
||||||
|
./pre-deploy.sh
|
||||||
|
docker compose up -d
|
||||||
|
# ✅ Todo funciona correctamente
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variables Críticas
|
||||||
|
|
||||||
|
| Variable | Qué es | Por qué es importante |
|
||||||
|
|----------|--------|----------------------|
|
||||||
|
| `POSTGRES_PASSWORD` | Contraseña de PostgreSQL | Base de datos principal |
|
||||||
|
| `REDIS_PASSWORD` | Contraseña de Redis | Cache y colas |
|
||||||
|
| `DB_PASS` | Contraseña para workers | Conexiones de workers |
|
||||||
|
| `SECRET_KEY` | Key secreta de la app | JWT, encriptación |
|
||||||
|
| `GRAFANA_PASSWORD` | Contraseña de Grafana | Dashboard de monitoring |
|
||||||
|
|
||||||
|
### Guardar tus Credenciales
|
||||||
|
|
||||||
|
**Opción 1: Gestor de contraseñas (Recomendado)**
|
||||||
|
```bash
|
||||||
|
# Copia las credenciales y guárdalas en:
|
||||||
|
# - LastPass
|
||||||
|
# - 1Password
|
||||||
|
# - KeePass
|
||||||
|
# - Bitwarden
|
||||||
|
```
|
||||||
|
|
||||||
|
**Opción 2: Archivo seguro local**
|
||||||
|
```bash
|
||||||
|
# Crea un archivo seguro fuera del proyecto
|
||||||
|
echo "POSTGRES_PASSWORD=..." > ~/rss2-credentials.txt
|
||||||
|
echo "REDIS_PASSWORD=..." >> ~/rss2-credentials.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Comandos Útiles
|
||||||
|
|
||||||
|
### Verificar Credenciales
|
||||||
|
```bash
|
||||||
|
# Ver que las contraseñas están definidas
|
||||||
|
grep -E "^(POSTGRES_PASSWORD|REDIS_PASSWORD|DB_PASS)=" .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reiniciar Servicios
|
||||||
|
```bash
|
||||||
|
# Reiniciar todos los servicios
|
||||||
|
docker compose restart
|
||||||
|
|
||||||
|
# Reiniciar solo base de datos
|
||||||
|
docker compose restart db
|
||||||
|
```
|
||||||
|
|
||||||
|
### Escalar Workers
|
||||||
|
```bash
|
||||||
|
# Añadir más traductores (CPU)
|
||||||
|
docker compose up -d --scale translator=3
|
||||||
|
|
||||||
|
# Añadir más traductores (GPU)
|
||||||
|
docker compose up -d --scale translator-gpu=2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ver Logs
|
||||||
|
```bash
|
||||||
|
# Ver logs en tiempo real
|
||||||
|
docker compose logs -f
|
||||||
|
|
||||||
|
# Ver logs específicos
|
||||||
|
docker compose logs -f db
|
||||||
|
docker compose logs -f redis
|
||||||
|
docker compose logs -f backend-go
|
||||||
|
```
|
||||||
|
|
||||||
|
### Limpiar y Reiniciar
|
||||||
|
```bash
|
||||||
|
# Detener y eliminar todo (incluye datos)
|
||||||
|
docker compose down -v
|
||||||
|
|
||||||
|
# Eliminar solo contenedores (mantiene datos)
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
# Eliminar volumen específico
|
||||||
|
docker volume rm rss2_db
|
||||||
|
docker volume rm rss2_redis
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Solución de Problemas
|
||||||
|
|
||||||
|
### Problema: WARN sobre variables no definidas
|
||||||
|
|
||||||
|
```
|
||||||
|
WARN: The "DB_PASS" variable is not set. Defaulting to a blank string.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solución:**
|
||||||
|
```bash
|
||||||
|
# Ejecutar pre-deploy para generar credenciales
|
||||||
|
./pre-deploy.sh --generate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problema: Contraseña vacía en healthcheck
|
||||||
|
|
||||||
|
```
|
||||||
|
redis-cli: Authentication failed
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solución:**
|
||||||
|
```bash
|
||||||
|
# Verificar REDIS_PASSWORD
|
||||||
|
grep REDIS_PASSWORD .env
|
||||||
|
|
||||||
|
# Si está vacío, regenerar
|
||||||
|
./pre-deploy.sh --generate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problema: Port 8888 en uso
|
||||||
|
|
||||||
|
```
|
||||||
|
ERROR: failed to start service: port 8888 already in use
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solución:**
|
||||||
|
```bash
|
||||||
|
# Cambiar puerto en docker-compose.yml
|
||||||
|
# O matar el proceso que usa el puerto
|
||||||
|
lsof -ti:8888 | xargs kill
|
||||||
|
|
||||||
|
# O usar otro puerto
|
||||||
|
docker compose up -d --scale nginx=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problema: Base de datos no inicia
|
||||||
|
|
||||||
|
```
|
||||||
|
postgres: cannot connect to database
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solución:**
|
||||||
|
```bash
|
||||||
|
# Ver logs de la base de datos
|
||||||
|
docker compose logs db
|
||||||
|
|
||||||
|
# Reiniciar base de datos
|
||||||
|
docker compose restart db
|
||||||
|
|
||||||
|
# Si persiste, eliminar y recrear (¡PERDERÁS DATOS!)
|
||||||
|
docker compose down -v
|
||||||
|
docker compose up -d db
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Estado de los Servicios
|
||||||
|
|
||||||
|
### Servicios Críticos
|
||||||
|
|
||||||
|
| Servicio | Puerto | Descripción |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| `nginx` | 8888 | Aplicación web |
|
||||||
|
| `backend-go` | 8080 | API interna |
|
||||||
|
| `db` | 5432 | PostgreSQL |
|
||||||
|
| `redis` | 6379 | Redis cache |
|
||||||
|
| `qdrant` | 6333 | Vector database |
|
||||||
|
|
||||||
|
### Dashboard de Monitorización
|
||||||
|
|
||||||
|
| Herramienta | Puerto | Acceso |
|
||||||
|
|-------------|--------|--------|
|
||||||
|
| Grafana | 3001 | http://127.0.0.1:3001 |
|
||||||
|
| Prometheus | 9090 | http://127.0.0.1:9090 |
|
||||||
|
|
||||||
|
**Nota:** Accede a Grafana desde `127.0.0.1` (localhost) por seguridad.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Actualización (Upgrade)
|
||||||
|
|
||||||
|
### Actualizar a Nueva Versión
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Pull las nuevas imágenes
|
||||||
|
docker compose pull
|
||||||
|
|
||||||
|
# 2. Backup de la base de datos
|
||||||
|
docker compose exec db pg_dump -U rss rss > backup.sql
|
||||||
|
|
||||||
|
# 3. Parar servicios
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
# 4. Actualizar código
|
||||||
|
git pull
|
||||||
|
|
||||||
|
# 5. Reiniciar
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# 6. Verificar
|
||||||
|
curl http://localhost:8888/api/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migrar Contraseñas
|
||||||
|
|
||||||
|
Si tienes credenciales existentes y quieres mantenerlas:
|
||||||
|
```bash
|
||||||
|
# 1. Verificar .env existente
|
||||||
|
grep POSTGRES_PASSWORD .env
|
||||||
|
|
||||||
|
# 2. Si quieres nuevas credenciales
|
||||||
|
./pre-deploy.sh --generate
|
||||||
|
|
||||||
|
# 3. Si quieres mantener las existentes
|
||||||
|
# No hagas nada, las credenciales seguirán funcionando
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Archivos Importantes
|
||||||
|
|
||||||
|
| Archivo | Propósito |
|
||||||
|
|---------|-----------|
|
||||||
|
| `.env` | Contraseñas y configuración (NO subir a Git) |
|
||||||
|
| `.env.example` | Plantilla (SÍ subir a Git) |
|
||||||
|
| `pre-deploy.sh` | Script de validación (SÍ subir a Git) |
|
||||||
|
| `generate_secure_credentials.sh` | Script de generación (SÍ subir a Git) |
|
||||||
|
| `docker-compose.yml` | Configuración de Docker (SÍ subir a Git) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Checklist Final
|
||||||
|
|
||||||
|
Antes de considerar tu despliegue como "listo", verifica:
|
||||||
|
|
||||||
|
- [ ] `.env` tiene credenciales generadas (no por defecto)
|
||||||
|
- [ ] `.env` está en `.gitignore`
|
||||||
|
- [ ] Puedes acceder a http://localhost:8888
|
||||||
|
- [ ] `docker compose ps` muestra todos los servicios "Up"
|
||||||
|
- [ ] La base de datos muestra "healthy"
|
||||||
|
- [ ] Los logs de la base de datos no tienen errores
|
||||||
|
- [ ] Redis responde a ping
|
||||||
|
- [ ] Qdrant está funcionando
|
||||||
|
|
||||||
|
**¡Listo! Tu RSS2 está desplegado y funcionando.** 🎉
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Documentación Adicional
|
||||||
|
|
||||||
|
- [README.md](./README.md) - Documentación general
|
||||||
|
- [SECURITY_GUIDE.md](./SECURITY_GUIDE.md) - Guía de seguridad
|
||||||
|
- [DOCKER.md](./DOCKER.md) - Configuración avanzada de Docker
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**RSS2** - *Transformando noticias en inteligencia con IA.*
|
||||||
|
|
|
||||||
467
README.md
467
README.md
|
|
@ -12,6 +12,10 @@ RSS2 es una plataforma avanzada de agregación, traducción, análisis y vectori
|
||||||
* **Traducción Neuronal de Alta Calidad**: Integración de NLLB-200 (vía CTranslate2) para traducir noticias de múltiples idiomas al español con precisión profesional.
|
* **Traducción Neuronal de Alta Calidad**: Integración de NLLB-200 (vía CTranslate2) para traducir noticias de múltiples idiomas al español con precisión profesional.
|
||||||
* **Inteligencia de Entidades (NER)**: Extracción y normalización automática de Personas, Organizaciones y Lugares para análisis de tendencias y mapeo de relaciones.
|
* **Inteligencia de Entidades (NER)**: Extracción y normalización automática de Personas, Organizaciones y Lugares para análisis de tendencias y mapeo de relaciones.
|
||||||
* **Búsqueda de Noticias Relacionadas**: Algoritmos de similitud que agrupan noticias sobre el mismo tema automáticamente.
|
* **Búsqueda de Noticias Relacionadas**: Algoritmos de similitud que agrupan noticias sobre el mismo tema automáticamente.
|
||||||
|
* **Detección de Idioma**: Identificación automática del idioma de origen para routing correcto a traducción.
|
||||||
|
* **Clustering de Noticias**: Agrupación automática de noticias relacionadas en eventos mediante embeddings.
|
||||||
|
* **Remote Workers GPU**: Workers de traducción GPUremotos conectados vía WebSocket para procesamiento distribuido.
|
||||||
|
* **Backup Automatizado**: Exportación completa de la base de datos en SQL o ZIP.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -22,14 +26,14 @@ El sistema se orquestra mediante Docker Compose y se divide en capas especializa
|
||||||
### Capa de Acceso y API
|
### Capa de Acceso y API
|
||||||
| Servicio | Tecnología | Descripción |
|
| Servicio | Tecnología | Descripción |
|
||||||
|---------|------------|-------------|
|
|---------|------------|-------------|
|
||||||
| **`nginx`** | Nginx Alpine | Gateway y Proxy Inverso (Puerto **8001**). |
|
| **`nginx`** | Nginx Alpine | Gateway y Proxy Inverso (Puerto **8888**). |
|
||||||
| **`rss2_frontend`** | React + Vite | Interfaz web de usuario moderna y responsiva. |
|
| **`rss2_frontend`** | React + Vite | Interfaz web de usuario moderna y responsiva. |
|
||||||
| **`backend-go`** | Go + Gin | API REST principal y gestión de lógica de negocio. |
|
| **`backend-go`** | Go + Gin | API REST principal y gestión de lógica de negocio. |
|
||||||
|
|
||||||
### Ingesta y Descubrimiento (Go)
|
### Ingesta y Descubrimiento (Go)
|
||||||
| Servicio | Tecnología | Descripción |
|
| Servicio | Tecnología | Descripción |
|
||||||
|---------|------------|-------------|
|
|---------|------------|-------------|
|
||||||
| **`rss-ingestor-go`** | Go | Crawler de alto rendimiento para feeds RSS. |
|
| **`rss-ingestor-go`** | Go | Crawler de alto rendimiento para feeds RSS (100 workers). |
|
||||||
| **`scraper`** | Go | Scraper profundo con sanitización de HTML y extracción de texto. |
|
| **`scraper`** | Go | Scraper profundo con sanitización de HTML y extracción de texto. |
|
||||||
| **`discovery`** | Go | Agente autónomo para descubrir nuevos feeds a partir de URLs. |
|
| **`discovery`** | Go | Agente autónomo para descubrir nuevos feeds a partir de URLs. |
|
||||||
|
|
||||||
|
|
@ -37,13 +41,18 @@ El sistema se orquestra mediante Docker Compose y se divide en capas especializa
|
||||||
| Servicio | Tecnología | Descripción |
|
| Servicio | Tecnología | Descripción |
|
||||||
|---------|------------|-------------|
|
|---------|------------|-------------|
|
||||||
| **`translator`** | NLLB-200 (CPU) | Traducción neuronal optimizada con CTranslate2. |
|
| **`translator`** | NLLB-200 (CPU) | Traducción neuronal optimizada con CTranslate2. |
|
||||||
| **`translator-gpu`**| NLLB-200 (GPU) | Traducción acelerada por hardware (CUDA). |
|
| **`translator-gpu`** | NLLB-200 (GPU) | Traducción acelerada por hardware (CUDA). |
|
||||||
| **`wiki-worker`** | Go | **[NUEVO]** Integración con Wikipedia y gestión de imágenes locales. |
|
| **`remote-translator`** | WebSocket | Worker GPU remoto (conexión vía ws://backend-go:8080/ws/worker). |
|
||||||
| **`embeddings`** | S-Transformers | Generación de vectores para búsqueda semántica. |
|
| **`wiki-worker`** | Go | Integración con Wikipedia y gestión de imágenes locales. |
|
||||||
| **`ner`** | Spacy / BERT | Reconocimiento de entidades nombradas (NER). |
|
| **`embeddings`** | S-Transformers | Generación de vectores para búsqueda semántica (paraphrase-multilingual-MiniLM-L12-v2). |
|
||||||
| **`llm-categorizer`**| Ollama / Mistral | Clasificación avanzada mediante modelos de lenguaje. |
|
| **`ner`** | Spacy / es_core_news_lg | Reconocimiento de entidades nombradas (NER). |
|
||||||
|
| **`llm-categorizer`** | Ollama / Mistral | Clasificación avanzada mediante modelos de lenguaje. |
|
||||||
| **`topics`** | Go | Matcher automático de países y temas predefinidos. |
|
| **`topics`** | Go | Matcher automático de países y temas predefinidos. |
|
||||||
| **`related`** | Go | Motor de detección de noticias relacionadas. |
|
| **`related`** | Go | Motor de detección de noticias relacionadas (similitud coseno). |
|
||||||
|
| **`qdrant-worker`** | Go | Vectorización y búsqueda semántica con Qdrant + Ollama. |
|
||||||
|
| **`cluster`** | Python | Agrupación de noticias por eventos. |
|
||||||
|
| **`langdetect`** | Python | Detección de idioma para routing de traducciones. |
|
||||||
|
| **`translation-scheduler`** | Python | Creador de tareas de traducción. |
|
||||||
|
|
||||||
### Capa de Almacenamiento
|
### Capa de Almacenamiento
|
||||||
| Servicio | Tecnología | Descripción |
|
| Servicio | Tecnología | Descripción |
|
||||||
|
|
@ -52,6 +61,90 @@ El sistema se orquestra mediante Docker Compose y se divide en capas especializa
|
||||||
| **`qdrant`** | Qdrant | Base de datos vectorial para búsqueda por similitud. |
|
| **`qdrant`** | Qdrant | Base de datos vectorial para búsqueda por similitud. |
|
||||||
| **`redis`** | Redis 7 | Colas de mensajes y caché de alto desempeño. |
|
| **`redis`** | Redis 7 | Colas de mensajes y caché de alto desempeño. |
|
||||||
|
|
||||||
|
### Monitoreo
|
||||||
|
| Servicio | Tecnología | Descripción |
|
||||||
|
|---------|------------|-------------|
|
||||||
|
| **`prometheus`** | Prometheus | Métricas del sistema. |
|
||||||
|
| **`grafana`** | Grafana | Dashboard (puerto **3001**). |
|
||||||
|
| **`cadvisor`** | cAdvisor | Monitoreo Docker. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Endpoints de API
|
||||||
|
|
||||||
|
### News
|
||||||
|
- `GET /api/news` - Listar noticias (paginado, filtros: q, category_id, country_id, translated_only)
|
||||||
|
- `GET /api/news/:id` - Ver noticia con entidades (incluye Wikipedia enrichment)
|
||||||
|
- `DELETE /api/news/:id` - Eliminar noticia (admin)
|
||||||
|
|
||||||
|
### Feeds
|
||||||
|
- `GET /api/feeds` - Listar feeds
|
||||||
|
- `POST /api/feeds` - Crear feed (auth requerido)
|
||||||
|
- `PUT /api/feeds/:id` - Actualizar feed (auth requerido)
|
||||||
|
- `DELETE /api/feeds/:id` - Eliminar feed (auth requerido)
|
||||||
|
- `POST /api/feeds/:id/toggle` - Activar/desactivar feed
|
||||||
|
- `POST /api/feeds/:id/reactivate` - Reactivar feed
|
||||||
|
|
||||||
|
### Search
|
||||||
|
- `GET /api/search?q=...` - Búsqueda texto (filtros: lang, categoria_id, pais_id)
|
||||||
|
- `GET /api/search?q=...&semantic=true` - Búsqueda semántica con Qdrant
|
||||||
|
|
||||||
|
### Entities
|
||||||
|
- `GET /api/entities?tipo=persona|organizacion|lugar` - Listar entidades (con aliasing)
|
||||||
|
|
||||||
|
### Admin
|
||||||
|
- `GET /api/admin/backup` - Backup SQL completo (descargable)
|
||||||
|
- `GET /api/admin/backup/news` - Backup noticias (ZIP)
|
||||||
|
- `GET /api/admin/users` - Listar usuarios
|
||||||
|
- `POST /api/admin/users/:id/promote` - Promover a admin
|
||||||
|
- `POST /api/admin/users/:id/demote` - Quitar admin
|
||||||
|
- `POST /api/admin/reset-db` - Resetear base de datos
|
||||||
|
- `POST /api/admin/aliases` - Crear alias de entidad
|
||||||
|
- `GET /api/admin/aliases/export` - Exportar aliases (CSV)
|
||||||
|
- `POST /api/admin/aliases/import` - Importar aliases (CSV)
|
||||||
|
- `POST /api/admin/entities/retype` - Cambiar tipo de entidad
|
||||||
|
- `POST /api/admin/workers/config` - Configurar workers (type, workers)
|
||||||
|
- `POST /api/admin/workers/start` - Iniciar workers traducción
|
||||||
|
- `POST /api/admin/workers/stop` - Detener workers traducción
|
||||||
|
- `GET /api/admin/workers/status` - Estado de workers
|
||||||
|
- `GET /api/admin/workers/remote` - Listar remote workers
|
||||||
|
- `POST /api/admin/workers/remote` - Crear remote worker
|
||||||
|
- `DELETE /api/admin/workers/remote/:id` - Eliminar remote worker
|
||||||
|
|
||||||
|
### Auth
|
||||||
|
- `POST /api/auth/login` - Iniciar sesión
|
||||||
|
- `POST /api/auth/register` - Registrarse
|
||||||
|
- `GET /api/auth/me` - Usuario actual (auth requerido)
|
||||||
|
- `GET /api/auth/check-first-user` - Verificar si existe primer usuario
|
||||||
|
|
||||||
|
### Stats
|
||||||
|
- `GET /api/stats` - Estadísticas globales (total news, feeds, users, hoy/semana/mes)
|
||||||
|
- `GET /api/categories` - Listar categorías
|
||||||
|
- `GET /api/countries` - Listar países con continentes
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
- `GET /ws/worker` - Conexión de remote workers (protocolo de autenticación con API key)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗄️ Esquema de Base de Datos (Tablas Principales)
|
||||||
|
|
||||||
|
- **noticias**: Artículos RSS (titulo, resumen, url, feed_id, categoria_id, pais_id, lang)
|
||||||
|
- **feeds**: Fuentes RSS (nombre, url, activo, ultimo_fetch)
|
||||||
|
- **traducciones**: Traducciones (noticia_id, lang_from, lang_to, titulo_trad, resumen_trad, status, vectorized)
|
||||||
|
- **tags**: Entidades (valor, tipo: persona/organizacion/lugar/tema, wiki_summary, wiki_url, image_path)
|
||||||
|
- **tags_noticia**: Relación noticia-tag (traduccion_id, noticia_id, tag_id)
|
||||||
|
- **entity_aliases**: Alias de entidades (canonical_name, alias, tipo)
|
||||||
|
- **categorias**: Categorías de noticias
|
||||||
|
- **paises**: Países con continentes
|
||||||
|
- **related_noticias**: Noticias relacionadas (traduccion_id, related_traduccion_id, score)
|
||||||
|
- **traduccion_embeddings**: Embeddings en BD (traduccion_id, model, embedding)
|
||||||
|
- **users**: Usuarios (email, username, password_hash, is_admin)
|
||||||
|
- **config**: Configuración del sistema (translator_type, translator_workers, translator_status)
|
||||||
|
- **remote_workers**: Workers remotos (name, api_key, capabilities, status, last_seen)
|
||||||
|
- **favoritos**: Noticias favoritas de usuarios
|
||||||
|
- **search_history**: Historial de búsquedas
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚙️ Guía de Configuración
|
## ⚙️ Guía de Configuración
|
||||||
|
|
@ -60,34 +153,228 @@ El sistema se orquestra mediante Docker Compose y se divide en capas especializa
|
||||||
* **Modo Básico (CPU)**: 4+ Cores CPU, 8GB RAM.
|
* **Modo Básico (CPU)**: 4+ Cores CPU, 8GB RAM.
|
||||||
* **Modo Avanzado (IA)**: NVIDIA GPU con 8GB+ VRAM (mínimo recomendado para LLM y Traducción GPU).
|
* **Modo Avanzado (IA)**: NVIDIA GPU con 8GB+ VRAM (mínimo recomendado para LLM y Traducción GPU).
|
||||||
|
|
||||||
### 2. Instalación Rápida
|
### 2. Instalación Rápida con Seguridad Automática
|
||||||
|
|
||||||
|
#### Opción A: Despliegue con Validación Automática (RECOMENDADO)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Paso 1: Clonar el proyecto
|
||||||
git clone <repo_url>
|
git clone <repo_url>
|
||||||
cd rss2
|
cd rss2
|
||||||
cp .env.example .env
|
|
||||||
# Edita .env con tus credenciales
|
# Paso 2: Generar credenciales seguras automáticamente
|
||||||
|
./pre-deploy.sh --generate
|
||||||
|
|
||||||
|
# Paso 3: Validar y desplegar
|
||||||
|
./pre-deploy.sh
|
||||||
|
|
||||||
|
# Paso 4: Si eliges opción 2 en el prompt, despliega manualmente
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Escalado de Workers (¡Importante!)
|
**Ventajas:**
|
||||||
Para aumentar la velocidad de procesamiento (especialmente la traducción), puedes escalar los workers:
|
- ✅ Genera credenciales seguras de 32 caracteres
|
||||||
|
- ✅ Valida que no haya contraseñas por defecto
|
||||||
|
- ✅ Muestra resumen de credenciales en consola
|
||||||
|
- ✅ Automático y seguro para producción
|
||||||
|
|
||||||
|
#### Opción B: Despliegue Manual (Para desarrolladores)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Ejecutar 4 traductores en paralelo
|
git clone <repo_url>
|
||||||
docker compose up -d --scale translator=4
|
cd rss2
|
||||||
|
|
||||||
# Si usas GPU y tienes capacidad
|
# Generar credenciales manualmente
|
||||||
docker compose up -d --scale translator-gpu=2
|
./generate_secure_credentials.sh
|
||||||
|
|
||||||
|
# Copiar credenciales a .env
|
||||||
|
cp .env.generated .env
|
||||||
|
|
||||||
|
# Desplegar
|
||||||
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🔐 Despliegue con Seguridad Automática (RECOMENDADO)
|
||||||
|
|
||||||
|
### 🛡️ Sistema de Validación Pre-Despliegue
|
||||||
|
|
||||||
|
RSS2 incluye `pre-deploy.sh`, un script inteligente que se ejecuta **antes** de `docker compose` para:
|
||||||
|
|
||||||
|
1. ✅ **Validar credenciales**: Asegura que `POSTGRES_PASSWORD`, `REDIS_PASSWORD`, `DB_PASS` estén definidas
|
||||||
|
2. ✅ **Generar automáticamente**: Crea credenciales seguras de 32 caracteres si faltan
|
||||||
|
3. ✅ **Prevenir errores**: Evita que los WARN de Docker por variables vacías
|
||||||
|
4. ✅ **Mostrar resumen**: Te muestra todas las credenciales en consola
|
||||||
|
|
||||||
|
### 🚀 Despliegue Rápido y Seguro (3 Comandos)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Generar credenciales seguras automáticamente
|
||||||
|
./pre-deploy.sh --generate
|
||||||
|
|
||||||
|
# 2. Validar y desplegar (elige opción 2 para despliegue manual)
|
||||||
|
./pre-deploy.sh
|
||||||
|
|
||||||
|
# 3. Alternativa: Despliegue manual después de validar
|
||||||
|
./pre-deploy.sh # ← Elige "2) Solo validar y salir"
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Flujo recomendado:**
|
||||||
|
```bash
|
||||||
|
./pre-deploy.sh --generate # Genera .env con credenciales seguras
|
||||||
|
./pre-deploy.sh # Valida y te pregunta si quieres desplegar
|
||||||
|
# Si eliges "2): Solo validar", luego:
|
||||||
|
docker compose up -d # Despliega manualmente
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📋 Opciones del Script `pre-deploy.sh`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Validar credenciales existentes
|
||||||
|
./pre-deploy.sh
|
||||||
|
|
||||||
|
# Generar credenciales seguras automáticamente
|
||||||
|
./pre-deploy.sh --generate
|
||||||
|
|
||||||
|
# Saltar validación (NO RECOMENDADO para producción)
|
||||||
|
./pre-deploy.sh --skip
|
||||||
|
|
||||||
|
# Ver ayuda
|
||||||
|
./pre-deploy.sh --help
|
||||||
|
```
|
||||||
|
|
||||||
|
**Salida típica del script:**
|
||||||
|
```
|
||||||
|
╔═══════════════════════════════════════════════════════════╗
|
||||||
|
║ 🔐 Pre-Deploy Security Check - RSS2 Platform ║
|
||||||
|
╚═══════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
✅ Archivo .env encontrado
|
||||||
|
✅ DEFINIDO: POSTGRES_PASSWORD=8x7f2k9m4p1q3w5e
|
||||||
|
✅ DEFINIDO: REDIS_PASSWORD=a9b8c7d6e5f4g3h2
|
||||||
|
✅ DEFINIDO: DB_PASS=8x7f2k9m4p1q3w5e
|
||||||
|
|
||||||
|
📋 Resumen de Credenciales Activas
|
||||||
|
POSTGRES_PASSWORD: ✓
|
||||||
|
REDIS_PASSWORD: ✓
|
||||||
|
DB_PASS: ✓
|
||||||
|
|
||||||
|
✅ VALIDACIÓN COMPLETADA - LISTO PARA DESPLEGAR
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔒 Variables Críticas que Requieren Contraseñas
|
||||||
|
|
||||||
|
| Variable | Descripción | Uso |
|
||||||
|
|----------|-------------|-----|
|
||||||
|
| `POSTGRES_PASSWORD` | Contraseña de PostgreSQL | Base de datos |
|
||||||
|
| `REDIS_PASSWORD` | Contraseña de Redis | Cache y colas |
|
||||||
|
| `DB_PASS` | Contraseña para workers | Conexiones de workers |
|
||||||
|
| `SECRET_KEY` | Key secreta de la aplicación | JWT, encriptación |
|
||||||
|
| `GRAFANA_PASSWORD` | Contraseña de Grafana | Dashboard de monitoring |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Escalado de Workers GPU (¡Importante!)
|
||||||
|
|
||||||
|
#### 🚀 Workers de Traducción GPU Multi-Instancia
|
||||||
|
|
||||||
|
RSS2 soporta múltiples workers de traducción ejecutándose en paralelo utilizando GPUs, lo que acelera significativamente el proceso de traducción masiva.
|
||||||
|
|
||||||
|
**Arquitectura Multi-Worker:**
|
||||||
|
- Cada worker es una instancia independiente del servicio `ctranslator_worker`
|
||||||
|
- Los workers compiten automáticamente por las traducciones pendientes usando `SELECT ... FOR UPDATE SKIP LOCKED`
|
||||||
|
- Cada worker procesa su propio lote (batch) de traducciones sin bloquearse mutuamente
|
||||||
|
- Soporta hasta 8 workers simultáneos (configurable vía panel admin)
|
||||||
|
|
||||||
|
**Requisitos GPU:**
|
||||||
|
```bash
|
||||||
|
# Verificar GPU disponibles
|
||||||
|
nvidia-smi
|
||||||
|
|
||||||
|
# Instalar drivers NVIDIA (si no los tienes)
|
||||||
|
# Ubuntu/Debian:
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y nvidia-driver-535
|
||||||
|
```
|
||||||
|
|
||||||
|
**Despliegue con Múltiples Workers GPU:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Opción 1: Desplegar 1 worker GPU (mínimo recomendado)
|
||||||
|
docker compose up -d --scale translator-gpu=1
|
||||||
|
|
||||||
|
# Opción 2: Desplegar 2 workers GPU (recomendado: 16GB+ VRAM total)
|
||||||
|
docker compose up -d --scale translator-gpu=2
|
||||||
|
|
||||||
|
# Opción 3: Desplegar 4 workers GPU (requiere: 32GB+ VRAM total)
|
||||||
|
docker compose up -d --scale translator-gpu=4
|
||||||
|
|
||||||
|
# Opción 4: Combinar CPU y GPU
|
||||||
|
# 2 workers CPU + 2 workers GPU
|
||||||
|
docker compose up -d --scale translator=2 --scale translator-gpu=2
|
||||||
|
```
|
||||||
|
|
||||||
|
**Panel de Administración:**
|
||||||
|
Desde `/admin/settings`, puedes:
|
||||||
|
- Activar modo GPU (CUDA) o CPU
|
||||||
|
- Configurar número de workers (1-8)
|
||||||
|
- Iniciar/detener workers automáticamente
|
||||||
|
|
||||||
|
**Configuración de GPU por Worker:**
|
||||||
|
Cada worker GPU está configurado con:
|
||||||
|
- `CT2_DEVICE=cuda` - Usa dispositivos CUDA
|
||||||
|
- `CT2_COMPUTE_TYPE=float16` - Precisión mixta para velocidad
|
||||||
|
- `TRANSLATOR_BATCH=128` - Tamaño de lote optimizado
|
||||||
|
- `PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:512` - Gestión de memoria PyTorch
|
||||||
|
- `NCCL_DEBUG=INFO` - Debug de comunicación multi-GPU
|
||||||
|
|
||||||
|
**Monitorización:**
|
||||||
|
```bash
|
||||||
|
# Ver estado de workers
|
||||||
|
docker compose --profile gpu ps translator-gpu
|
||||||
|
|
||||||
|
# Ver uso de GPU
|
||||||
|
watch -n 1 nvidia-smi
|
||||||
|
|
||||||
|
# Ver logs en tiempo real
|
||||||
|
docker compose logs -f translator-gpu
|
||||||
|
|
||||||
|
# Métricas en Grafana
|
||||||
|
# Dashboard: Translation Workers
|
||||||
|
# Métricas: translator_*_transitions_per_minute
|
||||||
|
```
|
||||||
|
|
||||||
|
**Performance Esperada:**
|
||||||
|
| Workers GPU | VRAM Total | Tasa de Traducción |
|
||||||
|
|-------------|------------|-------------------|
|
||||||
|
| 1 worker | 8GB+ | ~500 traducciones/min |
|
||||||
|
| 2 workers | 16GB+ | ~1000 traducciones/min |
|
||||||
|
| 4 workers | 32GB+ | ~2000 traducciones/min |
|
||||||
|
| 8 workers | 64GB+ | ~4000 traducciones/min |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🛡️ Administración y Mantenimiento
|
## 🛡️ Administración y Mantenimiento
|
||||||
|
|
||||||
### Copias de Seguridad (Backups)
|
### Copias de Seguridad (Backups)
|
||||||
|
|
||||||
Desde el panel de Administración (`/admin/settings`), puedes realizar:
|
Desde el panel de Administración (`/admin/settings`), puedes realizar:
|
||||||
* **Backup Completo**: Volcado SQL de toda la base de datos.
|
|
||||||
* **Backup de Noticias (ZIP)**: **[NUEVO]** Genera un archivo comprimido que incluye las tablas de noticias, traducciones y todas sus etiquetas. Ideal para migraciones de contenido.
|
**Backup Completo (SQL):**
|
||||||
|
- Descarga un archivo `.sql` con todo el contenido de la base de datos
|
||||||
|
- Incluye: noticias, feeds, traducciones, usuarios, etiquetas, favoritos, videos, eventos, historial de búsqueda
|
||||||
|
- Formato: PostgreSQL custom format con datos y estructura
|
||||||
|
- Tamaño típico: 10-100MB dependiendo del volumen de datos
|
||||||
|
|
||||||
|
**Backup de Noticias (ZIP):**
|
||||||
|
- Genera un archivo comprimido `.zip` con:
|
||||||
|
- Tabla `noticias` (artículos completos)
|
||||||
|
- Tabla `traducciones` (todas las traducciones)
|
||||||
|
- Tablas `tags` y `tags_noticia` (metadatos y relaciones)
|
||||||
|
- Ideal para: Migraciones de contenido, exportación de datos, backup ligero
|
||||||
|
- Tamaño típico: 5-50MB (solo contenido de noticias)
|
||||||
|
|
||||||
### Variables de Entorno Clave (`.env`)
|
### Variables de Entorno Clave (`.env`)
|
||||||
| Variable | Descripción |
|
| Variable | Descripción |
|
||||||
|
|
@ -95,7 +382,108 @@ Desde el panel de Administración (`/admin/settings`), puedes realizar:
|
||||||
| `WIKI_SLEEP` | Tiempo de espera entre peticiones a Wikipedia (evita bloqueos). |
|
| `WIKI_SLEEP` | Tiempo de espera entre peticiones a Wikipedia (evita bloqueos). |
|
||||||
| `SCHEDULER_BATCH`| Cantidad de noticias a enviar a traducir por ciclo. |
|
| `SCHEDULER_BATCH`| Cantidad de noticias a enviar a traducir por ciclo. |
|
||||||
| `TARGET_LANGS` | Idiomas destino (ej: `es`). |
|
| `TARGET_LANGS` | Idiomas destino (ej: `es`). |
|
||||||
| `OLLAMA_HOST` | Dirección del servidor Ollama para categorización. |
|
| `OLLAMA_URL` | URL del servidor Ollama para categorización. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Descripción de Workers
|
||||||
|
|
||||||
|
### rss-ingestor-go
|
||||||
|
Crawler RSS de alto rendimiento escrito en Go. Gestiona hasta 100 workers paralelos para procesar múltiples feeds simultáneamente. Detecta nuevos artículos, extrae metadatos básicos y los inserta en la base de datos.
|
||||||
|
|
||||||
|
### scraper (Go)
|
||||||
|
Scraper profundo que:
|
||||||
|
- Descarga el contenido completo de URLs de artículos
|
||||||
|
- Sanitiza HTML removiendo scripts y estilos
|
||||||
|
- Extrae texto limpio del body
|
||||||
|
- Limita el contenido a 20 noticias por ciclo para evitar sobrecarga
|
||||||
|
|
||||||
|
### discovery (Go)
|
||||||
|
Agente de descubrimiento de feeds que:
|
||||||
|
- Analiza URLs proporcionadas para detectar feeds RSS/Atom
|
||||||
|
- Soporta hasta 5 feeds por URL
|
||||||
|
- Ejecuta cada 900 segundos (15 minutos)
|
||||||
|
|
||||||
|
### translator (Python/CTranslate2)
|
||||||
|
Worker de traducción neuronal:
|
||||||
|
- Modelo: facebook/nllb-200-distilled-600M convertido a CTranslate2
|
||||||
|
- Dispositivo: CPU (int8)
|
||||||
|
- Batch: 32 traducciones por ciclo
|
||||||
|
- Locking: `FOR UPDATE SKIP LOCKED` para evitar procesamiento duplicado
|
||||||
|
- Soporta múltiples idiomas fuente
|
||||||
|
|
||||||
|
### translator-gpu (Python/CTranslate2)
|
||||||
|
Versión acelerada por GPU del translator:
|
||||||
|
- Dispositivo: CUDA
|
||||||
|
- Compute type: float16
|
||||||
|
- Batch: 128 traducciones por ciclo
|
||||||
|
- Ideal para procesamiento de alto volumen
|
||||||
|
|
||||||
|
### remote-translator (Python)
|
||||||
|
Worker remoto conectado vía WebSocket:
|
||||||
|
- Autenticación con API key
|
||||||
|
- Conexión a `ws://backend-go:8080/ws/worker`
|
||||||
|
- Modelo: facebook/nllb-200-1.3B (más grande)
|
||||||
|
- GPU requerida
|
||||||
|
|
||||||
|
### langdetect (Python)
|
||||||
|
Detector de idioma:
|
||||||
|
- Usa biblioteca `langdetect`
|
||||||
|
- Proceso: 1000 noticias por ciclo
|
||||||
|
- Establece lang_from para correcto routing a traducción
|
||||||
|
|
||||||
|
### ner (Python/spaCy)
|
||||||
|
Extracción de entidades nombradas:
|
||||||
|
- Modelo: es_core_news_lg
|
||||||
|
- Tipos: PERSON (persona), ORG (organizacion), LOC/GPE (lugar)
|
||||||
|
- También extrae topics (noun-chunks)
|
||||||
|
- Configurable via entity_config.json (blacklist, synonyms)
|
||||||
|
|
||||||
|
### embeddings (Python)
|
||||||
|
Generador de embeddings para búsqueda semántica:
|
||||||
|
- Modelo: paraphrase-multilingual-MiniLM-L12-v2
|
||||||
|
- Dispositivo: GPU (cuda)
|
||||||
|
- Genera vectores de 384 dimensiones
|
||||||
|
- Alimenta Qdrant
|
||||||
|
|
||||||
|
### qdrant-worker (Go)
|
||||||
|
Worker de vectorización Qdrant:
|
||||||
|
- Conecta a Qdrant (puerto 6333)
|
||||||
|
- Genera embeddings via Ollama (mxbai-embed-large)
|
||||||
|
- Sube puntos a collection "news_vectors"
|
||||||
|
- Actualiza estado en BD (vectorized=true)
|
||||||
|
|
||||||
|
### related (Go)
|
||||||
|
Detector de noticias relacionadas:
|
||||||
|
- Usa similitud coseno entre embeddings
|
||||||
|
- Almacena top-k relaciones en tabla related_noticias
|
||||||
|
- Soporta configuración de threshold mínimo
|
||||||
|
|
||||||
|
### wiki-worker (Go)
|
||||||
|
Enriquecedor de Wikipedia:
|
||||||
|
- Descarga summaries de Wikipedia API
|
||||||
|
- Descarga thumbnails de imágenes
|
||||||
|
- Almacena en `./data/wiki_images`
|
||||||
|
- Evita rate limiting con WIKI_SLEEP
|
||||||
|
|
||||||
|
### llm-categorizer (Python)
|
||||||
|
Categorizador con LLM:
|
||||||
|
- Conexión a Ollama
|
||||||
|
- Usa modelo Mistral para clasificación
|
||||||
|
- Procesa 10 noticias por batch
|
||||||
|
|
||||||
|
### cluster (Python)
|
||||||
|
Agrupador de noticias por eventos:
|
||||||
|
- Usa embeddings paraphrase-multilingual-MiniLM-L12-v2
|
||||||
|
- Threshold: 0.35 de distancia
|
||||||
|
- Agrupa noticias similares
|
||||||
|
|
||||||
|
### translation-scheduler (Python)
|
||||||
|
Creador de tareas de traducción:
|
||||||
|
- Query: noticias sin traducción al español
|
||||||
|
- Crea entradas en tabla traducciones
|
||||||
|
- Batch: 1000 por ciclo
|
||||||
|
- Ciclo: 30 segundos
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -121,4 +509,41 @@ Las respuestas de noticias ahora incluyen el objeto `entities` enriquecido:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**RSS2** - *Transformando noticias en inteligencia con IA localizada.*
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Backend (Go)
|
||||||
|
```bash
|
||||||
|
cd backend && go test ./internal/handlers -v -run TestLogin
|
||||||
|
cd backend && go test ./internal/auth -v -run TestGenerateToken
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
```bash
|
||||||
|
cd frontend && npm run test:ui
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Estructura de Archivos del Proyecto
|
||||||
|
|
||||||
|
```
|
||||||
|
rss2/
|
||||||
|
├── backend/ # API REST Go (Gin)
|
||||||
|
├── frontend/ # Interfaz React + TypeScript
|
||||||
|
├── workers/ # Workers Python
|
||||||
|
├── rss-ingestor-go/ # Crawler RSS Go
|
||||||
|
├── docker-compose.yml # Orquestación
|
||||||
|
├── nginx.conf # Config Nginx
|
||||||
|
├── monitoring/ # Prometheus + Grafana
|
||||||
|
├── init-db/ # Migraciones SQL
|
||||||
|
├── data/ # Datos persistentes
|
||||||
|
├── models/ # Modelos ML
|
||||||
|
├── hf_cache/ # Cache HuggingFace
|
||||||
|
├── pre-deploy.sh # Script seguridad
|
||||||
|
├── generate_secure_credentials.sh
|
||||||
|
└── AGENTS.md # Guía desarrollo
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**RSS2** - *Transformando noticias en inteligencia con IA localizada.*
|
||||||
220
README_DEPLOY.md
Normal file
220
README_DEPLOY.md
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
# 🚀 Despliegue RSS2 - 3 Comandos Simples
|
||||||
|
|
||||||
|
## ⚡ Flujo Rápido (3 pasos)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Generar credenciales seguras
|
||||||
|
./generate_secure_credentials.sh --force
|
||||||
|
|
||||||
|
# 2. Desplegar
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# 3. Verificar
|
||||||
|
docker compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
**¡Listo!** La aplicación estará disponible en `http://localhost:8888`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 ¿Por qué 3 pasos?
|
||||||
|
|
||||||
|
**Problema original:**
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
WARN: DB_PASS no definida → Contraseña vacía → ¡Inseguro!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solución:**
|
||||||
|
1. `./generate_secure_credentials.sh --force` → Crea `.env` con contraseñas de 32 caracteres
|
||||||
|
2. `docker compose up -d` → Docker carga `.env` automáticamente
|
||||||
|
3. `docker compose ps` → Verifica que todo esté bien
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Qué hace cada comando
|
||||||
|
|
||||||
|
### 1. `./generate_secure_credentials.sh --force`
|
||||||
|
|
||||||
|
- Crea contraseñas seguras de 32 caracteres aleatorios
|
||||||
|
- Genera archivo `.env` con todas las variables necesarias
|
||||||
|
- Guarda backup del `.env` anterior
|
||||||
|
- **Tiempo: 5 segundos**
|
||||||
|
|
||||||
|
**Salida:**
|
||||||
|
```
|
||||||
|
POSTGRES_PASSWORD: S5xwsI9HKjdaBWLpCkqp0mA0DJYZerpD
|
||||||
|
REDIS_PASSWORD: cVF79QuJulPO5auEhA2nNqv7mqAruzYh
|
||||||
|
SECRET_KEY: 2eb7c221245bb8d896f83255188de614e59fae6b8dfbf16eee8a23c60dde4ffa
|
||||||
|
GRAFANA_PASSWORD: R4J61V6OGLTCesrITwEvUPTm
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. `docker compose up -d`
|
||||||
|
|
||||||
|
- Lee `.env` automáticamente (está configurado en `docker-compose.yml`)
|
||||||
|
- Inicia todos los servicios en modo detached (background)
|
||||||
|
- Espera a que estén listos
|
||||||
|
- **Tiempo: 2-5 minutos** (primera vez que descarga imágenes)
|
||||||
|
|
||||||
|
### 3. `docker compose ps`
|
||||||
|
|
||||||
|
- Muestra estado de todos los servicios
|
||||||
|
- Deberías ver `Up` para todos
|
||||||
|
- **Tiempo: 1 segundo**
|
||||||
|
|
||||||
|
**Salida esperada:**
|
||||||
|
```
|
||||||
|
Name Status
|
||||||
|
rss2_db Up (healthy)
|
||||||
|
rss2_redis Up (healthy)
|
||||||
|
rss2_backend_go Up (running)
|
||||||
|
rss2_nginx Up (running)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Comandos Útiles
|
||||||
|
|
||||||
|
### Verificar credenciales
|
||||||
|
```bash
|
||||||
|
grep -E "^(POSTGRES_PASSWORD|REDIS_PASSWORD|DB_PASS)=" .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ver logs en tiempo real
|
||||||
|
```bash
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reiniciar servicios
|
||||||
|
```bash
|
||||||
|
# Todos
|
||||||
|
docker compose restart
|
||||||
|
|
||||||
|
# Solo base de datos
|
||||||
|
docker compose restart db
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verificar estado
|
||||||
|
```bash
|
||||||
|
docker compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### Limpiar todo (PERDERÁS DATOS)
|
||||||
|
```bash
|
||||||
|
docker compose down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Escalar workers
|
||||||
|
```bash
|
||||||
|
# Más traductores CPU
|
||||||
|
docker compose up -d --scale translator=3
|
||||||
|
|
||||||
|
# Más traductores GPU
|
||||||
|
docker compose up -d --scale translator-gpu=2
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Seguridad
|
||||||
|
|
||||||
|
### Contraseñas por defecto = PELIGRO ❌
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# MAL - Contraseñas vacías
|
||||||
|
docker compose up -d
|
||||||
|
WARN: DB_PASS no set
|
||||||
|
# Las contraseñas están vacías
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contraseñas generadas = SEGURO ✅
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# BIEN
|
||||||
|
./generate_secure_credentials.sh --force
|
||||||
|
docker compose up -d
|
||||||
|
# Contraseñas de 32 caracteres aleatorios
|
||||||
|
```
|
||||||
|
|
||||||
|
### Guardar tus credenciales
|
||||||
|
|
||||||
|
**Opción 1: Gestor de contraseñas (Recomendado)**
|
||||||
|
```bash
|
||||||
|
# Copia las contraseñas que muestra el script
|
||||||
|
# Guárdalas en: LastPass, 1Password, KeePass, Bitwarden
|
||||||
|
```
|
||||||
|
|
||||||
|
**Opción 2: Archivo seguro local**
|
||||||
|
```bash
|
||||||
|
# Crea un archivo fuera del proyecto
|
||||||
|
echo "POSTGRES_PASSWORD=..." > ~/rss2-credentials.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Estado de los Servicios
|
||||||
|
|
||||||
|
| Servicio | Puerto | Descripción |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| `nginx` | 8888 | Web app |
|
||||||
|
| `backend-go` | 8080 | API |
|
||||||
|
| `db` | 5432 | PostgreSQL |
|
||||||
|
| `redis` | 6379 | Cache |
|
||||||
|
| `qdrant` | 6333 | Vector DB |
|
||||||
|
|
||||||
|
**DASHBOARDS:**
|
||||||
|
- Grafana: http://127.0.0.1:3001
|
||||||
|
- Prometheus: http://127.0.0.1:9090
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Solución de Problemas
|
||||||
|
|
||||||
|
### WARN sobre contraseñas vacías
|
||||||
|
```bash
|
||||||
|
# Solución: Generar credenciales
|
||||||
|
./generate_secure_credentials.sh --force
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port 8888 en uso
|
||||||
|
```bash
|
||||||
|
# Cambiar puerto en nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Base de datos no inicia
|
||||||
|
```bash
|
||||||
|
# Ver logs
|
||||||
|
docker compose logs db
|
||||||
|
|
||||||
|
# Reiniciar
|
||||||
|
docker compose restart db
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redis autenticación fallida
|
||||||
|
```bash
|
||||||
|
# Ver REDIS_PASSWORD
|
||||||
|
grep REDIS_PASSWORD .env
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Checklist Final
|
||||||
|
|
||||||
|
Antes de considerar tu despliegue listo:
|
||||||
|
|
||||||
|
- [ ] `.env` tiene contraseñas generadas (no por defecto)
|
||||||
|
- [ ] `docker compose ps` muestra todos "Up"
|
||||||
|
- [ ] `http://localhost:8888` abre la app
|
||||||
|
- [ ] `curl http://localhost:8888/api/health` responde
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Documentación
|
||||||
|
|
||||||
|
- [README.md](./README.md) - Documentación completa
|
||||||
|
- [DEPLOY.md](./DEPLOY.md) - Guía detallada
|
||||||
|
- [SECURITY_GUIDE.md](./SECURITY_GUIDE.md) - Seguridad
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**RSS2** - *Transformando noticias en inteligencia con IA* 🚀
|
||||||
128
alias.sh
Executable file
128
alias.sh
Executable file
|
|
@ -0,0 +1,128 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Aliases y funciones para despliegue automático de RSS2
|
||||||
|
# Copia este archivo y ejecútalo: source alias.sh
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# FUNCIONES DE VALIDACIÓN AUTOMÁTICA
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
# Validar credenciales críticas
|
||||||
|
validate_creds() {
|
||||||
|
# Verificar si .env existe
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo "🔑 Generando credenciales seguras..."
|
||||||
|
./generate_secure_credentials.sh --force
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar contraseñas vacías
|
||||||
|
for var in POSTGRES_PASSWORD REDIS_PASSWORD DB_PASS; do
|
||||||
|
val=$(grep "^${var}=" .env 2>/dev/null | cut -d'=' -f2 | tr -d ' ')
|
||||||
|
if [ -z "$val" ] || [ "$val" = "change_*" ] || [ "$val" = "" ]; then
|
||||||
|
echo "🔑 $var está vacía, generando..."
|
||||||
|
./generate_secure_credentials.sh --force
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "✅ Credenciales OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# ALIASES PRINCIPALES
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
# Alias principal: docker compose con validación automática
|
||||||
|
alias dc="docker compose"
|
||||||
|
alias dcu="docker_compose_up" # docker compose up
|
||||||
|
alias dcd="docker_compose_down" # docker compose down
|
||||||
|
|
||||||
|
# Alias con validación automática
|
||||||
|
docker_compose_up() {
|
||||||
|
echo "🔐 Validando credenciales..."
|
||||||
|
validate_creds
|
||||||
|
|
||||||
|
echo "🚀 docker compose up -d"
|
||||||
|
dc up -d
|
||||||
|
}
|
||||||
|
|
||||||
|
docker_compose_down() {
|
||||||
|
echo "📦 docker compose down -v"
|
||||||
|
dc down -v
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alias para despliegue completo con validación
|
||||||
|
deploy() {
|
||||||
|
echo "================================"
|
||||||
|
echo "🚀 DESPLIEGUE AUTOMÁTICO RSS2"
|
||||||
|
echo "================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "1. 🔐 Validando credenciales..."
|
||||||
|
validate_creds
|
||||||
|
|
||||||
|
echo "2. 📦 Deteniendo y eliminando..."
|
||||||
|
dc down -v 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "3. 🚀 Iniciando servicios..."
|
||||||
|
dc up -d --build
|
||||||
|
|
||||||
|
echo "4. ✅ Verificando estado..."
|
||||||
|
dc ps
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "================================"
|
||||||
|
echo "✅ ¡Despliegue completado!"
|
||||||
|
echo "🌐 App: http://localhost:8888"
|
||||||
|
echo "================================"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alias para despliegue rápido sin rebuild
|
||||||
|
deploy-quick() {
|
||||||
|
echo "🚀 Despliegue rápido (sin rebuild)..."
|
||||||
|
dc up -d
|
||||||
|
|
||||||
|
echo "📊 Estado:"
|
||||||
|
dc ps
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alias para ver logs
|
||||||
|
tail-logs() {
|
||||||
|
echo "📊 Logs en tiempo real..."
|
||||||
|
dc logs -f
|
||||||
|
}
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# FUNCIONES DE UTILIDAD
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
# Generar credenciales
|
||||||
|
generate-secrets() {
|
||||||
|
echo "🔑 Generando credenciales seguras..."
|
||||||
|
./generate_secure_credentials.sh --force
|
||||||
|
}
|
||||||
|
|
||||||
|
# Backup
|
||||||
|
backup() {
|
||||||
|
echo "📦 Creando backup..."
|
||||||
|
dc exec db pg_dump -U rss rss > backup_$(date +%Y%m%d).sql
|
||||||
|
echo "✅ Backup guardado en ./backup_$(date +%Y%m%d).sql"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# MENSAJE DE BIENVENIDA
|
||||||
|
# ========================================
|
||||||
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
|
echo "🎉 RSS2 Deployment Aliases cargadas"
|
||||||
|
echo ""
|
||||||
|
echo "Comandos disponibles:"
|
||||||
|
echo " dc docker compose"
|
||||||
|
echo " dcu docker compose up (con validación)"
|
||||||
|
echo " dcd docker compose down"
|
||||||
|
echo " deploy Despliegue completo con validación"
|
||||||
|
echo " deploy-quick Despliegue rápido"
|
||||||
|
echo " tail-logs Ver logs en tiempo real"
|
||||||
|
echo " generate-secrets Generar credenciales"
|
||||||
|
echo " backup Crear backup"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
@ -20,8 +20,6 @@ import (
|
||||||
"github.com/rss2/backend/internal/models"
|
"github.com/rss2/backend/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func CreateAlias(c *gin.Context) {
|
func CreateAlias(c *gin.Context) {
|
||||||
var req models.EntityAliasRequest
|
var req models.EntityAliasRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
|
@ -82,7 +80,7 @@ func CreateAlias(c *gin.Context) {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reassign news mentions safely", "message": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reassign news mentions safely", "message": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete any remaining orphaned mentions of the alias that couldn't be merged (duplicates)
|
// Delete any remaining orphaned mentions of the alias that couldn't be merged (duplicates)
|
||||||
_, err = tx.Exec(ctx, "DELETE FROM tags_noticia WHERE tag_id = $1", aliasTagId)
|
_, err = tx.Exec(ctx, "DELETE FROM tags_noticia WHERE tag_id = $1", aliasTagId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -112,8 +110,6 @@ func CreateAlias(c *gin.Context) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func ExportAliases(c *gin.Context) {
|
func ExportAliases(c *gin.Context) {
|
||||||
rows, err := db.GetPool().Query(c.Request.Context(),
|
rows, err := db.GetPool().Query(c.Request.Context(),
|
||||||
"SELECT alias, canonical_name, tipo FROM entity_aliases ORDER BY tipo, canonical_name")
|
"SELECT alias, canonical_name, tipo FROM entity_aliases ORDER BY tipo, canonical_name")
|
||||||
|
|
@ -533,8 +529,8 @@ func StopWorkers(c *gin.Context) {
|
||||||
// PatchEntityTipo changes the tipo of all tags matching a given valor
|
// PatchEntityTipo changes the tipo of all tags matching a given valor
|
||||||
func PatchEntityTipo(c *gin.Context) {
|
func PatchEntityTipo(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
Valor string `json:"valor" binding:"required"`
|
Valor string `json:"valor" binding:"required"`
|
||||||
NewTipo string `json:"new_tipo" binding:"required"`
|
NewTipo string `json:"new_tipo" binding:"required"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request", "message": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request", "message": err.Error()})
|
||||||
|
|
@ -561,7 +557,7 @@ func PatchEntityTipo(c *gin.Context) {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch existing tags", "message": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch existing tags", "message": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type OldTag struct {
|
type OldTag struct {
|
||||||
ID int
|
ID int
|
||||||
Tipo string
|
Tipo string
|
||||||
|
|
@ -643,32 +639,15 @@ func PatchEntityTipo(c *gin.Context) {
|
||||||
|
|
||||||
// BackupDatabase runs pg_dump and returns the SQL as a downloadable file
|
// BackupDatabase runs pg_dump and returns the SQL as a downloadable file
|
||||||
func BackupDatabase(c *gin.Context) {
|
func BackupDatabase(c *gin.Context) {
|
||||||
dbHost := os.Getenv("DB_HOST")
|
// Ejecutar pg_dump desde dentro del contenedor db para evitar problemas de red
|
||||||
if dbHost == "" {
|
// Usamos docker exec para ejecutar el comando dentro del servicio db
|
||||||
dbHost = "db"
|
cmd := exec.Command("docker", "exec", "rss2_db", "pg_dump",
|
||||||
}
|
"-U", os.Getenv("POSTGRES_USER"),
|
||||||
dbPort := os.Getenv("DB_PORT")
|
"-d", os.Getenv("POSTGRES_DB"),
|
||||||
if dbPort == "" {
|
|
||||||
dbPort = "5432"
|
|
||||||
}
|
|
||||||
dbName := os.Getenv("DB_NAME")
|
|
||||||
if dbName == "" {
|
|
||||||
dbName = "rss"
|
|
||||||
}
|
|
||||||
dbUser := os.Getenv("DB_USER")
|
|
||||||
if dbUser == "" {
|
|
||||||
dbUser = "rss"
|
|
||||||
}
|
|
||||||
dbPass := os.Getenv("DB_PASS")
|
|
||||||
|
|
||||||
cmd := exec.Command("pg_dump",
|
|
||||||
"-h", dbHost,
|
|
||||||
"-p", dbPort,
|
|
||||||
"-U", dbUser,
|
|
||||||
"-d", dbName,
|
|
||||||
"--no-password",
|
"--no-password",
|
||||||
|
"--format=plain",
|
||||||
|
"--pghost=5432",
|
||||||
)
|
)
|
||||||
cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", dbPass))
|
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
|
|
@ -769,4 +748,3 @@ func BackupNewsZipped(c *gin.Context) {
|
||||||
c.Header("Cache-Control", "no-cache")
|
c.Header("Cache-Control", "no-cache")
|
||||||
c.Data(http.StatusOK, "application/zip", buf.Bytes())
|
c.Data(http.StatusOK, "application/zip", buf.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,16 @@
|
||||||
|
# ==================================================================================
|
||||||
|
# CONFIGURACIÓN DE SECRETOS - Se cargan desde .env
|
||||||
|
# ==================================================================================
|
||||||
|
# Las variables POSTGRES_PASSWORD, REDIS_PASSWORD, DB_PASS, SECRET_KEY, GRAFANA_PASSWORD
|
||||||
|
# deben estar definidas en un archivo .env antes de ejecutar docker compose up
|
||||||
|
#
|
||||||
|
# Para generar credenciales seguras automáticamente:
|
||||||
|
# ./generate_secure_credentials.sh --force
|
||||||
|
#
|
||||||
|
# Despliegue simple:
|
||||||
|
# docker compose up -d
|
||||||
|
# ==================================================================================
|
||||||
|
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: postgres:18-alpine
|
image: postgres:18-alpine
|
||||||
|
|
@ -25,6 +38,7 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $$POSTGRES_USER -d $$POSTGRES_DB || exit 1" ]
|
test: [ "CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $$POSTGRES_USER -d $$POSTGRES_DB || exit 1" ]
|
||||||
|
# NOTA: Si falla, verifica que POSTGRES_PASSWORD esté definido en .env
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 30
|
retries: 30
|
||||||
|
|
@ -43,7 +57,7 @@ services:
|
||||||
TZ: Europe/Madrid
|
TZ: Europe/Madrid
|
||||||
# SEGURIDAD: Redis con autenticación
|
# SEGURIDAD: Redis con autenticación
|
||||||
command: >
|
command: >
|
||||||
redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru --requirepass ${REDIS_PASSWORD}
|
redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru --requirepass ${REDIS_PASSWORD:-!ERROR!REDIS_PASSWORD_NEEDED!}
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/redis-data:/data
|
- ./data/redis-data:/data
|
||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
|
@ -55,6 +69,7 @@ services:
|
||||||
- rss2_redis
|
- rss2_redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
# VALIDACIÓN: Si REDIS_PASSWORD está vacío, esto fallará y detendrá el despliegue
|
||||||
test: [ "CMD", "redis-cli", "--no-auth-warning", "-a", "${REDIS_PASSWORD}", "ping" ]
|
test: [ "CMD", "redis-cli", "--no-auth-warning", "-a", "${REDIS_PASSWORD}", "ping" ]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
|
|
@ -282,8 +297,6 @@ services:
|
||||||
dockerfile: Dockerfile.translator
|
dockerfile: Dockerfile.translator
|
||||||
image: rss2-translator:latest
|
image: rss2-translator:latest
|
||||||
command: bash -lc "python -m workers.ctranslator_worker"
|
command: bash -lc "python -m workers.ctranslator_worker"
|
||||||
security_opt:
|
|
||||||
- seccomp=unconfined
|
|
||||||
environment:
|
environment:
|
||||||
DB_HOST: db
|
DB_HOST: db
|
||||||
DB_PORT: 5432
|
DB_PORT: 5432
|
||||||
|
|
@ -299,14 +312,60 @@ services:
|
||||||
HF_HOME: /app/hf_cache
|
HF_HOME: /app/hf_cache
|
||||||
TZ: Europe/Madrid
|
TZ: Europe/Madrid
|
||||||
TRANSLATOR_ID: ${TRANSLATOR_ID:-}
|
TRANSLATOR_ID: ${TRANSLATOR_ID:-}
|
||||||
|
PYTORCH_ENABLE_MPS_FALLBACK: 1
|
||||||
volumes:
|
volumes:
|
||||||
- ./workers:/app/workers
|
- ./workers:/app/workers
|
||||||
- ./hf_cache:/app/hf_cache
|
- ./hf_cache:/app/hf_cache
|
||||||
- ./models:/app/models
|
- ./models:/app/models
|
||||||
networks:
|
networks:
|
||||||
- backend
|
- backend
|
||||||
profiles:
|
depends_on:
|
||||||
- cpu-only
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# TRANSLATOR GPU - Multiple instances with --scale for parallel GPU processing
|
||||||
|
# Configure multiple GPU workers: docker compose up -d --scale translator-gpu=2
|
||||||
|
# ==================================================================================
|
||||||
|
translator-gpu:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.translator-gpu
|
||||||
|
image: rss2-translator-gpu:latest
|
||||||
|
command: bash -lc "python -m workers.ctranslator_worker"
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: ${DB_NAME:-rss}
|
||||||
|
DB_USER: ${DB_USER:-rss}
|
||||||
|
DB_PASS: ${DB_PASS}
|
||||||
|
TARGET_LANGS: es
|
||||||
|
TRANSLATOR_BATCH: 256
|
||||||
|
CT2_MODEL_PATH: /app/models/nllb-ct2-1.3b
|
||||||
|
CT2_DEVICE: cuda
|
||||||
|
CT2_COMPUTE_TYPE: float16
|
||||||
|
UNIVERSAL_MODEL: facebook/nllb-200-1.3B
|
||||||
|
HF_HOME: /app/hf_cache
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
TRANSLATOR_ID: ${TRANSLATOR_ID:-}
|
||||||
|
PYTORCH_CUDA_ALLOC_CONF: max_split_size_mb:512
|
||||||
|
NCCL_DEBUG: INFO
|
||||||
|
volumes:
|
||||||
|
- ./workers:/app/workers
|
||||||
|
- ./hf_cache:/app/hf_cache
|
||||||
|
- ./models:/app/models
|
||||||
|
- /dev/nvidia:/dev/nvidia
|
||||||
|
- /usr/local/nvidia:/usr/local/nvidia:ro
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: 1
|
||||||
|
capabilities: [gpu]
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
@ -335,8 +394,6 @@ services:
|
||||||
- ./models:/app/models
|
- ./models:/app/models
|
||||||
networks:
|
networks:
|
||||||
- backend
|
- backend
|
||||||
profiles:
|
|
||||||
- remote
|
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
|
|
@ -381,52 +438,6 @@ services:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# ==================================================================================
|
|
||||||
# TRANSLATOR GPU (CTranslate2 with CUDA)
|
|
||||||
# ==================================================================================
|
|
||||||
translator-gpu:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile.translator-gpu
|
|
||||||
image: rss2-translator-gpu:latest
|
|
||||||
container_name: rss2_translator_gpu
|
|
||||||
command: bash -lc "python -m workers.ctranslator_worker"
|
|
||||||
security_opt:
|
|
||||||
- seccomp=unconfined
|
|
||||||
environment:
|
|
||||||
DB_HOST: db
|
|
||||||
DB_PORT: 5432
|
|
||||||
DB_NAME: ${DB_NAME:-rss}
|
|
||||||
DB_USER: ${DB_USER:-rss}
|
|
||||||
DB_PASS: ${DB_PASS}
|
|
||||||
TARGET_LANGS: es
|
|
||||||
TRANSLATOR_BATCH: 64
|
|
||||||
CT2_MODEL_PATH: /app/models/nllb-ct2
|
|
||||||
CT2_DEVICE: cuda
|
|
||||||
CT2_COMPUTE_TYPE: float16
|
|
||||||
UNIVERSAL_MODEL: facebook/nllb-200-distilled-600M
|
|
||||||
HF_HOME: /app/hf_cache
|
|
||||||
TZ: Europe/Madrid
|
|
||||||
volumes:
|
|
||||||
- ./workers:/app/workers
|
|
||||||
- ./hf_cache:/app/hf_cache
|
|
||||||
- ./models:/app/models
|
|
||||||
networks:
|
|
||||||
- backend
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 4G
|
|
||||||
reservations:
|
|
||||||
devices:
|
|
||||||
- driver: nvidia
|
|
||||||
count: 1
|
|
||||||
capabilities: [ gpu ]
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
embeddings:
|
embeddings:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
|
|
|
||||||
802
docker-compose.yml.backup
Normal file
802
docker-compose.yml.backup
Normal file
|
|
@ -0,0 +1,802 @@
|
||||||
|
# ==================================================================================
|
||||||
|
# CONFIGURACIÓN DE SECRETOS - Se cargan desde .env
|
||||||
|
# ==================================================================================
|
||||||
|
# Las variables POSTGRES_PASSWORD, REDIS_PASSWORD, DB_PASS, SECRET_KEY, GRAFANA_PASSWORD
|
||||||
|
# deben estar definidas en un archivo .env antes de ejecutar docker compose up
|
||||||
|
#
|
||||||
|
# Para generar credenciales seguras automáticamente:
|
||||||
|
# ./generate_secure_credentials.sh --force
|
||||||
|
#
|
||||||
|
# Despliegue simple:
|
||||||
|
# docker compose up -d
|
||||||
|
# ==================================================================================
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:18-alpine
|
||||||
|
container_name: rss2_db
|
||||||
|
shm_size: 4gb
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-rss}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-rss}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C.UTF-8"
|
||||||
|
LANG: C.UTF-8
|
||||||
|
LC_ALL: C.UTF-8
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
PGDATA: /var/lib/postgresql/data/18/main
|
||||||
|
volumes:
|
||||||
|
- ./data/pgdata:/var/lib/postgresql/data
|
||||||
|
- ./init-db:/docker-entrypoint-initdb.d:rw
|
||||||
|
- ./docker-entrypoint-db.sh:/docker-entrypoint-db.sh:ro
|
||||||
|
entrypoint: ["bash", "/docker-entrypoint-db.sh"]
|
||||||
|
networks:
|
||||||
|
backend:
|
||||||
|
aliases:
|
||||||
|
- db
|
||||||
|
- rss2_db
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $$POSTGRES_USER -d $$POSTGRES_DB || exit 1" ]
|
||||||
|
# NOTA: Si falla, verifica que POSTGRES_PASSWORD esté definido en .env
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 30
|
||||||
|
start_period: 20s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 8G
|
||||||
|
reservations:
|
||||||
|
memory: 4G
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: rss2_redis
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
# SEGURIDAD: Redis con autenticación
|
||||||
|
command: >
|
||||||
|
redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru --requirepass ${REDIS_PASSWORD:-!ERROR!REDIS_PASSWORD_NEEDED!}
|
||||||
|
volumes:
|
||||||
|
- ./data/redis-data:/data
|
||||||
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
networks:
|
||||||
|
backend:
|
||||||
|
aliases:
|
||||||
|
- redis
|
||||||
|
- rss2_redis
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
# VALIDACIÓN: Si REDIS_PASSWORD está vacío, esto fallará y detendrá el despliegue
|
||||||
|
test: [ "CMD", "redis-cli", "--no-auth-warning", "-a", "${REDIS_PASSWORD}", "ping" ]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 768M
|
||||||
|
reservations:
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
|
rss-ingestor-go:
|
||||||
|
build:
|
||||||
|
context: ./rss-ingestor-go
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: rss2_ingestor_go
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: ${DB_NAME:-rss}
|
||||||
|
DB_USER: ${DB_USER:-rss}
|
||||||
|
DB_PASS: ${DB_PASS}
|
||||||
|
RSS_MAX_WORKERS: 100
|
||||||
|
RSS_POKE_INTERVAL_MIN: 60
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '2'
|
||||||
|
memory: 2G
|
||||||
|
reservations:
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
|
langdetect:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: rss2_langdetect_py
|
||||||
|
command: bash -lc "python -m workers.langdetect_worker"
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: ${DB_NAME:-rss}
|
||||||
|
DB_USER: ${DB_USER:-rss}
|
||||||
|
DB_PASS: ${DB_PASS}
|
||||||
|
LANG_DETECT_SLEEP: 60
|
||||||
|
LANG_DETECT_BATCH: 1000
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
volumes:
|
||||||
|
- ./workers:/app/workers
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# SCRAPER WORKER (Go) - Extrae artículos de URLs
|
||||||
|
# ==================================================================================
|
||||||
|
scraper:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.scraper
|
||||||
|
container_name: rss2_scraper
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: ${DB_NAME:-rss}
|
||||||
|
DB_USER: ${DB_USER:-rss}
|
||||||
|
DB_PASS: ${DB_PASS}
|
||||||
|
SCRAPER_SLEEP: 60
|
||||||
|
SCRAPER_BATCH: 10
|
||||||
|
SCRAPER_ENRICH_LIMIT: 20
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# DISCOVERY WORKER (Go) - Descubre RSS feeds
|
||||||
|
# ==================================================================================
|
||||||
|
discovery:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.discovery
|
||||||
|
container_name: rss2_discovery
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: ${DB_NAME:-rss}
|
||||||
|
DB_USER: ${DB_USER:-rss}
|
||||||
|
DB_PASS: ${DB_PASS}
|
||||||
|
DISCOVERY_INTERVAL: 900
|
||||||
|
DISCOVERY_BATCH: 10
|
||||||
|
MAX_FEEDS_PER_URL: 5
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# WIKI WORKER (Go) - Wikipedia info and thumbnails
|
||||||
|
# ==================================================================================
|
||||||
|
wiki-worker:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.wiki
|
||||||
|
container_name: rss2_wiki_worker
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: ${DB_NAME:-rss}
|
||||||
|
DB_USER: ${DB_USER:-rss}
|
||||||
|
DB_PASS: ${DB_PASS}
|
||||||
|
WIKI_SLEEP: 10
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
volumes:
|
||||||
|
- ./data/wiki_images:/app/data/wiki_images
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# BACKEND GO (API REST)
|
||||||
|
# ==================================================================================
|
||||||
|
backend-go:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: rss2_backend_go
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
DATABASE_URL: postgres://${POSTGRES_USER:-rss}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-rss}?sslmode=disable
|
||||||
|
REDIS_URL: redis://:${REDIS_PASSWORD:-rss_redis_pass_2024}@redis:6379
|
||||||
|
SECRET_KEY: ${SECRET_KEY:-change_this_to_a_long_random_string}
|
||||||
|
SERVER_PORT: "8080"
|
||||||
|
volumes:
|
||||||
|
- ./data/wiki_images:/app/data/wiki_images
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
- frontend
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# FRONTEND REACT
|
||||||
|
# ==================================================================================
|
||||||
|
rss2_frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: rss2_frontend
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
VITE_API_URL: /api
|
||||||
|
networks:
|
||||||
|
- frontend
|
||||||
|
depends_on:
|
||||||
|
- backend-go
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# NGINX (Puerto 8001 - sirve React + proxy API)
|
||||||
|
# ==================================================================================
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: rss2_nginx
|
||||||
|
ports:
|
||||||
|
- "8888:80"
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
networks:
|
||||||
|
- frontend
|
||||||
|
depends_on:
|
||||||
|
- rss2_frontend
|
||||||
|
- backend-go
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# TRANSLATOR CPU (CTranslate2) - Scale with: docker compose up -d --scale translator=3
|
||||||
|
# ==================================================================================
|
||||||
|
translator:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.translator
|
||||||
|
image: rss2-translator:latest
|
||||||
|
command: bash -lc "python -m workers.ctranslator_worker"
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: ${DB_NAME:-rss}
|
||||||
|
DB_USER: ${DB_USER:-rss}
|
||||||
|
DB_PASS: ${DB_PASS}
|
||||||
|
TARGET_LANGS: es
|
||||||
|
TRANSLATOR_BATCH: 32
|
||||||
|
CT2_MODEL_PATH: /app/models/nllb-ct2
|
||||||
|
CT2_DEVICE: cpu
|
||||||
|
CT2_COMPUTE_TYPE: int8
|
||||||
|
UNIVERSAL_MODEL: facebook/nllb-200-distilled-600M
|
||||||
|
HF_HOME: /app/hf_cache
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
TRANSLATOR_ID: ${TRANSLATOR_ID:-}
|
||||||
|
PYTORCH_ENABLE_MPS_FALLBACK: 1
|
||||||
|
volumes:
|
||||||
|
- ./workers:/app/workers
|
||||||
|
- ./hf_cache:/app/hf_cache
|
||||||
|
- ./models:/app/models
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
profiles:
|
||||||
|
- cpu-only
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# TRANSLATOR GPU - Multiple instances with --scale for parallel GPU processing
|
||||||
|
# Configure multiple GPU workers: docker compose up -d --scale translator-gpu=2
|
||||||
|
# ==================================================================================
|
||||||
|
translator-gpu:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.translator-gpu
|
||||||
|
image: rss2-translator-gpu:latest
|
||||||
|
command: bash -lc "python -m workers.ctranslator_worker"
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: ${DB_NAME:-rss}
|
||||||
|
DB_USER: ${DB_USER:-rss}
|
||||||
|
DB_PASS: ${DB_PASS}
|
||||||
|
TARGET_LANGS: es
|
||||||
|
TRANSLATOR_BATCH: 128
|
||||||
|
CT2_MODEL_PATH: /app/models/nllb-ct2
|
||||||
|
CT2_DEVICE: cuda
|
||||||
|
CT2_COMPUTE_TYPE: float16
|
||||||
|
UNIVERSAL_MODEL: facebook/nllb-200-distilled-600M
|
||||||
|
HF_HOME: /app/hf_cache
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
TRANSLATOR_ID: ${TRANSLATOR_ID:-}
|
||||||
|
PYTORCH_CUDA_ALLOC_CONF: max_split_size_mb:512
|
||||||
|
NCCL_DEBUG: INFO
|
||||||
|
volumes:
|
||||||
|
- ./workers:/app/workers
|
||||||
|
- ./hf_cache:/app/hf_cache
|
||||||
|
- ./models:/app/models
|
||||||
|
- /dev/nvidia:/dev/nvidia
|
||||||
|
- /usr/local/nvidia:/usr/local/nvidia:ro
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
profiles:
|
||||||
|
- gpu
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: 1
|
||||||
|
capabilities: [gpu]
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# REMOTE TRANSLATOR WORKER - Worker remoto que se conecta por WebSocket
|
||||||
|
# ==================================================================================
|
||||||
|
remote-translator:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.remote-worker
|
||||||
|
container_name: rss2_remote_translator
|
||||||
|
environment:
|
||||||
|
WORKER_NAME: ${WORKER_NAME:-remote-worker-1}
|
||||||
|
WORKER_API_KEY: ${WORKER_API_KEY:-}
|
||||||
|
WORKER_SERVER: ${WORKER_SERVER:-ws://backend-go:8080/ws/worker}
|
||||||
|
CT2_DEVICE: ${CT2_DEVICE:-cuda}
|
||||||
|
CT2_MODEL_PATH: /app/models/nllb-ct2
|
||||||
|
CT2_COMPUTE_TYPE: ${CT2_COMPUTE_TYPE:-float16}
|
||||||
|
UNIVERSAL_MODEL: facebook/nllb-200-1.3B
|
||||||
|
HF_HOME: /app/hf_cache
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
volumes:
|
||||||
|
- ./hf_cache:/app/hf_cache
|
||||||
|
- ./models:/app/models
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
profiles:
|
||||||
|
- remote
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 6G
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: 1
|
||||||
|
capabilities: [ gpu ]
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# TRANSLATION SCHEDULER - Creates translation jobs
|
||||||
|
# ==================================================================================
|
||||||
|
translation-scheduler:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.scheduler
|
||||||
|
image: rss2-scheduler:latest
|
||||||
|
container_name: rss2_translation_scheduler
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: ${DB_NAME:-rss}
|
||||||
|
DB_USER: ${DB_USER:-rss}
|
||||||
|
DB_PASS: ${DB_PASS}
|
||||||
|
TARGET_LANGS: es
|
||||||
|
SCHEDULER_BATCH: 1000
|
||||||
|
SCHEDULER_SLEEP: 30
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
volumes:
|
||||||
|
- ./workers:/app/workers
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 256M
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
embeddings:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: rss2_embeddings_py
|
||||||
|
command: bash -lc "python -m workers.embeddings_worker"
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: ${DB_NAME:-rss}
|
||||||
|
DB_USER: ${DB_USER:-rss}
|
||||||
|
DB_PASS: ${DB_PASS}
|
||||||
|
EMB_MODEL: sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
|
||||||
|
EMB_BATCH: 64
|
||||||
|
EMB_SLEEP_IDLE: 5
|
||||||
|
EMB_LANGS: es
|
||||||
|
EMB_LIMIT: 1000
|
||||||
|
DEVICE: cuda
|
||||||
|
HF_HOME: /app/hf_cache
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
volumes:
|
||||||
|
- ./workers:/app/workers
|
||||||
|
- ./hf_cache:/app/hf_cache
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 6G
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: 1
|
||||||
|
capabilities: [ gpu ]
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# TOPICS WORKER (Go) - Matching temas y países
|
||||||
|
# ==================================================================================
|
||||||
|
topics:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.topics
|
||||||
|
container_name: rss2_topics
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: ${DB_NAME:-rss}
|
||||||
|
DB_USER: ${DB_USER:-rss}
|
||||||
|
DB_PASS: ${DB_PASS}
|
||||||
|
TOPICS_SLEEP: 10
|
||||||
|
TOPICS_BATCH: 500
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# RELATED WORKER (Go) - Noticias relacionadas
|
||||||
|
# ==================================================================================
|
||||||
|
related:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.related
|
||||||
|
container_name: rss2_related
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: ${DB_NAME:-rss}
|
||||||
|
DB_USER: ${DB_USER:-rss}
|
||||||
|
DB_PASS: ${DB_PASS}
|
||||||
|
RELATED_SLEEP: 10
|
||||||
|
RELATED_BATCH: 200
|
||||||
|
RELATED_TOPK: 10
|
||||||
|
EMB_MODEL: mxbai-embed-large
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 1G
|
||||||
|
|
||||||
|
qdrant:
|
||||||
|
image: qdrant/qdrant:latest
|
||||||
|
container_name: rss2_qdrant
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
QDRANT__SERVICE__GRPC_PORT: 6334
|
||||||
|
volumes:
|
||||||
|
- ./data/qdrant_storage:/qdrant/storage
|
||||||
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '4'
|
||||||
|
memory: 4G
|
||||||
|
reservations:
|
||||||
|
memory: 2G
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# QDRANT WORKER (Go) - Vectorización y búsqueda semántica
|
||||||
|
# ==================================================================================
|
||||||
|
qdrant-worker:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.qdrant
|
||||||
|
container_name: rss2_qdrant_worker
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: ${DB_NAME:-rss}
|
||||||
|
DB_USER: ${DB_USER:-rss}
|
||||||
|
DB_PASS: ${DB_PASS}
|
||||||
|
QDRANT_HOST: qdrant
|
||||||
|
QDRANT_PORT: 6333
|
||||||
|
QDRANT_COLLECTION: news_vectors
|
||||||
|
OLLAMA_URL: http://ollama:11434
|
||||||
|
QDRANT_SLEEP: 30
|
||||||
|
QDRANT_BATCH: 100
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
qdrant:
|
||||||
|
condition: service_started
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 1G
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# NER WORKER (Python) - Extracción de entidades
|
||||||
|
# ==================================================================================
|
||||||
|
ner:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: rss2_ner
|
||||||
|
command: bash -lc "python -m workers.ner_worker"
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: ${DB_NAME:-rss}
|
||||||
|
DB_USER: ${DB_USER:-rss}
|
||||||
|
DB_PASS: ${DB_PASS}
|
||||||
|
NER_LANG: es
|
||||||
|
NER_BATCH: 64
|
||||||
|
HF_HOME: /app/hf_cache
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
volumes:
|
||||||
|
- ./workers:/app/workers
|
||||||
|
- ./hf_cache:/app/hf_cache
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '2'
|
||||||
|
memory: 2G
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# CLUSTER WORKER (Python) - Agrupación de noticias
|
||||||
|
# ==================================================================================
|
||||||
|
cluster:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: rss2_cluster_py
|
||||||
|
command: bash -lc "python -m workers.cluster_worker"
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: ${DB_NAME:-rss}
|
||||||
|
DB_USER: ${DB_USER:-rss}
|
||||||
|
DB_PASS: ${DB_PASS}
|
||||||
|
EVENT_DIST_THRESHOLD: 0.35
|
||||||
|
EMB_MODEL: sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
volumes:
|
||||||
|
- ./workers:/app/workers
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '2'
|
||||||
|
memory: 2G
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# LLM CATEGORIZER (Python) - Categorización con Ollama
|
||||||
|
# ==================================================================================
|
||||||
|
llm-categorizer:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: rss2_llm_categorizer
|
||||||
|
command: bash -lc "python -m workers.simple_categorizer_worker"
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: ${DB_NAME:-rss}
|
||||||
|
DB_USER: ${DB_USER:-rss}
|
||||||
|
DB_PASS: ${DB_PASS}
|
||||||
|
CATEGORIZER_BATCH_SIZE: 10
|
||||||
|
CATEGORIZER_SLEEP_IDLE: 5
|
||||||
|
TZ: Europe/Madrid
|
||||||
|
volumes:
|
||||||
|
- ./workers:/app/workers
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '2'
|
||||||
|
memory: 1G
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# MONITORING STACK - SECURED
|
||||||
|
# ==================================================================================
|
||||||
|
|
||||||
|
prometheus:
|
||||||
|
image: prom/prometheus:latest
|
||||||
|
container_name: rss2_prometheus
|
||||||
|
volumes:
|
||||||
|
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||||
|
- prometheus_data:/prometheus
|
||||||
|
command:
|
||||||
|
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||||
|
- '--storage.tsdb.path=/prometheus'
|
||||||
|
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
|
||||||
|
- '--web.console.templates=/usr/share/prometheus/consoles'
|
||||||
|
# SEGURIDAD: Sin exposición de puertos - acceso solo vía Grafana o túnel SSH
|
||||||
|
# ports:
|
||||||
|
# - "9090:9090"
|
||||||
|
networks:
|
||||||
|
- monitoring
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 2G
|
||||||
|
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana:latest
|
||||||
|
container_name: rss2_grafana
|
||||||
|
# SEGURIDAD: Acceso solo en localhost o vía túnel SSH
|
||||||
|
# Para acceso remoto, usar túnel SSH: ssh -L 3001:localhost:3001 user@server
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3001:3000"
|
||||||
|
environment:
|
||||||
|
# SEGURIDAD: Cambiar este password en producción
|
||||||
|
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-change_this_password}
|
||||||
|
- GF_USERS_ALLOW_SIGN_UP=false
|
||||||
|
- GF_SERVER_ROOT_URL=http://localhost:3001
|
||||||
|
- GF_SECURITY_COOKIE_SECURE=false
|
||||||
|
- GF_SECURITY_COOKIE_SAMESITE=lax
|
||||||
|
volumes:
|
||||||
|
- grafana_data:/var/lib/grafana
|
||||||
|
networks:
|
||||||
|
- monitoring
|
||||||
|
depends_on:
|
||||||
|
- prometheus
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 1G
|
||||||
|
|
||||||
|
cadvisor:
|
||||||
|
image: gcr.io/cadvisor/cadvisor:latest
|
||||||
|
container_name: rss2_cadvisor
|
||||||
|
# SEGURIDAD: Sin exposición de puertos - solo acceso interno
|
||||||
|
# ports:
|
||||||
|
# - "8081:8080"
|
||||||
|
volumes:
|
||||||
|
- /:/rootfs:ro
|
||||||
|
- /var/run:/var/run:ro
|
||||||
|
- /sys:/sys:ro
|
||||||
|
- /var/lib/docker/:/var/lib/docker:ro
|
||||||
|
- /dev/disk/:/dev/disk:ro
|
||||||
|
devices:
|
||||||
|
- /dev/kmsg
|
||||||
|
networks:
|
||||||
|
- monitoring
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# REDES SEGMENTADAS
|
||||||
|
# ==================================================================================
|
||||||
|
networks:
|
||||||
|
# Red frontal - Solo nginx y web app
|
||||||
|
frontend:
|
||||||
|
name: rss2_frontend
|
||||||
|
driver: bridge
|
||||||
|
internal: false
|
||||||
|
|
||||||
|
# Red backend - Base de datos, workers, redis, qdrant
|
||||||
|
backend:
|
||||||
|
name: rss2_backend
|
||||||
|
driver: bridge
|
||||||
|
internal: false # Acceso externo permitido (necesario para ingestor)
|
||||||
|
|
||||||
|
# Red de monitoreo - Prometheus, Grafana, cAdvisor
|
||||||
|
monitoring:
|
||||||
|
name: rss2_monitoring
|
||||||
|
driver: bridge
|
||||||
|
internal: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
prometheus_data:
|
||||||
|
grafana_data:
|
||||||
|
torch_extensions:
|
||||||
|
|
@ -18,6 +18,33 @@
|
||||||
|
|
||||||
set -e # Exit on error
|
set -e # Exit on error
|
||||||
|
|
||||||
|
# Argumentos para modo automático
|
||||||
|
FORCE="false"
|
||||||
|
|
||||||
|
# Verificar argumentos
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--force)
|
||||||
|
FORCE="true"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
echo "Generador de Credenciales Seguras"
|
||||||
|
echo ""
|
||||||
|
echo "Uso: ./generate_secure_credentials.sh [--force]"
|
||||||
|
echo ""
|
||||||
|
echo "Opciones:"
|
||||||
|
echo " --force Forzar reemplazo de .env sin preguntar"
|
||||||
|
echo " --help Mostrar esta ayuda"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Opción desconocida: $1"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
# Colores para output
|
# Colores para output
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
|
|
@ -157,17 +184,22 @@ EOF
|
||||||
|
|
||||||
echo -e "${GREEN}✅ Archivo generado: $ENV_FILE${NC}\n"
|
echo -e "${GREEN}✅ Archivo generado: $ENV_FILE${NC}\n"
|
||||||
|
|
||||||
# Preguntar si quiere reemplazar .env
|
# Preguntar si quiere reemplazar .env (si no se usa --force)
|
||||||
echo -e "${YELLOW}¿Deseas reemplazar el archivo .env actual con el generado?${NC}"
|
if [ "$FORCE" = "true" ]; then
|
||||||
echo -e "${YELLOW}(Recomendado: revisa $ENV_FILE primero)${NC}"
|
|
||||||
read -p "¿Continuar? (s/N): " -n 1 -r
|
|
||||||
echo
|
|
||||||
if [[ $REPLY =~ ^[SsYy]$ ]]; then
|
|
||||||
mv "$ENV_FILE" .env
|
mv "$ENV_FILE" .env
|
||||||
echo -e "${GREEN}✅ Archivo .env actualizado${NC}"
|
echo -e "${GREEN}✅ Archivo .env actualizado automáticamente${NC}"
|
||||||
else
|
else
|
||||||
echo -e "${YELLOW}⚠️ Archivo guardado como: $ENV_FILE${NC}"
|
echo -e "${YELLOW}¿Deseas reemplazar el archivo .env actual con el generado?${NC}"
|
||||||
echo -e "${YELLOW} Para usarlo: mv $ENV_FILE .env${NC}"
|
echo -e "${YELLOW}(Recomendado: revisa $ENV_FILE primero)${NC}"
|
||||||
|
read -p "¿Continuar? (s/N): " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[SsYy]$ ]]; then
|
||||||
|
mv "$ENV_FILE" .env
|
||||||
|
echo -e "${GREEN}✅ Archivo .env actualizado${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠️ Archivo guardado como: $ENV_FILE${NC}"
|
||||||
|
echo -e "${YELLOW} Para usarlo: mv $ENV_FILE .env${NC}"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
|
||||||
411
pre-deploy.sh
Executable file
411
pre-deploy.sh
Executable file
|
|
@ -0,0 +1,411 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# Pre-Deploy Script - Validación de Credenciales Seguras
|
||||||
|
# ==================================================================================
|
||||||
|
#
|
||||||
|
# Este script se ejecuta antes de docker compose up para:
|
||||||
|
# 1. Verificar que existen credenciales en .env
|
||||||
|
# 2. Generar credenciales seguras si faltan
|
||||||
|
# 3. Validar que las variables críticas estén definidas
|
||||||
|
# 4. Mostrar resumen de credenciales en consola
|
||||||
|
#
|
||||||
|
# Uso:
|
||||||
|
# ./pre-deploy.sh
|
||||||
|
#
|
||||||
|
# Salida:
|
||||||
|
# - Éxito: Muestra resumen de credenciales y permite continuar
|
||||||
|
# - Error: Sale con código 1 si faltan credenciales críticas
|
||||||
|
#
|
||||||
|
# ==================================================================================
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
# Colores para output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${BLUE}╔═══════════════════════════════════════════════════════════╗"
|
||||||
|
echo -e "║ 🔐 Pre-Deploy Security Check - RSS2 Platform ║"
|
||||||
|
echo -e "╚═══════════════════════════════════════════════════════════╝${NC}\n"
|
||||||
|
|
||||||
|
# Función para mostrar ayuda
|
||||||
|
show_help() {
|
||||||
|
cat << EOF
|
||||||
|
${BLUE}Uso:${NC}
|
||||||
|
./pre-deploy.sh [--generate|--skip]
|
||||||
|
|
||||||
|
${BLUE}Opciones:${NC}
|
||||||
|
--generate Generar credenciales seguras automáticamente
|
||||||
|
--skip Saltar validación y continuar (NO RECOMENDADO)
|
||||||
|
|
||||||
|
${BLUE}Ejemplos:${NC}
|
||||||
|
./pre-deploy.sh # Validar credenciales existentes
|
||||||
|
./pre-deploy.sh --generate # Generar nuevas credenciales
|
||||||
|
./pre-deploy.sh --skip # Saltar validación
|
||||||
|
|
||||||
|
${YELLOW}Nota:${NC} Las credenciales se guardan en .env para docker compose
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verificar argumentos
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
case "$1" in
|
||||||
|
--generate)
|
||||||
|
echo -e "${GREEN}🔄 Generando credenciales seguras...${NC}\n"
|
||||||
|
bash ./generate_secure_credentials.sh --force
|
||||||
|
exit $?
|
||||||
|
;;
|
||||||
|
--skip)
|
||||||
|
echo -e "${YELLOW}⚠️ Saltando validación de credenciales${NC}"
|
||||||
|
echo -e "${YELLOW}⚠️ Esto puede causar advertencias de WARN durante el despliegue${NC}"
|
||||||
|
echo ""
|
||||||
|
# Leer credenciales de ejemplo si no existen
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
if [ -f .env.example ]; then
|
||||||
|
cp .env.example .env
|
||||||
|
echo -e "${GREEN}✅ .env creado desde .env.example${NC}"
|
||||||
|
elif [ -f .env.secure.example ]; then
|
||||||
|
cp .env.secure.example .env
|
||||||
|
echo -e "${GREEN}✅ .env creado desde .env.secure.example${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Error: No hay .env.example para copiar${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Función para generar credenciales seguras
|
||||||
|
generate_credentials() {
|
||||||
|
if [ -f ./generate_secure_credentials.sh ]; then
|
||||||
|
echo -e "${GREEN}🔄 Generando credenciales seguras...${NC}\n"
|
||||||
|
bash ./generate_secure_credentials.sh --force
|
||||||
|
return $?
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Error: generate_secure_credentials.sh no encontrado${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verificar si .env existe y tiene contenido
|
||||||
|
check_env_file() {
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo -e "${RED}❌ Error: Archivo .env no encontrado${NC}"
|
||||||
|
echo -e "${YELLOW}💡 Generando credenciales seguras automáticamente...${NC}"
|
||||||
|
generate_credentials
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -s .env ]; then
|
||||||
|
echo -e "${RED}❌ Error: Archivo .env está vacío${NC}"
|
||||||
|
echo -e "${YELLOW}💡 Generando credenciales seguras automáticamente...${NC}"
|
||||||
|
generate_credentials
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ Archivo .env encontrado${NC}"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Función para leer variables del archivo .env
|
||||||
|
get_env_var() {
|
||||||
|
local var_name="$1"
|
||||||
|
grep "^${var_name}=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '[:space:]'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Función para validar variables críticas
|
||||||
|
validate_credentials() {
|
||||||
|
local missing=0
|
||||||
|
local critical_missing=0
|
||||||
|
local critical_vars=(POSTGRES_PASSWORD REDIS_PASSWORD DB_PASS)
|
||||||
|
|
||||||
|
echo -e "\n${BLUE}🔍 Validando credenciales críticas...${NC}\n"
|
||||||
|
|
||||||
|
for var in "${critical_vars[@]}"; do
|
||||||
|
local value
|
||||||
|
value=$(get_env_var "$var")
|
||||||
|
|
||||||
|
if [ -z "$value" ]; then
|
||||||
|
echo -e "${RED}❌ FALTA: $var está vacía${NC}"
|
||||||
|
((missing++))
|
||||||
|
((critical_missing++))
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✅ DEFINIDO: $var=${value:0:16}${NC}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $critical_missing -gt 0 ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Función para mostrar resumen de credenciales
|
||||||
|
show_credentials_summary() {
|
||||||
|
echo -e "\n${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
echo -e "${BLUE}📋 Resumen de Credenciales Activas${NC}"
|
||||||
|
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
|
||||||
|
|
||||||
|
if [ -f .env ]; then
|
||||||
|
echo "${BLUE}📁 Archivo .env:${NC}"
|
||||||
|
echo -e " ${GREEN}📍 Ruta:${NC} $(pwd)/.env"
|
||||||
|
echo -e " ${GREEN}📏 Tamaño:${NC} $(wc -c < .env) bytes"
|
||||||
|
echo -e " ${GREEN}📊 Líneas:${NC} $(wc -l < .env)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "${BLUE}🔑 Variables Críticas:${NC}"
|
||||||
|
echo -e " ${GREEN}✅ POSTGRES_PASSWORD:${NC} ✓"
|
||||||
|
echo -e " ${GREEN}✅ REDIS_PASSWORD:${NC} ✓"
|
||||||
|
echo -e " ${GREEN}✅ DB_PASS:${NC} ✓"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "${BLUE}📝 Otras Variables Importantes:${NC}"
|
||||||
|
for var in POSTGRES_USER DB_NAME SECRET_KEY GRAFANA_PASSWORD; do
|
||||||
|
local value
|
||||||
|
value=$(get_env_var "$var")
|
||||||
|
if [ -n "$value" ]; then
|
||||||
|
echo -e " ${GREEN}✅ $var:${NC} ${value:0:12}..."
|
||||||
|
else
|
||||||
|
echo -e " ${YELLOW}⚠️ $var:${NC} No definido"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ No se pudo cargar .env${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
echo -e "${GREEN}✅ Validación completada - Listo para desplegar${NC}"
|
||||||
|
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
|
||||||
|
|
||||||
|
# Preguntar si quiere copiar en clipboard (macOS)
|
||||||
|
if command -v pbcopy >/dev/null 2>&1; then
|
||||||
|
echo -e "${YELLOW}💡 Para copiar credenciales, usa: pbcopy < .env${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Función para mostrar advertencias de seguridad
|
||||||
|
show_security_warnings() {
|
||||||
|
echo -e "\n${YELLOW}⚠️ RECOMENDACIONES DE SEGURIDAD:${NC}\n"
|
||||||
|
|
||||||
|
if [ -f .env ]; then
|
||||||
|
if [ -f .gitignore ]; then
|
||||||
|
if grep -q "^.env" .gitignore; then
|
||||||
|
echo -e " ✅ .env está en .gitignore"
|
||||||
|
else
|
||||||
|
echo -e " ${RED}❌ .env NO está en .gitignore${NC}"
|
||||||
|
echo -e " ${YELLOW} → Añade .env al .gitignore${NC}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e " ${YELLOW}⚠️ No hay .gitignore, crea uno para proteger .env${NC}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e " ✅ Credenciales no están en Dockerfile (se inyectan en runtime)"
|
||||||
|
echo -e " ✅ Credenciales no están en docker-compose.yml (se cargan desde .env)"
|
||||||
|
echo -e " ✅ Variables sensibles usan ${NC}${GREEN}recomendados${NC}${BLUE} (no hardcoded)"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Función para desplegar con docker compose
|
||||||
|
deploy_with_compose() {
|
||||||
|
echo -e "\n${GREEN}🚀 DESPLIEGUE AUTOMÁTICO${NC}"
|
||||||
|
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
|
||||||
|
# Detener y limpiar contenedores
|
||||||
|
echo -e "${YELLOW}📦 Deteniendo contenedores...${NC}"
|
||||||
|
docker compose down -v 2>/dev/null || true
|
||||||
|
|
||||||
|
# Iniciar servicios
|
||||||
|
echo -e "${GREEN}📦 Construyendo e iniciando servicios...${NC}"
|
||||||
|
docker compose up -d --build
|
||||||
|
|
||||||
|
# Esperar a que los servicios estén saludables
|
||||||
|
echo -e "${GREEN}✅ Esperando a que los servicios estén listos...${NC}"
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Verificar estado
|
||||||
|
echo -e "${BLUE}📊 Estado de los servicios:${NC}"
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# Verificar base de datos
|
||||||
|
echo -e "${BLUE}🔍 Verificando base de datos...${NC}"
|
||||||
|
if docker compose ps db | grep -q "healthy\|Up"; then
|
||||||
|
echo -e "${GREEN}✅ Base de datos: Iniciada${NC}"
|
||||||
|
|
||||||
|
# Esperar a que el schema se inicialice
|
||||||
|
echo -e "${GREEN}✅ Esperando inicialización del schema...${NC}"
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Ejecutar scripts SQL si existen
|
||||||
|
if [ -d init-db ] && [ "$(ls -A init-db 2>/dev/null)" ]; then
|
||||||
|
echo -e "${YELLOW}📄 Ejecutando scripts de inicialización...${NC}"
|
||||||
|
docker compose exec -T db psql -U rss -d rss -f /docker-entrypoint-initdb.d/*.sql 2>&1 | head -20 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar Redis
|
||||||
|
echo -e "${BLUE}🔍 Verificando Redis...${NC}"
|
||||||
|
sleep 5
|
||||||
|
if docker compose ps redis | grep -q "Up"; then
|
||||||
|
echo -e "${GREEN}✅ Redis: Iniciado${NC}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Error: Base de datos no se inició correctamente${NC}"
|
||||||
|
docker compose logs db
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
echo -e "${GREEN}✅ DESPLIEGUE COMPLETADO${NC}"
|
||||||
|
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}🌐 Endpoints disponibles:${NC}"
|
||||||
|
echo -e " ${GREEN}App:${NC} http://localhost:8888"
|
||||||
|
echo -e " ${GREEN}Backend API:${NC} http://localhost:8888/api"
|
||||||
|
echo -e " ${GREEN}Grafana:${NC} http://127.0.0.1:3001"
|
||||||
|
echo -e " ${GREEN}Prometheus:${NC} http://127.0.0.1:9090"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Verificar si existe .env.backup y restaurarlo
|
||||||
|
if [ -f .env.backup ]; then
|
||||||
|
echo -e "${YELLOW}⚠️ Nota: Se encontró .env.backup, puedes restaurarlo si fue necesario${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Función principal
|
||||||
|
main() {
|
||||||
|
# Verificar que .env exista y tenga contenido
|
||||||
|
if ! check_env_file; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validar credenciales críticas
|
||||||
|
if ! validate_credentials; then
|
||||||
|
echo -e "\n${RED}❌ Error: Faltan credenciales críticas${NC}"
|
||||||
|
echo -e "${YELLOW}💡 Ejecuta: ./pre-deploy.sh --generate${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Mostrar resumen de credenciales
|
||||||
|
if ! show_credentials_summary; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Mostrar advertencias de seguridad
|
||||||
|
show_security_warnings
|
||||||
|
|
||||||
|
echo -e "\n${GREEN}╔═══════════════════════════════════════════════════════════╗"
|
||||||
|
echo -e "║ ✅ PRE-DEPLOY VALIDACIÓN EXITOSA - LISTO PARA INICIAR ║"
|
||||||
|
echo -e "╚═══════════════════════════════════════════════════════════╝${NC}\n"
|
||||||
|
|
||||||
|
# Preguntar si quiere desplegar automáticamente
|
||||||
|
echo -e "${BLUE}¿Deseas desplegar ahora?${NC}"
|
||||||
|
echo -e " 1) Desplegar automáticamente (recomendado)"
|
||||||
|
echo -e " 2) Solo validar y salir (modo manual)"
|
||||||
|
echo -e " "
|
||||||
|
read -p " Elige una opción: " choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1)
|
||||||
|
if deploy_with_compose; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "\n${RED}❌ Error en el despliegue${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
echo -e "\n${YELLOW}✅ Validación completada - Salir${NC}"
|
||||||
|
echo -e "${BLUE}▶️ Para desplegar manualmente, ejecuta:${NC}"
|
||||||
|
echo -e " ${GREEN}docker compose up -d${NC}"
|
||||||
|
echo ""
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${YELLOW}⚠️ Opción no válida, usando opción 2${NC}"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ejecutar
|
||||||
|
main
|
||||||
|
|
||||||
|
# ==================================================================================
|
||||||
|
# Verificación de modelo GPU (después del despliegue)
|
||||||
|
# ==================================================================================
|
||||||
|
check_gpu_model() {
|
||||||
|
MODEL_DIR="./models/nllb-ct2-1.3b"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo " 🤖 Verificando modelo de traducción GPU"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
if [ ! -d "$MODEL_DIR" ]; then
|
||||||
|
echo " ⚠️ Directorio de modelo no existe"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
model_ok=true
|
||||||
|
|
||||||
|
# Verificar model.bin
|
||||||
|
if [ -f "$MODEL_DIR/model.bin" ]; then
|
||||||
|
size=$(stat -c%s "$MODEL_DIR/model.bin" 2>/dev/null || echo 0)
|
||||||
|
if [ "$size" -gt 1000000 ]; then
|
||||||
|
echo " ✓ model.bin ($(numfmt --to=iec-i --suffix=B $size 2>/dev/null || echo "${size}B"))"
|
||||||
|
else
|
||||||
|
echo " ✗ model.bin demasiado pequeño"
|
||||||
|
model_ok=false
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ✗ model.bin no encontrado"
|
||||||
|
model_ok=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar config.json
|
||||||
|
if [ -f "$MODEL_DIR/config.json" ]; then
|
||||||
|
echo " ✓ config.json"
|
||||||
|
else
|
||||||
|
echo " ✗ config.json no encontrado"
|
||||||
|
model_ok=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar shared_vocabulary.json
|
||||||
|
if [ -f "$MODEL_DIR/shared_vocabulary.json" ]; then
|
||||||
|
echo " ✓ shared_vocabulary.json"
|
||||||
|
else
|
||||||
|
echo " ✗ shared_vocabulary.json no encontrado"
|
||||||
|
model_ok=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [ "$model_ok" = true ]; then
|
||||||
|
echo " ✓ Modelo verificado - workers GPU iniciarán rápidamente"
|
||||||
|
else
|
||||||
|
echo " ⚠️ Modelo incompleto - primer worker lo convertirá (5-10 min)"
|
||||||
|
echo ""
|
||||||
|
echo " Pre-convertir manualmente:"
|
||||||
|
echo " docker run --rm -v \$(pwd)/models:/app/models rss2-translator-gpu:latest \\"
|
||||||
|
echo " ct2-transformers-converter --model facebook/nllb-200-1.3B \\"
|
||||||
|
echo " --output_dir /app/models/nllb-ct2-1.3b --quantization float16 --force"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_gpu_model
|
||||||
60
prepare_model.sh
Normal file
60
prepare_model.sh
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# ==============================================================================
|
||||||
|
# Pre-deployment model preparation for translator-gpu
|
||||||
|
# Ensures the NLLB-1.3B model is properly converted before starting workers
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
MODEL_DIR="./models/nllb-ct2-1.3b"
|
||||||
|
REQUIRED_FILES=("model.bin" "config.json" "shared_vocabulary.json")
|
||||||
|
MIN_SIZES=(1000000 100 1000000) # Minimum sizes in bytes
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Pre-Deploy Model Preparation"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Check if model directory exists
|
||||||
|
if [ ! -d "$MODEL_DIR" ]; then
|
||||||
|
echo "Creating model directory..."
|
||||||
|
mkdir -p "$MODEL_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if all required files exist and have minimum size
|
||||||
|
all_present=true
|
||||||
|
for i in "${!REQUIRED_FILES[@]}"; do
|
||||||
|
file="${REQUIRED_FILES[$i]}"
|
||||||
|
min_size="${MIN_SIZES[$i]}"
|
||||||
|
|
||||||
|
if [ -f "$MODEL_DIR/$file" ]; then
|
||||||
|
size=$(stat -c%s "$MODEL_DIR/$file" 2>/dev/null || stat -f%z "$MODEL_DIR/$file" 2>/dev/null || echo 0)
|
||||||
|
if [ "$size" -lt "$min_size" ]; then
|
||||||
|
echo " ✗ $file is too small ($size bytes), needs re-conversion"
|
||||||
|
all_present=false
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo " ✓ $file OK ($(numfmt --to=iec-i --suffix=B $size))"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ✗ $file missing"
|
||||||
|
all_present=false
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$all_present" = true ]; then
|
||||||
|
echo ""
|
||||||
|
echo "✓ Model files verified - workers can start immediately"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "⚠ Model incomplete or missing - will be converted by first worker"
|
||||||
|
echo " This may take 5-10 minutes on first deployment"
|
||||||
|
echo ""
|
||||||
|
echo "To pre-convert the model manually (optional):"
|
||||||
|
echo " docker run --rm -v \$(pwd)/models:/app/models rss2-translator-gpu:latest \\"
|
||||||
|
echo " ct2-transformers-converter --model facebook/nllb-200-1.3B \\"
|
||||||
|
echo " --output_dir /app/models/nllb-ct2-1.3b --quantization float16 --force"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
|
@ -2,6 +2,7 @@ import os
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import fcntl
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
|
@ -19,19 +20,21 @@ LOG = logging.getLogger("translator_ct2")
|
||||||
TRANSLATOR_ID = os.environ.get("TRANSLATOR_ID", "")
|
TRANSLATOR_ID = os.environ.get("TRANSLATOR_ID", "")
|
||||||
TRANSLATOR_TOTAL = int(os.environ.get("TRANSLATOR_TOTAL", "1"))
|
TRANSLATOR_TOTAL = int(os.environ.get("TRANSLATOR_TOTAL", "1"))
|
||||||
|
|
||||||
|
|
||||||
def clean_text(text: str) -> str:
|
def clean_text(text: str) -> str:
|
||||||
if not text:
|
if not text:
|
||||||
return ""
|
return ""
|
||||||
text = re.sub(r'<[^>]+>', '', text)
|
text = re.sub(r"<[^>]+>", "", text)
|
||||||
text = text.replace('<unk>', '')
|
text = text.replace("<unk>", "")
|
||||||
text = text.replace(' ', ' ')
|
text = text.replace(" ", " ")
|
||||||
text = text.replace('&', '&')
|
text = text.replace("&", "&")
|
||||||
text = text.replace('<', '<')
|
text = text.replace("<", "<")
|
||||||
text = text.replace('>', '>')
|
text = text.replace(">", ">")
|
||||||
text = text.replace('"', '"')
|
text = text.replace(""", '"')
|
||||||
text = re.sub(r'\s+', ' ', text)
|
text = re.sub(r"\s+", " ", text)
|
||||||
return text.strip()
|
return text.strip()
|
||||||
|
|
||||||
|
|
||||||
DB_CONFIG = {
|
DB_CONFIG = {
|
||||||
"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)),
|
||||||
|
|
@ -40,12 +43,14 @@ DB_CONFIG = {
|
||||||
"password": os.environ.get("DB_PASS", "x"),
|
"password": os.environ.get("DB_PASS", "x"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _env_list(name: str, default="es"):
|
def _env_list(name: str, default="es"):
|
||||||
raw = os.environ.get(name)
|
raw = os.environ.get(name)
|
||||||
if raw:
|
if raw:
|
||||||
return [s.strip() for s in raw.split(",") if s.strip()]
|
return [s.strip() for s in raw.split(",") if s.strip()]
|
||||||
return [default]
|
return [default]
|
||||||
|
|
||||||
|
|
||||||
def _env_int(name: str, default: int = 8):
|
def _env_int(name: str, default: int = 8):
|
||||||
v = os.environ.get(name)
|
v = os.environ.get(name)
|
||||||
try:
|
try:
|
||||||
|
|
@ -53,14 +58,16 @@ def _env_int(name: str, default: int = 8):
|
||||||
except Exception:
|
except Exception:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
def _env_str(name: str, default=None):
|
def _env_str(name: str, default=None):
|
||||||
v = os.environ.get(name)
|
v = os.environ.get(name)
|
||||||
return v if v else default
|
return v if v else default
|
||||||
|
|
||||||
|
|
||||||
TARGET_LANGS = _env_list("TARGET_LANGS")
|
TARGET_LANGS = _env_list("TARGET_LANGS")
|
||||||
BATCH_SIZE = _env_int("TRANSLATOR_BATCH", 8)
|
BATCH_SIZE = _env_int("TRANSLATOR_BATCH", 128)
|
||||||
MAX_SRC_TOKENS = _env_int("MAX_SRC_TOKENS", 512)
|
MAX_SRC_TOKENS = _env_int("MAX_SRC_TOKENS", 256)
|
||||||
MAX_NEW_TOKENS = _env_int("MAX_NEW_TOKENS", 512)
|
MAX_NEW_TOKENS = _env_int("MAX_NEW_TOKENS", 256)
|
||||||
|
|
||||||
CT2_MODEL_PATH = _env_str("CT2_MODEL_PATH", "/app/models/nllb-ct2")
|
CT2_MODEL_PATH = _env_str("CT2_MODEL_PATH", "/app/models/nllb-ct2")
|
||||||
CT2_DEVICE = _env_str("CT2_DEVICE", "cpu")
|
CT2_DEVICE = _env_str("CT2_DEVICE", "cpu")
|
||||||
|
|
@ -69,87 +76,160 @@ UNIVERSAL_MODEL = _env_str("UNIVERSAL_MODEL", "facebook/nllb-200-distilled-600M"
|
||||||
BODY_CHARS_CHUNK = _env_int("BODY_CHARS_CHUNK", 900)
|
BODY_CHARS_CHUNK = _env_int("BODY_CHARS_CHUNK", 900)
|
||||||
|
|
||||||
LANG_CODE_MAP = {
|
LANG_CODE_MAP = {
|
||||||
"en": "eng_Latn", "es": "spa_Latn", "fr": "fra_Latn", "de": "deu_Latn",
|
"en": "eng_Latn",
|
||||||
"it": "ita_Latn", "pt": "por_Latn", "nl": "nld_Latn", "sv": "swe_Latn",
|
"es": "spa_Latn",
|
||||||
"da": "dan_Latn", "fi": "fin_Latn", "no": "nob_Latn",
|
"fr": "fra_Latn",
|
||||||
"pl": "pol_Latn", "cs": "ces_Latn", "sk": "slk_Latn",
|
"de": "deu_Latn",
|
||||||
"sl": "slv_Latn", "hu": "hun_Latn", "ro": "ron_Latn",
|
"it": "ita_Latn",
|
||||||
"el": "ell_Grek", "ru": "rus_Cyrl", "uk": "ukr_Cyrl",
|
"pt": "por_Latn",
|
||||||
"tr": "tur_Latn", "ar": "arb_Arab", "fa": "pes_Arab",
|
"nl": "nld_Latn",
|
||||||
"he": "heb_Hebr", "zh": "zho_Hans", "ja": "jpn_Jpan",
|
"sv": "swe_Latn",
|
||||||
"ko": "kor_Hang", "vi": "vie_Latn",
|
"da": "dan_Latn",
|
||||||
|
"fi": "fin_Latn",
|
||||||
|
"no": "nob_Latn",
|
||||||
|
"pl": "pol_Latn",
|
||||||
|
"cs": "ces_Latn",
|
||||||
|
"sk": "slk_Latn",
|
||||||
|
"sl": "slv_Latn",
|
||||||
|
"hu": "hun_Latn",
|
||||||
|
"ro": "ron_Latn",
|
||||||
|
"el": "ell_Grek",
|
||||||
|
"ru": "rus_Cyrl",
|
||||||
|
"uk": "ukr_Cyrl",
|
||||||
|
"tr": "tur_Latn",
|
||||||
|
"ar": "arb_Arab",
|
||||||
|
"fa": "pes_Arab",
|
||||||
|
"he": "heb_Hebr",
|
||||||
|
"zh": "zho_Hans",
|
||||||
|
"ja": "jpn_Jpan",
|
||||||
|
"ko": "kor_Hang",
|
||||||
|
"vi": "vie_Latn",
|
||||||
}
|
}
|
||||||
|
|
||||||
_tokenizer = None
|
_tokenizer = None
|
||||||
_translator = None
|
_translator = None
|
||||||
|
|
||||||
|
|
||||||
def ensure_model():
|
def ensure_model():
|
||||||
global _tokenizer, _translator
|
global _tokenizer, _translator
|
||||||
|
|
||||||
if _translator:
|
if _translator:
|
||||||
return
|
return
|
||||||
|
|
||||||
model_path = CT2_MODEL_PATH
|
model_path = CT2_MODEL_PATH
|
||||||
model_bin = os.path.join(model_path, "model.bin")
|
model_bin = os.path.join(model_path, "model.bin")
|
||||||
|
|
||||||
if not os.path.exists(model_bin):
|
# Check if model exists AND is complete (all required files present and non-empty)
|
||||||
LOG.info(f"CTranslate2 model not found at {model_path}, converting from {UNIVERSAL_MODEL}...")
|
required_files = ["model.bin", "config.json", "shared_vocabulary.json"]
|
||||||
|
model_exists = os.path.exists(model_bin)
|
||||||
|
|
||||||
|
if model_exists:
|
||||||
|
# Verify all files exist and have reasonable size
|
||||||
|
all_files_ok = True
|
||||||
|
for f in required_files:
|
||||||
|
fpath = os.path.join(model_path, f)
|
||||||
|
if not os.path.exists(fpath) or os.path.getsize(fpath) < 1000:
|
||||||
|
all_files_ok = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if not all_files_ok:
|
||||||
|
LOG.info(f"Model files incomplete or corrupted, re-converting...")
|
||||||
|
# Clean up corrupted files
|
||||||
|
for f in required_files:
|
||||||
|
try:
|
||||||
|
fpath = os.path.join(model_path, f)
|
||||||
|
if os.path.exists(fpath):
|
||||||
|
os.remove(fpath)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
model_exists = False
|
||||||
|
|
||||||
|
if not model_exists:
|
||||||
|
LOG.info(
|
||||||
|
f"CTranslate2 model not found at {model_path}, converting from {UNIVERSAL_MODEL}..."
|
||||||
|
)
|
||||||
convert_model()
|
convert_model()
|
||||||
|
|
||||||
LOG.info(f"Loading CTranslate2 model from {model_path} on {CT2_DEVICE}")
|
device = os.environ.get("CT2_DEVICE", "cpu")
|
||||||
|
LOG.info(f"Loading CTranslate2 model from {model_path} on {device}")
|
||||||
|
|
||||||
_translator = ctranslate2.Translator(
|
_translator = ctranslate2.Translator(
|
||||||
model_path,
|
model_path,
|
||||||
device=CT2_DEVICE,
|
device=device,
|
||||||
compute_type=CT2_COMPUTE_TYPE,
|
compute_type=CT2_COMPUTE_TYPE,
|
||||||
)
|
)
|
||||||
|
|
||||||
_tokenizer = AutoTokenizer.from_pretrained(UNIVERSAL_MODEL)
|
_tokenizer = AutoTokenizer.from_pretrained(UNIVERSAL_MODEL)
|
||||||
LOG.info("CTranslate2 model loaded successfully")
|
LOG.info("CTranslate2 model loaded successfully")
|
||||||
|
|
||||||
|
|
||||||
def convert_model():
|
def convert_model():
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
model_path = CT2_MODEL_PATH
|
model_path = CT2_MODEL_PATH
|
||||||
|
lock_file = os.path.join(model_path, ".converting.lock")
|
||||||
|
|
||||||
|
# Clean up any corrupted files from previous failed conversions
|
||||||
|
if os.path.exists(model_path):
|
||||||
|
for f in os.listdir(model_path):
|
||||||
|
if f.endswith(".lock") or f.startswith("."):
|
||||||
|
try:
|
||||||
|
os.remove(os.path.join(model_path, f))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
os.makedirs(model_path, exist_ok=True)
|
os.makedirs(model_path, exist_ok=True)
|
||||||
|
|
||||||
quantization = CT2_COMPUTE_TYPE if CT2_COMPUTE_TYPE != "auto" else "int8"
|
# Use lock file to prevent multiple workers from converting simultaneously
|
||||||
|
lock_fd = os.open(lock_file, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
||||||
cmd = [
|
try:
|
||||||
"ct2-transformers-converter",
|
quantization = CT2_COMPUTE_TYPE if CT2_COMPUTE_TYPE != "auto" else "float16"
|
||||||
"--model", UNIVERSAL_MODEL,
|
|
||||||
"--output_dir", model_path,
|
cmd = [
|
||||||
"--quantization", quantization,
|
"ct2-transformers-converter",
|
||||||
"--force"
|
"--model",
|
||||||
]
|
UNIVERSAL_MODEL,
|
||||||
|
"--output_dir",
|
||||||
LOG.info(f"Running: {' '.join(cmd)}")
|
model_path,
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=1800)
|
"--quantization",
|
||||||
|
quantization,
|
||||||
if result.returncode != 0:
|
"--force",
|
||||||
LOG.error(f"Model conversion failed: {result.stderr}")
|
]
|
||||||
raise RuntimeError("Failed to convert model")
|
|
||||||
|
LOG.info(f"Running: {' '.join(cmd)}")
|
||||||
LOG.info("Model conversion completed")
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=3600)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
LOG.error(f"Model conversion failed: {result.stderr}")
|
||||||
|
raise RuntimeError("Failed to convert model")
|
||||||
|
|
||||||
|
LOG.info("Model conversion completed")
|
||||||
|
finally:
|
||||||
|
os.close(lock_fd)
|
||||||
|
try:
|
||||||
|
os.remove(lock_file)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def translate_texts(src: str, tgt: str, texts: List[str]) -> List[str]:
|
def translate_texts(src: str, tgt: str, texts: List[str]) -> List[str]:
|
||||||
if not texts:
|
if not texts:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
ensure_model()
|
ensure_model()
|
||||||
|
|
||||||
clean = [(t or "").strip() for t in texts]
|
clean = [(t or "").strip() for t in texts]
|
||||||
if all(not t for t in clean):
|
if all(not t for t in clean):
|
||||||
return ["" for _ in clean]
|
return ["" for _ in clean]
|
||||||
|
|
||||||
src_code = LANG_CODE_MAP.get(src, f"{src}_Latn")
|
src_code = LANG_CODE_MAP.get(src, f"{src}_Latn")
|
||||||
tgt_code = LANG_CODE_MAP.get(tgt, "spa_Latn")
|
tgt_code = LANG_CODE_MAP.get(tgt, "spa_Latn")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_tokenizer.src_lang = src_code
|
_tokenizer.src_lang = src_code
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
sources = []
|
sources = []
|
||||||
for t in clean:
|
for t in clean:
|
||||||
if t:
|
if t:
|
||||||
|
|
@ -158,18 +238,18 @@ def translate_texts(src: str, tgt: str, texts: List[str]) -> List[str]:
|
||||||
sources.append(tokens)
|
sources.append(tokens)
|
||||||
else:
|
else:
|
||||||
sources.append([])
|
sources.append([])
|
||||||
|
|
||||||
target_prefix = [[tgt_code]] * len(sources)
|
target_prefix = [[tgt_code]] * len(sources)
|
||||||
|
|
||||||
results = _translator.translate_batch(
|
results = _translator.translate_batch(
|
||||||
sources,
|
sources,
|
||||||
target_prefix=target_prefix,
|
target_prefix=target_prefix,
|
||||||
beam_size=2,
|
beam_size=1,
|
||||||
max_decoding_length=MAX_NEW_TOKENS,
|
max_decoding_length=MAX_NEW_TOKENS,
|
||||||
repetition_penalty=2.0,
|
repetition_penalty=1.2,
|
||||||
no_repeat_ngram_size=3,
|
no_repeat_ngram_size=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
translated = []
|
translated = []
|
||||||
for result in results:
|
for result in results:
|
||||||
try:
|
try:
|
||||||
|
|
@ -197,18 +277,19 @@ def translate_texts(src: str, tgt: str, texts: List[str]) -> List[str]:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Error processing result: {e}")
|
LOG.error(f"Error processing result: {e}")
|
||||||
translated.append("")
|
translated.append("")
|
||||||
|
|
||||||
return translated
|
return translated
|
||||||
|
|
||||||
|
|
||||||
def split_body_into_chunks(text: str) -> List[str]:
|
def split_body_into_chunks(text: str) -> List[str]:
|
||||||
text = (text or "").strip()
|
text = (text or "").strip()
|
||||||
if len(text) <= BODY_CHARS_CHUNK:
|
if len(text) <= BODY_CHARS_CHUNK:
|
||||||
return [text] if text else []
|
return [text] if text else []
|
||||||
|
|
||||||
parts = re.split(r'(\n\n+|(?<=[\.\!\?؛؟。])\s+)', text)
|
parts = re.split(r"(\n\n+|(?<=[\.\!\?؛؟。])\s+)", text)
|
||||||
chunks = []
|
chunks = []
|
||||||
current = ""
|
current = ""
|
||||||
|
|
||||||
for part in parts:
|
for part in parts:
|
||||||
if not part:
|
if not part:
|
||||||
continue
|
continue
|
||||||
|
|
@ -220,31 +301,34 @@ def split_body_into_chunks(text: str) -> List[str]:
|
||||||
current = part
|
current = part
|
||||||
if current.strip():
|
if current.strip():
|
||||||
chunks.append(current.strip())
|
chunks.append(current.strip())
|
||||||
|
|
||||||
return chunks if chunks else [text]
|
return chunks if chunks else [text]
|
||||||
|
|
||||||
|
|
||||||
def translate_body_long(src: str, tgt: str, body: str) -> str:
|
def translate_body_long(src: str, tgt: str, body: str) -> str:
|
||||||
body = (body or "").strip()
|
body = (body or "").strip()
|
||||||
if not body:
|
if not body:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
chunks = split_body_into_chunks(body)
|
chunks = split_body_into_chunks(body)
|
||||||
if len(chunks) == 1:
|
if len(chunks) == 1:
|
||||||
return translate_texts(src, tgt, [body])[0]
|
return translate_texts(src, tgt, [body])[0]
|
||||||
|
|
||||||
translated_chunks = []
|
translated_chunks = []
|
||||||
for ch in chunks:
|
for ch in chunks:
|
||||||
tr = translate_texts(src, tgt, [ch])[0]
|
tr = translate_texts(src, tgt, [ch])[0]
|
||||||
translated_chunks.append(tr)
|
translated_chunks.append(tr)
|
||||||
|
|
||||||
return " ".join(translated_chunks)
|
return " ".join(translated_chunks)
|
||||||
|
|
||||||
|
|
||||||
def normalize_lang(lang: Optional[str], default: str = "es") -> Optional[str]:
|
def normalize_lang(lang: Optional[str], default: str = "es") -> Optional[str]:
|
||||||
if not lang:
|
if not lang:
|
||||||
return default
|
return default
|
||||||
lang = lang.strip().lower()[:2]
|
lang = lang.strip().lower()[:2]
|
||||||
return lang if lang else default
|
return lang if lang else default
|
||||||
|
|
||||||
|
|
||||||
def detect_lang(text: str) -> str:
|
def detect_lang(text: str) -> str:
|
||||||
if not text or len(text) < 10:
|
if not text or len(text) < 10:
|
||||||
return "en"
|
return "en"
|
||||||
|
|
@ -253,63 +337,75 @@ def detect_lang(text: str) -> str:
|
||||||
except Exception:
|
except Exception:
|
||||||
return "en"
|
return "en"
|
||||||
|
|
||||||
|
|
||||||
def process_batch(conn, rows):
|
def process_batch(conn, rows):
|
||||||
todo = []
|
todo = []
|
||||||
|
|
||||||
for r in rows:
|
for r in rows:
|
||||||
lang_to = normalize_lang(r.get("lang_to"), "es") or "es"
|
lang_to = normalize_lang(r.get("lang_to"), "es") or "es"
|
||||||
lang_from = normalize_lang(r.get("lang_from")) or detect_lang(r.get("titulo") or "")
|
lang_from = normalize_lang(r.get("lang_from")) or detect_lang(
|
||||||
|
r.get("titulo") or ""
|
||||||
|
)
|
||||||
|
|
||||||
titulo = (r.get("titulo") or "").strip()
|
titulo = (r.get("titulo") or "").strip()
|
||||||
resumen = (r.get("resumen") or "").strip()
|
resumen = (r.get("resumen") or "").strip()
|
||||||
|
|
||||||
if lang_from == lang_to:
|
if lang_from == lang_to:
|
||||||
# Mark as done and copy original text if languages match
|
# Mark as done and copy original text if languages match
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("""
|
cursor.execute(
|
||||||
|
"""
|
||||||
UPDATE traducciones
|
UPDATE traducciones
|
||||||
SET titulo_trad = %s, resumen_trad = %s, status = 'done'
|
SET titulo_trad = %s, resumen_trad = %s, status = 'done'
|
||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
""", (titulo, resumen, r.get("tr_id")))
|
""",
|
||||||
|
(titulo, resumen, r.get("tr_id")),
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
todo.append({
|
todo.append(
|
||||||
"tr_id": r.get("tr_id"),
|
{
|
||||||
"lang_from": lang_from,
|
"tr_id": r.get("tr_id"),
|
||||||
"lang_to": lang_to,
|
"lang_from": lang_from,
|
||||||
"titulo": titulo,
|
"lang_to": lang_to,
|
||||||
"resumen": resumen,
|
"titulo": titulo,
|
||||||
})
|
"resumen": resumen,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if not todo:
|
if not todo:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 1. FAST LOCKING: Commit locked_at immediately to inform other workers
|
# 1. FAST LOCKING: Commit locked_at immediately to inform other workers
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
tr_ids = [item["tr_id"] for item in todo]
|
tr_ids = [item["tr_id"] for item in todo]
|
||||||
cursor.execute(f"""
|
cursor.execute(
|
||||||
|
f"""
|
||||||
UPDATE traducciones
|
UPDATE traducciones
|
||||||
SET locked_at = NOW()
|
SET locked_at = NOW()
|
||||||
WHERE id = ANY(ARRAY[{','.join(['%s'] * len(tr_ids))}])
|
WHERE id = ANY(ARRAY[{",".join(["%s"] * len(tr_ids))}])
|
||||||
""", tr_ids)
|
""",
|
||||||
|
tr_ids,
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
groups = defaultdict(list)
|
groups = defaultdict(list)
|
||||||
for item in todo:
|
for item in todo:
|
||||||
key = (item["lang_from"], item["lang_to"])
|
key = (item["lang_from"], item["lang_to"])
|
||||||
groups[key].append(item)
|
groups[key].append(item)
|
||||||
|
|
||||||
for (lang_from, lang_to), items in groups.items():
|
for (lang_from, lang_to), items in groups.items():
|
||||||
LOG.info(f"Translating {lang_from} -> {lang_to} ({len(items)} items)")
|
LOG.info(f"Translating {lang_from} -> {lang_to} ({len(items)} items)")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
titles = [i["titulo"] for i in items]
|
titles = [i["titulo"] for i in items]
|
||||||
translated_titles = translate_texts(lang_from, lang_to, titles)
|
translated_titles = translate_texts(lang_from, lang_to, titles)
|
||||||
|
|
||||||
for item, tt in zip(items, translated_titles):
|
for item, tt in zip(items, translated_titles):
|
||||||
body = (item["resumen"] or "").strip()
|
body = (item["resumen"] or "").strip()
|
||||||
tb = ""
|
tb = ""
|
||||||
|
|
@ -319,52 +415,61 @@ def process_batch(conn, rows):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Body translation error for ID {item['tr_id']}: {e}")
|
LOG.error(f"Body translation error for ID {item['tr_id']}: {e}")
|
||||||
tb = item["resumen"]
|
tb = item["resumen"]
|
||||||
|
|
||||||
tt = clean_text((tt or "").strip())
|
tt = clean_text((tt or "").strip())
|
||||||
tb = clean_text((tb or "").strip())
|
tb = clean_text((tb or "").strip())
|
||||||
|
|
||||||
if not tt:
|
if not tt:
|
||||||
tt = item["titulo"]
|
tt = item["titulo"]
|
||||||
if not tb:
|
if not tb:
|
||||||
tb = item["resumen"]
|
tb = item["resumen"]
|
||||||
|
|
||||||
# 2. INDIVIDUAL COMMIT: Save each item as it's done
|
# 2. INDIVIDUAL COMMIT: Save each item as it's done
|
||||||
try:
|
try:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("""
|
cursor.execute(
|
||||||
|
"""
|
||||||
UPDATE traducciones
|
UPDATE traducciones
|
||||||
SET titulo_trad = %s, resumen_trad = %s, status = 'done', locked_at = NULL
|
SET titulo_trad = %s, resumen_trad = %s, status = 'done', locked_at = NULL
|
||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
""", (tt, tb, item["tr_id"]))
|
""",
|
||||||
|
(tt, tb, item["tr_id"]),
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Update error for ID {item['tr_id']}: {e}")
|
LOG.error(f"Update error for ID {item['tr_id']}: {e}")
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
|
|
||||||
LOG.info(f"Finished group {lang_from} -> {lang_to}")
|
LOG.info(f"Finished group {lang_from} -> {lang_to}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Batch group error {lang_from} -> {lang_to}: {e}")
|
LOG.error(f"Batch group error {lang_from} -> {lang_to}: {e}")
|
||||||
# Mark these as error to avoid infinite loop if it's a model crash
|
# Mark these as error to avoid infinite loop if it's a model crash
|
||||||
try:
|
try:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("""
|
cursor.execute(
|
||||||
|
"""
|
||||||
UPDATE traducciones SET status = 'error', locked_at = NULL
|
UPDATE traducciones SET status = 'error', locked_at = NULL
|
||||||
WHERE id = ANY(ARRAY[{','.join(['%s'] * len(items))}])
|
WHERE id = ANY(ARRAY[{','.join(['%s'] * len(items))}])
|
||||||
""", [i["tr_id"] for i in items])
|
""",
|
||||||
|
[i["tr_id"] for i in items],
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
except:
|
except:
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
|
|
||||||
|
|
||||||
def fetch_pending_translations(conn):
|
def fetch_pending_translations(conn):
|
||||||
cursor = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
cursor = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||||
|
|
||||||
worker_id = os.environ.get("HOSTNAME", f"worker-{os.getpid()}")
|
worker_id = os.environ.get("HOSTNAME", f"worker-{os.getpid()}")
|
||||||
|
total_found = 0
|
||||||
|
|
||||||
for lang in TARGET_LANGS:
|
for lang in TARGET_LANGS:
|
||||||
cursor.execute("""
|
cursor.execute(
|
||||||
|
"""
|
||||||
SELECT t.id as tr_id, t.lang_from, t.lang_to,
|
SELECT t.id as tr_id, t.lang_from, t.lang_to,
|
||||||
n.titulo, n.resumen, n.id as noticia_id
|
n.titulo, n.resumen, n.id as noticia_id
|
||||||
FROM traducciones t
|
FROM traducciones t
|
||||||
|
|
@ -375,31 +480,45 @@ def fetch_pending_translations(conn):
|
||||||
ORDER BY n.fecha DESC
|
ORDER BY n.fecha DESC
|
||||||
LIMIT %s
|
LIMIT %s
|
||||||
FOR UPDATE SKIP LOCKED
|
FOR UPDATE SKIP LOCKED
|
||||||
""", (lang, BATCH_SIZE))
|
""",
|
||||||
|
(lang, BATCH_SIZE),
|
||||||
|
)
|
||||||
|
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
if rows:
|
if rows:
|
||||||
LOG.info(f"Found {len(rows)} pending translations for {lang}")
|
LOG.info(f"Found {len(rows)} pending translations for {lang}")
|
||||||
process_batch(conn, rows)
|
process_batch(conn, rows)
|
||||||
|
total_found += len(rows)
|
||||||
|
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
return total_found
|
||||||
|
|
||||||
|
|
||||||
def connect_db():
|
def connect_db():
|
||||||
return psycopg2.connect(**DB_CONFIG)
|
return psycopg2.connect(**DB_CONFIG)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
LOG.info(f"CTranslate2 translator worker started (device={CT2_DEVICE}, instances={TRANSLATOR_TOTAL})")
|
LOG.info(
|
||||||
|
f"CTranslate2 translator worker started (device={CT2_DEVICE}, instances={TRANSLATOR_TOTAL})"
|
||||||
|
)
|
||||||
ensure_model()
|
ensure_model()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
conn = connect_db()
|
conn = connect_db()
|
||||||
fetch_pending_translations(conn)
|
total = fetch_pending_translations(conn)
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
if total == 0:
|
||||||
|
LOG.info("No pending translations, sleeping...")
|
||||||
|
else:
|
||||||
|
LOG.info(f"Processed {total} translations, sleeping...")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Error: {e}")
|
LOG.error(f"Error: {e}")
|
||||||
|
|
||||||
time.sleep(30)
|
time.sleep(30)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue