Gama

Extrator de citações jurídicas em português do Brasil. Lê um documento judicial e marca onde está cada citação de jurisprudência e de lei, com o tipo de cada uma. É o extrator da nossa solução para o Desafio Caça-Alucinações (BRACIS 2026 × Jusbrasil), que verifica se as citações de um texto existem de verdade num acervo de decisões.

O nome homenageia Luiz Gama (1830–1882), advogado abolicionista que libertou centenas de pessoas nos tribunais citando a lei com precisão.

Esta é a versão v1.3: as 12 primeiras das 22 camadas do v1.2, treinadas para imitar as probabilidades do v1.2 (destilação). Dentro da solução, a saída no estilo dos documentos do desafio é a mesma do v1.2; em texto real fica no mesmo nível, e roda 1,5 vez mais rápido. O v1.2 continua disponível na tag v1.2.

Código, pipeline completo e experimentos: github.com/vinimlo/gama.

Do texto ao veredito. Um trecho de documento com três citações passa por cinco etapas: extrator Gama, guarda, normalização do ruído de OCR, índice do acervo e classificação. A primeira citação, com ruído de OCR, casa com um registro e sai como real; a segunda não casa com nenhum e sai como inventada; a terceira não tem número e sai como incompleta.

O que o modelo faz

Classificação de tokens no esquema BIO, com três tipos:

Rótulo O que marca Exemplo
JURIS precedente identificado por número, súmula ou tema REsp nº 1.234.567/SP, Súmula 7 do STJ
LEI dispositivo de lei, com o nome do diploma art. 927 do Código de Processo Civil
VAGA julgado citado sem número: tribunal, ano e relator julgado do STJ proferido em 2021 pela relatoria de Nancy Andrighi

As bordas seguem as convenções do gabarito do desafio: artigo e preposição antes da citação ficam fora, pontuação depois fica fora, a UF no fim do número entra.

O modelo só diz onde a citação está. Se ela existe ou foi inventada, quem decide é uma consulta exata ao acervo, feita pelo resto do pipeline. Um modelo que decidisse isso sozinho estaria chutando com fluência, que é justamente a alucinação que se quer pegar.

Como usar

O repositório traz o bio.py, o mesmo módulo que rotulou o treino e decodifica a saída:

import importlib.util

import torch
from huggingface_hub import hf_hub_download
from transformers import AutoModelForTokenClassification, AutoTokenizer

REPO, REV = "vinimlo/gama", "5f924ca2fa77c2aae6afe6d770c78ca4438f52f3"
tok = AutoTokenizer.from_pretrained(REPO, revision=REV)
modelo = AutoModelForTokenClassification.from_pretrained(REPO, revision=REV).eval()

spec = importlib.util.spec_from_file_location("bio", hf_hub_download(REPO, "bio.py", revision=REV))
bio = importlib.util.module_from_spec(spec)
spec.loader.exec_module(bio)

texto = ("Nesse sentido, invoca-se o REsp nº 1.234.567/SP e o art. 927 do Código de Processo Civil, "
         "bem como o julgado do STJ proferido em 2021 pela relatoria de Nancy Andrighi.")
enc = tok(texto, return_offsets_mapping=True, return_tensors="pt")
with torch.no_grad():
    rotulos = modelo(input_ids=enc["input_ids"],
                     attention_mask=enc["attention_mask"]).logits[0].argmax(-1).tolist()
offsets = [tuple(o) for o in enc["offset_mapping"][0].tolist()]
for ini, fim, tipo in bio.decodificar(offsets, rotulos, texto):
    print(tipo, texto[ini:fim])
# JURIS REsp nº 1.234.567/SP
# LEI art. 927 do Código de Processo Civil
# VAGA julgado do STJ proferido em 2021 pela relatoria de Nancy Andrighi

O contexto é de 8.192 tokens, o bastante para a maioria dos documentos numa passada só. Para documentos maiores, para a confiança por citação e para a versão mais recente do bio.py, use o extrator do repositório no GitHub (src/gama/extratores/neural.py).

