framerudiments 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. {framerudiments-0.2.0 → framerudiments-0.3.0}/MANIFEST.in +2 -0
  2. {framerudiments-0.2.0/src/framerudiments.egg-info → framerudiments-0.3.0}/PKG-INFO +23 -1
  3. {framerudiments-0.2.0 → framerudiments-0.3.0}/README.md +22 -0
  4. framerudiments-0.3.0/docs/media/preset-test-cards.png +0 -0
  5. framerudiments-0.3.0/docs/media/test-card-inputs/colour-bars.png +0 -0
  6. framerudiments-0.3.0/docs/media/test-card-inputs/grid-rings.png +0 -0
  7. framerudiments-0.3.0/docs/media/test-card-inputs/text-card.png +0 -0
  8. framerudiments-0.3.0/docs/preset-profiling.md +200 -0
  9. {framerudiments-0.2.0 → framerudiments-0.3.0}/docs/visualisers.md +12 -0
  10. framerudiments-0.3.0/integration/test_preset_analysis.py +42 -0
  11. {framerudiments-0.2.0 → framerudiments-0.3.0}/integration/test_visualisers_gpu.py +16 -0
  12. {framerudiments-0.2.0 → framerudiments-0.3.0}/pyproject.toml +1 -1
  13. framerudiments-0.3.0/scripts/profile_visualisers.py +10 -0
  14. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/__init__.py +1 -1
  15. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/cli.py +31 -0
  16. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/io.py +11 -2
  17. framerudiments-0.3.0/src/framerudiments/preset_library.py +132 -0
  18. framerudiments-0.3.0/src/framerudiments/preset_profiling.py +296 -0
  19. framerudiments-0.3.0/src/framerudiments/profile_analysis.py +212 -0
  20. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/projectm_backend.py +18 -2
  21. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/visualiser_assets/butterchurn-page.js +16 -1
  22. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/visualiser_assets/butterchurn-worker.mjs +16 -2
  23. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/visualisers.py +12 -4
  24. {framerudiments-0.2.0 → framerudiments-0.3.0/src/framerudiments.egg-info}/PKG-INFO +23 -1
  25. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments.egg-info/SOURCES.txt +11 -0
  26. framerudiments-0.3.0/tests/test_preset_profiling.py +123 -0
  27. {framerudiments-0.2.0 → framerudiments-0.3.0}/AGENTS.md +0 -0
  28. {framerudiments-0.2.0 → framerudiments-0.3.0}/LICENSE +0 -0
  29. {framerudiments-0.2.0 → framerudiments-0.3.0}/docs/beat-alignment.md +0 -0
  30. {framerudiments-0.2.0 → framerudiments-0.3.0}/docs/extraction.md +0 -0
  31. {framerudiments-0.2.0 → framerudiments-0.3.0}/docs/media/beat-alignment-evidence.json +0 -0
  32. {framerudiments-0.2.0 → framerudiments-0.3.0}/docs/media/beat-alignment.mp4 +0 -0
  33. {framerudiments-0.2.0 → framerudiments-0.3.0}/docs/media/beat-alignment.png +0 -0
  34. {framerudiments-0.2.0 → framerudiments-0.3.0}/docs/media/demo-transcript.md +0 -0
  35. {framerudiments-0.2.0 → framerudiments-0.3.0}/docs/media/effects-pipeline.svg +0 -0
  36. {framerudiments-0.2.0 → framerudiments-0.3.0}/docs/media/full-frame-comparison.png +0 -0
  37. {framerudiments-0.2.0 → framerudiments-0.3.0}/docs/media/full-frame-demo.mp4 +0 -0
  38. {framerudiments-0.2.0 → framerudiments-0.3.0}/docs/media/media-evidence.json +0 -0
  39. {framerudiments-0.2.0 → framerudiments-0.3.0}/docs/project-format.md +0 -0
  40. {framerudiments-0.2.0 → framerudiments-0.3.0}/docs/rudiment-renderers.md +0 -0
  41. {framerudiments-0.2.0 → framerudiments-0.3.0}/docs/spectrum.md +0 -0
  42. {framerudiments-0.2.0 → framerudiments-0.3.0}/docs/workflow-media.md +0 -0
  43. {framerudiments-0.2.0 → framerudiments-0.3.0}/setup.cfg +0 -0
  44. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/__main__.py +0 -0
  45. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/atlas.py +0 -0
  46. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/beat_alignment.py +0 -0
  47. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/choreography.py +0 -0
  48. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/compositor.py +0 -0
  49. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/demo.py +0 -0
  50. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/effects.py +0 -0
  51. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/encoding.py +0 -0
  52. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/flight.py +0 -0
  53. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/motion.py +0 -0
  54. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/project.py +0 -0
  55. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/render.py +0 -0
  56. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/rudiment_renderers.py +0 -0
  57. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/spectrum.py +0 -0
  58. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/stemlab.py +0 -0
  59. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/subtitles.py +0 -0
  60. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/timeline.py +0 -0
  61. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/visualiser_assets/feedback.json +0 -0
  62. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/visualiser_assets/feedback.milk +0 -0
  63. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/visualiser_assets/julia.json +0 -0
  64. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/visualiser_assets/julia.milk +0 -0
  65. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/visualiser_assets/kaleidoscope.json +0 -0
  66. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/visualiser_assets/kaleidoscope.milk +0 -0
  67. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/visualiser_assets/package-lock.json +0 -0
  68. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/visualiser_assets/package.json +0 -0
  69. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments/visualiser_setup.py +0 -0
  70. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments.egg-info/dependency_links.txt +0 -0
  71. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments.egg-info/entry_points.txt +0 -0
  72. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments.egg-info/requires.txt +0 -0
  73. {framerudiments-0.2.0 → framerudiments-0.3.0}/src/framerudiments.egg-info/top_level.txt +0 -0
  74. {framerudiments-0.2.0 → framerudiments-0.3.0}/tests/test_adapters.py +0 -0
  75. {framerudiments-0.2.0 → framerudiments-0.3.0}/tests/test_beat_alignment.py +0 -0
  76. {framerudiments-0.2.0 → framerudiments-0.3.0}/tests/test_extracted_helpers.py +0 -0
  77. {framerudiments-0.2.0 → framerudiments-0.3.0}/tests/test_flight.py +0 -0
  78. {framerudiments-0.2.0 → framerudiments-0.3.0}/tests/test_lyrics.py +0 -0
  79. {framerudiments-0.2.0 → framerudiments-0.3.0}/tests/test_pipeline.py +0 -0
  80. {framerudiments-0.2.0 → framerudiments-0.3.0}/tests/test_rudiment_renderers.py +0 -0
  81. {framerudiments-0.2.0 → framerudiments-0.3.0}/tests/test_spectrum.py +0 -0
  82. {framerudiments-0.2.0 → framerudiments-0.3.0}/tests/test_timeline.py +0 -0
  83. {framerudiments-0.2.0 → framerudiments-0.3.0}/tests/test_visualisers.py +0 -0
