feat(forge): forge_loops — arranger un pack de loops, laisser le SMS trancher - #20
feat(forge): forge_loops — arranger un pack de loops, laisser le SMS trancher#20klodynlov wants to merge 4 commits into
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>
Session d'écoute : le classement est passé devant une oreille — non concluant
Les cinq sessions précédentes validaient 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 entièrement. Lot : 48 candidats
Le verdict qui compte est méthodologique, et il est à charge : le lot ne pouvait pas conclure. Test binomial bilatéral contre 0.5, α = 0.05 — sur n = 7, le seul résultat significatif possible était 7/7 (6/7 donne déjà p = 0.125), et la puissance pour une vraie détection de 0.80 était de 21 %. Le dimensionnement doit précéder la session : 20 / 30 / 49 / 90 jugements tranchés par tranche pour détecter 0.80 / 0.75 / 0.70 / 0.65. Ce que le lot n'autorise pas : conclure que le classement ne vaut rien. Ce qu'il n'autorise plus : présenter le n°1 de Forge comme le mieux construit sans dire qu'aucune oreille ne l'a confirmé. Le README et Deux pistes à n = 2, signalées comme telles : Jugements bruts dans |
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>
Session 7 — lot dimensionné, 71 jugements cumulés
Aucun n'est significatif — mais les deux ne disent pas la même chose, et c'est là qu'est le résultat. Le classement fin ne s'entend pas. Sur les écarts de 0.001 à 0.038 — ceux qui décident du n°1 contre le n°3 dans un rapport Forge — l'oreille est à 43 %, au hasard. Ce n'est pas un manque de puissance cette fois : à n = 30, un effet réel de 0.75 aurait été détecté 8 fois sur 10. Départager deux candidats que le SMS sépare de deux centièmes n'a pas de correspondant audible. Sur les gros écarts, un signal trop faible pour ce lot. 62 % va dans le sens attendu, le contraste entre tranches aussi (Fisher unilatéral p = 0.119). Le lot visait un effet de 0.75 ; l'effet réel, s'il existe, est plus près de 0.62, pour lequel 30 duels ne donnent que 29 % de puissance. Il en faudrait 106 par tranche — une dizaine d'heures pour un annotateur seul, ou trois à quatre annotateurs. Le coût est chiffré ; il n'est pas payé. La réserve, et elle se répète depuis la session 1. Sur 11 paires identiques, l'annotateur n'a répondu « aucune différence » que 3 fois. Le harnais le signale comme SUSPECT et il a raison — mais le chiffre informatif est le contraste : 27 % de « aucune différence » sur les paires identiques contre 1.7 % sur les duels réels (Fisher p = 0.011). Il ne répond pas au hasard, il refuse d'utiliser « aucune différence ». Chaque préférence inventée étant un pile ou face, les 62 % sont un plancher, pas une estimation. Pas de biais de position (original en A 31/59). Ce que ça change dans le dépôt. Écrit noir sur blanc dans le README : sur du loop arrangé comme sur du transcrit, Forge est un triage grossier — écarter le bas du classement se défend, couronner le n°1 ne se défend pas. Le 0.839 contre 0.760 de
|
…outer du classement (#21) Trois briques et un résultat qui tempère les deux premières. `examples/loop_index.py` indexe un pack de loops : rôle, tonalité, tempo, longueur, tessiture. La tonalité vient de l'étiquette du pack, jamais de l'estimateur quand elle existe — 0.400 sur une boucle isolée. Une famille (dossier + tonalité + tempo) est ce qui s'arrange ensemble sans transposer. 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 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, match nul (0.849 contre 0.863, médiane sur 3 graines). Par groupe : la forme monte, la mélodie s'effondre (C 0.597 contre 0.834) — deux corrections essayées et réfutées, gardées en commentaire. Le mode duel de `annotate` oppose enfin deux morceaux DIFFÉRENTS, le côté « original » devenant celui que le moteur place devant : c'est la première fois qu'un classement passe devant une oreille. Deux sessions, 71 jugements : le classement fin ne s'entend pas (écarts ≤ 0.04 → 43 %, avec la puissance pour le voir), les gros écarts portent un signal non établi (≥ 0.10 → 62 %, p = 0.13, plancher car l'annotateur sous-utilise « aucune différence »). Conséquence inscrite au README : Forge est un triage grossier — écarter le bas se défend, couronner le n°1 non. Aussi : la reprise d'une session d'écoute suivait le rang dans le lot, donc agrandir un lot recollait silencieusement les jugements rendus à d'autres comparaisons ; elle suit désormais l'identité de la comparaison. Reprend #20, fermée par GitHub à la suppression de sa branche de base.
Il manquait à Forge la source la plus banale d'un studio : un pack de loops. Les progressions et les riffs y sont écrits par des humains — ce que
make_corpusne sait pas inventer et qu'une transcription dégrade (−0.18). Ce qu'un pack n'a pas, c'est une forme. Forge la cherche.Deux briques
examples/loop_index.py— lit ce que les noms de fichiers et de dossiers annoncent, mesure ce qu'ils taisent. La tonalité vient de l'étiquette, jamais de l'estimateur quand elle existe (0.400 sur une boucle isolée, cf. #19) ; il n'est appelé que pour les non-étiquetés, et l'index dit toujours d'où vient l'information. Le rôle est lu dans le nom, tranché par le contenu sinon (polyphonie ≥ 1.8 = accompagnement, monodie grave = basse). Une famille = dossier + tonalité + tempo : ce qui s'arrange ensemble sans transposer. Sur 820 fichiers → 800 indexés, 76 familles arrangeables.examples/forge_loops.py— tire N plans (forme, longueur des sections, effectif, courbe d'énergie, parfois une modulation ou une mélodie empruntée à une autre famille, transposée à mode égal), tuile, écrit les marqueurs, puis réutilise tel quel le classement fiabilité-d'abord deforge.py(_select_and_report,--axes,--shortlist, exit 2 comme gate). Seule la batterie se prête d'une famille à l'autre : elle n'a pas de tonalité.python3 examples/loop_index.py ~/Desktop/SAMPLES/MIDI --out /tmp/index.json python3 examples/forge_loops.py /tmp/index.json sortie/ 48 3 --axes --shortlist 5Ce que ça vaut
À matière égale — même pack, plan écrit à la main contre plan cherché :
assemble_naija.py(PLAN à la main)forge_loops.py(48 plans, graine 3)Contre le générateur synthétique, à budget égal (48 candidats, graines 1-2-3), match nul — et c'est la ligne honnête, pas celle qui arrange :
forge.py(synthétique)forge_loops.py(pack)Où ça se joue — et deux hypothèses réfutées
Par groupe d'axes (médianes, 48 candidats) : la forme monte (A 0.846 vs 0.766), la cohérence globale aussi (F 0.649 vs 0.500) — l'apport de la recherche de plan. La mélodie s'effondre : C 0.597 vs 0.834, l'axe 15 (contour) portant presque tout l'écart, 0.471 vs 0.956.
Deux corrections essayées, mesurées, retirées (le constat reste en commentaire dans le code) :
Reste l'explication la plus économique : les lignes d'un pack ne satisfont pas les bandes de tolérance des axes 15 et 19 comme celles de
make_corpus, écrites sous les mêmes hypothèses que les axes. C'est la circularité vue d'un autre angle — le générateur maison a un avantage de naissance sur le juge maison, et il ne se voyait pas tant qu'on ne lui opposait pas de la matière étrangère.Divers
scripts/mesure_tonalite.pycesse de dupliquer la lecture des étiquettes (elle vit dansloop_index.py, le harnais la mesure) — mêmes chiffres après déduplication : 0.400 / 0.334.221 passed, 40 subtests passed·scripts/check.sh→ CHECK OK.🤖 Generated with Claude Code