framerudiments 0.4.6__tar.gz → 0.4.7__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 (263) hide show
  1. {framerudiments-0.4.6/src/framerudiments.egg-info → framerudiments-0.4.7}/PKG-INFO +8 -1
  2. {framerudiments-0.4.6 → framerudiments-0.4.7}/README.md +7 -0
  3. {framerudiments-0.4.6 → framerudiments-0.4.7}/RELEASE_NOTES.md +18 -0
  4. framerudiments-0.4.7/docs/bounded-preflight.md +45 -0
  5. framerudiments-0.4.7/docs/media/bounded-preflight.svg +19 -0
  6. {framerudiments-0.4.6 → framerudiments-0.4.7}/pyproject.toml +1 -1
  7. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/__init__.py +1 -1
  8. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/cli.py +12 -2
  9. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/render.py +39 -5
  10. {framerudiments-0.4.6 → framerudiments-0.4.7/src/framerudiments.egg-info}/PKG-INFO +8 -1
  11. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments.egg-info/SOURCES.txt +2 -0
  12. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_pipeline.py +49 -0
  13. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_rich_subtitles.py +2 -1
  14. {framerudiments-0.4.6 → framerudiments-0.4.7}/AGENTS.md +0 -0
  15. {framerudiments-0.4.6 → framerudiments-0.4.7}/LICENSE +0 -0
  16. {framerudiments-0.4.6 → framerudiments-0.4.7}/MANIFEST.in +0 -0
  17. {framerudiments-0.4.6 → framerudiments-0.4.7}/analysis-releases.json +0 -0
  18. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/ECOSYSTEM.md +0 -0
  19. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/FrameRudiments-PATCH_WORKFLOW.md +0 -0
  20. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/annular-modulation.md +0 -0
  21. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/authoring-workspace.md +0 -0
  22. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/beat-alignment.md +0 -0
  23. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/controlled-audio-response.md +0 -0
  24. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/expanded-effects.md +0 -0
  25. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/extraction.md +0 -0
  26. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/full-preset-analysis.md +0 -0
  27. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/hybrid-preset-analysis.md +0 -0
  28. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/keyframed-mattes.md +0 -0
  29. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/media/beat-alignment-evidence.json +0 -0
  30. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/media/beat-alignment.mp4 +0 -0
  31. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/media/beat-alignment.png +0 -0
  32. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/media/demo-transcript.md +0 -0
  33. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/media/effects-pipeline.svg +0 -0
  34. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/media/full-frame-comparison.png +0 -0
  35. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/media/full-frame-demo.mp4 +0 -0
  36. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/media/media-evidence.json +0 -0
  37. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/media/natural-materials.mp4 +0 -0
  38. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/media/natural-materials.png +0 -0
  39. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/media/palette-grade-comparison.png +0 -0
  40. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/media/preset-test-cards.png +0 -0
  41. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/media/rangoli-transcript.md +0 -0
  42. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/media/rangoli.mp4 +0 -0
  43. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/media/rangoli.png +0 -0
  44. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/media/test-card-inputs/colour-bars.png +0 -0
  45. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/media/test-card-inputs/grid-rings.png +0 -0
  46. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/media/test-card-inputs/text-card.png +0 -0
  47. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/multi-stem-modulation.md +0 -0
  48. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/music-reactive-3d.md +0 -0
  49. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/natural-materials.md +0 -0
  50. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/particle-smoke.md +0 -0
  51. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/phase3-compositing.md +0 -0
  52. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/preset-behaviour.md +0 -0
  53. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/preset-profiling.md +0 -0
  54. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/project-format.md +0 -0
  55. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/projectm-local-runtime.md +0 -0
  56. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/quality-effects.md +0 -0
  57. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/rangoli.md +0 -0
  58. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/rich-lyric-effects.md +0 -0
  59. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/rudiment-renderers.md +0 -0
  60. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/scoped-post-effects.md +0 -0
  61. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/solid-spatial-renderers.md +0 -0
  62. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/spatial-renderers.md +0 -0
  63. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/spatiotemporal-supersampling.md +0 -0
  64. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/spectrum.md +0 -0
  65. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/sprite-visualisations.md +0 -0
  66. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/stem-sources-and-masks.md +0 -0
  67. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/variable-bpm-word-subtitles.md +0 -0
  68. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/visualisation-atlas.md +0 -0
  69. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/visualisation-coverage-matrix.md +0 -0
  70. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/visualiser-routing.md +0 -0
  71. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/visualisers.md +0 -0
  72. {framerudiments-0.4.6 → framerudiments-0.4.7}/docs/workflow-media.md +0 -0
  73. {framerudiments-0.4.6 → framerudiments-0.4.7}/integration/test_fire_volume_gpu.py +0 -0
  74. {framerudiments-0.4.6 → framerudiments-0.4.7}/integration/test_music_spatial_gpu.py +0 -0
  75. {framerudiments-0.4.6 → framerudiments-0.4.7}/integration/test_preset_analysis.py +0 -0
  76. {framerudiments-0.4.6 → framerudiments-0.4.7}/integration/test_released_preset_analysis.py +0 -0
  77. {framerudiments-0.4.6 → framerudiments-0.4.7}/integration/test_visualisers_gpu.py +0 -0
  78. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/Run-ControlledPresetAnalysis.ps1 +0 -0
  79. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/Run-FullPresetAnalysis.ps1 +0 -0
  80. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/Run-HybridPresetAnalysis.ps1 +0 -0
  81. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/Run-PresetBehaviourAnalysis.ps1 +0 -0
  82. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/authoring_preview.py +0 -0
  83. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/bake_mantaflow_fire.py +0 -0
  84. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/build_sprite_assets.py +0 -0
  85. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/check_projectm_runtime.py +0 -0
  86. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/check_release_packages.py +0 -0
  87. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/controlled_preset_analysis.py +0 -0
  88. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/ensure_projectm_runtime.py +0 -0
  89. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/full_preset_analysis.py +0 -0
  90. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/generate_particle_gallery.py +0 -0
  91. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/generate_post_effect_gallery.py +0 -0
  92. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/generate_quality_gallery.py +0 -0
  93. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/generate_rich_lyric_gallery.py +0 -0
  94. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/generate_sprite_gallery.py +0 -0
  95. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/generate_supersampling_gallery.py +0 -0
  96. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/generate_timing_gallery.py +0 -0
  97. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/generate_visualisation_gallery.py +0 -0
  98. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/generate_visualiser_routing_gallery.py +0 -0
  99. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/install_analysis_releases.py +0 -0
  100. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/prepare_fire_atlas.py +0 -0
  101. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/prepare_pixelcue_taggers.py +0 -0
  102. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/preset_behaviour_pipeline.py +0 -0
  103. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/profile_visualisation_coverage.py +0 -0
  104. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/profile_visualisers.py +0 -0
  105. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/projectm_dll_diagnostics.py +0 -0
  106. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/render_natural_material_demo.py +0 -0
  107. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/repair_projectm_mingw.py +0 -0
  108. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/select_projectm_generator.py +0 -0
  109. {framerudiments-0.4.6 → framerudiments-0.4.7}/scripts/validate_patch.py +0 -0
  110. {framerudiments-0.4.6 → framerudiments-0.4.7}/setup.cfg +0 -0
  111. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/__main__.py +0 -0
  112. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/advanced_renderers.py +0 -0
  113. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/atlas.py +0 -0
  114. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/audio_probes.py +0 -0
  115. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/beat_alignment.py +0 -0
  116. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/beat_timeline.py +0 -0
  117. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/choreography.py +0 -0
  118. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/circular_renderers.py +0 -0
  119. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/compositor.py +0 -0
  120. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/controlled_profiling.py +0 -0
  121. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/demo.py +0 -0
  122. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/effect_catalogue.py +0 -0
  123. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/effect_renderers.py +0 -0
  124. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/effects.py +0 -0
  125. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/encoding.py +0 -0
  126. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/fire_assets/mantaflow/NOTICE.md +0 -0
  127. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/fire_assets/mantaflow/atlas.json +0 -0
  128. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/fire_assets/mantaflow/flames.png +0 -0
  129. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/fire_assets/marburg/NOTICE.md +0 -0
  130. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/fire_assets/marburg/atlas.json +0 -0
  131. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/fire_assets/marburg/flames.png +0 -0
  132. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/fire_assets.py +0 -0
  133. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/fire_fluid.py +0 -0
  134. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/fire_renderer.py +0 -0
  135. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/fire_volume.py +0 -0
  136. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/flight.py +0 -0
  137. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/gpu_spatial_backend.py +0 -0
  138. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/io.py +0 -0
  139. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/keyframes.py +0 -0
  140. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/layer_animation.py +0 -0
  141. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/layer_displacement.py +0 -0
  142. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/layer_masks.py +0 -0
  143. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/lyric_indicator.py +0 -0
  144. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/modulation.py +0 -0
  145. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/motion.py +0 -0
  146. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/music_spatial_renderers.py +0 -0
  147. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/natural_materials.py +0 -0
  148. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/output_quality.py +0 -0
  149. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/particle_engine.py +0 -0
  150. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/post_effects.py +0 -0
  151. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/preset_library.py +0 -0
  152. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/preset_profiling.py +0 -0
  153. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/profile_analysis.py +0 -0
  154. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/project.py +0 -0
  155. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/projectm_backend.py +0 -0
  156. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/quality_effects.py +0 -0
  157. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/rangoli_renderer.py +0 -0
  158. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/rich_subtitles.py +0 -0
  159. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/rudiment_renderers.py +0 -0
  160. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/scoped_effects.py +0 -0
  161. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/solid_spatial_renderers.py +0 -0
  162. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/spatial_renderers.py +0 -0
  163. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/spectrum.py +0 -0
  164. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_assets/arcade.png +0 -0
  165. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_assets/crystal.png +0 -0
  166. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_assets/fire.png +0 -0
  167. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_assets/generated-neon.png +0 -0
  168. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_assets/glass.png +0 -0
  169. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_assets/hud.png +0 -0
  170. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_assets/ice.png +0 -0
  171. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_assets/lcd-amber.png +0 -0
  172. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_assets/lcd-green.png +0 -0
  173. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_assets/lcd.png +0 -0
  174. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_assets/led.png +0 -0
  175. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_assets/manifest.json +0 -0
  176. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_assets/metal.png +0 -0
  177. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_assets/mono.png +0 -0
  178. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_assets/neon.png +0 -0
  179. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_assets/pixel.png +0 -0
  180. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_assets/plasma.png +0 -0
  181. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_assets/vfd.png +0 -0
  182. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_catalogue.py +0 -0
  183. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/sprite_renderers.py +0 -0
  184. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/stem_blend.py +0 -0
  185. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/stemlab.py +0 -0
  186. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/subtitles.py +0 -0
  187. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/super_sampling.py +0 -0
  188. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/terrain_field.py +0 -0
  189. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/timeline.py +0 -0
  190. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/visual_style.py +0 -0
  191. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/visualisation_catalogue.py +0 -0
  192. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/visualiser_assets/butterchurn-page.js +0 -0
  193. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/visualiser_assets/butterchurn-worker.mjs +0 -0
  194. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/visualiser_assets/feedback.json +0 -0
  195. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/visualiser_assets/feedback.milk +0 -0
  196. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/visualiser_assets/julia.json +0 -0
  197. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/visualiser_assets/julia.milk +0 -0
  198. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/visualiser_assets/kaleidoscope.json +0 -0
  199. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/visualiser_assets/kaleidoscope.milk +0 -0
  200. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/visualiser_assets/package-lock.json +0 -0
  201. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/visualiser_assets/package.json +0 -0
  202. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/visualiser_assets/rangoli-background.milk +0 -0
  203. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/visualiser_routing.py +0 -0
  204. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/visualiser_setup.py +0 -0
  205. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/visualisers.py +0 -0
  206. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments/word_timing.py +0 -0
  207. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments.egg-info/dependency_links.txt +0 -0
  208. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments.egg-info/entry_points.txt +0 -0
  209. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments.egg-info/requires.txt +0 -0
  210. {framerudiments-0.4.6 → framerudiments-0.4.7}/src/framerudiments.egg-info/top_level.txt +0 -0
  211. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/conftest.py +0 -0
  212. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_adapters.py +0 -0
  213. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_advanced_renderers.py +0 -0
  214. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_authoring_preview.py +0 -0
  215. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_beat_alignment.py +0 -0
  216. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_behaviour_pipeline.py +0 -0
  217. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_checkpoint_batches.py +0 -0
  218. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_controlled_audio.py +0 -0
  219. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_coverage_matrix.py +0 -0
  220. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_default_antialias.py +0 -0
  221. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_effect_batch.py +0 -0
  222. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_encoding.py +0 -0
  223. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_extracted_helpers.py +0 -0
  224. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_fire_methods.py +0 -0
  225. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_flight.py +0 -0
  226. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_full_preset_analysis.py +0 -0
  227. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_gpu_ci_isolation.py +0 -0
  228. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_hybrid_launcher.py +0 -0
  229. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_keyframes.py +0 -0
  230. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_layer_masks.py +0 -0
  231. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_lyrics.py +0 -0
  232. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_music_spatial_pipeline.py +0 -0
  233. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_music_spatial_renderers.py +0 -0
  234. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_natural_materials.py +0 -0
  235. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_particle_engine.py +0 -0
  236. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_patch_validation.py +0 -0
  237. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_phase3_animation.py +0 -0
  238. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_phase3_gpu_injection.py +0 -0
  239. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_preset_profiling.py +0 -0
  240. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_projectm_dll_diagnostics.py +0 -0
  241. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_projectm_generator.py +0 -0
  242. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_projectm_runtime_preflight.py +0 -0
  243. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_pytest_temp_isolation.py +0 -0
  244. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_quality_effects.py +0 -0
  245. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_rangoli_renderer.py +0 -0
  246. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_release_metadata.py +0 -0
  247. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_repair_projectm_mingw.py +0 -0
  248. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_rudiment_renderers.py +0 -0
  249. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_scoped_effects.py +0 -0
  250. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_solid_spatial_integration.py +0 -0
  251. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_solid_spatial_renderers.py +0 -0
  252. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_spatial_renderers.py +0 -0
  253. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_spectrum.py +0 -0
  254. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_sprite_renderers.py +0 -0
  255. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_stem_blend.py +0 -0
  256. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_stem_source_integration.py +0 -0
  257. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_supersampling.py +0 -0
  258. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_timeline.py +0 -0
  259. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_variable_bpm_words.py +0 -0
  260. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_visualisation_modulation.py +0 -0
  261. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_visualiser_routing.py +0 -0
  262. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_visualisers.py +0 -0
  263. {framerudiments-0.4.6 → framerudiments-0.4.7}/tests/test_windows_launcher_splitpath.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: framerudiments
