Skip to content

feat(forge): forge_loops — arranger un pack de loops, laisser le SMS trancher - #20

Closed
klodynlov wants to merge 4 commits into
feat/mesure-tonalitefrom
feat/forge-loops
Closed

feat(forge): forge_loops — arranger un pack de loops, laisser le SMS trancher#20
klodynlov wants to merge 4 commits into
feat/mesure-tonalitefrom
feat/forge-loops

Conversation

@klodynlov

Copy link
Copy Markdown
Owner

Empilée sur #19 (base feat/mesure-tonalite) : forge_loops s'appuie sur la lecture d'étiquettes introduite là-bas, et sur ce qu'elle a mesuré.

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_corpus ne 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 de forge.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 5

Ce que ça vaut

À matière égale — même pack, plan écrit à la main contre plan cherché :

Naija Waves score SMS fiabilité
assemble_naija.py (PLAN à la main) 0.760 0.97
forge_loops.py (48 plans, graine 3) 0.839 0.93

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 :

médiane sur 3 graines gagnant meilleur score médiane du peloton
forge.py (synthétique) 0.863 0.884 0.786
forge_loops.py (pack) 0.849 0.893 0.757

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) :

  • pousser l'accompagnement sous la note la plus grave de la mélodie, pour que la voix supérieure lue par Libretto soit bien la mélodie → groupe C inchangé (0.597), meilleur score 0.908 → 0.899 ;
  • faire entrer la mélodie beaucoup plus souvent → groupe C 0.597 → 0.574.

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.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.
  • Tests : fixture de pack minuscule, percussions sur le canal 9, registres, marqueurs, déterminisme à graine égale, et non-contamination de l'index partagé par un emprunt transposé.
  • Rien du pack n'entre dans le dépôt : l'index ne contient que des chemins.
  • 221 passed, 40 subtests passed · scripts/check.sh → CHECK OK.

🤖 Generated with Claude Code

klodynlov and others added 2 commits July 27, 2026 16:14
…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>
@klodynlov

Copy link
Copy Markdown
Owner Author

Session d'écoute : le classement est passé devant une oreille — non concluant

8bc7a78 ajoute le mode duel au protocole d'écoute et consigne la première session.

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. build_duel_tasks oppose deux fichiers distincts, le côté « original » devenant celui que le moteur place devant ; contrôles, équilibrage et anonymat inchangés.

Lot : 48 candidats forge_loops (graine 3), 14 duels en deux tranches d'écart, 4 contrôles, rendu v3-instrument (FluidSynth), positions 9/9.

tranche écart SMS l'oreille suit le moteur IC95
duel_ecart_fort 0.102 – 0.199 43 % (3/7) 0.16 – 0.75
duel_ecart_faible 0.004 – 0.026 57 % (4/7) 0.25 – 0.84
ensemble 50 % (7/14) 0.27 – 0.73

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 resultats_ecoute.md (session 6) le disent maintenant explicitement.

Deux pistes à n = 2, signalées comme telles : candidate_000 (le plus bas score du lot, 0.567) a gagné ses deux duels ; candidate_032 (score 0.813, fiabilité 0.64) a perdu les siens. Si ça se confirmait, la fiabilité pèserait plus que le score dans ce que l'oreille entend — au-delà de ce que la règle « tranche de fiabilité d'abord » capture déjà.

Jugements bruts dans jugements_duels.json, lot reprenable et cumulable. 226 passed.

klodynlov and others added 2 commits July 27, 2026 16:58
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>
@klodynlov

Copy link
Copy Markdown
Owner Author

Session 7 — lot dimensionné, 71 jugements cumulés

504b0a7. 96 candidats forge_loops, 30 duels par tranche, 11 contrôles, les 18 jugements de la session 6 repris et cumulés. Même annotateur, même rendu v3-instrument, 54 min d'écoute effective.

tranche écart SMS l'oreille suit le moteur IC95 p unilatéral
duel_ecart_fort 0.102 – 0.206 18/29 = 62 % 0.44 – 0.77 0.132
duel_ecart_faible 0.001 – 0.038 13/30 = 43 % 0.27 – 0.61 0.819
ensemble 31/59 = 53 % 0.40 – 0.65 0.397

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 forge_loops reste une mesure de charpente, pas une promesse d'écoute.

227 passed. Jugements bruts dans jugements_duels.json, cumulables pour une session 8.

@klodynlov

Copy link
Copy Markdown
Owner Author

Fermée automatiquement par GitHub à la suppression de la branche de base (feat/mesure-tonalite) lors du merge de #19, et non réouvrable. Le contenu est repris à l'identique dans #21, rebasé sur main. Les deux comptes rendus de session d'écoute ci-dessus restent la référence.

klodynlov added a commit that referenced this pull request Jul 27, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant