feat(forge): forge_loops, duels d'écoute — et ce qu'ils disent du classement - #21
Merged
Conversation
…trancher Il manquait à Forge la source la plus banale d'un studio : un pack de loops. La matière y est humaine — ce que `make_corpus` ne sait pas inventer et qu'une transcription dégrade. Ce qu'un pack n'a pas, c'est une forme : Forge la cherche. `loop_index.py` lit ce que les noms annoncent (tonalité, tempo, instrument) et mesure ce qu'ils taisent (longueur, polyphonie, tessiture). La tonalité vient de l'étiquette, jamais de l'estimateur quand elle existe — mesuré à 0.400 sur une boucle isolée. Une famille (dossier + tonalité + tempo) est ce qui s'arrange ensemble sans transposer ; seule la batterie se prête d'une famille à l'autre. Sur 820 fichiers : 800 indexés, 76 familles arrangeables. `forge_loops.py` tire N plans (forme, sections, effectif, courbe d'énergie, parfois une modulation ou une mélodie empruntée), tuile, écrit les marqueurs, et réutilise tel quel le classement fiabilité-d'abord de `forge.py`. À matière égale — même pack Naija Waves — le plan cherché bat le plan écrit à la main : 0.839 contre 0.760. Contre le générateur synthétique à budget égal (48 candidats, 3 graines), match nul : gagnant médian 0.849 contre 0.863, meilleur score 0.893 contre 0.884, peloton 0.757 contre 0.786. Par groupe : la forme monte (A 0.846 vs 0.766), la cohérence aussi (F 0.649 vs 0.500), la mélodie s'effondre (C 0.597 vs 0.834, l'axe 15 à lui seul 0.471 vs 0.956). Deux corrections essayées et réfutées, gardées en commentaire : accompagnement sous la mélodie (C inchangé, meilleur score 0.908 → 0.899) et mélodie plus fréquente (C 0.597 → 0.574). Reste l'explication la plus économique — les lignes d'un pack ne satisfont pas des bandes de tolérance écrites sous les mêmes hypothèses que le générateur maison. `scripts/mesure_tonalite.py` cesse de dupliquer la lecture des étiquettes : elle vit dans `loop_index.py`, le harnais la mesure. Mêmes chiffres après déduplication (0.400 / 0.334). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… dégradation Cinq sessions d'écoute ont validé des dégradations : un morceau contre sa version abîmée. Aucune n'avait testé un classement — un morceau contre un autre — qui est pourtant l'hypothèse sur laquelle Forge repose tout entier. `build_duel_tasks` oppose deux fichiers distincts, le côté « original » devenant celui que le moteur préfère. Contrôles, équilibrage des positions, anonymat du paquet client : inchangés, et c'est le but. `examples/forge_duels.py` construit les paires depuis n'importe quel forge_report.json, par tranche d'écart de score, en plafonnant la durée et en empêchant un candidat de monopoliser une tranche. Première session (session 6 dans resultats_ecoute.md) : 14 duels, 4 contrôles, rendu v3-instrument. Non concluante — 50 % (IC95 0.27-0.73), 43 % sur les écarts forts, 57 % sur les faibles. Le verdict qui compte est méthodologique et il est à charge : le lot ne pouvait pas conclure. Sur n = 7 par tranche, le seul résultat significatif possible était 7/7 ; la puissance pour une vraie détection de 0.80 était de 21 %. Dimensionnement consigné : 20, 30, 49 ou 90 jugements tranchés par tranche pour détecter 0.80, 0.75, 0.70 ou 0.65. Le lot n'autorise pas à conclure que le classement ne vaut rien. Il n'autorise plus non plus à présenter le n°1 de Forge comme le mieux construit sans préciser qu'aucune oreille ne l'a jamais confirmé. Aussi : canaux MIDI par pupitre dans forge_loops, pour que FluidSynth joue une basse comme une basse. Sans effet sur les scores — le moteur ne lit que « canal 9 ou pas », gagnant toujours 0.832 à graine égale. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La reprise se faisait sur `task_id`, c'est-à-dire sur la position dans le lot. Tant qu'on rejoue le même lot, les deux coïncident. Dès qu'on l'agrandit — passer de 14 à 60 duels pour obtenir la puissance qui manquait — tous les rangs glissent et les jugements déjà rendus se recollent à d'autres comparaisons. Silencieusement : rien ne plante, le fichier reste valide, les chiffres sont faux. `task_key` nomme la comparaison elle-même (paire ou fichier+dégradation, plus un rang pour les contrôles, seuls à se répéter dans un lot). Les jugements repris sont re-rattachés à leur tâche du nouveau lot ; ceux qui n'y figurent plus sont conservés — ils restent valides et cumulables. Les fichiers d'avant les clés en reçoivent une, reconstruite de leur contenu. `forge_duels.py` accepte `--reprendre` : les paires déjà jugées sont remises dans le lot plutôt qu'écartées, pour qu'un seul dépouillement couvre toute la session. Le plafond de duels par candidat suit la demande au lieu d'être figé à 2, et une tranche qui ne peut pas être remplie le dit au lieu de sortir courte en silence. Lot de la session 7 prêt : 96 candidats, 30 duels par tranche, 14 déjà jugés repris, 53 comparaisons neuves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…écart peut-être Lot dimensionné : 96 candidats forge_loops, 30 duels par tranche, 11 contrôles, les 18 jugements de la session 6 repris et cumulés — 71 jugements, un dépouillement, 54 minutes d'écoute. écart fort (0.102-0.206) : 18/29 = 62 % IC95 0.44-0.77 p = 0.132 écart faible (0.001-0.038) : 13/30 = 43 % IC95 0.27-0.61 p = 0.819 Aucun n'est significatif, et les deux ne disent pas la même chose. Le classement FIN ne s'entend pas : sur les écarts de quelques centièmes — ceux qui décident du n°1 contre le n°3 — l'oreille est au hasard, et cette fois la puissance était là (un effet réel de 0.75 aurait été vu 8 fois sur 10). Sur les gros écarts il y a un signal, trop faible pour ce lot : 62 % dans le sens attendu, contraste entre tranches dans le même sens (Fisher p = 0.119), mais établir 0.62 demanderait 106 duels par tranche. Le coût de la question est chiffré, il n'est pas payé. Réserve, et elle se répète depuis la session 1 : sur 11 paires IDENTIQUES, l'annotateur n'a dit « aucune différence » que 3 fois. Il n'appuie pas au hasard pour autant — 27 % contre 1.7 % sur les duels réels, Fisher p = 0.011 — mais chaque préférence inventée est un pile ou face qui tire le taux vers 0.5 : les 62 % sont un plancher, pas une estimation. Conséquence écrite noir sur blanc dans le README : sur du loop arrangé comme sur du transcrit, Forge est un triage grossier. Écarter le bas se défend, couronner le n°1 ne se défend pas. Le 0.839 contre 0.760 de forge_loops reste une mesure de charpente, pas une promesse d'écoute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Trois briques
examples/loop_index.py— index d'un pack de loops : lit ce que les noms de fichiers et de dossiers annoncent (tonalité, tempo, instrument), mesure ce qu'ils taisent (longueur, polyphonie, tessiture). La tonalité vient de l'étiquette, jamais de l'estimateur quand elle existe — 0.400 sur une boucle isolée (#19). Une famille = dossier + tonalité + tempo. Sur 820 fichiers : 800 indexés, 76 familles arrangeables.examples/forge_loops.py— tire N plans (forme, sections, effectif, courbe d'énergie, parfois une modulation ou une mélodie empruntée), tuile les boucles, écrit les marqueurs, et réutilise tel quel le classement fiabilité-d'abord deforge.py.Mode duel de
annotate(build_duel_tasks+examples/forge_duels.py) — le protocole d'écoute opposait un morceau à sa version dégradée ; il oppose maintenant deux morceaux différents, le côté « original » devenant celui que le moteur place devant. Contrôles, équilibrage et anonymat inchangés.Ce que ça vaut
À matière égale — même pack Naija Waves, plan écrit à la main contre plan cherché : 0.839 contre 0.760. Contre le générateur synthétique à budget égal (48 candidats, 3 graines), match nul : gagnant médian 0.849 contre 0.863.
Par groupe d'axes : la forme monte (A 0.846 vs 0.766), la cohérence aussi (F 0.649 vs 0.500), la mélodie s'effondre (C 0.597 vs 0.834 — l'axe 15 à lui seul 0.471 vs 0.956). Deux corrections essayées, mesurées, retirées (accompagnement sous la mélodie : aucun effet ; mélodie plus fréquente : pire). Reste l'explication la plus économique : les lignes de
make_corpussont écrites sous les mêmes hypothèses que les axes qui les notent.Ce que l'oreille en dit (sessions 6-7, 71 jugements)
Le classement fin ne s'entend pas — et cette fois la puissance était là (un effet de 0.75 aurait été vu 8 fois sur 10). Sur les gros écarts, un signal trop faible pour ce lot : établir 0.62 demanderait 106 duels par tranche. Réserve inscrite : sur 11 paires identiques, l'annotateur n'a dit « aucune différence » que 3 fois — chaque préférence inventée tire le taux vers 0.5, donc les 62 % sont un plancher.
Conséquence écrite dans le README : sur du loop arrangé comme sur du transcrit, Forge est un triage grossier. Écarter le bas se défend, couronner le n°1 ne se défend pas.
Divers
fix(annotate): la reprise d'une session suivaittask_id, c'est-à-dire le rang dans le lot — agrandir le lot recollait silencieusement les jugements rendus à d'autres comparaisons. Elle suit désormais l'identité de la comparaison.scripts/mesure_tonalite.pycesse de dupliquer la lecture des étiquettes (elle vit dansloop_index.py) — mêmes chiffres après déduplication.227 passed, 40 subtests passed·scripts/check.sh→ CHECK OK.🤖 Generated with Claude Code