3
- Version: 0.4.6
3
+ Version: 0.4.7
4
4
  Summary: Deterministic Shorts and lyric visualisers using DanceRudiments and StemLab evidence.
5
5
  Author: Kieran Simkin
6
6
  License-Expression: MIT
@@ -121,6 +121,7 @@ framerudiments --version
121
121
  framerudiments demo demo --font /path/to/a/font.ttf
122
122
  framerudiments plan demo/project.json --output demo-plan.json
123
123
  framerudiments preflight demo/project.json --output preflight --ffmpeg /path/to/ffmpeg
124
+ framerudiments preflight demo/project.json --output bounded-preflight --max-frames 3 --ffmpeg /path/to/ffmpeg
124
125
  framerudiments render demo/project.json --output render --ffmpeg /path/to/ffmpeg
125
126
  ```
126
127
 
@@ -130,6 +131,12 @@ ffprobe. Environment alternatives are `FRAMERUDIMENTS_FFMPEG` and
130
131
  font or FFmpeg location is hardcoded into the engine. The synthetic demo creates
131
132
  its own tone, sprite, LRC, CUE and four-dimensional recording.
132
133
 
134
+ For expensive long projects, `preflight --max-frames N` validates a bounded
135
+ sequential prefix and records the exact incomplete scope in `manifest.json`.
136
+ This preserves stateful particles and feedback inside the prefix without paying
137
+ for a complete unencoded pass before the complete encoded render. It never
138
+ claims later frames were checked. See [bounded preflight](docs/bounded-preflight.md).
139
+
133
140
  ## Capabilities
134
141
 
135
142
  * Portrait, landscape and square frame-addressed Pillow composition, at native
@@ -90,6 +90,7 @@ framerudiments --version
90
90
  framerudiments demo demo --font /path/to/a/font.ttf
91
91
  framerudiments plan demo/project.json --output demo-plan.json
92
92
  framerudiments preflight demo/project.json --output preflight --ffmpeg /path/to/ffmpeg
93
+ framerudiments preflight demo/project.json --output bounded-preflight --max-frames 3 --ffmpeg /path/to/ffmpeg
93
94
  framerudiments render demo/project.json --output render --ffmpeg /path/to/ffmpeg
94
95
  ```
