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__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
*.egg
|
||||
*.egg-info/
|
||||
dist/
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
.env
|
||||
env/
|
||||
ENV/
|
||||
.venv
|
||||
|
||||
# ==================================================================================
|
||||
# IDEs
|
||||
# ==================================================================================
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
pgdata/
|
||||
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
|
||||
|
||||
Thumbs.db
|
||||
|
||||
# ==================================================================================
|
||||
# SECURITY FILES - NEVER COMMIT THESE
|
||||
# SECURITY - NEVER COMMIT
|
||||
# ==================================================================================
|
||||
# Environment files with credentials
|
||||
.env
|
||||
.env.backup*
|
||||
.env.backup
|
||||
.env.backup2
|
||||
.env.generated
|
||||
.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
|
||||
!init-db/*.sql
|
||||
!migrations/*.sql
|
||||
backup_*.sql
|
||||
|
||||
# Redis backups
|
||||
*.rdb
|
||||
redis_backup_*.rdb
|
||||
|
||||
# Qdrant backups
|
||||
qdrant_backup_*.tar.gz
|
||||
|
||||
# Docker compose with real credentials (if you create variations)
|
||||
# ==================================================================================
|
||||
# Temporary files
|
||||
# ==================================================================================
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
*.log
|
||||
|
||||
# ==================================================================================
|
||||
# Docker
|
||||
# ==================================================================================
|
||||
docker-compose.override.yml
|
||||
|
||||
# Large Language Models
|
||||
models/llm/
|
||||
|
||||
# User Uploads
|
||||
static/uploads/
|
||||
|
||||
# Celery/Workers
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# System/IDE
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
# ==================================================================================
|
||||
# Keep directory structure (optional - uncomment if needed)
|
||||
# models/.gitkeep
|
||||
# data/.gitkeep
|
||||
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)
|
||||
* **NVIDIA GPU**: Required for translation, embeddings, and NER services.
|
||||
* **NVIDIA Container Toolkit**: Must be installed to allow Docker to access the GPU.
|
||||
* **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).
|
||||
```bash
|
||||
git clone https://github.com/tu-usuario/rss2.git
|
||||
cd rss2
|
||||
```
|
||||
|
||||
## Deployment Steps
|
||||
### Paso 2: Generar Credenciales Seguras
|
||||
|
||||
1. **Clone the Repository**
|
||||
```bash
|
||||
git clone <your-repo-url>
|
||||
cd <your-repo-name>
|
||||
```
|
||||
```bash
|
||||
./pre-deploy.sh --generate
|
||||
```
|
||||
|
||||
2. **Configure Environment Variables**
|
||||
Copy the example configuration file:
|
||||
```bash
|
||||
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).
|
||||
**Salida esperada:**
|
||||
```
|
||||
🔑 Generando credenciales seguras...
|
||||
|
||||
3. **Start the Services**
|
||||
Run the following command to build and start the application:
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
⚠️ IMPORTANTE: Guarda estas credenciales en un lugar seguro
|
||||
|
||||
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:
|
||||
```bash
|
||||
docker compose logs -f db
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
POSTGRES_PASSWORD: S5xwsI9HKjdaBWLpCkqp0mA0DJYZerpD
|
||||
REDIS_PASSWORD: cVF79QuJulPO5auEhA2nNqv7mqAruzYh
|
||||
SECRET_KEY: 2eb7c221245bb8d896f83255188de614e59fae6b8dfbf16eee8a23c60dde4ffa
|
||||
GRAFANA_PASSWORD: R4J61V6OGLTCesrITwEvUPTm
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
```
|
||||
|
||||
5. **Verify Deployment**
|
||||
Access the application at `http://<your-server-ip>:8001`.
|
||||
**⚠️ IMPORTANTE:** Copia estas contraseñas y guárdalas en un gestor de contraseñas. ¡Nunca se volverán a mostrar!
|
||||
|
||||
## 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.
|
||||
* **Data Persistence**: Database data is stored in `./pgdata` (mapped in docker-compose). Ensure this directory is backed up.
|
||||
* **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).
|
||||
```bash
|
||||
./pre-deploy.sh
|
||||
```
|
||||
|
||||
**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.
|
||||
* **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.
|
||||
* **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
|
||||
| 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. |
|
||||
| **`backend-go`** | Go + Gin | API REST principal y gestión de lógica de negocio. |
|
||||
|
||||
### Ingesta y Descubrimiento (Go)
|
||||
| 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. |
|
||||
| **`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 |
|
||||
|---------|------------|-------------|
|
||||
| **`translator`** | NLLB-200 (CPU) | Traducción neuronal optimizada con CTranslate2. |
|
||||
| **`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. |
|
||||
| **`embeddings`** | S-Transformers | Generación de vectores para búsqueda semántica. |
|
||||
| **`ner`** | Spacy / BERT | Reconocimiento de entidades nombradas (NER). |
|
||||
| **`llm-categorizer`**| Ollama / Mistral | Clasificación avanzada mediante modelos de lenguaje. |
|
||||
| **`translator-gpu`** | NLLB-200 (GPU) | Traducción acelerada por hardware (CUDA). |
|
||||
| **`remote-translator`** | WebSocket | Worker GPU remoto (conexión vía ws://backend-go:8080/ws/worker). |
|
||||
| **`wiki-worker`** | Go | Integración con Wikipedia y gestión de imágenes locales. |
|
||||
| **`embeddings`** | S-Transformers | Generación de vectores para búsqueda semántica (paraphrase-multilingual-MiniLM-L12-v2). |
|
||||
| **`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. |
|
||||
| **`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
|
||||
| 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. |
|
||||
| **`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
|
||||
|
|
@ -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 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
|
||||
# Paso 1: Clonar el proyecto
|
||||
git clone <repo_url>
|
||||
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
|
||||
```
|
||||
|
||||
### 3. Escalado de Workers (¡Importante!)
|
||||
Para aumentar la velocidad de procesamiento (especialmente la traducción), puedes escalar los workers:
|
||||
**Ventajas:**
|
||||
- ✅ 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
|
||||
# Ejecutar 4 traductores en paralelo
|
||||
docker compose up -d --scale translator=4
|
||||
git clone <repo_url>
|
||||
cd rss2
|
||||
|
||||
# Si usas GPU y tienes capacidad
|
||||
docker compose up -d --scale translator-gpu=2
|
||||
# Generar credenciales manualmente
|
||||
./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
|
||||
|
||||
### Copias de Seguridad (Backups)
|
||||
|
||||
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`)
|
||||
| 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). |
|
||||
| `SCHEDULER_BATCH`| Cantidad de noticias a enviar a traducir por ciclo. |
|
||||
| `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"
|
||||
)
|
||||
|
||||
|
||||
|
||||
func CreateAlias(c *gin.Context) {
|
||||
var req models.EntityAliasRequest
|
||||
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()})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
|
|
@ -112,8 +110,6 @@ func CreateAlias(c *gin.Context) {
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
func ExportAliases(c *gin.Context) {
|
||||
rows, err := db.GetPool().Query(c.Request.Context(),
|
||||
"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
|
||||
func PatchEntityTipo(c *gin.Context) {
|
||||
var req struct {
|
||||
Valor string `json:"valor" binding:"required"`
|
||||
NewTipo string `json:"new_tipo" binding:"required"`
|
||||
Valor string `json:"valor" binding:"required"`
|
||||
NewTipo string `json:"new_tipo" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
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()})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
type OldTag struct {
|
||||
ID int
|
||||
Tipo string
|
||||
|
|
@ -643,32 +639,15 @@ func PatchEntityTipo(c *gin.Context) {
|
|||
|
||||
// BackupDatabase runs pg_dump and returns the SQL as a downloadable file
|
||||
func BackupDatabase(c *gin.Context) {
|
||||
dbHost := os.Getenv("DB_HOST")
|
||||
if dbHost == "" {
|
||||
dbHost = "db"
|
||||
}
|
||||
dbPort := os.Getenv("DB_PORT")
|
||||
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,
|
||||
// Ejecutar pg_dump desde dentro del contenedor db para evitar problemas de red
|
||||
// Usamos docker exec para ejecutar el comando dentro del servicio db
|
||||
cmd := exec.Command("docker", "exec", "rss2_db", "pg_dump",
|
||||
"-U", os.Getenv("POSTGRES_USER"),
|
||||
"-d", os.Getenv("POSTGRES_DB"),
|
||||
"--no-password",
|
||||
"--format=plain",
|
||||
"--pghost=5432",
|
||||
)
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", dbPass))
|
||||
|
||||
var out bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
|
@ -769,4 +748,3 @@ func BackupNewsZipped(c *gin.Context) {
|
|||
c.Header("Cache-Control", "no-cache")
|
||||
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:
|
||||
db:
|
||||
image: postgres:18-alpine
|
||||
|
|
@ -25,6 +38,7 @@ services:
|
|||
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
|
||||
|
|
@ -43,7 +57,7 @@ services:
|
|||
TZ: Europe/Madrid
|
||||
# SEGURIDAD: Redis con autenticación
|
||||
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:
|
||||
- ./data/redis-data:/data
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
|
|
@ -55,6 +69,7 @@ services:
|
|||
- 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
|
||||
|
|
@ -282,8 +297,6 @@ services:
|
|||
dockerfile: Dockerfile.translator
|
||||
image: rss2-translator:latest
|
||||
command: bash -lc "python -m workers.ctranslator_worker"
|
||||
security_opt:
|
||||
- seccomp=unconfined
|
||||
environment:
|
||||
DB_HOST: db
|
||||
DB_PORT: 5432
|
||||
|
|
@ -299,14 +312,60 @@ services:
|
|||
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: 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:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
|
@ -335,8 +394,6 @@ services:
|
|||
- ./models:/app/models
|
||||
networks:
|
||||
- backend
|
||||
profiles:
|
||||
- remote
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
|
@ -381,52 +438,6 @@ services:
|
|||
condition: service_healthy
|
||||
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:
|
||||
build:
|
||||
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
|
||||
|
||||
# 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
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
|
|
@ -157,17 +184,22 @@ EOF
|
|||
|
||||
echo -e "${GREEN}✅ Archivo generado: $ENV_FILE${NC}\n"
|
||||
|
||||
# Preguntar si quiere reemplazar .env
|
||||
echo -e "${YELLOW}¿Deseas reemplazar el archivo .env actual con el generado?${NC}"
|
||||
echo -e "${YELLOW}(Recomendado: revisa $ENV_FILE primero)${NC}"
|
||||
read -p "¿Continuar? (s/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[SsYy]$ ]]; then
|
||||
# Preguntar si quiere reemplazar .env (si no se usa --force)
|
||||
if [ "$FORCE" = "true" ]; then
|
||||
mv "$ENV_FILE" .env
|
||||
echo -e "${GREEN}✅ Archivo .env actualizado${NC}"
|
||||
echo -e "${GREEN}✅ Archivo .env actualizado automáticamente${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Archivo guardado como: $ENV_FILE${NC}"
|
||||
echo -e "${YELLOW} Para usarlo: mv $ENV_FILE .env${NC}"
|
||||
echo -e "${YELLOW}¿Deseas reemplazar el archivo .env actual con el generado?${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
|
||||
|
||||
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 logging
|
||||
import re
|
||||
import fcntl
|
||||
from typing import List, Optional
|
||||
|
||||
import psycopg2
|
||||
|
|
@ -19,19 +20,21 @@ LOG = logging.getLogger("translator_ct2")
|
|||
TRANSLATOR_ID = os.environ.get("TRANSLATOR_ID", "")
|
||||
TRANSLATOR_TOTAL = int(os.environ.get("TRANSLATOR_TOTAL", "1"))
|
||||
|
||||
|
||||
def clean_text(text: str) -> str:
|
||||
if not text:
|
||||
return ""
|
||||
text = re.sub(r'<[^>]+>', '', text)
|
||||
text = text.replace('<unk>', '')
|
||||
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"<[^>]+>", "", text)
|
||||
text = text.replace("<unk>", "")
|
||||
text = text.replace(" ", " ")
|
||||
text = text.replace("&", "&")
|
||||
text = text.replace("<", "<")
|
||||
text = text.replace(">", ">")
|
||||
text = text.replace(""", '"')
|
||||
text = re.sub(r"\s+", " ", text)
|
||||
return text.strip()
|
||||
|
||||
|
||||
DB_CONFIG = {
|
||||
"host": os.environ.get("DB_HOST", "localhost"),
|
||||
"port": int(os.environ.get("DB_PORT", 5432)),
|
||||
|
|
@ -40,12 +43,14 @@ DB_CONFIG = {
|
|||
"password": os.environ.get("DB_PASS", "x"),
|
||||
}
|
||||
|
||||
|
||||
def _env_list(name: str, default="es"):
|
||||
raw = os.environ.get(name)
|
||||
if raw:
|
||||
return [s.strip() for s in raw.split(",") if s.strip()]
|
||||
return [default]
|
||||
|
||||
|
||||
def _env_int(name: str, default: int = 8):
|
||||
v = os.environ.get(name)
|
||||
try:
|
||||
|
|
@ -53,14 +58,16 @@ def _env_int(name: str, default: int = 8):
|
|||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def _env_str(name: str, default=None):
|
||||
v = os.environ.get(name)
|
||||
return v if v else default
|
||||
|
||||
|
||||
TARGET_LANGS = _env_list("TARGET_LANGS")
|
||||
BATCH_SIZE = _env_int("TRANSLATOR_BATCH", 8)
|
||||
MAX_SRC_TOKENS = _env_int("MAX_SRC_TOKENS", 512)
|
||||
MAX_NEW_TOKENS = _env_int("MAX_NEW_TOKENS", 512)
|
||||
BATCH_SIZE = _env_int("TRANSLATOR_BATCH", 128)
|
||||
MAX_SRC_TOKENS = _env_int("MAX_SRC_TOKENS", 256)
|
||||
MAX_NEW_TOKENS = _env_int("MAX_NEW_TOKENS", 256)
|
||||
|
||||
CT2_MODEL_PATH = _env_str("CT2_MODEL_PATH", "/app/models/nllb-ct2")
|
||||
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)
|
||||
|
||||
LANG_CODE_MAP = {
|
||||
"en": "eng_Latn", "es": "spa_Latn", "fr": "fra_Latn", "de": "deu_Latn",
|
||||
"it": "ita_Latn", "pt": "por_Latn", "nl": "nld_Latn", "sv": "swe_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",
|
||||
"en": "eng_Latn",
|
||||
"es": "spa_Latn",
|
||||
"fr": "fra_Latn",
|
||||
"de": "deu_Latn",
|
||||
"it": "ita_Latn",
|
||||
"pt": "por_Latn",
|
||||
"nl": "nld_Latn",
|
||||
"sv": "swe_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
|
||||
_translator = None
|
||||
|
||||
|
||||
def ensure_model():
|
||||
global _tokenizer, _translator
|
||||
|
||||
|
||||
if _translator:
|
||||
return
|
||||
|
||||
|
||||
model_path = CT2_MODEL_PATH
|
||||
model_bin = os.path.join(model_path, "model.bin")
|
||||
|
||||
if not os.path.exists(model_bin):
|
||||
LOG.info(f"CTranslate2 model not found at {model_path}, converting from {UNIVERSAL_MODEL}...")
|
||||
|
||||
# Check if model exists AND is complete (all required files present and non-empty)
|
||||
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()
|
||||
|
||||
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(
|
||||
model_path,
|
||||
device=CT2_DEVICE,
|
||||
device=device,
|
||||
compute_type=CT2_COMPUTE_TYPE,
|
||||
)
|
||||
|
||||
|
||||
_tokenizer = AutoTokenizer.from_pretrained(UNIVERSAL_MODEL)
|
||||
LOG.info("CTranslate2 model loaded successfully")
|
||||
|
||||
|
||||
def convert_model():
|
||||
import subprocess
|
||||
|
||||
|
||||
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)
|
||||
|
||||
quantization = CT2_COMPUTE_TYPE if CT2_COMPUTE_TYPE != "auto" else "int8"
|
||||
|
||||
cmd = [
|
||||
"ct2-transformers-converter",
|
||||
"--model", UNIVERSAL_MODEL,
|
||||
"--output_dir", model_path,
|
||||
"--quantization", quantization,
|
||||
"--force"
|
||||
]
|
||||
|
||||
LOG.info(f"Running: {' '.join(cmd)}")
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=1800)
|
||||
|
||||
if result.returncode != 0:
|
||||
LOG.error(f"Model conversion failed: {result.stderr}")
|
||||
raise RuntimeError("Failed to convert model")
|
||||
|
||||
LOG.info("Model conversion completed")
|
||||
|
||||
# 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)
|
||||
try:
|
||||
quantization = CT2_COMPUTE_TYPE if CT2_COMPUTE_TYPE != "auto" else "float16"
|
||||
|
||||
cmd = [
|
||||
"ct2-transformers-converter",
|
||||
"--model",
|
||||
UNIVERSAL_MODEL,
|
||||
"--output_dir",
|
||||
model_path,
|
||||
"--quantization",
|
||||
quantization,
|
||||
"--force",
|
||||
]
|
||||
|
||||
LOG.info(f"Running: {' '.join(cmd)}")
|
||||
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]:
|
||||
if not texts:
|
||||
return []
|
||||
|
||||
|
||||
ensure_model()
|
||||
|
||||
|
||||
clean = [(t or "").strip() for t in texts]
|
||||
if all(not t for t in clean):
|
||||
return ["" for _ in clean]
|
||||
|
||||
|
||||
src_code = LANG_CODE_MAP.get(src, f"{src}_Latn")
|
||||
tgt_code = LANG_CODE_MAP.get(tgt, "spa_Latn")
|
||||
|
||||
|
||||
try:
|
||||
_tokenizer.src_lang = src_code
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
sources = []
|
||||
for t in clean:
|
||||
if t:
|
||||
|
|
@ -158,18 +238,18 @@ def translate_texts(src: str, tgt: str, texts: List[str]) -> List[str]:
|
|||
sources.append(tokens)
|
||||
else:
|
||||
sources.append([])
|
||||
|
||||
|
||||
target_prefix = [[tgt_code]] * len(sources)
|
||||
|
||||
|
||||
results = _translator.translate_batch(
|
||||
sources,
|
||||
target_prefix=target_prefix,
|
||||
beam_size=2,
|
||||
beam_size=1,
|
||||
max_decoding_length=MAX_NEW_TOKENS,
|
||||
repetition_penalty=2.0,
|
||||
no_repeat_ngram_size=3,
|
||||
repetition_penalty=1.2,
|
||||
no_repeat_ngram_size=2,
|
||||
)
|
||||
|
||||
|
||||
translated = []
|
||||
for result in results:
|
||||
try:
|
||||
|
|
@ -197,18 +277,19 @@ def translate_texts(src: str, tgt: str, texts: List[str]) -> List[str]:
|
|||
except Exception as e:
|
||||
LOG.error(f"Error processing result: {e}")
|
||||
translated.append("")
|
||||
|
||||
|
||||
return translated
|
||||
|
||||
|
||||
def split_body_into_chunks(text: str) -> List[str]:
|
||||
text = (text or "").strip()
|
||||
if len(text) <= BODY_CHARS_CHUNK:
|
||||
return [text] if text else []
|
||||
|
||||
parts = re.split(r'(\n\n+|(?<=[\.\!\?؛؟。])\s+)', text)
|
||||
|
||||
parts = re.split(r"(\n\n+|(?<=[\.\!\?؛؟。])\s+)", text)
|
||||
chunks = []
|
||||
current = ""
|
||||
|
||||
|
||||
for part in parts:
|
||||
if not part:
|
||||
continue
|
||||
|
|
@ -220,31 +301,34 @@ def split_body_into_chunks(text: str) -> List[str]:
|
|||
current = part
|
||||
if current.strip():
|
||||
chunks.append(current.strip())
|
||||
|
||||
|
||||
return chunks if chunks else [text]
|
||||
|
||||
|
||||
def translate_body_long(src: str, tgt: str, body: str) -> str:
|
||||
body = (body or "").strip()
|
||||
if not body:
|
||||
return ""
|
||||
|
||||
|
||||
chunks = split_body_into_chunks(body)
|
||||
if len(chunks) == 1:
|
||||
return translate_texts(src, tgt, [body])[0]
|
||||
|
||||
|
||||
translated_chunks = []
|
||||
for ch in chunks:
|
||||
tr = translate_texts(src, tgt, [ch])[0]
|
||||
translated_chunks.append(tr)
|
||||
|
||||
|
||||
return " ".join(translated_chunks)
|
||||
|
||||
|
||||
def normalize_lang(lang: Optional[str], default: str = "es") -> Optional[str]:
|
||||
if not lang:
|
||||
return default
|
||||
lang = lang.strip().lower()[:2]
|
||||
return lang if lang else default
|
||||
|
||||
|
||||
def detect_lang(text: str) -> str:
|
||||
if not text or len(text) < 10:
|
||||
return "en"
|
||||
|
|
@ -253,63 +337,75 @@ def detect_lang(text: str) -> str:
|
|||
except Exception:
|
||||
return "en"
|
||||
|
||||
|
||||
def process_batch(conn, rows):
|
||||
todo = []
|
||||
|
||||
|
||||
for r in rows:
|
||||
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()
|
||||
resumen = (r.get("resumen") or "").strip()
|
||||
|
||||
|
||||
if lang_from == lang_to:
|
||||
# Mark as done and copy original text if languages match
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE traducciones
|
||||
SET titulo_trad = %s, resumen_trad = %s, status = 'done'
|
||||
WHERE id = %s
|
||||
""", (titulo, resumen, r.get("tr_id")))
|
||||
""",
|
||||
(titulo, resumen, r.get("tr_id")),
|
||||
)
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
continue
|
||||
|
||||
todo.append({
|
||||
"tr_id": r.get("tr_id"),
|
||||
"lang_from": lang_from,
|
||||
"lang_to": lang_to,
|
||||
"titulo": titulo,
|
||||
"resumen": resumen,
|
||||
})
|
||||
|
||||
|
||||
todo.append(
|
||||
{
|
||||
"tr_id": r.get("tr_id"),
|
||||
"lang_from": lang_from,
|
||||
"lang_to": lang_to,
|
||||
"titulo": titulo,
|
||||
"resumen": resumen,
|
||||
}
|
||||
)
|
||||
|
||||
if not todo:
|
||||
return
|
||||
|
||||
|
||||
# 1. FAST LOCKING: Commit locked_at immediately to inform other workers
|
||||
cursor = conn.cursor()
|
||||
tr_ids = [item["tr_id"] for item in todo]
|
||||
cursor.execute(f"""
|
||||
cursor.execute(
|
||||
f"""
|
||||
UPDATE traducciones
|
||||
SET locked_at = NOW()
|
||||
WHERE id = ANY(ARRAY[{','.join(['%s'] * len(tr_ids))}])
|
||||
""", tr_ids)
|
||||
WHERE id = ANY(ARRAY[{",".join(["%s"] * len(tr_ids))}])
|
||||
""",
|
||||
tr_ids,
|
||||
)
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
groups = defaultdict(list)
|
||||
for item in todo:
|
||||
key = (item["lang_from"], item["lang_to"])
|
||||
groups[key].append(item)
|
||||
|
||||
|
||||
for (lang_from, lang_to), items in groups.items():
|
||||
LOG.info(f"Translating {lang_from} -> {lang_to} ({len(items)} items)")
|
||||
|
||||
|
||||
try:
|
||||
titles = [i["titulo"] for i in items]
|
||||
translated_titles = translate_texts(lang_from, lang_to, titles)
|
||||
|
||||
|
||||
for item, tt in zip(items, translated_titles):
|
||||
body = (item["resumen"] or "").strip()
|
||||
tb = ""
|
||||
|
|
@ -319,52 +415,61 @@ def process_batch(conn, rows):
|
|||
except Exception as e:
|
||||
LOG.error(f"Body translation error for ID {item['tr_id']}: {e}")
|
||||
tb = item["resumen"]
|
||||
|
||||
|
||||
tt = clean_text((tt or "").strip())
|
||||
tb = clean_text((tb or "").strip())
|
||||
|
||||
|
||||
if not tt:
|
||||
tt = item["titulo"]
|
||||
if not tb:
|
||||
tb = item["resumen"]
|
||||
|
||||
|
||||
# 2. INDIVIDUAL COMMIT: Save each item as it's done
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE traducciones
|
||||
SET titulo_trad = %s, resumen_trad = %s, status = 'done', locked_at = NULL
|
||||
WHERE id = %s
|
||||
""", (tt, tb, item["tr_id"]))
|
||||
""",
|
||||
(tt, tb, item["tr_id"]),
|
||||
)
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
except Exception as e:
|
||||
LOG.error(f"Update error for ID {item['tr_id']}: {e}")
|
||||
conn.rollback()
|
||||
|
||||
|
||||
LOG.info(f"Finished group {lang_from} -> {lang_to}")
|
||||
|
||||
|
||||
except Exception as 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
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE traducciones SET status = 'error', locked_at = NULL
|
||||
WHERE id = ANY(ARRAY[{','.join(['%s'] * len(items))}])
|
||||
""", [i["tr_id"] for i in items])
|
||||
""",
|
||||
[i["tr_id"] for i in items],
|
||||
)
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
except:
|
||||
conn.rollback()
|
||||
|
||||
|
||||
def fetch_pending_translations(conn):
|
||||
cursor = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
|
||||
|
||||
worker_id = os.environ.get("HOSTNAME", f"worker-{os.getpid()}")
|
||||
|
||||
total_found = 0
|
||||
|
||||
for lang in TARGET_LANGS:
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT t.id as tr_id, t.lang_from, t.lang_to,
|
||||
n.titulo, n.resumen, n.id as noticia_id
|
||||
FROM traducciones t
|
||||
|
|
@ -375,31 +480,45 @@ def fetch_pending_translations(conn):
|
|||
ORDER BY n.fecha DESC
|
||||
LIMIT %s
|
||||
FOR UPDATE SKIP LOCKED
|
||||
""", (lang, BATCH_SIZE))
|
||||
|
||||
""",
|
||||
(lang, BATCH_SIZE),
|
||||
)
|
||||
|
||||
rows = cursor.fetchall()
|
||||
if rows:
|
||||
LOG.info(f"Found {len(rows)} pending translations for {lang}")
|
||||
process_batch(conn, rows)
|
||||
|
||||
total_found += len(rows)
|
||||
|
||||
cursor.close()
|
||||
return total_found
|
||||
|
||||
|
||||
def connect_db():
|
||||
return psycopg2.connect(**DB_CONFIG)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
while True:
|
||||
try:
|
||||
conn = connect_db()
|
||||
fetch_pending_translations(conn)
|
||||
total = fetch_pending_translations(conn)
|
||||
conn.close()
|
||||
|
||||
if total == 0:
|
||||
LOG.info("No pending translations, sleeping...")
|
||||
else:
|
||||
LOG.info(f"Processed {total} translations, sleeping...")
|
||||
except Exception as e:
|
||||
LOG.error(f"Error: {e}")
|
||||
|
||||
|
||||
time.sleep(30)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue