paces 0.0.19__tar.gz → 0.0.20__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.20}/PKG-INFO +1 -1
- {paces-0.0.19 → paces-0.0.20}/paces/measure.py +2 -2
- {paces-0.0.19 → paces-0.0.20}/paces/segmenters.py +51 -11
- {paces-0.0.19 → paces-0.0.20}/pyproject.toml +1 -1
- {paces-0.0.19 → paces-0.0.20}/tests/test_segment.py +29 -0
- {paces-0.0.19 → paces-0.0.20}/.gitignore +0 -0
- {paces-0.0.19 → paces-0.0.20}/LICENSE +0 -0
- {paces-0.0.19 → paces-0.0.20}/README.md +0 -0
- {paces-0.0.19 → paces-0.0.20}/docs/README.md +0 -0
- {paces-0.0.19 → paces-0.0.20}/docs/alignment/README.md +0 -0
- {paces-0.0.19 → paces-0.0.20}/docs/poc-reference/README.md +0 -0
- {paces-0.0.19 → paces-0.0.20}/docs/poc-reference/artifacts/clips.json +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/__init__.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/__main__.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/ask.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/bodies/__init__.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/bodies/_base.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/bodies/derivation.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/bodies/document.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/bodies/signal.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/bodies/step.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/corrections.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/cues.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/data/ask/ask.css +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/data/ask/ask.html +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/data/ask/ask.js +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/derivation.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/edits.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/evidence.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/excerpts.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/model.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/pose.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/projection.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/render.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/segmentation.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/timed_text.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/paces/tools.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/audio_synth.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/data/body_schemas.json +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/data/routine.json +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/poc_fixture.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/test_ask.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/test_chapters.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/test_cli_surface.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/test_corrections.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/test_cues.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/test_derivation.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/test_edit_protection_residuals.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/test_edits.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/test_evidence.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/test_evidence_schemas.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/test_excerpts.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/test_measure.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/test_pose.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/test_render_media.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/test_roundtrip_poc.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/test_smoke.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/tests/test_vertical_slice.py +0 -0
- {paces-0.0.19 → paces-0.0.20}/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.20
|
|
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
|
|
|
@@ -318,7 +318,7 @@ def _segment_grid_measured(inputs: Mapping[str, Any]) -> Segmentation:
|
|
|
318
318
|
grid = measurement.grid
|
|
319
319
|
if grid.tempo_bpm is None or grid.origin is None:
|
|
320
320
|
return Segmentation(grid=grid, confidence=measurement.confidence, flags=flags)
|
|
321
|
-
placed =
|
|
321
|
+
placed = place_on_grid(inputs["steps"], grid)
|
|
322
322
|
return Segmentation(
|
|
323
323
|
steps=placed.steps,
|
|
324
324
|
boundaries=placed.boundaries,
|
|
@@ -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,32 @@ 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)
|
|
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
|
|
File without changes
|