rendu fractal #183

Merged
Nora merged 14 commits from feature/improve-motion into develop 2026-09-11 09:34:53 +00:00
Owner
No description provided.
rendu fractal
All checks were successful
CI / Renderer - couverture JS (pull_request) Has been skipped
CI / Renderer - audit des dépendances JS (pull_request) Successful in 44s
CI / PHP - tests + static + lint (pull_request) Successful in 6m48s
CI / Renderer - tests + lint JS (pull_request) Successful in 6m56s
bcf6ad9471
Moteur de la future motion vidéo `swirl` : tourner la géométrie tirée sans
consommer de rng ni muter la source, donc même semis de points d'une frame à
l'autre. Isométrie ⇒ contractivité préservée et boucle fermée à 2π.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WeT8rPw7rmPVdZeS17jWA9
Huitième échappatoire meta.motion, pour les algos à cadrage AUTO (famille IFS)
dont `zoom`/`phase` sortiraient des frames identiques : leurs overrides sont
absorbés par le cadrage. Constante seule, le chemin de frames suit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WeT8rPw7rmPVdZeS17jWA9
Knob vidéo `swirl` (rad, défaut 0) : la banque tirée est tournée avant
l'accumulation, sans consommer de rng — le golden de l'estampe ne bouge pas
d'un octet et aucun bump d'engine_version n'est dû. meta déclare la motion, un
tour par clip, et une qualité d'accumulation dégradée pour le dérivé vidéo
(2,5 M points sur 450 px ≈ 0,5 s/frame contre 2,7 s à pleine qualité).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WeT8rPw7rmPVdZeS17jWA9
Rampe linéaire d'angle, un pas avant le tour complet : vitesse angulaire
constante et boucle qui se referme sur la géométrie publiée. Recompute paresseux,
une frame à la fois.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WeT8rPw7rmPVdZeS17jWA9
docs: plan de la motion vidéo swirl (flame-ifs)
All checks were successful
CI / Renderer - couverture JS (pull_request) Has been skipped
CI / Renderer - audit des dépendances JS (pull_request) Successful in 41s
CI / PHP - tests + static + lint (pull_request) Successful in 5m30s
CI / Renderer - tests + lint JS (pull_request) Successful in 5m51s
ff3892536f
Quatre tâches jouées, six restantes, les chiffres mesurés et les réserves. Se
supprime à la livraison.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WeT8rPw7rmPVdZeS17jWA9
Un `assert.deepEqual` qui ÉCHOUE sur deux buffers fait construire à Node un
diff de Myers ligne à ligne. Son `inspect` sort un OCTET PAR LIGNE (`compact:
false`, `maxArrayLength: Infinity`) et `internal/assert/myers_diff` empile un
`new Int32Array(2·(n+m)+1)` cloné à chaque niveau : O((n+m)²) octets. Deux
rasters 90×160 qui diffèrent = 57 600 lignes par côté ⇒ ~100 Go réclamés.

Cette mémoire est EXTERNE au tas V8 : `--max-old-space-size` ne la borne pas,
rien n'arrête le processus, la machine gèle avant l'OOM-killer. Vécu deux fois
le 2026-09-11 en phase RED de la motion `swirl` (52,8 Go utilisés, 564 Mo
libres, commit à 101,88 %, deux reboots durs).

`scripts/capped.sh` lance la commande dans un scope systemd utilisateur
(`MemoryMax=8G`, `MemorySwapMax=0`) : le cgroup compte TOUT, l'emballement est
tué en 137 et le bureau survit. Bonus, V8 dimensionne son tas sur la limite.
Sonde sur `true` d'abord, repli sur la commande nue là où le contrôleur n'est
pas délégué (CI en conteneur), donc `npm test` reste vert partout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QE2RdcPX6NLG13GrSPGjkt
`assert.deepEqual` sur un raster est une mine : tant que c'est vert il ne coûte
rien, le jour où ça rougit Node inspecte les deux buffers UN OCTET PAR LIGNE et
les diffe en Myers, avec un `Int32Array(2·(n+m)+1)` cloné à chaque niveau. Le
coût est O((n+m)²) d'une mémoire EXTERNE au tas V8 : aucune limite V8 ne mord,
le processus mange la RAM de la machine. Un golden 64×64 réclame déjà ~8,6 Go,
deux rasters 90×160 ~100 Go — et `node --test` en joue douze en parallèle.

