---
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/`
