| """ |
| 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) |
|
|
| |
| 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)) |
|
|