95
96
 
@@ -99,6 +100,12 @@ ffprobe. Environment alternatives are `FRAMERUDIMENTS_FFMPEG` and
99
100
  font or FFmpeg location is hardcoded into the engine. The synthetic demo creates
100
101
  its own tone, sprite, LRC, CUE and four-dimensional recording.
101
102
 
103
+ For expensive long projects, `preflight --max-frames N` validates a bounded
104
+ sequential prefix and records the exact incomplete scope in `manifest.json`.
105
+ This preserves stateful particles and feedback inside the prefix without paying
106
+ for a complete unencoded pass before the complete encoded render. It never
107
+ claims later frames were checked. See [bounded preflight](docs/bounded-preflight.md).
108
+
102
109
  ## Capabilities
103
110
 
104
111
  * Portrait, landscape and square frame-addressed Pillow composition, at native
@@ -1,3 +1,21 @@
1
+ # FrameRudiments 0.4.7
2
+
3
+ - `preflight --max-frames N` composes a declared sequential prefix for expensive
4
+ long projects instead of forcing a complete unencoded pass before rendering.
5
+ - The manifest records full versus bounded scope, frames rendered, total frames,
6
+ sequential-state behaviour and the explicit later-frame validation limit.
7
+ - The final rendered file still receives complete decode, CFR, frame-count,
8
+ audio and encoded-checkpoint verification; bounded preflight does not weaken
9
+ or relabel final technical QA.
10
+ - Focused regression coverage preserves default full-preflight behaviour, exact
11
+ prefix boundaries, final-prefix QA capture and fail-closed limits.
12
+ - Wrapped-word layout coverage now follows visual reading order across current
13
+ Pillow text metrics instead of assuming every word remains on one row.
14
+
15
+ ![Generated workload illustration for bounded preflight](docs/media/bounded-preflight.svg)
16
+
17
+ Generated explanatory diagram; not a benchmark or rendered campaign frame.
18
+
1
19
  # FrameRudiments 0.4.6
