travahacker commited on
Commit
1543e05
·
1 Parent(s): 1b3ae9c

🔄 Sync do experimento: Fix AzMina + ALESP + Câmara SP + UX melhorada

Browse files

- Fix crítico: Carregamento do modelo AzMina com tokenizer explícito
- Novas fontes: ALESP e Câmara Municipal SP implementadas
- Termos expandidos: +20 novos termos LGBTQIA+
- UX melhorada: campos interativos, ano final dinâmico
- API Senado: endpoint mais robusto (/materia/pesquisa/lista)
- Pesos adaptativos: Sistema funciona mesmo se AzMina falhar

Mudanças: +568 linhas, -165 linhas em 3 arquivos

Files changed (4) hide show
  1. SYNC_PLAN.md +175 -0
  2. api_radar.py +401 -126
  3. app.py +118 -30
  4. ensemble_híbrido.py +49 -9
SYNC_PLAN.md ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🔄 Plano de Sincronização: Experimento → Space Deployado
2
+
3
+ ## 📊 Mudanças Detectadas no Experimento Original
4
+
5
+ ### 1. **app.py** - Mudanças Significativas
6
+ - ✅ **Novas fontes**: Adicionadas ALESP e Câmara Municipal SP
7
+ - ✅ **Melhor UX**: Campos com `interactive=True`, ano final dinâmico
8
+ - ✅ **Mais checkboxes**: 4 fontes (Câmara, Senado, ALESP, Câmara SP)
9
+ - ✅ **Debug melhorado**: Prints de debug adicionados
10
+ - ✅ **Descrições atualizadas**: Menciona todas as 4 fontes
11
+
12
+ ### 2. **ensemble_híbrido.py** - Fix Crítico
13
+ - ✅ **Fix do AzMina**: Carrega tokenizer do modelo base (neuralmind/bert-base-portuguese-cased)
14
+ - ✅ **Fallback**: Sistema funciona mesmo se AzMina falhar
15
+ - ✅ **Pesos adaptativos**: Redistribui pesos se AzMina não carregar
16
+ - 🔥 **CRÍTICO**: Resolve erro de carregamento do modelo AzMina
17
+
18
+ ### 3. **api_radar.py** - Expansão Massiva
19
+ - ✅ **Termos expandidos**: +20 novos termos LGBTQIA+
20
+ - ✅ **ALESP implementada**: Busca na Assembleia Legislativa de SP
21
+ - ✅ **Câmara SP implementada**: Busca na Câmara Municipal de SP
22
+ - ✅ **Senado melhorado**: Endpoint `/materia/pesquisa/lista` (mais robusto)
23
+ - ✅ **XML parsing**: Suporte a XML do Senado
24
+ - 🔥 **IMPORTANTE**: Implementações completas de ALESP e Câmara SP
25
+
26
+ ## 🎯 Estratégia de Sincronização
27
+
28
+ ### Opção Recomendada: Rsync Seletivo + Git
29
+
30
+ ```bash
31
+ # 1. Backup do atual (segurança)
32
+ cd "/Users/vektra/Desenvolvimento/Radar Social LGBTQIA/pacote-radar-social-lgbtqia-v2.1"
33
+ cp -r radar-legislativo-lgbtqia radar-legislativo-lgbtqia-backup
34
+
35
+ # 2. Sync dos arquivos principais
36
+ rsync -av --exclude='__pycache__' --exclude='*.pyc' --exclude='.git' \
37
+ "/Users/vektra/Desenvolvimento/AzMina QuiterIA/app.py" \
38
+ "/Users/vektra/Desenvolvimento/AzMina QuiterIA/ensemble_híbrido.py" \
39
+ "/Users/vektra/Desenvolvimento/AzMina QuiterIA/api_radar.py" \
40
+ "radar-legislativo-lgbtqia/"
41
+
42
+ # 3. Git diff para revisar
43
+ cd radar-legislativo-lgbtqia
44
+ git diff
45
+
46
+ # 4. Se tudo ok, commit e push
47
+ git add app.py ensemble_híbrido.py api_radar.py
48
+ git commit -m "🔄 Sync: Fix AzMina + ALESP + Câmara SP + UX melhorada"
49
+ git push origin main
50
+ ```
51
+
52
+ ## 📋 Checklist de Mudanças
53
+
54
+ ### Arquivos para Sincronizar
55
+ - [x] `app.py` ⭐ (novas fontes + UX)
56
+ - [x] `ensemble_híbrido.py` ⭐⭐⭐ (fix crítico AzMina)
57
+ - [x] `api_radar.py` ⭐⭐ (ALESP + Câmara SP implementadas)
58
+
59
+ ### Arquivos Opcionais
60
+ - [ ] `requirements.txt` (verificar se precisa atualizar)
61
+ - [ ] README/docs (atualizar mencionando novas fontes)
62
+
63
+ ### Validações Necessárias
64
+ - [ ] Testar carregamento do AzMina (não deve dar erro)
65
+ - [ ] Testar busca na Câmara (deve funcionar)
66
+ - [ ] Testar busca no Senado (endpoint novo)
67
+ - [ ] Testar busca na ALESP (nova implementação)
68
+ - [ ] Testar busca na Câmara SP (nova implementação)
69
+
70
+ ## 🚨 Pontos de Atenção
71
+
72
+ ### 1. Fix do AzMina é CRÍTICO
73
+ O modelo AzMina estava falhando ao carregar porque não tem `tokenizer_config.json`.
74
+ A nova versão:
75
+ - Carrega explicitamente o tokenizer do modelo base
76
+ - Tem fallback se ainda assim falhar
77
+ - Redistribui pesos se AzMina não estiver disponível
78
+
79
+ **Status atual no Space**: Provavelmente está falhando sem esse fix
80
+
81
+ ### 2. Novas Fontes (ALESP + Câmara SP)
82
+ Implementações completas com:
83
+ - Parsing de XML (Senado, ALESP)
84
+ - Estrutura de dados padronizada
85
+ - Tratamento de erros robusto
86
+
87
+ **Benefício**: Cobertura legislativa municipal e estadual (SP)
88
+
89
+ ### 3. Termos LGBTQIA+ Expandidos
90
+ +20 novos termos incluindo:
91
+ - Identidades: bissexual, pansexual, não-binário
92
+ - Direitos: casamento igualitário, adoção homoafetiva
93
+ - Procedimentos: mudança de nome, retificação de registro
94
+
95
+ **Benefício**: Captura mais PLs relevantes
96
+
97
+ ## 📈 Impacto Esperado Após Sync
98
+
99
+ ### Performance
100
+ - ✅ AzMina carrega sem erro (fix crítico)
101
+ - ✅ Mais PLs encontradas (termos expandidos)
102
+ - ✅ Mais fontes disponíveis (4 vs 2)
103
+
104
+ ### User Experience
105
+ - ✅ Campos interativos (sliders respondem melhor)
106
+ - ✅ Ano final dinâmico (sempre ano atual)
107
+ - ✅ 4 checkboxes (mais opções de busca)
108
+ - ✅ Descrições claras sobre cada fonte
109
+
110
+ ### Cobertura
111
+ - 📊 **Antes**: Câmara + Senado (federal)
112
+ - 📊 **Depois**: Câmara + Senado + ALESP + Câmara SP (federal + estadual + municipal)
113
+
114
+ ## ⚙️ Execução do Sync
115
+
116
+ ### Método Automático (Recomendado)
117
+ ```bash
118
+ cd "/Users/vektra/Desenvolvimento/Radar Social LGBTQIA/pacote-radar-social-lgbtqia-v2.1/radar-legislativo-lgbtqia"
119
+
120
+ # Copiar arquivos atualizados
121
+ cp "/Users/vektra/Desenvolvimento/AzMina QuiterIA/app.py" .
122
+ cp "/Users/vektra/Desenvolvimento/AzMina QuiterIA/ensemble_híbrido.py" .
123
+ cp "/Users/vektra/Desenvolvimento/AzMina QuiterIA/api_radar.py" .
124
+
125
+ # Revisar mudanças
126
+ git diff
127
+
128
+ # Commit
129
+ git add app.py ensemble_híbrido.py api_radar.py
130
+ git commit -m "🔄 Sync do experimento: Fix AzMina + ALESP + Câmara SP + UX melhorada
131
+
132
+ - Fix crítico: Carregamento do modelo AzMina com tokenizer explícito
133
+ - Novas fontes: ALESP e Câmara Municipal SP implementadas
134
+ - Termos expandidos: +20 novos termos LGBTQIA+
135
+ - UX melhorada: campos interativos, ano final dinâmico
136
+ - API Senado: endpoint mais robusto (/materia/pesquisa/lista)"
137
+
138
+ # Push para HF Space
139
+ git push origin main
140
+ ```
141
+
142
+ ### Método Manual (Mais Controle)
143
+ 1. Abrir cada arquivo lado a lado
144
+ 2. Copiar mudanças manualmente
145
+ 3. Testar localmente antes de commit
146
+ 4. Commit e push
147
+
148
+ ## 🧪 Teste Local Antes de Deploy
149
+
150
+ ```bash
151
+ cd "/Users/vektra/Desenvolvimento/Radar Social LGBTQIA/pacote-radar-social-lgbtqia-v2.1/radar-legislativo-lgbtqia"
152
+
153
+ # Instalar/atualizar dependências
154
+ pip install -r requirements.txt
155
+
156
+ # Testar app
157
+ python app.py
158
+
159
+ # Verificar:
160
+ # 1. AzMina carrega sem erro
161
+ # 2. Interface mostra 4 checkboxes
162
+ # 3. Busca funciona em todas as fontes
163
+ ```
164
+
165
+ ## 📝 Atualizar Documentação
166
+
167
+ Após sync, atualizar:
168
+ - [ ] `README.md`: Mencionar ALESP e Câmara SP
169
+ - [ ] `DEPLOY_COMPLETO.md`: Adicionar novas fontes
170
+ - [ ] Card do Space: Atualizar descrição
171
+
172
+ ---
173
+
174
+ **Recomendação**: Executar sync automático agora, é safe e traz melhorias críticas! 🚀
175
+
api_radar.py CHANGED
@@ -9,6 +9,9 @@ from datetime import datetime, timedelta
9
  from typing import List, Dict, Optional
