docs: reescribir README, documentacion tecnica y diagramas

El README describia un despliegue que ya no existe: rama production, Flask
API, OBSEI, tensorflow 2.17 y numpy 1.19. Reescrito con la instalacion real,
las cifras actuales y como levantarlo como servicio.

Dos diagramas nuevos en INFO/DOCS/diagramas/, con el script que los genera al
lado para poder reeditarlos:

  flujos-de-informacion.svg  como cuatro agencias abastecen a casi toda la
                             prensa, y el circuito corto de la independiente
  arquitectura.svg           de la fuente al grafo

Se retira documentacion obsoleta o peligrosa: notas de planificacion fechadas,
un volcado de consola con una credencial en claro, un fichero con comandos
sshpass contra un host identificable, y un LEEME que describia un proyecto
Django que ya no existe. El directorio DOCS/ era copia byte a byte de
INFO/DOCS/.

Del documento de rediseno se extrae la parte tecnica a
ARQUITECTURA_EMBEDDINGS.md y se descarta el resto.

SEGURIDAD.md pasa de lista de agujeros abiertos a registro de las mitigaciones
aplicadas: todas estan corregidas y verificadas en el codigo actual. Corrige
ademas una afirmacion que ya era falsa, que el backend escuchaba en 0.0.0.0.

El dominio pasa a upleaks.org y las rutas de despliegue de los documentos se
sustituyen por $FLUJOS.
This commit is contained in:
hacklab 2026-08-23 17:12:18 +02:00
parent e2b1f27699
commit 4265909b55
19 changed files with 927 additions and 371 deletions

View file

@ -1,61 +0,0 @@
┌───────────────────┐
│ HTML + CSS UI │
│ │
│ - contenedor div │
│ #globWarContainer│
│ - form #paramForm │
└─────────┬─────────┘
┌─────────────────────────────────────────────────────────────┐
│ output_glob_war.js │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 1. Inicialización del gráfico │ │
│ │ └─ ForceGraph3D()(elem) configuras estilos, forces │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 2. getData(paramsObj) │ │
│ │ ├─ Construye URL: '/api/data?tema=guerra global…' │ │
│ │ ├─ fetch(url) → JSON { nodes: [...], links: [...] }│ │
│ │ ├─ Filtra enlaces inválidos (fuente/target existe)│ │
│ │ ├─ Calcula grado de cada nodo │ │
│ │ ├─ Filtra nodos con grado < MIN_DEGREE │ │
│ │ └─ Devuelve { nodes, links } │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌────────────────────────────────┐ │
│ │ │ 3. graph.graphData(data) │ │
│ │ │ Renderiza nodos + enlaces │ │
│ │ └────────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────┐ ┌────────────────────────┐ │
│ │ showNodeContent(content) │ │ centerGraph() │ │
│ │ └─ despliega content │ │ └─ zoomToFit + padding │ │
│ └──────────────────────────────┘ └────────────────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 4. Event Listeners │ │
│ │ - form submit → llama getData(paramsObj) │ │
│ │ - window resize → llama centerGraph() │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ FLUJOS_APP.js │
│ ┌ getDataHandler(req, res) ┐ │
│ │ - recibe paramsQuery │ │
│ │ - consulta Mongo: │ │
│ │ • filtra por tema │ │
│ │ • aplica límites nodos/complejidad/fechas │
│ │ - construye { nodes, links } JSON │
│ └───────────────────────────┘ │
│ │ │
│ ▼ │
│ HTTP GET /api/data → getDataHandler → responde JSON │
└─────────────────────────────────────────────────────────────┘

View file

@ -1,32 +0,0 @@
┌──────────────────────────┐
│ Cliente JS (output_*.js) │
│ getData({tema, …}) │
└────────────┬─────────────┘
│ HTTP GET /api/data?tema=…&nodos=…&minSim=…
┌─────────────────────────────────────────┐
│ FLUJOS_APP.js Express /api/data │
│ │
│ 1) Parsea req.query: tema, nodos, minSim│
│ 2) nodesQuery = { tema, subtema?, … } │
│ 3) .find(nodesQuery).limit(nodos) → │
│ wikipediaNodes, noticiasNodes, │
│ torrentsNodes │
│ 4) Combina y formatea a `formattedNodes`│
│ 5) nodeIds = formattedNodes.map(id) │
│ 6) linksQuery = { │
│ noticia1: { $in: nodeIds }, │
│ noticia2: { $in: nodeIds }, │
│ porcentaje_similitud: { $gte: min }│
│ } │
│ 7) comparacionesCollection.find(linksQuery) → rawLinks │
│ 8) Formatea rawLinks a `{ source, target, value }` │
│ 9) Responde `{ nodes: formattedNodes, links: formattedLinks }` │
└────────────┬────────────────────────────┘
┌───────────────────────────┐
│ Cliente JS recibe JSON │
│ Filtra nodos sin conexiones│
│ Dibuja ForceGraph3D │
└───────────────────────────┘

View file

