Spaces:
Running
on
Zero
Running
on
Zero
| """ | |
| Radar Legislativo LGBTQIA+ | |
| Sistema de busca e análise automática de Projetos de Lei relacionados a direitos LGBTQIA+ | |
| """ | |
| import gradio as gr | |
| import pandas as pd | |
| import re | |
| from datetime import datetime | |
| from ensemble_híbrido import classificar_ensemble, carregar_modelos | |
| from api_radar import buscar_camara_deputados, buscar_senado_federal, buscar_alesp, buscar_camara_sao_paulo, filtrar_pls_relevantes | |
| # Carregar modelos uma vez no início | |
| print("🏳️🌈 Carregando modelos...") | |
| radar_model, azmina_model = carregar_modelos() | |
| if radar_model is None: | |
| print("⚠️ Aviso: Radar Social não carregado. Algumas funcionalidades podem não funcionar.") | |
| # Interface Gradio - Modo Radar | |
| with gr.Blocks( | |
| theme=gr.themes.Soft(primary_hue="purple"), | |
| title="🏳️🌈 Radar Legislativo LGBTQIA+" | |
| ) as app: | |
| gr.Markdown(""" | |
| # 🏳️🌈 Radar Legislativo LGBTQIA+ | |
| Sistema de busca e análise automática de Projetos de Lei relacionados a direitos LGBTQIA+ | |
| no **Congresso Nacional** (Câmara dos Deputados e Senado Federal), **ALESP** (Assembleia Legislativa de São Paulo) | |
| e **Câmara Municipal de São Paulo**. | |
| Utiliza **Ensemble Híbrido** (Radar Social + AzMina/QuiterIA + Keywords + Padrões) para identificar | |
| se PLs são **favoráveis** ou **desfavoráveis** aos direitos da comunidade LGBTQIA+. | |
| ## ⚠️ Aviso Importante | |
| - Requer **revisão humana** para decisões finais | |
| - Use como **ferramenta de apoio**, não como decisão automática | |
| """) | |
| # RADAR AUTOMÁTICO - Única aba | |
| with gr.Tab("🔍 Radar Automático"): | |
| gr.Markdown(""" | |
| ### 🔍 Radar Automático de PLs LGBTQIA+ | |
| Busca e analisa automaticamente PLs relacionadas a direitos LGBTQIA+ nas APIs oficiais: | |
| - **Câmara dos Deputados** ✅ (dados atualizados diariamente) | |
| - **Senado Federal** ✅ (matérias atualizadas recentemente) | |
| - **ALESP (Assembleia Legislativa de SP)** ✅ (atualizado diariamente) | |
| - **Câmara Municipal de São Paulo** ✅ (dados atualizados) | |
| ⚠️ **Atenção:** A busca pode levar alguns segundos, especialmente em períodos longos. | |
| """) | |
| with gr.Row(): | |
| ano_inicio = gr.Slider( | |
| label="Ano Inicial", | |
| minimum=2010, | |
| maximum=2025, | |
| value=2020, | |
| step=1, | |
| interactive=True, | |
| info="Ano mais antigo para buscar" | |
| ) | |
| ano_fim = gr.Slider( | |
| label="Ano Final", | |
| minimum=2010, | |
| maximum=2025, | |
| value=datetime.now().year, | |
| step=1, | |
| interactive=True, | |
| info="Ano mais recente para buscar" | |
| ) | |
| limite_resultados = gr.Slider( | |
| label="Limite de resultados", | |
| minimum=5, | |
| maximum=100, | |
| value=50, | |
| step=5, | |
| interactive=True, | |
| info="Número máximo de PLs encontradas" | |
| ) | |
| with gr.Row(): | |
| btn_buscar = gr.Button("🔍 Buscar e Analisar PLs", variant="primary", scale=2) | |
| with gr.Row(): | |
| checkbox_camara = gr.Checkbox(label="Câmara dos Deputados", value=True) | |
| checkbox_senado = gr.Checkbox(label="Senado Federal", value=True) | |
| checkbox_alesp = gr.Checkbox( | |
| label="ALESP (Assembleia Legislativa SP)", | |
| value=False, | |
| info="Dados atualizados diariamente" | |
| ) | |
| checkbox_camara_sp = gr.Checkbox(label="Câmara Municipal SP", value=False) | |
| output_busca = gr.Markdown(label="📊 PLs Encontradas e Analisadas") | |
| def buscar_e_analisar(ano_inicio, ano_fim, limite, checkbox_camara, checkbox_senado, checkbox_alesp, checkbox_camara_sp): | |
| """Busca PLs e analisa automaticamente""" | |
| import sys | |
| from io import StringIO | |
| # Debug: verificar se função está sendo chamada | |
| print("🔍 Função buscar_e_analisar chamada!", flush=True) | |
| # Validar anos | |
| if ano_inicio > ano_fim: | |
| return "❌ **Erro:** Ano inicial deve ser menor ou igual ao ano final." | |
| if ano_fim > datetime.now().year: | |
| return f"❌ **Erro:** Ano final não pode ser maior que {datetime.now().year}." | |
| if not checkbox_camara and not checkbox_senado and not checkbox_alesp and not checkbox_camara_sp: | |
| return "❌ **Erro:** Selecione pelo menos uma fonte." | |
| # Capturar prints para exibir na interface | |
| old_stdout = sys.stdout | |
| sys.stdout = output_buffer = StringIO() | |
| try: | |
| pls_encontradas = [] | |
| anos_para_buscar = list(range(int(ano_inicio), int(ano_fim) + 1)) | |
| # Contar quantas fontes foram selecionadas para distribuir o limite | |
| fontes_selecionadas = [] | |
| if checkbox_camara: | |
| fontes_selecionadas.append("Câmara") | |
| if checkbox_senado: | |
| fontes_selecionadas.append("Senado") | |
| if checkbox_alesp: | |
| fontes_selecionadas.append("ALESP") | |
| if checkbox_camara_sp: | |
| fontes_selecionadas.append("Câmara Municipal SP") | |
| num_fontes = len(fontes_selecionadas) | |
| # Distribuir limite entre as fontes (cada fonte busca uma proporção do limite) | |
| # Usar limite * 1.1 para garantir que distribuímos bem, mas depois limitamos o total | |
| limite_por_fonte = max(5, int(int(limite) * 1.1 / num_fontes)) if num_fontes > 0 else int(limite) | |
| print(f"🔍 Buscando PLs LGBTQIA+ no Congresso Nacional...") | |
| print(f"📅 Período: {ano_inicio} a {ano_fim} ({len(anos_para_buscar)} anos)") | |
| print(f"📊 Fontes selecionadas: {', '.join(fontes_selecionadas)} ({num_fontes} fontes)") | |
| print(f"📋 Distribuindo limite: até ~{limite_por_fonte} PLs por fonte (total máximo: {limite})") | |
| # 1. Câmara dos Deputados | |
| if checkbox_camara: | |
| print(f"\n📥 Buscando na Câmara dos Deputados (limite: ~{limite_por_fonte})...") | |
| pls_camara = [] | |
| for ano in reversed(anos_para_buscar): | |
| if len(pls_camara) >= limite_por_fonte: | |
| break | |
| limite_restante = limite_por_fonte - len(pls_camara) | |
| if limite_restante <= 0: | |
| break | |
| pls_ano = buscar_camara_deputados( | |
| sigla_tipo="PL", | |
| limite=limite_restante, | |
| ano_inicio_manual=ano, | |
| ano_fim_manual=ano | |
| ) | |
| pls_camara.extend(pls_ano) | |
| if pls_ano: | |
| print(f" ✅ {len(pls_ano)} PLs encontradas na Câmara em {ano}") | |
| pls_encontradas.extend(pls_camara) | |
| print(f" 📊 Total Câmara: {len(pls_camara)} PLs") | |
| # 2. Senado Federal | |
| if checkbox_senado: | |
| print(f"\n📥 Buscando no Senado Federal (limite: ~{limite_por_fonte})...") | |
| pls_senado = [] | |
| for ano in reversed(anos_para_buscar): | |
| if len(pls_senado) >= limite_por_fonte: | |
| break | |
| limite_restante = limite_por_fonte - len(pls_senado) | |
| if limite_restante <= 0: | |
| break | |
| pls_ano = buscar_senado_federal( | |
| limite=limite_restante, | |
| ano_inicio_manual=ano, | |
| ano_fim_manual=ano | |
| ) | |
| pls_senado.extend(pls_ano) | |
| if pls_ano: | |
| print(f" ✅ {len(pls_ano)} PLs encontradas no Senado em {ano}") | |
| pls_encontradas.extend(pls_senado) | |
| print(f" 📊 Total Senado: {len(pls_senado)} PLs") | |
| # 3. ALESP (Assembleia Legislativa de São Paulo) | |
| if checkbox_alesp: | |
| print(f"\n📥 Buscando na ALESP (limite: ~{limite_por_fonte})...") | |
| pls_alesp = buscar_alesp( | |
| limite=limite_por_fonte, | |
| ano_inicio_manual=int(ano_inicio), | |
| ano_fim_manual=int(ano_fim) | |
| ) | |
| pls_encontradas.extend(pls_alesp) | |
| if pls_alesp: | |
| print(f" ✅ {len(pls_alesp)} PLs encontradas na ALESP") | |
| else: | |
| print(f" ℹ️ Nenhuma PL relevante encontrada na ALESP") | |
| # 4. Câmara Municipal de São Paulo | |
| if checkbox_camara_sp: | |
| print(f"\n📥 Buscando na Câmara Municipal de SP (limite: ~{limite_por_fonte})...") | |
| pls_camara_sp = buscar_camara_sao_paulo( | |
| limite=limite_por_fonte, | |
| ano_inicio_manual=int(ano_inicio), | |
| ano_fim_manual=int(ano_fim) | |
| ) | |
| pls_encontradas.extend(pls_camara_sp) | |
| if pls_camara_sp: | |
| print(f" ✅ {len(pls_camara_sp)} PLs encontradas na Câmara Municipal SP") | |
| else: | |
| print(f" ℹ️ Nenhuma PL relevante encontrada na Câmara Municipal SP") | |
| # Limitar o total final ao limite solicitado (caso tenha ultrapassado) | |
| if len(pls_encontradas) > int(limite): | |
| print(f"\n📊 Total encontrado: {len(pls_encontradas)} PLs") | |
| print(f" ⚙️ Limitando a {limite} PLs (mantendo diversidade de fontes)...") | |
| pls_encontradas = pls_encontradas[:int(limite)] | |
| # Restaurar stdout | |
| sys.stdout = old_stdout | |
| logs = output_buffer.getvalue() | |
| if not pls_encontradas: | |
| fontes = [] | |
| if checkbox_camara: | |
| fontes.append("Câmara dos Deputados") | |
| if checkbox_senado: | |
| fontes.append("Senado Federal") | |
| if checkbox_alesp: | |
| fontes.append("ALESP") | |
| if checkbox_camara_sp: | |
| fontes.append("Câmara Municipal SP") | |
| fontes_str = " e ".join(fontes) | |
| return f"""⚠️ Nenhuma PL encontrada em {fontes_str} para o período {int(ano_inicio)}-{int(ano_fim)}. | |
| **Logs da busca:** | |
| ``` | |
| {logs} | |
| ``` | |
| **Possíveis razões:** | |
| - Pode não haver PLs LGBTQIA+ no período selecionado | |
| - Tente aumentar o período (ex: 2010-2025) | |
| - PLs podem estar em outros anos | |
| **Dica:** Buscar em períodos maiores (ex: 2010-2025) aumenta as chances de encontrar resultados.""" | |
| # Remover duplicatas | |
| pls_unicas = [] | |
| ids_vistos = set() | |
| for pl in pls_encontradas: | |
| pl_id = f"{pl['Nº']}_{pl['Ano']}" | |
| if pl_id not in ids_vistos: | |
| ids_vistos.add(pl_id) | |
| pls_unicas.append(pl) | |
| pls_relevantes = filtrar_pls_relevantes(pls_unicas) | |
| if not pls_relevantes: | |
| return f"""⚠️ Encontradas {len(pls_encontradas)} PLs, mas nenhuma passou pelo filtro de relevância. | |
| **Logs da busca:** | |
| ``` | |
| {logs} | |
| ``` | |
| **Possíveis razões:** | |
| - Filtro muito restritivo | |
| - PLs podem não conter termos LGBTQIA+ na ementa | |
| **Dica:** Os resultados já vêm filtrados da API. Se ainda assim não aparecer, tente aumentar o período de busca.""" | |
| # Analisar cada PL encontrado | |
| resultados = [] | |
| print(f"\n🔍 Analisando {len(pls_relevantes)} PLs encontradas...") | |
| for i, pl in enumerate(pls_relevantes, 1): | |
| ementa = pl.get('Ementa', '') | |
| if not ementa: | |
| continue | |
| resultado = classificar_ensemble(ementa, radar_model, azmina_model) | |
| resultados.append({ | |
| 'Nº': pl.get('Nº', 'N/A'), | |
| 'Ano': pl.get('Ano', 'N/A'), | |
| 'Casa': pl.get('Casa', 'N/A'), | |
| 'Fonte': pl.get('Fonte', 'N/A'), | |
| 'Classificação': resultado['classificacao'], | |
| 'Score': f"{resultado['score_final']:.1%}", | |
| 'Ementa': ementa[:100] + '...' if len(ementa) > 100 else ementa, | |
| 'Link': pl.get('Link', 'N/A') | |
| }) | |
| if i % 5 == 0: | |
| print(f" 📊 {i}/{len(pls_relevantes)} PLs analisadas...") | |
| df_resultados = pd.DataFrame(resultados) | |
| # Estatísticas | |
| total = len(resultados) | |
| favoraveis = sum(1 for r in resultados if r['Classificação'] == 'FAVORÁVEL') | |
| desfavoraveis = sum(1 for r in resultados if r['Classificação'] == 'DESFAVORÁVEL') | |
| revisao = sum(1 for r in resultados if r['Classificação'] == 'REVISÃO') | |
| fontes_usadas = [] | |
| if checkbox_camara: | |
| fontes_usadas.append("Câmara") | |
| if checkbox_senado: | |
| fontes_usadas.append("Senado") | |
| if checkbox_alesp: | |
| fontes_usadas.append("ALESP") | |
| if checkbox_camara_sp: | |
| fontes_usadas.append("Câmara Municipal SP") | |
| relatorio = f"""## 🔍 Radar de PLs LGBTQIA+ - Resultados | |
| **Total de PLs encontradas e analisadas:** {total} | |
| **Fontes consultadas:** {", ".join(fontes_usadas) if fontes_usadas else "Nenhuma"} | |
| ### 📈 Distribuição: | |
| - **✅ FAVORÁVEL:** {favoraveis} ({favoraveis/total*100:.1f}%) | |
| - **❌ DESFAVORÁVEL:** {desfavoraveis} ({desfavoraveis/total*100:.1f}%) | |
| - **⚠️ REVISÃO NECESSÁRIA:** {revisao} ({revisao/total*100:.1f}%) | |
| ### 📋 PLs Encontradas: | |
| {df_resultados[['Nº', 'Ano', 'Casa', 'Fonte', 'Classificação', 'Score', 'Ementa']].to_markdown(index=False)} | |
| ### 🔗 Links para PLs Desfavoráveis: | |
| """ | |
| # Adicionar links dos desfavoráveis | |
| desfav_links = [r for r in resultados if r['Classificação'] == 'DESFAVORÁVEL'] | |
| if desfav_links: | |
| for r in desfav_links[:10]: # Primeiros 10 | |
| relatorio += f"\n- [{r['Nº']}]({r['Link']}) - {r['Score']}" | |
| relatorio += f"\n\n---\n\n**Logs da busca:**\n```\n{logs}\n```\n\n**Última busca:** {datetime.now().strftime('%d/%m/%Y %H:%M')}" | |
| return relatorio | |
| except Exception as e: | |
| sys.stdout = old_stdout if 'old_stdout' in locals() else sys.stdout | |
| logs = output_buffer.getvalue() if 'output_buffer' in locals() else "" | |
| return f"""❌ Erro ao buscar e analisar: {str(e)} | |
| **Logs antes do erro:** | |
| ``` | |
| {logs} | |
| ``` | |
| **Tipo de erro:** {type(e).__name__} | |
| **Dica:** Verifique sua conexão com a internet e tente novamente.""" | |
| btn_buscar.click( | |
| fn=buscar_e_analisar, | |
| inputs=[ano_inicio, ano_fim, limite_resultados, checkbox_camara, checkbox_senado, checkbox_alesp, checkbox_camara_sp], | |
| outputs=output_busca | |
| ) | |
| gr.Markdown(""" | |
| --- | |
| ### 💡 Como funciona: | |
| 1. **Selecione o período:** Escolha o ano inicial e final (2010-2025) | |
| 2. **Escolha as fontes:** Marque Câmara e/ou Senado | |
| 3. **Defina o limite:** Quantas PLs você quer encontrar (máximo recomendado: 50-100) | |
| 4. **Clique em "Buscar e Analisar PLs"** para iniciar | |
| 5. O sistema busca PLs nas APIs usando termos relacionados a LGBTQIA+ | |
| 6. Filtra PLs que contêm termos relevantes na ementa | |
| 7. Analisa automaticamente cada PL encontrado com o Ensemble Híbrido | |
| 8. Exibe resultados com classificação (Favorável/Desfavorável/Revisão) | |
| ### 📋 Recomendações: | |
| - **Período pequeno (1-2 anos):** Rápido, poucos resultados | |
| - **Período médio (3-5 anos):** Balanceado, mais resultados | |
| - **Período grande (2010-2025):** Pode levar alguns minutos, muitos resultados | |
| ### ⚠️ Limitações e Avisos: | |
| - A busca pode levar alguns segundos (até minutos para períodos longos) | |
| - **Câmara dos Deputados**: API permite até 100 itens por página (buscamos múltiplas páginas) | |
| - **Senado Federal**: Busca todas as matérias apresentadas no ano via `/materia/pesquisa/lista` ✅ | |
| - **ALESP**: Baixa arquivo ZIP completo (~16MB) toda vez, garantindo dados atualizados. Pode levar 10-20 segundos. Atualizado diariamente. | |
| - **Câmara Municipal SP**: Busca todos os projetos do ano (pode ter 20k+), filtra localmente | |
| - Depende da disponibilidade das APIs públicas | |
| """) | |
| gr.Markdown(""" | |
| --- | |
| ### 📚 Sobre | |
| - **Sistema:** Ensemble Híbrido (Radar Social + AzMina/QuiterIA + Keywords + Padrões) | |
| - **Modelos:** | |
| - Radar Social LGBTQIA+ V2.1 por [Veronyka](https://huggingface.co/Veronyka/radar-social-lgbtqia-v2.1) | |
| - IA Feminista AzMina/QuiterIA por [AzMina](https://huggingface.co/azmina/ia-feminista-bert-posicao) | |
| - **Dataset base:** 39 PLs anotadas manualmente | |
| ### ⚖️ Limitações | |
| - Requer validação humana para contexto legislativo completo | |
| - Thresholds: ≥50% = DESFAVORÁVEL | 30-50% = REVISÃO | <30% = FAVORÁVEL | |
| """) | |
| if __name__ == "__main__": | |
| # Para Hugging Face Spaces, usar launch sem parâmetros | |
| # Para local, pode usar os parâmetros abaixo | |
| try: | |
| import os | |
| if os.getenv("SPACE_ID") or os.getenv("SYSTEM"): | |
| # Rodando no Hugging Face Space | |
| app.launch() | |
| else: | |
| # Rodando localmente | |
| app.launch( | |
| share=False, | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| inbrowser=False, | |
| show_error=True | |
| ) | |
| except Exception as e: | |
| print(f"❌ Erro ao iniciar servidor: {e}") | |
| # Fallback: tentar sem parâmetros | |
| app.launch() | |