10
  import re
11
  import time
 
 
 
12
 
13
  # URLs das APIs
14
  API_CAMARA = "https://dadosabertos.camara.leg.br/api/v2"
@@ -19,6 +22,7 @@ API_ALESP = None # Verificar se há API pública
19
  # Termos para filtrar PLs relacionadas a LGBTQIA+
20
  # TERMOS ESPECÍFICOS primeiro (mais relevantes)
21
  TERMOS_BUSCA_ESPECIFICOS = [
 
22
  "lgbt",
23
  "lgbtqia",
24
  "lgbtqia+",
@@ -29,10 +33,29 @@ TERMOS_BUSCA_ESPECIFICOS = [
29
  "homofobia",
30
  "transfobia",
31
  "homossexual",
 
 
32
  "identidade de gênero",
33
  "orientação sexual",
34
  "diversidade sexual",
 
 
 
 
 
 
 
35
  "nome social",
 
 
 
 
 
 
 
 
 
 
36
  "terapia de conversão",
37
  "cura gay",
38
  "reparação sexual"
@@ -51,7 +74,11 @@ TERMOS_BUSCA_CONTEXTUAIS = [
51
  "lules",
52
  "símbolos religiosos.*parada",
53
  "menor.*evento.*lgbt",
54
- "comunidade lgbt"
 
 
 
 
55
  ]
56
 
57
  TERMOS_BUSCA = TERMOS_BUSCA_ESPECIFICOS + TERMOS_BUSCA_CONTEXTUAIS
@@ -249,7 +276,10 @@ def buscar_senado_federal(
249
  Busca PLs no Senado Federal
250
 
251
  API: https://legis.senado.leg.br/dadosabertos
252
- Nota: API do Senado é mais complexa, esta é uma implementação básica que busca PLS por ano
 
 
 
253
  """
254
  if termos is None:
255
  termos = TERMOS_BUSCA
@@ -257,167 +287,412 @@ def buscar_senado_federal(
257
  # Determinar anos para buscar
258
  ano_atual = datetime.now().year
259
  if ano_inicio_manual is not None and ano_fim_manual is not None:
260
- anos_para_buscar = [ano_inicio_manual] if ano_inicio_manual == ano_fim_manual else list(range(ano_inicio_manual, ano_fim_manual + 1))
261
  else:
262
  anos_para_buscar = [ano_atual]
263
 
264
  pls_encontradas = []
265
 
266
- # API do Senado Federal
267
- # Endpoint: /dadosabertos/materia/atualizadas
268
- # Retorna matérias atualizadas com ementa completa
269
- # Estrutura: ListaMateriasAtualizadas -> Materias -> Materia[] -> DadosBasicosMateria.EmentaMateria
270
- # Documentação: https://legis.senado.leg.br/dadosabertos/api-docs/swagger-ui/index.html
271
-
272
- print(f" 📥 Buscando matérias atualizadas no Senado...")
273
 
274
- url_base = "https://legis.senado.leg.br/dadosabertos/materia/atualizadas"
275
 
276
  try:
277
- headers = {"Accept": "application/json"}
278
- response = requests.get(url_base, headers=headers, timeout=20)
279
- response.raise_for_status()
280
-
281
- data = response.json()
282
-
283
- if 'ListaMateriasAtualizadas' not in data:
284
- print(f" ⚠️ Estrutura de resposta inesperada do Senado")
285
- return []
286
-
287
- lista = data['ListaMateriasAtualizadas']
288
- materias_data = lista.get('Materias', {})
289
-
290
- if isinstance(materias_data, dict) and 'Materia' in materias_data:
291
- materias_list = materias_data['Materia']
292
- materias_list = materias_list if isinstance(materias_list, list) else [materias_list]
293
- elif isinstance(materias_data, list):
294
- materias_list = materias_data
295
- else:
296
- materias_list = []
297
-
298
- if not materias_list:
299
- print(f" ℹ️ Nenhuma matéria encontrada no Senado")
300
- return []
301
-
302
- print(f" 📊 Total de matérias atualizadas: {len(materias_list)} (antes do filtro)")
303
-
304
- # Filtrar por ano e termos LGBTQIA+
305
- for materia in materias_list:
306
  if len(pls_encontradas) >= limite:
307
  break
308
 
309
- # Extrair informações
310
- ident = materia.get('IdentificacaoMateria', {})
311
- dados = materia.get('DadosBasicosMateria', {})
312
-
313
- ano_materia = ident.get('AnoMateria', '')
314
- sigla = ident.get('SiglaSubtipoMateria', '')
315
- numero = ident.get('NumeroMateria', '')
316
- codigo = ident.get('CodigoMateria', '')
317
-
318
- # Filtrar por ano se especificado
319
- if ano_inicio_manual is not None and ano_fim_manual is not None:
320
- try:
321
- ano_int = int(ano_materia) if ano_materia else 0
322
- if not (ano_inicio_manual <= ano_int <= ano_fim_manual):
323
- continue
324
- except:
325
  continue
326
-
327
- # Filtrar apenas PLS (ou outras siglas de projeto de lei)
328
- if sigla not in ['PLS', 'PLC', 'PL']:
329
- continue
330
-
331
- # Obter ementa
332
- ementa = dados.get('EmentaMateria', '')
333
- if not ementa or len(ementa) < 10:
334
- continue
335
-
336
- ementa_lower = ementa.lower()
337
-
338
- # Filtrar por termos LGBTQIA+ (mesma lógica da Câmara)
339
- tem_termo_especifico = False
340
- for termo in TERMOS_BUSCA_ESPECIFICOS:
341
- if termo == 'trans' and 'trans' in ementa_lower:
342
- if re.search(r'\btrans\b', ementa_lower) and (
343
- any(palavra in ementa_lower for palavra in ['gênero', 'sexual', 'identidade', 'lgbt', 'transfobia', 'transexual', 'transgênero']) or
344
- any(palavra in ementa_lower for palavra in ['proíbe', 'veda', 'restringe', 'garante', 'reconhece', 'criminaliza', 'direito', 'direitos'])
345
- ):
346
- tem_termo_especifico = True
347
- break
348
- elif termo.lower() in ementa_lower:
349
- tem_termo_especifico = True
350
- break
351
-
352
- palavras_legislativas = ['proíbe', 'veda', 'restringe', 'garante', 'reconhece', 'criminaliza',
353
- 'orientação', 'identidade', 'gênero', 'sexual', 'direito', 'direitos',
354
- 'dispõe', 'altera', 'estabelece', 'define']
355
- tem_termo_contextual = any(
356
- termo.lower() in ementa_lower
357
- for termo in TERMOS_BUSCA_CONTEXTUAIS[:8]
358
- ) and any(
359
- palavra in ementa_lower for palavra in palavras_legislativas
360
- )
361
-
362
- if tem_termo_especifico or tem_termo_contextual:
363
- autor = 'N/A'
364
- if 'AutoresPrincipais' in materia:
365
- autor_data = materia.get('AutoresPrincipais', {})
366
- if isinstance(autor_data, dict) and 'AutorPrincipal' in autor_data:
367
- autor_obj = autor_data['AutorPrincipal']
368
- if isinstance(autor_obj, dict):
369
- autor = autor_obj.get('NomeAutor', 'N/A')
370
 
371
- data_apresentacao = dados.get('DataApresentacao', 'N/A')
372
 
373
- link = f"https://www25.senado.leg.br/web/atividade/materias/-/materia/{codigo}" if codigo else f"https://www25.senado.leg.br/web/atividade/materias"
 
 
 
 
 
 
374
 
375
- pls_encontradas.append({
376
- 'Nº': f"{sigla} {numero}/{ano_materia}",
377
- 'Ano': str(ano_materia),
378
- 'Casa': 'Senado',
379
- 'Ementa': ementa,
380
- 'Autores': autor,
381
- 'Data': data_apresentacao,
382
- 'Link': link,
383
- 'Status': ident.get('DescricaoIdentificacaoMateria', 'N/A'),
384
- 'Fonte': 'Senado Federal'
385
- })
386
-
387
- print(f" ✅ {len(pls_encontradas)} PLs relevantes encontradas no Senado")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
 
 
389
  return pls_encontradas[:limite]
390
 
391
- except requests.exceptions.HTTPError as e:
392
- print(f" ⚠️ Erro HTTP ao buscar no Senado: {e.response.status_code}")
393
- return []
394
  except Exception as e:
395
- print(f" ⚠️ Erro ao buscar no Senado: {str(e)[:100]}")
396
  return []
397
 
398
  def buscar_camara_sao_paulo(
399
  termos: List[str] = None,
 
 
400
  limite: int = 50
401
  ) -> List[Dict]:
402
  """
403
  Busca PLs na Câmara Municipal de São Paulo
404
 
405
- Nota: Pode não ter API pública - implementação futura via scraping
 
 
406
  """
407
- print("⚠️ Busca na Câmara Municipal de SP ainda não implementada")
408
- return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
 
410
  def buscar_alesp(
411
  termos: List[str] = None,
 
 
412
  limite: int = 50
413
  ) -> List[Dict]:
414
  """
415
- Busca PLs na ALESP
 
 
 
 
416
 
417
- Nota: Pode não ter API pública - implementação futura via scraping
 
418
  """
419
- print("⚠️ Busca na ALESP ainda não implementada")
420
- return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
 
422
  def buscar_todas_fontes(
423
  termos: List[str] = None,
 
9
  from typing import List, Dict, Optional
10
  import re
11
  import time
12
+ import xml.etree.ElementTree as ET
13
+ import zipfile
14
+ from io import BytesIO
15
 
16
  # URLs das APIs
17
  API_CAMARA = "https://dadosabertos.camara.leg.br/api/v2"
 
22
  # Termos para filtrar PLs relacionadas a LGBTQIA+
23
  # TERMOS ESPECÍFICOS primeiro (mais relevantes)
24
  TERMOS_BUSCA_ESPECIFICOS = [
25
+ # Termos básicos
26
  "lgbt",
27
  "lgbtqia",
28
  "lgbtqia+",
 
33
  "homofobia",
34
  "transfobia",
35
  "homossexual",
36
+
37
+ # Identidade e orientação
38
  "identidade de gênero",
39
  "orientação sexual",
40
  "diversidade sexual",
41
+ "bissexual",
42
+ "pansexual",
43
+ "não-binário",
44
+ "não binário",
45
+ "cisgênero",
46
+
47
+ # Direitos e procedimentos
48
  "nome social",
49
+ "casamento igualitário",
50
+ "união homoafetiva",
51
+ "adoção homoafetiva",
52
+ "mudança de nome",
53
+ "retificação de registro",
54
+
55
+ # Discriminação e violência
56
+ "discriminação sexual",
57
+ "preconceito sexual",
58
+ "criminalização da homofobia",
59
  "terapia de conversão",
60
  "cura gay",
61
  "reparação sexual"
 
74
  "lules",
75
  "símbolos religiosos.*parada",
76
  "menor.*evento.*lgbt",
77
+ "comunidade lgbt",
78
+ "sexo biológico",
79
+ "gênero biológico",
80
+ "família tradicional",
81
+ "masculino e feminino"
82
  ]
83
 
84
  TERMOS_BUSCA = TERMOS_BUSCA_ESPECIFICOS + TERMOS_BUSCA_CONTEXTUAIS
 
276
  Busca PLs no Senado Federal
277
 
278
  API: https://legis.senado.leg.br/dadosabertos
279
+ Endpoint: /materia/pesquisa/lista
280
+
281
+ ✅ Este endpoint permite buscar matérias por ano de apresentação, resolvendo
282
+ o problema de lacunas em dados históricos.
283
  """
284
  if termos is None:
285
  termos = TERMOS_BUSCA
 
287
  # Determinar anos para buscar
288
  ano_atual = datetime.now().year
289
  if ano_inicio_manual is not None and ano_fim_manual is not None:
290
+ anos_para_buscar = list(range(ano_inicio_manual, ano_fim_manual + 1))
291
  else:
292
  anos_para_buscar = [ano_atual]
293
 
294
  pls_encontradas = []
295
 
296
+ # API do Senado Federal - endpoint /materia/pesquisa/lista
297
+ url_base = "https://legis.senado.leg.br/dadosabertos/materia/pesquisa/lista"
 
 
 
 
 
298
 
299
+ print(f" 📥 Buscando no Senado (anos {min(anos_para_buscar)}-{max(anos_para_buscar)})...")
300
 
301
  try:
302
+ for ano in reversed(anos_para_buscar):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  if len(pls_encontradas) >= limite:
304
  break
305
 
306
+ try:
307
+ # Buscar todas as matérias apresentadas no ano especificado
308
+ params = {'ano': str(ano)}
309
+
310
+ response = requests.get(url_base, params=params, headers={'Accept': 'application/json'}, timeout=30)
311
+ response.raise_for_status()
312
+
313
+ data = response.json()
314
+
315
+ if 'PesquisaBasicaMateria' not in data:
316
+ print(f" ℹ️ Resposta inesperada do Senado em {ano}")
 
 
 
 
 
317
  continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
 
319
+ materias_data = data['PesquisaBasicaMateria'].get('Materias', {})
320
 
321
+ if isinstance(materias_data, dict) and 'Materia' in materias_data:
322
+ materias = materias_data['Materia']
323
+ materias = materias if isinstance(materias, list) else [materias]
324
+ elif isinstance(materias_data, list):
325
+ materias = materias_data
326
+ else:
327
+ materias = []
328
 
329
+ if not materias:
330
+ print(f" ℹ️ Nenhuma matéria encontrada no Senado em {ano}")
331
+ continue
332
+
333
+ print(f" 📊 Senado {ano}: {len(materias)} matérias (antes do filtro)")
334
+
335
+ # Processar cada matéria
336
+ materias_ano = 0
337
+ for materia in materias:
338
+ if len(pls_encontradas) >= limite:
339
+ break
340
+
341
+ try:
342
+ # Extrair informações da matéria (estrutura simplificada da API /pesquisa/lista)
343
+ sigla = materia.get('Sigla', '')
344
+ numero = materia.get('Numero', '')
345
+ ano_materia = materia.get('Ano', '')
346
+ codigo = materia.get('Codigo', '')
347
+ ementa = materia.get('Ementa', '')
348
+ autor = materia.get('Autor', 'N/A')
349
+ data = materia.get('Data', 'N/A')
350
+
351
+ # Filtrar apenas Projetos de Lei (PL, PLS, PLC, PLP)
352
+ if sigla not in ['PLS', 'PLC', 'PL', 'PLP']:
353
+ continue
354
+
355
+ if not ementa or len(ementa) < 10:
356
+ continue
357
+
358
+ ementa_lower = ementa.lower()
359
+
360
+ # Filtrar por termos LGBTQIA+ (mesma lógica das outras fontes)
361
+ tem_termo_especifico = False
362
+ for termo in TERMOS_BUSCA_ESPECIFICOS:
363
+ if termo == 'trans' and 'trans' in ementa_lower:
364
+ if re.search(r'\btrans\b', ementa_lower) and (
365
+ any(p in ementa_lower for p in ['gênero', 'sexual', 'identidade', 'lgbt', 'transfobia', 'transexual', 'transgênero']) or
366
+ any(p in ementa_lower for p in ['proíbe', 'veda', 'restringe', 'garante', 'reconhece', 'criminaliza', 'direito', 'direitos'])
367
+ ):
368
+ tem_termo_especifico = True
369
+ break
370
+ elif termo.lower() in ementa_lower:
371
+ tem_termo_especifico = True
372
+ break
373
+
374
+ palavras_legislativas = ['proíbe', 'veda', 'restringe', 'garante', 'reconhece', 'criminaliza',
375
+ 'orientação', 'identidade', 'gênero', 'sexual', 'direito', 'direitos',
376
+ 'dispõe', 'altera', 'estabelece', 'define']
377
+ tem_termo_contextual = any(
378
+ termo.lower() in ementa_lower
379
+ for termo in TERMOS_BUSCA_CONTEXTUAIS[:8]
380
+ ) and any(
381
+ palavra in ementa_lower for palavra in palavras_legislativas
382
+ )
383
+
384
+ if tem_termo_especifico or tem_termo_contextual:
385
+ # Construir link para matéria
386
+ link = f"https://www25.senado.leg.br/web/atividade/materias/-/materia/{codigo}" if codigo else "https://www25.senado.leg.br/web/atividade/materias"
387
+
388
+ pls_encontradas.append({
389
+ 'Nº': f"{sigla} {numero}/{ano_materia}",
390
+ 'Ano': str(ano_materia),
391
+ 'Casa': 'Senado',
392
+ 'Ementa': ementa,
393
+ 'Autores': autor,
394
+ 'Data': data[:10] if isinstance(data, str) and len(data) >= 10 else str(data),
395
+ 'Link': link,
396
+ 'Status': materia.get('DescricaoIdentificacao', 'N/A'),
397
+ 'Fonte': 'Senado Federal'
398
+ })
399
+
400
+ materias_ano += 1
401
+
402
+ except Exception as e:
403
+ # Pular matéria se houver erro no parse
404
+ continue
405
+
406
+ if materias_ano > 0:
407
+ print(f" ✅ Senado {ano}: {materias_ano} PLs relevantes")
408
+
409
+ except requests.exceptions.HTTPError as e:
410
+ print(f" ⚠️ Erro HTTP no Senado ({ano}): {e.response.status_code}")
411
+ continue
412
+ except Exception as e:
413
+ print(f" ⚠️ Erro no Senado ({ano}): {str(e)[:80]}")
414
+ continue
415
 
416
+ print(f" 📊 Total Senado: {len(pls_encontradas)} PLs")
417
  return pls_encontradas[:limite]
418
 
 
 
 
419
  except Exception as e:
420
+ print(f" ⚠️ Erro geral ao buscar no Senado: {str(e)[:100]}")
421
  return []
422
 
423
  def buscar_camara_sao_paulo(
424
  termos: List[str] = None,
425
+ ano_inicio_manual: Optional[int] = None,
426
+ ano_fim_manual: Optional[int] = None,
427
  limite: int = 50
428
  ) -> List[Dict]:
429
  """
430
  Busca PLs na Câmara Municipal de São Paulo
431
 
432
+ Web Service: https://splegisws.saopaulo.sp.leg.br/ws/ws2.asmx
433
+ Método: ProjetosPorAnoJSON
434
+ Portal de Dados Abertos: https://www.saopaulo.sp.leg.br/transparencia/dados-abertos/dados-disponibilizados-em-formato-aberto/
435
  """
436
+ if termos is None:
437
+ termos = TERMOS_BUSCA
438
+
439
+ print(f"📥 Buscando projetos na Câmara Municipal de SP...")
440
+
441
+ # URL do web service
442
+ base_url = "https://splegisws.saopaulo.sp.leg.br/ws/ws2.asmx/ProjetosPorAnoJSON"
443
+
444
+ pls_encontradas = []
445
+
446
+ # Determinar anos para buscar
447
+ ano_atual = datetime.now().year
448
+
449
+ if ano_inicio_manual is not None and ano_fim_manual is not None:
450
+ anos_para_buscar = list(range(ano_inicio_manual, ano_fim_manual + 1))
451
+ else:
452
+ # Padrão: ano atual
453
+ anos_para_buscar = [ano_atual]
454
+
455
+ try:
456
+ # Buscar projetos por ano
457
+ for ano in reversed(anos_para_buscar):
458
+ if len(pls_encontradas) >= limite:
459
+ break
460
+
461
+ print(f" 📅 Buscando projetos de {ano}...")
462
+
463
+ try:
464
+ # Chamar web service
465
+ params = {'Ano': ano}
466
+ response = requests.get(base_url, params=params, timeout=30)
467
+ response.raise_for_status()
468
+
469
+ projetos = response.json()
470
+ if not isinstance(projetos, list):
471
+ print(f" ⚠️ Resposta não é lista: {type(projetos)}")
472
+ continue
473
+
474
+ print(f" 📊 {len(projetos)} projetos encontrados em {ano} (antes do filtro)")
475
+
476
+ # Filtrar por termos LGBTQIA+
477
+ for projeto in projetos:
478
+ if len(pls_encontradas) >= limite:
479
+ break
480
+
481
+ ementa = projeto.get('ementa', '')
482
+ if not ementa or len(ementa) < 10:
483
+ continue
484
+
485
+ ementa_lower = ementa.lower()
486
+
487
+ # Filtrar por termos específicos primeiro
488
+ tem_termo_especifico = False
489
+ for termo in TERMOS_BUSCA_ESPECIFICOS:
490
+ if termo == 'trans' and 'trans' in ementa_lower:
491
+ if re.search(r'\btrans\b', ementa_lower) and (
492
+ any(palavra in ementa_lower for palavra in ['gênero', 'sexual', 'identidade', 'lgbt', 'transfobia', 'transexual', 'transgênero']) or
493
+ any(palavra in ementa_lower for palavra in ['proíbe', 'veda', 'restringe', 'garante', 'reconhece', 'criminaliza', 'direito', 'direitos'])
494
+ ):
495
+ tem_termo_especifico = True
496
+ break
497
+ elif termo.lower() in ementa_lower:
498
+ tem_termo_especifico = True
499
+ break
500
+
501
+ # Verificar termos contextuais
502
+ palavras_legislativas = ['proíbe', 'veda', 'restringe', 'garante', 'reconhece', 'criminaliza',
503
+ 'orientação', 'identidade', 'gênero', 'sexual', 'direito', 'direitos',
504
+ 'dispõe', 'altera', 'estabelece', 'define']
505
+ tem_termo_contextual = any(
506
+ termo.lower() in ementa_lower
507
+ for termo in TERMOS_BUSCA_CONTEXTUAIS[:8]
508
+ ) and any(
509
+ palavra in ementa_lower for palavra in palavras_legislativas
510
+ )
511
+
512
+ if tem_termo_especifico or tem_termo_contextual:
513
+ tipo = projeto.get('tipo', 'PL')
514
+ numero = projeto.get('numero', 'N/A')
515
+ ano_projeto = projeto.get('ano', 'N/A')
516
+ data_projeto = projeto.get('data', 'N/A')
517
+ chave = projeto.get('chave', '')
518
+
519
+ # Construir link (baseado na estrutura comum da Câmara SP)
520
+ link = f"https://www.saopaulo.sp.leg.br/vereadores/projetos-de-lei/?projeto={chave}" if chave else f"https://www.saopaulo.sp.leg.br/"
521
+
522
+ pls_encontradas.append({
523
+ 'Nº': f"{tipo} {numero}/{ano_projeto}",
524
+ 'Ano': str(ano_projeto),
525
+ 'Casa': 'Câmara Municipal SP',
526
+ 'Ementa': ementa,
527
+ 'Autores': 'N/A', # Pode obter via ProjetosAutoresJSON se necessário
528
+ 'Data': data_projeto[:10] if isinstance(data_projeto, str) and len(data_projeto) >= 10 else str(data_projeto),
529
+ 'Link': link,
530
+ 'Status': 'N/A',
531
+ 'Fonte': 'Câmara Municipal de São Paulo'
532
+ })
533
+
534
+ if pls_encontradas:
535
+ print(f" ✅ {len(pls_encontradas)} projetos relevantes encontrados em {ano}")
536
+
537
+ except requests.exceptions.HTTPError as e:
538
+ print(f" ⚠️ Erro HTTP ao buscar projetos de {ano}: {e.response.status_code}")
539
+ continue
540
+ except Exception as e:
541
+ print(f" ⚠️ Erro ao buscar projetos de {ano}: {str(e)[:100]}")
542
+ continue
543
+
544
+ print(f" ✅ Total: {len(pls_encontradas)} projetos relevantes encontrados na Câmara Municipal SP")
545
+
546
+ return pls_encontradas[:limite]
547
+
548
+ except Exception as e:
549
+ print(f" ⚠️ Erro geral ao buscar na Câmara Municipal SP: {str(e)[:150]}")
550
+ return []
551
 
552
  def buscar_alesp(
553
  termos: List[str] = None,
554
+ ano_inicio_manual: Optional[int] = None,
555
+ ano_fim_manual: Optional[int] = None,
556
  limite: int = 50
557
  ) -> List[Dict]:
558
  """
559
+ Busca PLs na ALESP (Assembleia Legislativa de São Paulo)
560
+
561
+ Portal: https://www.al.sp.gov.br/dados-abertos/
562
+ Arquivo: https://www.al.sp.gov.br/repositorioDados/processo_legislativo/proposituras.zip
563
+ Formato: ZIP contendo XML com todas as proposituras (atualizado diariamente)
564
 
565
+ Frequência de atualização: Diária
566
+ Portal de dados abertos: https://www.al.sp.gov.br/dados-abertos/recurso/56
567
  """
568
+ if termos is None:
569
+ termos = TERMOS_BUSCA
570
+
571
+ print(f" 📥 Buscando proposituras na ALESP...")
572
+ # NOTA: O arquivo proposituras.zip é atualizado DIARIAMENTE no portal da ALESP.
573
+ # Para garantir dados atualizados, baixamos o arquivo toda vez que uma busca é feita.
574
+ # Isso garante que mesmo no Hugging Face Space, sempre teremos os dados mais recentes.
575
+
576
+ # URL do arquivo ZIP
577
+ url_zip = "https://www.al.sp.gov.br/repositorioDados/processo_legislativo/proposituras.zip"
578
+
579
+ pls_encontradas = []
580
+
581
+ try:
582
+ # Baixar arquivo ZIP (sob demanda - sempre busca a versão mais recente)
583
+ print(f" 📦 Baixando arquivo proposituras.zip atualizado (última atualização do portal)...")
584
+ print(f" ⏱️ Isso garante dados atualizados diariamente (pode levar 10-20 segundos)")
585
+ response = requests.get(url_zip, timeout=120, stream=True)
586
+ response.raise_for_status()
587
+
588
+ zip_data = BytesIO(response.content)
589
+
590
+ with zipfile.ZipFile(zip_data, 'r') as zip_ref:
591
+ files = zip_ref.namelist()
592
+ if not files:
593
+ print(f" ⚠️ ZIP vazio")
594
+ return []
595
+
596
+ xml_file = files[0]
597
+ print(f" 📄 Extraindo {xml_file}...")
598
+
599
+ # Ler XML (pode ser grande, mas preciso parsear)
600
+ xml_content = zip_ref.read(xml_file)
601
+ print(f" 📊 XML extraído: {len(xml_content)/1024/1024:.1f}MB")
602
+
603
+ # Parsear XML
604
+ root = ET.fromstring(xml_content)
605
+
606
+ # Buscar todas as proposituras
607
+ proposituras = root.findall('.//propositura')
608
+ total_props = len(proposituras)
609
+ print(f" 📋 Total de proposituras no arquivo: {total_props}")
610
+
611
+ # Filtrar proposituras
612
+ for propositura in proposituras:
613
+ if len(pls_encontradas) >= limite:
614
+ break
615
+
616
+ # Extrair campos do XML
617
+ ano_text = propositura.findtext('AnoLegislativo', '')
618
+ numero_text = propositura.findtext('NroLegislativo', '')
619
+ ementa = propositura.findtext('Ementa', '')
620
+ id_doc = propositura.findtext('IdDocumento', '')
621
+ data_entrada = propositura.findtext('DtEntradaSistema', '')
622
+ natureza_id = propositura.findtext('IdNatureza', '')
623
+
624
+ if not ementa or len(ementa) < 10:
625
+ continue
626
+
627
+ # Filtrar por ano se especificado
628
+ if ano_inicio_manual is not None and ano_fim_manual is not None:
629
+ try:
630
+ ano_int = int(ano_text) if ano_text else 0
631
+ if not (ano_inicio_manual <= ano_int <= ano_fim_manual):
632
+ continue
633
+ except:
634
+ continue
635
+
636
+ # Filtrar por termos LGBTQIA+
637
+ ementa_lower = ementa.lower()
638
+
639
+ tem_termo_especifico = False
640
+ for termo in TERMOS_BUSCA_ESPECIFICOS:
641
+ if termo == 'trans' and 'trans' in ementa_lower:
642
+ if re.search(r'\btrans\b', ementa_lower) and (
643
+ any(palavra in ementa_lower for palavra in ['gênero', 'sexual', 'identidade', 'lgbt', 'transfobia', 'transexual', 'transgênero']) or
644
+ any(palavra in ementa_lower for palavra in ['proíbe', 'veda', 'restringe', 'garante', 'reconhece', 'criminaliza', 'direito', 'direitos'])
645
+ ):
646
+ tem_termo_especifico = True
647
+ break
648
+ elif termo.lower() in ementa_lower:
649
+ tem_termo_especifico = True
650
+ break
651
+
652
+ palavras_legislativas = ['proíbe', 'veda', 'restringe', 'garante', 'reconhece', 'criminaliza',
653
+ 'orientação', 'identidade', 'gênero', 'sexual', 'direito', 'direitos',
654
+ 'dispõe', 'altera', 'estabelece', 'define']
655
+ tem_termo_contextual = any(
656
+ termo.lower() in ementa_lower
657
+ for termo in TERMOS_BUSCA_CONTEXTUAIS[:8]
658
+ ) and any(
659
+ palavra in ementa_lower for palavra in palavras_legislativas
660
+ )
661
+
662
+ if tem_termo_especifico or tem_termo_contextual:
663
+ # Determinar sigla do tipo (pode estar em outros campos)
664
+ sigla = 'PL' # Padrão
665
+ tipo_text = propositura.findtext('Sigla', '') or propositura.findtext('Tipo', '')
666
+ if tipo_text:
667
+ sigla = tipo_text.upper()
668
+
669
+ # Link para propositura (formato comum da ALESP)
670
+ link = f"https://www.al.sp.gov.br/propositura/?id={id_doc}" if id_doc else "https://www.al.sp.gov.br/"
671
+
672
+ pls_encontradas.append({
673
+ 'Nº': f"{sigla} {numero_text}/{ano_text}" if numero_text and ano_text else f"Nº {id_doc}",
674
+ 'Ano': ano_text or 'N/A',
675
+ 'Casa': 'ALESP',
676
+ 'Ementa': ementa,
677
+ 'Autores': propositura.findtext('Autor', 'N/A'),
678
+ 'Data': data_entrada[:10] if data_entrada else 'N/A', # Apenas data, sem hora
679
+ 'Link': link,
680
+ 'Status': 'N/A',
681
+ 'Fonte': 'ALESP'
682
+ })
683
+
684
+ print(f" ✅ {len(pls_encontradas)} proposituras relevantes encontradas na ALESP")
685
+
686
+ return pls_encontradas[:limite]
687
+
688
+ except requests.exceptions.HTTPError as e:
689
+ print(f" ⚠️ Erro HTTP ao buscar na ALESP: {e.response.status_code}")
690
+ return []
691
+ except Exception as e:
692
+ print(f" ⚠️ Erro ao buscar na ALESP: {str(e)[:150]}")
693
+ import traceback
694
+ print(f" Detalhes: {traceback.format_exc()[:200]}")
695
+ return []
696
 
697
  def buscar_todas_fontes(
698
  termos: List[str] = None,
app.py CHANGED
@@ -8,7 +8,7 @@ import pandas as pd
8
  import re
9
  from datetime import datetime
10
  from ensemble_híbrido import classificar_ensemble, carregar_modelos
11
- from api_radar import buscar_camara_deputados, buscar_senado_federal, filtrar_pls_relevantes
12
 
13
  # Carregar modelos uma vez no início
14
  print("🏳️‍🌈 Carregando modelos...")
@@ -26,7 +26,8 @@ with gr.Blocks(
26
  # 🏳️‍🌈 Radar Legislativo LGBTQIA+
27
 
28
  Sistema de busca e análise automática de Projetos de Lei relacionados a direitos LGBTQIA+
29
- no **Congresso Nacional** (Câmara dos Deputados e Senado Federal).
 
30
 
31
  Utiliza **Ensemble Híbrido** (Radar Social + AzMina/QuiterIA + Keywords + Padrões) para identificar
32
  se PLs são **favoráveis** ou **desfavoráveis** aos direitos da comunidade LGBTQIA+.
@@ -38,13 +39,15 @@ with gr.Blocks(
38
 
39
 
40
  # RADAR AUTOMÁTICO - Única aba
41
- with gr.Tab("🔍 Radar Automático - Congresso Nacional"):
42
  gr.Markdown("""
43
- ### 🔍 Radar Automático de PLs LGBTQIA+ - Congresso Nacional
44
 
45
  Busca e analisa automaticamente PLs relacionadas a direitos LGBTQIA+ nas APIs oficiais:
46
- - **Câmara dos Deputados** ✅
47
- - **Senado Federal** ✅
 
 
48
 
49
  ⚠️ **Atenção:** A busca pode levar alguns segundos, especialmente em períodos longos.
50
  """)
@@ -56,6 +59,7 @@ with gr.Blocks(
56
  maximum=2025,
57
  value=2020,
58
  step=1,
 
59
  info="Ano mais antigo para buscar"
60
  )
61
 
@@ -63,8 +67,9 @@ with gr.Blocks(
63
  label="Ano Final",
64
  minimum=2010,
65
  maximum=2025,
66
- value=2025,
67
  step=1,
 
68
  info="Ano mais recente para buscar"
69
  )
70
 
@@ -74,21 +79,33 @@ with gr.Blocks(
74
  maximum=100,
75
  value=50,
76
  step=5,
 
77
  info="Número máximo de PLs encontradas"
78
  )
79
 
80
  with gr.Row():
81
  btn_buscar = gr.Button("🔍 Buscar e Analisar PLs", variant="primary", scale=2)
 
 
82
  checkbox_camara = gr.Checkbox(label="Câmara dos Deputados", value=True)
83
  checkbox_senado = gr.Checkbox(label="Senado Federal", value=True)
 
 
 
 
 
 
84
 
85
  output_busca = gr.Markdown(label="📊 PLs Encontradas e Analisadas")
86
 
87
- def buscar_e_analisar(ano_inicio, ano_fim, limite, buscar_camara, buscar_senado):
88
  """Busca PLs e analisa automaticamente"""
89
  import sys
90
  from io import StringIO
91
 
 
 
 
92
  # Validar anos
93
  if ano_inicio > ano_fim:
94
  return "❌ **Erro:** Ano inicial deve ser menor ou igual ao ano final."
@@ -96,8 +113,8 @@ with gr.Blocks(
96
  if ano_fim > datetime.now().year:
97
  return f"❌ **Erro:** Ano final não pode ser maior que {datetime.now().year}."
98
 
99
- if not buscar_camara and not buscar_senado:
100
- return "❌ **Erro:** Selecione pelo menos uma fonte (Câmara ou Senado)."
101
 
102
  # Capturar prints para exibir na interface
103
  old_stdout = sys.stdout
@@ -107,17 +124,37 @@ with gr.Blocks(
107
  pls_encontradas = []
108
  anos_para_buscar = list(range(int(ano_inicio), int(ano_fim) + 1))
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  print(f"🔍 Buscando PLs LGBTQIA+ no Congresso Nacional...")
111
  print(f"📅 Período: {ano_inicio} a {ano_fim} ({len(anos_para_buscar)} anos)")
 
 
112
 
113
  # 1. Câmara dos Deputados
114
- if buscar_camara:
115
- print(f"\n📥 Buscando na Câmara dos Deputados...")
 
116
  for ano in reversed(anos_para_buscar):
117
- if len(pls_encontradas) >= int(limite):
118
  break
119
 
120
- limite_restante = int(limite) - len(pls_encontradas)
121
  if limite_restante <= 0:
122
  break
123
 
@@ -127,18 +164,22 @@ with gr.Blocks(
127
  ano_inicio_manual=ano,
128
  ano_fim_manual=ano
129
  )
130
- pls_encontradas.extend(pls_ano)
131
  if pls_ano:
132
  print(f" ✅ {len(pls_ano)} PLs encontradas na Câmara em {ano}")
 
 
 
133
 
134
  # 2. Senado Federal
135
- if buscar_senado and len(pls_encontradas) < int(limite):
136
- print(f"\n📥 Buscando no Senado Federal...")
 
137
  for ano in reversed(anos_para_buscar):
138
- if len(pls_encontradas) >= int(limite):
139
  break
140
 
141
- limite_restante = int(limite) - len(pls_encontradas)
142
  if limite_restante <= 0:
143
  break
144
 
@@ -147,9 +188,46 @@ with gr.Blocks(
147
  ano_inicio_manual=ano,
148
  ano_fim_manual=ano
149
  )
150
- pls_encontradas.extend(pls_ano)
151
  if pls_ano:
152
  print(f" ✅ {len(pls_ano)} PLs encontradas no Senado em {ano}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
  # Restaurar stdout
155
  sys.stdout = old_stdout
@@ -157,10 +235,14 @@ with gr.Blocks(
157
 
158
  if not pls_encontradas:
159
  fontes = []
160
- if buscar_camara:
161
  fontes.append("Câmara dos Deputados")
162
- if buscar_senado:
163
  fontes.append("Senado Federal")
 
 
 
 
164
  fontes_str = " e ".join(fontes)
165
 
166
  return f"""⚠️ Nenhuma PL encontrada em {fontes_str} para o período {int(ano_inicio)}-{int(ano_fim)}.
@@ -236,10 +318,14 @@ with gr.Blocks(
236
  revisao = sum(1 for r in resultados if r['Classificação'] == 'REVISÃO')
237
 
238
  fontes_usadas = []
239
- if buscar_camara:
240
  fontes_usadas.append("Câmara")
241
- if buscar_senado:
242
  fontes_usadas.append("Senado")
 
 
 
 
243
 
244
  relatorio = f"""## 🔍 Radar de PLs LGBTQIA+ - Resultados
245
 
@@ -284,7 +370,7 @@ with gr.Blocks(
284
 
285
  btn_buscar.click(
286
  fn=buscar_e_analisar,
287
- inputs=[ano_inicio, ano_fim, limite_resultados, checkbox_camara, checkbox_senado],
288
  outputs=output_busca
289
  )
290
 
@@ -307,11 +393,13 @@ with gr.Blocks(
307
  - **Período médio (3-5 anos):** Balanceado, mais resultados
308
  - **Período grande (2010-2025):** Pode levar alguns minutos, muitos resultados
309
 
310
- ### ⚠️ Limitações:
311
- - A busca pode levar alguns segundos (até minutos para períodos longos)
312
- - A API da Câmara permite até 100 itens por página (buscamos múltiplas páginas)
313
- - A API do Senado ainda está em desenvolvimento básico
314
- - Depende da disponibilidade das APIs públicas
 
 
315
  """)
316
 
317
  gr.Markdown("""
 
8
  import re
9
  from datetime import datetime
10
  from ensemble_híbrido import classificar_ensemble, carregar_modelos
11
+ from api_radar import buscar_camara_deputados, buscar_senado_federal, buscar_alesp, buscar_camara_sao_paulo, filtrar_pls_relevantes
12
 
13
  # Carregar modelos uma vez no início
14
  print("🏳️‍🌈 Carregando modelos...")
 
26
  # 🏳️‍🌈 Radar Legislativo LGBTQIA+
27
 
28
  Sistema de busca e análise automática de Projetos de Lei relacionados a direitos LGBTQIA+
29
+ no **Congresso Nacional** (Câmara dos Deputados e Senado Federal), **ALESP** (Assembleia Legislativa de São Paulo)
30
+ e **Câmara Municipal de São Paulo**.
31
 
32
  Utiliza **Ensemble Híbrido** (Radar Social + AzMina/QuiterIA + Keywords + Padrões) para identificar
33
  se PLs são **favoráveis** ou **desfavoráveis** aos direitos da comunidade LGBTQIA+.
 
39
 
40
 
41
  # RADAR AUTOMÁTICO - Única aba
42
+ with gr.Tab("🔍 Radar Automático"):
43
  gr.Markdown("""
44
+ ### 🔍 Radar Automático de PLs LGBTQIA+
45
 
46
  Busca e analisa automaticamente PLs relacionadas a direitos LGBTQIA+ nas APIs oficiais:
47
+ - **Câmara dos Deputados** ✅ (dados atualizados diariamente)
48
+ - **Senado Federal** ✅ (matérias atualizadas recentemente)
49
+ - **ALESP (Assembleia Legislativa de SP)** ✅ (atualizado diariamente)
50
+ - **Câmara Municipal de São Paulo** ✅ (dados atualizados)
51
 
52
  ⚠️ **Atenção:** A busca pode levar alguns segundos, especialmente em períodos longos.
53
  """)
 
59
  maximum=2025,
60
  value=2020,
61
  step=1,
62
+ interactive=True,
63
  info="Ano mais antigo para buscar"
64
  )
65
 
 
67
  label="Ano Final",
68
  minimum=2010,
69
  maximum=2025,
70
+ value=datetime.now().year,
71
  step=1,
72
+ interactive=True,
73
  info="Ano mais recente para buscar"
74
  )
75
 
 
79
  maximum=100,
80
  value=50,
81
  step=5,
82
+ interactive=True,
83
  info="Número máximo de PLs encontradas"
84
  )
85
 
86
  with gr.Row():
87
  btn_buscar = gr.Button("🔍 Buscar e Analisar PLs", variant="primary", scale=2)
88
+
89
+ with gr.Row():
90
  checkbox_camara = gr.Checkbox(label="Câmara dos Deputados", value=True)
91
  checkbox_senado = gr.Checkbox(label="Senado Federal", value=True)
92
+ checkbox_alesp = gr.Checkbox(
93
+ label="ALESP (Assembleia Legislativa SP)",
94
+ value=False,
95
+ info="Dados atualizados diariamente"
96
+ )
97
+ checkbox_camara_sp = gr.Checkbox(label="Câmara Municipal SP", value=False)
98
 
99
  output_busca = gr.Markdown(label="📊 PLs Encontradas e Analisadas")
100
 
101
+ def buscar_e_analisar(ano_inicio, ano_fim, limite, checkbox_camara, checkbox_senado, checkbox_alesp, checkbox_camara_sp):
102
  """Busca PLs e analisa automaticamente"""
103
  import sys
104
  from io import StringIO
105
 
106
+ # Debug: verificar se função está sendo chamada
107
+ print("🔍 Função buscar_e_analisar chamada!", flush=True)
108
+
109
  # Validar anos
110
  if ano_inicio > ano_fim:
111
  return "❌ **Erro:** Ano inicial deve ser menor ou igual ao ano final."
 
113
  if ano_fim > datetime.now().year:
114
  return f"❌ **Erro:** Ano final não pode ser maior que {datetime.now().year}."
115
 
116
+ if not checkbox_camara and not checkbox_senado and not checkbox_alesp and not checkbox_camara_sp:
117
+ return "❌ **Erro:** Selecione pelo menos uma fonte."
118
 
119
  # Capturar prints para exibir na interface
120
  old_stdout = sys.stdout
 
124
  pls_encontradas = []
125
  anos_para_buscar = list(range(int(ano_inicio), int(ano_fim) + 1))
126
 
127
+ # Contar quantas fontes foram selecionadas para distribuir o limite
128
+ fontes_selecionadas = []
129
+ if checkbox_camara:
130
+ fontes_selecionadas.append("Câmara")
131
+ if checkbox_senado:
132
+ fontes_selecionadas.append("Senado")
133
+ if checkbox_alesp:
134
+ fontes_selecionadas.append("ALESP")
135
+ if checkbox_camara_sp:
136
+ fontes_selecionadas.append("Câmara Municipal SP")
137
+
138
+ num_fontes = len(fontes_selecionadas)
139
+
140
+ # Distribuir limite entre as fontes (cada fonte busca uma proporção do limite)
141
+ # Usar limite * 1.1 para garantir que distribuímos bem, mas depois limitamos o total
142
+ limite_por_fonte = max(5, int(int(limite) * 1.1 / num_fontes)) if num_fontes > 0 else int(limite)
143
+
144
  print(f"🔍 Buscando PLs LGBTQIA+ no Congresso Nacional...")
145
  print(f"📅 Período: {ano_inicio} a {ano_fim} ({len(anos_para_buscar)} anos)")
146
+ print(f"📊 Fontes selecionadas: {', '.join(fontes_selecionadas)} ({num_fontes} fontes)")
147
+ print(f"📋 Distribuindo limite: até ~{limite_por_fonte} PLs por fonte (total máximo: {limite})")
148
 
149
  # 1. Câmara dos Deputados
150
+ if checkbox_camara:
151
+ print(f"\n📥 Buscando na Câmara dos Deputados (limite: ~{limite_por_fonte})...")
152
+ pls_camara = []
153
  for ano in reversed(anos_para_buscar):
154
+ if len(pls_camara) >= limite_por_fonte:
155
  break
156
 
157
+ limite_restante = limite_por_fonte - len(pls_camara)
158
  if limite_restante <= 0:
159
  break
160
 
 
164
  ano_inicio_manual=ano,
165
  ano_fim_manual=ano
166
  )
167
+ pls_camara.extend(pls_ano)
168
  if pls_ano:
169
  print(f" ✅ {len(pls_ano)} PLs encontradas na Câmara em {ano}")
170
+
171
+ pls_encontradas.extend(pls_camara)
172
+ print(f" 📊 Total Câmara: {len(pls_camara)} PLs")
173
 
174
  # 2. Senado Federal
175
+ if checkbox_senado:
176
+ print(f"\n📥 Buscando no Senado Federal (limite: ~{limite_por_fonte})...")
177
+ pls_senado = []
178
  for ano in reversed(anos_para_buscar):
179
+ if len(pls_senado) >= limite_por_fonte:
180
  break
181
 
182
+ limite_restante = limite_por_fonte - len(pls_senado)
183
  if limite_restante <= 0:
184
  break
185
 
 
188
  ano_inicio_manual=ano,
189
  ano_fim_manual=ano
190
  )
191
+ pls_senado.extend(pls_ano)
192
  if pls_ano:
193
  print(f" ✅ {len(pls_ano)} PLs encontradas no Senado em {ano}")
194
+
195
+ pls_encontradas.extend(pls_senado)
196
+ print(f" 📊 Total Senado: {len(pls_senado)} PLs")
197
+
198
+ # 3. ALESP (Assembleia Legislativa de São Paulo)
199
+ if checkbox_alesp:
200
+ print(f"\n📥 Buscando na ALESP (limite: ~{limite_por_fonte})...")
201
+ pls_alesp = buscar_alesp(
202
+ limite=limite_por_fonte,
203
+ ano_inicio_manual=int(ano_inicio),
204
+ ano_fim_manual=int(ano_fim)
205
+ )
206
+ pls_encontradas.extend(pls_alesp)
207
+ if pls_alesp:
208
+ print(f" ✅ {len(pls_alesp)} PLs encontradas na ALESP")
209
+ else:
210
+ print(f" ℹ️ Nenhuma PL relevante encontrada na ALESP")
211
+
212
+ # 4. Câmara Municipal de São Paulo
213
+ if checkbox_camara_sp:
214
+ print(f"\n📥 Buscando na Câmara Municipal de SP (limite: ~{limite_por_fonte})...")
215
+ pls_camara_sp = buscar_camara_sao_paulo(
216
+ limite=limite_por_fonte,
217
+ ano_inicio_manual=int(ano_inicio),
218
+ ano_fim_manual=int(ano_fim)
219
+ )
220
+ pls_encontradas.extend(pls_camara_sp)
221
+ if pls_camara_sp:
222
+ print(f" ✅ {len(pls_camara_sp)} PLs encontradas na Câmara Municipal SP")
223
+ else:
224
+ print(f" ℹ️ Nenhuma PL relevante encontrada na Câmara Municipal SP")
225
+
226
+ # Limitar o total final ao limite solicitado (caso tenha ultrapassado)
227
+ if len(pls_encontradas) > int(limite):
228
+ print(f"\n📊 Total encontrado: {len(pls_encontradas)} PLs")
229
+ print(f" ⚙️ Limitando a {limite} PLs (mantendo diversidade de fontes)...")
230
+ pls_encontradas = pls_encontradas[:int(limite)]
231
 
232
  # Restaurar stdout
233
  sys.stdout = old_stdout
 
235
 
236
  if not pls_encontradas:
237
  fontes = []
238
+ if checkbox_camara:
239
  fontes.append("Câmara dos Deputados")
240
+ if checkbox_senado:
241
  fontes.append("Senado Federal")
242
+ if checkbox_alesp:
243
+ fontes.append("ALESP")
244
+ if checkbox_camara_sp:
245
+ fontes.append("Câmara Municipal SP")
246
  fontes_str = " e ".join(fontes)
247
 
248
  return f"""⚠️ Nenhuma PL encontrada em {fontes_str} para o período {int(ano_inicio)}-{int(ano_fim)}.
 
318
  revisao = sum(1 for r in resultados if r['Classificação'] == 'REVISÃO')
319
 
320
  fontes_usadas = []
321
+ if checkbox_camara:
322
  fontes_usadas.append("Câmara")
323
+ if checkbox_senado:
324
  fontes_usadas.append("Senado")
325
+ if checkbox_alesp:
326
+ fontes_usadas.append("ALESP")
327
+ if checkbox_camara_sp:
328
+ fontes_usadas.append("Câmara Municipal SP")
329
 
330
  relatorio = f"""## 🔍 Radar de PLs LGBTQIA+ - Resultados
331
 
 
370
 
371
  btn_buscar.click(
372
  fn=buscar_e_analisar,
373
+ inputs=[ano_inicio, ano_fim, limite_resultados, checkbox_camara, checkbox_senado, checkbox_alesp, checkbox_camara_sp],
374
  outputs=output_busca
375
  )
376
 
 
393
  - **Período médio (3-5 anos):** Balanceado, mais resultados
394
  - **Período grande (2010-2025):** Pode levar alguns minutos, muitos resultados
395
 
396
+ ### ⚠️ Limitações e Avisos:
397
+ - A busca pode levar alguns segundos (até minutos para períodos longos)
398
+ - **Câmara dos Deputados**: API permite até 100 itens por página (buscamos múltiplas páginas)
399
+ - **Senado Federal**: Busca todas as matérias apresentadas no ano via `/materia/pesquisa/lista` ✅
400
+ - **ALESP**: Baixa arquivo ZIP completo (~16MB) toda vez, garantindo dados atualizados. Pode levar 10-20 segundos. Atualizado diariamente.
401
+ - **Câmara Municipal SP**: Busca todos os projetos do ano (pode ter 20k+), filtra localmente
402
+ - Depende da disponibilidade das APIs públicas
403
  """)
404
 
405
  gr.Markdown("""
ensemble_híbrido.py CHANGED
@@ -125,15 +125,44 @@ def carregar_modelos():
125
  radar = None
126
 
127
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  azmina = pipeline(
129
  "text-classification",
130
- model=MODEL_AZMINA,
 
131
  device=-1 # CPU
132
  )
133
  print(" ✅ AzMina carregado")
134
  except Exception as e:
135
- print(f" ⚠️ Erro ao carregar AzMina: {e}")
136
- azmina = None
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
  return radar, azmina
139
 
@@ -190,12 +219,23 @@ def classificar_ensemble(
190
 
191
  if pesos is None:
192
  # Pesos ajustados: dar mais peso a keywords e padrões (mais específicos para legislação)
193
- pesos = {
194
- 'radar': 0.20, # Detecção de ódio (menos relevante em legislação)
195
- 'azmina': 0.15, # Perspectiva feminista (proxy, não ideal) - REDUZIDO
196
- 'keywords': 0.35, # Keywords específicas (MAIS IMPORTANTE - legislação tem termos claros)
197
- 'padroes': 0.30 # Padrões legislativos (CRÍTICO para detectar restrições) - AUMENTADO
198
- }
 
 
 
 
 
 
 
 
 
 
 
199
 
200
  resultados = {}
201
 
 
125
  radar = None
126
 
127
  try:
128
+ # AzMina não tem tokenizer_config.json no repositório, então usamos o tokenizer do modelo base
129
+ # Conforme README do modelo: base_model = neuralmind/bert-base-portuguese-cased
130
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
131
+
132
+ # Modelo base conforme documentado no README do repositório AzMina
133
+ base_model = "neuralmind/bert-base-portuguese-cased"
134
+
135
+ print(" 🔧 Carregando AzMina com tokenizer do modelo base...")
136
+ # Carregar tokenizer do modelo base (mesmo usado no treinamento do AzMina)
137
+ tokenizer = AutoTokenizer.from_pretrained(base_model)
138
+ # Carregar apenas o modelo AzMina (fine-tuned)
139
+ model = AutoModelForSequenceClassification.from_pretrained(MODEL_AZMINA)
140
+
141
+ # Criar pipeline combinando modelo AzMina + tokenizer do modelo base
142
+ # Isso é seguro porque o AzMina foi treinado com esse tokenizer específico
143
  azmina = pipeline(
144
  "text-classification",
145
+ model=model,
146
+ tokenizer=tokenizer,
147
  device=-1 # CPU
148
  )
149
  print(" ✅ AzMina carregado")
150
  except Exception as e:
151
+ error_msg = str(e)
152
+ print(f" ⚠️ Erro ao carregar AzMina: {error_msg[:150]}")
153
+ print(" ℹ️ Tentando método alternativo (pipeline direto)...")
154
+ try:
155
+ # Fallback: tentar pipeline direto (provavelmente falhará, mas tentamos)
156
+ azmina = pipeline(
157
+ "text-classification",
158
+ model=MODEL_AZMINA,
159
+ device=-1
160
+ )
161
+ print(" ✅ AzMina carregado (método alternativo)")
162
+ except Exception as e2:
163
+ print(f" ❌ AzMina não pôde ser carregado: {str(e2)[:100]}")
164
+ print(" ⚠️ Sistema funcionará apenas com Radar Social + Keywords + Padrões")
165
+ azmina = None
166
 
167
  return radar, azmina
168
 
 
219
 
220
  if pesos is None:
221
  # Pesos ajustados: dar mais peso a keywords e padrões (mais específicos para legislação)
222
+ # Se AzMina não estiver disponível, redistribuir seu peso proporcionalmente
223
+ if azmina_model is None:
224
+ # Sem AzMina: aumentar peso de keywords e padrões proporcionalmente
225
+ pesos = {
226
+ 'radar': 0.20, # Detecção de ódio
227
+ 'azmina': 0.0, # AzMina não disponível
228
+ 'keywords': 0.40, # Aumentado de 0.35 para 0.40 (+0.05 do AzMina)
229
+ 'padroes': 0.40 # Aumentado de 0.30 para 0.40 (+0.10 do AzMina)
230
+ }
231
+ else:
232
+ # Com ambos os modelos: distribuição otimizada
233
+ pesos = {
234
+ 'radar': 0.20, # Detecção de ódio (menos relevante em legislação)
235
+ 'azmina': 0.15, # Perspectiva feminista (proxy, não ideal) - REDUZIDO
236
+ 'keywords': 0.35, # Keywords específicas (MAIS IMPORTANTE - legislação tem termos claros)
237
+ 'padroes': 0.30 # Padrões legislativos (CRÍTICO para detectar restrições) - AUMENTADO
238
+ }
239
 
240
  resultados = {}
241