@ -1,27 +0,0 @@
proyecto/
|
|-- .gitignore # Archivos y directorios ignorados por git
|-- package-lock.json # Bloquea las versiones de las dependencias
|-- FLUJOS_APP.js # Punto de entrada de tu aplicación
|
|-- DOCS/ # Documentación del proyecto
|
|-- BACK_BACK/ # Contiene elementos relacionados con el backend
| |-- entorno/ # Configuraciones del entorno o archivos relacionados
| |-- requirements/ # Dependencias y requerimientos del proyecto
| |-- OBSEI/ # (No utilizado, considerar eliminar si no es necesario)
|
|-- VISUALIZACION/ # Código relacionado con la parte visual o frontend
| |-- node_modules/ # Módulos de Node.js
| |-- package.json # Dependencias del frontend y scripts
| |-- package-lock.json # Bloquea las versiones de las dependencias del frontend
| |-- public/ # Archivos estáticos (HTML, CSS, JS, imágenes)
| | |-- images/ # Imágenes
| | |-- ... # Otros archivos estáticos (html, css, js)
| |-- controllers/ # Controladores para la lógica del negocio
| |-- models/ # Modelos o esquemas de la base de datos
| |-- graphql/ # Definiciones para GraphQL (si se usa)
| |-- routes/ # Rutas de la aplicación
|
|-- .gitignore # Archivos y directorios ignorados por git
`-- package-lock.json # Bloquea las versiones de las dependencias

View file

View file

@ -0,0 +1,180 @@
# Arquitectura de embeddings y correlación semántica
Cómo se representan los documentos como vectores y cómo se derivan de ahí las aristas
noticia↔leak del grafo. Reemplaza el solape de palabras del pipeline original.
Implementación: `coconews/workers/backfill_embeddings.py` (generación) y
`FLUJOS_DATOS/COMPARACIONES/motor_comparacion.py` (correlación).
---
### 2.4 Esquema canónico unificado (aditivo, no destructivo)
Una sola forma para todos los nodos (`noticias`, `torrents`, `wikipedia`, `imagenes`, `imagenes_wiki`):
```
{
_id,
archivo : string // UNIQUE. Clave de dedup y FK hacia comparaciones.
source_type : string // 'rss' | 'leak' | 'wikipedia' | 'imagen' | 'legacy_news'
tema, subtema : string
texto : string // ver 2.5 sobre la realidad del texto de leaks
fecha : ISODate|null
// --- bloque embedding (idéntico en todas las colecciones) ---
embedding : double[384] | null // SIEMPRE L2-normalizado en escritura (ver 4.x)
embedding_model : 'paraphrase-multilingual-MiniLM-L12-v2'
embedding_dim : 384
procesado_embedding: bool
apto_embedding : bool // NUEVO: gate de calidad (ver 4.x). false = no entra al índice
// --- solo docs largos troceados (rss / leak / wiki) ---
chunks : [ { idx, texto, embedding:double[384] } ]
// --- solo rss ---
url, titulo_original, lang, traducido, procesado_ner, entidades, fuente
}
```
**Nota de almacenamiento (corrección al borrador):** MongoDB BSON **no tiene tipo float de 32 bits** — todo número de array se guarda como `double` de 64 bits, escribas `np.float32` o no. Por tanto **guardar float32 NO ahorra disco en Mongo** (el ahorro es real solo en la RAM del índice FAISS al castear a `np.float32` antes de construirlo). El campo `embedding` ocupa 384·8 = **3 KB/doc en disco**; ver el cómputo de huella total en 2.7.
### 2.5 Realidad del texto de los leaks (bloqueante de calidad)
**El borrador asumía "texto completo sin limpiar"; eso es imposible para leaks.** El único texto de leak en disco es `txt/*_limpio.txt`, producido por `procesar_torrents.py::limpiar_texto`, que **pasa a minúsculas, elimina TODA la puntuación y quita stopwords ESPAÑOLAS sobre texto mayormente INGLÉS**. El directorio `files/` (texto crudo) está **vacío (0 ficheros)**. Embeber esa sopa de tokens des-puntuada y con acentos rotos (`proteccién`) da **peores** vectores que prosa limpia, porque los sentence-transformers dependen de puntuación y palabras función.
Decisión (requiere confirmación, ver §5-R1): **re-extraer el texto crudo desde los `.torrent`/ZIP originales** para `texto` (y rehacer chunking sobre prosa real), o **aceptar embeddings degradados** sobre `*_limpio.txt`. Recomendación: re-extraer, porque la calidad del matcher depende directamente de esto, pero acotar a un subconjunto curado primero (ver §5-R1).
### 2.6 Clave de dedup, namespaces y watermark
- **Dedup key = `archivo`** con índice único en **todas** las colecciones de nodo. Hoy falta el único en `torrents` (`pipeline_maestro.py::setup_indices`, líneas ~80-91, solo lo crea para wikipedia/noticias/imagenes/imagenes_wiki). **Extender `setup_indices` para incluir `torrents`.**
- **Normalización de namespaces — con corrección de viabilidad:**
- rss: el ingestor **hoy escribe `archivo = md5(link).hexdigest() + '.txt'`** (CON `.txt`, SIN prefijo `rss_`; el borrador lo decía mal). La migración debe **renombrar TODOS los `archivo` de `noticias` también**`rss_<md5>` y reescribir sus edges. Esto **no es "aditivo y seguro"**: es un rename global de la clave de dedup en la colección canónica (ver 2.7, switch atómico).
- leak: **el re-key a `leak_<sha1(ruta_original)>` es inviable**`procesar_torrents.py` **nunca escribe en Mongo** (no hay `MongoClient`), los torrents los inserta `pipeline_completo.py::procesar_documentos` con `archivo = basename(<relpath_saneado>_limpio.txt)`, y `limpiar_nombre_archivo` mapea `[\/*?:"<>|]→_` de forma **lossy**: la ruta original **no es recuperable** del nombre guardado. Decisión: **conservar el `basename` actual como clave**, prefijándolo a **`leak_<basename_sin_extension>`** (rename puramente textual sobre lo que ya existe, sin necesidad de la ruta original). Las colisiones reales de `procesar_torrents.py` (el `txt/` plano donde `<name>_limpio.txt` se pisa) se arreglan **en la re-extracción** (§2.5), no en la migración de claves.
- wiki: `wiki_<basename>`.
- **Watermark:**
- **Por documento:** `procesado_embedding=false` = pendiente de embeber (reanudable, idempotente, ya existe).
- **Por corrida:** `pipeline_log` (`fase`, `estado`, `fin`). **Corrección sobre `--desde`:** `pipeline_maestro` ya lo pasa (líneas 341-346) y `pipeline_completo.py` lo parsea (línea 219) y **lo ignora**. Pero honrarlo **no es un fix barato**: la comparación actual enumera **directorios de disco** (`os.listdir` de `.../tokenized`), no tiene `fecha` de Mongo en tiempo de comparación. **`--desde` solo se puede honrar de verdad DESPUÉS de reescribir el motor para sacar candidatos de Mongo (F5)** — está contabilizado dentro de F5, no como tarea suelta.
### 2.7 Migración sin dejar el grafo a oscuras (switch atómico)
El invariante load-bearing es **`comparaciones.noticia1/noticia2 == nodo.archivo`** (`FLUJOS_APP.js` construye `nodeIds` desde `result.archivo.trim()` y casa links por `noticiaN.trim()`). Renombrar `archivo` (incluido el de coconews: `md5+.txt → rss_<md5>`) **mientras** el productor legacy aún escribe `basename` rompe el grafo en cualquier estado intermedio (cero links → grafo vacío). Reglas:
1. **El rename de `archivo` y la reescritura de `comparaciones.noticia1/2` son UN solo switch atómico** (script único, idealmente regenerando `comparaciones` desde cero en la regeneración con `motor_comparacion.py` en vez de reescribir 52M docs in-place — ver §5-R4).
2. **Canonicalización de orden de pares ANTES del índice único:** almacenar siempre `noticia1 = min(a,b)`, `noticia2 = max(a,b)` (lexicográfico). Sin esto, el build del único `{noticia1,noticia2}` **falla** sobre los pares invertidos `(A,B)/(B,A)` que ya existen, y los re-runs duplican.
3. **Plan de rollback / "el grafo sigue vivo":** la regeneración de `comparaciones` (la regeneración con `motor_comparacion.py`) se hace en una colección nueva `comparaciones_v2` y se promueve con `renameCollection` (atómico, sub-segundo) solo cuando está poblada y con índices; si algo falla se descarta `comparaciones_v2` y el front sigue sirviendo la vieja. Ver coste/ventana en §4.
### 2.8 Pipeline de sync idempotente
Todo con **upsert keyed-by-`archivo`** (`update_one(..., upsert=True)`), nunca `insert_one`:
```
ingestor (rss) → upsert por archivo, swallow DuplicateKeyError [YA ASÍ]
procesar_documentos → cambiar insert_one → update_one upsert (leak/wiki)
embedder (generaliz.) → find({procesado_embedding:false, apto_embedding:true}) por colección → $set embedding
```
`pipeline_mongolo.py` (`insert_one`, no idempotente) **se retira** y, crítico, **hay que encontrar y desactivar quien lo relanzó hoy a las 00:53** (cron/timer/parent vivo) — matar los hijos con SIGKILL no basta si el padre sigue respawneando.
## 3. COMPARACIÓN con embeddings
### 3.1 Qué se reemplaza exactamente
`comparar_archivos`/`contar_palabras` (`pipeline_completo.py:76-99`): lee ficheros tokenizados de disco, `Counter()` de palabras, `porcentaje = sum(min(c1,c2) sobre comunes) / (len1+len2) * 100` sobre tokens **crudos incluyendo stopwords**, sin TF-IDF, sin poda → cruz completa O(n·m) (~41k noticias × ~36k torrents) → 52M docs en `comparaciones`, con docs idénticos topando al ~50%. Se reemplaza por **embedding por documento (una vez) + coseno top-k vía FAISS**.
### 3.2 Modelo, vectores y normalización
- **Modelo:** `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2`, 384-dim, cacheado por Hugging Face en `~/.cache/huggingface` (~458 MB), CPU, multilingüe.
- **Realidad cross-lingual (clave):** `translator.py:26` traduce **todas las noticias a ESPAÑOL** (`target='es'`); los **leaks NO se traducen**. La comparación real es **prosa-española-noticia vs sopa-de-keywords-inglesa-leak**. El coseno cross-lingual "mismo evento" de MiniLM suele caer en **0.350.5**, justo sobre el umbral propuesto. Por eso el umbral **se calibra con medición real, no por intuición** (§3.6) y se considera **umbral por par de idiomas** (mismo-idioma corre más alto que cross-idioma).
- **L2-normalización explícita (no hand-waved):** `encode()` de sentence-transformers 5.4.1 tiene `normalize_embeddings=False` por defecto, y `embedder.py:40` guarda `emb.tolist()` **crudo**. Para que `porcentaje_similitud = coseno*100` sea coseno, **tanto los vectores del índice como los de query deben estar normalizados**. Regla: **normalizar en escritura** (`emb = emb / np.linalg.norm(emb)` antes del `$set`) **y** re-normalizar defensivamente al cargar en FAISS. Así el inner product de `IndexFlatIP` == coseno y el umbral tiene sentido.
- **Chunking por tokens, no por chars (corrección):** hoy `texto[:512]` son **512 caracteres** (~80 palabras), no tokens — se pierde casi todo. Cambiar a **ventanas de ~256 tokens, solape ~32**, embeber cada chunk → `chunks[]`. **Cap de chunks content-aware, NO truncado duro:** el cap de "máx 20 chunks" del borrador descarta el 99.5% de un leak de 22 MB. En su lugar: si un doc excede el cap, **muestrear/representar todo el doc** (p.ej. embeber todos los chunks pero, para el índice, un cap por muestreo uniforme + el centroide), nunca quedarse con los primeros 20.
- **Gate de calidad `apto_embedding` (antídoto de falsos positivos):** muchos "leaks" no son prosa comparable — firmware/keys de calculadora (`voyage 200 freeware flash application signing key n5322...`), blobs hex, y en el lado noticias hay basura tipo `$100_OFF___TUSHY_Discount_Codes`. Un modelo de 384-dim mapea docs cortos de solo-keywords/hex a una región degenerada donde los cosenos son espuriamente altos. **Antes de embeber:** filtrar por nº mínimo de tokens reales, detectar y saltar blobs hex/key/code, detectar idioma. `apto_embedding=false` → no entra al índice ni forma edges.
### 3.3 Motor ANN: **FAISS (faiss-cpu), `IndexFlatIP`** — decisión final
| Opción | Veredicto |
|---|---|
| `$vectorSearch` (Atlas) | ❌ Imposible: Atlas-only; aquí Community self-hosted. |
| pgvector | ❌ Introduce Postgres, ausente del stack. Coste operativo injustificado. |
| Qdrant | ⚠️ Sobredimensionado: servicio extra, REST. Y un daemon más que puede crash-loopear (justo lo que estamos apagando). |
| hnswlib | ⚠️ Segunda opción: HNSW en un fichero, pip puro, persistente. Solo si un perfil real exige ANN aproximado. |
| brute-force numpy | ⚠️ Correcto pero innecesario: `IndexFlatIP` hace lo mismo (coseno exacto) más rápido y limpio. |
| **FAISS `IndexFlatIP`** | ✅ **Elegido.** |
**Justificación por volumen — corregida (el borrador omitía Wikipedia y subestimaba ~2×):**
| Corpus | Ficheros | Vectores (chunking, cap content-aware) |
|---|---|---|
| noticias (rss) | ~20k docs | ~20k+ (mayoría 1 chunk) |
| torrents (leaks) | 36.183 ficheros (hoy ~500 en Mongo) | ~288k chunks |
| wikipedia | 28.427 ficheros no vacíos | ~207k chunks |
| **Total (techo realista)** | | **~515k vectores** |
- **RAM del índice:** 515k × 384 × 4 B (float32 en FAISS) ≈ **791 MB** → trivial en 62 GB.
- **Coste de búsqueda:** ~20k queries (noticias) × ~495k corpus vía BLAS multinúcleo ≈ **1.36.3 min** por rebuild completo (medición teórica; confirmar con benchmark real en el box, §4-F0b).
- **Coseno exacto** sobre vectores L2-normalizados → sin pérdida de recall.
- **Cero daemons nuevos:** el índice se construye en-proceso dentro del job de comparación. Para crecimiento futuro a millones, `IndexFlatIP → IndexHNSWFlat` es un cambio de una línea.
- **Persistencia/staleness (gap del borrador):** el flat index **no es incrementalmente append-and-search-only-new** sin bookkeeping. Decisión explícita: **rebuild completo cada corrida** (es sub-minuto a pocos minutos, barato). Para sync incremental real (`--desde`), el rebuild completo sigue siendo la vía; si en el futuro las noticias crecen mucho (eje temporal: 20k es un snapshot, no un techo — `coconews` ingesta indefinidamente; a 200k noticias el rebuild es ~1 h), se reevalúa HNSW persistente.
### 3.4 Generación de candidatos (mata el O(n·m)) — con énfasis corregido
**El verdadero lever de escalado es `top-k`, no el bloqueo por tema.** El borrador presentaba el bloqueo por `tema` como mecanismo principal, pero `asignar_tema_y_subtema` hace matching de keywords **en español** sobre leaks **en inglés** → casi todos caen en `('otros','general')`, así que el bloqueo **colapsa a un índice global** justo para el corpus que domina el conteo de vectores. Como el global es solo ~6 min, es viable. Plan:
1. **Índice global ANN** (no por tema) como base, dado que el bloqueo por tema no es fiable para leaks. Opcionalmente, **clasificar leaks por embedding** (vecino más cercano a centroides de tema) para recuperar el bloqueo como optimización, no como dependencia (§5-R3).
2. **`top-k` por noticia**, con `k` derivado del cap real del front (ver 3.5).
3. Solo esos vecinos se escriben como edges. **Adiós a los 52M de pares casi-cero.**
### 3.5 Reducción chunk→edge y presupuesto de `k` (gap central resuelto)
**Reducción chunk→doc (el borrador la dejaba ambigua, era un hueco de corrección):** el índice se construye con **vectores a nivel de chunk** (recall local: "¿algún fragmento del leak menciona este evento?"). La búsqueda devuelve top-k **chunks**; se **colapsan a nivel de doc** así:
- `score_doc(noticia, leak) = max sobre chunks` del coseno, **pero** con un **guard anti-inflación**: para leaks muy largos (muchos chunks) el `max` casi siempre encuentra un chunk espurio ≥ umbral. Mitigación: exigir **al menos `m≥2` chunks distintos del leak** por encima de un umbral secundario, **o** combinar `max` con el coseno del **centroide del doc** (`score = 0.5·max_chunk + 0.5·centroide`) para penalizar el "un solo chunk casual". Se calibra junto al umbral (§3.6).
- Se escribe **un único edge por par `(noticia_archivo, leak_archivo)`** con el `score_doc` resultante → **no se reinfla `comparaciones`** ni colisionan múltiples chunk-hits sobre la clave `(noticia1,noticia2)`.
**Presupuesto de `k` respetando el cap real del front (corrección):** `MAX_LINKS_PER_NODE=8` en `FLUJOS_APP.js` **no** es "8 leaks por noticia": el front (líneas 228-241) **ordena TODOS los candidatos globalmente por `porcentaje_similitud` y hace asignación greedy capando el grado de AMBOS extremos a 8, cruzando TODOS los corpus** (noticias+wiki+torrents+imágenes). Implicaciones:
- Un leak popular que es top-8 de 50 noticias será **descartado** para 42 de ellas por el cap del lado-leak.
- Una noticia que ya enlaza a wiki/imágenes ve sus edges de leak **desplazados**.
Por tanto, **generar top-8-por-noticia no produce 8 edges visibles**. Plan: **generar `k` algo mayor (p.ej. `k=1520` por noticia) como sobre-aprovisionamiento**, dejar que la asignación greedy simétrica cross-corpus del front recorte, y **medir** cuántos news↔leak edges sobreviven realmente al render para ajustar `k`. El cap no es una excusa para under-generar.
### 3.6 Umbral, calibración y evaluación (requisito previo, NO opcional)
- **No se compromete 0.45 a ciegas.** Antes de regenerar el grafo: **etiquetar a mano ~50 pares news↔leak conocidos** (y específicamente un subconjunto **cross-lingual** español↔inglés), generar el **histograma de coseno** sobre una muestra real, y elegir el umbral donde precision/recall sea aceptable. Es esperable un **umbral por par de idiomas** (same-language más alto que cross-language) para no infra-enlazar justo los pares español-noticia↔inglés-leak que son el objetivo del proyecto.
- **Mapeo al contrato:** `porcentaje_similitud = round(score_doc * 100, 2)`.
- **Recalibración del slider `complejidad` es OBLIGATORIA, no opcional (corrección):** el default `20`, el rango `1-40` y el `IMG_UMBRAL>=3` se ajustaron al overlap viejo (que topaba al ~50% e incluía stopwords). La distribución de `coseno·100` es **completamente distinta** → reusar esos números es una **regresión visual garantizada**. Tras ver la distribución real hay que re-tunear defaults del front (cambio mínimo en `FLUJOS_APP.js`, sí toca front — único toque necesario).
### 3.7 Documento de edge
```jsonc
{
noticia1: "leak_9f3c...", // = min(archivo_a, archivo_b) (canonicalizado, §2.7)
noticia2: "rss_ab12...", // = max(archivo_a, archivo_b)
porcentaje_similitud: 57.3, // = score_doc*100
// --- aditivos, ignorados por el front, útiles para auditoría ---
metodo: "embedding",
cosine: 0.573,
agregacion: "max_chunk+centroide",
modelo: "paraphrase-multilingual-MiniLM-L12-v2"
}
```
`FLUJOS_APP.js` lee solo `noticia1/noticia2/porcentaje_similitud` (+ rama `source1_type='imagen'`, intacta) → **cero cambios en el front salvo la recalibración del slider de §3.6**.
### 3.8 Throughput del backfill (el verdadero riesgo de escala — corregido a fondo)
> **Medido sobre texto real de leaks** (sentence-transformers 5.4.1, CPU de 24 cores con AVX2, sin GPU): **~59-66 chunks/s** (BATCH 20≈128: torch ya satura los cores por llamada). **Encode de 515k chunks ≈ 2.2 h**, NO 40-60 h. Las 40-60 h eran el wall-clock del loop ACTUAL (`sleep(2)`×~25k batches ≈14 h + 515k `update_one`). Quitando `sleep(2)` + `bulk_write` → backfill total **~2.5-3 h**. `normalize_embeddings=True` da vectores unitarios dim=384 (L2-norm = un flag). **GPU no es el cuello de botella.**
El borrador llamaba al backfill "un job nocturno, puede tardar horas". **Con la cadencia ACTUAL del embedder eso está mal por un orden de magnitud:** `BATCH=20` + `model.encode()` + **`time.sleep(2)` tras cada batch no vacío** + **`update_one` por documento** (no `bulk_write`). Backfillear ~515k chunks así = **4060 HORAS** (solo el `sleep(2)` sobre ~25.757 batches son ~14 h de puro sleep; los 515k round-trips individuales añaden más). **La pregunta de GPU (§5-R7) es un distractor: el cuello de botella es la estructura del loop, no la falta de GPU.**
**Modo backfill explícito (separado del daemon trickle):**
1. **`BATCH ≥ 128`** (o mayor según RAM/CPU).
2. **Eliminar `sleep(2)`** en modo backfill (el sleep tiene sentido para el daemon incremental, no para una carga masiva).
3. **`bulk_write(ordered=False)`** en vez de `update_one` por doc — la mejora individual de mayor palanca.
4. **Medir chunks/s real en el box** (con/sin AVX-512) antes de comprometer un ETA — un único `model.encode` sobre un batch de chunks de 256 tokens convierte el ETA de adivinanza en número (§4-F0b).
### 3.9 Índices que faltan (bloqueante para que el front no haga full-scan)
`comparaciones` (52M docs) **no tiene índice en `noticia1`/`noticia2`** → la `linksQuery` con `$in` (`FLUJOS_APP.js:213-222`) es un **scan de colección**. Crear compuesto `{noticia1:1, noticia2:1}`. El **único** `{noticia1,noticia2}` solo **después** de canonicalizar el orden de pares (§2.7) — si no, el build falla sobre los invertidos.
### 3.10 Huella en disco y presión sobre WiredTiger (gap del borrador)
515k vectores × 384 × 8 B (double BSON) ≈ **1.6 GB** de arrays `embedding` añadidos a la base, **más** los `chunks[].embedding` inline duplicando vector por chunk dentro de cada doc (esto **infla el tamaño de cada doc** de `torrents`/`wikipedia` y por tanto el working-set). El caché WiredTiger por defecto es ~50% de 62 GB. **Acción:** monitorizar el crecimiento, y si los `chunks[]` inline pesan demasiado en el working-set, considerar una **colección separada `chunks_embeddings`** (un doc por chunk) en vez de array inline. Decisión a tomar con el número real tras F3.
---

View file

@ -218,6 +218,6 @@ La asignación la hace `pipeline_completo.py::asignar_tema_y_subtema()` buscando
```
MongoDB FLUJOS_DATOS
└── imagenes_wiki / imagenes
└── image_path → /var/www/theflows.net/flujos/FLUJOS_DATOS/IMAGENES/output/wiki_images/{tema_slug}/{archivo}
servido como → https://theflows.net/wiki-images/{tema_slug}/{archivo}
└── image_path → $FLUJOS/FLUJOS_DATOS/IMAGENES/output/wiki_images/{tema_slug}/{archivo}
servido como → https://upleaks.org/wiki-images/{tema_slug}/{archivo}
```

View file

@ -1,7 +1,7 @@
# Pipeline Maestro — Contexto técnico FLUJOS
**Fecha:** 2026-04-21
**Fecha:** 2026-04-24 (actualizado)
**Archivo:** `FLUJOS_DATOS/pipeline_maestro.py`
**Scheduler:** systemd timer (semanal, domingos 3:00 AM)
**Scheduler:** systemd timer `flujos-pipeline.timer` — cada 12H (`OnUnitActiveSec=12H`)
---
@ -11,9 +11,13 @@ El pipeline maestro orquesta las tres fases del sistema en orden estricto:
```
FASE 1 — SCRAPING
├── Wikipedia (main.py) → colección wikipedia
├── Noticias (main_noticias.py) → colección noticias
└── Imágenes Wikipedia (wikipedia_image_scraper.py) → colección imagenes_wiki
├── Wikipedia (main.py) → colección wikipedia
├── Noticias (main_noticias.py) → colección noticias
├── Imágenes Wikipedia (wikipedia_image_scraper.py) → colección imagenes_wiki
└── Comparaciones imagen-texto (comparar_imagenes_wiki.py) → colección comparaciones
[AÑADIDO 2026-04-24: TF-IDF entre imagenes_wiki y corpus texto,
necesario porque Qwen (fase 2) tarda demasiado y imagenes_wiki
queda sin enlaces en el grafo hasta que Qwen termine]
FASE 2 — ANÁLISIS
├── Qwen3-VL imágenes (pipeline_imagenes.py --analizar) → colección imagenes
@ -29,7 +33,7 @@ FASE 3 — COMPARACIÓN
```bash
# Ejecución completa (las 3 fases)
/var/www/theflows.net/flujos/FLUJOS_DATOS/myenv/bin/python3 pipeline_maestro.py
$FLUJOS/FLUJOS_DATOS/myenv/bin/python3 pipeline_maestro.py
# Solo una fase
python pipeline_maestro.py --solo-fase scraping
@ -141,11 +145,11 @@ Requires=mongod.service
[Service]
Type=oneshot
User=capitansito
WorkingDirectory=/var/www/theflows.net/flujos/FLUJOS_DATOS
WorkingDirectory=$FLUJOS/FLUJOS_DATOS
Environment=MONGO_URL=mongodb://localhost:27017
Environment=DB_NAME=FLUJOS_DATOS
ExecStart=/var/www/theflows.net/flujos/FLUJOS_DATOS/myenv/bin/python3 \
/var/www/theflows.net/flujos/FLUJOS_DATOS/pipeline_maestro.py
ExecStart=$FLUJOS/FLUJOS_DATOS/myenv/bin/python3 \
$FLUJOS/FLUJOS_DATOS/pipeline_maestro.py
TimeoutStartSec=43200
Nice=15
CPUQuota=400%
@ -175,7 +179,13 @@ Unit=flujos-pipeline.service
WantedBy=timers.target
```
Cambiado de semanal (`OnCalendar=Sun`) a cada 2 días (`OnUnitActiveSec=2d`) el 2026-04-22. `Persistent=true` hace que si el servidor estaba apagado cuando tocaba, el timer se dispara 2 minutos después del siguiente arranque.
Timer activo a 12H (`OnUnitActiveSec=12H`). `Persistent=true` hace que si el servidor estaba apagado cuando tocaba, el timer se dispara 2 minutos después del siguiente arranque.
**AVISO 2026-04-24:** El servicio falló por timeout (SIGTERM a las 12h) el 2026-04-22 porque la fase 2 (Qwen, 186 imágenes × ~3 min/imagen ≈ 9h) más la fase 1 (~2h) superaron `TimeoutStartSec=43200`. El timer quedó en estado `elapsed/n/a`. Para resetear manualmente:
```bash
sudo systemctl reset-failed flujos-pipeline.service
sudo systemctl start flujos-pipeline.timer
```
### Comandos de gestión
@ -237,7 +247,7 @@ journalctl -u flujos-pipeline.service --since "2026-04-20"
---
## Errores conocidos y fixes aplicados (2026-04-22)
## Errores conocidos y fixes aplicados
| Script | Error | Fix |
|---|---|---|
@ -248,7 +258,7 @@ journalctl -u flujos-pipeline.service --since "2026-04-20"
Todos los módulos se instalan dentro del entorno virtual:
```bash
/var/www/theflows.net/flujos/FLUJOS_DATOS/myenv/bin/python3 -m pip install <modulo>
$FLUJOS/FLUJOS_DATOS/myenv/bin/python3 -m pip install <modulo>
```
No usar `pip` directamente (el ejecutable `pip` del venv puede estar roto; usar `python3 -m pip`).
@ -278,7 +288,8 @@ pipeline_maestro.py
├── [FASE 1] fase_scraping()
│ ├── subprocess: WIKIPEDIA/main.py
│ ├── subprocess: NOTICIAS/main_noticias.py
│ └── subprocess: IMAGENES/wikipedia_image_scraper.py --flujos --max 20 --mongo
│ ├── subprocess: IMAGENES/wikipedia_image_scraper.py --flujos --max 20 --mongo
│ └── subprocess: IMAGENES/comparar_imagenes_wiki.py --umbral 3.0 ← AÑADIDO 2026-04-24
├── [FASE 2] fase_analisis()
│ ├── subprocess: IMAGENES/pipeline_imagenes.py --analizar --carpeta ... --mongo

View file

@ -25,7 +25,7 @@
Internet
nginx (theflows.net:443, TLS Let's Encrypt)
nginx (upleaks.org:443, TLS Let's Encrypt)
├── / → FLUJOS/VISUALIZACION/public/ (estáticos)
├── /api/ → proxy → Node.js :3000
└── /wiki-images/ → (Node.js sirve estáticos de IMAGENES/output/wiki_images/)
@ -63,7 +63,7 @@ PORT=3000
## Rutas críticas del servidor
```
/var/www/theflows.net/flujos/
$FLUJOS/
├── docs/ # esta documentación
├── FLUJOS/
│ ├── BACK_BACK/FLUJOS_APP.js # servidor Node.js

View file

@ -35,20 +35,21 @@ Descarga imágenes de Wikipedia por tema usando la **Wikimedia REST API**. Para
### Temas de FLUJOS que se scrapean (`TEMAS_FLUJOS`)
```python
# ACTUALIZADO 2026-04-24: cambiados a los 5 temas del frontend
# (antes eran 10 con nombres académicos que no coincidían con los temas del backend)
TEMAS_FLUJOS = [
"cambio climático",
"geopolítica conflictos",
"seguridad internacional espionaje",
"libertad de prensa periodismo",
"corporaciones poder económico",
"populismo extremismo",
"desinformación redes sociales",
"privacidad vigilancia masiva",
"biodiversidad medioambiente",
"inteligencia artificial tecnología",
"guerra global",
"inteligencia y seguridad",
"economía y corporaciones",
"demografía y sociedad",
]
```
> **Por qué cambió:** Los 10 temas originales ("geopolítica conflictos", "seguridad internacional espionaje", etc.) no coincidían con los temas validados en `FLUJOS_APP.js` (`TEMAS_VALIDOS`), por lo que las imágenes se descargaban pero nunca aparecían en el grafo porque la query `{tema: "inteligencia y seguridad"}` no encontraba documentos con `tema: "seguridad internacional espionaje"`.
> Las imágenes scrapeadas con los temas antiguos siguen en disco (`output/wiki_images/geopolítica_conflictos/`, etc.) pero solo las de los 5 temas nuevos están accesibles desde el frontend.
### Filtros de calidad (imágenes que se descartan)
```python
@ -103,7 +104,7 @@ python wikipedia_image_scraper.py --tema "climate change" --lang en --max 40
```json
{
"archivo": "cambio_climático_003.jpg",
"image_path": "/var/www/theflows.net/flujos/FLUJOS_DATOS/IMAGENES/output/wiki_images/cambio_climático/cambio_climático_003.jpg",
"image_path": "$FLUJOS/FLUJOS_DATOS/IMAGENES/output/wiki_images/cambio_climático/cambio_climático_003.jpg",
"image_url": "https://upload.wikimedia.org/wikipedia/commons/...",
"tema": "cambio climático",
"subtema": "derretimiento glaciar ártico",
@ -251,7 +252,7 @@ python-dotenv
Instalación en el venv:
```bash
/var/www/theflows.net/flujos/FLUJOS_DATOS/myenv/bin/python3 -m pip install -r requirements_imagenes.txt
$FLUJOS/FLUJOS_DATOS/myenv/bin/python3 -m pip install -r requirements_imagenes.txt
```
---
@ -263,9 +264,24 @@ Instalación en el venv:
- Imágenes analizadas con Qwen en `imagenes` (MongoDB): proceso en curso / pendiente de primera ejecución completa
- El modelo Qwen se descarga automáticamente la primera vez que se llama (~16 GB, puede tardar 3060 min)
## Script auxiliar: comparar_imagenes_wiki.py (AÑADIDO 2026-04-24)
Script que genera comparaciones TF-IDF entre `imagenes_wiki` y el corpus de texto (wikipedia + noticias + torrents) sin necesidad de Qwen. Sirve como fallback mientras `imagenes` (Qwen) esté vacío.
```bash
python comparar_imagenes_wiki.py # todos los temas, umbral 3%
python comparar_imagenes_wiki.py --umbral 5.0 # más restrictivo
python comparar_imagenes_wiki.py --tema "cambio climático"
```
Los documentos generados en `comparaciones` tienen los campos extendidos:
`source1_type: "imagen"`, `source2_type: "wikipedia"|"noticias"|"torrents"`, `tema`.
Cuando Qwen finalmente analice las imágenes, `pipeline_imagenes.py --analizar --mongo` generará comparaciones más ricas (basadas en keywords VLM) que COEXISTIRÁN con estas TF-IDF (misma colección, no se sobreescriben).
## Pendiente / mejoras futuras
- Conectar nodos imagen a la colección `comparaciones` (requiere embeddings de imagen o comparación texto-descripción)
- Activar GPU cuando esté disponible (cambiar `device_map="cpu"``"cuda"`)
- Aumentar `--max` a 50+ imágenes por tema una vez el análisis esté validado
- Aumentar `--max` a 50+ imágenes por tema
- Resolver el timeout de Qwen (186 imgs × 3min/img ≈ 9h): separarlo en un systemd service propio con `TimeoutStartSec=86400`
- Añadir soporte para imágenes de noticias (no solo Wikipedia)

View file

@ -175,7 +175,7 @@ FLUJOS_DATOS/WIKIPEDIA/
`pipeline_maestro.py` Fase 1 llama:
```bash
/var/www/theflows.net/flujos/FLUJOS_DATOS/myenv/bin/python3 \
$FLUJOS/FLUJOS_DATOS/myenv/bin/python3 \
FLUJOS_DATOS/WIKIPEDIA/main.py
```

View file

@ -1,13 +1,28 @@
# Informe de Seguridad — FLUJOS
**Fecha:** 2026-04-21
**Auditor:** Análisis de código estático + pruebas manuales en local
# Seguridad — FLUJOS
**Scope:** API REST (`FLUJOS_APP.js`), frontend JS, configuración nginx/systemd
## Estado
**Todas las vulnerabilidades de este documento están corregidas.** Lo que sigue es el
registro de la auditoría y de las mitigaciones que se aplicaron, no una lista de agujeros
abiertos. Se conserva porque explica por qué el código valida como valida.
Comprobable en `FLUJOS/BACK_BACK/FLUJOS_APP.js`:
| Mitigación | Dónde |
|---|---|
| Bind sólo a loopback | `app.listen(port, '127.0.0.1', ...)` |
| Helmet + CSP propia | `app.use(helmet())` y `helmet.contentSecurityPolicy` |
| Rechazo de operadores Mongo | `sanitizeParam()` descarta todo lo que no sea string |
| Lista blanca de temas | `TEMAS_VALIDOS` |
| Escape de metacaracteres | en `palabraClave`, antes de construir el `$regex` |
| Rate limiting | en el buzón de envíos |
| Panel admin fail-closed | `requireAdmin`, token por env o fichero fuera del webroot |
---
## Resumen ejecutivo
La aplicación despliega una API Express + MongoDB sin autenticación ni rate limiting. Se han identificado **2 vulnerabilidades críticas** explotables ahora mismo, 3 altas y 2 medias.
## Auditoría original
---
@ -196,21 +211,22 @@ Todos los ítems críticos y altos han sido corregidos en producción:
---
## Configuración nginx relevante (theflows.net)
## Configuración nginx relevante
```
/etc/nginx/sites-enabled/theflows.net
/etc/nginx/sites-enabled/upleaks.org
├── HTTP :80 → redirect 301 HTTPS
├── HTTPS :443 ssl http2 (Let's Encrypt)
│ ├── root: FLUJOS/VISUALIZACION/public (estáticos directos)
│ └── /api/ → proxy_pass http://127.0.0.1:3000/api/
```
**Problema:** el backend Node también escucha en `0.0.0.0:3000`, así que nginx solo es un proxy opcional, no obligatorio.
El backend Node escucha únicamente en `127.0.0.1:3000`, así que nginx no es un proxy
opcional: es la única vía de entrada.
---
## Orden de prioridad de fixes
## Orden en que se aplicaron los fixes
| # | Vulnerabilidad | Impacto | Esfuerzo |
|---|---|---|---|

View file

@ -8,7 +8,7 @@
## Arquitectura general
```
theflows.net (nginx)
upleaks.org (nginx)
├── / → archivos estáticos desde VISUALIZACION/public/
└── /api/ → proxy a Node.js :3000
└── GET /api/data?tema=...&nodos=...&complejidad=...
@ -67,7 +67,7 @@ Parámetros de query:
}
```
El backend limita imágenes a `floor(nodosLimit / 3)` para no saturar el grafo.
El backend limita imágenes a `max(3, floor(nodosLimit / 3))` para escalar proporcionalmente con los nodos de texto.
Prefiere `imagenes` (analizadas con Qwen) sobre `imagenes_wiki` (fallback scrapeado).
---
@ -218,14 +218,14 @@ La CSP actual (Helmet) permite `unsafe-inline` en scripts — pendiente de corre
## Imágenes wiki — ruta física a URL
```
Disco: /var/www/theflows.net/flujos/FLUJOS_DATOS/IMAGENES/output/wiki_images/{tema_slug}/{archivo}
Disco: $FLUJOS/FLUJOS_DATOS/IMAGENES/output/wiki_images/{tema_slug}/{archivo}
Express: app.use('/wiki-images', express.static('.../wiki_images'))
URL: https://theflows.net/wiki-images/{tema_slug}/{archivo}
URL: https://upleaks.org/wiki-images/{tema_slug}/{archivo}
```
El backend construye `image_url` en el API:
```javascript
const WIKI_IMAGES_BASE = '/var/www/theflows.net/flujos/FLUJOS_DATOS/IMAGENES/output/wiki_images/';
const WIKI_IMAGES_BASE = '$FLUJOS/FLUJOS_DATOS/IMAGENES/output/wiki_images/';
const relativePath = result.image_path.replace(WIKI_IMAGES_BASE, '');
image_url = `/wiki-images/${relativePath}`;
```

View file

@ -1,112 +0,0 @@
=============================================================================
DOCUMENTACIÓN DEL PROYECTO FLUJOS
=============================================================================
INTRODUCCIÓN:
-------------
El proyecto FLUJOS es una aplicación web diseñada para unir noticias y eventos del pasado para entender la historia como una sucesion de eventos .
Combatir la desinformacion es el principal objetivo del proeycto .
ESTRUCTURA DEL PROYECTO:
-------------------------
El proyecto FLUJOS está organizado de la siguiente manera:
- /flujos: Carpeta principal del proyecto.
- /BACK: Contiene los archivos y scripts relacionados con la parte del servidor y la base de datos.
- /FRONT: Contiene los archivos de la aplicación web del lado del cliente.
- /TENSOR_FLOW: Carpeta que alberga los componentes relacionados con TensorFlow y la inteligencia artificial.
- /DOCUMENTACIÓN: Documentación técnica y de usuario.
INSTALACIÓN:
------------
Para configurar y ejecutar el proyecto FLUJOS, sigue estos pasos:
1. Clona el repositorio desde GitLab:
git clone https://gitlab.com/tu-usuario/flujos.git
2. Crea y activa un entorno virtual:
python3 -m venv env
source env/bin/activate
3. Instala las dependencias:
pip install -r requirements.txt
4. Configura la base de datos y realiza las migraciones:
python manage.py migrate
5. Crea un superusuario para administrar la aplicación:
python manage.py createsuperuser
EJECUCIÓN:
----------
Para ejecutar el proyecto FLUJOS en un entorno de desarrollo local, utiliza el siguiente comando:
python manage.py runserver
El proyecto estará disponible en http://localhost:8000/.
ESTRUCTURA DEL PROYECTO (DETALLES):
-------------------------------------
- /BACK: Esta carpeta contiene la lógica del servidor Django y se encarga de la autenticación de usuarios y la gestión de la base de datos.
- /FRONT: Aquí se encuentran los archivos estáticos y la interfaz de usuario de la aplicación web.
- /TENSOR_FLOW: Carpeta dedicada a las implementaciones de TensorFlow y el procesamiento de datos relacionado con la inteligencia artificial.
AUTENTICACIÓN DE USUARIOS:
--------------------------
El sistema de autenticación de usuarios permite a los usuarios registrarse, iniciar sesión y gestionar sus cuentas de usuario. Los datos se almacenan en una base de datos Elasticsearch.
ENVÍO DE CORREOS ELECTRÓNICOS:
-------------------------------
El proyecto FLUJOS incluye un sistema de envío de correos electrónicos para funciones como verificación de correo electrónico y recuperación de contraseñas.
SEGURIDAD Y PRIVACIDAD:
-----------------------
El proyecto FLUJOS se preocupa por la seguridad y privacidad de los usuarios y emplea medidas de seguridad estándar.
IMPLEMENTACIÓN DE PGP:
----------------------
El proyecto utiliza PGP para garantizar la seguridad de las comunicaciones entre periodistas y la aplicación web.
PRUEBAS:
--------
Para ejecutar las pruebas unitarias y de integración, utiliza el siguiente comando:
python manage.py test
DESPLEGUE:
----------
El despliegue del proyecto FLUJOS en un entorno de producción requiere [Instrucciones y mejores prácticas para desplegar el proyecto en un entorno de producción real].
CONTRIBUCIONES:
---------------
¡Agradecemos las contribuciones! Si deseas contribuir al proyecto, sigue las pautas en [Enlace a las pautas de contribución en GitLab].
CONTACTO:
---------
Para obtener ayuda o más información, comunícate con [Información de contacto].
LICENCIA:
---------
Este proyecto se distribuye bajo la licencia [Nombre de la licencia]. Consulta el archivo LICENSE para más detalles.
=============================================================================

View file

@ -0,0 +1,81 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1500 770" width="1500" height="770" font-family="DejaVu Sans Mono, Menlo, Consolas, monospace" role="img" aria-label="Arquitectura de UP-LEAKS: fuentes, workers, base de datos, correlacion, API y visualizacion">
<defs>
<marker id="a1" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#39ff14" opacity="0.85"/></marker>
<marker id="a2" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#3ad6ff" opacity="0.85"/></marker>
<marker id="a3" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#ffb020" opacity="0.85"/></marker>
<marker id="a4" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#ff5ea8" opacity="0.85"/></marker>
</defs>
<rect width="1500" height="770" fill="#050505"/>
<text x="46" y="54" fill="#39ff14" font-size="27" font-weight="bold" letter-spacing="3">ARQUITECTURA</text>
<text x="46" y="80" fill="#8a938a" font-size="14">De la fuente al grafo. Cada bloque es un proceso independiente; se comunican solo por MongoDB.</text>
<line x1="46" y1="96" x2="1454" y2="96" stroke="#39ff14" stroke-opacity="0.28"/>
<rect x="46" y="130" width="300" height="174" rx="4" fill="#0d0f0d" stroke="#3ad6ff" stroke-width="1.5" stroke-opacity="0.85"/>
<text x="62" y="156" fill="#3ad6ff" font-size="13" letter-spacing="1.5">FUENTES</text>
<text x="62" y="190" fill="#8a938a" font-size="12.5">236 feeds RSS</text>
<text x="330" y="190" fill="#5a625a" font-size="11.5" text-anchor="end">5 idiomas</text>
<text x="62" y="220" fill="#8a938a" font-size="12.5">API de Wikipedia</text>
<text x="330" y="220" fill="#5a625a" font-size="11.5" text-anchor="end">28.266</text>
<text x="62" y="250" fill="#8a938a" font-size="12.5">WikiLeaks</text>
<text x="330" y="250" fill="#5a625a" font-size="11.5" text-anchor="end">36.182</text>
<text x="62" y="280" fill="#8a938a" font-size="12.5">buzón de envíos</text>
<text x="330" y="280" fill="#5a625a" font-size="11.5" text-anchor="end">periodistas</text>
<rect x="402" y="130" width="320" height="174" rx="4" fill="#0d0f0d" stroke="#39ff14" stroke-width="1.5" stroke-opacity="0.85"/>
<text x="418" y="156" fill="#39ff14" font-size="13" letter-spacing="1.5">WORKERS · coconews/</text>
<text x="418" y="190" fill="#8a938a" font-size="12.5">ingestor.py</text>
<text x="706" y="190" fill="#5a625a" font-size="11.5" text-anchor="end">cada 15 min</text>
<text x="418" y="220" fill="#8a938a" font-size="12.5">translator.py</text>
<text x="706" y="220" fill="#5a625a" font-size="11.5" text-anchor="end">→ español</text>
<text x="418" y="250" fill="#8a938a" font-size="12.5">ner.py</text>
<text x="706" y="250" fill="#5a625a" font-size="11.5" text-anchor="end">spaCy</text>
<text x="418" y="280" fill="#8a938a" font-size="12.5">embedder.py</text>
<text x="706" y="280" fill="#5a625a" font-size="11.5" text-anchor="end">MiniLM 384d</text>
<rect x="778" y="130" width="340" height="234" rx="4" fill="#0d0f0d" stroke="#ffb020" stroke-width="1.5" stroke-opacity="0.85"/>
<text x="794" y="156" fill="#ffb020" font-size="13" letter-spacing="1.5">MONGODB · FLUJOS_DATOS</text>
<text x="794" y="190" fill="#8a938a" font-size="12.5">noticias</text>
<text x="1102" y="190" fill="#5a625a" font-size="11.5" text-anchor="end">98.110</text>
<text x="794" y="220" fill="#8a938a" font-size="12.5">wikipedia</text>
<text x="1102" y="220" fill="#5a625a" font-size="11.5" text-anchor="end">28.266</text>
<text x="794" y="250" fill="#8a938a" font-size="12.5">torrents</text>
<text x="1102" y="250" fill="#5a625a" font-size="11.5" text-anchor="end">36.182</text>
<text x="794" y="280" fill="#8a938a" font-size="12.5">embeddings_chunks</text>
<text x="1102" y="280" fill="#5a625a" font-size="11.5" text-anchor="end">531.306</text>
<text x="794" y="310" fill="#8a938a" font-size="12.5">coconews_entidades</text>
<text x="1102" y="310" fill="#5a625a" font-size="11.5" text-anchor="end">76.857</text>
<text x="794" y="340" fill="#8a938a" font-size="12.5">comparaciones</text>
<text x="1102" y="340" fill="#5a625a" font-size="11.5" text-anchor="end">115M</text>
<rect x="1154" y="130" width="300" height="204" rx="4" fill="#0d0f0d" stroke="#ff5ea8" stroke-width="1.5" stroke-opacity="0.85"/>
<text x="1170" y="156" fill="#ff5ea8" font-size="13" letter-spacing="1.5">APIs</text>
<text x="1170" y="190" fill="#8a938a" font-size="12.5">FLUJOS_APP.js</text>
<text x="1438" y="190" fill="#5a625a" font-size="11.5" text-anchor="end">:3000</text>
<text x="1170" y="220" fill="#8a938a" font-size="12.5"> /api/data</text>
<text x="1438" y="220" fill="#5a625a" font-size="11.5" text-anchor="end">grafo</text>
<text x="1170" y="250" fill="#8a938a" font-size="12.5"> /api/stats</text>
<text x="1438" y="250" fill="#5a625a" font-size="11.5" text-anchor="end">métricas</text>
<text x="1170" y="280" fill="#8a938a" font-size="12.5"> /api/submit</text>
<text x="1438" y="280" fill="#5a625a" font-size="11.5" text-anchor="end">buzón</text>
<text x="1170" y="310" fill="#8a938a" font-size="12.5">coconews server.js</text>
<text x="1438" y="310" fill="#5a625a" font-size="11.5" text-anchor="end">:3100</text>
<line x1="352" y1="200" x2="392" y2="200" stroke="#3ad6ff" stroke-width="1.6" stroke-opacity="0.65" marker-end="url(#a2)"/>
<line x1="728" y1="200" x2="768" y2="200" stroke="#39ff14" stroke-width="1.6" stroke-opacity="0.65" marker-end="url(#a1)"/>
<line x1="1124" y1="200" x2="1144" y2="200" stroke="#ffb020" stroke-width="1.6" stroke-opacity="0.65" marker-end="url(#a3)"/>
<rect x="46" y="420" width="1072" height="150" rx="4" fill="#0d0f0d" stroke="#39ff14" stroke-width="1.5"/>
<text x="64" y="448" fill="#39ff14" font-size="13" letter-spacing="1.5">CORRELACIÓN SEMÁNTICA · motor_comparacion.py</text>
<text x="64" y="476" fill="#7cff5a" font-size="12">1. vectorizar</text>
<text x="196" y="476" fill="#8a938a" font-size="12">cada documento y cada fragmento → 384 dimensiones</text>
<text x="64" y="500" fill="#7cff5a" font-size="12">2. coseno</text>
<text x="196" y="500" fill="#8a938a" font-size="12">noticia contra todo el corpus de leaks, reducido por máximo de fragmento</text>
<text x="64" y="524" fill="#7cff5a" font-size="12">3. anti-hub</text>
<text x="196" y="524" fill="#8a938a" font-size="12">score = cos λ·ln(1+grado); descarta los documentos catch-all</text>
<text x="64" y="548" fill="#7cff5a" font-size="12">4. arista</text>
<text x="196" y="548" fill="#8a938a" font-size="12">si pasa el umbral, se escribe el par en `comparaciones`</text>
<rect x="1154" y="420" width="300" height="150" rx="4" fill="#0d0f0d" stroke="#ff5ea8" stroke-width="1.5"/>
<text x="1170" y="448" fill="#ff5ea8" font-size="13" letter-spacing="1.5">NAVEGADOR</text>
<text x="1170" y="476" fill="#8a938a" font-size="12.5">nginx · upleaks.org</text>
<text x="1170" y="500" fill="#8a938a" font-size="12.5">3d-force-graph + Three.js</text>
<text x="1170" y="524" fill="#8a938a" font-size="12.5">5 vistas temáticas</text>
<text x="1170" y="548" fill="#8a938a" font-size="12.5">semanal / todas</text>
<line x1="1124" y1="495" x2="1144" y2="495" stroke="#ff5ea8" stroke-width="1.6" stroke-opacity="0.6" marker-end="url(#a4)"/>
<text x="46" y="650" fill="#5a625a" font-size="12">Los workers no se llaman entre sí: cada uno marca su trabajo en el documento (`traducido`, `procesado_ner`, `procesado_embedding`) y el siguiente recoge lo pendiente.</text>
<text x="46" y="672" fill="#5a625a" font-size="12">Si uno cae, los demás siguen; al volver, retoma la cola donde estaba. No hay orquestador ni cola de mensajes: el estado vive en la base.</text>
<text x="46" y="750" fill="#5a625a" font-size="11">upleaks.org — software libre — gitea.laenre.net/hacklab/FLUJOS</text>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -0,0 +1,134 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1500 940" width="1500" height="940" font-family="DejaVu Sans Mono, Menlo, Consolas, monospace" role="img" aria-label="Diagrama de los flujos de informacion: de las agencias de noticias a la prensa generalista y la prensa independiente">
<defs>
<linearGradient id="cuello" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#39ff14" stop-opacity="0.55"/><stop offset="100%" stop-color="#39ff14" stop-opacity="0.06"/></linearGradient>
<linearGradient id="indg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#3ad6ff" stop-opacity="0.45"/><stop offset="100%" stop-color="#3ad6ff" stop-opacity="0.06"/></linearGradient>
<marker id="fl" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#39ff14" opacity="0.75"/></marker>
<marker id="flc" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#3ad6ff" opacity="0.8"/></marker>
</defs>
<rect width="1500" height="940" fill="#050505"/>
<text x="46" y="58" fill="#39ff14" font-size="30" font-weight="bold" letter-spacing="3">DE DÓNDE VIENE LA NOTICIA QUE ACABAS DE LEER</text>
<text x="46" y="86" fill="#8a938a" font-size="15">Cuatro agencias abastecen a casi toda la prensa del mundo. Lo que parece pluralidad suele ser el mismo teletipo repetido.</text>
<line x1="46" y1="102" x2="1454" y2="102" stroke="#39ff14" stroke-opacity="0.28"/>
<text x="46" y="120" fill="#7cff5a" font-size="13" letter-spacing="2">ORIGEN — 4 AGENCIAS</text>
<rect x="114" y="132" width="300" height="78" rx="4" fill="#0d0f0d" stroke="#39ff14" stroke-width="1.6"/>
<text x="264" y="166" fill="#39ff14" font-size="23" font-weight="bold" text-anchor="middle">REUTERS</text>
<text x="264" y="190" fill="#8a938a" font-size="11.5" text-anchor="middle">Thomson Reuters — Reino Unido/Canadá</text>
<rect x="438" y="132" width="300" height="78" rx="4" fill="#0d0f0d" stroke="#39ff14" stroke-width="1.6"/>
<text x="588" y="166" fill="#39ff14" font-size="23" font-weight="bold" text-anchor="middle">AFP</text>
<text x="588" y="190" fill="#8a938a" font-size="11.5" text-anchor="middle">Agence France-Presse — Francia</text>
<rect x="762" y="132" width="300" height="78" rx="4" fill="#0d0f0d" stroke="#39ff14" stroke-width="1.6"/>
<text x="912" y="166" fill="#39ff14" font-size="23" font-weight="bold" text-anchor="middle">AP</text>
<text x="912" y="190" fill="#8a938a" font-size="11.5" text-anchor="middle">Associated Press — EE. UU.</text>
<rect x="1086" y="132" width="300" height="78" rx="4" fill="#0d0f0d" stroke="#39ff14" stroke-width="1.6"/>
<text x="1236" y="166" fill="#39ff14" font-size="23" font-weight="bold" text-anchor="middle">EFE</text>
<text x="1236" y="190" fill="#8a938a" font-size="11.5" text-anchor="middle">Agencia EFE — España</text>
<path d="M108,210 L1392,210 L868,302 L632,302 Z" fill="url(#cuello)"/>
<line x1="264" y1="210" x2="750" y2="296" stroke="#39ff14" stroke-opacity="0.5" stroke-width="1.2" marker-end="url(#fl)"/>
<line x1="588" y1="210" x2="750" y2="296" stroke="#39ff14" stroke-opacity="0.5" stroke-width="1.2" marker-end="url(#fl)"/>
<line x1="912" y1="210" x2="750" y2="296" stroke="#39ff14" stroke-opacity="0.5" stroke-width="1.2" marker-end="url(#fl)"/>
<line x1="1236" y1="210" x2="750" y2="296" stroke="#39ff14" stroke-opacity="0.5" stroke-width="1.2" marker-end="url(#fl)"/>
<text x="750" y="328" fill="#39ff14" font-size="15" font-weight="bold" text-anchor="middle" letter-spacing="1">EL MISMO TELETIPO</text>
<text x="750" y="348" fill="#8a938a" font-size="12" text-anchor="middle">un despacho, un ángulo, un titular</text>
<rect x="51" y="416" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="110" y="441" fill="#8a938a" font-size="12" text-anchor="middle">BBC</text>
<line x1="750" y1="358" x2="110" y2="413" stroke="#39ff14" stroke-opacity="0.22" stroke-width="0.9"/>
<rect x="179" y="416" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="238" y="441" fill="#8a938a" font-size="12" text-anchor="middle">The Guardian</text>
<rect x="307" y="416" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="366" y="441" fill="#8a938a" font-size="12" text-anchor="middle">CNN</text>
<rect x="435" y="416" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="494" y="441" fill="#8a938a" font-size="12" text-anchor="middle">El País</text>
<line x1="750" y1="358" x2="494" y2="413" stroke="#39ff14" stroke-opacity="0.22" stroke-width="0.9"/>
<rect x="563" y="416" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="622" y="441" fill="#8a938a" font-size="12" text-anchor="middle">Le Monde</text>
<rect x="691" y="416" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="750" y="441" fill="#8a938a" font-size="12" text-anchor="middle">Der Spiegel</text>
<rect x="819" y="416" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="878" y="441" fill="#8a938a" font-size="12" text-anchor="middle">La Vanguardia</text>
<rect x="947" y="416" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="1006" y="441" fill="#8a938a" font-size="12" text-anchor="middle">The Times</text>
<line x1="750" y1="358" x2="1006" y2="413" stroke="#39ff14" stroke-opacity="0.22" stroke-width="0.9"/>
<rect x="1075" y="416" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="1134" y="441" fill="#8a938a" font-size="12" text-anchor="middle">ABC</text>
<rect x="1203" y="416" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="1262" y="441" fill="#8a938a" font-size="12" text-anchor="middle">Corriere</text>
<rect x="1331" y="416" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="1390" y="441" fill="#8a938a" font-size="12" text-anchor="middle">El Mundo</text>
<line x1="750" y1="358" x2="1390" y2="413" stroke="#39ff14" stroke-opacity="0.22" stroke-width="0.9"/>
<rect x="51" y="466" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="110" y="491" fill="#8a938a" font-size="12" text-anchor="middle">FAZ</text>
<rect x="179" y="466" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="238" y="491" fill="#8a938a" font-size="12" text-anchor="middle">NYT</text>
<rect x="307" y="466" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="366" y="491" fill="#8a938a" font-size="12" text-anchor="middle">WaPo</text>
<rect x="435" y="466" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="494" y="491" fill="#8a938a" font-size="12" text-anchor="middle">Sky News</text>
<rect x="563" y="466" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="622" y="491" fill="#8a938a" font-size="12" text-anchor="middle">RTVE</text>
<rect x="691" y="466" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="750" y="491" fill="#8a938a" font-size="12" text-anchor="middle">20minutos</text>
<rect x="819" y="466" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="878" y="491" fill="#8a938a" font-size="12" text-anchor="middle">Antena 3</text>
<rect x="947" y="466" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="1006" y="491" fill="#8a938a" font-size="12" text-anchor="middle">La Sexta</text>
<rect x="1075" y="466" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="1134" y="491" fill="#8a938a" font-size="12" text-anchor="middle">Yahoo News</text>
<rect x="1203" y="466" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="1262" y="491" fill="#8a938a" font-size="12" text-anchor="middle">MSN</text>
<rect x="1331" y="466" width="118" height="40" rx="3" fill="#101310" stroke="#5a625a" stroke-width="1"/>
<text x="1390" y="491" fill="#8a938a" font-size="12" text-anchor="middle">Google News</text>
<rect x="40" y="376" width="439" height="26" fill="#050505"/>
<text x="46" y="395" fill="#7cff5a" font-size="13" letter-spacing="2">REPLICACIÓN — CIENTOS DE CABECERAS, UNA SOLA FUENTE</text>
<text x="750" y="538" fill="#5a625a" font-size="12" text-anchor="middle">… y varios miles más. Cambia el logotipo, no el contenido.</text>
<line x1="46" y1="562" x2="1454" y2="562" stroke="#39ff14" stroke-opacity="0.22" stroke-dasharray="5 6"/>
<text x="46" y="596" fill="#3ad6ff" font-size="13" letter-spacing="2">EL OTRO CIRCUITO — PRENSA INDEPENDIENTE</text>
<text x="46" y="616" fill="#8a938a" font-size="12.5">Menos cabeceras, pero cada una con fuente propia. No replican: producen.</text>
<rect x="46" y="638" width="290" height="148" rx="4" fill="#0d0f0d" stroke="#3ad6ff" stroke-width="1.3" stroke-opacity="0.75"/>
<text x="62" y="660" fill="#3ad6ff" font-size="12" letter-spacing="1">FUENTES PROPIAS</text>
<text x="62" y="682" fill="#8a938a" font-size="12"> documentos filtrados</text>
<text x="62" y="706" fill="#8a938a" font-size="12"> peticiones FOIA</text>
<text x="62" y="730" fill="#8a938a" font-size="12"> reporteo sobre el terreno</text>
<text x="62" y="754" fill="#8a938a" font-size="12"> archivos judiciales</text>
<text x="62" y="778" fill="#8a938a" font-size="12"> OSINT y satélite</text>
<rect x="376" y="638" width="232" height="52" rx="3" fill="#0a1114" stroke="#3ad6ff" stroke-width="1.1" stroke-opacity="0.6"/>
<text x="390" y="660" fill="#3ad6ff" font-size="13.5">Bellingcat</text>
<text x="390" y="677" fill="#5a625a" font-size="11">OSINT</text>
<line x1="336" y1="696" x2="372" y2="664" stroke="#3ad6ff" stroke-opacity="0.45" stroke-width="1.2" marker-end="url(#flc)"/>
<rect x="620" y="638" width="232" height="52" rx="3" fill="#0a1114" stroke="#3ad6ff" stroke-width="1.1" stroke-opacity="0.6"/>
<text x="634" y="660" fill="#3ad6ff" font-size="13.5">The Intercept</text>
<text x="634" y="677" fill="#5a625a" font-size="11">filtraciones</text>
<rect x="864" y="638" width="232" height="52" rx="3" fill="#0a1114" stroke="#3ad6ff" stroke-width="1.1" stroke-opacity="0.6"/>
<text x="878" y="660" fill="#3ad6ff" font-size="13.5">Declassified UK</text>
<text x="878" y="677" fill="#5a625a" font-size="11">FOIA</text>
<rect x="1108" y="638" width="232" height="52" rx="3" fill="#0a1114" stroke="#3ad6ff" stroke-width="1.1" stroke-opacity="0.6"/>
<text x="1122" y="660" fill="#3ad6ff" font-size="13.5">Meduza</text>
<text x="1122" y="677" fill="#5a625a" font-size="11">exilio</text>
<rect x="376" y="702" width="232" height="52" rx="3" fill="#0a1114" stroke="#3ad6ff" stroke-width="1.1" stroke-opacity="0.6"/>
<text x="390" y="724" fill="#3ad6ff" font-size="13.5">Amu TV / 8am</text>
<text x="390" y="741" fill="#5a625a" font-size="11">exilio afgano</text>
<line x1="336" y1="696" x2="372" y2="728" stroke="#3ad6ff" stroke-opacity="0.45" stroke-width="1.2" marker-end="url(#flc)"/>
<rect x="620" y="702" width="232" height="52" rx="3" fill="#0a1114" stroke="#3ad6ff" stroke-width="1.1" stroke-opacity="0.6"/>
<text x="634" y="724" fill="#3ad6ff" font-size="13.5">Dabanga</text>
<text x="634" y="741" fill="#5a625a" font-size="11">Sudán</text>
<rect x="864" y="702" width="232" height="52" rx="3" fill="#0a1114" stroke="#3ad6ff" stroke-width="1.1" stroke-opacity="0.6"/>
<text x="878" y="724" fill="#3ad6ff" font-size="13.5">elDiario.es</text>
<text x="878" y="741" fill="#5a625a" font-size="11">socios</text>
<rect x="1108" y="702" width="232" height="52" rx="3" fill="#0a1114" stroke="#3ad6ff" stroke-width="1.1" stroke-opacity="0.6"/>
<text x="1122" y="724" fill="#3ad6ff" font-size="13.5">CTXT</text>
<text x="1122" y="741" fill="#5a625a" font-size="11">lectores</text>
<rect x="46" y="800" width="1408" height="108" rx="5" fill="#0d0f0d" stroke="#39ff14" stroke-width="2"/>
<text x="72" y="838" fill="#39ff14" font-size="25" font-weight="bold" letter-spacing="3">UP-LEAKS</text>
<text x="72" y="862" fill="#8a938a" font-size="12.5">Ingiere los dos circuitos y los cruza contra el archivo de WikiLeaks.</text>
<text x="72" y="882" fill="#8a938a" font-size="12.5">Donde una noticia y un documento filtrado hablan de lo mismo, aparece una arista.</text>
<text x="940" y="846" fill="#39ff14" font-size="26" font-weight="bold">236</text>
<text x="940" y="868" fill="#5a625a" font-size="11.5" letter-spacing="1">feeds</text>
<text x="1112" y="846" fill="#39ff14" font-size="26" font-weight="bold">36.182</text>
<text x="1112" y="868" fill="#5a625a" font-size="11.5" letter-spacing="1">documentos</text>
<text x="1284" y="846" fill="#39ff14" font-size="26" font-weight="bold">115M</text>
<text x="1284" y="868" fill="#5a625a" font-size="11.5" letter-spacing="1">aristas</text>
<path d="M1400,546 L1400,786" fill="none" stroke="#39ff14" stroke-opacity="0.45" stroke-width="1.6" stroke-dasharray="4 4" marker-end="url(#fl)"/>
<text x="1408" y="658" fill="#5a625a" font-size="11">generalista</text>
<path d="M496,770 L496,786" fill="none" stroke="#3ad6ff" stroke-opacity="0.6" stroke-width="1.6" marker-end="url(#flc)"/>
<text x="508" y="780" fill="#5a625a" font-size="11">independiente</text>
<text x="46" y="920" fill="#5a625a" font-size="11">upleaks.org — software libre — gitea.laenre.net/hacklab/FLUJOS</text>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,129 @@
#!/usr/bin/env python3
# Genera el diagrama de arquitectura de UP-LEAKS / FLUJOS.
import io, sys
W, H = 1500, 770
NEON, NEON2 = "#39ff14", "#7cff5a"
BG, PANEL = "#050505", "#0d0f0d"
GRIS, GRIS2 = "#8a938a", "#5a625a"
CIAN, AMBAR, MAGENTA = "#3ad6ff", "#ffb020", "#ff5ea8"
o = []
a = o.append
a(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" height="{H}" '
f'font-family="DejaVu Sans Mono, Menlo, Consolas, monospace" role="img" '
f'aria-label="Arquitectura de UP-LEAKS: fuentes, workers, base de datos, correlacion, API y visualizacion">')
a('<defs>')
for nom, col in (("a1", NEON), ("a2", CIAN), ("a3", AMBAR), ("a4", MAGENTA)):
a(f'<marker id="{nom}" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto">'
f'<path d="M0,0 L7,3 L0,6 Z" fill="{col}" opacity="0.85"/></marker>')
a('</defs>')
a(f'<rect width="{W}" height="{H}" fill="{BG}"/>')
a(f'<text x="46" y="54" fill="{NEON}" font-size="27" font-weight="bold" letter-spacing="3">ARQUITECTURA</text>')
a(f'<text x="46" y="80" fill="{GRIS}" font-size="14">'
f'De la fuente al grafo. Cada bloque es un proceso independiente; se comunican solo por MongoDB.</text>')
a(f'<line x1="46" y1="96" x2="{W-46}" y2="96" stroke="{NEON}" stroke-opacity="0.28"/>')
def columna(x, y, w, titulo, color, items, alto_item=30, cab=40):
h = cab + len(items) * alto_item + 14
a(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="4" fill="{PANEL}" '
f'stroke="{color}" stroke-width="1.5" stroke-opacity="0.85"/>')
a(f'<text x="{x+16}" y="{y+26}" fill="{color}" font-size="13" letter-spacing="1.5">{titulo}</text>')
for i, it in enumerate(items):
yy = y + cab + 20 + i * alto_item
if isinstance(it, tuple):
a(f'<text x="{x+16}" y="{yy}" fill="{GRIS}" font-size="12.5">{it[0]}</text>')
a(f'<text x="{x+w-16}" y="{yy}" fill="{GRIS2}" font-size="11.5" text-anchor="end">{it[1]}</text>')
else:
a(f'<text x="{x+16}" y="{yy}" fill="{GRIS}" font-size="12.5">{it}</text>')
return h
Y = 130
X1, W1 = 46, 300
X2, W2 = 402, 320
X3, W3 = 778, 340
X4, W4 = 1154, 300
h1 = columna(X1, Y, W1, "FUENTES", CIAN, [
("236 feeds RSS", "5 idiomas"),
("API de Wikipedia", "28.266"),
("WikiLeaks", "36.182"),
("buzón de envíos", "periodistas"),
])
h2 = columna(X2, Y, W2, "WORKERS · coconews/", NEON, [
("ingestor.py", "cada 15 min"),
("translator.py", "→ español"),
("ner.py", "spaCy"),
("embedder.py", "MiniLM 384d"),
])
h3 = columna(X3, Y, W3, "MONGODB · FLUJOS_DATOS", AMBAR, [
("noticias", "98.110"),
("wikipedia", "28.266"),
("torrents", "36.182"),
("embeddings_chunks", "531.306"),
("coconews_entidades", "76.857"),
("comparaciones", "115M"),
])
h4 = columna(X4, Y, W4, "APIs", MAGENTA, [
("FLUJOS_APP.js", ":3000"),
(" /api/data", "grafo"),
(" /api/stats", "métricas"),
(" /api/submit", "buzón"),
("coconews server.js", ":3100"),
])
ymid = Y + 70
for (xa, wa, xb, col, mk) in ((X1, W1, X2, CIAN, "a2"), (X2, W2, X3, NEON, "a1"), (X3, W3, X4, AMBAR, "a3")):
a(f'<line x1="{xa+wa+6}" y1="{ymid}" x2="{xb-10}" y2="{ymid}" stroke="{col}" '
f'stroke-width="1.6" stroke-opacity="0.65" marker-end="url(#{mk})"/>')
# ── correlacion ───────────────────────────────────────────────────────────
Y2 = Y + max(h1, h2, h3, h4) + 56
a(f'<rect x="{X1}" y="{Y2}" width="{X3+W3-X1}" height="150" rx="4" fill="{PANEL}" '
f'stroke="{NEON}" stroke-width="1.5"/>')
a(f'<text x="{X1+18}" y="{Y2+28}" fill="{NEON}" font-size="13" letter-spacing="1.5">'
f'CORRELACIÓN SEMÁNTICA · motor_comparacion.py</text>')
pasos = [
("1. vectorizar", "cada documento y cada fragmento → 384 dimensiones"),
("2. coseno", "noticia contra todo el corpus de leaks, reducido por máximo de fragmento"),
("3. anti-hub", "score = cos λ·ln(1+grado); descarta los documentos catch-all"),
("4. arista", "si pasa el umbral, se escribe el par en `comparaciones`"),
]
for i, (t, d) in enumerate(pasos):
yy = Y2 + 56 + i * 24
a(f'<text x="{X1+18}" y="{yy}" fill="{NEON2}" font-size="12">{t}</text>')
a(f'<text x="{X1+150}" y="{yy}" fill="{GRIS}" font-size="12">{d}</text>')
# ── navegador ─────────────────────────────────────────────────────────────
a(f'<rect x="{X4}" y="{Y2}" width="{W4}" height="150" rx="4" fill="{PANEL}" '
f'stroke="{MAGENTA}" stroke-width="1.5"/>')
a(f'<text x="{X4+16}" y="{Y2+28}" fill="{MAGENTA}" font-size="13" letter-spacing="1.5">NAVEGADOR</text>')
for i, t in enumerate(["nginx · upleaks.org", "3d-force-graph + Three.js",
"5 vistas temáticas", "semanal / todas"]):
a(f'<text x="{X4+16}" y="{Y2+56+i*24}" fill="{GRIS}" font-size="12.5">{t}</text>')
a(f'<line x1="{X3+W3+6}" y1="{Y2+75}" x2="{X4-10}" y2="{Y2+75}" stroke="{MAGENTA}" '
f'stroke-width="1.6" stroke-opacity="0.6" marker-end="url(#a4)"/>')
# ── nota al pie ───────────────────────────────────────────────────────────
Y3 = Y2 + 150 + 62
a(f'<text x="{X1}" y="{Y3+18}" fill="{GRIS2}" font-size="12">'
f'Los workers no se llaman entre sí: cada uno marca su trabajo en el documento '
f'(`traducido`, `procesado_ner`, `procesado_embedding`) y el siguiente recoge lo pendiente.</text>')
a(f'<text x="{X1}" y="{Y3+40}" fill="{GRIS2}" font-size="12">'
f'Si uno cae, los demás siguen; al volver, retoma la cola donde estaba. No hay orquestador '
f'ni cola de mensajes: el estado vive en la base.</text>')
a(f'<text x="46" y="{H-20}" fill="{GRIS2}" font-size="11">'
f'upleaks.org — software libre — gitea.laenre.net/hacklab/FLUJOS</text>')
a('</svg>')
io.open(sys.argv[1], 'w', encoding='utf-8').write('\n'.join(o))
print("escrito", sys.argv[1], "| alto util:", Y3 + 40)

View file

@ -0,0 +1,196 @@
#!/usr/bin/env python3
# Genera el diagrama grande de flujos de informacion (UP-LEAKS).
import io, math, sys
W, H = 1500, 940
NEON, NEON2 = "#39ff14", "#7cff5a"
BG, PANEL = "#050505", "#0d0f0d"
GRIS, GRIS2 = "#8a938a", "#5a625a"
AMBAR = "#ffb020"
CIAN = "#3ad6ff"
AGENCIAS = [
("REUTERS", "Thomson Reuters — Reino Unido/Canadá"),
("AFP", "Agence France-Presse — Francia"),
("AP", "Associated Press — EE. UU."),
("EFE", "Agencia EFE — España"),
]
# Cabeceras generalistas: todas beben de los mismos teletipos
CABECERAS = [
"BBC", "The Guardian", "CNN", "El País", "Le Monde", "Der Spiegel",
"La Vanguardia", "The Times", "ABC", "Corriere", "El Mundo", "FAZ",
"NYT", "WaPo", "Sky News", "RTVE", "20minutos", "Antena 3",
"La Sexta", "Yahoo News", "MSN", "Google News",
]
# Prensa independiente: circuito corto, fuentes propias
INDEPENDIENTES = [
("Bellingcat", "OSINT"),
("The Intercept", "filtraciones"),
("Declassified UK", "FOIA"),
("Meduza", "exilio"),
("Amu TV / 8am", "exilio afgano"),
("Dabanga", "Sudán"),
("elDiario.es", "socios"),
("CTXT", "lectores"),
]
FUENTES_PROPIAS = [
"documentos filtrados", "peticiones FOIA", "reporteo sobre el terreno",
"archivos judiciales", "OSINT y satélite",
]
o = []
a = o.append
a(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" height="{H}" '
f'font-family="DejaVu Sans Mono, Menlo, Consolas, monospace" role="img" '
f'aria-label="Diagrama de los flujos de informacion: de las agencias de noticias a la prensa generalista y la prensa independiente">')
a('<defs>')
a(f'<linearGradient id="cuello" x1="0" y1="0" x2="0" y2="1">'
f'<stop offset="0%" stop-color="{NEON}" stop-opacity="0.55"/>'
f'<stop offset="100%" stop-color="{NEON}" stop-opacity="0.06"/></linearGradient>')
a(f'<linearGradient id="indg" x1="0" y1="0" x2="0" y2="1">'
f'<stop offset="0%" stop-color="{CIAN}" stop-opacity="0.45"/>'
f'<stop offset="100%" stop-color="{CIAN}" stop-opacity="0.06"/></linearGradient>')
a(f'<marker id="fl" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto">'
f'<path d="M0,0 L7,3 L0,6 Z" fill="{NEON}" opacity="0.75"/></marker>')
a(f'<marker id="flc" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto">'
f'<path d="M0,0 L7,3 L0,6 Z" fill="{CIAN}" opacity="0.8"/></marker>')
a('</defs>')
a(f'<rect width="{W}" height="{H}" fill="{BG}"/>')
# ── titulo ────────────────────────────────────────────────────────────────
a(f'<text x="46" y="58" fill="{NEON}" font-size="30" font-weight="bold" letter-spacing="3">'
f'DE DÓNDE VIENE LA NOTICIA QUE ACABAS DE LEER</text>')
a(f'<text x="46" y="86" fill="{GRIS}" font-size="15">'
f'Cuatro agencias abastecen a casi toda la prensa del mundo. Lo que parece pluralidad '
f'suele ser el mismo teletipo repetido.</text>')
a(f'<line x1="46" y1="102" x2="{W-46}" y2="102" stroke="{NEON}" stroke-opacity="0.28"/>')
# ── capa 1: agencias ──────────────────────────────────────────────────────
y_ag, h_ag = 132, 78
ancho = 300
x0 = (W - (len(AGENCIAS) * ancho + (len(AGENCIAS) - 1) * 24)) / 2
a(f'<text x="46" y="{y_ag-12}" fill="{NEON2}" font-size="13" letter-spacing="2">'
f'ORIGEN — 4 AGENCIAS</text>')
centros_ag = []
for i, (nom, sub) in enumerate(AGENCIAS):
x = x0 + i * (ancho + 24)
a(f'<rect x="{x:.0f}" y="{y_ag}" width="{ancho}" height="{h_ag}" rx="4" '
f'fill="{PANEL}" stroke="{NEON}" stroke-width="1.6"/>')
a(f'<text x="{x+ancho/2:.0f}" y="{y_ag+34}" fill="{NEON}" font-size="23" font-weight="bold" '
f'text-anchor="middle">{nom}</text>')
a(f'<text x="{x+ancho/2:.0f}" y="{y_ag+58}" fill="{GRIS}" font-size="11.5" '
f'text-anchor="middle">{sub}</text>')
centros_ag.append(x + ancho / 2)
# ── cuello de botella ─────────────────────────────────────────────────────
y_c1, y_c2 = y_ag + h_ag, y_ag + h_ag + 92
cx = W / 2
a(f'<path d="M{x0-6:.0f},{y_c1} L{W-x0+6:.0f},{y_c1} L{cx+118:.0f},{y_c2} L{cx-118:.0f},{y_c2} Z" '
f'fill="url(#cuello)"/>')
for c in centros_ag:
a(f'<line x1="{c:.0f}" y1="{y_c1}" x2="{cx:.0f}" y2="{y_c2-6}" stroke="{NEON}" '
f'stroke-opacity="0.5" stroke-width="1.2" marker-end="url(#fl)"/>')
a(f'<text x="{cx:.0f}" y="{y_c2+26}" fill="{NEON}" font-size="15" font-weight="bold" '
f'text-anchor="middle" letter-spacing="1">EL MISMO TELETIPO</text>')
a(f'<text x="{cx:.0f}" y="{y_c2+46}" fill="{GRIS}" font-size="12" text-anchor="middle">'
f'un despacho, un ángulo, un titular</text>')
# ── capa 2: cabeceras generalistas ────────────────────────────────────────
y_ab = y_c2 + 104
_etq = 'REPLICACIÓN — CIENTOS DE CABECERAS, UNA SOLA FUENTE'
_lbl = [] # se pinta DESPUES del abanico, con fondo, para que no lo crucen lineas
cols, cw, ch, gap = 11, 118, 40, 10
gw = cols * cw + (cols - 1) * gap
gx = (W - gw) / 2
for i, nom in enumerate(CABECERAS):
r, c = divmod(i, cols)
x = gx + c * (cw + gap)
y = y_ab + 10 + r * (ch + gap)
a(f'<rect x="{x:.0f}" y="{y:.0f}" width="{cw}" height="{ch}" rx="3" fill="#101310" '
f'stroke="{GRIS2}" stroke-width="1"/>')
a(f'<text x="{x+cw/2:.0f}" y="{y+25:.0f}" fill="{GRIS}" font-size="12" '
f'text-anchor="middle">{nom}</text>')
if r == 0 and c in (0, 3, 7, 10):
a(f'<line x1="{cx:.0f}" y1="{y_c2+56}" x2="{x+cw/2:.0f}" y2="{y-3:.0f}" '
f'stroke="{NEON}" stroke-opacity="0.22" stroke-width="0.9"/>')
a(f'<rect x="40" y="{y_ab-30}" width="{len(_etq)*8.6:.0f}" height="26" fill="{BG}"/>')
a(f'<text x="46" y="{y_ab-11}" fill="{NEON2}" font-size="13" letter-spacing="2">{_etq}</text>')
filas = math.ceil(len(CABECERAS) / cols)
y_ab_fin = y_ab + 10 + filas * (ch + gap)
a(f'<text x="{cx:.0f}" y="{y_ab_fin+22:.0f}" fill="{GRIS2}" font-size="12" text-anchor="middle">'
f'… y varios miles más. Cambia el logotipo, no el contenido.</text>')
# ── separador ─────────────────────────────────────────────────────────────
y_sep = y_ab_fin + 46
a(f'<line x1="46" y1="{y_sep}" x2="{W-46}" y2="{y_sep}" stroke="{NEON}" '
f'stroke-opacity="0.22" stroke-dasharray="5 6"/>')
# ── capa 3: prensa independiente ──────────────────────────────────────────
y_in = y_sep + 34
a(f'<text x="46" y="{y_in}" fill="{CIAN}" font-size="13" letter-spacing="2">'
f'EL OTRO CIRCUITO — PRENSA INDEPENDIENTE</text>')
a(f'<text x="46" y="{y_in+20}" fill="{GRIS}" font-size="12.5">'
f'Menos cabeceras, pero cada una con fuente propia. No replican: producen.</text>')
fx, fy = 46, y_in + 42
a(f'<rect x="{fx}" y="{fy}" width="290" height="{len(FUENTES_PROPIAS)*24+28}" rx="4" '
f'fill="{PANEL}" stroke="{CIAN}" stroke-width="1.3" stroke-opacity="0.75"/>')
a(f'<text x="{fx+16}" y="{fy+22}" fill="{CIAN}" font-size="12" letter-spacing="1">FUENTES PROPIAS</text>')
for i, f in enumerate(FUENTES_PROPIAS):
a(f'<text x="{fx+16}" y="{fy+44+i*24}" fill="{GRIS}" font-size="12"> {f}</text>')
icw, ich, igap = 232, 52, 12
igx = fx + 330
for i, (nom, sub) in enumerate(INDEPENDIENTES):
r, c = divmod(i, 4)
x = igx + c * (icw + igap)
y = fy + r * (ich + igap)
a(f'<rect x="{x:.0f}" y="{y:.0f}" width="{icw}" height="{ich}" rx="3" fill="#0a1114" '
f'stroke="{CIAN}" stroke-width="1.1" stroke-opacity="0.6"/>')
a(f'<text x="{x+14:.0f}" y="{y+22:.0f}" fill="{CIAN}" font-size="13.5">{nom}</text>')
a(f'<text x="{x+14:.0f}" y="{y+39:.0f}" fill="{GRIS2}" font-size="11">{sub}</text>')
if c == 0:
a(f'<line x1="{fx+290}" y1="{fy+58:.0f}" x2="{x-4:.0f}" y2="{y+ich/2:.0f}" '
f'stroke="{CIAN}" stroke-opacity="0.45" stroke-width="1.2" marker-end="url(#flc)"/>')
y_in_fin = fy + 2 * (ich + igap)
# ── capa 4: up-leaks ──────────────────────────────────────────────────────
y_up = y_in_fin + 34
bw, bh = W - 92, 108
a(f'<rect x="46" y="{y_up}" width="{bw}" height="{bh}" rx="5" fill="{PANEL}" '
f'stroke="{NEON}" stroke-width="2"/>')
a(f'<text x="72" y="{y_up+38}" fill="{NEON}" font-size="25" font-weight="bold" '
f'letter-spacing="3">UP-LEAKS</text>')
a(f'<text x="72" y="{y_up+62}" fill="{GRIS}" font-size="12.5">'
f'Ingiere los dos circuitos y los cruza contra el archivo de WikiLeaks.</text>')
a(f'<text x="72" y="{y_up+82}" fill="{GRIS}" font-size="12.5">'
f'Donde una noticia y un documento filtrado hablan de lo mismo, aparece una arista.</text>')
for i, (etq, val) in enumerate([("236", "feeds"), ("36.182", "documentos"), ("115M", "aristas")]):
x = 940 + i * 172
a(f'<text x="{x}" y="{y_up+46}" fill="{NEON}" font-size="26" font-weight="bold">{etq}</text>')
a(f'<text x="{x}" y="{y_up+68}" fill="{GRIS2}" font-size="11.5" letter-spacing="1">{val}</text>')
# flechas de entrada
# generalista: baja por la derecha bordeando el bloque independiente
a(f'<path d="M{W-100},{y_ab_fin+30:.0f} L{W-100},{y_up-14}" fill="none" stroke="{NEON}" '
f'stroke-opacity="0.45" stroke-width="1.6" stroke-dasharray="4 4" marker-end="url(#fl)"/>')
a(f'<text x="{W-92}" y="{(y_ab_fin+y_up)/2:.0f}" fill="{GRIS2}" font-size="11">generalista</text>')
# independiente: sube desde el bloque cian
a(f'<path d="M{igx+120:.0f},{y_in_fin+4:.0f} L{igx+120:.0f},{y_up-14}" fill="none" stroke="{CIAN}" '
f'stroke-opacity="0.6" stroke-width="1.6" marker-end="url(#flc)"/>')
a(f'<text x="{igx+132:.0f}" y="{y_up-20:.0f}" fill="{GRIS2}" font-size="11">independiente</text>')
a(f'<text x="46" y="{H-20}" fill="{GRIS2}" font-size="11">'
f'upleaks.org — software libre — gitea.laenre.net/hacklab/FLUJOS</text>')
a('</svg>')
io.open(sys.argv[1], 'w', encoding='utf-8').write('\n'.join(o))
print("escrito", sys.argv[1], "| alto util:", y_up + bh)

View file

@ -92,7 +92,7 @@
--- Ver variable de entorno y configuración MongoDB ---
cat /var/www/theflows.net/flujos/.env
cat $FLUJOS/.env
--- Listar colecciones en MongoDB ---
@ -129,33 +129,33 @@
--- Contar archivos en disco por sección ---
# NOTICIAS artículos
find /var/www/theflows.net/flujos/FLUJOS_DATOS/NOTICIAS/articulos/ -type f | wc -l
find $FLUJOS/FLUJOS_DATOS/NOTICIAS/articulos/ -type f | wc -l
# NOTICIAS archivos raw
find /var/www/theflows.net/flujos/FLUJOS_DATOS/NOTICIAS/archivos/ -type f | wc -l
find $FLUJOS/FLUJOS_DATOS/NOTICIAS/archivos/ -type f | wc -l
# WIKIPEDIA artículos
find /var/www/theflows.net/flujos/FLUJOS_DATOS/WIKIPEDIA/articulos_wikipedia/ -type f | wc -l
find $FLUJOS/FLUJOS_DATOS/WIKIPEDIA/articulos_wikipedia/ -type f | wc -l
# WIKIPEDIA tokenizados
find /var/www/theflows.net/flujos/FLUJOS_DATOS/WIKIPEDIA/articulos_tokenizados/ -type f | wc -l
find $FLUJOS/FLUJOS_DATOS/WIKIPEDIA/articulos_tokenizados/ -type f | wc -l
# TORRENTS total
find /var/www/theflows.net/flujos/FLUJOS_DATOS/TORRENTS/ -type f | wc -l
find $FLUJOS/FLUJOS_DATOS/TORRENTS/ -type f | wc -l
# WikiLeaks txt
find /var/www/theflows.net/flujos/FLUJOS_DATOS/TORRENTS/TORRENTS_WIKILEAKS_COMPLETO/txt/ -type f | wc -l
find $FLUJOS/FLUJOS_DATOS/TORRENTS/TORRENTS_WIKILEAKS_COMPLETO/txt/ -type f | wc -l
# WikiLeaks tokenizados
find /var/www/theflows.net/flujos/FLUJOS_DATOS/TORRENTS/TORRENTS_WIKILEAKS_COMPLETO/tokenized/ -type f | wc -l
find $FLUJOS/FLUJOS_DATOS/TORRENTS/TORRENTS_WIKILEAKS_COMPLETO/tokenized/ -type f | wc -l
--- Tamaño en disco por sección ---
du -sh /var/www/theflows.net/flujos/FLUJOS_DATOS/NOTICIAS/
du -sh /var/www/theflows.net/flujos/FLUJOS_DATOS/WIKIPEDIA/
du -sh /var/www/theflows.net/flujos/FLUJOS_DATOS/TORRENTS/
du -sh /var/www/theflows.net/flujos/FLUJOS_DATOS/MONGO/
du -sh $FLUJOS/FLUJOS_DATOS/NOTICIAS/
du -sh $FLUJOS/FLUJOS_DATOS/WIKIPEDIA/
du -sh $FLUJOS/FLUJOS_DATOS/TORRENTS/
du -sh $FLUJOS/FLUJOS_DATOS/MONGO/
--- Estimación de artículos únicos comparados (cálculo matemático) ---

187
README.md
View file

@ -1,116 +1,141 @@
# FLUJOS
# FLUJOS · UP-LEAKS
Software libre para analizar y visualizar flujos de información. Conecta noticias, documentos y eventos históricos para entender la historia como una sucesión de eventos y combatir la desinformación.
Cruza lo que publica la prensa con lo que dicen los documentos filtrados, y lo dibuja
como un grafo que se puede recorrer.
---
En marcha: **[upleaks.org](https://upleaks.org)**
![Flujos de información](INFO/DOCS/diagramas/flujos-de-informacion.svg)
## La idea
Cuatro agencias —Reuters, AFP, AP, EFE— abastecen a casi toda la prensa del planeta.
Abres diez periódicos distintos y lees el mismo despacho diez veces. Esa repetición
parece pluralidad y no lo es.
FLUJOS ingiere los dos circuitos a la vez: el de las cabeceras que replican teletipos y
el de la prensa independiente que produce fuente propia. Los cruza contra el archivo de
WikiLeaks y levanta una arista cada vez que una noticia y un documento filtrado hablan de
lo mismo.
El resultado no es un buscador. Es un mapa donde se ve qué se cuenta, quién lo cuenta y
qué había escrito sobre eso antes de que se contara.
## Qué hay dentro ahora mismo
| | |
|---|---|
| Noticias | 98.110 de 236 feeds en 5 idiomas |
| Artículos de Wikipedia | 28.266 |
| Documentos de WikiLeaks | 36.182 |
| Fragmentos vectorizados | 531.306 |
| Aristas del grafo | 115.035.059 |
## Arquitectura
![Arquitectura](INFO/DOCS/diagramas/arquitectura.svg)
Cinco procesos independientes que no se hablan entre sí. Cada uno marca su trabajo en el
documento (`traducido`, `procesado_ner`, `procesado_embedding`) y el siguiente recoge lo
que quedó pendiente. Si uno se cae, los demás siguen; cuando vuelve, retoma la cola donde
estaba. El estado vive en MongoDB, no en un orquestador.
- `coconews/workers/ingestor.py` — lee los 236 feeds cada 15 minutos
- `coconews/workers/translator.py` — traduce al español
- `coconews/workers/ner.py` — extrae entidades con spaCy
- `coconews/workers/embedder.py` — vectoriza con MiniLM multilingüe (384 dimensiones)
- `FLUJOS_DATOS/COMPARACIONES/motor_comparacion.py` — correlación semántica noticia↔leak
La documentación técnica de cada pieza está en [`INFO/DOCS/CONTEXT/`](INFO/DOCS/CONTEXT/),
y la [wiki](https://gitea.laenre.net/hacklab/FLUJOS/wiki) cuenta el conjunto.
## Requisitos
- Node.js >= 18.x
- Node.js >= 18
- Python >= 3.11
- MongoDB >= 6.x
### Instalar MongoDB (Ubuntu/Debian)
```bash
sudo apt install -y gnupg curl
curl -fsSL https://pgp.mongodb.com/server-6.0.asc | sudo gpg -o /usr/share/keyrings/mongodb-server-6.0.gpg --dearmor
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-6.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/6.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list
sudo apt update && sudo apt install -y mongodb-org
sudo systemctl enable --now mongod
```
---
- MongoDB >= 6
## Instalación
```bash
git clone https://gitea.laenre.net/hacklab/FLUJOS.git
cd FLUJOS
git checkout production
cp .env.example .env
```
### Variables de entorno
Todas las rutas se deducen de dónde esté el repo. Las variables de `.env` solo hacen
falta si quieres cambiar algo: puertos, la URL de MongoDB o dónde viven los datos.
```bash
cp .env.example FLUJOS/BACK_BACK/.env
# API de visualización
cd FLUJOS/BACK_BACK && npm install && npm start # :3000
# API de noticias
cd coconews/api && npm install && node server.js # :3100
# Workers
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
python3 -m spacy download es_core_news_sm
```
Edita `.env` con tus valores:
Con eso, la interfaz está en `http://localhost:3000`. La base arranca vacía: hay que
poblarla.
| Variable | Por defecto | Descripción |
|-------------|-------------------------------|------------------------------|
| `MONGO_URL` | `mongodb://localhost:27017` | URL de conexión a MongoDB |
| `DB_NAME` | `FLUJOS_DATOS` | Nombre de la base de datos |
| `PORT` | `3000` | Puerto del servidor |
### Dependencias Node.js
## Poblar la base
```bash
cd FLUJOS/BACK_BACK && npm install
cd ../VISUALIZACION && npm install
# Noticias: arranca el ciclo de ingesta y no para
python3 coconews/workers/import_feeds.py # carga el catálogo de feeds
python3 coconews/workers/ingestor.py
# Wikipedia y WikiLeaks
python3 FLUJOS_DATOS/WIKIPEDIA/main.py
python3 FLUJOS_DATOS/TORRENTS/procesar_torrents.py
# Vectorizar y correlacionar
python3 coconews/workers/backfill_embeddings.py
python3 FLUJOS_DATOS/COMPARACIONES/motor_comparacion.py --salida comparaciones_v2
```
### Dependencias Python
El motor de correlación acepta `--dry-run` para calibrar umbrales antes de escribir nada.
## Como servicio
Las unidades de `coconews/systemd/` son plantillas. El script las rellena con tu ruta,
tu usuario y tu binario de node:
```bash
cd ../../FLUJOS_DATOS
python3 -m venv myenv
source myenv/bin/activate
pip install transformers==4.10.0 tensorflow==2.17.0 scikit-learn==0.24.2 \
pandas==1.3.2 numpy==1.19.5 nltk==3.6.2 django obsei[all]
python FLUJOS_DATOS/manage.py migrate
sudo coconews/systemd/instalar.sh
sudo systemctl enable --now coconews-ingestor coconews-translator \
coconews-ner coconews-embedder coconews-api
```
---
## Arrancar
```bash
# Servidor principal
cd FLUJOS/BACK_BACK && npm start
# API Python (en otra terminal, con el venv activo)
cd FLUJOS/BACK_BACK && python flask_api/app.py
```
Disponible en `http://localhost:3000`
---
## Base de datos
La app lee de MongoDB (`FLUJOS_DATOS`). Las colecciones se pueblan con el pipeline de datos:
```bash
# 1. Scraping
python FLUJOS_DATOS/NOTICIAS/main_noticias.py
python FLUJOS_DATOS/WIKIPEDIA/main.py
python FLUJOS_DATOS/TORRENTS/procesar_torrents.py
# 2. Comparaciones
python FLUJOS_DATOS/COMPARACIONES/pipeline_completo.py
```
Consulta `FLUJOS_DATOS/DOCS/` para más detalle.
---
## Estructura
```
flujos/
FLUJOS/
├── FLUJOS/
│ ├── BACK_BACK/ # Servidor Express + Flask API + OBSEI
│ └── VISUALIZACION/ # Frontend (Three.js, force-graph)
└── FLUJOS_DATOS/
├── NOTICIAS/ # Scraper de noticias
├── WIKIPEDIA/ # Extracción Wikipedia
├── TORRENTS/ # Documentos WikiLeaks
├── COMPARACIONES/ # Similitud entre documentos
└── DOCS/ # Documentación técnica
│ ├── BACK_BACK/ API Express que sirve el grafo (:3000)
│ └── VISUALIZACION/ frontend, Three.js + 3d-force-graph
├── coconews/ ingesta de noticias: workers, API y unidades systemd
├── FLUJOS_DATOS/
│ ├── NOTICIAS/ scraper histórico de medios
│ ├── WIKIPEDIA/ extracción de artículos
│ ├── TORRENTS/ documentos de WikiLeaks
│ ├── COMPARACIONES/ correlación semántica
│ └── IMAGENES/ scraper y análisis de imágenes
└── INFO/DOCS/ documentación técnica y diagramas
```
## Envíos
Hay un buzón para periodistas y filtradores en `/journalist`. Los envíos se guardan fuera
del webroot, nunca se sirven como estáticos y no se registra la IP de quien envía.
---
*"Los gigantes vistos en perspectiva parecen marionetas."*
> No basta con hablar de cambio.
> Muéstrame el código.
Software libre. Ver [`VOICES.md`](VOICES.md).