Rodar a solução completa

Os pesos sozinhos só acham as citações. A solução inteira, com a guarda, a consulta ao acervo e a confiança calibrada, roda em Docker a partir do repositório no GitHub:

git clone https://github.com/vinimlo/gama && cd gama
bash run.sh --preparar                                      # com rede, uma vez
bash run.sh <caminho_db> <pasta_txt> <arquivo_saida.csv>    # sem rede

O primeiro comando constrói a imagem e baixa estes pesos na revisão fixa. O segundo lê a base SQLite do acervo e uma pasta de .txt, roda sem rede e grava o CSV no formato da submissão, com um JSON por documento ao lado. Usa a GPU se o Docker tiver o runtime NVIDIA; sem ela roda em CPU, a cerca de 1 s por documento.

A guarda da solução

A guarda. Eixo de confiança de 0,50 a 1,00 com o limiar em 0,95. Abaixo do limiar o trecho do modelo sai e entra o da régua, se ela achou algo ali; do limiar para cima vale o modelo. A mediana dos falsos positivos em ementa real fica em 0,83; o menor dos 4.447 acertos no estresse difícil fica em 0,9993. No estilo do desafio, 4.626 documentos saem iguais com e sem a guarda. Em texto real, 305 ementas, o F1 de extração vai de 0,620 a 0,818.

Na solução, o modelo não trabalha sozinho. Entre ele e o resto do pipeline há uma guarda, e os números "com a guarda" desta página são dela. Quem usar só os pesos obtém o "Gama sozinho" das tabelas.

A guarda existe porque o modelo foi treinado no estilo dos documentos do desafio e, fora dele, erra de um jeito previsível. Em ementa real ele pode tomar o "Rel. Min. Fulano, julgado em ..." que vem logo depois de um precedente numerado por uma referência vaga, ou marcar pedaços soltos como "Rel", "2011" e "DJe". Mas ele sabe quando hesita. O que separa esses erros dos acertos é a confiança do span: a média, sobre os tokens rotulados como citação, da probabilidade que o modelo deu ao rótulo escolhido. No estilo do desafio, o menor dos 4.447 acertos do v1.3 no estresse difícil tem 0,9993 (com o v1.2, nenhum de 34.171 acertos ficou abaixo de 0,98). Os falsos positivos do v1.3 em ementa têm mediana de 0,83.

São duas regras, nesta ordem:

  1. VAGA a até 2 caracteres de outro span sai. No molde do desafio, a referência vaga é uma frase própria, e a mais próxima de outra citação fica a 4 caracteres.
  2. Span com confiança abaixo de 0,95 sai. No lugar entra o span de uma régua de expressões regulares que cruza aquele trecho, se houver, desde que não cruze um span confiante do modelo.

No estilo do desafio nenhuma das duas dispara. Em 4.626 documentos, a saída é a mesma com e sem guarda, e a nota oficial não se mexe. Fora dele, sim. Nas 305 ementas do benchmark, o F1 de extração vai de 0,620 a 0,818. Num teste de 172 ementas sorteadas depois, fora de tudo o que foi usado para escolher as regras, o ganho foi +0,205 (IC95 +0,154 a +0,246). A implementação está em src/gama/extratores/guarda.py no GitHub; o extrator da solução, --extrator neural, já a inclui.

Resultados

v1.3 contra v1.2, dentro da solução

Solução completa (extrator, guarda, resolver, confiança calibrada) com cada versão do modelo.

v1.2 v1.3
Dev, estresse difícil e 4.000 sintéticos (4.626 documentos), métrica oficial 1,10000 / 1,09999 / 1,09999 iguais, com o JSON final idêntico documento a documento
Texto real, F1 de extração, 172 ementas separadas antes da escolha da guarda 0,820 0,839 (+0,019; IC95 +0,003 a +0,033)
Tempo por documento, NVIDIA L4 / CPU, medidos lado a lado 0,071 s / 1,62 s 0,046 s / 0,93 s
Parâmetros / pesos em FP32 307,5M / 1,23 GB 257,4M / 1,03 GB