2
20
 
3
21
  - Encoding now accepts the complete named libx264 preset range: `ultrafast`,
@@ -0,0 +1,45 @@
1
+ # Bounded sequential preflight
2
+
3
+ `framerudiments preflight` remains a full, unencoded composition pass by
4
+ default. For a long or unusually expensive project, `--max-frames N` composes
5
+ only the first `N` frames in order:
6
+
7
+ ```sh
8
+ framerudiments preflight project.json --output preflight-prefix --max-frames 3
9
+ ```
10
+
11
+ The mode is a cost and failure-surface check. It verifies pinned inputs,
12
+ audio identity, project construction, sequential state creation, the declared
13
+ prefix and cleanup. It saves frame zero, ordinary checkpoints encountered in
14
+ the prefix and the last rendered prefix frame. `manifest.json` records:
15
+
16
+ ```json
17
+ {
18
+ "preflight_scope": {
19
+ "mode": "bounded-prefix",
20
+ "frames_rendered": 3,
21
+ "total_frames": 15850,
22
+ "complete": false,
23
+ "sequential": true,
24
+ "note": "Only the declared sequential prefix was composed; later frames remain unvalidated."
25
+ }
26
+ }
27
+ ```
28
+
29
+ ## Why the prefix is sequential
30
+
31
+ Feedback visualisers, emitters and historical effects can depend on every
32
+ preceding frame. Randomly drawing distant checkpoints would reset or omit that
33
+ history and could create misleading evidence. The bounded mode therefore starts
34
+ at frame zero and advances normally. It does not describe later checkpoints,
35
+ the ending, full cleanup behaviour or the encoded result.
36
+
37
+ Run the complete `render` afterward. The renderer still composes every frame,
38
+ encodes the soundtrack, extracts all encoded QA checkpoints and verifies full
39
+ decode, constant frame rate, exact frame count, timestamps, dimensions, audio
40
+ shape and duration. Creative review and attended listening remain separate.
41
+
42
+ ![Generated comparison of full and bounded preflight workloads](media/bounded-preflight.svg)
43
+
44
+ Generated explanatory diagram. The short amber prefix represents frames that
45
+ were actually composed; the outlined remainder is explicitly unvalidated.
@@ -0,0 +1,19 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="960" height="360" viewBox="0 0 960 360" role="img" aria-labelledby="title desc">
2
+ <title id="title">Full preflight compared with bounded sequential preflight</title>
3
+ <desc id="desc">A full preflight composes every frame before rendering. A bounded preflight composes a short sequential prefix and labels the remainder unvalidated.</desc>
4
+ <rect width="960" height="360" fill="#16051f"/>
5
+ <text x="48" y="55" fill="#fff2cf" font-family="sans-serif" font-size="28" font-weight="700">Generated workflow illustration</text>
6
+ <text x="48" y="105" fill="#fff2cf" font-family="sans-serif" font-size="22">Full preflight</text>
7
+ <rect x="250" y="78" width="650" height="38" rx="19" fill="#ffb52e"/>
8
+ <text x="575" y="104" fill="#351300" text-anchor="middle" font-family="sans-serif" font-size="17" font-weight="700">every frame composed without encoding</text>
9
+ <path d="M575 126v45" stroke="#ffcf62" stroke-width="4" marker-end="url(#arrow)"/>
10
+ <text x="48" y="201" fill="#fff2cf" font-family="sans-serif" font-size="22">Complete render</text>
11
+ <rect x="250" y="174" width="650" height="38" rx="19" fill="#ff7a28"/>
12
+ <text x="575" y="200" fill="#351300" text-anchor="middle" font-family="sans-serif" font-size="17" font-weight="700">every frame composed again and encoded</text>
13
+ <text x="48" y="279" fill="#fff2cf" font-family="sans-serif" font-size="22">Bounded preflight</text>
14
+ <rect x="250" y="252" width="92" height="38" rx="19" fill="#ffb52e"/>
15
+ <rect x="342" y="252" width="558" height="38" rx="19" fill="none" stroke="#9c759f" stroke-width="3" stroke-dasharray="10 10"/>
16
+ <text x="296" y="278" fill="#351300" text-anchor="middle" font-family="sans-serif" font-size="16" font-weight="700">prefix</text>
17
+ <text x="621" y="278" fill="#d8b9dc" text-anchor="middle" font-family="sans-serif" font-size="17">later frames explicitly unvalidated</text>
18
+ <defs><marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto"><path d="M0,0 L0,6 L9,3 z" fill="#ffcf62"/></marker></defs>
19
+ </svg>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "framerudiments"
7
- version = "0.4.6"
7
+ version = "0.4.7"
8
8
  description = "Deterministic Shorts and lyric visualisers using DanceRudiments and StemLab evidence."
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  """Offline frame composition on a traceable musical clock."""
2
2
 
