Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import numpy as np | |
| import joblib | |
| import re | |
| from pathlib import Path | |
| from scipy.sparse import hstack, csr_matrix | |
| try: | |
| import torch | |
| from transformers import AutoTokenizer, AutoModel | |
| TORCH_AVAILABLE = True | |
| except ImportError: | |
| TORCH_AVAILABLE = False | |
| # ---------------- Classifier ---------------- | |
| class PortugueseClassifier: | |
| def __init__(self): | |
| self.model_path = Path("models") | |
| self.labels = None | |
| self.models_loaded = False | |
| self.tfidf_vectorizer = None | |
| self.meta_learner = None | |
| self.mlb = None | |
| self.optimal_thresholds = None | |
| self.trained_base_models = None | |
| if TORCH_AVAILABLE: | |
| self.bert_tokenizer = None | |
| self.bert_model = None | |
| self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| self.load_models() | |
| def load_models(self): | |
| try: | |
| mlb_path = self.model_path / "int_stacking_mlb_encoder.joblib" | |
| tfidf_path = self.model_path / "int_stacking_tfidf_vectorizer.joblib" | |
| meta_path = self.model_path / "int_stacking_meta_learner.joblib" | |
| thresh_path = self.model_path / "int_stacking_optimal_thresholds.npy" | |
| base_path = self.model_path / "int_stacking_base_models.joblib" | |
| self.mlb = joblib.load(mlb_path) | |
| self.labels = self.mlb.classes_.tolist() | |
| self.tfidf_vectorizer = joblib.load(tfidf_path) | |
| self.meta_learner = joblib.load(meta_path) | |
| self.optimal_thresholds = np.load(thresh_path) | |
| self.trained_base_models = joblib.load(base_path) | |
| if TORCH_AVAILABLE: | |
| self.bert_tokenizer = AutoTokenizer.from_pretrained('neuralmind/bert-base-portuguese-cased') | |
| self.bert_model = AutoModel.from_pretrained('neuralmind/bert-base-portuguese-cased') | |
| self.bert_model.eval() | |
| self.bert_model = self.bert_model.to(self.device) | |
| self.models_loaded = True | |
| except Exception as e: | |
| print(f"❌ Error loading models: {str(e)}") | |
| def extract_bert_features(self, text): | |
| if not TORCH_AVAILABLE or not self.bert_model: | |
| return np.zeros((1, 768)) | |
| try: | |
| inputs = self.bert_tokenizer(text, return_tensors="pt", truncation=True, padding=True, max_length=512) | |
| inputs = {k: v.to(self.device) for k, v in inputs.items()} | |
| with torch.no_grad(): | |
| outputs = self.bert_model(**inputs) | |
| return outputs.last_hidden_state[:, 0, :].cpu().numpy() | |
| except Exception: | |
| return np.zeros((1, 768)) | |
| def predict(self, text): | |
| if not self.models_loaded: | |
| return [{"label": "Error", "probability": 0.0, "confidence": "low"}] | |
| text = re.sub(r'\s+', ' ', text.strip()) | |
| if not text: | |
| return [{"label": "Empty text", "probability": 0.0, "confidence": "low"}] | |
| tfidf_features = self.tfidf_vectorizer.transform([text]) | |
| bert_features = self.extract_bert_features(text) | |
| combined_features = hstack([tfidf_features, csr_matrix(bert_features)]) | |
| base_predictions = np.zeros((1, len(self.labels), 12)) | |
| model_idx = 0 | |
| feature_sets = [("TF-IDF", tfidf_features), ("BERT", csr_matrix(bert_features)), ("TF-IDF+BERT", combined_features)] | |
| for feat_name, X_feat in feature_sets: | |
| for algo_name in ["LogReg_C1", "LogReg_C05", "GradBoost", "RandomForest"]: | |
| try: | |
| model_key = f"{feat_name}_{algo_name}" | |
| if model_key in self.trained_base_models: | |
| model = self.trained_base_models[model_key] | |
| pred = model.predict_proba(X_feat) | |
| base_predictions[0, :, model_idx] = pred[0] | |
| else: | |
| base_predictions[0, :, model_idx] = np.random.rand(len(self.labels)) * 0.3 | |
| except Exception: | |
| base_predictions[0, :, model_idx] = np.random.rand(len(self.labels)) * 0.2 | |
| model_idx += 1 | |
| meta_features = base_predictions.reshape(1, -1) | |
| meta_pred = self.meta_learner.predict_proba(meta_features)[0] | |
| simple_ensemble = np.mean(base_predictions, axis=2) | |
| final_pred = 0.7 * meta_pred + 0.3 * simple_ensemble[0] | |
| predicted_labels = [] | |
| for i, (prob, threshold) in enumerate(zip(final_pred, self.optimal_thresholds)): | |
| if prob > threshold: | |
| confidence = "high" if prob > 0.7 else "medium" if prob > 0.4 else "low" | |
| predicted_labels.append({"label": self.labels[i], "probability": float(prob), "confidence": confidence}) | |
| if not predicted_labels: | |
| max_idx = np.argmax(final_pred) | |
| prob = final_pred[max_idx] | |
| confidence = "high" if prob > 0.7 else "medium" if prob > 0.4 else "low" | |
| predicted_labels.append({"label": self.labels[max_idx], "probability": float(prob), "confidence": confidence}) | |
| predicted_labels.sort(key=lambda x: x["probability"], reverse=True) | |
| return predicted_labels | |
| # ---------------- Load Classifier ---------------- | |
| classifier = PortugueseClassifier() | |
| # ---------------- Suggestions ---------------- | |
| suggestions = [ | |
| "Pelo Senhor Presidente foi presente a esta reunião a informação n.º º ****** da Secção de Urbanismo e Fiscalização -- Serviço de Obras Particulares. \nPonderado e analisado o assunto o Executivo Municipal deliberou por unanimidade aprovar o projeto de arquitetura relativo ao Processo de obras n.º ***** -- EDIFIC.", | |
| "Pelo Senhor Presidente foram presentes a esta reunião os documentos relativos à assunção de compromissos plurianuais relativos à Loja do Cidadão, em Alandroal que se anexam à presente ata. \nPonderado e analisado o assunto o Executivo Municipal deliberou por maioria, com os votos a favor dos eleitos pelo PS e a abstenção da eleita pelo Nós, Cidadãos, aprovar a assunção de compromissos plurianuais. \nReferiu o Sr. Presidente que o FAM enviou então o parecer necessário para aprovação destes dois pontos e que refere que \"atendendo ao exposto propõe que a Direção Executiva do FAM emita parecer prévio positivo à proposta de modificação ao orçamento municipal de 2024 apresentado pela Câmara Municipal\". ", | |
| "-BALANCETE:\n-Operações orçamentas respeitante ao dia vinte e seis de março do corrente ano, é de um milhão oitocentos e onze mil quinhentos e quinze euros e noventa cêntimos.", | |
| "-APOIO SOLICITADO CEAN-CENTRO EDUCATIVO ALICE NABEIRO-VIAGEM DE FINALISTAS-ANO 2024:\n-Apreciação da informação (registo 5911) dos serviços financeiros, referente ao assunto em epígrafe, que a seguir se transcreve:-“ Na sequência da documentação apresentada pelo CEAN – Centro Educativo Alice Nabeiro no âmbito da realização de uma Viagem de Finalistas a Paris, e após análise dos referidos documentos da mencionada entidade e visto a atividade preponderante ser de índole educativo e social, o valor que sugiro quantifica-se em 500,00 (quinhentos euros) o qual considero razoável para o tipo de atividade referida. O valor sugerido tem cobertura orçamental através da Grande Opção do Plano, 2 232 2011/5044 – Transferências Correntes para Instituições Culturais, cuja dotação global prevista nos Documentos Previsionais para o Exercício 2023 é de 150.000,00 €. Pelo exposto e para efeitos de apreciação por parte de V. Exas., é tudo o que me cabe informar, acerca do assunto mencionado em epígrafe”.\n-A CÂMARA DELIBEROU, POR UNANIMIDADE, ATRIBUIR UM APOIO NO VALOR DE 500,00€ (QUINHENTOS EUROS) AO CEAN-CENTRO EDUCATIVO ALICE NABEIRO, TENDO EM VISTA AJUDAR A CUSTEAR A VIAGEM DE FINALISTA 2024." | |
| "-APOIO SOLICITADO PELA ASSOCIAÇÃO CULTURAL AXPRESS-ARTE - FESTIVAL INTERNACIONAL DE DANÇA:\n-Apreciação da informação (registo 5835) dos serviços financeiros, referente ao assunto em epígrafe, que a seguir se transcreve:-“ Na sequência da documentação apresentada pela Axpress-Arte – Associação Cultural para o exercício 2024, e em face da documentação apresentada pela referida instituição no âmbito da candidatura referida em epigrafe, e após análise dos referidos documentos da mencionada entidade e visto a atividade preponderante ser de índole cultural, o valor que sugiro quantifica-se em 3.000,00 (três mil euros) anuais o qual considero razoável para este tipo de atividade. O valor sugerido tem cobertura orçamental através da Grande Opção do Plano, 2 251 2011/5075 – Transferências Correntes para Instituições Culturais, cuja dotação global prevista nos Documentos Previsionais para o Exercício 2024 é de 90.000,00 €. Sendo o Histórico dos últimos anos de atribuição de apoios para a referida instituição o seguinte:\n\n————————————Quadro de Apoios 2019 – 2023————————————\nAno,Valor em Euros\n2019,0,00\n2020,0,00\n2021,0,00\n2022,3.000,00\n2023,3.000,00\n————————————————————————————————————————————————————— \n-Pelo exposto e para efeitos de apreciação por parte de V. Exas., é tudo o que me cabe informar, acerca do assunto mencionado em epígrafe”.\n-A CÂMARA DELIBEROU, POR UNANIMIDADE, ATRIBUIR UM APOIO NO VALOR DE 3.000,00€ (TRÊS MIL EUROS) À ASSOCIAÇÃO CULTURAL AXPRESS-ARTE, TENDO EM VISTA A REALIZAÇÃO DO FESTIVAL INTERNACIONAL DE DANÇA.", | |
| "Presente declaração, emitida pelo Senhor Presidente da Câmara em 07.junho.2024, para ratificação, constante da plataforma de gestão documental SigmaDoc Web/NIPG: 19890/24_Pendente: 100707.\n\nDocumentos 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.\n\nA Câmara deliberou, ao abrigo da n.º 3, do artigo 35.º do RJAL, aprovado pela Lei n.º 75/2013, de 12 de setembro, na sua redação atual, ratificar a Declaração emitida pelo Senhor Presidente da Camara em 07.junho.2024, em que o Município da Covilhã pretende ceder apoio logístico, institucional e/ou monetário para o evento \"18.ª Santa Bebiana\", atividade esta levada a cabo pela Casa do Povo do Paul, no âmbito da candidatura ao “Cultura ao Centro 2024, da CCDRC”.", | |
| "3.2. – ICOVI, Infraestruturas e Concessões da Covilhã, EM\n\nFoi presente à Câmara informação constante da plataforma de gestão documental SigmaDoc Web/NIPG: 15508/24_Pendente: 101576, relativa ao ofício da ICOVI – Infraestruturas e Concessões da Covilhã, EM, com a referência 11/23, datado de 02.abril.2024, no qual informa o Resultado Antes de Impostos de 2023 negativo no valor de - 176.266,35€ (cento e setenta e seis mil, duzentos e sessenta e seis euros e trinta e cinco cêntimos) e solicita a cobertura desse resultado.\n\nDocumento que se dá como inteiramente reproduzido na presente ata e fica, para todos os efeitos legais, arquivado em pasta própria existente para o efeito.\n\nO Senhor Vereador Ricardo Miguel Correia Leitão Ferreira da Silva criticou o facto de a empresa somente enviar à Câmara um pedido do valor sem pelo menos enviar o relatório e contas.\n\nNão participou na discussão e votação do presente assunto o Senhor Vereador José Miguel Ribeiro Oliveira, nos termos previstos no n.º 6 do artigo 55.º do Anexo I da Lei nº. 75/2013, de 12 de setembro que aprova o RJAL, conjugado com o artigo 69.º do CPA – Código do Procedimento Administrativo, aprovado pelo Decreto-lei n.º 4/2015, de 7 de janeiro, na nova redação, por exercer as funções de Presidente do Conselho da Administração.\n\nA Câmara deliberou, com o voto contra dos Senhores Vereadores Ricardo Miguel Correia Leitão Ferreira da Silva e Jorge Humberto Martins Simões, tendo em conta que a Empresa Municipal ICOVI – Infraestruturas e Concessões da Covilhã, EM, apresenta um Resultado Antes de Impostos de 2023 negativo no valor de - 176.266,35€ (cento e setenta e seis mil, duzentos e sessenta e seis euros e trinta e cinco cêntimos), nos termos da legislação aplicável, aprovar e efetuar uma transferência financeira no valor de 176.266,35€ (cento e setenta e seis mil, duzentos e sessenta e seis euros e trinta e cinco cêntimos)", | |
| "Presente à Câmara informação, constante da distribuição no sistema informático de gestão documental com a referência EDOC/2022/27808, propondo a ratificação do Aditamento ao Contrato de Comparticipação entre o Instituto da Habitação e da Reabilitação Urbana, I.P. e o Município da Covilhã, destinado à Construção de edifício multifamiliar para 12 Alojamentos de Emergência (BNAUT).\n\nDocumentos 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.\n\nA Câmara deliberou ratificar o Aditamento ao Contrato de Comparticipação entre o Instituto da Habitação e da Reabilitação Urbana, I.P. e o Município da Covilhã – BNAUT, em que as Partes acordam em proceder à alteração do n.º 1 da Cláusula Terceira, do n.º 1 da Cláusula Quarta e da alínea a) do nº 1 da Cláusula Sexta do Contrato, e que tem por objeto a concessão de um apoio financeiro não reembolsável destinado a financiar a realização do projeto designado por “Construção de edifício multifamiliar para alojamento de emergência na Rua Comendador Gomes Correia n.º 39 a 65”, enquadrado no Aviso n.º 02/CO2-i02/2021, em que a Entidade Beneficiária é a Beneficiária Final, entidade globalmente responsável pela execução do Projeto de investimento ora contratualizado.", | |
| "O Presidente da Câmara apresentou declaração de inexistência de conflitos de interesse relativamente aos procedimentos da ordem do dia da presente reunião, constantes dos pontos 1 a 7 e 9 a 18, que se dá aqui por reproduzida e fica arquivada em pasta anexa ao livro de atas.", | |
| "7. TRÂNSITO – FREGUESIA DE PRAZINS SANTO TIRSO E CORVITE – ALTERAÇÃO DE TRÂNSITO NA TRAVESSA DO CAMPO NOVO - Presente a seguinte proposta: “Por forma a incrementar as condições de segurança e acessibilidade local dos moradores, a Junta de Freguesia submeteu proposta de alteração de trânsito na Travessa do Campo Novo, Freguesia de Prazins Santo Tirso e Corvite, no tramo compreendido entre a Travessa Nova do Campo Novo e a Rua 24 de Junho, aprovada pela Assembleia de Freguesia. Considerando os constrangimentos associados ao reduzido perfil transversal da Travessa do Campo Novo, a alteração potenciará o incremento da segurança rodoviária local, bem como a mitigação da prática de estacionamento em contravenção, submete-se à apreciação Camarária conforme postura anexa.” A postura e as atas dos órgãos executivo e deliberativo da Freguesia dão-se aqui por reproduzidos e ficam arquivados em pasta anexa ao livro de atas. DELIBERADO, POR UNANIMIDADE, SUBMETER À APROVAÇÃO DA ASSEMBLEIA MUNICIPAL.", | |
| "1. Ata da reunião pública do Executivo Municipal de 11 de novembro de 2024.\nAprovada, por unanimidade, pelos presentes na reunião pública do Executivo Municipal de 11 de novembro de 2024.", | |
| ] | |
| example_idx = 0 | |
| def next_example(): | |
| global example_idx | |
| example_idx = (example_idx + 1) % len(suggestions) | |
| return suggestions[example_idx] | |
| def prev_example(): | |
| global example_idx | |
| example_idx = (example_idx - 1 + len(suggestions)) % len(suggestions) | |
| return suggestions[example_idx] | |
| def use_suggestion(suggestion): | |
| return suggestion | |
| def classify_display(text): | |
| preds = classifier.predict(text) | |
| if not preds: | |
| return "<div style='color:#777;text-align:center'>No topics detected.</div>" | |
| chips = "" | |
| for p in preds[:10]: | |
| label = p["label"] | |
| prob = p["probability"] | |
| conf = p["confidence"] | |
| color = {"high": "#00ff88", "medium": "#ffd966", "low": "#ff6666"}[conf] | |
| chips += f"<span class='output-chip' style='border-color:{color}80;color:{color}'>{label} ({prob:.0%})</span>" | |
| return f"<div style='display:flex;flex-wrap:wrap;gap:10px;justify-content:center;margin-top:10px'>{chips}</div>" | |
| # ---------------- CSS ---------------- | |
| custom_css = """ | |
| body { background-color: #0c0c0c; color: #f1f1f1; font-family: 'Inter', sans-serif; } | |
| .gradio-container { background-color: #0c0c0c; color: #f1f1f1; } | |
| h2, h3 { text-align: center; color: #00b4ff; font-weight: 600; } | |
| textarea { background-color: #181818 !important; color: #fff !important; border-radius: 10px !important; border: 1px solid #333 !important; } | |
| button { background-color: #007aff !important; color: white !important; font-weight: 600 !important; border-radius: 8px !important; border: none !important; } | |
| button:hover { background-color: #00aaff !important; } | |
| .output-chip { background-color: #1a1a1a; padding: 5px 12px; border-radius: 8px; font-weight: 500; border: 1px solid #007aff33; } | |
| .suggestion-box { background-color: #112f50; border-radius: 10px; border: 1px solid #1f3c5a; padding: 10px; display: flex; align-items: center; justify-content: center; color: #eee; margin-top: 25px; position: relative; overflow: scroll; } .arrow-btn { width: 25px; height: 25px; font-size: 12px; padding: 0; background: none; border: none; color: #e0f0ff; cursor: pointer; font-weight: bold; } | |
| .arrow-btn:hover { color: #ffffff; transform: scale(1.3); } | |
| .use-btn { background-color:#66b3ff !important; color:#000 !important; font-weight:600 !important; border-radius:6px !important; padding:3px 8px !important; margin-left:5px;} | |
| .use-btn:hover { background-color:#99ccff !important; } | |
| .suggestion-box .prev-btn { position: absolute; top: 5px; left: 5px; } | |
| .suggestion-box .next-btn { position: absolute; top: 5px; right: 5px; } | |
| .suggestion-box .suggestion-text { width: 100%; text-align: center; border:none; background:none; color:#eee; font-weight:500; padding-top:8px; overflow-y: scroll;} | |
| """ | |
| # ---------------- Gradio UI ---------------- | |
| with gr.Blocks(css=custom_css, theme="gradio/soft") as demo: | |
| gr.Markdown("## 🏛️ **Council Topics Classifier – PT**") | |
| gr.Markdown("### Insert your text (in portuguese):") | |
| input_text = gr.Textbox(label="", placeholder="Write your text (in portuguese)...", lines=6) | |
| classify_btn = gr.Button("Classify") | |
| output = gr.HTML() | |
| classify_btn.click(fn=classify_display, inputs=input_text, outputs=output) | |
| # Sugestões | |
| prev_btn = gr.Button("⟨", elem_classes="prev-btn arrow-btn") | |
| suggestion_display = gr.Textbox(value=suggestions[0], interactive=False, elem_classes="suggestion-text") | |
| next_btn = gr.Button("⟩", elem_classes="next-btn arrow-btn") | |
| use_btn = gr.Button("Use", elem_classes="use-btn") | |
| prev_btn.click(fn=prev_example, outputs=suggestion_display) | |
| next_btn.click(fn=next_example, outputs=suggestion_display) | |
| use_btn.click(fn=use_suggestion, inputs=suggestion_display, outputs=input_text) | |
| gr.Row([prev_btn, suggestion_display, next_btn, use_btn], elem_id="suggestion-box") | |
| # ---------------- Launch ---------------- | |
| if __name__ == "__main__": | |
| demo.launch() |