framerudiments 0.1.4__tar.gz → 0.2.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.
- {framerudiments-0.1.4/src/framerudiments.egg-info → framerudiments-0.2.0}/PKG-INFO +14 -1
- {framerudiments-0.1.4 → framerudiments-0.2.0}/README.md +13 -0
- framerudiments-0.2.0/docs/beat-alignment.md +56 -0
- framerudiments-0.2.0/docs/media/beat-alignment-evidence.json +173 -0
- framerudiments-0.2.0/docs/media/beat-alignment.mp4 +0 -0
- framerudiments-0.2.0/docs/media/beat-alignment.png +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/docs/project-format.md +8 -1
- {framerudiments-0.1.4 → framerudiments-0.2.0}/pyproject.toml +1 -1
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/__init__.py +1 -1
- framerudiments-0.2.0/src/framerudiments/beat_alignment.py +57 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/project.py +21 -4
- {framerudiments-0.1.4 → framerudiments-0.2.0/src/framerudiments.egg-info}/PKG-INFO +14 -1
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments.egg-info/SOURCES.txt +6 -0
- framerudiments-0.2.0/tests/test_beat_alignment.py +144 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/AGENTS.md +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/LICENSE +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/MANIFEST.in +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/docs/extraction.md +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/docs/media/demo-transcript.md +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/docs/media/effects-pipeline.svg +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/docs/media/full-frame-comparison.png +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/docs/media/full-frame-demo.mp4 +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/docs/media/media-evidence.json +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/docs/rudiment-renderers.md +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/docs/spectrum.md +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/docs/visualisers.md +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/docs/workflow-media.md +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/integration/test_visualisers_gpu.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/setup.cfg +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/__main__.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/atlas.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/choreography.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/cli.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/compositor.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/demo.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/effects.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/encoding.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/flight.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/io.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/motion.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/projectm_backend.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/render.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/rudiment_renderers.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/spectrum.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/stemlab.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/subtitles.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/timeline.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualiser_assets/butterchurn-page.js +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualiser_assets/butterchurn-worker.mjs +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualiser_assets/feedback.json +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualiser_assets/feedback.milk +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualiser_assets/julia.json +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualiser_assets/julia.milk +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualiser_assets/kaleidoscope.json +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualiser_assets/kaleidoscope.milk +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualiser_assets/package-lock.json +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualiser_assets/package.json +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualiser_setup.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualisers.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments.egg-info/dependency_links.txt +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments.egg-info/entry_points.txt +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments.egg-info/requires.txt +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments.egg-info/top_level.txt +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/tests/test_adapters.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/tests/test_extracted_helpers.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/tests/test_flight.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/tests/test_lyrics.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/tests/test_pipeline.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/tests/test_rudiment_renderers.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/tests/test_spectrum.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/tests/test_timeline.py +0 -0
- {framerudiments-0.1.4 → framerudiments-0.2.0}/tests/test_visualisers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: framerudiments
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Deterministic Shorts and lyric visualisers using DanceRudiments and StemLab evidence.
|
|
5
5
|
Author: Kieran Simkin
|
|
6
6
|
License-Expression: MIT
|
|
@@ -29,6 +29,19 @@ FrameRudiments renders Shorts, Canvas-style clips and full lyric visualisers
|
|
|
29
29
|
from one traceable timeline. It is the offline Python renderer in the
|
|
30
30
|
[DanceFlow ecosystem](https://kieransimkin.co.uk/danceflow/).
|
|
31
31
|
|
|
32
|
+
**0.2.0** accepts source-bound StemLab BPM/first-beat readings through
|
|
33
|
+
`clock.beat_alignment`. Procedural motion follows the fractional beat phase
|
|
34
|
+
after excerpt/repeat mapping. Audio, lyric/cue timestamps and captured
|
|
35
|
+
recordings retain their original source clock. See [beat alignment](docs/beat-alignment.md).
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
[Play the four-second demonstration](docs/media/beat-alignment.mp4): original
|
|
40
|
+
120 BPM clicks start at 0.125 seconds; the right movement uses the -16 pip
|
|
41
|
+
reading. Text and recorded bands keep source time. This synthetic example
|
|
42
|
+
demonstrates clock composition; it does not establish song-analysis accuracy.
|
|
43
|
+
[Encoded-frame verification and hashes](docs/media/beat-alignment-evidence.json).
|
|
44
|
+
|
|
32
45
|
It consolidates the shared Python/Pillow/FFmpeg mechanisms developed across
|
|
33
46
|
the existing Gilt Trip, Blame Putin, Clay, Paper Dreams, Free Danny Tommo,
|
|
34
47
|
Silly Sausage Britain and Moderate or Good renderers. Release-specific artwork,
|
|
@@ -4,6 +4,19 @@ FrameRudiments renders Shorts, Canvas-style clips and full lyric visualisers
|
|
|
4
4
|
from one traceable timeline. It is the offline Python renderer in the
|
|
5
5
|
[DanceFlow ecosystem](https://kieransimkin.co.uk/danceflow/).
|
|
6
6
|
|
|
7
|
+
**0.2.0** accepts source-bound StemLab BPM/first-beat readings through
|
|
8
|
+
`clock.beat_alignment`. Procedural motion follows the fractional beat phase
|
|
9
|
+
after excerpt/repeat mapping. Audio, lyric/cue timestamps and captured
|
|
10
|
+
recordings retain their original source clock. See [beat alignment](docs/beat-alignment.md).
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
[Play the four-second demonstration](docs/media/beat-alignment.mp4): original
|
|
15
|
+
120 BPM clicks start at 0.125 seconds; the right movement uses the -16 pip
|
|
16
|
+
reading. Text and recorded bands keep source time. This synthetic example
|
|
17
|
+
demonstrates clock composition; it does not establish song-analysis accuracy.
|
|
18
|
+
[Encoded-frame verification and hashes](docs/media/beat-alignment-evidence.json).
|
|
19
|
+
|
|
7
20
|
It consolidates the shared Python/Pillow/FFmpeg mechanisms developed across
|
|
8
21
|
the existing Gilt Trip, Blame Putin, Clay, Paper Dreams, Free Danny Tommo,
|
|
9
22
|
Silly Sausage Britain and Moderate or Good renderers. Release-specific artwork,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Beat alignment from saved StemLab evidence
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
[Play the four-second example](media/beat-alignment.mp4). Original synthetic
|
|
6
|
+
120 BPM clicks begin at 0.125 seconds. The left native bounce shows the original
|
|
7
|
+
grid; the right follows the -16 pip reading. The bands retain their recording
|
|
8
|
+
clock. Text changes at the original LRC times: “Signals into motion” at 0,
|
|
9
|
+
“Words into light” at 1.25, “Keep the clock in sight” at 2.50, then clears at
|
|
10
|
+
3.75 seconds. The synthetic text is a caption fixture, not sung lyrics.
|
|
11
|
+
[Encoded verification, source identity and hashes](media/beat-alignment-evidence.json).
|
|
12
|
+
|
|
13
|
+
Generate a source-bound reading from saved StemLab evidence, reviewing canonical,
|
|
14
|
+
combined, fitted and half/double-time interpretations and individual detectors:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
stemlab beat-offset RESULTS --output NEW --audio EXACT-MASTER.wav \
|
|
18
|
+
--alignment-analysis consensus --alignment-bpm-basis combined_fit
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Use `canonical` only when canonical BPM has an established authority. Retain the
|
|
22
|
+
full comparison report; a good beat-grid fit is not a listening or downbeat
|
|
23
|
+
confirmation. A normal half-time reading does not mean a detector failed.
|
|
24
|
+
|
|
25
|
+
Pin the resulting file exactly as every other project input:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
"clock": {
|
|
29
|
+
"authority": "Reviewed StemLab consensus constant-grid fit",
|
|
30
|
+
"status": "estimated",
|
|
31
|
+
"phase_status": "estimated",
|
|
32
|
+
"beat_alignment": {"path": "beat-alignment.json", "sha256": "EXACT_FILE_SHA256"}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The adapter validates the master SHA-256, source sample rate/frame bounds,
|
|
37
|
+
64-pip units, detector/report identities and exact rational clock equation.
|
|
38
|
+
An explicit BPM must agree. Do not also supply `origin` or `beat_grid`; the
|
|
39
|
+
reading owns this clock. An estimated BPM cannot become `confirmed` through
|
|
40
|
+
project metadata, and phase authority stays separate.
|
|
41
|
+
|
|
42
|
+
The existing BeatClock uses the first source beat as its origin. At that instant
|
|
43
|
+
the procedural clock is pip zero; before it, pips can be negative. Excerpts and
|
|
44
|
+
repeats map source time first and apply phase once. Keep the original master,
|
|
45
|
+
LRC, CUE and captured recording clocks unchanged. A recording's captured beat
|
|
46
|
+
zero and integer samples remain authoritative, including Fourier dimensions.
|
|
47
|
+
|
|
48
|
+
Missing, mismatched or poor evidence remains a review hold. A constant-grid
|
|
49
|
+
reading is not proof that a variable-tempo song fits one BPM. Existing projects
|
|
50
|
+
without a reading retain their explicit clock and report unverified phase when
|
|
51
|
+
no phase status was supplied.
|
|
52
|
+
|
|
53
|
+
Reproduce the actual fixture with `python examples/beat-alignment/generate.py
|
|
54
|
+
NEW-DIRECTORY --font /path/to/font.ttf`, then use normal `plan`, `preflight` and
|
|
55
|
+
`render` commands. Keep technical encoded QA separate from attended listening,
|
|
56
|
+
creative approval and any platform publication.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
{
|
|
2
|
+
"context": "Actual FrameRudiments 0.2.0 encoded synthetic clock-composition example",
|
|
3
|
+
"frames": 240,
|
|
4
|
+
"fps": 60,
|
|
5
|
+
"dimensions": [
|
|
6
|
+
360,
|
|
7
|
+
640
|
|
8
|
+
],
|
|
9
|
+
"duration_seconds": 4,
|
|
10
|
+
"source_audio_sha256": "83e47effac0c8cf6c6c8c70f3babec9b55f61fcea0ba1a97822528e85b8ffe8c",
|
|
11
|
+
"first_beat_seconds": 0.125,
|
|
12
|
+
"offset_pips": -16,
|
|
13
|
+
"pips_per_beat": 64,
|
|
14
|
+
"screenshot_frame": 75,
|
|
15
|
+
"screenshot_source_seconds": 1.25,
|
|
16
|
+
"verification": {
|
|
17
|
+
"probe": {
|
|
18
|
+
"streams": [
|
|
19
|
+
{
|
|
20
|
+
"index": 0,
|
|
21
|
+
"codec_name": "h264",
|
|
22
|
+
"codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
|
|
23
|
+
"profile": "High",
|
|
24
|
+
"codec_type": "video",
|
|
25
|
+
"codec_tag_string": "avc1",
|
|
26
|
+
"codec_tag": "0x31637661",
|
|
27
|
+
"mime_codec_string": "avc1.64001f",
|
|
28
|
+
"width": 360,
|
|
29
|
+
"height": 640,
|
|
30
|
+
"coded_width": 368,
|
|
31
|
+
"coded_height": 640,
|
|
32
|
+
"has_b_frames": 2,
|
|
33
|
+
"pix_fmt": "yuv420p",
|
|
34
|
+
"level": 31,
|
|
35
|
+
"color_range": "tv",
|
|
36
|
+
"color_space": "bt709",
|
|
37
|
+
"chroma_location": "left",
|
|
38
|
+
"field_order": "progressive",
|
|
39
|
+
"refs": 4,
|
|
40
|
+
"is_avc": "true",
|
|
41
|
+
"nal_length_size": "4",
|
|
42
|
+
"id": "0x1",
|
|
43
|
+
"r_frame_rate": "60/1",
|
|
44
|
+
"avg_frame_rate": "60/1",
|
|
45
|
+
"time_base": "1/15360",
|
|
46
|
+
"start_pts": 0,
|
|
47
|
+
"start_time": "0.000000",
|
|
48
|
+
"duration_ts": 61440,
|
|
49
|
+
"duration": "4.000000",
|
|
50
|
+
"bit_rate": "108766",
|
|
51
|
+
"bits_per_raw_sample": "8",
|
|
52
|
+
"nb_frames": "240",
|
|
53
|
+
"nb_read_frames": "240",
|
|
54
|
+
"extradata_size": 49,
|
|
55
|
+
"disposition": {
|
|
56
|
+
"default": 1,
|
|
57
|
+
"dub": 0,
|
|
58
|
+
"original": 0,
|
|
59
|
+
"comment": 0,
|
|
60
|
+
"lyrics": 0,
|
|
61
|
+
"karaoke": 0,
|
|
62
|
+
"forced": 0,
|
|
63
|
+
"hearing_impaired": 0,
|
|
64
|
+
"visual_impaired": 0,
|
|
65
|
+
"clean_effects": 0,
|
|
66
|
+
"attached_pic": 0,
|
|
67
|
+
"timed_thumbnails": 0,
|
|
68
|
+
"non_diegetic": 0,
|
|
69
|
+
"captions": 0,
|
|
70
|
+
"descriptions": 0,
|
|
71
|
+
"metadata": 0,
|
|
72
|
+
"dependent": 0,
|
|
73
|
+
"still_image": 0,
|
|
74
|
+
"multilayer": 0
|
|
75
|
+
},
|
|
76
|
+
"tags": {
|
|
77
|
+
"language": "und",
|
|
78
|
+
"handler_name": "VideoHandler",
|
|
79
|
+
"encoder": "Lavc63.1.101 libx264"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"index": 1,
|
|
84
|
+
"codec_name": "aac",
|
|
85
|
+
"codec_long_name": "AAC (Advanced Audio Coding)",
|
|
86
|
+
"profile": "LC",
|
|
87
|
+
"codec_type": "audio",
|
|
88
|
+
"codec_tag_string": "mp4a",
|
|
89
|
+
"codec_tag": "0x6134706d",
|
|
90
|
+
"mime_codec_string": "mp4a.40.2",
|
|
91
|
+
"sample_fmt": "fltp",
|
|
92
|
+
"sample_rate": "48000",
|
|
93
|
+
"channels": 2,
|
|
94
|
+
"channel_layout": "stereo",
|
|
95
|
+
"bits_per_sample": 0,
|
|
96
|
+
"initial_padding": 1024,
|
|
97
|
+
"id": "0x2",
|
|
98
|
+
"r_frame_rate": "0/0",
|
|
99
|
+
"avg_frame_rate": "0/0",
|
|
100
|
+
"time_base": "1/48000",
|
|
101
|
+
"start_pts": 0,
|
|
102
|
+
"start_time": "0.000000",
|
|
103
|
+
"duration_ts": 192000,
|
|
104
|
+
"duration": "4.000000",
|
|
105
|
+
"bit_rate": "35574",
|
|
106
|
+
"nb_frames": "189",
|
|
107
|
+
"nb_read_frames": "188",
|
|
108
|
+
"extradata_size": 5,
|
|
109
|
+
"disposition": {
|
|
110
|
+
"default": 1,
|
|
111
|
+
"dub": 0,
|
|
112
|
+
"original": 0,
|
|
113
|
+
"comment": 0,
|
|
114
|
+
"lyrics": 0,
|
|
115
|
+
"karaoke": 0,
|
|
116
|
+
"forced": 0,
|
|
117
|
+
"hearing_impaired": 0,
|
|
118
|
+
"visual_impaired": 0,
|
|
119
|
+
"clean_effects": 0,
|
|
120
|
+
"attached_pic": 0,
|
|
121
|
+
"timed_thumbnails": 0,
|
|
122
|
+
"non_diegetic": 0,
|
|
123
|
+
"captions": 0,
|
|
124
|
+
"descriptions": 0,
|
|
125
|
+
"metadata": 0,
|
|
126
|
+
"dependent": 0,
|
|
127
|
+
"still_image": 0,
|
|
128
|
+
"multilayer": 0
|
|
129
|
+
},
|
|
130
|
+
"tags": {
|
|
131
|
+
"language": "und",
|
|
132
|
+
"handler_name": "SoundHandler"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
"format": {
|
|
137
|
+
"nb_streams": 2,
|
|
138
|
+
"nb_programs": 0,
|
|
139
|
+
"nb_stream_groups": 0,
|
|
140
|
+
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
|
|
141
|
+
"format_long_name": "QuickTime / MOV",
|
|
142
|
+
"start_time": "0.000000",
|
|
143
|
+
"duration": "4.000000",
|
|
144
|
+
"size": "79895",
|
|
145
|
+
"bit_rate": "159790",
|
|
146
|
+
"probe_score": 100,
|
|
147
|
+
"tags": {
|
|
148
|
+
"major_brand": "isom",
|
|
149
|
+
"minor_version": "512",
|
|
150
|
+
"compatible_brands": "isomiso2avc1mp41",
|
|
151
|
+
"encoder": "Lavf63.1.101"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
"full_decode": "passed",
|
|
156
|
+
"decoded_timestamps": "CFR from zero; all frames checked",
|
|
157
|
+
"audio_duration_error_seconds": 0.0,
|
|
158
|
+
"mp4_atoms": [
|
|
159
|
+
"ftyp",
|
|
160
|
+
"moov",
|
|
161
|
+
"free",
|
|
162
|
+
"mdat"
|
|
163
|
+
],
|
|
164
|
+
"listening_approved": false,
|
|
165
|
+
"platform_safe_zone_verified": false
|
|
166
|
+
},
|
|
167
|
+
"description": "Left: original pip-zero movement. Right: source-bound aligned movement. Bottom: captured recording keeps source clock. Text changes at original LRC timestamps. Soundtrack: original synthetic clicks.",
|
|
168
|
+
"limits": "Synthetic score, not detector accuracy, canonical-song authority or attended listening approval. Generated illustration omitted because actual rendered output demonstrates the composition.",
|
|
169
|
+
"files": {
|
|
170
|
+
"beat-alignment.mp4": "7e8a62a67fcc2967540121bd848263d2eff2bea02ca77e0b01f3be2966d45ccb",
|
|
171
|
+
"beat-alignment.png": "1755f09c51382cf04adbe0b18ce702036807e8b595633cc10cd116bfb9c5f88a"
|
|
172
|
+
}
|
|
173
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -11,7 +11,7 @@ the inputs are checked again before and after a render.
|
|
|
11
11
|
| `purpose` | `short` (120 ms endpoint fades by default), `visualiser`, or `canvas` (no fades by default). |
|
|
12
12
|
| `width`, `height`, `fps`, `fps_reason` | Even dimensions, fps defaults to 60; other rates need a reason. No platform compliance is inferred. |
|
|
13
13
|
| `audio` | `source` file reference, verified `sample_rate`, start-inclusive `start_sample`, end-exclusive `end_sample`, optional positive integer `repeats` and `fade_seconds`. |
|
|
14
|
-
| `clock` | `bpm` or `
|
|
14
|
+
| `clock` | `bpm`, `beat_grid` or source-bound `beat_alignment` file reference; `authority`, `status` (`confirmed`, `estimated`, `synthetic`), optional `origin` in source seconds and `phase_status`. A reading owns its BPM/origin and cannot be combined with `beat_grid` or `origin`. |
|
|
15
15
|
| `font` | Pinned TTF/OTF file for authored text. |
|
|
16
16
|
| `lyrics`, `cues` | Separate LRC references; `.cue` files are also supported for structural cues. |
|
|
17
17
|
| `canonical_lyrics` | Optional pinned plain text whose performed nonempty lines must exactly match the LRC. Bracketed structural lines in this comparison file are excluded. |
|
|
@@ -30,6 +30,13 @@ control their appearance. `mapping` amplitudes are `dx`, `dy`, `rotation`
|
|
|
30
30
|
and `scale_axis` select index 0,1,2 of the motion projection. Defaults are
|
|
31
31
|
0,1,2,2 respectively. Set `y_axis: 0` to map a one-dimensional bounce vertically.
|
|
32
32
|
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
[Watch the synthetic clock example](media/beat-alignment.mp4) and follow the
|
|
36
|
+
[source-bound alignment setup](beat-alignment.md). Its first click at 0.125
|
|
37
|
+
seconds maps to pip zero; the screenshot is encoded frame 75 at source 1.25
|
|
38
|
+
seconds. [Verification and media hashes](media/beat-alignment-evidence.json).
|
|
39
|
+
|
|
33
40
|
Atlas configuration is `{columns, rows, count, cycle_ticks, phase_beats?}`.
|
|
34
41
|
The atlas must divide into exact cells. One beat is 16 ticks; cycle ticks are
|
|
35
42
|
explicitly authored rather than silently quantised. Cycles use a continuous
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "framerudiments"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.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,57 @@
|
|
|
1
|
+
"""Validate a source-bound StemLab clock handoff; no detector or DSP implementation."""
|
|
2
|
+
|
|
3
|
+
from fractions import Fraction
|
|
4
|
+
import math
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from .io import read_json
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def beat_alignment(path, source_sha256, sample_rate, end_sample):
|
|
12
|
+
path = Path(path)
|
|
13
|
+
if path.stat().st_size > 16384:
|
|
14
|
+
raise ValueError("Beat alignment exceeds 16 KiB")
|
|
15
|
+
reading = read_json(path)
|
|
16
|
+
if (
|
|
17
|
+
reading.get("schema") != "stemlab.beat-alignment.v1"
|
|
18
|
+
or reading.get("pips_per_beat") != 64
|
|
19
|
+
or reading.get("canonical_first_beat_pip") != 0
|
|
20
|
+
or reading.get("source_sha256") != source_sha256
|
|
21
|
+
):
|
|
22
|
+
raise ValueError("Beat alignment requires the exact audio master and 64-pip contract")
|
|
23
|
+
for key in ("source_sha256", "evidence_sha256", "report_sha256"):
|
|
24
|
+
if not isinstance(reading.get(key), str) or not re.fullmatch("[a-f0-9]{64}", reading[key]):
|
|
25
|
+
raise ValueError("Beat alignment needs source, detector and report SHA-256 identities")
|
|
26
|
+
for key in ("analysis_id", "bpm_basis"):
|
|
27
|
+
if not isinstance(reading.get(key), str) or not re.fullmatch("[a-zA-Z0-9_.-]{1,128}", reading[key]):
|
|
28
|
+
raise ValueError("Beat alignment needs an explicit analysis and BPM basis")
|
|
29
|
+
exact = []
|
|
30
|
+
for key in ("exact_bpm", "first_beat_exact_seconds", "exact_offset_pips"):
|
|
31
|
+
value = reading.get(key)
|
|
32
|
+
if not isinstance(value, str) or not re.fullmatch(r"-?\d{1,80}(?:/[1-9]\d{0,79})?", value):
|
|
33
|
+
raise ValueError("Beat alignment requires bounded exact rational clock values")
|
|
34
|
+
exact.append(Fraction(value))
|
|
35
|
+
tempo, first, offset = exact
|
|
36
|
+
if not 20 <= tempo <= 400 or not 0 <= first <= 86400 or first * tempo * 64 / 60 + offset != 0:
|
|
37
|
+
raise ValueError("Beat alignment must map the first source beat to pip zero")
|
|
38
|
+
for key, value in zip(("bpm", "first_beat_seconds", "offset_pips"), exact):
|
|
39
|
+
actual = reading.get(key)
|
|
40
|
+
if type(actual) not in (int, float) or not math.isfinite(actual) or abs(actual - float(value)) > 1e-7:
|
|
41
|
+
raise ValueError("Exact and numeric beat alignment values disagree")
|
|
42
|
+
audio = reading.get("source_audio") or {}
|
|
43
|
+
if (
|
|
44
|
+
reading.get("source_audio_verified") is not True
|
|
45
|
+
or audio.get("sample_rate") != sample_rate
|
|
46
|
+
or type(audio.get("num_frames")) is not int
|
|
47
|
+
or audio["num_frames"] < end_sample
|
|
48
|
+
or first >= Fraction(audio["num_frames"], sample_rate)
|
|
49
|
+
):
|
|
50
|
+
raise ValueError("Beat alignment source sample rate/frame bounds disagree with the project")
|
|
51
|
+
if reading.get("bpm_status") not in {"canonical", "estimated"} or reading.get("phase_status") not in {
|
|
52
|
+
"estimated",
|
|
53
|
+
"confirmed",
|
|
54
|
+
"synthetic",
|
|
55
|
+
}:
|
|
56
|
+
raise ValueError("Beat alignment must retain BPM and phase authority separately")
|
|
57
|
+
return reading
|
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from .io import read_json, file_ref, fields, integer, number, sha256
|
|
7
7
|
from .timeline import BeatClock, parse_lrc, parse_cue, frame_ledger
|
|
8
8
|
from .stemlab import beat_grid, selected_loop
|
|
9
|
+
from .beat_alignment import beat_alignment
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class Project:
|
|
@@ -58,11 +59,25 @@ class Project:
|
|
|
58
59
|
self.fade = number(self.audio.get("fade_seconds", .12 if self.purpose == "short" else 0),
|
|
59
60
|
"fade seconds", minimum=0, maximum=float(self.duration) / 2)
|
|
60
61
|
cfg = data["clock"]
|
|
61
|
-
fields(cfg, {"bpm", "origin", "authority", "status", "phase_status", "beat_grid"}, "clock")
|
|
62
|
+
fields(cfg, {"bpm", "origin", "authority", "status", "phase_status", "beat_grid", "beat_alignment"}, "clock")
|
|
62
63
|
if not cfg.get("authority") or cfg.get("status") not in {"confirmed", "estimated", "synthetic"}:
|
|
63
64
|
raise ValueError("Clock needs authority and confirmed/estimated/synthetic status")
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
self.alignment = None
|
|
66
|
+
if "beat_alignment" in cfg:
|
|
67
|
+
if "beat_grid" in cfg or "origin" in cfg:
|
|
68
|
+
raise ValueError('beat_alignment cannot be combined with a separate beat grid or origin')
|
|
69
|
+
ref = self.ref(cfg['beat_alignment'])
|
|
70
|
+
self.alignment = beat_alignment(ref['path'], self.audio_ref['sha256'], self.sample_rate, self.end_sample)
|
|
71
|
+
if 'bpm' in cfg and abs(number(cfg['bpm'], 'BPM')-self.alignment['bpm']) > 1e-8:
|
|
72
|
+
raise ValueError('Explicit BPM conflicts with the beat alignment reading')
|
|
73
|
+
if self.alignment['bpm_status'] == 'estimated' and cfg['status'] != 'estimated':
|
|
74
|
+
raise ValueError('An estimated alignment BPM cannot be promoted to confirmed or synthetic')
|
|
75
|
+
if 'phase_status' in cfg and cfg['phase_status'] != self.alignment['phase_status']:
|
|
76
|
+
raise ValueError('Project phase authority conflicts with the saved reading')
|
|
77
|
+
self.clock = BeatClock(bpm=self.alignment['bpm'], origin=self.alignment['first_beat_seconds'])
|
|
78
|
+
else:
|
|
79
|
+
grid = beat_grid(self.ref(cfg["beat_grid"])["path"]) if "beat_grid" in cfg else None
|
|
80
|
+
self.clock = BeatClock(bpm=cfg.get("bpm"), origin=cfg.get("origin", 0), beats=grid)
|
|
66
81
|
self.font_ref = self.ref(data["font"]) if "font" in data else None
|
|
67
82
|
if (data.get("lyrics") or data.get("identity")) and not self.font_ref:
|
|
68
83
|
raise ValueError("Pin an explicit font for all authored text")
|
|
@@ -119,6 +134,8 @@ class Project:
|
|
|
119
134
|
repeats=self.repeats, duration_seconds=float(self.duration),
|
|
120
135
|
video_duration_seconds=self.frame_count / self.fps,
|
|
121
136
|
frame_rounding="nearest, ties toward positive infinity; stable source ordering",
|
|
122
|
-
clock=self.data[
|
|
137
|
+
clock={**self.data['clock'], 'alignment': self.alignment,
|
|
138
|
+
'phase_status': self.alignment['phase_status'] if self.alignment else self.data['clock'].get('phase_status', 'unverified')},
|
|
139
|
+
endpoint_fade_seconds=self.fade, endpoint_fade_curve="qsin",
|
|
123
140
|
lyrics=self.lyrics, cues=self.cues, stemlab_loop=self.loop, effects=self.data.get("effects", []), inputs=self.inputs,
|
|
124
141
|
state="prepared; perceptual review pending")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: framerudiments
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Deterministic Shorts and lyric visualisers using DanceRudiments and StemLab evidence.
|
|
5
5
|
Author: Kieran Simkin
|
|
6
6
|
License-Expression: MIT
|
|
@@ -29,6 +29,19 @@ FrameRudiments renders Shorts, Canvas-style clips and full lyric visualisers
|
|
|
29
29
|
from one traceable timeline. It is the offline Python renderer in the
|
|
30
30
|
[DanceFlow ecosystem](https://kieransimkin.co.uk/danceflow/).
|
|
31
31
|
|
|
32
|
+
**0.2.0** accepts source-bound StemLab BPM/first-beat readings through
|
|
33
|
+
`clock.beat_alignment`. Procedural motion follows the fractional beat phase
|
|
34
|
+
after excerpt/repeat mapping. Audio, lyric/cue timestamps and captured
|
|
35
|
+
recordings retain their original source clock. See [beat alignment](docs/beat-alignment.md).
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
[Play the four-second demonstration](docs/media/beat-alignment.mp4): original
|
|
40
|
+
120 BPM clicks start at 0.125 seconds; the right movement uses the -16 pip
|
|
41
|
+
reading. Text and recorded bands keep source time. This synthetic example
|
|
42
|
+
demonstrates clock composition; it does not establish song-analysis accuracy.
|
|
43
|
+
[Encoded-frame verification and hashes](docs/media/beat-alignment-evidence.json).
|
|
44
|
+
|
|
32
45
|
It consolidates the shared Python/Pillow/FFmpeg mechanisms developed across
|
|
33
46
|
the existing Gilt Trip, Blame Putin, Clay, Paper Dreams, Free Danny Tommo,
|
|
34
47
|
Silly Sausage Britain and Moderate or Good renderers. Release-specific artwork,
|
|
@@ -3,12 +3,16 @@ LICENSE
|
|
|
3
3
|
MANIFEST.in
|
|
4
4
|
README.md
|
|
5
5
|
pyproject.toml
|
|
6
|
+
docs/beat-alignment.md
|
|
6
7
|
docs/extraction.md
|
|
7
8
|
docs/project-format.md
|
|
8
9
|
docs/rudiment-renderers.md
|
|
9
10
|
docs/spectrum.md
|
|
10
11
|
docs/visualisers.md
|
|
11
12
|
docs/workflow-media.md
|
|
13
|
+
docs/media/beat-alignment-evidence.json
|
|
14
|
+
docs/media/beat-alignment.mp4
|
|
15
|
+
docs/media/beat-alignment.png
|
|
12
16
|
docs/media/demo-transcript.md
|
|
13
17
|
docs/media/effects-pipeline.svg
|
|
14
18
|
docs/media/full-frame-comparison.png
|
|
@@ -18,6 +22,7 @@ integration/test_visualisers_gpu.py
|
|
|
18
22
|
src/framerudiments/__init__.py
|
|
19
23
|
src/framerudiments/__main__.py
|
|
20
24
|
src/framerudiments/atlas.py
|
|
25
|
+
src/framerudiments/beat_alignment.py
|
|
21
26
|
src/framerudiments/choreography.py
|
|
22
27
|
src/framerudiments/cli.py
|
|
23
28
|
src/framerudiments/compositor.py
|
|
@@ -54,6 +59,7 @@ src/framerudiments/visualiser_assets/kaleidoscope.milk
|
|
|
54
59
|
src/framerudiments/visualiser_assets/package-lock.json
|
|
55
60
|
src/framerudiments/visualiser_assets/package.json
|
|
56
61
|
tests/test_adapters.py
|
|
62
|
+
tests/test_beat_alignment.py
|
|
57
63
|
tests/test_extracted_helpers.py
|
|
58
64
|
tests/test_flight.py
|
|
59
65
|
tests/test_lyrics.py
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import wave
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from framerudiments.io import sha256, write_json
|
|
7
|
+
from framerudiments.project import Project
|
|
8
|
+
from framerudiments.timeline import parse_lrc
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def fixture(tmp_path):
|
|
12
|
+
audio = tmp_path / "master.wav"
|
|
13
|
+
with wave.open(str(audio), "wb") as stream:
|
|
14
|
+
stream.setparams((1, 2, 48000, 0, "NONE", "not compressed"))
|
|
15
|
+
stream.writeframes(b"\0\0" * 48000 * 4)
|
|
16
|
+
reading = dict(
|
|
17
|
+
schema="stemlab.beat-alignment.v1",
|
|
18
|
+
source_sha256=sha256(audio),
|
|
19
|
+
source_audio_verified=True,
|
|
20
|
+
source_audio=dict(sample_rate=48000, num_frames=192000),
|
|
21
|
+
bpm=120,
|
|
22
|
+
exact_bpm="120",
|
|
23
|
+
bpm_basis="combined_fit",
|
|
24
|
+
bpm_status="estimated",
|
|
25
|
+
phase_status="estimated",
|
|
26
|
+
analysis_id="consensus",
|
|
27
|
+
evidence_sha256="b" * 64,
|
|
28
|
+
report_sha256="c" * 64,
|
|
29
|
+
first_beat_seconds=0.125,
|
|
30
|
+
first_beat_exact_seconds="1/8",
|
|
31
|
+
offset_pips=-16,
|
|
32
|
+
exact_offset_pips="-16",
|
|
33
|
+
pips_per_beat=64,
|
|
34
|
+
canonical_first_beat_pip=0,
|
|
35
|
+
)
|
|
36
|
+
path = tmp_path / "alignment.json"
|
|
37
|
+
write_json(path, reading)
|
|
38
|
+
data = dict(
|
|
39
|
+
schema="framerudiments.project.v1",
|
|
40
|
+
purpose="canvas",
|
|
41
|
+
width=180,
|
|
42
|
+
height=320,
|
|
43
|
+
fps=60,
|
|
44
|
+
audio=dict(
|
|
45
|
+
source=dict(path="master.wav", sha256=sha256(audio)),
|
|
46
|
+
sample_rate=48000,
|
|
47
|
+
start_sample=0,
|
|
48
|
+
end_sample=192000,
|
|
49
|
+
fade_seconds=0,
|
|
50
|
+
),
|
|
51
|
+
clock=dict(
|
|
52
|
+
authority="Synthetic source-bound test reading",
|
|
53
|
+
status="estimated",
|
|
54
|
+
beat_alignment=dict(path="alignment.json", sha256=sha256(path)),
|
|
55
|
+
),
|
|
56
|
+
layers=[],
|
|
57
|
+
background=dict(colour="#101b2e"),
|
|
58
|
+
)
|
|
59
|
+
return data, reading, path
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_first_beat_and_negative_prebeat_pips_preserve_source_timing(tmp_path):
|
|
63
|
+
data, reading, path = fixture(tmp_path)
|
|
64
|
+
project = Project(data, tmp_path)
|
|
65
|
+
assert project.clock.pip_at(0.125) == 0
|
|
66
|
+
assert project.clock.pip_at(0) == -16
|
|
67
|
+
assert project.clock.pip_at(0.625) == 64
|
|
68
|
+
assert project.source_time(0) == 0
|
|
69
|
+
assert project.summary()["clock"]["alignment"]["report_sha256"] == "c" * 64
|
|
70
|
+
assert project.summary()["clock"]["phase_status"] == "estimated"
|
|
71
|
+
lyrics = tmp_path / "lyrics.lrc"
|
|
72
|
+
lyrics.write_text("[00:00.125]Unshifted words\n[00:01.125]Later words\n", encoding="utf-8")
|
|
73
|
+
ledger = project.ledger(parse_lrc(lyrics))
|
|
74
|
+
assert [r["frame"] for r in ledger if r["text"]] == [8, 68]
|
|
75
|
+
assert next(r for r in ledger if r["text"])["source_seconds"] == 0.125
|
|
76
|
+
baseline = copy.deepcopy(data)
|
|
77
|
+
baseline["clock"] = dict(bpm=120, authority="Original source clock", status="estimated")
|
|
78
|
+
assert ledger == Project(baseline, tmp_path).ledger(parse_lrc(lyrics))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_excerpt_and_repeat_mapping_precedes_alignment_once(tmp_path):
|
|
82
|
+
data, _, _ = fixture(tmp_path)
|
|
83
|
+
data["audio"].update(start_sample=30000, end_sample=78000, repeats=2)
|
|
84
|
+
project = Project(data, tmp_path)
|
|
85
|
+
assert project.source_time(0) == 0.625
|
|
86
|
+
assert project.clock.pip_at(project.source_time(0)) == 64
|
|
87
|
+
assert project.clock.pip_at(project.source_time(60)) == 64
|
|
88
|
+
assert project.continuous_beat(60) == pytest.approx(project.continuous_beat(0) + 2)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@pytest.mark.parametrize(
|
|
92
|
+
"change",
|
|
93
|
+
[
|
|
94
|
+
{"source_sha256": "a" * 64},
|
|
95
|
+
{"exact_offset_pips": "-15"},
|
|
96
|
+
{"source_audio_verified": False},
|
|
97
|
+
{"first_beat_seconds": 4},
|
|
98
|
+
{"phase_status": "automatic"},
|
|
99
|
+
{"exact_bpm": "1e999"},
|
|
100
|
+
],
|
|
101
|
+
)
|
|
102
|
+
def test_conflicting_readings_are_rejected(tmp_path, change):
|
|
103
|
+
data, reading, path = fixture(tmp_path)
|
|
104
|
+
reading.update(change)
|
|
105
|
+
write_json(path, reading)
|
|
106
|
+
data["clock"]["beat_alignment"]["sha256"] = sha256(path)
|
|
107
|
+
with pytest.raises(ValueError):
|
|
108
|
+
Project(data, tmp_path)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@pytest.mark.parametrize(
|
|
112
|
+
"change",
|
|
113
|
+
[{"origin": 0}, {"beat_grid": {}}, {"bpm": 145}, {"status": "confirmed"}, {"phase_status": "confirmed"}],
|
|
114
|
+
)
|
|
115
|
+
def test_no_double_origin_or_authority_promotion(tmp_path, change):
|
|
116
|
+
data, _, _ = fixture(tmp_path)
|
|
117
|
+
data["clock"].update(change)
|
|
118
|
+
with pytest.raises(ValueError):
|
|
119
|
+
Project(data, tmp_path)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_recording_clock_and_source_sample_bounds_remain_independent(tmp_path):
|
|
123
|
+
from dancerudiments_authoring.recording import Recording, RecordingClock, write_recording
|
|
124
|
+
from framerudiments.motion import Motion
|
|
125
|
+
|
|
126
|
+
data, _, _ = fixture(tmp_path)
|
|
127
|
+
project = Project(data, tmp_path)
|
|
128
|
+
recording = Recording(
|
|
129
|
+
"source_capture",
|
|
130
|
+
("level",),
|
|
131
|
+
tuple((i / 511,) for i in range(512)),
|
|
132
|
+
RecordingClock(120, "synthetic source capture", "confirmed", 0, "supplied", 0),
|
|
133
|
+
)
|
|
134
|
+
path = tmp_path / "recording.json"
|
|
135
|
+
write_recording(path, recording)
|
|
136
|
+
motion = Motion(
|
|
137
|
+
dict(kind="recording", file=dict(path=str(path), sha256=sha256(path))), tmp_path, project.clock
|
|
138
|
+
)
|
|
139
|
+
assert motion.sample(0.5)[0] == pytest.approx(64 / 511)
|
|
140
|
+
assert project.clock.pip_at(0.5) == 48
|
|
141
|
+
wrong = copy.deepcopy(data)
|
|
142
|
+
wrong["audio"]["end_sample"] = 192001
|
|
143
|
+
with pytest.raises(ValueError, match="frame bounds"):
|
|
144
|
+
Project(wrong, tmp_path)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualiser_assets/feedback.json
RENAMED
|
File without changes
|
{framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualiser_assets/feedback.milk
RENAMED
|
File without changes
|
{framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualiser_assets/julia.json
RENAMED
|
File without changes
|
{framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualiser_assets/julia.milk
RENAMED
|
File without changes
|
{framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualiser_assets/kaleidoscope.json
RENAMED
|
File without changes
|
{framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualiser_assets/kaleidoscope.milk
RENAMED
|
File without changes
|
{framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualiser_assets/package-lock.json
RENAMED
|
File without changes
|
{framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments/visualiser_assets/package.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{framerudiments-0.1.4 → framerudiments-0.2.0}/src/framerudiments.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|