As 172 ementas foram separadas antes da escolha da guarda e mediram a guarda uma vez só. Depois elas também serviram para comparar os alunos e escolher o v1.3, então para a diferença entre v1.2 e v1.3 são dado de desenvolvimento, não um teste independente. E retreinado com outras sementes o v1.2 varia mais do que essa diferença (0,785 a 0,842 nas 172, com a guarda).

v1.3 contra extratores sem treino

Gama contra extratores sem treino: no estresse difícil, Gama v1.3 com a guarda 1,09999, sozinho 1,09999, régua 0,84880, Qwen3-8B 0,81065, GLiNER 2.5 0,44445; em texto real, Gama v1.3 com a guarda 0,818, sozinho 0,620, régua 0,663, Qwen3-8B 0,707, GLiNER 2.5 0,608

Métrica oficial do desafio, macroF1 · (1 − 0,5·τ) · (1 + 0,10·(1 − Brier)), com o mesmo resolver para todos os extratores. O teto prático é 1,10.

Conjunto Gama v1.3 Gama v1.3 com a guarda da solução Régua (regex) Qwen3-8B zero-shot GLiNER 2.5 zero-shot
Dev da organização, 26 documentos 1,10000 1,10000 1,09848 não medido¹ não medido¹
Estresse difícil, 600 documentos, métrica oficial 1,09999 1,09999 0,84880 0,81065 0,44445
Texto real, 305 ementas, F1 de extração 0,620 0,818 0,663 0,707 0,608
Tempo por documento, NVIDIA L4 0,035 s 0,037 s 0,001 s 11,7 s 0,110 s

¹ Os documentos do dev são da organização e não saem da nossa máquina.

O estresse difícil segue o estilo dos documentos do desafio, com frases escritas por um LLM que nenhum modelo viu no treino e ruído de OCR forte. Nele o fine-tune é a diferença: o melhor extrator sem treino fica abaixo até da régua. O texto real são ementas do STF, STJ e TJRJ (celsowm/jurisprudencias_br, CC-BY-4.0), com citações anotadas por dois LLMs e adjudicadas por critério escrito. Ali a ordem se inverte: um LLM genérico de 8B extrai melhor que o modelo sozinho, a um custo cerca de 330 vezes maior por documento. A guarda da solução troca os trechos em que o modelo hesita (confiança < 0,95) pelos da régua e põe o Gama de volta na frente.

Na fase de treino do Kaggle, a solução completa com o v1.2 marcou 1,09999.

v1.3 contra modelos treinados nos mesmos dados

O gráfico acima compara o Gama com extratores sem treino. Para separar o que vem do fine-tune do que vem da escolha do modelo, treinamos concorrentes nos mesmos documentos sintéticos do v1.2 e medimos todos pelo mesmo harness, sozinhos e com a guarda da solução. F1 de extração em texto real; as 305 ementas do benchmark escolheram os limiares, e as 172 separadas depois só confirmam.

Texto real sozinho, 305 / 172 Texto real com a guarda, 305 / 172 Estresse difícil sozinho, métrica oficial
Gama v1.3 0,620 / 0,635 0,818 / 0,839 1,09999
BERTimbau, mesma receita do v1.2 0,697 / 0,705 0,813 / 0,826 1,09999
GLiNER 2.5 multi treinado 0,808 / 0,824 0,816 / 0,837 1,09615
mmBERT-base com o encoder congelado, só a saída treinada 0,243 / 0,247 0,350 / 0,353 0,90182

