Skip to content

feat(forge): forge_loops, duels d'écoute — et ce qu'ils disent du classement - #21

Merged
klodynlov merged 4 commits into
mainfrom
feat/forge-loops
Jul 27, 2026
Merged

feat(forge): forge_loops, duels d'écoute — et ce qu'ils disent du classement#21
klodynlov merged 4 commits into
mainfrom
feat/forge-loops

Conversation

@klodynlov

Copy link
Copy Markdown
Owner

Reprend #20 à l'identique (même branche, rebasée sur main après le squash de #19). GitHub a fermé #20 quand sa branche de base a été supprimée au merge, et refuse de la rouvrir ; toute la discussion et les deux comptes rendus de session sont là-bas.

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 de forge.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_corpus sont écrites sous les mêmes hypothèses que les axes qui les notent.

Ce que l'oreille en dit (sessions 6-7, 71 jugements)

tranche écart SMS l'oreille suit le moteur IC95 p unilatéral
écart fort 0.102 – 0.206 18/29 = 62 % 0.44 – 0.77 0.132
écart faible 0.001 – 0.038 13/30 = 43 % 0.27 – 0.61 0.819

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 suivait task_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.py cesse de dupliquer la lecture des étiquettes (elle vit dans loop_index.py) — mêmes chiffres après déduplication.
  • Rien du pack n'entre dans le dépôt : l'index ne contient que des chemins.
  • 227 passed, 40 subtests passed · scripts/check.sh → CHECK OK.

🤖 Generated with Claude Code

klodynlov and others added 4 commits July 27, 2026 17:50
…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>
@klodynlov
klodynlov merged commit 64a390b into main Jul 27, 2026
10 checks passed
@klodynlov
klodynlov deleted the feat/forge-loops branch July 27, 2026 15:55
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