OPF · Privacy Filter · Português Brasileiro · LGPD
Identificação de informações pessoais em documentos em português
Fine-tune do openai/privacy-filter em 914 mil amostras PT-BR — 73 rótulos
cobrindo dados pessoais sensíveis nos termos do art. 5º, II da Lei nº 13.709/2018, com
placeholder de substituição já emitido pelo decoder Viterbi constrained.
""")
gr.Markdown(DESCRIPTION, elem_id="intro")
with gr.Row():
with gr.Column(scale=2):
input_text = gr.Textbox(
label="Texto para análise",
placeholder="Cole aqui o trecho a ser revisado — peça processual, contrato, e-mail, relatório...",
lines=10,
)
with gr.Column(scale=1):
discard_overlapping = gr.Checkbox(
label="Descartar spans sobrepostos do mesmo rótulo",
value=False,
info="Quando marcado, o cabeçalho do resumo sinaliza a opção.",
)
with gr.Row():
labels_filter = gr.Dropdown(
choices=ALL_LABELS,
value=PTBR_LABELS,
label="Rótulos considerados",
info="Padrão: 24 labels PT-BR canônicos. Deixe vazio para usar todos os 73.",
multiselect=True,
)
btn = gr.Button("Analisar texto", variant="primary", elem_id="analyze-btn")
output_highlight = gr.HighlightedText(
label="Texto anotado",
combine_adjacent=False,
show_legend=True,
elem_id="highlight-block",
)
with gr.Group(elem_id="summary-block"):
output_summary = gr.Markdown(label="Detalhamento por categoria")
btn.click(predict,
[input_text, labels_filter, discard_overlapping],
[output_highlight, output_summary])
with gr.Group(elem_id="examples-block"):
gr.Examples(
examples=EXAMPLES,
inputs=[input_text, labels_filter, discard_overlapping],
outputs=[output_highlight, output_summary],
fn=predict,
cache_examples=False,
examples_per_page=6,
label="Exemplos de referência (textos construídos manualmente, fora do conjunto de treino)",
)
with gr.Group(elem_id="ref-card"):
gr.Markdown("""
### Desempenho do modelo
Ranking apples-to-apples (mesma val PT-BR 5k, mesmas 24 labels canônicas, nervaluate):
| Modelo | partial F1 | exact F1 |
|---|---|---|
| **opf-finetune (este)** | **0.897** | 0.853 |
| mmBERT-small × 3 | 0.888 | **0.870** |
| ettin-68m | 0.822 | 0.789 |
| ettin-32m | 0.808 | 0.769 |
### Recipe
| Item | Detalhe |
|---|---|
| Modelo | `arthrod/gliner-opf-ptbr-pii-v1` (privado) |
| Backbone | openai/privacy-filter (8L sparse-MoE, 128 experts, top-4 routing) |
| Treino | 9-round chunked sweep (3 ép × 3 saves), 914 mil linhas, ~3h45m em MI300X |
| Decoder | Viterbi constrained (BIESO) |
| Best ckpt | `e3_c3` — typed F1 0.885 (P 0.894 / R 0.876) |
| Batch / ctx | 32 janelas × grad-accum 4 (efetivo 128), n-ctx 256, bf16 |
| Optimizer | AdamW lr 1e-5, wd 0.01, max-grad-norm 1.0 |
### Diferenças vs GLiNER (mmBERT/ettin)
- Vocabulário de **73 labels** vs 24 (inclui SSN, passport, IP, MAC, swift_bic, etc.)
- Inferência via Viterbi constrained (BIESO) em vez de token-level argmax
- Emite `placeholder` pronto para substituição — não apenas o rótulo
- Sem easter-egg `berco-de-tiradentes` (use o demo mmBERT/ettin para isso)
### Catálogo de rótulos
**Documento oficial / identidade** — `cpf_document_number`, `rg_document_number`, `pis_document_number`, `id_card_number`, `passport_number`, `driver_license_number`, `social_security_number`, `tax_id_number`, `certificate_license_number`
**Dado financeiro** — `credit_card`, `account_number`, `bank_routing_number`, `swift_bic`, `cvv`, `pin`, `customer_id`
**Contato** — `phone_number`, `email_address`, `fax_number`, `url`, `ipv4`, `ipv6`, `mac_address`
**Nome** — `first_name`, `middle_name`, `last_name`, `user_name`, `title`
**Data de nascimento / idade** — `dob`, `date`, `time`, `date_time`, `age`
**Localização** — `location_street`, `location_building_number`, `location_neighborhood`, `location_city`, `location_state`, `location_state_abbreviation`, `location_zip`, `location_full_address`, `country`, `county`, `coordinate`
**Dado pessoal sensível (LGPD art. 5º, II)** — `personal_description_of_ethnicity`, `personal_description_of_medical_conditions`, `personal_description_of_organizational_affiliation`, `personal_description_of_political_opinion`, `personal_description_of_religious_convictions`, `personal_description_of_sexual_information`, `religious_belief`, `political_view`, `sexuality`, `sex_or_gender`, `biometric_identifier`, `blood_type`
**Credencial / identificador técnico** — `password`, `api_key`, `http_cookie`, `device_identifier`, `unique_id`, `employee_id`, `vehicle_identifier`, `license_plate`, `medical_record_number`, `health_plan_beneficiary_number`
**Contexto profissional** — `occupation`, `employment_status`, `education_level`, `company_name`, `language`
""")
# Inject expansion script as a child HTML component — Gradio 5.40's
# Blocks.launch() does not accept a `head` kwarg, but a rendered
""")
if __name__ == "__main__":
demo.queue(max_size=10).launch()