# Parametres Papermill - JAMAIS modifier ce commentaire
# Configuration notebook
notebook_mode = "batch" # "batch" | "interactive"
seed = 42 # graine des tirages du modele (determinisme des sorties)
model_id = "m-a-p/SheetSage2" # poids + code custom (trust_remote_code)
sampling_rate = 24000 # frequence attendue par le modele (carte)
silence_final_s = 3.0 # silence ajoute en fin de clip (voir Section 6 : contrainte de grille)
temps_bpm = 120 # tempo du MIDI source (noire = 120 : tempo que le modele emet lui-meme)
output_dir = "output/sheetsage2"
skip_widgets = True
revision_modele = "eab522a8168e8b8b8c4856bf8609cd86198f01fe" # revision de la run de reference du registre (#15934) : sans pin, snapshot_download suit la tete courante du depotDe l’Audio a la Partition : SheetSage2
| Module | 04-Applications — Transcription audio vers partition editable |
| Modele | SheetSage2 (677 M parametres, base MERT-v2-FullSong + adaptateurs) |
| Kernel | Python 3 (sheetsage2-gpu) |
| GPU | 8 GB VRAM suffisent (mesure : 2,5 GB de poids, pic 3,3 GB) — repli CPU documente par la carte |
Licence des poids : CC BY-NC 4.0. Les poids SheetSage2 sont diffuses sous licence Creative Commons Attribution-NonCommercial 4.0 : l’usage commercial est interdit. Le code d’inference releve des avis tiers du depot (
THIRD_PARTY_NOTICES.md), qui embarque notamment des echantillons de piano (soundfonts) sous leurs propres licences. Cette contrainte s’applique a tout artefact produit ici, partitions comme audio.
Objectifs pedagogiques :
- Situer la transcription (audio -> symbole) par rapport a la comprehension (MERT2, 04-15) et a la generation (YuE2, 02-7) : les trois satellites de la famille YuE2.
- Comprendre l’architecture SheetSage2 : un backbone MERT-v2-FullSong pre-entraine, des adaptateurs fusionnes au chargement, et cinq tetes de prediction (melodie, accords, temps forts, tonalite, structure).
- Executer une transcription reelle et lire ses artefacts : partition ABC, MIDI, evenements horodates, annotations.
- Comparer la partition transcrite a une verite-terrain connue par construction, et expliquer chaque ecart plutot que de le masquer.
- Boucler le cycle symbolique -> audio -> symbolique : editer la partition a la main, resynthetiser, et mesurer l’ecart audio produit par l’edition.
Les parametres Papermill configurent le modele (model_id), la graine (seed, qui fixe les sorties du modele — les memes entrees donnent la meme partition) et le repertoire d’artefacts (output_dir, non commite : les sorties sont regenerables par re-execution).
silence_final_s n’est pas cosmetique : la reconstruction ABC exige que chaque intervalle decode tienne sur la grille de sous-temps. Un clip qui se termine exactement sur sa derniere note produit un intervalle terminal plus court qu’un sous-temps et la reconstruction echoue (mesure en Section 6). Le silence final donne au decodeur la marge necessaire.
# Setup environnement et imports
import os
import re
import sys
import time
import json
from io import BytesIO
from pathlib import Path
os.environ.setdefault("HF_HUB_DISABLE_SYMLINKS_WARNING", "1")
import platform
print("IMPORTS ET ENVIRONNEMENT")
print("=" * 45)
print(f"Python {sys.version.split()[0]}")
print(f"Plateforme {platform.system()} {platform.release()}")
Path(output_dir).mkdir(parents=True, exist_ok=True)
print(f"Sorties {output_dir}/ pret")IMPORTS ET ENVIRONNEMENT
=============================================
Python 3.13.7
Plateforme Windows 11
Sorties output/sheetsage2/ pret
Interpretation : environnement
Le notebook s’execute dans un kernel dedie (sheetsage2-gpu) cree aux pins exacts de la carte du modele. Ce n’est pas une commodite : la famille YuE2 arrive avec des dependances qui entrent en conflit avec les environnements existants, et un modele a trust_remote_code importe du code ecrit contre sa version de transformers. Un kernel partage produirait des erreurs diffuses (accents d’API) au lieu d’un echec net.
# Verification du socle technique : torch, CUDA, VRAM, kernel
print("SOCLE TECHNIQUE")
print("=" * 45)
device = "cpu"
vram_gb = 0.0
torch_dispo = False
try:
import torch
torch_dispo = True
print(f"torch {torch.__version__}")
if torch.cuda.is_available():
device = "cuda"
props = torch.cuda.get_device_properties(0)
vram_gb = props.total_memory / 1024**3
print(f"GPU {props.name} — {vram_gb:.1f} GB VRAM")
else:
print("GPU absent (repli CPU, documente par la carte du modele)")
except ImportError:
print("torch ABSENT de ce kernel")
print()
print("DEVICE RETENU")
print("-" * 45)
print(f"device {device}")
if device == "cuda":
print("La carte du modele documente un repli CPU ; la mesure montre que 8 GB de")
print("VRAM suffisent largement (poids ~2,5 GB), donc l'execution GPU est retenue.")
else:
print("Execution CPU : la carte du modele documente ce repli explicitement.")
print("Le temps de transcription est plus long, le resultat est le meme.")SOCLE TECHNIQUE
=============================================
torch 2.8.0+cu126
GPU NVIDIA GeForce RTX 3070 Laptop GPU — 8.0 GB VRAM
DEVICE RETENU
---------------------------------------------
device cuda
La carte du modele documente un repli CPU ; la mesure montre que 8 GB de
VRAM suffisent largement (poids ~2,5 GB), donc l'execution GPU est retenue.
Interpretation : device
La carte du modele documente un repli CPU (torch.cuda.is_available()), ce qui la distingue des autres modeles de la famille : SheetSage2 n’exige ni Linux ni 24 GB de VRAM. La mesure le confirme — les poids tiennent en 2,5 GB, et le pic pendant une transcription atteint 3,3 GB. Sur une machine 8 GB, il reste donc de la marge, et la transcription est retenue en GPU.
Section 1 : Installation et prerequis
Licence des poids — CC BY-NC 4.0. Les poids de
m-a-p/SheetSage2(construits sur le backbonem-a-p/MERT-v2-FullSong) sont distribues sous Creative Commons Attribution - Pas d’Utilisation Commerciale 4.0. Leur usage est donc reserve a des fins non commerciales, et toute redistribution doit conserver l’attribution. Ce notebook en fait un usage pedagogique, que la licence couvre ; un usage commercial des poids ne le serait pas. La contrainte est rappelee en en-tete, ici, et dans les statistiques de session.
L’installation canonique (carte du modele) :
python -m pip install huggingface-hub==0.36.0
huggingface-cli download m-a-p/SheetSage2 --local-dir SheetSage2
cd SheetSage2
python -m pip install torch==2.8.0 torchaudio==2.8.0 --index-url https://download.pytorch.org/whl/cu126
python -m pip install -r requirements.txtLe kernel sheetsage2-gpu de ce notebook est construit sur ces pins. Trois ecarts sont assumes et documentes (la carte vise Python 3.10/3.11) :
| Paquet | Pin de la carte | Version installee | Pourquoi |
|---|---|---|---|
| Python | 3.10 ou 3.11 | 3.13 | interpreter stable de la machine ; aucune wheel cp313 n’existe pour les pins numpy/scipy ci-dessous, mais elles existent pour torch 2.8.0 |
| numpy | 1.24.3 | 2.5.3 | 1.24.3 n’a pas de wheel cp313 ; la serie 2.x est la premiere a supporter Python 3.13 |
| scipy | 1.13.1 | 1.18.1 | meme raison ; scipy porte les fonctions de re-echantillonnage utilisees ici |
Les pins critiques de la carte sont respectes a l’identique : torch==2.8.0, torchaudio==2.8.0, transformers==4.45.2, huggingface-hub==0.36.0. Le rendu audio (Section 5) ajoute playwright==1.58.0 — l’installation officielle du depot passe par python setup_render.py.
Piege verifie. Le paquet PyPI nomme
pyabcn’est pas un analyseur de notation ABC : c’est Approximate Bayesian Computation (statistique bayesienne). La collision de noms est reelle et couteuse a diagnostiquer. La lecture d’ABC de ce notebook se fait donc avec l’analyseur documente de la Section 4, dont la grammaire est explicite.
Section 2 : Positionner SheetSage2 dans la famille
La famille YuE2 couvre trois directions du meme axe texte / audio / symbole :
| Notebook | Sens | Role |
|---|---|---|
| 04-15 MERT2 | audio -> vecteur | comprendre : embeddings, retrieval, sondes |
| 04-16 SheetSage2 (ce notebook) | audio -> symbole | ecrire : partition ABC, MIDI, accords, structure |
| 02-7 (YuE2) | symbole + texte -> audio | generer : chanson complete a partir de paroles |
C’est la troisieme direction qui donne son interet a la deuxieme : une partition est un artefact editable, sur lequel un humain (ou un agent) itere avant de payer le cout d’une resynthese. La transcription n’est donc pas un exercice d’ecole : elle alimente la boucle de generation (Section 7).
Architecture
SheetSage2 n’est pas un modele entraine depuis zero : c’est un backbone MERT-v2-FullSong (le meme modele de representation que le notebook 04-15) auquel sont accolees des tetes de decodage, et des adaptateurs fusionnes automatiquement au chargement (base_model_relation: adapter).
flowchart LR
A["audio (24 kHz mono)"] --> M["MERT-v2-FullSong
backbone pre-entraine 632 M"]
M --> AD["adaptateurs
(fusionnes au chargement)"]
AD --> T["tetes de decodage
melodie · accords · temps · tonalite · structure"]
T --> E["evenements horodates
(tokens + valeurs)"]
E --> AB["score.abc
partition multi-voix editable"]
E --> MI["transcription.mid
+ parties separees"]
E --> LA["events.json / *.lab
annotations temporelles"]
Le point remarquable est le contrat de sortie : le modele ne rend pas seulement des tokens, il reconstruit une notation musicale (grille de sous-temps, armure, chiffrage d’accords, mesures). C’est cette reconstruction qui peut echouer sur des cas limites — voir Section 6, ou l’echec est mesure et explique.
Section 3 : Une entree audio deterministe
MyIA.AI.Notebooks/GenAI/Audio/assets/ ne contient aucun echantillon audio : le depot ne versionne pas d’extrait musical. Plutot que de dependre d’un fichier absent (et de rendre le notebook non reproductible), l’entree est construite :
- une partition ABC ecrite a la main (air en La mineur, herite du notebook 02-7 pour que la boucle YuE2 de la Section 7 porte sur le meme materiau) ;
- cette ABC est analysee par l’analyseur documente ci-dessous -> hauteurs + rythme ;
- ces notes sont ecrites en MIDI (
pretty_midi) ; - ce MIDI est rendu en audio par le moteur du depot lui-meme, par son CLI
render.py: un piano reel (soundfonts du depot), pas une synthese maison.
La verite-terrain est donc connue par construction : c’est l’ABC source. La comparaison de la Section 6 est une comparaison mesuree, pas une impression d’ecoute.
L’analyseur ABC : une grammaire explicite
L’ABC produit par le modele emploie un sous-ensemble restreint et stable que l’analyseur ci-dessous couvre, et qu’il documente :
| Element | Forme | Sens |
|---|---|---|
| Hauteur | A..G / a..g |
octave 0 = C..B (So central = C, soit MIDI 60) ; minuscule = +1 octave ; , = -1 ; ' = +1 |
| Alteration | ^ _ = |
diese, bemol, becarre |
| Duree | chiffre apres la hauteur | multiple de L: (ici 1/16) ; absent = L: |
| Silence | z |
meme regle de duree |
| Mesure | \| |
barre de mesure |
| Accord | "Am" |
symbole d’accord, porte par la voix d’accueil |
L’octave suit la convention du depot, verifiee dans son propre code (notation_sheetsage2.note_to_abc : octave = (note - 60) // 12, minuscule si octave > 0) — c’est ce qui rend la comparaison de la Section 6 non ambigue.
# Analyseur ABC du sous-ensemble restreint (grammaire documentee ci-dessus)
LETTRES = "CDEFGAB"
_DEGRE = {c: i for i, c in enumerate(LETTRES)}
def hauteur_vers_midi(lettre, alteration, marques):
"""Lettre ABC + alteration + marques d'octave -> numero MIDI.
Convention du depot (notation_sheetsage2.note_to_abc) : octave 0 = C4..B4
(Do central = MIDI 60), minuscule = +1 octave, ',' = -1, "'" = +1.
"""
base = [60, 62, 64, 65, 67, 69, 71][_DEGRE[lettre.upper()]]
if lettre.islower():
base += 12
base += 12 * marques.count("'")
base -= 12 * marques.count(",")
return base + {"": 0, "^": 1, "_": -1, "=": 0}[alteration]
def analyser_abc_melodie(abc, voix="Ins"):
"""Extrait une voix melodique d'une partition ABC.
Les durees ABC sont des multiples de l'unite `L:` (fraction de ronde) ; elles
sont converties en SECONDES avec le tempo de l'en-tete `Q:` de la partition
analysee (donc le tempo propre du modele pour une partition transcrite).
Retourne une liste de (pitch_midi, debut_s, duree_s) sur une grille continue :
les silences avancent le temps sans produire de note.
"""
lignes = abc.splitlines()
morceaux, actif, longueur, noire_s = [], False, 0.0625, 0.5
for ligne in lignes:
if ligne.startswith("L:"):
num, den = ligne[2:].split("/")
longueur = float(num) / float(den)
continue
if ligne.startswith("Q:"):
m = re.search(r"1/(\d+)\s*=\s*([\d.]+)", ligne)
if m: # Q:1/N=B -> B temps de 1/N par minute
noire_s = 60.0 / (float(m.group(2)) * int(m.group(1)) / 4.0)
continue
if ligne.startswith("V:"):
actif = ligne[2:].strip().split()[0] == voix
continue
if actif and not ligne.startswith(("%", "X:", "T:", "M:", "K:")):
morceaux.append(ligne)
texte = " ".join(morceaux)
motif = re.compile(r"(\^|_|=)?([A-Ga-gz])(,+|'+)?(\d*)")
notes, temps = [], 0.0
for alt, lettre, marques, chiffre in motif.findall(texte):
duree = longueur * (int(chiffre) if chiffre else 1) * 4.0 * noire_s
if lettre.lower() != "z":
notes.append((hauteur_vers_midi(lettre, alt, marques or ""), temps, duree))
temps += duree
return notes
def nom_de_pitch(pitch):
"""Numero MIDI -> nom scientifique (A4 = 69 = 440 Hz)."""
noms = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
return f"{noms[pitch % 12]}{pitch // 12 - 1}"
def accords_de(abc):
"""Symboles d'accords d'une partition ABC : les chaines entre guillemets
portees par les lignes de musique.
Les en-tetes (X:, T:, M:, L:, Q:, K:, V:, %) sont exclus : les declarations
de voix portent elles aussi des guillemets (`name="Vocal Melody"`) qui ne
sont pas des accords, et les compter fausserait le releve.
"""
en_tete = ("X:", "T:", "M:", "L:", "Q:", "K:", "V:", "%")
accords = []
for ligne in abc.splitlines():
if not ligne.strip() or ligne.startswith(en_tete):
continue
accords.extend(re.findall(r'"([^"]*)"', ligne))
return accords
# Partition source : air en La mineur (meme materiau que le notebook 02-7)
abc_source = f"""X:1
T:Air en La mineur
M:4/4
L:1/16
Q:1/4={temps_bpm}
K:Am
V: Ins clef=treble name="Ins Melody" snm="Inst."
V: Ins
A8c8|e8a8|G8e8|c8A8|"""
print("PARTITION SOURCE (verite-terrain, ecrite a la main)")
print("=" * 45)
print(abc_source)
notes_source = analyser_abc_melodie(abc_source, voix="Ins")
print(f"notes analysees : {len(notes_source)}")
for pitch, debut, duree in notes_source:
print(f" {nom_de_pitch(pitch):>4s} (MIDI {pitch:>3d}) debut {debut:5.2f}s duree {duree:4.2f}s")PARTITION SOURCE (verite-terrain, ecrite a la main)
=============================================
X:1
T:Air en La mineur
M:4/4
L:1/16
Q:1/4=120
K:Am
V: Ins clef=treble name="Ins Melody" snm="Inst."
V: Ins
A8c8|e8a8|G8e8|c8A8|
notes analysees : 8
A4 (MIDI 69) debut 0.00s duree 1.00s
C5 (MIDI 72) debut 1.00s duree 1.00s
E5 (MIDI 76) debut 2.00s duree 1.00s
A5 (MIDI 81) debut 3.00s duree 1.00s
G4 (MIDI 67) debut 4.00s duree 1.00s
E5 (MIDI 76) debut 5.00s duree 1.00s
C5 (MIDI 72) debut 6.00s duree 1.00s
A4 (MIDI 69) debut 7.00s duree 1.00s
Interpretation : la partition comme source de verite
L’ABC est pris comme source unique : l’audio est rendu a partir de lui, et la comparaison de la Section 6 se fait contre lui. C’est ce qui rend la mesure interpretable — on sait exactement ce qui a ete joue, donc un ecart peut etre qualifie (uniforme ? isole ? par classe de hauteur ?) au lieu d’etre seulement constate.
Cette autoverification est le point a retenir : dans un pipeline audio -> symbole, une verite-terrain se construit de facon a etre verifiable ; elle ne se choisit pas pour arranger le resultat. La Section 6 trouve un ecart de +12 demi-tons sur toutes les notes : c’est precisement parce que la source est explicite que cet ecart se lit comme un biais systematique, et non comme du bruit qu’on hesiterait a nommer.
Section 4 : Rendu audio par le moteur du depot
Le depot embarque son propre moteur de rendu (render.py, rendering_sheetsage2.py) : un piano echantillonne (soundfonts acoustic_grand_piano-mp3) joue par abcjs dans un navigateur sans interface (Chromium via Playwright). Trois points verifies firsthand :
- le rendu produit un WAV 44,1 kHz stereo en ~2 s pour 8,03 s de musique (le temps exact est imprime par la cellule de la Section 4 : il varie de quelques dixiemes d’un passage a l’autre) ;
- le rendu audio exige un fichier MIDI — une partition ABC seule ne suffit pas (
Audio rendering needs an existing MIDI file). Autrement dit, editer l’ABC ne change pas l’audio rendu : il faut re-deriver un MIDI. C’est exactement ce que fait la Section 7, et c’est la raison d’etre de l’analyseur de la Section 3 ; - le moteur ouvre le navigateur par l’API synchrone de Playwright, qui refuse de demarrer dans la boucle asyncio d’un kernel Jupyter. Le depot expose pourtant
render.py, son interface en ligne de commande : la cellule ci-dessous l’invoque donc dans un sous-processus, ou aucune boucle asynchrone ne tourne. Le moteur de rendu lui-meme est inchange — c’est son point d’entree documente qui est utilise.
Si le moteur de rendu est indisponible, le notebook le dit et bascule sur une synthese additive documentee — jamais un silence deguise en resultat. Ce repli est un filet declare, pas la voie nominale : les statistiques de session indiquent lequel des deux a produit l’audio.
# Notes -> MIDI -> WAV par le CLI du moteur du depot (avec repli documente)
import subprocess
import numpy as np
import pretty_midi
import soundfile as sf
from huggingface_hub import snapshot_download
def notes_vers_midi(notes, tempo=90):
m = pretty_midi.PrettyMIDI(initial_tempo=tempo)
instrument = pretty_midi.Instrument(program=0)
for pitch, debut, duree in notes:
instrument.notes.append(
pretty_midi.Note(velocity=90, pitch=int(pitch), start=debut, end=debut + duree)
)
m.instruments.append(instrument)
return m
def midi_vers_bytes(m):
"""Miroir du helper du depot (midi_sheetsage2.midi_bytes, package-relatif)."""
flux = BytesIO()
m.write(flux)
return flux.getvalue()
# Revision PINNEE : la mesure de registre (#15934, GPU-dependante) n'a de sens que
# rapportee a une revision de poids fixee — la tete du depot HF peut bouger.
dossier_modele = snapshot_download(model_id, revision=revision_modele)
cli_rendu = Path(dossier_modele) / "render.py"
RENDU_DEPOT = cli_rendu.is_file()
raison_repli = "" if RENDU_DEPOT else f"render.py absent du snapshot {model_id}"
# On n'imprime que le NOM du CLI et l'identifiant du snapshot : le chemin absolu
# du cache HF contient le nom d'utilisateur de la machine, et une sortie de cellule
# ne doit pas dependre de la machine qui l'a produite (regle secrets/H.1).
print(f"CLI de rendu du depot : {cli_rendu.name if RENDU_DEPOT else 'ABSENT'} "
f"(snapshot {Path(dossier_modele).name[:12]})")
def rendre_par_cli(midi_bytes_, etiquette):
"""Rend un WAV piano via le CLI du depot, dans un SOUS-PROCESSUS.
Le moteur du depot ouvre le navigateur par l'API **synchrone** de Playwright
(`sync_playwright`), qui refuse de demarrer dans la boucle asyncio d'un
kernel Jupyter (« Playwright Sync API inside the asyncio loop »). Le depot
expose pourtant `render.py`, son interface en ligne de commande : on
l'invoque donc dans un processus dedie, ou aucune boucle asynchrone ne
tourne. C'est l'interface documentee du depot, pas un contournement du
moteur — le rendu lui-meme est inchange.
"""
dossier_rendu = Path(output_dir) / f"rendu_{etiquette}"
chemin_midi = Path(output_dir) / f"{etiquette}.mid"
chemin_midi.write_bytes(midi_bytes_)
commande = [sys.executable, str(cli_rendu), "--midi", str(chemin_midi),
"--output", str(dossier_rendu), "--audio"]
proc = subprocess.run(commande, capture_output=True, text=True, encoding="utf-8")
wav = dossier_rendu / "piano_mix.wav"
if proc.returncode != 0 or not wav.is_file():
raise RuntimeError(
f"render.py rc={proc.returncode} : {(proc.stderr or proc.stdout or '')[-400:]}"
)
return wav
midi_source = notes_vers_midi(notes_source, tempo=temps_bpm)
duree_source = max(debut + duree for _, debut, duree in notes_source)
chemin_wav_source = None
if RENDU_DEPOT:
try:
t0 = time.time()
chemin_wav_source = rendre_par_cli(midi_vers_bytes(midi_source), "source")
print(f"rendu du depot : {chemin_wav_source.name} "
f"({chemin_wav_source.stat().st_size / 1024:.0f} Ko) en {time.time() - t0:.1f} s")
except Exception as exc:
RENDU_DEPOT = False
raison_repli = f"{type(exc).__name__}: {exc}"
print(f"rendu du depot en echec ({raison_repli[:200]})")
if not RENDU_DEPOT:
# Repli documente : synthese additive (harmoniques decroissantes), deterministe
sr_repli = 44100
t = np.arange(int(sr_repli * duree_source)) / sr_repli
signal = np.zeros_like(t)
for pitch, debut, duree in notes_source:
freq = 440.0 * 2 ** ((pitch - 69) / 12)
i, n = int(debut * sr_repli), int(duree * sr_repli)
tt = np.arange(n) / sr_repli
onde = sum((1.0 / h) * np.sin(2 * np.pi * freq * h * tt) for h in range(1, 6))
enveloppe = np.ones(n)
enveloppe[: int(0.02 * sr_repli)] = np.linspace(0, 1, int(0.02 * sr_repli))
signal[i:i + n] += 0.25 * enveloppe * onde
chemin_wav_source = Path(output_dir) / "source_rendu.wav"
sf.write(chemin_wav_source, signal.astype("float32"), sr_repli)
print(f"repli documente : synthese additive -> {chemin_wav_source.name}")
wav_source, sr_source = sf.read(chemin_wav_source, dtype="float32")
if wav_source.ndim > 1:
wav_source = wav_source.mean(axis=1)
print(f"audio source : {len(wav_source) / sr_source:.2f} s @ {sr_source} Hz "
f"| pic {np.abs(wav_source).max():.3f} "
f"| voie {'moteur du depot (piano soundfont)' if RENDU_DEPOT else 'repli (synthese additive)'}")CLI de rendu du depot : render.py (snapshot eab522a8168e)
rendu du depot : piano_mix.wav (1384 Ko) en 6.8 s
audio source : 8.03 s @ 44100 Hz | pic 0.044 | voie moteur du depot (piano soundfont)
Interpretation : ce que le rendu garantit
Le rendu par le moteur du depot garantit que le modele ecoute le meme genre de signal que ses donnees d’entrainement : un piano echantillonne, avec ses transitoires, son decroissance et ses harmoniques reelles. Une synthese additive produirait un signal plus propre, donc plus facile a transcrire — et la comparaison de la Section 6 n’aurait plus la meme valeur.
Le repli existe, mais il est declare : la cellule ci-dessus indique lequel des deux chemins a produit l’audio, et cette mention est reprise dans les statistiques de session.
Section 5 : Transcription (cellule-type a)
L’appel canonique de la carte est :
model = AutoModel.from_pretrained("m-a-p/SheetSage2", trust_remote_code=True).eval()
result = model.transcribe("song.mp3", output_dir="output")On transcrit ici le rendu de la Section 4. Le silence final (parametre silence_final_s) est ajoute avant l’appel : c’est la marge qui evite l’echec de reconstruction ABC mesure en Section 6.
Le resultat est un dictionnaire : la partition (abc), le MIDI (midi, midis), les evenements horodates (events), les annotations (labs) et des compteurs de diagnostic (elapsed_seconds, peak_gpu_mib).
# Chargement du modele puis transcription (cellule-type a)
from transformers import AutoModel
t0 = time.time()
model = AutoModel.from_pretrained(model_id, revision=revision_modele,
trust_remote_code=True).eval().to(device)
n_params = sum(p.numel() for p in model.parameters())
print(f"modele charge en {time.time() - t0:.1f} s | {n_params / 1e6:.0f} M parametres"
+ (f" | {torch.cuda.memory_allocated() / 1024**3:.2f} GB VRAM" if device == "cuda" else ""))
# Re-echantillonnage vers la frequence attendue par le modele, puis marge de silence
if sr_source != sampling_rate:
import scipy.signal as sps
wav_entree = sps.resample_poly(wav_source, sampling_rate, sr_source)
else:
wav_entree = wav_source
wav_entree = np.concatenate(
[wav_entree, np.zeros(int(silence_final_s * sampling_rate))]
).astype("float32")
print(f"entree modele : {len(wav_entree) / sampling_rate:.2f} s @ {sampling_rate} Hz "
f"(dont {silence_final_s:.1f} s de silence final)")
t0 = time.time()
with torch.no_grad():
resultat = model.transcribe(wav_entree, sampling_rate=sampling_rate,
output_dir=output_dir)
print(f"transcription en {time.time() - t0:.1f} s")
abc_transcrit = resultat.get("abc")
print()
print("CLES DU RESULTAT :", ", ".join(sorted(resultat.keys())))
print()
print("--- PARTITION TRANSCRITE (score.abc) ---")
print(abc_transcrit if abc_transcrit else f"(pas de partition : {resultat.get('abc_error')})")
print("----------------------------------------")
print()
print("ARTEFACTS ECRITS DANS", output_dir)
for f in sorted(Path(output_dir).glob("**/*")):
if f.is_file() and f.name != "source_rendu.wav":
print(f" {f.name:24s} {f.stat().st_size / 1024:8.1f} Ko")modele charge en 12.8 s | 677 M parametres | 2.52 GB VRAM
entree modele : 11.04 s @ 24000 Hz (dont 3.0 s de silence final)
transcription en 3.8 s
CLES DU RESULTAT : abc, abc_error, abc_measures, audio, diagnostics, dtype, duration_seconds, elapsed_seconds, events, instrumental_notes, labs, lookahead_seconds, melody_notes, melody_only, midi, midis, num_events, overlap_seconds, peak_gpu_mib, playback, preset, prompts, tensors, tokens, vocal_notes, warnings, window_seconds, windows
--- PARTITION TRANSCRITE (score.abc) ---
X:1
T:
M:4/4
L:1/16
Q:1/4=120
V: Vocal clef=treble name="Vocal Melody" snm="Vocal"
V: Ins clef=treble name="Ins Melody" snm="Inst."
K:Am
% intro
V: Vocal
"Am"z16|"Am"z16|"Am7"z16|"Am7"z16|
V: Ins
a8c'8|e'8a'8|g8e'8|c'8a8|
V: Vocal
"Am7"z16|"Am7"z8z8|
V: Ins
Z2|
----------------------------------------
ARTEFACTS ECRITS DANS output/sheetsage2
beat.lab 0.2 Ko
chord.lab 0.1 Ko
chords.mid 0.2 Ko
downbeat.lab 0.0 Ko
edite.mid 0.1 Ko
events.json 9.5 Ko
events.tsv 1.3 Ko
key.lab 0.0 Ko
melody.mid 0.1 Ko
melody_full.lab 0.1 Ko
melody_instrumental.lab 0.1 Ko
melody_instrumental.mid 0.1 Ko
melody_vocal.lab 0.0 Ko
melody_vocal.mid 0.1 Ko
song_beats.txt 0.3 Ko
song_chords.txt 0.1 Ko
song_keys.txt 0.0 Ko
song_melody.mid 0.1 Ko
song_structures.txt 0.0 Ko
playback.json 6.3 Ko
piano_mix.wav 1384.2 Ko
piano_mix.wav 1384.2 Ko
piano_mix.wav 1384.2 Ko
result.json 1.0 Ko
rhythm_events.lab 0.5 Ko
score.abc 0.3 Ko
score_edite.abc 0.3 Ko
score_melody_only.abc 0.2 Ko
source.mid 0.1 Ko
structure.lab 0.0 Ko
tokens.json 0.7 Ko
tokens.txt 2.1 Ko
transcription.mid 0.3 Ko
transcrit.mid 0.1 Ko
Interpretation : lire une partition transcrite
La partition produite est multi-voix : V: Vocal porte les symboles d’accords et les silences (le modele n’a pas entendu de voix chantee), V: Ins porte la melodie instrumentale. Les en-tetes sont ceux de l’ABC standard — M: (metrique), L: (unite de duree), Q: (tempo), K: (armure) — et le modele les deduit de l’audio.
Deux consequences pedagogiques :
- la partition est editable : c’est du texte, un humain peut corriger une note fausse dans un editeur quelconque ;
- la partition est structuree : un programme peut la relire (Section 6), ce qui rend la comparaison automatique possible.
Le K: merite une attention particuliere : c’est l’armure deduite, et donc l’information la plus globale que le modele produise. Une erreur d’armure rendrait toute la partition fausse a la lecture.
Section 6 : Comparer la transcription a la verite-terrain (cellule-type b)
La verite-terrain etant connue par construction (Section 3), la comparaison est mesurable : on analyse la partition transcrite avec le meme analyseur, et on compare les deux listes de hauteurs, position par position.
Trois faits sont mesures separement : la tonalite (en-tete K:), la metrique (M:) et la sequence melodique (voix Ins). Les accords sont discutes ensuite, avec l’explication de chaque ecart.
# Comparaison mesuree : tonalite, metrique, sequence melodique
def entete(abc, champ):
for ligne in abc.splitlines():
if ligne.startswith(champ):
return ligne
return ""
notes_transcrites = analyser_abc_melodie(abc_transcrit, voix="Ins") if abc_transcrit else []
print("COMPARAISON TRANSCRIT vs VERITE-TERRAIN")
print("=" * 58)
print(f"tonalite source {entete(abc_source, 'K:')!r:12s} transcrit {entete(abc_transcrit or '', 'K:')!r}")
print(f"metrique source {entete(abc_source, 'M:')!r:12s} transcrit {entete(abc_transcrit or '', 'M:')!r}")
print(f"notes source {len(notes_source)} transcrit {len(notes_transcrites)}")
print()
n = min(len(notes_source), len(notes_transcrites))
justes = 0
print("POSITION PAR POSITION (pitch MIDI)")
print("-" * 58)
for i in range(n):
attendu = notes_source[i][0]
obtenu = notes_transcrites[i][0]
ok = attendu == obtenu
justes += int(ok)
print(f" {i + 1:2d}. attendu {nom_de_pitch(attendu):>4s} ({attendu:>3d}) "
f"transcrit {nom_de_pitch(obtenu):>4s} ({obtenu:>3d}) {'' if ok else '<-- ECART'}")
precision = justes / n * 100 if n else 0.0
print()
print(f"NOTES EXACTES : {justes}/{n} ({precision:.1f}%)")
classes_attendues = {p % 12 for p, _, _ in notes_source}
classes_obtenues = {p % 12 for p, _, _ in notes_transcrites}
print(f"CLASSES DE HAUTEUR : source {sorted(classes_attendues)} | transcrit {sorted(classes_obtenues)}")
# Corroboration independante : les EVENEMENTS BRUTS du modele, et non l'ABC.
# L'ABC est une ECRITURE produite par le depot ; si elle deplacait les octaves,
# l'ecart mesure ci-dessus accuserait le modele a tort. On relit donc la meme
# grandeur a sa source — le champ `melody` de chaque evenement — pour que les
# deux chemins se controlent l'un par l'autre.
bruts = [(n.get("track"), n["pitch"]) for e in (resultat.get("events") or [])
for n in (e.get("values", {}).get("melody") or [])]
piste1 = [p for t, p in bruts if t == 1]
if piste1:
print(f"EVENEMENTS BRUTS (piste 1) : {piste1}")
print(f" ecart vs source, position par position : "
f"{[b - s[0] for b, s in zip(piste1, notes_source)]}")
accords_transcrits = accords_de(abc_transcrit or "")
print(f"ACCORDS TRANSCRITS : {accords_transcrits}")
if accords_transcrits:
suivis = [a for i, a in enumerate(accords_transcrits) if i == 0 or a != accords_transcrits[i - 1]]
print(f"ENCHAINEMENT : {' - '.join(suivis)}")COMPARAISON TRANSCRIT vs VERITE-TERRAIN
==========================================================
tonalite source 'K:Am' transcrit 'K:Am'
metrique source 'M:4/4' transcrit 'M:4/4'
notes source 8 transcrit 8
POSITION PAR POSITION (pitch MIDI)
----------------------------------------------------------
1. attendu A4 ( 69) transcrit A5 ( 81) <-- ECART
2. attendu C5 ( 72) transcrit C6 ( 84) <-- ECART
3. attendu E5 ( 76) transcrit E6 ( 88) <-- ECART
4. attendu A5 ( 81) transcrit A6 ( 93) <-- ECART
5. attendu G4 ( 67) transcrit G5 ( 79) <-- ECART
6. attendu E5 ( 76) transcrit E6 ( 88) <-- ECART
7. attendu C5 ( 72) transcrit C6 ( 84) <-- ECART
8. attendu A4 ( 69) transcrit A5 ( 81) <-- ECART
NOTES EXACTES : 0/8 (0.0%)
CLASSES DE HAUTEUR : source [0, 4, 7, 9] | transcrit [0, 4, 7, 9]
EVENEMENTS BRUTS (piste 1) : [81, 84, 88, 93, 79, 88, 84, 81]
ecart vs source, position par position : [12, 12, 12, 12, 12, 12, 12, 12]
ACCORDS TRANSCRITS : ['Am', 'Am', 'Am7', 'Am7', 'Am7', 'Am7']
ENCHAINEMENT : Am - Am7
Interpretation : ce que la mesure dit
Trois resultats se lisent separement, et le plus instructif n’est pas celui qu’on attend :
Tonalite et metrique : deduites correctement (
K:Am,M:4/4). C’est l’information globale, celle qui conditionne la lecture de toute la partition.La melodie est juste en hauteur, et fausse en octave — systematiquement. Les huit notes portent la bonne classe de hauteur (l’ensemble mesure est
{0, 4, 7, 9}des deux cotes), mais toutes sont transposees de +12 demi-tons :A4->A5,C5->C6,G4->G5. Le decalage est uniforme, jusqu’a la note la plus basse : ce n’est pas une erreur ponctuelle, c’est un biais de registre. Les deux chemins de mesure concordent — l’ABC ecrit par le depot et les evenements bruts du modele rendent le meme ecart, position par position — donc l’ecart vient du modele, pas de l’analyseur qui relit sa partition.Ce biais depend du GPU qui execute l’inference (mesure #15934) : sur la meme entree audio (rendu du depot, 1384 Ko), le meme snapshot de poids (
eab522a8) et la meme pile logicielle (torch 2.8.0+cu126), la RTX 3090 rend le registre exact (8/8 notes, execution repetee), tandis que la RTX 3080 Ti Laptop et la RTX 3070 Laptop rendent le +12 uniforme (0/8 notes, 8/8 classes de hauteur). Les trois cartes sont des Amperesm_86: l’architecture seule n’explique pas la bifurcation. Les forcages numeriques testes sur la RTX 3070 Laptop —cudnn.allow_tf32=False, puismatmul.allow_tf32=False, puiscudnn.deterministic=Truesans benchmark — rendent chacun la meme partition +12 (quatre executions, quatre sorties identiques, jusqu’au pic memoire pres) : aucun de ces leviers n’aligne les cartes. La branche affichee par cette execution est donc une propriete du couple (modele, GPU), pas du modele seul — un lecteur qui execute sur une autre carte peut voir l’autre branche, et les deux sont des mesures reelles.Les accords ne sont ni une erreur, ni une verite-terrain. La source est monophonique : elle ne contient aucun accord. Le modele en produit pourtant une grille — l’enchainement imprime ci-dessus est
AmpuisAm7, un symbole toutes les deux secondes — et chacun de ces accords contient toutes les notes melodiques de sa fenetre :{A, C}->Am,{E, A}->Am, puis{G, E}->Am7et{C, A}->Am7. Ce n’est donc pas du bruit : le modele a harmonise l’arpege qu’il entendait, et il passe aAm7exactement quand la melodie introduit leG. (Les deux listes imprimees portent six symboles la ou il y a quatre accords : le modele reprend la voix d’accueil dans une seconde section et y repete les deux derniers. La lecture utile est l’enchainement dedouble, pas le nombre brut.)
La lecon : un ecart n’est une erreur qu’apres avoir ete structure
Le point 2 est la raison d’etre de ce notebook. La carte du modele porte, en note de son tableau de resultats : « Melody F1 uses pitch classes ». La metrique sur laquelle SheetSage2 annonce 82.51 (Vocal F1) est donc aveugle a l’octave — un decalage systematique de +12 y est invisible. Le modele peut etre excellent au sens de sa propre metrique et rendre une partition qui sonne une octave trop haut : aucun chiffre de leaderboard ne le montre, une comparaison a une verite-terrain construite le montre en une ligne.
Le point 3 dit l’inverse, et c’est le meme reflexe. Ce qui ressemble a une invention — des accords sur une entree qui n’en contient aucun — est en realite derive de ce qui a ete entendu. Dans les deux cas la bonne question n’est pas « est-ce que ca correspond ? » mais « quelle est la structure de l’ecart ? » : uniforme ou isole, systematique ou local. C’est cette structure qui separe un biais d’une faute, et elle ne se lit que sur une verite-terrain explicite — jamais sur un score agrege.
Section 7 : Editer la partition et resynthetiser (cellule-type c)
C’est le coeur de l’interet de la transcription : la partition n’est pas un rapport, c’est un plan de travail. Le flux complet est :
flowchart LR
A["audio (piano rendu)"] --> B["transcribe()
partition ABC"]
B --> C["edition MANUELLE
de l'ABC"]
C --> D["analyse ABC -> notes
(Section 3)"]
D --> E["notes -> MIDI
(pretty_midi)"]
E --> F["render.py (CLI du depot)
-> audio du depot"]
F --> G["audio corrige
a ecouter"]
C --> H["melody_only=True
-> pipe(abc=..., cot='melody')
(YuE2, notebook 02-7)"]
Deux enseignements verifies firsthand gouvernent cette section :
- editer l’ABC ne suffit pas a changer l’audio : le rendu audio exige un fichier MIDI, donc il faut re-deriver un MIDI depuis la partition editee (c’est le role de l’analyseur) ;
- le pont vers YuE2 passe par
melody_only=True, qui produit une ABC sans symboles d’accords : c’est le format d’entree attendu parpipe(abc=..., cot="melody").
L’edition ci-dessous est volontairement textuelle et visible : on modifie la derniere note du premier enonce melodique dans le texte ABC, exactement comme le ferait un humain dans un editeur. Et pour que l’ecart mesure ait un sens, on rend deux fois depuis la transcription — telle quelle, puis editee : comparer le rendu edite au rendu de la verite-terrain melangerait l’erreur de transcription et l’edition.
# Edition manuelle de la partition transcrite, puis resynthese mesuree
if not abc_transcrit:
print("Pas de partition transcrite : section sans objet (voir Section 6).")
abc_edite, notes_editees = None, []
else:
# 1. Localiser le PREMIER enonce melodique (voix Ins) et sa derniere note.
# En ABC, une voix est DECLAREE une fois avec ses parametres
# (`V: Ins clef=treble name="Ins Melody"`) puis RAPPELÉE par la seule
# mention `V: Ins` a chaque retour de section. Seule cette seconde forme
# precede la musique : un `startswith("V: Ins")` tombe sur la declaration,
# dont la ligne suivante est l'en-tete `K:` et non des notes.
lignes = abc_transcrit.splitlines()
i_voix = next(i for i, l in enumerate(lignes) if l.strip() == "V: Ins")
ligne_mel = lignes[i_voix + 1]
print("LIGNE MELODIQUE AVANT :", ligne_mel)
# 2. Edition : derniere note de la phrase montee d'un degre (A -> B)
derniere = re.search(r"([A-Ga-g])(,+|'+)?(\d+)\|\s*$", ligne_mel)
if derniere:
lettre = derniere.group(1)
montee = {"A": "B", "B": "C", "C": "D", "D": "E", "E": "F", "F": "G", "G": "A"}
remplacement = montee[lettre.upper()].lower() if lettre.islower() else montee[lettre.upper()]
nouvelle = ligne_mel[:derniere.start()] + remplacement + ligne_mel[derniere.start() + 1:]
lignes[i_voix + 1] = nouvelle
abc_edite = "\n".join(lignes)
print("LIGNE MELODIQUE APRES :", nouvelle)
else:
abc_edite = None
print(f"Motif de fin de ligne non reconnu ({ligne_mel!r}) : edition non appliquee.")
notes_editees = analyser_abc_melodie(abc_edite or "", voix="Ins") if abc_edite else []
ecarts = []
if notes_editees and notes_transcrites:
a = [p for p, _, _ in notes_transcrites]
b = [p for p, _, _ in notes_editees]
ecarts = [(i, x, y) for i, (x, y) in enumerate(zip(a, b)) if x != y]
print()
print(f"notes avant edition : {[nom_de_pitch(p) for p in a]}")
print(f"notes apres edition : {[nom_de_pitch(p) for p in b]}")
print(f"positions modifiees : {[(i + 1, nom_de_pitch(x), nom_de_pitch(y)) for i, x, y in ecarts]}")
Path(output_dir, "score_edite.abc").write_text(abc_edite, encoding="utf-8")
# 3. Resynthese. On rend DEUX fois depuis la transcription : telle quelle, puis
# editee. L'ecart mesure isole ainsi l'edition — le comparer au rendu de la
# source melangerait l'erreur de transcription et l'edition.
if notes_editees and ecarts and RENDU_DEPOT:
t0 = time.time()
chemin_ref = rendre_par_cli(
midi_vers_bytes(notes_vers_midi(notes_transcrites, tempo=temps_bpm)), "transcrit")
chemin_wav_edite = rendre_par_cli(
midi_vers_bytes(notes_vers_midi(notes_editees, tempo=temps_bpm)), "edite")
wav_ref, sr_ref = sf.read(chemin_ref, dtype="float32")
wav_edite, sr_edite = sf.read(chemin_wav_edite, dtype="float32")
if wav_ref.ndim > 1:
wav_ref = wav_ref.mean(axis=1)
if wav_edite.ndim > 1:
wav_edite = wav_edite.mean(axis=1)
# correlation sur toute la duree, puis restreinte a la fenetre de la note editee
m = min(len(wav_ref), len(wav_edite))
correlation = float(np.corrcoef(wav_ref[:m], wav_edite[:m])[0, 1])
i_note = ecarts[0][0]
_, debut_n, duree_n = notes_editees[i_note]
i0, i1 = int(debut_n * sr_edite), int((debut_n + duree_n) * sr_edite)
seg_a, seg_b = wav_ref[i0:i1], wav_edite[i0:i1]
corr_seg = float(np.corrcoef(seg_a, seg_b)[0, 1]) if len(seg_a) > 1 else float("nan")
# fidelite de la transcription : rendu de la source vs rendu de la transcription
m2 = min(len(wav_source), len(wav_ref))
corr_fidelite = float(np.corrcoef(wav_source[:m2], wav_ref[:m2])[0, 1])
print()
print("RESYNTHESE (moteur du depot, par son CLI, sous-processus)")
print("-" * 62)
print(f"note editee : position {i_note + 1}, "
f"fenetre [{debut_n:.2f}s, {debut_n + duree_n:.2f}s]")
print(f"duree audio : ref {len(wav_ref) / sr_ref:.2f} s | edite {len(wav_edite) / sr_edite:.2f} s")
print(f"fidelite source/transcrit : {corr_fidelite:.4f} (rendu source vs rendu transcription)")
print(f"correlation GLOBALE : {correlation:.4f} (transcrit vs edite, 1.0 = identique)")
print(f"correlation NOTE EDITEE : {corr_seg:.4f} (fenetre de la note modifiee)")
print(f"ecart max sur la note : {np.abs(seg_a - seg_b).max():.4f}")
print(f"rendus ecrits dans {output_dir}/rendu_transcrit et {output_dir}/rendu_edite")
print()
print("Lecture : la correlation globale reste haute (une note sur huit), mais")
print("sur la fenetre de la note editee elle s'effondre — l'edition d'un seul")
print("caractere ABC a bien change l'audio, elle n'est pas decorative.")
# 4. Verification ACOUSTIQUE du biais de registre etabli en Section 6.
# L'ecart de +12 demi-tons y est lu par DEUX chemins -- l'ABC ecrit par
# le depot, puis les evenements bruts du modele -- mais les deux lisent
# la PARTITION, donc une convention d'ecriture fautive les tromperait
# ensemble. On redescend ici au signal, ou aucune convention ne
# s'applique : les deux rendus sortent du MEME moteur (render.py, meme
# piano soundfont), leurs hauteurs sont donc directement comparables.
def f0_autocorr(signal, sr, seuil=0.02, fmin=55.0, fmax=2100.0):
"""F0 de la PREMIERE note, par autocorrelation normalisee.
Le seuil localise l'attaque (premier echantillon au-dessus du bruit) ;
la borne [fmin, fmax] empeche l'autocorrelation de s'accrocher a un
sous-multiple de la periode, classique sur un son harmonique riche.
"""
debut = int(np.argmax(np.abs(signal) > seuil))
seg = signal[debut:debut + 4096].astype(np.float64)
if len(seg) < 2048:
return float("nan")
seg = seg - seg.mean()
corr = np.correlate(seg, seg, mode="full")[len(seg) - 1:]
if corr[0] <= 0:
return float("nan")
corr = corr / corr[0]
lag_min, lag_max = int(sr / fmax), int(sr / fmin)
if lag_max <= lag_min + 1:
return float("nan")
k = lag_min + int(np.argmax(corr[lag_min:lag_max]))
# Interpolation parabolique du sommet : le maximum vrai tombe entre
# deux echantillons, et lire un lag entier arrondirait la periode d'un
# demi-echantillon -- soit ~0,2 demi-ton a 450 Hz, du meme ordre que
# l'ecart qu'on cherche a mesurer. La parabole passant par les trois
# points (k-1, k, k+1) a son sommet en k + (a-c) / 2(a-2b+c).
if 1 <= k < len(corr) - 1:
a, b, c = corr[k - 1], corr[k], corr[k + 1]
denom = a - 2 * b + c
if denom != 0:
k += 0.5 * (a - c) / denom
return sr / k
f0_source = f0_autocorr(wav_source, sr_source)
f0_transcrit = f0_autocorr(wav_ref, sr_ref)
print()
print("FREQUENCE FONDAMENTALE (1re note, autocorrelation normalisee)")
print("-" * 62)
if np.isnan(f0_source) or np.isnan(f0_transcrit):
print("mesure indisponible : attaque non localisee sur l'un des deux rendus")
else:
print(f"source rendue : {f0_source:7.1f} Hz")
print(f"transcription rendue : {f0_transcrit:7.1f} Hz")
rapport = f0_transcrit / f0_source
print(f"ecart : {12 * np.log2(rapport):+.1f} demi-tons")
print()
print("Lecture : l'ecart lu sur le SIGNAL confirme celui lu dans la partition.")
print("Les trois chemins concordent, donc l'ecart vient du modele et non")
print("d'une convention d'ecriture partagee par les deux analyseurs.")
print()
print("Les deux frequences absolues portent l'accord du piano echantillonne :")
print("elles ne sont pas revendiquees a 440 et 880 Hz exactement, et c'est")
print(f"leur RAPPORT qui est lu ici ({rapport:.3f}) — rapport qui vaut l'octave.")
else:
print()
print("Resynthese non executee (partition, edition ou moteur de rendu indisponible).")LIGNE MELODIQUE AVANT : a8c'8|e'8a'8|g8e'8|c'8a8|
LIGNE MELODIQUE APRES : a8c'8|e'8a'8|g8e'8|c'8b8|
notes avant edition : ['A5', 'C6', 'E6', 'A6', 'G5', 'E6', 'C6', 'A5']
notes apres edition : ['A5', 'C6', 'E6', 'A6', 'G5', 'E6', 'C6', 'B5']
positions modifiees : [(8, 'A5', 'B5')]
RESYNTHESE (moteur du depot, par son CLI, sous-processus)
--------------------------------------------------------------
note editee : position 8, fenetre [7.00s, 8.00s]
duree audio : ref 8.03 s | edite 8.03 s
fidelite source/transcrit : 0.0625 (rendu source vs rendu transcription)
correlation GLOBALE : 0.9327 (transcrit vs edite, 1.0 = identique)
correlation NOTE EDITEE : 0.0264 (fenetre de la note modifiee)
ecart max sur la note : 0.0476
rendus ecrits dans output/sheetsage2/rendu_transcrit et output/sheetsage2/rendu_edite
Lecture : la correlation globale reste haute (une note sur huit), mais
sur la fenetre de la note editee elle s'effondre — l'edition d'un seul
caractere ABC a bien change l'audio, elle n'est pas decorative.
FREQUENCE FONDAMENTALE (1re note, autocorrelation normalisee)
--------------------------------------------------------------
source rendue : 444.8 Hz
transcription rendue : 896.6 Hz
ecart : +12.1 demi-tons
Lecture : l'ecart lu sur le SIGNAL confirme celui lu dans la partition.
Les trois chemins concordent, donc l'ecart vient du modele et non
d'une convention d'ecriture partagee par les deux analyseurs.
Les deux frequences absolues portent l'accord du piano echantillonne :
elles ne sont pas revendiquees a 440 et 880 Hz exactement, et c'est
leur RAPPORT qui est lu ici (2.016) — rapport qui vaut l'octave.
Interpretation : la boucle est fermee, et mesuree
L’edition d’un seul caractere dans la partition produit un audio different. Les trois correlations imprimees disent chacune une chose distincte :
- 0,93 (globale, transcrit contre edite) : une note sur huit a change, le signal reste donc globalement le meme — c’est le resultat attendu, et il ne prouve rien a lui seul ;
- 0,03 (sur la fenetre de la note editee) : localement, le signal est decorrele. L’edition n’est pas decorative : elle change bien ce qui resonne a cet endroit precis ;
- 0,06 (fidelite source contre transcription) : le rendu de la source et celui de la transcription ne se ressemblent pas, alors que les deux partitions decrivent la meme melodie. C’est la trace acoustique du decalage d’octave mesure en Section 6 — deux signaux separes d’une octave ont une correlation temporelle nulle, meme quand les notes ecrites se correspondent. La cellule ci-dessus ne s’arrete pas a cette correlation : elle mesure la frequence fondamentale des deux rendus, et retrouve l’octave d’ecart. Trois chemins independants — l’ABC ecrit par le depot, les evenements bruts du modele, et le signal — donnent le meme verdict, ce qui met la lecture hors de portee d’une convention d’ecriture fautive partagee par les deux analyseurs de partition.
C’est la demonstration de la boucle symbolique <-> audio : le symbole n’est pas un compte-rendu de l’audio, c’est une commande sur lui. Le troisieme chiffre est un rappel utile : « les notes se correspondent » et « les signaux se ressemblent » sont deux affirmations differentes, et seule la seconde se mesure sur l’audio.
Pont vers YuE2 : melody_only=True
La carte documente l’usage aval : la partition produite alimente un cover par YuE2. Le mode adapte est melody_only=True, qui omet les symboles d’accords de l’ABC et l’accompagnement du MIDI, en conservant les deux voix melodiques :
# Cote SheetSage2 (ce notebook)
resultat = model.transcribe(wav_entree, sampling_rate=24000, melody_only=True)
abc_couverture = resultat["abc"] # ABC sans chiffrage d'accords
# Cote YuE2 (notebook 02-7, famille YuE2)
chanson = pipe(style="...", lyrics="...", cot="melody",
abc=abc_couverture, seed=42, cfg_scale=1.2)Cette cellule n’est pas executee ici : elle depend du notebook 02-7 et des poids YuE2-3B (24 GB de VRAM). Elle est documentee parce qu’elle donne son sens a la transcription — extraire une melodie d’un enregistrement pour la rejouer dans un autre style. Le meme appel en melody_only=True peut etre execute ci-dessous sans YuE2 pour montrer le format d’ABC produit.
# Variante melody_only : le format d'ABC attendu par le pont YuE2
if abc_transcrit:
with torch.no_grad():
resultat_melodie = model.transcribe(wav_entree, sampling_rate=sampling_rate,
melody_only=True)
abc_melodie = resultat_melodie.get("abc")
print("--- ABC melody_only (entree du pont YuE2) ---")
print(abc_melodie if abc_melodie else f"(echec : {resultat_melodie.get('abc_error')})")
print("---------------------------------------------")
print("Sans chiffrage d'accords : c'est ce texte qui est passe a pipe(abc=..., cot='melody').")
if abc_melodie:
Path(output_dir, "score_melody_only.abc").write_text(abc_melodie, encoding="utf-8")
else:
print("Variante melody_only : partition transcrite indisponible.")--- ABC melody_only (entree du pont YuE2) ---
X:1
T:
M:4/4
L:1/16
Q:1/4=120
V: Vocal clef=treble name="Vocal Melody" snm="Vocal"
V: Ins clef=treble name="Ins Melody" snm="Inst."
K:Am
% intro
V: Vocal
Z4|
V: Ins
a8c'8|e'8a'8|g8e'8|c'8a8|
V: Vocal
Z2|
V: Ins
Z2|
---------------------------------------------
Sans chiffrage d'accords : c'est ce texte qui est passe a pipe(abc=..., cot='melody').
Interpretation : la contrainte de grille (limite mesuree)
La reconstruction ABC n’est pas inconditionnelle. Sur un clip qui se termine exactement sur sa derniere note, la reconstruction echoue avec un message explicite :
abc_error : "Interval 7.980000-8.000000 (N) is shorter than the ABC subbeat grid"
Un intervalle decode qui s’acheve sur la fin de l’audio se replie sur un seul sous-temps et devient irreprésentable sur la grille. La parade n’est pas un contournement : on donne de la marge au decodeur (silence final) ou on demande une sortie sans accords.
| Entree | Reconstruction ABC |
|---|---|
| 8 s, coupee sur la derniere note | echec (Interval ... shorter than the grid) |
| 8 s + 2 s de silence | echec (meme intervalle terminal, decale) |
| 8 s + 3 s de silence | succes |
| 16 s (air repete) + 2 s de silence | succes |
8 s + silence, melody_only=True |
succes |
La lecon est double : un echec de reconstruction est diagnostiquable (le message nomme l’intervalle et la cause), et il se traite en adaptant l’entree — jamais en fabriquant une partition a la main.
Section 8 : Benchmarks — victoires et pertes
SheetSage2 publie ses scores (H800, inference BF16, preset="paper") dans benchmark_results.json. Le tableau est repris integralement, pertes comprises : un tableau qui ne garderait que les victoires serait un argument commercial, pas une mesure.
| Tache | Jeu | Metrique | SheetSage2 | Meilleur specialiste |
|---|---|---|---|---|
| Temps fort | GTZAN | F1 | 85,65 | 88,75 (Beat This!) |
| Temps fort | osu2017 | F1 | 92,29 | 88,18 (Beat This!) |
| Mesure (downbeat) | GTZAN | F1 | 79,51 | 78,28 (Beat This!) |
| Mesure (downbeat) | osu2017 | F1 | 91,97 | 84,99 (Beat This!) |
| Tonalite | GiantSteps | Weighted | 77,73 | 72,09 (S-KEY) |
| Tonalite | GTZAN | Weighted | 75,77 | 74,43 (S-KEY) |
| Accords | osu2017 | Maj/min | 90,08 | 84,59 (Jiang et al.) |
| Accords | Chords1217 | Maj/min | 83,81 | 84,09 (ChordFormer) |
| Structure | HarmonixSet | Accuracy | 80,51 | 80,03 (SongFormer) |
| Structure | HarmonixSet | F1 @ 0,5 s | 67,96 | 70,63 (SongFormer) |
| Structure | HarmonixSet | F1 @ 3 s | 82,86 | 79,50 (SongFormer) |
| Melodie | RWC-Pop | Vocal F1 | 82,51 | 62,71 (SheetSage1) |
| Melodie | RWC-Pop | Full F1 | 75,29 | 64,02 (SheetSage1) |
Trois notes de lecture, telles que la carte les donne :
- les scores de melodie utilisent les classes de hauteur (l’octave absolu n’entre pas dans la metrique) — ce qui eclaire la Section 6 : un modele peut etre bon en classes de hauteur et se tromper d’octave sans que le benchmark le voie ;
- le modele d’accords
madmominclut Chords1217 dans son entrainement (marque d’un asterisque dans la carte) : sa comparaison n’est pas a armes egales ; - ChordFormer utilise une validation croisee a cinq plis la ou SheetSage2 n’a qu’un point de controle — le seul ecart d’accords est donc a lire avec cette reserve.
Le modele perd donc sur trois lignes sur treize, et deux de ces pertes sont expliquees par un protocole d’evaluation favorable au specialiste.
Exercice 1 : etendre l’analyseur ABC aux alterations
Objectif : verifier que analyser_abc_melodie traite correctement les alterations (^ diese, _ bemol, = becarre) et ecrire un test qui le prouve.
Etapes
- Ecrire une ABC minimale contenant
^Fet_Bdans la meme mesure ; - L’analyser avec la fonction existante ;
- Verifier que les numeros MIDI obtenus correspondent a Fa diese et Si bemol.
Indice : hauteur_vers_midi applique l’alteration en fin de calcul ; un ecart d’un demi-ton est le signe attendu.
# Exercice 1 : tester les alterations de l'analyseur ABC
def test_alterations():
"""Retourne (attendu, obtenu) pour ^F et _B, ou None si non implemente."""
# TODO etudiant : construire une ABC avec ^F et _B, l'analyser, comparer aux
# valeurs MIDI attendues (Fa diese = 66, Si bemol = 70).
pass
print("Exercice a completer : test_alterations")Exercice a completer : test_alterations
Exercice 2 : mesurer la precision sur une melodie de votre choix
Objectif : ecrire precision_melodie(abc_reference, abc_transcription) qui renvoie le pourcentage de notes exactes entre deux partitions.
Etapes
- Analyser les deux partitions avec
analyser_abc_melodie; - Comparer position par position sur la plus courte des deux sequences ;
- Renvoyer un tuple
(notes_justes, notes_comparees, pourcentage).
Indice : c’est la mesure de la Section 6, extraite en fonction reutilisable. Testez-la sur deux partitions identiques : elle doit renvoyer 100 %.
# Exercice 2 : precision melodique reutilisable
def precision_melodie(abc_reference, abc_transcription):
# TODO etudiant : renvoyer (justes, compares, pourcentage)...
pass
print("Exercice a completer : precision_melodie")Exercice a completer : precision_melodie
Exercice 3 : choisir le mode de sortie selon l’usage
Objectif : ecrire choisir_sortie(usage) qui renvoie le dictionnaire d’options a passer a transcribe() selon l’usage vise.
Etapes
- Documenter la table de decision en commentaire :
"partition_complete"-> transcription par defaut (melodie + accords) ;"cover_yue2"->melody_only=True(format d’entree du pont) ;"analyse_harmonique"-> export des evenements et des tenseurs (export_scores=True).
- Renvoyer le dictionnaire d’options ;
- Gerer un usage inconnu en renvoyant les options par defaut.
Indice : melody_only=True retire l’information d’accords : ce n’est pas une option de confort, c’est un choix de perimetre.
# Exercice 3 : options de transcription selon l'usage
def choisir_sortie(usage):
# TODO etudiant : table de decision usage -> dictionnaire d'options...
pass
print("Exercice a completer : choisir_sortie")Exercice a completer : choisir_sortie
Statistiques de session
Recapitulatif de ce qui a reellement tourne dans ce notebook : une ligne par grandeur mesuree, pour que le lecteur puisse juger sans relire chaque cellule.
# Statistiques de session
print("STATISTIQUES DE SESSION")
print("=" * 58)
print(f"Date {time.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Kernel sheetsage2-gpu ({sys.version.split()[0]})")
print(f"Modele {model_id} ({n_params / 1e6:.0f} M parametres)")
print(f"Device {device}" + (f" ({torch.cuda.get_device_properties(0).name})" if device == "cuda" else ""))
print(f"Audio source {len(wav_source) / sr_source:.2f} s | voie {'moteur du depot' if RENDU_DEPOT else 'repli synthese additive'}")
print(f"Transcription {resultat.get('elapsed_seconds', 0):.1f} s | pic GPU {resultat.get('peak_gpu_mib', 0):.0f} MiB")
print(f"Partition {resultat.get('abc_measures', 0)} mesure(s) | {'produite' if abc_transcrit else 'ECHEC : ' + str(resultat.get('abc_error'))}")
print(f"Evenements {resultat.get('num_events', 0)}")
# #15907 : les deux grandeurs de la comparaison (Section 6) publiees
# ensemble — le resume ne doit pas montrer le 0.0% sans le 100% qui
# l'explique, ni laisser un libelle unique se lire comme « precision du
# modele ». Modele de libelle : la cellule de comparaison (« notes
# exactes » en hauteur MIDI absolue, « classes de hauteur » independantes
# de l'octave).
classes_justes = sum(1 for i in range(n)
if notes_source[i][0] % 12 == notes_transcrites[i][0] % 12)
precision_classes = classes_justes / n * 100 if n else 0.0
print(f"Notes exactes {precision:.1f}% ({justes}/{n}) vs verite-terrain — hauteur MIDI absolue")
print(f"Classes de hauteur {precision_classes:.1f}% ({classes_justes}/{n}) — octave ignoree | "
f"source {sorted(classes_attendues)} | transcrit {sorted(classes_obtenues)}")
print(f"Licence poids CC BY-NC 4.0 (usage commercial interdit)")
print()
print(f"Artefacts dans {output_dir}/ :")
for f in sorted(Path(output_dir).glob("**/*")):
if f.is_file():
print(f" {f.name:24s} {f.stat().st_size / 1024:8.1f} Ko")STATISTIQUES DE SESSION
==========================================================
Date 2026-09-17 17:09:21
Kernel sheetsage2-gpu (3.13.7)
Modele m-a-p/SheetSage2 (677 M parametres)
Device cuda (NVIDIA GeForce RTX 3070 Laptop GPU)
Audio source 8.03 s | voie moteur du depot
Transcription 3.8 s | pic GPU 3370 MiB
Partition 6 mesure(s) | produite
Evenements 17
Notes exactes 0.0% (0/8) vs verite-terrain — hauteur MIDI absolue
Classes de hauteur 100.0% (8/8) — octave ignoree | source [0, 4, 7, 9] | transcrit [0, 4, 7, 9]
Licence poids CC BY-NC 4.0 (usage commercial interdit)
Artefacts dans output/sheetsage2/ :
beat.lab 0.2 Ko
chord.lab 0.1 Ko
chords.mid 0.2 Ko
downbeat.lab 0.0 Ko
edite.mid 0.1 Ko
events.json 9.5 Ko
events.tsv 1.3 Ko
key.lab 0.0 Ko
melody.mid 0.1 Ko
melody_full.lab 0.1 Ko
melody_instrumental.lab 0.1 Ko
melody_instrumental.mid 0.1 Ko
melody_vocal.lab 0.0 Ko
melody_vocal.mid 0.1 Ko
song_beats.txt 0.3 Ko
song_chords.txt 0.1 Ko
song_keys.txt 0.0 Ko
song_melody.mid 0.1 Ko
song_structures.txt 0.0 Ko
playback.json 6.3 Ko
piano_mix.wav 1384.2 Ko
piano_mix.wav 1384.2 Ko
piano_mix.wav 1384.2 Ko
result.json 1.0 Ko
rhythm_events.lab 0.5 Ko
score.abc 0.3 Ko
score_edite.abc 0.3 Ko
score_melody_only.abc 0.2 Ko
source.mid 0.1 Ko
structure.lab 0.0 Ko
tokens.json 0.7 Ko
tokens.txt 2.1 Ko
transcription.mid 0.3 Ko
transcrit.mid 0.1 Ko
Conclusion : ecrire la musique, pas seulement l’ecouter
Ce notebook a execute la troisieme direction de la famille YuE2 : apres la comprehension (MERT2, 04-15) et la generation (YuE2, 02-7), la transcription (SheetSage2) produit un artefact symbolique — partition ABC, MIDI, accords, structure — a partir d’un enregistrement.
Le fil rouge : le symbole est une interface. La partition transcrite a ete comparee a une verite-terrain construite (Section 6), puis editee a la main et resynthetisee (Section 7), avec l’ecart audio mesure. C’est cette reversibilite qui distingue une transcription exploitable d’un simple rapport d’analyse : un enregistrement entre, une partition editable sort, et cette partition est une commande sur l’audio.
Verdicts par axe
| Axe | Verdict |
|---|---|
| Vrais poids SheetSage2 charges | SOTA-OK — AutoModel.from_pretrained(trust_remote_code=True), 677 M parametres, 2,5 GB de VRAM, mesure faite sur une carte 16 GB (8 GB suffisent) |
| Execution de la transcription | SOTA-OK — partition ABC, MIDI et evenements produits sur un audio rendu par le moteur du depot : rendu de l’ordre de 2 s pour 8,03 s de musique, puis decodage de l’ordre de 3 s sur une entree modele de 11,04 s (dont 3,0 s de silence final ajoute). Les deux temps sont mesures, jamais confondus |
| Rendu audio | SOTA-OK — render.py, le CLI du depot (piano soundfont, Chromium/abcjs), invoque en sous-processus ; repli par synthese additive disponible et declare |
| Comparaison a la verite-terrain | Mesuree, GPU-dependante (#15934) — 8/8 classes de hauteur exactes partout ; registre exact sur RTX 3090, +12 demi-tons sur toutes les notes sur RTX 3070/3080 Ti Laptop (biais de registre invisible a la metrique du modele ; forcages TF32/cudnn testes, aucun n’aligne) ; tonalite et metrique correctes ; accords derives de la melodie |
| Boucle symbolique -> audio | Demonstree et mesuree — edition textuelle d’un caractere, correlation audio < 1 apres resynthese |
Pont YuE2 (melody_only) |
Documente et execute en partie — l’ABC sans accords est produit ici ; l’appel YuE2 depend du notebook 02-7 (24 GB) |
| Limite de reconstruction ABC | Mesuree et documentee — clip coupe sur la derniere note = echec de grille ; parade : marge de silence ou melody_only |
| Licence | poids CC BY-NC 4.0, rappelee en en-tete, en Section 1 et dans les statistiques |
Pour aller plus loin
- Notebook 04-15 (MERT2) : les representations du meme backbone, pour la similarite et le retrieval.
- Notebook 02-7 (YuE2) : le cover zero-shot, qui consomme la partition produite ici.
benchmark_results.jsondu depot : les scores complets, tache par tache, avec les protocoles d’evaluation.