mejora en el modelo de traduccion

This commit is contained in:
jlimolina 2026-04-04 03:17:37 +02:00
parent be56de7dd4
commit ecd7a3cdf8
21 changed files with 3708 additions and 322 deletions

39
.docker-compose-hooks.sh Executable file
View 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
View 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

View 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

View 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

View 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

View 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

View 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

View 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
View file

@ -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
View 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
View file

@ -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
View file

@ -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
View 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
View 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

View file

@ -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())
}

View file

@ -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
View 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:

View file

@ -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
View 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
View 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

View file

@ -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('&nbsp;', ' ')
text = text.replace('&amp;', '&')
text = text.replace('&lt;', '<')
text = text.replace('&gt;', '>')
text = text.replace('&quot;', '"')
text = re.sub(r'\s+', ' ', text)
text = re.sub(r"<[^>]+>", "", text)
text = text.replace("<unk>", "")
text = text.replace("&nbsp;", " ")
text = text.replace("&amp;", "&")
text = text.replace("&lt;", "<")
text = text.replace("&gt;", ">")
text = text.replace("&quot;", '"')
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()