`test-kit/bytes.js` compare les octets à la main et échoue par
`assert.fail(message)` : rien n'est jamais inspecté, et le message est plus
utile que le diff (index du premier octet qui diffère, combien diffèrent, les
deux sha256). Quatre paires : bytes, frame, frames, plus `digestOf`.

Converti là où la valeur comparée est une image : `artwork-test-kit` (donc les
quatre lois des 70 algorithmes d'un coup), les comparaisons de `render()`
propres à quinze algos, le chemin vidéo (source, draw-on, wipe, band-text),
les trois pipelines et `canvas-raw`. Les `deepEqual` restants portent sur des
objets courts (ops, clés, pixels à 3 composantes) : lisibles et sans danger.

2808/2810 verts ; les deux rouges sont la dérive SIMD libvips connue de
`pipeline-slide`, sur sha256, sans rapport.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QE2RdcPX6NLG13GrSPGjkt
`frameSourceFor` connaît sa quatrième motion à recompute : elle ne déplace ni
la caméra (zoom) ni un champ (phase/pulse) mais la GÉOMÉTRIE tirée. La branche
passe `{ swirl }` au `recompute` injecté, part de la valeur portée par l'œuvre
et honore un `swirlTurns` forcé (`meta.swirlTurns`, par défaut un tour).

Deux tests : le balayage démarre à la géométrie publiée, tourne vraiment et
s'arrête avant 2π (jamais de doublon de la première frame) ; trois tours font
bien trois fois un tour. Comparaison à 1e-9 : la rampe divise par frameCount,
3×(2π·k/n) et 2π·3k/n ne tombent pas sur le même flottant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QE2RdcPX6NLG13GrSPGjkt
Deux volets.

(a) `pipeline-video.js` lit `meta.swirlTurns` et le passe au dispatcher, comme
il le fait déjà pour `zoomFactor`/`phaseCycles`/`pulseCycles`. Jamais un switch
sur le code d'algo.

(b) `recomputeFrameFor` fusionne `meta.videoParams` entre les params gelés et
la surcharge de frame : params gelés < qualité vidéo < surcharge. La vidéo est
un dérivé jetable (hors canonical_hash, hors signature), donc un algo dont une
frame à pleine qualité coûterait des secondes déclare une qualité
d'accumulation réduite réservée au film — flame-ifs : 2,5 M de points sur un
buffer de 450 px au lieu de 20 M sur 2500, soit ~480 ms/frame contre 2670.
L'estampe garde ses valeurs pleines. La signature ne bouge pas : la closure
résolvait déjà le meta.

La trappe est GÉNÉRIQUE et optionnelle : `{ ...params, ...undefined }` vaut
`params`, donc un algo qui n'en déclare pas se recalcule au pixel près.

Quatre tests unitaires plus le bout-en-bout du pipeline, jumeau de celui de
`phase` : la première frame filmée doit tomber sur un recompute fait à la
qualité vidéo — si le pipeline filmait à la qualité de l'estampe, les octets ne
tomberaient pas. Suite renderer 2815/2817 (les deux rouges = dérive SIMD
libvips connue de pipeline-slide).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QE2RdcPX6NLG13GrSPGjkt
Deux changements de cette tranche bougent les octets de la vidéo, donc la
version qui les identifie doit bouger : la motion `swirl` (flame-ifs quitte
ken-burns pour un recompute par frame qui TOURNE sa banque de transfos) et la
trappe générique `meta.videoParams` (qualité d'accumulation réduite réservée au
film). L'entrée de changelog dit les deux, et pourquoi la rotation est la seule
motion qui marche sur un algo qui cadre automatiquement.

Lockstep PHP : `VideoFormatCatalog::VIDEO_ENGINE_VERSION` et son test unitaire.
Un renderer neuf refuse (422) un body estampillé de l'ancienne version et
inversement — `app` et `renderer` se déploient ensemble.

L'estampe ne bouge pas : `swirl` vaut 0 par défaut et n'ajoute aucun `rng()`,
le golden byte-hash de flame-ifs et `ENGINE_VERSION` sont intacts. Pas de
migration, pas de re-rendu, pas de certificat à refaire.

Vérifié : PHPUnit 6/6 sur les deux classes touchées, PHPStan niveau 9 OK, mago
lint OK, suite renderer 2815/2817 (les deux rouges = dérive SIMD libvips connue
de pipeline-slide) et 0 erreur de lint JS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QE2RdcPX6NLG13GrSPGjkt
`CLAUDE.md` annonçait « ken-burns / zoom / phase selon meta.motion » : trois
motions sur huit, la liste avait décroché. Elle dit maintenant que ken-burns
est le repli par défaut et nomme les sept que l'algo peut forcer — draw-on,
spin, zoom, phase, pulse, wipe, swirl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QE2RdcPX6NLG13GrSPGjkt
Un reel flame-ifs complet à la résolution de prod (450 frames, bandes +
musique, ffmpeg réel) : 292,6 s, 650 ms/frame — 35 % au-dessus de l'estimation
du plan, marge ×3,08 sous le timeout de 900 s. Sortie conforme, bandes
correctes, boucle refermée (frames 0 et 449 indiscernables).

Le point dur n'est pas celui qu'annonçait la réserve. Le CPU passe bien : le
goulot est le JS mono-thread, ffmpeg plafonnait à 8,6 % avec onze cœurs libres,
donc les 2 cœurs de prod coûtent peu. C'est la MÉMOIRE qui serre : ≥ 1,83 Go
dans le conteneur (node 0,52 + ffmpeg ≥ 1,31, libx264 preset medium en
1080×1920) pour un `mem_limit: 2gb` partagé avec le print quotidien, les
mockups et les slides.

La réserve sur l'amincissement est corrigée par la mesure : l'écart de densité
va dans les DEUX sens et dépend du tirage (7,3 → 35,4 %, 2,4 → 1,5 %,
66,4 → 57,5 % de pixels encrés entre l'estampe et le mi-tour). Noté au passage :
certains tirages dans les bornes du paramSpec sont vides à tout angle (défaut
préexistant de flame-ifs, pas de la motion), et le rectangle des tirages denses
est l'image de la variation `sinusoidal`, pas un artefact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QE2RdcPX6NLG13GrSPGjkt
infra: give the renderer 3 GB, ffmpeg lives in that container
All checks were successful
CI / Renderer - couverture JS (pull_request) Has been skipped
CI / Renderer - audit des dépendances JS (pull_request) Successful in 47s
CI / PHP - tests + static + lint (pull_request) Successful in 5m52s
CI / Renderer - tests + lint JS (pull_request) Successful in 6m19s
a96bad4b7d
ffmpeg est spawné par encode.js DANS le conteneur renderer : son empreinte
s'ajoute à celle de node sous la même limite. Mesuré sur un reel flame-ifs à la
résolution de prod (1080×1920, 450 frames) : node reste plat à 0,52 Go, mais
libx264 `-preset medium -crf 18` monte à ≥ 1,31 Go de lookahead, soit ≥ 1,83 Go
pour un plafond de 2 Go — et le même conteneur sert au même moment le print
quotidien, les mockups et les slides.

`cpus` reste à 2.0 : le goulot du tournage est le JS mono-thread (ffmpeg
plafonnait à 8,6 % avec onze cœurs libres), des cœurs de plus ne rendraient pas
un reel plus vite.

`compose.yaml` est le seul fichier qui porte les ressources du renderer —
`compose.prod.yaml` n'y déclare qu'un bloc `environment` — donc la prod hérite
des 3 Go. Une limite ne s'applique pas à un conteneur déjà lancé : le go-live
recrée le service, ce que le rebuild de l'image imposait déjà.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QE2RdcPX6NLG13GrSPGjkt
Nora merged commit f6bfadf4de into develop 2026-09-11 09:34:53 +00:00
Nora deleted branch feature/improve-motion 2026-09-11 09:34:53 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Nora/ArtAutomatic!183
No description provided.