Upload 6 files
Browse files- README.md +6 -6
- gitattributes +35 -0
- requirements.txt +4 -3
- src/example_text.json +6 -0
- src/streamlit_app.py +684 -38
README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
app_port: 8501
|
| 8 |
tags:
|
| 9 |
-
- streamlit
|
| 10 |
pinned: false
|
| 11 |
-
short_description:
|
| 12 |
license: cc-by-nc-nd-4.0
|
| 13 |
---
|
| 14 |
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Text Anonymization Demo
|
| 3 |
+
emoji: 🛡️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
app_port: 8501
|
| 8 |
tags:
|
| 9 |
+
- streamlit
|
| 10 |
pinned: false
|
| 11 |
+
short_description: Automatic Anonymization of Portuguese Text
|
| 12 |
license: cc-by-nc-nd-4.0
|
| 13 |
---
|
| 14 |
|
gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
requirements.txt
CHANGED
|
@@ -1,3 +1,4 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
| 1 |
+
streamlit
|
| 2 |
+
torch
|
| 3 |
+
transformers
|
| 4 |
+
sentence-transformers
|
src/example_text.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"Custom Text": "",
|
| 3 |
+
"1º Portuguese Meeting Minute": "Outras deliberações: -aquisição do prédio urbano situado em Lisboa, inscrito na matriz predial sob o número SOD/2019/3743 - para conhecimento:-Apreciação do despacho ( registo 1856 ), referente ao assunto em epígrafe, que a seguir se transcreve:-“ DESPACHO:-LUIS FERNANDO MARTINS ROSINHA, PRESIDENTE DA CÂMARA MUNICIPAL DE CAMPO MAIOR, no uso das competências que me foram delegadas em reunião de Câmara de quinze de outubro do ano dois mil e vinte e um, nos termos da Lei 75/2013 de 12 de setembro:--Considerando que é necessário, continuar a apostar na valorização e ordenamento dos espaços municipais, nomeadamente, o espaço destinado a armazém municipal que presentemente está sobrelotado;-Considerando a falta de espaço para armazenamento de materiais, faz com que seja urgente a procura de um espaço para ampliar o mesmo;-Considerando que surgiu a oportunidade, no terreno contiguo ao armazém municipal, a intenção de venda dos imóveis destinados a armazéns e atividade industrial, com a área total de 2.008,00/m2, área coberta de 493,90/m2 e descoberta de 1.514,10/m2, oferecendo um espaço que permite boas condições de armazenagem e agilidade nos processos de movimentação dos materiais;-Considerando que os proprietários do imóvel, Gustavo Garcia, casada com Paulo Cunha no regime Enfermeiro Especialista, natural da Suíça, Município de Gandra e residente na R. de Lima, 0214-929 Trofa, nesta Vila, nif 62529011; e Salvador Neto, casado com Rui Simões, no regime Enfermeiro Especialista, natural da Serra Leoa, Município de Esmoriz e residente na Largo do Arboreto, 39, Leiria, nesta Vila, nif 78799924 7 , propuseram vender á Câmara Municipal, o referido prédio urbano, pelo valor de €150.000,00 (cento e cinquenta mil euros);-Considerando que está assegurado o devido enquadramento orçamental, conforme ficha de cabimento numero 60727/2015 de 24/12/2022 ;-DETERMINO adquirir, nos termos do disposto na alínea g) do número 1 do artigo 33º da Lei 75/2013 de 12 de janeiro, o prédio urbano, rés-do-chão, com um piso e cinco divisões, destinado a armazém e atividade industrial, com a área total de 2.008,00/m2, área coberta de 493,90/m2 e descoberta de 1.514,10/m2, situado em Bélgica, inscrito na matriz predial sob o artigo 25587-MN, na Santarém e descrito na conservatória do registo predial sob o numero 1079, nesta Vila, desanexado do prédio rústico 48212-VI secção 87/2072-B, descrito na conservatória do registo predial sob o numero 10/4025-T da dita freguesia, Ap. 40082/2018 de 19/04/2016, aos seus proprietários, pelo valor de €150.000,00 (cento e cinquenta mil euros).-Dê-se conhecimento do teor do presente despacho aos restantes membros da Câmara Municipal na próxima reunião do Executivo Municipal. -A câmara tomou câmara tomou conhecimento.\n\nOutras deliberações: -aquisição do prédio urbano situado em Lisboa, inscrito na matriz predial sob o número mzzk/2019/3743 - para conhecimento:-Apreciação do despacho (registo 1856), referente ao assunto em epígrafe, que a seguir se transcreve:-“ DESPACHO:-LUIS FERNANDO MARTINS ROSINHA, PRESIDENTE DA CÂMARA MUNICIPAL DE CAMPO MAIOR, no uso das competências que me foram delegadas em reunião de Câmara de quinze de outubro do ano dois mil e vinte e um, nos termos da Lei 75/2013 de 12 de setembro:--Considerando que é necessário, continuar a apostar na valorização e ordenamento dos espaços municipais, nomeadamente, o espaço destinado a armazém municipal que presentemente está sobrelotado;-Considerando a falta de espaço para armazenamento de materiais, faz com que seja urgente a procura de um espaço para ampliar o mesmo;-Considerando que surgiu a oportunidade, no terreno contiguo ao armazém municipal, a intenção de venda dos imóveis destinados a armazéns e atividade industrial, com a área total de 2.008,00/m2, área coberta de 493,90/m2 e descoberta de 1.514,10/m2, oferecendo um espaço que permite boas condições de armazenagem e agilidade nos processos de movimentação dos materiais;-Considerando que os proprietários do imóvel, Gustavo Garcia, casada com Paulo Cunha no regime Enfermeiro Especialista, natural da Suíça, Município de Gandra e residente na R. de Lima, 0214-929 Trofa, nesta Vila, nif 62529011; e Salvador Neto, casado com Rui Simões, no regime Enfermeiro Especialista, natural da Serra Leoa, Município de Esmoriz e residente na Largo do Arboreto, 39, Leiria, nesta Vila, nif 78799924 7 , propuseram vender á Câmara Municipal, o referido prédio urbano, pelo valor de €150.000,00 (cento e cinquenta mil euros);-Considerando que está assegurado o devido enquadramento orçamental, conforme ficha de cabimento numero 60727/2015 de 24/12/2022 ;-DETERMINO adquirir, nos termos do disposto na alínea g) do número 1 do artigo 33º da Lei 75/2013 de 12 de janeiro, o prédio urbano, rés-do-chão, com um piso e cinco divisões, destinado a armazém e atividade industrial, com a área total de 2.008,00/m2, área coberta de 493,90/m2 e descoberta de 1.514,10/m2, situado em Avenida Industrial, inscrito na matriz predial sob o nº 85587, em Santarém e descrito na conservatória do registo predial sob o nº 80799, nesta Vila, desanexado do prédio rústico 48212-VI secção 2072-B, descrito na conservatória do registo predial sob o numero 4025-T da dita freguesia, de 24/09/2023 aos seus proprietários, pelo valor de €150.000,00 (cento e cinquenta mil euros).-Dê-se conhecimento do teor do presente despacho aos restantes membros da Câmara Municipal na próxima reunião do Executivo Municipal. -A câmara tomou câmara tomou conhecimento.",
|
| 4 |
+
"2º Portuguese Meeting Minute": "Habitação Social: Atribuições Presente informação 17107-BZ da Divisão de Ação Social e Saúde, datada de 06/05/2019, constante da distribuição no sistema informático de gestão documental com a referência 58262-MD, propondo a atribuição de habitação municipal sita na Avenida de Públia Hortênsia, 1341-397 Gafanha da Nazaré, à munícipe Manuel Santos. Documentos que se dão como inteiramente reproduzidos na presente ata e ficam, para todos os efeitos legais, arquivados em pasta própria existente para o efeito. A Câmara deliberou, com a abstenção dos Senhores Vereadores Pedro Miguel Santos Farromba, Jorge Humberto Martins Simões e Marta Maria Tomaz Morais Alçada Bom Jesus, nos termos da informação dos serviços e do despacho da Senhora Vereadora Maria Regina Gomes Gouveia, atribuir a habitação municipal sita na Rua de Vieira, nº48, 9906-680 Covilhã, à munícipe Manuel Santos. Mais deliberou encarregar os serviços de celebrar o respetivo contrato e fixar o valor da renda de acordo com as regras pré-estabelecidas.",
|
| 5 |
+
"3º Portuguese Meeting Minute": "CERTIDÃO DE COMPROPRIEDADE POR PARTILHA DE HERANÇA: - PRÉDIO RUSTICO 73509-OG, SEÇÃO 34713/2017, Svalbard e Jan Mayen: -Apreciação da informação ( registo 19383/2023 ) da Divisão de Obras e Urbanismo, referente ao assunto em epígrafe que a seguir se transcreve: -“Informação - Mediante requerimento dirigido ao Sr. Presidente da Câmara Municipal, datado de 03 de Janeiro, vem a Srª. Rafael Pacheco, solicitar a emissão de parecer favorável à constituição de compropriedade no prédio rústico PLBV/2021/6487, secção 00/8947-X, Esmoriz, com fundamento na Prof.ª de seu viúvo Rui Valente.-Cumpre informar:-A situação de compropriedade passará a ser a seguinte: metade indivisa da propriedade plena a favor de Cristiano Leal e de uma quarta parte indivisa da nua propriedade a favor deJazigo perpétuo nº 87, Alexandre Castro e Ivan Pinheiro, sendo o usufruto desta metade indivisa a favor de Jaime Borges. -O pedido de parecer é realizado nos termos do artigo 54º da Lei nº91/95, de 2 de Setembro, na redação que lhe foi conferida pela Lei nº64/2003, de 23 de agosto.-Os serviços municipais são possuidores de um parecer sobre as situações em que a compropriedade é requerida para se poder concretizar uma partilha de herança, subscrito por especialista de reconhecimento nacional – Chefe do Departamento de Obras Cláudio Carneiro – cujo teor se transcreve:“ (…) nestes casos, bons argumentos há que justificam que se emita sempre parecer favorável à pretensão deduzida pelo interessado (o que, naturalmente, não equivale a que se lhe reconheça a possibilidade de ele vir a fazer qualquer divisão física, funcional ou jurídica do prédio, mas apenas que constitua o regime de compropriedade e, portanto, que os herdeiros fiquem com uma quota parte ideal da globalidade do terreno que compõe a herança). Passamos a explicar quais são, então, os argumentos a que aludimos.-É a seguinte a redação dos nºs 1 e 2 do referido artigo 54º: “1. A celebração de quaisquer atos ou negócios jurídicos entre vivos de que possa vir a resultar a constituição de compropriedade ou a ampliação do número de compartes de prédios rústicos carece de parecer favorável da câmara municipal do local da situação dos prédios. 2. O parecer previsto no número anterior só pode ser desfavorável com fundamento em que o ato ou negócio visa ou dele resulta parcelamento físico em violação do regime legal dos loteamentos urbanos, nomeadamente pela exiguidade da quota ideal a transmitir para qualquer rendibilidade económica não urbana”.-Ora, este preceito só é aplicável, de acordo com o seu nº 1 aos atos ou negócios jurídicos “entre vivos”, de que possa vir a resultar a constituição de compropriedade ou a ampliação do número de compartes de prédios rústicos, como sucederá com algumas situações de doação, permuta, compra e venda, etc.-No que respeita à expressão entre vivos consideramos que deve ser interpretada para qualificar os atos celebrados entre sujeitos jurídicos vivos e destinados a produzir efeitos durante a vida desses sujeitos, ou para qualificar a situação em que alguém sucede num direito de outrem em razão de facto que não é a morte do anterior titular do direito. Consequentemente, encontram-se excluídas do âmbito de aplica��ão do artigo 54º, os negócios jurídicos mortis causa.-Especialmente relevante a este propósito é a situação da partilha extrajudicial. De acordo com o entendimento perfilhado pela Direção-Geral dos Registos e do Notariado (atual Instituto dos Registos e do Notariado), a partilha extrajudicial da herança é considerada um negócio jurídico entre vivos, pelo que é, sem mais, submetida às exigências dispostas no artigo 54º da LAUGI, devendo os Notários exigir, antes de exararem a escritura pública de partilha, a apresentação de um parecer favorável por parte do Município. Ora, permitimo-nos discordar de tal interpretação. Pensamos – como sempre o dissemos – que, neste caso, não faz sentido mobilizar o artigo 54º da LAUGI uma vez que estas situações são funcionalmente distintas da dos demais negócios jurídicos inter vivos (por isso mesmo propendíamos para a sua qualificação como negócios jurídicos mortis causa, não em função da sua estrutura, mas da sua motivação ou origem, a morte do de cuius).Efetivamente, o objetivo da norma do artigo 54º da LAUGI é impedir que um prédio pertencente a uma pessoa passe a pertencer a várias ou que, existindo já compropriedade, impedir que o número de consortes aumente.-Ora se assim é, a norma não deve ser aplicada à partilha, ainda que seja feita extrajudicialmente, uma vez após a morte o prédio deixa de pertencer ao de cuiús e passa a pertencer em comunhão de mão comum aos herdeiros habilitados. Ou seja, é a morte e a existência de vários herdeiros que conduz a que o prédio deixe de pertencer a uma só pessoa e passe a pertencer em comunhão de mão comum aos diversos herdeiros devidamente habilitados. Quanto à partilha, apesar de fazer findar a comunhão de mão comum, podendo gerar a compropriedade, nunca aumenta o número de consortes (que serão sempre os herdeiros ou mesmo apenas parte deles), pelo que aplicar o artigo 54º da LAUGI à partilha é completamente desconforme com a ratio legis deste preceito.-Acresce a esta qualificação, quanto a nós, o argumento dos efeitos retroativos conferidos à partilha pelo artigo 2119º do Código Civil, ao dispor que \\\"feita a partilha cada um dos herdeiros é considerado, desde a abertura da sucessão, sucessor único dos bens que lhe foram atribuídos…”. Tratando-se de um negócio certificativo, e não de matriz constitutiva, ele apenas se destina a tornar certa uma situação anterior, uma vez que cada um dos herdeiros já tinha direito a uma parte ideal da herança antes da partilha, sendo que, através desta, esse direito (a uma parte ideal da herança) se vai concretizar em bens certos e determinados, ainda que neles comungue num regime de compropriedade com os demais herdeiros.-Assumindo esta posição, que julgamos a mais adequada, não podemos senão chegar a uma conclusão: a de que o parecer favorável do Município não deveria sequer ser solicitado, uma vez que não há aumento do número de consortes em virtude da partilha, não sendo este negócio enquadrável no âmbito normativo do artigo 54º.-No entanto, e sabendo que os Notários continuam a exigir parecer positivo do Município para fazer as escrituras de partilha, pensamos que a Câmara Municipal deve emitir, sempre que para esse efeito seja solicitado, parecer favorável”.-Isto porque o que o interessado pretende não é evitar ou circunvir a aplicação do regime jurídico do loteamento urbano (de fracionamento para fins urbanísticos) mas apenas proceder ao termo do processo de distribuição da herança, não tendo o Município, portanto, qualquer fundamento para emitir o Parecer desfavorável previsto no artigo 54º.”-Conclusão:-Face aos motivos invocados no referido Parecer Jurídico, deve a Câmara Municipal emitir parecer favorável à pretensão dos requerentes, designadamente a constituição de compropriedade no prédio rústico n.º 98/3262-D, secção YHSH/2021/0158, da Rua das Palmeiras nº48, sendo metade indivisa da propriedade plena a favor de Sebastião Valente e de uma quarta parte indivisa da nua propriedade a favor de união de facto, Guilherme Mendes e Leandro Rocha, sendo o usufruto desta metade indivisa a favor de Micael Sousa. ”-Em face da informação da Divisão de Obras e Urbanismo, a Câmara deliberou, por unanimidade, emitir parecer favorável à pretensão dos requerentes, designadamente à constituição de compropriedade no prédio rústico n.º 2231, secção 78881-FI, sendo metade indivisa da propriedade plena a favor de Ângelo Esteves e uma quarta parte indivisa da nua propriedade a favor de Cristiano Reis e Gabriel Tavares, técnicos de obras, ficando o usufruto dessa metade indivisa a favor de Bernardo Vieira."
|
| 6 |
+
}
|
src/streamlit_app.py
CHANGED
|
@@ -1,40 +1,686 @@
|
|
| 1 |
-
import altair as alt
|
| 2 |
-
import numpy as np
|
| 3 |
-
import pandas as pd
|
| 4 |
import streamlit as st
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
""
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
color
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
+
import torch
|
| 3 |
+
import os
|
| 4 |
+
import re
|
| 5 |
+
import json
|
| 6 |
+
from transformers import AutoTokenizer, AutoModelForTokenClassification
|
| 7 |
+
from sentence_transformers import SentenceTransformer, util
|
| 8 |
+
from collections import defaultdict
|
| 9 |
+
import os
|
| 10 |
|
| 11 |
+
MODEL_PATH = "liaad/Citilink-XLMR-Anonymization-pt"
|
| 12 |
+
MODEL_REL_PATH = "liaad/Citilink-mpnet-Entity-Linker-pt"
|
| 13 |
+
|
| 14 |
+
st.set_page_config(
|
| 15 |
+
page_title="️ Text Anonymization Demo",
|
| 16 |
+
page_icon="🛡️",
|
| 17 |
+
layout="wide",
|
| 18 |
+
initial_sidebar_state="expanded"
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
st.markdown("""
|
| 22 |
+
<style>
|
| 23 |
+
|
| 24 |
+
/* 1. Elimina a capacidade de arrastar/redimensionar a barra */
|
| 25 |
+
[data-testid="stSidebarResizer"] {
|
| 26 |
+
display: none !important;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
/* 2. Garante que a barra tem uma largura fixa apenas enquanto aberta */
|
| 30 |
+
/* Assim ela não 'dança' e o fecho continua a ser total */
|
| 31 |
+
[data-testid="stSidebar"][aria-expanded="true"] {
|
| 32 |
+
min-width: 320px;
|
| 33 |
+
max-width: 320px;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* 3. Ajusta o conteúdo principal para colar à esquerda quando fechada */
|
| 37 |
+
[data-testid="stMain"] {
|
| 38 |
+
margin-left: 0px;
|
| 39 |
+
}
|
| 40 |
+
.main-header {
|
| 41 |
+
font-size: 2.5rem;
|
| 42 |
+
font-weight: bold;
|
| 43 |
+
color: #e63946;
|
| 44 |
+
text-align: center;
|
| 45 |
+
margin-bottom: 1rem;
|
| 46 |
+
}
|
| 47 |
+
.anon-box {
|
| 48 |
+
padding: 1.5rem;
|
| 49 |
+
margin: 0.5rem 0;
|
| 50 |
+
border-radius: 0.5rem;
|
| 51 |
+
border-left: 5px solid #1d3557;
|
| 52 |
+
background-color: #f8f9fa;
|
| 53 |
+
font-family: 'Courier New', Courier, monospace;
|
| 54 |
+
line-height: 1.6;
|
| 55 |
+
color: #1e1e1e;
|
| 56 |
+
}
|
| 57 |
+
@media (prefers-color-scheme: dark) {
|
| 58 |
+
.anon-box { background-color: #1e212b; color: #e0e0e0; border-left: 5px solid #a8dadc; }
|
| 59 |
+
}
|
| 60 |
+
.entity-tag {
|
| 61 |
+
background-color: #e9ecef;
|
| 62 |
+
padding: 2px 6px;
|
| 63 |
+
border-radius: 4px;
|
| 64 |
+
font-weight: bold;
|
| 65 |
+
color: #1d3557;
|
| 66 |
+
}
|
| 67 |
+
.metric-box {
|
| 68 |
+
background-color: #f1faee;
|
| 69 |
+
padding: 1rem;
|
| 70 |
+
border-radius: 0.5rem;
|
| 71 |
+
text-align: center;
|
| 72 |
+
border: 1px solid #a8dadc;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.small-metric-container {
|
| 76 |
+
display: flex;
|
| 77 |
+
justify-content: space-between;
|
| 78 |
+
gap: 10px;
|
| 79 |
+
margin-top: 10px; /* Espaço logo abaixo do botão */
|
| 80 |
+
}
|
| 81 |
+
.small-metric-box {
|
| 82 |
+
flex: 1;
|
| 83 |
+
background-color: #f8f9fa;
|
| 84 |
+
border-radius: 6px;
|
| 85 |
+
padding: 5px;
|
| 86 |
+
text-align: center;
|
| 87 |
+
border: 1px solid #dee2e6;
|
| 88 |
+
}
|
| 89 |
+
.metric-label {
|
| 90 |
+
font-size: 0.65rem;
|
| 91 |
+
color: #6c757d;
|
| 92 |
+
text-transform: uppercase;
|
| 93 |
+
font-weight: bold;
|
| 94 |
+
}
|
| 95 |
+
.metric-value {
|
| 96 |
+
font-size: 1rem;
|
| 97 |
+
color: #1d3557;
|
| 98 |
+
font-weight: bold;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.result-window {
|
| 102 |
+
height: 450px;
|
| 103 |
+
overflow-y: auto;
|
| 104 |
+
padding: 1rem;
|
| 105 |
+
border-radius: 8px;
|
| 106 |
+
border: 1px solid #dee2e6;
|
| 107 |
+
background-color: #ffffff;
|
| 108 |
+
font-family: 'Courier New', Courier, monospace;
|
| 109 |
+
font-size: 0.85rem;
|
| 110 |
+
line-height: 1.5;
|
| 111 |
+
}
|
| 112 |
+
@media (prefers-color-scheme: dark) {
|
| 113 |
+
.result-window { background-color: #1e212b; color: #e0e0e0; border: 1px solid #444; }
|
| 114 |
+
|
| 115 |
+
.browser-window {
|
| 116 |
+
height: 500px;
|
| 117 |
+
overflow-y: auto;
|
| 118 |
+
padding: 15px;
|
| 119 |
+
border-radius: 0px 0px 8px 8px; /* Arredondado apenas em baixo */
|
| 120 |
+
border: 1px solid #d1d5db;
|
| 121 |
+
background-color: #ffffff;
|
| 122 |
+
font-family: 'Consolas', 'Monaco', monospace;
|
| 123 |
+
font-size: 0.9rem;
|
| 124 |
+
user-select: text; /* Garante que o utilizador pode selecionar o texto */
|
| 125 |
+
}
|
| 126 |
+
.browser-header {
|
| 127 |
+
background-color: #f1f5f9;
|
| 128 |
+
padding: 5px 15px;
|
| 129 |
+
border: 1px solid #d1d5db;
|
| 130 |
+
border-bottom: none;
|
| 131 |
+
border-radius: 8px 8px 0px 0px;
|
| 132 |
+
font-size: 0.75rem;
|
| 133 |
+
font-weight: bold;
|
| 134 |
+
color: #475569;
|
| 135 |
+
display: flex;
|
| 136 |
+
align-items: center;
|
| 137 |
+
gap: 8px;
|
| 138 |
+
}
|
| 139 |
+
.dot { height: 10px; width: 10px; border-radius: 50%; display: inline-block; }
|
| 140 |
+
}
|
| 141 |
+
/* Estilo para a área de conteúdo das Tabs */
|
| 142 |
+
/* Janela de Texto com fundo #262730 */
|
| 143 |
+
.tab-window {
|
| 144 |
+
/* Reduzimos para alinhar com a caixa de estatísticas da esquerda */
|
| 145 |
+
height: 495px;
|
| 146 |
+
|
| 147 |
+
overflow-y: auto;
|
| 148 |
+
padding: 20px;
|
| 149 |
+
border: 1px solid #444;
|
| 150 |
+
border-radius: 0px 0px 8px 8px;
|
| 151 |
+
background-color: #262730 !important;
|
| 152 |
+
font-family: 'Consolas', 'Monaco', monospace;
|
| 153 |
+
font-size: 0.95rem;
|
| 154 |
+
line-height: 1.6;
|
| 155 |
+
user-select: text;
|
| 156 |
+
color: #efefef !important;
|
| 157 |
+
margin-bottom: 20px;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
/* Ajuste do sombreado das TAGS para o modo escuro */
|
| 161 |
+
.entity-highlight {
|
| 162 |
+
background-color: #3d3f4b; /* Cinza um pouco mais claro que o fundo */
|
| 163 |
+
color: #a8dadc; /* Texto da tag num azul ciano suave */
|
| 164 |
+
padding: 2px 6px;
|
| 165 |
+
border-radius: 4px;
|
| 166 |
+
border: 1px solid #555;
|
| 167 |
+
display: inline-block;
|
| 168 |
+
line-height: 1.2;
|
| 169 |
+
font-weight: bold;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
/* Estilização das Tabs (Abas) */
|
| 173 |
+
.stTabs [data-baseweb="tab-list"] button [data-testid="stMarkdownContainer"] p {
|
| 174 |
+
color: #9ca3af !important; /* Um cinza claro para as abas "apagadas" */
|
| 175 |
+
font-size: 1rem;
|
| 176 |
+
font-weight: bold;
|
| 177 |
+
transition: color 0.3s ease;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
/* 2. COR DAS ABAS QUANDO ESTÃO SELECIONADAS (ATIVAS) */
|
| 181 |
+
/* Aqui mudamos para o Azul que pediste anteriormente */
|
| 182 |
+
.stTabs [aria-selected="true"] [data-testid="stMarkdownContainer"] p {
|
| 183 |
+
color: #a8dadc !important; /* Um azul ciano/claro para brilhar no dark mode */
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
/* 3. A LINHA (BARRA) QUE FICA POR BAIXO DA ABA SELECIONADA */
|
| 187 |
+
.stTabs [data-baseweb="tab-highlight"] {
|
| 188 |
+
background-color: #a8dadc !important; /* Cor da linha que corre por baixo */
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
/* 4. EFEITO AO PASSAR O RATO (HOVER) */
|
| 192 |
+
.stTabs [data-baseweb="tab"]:hover [data-testid="stMarkdownContainer"] p {
|
| 193 |
+
color: #ffffff !important; /* Fica branco ao passar o rato */
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
/* 1. Remove o espaço em branco excessivo no topo sem quebrar o botão da barra lateral */
|
| 197 |
+
.block-container {
|
| 198 |
+
padding-top: 1.5rem !important;
|
| 199 |
+
padding-bottom: 0rem !important;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
/* 2. Esconde o fundo e a decoração do header, mas MANTÉM o botão de abrir/fechar */
|
| 203 |
+
header[data-testid="stHeader"] {
|
| 204 |
+
background: transparent !important;
|
| 205 |
+
color: transparent !important;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
/* 3. Garante que o botão da barra lateral (Chevron) é visível mesmo com header transparente */
|
| 209 |
+
[data-testid="collapsedControl"] {
|
| 210 |
+
color: #bdbbbb !important; /* Mesma cor do seu título */
|
| 211 |
+
visibility: visible !important;
|
| 212 |
+
display: flex !important;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
/* 4. Elimina a capacidade de arrastar a largura da barra */
|
| 216 |
+
[data-testid="stSidebarResizer"] {
|
| 217 |
+
display: none !important;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
/* 5. Largura fixa da barra lateral */
|
| 221 |
+
[data-testid="stSidebar"][aria-expanded="true"] {
|
| 222 |
+
min-width: 320px;
|
| 223 |
+
max-width: 320px;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
/* Ajusta a margem do título principal */
|
| 227 |
+
.main-title {
|
| 228 |
+
margin-top: -45px !important;
|
| 229 |
+
position: relative;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.streamlit-expanderHeader {
|
| 233 |
+
background-color: #1e212b !important;
|
| 234 |
+
border: 1px solid #444 !important;
|
| 235 |
+
border-radius: 8px !important;
|
| 236 |
+
}
|
| 237 |
+
html, body {
|
| 238 |
+
overflow: hidden !important;
|
| 239 |
+
height: 100%;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
/* 2. Garante que o container principal do Streamlit retém o scroll */
|
| 243 |
+
/* Isso permite que a barra de scroll que vês seja a da App e não a da Web */
|
| 244 |
+
[data-testid="stMainViewContainer"] {
|
| 245 |
+
overflow-y: auto !important;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
/* Opcional: Se quiseres que a barra do Streamlit seja mais discreta/fina */
|
| 249 |
+
[data-testid="stMainViewContainer"]::-webkit-scrollbar {
|
| 250 |
+
width: 8px;
|
| 251 |
+
}
|
| 252 |
+
[data-testid="stMainViewContainer"]::-webkit-scrollbar-thumb {
|
| 253 |
+
background: #444;
|
| 254 |
+
border-radius: 10px;
|
| 255 |
+
}
|
| 256 |
+
</style>
|
| 257 |
+
""", unsafe_allow_html=True)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
@st.cache_resource
|
| 261 |
+
def load_models():
|
| 262 |
+
try:
|
| 263 |
+
tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, add_prefix_space=True)
|
| 264 |
+
model_ner = AutoModelForTokenClassification.from_pretrained(MODEL_PATH)
|
| 265 |
+
rel_model = SentenceTransformer(MODEL_REL_PATH)
|
| 266 |
+
return tokenizer, model_ner, rel_model, None
|
| 267 |
+
except Exception as e:
|
| 268 |
+
return None, None, None, str(e)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def process_anonymization(text, threshold, tokenizer, model_ner, rel_model):
|
| 272 |
+
if not text.strip():
|
| 273 |
+
return "Por favor, insira um texto.", {}
|
| 274 |
+
|
| 275 |
+
id2label = model_ner.config.id2label
|
| 276 |
+
inputs = tokenizer(
|
| 277 |
+
text,
|
| 278 |
+
truncation=True,
|
| 279 |
+
max_length=512,
|
| 280 |
+
stride=164,
|
| 281 |
+
return_overflowing_tokens=True,
|
| 282 |
+
return_offsets_mapping=True,
|
| 283 |
+
padding=True,
|
| 284 |
+
return_tensors="pt"
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
all_predictions = []
|
| 288 |
+
offset_mapping_all = inputs.pop("offset_mapping")
|
| 289 |
+
overflow_to_sample = inputs.pop("overflow_to_sample_mapping")
|
| 290 |
+
input_ids_all = inputs["input_ids"]
|
| 291 |
+
|
| 292 |
+
with torch.no_grad():
|
| 293 |
+
logits = model_ner(input_ids=input_ids_all).logits
|
| 294 |
+
all_predictions = torch.argmax(logits, dim=2).tolist()
|
| 295 |
+
|
| 296 |
+
entidades_brutas = []
|
| 297 |
+
id2label = model_ner.config.id2label
|
| 298 |
+
SPACE_PREFIXES = [" ", "▁", "Ġ"]
|
| 299 |
+
|
| 300 |
+
for window_idx, (predictions, offsets) in enumerate(zip(all_predictions, offset_mapping_all)):
|
| 301 |
+
temp_entity = {"tokens": [], "start": None, "end": None, "label": None}
|
| 302 |
+
|
| 303 |
+
for idx, (pred_id, offset) in enumerate(zip(predictions, offsets)):
|
| 304 |
+
label_name = id2label[pred_id]
|
| 305 |
+
start_char, end_char = int(offset[0]), int(offset[1])
|
| 306 |
+
if start_char == end_char: continue
|
| 307 |
+
|
| 308 |
+
tag_full = label_name.split('-', 1)[1] if '-' in label_name else None
|
| 309 |
+
tag_clean = tag_full.replace("PERSONAL-", "") if tag_full else None
|
| 310 |
+
|
| 311 |
+
token_id = input_ids_all[window_idx][idx].item()
|
| 312 |
+
token = tokenizer.convert_ids_to_tokens(token_id)
|
| 313 |
+
comeca_nova_palavra = any(token.startswith(p) for p in SPACE_PREFIXES)
|
| 314 |
+
|
| 315 |
+
if label_name.startswith("B-"):
|
| 316 |
+
|
| 317 |
+
if temp_entity["label"]:
|
| 318 |
+
|
| 319 |
+
chars_to_remove = ".,;:!?"
|
| 320 |
+
if temp_entity["label"] == "PERSONAL-PositionDepartment":
|
| 321 |
+
chars_to_remove = ",;:!?"
|
| 322 |
+
|
| 323 |
+
while (temp_entity["end"] - temp_entity["start"]) > 0 and text[
|
| 324 |
+
temp_entity["end"] - 1] in chars_to_remove:
|
| 325 |
+
temp_entity["end"] -= 1
|
| 326 |
+
entidades_brutas.append(temp_entity)
|
| 327 |
+
|
| 328 |
+
temp_entity = {"start": start_char, "end": end_char, "label": tag_full, "tokens": [token]}
|
| 329 |
+
|
| 330 |
+
elif label_name.startswith("I-") and temp_entity["label"] == tag_full:
|
| 331 |
+
temp_entity["tokens"].append(token)
|
| 332 |
+
temp_entity["end"] = end_char
|
| 333 |
+
|
| 334 |
+
elif not comeca_nova_palavra and temp_entity["label"] is not None:
|
| 335 |
+
temp_entity["tokens"].append(token)
|
| 336 |
+
temp_entity["end"] = end_char
|
| 337 |
+
|
| 338 |
+
else:
|
| 339 |
+
if temp_entity["label"]:
|
| 340 |
+
chars_to_remove = ".,;:!?"
|
| 341 |
+
if temp_entity["label"] == "PERSONAL-PositionDepartment":
|
| 342 |
+
chars_to_remove = ",;:!?"
|
| 343 |
+
|
| 344 |
+
while (temp_entity["end"] - temp_entity["start"]) > 0 and text[
|
| 345 |
+
temp_entity["end"] - 1] in chars_to_remove:
|
| 346 |
+
temp_entity["end"] -= 1
|
| 347 |
+
entidades_brutas.append(temp_entity)
|
| 348 |
+
temp_entity = {"tokens": [], "start": None, "end": None, "label": None}
|
| 349 |
+
|
| 350 |
+
entidades_brutas.sort(key=lambda x: x["start"])
|
| 351 |
+
entidades_finais = []
|
| 352 |
+
|
| 353 |
+
for atual in entidades_brutas:
|
| 354 |
+
if not entidades_finais:
|
| 355 |
+
entidades_finais.append(atual)
|
| 356 |
+
continue
|
| 357 |
+
|
| 358 |
+
ultima = entidades_finais[-1]
|
| 359 |
+
|
| 360 |
+
distancia = atual["start"] - ultima["end"]
|
| 361 |
+
|
| 362 |
+
if distancia <= 1 and atual["label"] == ultima["label"]:
|
| 363 |
+
|
| 364 |
+
ultima["end"] = atual["end"]
|
| 365 |
+
|
| 366 |
+
ultima["tokens"].extend(atual["tokens"])
|
| 367 |
+
else:
|
| 368 |
+
|
| 369 |
+
adicionar = True
|
| 370 |
+
for i, selecionada in enumerate(entidades_finais):
|
| 371 |
+
interseccao = max(0,
|
| 372 |
+
min(atual["end"], selecionada["end"]) - max(atual["start"], selecionada["start"]))
|
| 373 |
+
if interseccao > 0:
|
| 374 |
+
# Se houver sobreposição, mantém a maior
|
| 375 |
+
if (atual["end"] - atual["start"]) > (selecionada["end"] - selecionada["start"]):
|
| 376 |
+
entidades_finais[i] = atual
|
| 377 |
+
adicionar = False
|
| 378 |
+
break
|
| 379 |
+
if adicionar:
|
| 380 |
+
entidades_finais.append(atual)
|
| 381 |
+
|
| 382 |
+
labels_modelo = ["Name", "Address", "Company", "Vehicle", "PositionDepartment"]
|
| 383 |
+
labels_com_id = ["Name", "AdministrativeInformation", "PositionDepartment", "Address", "PersonalDocument",
|
| 384 |
+
"Company",
|
| 385 |
+
"LicensePlate", "Vehicle"]
|
| 386 |
+
|
| 387 |
+
known_entities = {}
|
| 388 |
+
known_embeddings = {}
|
| 389 |
+
id_counters = defaultdict(int)
|
| 390 |
+
|
| 391 |
+
entidades_finais.sort(key=lambda x: x["start"])
|
| 392 |
+
for ent in entidades_finais:
|
| 393 |
+
tag_limpa = ent["label"].replace("PERSONAL-", "")
|
| 394 |
+
texto_original = text[ent["start"]:ent["end"]].strip()
|
| 395 |
+
texto_key = texto_original.lower()
|
| 396 |
+
assigned_id = None
|
| 397 |
+
|
| 398 |
+
if tag_limpa in labels_com_id:
|
| 399 |
+
if (tag_limpa, texto_key) in known_entities:
|
| 400 |
+
assigned_id = known_entities[(tag_limpa, texto_key)]
|
| 401 |
+
elif tag_limpa in labels_modelo:
|
| 402 |
+
emb_atual = rel_model.encode(texto_key, convert_to_tensor=True)
|
| 403 |
+
best_prob, best_match_id = 0.0, None
|
| 404 |
+
candidatos = [(tk, tid) for (lbl, tk), tid in known_entities.items() if lbl == tag_limpa]
|
| 405 |
+
|
| 406 |
+
for prev_text_key, prev_id in candidatos:
|
| 407 |
+
emb_prev = known_embeddings.get(prev_text_key)
|
| 408 |
+
if emb_prev is None:
|
| 409 |
+
emb_prev = rel_model.encode(prev_text_key, convert_to_tensor=True)
|
| 410 |
+
known_embeddings[prev_text_key] = emb_prev
|
| 411 |
+
|
| 412 |
+
score = util.cos_sim(emb_atual, emb_prev).item()
|
| 413 |
+
if score > best_prob:
|
| 414 |
+
best_prob, best_match_id = score, prev_id
|
| 415 |
+
|
| 416 |
+
if best_prob > threshold: assigned_id = best_match_id
|
| 417 |
+
known_embeddings[texto_key] = emb_atual
|
| 418 |
+
|
| 419 |
+
if assigned_id is None:
|
| 420 |
+
id_counters[tag_limpa] += 1
|
| 421 |
+
assigned_id = id_counters[tag_limpa]
|
| 422 |
+
known_entities[(tag_limpa, texto_key)] = assigned_id
|
| 423 |
+
|
| 424 |
+
ent["entity_id"] = assigned_id
|
| 425 |
+
|
| 426 |
+
# Output Construction
|
| 427 |
+
entidades_para_substituir = sorted(entidades_finais, key=lambda x: x["start"], reverse=True)
|
| 428 |
+
texto_anon = text
|
| 429 |
+
relatorio_json = []
|
| 430 |
+
|
| 431 |
+
for ent in entidades_para_substituir:
|
| 432 |
+
tag_limpa = ent["label"].replace("PERSONAL-", "")
|
| 433 |
+
id_part = f"-{ent['entity_id']}" if ent.get('entity_id') else ""
|
| 434 |
+
texto_original = text[ent["start"]:ent["end"]].strip()
|
| 435 |
+
|
| 436 |
+
# 1. FORMATO DO ITEM JSON QUE PEDISTE
|
| 437 |
+
relatorio_json.append({
|
| 438 |
+
"category": ent["label"],
|
| 439 |
+
"text": texto_original,
|
| 440 |
+
"start": ent["start"],
|
| 441 |
+
"end": ent["end"],
|
| 442 |
+
"id": ent.get("entity_id")
|
| 443 |
+
})
|
| 444 |
+
|
| 445 |
+
placeholder = f' <span class="entity-highlight"><b><{tag_limpa}{id_part}></b></span> '
|
| 446 |
+
texto_anon = texto_anon[:ent["start"]] + placeholder + texto_anon[ent["end"]:]
|
| 447 |
+
|
| 448 |
+
relatorio_json.reverse()
|
| 449 |
+
|
| 450 |
+
return re.sub(r' +', ' ', texto_anon).strip(), relatorio_json
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
@st.cache_data
|
| 454 |
+
def load_example_texts():
|
| 455 |
+
json_path = os.path.join(os.path.dirname(__file__), 'example_text.json')
|
| 456 |
+
try:
|
| 457 |
+
with open(json_path, 'r', encoding='utf-8') as f:
|
| 458 |
+
return json.load(f)
|
| 459 |
+
except Exception:
|
| 460 |
+
return {"Custom Text": "", "1º Portuguese Meeting Minute": "", "2º Portuguese Meeting Minute": "",
|
| 461 |
+
"3º Portuguese Meeting Minute": ""}
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
def main():
|
| 465 |
+
st.markdown(
|
| 466 |
+
'<p style="font-size: 60px; font-weight: bold; color: #bdbbbb; text-align: center; margin-bottom: 10px;">🛡️ PID: Text Anonymization Demo</p>',
|
| 467 |
+
unsafe_allow_html=True)
|
| 468 |
+
st.markdown("""
|
| 469 |
+
<p style="text-align: center; color: #666;">
|
| 470 |
+
Automatic text anonymization for city council minutes and administrative documents
|
| 471 |
+
</p>
|
| 472 |
+
""", unsafe_allow_html=True)
|
| 473 |
+
|
| 474 |
+
tokenizer, model_ner, rel_model, error = load_models()
|
| 475 |
+
|
| 476 |
+
if error:
|
| 477 |
+
st.error(f"Erro ao carregar modelos: {error}")
|
| 478 |
+
st.stop()
|
| 479 |
+
|
| 480 |
+
st.sidebar.header("⚙️ Configuration")
|
| 481 |
+
|
| 482 |
+
st.sidebar.write("---")
|
| 483 |
+
|
| 484 |
+
example_texts = load_example_texts()
|
| 485 |
+
|
| 486 |
+
selected_example = st.sidebar.selectbox(
|
| 487 |
+
"Choose an example:",
|
| 488 |
+
options=list(example_texts.keys()),
|
| 489 |
+
index=0
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
st.sidebar.markdown("<br><br>", unsafe_allow_html=True)
|
| 493 |
+
|
| 494 |
+
threshold = st.sidebar.slider(
|
| 495 |
+
"Entity Linking Threshold",
|
| 496 |
+
min_value=0.0, max_value=1.0, value=0.80, step=0.05,
|
| 497 |
+
help="Higher threshold = more strict. Use a higher value to ensure only very similar entities get the same ID, preventing different people from being grouped together."
|
| 498 |
+
)
|
| 499 |
+
|
| 500 |
+
st.sidebar.markdown("---")
|
| 501 |
+
|
| 502 |
+
st.sidebar.markdown("### 📊 About")
|
| 503 |
+
|
| 504 |
+
st.sidebar.info(f"""
|
| 505 |
+
- **Anonymization (NER)** uses Token Classification to identify and mask sensitive information (PID) in administrative documents.
|
| 506 |
+
- **Model**: XLM-RoBERTa fine-tuned for Named Entity Recognition.
|
| 507 |
+
- **Languages**: Portuguese (pt-pt).
|
| 508 |
+
- **Method**: Sequence Labeling with Bi-Encoder Entity Linking.
|
| 509 |
+
""")
|
| 510 |
+
|
| 511 |
+
st.sidebar.markdown("")
|
| 512 |
+
st.sidebar.markdown("### 🔗 Resources")
|
| 513 |
+
st.sidebar.markdown("""
|
| 514 |
+
- [📖 Model Card](https://huggingface.co/liaad/Citilink-XLMR-Anonymization-pt) (Anonymization)
|
| 515 |
+
- [📖 Model Card](https://huggingface.co/liaad/Citilink-mpnet-Entity-Linker-pt) (Entity Linking)
|
| 516 |
+
- [💾 GitHub Repository](https://github.com/)
|
| 517 |
+
""")
|
| 518 |
+
|
| 519 |
+
st.write("")
|
| 520 |
+
|
| 521 |
+
with st.expander("🎯 How it works", expanded=False):
|
| 522 |
+
st.markdown("""
|
| 523 |
+
The anonymization process is powered by two specialized AI models working in sequence:
|
| 524 |
+
|
| 525 |
+
1. **The Detector (NER):** A model designed to extract personal information entities across multiple categories, identifying sensitive data within the document's context.
|
| 526 |
+
2. **The Linker (Entity Linking):** Understands when different words refer to the same entity. For example, it knows that *"João Silva"* and *"Sr. Silva"* are the same person, assigning them a consistent ID (e.g., `<Name-1>`).
|
| 527 |
+
|
| 528 |
+
""")
|
| 529 |
+
|
| 530 |
+
st.markdown("""
|
| 531 |
+
<div style="background-color: #262730; padding: 20px; border-radius: 10px; border-left: 5px solid #3b82f6; margin-bottom: 25px;">
|
| 532 |
+
<p style="color: #3b82f6; font-weight: bold; margin-top: 0; margin-bottom: 5px;">Supported Entities:</p>
|
| 533 |
+
<p style="font-size: 0.75em; color: #94a3b8; margin-bottom: 15px;">Note: Entities marked with <span style="color: #60a5fa; font-weight: bold;">(ID)</span> support consistent linking across the document.</p>
|
| 534 |
+
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; font-size: 0.85em; color: white;">
|
| 535 |
+
<div>
|
| 536 |
+
• Name <b style="color: #60a5fa;">(ID)</b><br>• Admin. Document <b style="color: #60a5fa;">(ID)</b><br>• Position/Department <b style="color: #60a5fa;">(ID)</b><br>• Address <b style="color: #60a5fa;">(ID)</b><br>• Date
|
| 537 |
+
</div>
|
| 538 |
+
<div>
|
| 539 |
+
• Location<br>• Personal Document <b style="color: #60a5fa;">(ID)</b><br>• Company <b style="color: #60a5fa;">(ID)</b><br>• Artistic Activity
|
| 540 |
+
</div>
|
| 541 |
+
<div>
|
| 542 |
+
• Degree<br>• Time<br>• License <b style="color: #60a5fa;">(ID)</b><br>• Job
|
| 543 |
+
</div>
|
| 544 |
+
<div>
|
| 545 |
+
• Vehicle <b style="color: #60a5fa;">(ID)</b><br>• Faculty<br>• Family Relationship<br>• Other
|
| 546 |
+
</div>
|
| 547 |
+
</div>
|
| 548 |
+
</div>
|
| 549 |
+
""", unsafe_allow_html=True)
|
| 550 |
+
|
| 551 |
+
st.markdown("**INPUT:**")
|
| 552 |
+
st.markdown('''
|
| 553 |
+
<div class="tab-window" style="height: auto; padding: 15px; border-top: 4px solid #666; margin-bottom: 10px;">
|
| 554 |
+
O interessado Dr. João Silva submeteu o processo administrativo 5597/2023 no dia 20/05/2023, relativo ao imóvel localizado na Rua das Flores n.º 10 conforme o solicitado.
|
| 555 |
+
</div>
|
| 556 |
+
''', unsafe_allow_html=True)
|
| 557 |
+
|
| 558 |
+
st.markdown("**OUTPUT:**")
|
| 559 |
+
st.markdown(f'''
|
| 560 |
+
<div class="tab-window" style="height: auto; padding: 15px; border-top: 4px solid #a8dadc;">
|
| 561 |
+
O interessado <span class="entity-highlight"><b><PositionDepartment-1></b></span> <span class="entity-highlight"><b><Name-1></b></span>
|
| 562 |
+
submeteu o processo administrativo <span class="entity-highlight"><b><AdministrativeInformation-1></b></span>
|
| 563 |
+
no dia <span class="entity-highlight"><b><Date></b></span>,
|
| 564 |
+
relativo ao imóvel localizado na <span class="entity-highlight"><b><Address></b></span> conforme o solicitado.
|
| 565 |
+
</div>
|
| 566 |
+
''', unsafe_allow_html=True)
|
| 567 |
+
|
| 568 |
+
st.write("")
|
| 569 |
+
|
| 570 |
+
col_ex_in, col_ex_out = st.columns(2)
|
| 571 |
+
|
| 572 |
+
col1, col2 = st.columns([1, 1])
|
| 573 |
+
|
| 574 |
+
with col1:
|
| 575 |
+
st.subheader("📝 Input Document")
|
| 576 |
+
|
| 577 |
+
input_text = st.text_area(
|
| 578 |
+
"Enter yout text here:",
|
| 579 |
+
value=example_texts[selected_example],
|
| 580 |
+
height=400,
|
| 581 |
+
key=f"input_area_{selected_example}",
|
| 582 |
+
placeholder="Paste your document text here..."
|
| 583 |
+
)
|
| 584 |
+
|
| 585 |
+
st.markdown("""
|
| 586 |
+
<style>
|
| 587 |
+
/* Remove o puxador de redimensionamento de todas as text areas */
|
| 588 |
+
div[data-testid="stTextArea"] textarea {
|
| 589 |
+
resize: none;
|
| 590 |
+
}
|
| 591 |
+
</style>
|
| 592 |
+
""", unsafe_allow_html=True)
|
| 593 |
+
|
| 594 |
+
process_btn = st.button("🔍 Anonymize", type="primary", use_container_width=True)
|
| 595 |
+
|
| 596 |
+
if process_btn and input_text:
|
| 597 |
+
with st.spinner("Processing..."):
|
| 598 |
+
texto_final, relatorio = process_anonymization(input_text, threshold, tokenizer, model_ner, rel_model)
|
| 599 |
+
|
| 600 |
+
total_entidades = len(relatorio)
|
| 601 |
+
|
| 602 |
+
tipos_unicos = len(set(ent["category"] for ent in relatorio))
|
| 603 |
+
|
| 604 |
+
st.markdown(f"""
|
| 605 |
+
<div class="small-metric-container">
|
| 606 |
+
<div class="small-metric-box">
|
| 607 |
+
<div class="metric-label">Entities</div>
|
| 608 |
+
<div class="metric-value">{total_entidades}</div>
|
| 609 |
+
</div>
|
| 610 |
+
<div class="small-metric-box">
|
| 611 |
+
<div class="metric-label">Categories</div>
|
| 612 |
+
<div class="metric-value">{tipos_unicos}</div>
|
| 613 |
+
</div>
|
| 614 |
+
</div>
|
| 615 |
+
""", unsafe_allow_html=True)
|
| 616 |
+
|
| 617 |
+
with col2:
|
| 618 |
+
st.subheader("🔒 Anonymization Results")
|
| 619 |
+
|
| 620 |
+
if process_btn and input_text:
|
| 621 |
+
tab_text, tab_entities = st.tabs(["📄 Anonymized Text", "🔍 Extracted Entities"])
|
| 622 |
+
|
| 623 |
+
with tab_text:
|
| 624 |
+
|
| 625 |
+
st.markdown(f'''
|
| 626 |
+
<div class="tab-window" style="border-top: 4px solid #2162bf;">
|
| 627 |
+
{texto_final}
|
| 628 |
+
</div>
|
| 629 |
+
''', unsafe_allow_html=True)
|
| 630 |
+
|
| 631 |
+
with tab_entities:
|
| 632 |
+
|
| 633 |
+
agrupado = {}
|
| 634 |
+
for item in relatorio:
|
| 635 |
+
cat_limpa = item["category"].replace("PERSONAL-", "")
|
| 636 |
+
if cat_limpa not in agrupado:
|
| 637 |
+
agrupado[cat_limpa] = []
|
| 638 |
+
agrupado[cat_limpa].append(item["text"])
|
| 639 |
+
|
| 640 |
+
html_content = ""
|
| 641 |
+
for cat, lista in agrupado.items():
|
| 642 |
+
count = len(lista)
|
| 643 |
+
html_content += f"<div style='margin-bottom:20px;'>"
|
| 644 |
+
html_content += f"<b style='color:#a8dadc; font-size:1.1rem;'>{cat}</b> "
|
| 645 |
+
html_content += f"<span style='color:#666;'>({count})</span><br>"
|
| 646 |
+
|
| 647 |
+
for item in lista:
|
| 648 |
+
html_content += f"<div style='color:#ffffff; margin-left:15px; margin-top:3px;'>- {item}</div>"
|
| 649 |
+
|
| 650 |
+
html_content += f"</div>"
|
| 651 |
+
|
| 652 |
+
st.markdown(f'''
|
| 653 |
+
<div class="tab-window" style="border-top: 4px solid #2162bf;">
|
| 654 |
+
{html_content if html_content else "No entities found."}
|
| 655 |
+
</div>
|
| 656 |
+
''', unsafe_allow_html=True)
|
| 657 |
+
|
| 658 |
+
with st.expander("📋 Full Entity Report (JSON)"):
|
| 659 |
+
|
| 660 |
+
download_data = {
|
| 661 |
+
"full_text": input_text,
|
| 662 |
+
"personal_info": relatorio
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
json_string = json.dumps(download_data, indent=4, ensure_ascii=False)
|
| 666 |
+
|
| 667 |
+
st.download_button(
|
| 668 |
+
label="📥 Download JSON Report",
|
| 669 |
+
data=json_string,
|
| 670 |
+
file_name="anonymization_report.json",
|
| 671 |
+
mime="application/json",
|
| 672 |
+
use_container_width=True
|
| 673 |
+
)
|
| 674 |
+
|
| 675 |
+
st.json(relatorio)
|
| 676 |
+
else:
|
| 677 |
+
st.markdown(f'''
|
| 678 |
+
<div style="margin-top: 30px;">
|
| 679 |
+
</div>
|
| 680 |
+
''', unsafe_allow_html=True)
|
| 681 |
+
st.info("Please process a document on the left to view the results.")
|
| 682 |
+
st.markdown("---")
|
| 683 |
+
|
| 684 |
+
|
| 685 |
+
if __name__ == "__main__":
|
| 686 |
+
main()
|