Sozinhos, BERTimbau e GLiNER treinados extraem melhor que o Gama em texto real. Com a guarda, nenhum passa o v1.3. O GLiNER 2.5 treinado chega ao mesmo nível sem guarda nenhuma (−0,010 nas 305 e −0,015 nas 172, IC95 cruzando zero), porque já generaliza fora do molde; a guarda quase não o ajuda, já que a maior parte dos erros dele sai com score acima de 0,99. O Gama especializa no estilo do desafio e compensa sabendo quando hesita, e no estilo do desafio segue na frente (1,09999 contra 1,09615). Uma semente por modelo: não é equivalência. Até 29/09 estas tabelas usavam o GLiNER multi v2.1, que era pior nos dois papéis.

O encoder congelado vai longe no estilo do desafio e desaba em texto real. O fine-tune rende pela adaptação do encoder. O mmBERT-base original, com uma cabeça de rótulos sem treino, dá F1 de 0,0005: ruído, como se esperava.

Retreinamos também o v1.2 e o v1.3 com as sementes 7 e 21. Com a guarda, o v1.2 vai de 0,760 a 0,830 nas 305 ementas; o v1.3, destilado do mesmo professor, fica entre 0,812 e 0,818. No estilo do desafio as seis sementes dão o mesmo JSON final no dev e 1,09999 no estresse com a guarda.

Todo número de texto real aqui usa um gabarito de LLM adjudicado, e cada concorrente teve uma semente. Um teste com gabarito humano, num conjunto que não participou de nenhuma escolha, ainda não foi feito. Desenho e intervalos: controles.

Treino

De onde vêm os dados e os pesos. O dev set da organização, que não sai da máquina, é desmontado em bancos de frases; LLMs abertos só ampliam as frases; o gerador por moldes produz o gama-goldenset, com 6.000 documentos na pasta final_v3. O mmBERT-base na revisão c595503 é ajustado nesses documentos e vira o Gama v1.2, revisão ad06ffd; o v1.2 é destilado nas 12 primeiras camadas, com os mesmos documentos e 1.818 ementas reais sem rótulo do celsowm/jurisprudencias_br, e vira o Gama v1.3, revisão 5f924ca.

v1.3, destilado do v1.2:

  • Aluno: as camadas 0 a 11 do v1.2, com embeddings e cabeça herdados. A poda das camadas de cima segue Sajjad et al., 2004.03844.
  • Perda: 0,5·CE(ouro) + T²·KL(professor/T ‖ aluno/T) + 0,5·KL(professor ‖ aluno), T = 2, com o v1.2 rodando no próprio treino. O aluno copia as probabilidades, não só o rótulo, porque a solução usa a confiança do modelo.
  • Dados: os mesmos 6.000 documentos sintéticos e 1.818 ementas reais sem rótulo, onde só vale a imitação do v1.2. As ementas vêm do celsowm/jurisprudencias_br, que o Celso F. publica sob CC-BY-4.0 com decisões do STF, do STJ e do TJRJ coletadas pelo Juriscraper; nenhuma delas está nos testes. Foi nesse texto que o limite do v1.2 fora dos moldes apareceu, e é nele que o v1.3 aprende a reproduzir a confiança do professor fora do estilo do desafio, que é de onde a guarda lê.
  • Hiperparâmetros: 3 épocas, max_len 2048, lote 8, taxa de aprendizado 5e-5, semente 13.
  • Hardware: 1× A100 80 GB, via HF Jobs, 18 minutos. Script: treino/destilar.py no GitHub.

v1.2, o professor:

  • Base: jhu-clsp/mmBERT-base (MIT), revisão c5955035435e2bf121cde7f3c8863ef52ff35d82.
  • Dados: 6.000 documentos sintéticos, nenhum real. Os documentos do desafio saem de um gerador por moldes; desmontamos o dev set em bancos de peças e remontamos documentos novos citando fichas reais do acervo, com rótulo exato por construção. LLMs de pesos abertos (DeepSeek-V4-Pro, Kimi-K3) só ampliaram os bancos de frases, sem escrever citação nem rótulo. Um injetor de ruído de OCR completa o conjunto.
  • Hiperparâmetros: 3 épocas, max_len 1024, lote 8, taxa de aprendizado 5e-5, semente 13.
  • Hardware: 1× A100 80 GB, via HF Jobs. Script: treino/treinar.py no GitHub.

