CERCON / scripts /generate_synthetic.py
carlosh10's picture
feat: Adiciona gerador de dataset sintetico NT-01/2025 para fine-tuning
23ad902 verified
#!/usr/bin/env python3
"""
scripts/generate_synthetic.py
Gerador de dataset sintetico para fine-tuning do Agente CBMGO (CERCON)
Gera exemplos no formato SFT (Supervised Fine-Tuning) para Llama-3 / Mistral
"""
import json
import random
import argparse
from pathlib import Path
from datetime import datetime
OCUPACOES = [
{"grupo": "A-1", "desc": "Residencial unifamiliar", "ex": "casa, sobrado, chale"},
{"grupo": "A-2", "desc": "Residencial multifamiliar", "ex": "apartamento, flat, kitnet"},
{"grupo": "B-1", "desc": "Servico de hospedagem", "ex": "hotel, motel, pousada"},
{"grupo": "B-2", "desc": "Hospedagem longa permanencia", "ex": "apart-hotel, residencial senior"},
{"grupo": "C-1", "desc": "Comercio pequeno porte", "ex": "loja, boutique, sapataria"},
{"grupo": "C-2", "desc": "Comercio medio e grande porte", "ex": "supermercado, shopping"},
{"grupo": "D-1", "desc": "Reuniao publica - esporte e lazer", "ex": "ginasio, estadio, piscina"},
{"grupo": "D-2", "desc": "Reuniao publica - cultura e religiao", "ex": "teatro, cinema, igreja"},
{"grupo": "D-3", "desc": "Reuniao publica - ensino", "ex": "escola, universidade, creche"},
{"grupo": "E-1", "desc": "Saude e assistencia social", "ex": "hospital, clinica, UBS"},
{"grupo": "F-1", "desc": "Servico profissional e negocios", "ex": "escritorio, banco, consultorio"},
{"grupo": "F-2", "desc": "Servico de transporte", "ex": "aeroporto, rodoviaria, correios"},
{"grupo": "G-1", "desc": "Servicos automotivos", "ex": "garagem, estacionamento, posto"},
{"grupo": "H-1", "desc": "Industrial baixo risco", "ex": "industria alimenticia, textil"},
{"grupo": "H-2", "desc": "Industrial medio risco", "ex": "metalurgica, moveleira, grafica"},
{"grupo": "H-3", "desc": "Industrial alto risco", "ex": "quimica, petroquimica, fogos"},
{"grupo": "I-1", "desc": "Deposito baixo risco", "ex": "arquivo, deposito de papel"},
{"grupo": "I-2", "desc": "Deposito medio risco", "ex": "deposito de madeira, borracha"},
{"grupo": "I-3", "desc": "Deposito alto risco", "ex": "deposito de liquidos inflamaveis"},
{"grupo": "J-1", "desc": "Explosivos e inflamaveis especiais", "ex": "refinaria, base de gas"},
]
CIDADES_GOIAS = [
"Goiania", "Aparecida de Goiania", "Anapolis", "Rio Verde", "Luziania",
"Aguas Lindas de Goias", "Valparaiso de Goias", "Trindade", "Formosa", "Novo Gama",
"Itumbiara", "Senador Canedo", "Catalao", "Jatai", "Planaltina",
"Caldas Novas", "Goianesia", "Mineiros", "Inhumas", "Goiatuba",
]
RESPONSAVEIS = [
("Joao Silva", "CREA-GO 12345-D"),
("Maria Santos", "CREA-GO 67890-D"),
("Carlos Oliveira", "CAU A123456-0"),
("Ana Ferreira", "CREA-GO 11111-D"),
("Pedro Alves", "CAU A654321-0"),
("Lucia Martins", "CREA-GO 99999-D"),
]
PERGUNTAS_CLASSIFICACAO = [
"Qual e o grupo de ocupacao para {desc}?",
"Uma {desc} se enquadra em qual grupo da NT-01/2025?",
"Como classificar uma {desc} segundo a NT-01/2025 do CBMGO?",
"Qual a classificacao de ocupacao para {desc} no CBMGO?",
"Preciso classificar uma {desc}. Qual grupo de ocupacao devo usar?",
]
PERGUNTAS_CALCULO = [
"Quantos extintores sao necessarios para uma {desc} com {area}m2?",
"Como calcular as medidas de seguranca para uma {desc} de {area}m2?",
"Qual o numero minimo de hidrantes para uma {desc} com {area}m2?",
"Uma {desc} de {area}m2 e {pavimentos} pavimentos precisa de SPDA?",
"Quais medidas de seguranca sao obrigatorias para uma {desc} de {area}m2?",
]
PERGUNTAS_MEMORIAL = [
"Gere um memorial descritivo de PPCI para uma {desc} em {cidade}.",
"Preciso do memorial de prevencao contra incendio para {desc} com {area}m2.",
"Elabore o memorial descritivo conforme NT-01/2025 para {desc}.",
"Crie um memorial de PPCI completo para {desc} de {area}m2 em {cidade}.",
]
PERGUNTAS_NORMAS = [
"Quais sao as exigencias da NT-01/2025 para {desc}?",
"O que a NT-01/2025 determina sobre {desc}?",
"Quais normas se aplicam para {desc} segundo o CBMGO?",
"Quais sao os requisitos minimos de prevencao de incendio para {desc}?",
]
def gerar_resposta_classificacao(ocupacao):
grupo = ocupacao["grupo"]
desc = ocupacao["desc"]
ex = ocupacao["ex"]
letra = grupo.split("-")[0]
descricoes = {
"A": "residencial", "B": "hospedagem", "C": "comercial",
"D": "reuniao de publico", "E": "saude", "F": "servicos",
"G": "automotivo", "H": "industrial", "I": "deposito", "J": "especial",
}
return f"""Com base na NT-01/2025 do CBMGO, a classificacao correta e:
**Grupo: {grupo} - {desc}**
Divisao {letra} ({descricoes.get(letra, "especial")}): edificacoes como {ex}.
**Implicacoes para o PPCI:**
- Grupo {grupo} determina medidas de seguranca obrigatorias
- Consulte Tabela 1 da NT-01/2025 para requisitos especificos
- Area total e pavimentos influenciam exigencias adicionais
]
PERGUNTAS_CALCULO = [
"Quantos extintores sao necessarios para uma {desc} com {area}m2?",
"Como calcular as medidas de seguranca para uma {desc} de {area}m2?",
"Qual o numero minimo de hidrantes para uma {desc} com {area}m2?",
"Uma {desc} de {area}m2 e {pavimentos} pavimentos precisa de SPDA?",
"Quais medidas de seguranca sao obrigatorias para uma {desc} de {area}m2?",
]
PERGUNTAS_MEMORIAL = [
"Gere um memorial descritivo de PPCI para uma {desc} em {cidade}.",
"Preciso do memorial de prevencao contra incendio para {desc} com {area}m2.",
"Elabore o memorial descritivo conforme NT-01/2025 para {desc}.",
"Crie um memorial de PPCI completo para {desc} de {area}m2 em {cidade}.",
]
PERGUNTAS_NORMAS = [
"Quais sao as exigencias da NT-01/2025 para {desc}?",
"O que a NT-01/2025 determina sobre {desc}?",
"Quais normas se aplicam para {desc} segundo o CBMGO?",
"Quais sao os requisitos minimos de prevencao de incendio para {desc}?",
]
def gerar_resposta_classificacao(ocupacao):
grupo = ocupacao["grupo"]
desc = ocupacao["desc"]
ex = ocupacao["ex"]
letra = grupo.split("-")[0]
descricoes = {
"A": "residencial", "B": "hospedagem", "C": "comercial",
"D": "reuniao de publico", "E": "saude", "F": "servicos",
"G": "automotivo", "H": "industrial", "I": "deposito", "J": "especial",
}
return f"""Com base na NT-01/2025 do CBMGO, a classificacao correta e:
**Grupo: {grupo} - {desc}**
Divisao {letra} ({descricoes.get(letra, "especial")}): edificacoes como {ex}.
**Implicacoes para o PPCI:**
- Grupo {grupo} determina medidas de seguranca obrigatorias
- Consulte Tabela 1 da NT-01/2025 para requisitos especificos
- Area total e pavimentos influenciam exigencias adicionais
Para duvidas, consulte o CBMGO ou profissional habilitado (CREA/CAU)."""
def gerar_resposta_calculo(ocupacao, area, pavimentos):
grupo = ocupacao["grupo"]
risco = "alto" if grupo[0] in ["H","I","J"] else ("medio" if grupo[0] in ["C","D","G"] else "baixo")
area_ext = 150 if risco == "alto" else (250 if risco == "medio" else 500)
num_ext = max(2, int(area / area_ext) + 1)
precisa_hidrante = area > 750 or pavimentos >= 3
altura = pavimentos * 3.0
precisa_spda = altura > 15 or area > 1000
precisa_ilum = area > 200 or pavimentos >= 2
return f"""Para edificacao {grupo} com {area:.0f}m2 e {pavimentos} pavimento(s):
**EXTINTORES:**
- Risco: {risco.upper()} | Area/extintor: {area_ext}m2
- Quantidade minima: {num_ext} extintores ABC 6kg
**HIDRANTES:**
- {"OBRIGATORIO" if precisa_hidrante else "Verificar com CBMGO"}
**SPDA:**
- {"OBRIGATORIO (altura {:.1f}m)".format(altura) if precisa_spda else "Avaliar necessidade"}
**ILUMINACAO DE EMERGENCIA:**
- {"OBRIGATORIA - autonomia minima 1h (NBR 10898)" if precisa_ilum else "Recomendada"}
*Nota: Estes calculos sao estimativas. PPCI deve ser elaborado por profissional habilitado.*"""
def gerar_memorial(ocupacao, area, pavimentos, cidade, responsavel):
grupo = ocupacao["grupo"]
desc = ocupacao["desc"]
nome_resp, registro = responsavel
data_atual = datetime.now().strftime("%d/%m/%Y")
altura = pavimentos * 3.0
risco = "alto" if grupo[0] in ["H","I","J"] else ("medio" if grupo[0] in ["C","D","G"] else "baixo")
area_ext = 150 if risco == "alto" else (250 if risco == "medio" else 500)
num_ext = max(2, int(area / area_ext) + 1)
carga = "500 MJ/m2" if risco == "baixo" else ("1.000 MJ/m2" if risco == "medio" else "2.000 MJ/m2")
return f"""MEMORIAL DESCRITIVO DE PREVENCAO E PROTECAO CONTRA INCENDIO E PANICO
PLANO DE PREVENCAO CONTRA INCENDIO - PPCI
1. IDENTIFICACAO DO PROJETO
Proprietario: [A PREENCHER]
Endereco: [Endereco completo], {cidade} - GO
Uso/Ocupacao: {desc} (Grupo {grupo} - NT-01/2025)
Area Total: {area:.2f} m2 | Pavimentos: {pavimentos} | Altura: {altura:.2f} m
Data: {data_atual}
Responsavel Tecnico: {nome_resp} | {registro}
2. OBJETIVO
Este memorial apresenta medidas de seguranca conforme:
- NT-01/2025 do CBMGO | ABNT NBR 9077:2001 | Lei Estadual 15.802/2006
3. CARACTERISTICAS DA EDIFICACAO
- Grupo: {grupo} - {desc}
- Risco de Incendio: {risco.upper()}
- Carga de Incendio Especifica: {carga}
- Estrutura: Alvenaria convencional / Concreto armado
4. MEDIDAS DE SEGURANCA
4.1 Extintores: {num_ext} extintores ABC 6kg (1 a cada {area_ext}m2)
4.2 Saidas de Emergencia: largura minima 1,20m, abertura no sentido do escape
4.3 Iluminacao de Emergencia: autonomia 1h, 30 lux nas saidas (NBR 10898)
4.4 Sinalizacao: conforme ABNT NBR 13434
5. RESPONSABILIDADE TECNICA
{nome_resp} | {registro}
ART/RRT No: [A PREENCHER]
{cidade}, {data_atual}"""
def gerar_exemplo_classificacao():
ocupacao = random.choice(OCUPACOES)
tmpl = random.choice(PERGUNTAS_CLASSIFICACAO)
pergunta = tmpl.format(desc=ocupacao["desc"], grupo=ocupacao["grupo"])
return {
"instruction": pergunta, "input": "",
"output": gerar_resposta_classificacao(ocupacao),
"category": "classificacao", "grupo": ocupacao["grupo"],
"source": "synthetic_nt01_2025"
}
def gerar_exemplo_calculo():
ocupacao = random.choice(OCUPACOES)
area = random.choice([100,150,200,300,500,750,1000,1500,2000,3000,5000])
pavimentos = random.choice([1,2,3,4,5,8,10,15])
tmpl = random.choice(PERGUNTAS_CALCULO)
pergunta = tmpl.format(desc=ocupacao["desc"], area=area, pavimentos=pavimentos, grupo=ocupacao["grupo"])
return {
"instruction": pergunta,
"input": f"Area: {area}m2, Pavimentos: {pavimentos}, Grupo: {ocupacao['grupo']}",
"output": gerar_resposta_calculo(ocupacao, area, pavimentos),
"category": "calculo", "grupo": ocupacao["grupo"],
"area": area, "pavimentos": pavimentos, "source": "synthetic_nt01_2025"
}
def gerar_exemplo_memorial():
ocupacao = random.choice(OCUPACOES)
area = random.choice([150,200,300,500,750,1000,1500,2000])
pavimentos = random.choice([1,2,3,4,5])
cidade = random.choice(CIDADES_GOIAS)
responsavel = random.choice(RESPONSAVEIS)
tmpl = random.choice(PERGUNTAS_MEMORIAL)
pergunta = tmpl.format(desc=ocupacao["desc"], area=area, cidade=cidade, grupo=ocupacao["grupo"])
return {
"instruction": pergunta,
"input": f"Edificacao: {ocupacao['desc']}, Area: {area}m2, Pavimentos: {pavimentos}, Cidade: {cidade}",
"output": gerar_memorial(ocupacao, area, pavimentos, cidade, responsavel),
"category": "memorial", "grupo": ocupacao["grupo"],
"area": area, "cidade": cidade, "source": "synthetic_nt01_2025"
}
def gerar_exemplo_normas():
ocupacao = random.choice(OCUPACOES)
tmpl = random.choice(PERGUNTAS_NORMAS)
pergunta = tmpl.format(desc=ocupacao["desc"], grupo=ocupacao["grupo"])
grupo = ocupacao["grupo"]
letra = grupo.split("-")[0]
normas_por_grupo = {
"A": ["NBR 9077","NBR 10897"], "B": ["NBR 9077","NBR 13714","NBR 12693"],
"C": ["NBR 9077","NBR 12693"], "D": ["NBR 9077","NBR 13434","NR-23"],
"E": ["RDC ANVISA","NBR 9077","NBR 12693"], "H": ["NBR 14276","NR-23","NBR 12693"],
}
normas = normas_por_grupo.get(letra, ["NBR 9077","NBR 12693","NT-01/2025"])
normas_str = chr(10).join(f"- {n}" for n in normas)
resposta = f"""Para {ocupacao["desc"]} (grupo {grupo}), NT-01/2025 determina:
**Normas Aplicaveis:**
{normas_str}
- NT-01/2025 CBMGO | Lei Estadual 15.802/2006
**Exigencias Principais:**
- Extintores com manutencao anual (NBR 12693)
- Sinalizacao de emergencia (NBR 13434)
- Rotas de saida desobstruidas (NBR 9077)
- PPCI aprovado pelo CBMGO antes da ocupacao
**Periodicidade de Vistorias:**
- Inicial: Antes da ocupacao
- Renovacao: 3 anos (baixo risco) / 1 ano (alto risco)"""
return {
"instruction": pergunta, "input": "",
"output": resposta, "category": "normas",
"grupo": grupo, "source": "synthetic_nt01_2025"
}
def formatar_chatml(exemplo):
system_msg = "Voce e o CERCON, assistente do CBMGO especializado em NT-01/2025 e prevencao de incendio em Goias."
user_content = exemplo["instruction"]
if exemplo.get("input"):
user_content += f"\n\nDados fornecidos:\n{exemplo['input']}"
return {
"messages": [
{"role": "system", "content": system_msg},
{"role": "user", "content": user_content},
{"role": "assistant", "content": exemplo["output"]}
]
}
def formatar_alpaca(exemplo):
return {
"instruction": exemplo["instruction"],
"input": exemplo.get("input", ""),
"output": exemplo["output"]
}
def formatar_llama3(exemplo):
system_msg = "Voce e o CERCON, assistente do CBMGO especializado em NT-01/2025."
user_content = exemplo["instruction"]
if exemplo.get("input"):
user_content += f"\n\n{exemplo['input']}"
text = f"<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n{system_msg}<|eot_id|><|start_header_id|>user<|end_header_id|>\n\n{user_content}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n{exemplo['output']}<|eot_id|>"
return {"text": text}
def gerar_dataset(n_total=2000, output_dir="data", formato="chatml", seed=42):
random.seed(seed)
Path(output_dir).mkdir(parents=True, exist_ok=True)
n_class = int(n_total * 0.30)
n_calc = int(n_total * 0.25)
n_mem = int(n_total * 0.30)
n_norm = n_total - n_class - n_calc - n_mem
print(f"Gerando dataset sintetico NT-01/2025 CBMGO")
print(f"Total: {n_total} | Classificacao: {n_class} | Calculo: {n_calc} | Memorial: {n_mem} | Normas: {n_norm}")
exemplos = []
print("Gerando exemplos de classificacao...")
for _ in range(n_class): exemplos.append(gerar_exemplo_classificacao())
print("Gerando exemplos de calculo...")
for _ in range(n_calc): exemplos.append(gerar_exemplo_calculo())
print("Gerando exemplos de memorial...")
for _ in range(n_mem): exemplos.append(gerar_exemplo_memorial())
print("Gerando exemplos de normas...")
for _ in range(n_norm): exemplos.append(gerar_exemplo_normas())
random.shuffle(exemplos)
formatadores = {"alpaca": formatar_alpaca, "chatml": formatar_chatml, "llama3": formatar_llama3}
fmt = formatadores.get(formato, formatar_chatml)
formatados = [fmt(e) for e in exemplos]
split = int(len(formatados) * 0.9)
treino, val = formatados[:split], formatados[split:]
out = Path(output_dir)
with open(out / f"train_{formato}.jsonl", "w", encoding="utf-8") as f:
for ex in treino: f.write(json.dumps(ex, ensure_ascii=False) + "\n")
print(f"Treino: {out}/train_{formato}.jsonl ({len(treino)} exemplos)")
with open(out / f"validation_{formato}.jsonl", "w", encoding="utf-8") as f:
for ex in val: f.write(json.dumps(ex, ensure_ascii=False) + "\n")
print(f"Validacao: {out}/validation_{formato}.jsonl ({len(val)} exemplos)")
meta = {
"total": n_total, "treino": len(treino), "validacao": len(val),
"formato": formato, "seed": seed,
"distribuicao": {"classificacao": n_class, "calculo": n_calc, "memorial": n_mem, "normas": n_norm},
"base_normativa": "NT-01/2025 CBMGO",
"gerado_em": datetime.now().isoformat(),
"versao": "1.0.0",
}
with open(out / "dataset_metadata.json", "w", encoding="utf-8") as f:
json.dump(meta, f, ensure_ascii=False, indent=2)
print(f"Metadados: {out}/dataset_metadata.json")
print(f"\nDataset gerado com sucesso! Total: {n_total} exemplos")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Gerador de dataset sintetico NT-01/2025 CBMGO")
parser.add_argument("--n", type=int, default=2000, help="Numero de exemplos (default: 2000)")
parser.add_argument("--output", type=str, default="data", help="Diretorio de saida (default: data)")
parser.add_argument("--formato", type=str, default="chatml",
choices=["alpaca","chatml","llama3"], help="Formato SFT (default: chatml)")
parser.add_argument("--seed", type=int, default=42, help="Semente aleatoria (default: 42)")
args = parser.parse_args()
gerar_dataset(n_total=args.n, output_dir=args.output, formato=args.formato, seed=args.seed)