| """ |
| Classificador de Ocupacao - NT-01/2025 CBMGO |
| Classifica edificacoes em grupos/divisoes e lista exigencias. |
| Com LLM (cbmgo/llama3-8b-ocupacao) ou regras locais (fallback). |
| """ |
| import json |
| import re |
|
|
| try: |
| from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM |
| import torch |
| HAS_TRANSFORMERS = True |
| except ImportError: |
| HAS_TRANSFORMERS = False |
|
|
|
|
| |
| |
| |
| TABELA_OCUPACOES = { |
| "A-1": { |
| "descricao": "Residencia unifamiliar", |
| "exemplos": ["casa", "sobrado", "chacara", "sitio"], |
| "exigencias_base": ["extintores_portateis", "sinalizacao_emergencia"], |
| "area_extintor_m2": 500, |
| "capacidade_extintor": "2-A:10-B:C" |
| }, |
| "A-2": { |
| "descricao": "Residencia multifamiliar", |
| "exemplos": ["apartamento", "condominio", "edificio residencial"], |
| "exigencias_base": ["extintores_portateis", "sinalizacao_emergencia", "hidrantes"], |
| "area_extintor_m2": 500, |
| "capacidade_extintor": "2-A:10-B:C" |
| }, |
| "B-1": { |
| "descricao": "Hotel, pousada, hostel", |
| "exemplos": ["hotel", "pousada", "hostel", "motel", "hospedagem"], |
| "exigencias_base": ["extintores_portateis", "hidrantes", "sinalizacao_emergencia", |
| "iluminacao_emergencia", "deteccao_automatica", "alarme_incendio"], |
| "area_extintor_m2": 250, |
| "capacidade_extintor": "2-A:20-B:C" |
| }, |
| "C-1": { |
| "descricao": "Teatro, cinema, auditorio", |
| "exemplos": ["teatro", "cinema", "auditorio", "show", "espetaculo"], |
| "exigencias_base": ["extintores_portateis", "hidrantes", "sinalizacao_emergencia", |
| "iluminacao_emergencia", "alarme_incendio", "saidas_emergencia_multiplas"], |
| "area_extintor_m2": 250, |
| "capacidade_extintor": "2-A:10-B:C" |
| }, |
| "C-2": { |
| "descricao": "Igreja, templo, local de culto", |
| "exemplos": ["igreja", "templo", "culto", "missa", "evangelico"], |
| "exigencias_base": ["extintores_portateis", "hidrantes", "sinalizacao_emergencia", |
| "iluminacao_emergencia"], |
| "area_extintor_m2": 300, |
| "capacidade_extintor": "2-A:10-B:C" |
| }, |
| "D-1": { |
| "descricao": "Comercio em geral, loja, mercado", |
| "exemplos": ["loja", "mercado", "supermercado", "comercio", "varejo", "boutique"], |
| "exigencias_base": ["extintores_portateis", "hidrantes", "sinalizacao_emergencia"], |
| "area_extintor_m2": 400, |
| "capacidade_extintor": "2-A:20-B:C" |
| }, |
| "D-2": { |
| "descricao": "Escritorio, servico profissional", |
| "exemplos": ["escritorio", "consultorio", "clinica", "advocacia", "contabilidade", "banco"], |
| "exigencias_base": ["extintores_portateis", "sinalizacao_emergencia"], |
| "area_extintor_m2": 500, |
| "capacidade_extintor": "2-A:10-B:C" |
| }, |
| "E-1": { |
| "descricao": "Escola, creche, universidade", |
| "exemplos": ["escola", "colegio", "creche", "universidade", "faculdade", "educacional"], |
| "exigencias_base": ["extintores_portateis", "hidrantes", "sinalizacao_emergencia", |
| "iluminacao_emergencia", "saidas_emergencia"], |
| "area_extintor_m2": 300, |
| "capacidade_extintor": "2-A:10-B:C" |
| }, |
| "F-1": { |
| "descricao": "Hospital, clinica com internacao", |
| "exemplos": ["hospital", "uti", "pronto-socorro", "internacao"], |
| "exigencias_base": ["extintores_portateis", "hidrantes", "sinalizacao_emergencia", |
| "iluminacao_emergencia", "deteccao_automatica", "alarme_incendio", |
| "spda", "chuveiros_automaticos", "plano_emergencia"], |
| "area_extintor_m2": 200, |
| "capacidade_extintor": "2-A:20-B:C" |
| }, |
| "G-1": { |
| "descricao": "Garagem, estacionamento coberto", |
| "exemplos": ["garagem", "estacionamento", "parking", "auto"], |
| "exigencias_base": ["extintores_portateis", "hidrantes", "sinalizacao_emergencia", |
| "ventilacao_mecanica"], |
| "area_extintor_m2": 250, |
| "capacidade_extintor": "2-A:40-B:C" |
| }, |
| "H-1": { |
| "descricao": "Deposito, armazem, galpao", |
| "exemplos": ["deposito", "armazem", "galpao", "estoque", "almoxarifado"], |
| "exigencias_base": ["extintores_portateis", "hidrantes", "sinalizacao_emergencia"], |
| "area_extintor_m2": 500, |
| "capacidade_extintor": "2-A:10-B:C" |
| }, |
| "I-1": { |
| "descricao": "Industrial - baixo risco", |
| "exemplos": ["fabrica", "industria", "manufatura", "producao", "montagem"], |
| "exigencias_base": ["extintores_portateis", "hidrantes", "sinalizacao_emergencia", |
| "iluminacao_emergencia"], |
| "area_extintor_m2": 300, |
| "capacidade_extintor": "4-A:20-B:C" |
| }, |
| "I-2": { |
| "descricao": "Industrial - medio/alto risco", |
| "exemplos": ["quimica", "petroleo", "gas", "combustivel", "tinta", "solvente"], |
| "exigencias_base": ["extintores_portateis", "hidrantes", "sinalizacao_emergencia", |
| "iluminacao_emergencia", "chuveiros_automaticos", "spda", |
| "deteccao_automatica", "alarme_incendio"], |
| "area_extintor_m2": 150, |
| "capacidade_extintor": "4-A:40-B:C" |
| }, |
| } |
|
|
| |
| GRUPO_MAP = { |
| "A": "A-2", "B": "B-1", "C": "C-1", "D": "D-1", |
| "E": "E-1", "F": "F-1", "G": "G-1", "H": "H-1", "I": "I-1" |
| } |
|
|
|
|
| class ClassificadorOcupacao: |
| """ |
| Classifica edificacoes conforme NT-01/2025 Tabela 1. |
| |
| Uso: |
| clf = ClassificadorOcupacao() |
| resultado = clf.classificar("Loja de roupas com 800m2 no centro comercial") |
| """ |
| |
| def __init__(self, model_id: str = "cbmgo/llama3-8b-ocupacao", use_llm: bool = False): |
| self.model_id = model_id |
| self.use_llm = use_llm and HAS_TRANSFORMERS |
| self._pipe = None |
| |
| def _load_llm(self): |
| if self._pipe is None and self.use_llm: |
| print(f"Carregando modelo: {self.model_id}") |
| self._pipe = pipeline( |
| "text-generation", |
| model=self.model_id, |
| device_map="auto", |
| max_new_tokens=512, |
| temperature=0.1, |
| ) |
| |
| def classificar_llm(self, descricao: str) -> dict: |
| """Classificacao via LLM fine-tunado.""" |
| self._load_llm() |
| prompt = f"""Classifique a edificacao conforme NT-01/2025 CBMGO Tabela 1. |
| Responda APENAS em JSON valido. |
| |
| Edificacao: {descricao} |
| |
| JSON de resposta: |
| {{ |
| "grupo": "letra do grupo (A-I)", |
| "divisao": "ex: D-1", |
| "descricao_ocupacao": "descricao do tipo", |
| "exigencias": ["lista", "de", "sistemas", "obrigatorios"], |
| "area_extintor_m2": numero, |
| "capacidade_extintor": "ex: 2-A:10-B:C", |
| "justificativa": "breve explicacao" |
| }}""" |
| |
| saida = self._pipe(prompt)[0]["generated_text"] |
| |
| match = re.search(r'\{[^{}]*"grupo"[^{}]*\}', saida, re.DOTALL) |
| if match: |
| try: |
| return json.loads(match.group()) |
| except json.JSONDecodeError: |
| pass |
| |
| |
| return self.classificar_regras(descricao) |
| |
| def classificar_regras(self, descricao: str) -> dict: |
| """Classificacao via regras locais (sem GPU).""" |
| desc_lower = descricao.lower() |
| |
| melhor_match = None |
| melhor_score = 0 |
| |
| for divisao, info in TABELA_OCUPACOES.items(): |
| score = 0 |
| for exemplo in info["exemplos"]: |
| if exemplo in desc_lower: |
| score += 2 |
| |
| palavras = desc_lower.split() |
| for palavra in palavras: |
| for exemplo in info["exemplos"]: |
| if len(palavra) > 3 and (palavra in exemplo or exemplo in palavra): |
| score += 1 |
| if score > melhor_score: |
| melhor_score = score |
| melhor_match = divisao |
| |
| |
| if melhor_match is None or melhor_score == 0: |
| melhor_match = "D-1" |
| |
| info = TABELA_OCUPACOES[melhor_match] |
| grupo = melhor_match[0] |
| |
| |
| exigencias = list(info["exigencias_base"]) |
| |
| |
| area_match = re.search(r'(\d+)\s*m2', desc_lower) |
| area = float(area_match.group(1)) if area_match else 0 |
| |
| altura_match = re.search(r'(\d+)\s*(andar|pavimento|piso|m de altura)', desc_lower) |
| altura = float(altura_match.group(1)) if altura_match else 0 |
| |
| if 'andar' in desc_lower or 'pavimento' in desc_lower: |
| altura = altura * 3.0 |
| |
| if area > 1000 and "spda" not in exigencias: |
| exigencias.append("spda") |
| if altura > 12 and "iluminacao_emergencia" not in exigencias: |
| exigencias.append("iluminacao_emergencia") |
| if altura > 12 and "saidas_emergencia" not in exigencias: |
| exigencias.append("saidas_emergencia") |
| |
| return { |
| "grupo": grupo, |
| "divisao": melhor_match, |
| "descricao_ocupacao": info["descricao"], |
| "exigencias": list(set(exigencias)), |
| "area_extintor_m2": info["area_extintor_m2"], |
| "capacidade_extintor": info["capacidade_extintor"], |
| "justificativa": f"Classificado como {melhor_match} - {info['descricao']} (score={melhor_score})", |
| "referencia": "NT-01/2025 CBMGO - Tabela 1" |
| } |
| |
| def classificar(self, descricao: str) -> dict: |
| """Interface principal: usa LLM se disponivel, regras locais caso contrario.""" |
| if self.use_llm and self._pipe is not None: |
| return self.classificar_llm(descricao) |
| return self.classificar_regras(descricao) |
| |
| def formatar_resultado(self, resultado: dict) -> str: |
| """Formata resultado para exibicao.""" |
| exig = resultado.get("exigencias", []) |
| exig_txt = "\n".join([f" - {e.replace('_',' ').title()}" for e in exig]) |
| |
| return ( |
| "=== CLASSIFICACAO NT-01/2025 CBMGO ===\n" |
| f"Grupo/Divisao: {resultado.get('divisao','?')} - {resultado.get('descricao_ocupacao','')}\n" |
| f"Grupo geral: {resultado.get('grupo','?')}\n\n" |
| f"SISTEMAS EXIGIDOS ({len(exig)}):\n{exig_txt}\n\n" |
| f"EXTINTORES:\n" |
| f" Area por extintor: {resultado.get('area_extintor_m2','?')} m2\n" |
| f" Capacidade minima: {resultado.get('capacidade_extintor','?')}\n\n" |
| f"Justificativa: {resultado.get('justificativa','')}\n" |
| f"Ref: {resultado.get('referencia','NT-01/2025 CBMGO')}\n" |
| ) |
|
|
|
|
| |
| _clf_instance = None |
|
|
| def get_classificador(use_llm: bool = False) -> ClassificadorOcupacao: |
| global _clf_instance |
| if _clf_instance is None: |
| _clf_instance = ClassificadorOcupacao(use_llm=use_llm) |
| return _clf_instance |
|
|
|
|
| if __name__ == "__main__": |
| clf = ClassificadorOcupacao() |
| testes = [ |
| "Loja de roupas com 800m2", |
| "Hospital universitario com UTI", |
| "Escola municipal de ensino fundamental", |
| "Fabrica de produtos quimicos", |
| "Edificio residencial de 15 andares", |
| ] |
| for teste in testes: |
| res = clf.classificar(teste) |
| print(clf.formatar_resultado(res)) |
| print("=" * 50) |
|
|