O comando exato de cada passo, dos bancos de frases aos pesos, está no guia de reprodução.

Limitações

  • O modelo especializou no estilo dos documentos do desafio. Em ementas reais, com citação em lista depois de "DJe", entre parênteses ou em caixa-alta, ele marca fragmentos soltos ("Rel", "2011", "DJe"); sozinho, fica atrás da régua de regex (0,620 contra 0,663). Na solução, a guarda troca os trechos em que o modelo hesita pelos da régua, e o F1 em texto real sobe para 0,818 com o v1.3.
  • Sozinho, o modelo não é o melhor extrator em texto real: um BERTimbau e um GLiNER 2.5 treinados nos mesmos dados passam dele. A vantagem da solução vem da guarda, que depende da confiança do modelo.
  • Só português do Brasil e formatos de citação dos tribunais brasileiros.
  • Sozinho, não verifica nada: achar a citação é metade do trabalho, a outra metade é a consulta ao acervo.

Reprodutibilidade

A revisão acima é fixa e é a que a solução usa. A inferência é determinística (eval(), sem amostragem, algoritmos determinísticos do torch) e dá a mesma saída em GPU e em CPU. Os dados de treino estão em vinimlo/gama-goldenset, nas revisões 31474b1 (v1.2) e ec430c0 (v1.3).

Referências

A lista comentada está em literatura.

Como citar

@misc{gama2026,
  title        = {Gama: extrator de citações jurídicas do Desafio Caça-Alucinações (BRACIS 2026 × Jusbrasil)},
  author       = {Melo, Vinícius and Siron, Gabriel},
  year         = {2026},
  howpublished = {\url{https://huggingface.co/vinimlo/gama}},
  note         = {v1.3, revisão 5f924ca}
}

Equipe

Vinícius Melo e Gabriel Siron, para o Desafio Caça-Alucinações (BRACIS 2026 × Jusbrasil).

In English

Gama is a token-classification model (BIO: JURIS case-law citations, LEI statute provisions, VAGA unnumbered references by court, year and rapporteur) for Brazilian Portuguese legal documents. Version v1.3 keeps the first 12 of the 22 layers of v1.2 (fine-tuned from mmBERT-base on 6,000 synthetic documents) and is distilled from it: inside the full pipeline it gives the same outputs on challenge-style documents, the same level on real court summaries (within seed variance), and runs 1.5 times faster. It only locates citations; whether a citation exists is decided by an exact lookup in a frozen collection of decisions. On challenge-style documents it clearly beats zero-shot open models (Qwen3-8B, GLiNER 2.5); on real court summaries outside that style, a zero-shot Qwen3-8B extracts better than the model alone at about 330 times the cost, and the pipeline's guard (falling back to rules where the model is unsure) puts Gama back ahead. Trained on the same synthetic data, BERTimbau and a fine-tuned GLiNER 2.5 extract better than the model alone on real court summaries, but neither beats v1.3 once the guard is on (GLiNER 2.5 reaches the same level with no guard at all, within one seed); with a frozen encoder and only the output layer trained, mmBERT-base collapses on real text (F1 0.243), so the gain comes from adapting the encoder. Real-text gold labels are LLM-adjudicated, not human. The full pipeline runs in Docker from the GitHub repository with bash run.sh --preparar (network, once) and bash run.sh <db_path> <txt_folder> <output.csv> (offline). Built for the BRACIS 2026 × Jusbrasil hallucination detection challenge.

Downloads last month
101
Safetensors
Model size
0.3B params
Tensor type
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for vinimlo/gama

Finetuned
(162)
this model

Dataset used to train vinimlo/gama

Papers for vinimlo/gama