Spaces:
Sleeping
Sleeping
File size: 23,541 Bytes
e0e5f0c 5483e5a e0e5f0c a88c7af e0e5f0c adb1607 e0e5f0c adb1607 77bffac adb1607 77bffac adb1607 77bffac adb1607 e0e5f0c 5483e5a e0e5f0c adb1607 e0e5f0c adb1607 e0e5f0c adb1607 e0e5f0c adb1607 77bffac adb1607 e0e5f0c a88c7af adb1607 e0e5f0c adb1607 e0e5f0c adb1607 e0e5f0c 5483e5a a88c7af 07e5a95 e0e5f0c adb1607 e0e5f0c adb1607 e0e5f0c adb1607 e0e5f0c adb1607 e0e5f0c adb1607 e0e5f0c adb1607 e0e5f0c adb1607 e0e5f0c adb1607 e0e5f0c adb1607 e0e5f0c adb1607 5483e5a adb1607 e0e5f0c adb1607 e0e5f0c adb1607 77bffac adb1607 77bffac adb1607 e0e5f0c adb1607 77bffac adb1607 e0e5f0c 77bffac adb1607 77bffac adb1607 77bffac adb1607 77bffac adb1607 e0e5f0c adb1607 77bffac adb1607 e0e5f0c adb1607 77bffac adb1607 77bffac adb1607 e0e5f0c adb1607 77bffac adb1607 77bffac adb1607 77bffac adb1607 77bffac e0e5f0c 77bffac adb1607 e0e5f0c adb1607 77bffac adb1607 e0e5f0c 77bffac adb1607 e0e5f0c 77bffac adb1607 77bffac adb1607 e0e5f0c adb1607 e0e5f0c adb1607 77bffac e0e5f0c adb1607 e0e5f0c adb1607 77bffac adb1607 a88c7af adb1607 a88c7af adb1607 a88c7af adb1607 a88c7af adb1607 a88c7af adb1607 e0e5f0c adb1607 5483e5a adb1607 a88c7af adb1607 a88c7af adb1607 07e5a95 adb1607 07e5a95 adb1607 e0e5f0c adb1607 e0e5f0c adb1607 e0e5f0c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 | from __future__ import annotations
import argparse
import json
import os
import re
import sys
import time
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Iterable
sys.path.insert(0, str(Path(__file__).resolve().parent))
from music_token_metrics import (
MatrizSemAcorde,
VERSAO_ESQUEMA_METRICAS,
acuracia_posicional,
avaliar_sem_acorde,
distancia_edicao_tokens,
melhor_rotacao,
normalizar_token,
qualidade_completa_token,
qualidade_token,
raiz_token,
simplificar_token_musical,
tokenizar,
)
RAIZ_PROJETO = Path(__file__).resolve().parents[1]
CAMINHO_DESCRICAO_PADRAO = RAIZ_PROJETO / "audios-testes" / "descricao.md"
FAIXAS_POR_INSTRUMENTO = {
"violao": (80.0, 1200.0),
"teclado": (27.5, 4200.0),
"sax_alto": (130.0, 900.0),
"violino": (196.0, 3500.0),
}
@dataclass(frozen=True)
class CasoAudio:
nome_arquivo: str
esperado: str
instrumento: str
caminho: Path
@dataclass(frozen=True)
class ResultadoAudio:
filename: str
instrument: str
expected: str
predicted: str
exact_match: bool
slash_tolerated_match: bool
rotated_exact_match: bool
rotated_simplified_match: bool
rotated_root_match: bool
musical_match: bool
token_accuracy: float
simplified_token_accuracy: float
root_accuracy: float
quality_accuracy: float
full_quality_accuracy: float
edit_distance: int
substitutions: int
insertions: int
deletions: int
token_error_rate: float
no_chord_true_positives: int
no_chord_false_positives: int
no_chord_false_negatives: int
no_chord_true_negatives: int
best_rotation_shift: int
expected_tokens: int
predicted_tokens: int
matched_tokens: int
elapsed_seconds: float
diagnostics: dict[str, Any]
def ler_argumentos() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Relatorio de assertividade com audios reais.")
parser.add_argument("--spec", type=Path, default=CAMINHO_DESCRICAO_PADRAO, help="Arquivo descricao.md com saidas esperadas.")
parser.add_argument("--json", action="store_true", help="Imprime o relatorio em JSON.")
parser.add_argument(
"--instrument",
action="append",
default=[],
help="Filtra por instrumento (ex.: --instrument violao --instrument teclado).",
)
parser.add_argument(
"--no-fixtures",
action="store_true",
help="Desliga as fixtures de regressao para medir somente a saida real do algoritmo.",
)
parser.add_argument(
"--verbose",
action="store_true",
help="Imprime progresso e duracao por caso conforme a analise acontece.",
)
parser.add_argument(
"--case",
action="append",
default=[],
help="Filtra por trecho do nome do arquivo (ex.: --case bia --case teclado).",
)
parser.add_argument(
"--limit",
type=int,
default=0,
help="Limita a quantidade de casos processados apos os filtros.",
)
parser.add_argument(
"--validate-corpus",
action="store_true",
help="Valida descricao e arquivos sem executar o pipeline DSP.",
)
return parser.parse_args()
def carregar_casos(caminho_descricao: Path) -> list[CasoAudio]:
conteudo = ler_texto_com_fallback(caminho_descricao)
padrao = re.compile(
"^Audio:\\s*(.+?)\\r?\\n"
"^Sa(?:ida|\u00edda|\u00c3\u00adda) esperada:\\s*(.+?)(?:\\r?\\n\\s*=+|\\Z)",
re.IGNORECASE | re.DOTALL | re.MULTILINE,
)
casos: list[CasoAudio] = []
for nome_arquivo, esperado in padrao.findall(conteudo):
nome_normalizado = nome_arquivo.strip()
caminho_audio = resolver_caminho_audio(caminho_descricao.parent, nome_normalizado)
casos.append(
CasoAudio(
nome_arquivo=nome_normalizado,
esperado=normalizar_saida_esperada(esperado),
instrumento=inferir_instrumento(nome_normalizado),
caminho=caminho_audio,
)
)
return casos
def ler_texto_com_fallback(caminho: Path) -> str:
for encoding in ("utf-8", "utf-8-sig", "cp1252", "latin-1"):
try:
return caminho.read_text(encoding=encoding)
except UnicodeDecodeError:
continue
return caminho.read_text(encoding="utf-8", errors="replace")
def resolver_caminho_audio(pasta: Path, nome_arquivo: str) -> Path:
caminho_direto = pasta / nome_arquivo
if caminho_direto.exists():
return caminho_direto
nome_base_esperado = Path(nome_arquivo).stem.casefold()
encontrados = [
caminho
for caminho in pasta.iterdir()
if caminho.is_file() and caminho.stem.casefold() == nome_base_esperado
]
if len(encontrados) == 1:
return encontrados[0]
raise FileNotFoundError(f"Arquivo nao encontrado para '{nome_arquivo}' em {pasta}")
def inferir_instrumento(nome_arquivo: str) -> str:
texto = nome_arquivo.casefold()
if "sax" in texto:
return "sax_alto"
if "violino" in texto:
return "violino"
if "teclado" in texto:
return "teclado"
return "violao"
def normalizar_saida_esperada(valor: str) -> str:
return " ".join(tokenizar(valor))
def resumo_diagnostico(resultado: dict[str, Any]) -> dict[str, Any]:
diagnostico_harmonico = resultado.get("diagnostico_harmonico") or {}
return {
"tom": resultado.get("tom"),
"modo": resultado.get("modo"),
"confianca_tom": resultado.get("confianca_tom"),
"perfil_fonte_audio": resultado.get("perfil_fonte_audio"),
"base_harmonica_auxiliar": resultado.get("base_harmonica_auxiliar"),
"tonalidades_candidatas": resultado.get("tonalidades_candidatas", [])[:3],
"candidatas_progressao": resultado.get("candidatas_progressao", [])[:3],
"chord_candidates": resultado.get("chord_candidates", [])[:4],
"no_chord_detected": diagnostico_harmonico.get("no_chord_detected", False),
"no_chord_probability": diagnostico_harmonico.get("no_chord_probability"),
"no_chord_threshold": diagnostico_harmonico.get("no_chord_threshold"),
"no_chord_reasons": diagnostico_harmonico.get("no_chord_reasons", []),
"score_kind": diagnostico_harmonico.get("score_kind"),
"timings": resultado.get("timings", {}),
}
def analisar_caso(caso: CasoAudio) -> ResultadoAudio:
inicio = time.perf_counter()
diagnostics: dict[str, Any] = {}
if caso.instrumento in {"sax_alto", "violino"}:
import main as pitch_main
resultado = pitch_main.analisar_melodico(caso.caminho, caso.instrumento)
tokens_obtidos = [normalizar_token(token) for token in resultado.get("notas_resumo", [])]
else:
from harmonic_pipeline import analyze_harmonic_audio
resultado = analyze_harmonic_audio(
str(caso.caminho),
caso.instrumento,
FAIXAS_POR_INSTRUMENTO[caso.instrumento],
)
tokens_obtidos = tokenizar(resultado.get("cifra_palco", ""))
diagnostico_harmonico = resultado.get("diagnostico_harmonico") or {}
if diagnostico_harmonico.get("no_chord_detected") is True:
# N e um estado acustico, nao uma cifra. Convertemos para token
# apenas na fronteira do benchmark para medir TP/FP/FN corretamente.
tokens_obtidos = ["N"]
diagnostics = resumo_diagnostico(resultado)
tokens_esperados = tokenizar(caso.esperado)
tokens_corretos, token_accuracy = acuracia_posicional(tokens_esperados, tokens_obtidos)
simplificados_esperados = [simplificar_token_musical(token) for token in tokens_esperados]
simplificados_obtidos = [simplificar_token_musical(token) for token in tokens_obtidos]
_, simplified_token_accuracy = acuracia_posicional(simplificados_esperados, simplificados_obtidos)
raizes_esperadas = [raiz_token(token) for token in tokens_esperados]
raizes_obtidas = [raiz_token(token) for token in tokens_obtidos]
_, root_accuracy = acuracia_posicional(raizes_esperadas, raizes_obtidas)
qualidades_esperadas = [qualidade_token(token) for token in tokens_esperados]
qualidades_obtidas = [qualidade_token(token) for token in tokens_obtidos]
_, quality_accuracy = acuracia_posicional(qualidades_esperadas, qualidades_obtidas)
qualidades_completas_esperadas = [qualidade_completa_token(token) for token in tokens_esperados]
qualidades_completas_obtidas = [qualidade_completa_token(token) for token in tokens_obtidos]
_, full_quality_accuracy = acuracia_posicional(
qualidades_completas_esperadas,
qualidades_completas_obtidas,
)
edicao = distancia_edicao_tokens(tokens_esperados, tokens_obtidos)
matriz_sem_acorde = avaliar_sem_acorde(tokens_esperados, tokens_obtidos)
exact_match = tokens_esperados == tokens_obtidos
slash_tolerated_match = simplificados_esperados == simplificados_obtidos
rotated_exact_match, _, exact_shift = melhor_rotacao(tokens_esperados, tokens_obtidos, lambda valor: valor)
rotated_simplified_match, _, simplified_shift = melhor_rotacao(
tokens_esperados,
tokens_obtidos,
simplificar_token_musical,
)
rotated_root_match, _, root_shift = melhor_rotacao(tokens_esperados, tokens_obtidos, raiz_token)
musical_match = exact_match or slash_tolerated_match or rotated_simplified_match
return ResultadoAudio(
filename=caso.caminho.name,
instrument=caso.instrumento,
expected=" ".join(tokens_esperados),
predicted=" ".join(tokens_obtidos),
exact_match=exact_match,
slash_tolerated_match=slash_tolerated_match,
rotated_exact_match=rotated_exact_match,
rotated_simplified_match=rotated_simplified_match,
rotated_root_match=rotated_root_match,
musical_match=musical_match,
token_accuracy=token_accuracy,
simplified_token_accuracy=simplified_token_accuracy,
root_accuracy=root_accuracy,
quality_accuracy=quality_accuracy,
full_quality_accuracy=full_quality_accuracy,
edit_distance=edicao.distancia,
substitutions=edicao.substituicoes,
insertions=edicao.insercoes,
deletions=edicao.remocoes,
token_error_rate=edicao.distancia / max(len(tokens_esperados), 1),
no_chord_true_positives=matriz_sem_acorde.verdadeiros_positivos,
no_chord_false_positives=matriz_sem_acorde.falsos_positivos,
no_chord_false_negatives=matriz_sem_acorde.falsos_negativos,
no_chord_true_negatives=matriz_sem_acorde.verdadeiros_negativos,
best_rotation_shift=exact_shift or simplified_shift or root_shift,
expected_tokens=len(tokens_esperados),
predicted_tokens=len(tokens_obtidos),
matched_tokens=tokens_corretos,
elapsed_seconds=time.perf_counter() - inicio,
diagnostics=diagnostics,
)
def resumir_resultados(resultados: Iterable[ResultadoAudio]) -> dict[str, Any]:
lista = list(resultados)
total = len(lista)
acertos_exatos = sum(1 for item in lista if item.exact_match)
acertos_musicais = sum(1 for item in lista if item.musical_match)
por_instrumento: dict[str, dict[str, Any]] = {}
for item in lista:
resumo = por_instrumento.setdefault(
item.instrument,
{
"cases": 0,
"exact_matches": 0,
"musical_matches": 0,
"mean_token_accuracy": 0.0,
"mean_simplified_accuracy": 0.0,
"mean_root_accuracy": 0.0,
"mean_quality_accuracy": 0.0,
"mean_full_quality_accuracy": 0.0,
"mean_elapsed_seconds": 0.0,
"edit_distance": 0,
"substitutions": 0,
"insertions": 0,
"deletions": 0,
"expected_tokens": 0,
"no_chord_true_positives": 0,
"no_chord_false_positives": 0,
"no_chord_false_negatives": 0,
"no_chord_true_negatives": 0,
},
)
resumo["cases"] += 1
resumo["exact_matches"] += 1 if item.exact_match else 0
resumo["musical_matches"] += 1 if item.musical_match else 0
resumo["mean_token_accuracy"] += item.token_accuracy
resumo["mean_simplified_accuracy"] += item.simplified_token_accuracy
resumo["mean_root_accuracy"] += item.root_accuracy
resumo["mean_quality_accuracy"] += item.quality_accuracy
resumo["mean_full_quality_accuracy"] += item.full_quality_accuracy
resumo["mean_elapsed_seconds"] += item.elapsed_seconds
resumo["edit_distance"] += item.edit_distance
resumo["substitutions"] += item.substitutions
resumo["insertions"] += item.insertions
resumo["deletions"] += item.deletions
resumo["expected_tokens"] += item.expected_tokens
resumo["no_chord_true_positives"] += item.no_chord_true_positives
resumo["no_chord_false_positives"] += item.no_chord_false_positives
resumo["no_chord_false_negatives"] += item.no_chord_false_negatives
resumo["no_chord_true_negatives"] += item.no_chord_true_negatives
for resumo in por_instrumento.values():
quantidade = max(int(resumo["cases"]), 1)
resumo["mean_token_accuracy"] = resumo["mean_token_accuracy"] / quantidade
resumo["mean_simplified_accuracy"] = resumo["mean_simplified_accuracy"] / quantidade
resumo["mean_root_accuracy"] = resumo["mean_root_accuracy"] / quantidade
resumo["mean_quality_accuracy"] = resumo["mean_quality_accuracy"] / quantidade
resumo["mean_full_quality_accuracy"] = resumo["mean_full_quality_accuracy"] / quantidade
resumo["mean_elapsed_seconds"] = resumo["mean_elapsed_seconds"] / quantidade
resumo["token_error_rate"] = resumo["edit_distance"] / max(resumo["expected_tokens"], 1)
resumo["no_chord"] = resumir_matriz_sem_acorde(
MatrizSemAcorde(
verdadeiros_positivos=resumo.pop("no_chord_true_positives"),
falsos_positivos=resumo.pop("no_chord_false_positives"),
falsos_negativos=resumo.pop("no_chord_false_negatives"),
verdadeiros_negativos=resumo.pop("no_chord_true_negatives"),
)
)
matriz_sem_acorde_global = MatrizSemAcorde()
for item in lista:
matriz_sem_acorde_global = matriz_sem_acorde_global.somar(
MatrizSemAcorde(
verdadeiros_positivos=item.no_chord_true_positives,
falsos_positivos=item.no_chord_false_positives,
falsos_negativos=item.no_chord_false_negatives,
verdadeiros_negativos=item.no_chord_true_negatives,
)
)
total_tokens_esperados = sum(item.expected_tokens for item in lista)
distancia_edicao_total = sum(item.edit_distance for item in lista)
return {
"metrics_schema_version": VERSAO_ESQUEMA_METRICAS,
"fixtures_enabled": fixtures_habilitadas(),
"total_cases": total,
"exact_matches": acertos_exatos,
"exact_match_rate": (acertos_exatos / total) if total else 0.0,
"musical_matches": acertos_musicais,
"musical_match_rate": (acertos_musicais / total) if total else 0.0,
"mean_token_accuracy": (sum(item.token_accuracy for item in lista) / total) if total else 0.0,
"mean_simplified_accuracy": (sum(item.simplified_token_accuracy for item in lista) / total) if total else 0.0,
"mean_root_accuracy": (sum(item.root_accuracy for item in lista) / total) if total else 0.0,
"mean_quality_accuracy": (sum(item.quality_accuracy for item in lista) / total) if total else 0.0,
"mean_full_quality_accuracy": (
sum(item.full_quality_accuracy for item in lista) / total
) if total else 0.0,
"token_error_rate": distancia_edicao_total / max(total_tokens_esperados, 1),
"edit_operations": {
"distance": distancia_edicao_total,
"substitutions": sum(item.substitutions for item in lista),
"insertions": sum(item.insertions for item in lista),
"deletions": sum(item.deletions for item in lista),
"expected_tokens": total_tokens_esperados,
},
"no_chord": resumir_matriz_sem_acorde(matriz_sem_acorde_global),
"mean_elapsed_seconds": (sum(item.elapsed_seconds for item in lista) / total) if total else 0.0,
"by_instrument": por_instrumento,
"results": [asdict(item) for item in lista],
}
def resumir_matriz_sem_acorde(matriz: MatrizSemAcorde) -> dict[str, Any]:
return {
"support": matriz.suporte,
"true_positives": matriz.verdadeiros_positivos,
"false_positives": matriz.falsos_positivos,
"false_negatives": matriz.falsos_negativos,
"true_negatives": matriz.verdadeiros_negativos,
"accuracy": matriz.acuracia,
"precision": matriz.precisao,
"recall": matriz.revocacao,
"f1": matriz.f1,
"zero_division": 0,
}
def imprimir_relatorio_humano(resumo: dict[str, Any]) -> None:
modo = "com fixtures de regressao" if resumo["fixtures_enabled"] else "sem fixtures"
print("Relatorio de Assertividade")
print(f"Esquema de metricas: {resumo['metrics_schema_version']}")
print(f"Modo: {modo}")
print(f"Casos: {resumo['total_cases']}")
print(f"Acerto exato: {resumo['exact_matches']}/{resumo['total_cases']} ({resumo['exact_match_rate']:.1%})")
print(f"Acerto musical: {resumo['musical_matches']}/{resumo['total_cases']} ({resumo['musical_match_rate']:.1%})")
print(f"Media por token: {resumo['mean_token_accuracy']:.1%}")
print(f"Media sem baixo/ornamento: {resumo['mean_simplified_accuracy']:.1%}")
print(f"Media de raiz: {resumo['mean_root_accuracy']:.1%}")
print(f"Media de qualidade: {resumo['mean_quality_accuracy']:.1%}")
print(f"Media de qualidade completa: {resumo['mean_full_quality_accuracy']:.1%}")
operacoes = resumo["edit_operations"]
print(
f"Taxa de erro sequencial: {resumo['token_error_rate']:.1%} "
f"(S={operacoes['substitutions']}, I={operacoes['insertions']}, D={operacoes['deletions']})"
)
no_chord = resumo["no_chord"]
print(
f"N/no-chord: suporte={no_chord['support']}, FP={no_chord['false_positives']}, "
f"FN={no_chord['false_negatives']}, precisao={no_chord['precision']:.1%}, "
f"revocacao={no_chord['recall']:.1%}, F1={no_chord['f1']:.1%}"
)
print(f"Tempo medio por caso: {resumo['mean_elapsed_seconds']:.2f}s")
print("")
print("Por instrumento:")
for instrumento, dados in sorted(resumo["by_instrument"].items()):
print(
f"- {instrumento}: {int(dados['exact_matches'])}/{int(dados['cases'])} exatos "
f"({(dados['exact_matches'] / max(dados['cases'], 1)):.1%}), "
f"musical {int(dados['musical_matches'])}/{int(dados['cases'])} "
f"({(dados['musical_matches'] / max(dados['cases'], 1)):.1%}), "
f"raiz {(dados['mean_root_accuracy']):.1%}, "
f"qualidade completa {(dados['mean_full_quality_accuracy']):.1%}, "
f"TER {(dados['token_error_rate']):.1%}"
)
print("")
print("Casos:")
for item in resumo["results"]:
status = "OK" if item["exact_match"] else ("MUSICAL" if item["musical_match"] else "FALHOU")
total_tokens = max(item["expected_tokens"], item["predicted_tokens"], 1)
print(f"- {status} [{item['instrument']}] {item['filename']}")
print(f" esperado: {item['expected']}")
print(f" obtido: {item['predicted']}")
print(f" tokens: {item['matched_tokens']}/{total_tokens} ({item['token_accuracy']:.1%})")
print(
f" musical: sem-baixo {str(item['slash_tolerated_match']).lower()}, "
f"rotacao {str(item['rotated_simplified_match']).lower()}, "
f"raiz {item['root_accuracy']:.1%}, qualidade {item['quality_accuracy']:.1%}, "
f"qualidade completa {item['full_quality_accuracy']:.1%}"
)
print(
f" edicao: TER {item['token_error_rate']:.1%}, "
f"S={item['substitutions']}, I={item['insertions']}, D={item['deletions']}"
)
print(f" tempo: {item['elapsed_seconds']:.2f}s")
if not item["exact_match"] and item.get("diagnostics"):
diagnostico = item["diagnostics"]
print(
f" diag: tom {diagnostico.get('tom') or '-'} {diagnostico.get('modo') or ''} "
f"conf {diagnostico.get('confianca_tom') or '-'}"
)
candidatos = diagnostico.get("candidatas_progressao") or []
if candidatos:
resumo_candidatos = [
" ".join(candidato.get("acordes", [])[:6])
for candidato in candidatos[:2]
]
print(f" cand: {' | '.join(resumo_candidatos)}")
def analisar_casos(casos: list[CasoAudio], verbose: bool = False) -> list[ResultadoAudio]:
resultados: list[ResultadoAudio] = []
total = len(casos)
for indice, caso in enumerate(casos, start=1):
if verbose:
print(f"[{indice}/{total}] analisando [{caso.instrumento}] {caso.caminho.name}...", flush=True)
resultado = analisar_caso(caso)
resultados.append(resultado)
if verbose:
status = "OK" if resultado.exact_match else ("MUSICAL" if resultado.musical_match else "FALHOU")
print(
f"[{indice}/{total}] {status} em {resultado.elapsed_seconds:.2f}s | "
f"{resultado.filename} | token {resultado.token_accuracy:.1%}",
flush=True,
)
return resultados
def filtrar_casos(casos: list[CasoAudio], args: argparse.Namespace) -> list[CasoAudio]:
filtrados = list(casos)
if args.instrument:
instrumentos = {valor.strip().lower() for valor in args.instrument if valor.strip()}
filtrados = [caso for caso in filtrados if caso.instrumento.lower() in instrumentos]
if args.case:
termos = [valor.strip().casefold() for valor in args.case if valor.strip()]
filtrados = [
caso
for caso in filtrados
if any(termo in caso.nome_arquivo.casefold() for termo in termos)
]
if args.limit > 0:
filtrados = filtrados[: args.limit]
return filtrados
def fixtures_habilitadas() -> bool:
valor = str(os.getenv("AUDIO_REGRESSION_FIXTURES", "0")).strip().lower()
return valor in {"1", "true", "on", "yes"}
def main() -> int:
args = ler_argumentos()
if args.no_fixtures:
os.environ["AUDIO_REGRESSION_FIXTURES"] = "0"
casos = filtrar_casos(carregar_casos(args.spec), args)
if not casos:
raise RuntimeError(f"Nenhum caso de audio valido encontrado em {args.spec}")
if args.validate_corpus:
print(f"Corpus valido: {len(casos)} casos em {args.spec}")
return 0
resultados = analisar_casos(casos, verbose=args.verbose)
resumo = resumir_resultados(resultados)
if args.json:
print(json.dumps(resumo, ensure_ascii=False, indent=2))
else:
imprimir_relatorio_humano(resumo)
return 0
if __name__ == "__main__":
raise SystemExit(main())
|