3
- __version__ = "0.4.6"
3
+ __version__ = "0.4.7"
4
4
 
5
5
  from .project import Project
6
6
  from .render import render, preflight
@@ -76,7 +76,13 @@ def main(argv=None):
76
76
  item.add_argument("--output", required=True, help="A new evidence/output directory; never overwrites")
77
77
  item.add_argument("--ffmpeg")
78
78
  item.add_argument("--ffprobe")
79
- if name == "render":
79
+ if name == "preflight":
80
+ item.add_argument(
81
+ "--max-frames",
82
+ type=int,
83
+ help="Compose only this many initial sequential frames; later frames remain unvalidated",
84
+ )
85
+ else:
80
86
  item.add_argument("--preset", default="fast")
81
87
  item.add_argument("--crf", type=int, default=20)
82
88
  args = parser.parse_args(argv)
@@ -161,7 +167,11 @@ def main(argv=None):
161
167
  print(output)
162
168
  else:
163
169
  runner = preflight if args.command == "preflight" else render
164
- extra = {"preset": args.preset, "crf": args.crf} if args.command == "render" else {}
170
+ extra = (
171
+ {"preset": args.preset, "crf": args.crf}
172
+ if args.command == "render"
173
+ else {"max_frames": args.max_frames}
174
+ )
165
175
  result = runner(args.project, args.output, ffmpeg=args.ffmpeg, ffprobe=args.ffprobe, **extra)
