carlosh10 commited on
Commit
16e2ec0
·
verified ·
1 Parent(s): 5e5dced

feat: Adiciona auditor de projetos PSCIP NT-01/2025

Browse files
Files changed (1) hide show
  1. modules/auditor.py +179 -0
modules/auditor.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Auditor de Projetos PSCIP - NT-01/2025 CBMGO
3
+ Verifica conformidade de memoriais descritivos e projetos.
4
+ """
5
+ import json
6
+ import re
7
+ import os
8
+ from datetime import datetime
9
+ from typing import Dict, List, Tuple
10
+
11
+
12
+ class AuditorProjetos:
13
+ TERMOS_BUSCA = {
14
+ "extintores_portateis": ["extintor","extintores","po abc","co2","capacidade extintora"],
15
+ "hidrantes": ["hidrante","hidrantes","mangotinho","coluna seca","coluna de incendio","nbr 13714"],
16
+ "sinalizacao_emergencia": ["sinalizacao","sinaliza","emergencia","saida","rota de fuga","nbr 13434"],
17
+ "iluminacao_emergencia": ["iluminacao de emergencia","autonomia","bloco autonomo","nbr 10898"],
18
+ "spda": ["spda","para-raios","descargas atmosfericas","nbr 5419"],
19
+ "alarme_incendio": ["alarme","acionador manual","central de alarme","nbr 17240"],
20
+ "deteccao_automatica": ["detector","detectores","detector de fumaca","deteccao automatica"],
21
+ "chuveiros_automaticos": ["chuveiro","chuveiros","sprinkler","nbr 10897"],
22
+ "saidas_emergencia": ["saida de emergencia","rota de fuga","porta corta-fogo","nbr 9077"],
23
+ "reservatorio_incendio": ["reservatorio","reserva tecnica","cisterna"],
24
+ "plano_emergencia": ["plano de emergencia","brigada","simulacro","nbr 15219"],
25
+ "ventilacao_mecanica": ["ventilacao","exaustao","renovacao de ar"],
26
+ }
27
+ SEVERIDADE = {
28
+ "extintores_portateis": "CRITICO","hidrantes": "CRITICO",
29
+ "sinalizacao_emergencia": "CRITICO","iluminacao_emergencia": "IMPORTANTE",
30
+ "spda": "IMPORTANTE","alarme_incendio": "IMPORTANTE",
31
+ "deteccao_automatica": "IMPORTANTE","chuveiros_automaticos": "CRITICO",
32
+ "saidas_emergencia": "CRITICO","reservatorio_incendio": "IMPORTANTE",
33
+ "plano_emergencia": "ALERTA","ventilacao_mecanica": "ALERTA",
34
+ }
35
+
36
+ def __init__(self, regras_path="data/regras_declarativas.json"):
37
+ self.regras = []
38
+ if os.path.exists(regras_path):
39
+ with open(regras_path, "r", encoding="utf-8") as f:
40
+ self.regras = json.load(f)
41
+
42
+ def _aplicar_regras(self, dados):
43
+ exigencias = {"extintores_portateis", "sinalizacao_emergencia"}
44
+ area = dados.get("area_m2", 0)
45
+ altura = dados.get("altura_m", 0)
46
+ ocupacao = dados.get("ocupacao", "").lower()
47
+ lotacao = dados.get("lotacao", 0)
48
+
49
+ if area > 750 or ocupacao in ["comercial","servicos","industrial"]:
50
+ exigencias.add("hidrantes")
51
+ if altura > 12:
52
+ exigencias.update(["iluminacao_emergencia","saidas_emergencia"])
53
+ if area > 1000:
54
+ exigencias.add("spda")
55
+ if lotacao > 100 or area > 750:
56
+ exigencias.add("alarme_incendio")
57
+ if lotacao > 200:
58
+ exigencias.add("deteccao_automatica")
59
+ if (ocupacao == "industrial" and area > 2000) or altura > 30:
60
+ exigencias.add("chuveiros_automaticos")
61
+ if any(u in ocupacao for u in ["hospital","saude"]):
62
+ exigencias.update(["chuveiros_automaticos","deteccao_automatica",
63
+ "alarme_incendio","plano_emergencia"])
64
+ if "garagem" in ocupacao or "estacionamento" in ocupacao:
65
+ exigencias.add("ventilacao_mecanica")
66
+
67
+ for regra in self.regras:
68
+ match = True
69
+ for k, v in regra.get("se", {}).items():
70
+ if k == "ocupacao":
71
+ if dados.get("ocupacao","").lower() != str(v).lower():
72
+ match = False; break
73
+ elif isinstance(v, str) and v.startswith(">"):
74
+ if dados.get(k, 0) <= float(v[1:]):
75
+ match = False; break
76
+ elif isinstance(v, str) and v.startswith("<"):
77
+ if dados.get(k, 0) >= float(v[1:]):
78
+ match = False; break
79
+ elif str(dados.get(k,"")) != str(v):
80
+ match = False; break
81
+ if match:
82
+ for e in regra.get("entao", []):
83
+ exigencias.add(e)
84
+
85
+ return list(exigencias)
86
+
87
+ def auditar_memorial(self, memorial_txt, dados_projeto):
88
+ if not memorial_txt or len(memorial_txt) < 50:
89
+ return {"status":"INVALIDO","apto":False,"erros":[],"alertas":[],"conformes":[],"score":0}
90
+
91
+ ml = memorial_txt.lower()
92
+ exigencias = self._aplicar_regras(dados_projeto)
93
+ erros, alertas, conformes = [], [], []
94
+
95
+ for exig in exigencias:
96
+ termos = self.TERMOS_BUSCA.get(exig, [exig.replace("_"," ")])
97
+ enc = [t for t in termos if t in ml]
98
+ sev = self.SEVERIDADE.get(exig, "ALERTA")
99
+ item = {"exigencia": exig, "descricao": exig.replace("_"," ").title(),
100
+ "severidade": sev, "termos_encontrados": enc}
101
+ if enc:
102
+ conformes.append(item)
103
+ else:
104
+ item["mensagem"] = f"{item['descricao']} - nao encontrado"
105
+ (erros if sev == "CRITICO" else alertas).append(item)
106
+
107
+ # Alertas de dados tecnicos
108
+ if not re.search(r'\d+\s*m[2²]', memorial_txt, re.IGNORECASE):
109
+ alertas.append({"tipo":"DADO","mensagem":"Area em m2 nao encontrada","severidade":"ALERTA"})
110
+ if not re.search(r'(responsavel tecnico|crea|cau)', ml):
111
+ alertas.append({"tipo":"RT","mensagem":"Responsavel Tecnico nao identificado","severidade":"IMPORTANTE"})
112
+ if "nt-01" not in ml and "nt 01" not in ml:
113
+ alertas.append({"tipo":"REF","mensagem":"Referencia NT-01/2025 nao encontrada","severidade":"ALERTA"})
114
+
115
+ total = len(exigencias)
116
+ score = round(len(conformes) / total * 100 if total > 0 else 0, 1)
117
+ apto = len(erros) == 0
118
+
119
+ return {
120
+ "status": "APTO PARA PROTOCOLO" if apto else "PENDENCIAS ENCONTRADAS",
121
+ "score_conformidade": score, "apto": apto,
122
+ "total_exigencias": total, "conformes": conformes,
123
+ "erros": erros, "alertas": alertas,
124
+ "data_auditoria": datetime.now().isoformat(),
125
+ }
126
+
127
+ def formatar_relatorio(self, resultado):
128
+ status = resultado.get("status","?")
129
+ score = resultado.get("score_conformidade", 0)
130
+ erros = resultado.get("erros", [])
131
+ alertas = resultado.get("alertas", [])
132
+ conformes = resultado.get("conformes", [])
133
+
134
+ linhas = [
135
+ "=" * 60,
136
+ "RELATORIO DE AUDITORIA - PSCIP",
137
+ f"Data: {datetime.now().strftime('%d/%m/%Y %H:%M')}",
138
+ f"STATUS: {status}",
139
+ f"Score de conformidade: {score}%",
140
+ "=" * 60,
141
+ ]
142
+ if conformes:
143
+ linhas.append(f"\nITENS CONFORMES ({len(conformes)}):")
144
+ for c in conformes:
145
+ linhas.append(f" [OK] {c['descricao']}")
146
+ if erros:
147
+ linhas.append(f"\nERROS CRITICOS ({len(erros)}) - IMPEDEM PROTOCOLO:")
148
+ for e in erros:
149
+ linhas.append(f" [!!] {e['descricao']} - INCLUIR NO MEMORIAL")
150
+ if alertas:
151
+ linhas.append(f"\nALERTAS ({len(alertas)}):")
152
+ for a in alertas:
153
+ linhas.append(f" [{a.get('severidade','?')}] {a.get('mensagem',a.get('descricao',''))}")
154
+ linhas.extend(["", "Ref: NT-01/2025 CBMGO", "=" * 60])
155
+ return "\n".join(linhas)
156
+
157
+
158
+ _auditor_instance = None
159
+
160
+ def get_auditor():
161
+ global _auditor_instance
162
+ if _auditor_instance is None:
163
+ _auditor_instance = AuditorProjetos()
164
+ return _auditor_instance
165
+
166
+
167
+ if __name__ == "__main__":
168
+ auditor = AuditorProjetos()
169
+ memorial = """MEMORIAL DESCRITIVO - PSCIP
170
+ Edificacao comercial 1500m2, 2 pavimentos.
171
+ Extintores portateis: 8 un, 2-A:20-B:C, NT-01/2025.
172
+ Hidrantes: coluna seca, 300 L/min, NBR 13714.
173
+ Sinalizacao de emergencia: NBR 13434.
174
+ SPDA: obrigatorio area > 1000m2, NBR 5419.
175
+ Responsavel Tecnico: Eng. Silva CREA 123456
176
+ """
177
+ dados = {"area_m2": 1500, "altura_m": 7, "ocupacao": "comercial", "lotacao": 100}
178
+ res = auditor.auditar_memorial(memorial, dados)
179
+ print(auditor.formatar_relatorio(res))