Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import re | |
| from dataclasses import dataclass | |
| from typing import Callable | |
| VERSAO_ESQUEMA_METRICAS = "music-token-metrics-v2" | |
| def gerar_variantes_mojibake(texto: str, rodadas: int = 3) -> set[str]: | |
| variantes: set[str] = set() | |
| atual = texto | |
| for _ in range(rodadas): | |
| try: | |
| atual = atual.encode("utf-8").decode("cp1252") | |
| except UnicodeError: | |
| break | |
| variantes.add(atual) | |
| return variantes | |
| APELIDOS_DE_NOTAS = { | |
| "DO": "C", | |
| "DO#": "C#", | |
| "REB": "C#", | |
| "RE": "D", | |
| "RE#": "D#", | |
| "MIB": "D#", | |
| "MI": "E", | |
| "FA": "F", | |
| "FA#": "F#", | |
| "SOLB": "F#", | |
| "SOL": "G", | |
| "SOL#": "G#", | |
| "LAB": "G#", | |
| "LA": "A", | |
| "LA#": "A#", | |
| "SIB": "A#", | |
| "SI": "B", | |
| } | |
| NOTAS_POR_PC = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] | |
| PC_POR_NOTA = { | |
| "C": 0, | |
| "C#": 1, | |
| "DB": 1, | |
| "D": 2, | |
| "D#": 3, | |
| "EB": 3, | |
| "E": 4, | |
| "F": 5, | |
| "F#": 6, | |
| "GB": 6, | |
| "G": 7, | |
| "G#": 8, | |
| "AB": 8, | |
| "A": 9, | |
| "A#": 10, | |
| "BB": 10, | |
| "B": 11, | |
| "CB": 11, | |
| } | |
| SUBSTITUICOES_TOKEN = { | |
| **{valor: "#" for valor in ("\u266f", *gerar_variantes_mojibake("\u266f"))}, | |
| **{valor: "B" for valor in ("\uff22", *gerar_variantes_mojibake("\uff22"))}, | |
| } | |
| TOKENS_SEM_ACORDE = { | |
| "N", | |
| "NC", | |
| "NONE", | |
| "NOCHORD", | |
| "SEMACORDE", | |
| "SILENCE", | |
| "SILENCIO", | |
| } | |
| class MatrizSemAcorde: | |
| verdadeiros_positivos: int = 0 | |
| falsos_positivos: int = 0 | |
| falsos_negativos: int = 0 | |
| verdadeiros_negativos: int = 0 | |
| def total(self) -> int: | |
| return ( | |
| self.verdadeiros_positivos | |
| + self.falsos_positivos | |
| + self.falsos_negativos | |
| + self.verdadeiros_negativos | |
| ) | |
| def suporte(self) -> int: | |
| return self.verdadeiros_positivos + self.falsos_negativos | |
| def acuracia(self) -> float: | |
| return ( | |
| (self.verdadeiros_positivos + self.verdadeiros_negativos) / self.total | |
| if self.total | |
| else 1.0 | |
| ) | |
| def precisao(self) -> float: | |
| denominador = self.verdadeiros_positivos + self.falsos_positivos | |
| return self.verdadeiros_positivos / denominador if denominador else 0.0 | |
| def revocacao(self) -> float: | |
| return self.verdadeiros_positivos / self.suporte if self.suporte else 0.0 | |
| def f1(self) -> float: | |
| denominador = self.precisao + self.revocacao | |
| return (2.0 * self.precisao * self.revocacao / denominador) if denominador else 0.0 | |
| def somar(self, outra: "MatrizSemAcorde") -> "MatrizSemAcorde": | |
| return MatrizSemAcorde( | |
| verdadeiros_positivos=self.verdadeiros_positivos + outra.verdadeiros_positivos, | |
| falsos_positivos=self.falsos_positivos + outra.falsos_positivos, | |
| falsos_negativos=self.falsos_negativos + outra.falsos_negativos, | |
| verdadeiros_negativos=self.verdadeiros_negativos + outra.verdadeiros_negativos, | |
| ) | |
| class DistanciaEdicao: | |
| distancia: int = 0 | |
| substituicoes: int = 0 | |
| insercoes: int = 0 | |
| remocoes: int = 0 | |
| def tokenizar(valor: str) -> list[str]: | |
| tokens_brutos = [token.strip() for token in re.split(r"\s+", valor.strip()) if token.strip()] | |
| return [normalizar_token(token) for token in tokens_brutos] | |
| def normalizar_token(token: str) -> str: | |
| texto = token.strip() | |
| for origem, destino in SUBSTITUICOES_TOKEN.items(): | |
| texto = texto.replace(origem, destino) | |
| if "/" in texto: | |
| principal, baixo = texto.split("/", 1) | |
| principal_norm = normalizar_parte_token(principal) | |
| baixo_norm = normalizar_parte_token(baixo) | |
| return f"{principal_norm}/{baixo_norm}" if principal_norm and baixo_norm else principal_norm | |
| return normalizar_parte_token(texto) | |
| def normalizar_parte_token(token: str) -> str: | |
| texto = token.strip().replace("♯", "#").replace("♭", "b") | |
| texto = re.sub(r"^([A-Ga-g](?:#|b)?)M(?=\d)", r"\1maj", texto) | |
| texto = re.sub(r"^([A-Ga-g](?:#|b)?)[-−](?=\d|$)", r"\1m", texto) | |
| texto = re.sub(r"^([A-Ga-g](?:#|b)?)\+(?=\d|$)", r"\1aug", texto) | |
| texto = texto.replace("Δ", "maj") | |
| texto = re.sub(r"[øØ](?:7)?", "m7b5", texto) | |
| texto = re.sub(r"[°º](7)?", lambda match: "dim7" if match.group(1) else "dim", texto) | |
| limpo = re.sub(r"[^A-Za-z0-9#bB]+", "", texto) | |
| maiusculo = limpo.upper() | |
| if maiusculo in TOKENS_SEM_ACORDE: | |
| return "N" | |
| return APELIDOS_DE_NOTAS.get(maiusculo, maiusculo if maiusculo else token.strip()) | |
| def remover_baixo_alternativo(token: str) -> str: | |
| return str(token or "").split("/", 1)[0].strip() | |
| def raiz_token(token: str) -> str: | |
| principal = remover_baixo_alternativo(normalizar_token(token)) | |
| if eh_sem_acorde(principal): | |
| return "" | |
| match = re.match(r"^([A-G])(#|B)?", principal.upper()) | |
| if not match: | |
| return "" | |
| nota = f"{match.group(1)}{match.group(2) or ''}" | |
| pc = PC_POR_NOTA.get(nota) | |
| return NOTAS_POR_PC[pc] if pc is not None else nota | |
| def sufixo_token(token: str) -> str: | |
| principal = remover_baixo_alternativo(normalizar_token(token)).upper() | |
| match = re.match(r"^[A-G](?:#|B)?(.*)$", principal) | |
| return match.group(1) if match else "" | |
| def qualidade_token(token: str) -> str: | |
| if eh_sem_acorde(token): | |
| return "no_chord" | |
| if not raiz_token(token): | |
| return "unknown" | |
| sufixo = sufixo_token(token) | |
| if sufixo.startswith("SUS"): | |
| return "sus" | |
| if sufixo.startswith("DIM"): | |
| return "dim" | |
| if sufixo.startswith("AUG"): | |
| return "aug" | |
| if sufixo.startswith("M") and not sufixo.startswith("MAJ"): | |
| return "minor" | |
| return "major" | |
| def qualidade_completa_token(token: str) -> str: | |
| """Retorna a qualidade sem colapsar extensoes harmonicamente distintas.""" | |
| if eh_sem_acorde(token): | |
| return "no_chord" | |
| if not raiz_token(token): | |
| return "unknown" | |
| sufixo = sufixo_token(token) | |
| aliases = { | |
| "": "major", | |
| "MAJ": "major", | |
| "M": "minor", | |
| "MIN": "minor", | |
| "-": "minor", | |
| "7": "dominant7", | |
| "MAJ7": "major7", | |
| "MAJOR7": "major7", | |
| "7M": "major7", | |
| "M7": "minor7", | |
| "MIN7": "minor7", | |
| "6": "major6", | |
| "MAJ6": "major6", | |
| "M6": "minor6", | |
| "MIN6": "minor6", | |
| "9": "dominant9", | |
| "MAJ9": "major9", | |
| "M9": "minor9", | |
| "MIN9": "minor9", | |
| "ADD9": "add9", | |
| "SUS": "sus4", | |
| "SUS2": "sus2", | |
| "SUS4": "sus4", | |
| "DIM": "diminished", | |
| "DIM7": "diminished7", | |
| "M7B5": "half_diminished7", | |
| "MIN7B5": "half_diminished7", | |
| "AUG": "augmented", | |
| "+": "augmented", | |
| "5": "power5", | |
| } | |
| return aliases.get(sufixo, f"other:{sufixo}" if sufixo else "major") | |
| def eh_sem_acorde(token: str) -> bool: | |
| principal = remover_baixo_alternativo(str(token or "")) | |
| limpo = re.sub(r"[^A-Za-z0-9]+", "", principal).upper() | |
| return limpo in TOKENS_SEM_ACORDE | |
| def simplificar_token_musical(token: str) -> str: | |
| if eh_sem_acorde(token): | |
| return "N" | |
| raiz = raiz_token(token) | |
| if not raiz: | |
| return "" | |
| sufixo = { | |
| "minor": "M", | |
| "sus": "SUS", | |
| "dim": "DIM", | |
| "aug": "AUG", | |
| "major": "", | |
| }.get(qualidade_token(token), "") | |
| return f"{raiz}{sufixo}" | |
| def acuracia_posicional(esperado: list[str], obtido: list[str]) -> tuple[int, float]: | |
| acertos = sum(1 for left, right in zip(esperado, obtido) if left == right) | |
| total = max(len(esperado), len(obtido), 1) | |
| return acertos, acertos / total | |
| def avaliar_sem_acorde(esperado: list[str], obtido: list[str]) -> MatrizSemAcorde: | |
| """Calcula a matriz binaria de N, contando ausencias como erros de segmentacao.""" | |
| verdadeiros_positivos = 0 | |
| falsos_positivos = 0 | |
| falsos_negativos = 0 | |
| verdadeiros_negativos = 0 | |
| total = max(len(esperado), len(obtido)) | |
| for indice in range(total): | |
| esperado_n = indice < len(esperado) and eh_sem_acorde(esperado[indice]) | |
| obtido_n = indice < len(obtido) and eh_sem_acorde(obtido[indice]) | |
| if esperado_n and obtido_n: | |
| verdadeiros_positivos += 1 | |
| elif obtido_n: | |
| falsos_positivos += 1 | |
| elif esperado_n: | |
| falsos_negativos += 1 | |
| else: | |
| verdadeiros_negativos += 1 | |
| return MatrizSemAcorde( | |
| verdadeiros_positivos=verdadeiros_positivos, | |
| falsos_positivos=falsos_positivos, | |
| falsos_negativos=falsos_negativos, | |
| verdadeiros_negativos=verdadeiros_negativos, | |
| ) | |
| def distancia_edicao_tokens( | |
| esperado: list[str], | |
| obtido: list[str], | |
| transformador: Callable[[str], str] = lambda valor: valor, | |
| ) -> DistanciaEdicao: | |
| """Levenshtein deterministico com decomposicao em substituicoes/insercoes/remocoes.""" | |
| esperado_norm = [transformador(token) for token in esperado] | |
| obtido_norm = [transformador(token) for token in obtido] | |
| linhas = len(esperado_norm) + 1 | |
| colunas = len(obtido_norm) + 1 | |
| tabela: list[list[DistanciaEdicao]] = [ | |
| [DistanciaEdicao() for _ in range(colunas)] for _ in range(linhas) | |
| ] | |
| for indice in range(1, linhas): | |
| tabela[indice][0] = DistanciaEdicao( | |
| distancia=indice, | |
| remocoes=indice, | |
| ) | |
| for indice in range(1, colunas): | |
| tabela[0][indice] = DistanciaEdicao( | |
| distancia=indice, | |
| insercoes=indice, | |
| ) | |
| for linha in range(1, linhas): | |
| for coluna in range(1, colunas): | |
| anterior = tabela[linha - 1][coluna - 1] | |
| if esperado_norm[linha - 1] == obtido_norm[coluna - 1]: | |
| tabela[linha][coluna] = anterior | |
| continue | |
| candidatos = ( | |
| DistanciaEdicao( | |
| distancia=anterior.distancia + 1, | |
| substituicoes=anterior.substituicoes + 1, | |
| insercoes=anterior.insercoes, | |
| remocoes=anterior.remocoes, | |
| ), | |
| DistanciaEdicao( | |
| distancia=tabela[linha][coluna - 1].distancia + 1, | |
| substituicoes=tabela[linha][coluna - 1].substituicoes, | |
| insercoes=tabela[linha][coluna - 1].insercoes + 1, | |
| remocoes=tabela[linha][coluna - 1].remocoes, | |
| ), | |
| DistanciaEdicao( | |
| distancia=tabela[linha - 1][coluna].distancia + 1, | |
| substituicoes=tabela[linha - 1][coluna].substituicoes, | |
| insercoes=tabela[linha - 1][coluna].insercoes, | |
| remocoes=tabela[linha - 1][coluna].remocoes + 1, | |
| ), | |
| ) | |
| tabela[linha][coluna] = min( | |
| candidatos, | |
| key=lambda item: ( | |
| item.distancia, | |
| item.insercoes + item.remocoes, | |
| item.substituicoes, | |
| item.insercoes, | |
| ), | |
| ) | |
| return tabela[-1][-1] | |
| def melhor_rotacao( | |
| esperado: list[str], | |
| obtido: list[str], | |
| transformador: Callable[[str], str], | |
| ) -> tuple[bool, float, int]: | |
| if not esperado or len(esperado) != len(obtido): | |
| return False, 0.0, 0 | |
| esperado_norm = [transformador(token) for token in esperado] | |
| melhor_acuracia = 0.0 | |
| melhor_shift = 0 | |
| for shift in range(len(obtido)): | |
| rotacionado = obtido[shift:] + obtido[:shift] | |
| obtido_norm = [transformador(token) for token in rotacionado] | |
| _, acuracia = acuracia_posicional(esperado_norm, obtido_norm) | |
| if acuracia > melhor_acuracia: | |
| melhor_acuracia = acuracia | |
| melhor_shift = shift | |
| return melhor_acuracia >= 1.0, melhor_acuracia, melhor_shift | |