166
176
  print(json.dumps({"state": result["state"], "frames": result["frames"], "output": args.output}))
167
177
  return 0
@@ -107,9 +107,38 @@ def _prepare(project, output, ffmpeg, ffprobe, scene, renderers):
107
107
  return project, output, ffmpeg, ffprobe, compositor
108
108
 
109
109
 
110
- def preflight(project, output, *, ffmpeg=None, ffprobe=None, scene=None, renderers=None):
111
- p, out, ffmpeg, ffprobe, compositor = _prepare(project, output, ffmpeg, ffprobe, scene, renderers)
112
- selected = set(checkpoints(p))
110
+ def preflight(
111
+ project,
112
+ output,
113
+ *,
114
+ ffmpeg=None,
115
+ ffprobe=None,
116
+ scene=None,
117
+ renderers=None,
118
+ max_frames=None,
119
+ ):
120
+ p = Project.load(project) if not isinstance(project, Project) else project
121
+ if max_frames is None:
122
+ frame_count = p.frame_count
123
+ elif type(max_frames) is not int or not 1 <= max_frames <= p.frame_count:
124
+ raise ValueError(f"Preflight max_frames must be an integer in [1,{p.frame_count}]")
125
+ else:
126
+ frame_count = max_frames
127
+ p, out, ffmpeg, ffprobe, compositor = _prepare(p, output, ffmpeg, ffprobe, scene, renderers)
128
+ selected = {frame for frame in checkpoints(p) if frame < frame_count}
129
+ selected.add(frame_count - 1)
130
+ scope = dict(
131
+ mode="full" if frame_count == p.frame_count else "bounded-prefix",
132
+ frames_rendered=frame_count,
133
+ total_frames=p.frame_count,
134
+ complete=frame_count == p.frame_count,
135
+ sequential=True,
136
+ note=(
137
+ "All project frames were composed."
138
+ if frame_count == p.frame_count
139
+ else "Only the declared sequential prefix was composed; later frames remain unvalidated."
140
+ ),
141
+ )
113
142
  effects = None
