mmpSearch/scripts/classificacao/classificacao_mestre.py

458 lines
15 KiB
Python

import yaml
import json
import os
import logging
import csv
import unicodedata
import time
import psutil
import glob # Import necessário para ler múltiplos arquivos
from datetime import datetime, timedelta
# --- CONFIGURAÇÕES GERAIS ---
BASE_DIR = "/var/www/html/trens/src_mmpSearch"
LOG_DIR = os.path.join(BASE_DIR, "logs/classificacao_pedagogica")
os.makedirs(LOG_DIR, exist_ok=True)
TIMESTAMP = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
# Configuração de Log
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - CLASSIFICADOR - %(levelname)s - %(message)s",
handlers=[
logging.FileHandler(os.path.join(LOG_DIR, f"log_pedagogico_{TIMESTAMP}.log")),
logging.StreamHandler(),
],
)
# --- ARQUIVOS DE ENTRADA/SAÍDA ---
def encontrar_caminho(nome_arquivo):
"""Procura o arquivo em locais comuns do projeto."""
candidatos = [
os.path.join(BASE_DIR, "saida_analises", nome_arquivo),
os.path.join(BASE_DIR, "data", nome_arquivo),
os.path.join(BASE_DIR, nome_arquivo),
nome_arquivo,
]
for c in candidatos:
if os.path.exists(c):
return c
return nome_arquivo
# --- MUDANÇA AQUI: Aponta para a pasta dos lotes ---
PASTA_LOTES_YAML = os.path.join(BASE_DIR, "lotes_yaml")
# ARQUIVO_YAML = "all.yml" <-- Removido/Comentado
ARQUIVO_AUDIO_INPUT = encontrar_caminho("db_final_completo.json")
ARQUIVO_FINAL_OUTPUT = os.path.join(
BASE_DIR, "saida_analises", "db_projetos_classificados.json"
)
ARQUIVO_AUDITORIA = os.path.join(
BASE_DIR, "saida_analises", "audit_classificacao_pedagogica.csv"
)
# --- LISTAS DE REFERÊNCIA ---
PLUGINS_NATIVOS = [
"tripleoscillator",
"zynaddsubfx",
"sfxr",
"organic",
"audiofileprocessor",
"lb302",
"kicker",
"watsyn",
"bitinvader",
"freeboy",
"mallets",
"vibed",
]
# --- CLASSE DE AUDITORIA DE PERFORMANCE ---
class AuditoriaPerformance:
def __init__(self):
self.inicio_global = time.time()
self.process = psutil.Process(os.getpid())
self.marcos = [1, 10, 100, 500, 1000, 2000, 5000]
def get_metricas(self):
"""Retorna uso atual de RAM (MB) e CPU (%)"""
try:
ram_mb = self.process.memory_info().rss / (1024 * 1024)
cpu_pct = self.process.cpu_percent(interval=None)
return round(ram_mb, 2), cpu_pct
except:
return 0.0, 0.0
def verificar_marco(self, contagem):
if contagem in self.marcos:
tempo_decorrido = time.time() - self.inicio_global
logging.info(
f"--- MARCO: {contagem} projetos processados em {str(timedelta(seconds=int(tempo_decorrido)))} ---"
)
# --- FUNÇÕES AUXILIARES ---
def normalizar_chave(texto):
if not texto:
return ""
s = str(texto)
if "." in s:
s = os.path.splitext(s)[0]
s = unicodedata.normalize("NFKD", s).encode("ASCII", "ignore").decode("ASCII")
return s.lower().replace(" ", "").replace("-", "").replace("_", "").strip()
def calcular_nivel_ponderado(
num_tracks, tem_automacao, usa_zyn, qtd_secoes_audio, fx_criativos
):
pontos = 0
# 1. Volume
if num_tracks >= 8:
pontos += 1
if num_tracks >= 16:
pontos += 2
if num_tracks >= 32:
pontos += 3
if num_tracks >= 64:
pontos += 4
elif num_tracks >= 128:
pontos += 5
# 2. Técnica
if tem_automacao:
pontos += 2
if usa_zyn:
pontos += 1.5
if fx_criativos:
pontos += 1
# 3. Estrutura (Áudio)
if qtd_secoes_audio >= 3:
pontos += 1.5
elif qtd_secoes_audio >= 5:
pontos += 3
elif qtd_secoes_audio >= 10:
pontos += 5
if pontos < 3:
return "Iniciante"
if pontos < 6:
return "Intermediário"
return "Avançado"
def analisar_tags_pedagogicas(tracks, bpm, qtd_secoes):
tags = []
tem_kicker = False
tem_zyn = False
tem_automacao = False
tem_fx_chain = False
rolagem_trap = False
for t in tracks:
inst = t.get("instrumenttrack")
if not inst:
continue
nome = inst.get("name", "").lower()
plugin = inst.get("plugin_name", "").lower()
if plugin == "kicker":
tem_kicker = True
if plugin == "zynaddsubfx":
tem_zyn = True
if "automation" in str(inst).lower():
tem_automacao = True
fx = inst.get("fxchain", {})
if fx and int(fx.get("numofeffects", 0)) > 0:
tem_fx_chain = True
patterns = inst.get("patterns", [])
if patterns:
for pat in patterns:
steps = pat.get("steps", [])
consecutivos = 0
for step in steps:
val = 1 if step else 0
consecutivos = (consecutivos + 1) if val else 0
if consecutivos >= 3 and ("hat" in nome):
rolagem_trap = True
if tem_kicker:
tags.append("Síntese de Bateria")
if tem_zyn:
tags.append("Design de Som Avançado")
if tem_automacao:
tags.append("Automação")
if tem_fx_chain:
tags.append("Mixagem com FX")
if rolagem_trap and bpm > 115:
tags.append("Trap Rhythm")
if bpm < 100 and not rolagem_trap:
tags.append("Boombap Groove")
if qtd_secoes >= 4:
tags.append("Arranjo Completo")
elif qtd_secoes <= 1:
tags.append("Loop Estático")
else:
tags.append("Estrutura Simples")
return tags, tem_automacao, tem_zyn, tem_fx_chain
# --- FUNÇÃO PARA CARREGAR LOTES YAML ---
def carregar_todos_yamls(pasta_lotes):
"""Lê todos os arquivos YAML da pasta especificada e retorna uma lista única."""
logging.info(f"Buscando arquivos YAML em: {pasta_lotes}")
padrao = os.path.join(pasta_lotes, "*.yml")
arquivos = sorted(glob.glob(padrao))
todos_projetos = []
if not arquivos:
logging.warning("Nenhum arquivo YAML encontrado na pasta de lotes.")
# Tenta fallback para o arquivo único antigo se existir
caminho_unico = encontrar_caminho("all.yml")
if os.path.exists(caminho_unico):
logging.info(f"Fallback: Carregando arquivo único {caminho_unico}")
with open(caminho_unico, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
return []
for arq in arquivos:
try:
logging.info(f"Carregando lote: {os.path.basename(arq)}")
with open(arq, "r", encoding="utf-8") as f:
lote = yaml.safe_load(f)
if lote:
# Garante que seja uma lista
if isinstance(lote, list):
todos_projetos.extend(lote)
elif isinstance(lote, dict):
todos_projetos.append(lote)
except Exception as e:
logging.error(f"Erro ao ler arquivo {arq}: {e}")
logging.info(f"Total de projetos carregados dos lotes: {len(todos_projetos)}")
return todos_projetos
# --- MAIN ---
def main():
logging.info("--- INICIANDO CLASSIFICADOR PEDAGÓGICO (COM AUDITORIA E LOTES) ---")
auditor = AuditoriaPerformance()
# 1. Carregar YAML (Adaptado para Lotes)
if not os.path.exists(PASTA_LOTES_YAML):
logging.warning(f"Pasta de lotes não encontrada: {PASTA_LOTES_YAML}")
# Tenta criar se não existir (embora deva existir com arquivos)
# os.makedirs(PASTA_LOTES_YAML, exist_ok=True)
dados_projetos = carregar_todos_yamls(PASTA_LOTES_YAML)
if not dados_projetos:
logging.critical("Nenhum dado de projeto carregado. Encerrando.")
return
# 2. Carregar JSON de Áudio
lookup_audio = {}
total_audio_carregado = 0
if os.path.exists(ARQUIVO_AUDIO_INPUT):
logging.info(f"Lendo Análise de Áudio: {ARQUIVO_AUDIO_INPUT}")
with open(ARQUIVO_AUDIO_INPUT, "r", encoding="utf-8") as f:
lista_audio = json.load(f)
for item in lista_audio:
chave = normalizar_chave(item.get("arquivo", ""))
if chave:
lookup_audio[chave] = item
total_audio_carregado = len(lista_audio)
else:
logging.warning(f"Arquivo de áudio {ARQUIVO_AUDIO_INPUT} não encontrado.")
db_final = []
estatisticas = {"Iniciante": 0, "Intermediário": 0, "Avançado": 0, "Total": 0}
# --- PREPARAR CSV DE AUDITORIA ---
with open(ARQUIVO_AUDITORIA, "w", newline="", encoding="utf-8") as csvfile:
writer = csv.writer(csvfile)
# Cabeçalho expandido com métricas de performance
writer.writerow(
[
"Timestamp",
"ID_Projeto",
"Audio_Encontrado",
"Qtd_Secoes",
"Nivel",
"Genero_Final",
"Tempo_Proc_ms",
"RAM_Uso_MB",
"Tags",
]
)
logging.info(f"Processando {len(dados_projetos)} projetos...")
contagem_processados = 0
for proj in dados_projetos:
if not proj or not isinstance(proj, dict):
continue
# --- INÍCIO TIMER INDIVIDUAL ---
inicio_item = time.perf_counter()
nome_arquivo = proj.get("file", "")
if not nome_arquivo:
continue
try:
# Lógica de Classificação
chave_proj = normalizar_chave(nome_arquivo)
dados_audio = lookup_audio.get(chave_proj, {})
analise_tec = dados_audio.get("analise_tecnica", {})
analise_ia = dados_audio.get("analise_ia", {})
raw_bpm = proj.get("bpm", 120)
try:
bpm_projeto = float(raw_bpm)
except:
bpm_projeto = 120.0
est_seg = analise_tec.get("estrutura_segundos", [])
qtd_secoes = len(est_seg) if isinstance(est_seg, list) else 1
tracks = proj.get("tracks", [])
tags, tem_auto, tem_zyn, tem_fx = analisar_tags_pedagogicas(
tracks, bpm_projeto, qtd_secoes
)
plugins_usados = set()
usa_vst_externo = False
for t in tracks:
inst_track = t.get("instrumenttrack", {})
if not inst_track:
continue
p = inst_track.get("plugin_name", "")
if p:
p_norm = p.lower()
plugins_usados.add(p_norm)
if p_norm not in PLUGINS_NATIVOS:
usa_vst_externo = True
nivel = calcular_nivel_ponderado(
len(tracks), tem_auto, tem_zyn, qtd_secoes, tem_fx
)
estatisticas[nivel] += 1
estatisticas["Total"] += 1
genero = "Desconhecido"
if analise_ia and "estilo_principal" in analise_ia:
genero = analise_ia["estilo_principal"]
elif (
"genero_macro" in analise_ia
and analise_ia["genero_macro"] != "Unknown"
):
genero = analise_ia["genero_macro"]
if genero == "Desconhecido" or genero == "Unknown":
if "Trap Rhythm" in tags:
genero = "Trap (Rule-based)"
elif bpm_projeto > 120:
genero = "Electronic (Rule-based)"
else:
genero = "HipHop/Downtempo (Rule-based)"
# Objeto Final
item = {
"id": nome_arquivo,
"titulo": proj.get("original_title", nome_arquivo),
"match_audio": bool(dados_audio),
"pedagogia": {
"nivel": nivel,
"tags_aprendizado": tags,
"plugins_principais": list(plugins_usados),
"requer_vst_externo": usa_vst_externo,
},
"tecnica": {
"bpm": bpm_projeto,
"tom": analise_tec.get("tom", "N/A"),
"escala": analise_tec.get("escala", "N/A"),
"estrutura_qtd_secoes": qtd_secoes,
"intensidade_media": analise_tec.get("intensidade_db", "N/A"),
},
"classificacao": {
"genero": genero,
"mood": "Energético"
if analise_tec.get("intensidade_db", -20) > -10
else "Suave",
},
}
db_final.append(item)
# --- FIM TIMER INDIVIDUAL ---
tempo_ms = (time.perf_counter() - inicio_item) * 1000
ram_atual, _ = auditor.get_metricas()
ts_agora = datetime.now().strftime("%H:%M:%S")
# Registra Auditoria com Performance
writer.writerow(
[
ts_agora,
nome_arquivo,
"SIM" if dados_audio else "NAO",
qtd_secoes,
nivel,
genero,
f"{tempo_ms:.3f}", # Tempo em milissegundos com 3 casas
f"{ram_atual:.2f}", # RAM em MB
"|".join(tags),
]
)
contagem_processados += 1
auditor.verificar_marco(contagem_processados)
except Exception as e:
logging.error(
f"Erro ao processar projeto '{proj.get('file', 'unknown')}': {e}"
)
continue
# Salvar Resultado JSON
logging.info(f"Salvando JSON Final: {ARQUIVO_FINAL_OUTPUT}")
with open(ARQUIVO_FINAL_OUTPUT, "w", encoding="utf-8") as f:
json.dump(db_final, f, indent=4, ensure_ascii=False)
tempo_total = time.time() - auditor.inicio_global
logging.info("--- RELATÓRIO FINAL ---")
logging.info(f"Tempo Total de Execução: {str(timedelta(seconds=int(tempo_total)))}")
logging.info(f"Total Processado: {estatisticas['Total']}")
logging.info(f"Iniciantes: {estatisticas['Iniciante']}")
logging.info(f"Intermediários: {estatisticas['Intermediário']}")
logging.info(f"Avançados: {estatisticas['Avançado']}")
logging.info(
f"Taxa de Casamento com Áudio: {len(lookup_audio)}/{total_audio_carregado}"
)
logging.info(f"Auditoria salva em: {ARQUIVO_AUDITORIA}")
if __name__ == "__main__":
main()