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