114
143
  try:
115
144
  effects = (
@@ -118,7 +147,7 @@ def preflight(project, output, *, ffmpeg=None, ffprobe=None, scene=None, rendere
118
147
  else None
119
148
  )
120
149
  with (out / "rudiment-analysis.jsonl").open("w", encoding="utf-8") as analysis:
121
- for n in range(p.frame_count):
150
+ for n in range(frame_count):
122
151
  image, context = compositor.frame(n, apply_quality=False)
123
152
  if effects:
124
153
  if n in selected:
@@ -141,10 +170,15 @@ def preflight(project, output, *, ffmpeg=None, ffprobe=None, scene=None, rendere
141
170
  p.verify_inputs()
142
171
  result = dict(
143
172
  p.summary(),
173
+ preflight_scope=scope,
144
174
  composition=compositor.evidence(),
145
175
  full_frame_effects=effects.evidence() if effects else None,
146
176
  analysis_sidecar=_sidecar(out),
147
- state="preflight passed; no encoded video",
177
+ state=(
178
+ "full preflight passed; no encoded video"
179
+ if scope["complete"]
180
+ else "bounded-prefix preflight passed; later frames unvalidated; no encoded video"
181
+ ),
148
182
  )
149
183
  write_json(out / "manifest.json", result)
150
184
  return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: framerudiments
3
- Version: 0.4.6
3
+ Version: 0.4.7
4
4
  Summary: Deterministic Shorts and lyric visualisers using DanceRudiments and StemLab evidence.
5
5
  Author: Kieran Simkin
6
6
  License-Expression: MIT
@@ -121,6 +121,7 @@ framerudiments --version
121
121
  framerudiments demo demo --font /path/to/a/font.ttf
122
122
  framerudiments plan demo/project.json --output demo-plan.json
123
123
  framerudiments preflight demo/project.json --output preflight --ffmpeg /path/to/ffmpeg
124
+ framerudiments preflight demo/project.json --output bounded-preflight --max-frames 3 --ffmpeg /path/to/ffmpeg
124
125
  framerudiments render demo/project.json --output render --ffmpeg /path/to/ffmpeg
125
126
  ```
126
127
 
@@ -130,6 +131,12 @@ ffprobe. Environment alternatives are `FRAMERUDIMENTS_FFMPEG` and
130
131
  font or FFmpeg location is hardcoded into the engine. The synthetic demo creates
131
132
  its own tone, sprite, LRC, CUE and four-dimensional recording.
132
133
 
134
+ For expensive long projects, `preflight --max-frames N` validates a bounded
135
+ sequential prefix and records the exact incomplete scope in `manifest.json`.
136
+ This preserves stateful particles and feedback inside the prefix without paying
137
+ for a complete unencoded pass before the complete encoded render. It never
138
+ claims later frames were checked. See [bounded preflight](docs/bounded-preflight.md).
139
+
133
140
  ## Capabilities
134
141
 
135
142
  * Portrait, landscape and square frame-addressed Pillow composition, at native
@@ -10,6 +10,7 @@ docs/FrameRudiments-PATCH_WORKFLOW.md
10
10
  docs/annular-modulation.md
11
11
  docs/authoring-workspace.md
12
12
  docs/beat-alignment.md
13
+ docs/bounded-preflight.md
13
14
  docs/controlled-audio-response.md
14
15
  docs/expanded-effects.md
15
16
  docs/extraction.md
@@ -45,6 +46,7 @@ docs/workflow-media.md
45
46
  docs/media/beat-alignment-evidence.json
46
47
  docs/media/beat-alignment.mp4
47
48
  docs/media/beat-alignment.png
49
+ docs/media/bounded-preflight.svg
48
50
  docs/media/demo-transcript.md
49
51
  docs/media/effects-pipeline.svg
50
52
  docs/media/full-frame-comparison.png
@@ -135,3 +135,52 @@ def test_custom_scene_hook_and_preflight(demo, tmp_path):
135
135
  assert calls == list(range(240))
136
136
  assert result["composition"]["custom_scene"]
137
137
  assert "no encoded" in result["state"]
138
+ assert result["preflight_scope"] == {
139
+ "mode": "full",
140
+ "frames_rendered": 240,
141
+ "total_frames": 240,
142
+ "complete": True,
143
+ "sequential": True,
144
+ "note": "All project frames were composed.",
145
+ }
146
+
147
+ calls.clear()
148
+ bounded = preflight(demo, tmp_path / "bounded-preflight", scene=scene, max_frames=3)
149
+ assert calls == [0, 1, 2]
150
+ assert bounded["preflight_scope"] == {
151
+ "mode": "bounded-prefix",
152
+ "frames_rendered": 3,
153
+ "total_frames": 240,
154
+ "complete": False,
155
+ "sequential": True,
156
+ "note": "Only the declared sequential prefix was composed; later frames remain unvalidated.",
157
+ }
158
+ assert "later frames unvalidated" in bounded["state"]
159
+ assert (tmp_path / "bounded-preflight" / "qa" / "source-00000002.png").exists()
160
+
161
+
162
+ @pytest.mark.parametrize("limit", [0, -1, 241, 1.5, True])
163
+ def test_bounded_preflight_rejects_invalid_frame_limit(demo, tmp_path, limit):
164
+ with pytest.raises(ValueError, match="Preflight max_frames"):
165
+ preflight(demo, tmp_path / f"bad-preflight-{limit}", max_frames=limit)
166
+
167
+
168
+ def test_cli_forwards_bounded_preflight_limit(monkeypatch, capsys):
169
+ from framerudiments import cli
170
+
171
+ received = {}
172
+
173
+ def fake_preflight(project, output, **kwargs):
174
+ received.update(project=project, output=output, **kwargs)
175
+ return {"state": "bounded-prefix preflight passed", "frames": 240}
176
+
177
+ monkeypatch.setattr(cli, "preflight", fake_preflight)
178
+ assert cli.main(["preflight", "project.json", "--output", "out", "--max-frames", "3"]) == 0
179
+ assert received == {
180
+ "project": "project.json",
181
+ "output": "out",
182
+ "ffmpeg": None,
183
+ "ffprobe": None,
184
+ "max_frames": 3,
185
+ }
186
+ assert '"state": "bounded-prefix preflight passed"' in capsys.readouterr().out
@@ -199,7 +199,8 @@ def test_word_centres_follow_wrapped_text_layout_and_stay_in_box(project):
199
199
  centres = word_centres(text, "Lights follow every word")
200
200
  assert len(centres) == 4
201
201
  assert all(text.box[0] <= x <= text.box[2] for x, _ in centres)
202
- assert all(a[0] < b[0] for a, b in zip(centres, centres[1:]))
202
+ assert all(a[1] <= b[1] for a, b in zip(centres, centres[1:]))
203
+ assert all(a[0] < b[0] for a, b in zip(centres, centres[1:]) if a[1] == b[1])
203
204
 
204
205
 
205
206
  def test_rich_highlight_can_tint_single_word_without_destroying_gradients(project):
File without changes
File without changes
File without changes