""" Auditor de Projetos PSCIP - NT-01/2025 CBMGO Verifica conformidade de memoriais descritivos e projetos. """ import json import re import os from datetime import datetime from typing import Dict, List, Tuple class AuditorProjetos: TERMOS_BUSCA = { "extintores_portateis": ["extintor","extintores","po abc","co2","capacidade extintora"], "hidrantes": ["hidrante","hidrantes","mangotinho","coluna seca","coluna de incendio","nbr 13714"], "sinalizacao_emergencia": ["sinalizacao","sinaliza","emergencia","saida","rota de fuga","nbr 13434"], "iluminacao_emergencia": ["iluminacao de emergencia","autonomia","bloco autonomo","nbr 10898"], "spda": ["spda","para-raios","descargas atmosfericas","nbr 5419"], "alarme_incendio": ["alarme","acionador manual","central de alarme","nbr 17240"], "deteccao_automatica": ["detector","detectores","detector de fumaca","deteccao automatica"], "chuveiros_automaticos": ["chuveiro","chuveiros","sprinkler","nbr 10897"], "saidas_emergencia": ["saida de emergencia","rota de fuga","porta corta-fogo","nbr 9077"], "reservatorio_incendio": ["reservatorio","reserva tecnica","cisterna"], "plano_emergencia": ["plano de emergencia","brigada","simulacro","nbr 15219"], "ventilacao_mecanica": ["ventilacao","exaustao","renovacao de ar"], } SEVERIDADE = { "extintores_portateis": "CRITICO","hidrantes": "CRITICO", "sinalizacao_emergencia": "CRITICO","iluminacao_emergencia": "IMPORTANTE", "spda": "IMPORTANTE","alarme_incendio": "IMPORTANTE", "deteccao_automatica": "IMPORTANTE","chuveiros_automaticos": "CRITICO", "saidas_emergencia": "CRITICO","reservatorio_incendio": "IMPORTANTE", "plano_emergencia": "ALERTA","ventilacao_mecanica": "ALERTA", } def __init__(self, regras_path="data/regras_declarativas.json"): self.regras = [] if os.path.exists(regras_path): with open(regras_path, "r", encoding="utf-8") as f: self.regras = json.load(f) def _aplicar_regras(self, dados): exigencias = {"extintores_portateis", "sinalizacao_emergencia"} area = dados.get("area_m2", 0) altura = dados.get("altura_m", 0) ocupacao = dados.get("ocupacao", "").lower() lotacao = dados.get("lotacao", 0) if area > 750 or ocupacao in ["comercial","servicos","industrial"]: exigencias.add("hidrantes") if altura > 12: exigencias.update(["iluminacao_emergencia","saidas_emergencia"]) if area > 1000: exigencias.add("spda") if lotacao > 100 or area > 750: exigencias.add("alarme_incendio") if lotacao > 200: exigencias.add("deteccao_automatica") if (ocupacao == "industrial" and area > 2000) or altura > 30: exigencias.add("chuveiros_automaticos") if any(u in ocupacao for u in ["hospital","saude"]): exigencias.update(["chuveiros_automaticos","deteccao_automatica", "alarme_incendio","plano_emergencia"]) if "garagem" in ocupacao or "estacionamento" in ocupacao: exigencias.add("ventilacao_mecanica") for regra in self.regras: match = True for k, v in regra.get("se", {}).items(): if k == "ocupacao": if dados.get("ocupacao","").lower() != str(v).lower(): match = False; break elif isinstance(v, str) and v.startswith(">"): if dados.get(k, 0) <= float(v[1:]): match = False; break elif isinstance(v, str) and v.startswith("<"): if dados.get(k, 0) >= float(v[1:]): match = False; break elif str(dados.get(k,"")) != str(v): match = False; break if match: for e in regra.get("entao", []): exigencias.add(e) return list(exigencias) def auditar_memorial(self, memorial_txt, dados_projeto): if not memorial_txt or len(memorial_txt) < 50: return {"status":"INVALIDO","apto":False,"erros":[],"alertas":[],"conformes":[],"score":0} ml = memorial_txt.lower() exigencias = self._aplicar_regras(dados_projeto) erros, alertas, conformes = [], [], [] for exig in exigencias: termos = self.TERMOS_BUSCA.get(exig, [exig.replace("_"," ")]) enc = [t for t in termos if t in ml] sev = self.SEVERIDADE.get(exig, "ALERTA") item = {"exigencia": exig, "descricao": exig.replace("_"," ").title(), "severidade": sev, "termos_encontrados": enc} if enc: conformes.append(item) else: item["mensagem"] = f"{item['descricao']} - nao encontrado" (erros if sev == "CRITICO" else alertas).append(item) # Alertas de dados tecnicos if not re.search(r'\d+\s*m[2²]', memorial_txt, re.IGNORECASE): alertas.append({"tipo":"DADO","mensagem":"Area em m2 nao encontrada","severidade":"ALERTA"}) if not re.search(r'(responsavel tecnico|crea|cau)', ml): alertas.append({"tipo":"RT","mensagem":"Responsavel Tecnico nao identificado","severidade":"IMPORTANTE"}) if "nt-01" not in ml and "nt 01" not in ml: alertas.append({"tipo":"REF","mensagem":"Referencia NT-01/2025 nao encontrada","severidade":"ALERTA"}) total = len(exigencias) score = round(len(conformes) / total * 100 if total > 0 else 0, 1) apto = len(erros) == 0 return { "status": "APTO PARA PROTOCOLO" if apto else "PENDENCIAS ENCONTRADAS", "score_conformidade": score, "apto": apto, "total_exigencias": total, "conformes": conformes, "erros": erros, "alertas": alertas, "data_auditoria": datetime.now().isoformat(), } def formatar_relatorio(self, resultado): status = resultado.get("status","?") score = resultado.get("score_conformidade", 0) erros = resultado.get("erros", []) alertas = resultado.get("alertas", []) conformes = resultado.get("conformes", []) linhas = [ "=" * 60, "RELATORIO DE AUDITORIA - PSCIP", f"Data: {datetime.now().strftime('%d/%m/%Y %H:%M')}", f"STATUS: {status}", f"Score de conformidade: {score}%", "=" * 60, ] if conformes: linhas.append(f"\nITENS CONFORMES ({len(conformes)}):") for c in conformes: linhas.append(f" [OK] {c['descricao']}") if erros: linhas.append(f"\nERROS CRITICOS ({len(erros)}) - IMPEDEM PROTOCOLO:") for e in erros: linhas.append(f" [!!] {e['descricao']} - INCLUIR NO MEMORIAL") if alertas: linhas.append(f"\nALERTAS ({len(alertas)}):") for a in alertas: linhas.append(f" [{a.get('severidade','?')}] {a.get('mensagem',a.get('descricao',''))}") linhas.extend(["", "Ref: NT-01/2025 CBMGO", "=" * 60]) return "\n".join(linhas) _auditor_instance = None def get_auditor(): global _auditor_instance if _auditor_instance is None: _auditor_instance = AuditorProjetos() return _auditor_instance if __name__ == "__main__": auditor = AuditorProjetos() memorial = """MEMORIAL DESCRITIVO - PSCIP Edificacao comercial 1500m2, 2 pavimentos. Extintores portateis: 8 un, 2-A:20-B:C, NT-01/2025. Hidrantes: coluna seca, 300 L/min, NBR 13714. Sinalizacao de emergencia: NBR 13434. SPDA: obrigatorio area > 1000m2, NBR 5419. Responsavel Tecnico: Eng. Silva CREA 123456 """ dados = {"area_m2": 1500, "altura_m": 7, "ocupacao": "comercial", "lotacao": 100} res = auditor.auditar_memorial(memorial, dados) print(auditor.formatar_relatorio(res))