docs: issues 0041 (split thresholds) + 0042 (GLiNER2), supersedes mREBEL

Cierra el ciclo del analysis gliner_glirel_tuning: documenta en app.md el
pipeline NER+RE disponible en el registry y abre los dos issues que faltan
para cablearlo en extract_graph_hybrid + panel paste_extract. Archiva el
0042 original (mREBEL) tras la decision a favor de GLiNER2 (Apache 2.0,
joint NER+RE, 20-30x mas rapido en CPU).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-05 10:07:43 +02:00
parent 3c98fee443
commit 5e6023f639
4 changed files with 498 additions and 0 deletions
@@ -0,0 +1,174 @@
---
id: 0042
title: Sustituir GLiREL por mREBEL como extractor de relaciones (uso no comercial)
status: pending
priority: high
created: 2026-05-04
parent: 0013
depends_on: [0041]
---
## Contexto
Tras integrar GLiREL en `extract_graph_hybrid` (issue 0013), el analisis empirico (`projects/osint_graph/analysis/gliner_glirel_tuning/`) revelo que GLiREL falla estrepitosamente sobre narrativa empresarial castellana: 51 falsos positivos a `t=0.15`, 1 unica relacion (tambien falsa) a `t=0.30`. **No hay sweet spot**.
Probado el sustituto **`Babelscape/mrebel-large`** (mBART seq2seq que genera tripletas directamente del texto) sobre el mismo corpus: **8 tripletas crudas, 5 alineables con entidades GLiNER, 4 inequivocamente correctas, cero falsos absurdos**.
Decision documentada en `vaults/osint_nlp_models/decisions/2026-05-04-mrebel-over-glirel.md`.
## Restriccion de licencia
mREBEL es **CC BY-NC-SA 4.0 — uso no comercial**. Compatible con `graph_explorer` actual (uso personal de investigacion). Bloqueante si pasa a producto comercial. Plan de contingencia documentado en `vaults/osint_nlp_models/decisions/2026-05-04-license-constraint.md`. **Hay que marcar la restriccion en el codigo y en el `app.md`.**
## Objetivo
Anadir mREBEL como capa 4 alternativa en `extract_graph_hybrid`, configurable por el usuario en el panel `paste_extract`. GLiREL no se borra — queda disponible para textos en ingles donde su comportamiento puede ser razonable.
## Diseño
### 1. Funciones nuevas en el registry
```
python/functions/datascience/
├── mrebel_load_model.py # carga + cache (mBART, src_lang, tgt_lang)
├── extract_relations_mrebel.py # frase a frase, devuelve list[RelationCandidate]
└── _mrebel_parser.py # parser del wire format <triplet>...</s>
```
Plantilla `mrebel_load_model.py`:
```python
"""LICENSE: CC BY-NC-SA 4.0 — non-commercial use only.
Carga (y cachea) Babelscape/mrebel-large para extraccion de relaciones.
"""
from typing import Any
_MODEL_CACHE: dict[tuple[str, str], Any] = {}
def mrebel_load_model(
model_name: str = "Babelscape/mrebel-large",
src_lang: str = "es_XX",
tgt_lang: str = "tp_XX",
) -> tuple[Any, Any]:
key = (model_name, src_lang)
if key in _MODEL_CACHE: return _MODEL_CACHE[key]
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
tok = AutoTokenizer.from_pretrained(model_name, src_lang=src_lang, tgt_lang=tgt_lang)
mdl = AutoModelForSeq2SeqLM.from_pretrained(model_name)
_MODEL_CACHE[key] = (tok, mdl)
return tok, mdl
```
`extract_relations_mrebel.py` toma `text`, `entities` (de GLiNER), trocea por frases con `re.split`, llama a mREBEL frase a frase, parsea tripletas, hace **string-match con `entity.name`**, y devuelve `list[RelationCandidate]` con `relation_type` igual al `type` que emite mREBEL (vocabulario Wikidata). Las tripletas sin match se descartan silenciosamente.
### 2. Cambio en `extract_graph_hybrid.py`
Anadir parametro `relation_engine` con tres valores:
```python
def extract_graph_hybrid(
chunks, entity_schema, relation_types,
gliner_model,
relation_engine: str = "mrebel", # 'glirel' | 'mrebel' | 'none'
glirel_model=None, # solo si engine == 'glirel'
mrebel_models=None, # tuple (tok, mdl) si engine == 'mrebel'
llm_chat_json=None,
entity_threshold=0.5,
relation_threshold=0.15,
):
...
if relation_engine == "glirel":
rels = extract_relations_glirel(chunk, ents, relation_types, glirel_model, threshold=relation_threshold)
elif relation_engine == "mrebel":
rels = extract_relations_mrebel(chunk, ents, mrebel_models)
elif relation_engine == "none":
rels = []
...
```
**`relation_types` es ignorado** cuando `engine == 'mrebel'` (el modelo emite vocab Wikidata cerrado). Quiza convenga dejar un parametro opcional `accept_relation_types` que filtre la salida si el usuario quiere taxonomia controlada.
### 3. Cambio en `enrichers/paste_extract/run.py`
```yaml
# manifest
params:
text: { type: string, required: true }
use_hybrid: { type: bool, default: true }
relation_engine: { type: enum, values: [glirel, mrebel, none], default: mrebel }
entity_threshold: { type: float, default: 0.5 }
relation_threshold: { type: float, default: 0.15 } # solo aplicable si engine=glirel
```
### 4. Cambios en el panel C++ (`extract_panel.cpp`)
- Combo `Relation engine: [GLiREL | mREBEL | None]` debajo del toggle hybrid.
- **Banner de licencia** cuando `engine == mREBEL`: amarillo, texto fijo `"mREBEL: CC BY-NC-SA — non-commercial use only"`.
- **Barra de progreso por frase** cuando engine=mREBEL (latencia ~3s/frase). El subprocess emite `PROGRESS:0.X frase_N` cada frase, el panel lo lee y refresca un `ImGui::ProgressBar`.
### 5. `app.md` — declarar la restriccion
```yaml
---
...
notes: |
El motor de relaciones por defecto es mREBEL (Babelscape/mrebel-large),
bajo licencia CC BY-NC-SA 4.0. NO usar en contextos comerciales sin
cambiar el motor a GLiREL (Apache 2.0) o LLM externo.
optional_dependencies:
- "transformers (mREBEL/GLiREL)"
- "sentencepiece (mBART tokenizer)"
---
```
### 6. Tests pytest
`tests/test_extract_relations_mrebel.py`:
- Test parser de output mREBEL (formato `<triplet> ... </s>`).
- Test string-match con entidades fuzzy (substring + exact).
- Test que tripletas sin match desaparecen.
- Test smoke con stub de modelo (mock que retorna decoded canónico).
- Test integracion: pasar texto ES → ents GLiNER fake → tripletas alineadas.
`tests/test_paste_extract.py`: añadir test del path `relation_engine=mrebel`.
### 7. Documentacion
- `app.md` — seccion de relaciones explica los 3 engines + licencia.
- `python/functions/datascience/mrebel_load_model.md` — frontmatter completo + restriccion de licencia.
## Definicion de hecho
- Pego el texto del corpus es_corporate, selecciono `engine=mREBEL`, pulso Extract.
- Tras ~25s veo las 4 relaciones limpias en la tabla (Pablo Isla→Inditex, Endesa→Marina Serrano, BBVA→Carlos Torres, Arteixo→A Coruna).
- Cambio a `engine=GLiREL` y veo las 51 espurias (validacion contraria).
- Apply solo guarda las marcadas; dedupe sigue funcionando.
- Banner de licencia amarillo es visible cuando engine=mREBEL.
- Tests verdes en WSL.
## Latencia / UX
- mREBEL es ~50× mas lento que GLiREL (3s/frase vs 50ms/grafo entero).
- Para texto largo (20+ frases) son ~60s. **Barra de progreso es obligatoria.**
- Plan: emitir `PROGRESS:f frase=N/M` cada frase; panel C++ refresca ProgressBar.
- Cancelacion: boton "Cancel" mata el subprocess limpiamente (issue futuro si no es trivial).
## Out of scope (issues separados)
- LLM como validator semantico de tripletas mREBEL — futuro.
- mREBEL en GPU (issue de infra, requires CUDA). En CPU es usable.
- Mapeo del vocabulario Wikidata a una taxonomia OSINT custom — issue futuro.
- Cancelacion del subprocess (Cancel button) — micro-issue.
## Riesgos
- **Latencia.** El usuario puede sentir que la app esta colgada sin la barra de progreso. Mitigacion: progress por frase + tip "modelo grande, primer texto descarga 2.4 GB".
- **Cobertura del vocabulario Wikidata** en dominios fuera de business/journalism. Para narrativa funciona; para OSINT, drama tipico, puede no encajar — habra que probar y posiblemente añadir mapeo custom (issue futuro).
- **Licencia.** Reducible solo via diseño claro de UI (banner) + documentacion (app.md, vault).
## Relacionado
- `vaults/osint_nlp_models/models/mrebel.md` — observaciones empiricas
- `vaults/osint_nlp_models/decisions/2026-05-04-mrebel-over-glirel.md` — ADR
- Notebooks 01, 02, 03 en `analysis/gliner_glirel_tuning/`