paces 0.0.19__tar.gz → 0.0.21__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.
- {paces-0.0.19 → paces-0.0.21}/PKG-INFO +1 -1
- {paces-0.0.19 → paces-0.0.21}/paces/measure.py +14 -6
- {paces-0.0.19 → paces-0.0.21}/paces/model.py +5 -1
- {paces-0.0.19 → paces-0.0.21}/paces/segmenters.py +51 -11
- {paces-0.0.19 → paces-0.0.21}/pyproject.toml +1 -1
- {paces-0.0.19 → paces-0.0.21}/tests/test_segment.py +61 -0
- {paces-0.0.19 → paces-0.0.21}/.gitignore +0 -0
- {paces-0.0.19 → paces-0.0.21}/LICENSE +0 -0
- {paces-0.0.19 → paces-0.0.21}/README.md +0 -0
- {paces-0.0.19 → paces-0.0.21}/docs/README.md +0 -0
- {paces-0.0.19 → paces-0.0.21}/docs/alignment/README.md +0 -0
- {paces-0.0.19 → paces-0.0.21}/docs/poc-reference/README.md +0 -0
- {paces-0.0.19 → paces-0.0.21}/docs/poc-reference/artifacts/clips.json +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/__init__.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/__main__.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/ask.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/bodies/__init__.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/bodies/_base.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/bodies/derivation.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/bodies/document.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/bodies/signal.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/bodies/step.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/corrections.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/cues.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/data/ask/ask.css +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/data/ask/ask.html +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/data/ask/ask.js +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/derivation.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/edits.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/evidence.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/excerpts.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/pose.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/projection.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/render.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/segmentation.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/timed_text.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/paces/tools.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/audio_synth.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/data/body_schemas.json +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/data/routine.json +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/poc_fixture.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/test_ask.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/test_chapters.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/test_cli_surface.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/test_corrections.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/test_cues.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/test_derivation.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/test_edit_protection_residuals.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/test_edits.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/test_evidence.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/test_evidence_schemas.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/test_excerpts.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/test_measure.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/test_pose.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/test_render_media.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/test_roundtrip_poc.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/test_smoke.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/test_vertical_slice.py +0 -0
- {paces-0.0.19 → paces-0.0.21}/tests/video_synth.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: paces
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.21
|
|
4
4
|
Summary: Turn instructional media into structured, interactive learning material
|
|
5
5
|
Project-URL: Homepage, https://github.com/thorwhalen/paces
|
|
6
6
|
Project-URL: Repository, https://github.com/thorwhalen/paces
|
|
@@ -45,7 +45,7 @@ from paces.model import MetricGrid, decimal_str, seconds_per_unit
|
|
|
45
45
|
from paces.segmenters import (
|
|
46
46
|
Capability,
|
|
47
47
|
Segmentation,
|
|
48
|
-
|
|
48
|
+
place_on_grid,
|
|
49
49
|
register,
|
|
50
50
|
)
|
|
51
51
|
|
|
@@ -231,7 +231,7 @@ def measure_grid(
|
|
|
231
231
|
flags += (f"tempo-unmeasured: {tempo_unmeasured_note}{suffix}",)
|
|
232
232
|
|
|
233
233
|
# Explicit beats inferred — but a disagreement is a finding, not a secret.
|
|
234
|
-
final_tempo = known_tempo
|
|
234
|
+
final_tempo = known_tempo if known_tempo is not None else measured_tempo
|
|
235
235
|
if (
|
|
236
236
|
known_tempo is not None
|
|
237
237
|
and measured_tempo is not None
|
|
@@ -243,7 +243,7 @@ def measure_grid(
|
|
|
243
243
|
f"measures {measured_tempo} — using yours; drop tempoBpm from "
|
|
244
244
|
"grid= to use the measured value",
|
|
245
245
|
)
|
|
246
|
-
final_origin = known_origin
|
|
246
|
+
final_origin = known_origin if known_origin is not None else measured_origin
|
|
247
247
|
if known_origin is None and measured_origin is not None:
|
|
248
248
|
flags += (
|
|
249
249
|
f"origin-estimated: first beat of the music region "
|
|
@@ -267,11 +267,19 @@ def measure_grid(
|
|
|
267
267
|
confidence = UNMEASURED_CONFIDENCE
|
|
268
268
|
else:
|
|
269
269
|
confidence = (
|
|
270
|
-
KNOWN_ORIGIN_CONFIDENCE
|
|
270
|
+
KNOWN_ORIGIN_CONFIDENCE
|
|
271
|
+
if known_origin is not None
|
|
272
|
+
else DFLT_MEASURED_CONFIDENCE
|
|
271
273
|
)
|
|
272
274
|
|
|
273
275
|
spu = seconds_per_unit(grid)
|
|
274
|
-
if
|
|
276
|
+
if (
|
|
277
|
+
total_units
|
|
278
|
+
and total_units > 0
|
|
279
|
+
and spu
|
|
280
|
+
and final_origin is not None
|
|
281
|
+
and region is not None
|
|
282
|
+
):
|
|
275
283
|
routine_s = total_units * spu
|
|
276
284
|
region_s = region.end - float(Fraction(final_origin))
|
|
277
285
|
evidence.update(routine_s=round(routine_s, 2), region_s=round(region_s, 2))
|
|
@@ -318,7 +326,7 @@ def _segment_grid_measured(inputs: Mapping[str, Any]) -> Segmentation:
|
|
|
318
326
|
grid = measurement.grid
|
|
319
327
|
if grid.tempo_bpm is None or grid.origin is None:
|
|
320
328
|
return Segmentation(grid=grid, confidence=measurement.confidence, flags=flags)
|
|
321
|
-
placed =
|
|
329
|
+
placed = place_on_grid(inputs["steps"], grid)
|
|
322
330
|
return Segmentation(
|
|
323
331
|
steps=placed.steps,
|
|
324
332
|
boundaries=placed.boundaries,
|
|
@@ -345,7 +345,11 @@ def resolve(doc: StepDocument) -> dict:
|
|
|
345
345
|
"""
|
|
346
346
|
grid = doc.metric
|
|
347
347
|
spu = seconds_per_unit(grid) if grid is not None else None
|
|
348
|
-
origin =
|
|
348
|
+
origin = (
|
|
349
|
+
float(Fraction(grid.origin))
|
|
350
|
+
if grid is not None and grid.origin is not None
|
|
351
|
+
else None
|
|
352
|
+
)
|
|
349
353
|
|
|
350
354
|
def _wall(offset_units: float) -> float | None:
|
|
351
355
|
if spu is None or origin is None:
|
|
@@ -499,42 +499,77 @@ def segment(
|
|
|
499
499
|
# ── the two v1 segmenters ───────────────────────────────────────────────────
|
|
500
500
|
|
|
501
501
|
|
|
502
|
-
def
|
|
502
|
+
def place_on_grid(steps: Sequence, grid: MetricGrid | Mapping) -> Segmentation:
|
|
503
503
|
"""Place an ordered step list on the timeline using a metric grid.
|
|
504
504
|
|
|
505
505
|
The que-calor shape: cumulative domain durations from the grid's origin,
|
|
506
506
|
``span = origin + cumulative_units * seconds_per_unit``. Children subdivide
|
|
507
|
-
their parent's span the same way.
|
|
507
|
+
their parent's span the same way. ``steps`` takes any form
|
|
508
|
+
:func:`segment` takes (``(name, duration)`` pairs, mappings with
|
|
509
|
+
``children``); every step needs a positive duration, and the grid a tempo
|
|
510
|
+
and an origin — anything missing or invalid is a ``ValueError`` naming it.
|
|
511
|
+
|
|
512
|
+
>>> seg = place_on_grid([("a", 1), ("b", 2)],
|
|
513
|
+
... {"unit": "eight", "subdivisions": 8,
|
|
514
|
+
... "tempoBpm": "120", "origin": "1"})
|
|
515
|
+
>>> [s.spans for s in seg.steps], seg.boundaries
|
|
516
|
+
([((1.0, 5.0),), ((5.0, 13.0),)], (1.0, 5.0, 13.0))
|
|
508
517
|
"""
|
|
509
|
-
|
|
518
|
+
|
|
519
|
+
def _all_rows(rows: list[dict]):
|
|
520
|
+
for row in rows:
|
|
521
|
+
yield row
|
|
522
|
+
yield from _all_rows(row["children"])
|
|
523
|
+
|
|
524
|
+
grid = _normalize_grid(grid)
|
|
525
|
+
rows = _normalize_steps(steps)
|
|
526
|
+
if grid is None:
|
|
527
|
+
raise ValueError("place_on_grid needs a grid; got None")
|
|
528
|
+
missing = [
|
|
529
|
+
name
|
|
530
|
+
for name, value in (("tempoBpm", grid.tempo_bpm), ("origin", grid.origin))
|
|
531
|
+
if value is None
|
|
532
|
+
]
|
|
533
|
+
if missing:
|
|
534
|
+
raise ValueError(f"place_on_grid needs a grid with {' and '.join(missing)}")
|
|
535
|
+
if not rows:
|
|
536
|
+
raise ValueError("place_on_grid needs at least one step")
|
|
537
|
+
undated = [row["id"] for row in _all_rows(rows) if row["duration"] is None]
|
|
538
|
+
if undated:
|
|
539
|
+
raise ValueError(f"place_on_grid needs a duration for every step: {undated}")
|
|
540
|
+
bad = [
|
|
541
|
+
row["id"]
|
|
542
|
+
for row in _all_rows(rows)
|
|
543
|
+
if not (math.isfinite(row["duration"]) and row["duration"] > 0)
|
|
544
|
+
]
|
|
545
|
+
if bad:
|
|
546
|
+
raise ValueError(f"place_on_grid needs positive, finite durations: {bad}")
|
|
510
547
|
spu = seconds_per_unit(grid)
|
|
511
|
-
origin = float(grid.origin)
|
|
548
|
+
origin = float(grid.origin)
|
|
512
549
|
|
|
513
|
-
def _place(rows: list[dict], at_units: float) ->
|
|
550
|
+
def _place(rows: list[dict], at_units: float) -> list[SegStep]:
|
|
514
551
|
placed = []
|
|
515
552
|
for row in rows:
|
|
516
553
|
duration = row["duration"]
|
|
517
554
|
start = origin + at_units * spu
|
|
518
555
|
end = origin + (at_units + duration) * spu
|
|
519
|
-
children, _ = _place(row["children"], at_units)
|
|
520
556
|
placed.append(
|
|
521
557
|
SegStep(
|
|
522
558
|
id=row["id"],
|
|
523
559
|
name=row["name"],
|
|
524
560
|
spans=((start, end),),
|
|
525
561
|
confidence=DFLT_GRID_CONFIDENCE,
|
|
526
|
-
children=tuple(children),
|
|
562
|
+
children=tuple(_place(row["children"], at_units)),
|
|
527
563
|
source="grid-placed",
|
|
528
564
|
evidence={"duration_units": duration, "offset_units": at_units},
|
|
529
565
|
)
|
|
530
566
|
)
|
|
531
567
|
at_units += duration
|
|
532
|
-
return placed
|
|
568
|
+
return placed
|
|
533
569
|
|
|
534
|
-
|
|
535
|
-
boundaries = tuple(origin + units * spu for units in _cumulative(inputs["steps"]))
|
|
570
|
+
boundaries = tuple(origin + units * spu for units in _cumulative(rows))
|
|
536
571
|
return Segmentation(
|
|
537
|
-
steps=tuple(
|
|
572
|
+
steps=tuple(_place(rows, 0.0)),
|
|
538
573
|
boundaries=boundaries,
|
|
539
574
|
unit=grid.unit,
|
|
540
575
|
grid=grid,
|
|
@@ -542,6 +577,11 @@ def _segment_grid_placed(inputs: Mapping[str, Any]) -> Segmentation:
|
|
|
542
577
|
)
|
|
543
578
|
|
|
544
579
|
|
|
580
|
+
def _segment_grid_placed(inputs: Mapping[str, Any]) -> Segmentation:
|
|
581
|
+
"""The ``grid-placed`` capability: :func:`place_on_grid` over the facade's inputs."""
|
|
582
|
+
return place_on_grid(inputs["steps"], inputs["grid"])
|
|
583
|
+
|
|
584
|
+
|
|
545
585
|
def _cumulative(rows: list[dict]) -> list[float]:
|
|
546
586
|
offsets, at = [0.0], 0.0
|
|
547
587
|
for row in rows:
|
|
@@ -170,3 +170,64 @@ def test_decimal_str_refuses_non_finite_values(value):
|
|
|
170
170
|
|
|
171
171
|
with pytest.raises(ValueError, match="non-finite"):
|
|
172
172
|
decimal_str(value)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_place_on_grid_is_the_public_placement():
|
|
176
|
+
from paces.segmenters import place_on_grid
|
|
177
|
+
|
|
178
|
+
seg = place_on_grid(ROUTINE_STEPS, GRID)
|
|
179
|
+
assert [s.spans for s in seg.steps] == [
|
|
180
|
+
s.spans for s in segment(steps=ROUTINE_STEPS, grid=GRID).steps
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@pytest.mark.parametrize(
|
|
185
|
+
"steps, grid, match",
|
|
186
|
+
[
|
|
187
|
+
(ROUTINE_STEPS, {"unit": "eight", "subdivisions": 8}, "tempoBpm and origin"),
|
|
188
|
+
(ROUTINE_STEPS, {**GRID, "origin": None}, "with origin$"),
|
|
189
|
+
([], GRID, "at least one step"),
|
|
190
|
+
([("back", -1)], GRID, r"positive, finite durations: \['back'\]"),
|
|
191
|
+
([("nan", float("nan"))], GRID, "positive, finite"),
|
|
192
|
+
([("zero", 0)], GRID, "positive, finite"),
|
|
193
|
+
(["no duration"], GRID, "duration for every step"),
|
|
194
|
+
([{"name": "p", "duration": 2, "children": ["c"]}], GRID, r"\['c'\]"),
|
|
195
|
+
],
|
|
196
|
+
)
|
|
197
|
+
def test_place_on_grid_names_what_is_missing(steps, grid, match):
|
|
198
|
+
from paces.segmenters import place_on_grid
|
|
199
|
+
|
|
200
|
+
with pytest.raises(ValueError, match=match):
|
|
201
|
+
place_on_grid(steps, grid)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@pytest.mark.parametrize("origin, start", [("0", 0.0), ("0.0", 0.0), ("51.2", 51.2)])
|
|
205
|
+
def test_resolve_computes_wall_clock_from_any_origin_including_zero(origin, start):
|
|
206
|
+
"""An origin of 0 is a real origin, not a missing one (#46)."""
|
|
207
|
+
from paces.model import resolve
|
|
208
|
+
|
|
209
|
+
doc = to_document(
|
|
210
|
+
segment(steps=[("a", 1)], grid={**GRID, "origin": origin}),
|
|
211
|
+
doc_id="d",
|
|
212
|
+
title="t",
|
|
213
|
+
)
|
|
214
|
+
assert resolve(doc)["steps"][0]["start_s"] == pytest.approx(start)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_resolve_without_an_origin_has_no_wall_clock():
|
|
218
|
+
from paces.model import resolve
|
|
219
|
+
|
|
220
|
+
doc = to_document(segment(steps=[("a", 1)], grid=GRID), doc_id="d", title="t")
|
|
221
|
+
doc = doc.model_copy(
|
|
222
|
+
update={"metric": doc.metric.model_copy(update={"origin": None})}
|
|
223
|
+
)
|
|
224
|
+
assert resolve(doc)["steps"][0]["start_s"] is None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_resolve_treats_a_numeric_zero_origin_as_an_origin():
|
|
228
|
+
"""A grid updated in place skips wire validation; 0 must still be 0 (#46)."""
|
|
229
|
+
from paces.model import resolve
|
|
230
|
+
|
|
231
|
+
doc = to_document(segment(steps=[("a", 1)], grid=GRID), doc_id="d", title="t")
|
|
232
|
+
doc = doc.model_copy(update={"metric": doc.metric.model_copy(update={"origin": 0})})
|
|
233
|
+
assert resolve(doc)["steps"][0]["start_s"] == 0.0
|
|
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
|
|
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
|