275 lines
8.5 KiB
Python
275 lines
8.5 KiB
Python
import yaml
|
|
import json
|
|
import os
|
|
import logging
|
|
|
|
# --- CONFIGURAÇÕES DE LOG ---
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s - CLASSIFICADOR - %(levelname)s - %(message)s",
|
|
handlers=[
|
|
logging.FileHandler("classificacao_pedagogica.log"),
|
|
logging.StreamHandler(),
|
|
],
|
|
)
|
|
|
|
# --- CONFIGURAÇÕES ---
|
|
ARQUIVO_YAML = "all.yml"
|
|
ARQUIVO_AUDIO = "analise_audio.json"
|
|
ARQUIVO_FINAL = "db_projetos_classificados.json"
|
|
|
|
PLUGINS_NATIVOS = [
|
|
"tripleoscillator",
|
|
"zynaddsubfx",
|
|
"sfxr",
|
|
"organic",
|
|
"audiofileprocessor",
|
|
"lb302",
|
|
"kicker",
|
|
"watsyn",
|
|
"bitinvader",
|
|
"freeboy",
|
|
"mallets",
|
|
"vibed",
|
|
]
|
|
|
|
# --- LÓGICA PEDAGÓGICA AVANÇADA ---
|
|
|
|
|
|
def calcular_nivel_ponderado(
|
|
num_tracks, tem_automacao, plugins_complexos, qtd_secoes_audio, fx_criativos
|
|
):
|
|
"""
|
|
Calcula nível pedagógico (Iniciante, Intermediário, Avançado)
|
|
baseado em esforço de engenharia e complexidade musical.
|
|
"""
|
|
pontos = 0
|
|
|
|
# 1. Volume de Trabalho
|
|
if num_tracks >= 8:
|
|
pontos += 1
|
|
if num_tracks >= 16:
|
|
pontos += 1
|
|
|
|
# 2. Profundidade Técnica
|
|
if tem_automacao:
|
|
pontos += 2 # Automação é sinal forte de polimento
|
|
if plugins_complexos:
|
|
pontos += 1.5 # Usar ZynAddSubFx requer estudo
|
|
if fx_criativos:
|
|
pontos += 1 # Usar efeitos além do básico
|
|
|
|
# 3. Estrutura Musical (Vinda do Essentia)
|
|
# Se o áudio tem > 3 seções detectadas, há arranjo (não é só loop)
|
|
if qtd_secoes_audio >= 3:
|
|
pontos += 2
|
|
elif qtd_secoes_audio >= 5:
|
|
pontos += 3
|
|
|
|
# Classificação
|
|
if pontos < 3:
|
|
return "Iniciante"
|
|
if pontos < 6:
|
|
return "Intermediário"
|
|
return "Avançado"
|
|
|
|
|
|
def analisar_tags_pedagogicas(tracks, bpm, qtd_secoes, tom, escala):
|
|
tags = []
|
|
|
|
# Flags de estado técnico
|
|
tem_zyn = False
|
|
tem_automacao = False
|
|
tem_fx_chain = False
|
|
qtd_nativos = 0
|
|
qtd_vst_externos = 0
|
|
tem_audio_processor = False
|
|
|
|
for t in tracks:
|
|
inst = t.get("instrumenttrack")
|
|
if not inst:
|
|
continue
|
|
|
|
plugin = inst.get("plugin_name", "").lower()
|
|
|
|
# Mapeamento de Plugins e Dependências
|
|
if plugin in PLUGINS_NATIVOS:
|
|
qtd_nativos += 1
|
|
elif plugin == "vestige":
|
|
qtd_vst_externos += 1
|
|
|
|
if plugin == "zynaddsubfx":
|
|
tem_zyn = True
|
|
|
|
if plugin == "audiofileprocessor":
|
|
tem_audio_processor = 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
|
|
|
|
# --- 1. Estética da Escassez e Modulação Marginal ---
|
|
if qtd_nativos > 0 and qtd_vst_externos == 0:
|
|
tags.append("Estética da Escassez: Síntese Nativa")
|
|
|
|
if tem_automacao and qtd_vst_externos == 0:
|
|
tags.append("Design de Som Marginal (Automação como Timbre)")
|
|
|
|
# --- 2. Taxonomia de Bloom (Carga Cognitiva) ---
|
|
if tem_zyn or tem_fx_chain:
|
|
tags.append("Bloom: Criação (Sound Design Complexo)")
|
|
elif tem_audio_processor and not tem_automacao:
|
|
tags.append("Bloom: Aplicação (Sample Inalterado)")
|
|
|
|
# --- 3. Fluência Estrutural (Macroforma Musical) ---
|
|
if qtd_secoes >= 3:
|
|
tags.append("Fluência Estrutural: Arranjo Completo (Macroforma)")
|
|
else:
|
|
tags.append("Fluência Estrutural: Loop Estático")
|
|
|
|
# --- 4. Acessibilidade Ergonômica (Música de Computador) ---
|
|
if tom == "A" and escala == "minor":
|
|
tags.append("Ergonomia Autodidata (Teclado QWERTY / Lá Menor)")
|
|
|
|
# --- 5. Herança Cultural / Andamento ---
|
|
if 80 <= bpm <= 95:
|
|
tags.append("Cadência Clássica (Boom Bap)")
|
|
elif bpm >= 130:
|
|
tags.append("Estética Trap/Funk/Eletrônica / Alta Energia")
|
|
|
|
return tags
|
|
|
|
|
|
def main():
|
|
logging.info("Iniciando Classificador Mestre...")
|
|
|
|
# 1. Carregar YAML
|
|
if not os.path.exists(ARQUIVO_YAML):
|
|
logging.critical(f"{ARQUIVO_YAML} não encontrado.")
|
|
return
|
|
|
|
with open(ARQUIVO_YAML, "r", encoding="utf-8") as f:
|
|
dados_projetos = yaml.safe_load(f)
|
|
|
|
# 2. Carregar JSON de Áudio
|
|
lookup_audio = {}
|
|
if os.path.exists(ARQUIVO_AUDIO):
|
|
with open(ARQUIVO_AUDIO, "r", encoding="utf-8") as f:
|
|
lista_audio = json.load(f)
|
|
for item in lista_audio:
|
|
# Normaliza chave de busca (nome do arquivo sem extensão)
|
|
nome_base = os.path.splitext(item["arquivo"])[0]
|
|
lookup_audio[nome_base] = item
|
|
else:
|
|
logging.warning(
|
|
"JSON de áudio não encontrado. Classificação será apenas estática."
|
|
)
|
|
|
|
db_final = []
|
|
|
|
logging.info(f"Processando {len(dados_projetos)} entradas do YAML...")
|
|
|
|
for proj in dados_projetos:
|
|
if not proj:
|
|
continue
|
|
|
|
try:
|
|
nome_arquivo = proj.get("file", "")
|
|
if not nome_arquivo:
|
|
continue
|
|
|
|
# Tenta casar com dados de áudio
|
|
# Nota: Seu YAML usa nomes como 'trap-fyrebreak', o arquivo de audio deve ser 'trap-fyrebreak.wav'
|
|
# O lookup_audio já está sem extensão.
|
|
dados_audio = lookup_audio.get(nome_arquivo, {})
|
|
analise_tec = dados_audio.get("analise_tecnica", {})
|
|
analise_ia = dados_audio.get("analise_ia", {})
|
|
|
|
# Dados Técnicos
|
|
raw_bpm = proj.get("bpm", 120)
|
|
bpm_projeto = (
|
|
float(raw_bpm) if str(raw_bpm).replace(".", "", 1).isdigit() else 120.0
|
|
)
|
|
|
|
# Feature nova: Quantidade de seções (Intro, Verso...)
|
|
qtd_secoes = analise_tec.get("qtd_secoes_detectadas", 1)
|
|
|
|
# Análise Estática (Tracks e Plugins)
|
|
tracks = proj.get("tracks", [])
|
|
tags, tem_auto, tem_zyn, tem_fx = analisar_tags_pedagogicas(
|
|
tracks=proj.get("tracks", []),
|
|
bpm=bpm_projeto,
|
|
qtd_secoes=qtd_secoes,
|
|
tom=analise_tec.get("tom", "N/A"),
|
|
escala=analise_tec.get("escala", "N/A"),
|
|
)
|
|
|
|
# Verifica plugins externos
|
|
plugins_usados = set()
|
|
usa_vst_externo = False
|
|
for t in tracks:
|
|
p = t.get("instrumenttrack", {}).get("plugin_name", "")
|
|
if p:
|
|
plugins_usados.add(p)
|
|
if p.lower() not in PLUGINS_NATIVOS:
|
|
usa_vst_externo = True
|
|
|
|
# Cálculo de Nível (Nova Lógica)
|
|
nivel = calcular_nivel_ponderado(
|
|
len(tracks), tem_auto, tem_zyn, qtd_secoes, tem_fx
|
|
)
|
|
|
|
# Define Gênero (IA tem prioridade, depois regras manuais)
|
|
genero = analise_ia.get("genero_predito", "Desconhecido")
|
|
if genero == "Desconhecido" or analise_ia.get("confianca_genero", 0) < 0.4:
|
|
# Fallback manual simples
|
|
if "Trap Rhythm" in tags:
|
|
genero = "Trap"
|
|
elif bpm_projeto > 120:
|
|
genero = "Electronic"
|
|
else:
|
|
genero = "HipHop/Downtempo"
|
|
|
|
# Montagem do Objeto
|
|
item = {
|
|
"id": nome_arquivo,
|
|
"titulo": proj.get("original_title", nome_arquivo),
|
|
"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_detectada": f"{qtd_secoes} seções distintas",
|
|
},
|
|
"classificacao": {
|
|
"genero": genero,
|
|
"mood": "Energético"
|
|
if analise_tec.get("intensidade", -20) > -10
|
|
else "Suave",
|
|
},
|
|
}
|
|
|
|
db_final.append(item)
|
|
|
|
except Exception as e:
|
|
logging.error(f"Erro ao processar projeto '{proj.get('file')}': {e}")
|
|
continue
|
|
|
|
# Salvar Resultado
|
|
with open(ARQUIVO_FINAL, "w", encoding="utf-8") as f:
|
|
json.dump(db_final, f, indent=4, ensure_ascii=False)
|
|
|
|
logging.info(f"Sucesso! {len(db_final)} projetos classificados e salvos.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|