@@ -5,3 +5,5 @@ recursive-include tests *.py
5
5
  recursive-include integration *.py
6
6
 
7
7
  prune models
8
+
9
+ recursive-include scripts *.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: framerudiments
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Deterministic Shorts and lyric visualisers using DanceRudiments and StemLab evidence.
5
5
  Author: Kieran Simkin
6
6
  License-Expression: MIT
@@ -242,3 +242,25 @@ Playback is user-controlled; no automatic media playback is required.
242
242
 
243
243
  Generated vector illustration of the processing order. The comparison above
244
244
  and linked video are captured renderer output; the diagram is explanatory.
245
+
246
+ ## Explore and profile the preset library
247
+
248
+ Version 0.3.0 adds a separately installed, checksum-pinned library of **14,680
249
+ distinct upstream presets** (1,738 Butterchurn and 12,942 projectM), with source
250
+ notices, duplicate aliases and 67 shared textures. Real-engine acceptance is
251
+ recorded per preset; available catalogue entries are not claimed as tested.
252
+
253
+ Use `framerudiments presets-install`, then `framerudiments presets-profile` or
254
+ `scripts/profile_visualisers.py` to feed ordinary cover/lyric/audio projects and
255
+ three static calibration cards through each engine, replay every feedback frame,
256
+ capture chosen intervals and invoke released PrismMoves/PixelCue. Reports keep
257
+ input/output, temporal, tune/silence and same-card/different-tune comparisons
258
+ separate, with measured response ranges and explicit unknowns.
259
+
260
+ ![Generated static input cards for controlled preset experiments](https://raw.githubusercontent.com/kieransimkin/FrameRudiments/v0.3.0/docs/media/preset-test-cards.png)
261
+
262
+ Generated calibration illustrations; not preset-output evidence. The maintained
263
+ My Songs suite uses Arcadians and The Truth is a Virus with their canonical
264
+ masters, covers and timed lyrics; those private assets are not bundled.
265
+ See [the complete capture, analysis and synthesis workflow](https://github.com/kieransimkin/FrameRudiments/blob/v0.3.0/docs/preset-profiling.md)
266
+ for pinned packs, licences, runtime setup, resume and interpretation limits.
@@ -217,3 +217,25 @@ Playback is user-controlled; no automatic media playback is required.
217
217
 
218
218
  Generated vector illustration of the processing order. The comparison above
219
219
  and linked video are captured renderer output; the diagram is explanatory.
220
+
221
+ ## Explore and profile the preset library
222
+
223
+ Version 0.3.0 adds a separately installed, checksum-pinned library of **14,680
224
+ distinct upstream presets** (1,738 Butterchurn and 12,942 projectM), with source
225
+ notices, duplicate aliases and 67 shared textures. Real-engine acceptance is
226
+ recorded per preset; available catalogue entries are not claimed as tested.
227
+
228
+ Use `framerudiments presets-install`, then `framerudiments presets-profile` or
229
+ `scripts/profile_visualisers.py` to feed ordinary cover/lyric/audio projects and
230
+ three static calibration cards through each engine, replay every feedback frame,
231
+ capture chosen intervals and invoke released PrismMoves/PixelCue. Reports keep
232
+ input/output, temporal, tune/silence and same-card/different-tune comparisons
233
+ separate, with measured response ranges and explicit unknowns.
234
+
235
+ ![Generated static input cards for controlled preset experiments](https://raw.githubusercontent.com/kieransimkin/FrameRudiments/v0.3.0/docs/media/preset-test-cards.png)
236
+
237
+ Generated calibration illustrations; not preset-output evidence. The maintained
238
+ My Songs suite uses Arcadians and The Truth is a Virus with their canonical
239
+ masters, covers and timed lyrics; those private assets are not bundled.
240
+ See [the complete capture, analysis and synthesis workflow](https://github.com/kieransimkin/FrameRudiments/blob/v0.3.0/docs/preset-profiling.md)
241
+ for pinned packs, licences, runtime setup, resume and interpretation limits.
@@ -0,0 +1,200 @@
1
+ # Preset libraries and input-response profiles
2
+
3
+ FrameRudiments installs **14,680 distinct upstream presets** separately from the
4
+ Python package: **1,738 Butterchurn** and **12,942 projectM** entries. Original
5
+ names remain searchable aliases after exact-byte deduplication. The five preset
6
+ packs contain 16,334 files before deduplication; 67 shared textures accompany them.
7
+ The six existing built-in presets (three per engine) remain available through
8
+ the ordinary project format.
9
+
10
+ ![Generated calibration cards used as static inputs](media/preset-test-cards.png)
11
+
12
+ Generated calibration illustrations: colour bars with grayscale ramp,
13
+ asymmetric grid/rings, and light/dark text. These are input cards, not evidence
14
+ of a preset's appearance. Their PNG hashes are recorded with each experiment.
15
+
16
+ The [actual four-second full-frame demonstration](https://kieransimkin.github.io/DanceFlow/framerudiments.html)
17
+ shows the composition/effect pipeline, with user-controlled video playback and
18
+ an accompanying transcript. It uses the earlier synthetic software demo;
19
+ profiling inputs are supplied separately and are never silently substituted.
20
+
21
+ ## Install the complete pinned library
22
+
23
+ ```powershell
24
+ framerudiments presets-install local/preset-library
25
+ ```
26
+
27
+ Installation explicitly downloads upstream executable equations/shaders. No
28
+ download happens during ordinary rendering. Every archive is SHA-256 pinned;
29
+ the catalogue retains source commits, notices, aliases, preset hashes and texture
30
+ hashes. Packages and caches remain outside the wheel/sdist. The installer reads
31
+ bounded ordinary archive members and writes content-addressed preset filenames,
32
+ avoiding original-path traversal and Windows filename/length problems.
33
+
34
+ | Pack | Source pin | Files before deduplication |
35
+ |---|---|---:|
36
+ | Butterchurn presets, all six stable bundles | npm 2.4.7 | 1,754 |
37
+ | Cream of the Crop | 0180df21f5e0bd39b9060cc5de420ed2f1f9e509 | 9,795 |
38
+ | Classic projectM | 14a6244a7d32eb7e114e1a92d1cb93358cdcc54a | 4,193 |
39
+ | Original MilkDrop | e03b83e3338d8f1ed6cbcf908c719f249ef24288 | 552 |
40
+ | En D | fff71ea81223109f3558351667eef851f2781c96 | 40 |
41
+ | Shared textures | 6368812f27bc747b517218fbf89d21d59afce4d9 | 67 images |
42
+
43
+ Butterchurn 2.6.7 accepts the stable pack's JavaScript-equation format. The
44
+ upstream 3.0 beta pack uses EEL fields and is not represented as compatible with
45
+ this engine. projectM remains pinned to unmodified development commit
46
+ `349b7cc2db312d47cef6be24c6856ced6643a385` (4.2.0-dev).
47
+
48
+ Butterchurn's pack supplies an MIT notice. Some projectM packs supply no explicit
49
+ licence grant; Cream/En D describe a historical public-domain assumption, which
50
+ does not establish author permission. Keep their exact notices and provenance.
51
+ FrameRudiments does not relicense or redistribute these packs as its own MIT
52
+ assets. Source availability, shader compatibility and public-use eligibility are
53
+ different states. Additional community packs require their own provenance and
54
+ format review; raw pack size alone does not establish useful coverage.
55
+
56
+ ## Supply the two tunes, covers and lyrics
57
+
58
+ Create an ordinary untreated FrameRudiments project for each sample, with pinned
59
+ audio, cover background, font and timed LRC. Select explicit source sample bounds
60
+ covering the desired excerpt. Use saved StemLab evidence when choosing musical
61
+ sections or motion; the profiling runner does not estimate tempo or retime lyrics.
62
+ Native DanceRudiments sprite/atlas layers can remain in these projects and enter
63
+ the same full-frame processing path. The static-card conditions remove authored
64
+ layers and lyrics to isolate the calibration imagery.
65
+
66
+ ```json
67
+ {
68
+ "schema": "framerudiments.preset-suite.v1",
69
+ "projects": [
70
+ {"path": "arcadians-project.json", "sha256": "EXACT_PROJECT_SHA256"},
71
+ {"path": "truth-project.json", "sha256": "EXACT_PROJECT_SHA256"}
72
+ ],
73
+ "input_mix": 0.35,
74
+ "seed": 123
75
+ }
76
+ ```
77
+
78
+ For the maintained My Songs experiment the selected tunes are **Arcadians** and
79
+ **The Truth is a Virus**, with their canonical masters, covers and manually timed
80
+ lyrics. Their private suite and evidence stay outside the public repository.
81
+ An optional `assets` list pins additional canonical/provenance files. Preserve
82
+ the original LRC if an export needs a documented parser-input adapter; never
83
+ silently alter its words or timings. An estimated clock used by a static test
84
+ does not become an approved musical grid.
85
+
86
+ ## Capture, analyse and resume
87
+
88
+ Set up the two optional engines as described in [visualisers.md](visualisers.md).
89
+ Use a separate Python 3.13 interpreter with released **PrismMoves 0.1.0** and
90
+ **PixelCue 0.9.0**, plus NumPy/Pillow, for analysis. This keeps heavy optional
91
+ analysis dependencies out of the renderer's base installation. Existing local
92
+ model-free APIs are sufficient; no model download is needed.
93
+
94
+ ```powershell
95
+ framerudiments presets-profile local/suite.json local/preset-library/catalogue.json `
96
+ --output local/response-run --intervals 0 1 2 4 `
97
+ --prism-python path/to/prism-python
98
+
99
+ # Equivalent script entrypoint in the source distribution:
100
+ python scripts/profile_visualisers.py local/suite.json local/preset-library/catalogue.json `
101
+ --output local/response-run --intervals 0 1 2 4 --prism-python path/to/prism-python
102
+ ```
103
+
104
+ Intervals are output seconds, rounded up to the next available frame and rejected
105
+ outside the programme. Every preceding frame is replayed sequentially. Sparse
106
+ captures skip unnecessary GPU readback; they do not skip feedback simulation.
107
+ Each preset/condition starts a clean engine with the same mix, frame cadence,
108
+ resolution and authored seed. projectM has no public seed API, so its random
109
+ state remains an explicit confound. GPU/driver output is not cross-platform
110
+ bit-identical.
111
+
112
+ Default conditions cross each sample with cover/timed-lyrics, colour bars,
113
+ grid/rings and text card, then programme audio and all-zero PCM controls: **16
114
+ conditions for two samples**. Programme PCM uses the encoder's exact native-rate
115
+ trim/repeat/fade filters and causal windows. Album imagery and captions undergo
116
+ the same effects as the rest of the composition. Static cards do not inherit
117
+ album colours or captions. The catalogue's verified textures are attached to the
118
+ actual engine APIs, with runtime receipts.
119
+
120
+ Use `--id EXACT_CATALOGUE_ID` repeatedly for a selected batch, or `--limit N`
121
+ for a pilot; omitted limits select the full catalogue. `--image cover-lyrics`
122
+ and `--audio-mode programme` narrow a declared experiment. Changing conditions
123
+ requires a new output directory. A full sweep of thousands of presets is an
124
+ explicit substantial workload: benchmark a representative batch first, retain
125
+ elapsed time and storage evidence, then use the chosen settings consistently.
126
+ Lowering frame rate changes feedback behaviour and must remain a labelled
127
+ preview, with `fps_reason`, rather than a production-appearance claim.
128
+
129
+ Re-run the identical command with `--resume` to reuse verified captures. Add
130
+ `--retry-failed` for fresh attempts while retaining failures and older profiles.
131
+ Changed inputs, runtime binaries, host bridges, textures, settings or capture
132
+ bytes prevent stale reuse. Failed shaders are retained explicitly; native shader
133
+ fallback errors are not accepted as a successful preset render.
134
+
135
+ ## Assemble one coherent response profile
136
+
137
+ 1. Read `experiment.json` and `index.json` for exact scope and coverage. Available
138
+ catalogue entries are not completed profiles. Every preset has `captures.json`
139
+ with per-condition times, hashes, PCM and engine/cleanup receipts.
140
+ 2. Open `identity/report.html` to inspect matching input/output frames and the
141
+ original condition families. `identity/prism-*/` contains the actual PrismMoves
142
+ reports, measured palette graphics, PixelCue evidence and checksums. Statistics
143
+ remain observations; optional `--model clip`, `dinov2`, `blip` or `ocr` outputs
144
+ remain interpretations, with unavailable receipts. Models load local weights
145
+ only under the experiment's `models/`; this runner never enables downloads.
146
+ 3. Use `identity/response.json` for source-to-output, successive-capture,
147
+ programme-versus-silence and same-card/different-tune comparisons. PixelCue's
148
+ `compare_profiles` supplies separate hue, texture and saturation distances and
149
+ supported embedding similarities. Named PixelCue luminance, edge/mirror,
150
+ texture/co-occurrence and spatial-frequency values also retain signed
151
+ input/output changes and observed ranges. No private analysis engine or invented
152
+ overall identity score is introduced.
153
+ 4. Review palette, geometry, texture, text treatment and temporal behaviour for
154
+ each family before synthesising. Describe a visual thesis supported by named
155
+ conditions; retain controlled variation and outliers. Frame grabs from one
156
+ run share a lineage and are not independent artistic authorities. Keep a
157
+ response range when tunes or cards differ rather than averaging incompatible
158
+ looks into one claimed invariant.
159
+ 5. Record remaining unknowns and production review: glyph legibility, safe zones,
160
+ sustained motion, flash safety, exact final frame rate, audio listening,
161
+ resource warnings and public-use rights. Measurements cannot establish these.
162
+ A failed preset receives a failure/unknown record, not an invented identity.
163
+
164
+ ## Potential problems
165
+
166
+ - Thousands of available entries have not all been GPU accepted. Inspect each
167
+ capture state and runtime warnings; missing custom textures and old shader
168
+ dialects can produce failures or an altered appearance.
169
+ - Native GPU/driver stalls may still require operator recovery; a completed
170
+ Python report is not evidence that an interrupted job cleaned up. The normal
171
+ Butterchurn route closes its owned headless browser and removes only its
172
+ freshly allocated profile; projectM destroys its native instance/window.
173
+ - A report with unavailable neural/OCR models still has useful statistics.
174
+ Keep it partial; installing random/untrained checkpoints would not establish
175
+ visual understanding. No paid inference or automatic model sweep is implied.
176
+ - Comparison settings (matte, ICC handling, analysis edge, frame rate, source
177
+ offset, input mix) influence results. Preserve them across a comparison batch.
178
+ - Source PNGs and processed PNGs are renderer evidence, not decoded-video QA.
179
+ Encode and inspect the selected production preset using the normal render
180
+ pipeline before publishing a campaign video.
181
+
182
+ Evidence JSON writes use same-directory temporary files and atomic replacement,
183
+ so a failed update preserves the previous valid receipt. Disk exhaustion can
184
+ still leave incomplete PNGs or reports: retain the interrupted attempt and
185
+ resume only after hash validation, or start a fresh output directory.
186
+
187
+ On Windows, Chromium's internal cache filenames can exceed its path limit even
188
+ when Python can write the evidence folder. Profiling creates fresh profiles in
189
+ the short experiment-level `.browser/` directory, with normal verified cleanup.
190
+ For a deeply nested output root set `FRAMERUDIMENTS_BROWSER_PROFILE_ROOT` to a
191
+ short writable project-local folder. Existing user browser profiles are never
192
+ opened, read or reused.
193
+
194
+ To use a catalogue entry in an ordinary render, copy its verified `preset`
195
+ `{path, sha256}` reference into an effect stage with `engine` and
196
+ `trusted_preset: true` after the explicit library review. Set
197
+ `FRAMERUDIMENTS_TEXTURES_DIR` to the installed library's `textures/` directory
198
+ when the preset needs it; the runtime records image hashes and native warnings.
199
+ The profiler does this texture attachment automatically from the verified
200
+ catalogue. Keep public-use eligibility separate from rendering acceptance.
@@ -175,3 +175,15 @@ Playback is user-controlled; no automatic media playback is required.
175
175
 
176
176
  Generated vector illustration of the processing order. The comparison above
177
177
  and linked video are captured renderer output; the diagram is explanatory.
178
+
179
+ ## Expanded libraries and response experiments
180
+
181
+ The [preset profiling workflow](preset-profiling.md) installs the complete pinned
182
+ upstream catalogue and uses matching-time captures, three static test cards,
183
+ programme/silence controls and released PrismMoves/PixelCue comparisons.
184
+
185
+ ![Generated calibration input cards](media/preset-test-cards.png)
186
+
187
+ Generated input illustrations. Per-preset rendered captures and statistical
188
+ reports are retained separately by each experiment; catalogue presence does
189
+ not establish GPU compatibility or public-use rights.
@@ -0,0 +1,42 @@
1
+ """Actual PrismMoves/PixelCue measurements on sequential native-engine captures."""
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+
6
+ from PIL import Image
7
+
8
+ from framerudiments.demo import create_demo
9
+ from framerudiments.io import read_json, write_json, sha256
10
+ from framerudiments.visualisers import ASSETS
11
+ from framerudiments.preset_profiling import profile_presets
12
+ from framerudiments.profile_analysis import summarise
13
+
14
+
15
+ def test_real_prism_pixelcue_response_synthesis_and_controls(tmp_path):
16
+ project = create_demo(tmp_path/'sample', os.environ['FRAMERUDIMENTS_TEST_FONT'], width=128, height=128)
17
+ cover = project.parent/'cover.png'
18
+ Image.new('RGB', (128,128), 'navy').save(cover)
19
+ data = read_json(project)
20
+ data['background'] = dict(image=dict(path=str(cover),sha256=sha256(cover)))
21
+ data['layers'], data['identity'] = [], []
22
+ data['audio'].update(end_sample=48000,fade_seconds=0)
23
+ write_json(project,data)
24
+ suite = tmp_path/'suite.json'
25
+ write_json(suite,dict(schema='framerudiments.preset-suite.v1',projects=[dict(path=str(project),sha256=sha256(project))]))
26
+ preset=ASSETS/'feedback.milk'
27
+ catalogue=tmp_path/'catalogue.json'
28
+ write_json(catalogue,dict(schema='framerudiments.preset-library.v1',presets=[dict(id='native',name='Native fixture',engine='projectm',preset=dict(path=str(preset),sha256=sha256(preset)))]))
29
+ result=profile_presets(suite,catalogue,tmp_path/'run',intervals=[0,.1],images=['cover-lyrics','grid-rings'])
30
+ response=summarise(result[0]['manifest'],max_edge=64,models_dir=tmp_path/'models')
31
+ assert response['state']=='completed'
32
+ assert len(response['conditions'])==4
33
+ assert len(response['controlled_comparisons'])==2
34
+ assert all(len(x['source_to_output'])==2 for x in response['conditions'])
35
+ assert all(len(x['temporal'])==1 for x in response['conditions'])
36
+ assert response['settings']['versions']['prismmoves']=='0.1.0'
37
+ assert response['settings']['versions']['pixelcue']=='0.9.0'
38
+ assert all('median' in x for x in response['measured_response_ranges'].values())
39
+ assert summarise(result[0]['manifest'],max_edge=64,models_dir=tmp_path/'models')==response
40
+ report=Path(result[0]['manifest']).parent/'identity/report.html'
41
+ assert report.is_file() and 'matching-time capture' in report.read_text(encoding='utf-8')
42
+ assert json.loads((report.parent/'response.json').read_text())['settings']==response['settings']
@@ -133,3 +133,19 @@ def test_projectm_shader_error_cannot_silently_fall_back(tmp_path, kind):
133
133
  path.write_text(preset, encoding="utf-8")
134
134
  with pytest.raises(RuntimeError, match="projectM preset failed"):
135
135
  ProjectMBackend((160, 240), 60, path, .8, 0, tmp_path)
136
+
137
+
138
+ @pytest.mark.parametrize("factory,extension", [(ProjectMBackend,"milk"),(ButterchurnBackend,"json")])
139
+ def test_sparse_capture_advances_real_feedback_and_retains_frame_clock(tmp_path,factory,extension):
140
+ image=glyph_frame()
141
+ backend=factory(image.size,60,ASSETS/f"feedback.{extension}",.75,12,tmp_path)
142
+ pcm=array("f",[0]*2048)
143
+ try:
144
+ for n in range(7):
145
+ assert backend.advance(image,SimpleNamespace(frame=n,output_seconds=n/60),pcm,capture=False) is None
146
+ final=backend.advance(image,SimpleNamespace(frame=7,output_seconds=7/60),pcm,capture=True)
147
+ assert final.mode=="RGBA" and final.size==image.size
148
+ assert final.getpixel((35,30))[0]>final.getpixel((35,30))[2]+80
149
+ finally:
150
+ backend.close()
151
+ assert all(backend.evidence["cleanup"].values())
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "framerudiments"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Deterministic Shorts and lyric visualisers using DanceRudiments and StemLab evidence."
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
@@ -0,0 +1,10 @@
1
+ """Feed projects/audio to presets, capture intervals and invoke PrismMoves.
2
+
3
+ Usage: python scripts/profile_visualisers.py SUITE CATALOGUE --output NEW_DIRECTORY
4
+ All remaining options are the maintained `framerudiments presets-profile` options.
5
+ """
6
+ import sys
7
+ from framerudiments.cli import main
8
+
9
+ if __name__ == "__main__":
10
+ raise SystemExit(main(["presets-profile", *sys.argv[1:]]))
@@ -1,5 +1,5 @@
1
1
  """Offline frame composition on a traceable musical clock."""
2
- __version__ = "0.2.0"
2
+ __version__ = "0.3.0"
3
3
 
4
4
  from .project import Project
5
5
  from .render import render, preflight
@@ -21,6 +21,24 @@ def main(argv=None):
21
21
  setup.add_argument("directory")
22
22
  setup.add_argument("--cmake-arg", action="append", default=[])
23
23
  setup.add_argument("--parallel", type=int, default=4)
24
+ libraries = commands.add_parser("presets-install", help="Install separately pinned upstream executable preset libraries")
25
+ libraries.add_argument("directory")
26
+ libraries.add_argument("--pack", action="append", default=[])
27
+ libraries.add_argument("--cache", help="Optional folder of exact checksum-pinned archives")
28
+ profiling = commands.add_parser("presets-profile", help="Replay sample covers/lyrics, static cards and audio; capture and profile preset response")
29
+ profiling.add_argument("suite")
30
+ profiling.add_argument("catalogue")
31
+ profiling.add_argument("--output", required=True)
32
+ profiling.add_argument("--intervals", nargs="+", type=float, default=[0, 1, 2, 4])
33
+ profiling.add_argument("--image", action="append", default=[])
34
+ profiling.add_argument("--audio-mode", action="append", choices=("programme", "silence"), default=[])
35
+ profiling.add_argument("--id", action="append", default=[])
36
+ profiling.add_argument("--limit", type=int)
37
+ profiling.add_argument("--resume", action="store_true")
38
+ profiling.add_argument("--retry-failed", action="store_true")
39
+ profiling.add_argument("--prism-python", help="Python with released PrismMoves and PixelCue installed; no model downloads")
40
+ profiling.add_argument("--model", action="append", choices=("clip", "dinov2", "blip", "ocr"), default=[])
41
+ profiling.add_argument("--max-edge", type=int, default=256)
24
42
  commands.add_parser("renderers", help="List available built-in rudiment renderers")
25
43
  demo = commands.add_parser("demo", help="Create a synthetic audio/image/recording project")
26
44
  demo.add_argument("directory")
@@ -63,6 +81,19 @@ def main(argv=None):
63
81
  result = (setup_butterchurn(args.directory) if args.engine == "butterchurn" else
64
82
  setup_projectm(args.directory, cmake_args=args.cmake_arg, parallel=args.parallel))
65
83
  print(format_result(result))
84
+ elif args.command == "presets-install":
85
+ from .preset_library import PACKS, install_library
86
+ result = install_library(args.directory, packs=args.pack or tuple(PACKS), cache=args.cache)
87
+ print(json.dumps(dict(presets=len(result["presets"]), textures=len(result["textures"]))))
88
+ elif args.command == "presets-profile":
89
+ from .preset_profiling import CARDS, profile_presets
90
+ result = profile_presets(args.suite, args.catalogue, args.output, intervals=args.intervals,
91
+ images=args.image or ("cover-lyrics", *CARDS), audio_modes=args.audio_mode or ("programme", "silence"),
92
+ ids=args.id, limit=args.limit, resume=args.resume, retry_failed=args.retry_failed,
93
+ prism_python=args.prism_python, models=args.model, max_edge=args.max_edge)
94
+ print(json.dumps(dict(presets=len(result), captures=sum(x["state"] == "captured" for x in result))))
95
+ if any(x["state"] != "captured" or args.prism_python and x["analysis_state"] != "completed" for x in result):
96
+ return 2
66
97
  elif args.command == "renderers":
67
98
  print(json.dumps(default_renderers().names))
68
99
  elif args.command == "plan":
@@ -2,6 +2,8 @@
2
2
  import hashlib
3
3
  import json
4
4
  import math
5
+ import os
6
+ import tempfile
5
7
  from pathlib import Path
6
8
 
7
9
 
@@ -28,8 +30,15 @@ def read_json(path):
28
30
 
29
31
 
30
32
  def write_json(path, value):
31
- Path(path).write_text(json.dumps(value, ensure_ascii=False, allow_nan=False, indent=2) + "\n",
32
- encoding="utf-8")
33
+ path = Path(path)
34
+ text = json.dumps(value, ensure_ascii=False, allow_nan=False, indent=2) + "\n"
35
+ fd, temporary = tempfile.mkstemp(prefix="." + path.name + ".", suffix=".tmp", dir=path.parent)
36
+ try:
37
+ with os.fdopen(fd, "w", encoding="utf-8") as stream:
38
+ stream.write(text)
39
+ os.replace(temporary, path)
40
+ finally:
41
+ Path(temporary).unlink(missing_ok=True)
33
42
 
34
43
 
35
44
  def number(value, name, *, minimum=None, maximum=None):
@@ -0,0 +1,132 @@
1
+ """Pinned, separately installed upstream preset libraries; never bundled as MIT assets."""
2
+ import hashlib
3
+ import io
4
+ import json
5
+ from pathlib import Path, PurePosixPath
6
+ import tarfile
7
+ import urllib.request
8
+ import zipfile
9
+
10
+ from .io import sha256, write_json
11
+
12
+ PACKS = {
13
+ "butterchurn": dict(engine="butterchurn", version="2.4.7", licence="MIT",
14
+ url="https://registry.npmjs.org/butterchurn-presets/-/butterchurn-presets-2.4.7.tgz",
15
+ sha256="d9c067b354e4ed87b3a1dace1dc59c9d64e6a99300a82d5fa086f696e35204a7"),
16
+ "cream": dict(engine="projectm", repository="projectM-visualizer/presets-cream-of-the-crop",
17
+ commit="0180df21f5e0bd39b9060cc5de420ed2f1f9e509",
18
+ sha256="5600f974497d8f73295393fbdf866895ceb89efe82892f5e2d019358b2659202"),
19
+ "classic": dict(engine="projectm", repository="projectM-visualizer/presets-projectm-classic",
20
+ commit="14a6244a7d32eb7e114e1a92d1cb93358cdcc54a",
21
+ sha256="328a41acf9216057fce0d873b244ca06e1aee5b466f7560df4903fdf28832ad0"),
22
+ "original": dict(engine="projectm", repository="projectM-visualizer/presets-milkdrop-original",
23
+ commit="e03b83e3338d8f1ed6cbcf908c719f249ef24288",
24
+ sha256="4479bbd10e4f4a005d636f7b0ed26004042541a62a925ca97e12ff13765b688d"),
25
+ "en-d": dict(engine="projectm", repository="projectM-visualizer/presets-en-d",
26
+ commit="fff71ea81223109f3558351667eef851f2781c96",
27
+ sha256="6badaf79e1ff095a5b56d38a380dd9b9ac8c2b5c6f6770879cbc0f65d549b5e1"),
28
+ "textures": dict(engine="textures", repository="projectM-visualizer/presets-milkdrop-texture-pack",
29
+ commit="6368812f27bc747b517218fbf89d21d59afce4d9",
30
+ sha256="38e93612f79a313830480d7458d8eb065d2b67992211a5dd0c92ef87b8607f6a"),
31
+ }
32
+ for _pin in PACKS.values():
33
+ if "url" not in _pin:
34
+ _pin["url"] = f"https://codeload.github.com/{_pin['repository']}/zip/{_pin['commit']}"
35
+ _pin["licence"] = "No explicit preset/texture grant established; retain upstream notices; not redistributed in FrameRudiments"
36
+
37
+
38
+ def archive_members(data, *, tar=False):
39
+ """Read bounded ordinary files only; never extract paths, links or devices."""
40
+ if tar:
41
+ archive = tarfile.open(fileobj=io.BytesIO(data))
42
+ entries = ((x.name, x.size, lambda x=x: archive.extractfile(x).read())
43
+ for x in archive.getmembers() if x.isfile())
44
+ else:
45
+ archive = zipfile.ZipFile(io.BytesIO(data))
46
+ entries = ((x.filename, x.file_size, lambda x=x: archive.read(x))
47
+ for x in archive.infolist() if not x.is_dir())
48
+ total = 0
49
+ with archive:
50
+ for name, size, read in entries:
51
+ path = PurePosixPath(name)
52
+ if path.is_absolute() or ".." in path.parts or "\\" in name or ":" in name:
53
+ raise ValueError(f"Unsafe archive member: {name}")
54
+ total += size
55
+ if size > 32 * 1024**2 or total > 512 * 1024**2:
56
+ raise ValueError("Preset archive exceeds bounded extraction size")
57
+ yield str(PurePosixPath(*path.parts[1:])), read()
58
+
59
+
60
+ def install_library(directory, *, packs=tuple(PACKS), cache=None):
61
+ """Explicit download/installation. Calling this acknowledges upstream executable presets."""
62
+ out = Path(directory).resolve()
63
+ if out.exists():
64
+ raise ValueError("Choose a new preset-library directory")
65
+ if not packs or set(packs) - PACKS.keys():
66
+ raise ValueError("Unknown or empty preset-pack selection")
67
+ out.mkdir(parents=True)
68
+ rows, receipts, textures = [], [], []
69
+ seen = {}
70
+ for name in packs:
71
+ pin = PACKS[name]
72
+ filename = "butterchurn-presets-2.4.7.tgz" if name == "butterchurn" else name + ".zip"
73
+ cached = Path(cache) / filename if cache else None
74
+ if cached and cached.is_file():
75
+ data = cached.read_bytes()
76
+ else:
77
+ with urllib.request.urlopen(pin["url"], timeout=180) as response:
78
+ data = response.read(64 * 1024**2 + 1)
79
+ if hashlib.sha256(data).hexdigest() != pin["sha256"]:
80
+ raise ValueError(f"Pinned preset archive checksum differs: {name}")
81
+ pack = out / name
82
+ pack.mkdir()
83
+ notices, count = [], 0
84
+ for original, content in archive_members(data, tar=name == "butterchurn"):
85
+ path = PurePosixPath(original)
86
+ if path.name.lower().startswith(("license", "copying", "readme")):
87
+ target = pack / path.name
88
+ target.write_bytes(content)
89
+ notices.append(dict(path=str(target), sha256=sha256(target)))
90
+ suffix = path.suffix.lower()
91
+ is_preset = (name == "butterchurn" and original.startswith("presets/converted/") and suffix == ".json"
92
+ or pin["engine"] == "projectm" and suffix in {".milk", ".prjm"})
93
+ is_texture = name == "textures" and suffix in {".png", ".jpg", ".jpeg", ".bmp", ".dds", ".tga"}
94
+ if not is_preset and not is_texture:
95
+ continue
96
+ digest = hashlib.sha256(content).hexdigest()
97
+ if is_texture:
98
+ target = pack / path.name
99
+ if target.exists() and target.read_bytes() != content:
100
+ raise ValueError(f"Texture basename collision: {original}")
101
+ target.write_bytes(content)
102
+ textures.append(dict(path=str(target), sha256=digest, original=original))
103
+ continue
104
+ count += 1
105
+ key = (pin["engine"], digest)
106
+ alias = dict(pack=name, original=original, name=path.stem)
107
+ if key in seen:
108
+ seen[key]["aliases"].append(alias)
109
+ continue
110
+ target = pack / (digest + suffix)
111
+ target.write_bytes(content)
112
+ row = dict(id=pin["engine"] + ":" + digest, engine=pin["engine"], name=path.stem,
113
+ preset=dict(path=str(target), sha256=digest), aliases=[alias],
114
+ source_pack=name, licence=pin["licence"], compatibility="unverified until actual rendering")
115
+ rows.append(row)
116
+ seen[key] = row
117
+ receipts.append(dict(id=name, **pin, preset_files=count, notices=notices))
118
+ result = dict(schema="framerudiments.preset-library.v1", packs=receipts, presets=rows,
119
+ textures=textures, limits="Source membership and syntax are not shader/GPU acceptance or rights clearance.")
120
+ write_json(out / "catalogue.json", result)
121
+ return result
122
+
123
+
124
+ def load_library(path):
125
+ path = Path(path).resolve()
126
+ data = json.loads(path.read_text(encoding="utf-8"))
127
+ if data.get("schema") != "framerudiments.preset-library.v1":
128
+ raise ValueError("Expected framerudiments.preset-library.v1")
129
+ ids = [x["id"] for x in data["presets"]]
130
+ if len(ids) != len(set(ids)):
131
+ raise ValueError("Duplicate preset identities")
132
+ return data