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.
Files changed (59) hide show
  1. {paces-0.0.19 → paces-0.0.20}/PKG-INFO +1 -1
  2. {paces-0.0.19 → paces-0.0.20}/paces/measure.py +2 -2
  3. {paces-0.0.19 → paces-0.0.20}/paces/segmenters.py +51 -11
  4. {paces-0.0.19 → paces-0.0.20}/pyproject.toml +1 -1
  5. {paces-0.0.19 → paces-0.0.20}/tests/test_segment.py +29 -0
  6. {paces-0.0.19 → paces-0.0.20}/.gitignore +0 -0
  7. {paces-0.0.19 → paces-0.0.20}/LICENSE +0 -0
  8. {paces-0.0.19 → paces-0.0.20}/README.md +0 -0
  9. {paces-0.0.19 → paces-0.0.20}/docs/README.md +0 -0
  10. {paces-0.0.19 → paces-0.0.20}/docs/alignment/README.md +0 -0
  11. {paces-0.0.19 → paces-0.0.20}/docs/poc-reference/README.md +0 -0
  12. {paces-0.0.19 → paces-0.0.20}/docs/poc-reference/artifacts/clips.json +0 -0
  13. {paces-0.0.19 → paces-0.0.20}/paces/__init__.py +0 -0
  14. {paces-0.0.19 → paces-0.0.20}/paces/__main__.py +0 -0
  15. {paces-0.0.19 → paces-0.0.20}/paces/ask.py +0 -0
  16. {paces-0.0.19 → paces-0.0.20}/paces/bodies/__init__.py +0 -0
  17. {paces-0.0.19 → paces-0.0.20}/paces/bodies/_base.py +0 -0
  18. {paces-0.0.19 → paces-0.0.20}/paces/bodies/derivation.py +0 -0
  19. {paces-0.0.19 → paces-0.0.20}/paces/bodies/document.py +0 -0
  20. {paces-0.0.19 → paces-0.0.20}/paces/bodies/signal.py +0 -0
  21. {paces-0.0.19 → paces-0.0.20}/paces/bodies/step.py +0 -0
  22. {paces-0.0.19 → paces-0.0.20}/paces/corrections.py +0 -0
  23. {paces-0.0.19 → paces-0.0.20}/paces/cues.py +0 -0
  24. {paces-0.0.19 → paces-0.0.20}/paces/data/ask/ask.css +0 -0
  25. {paces-0.0.19 → paces-0.0.20}/paces/data/ask/ask.html +0 -0
  26. {paces-0.0.19 → paces-0.0.20}/paces/data/ask/ask.js +0 -0
  27. {paces-0.0.19 → paces-0.0.20}/paces/derivation.py +0 -0
  28. {paces-0.0.19 → paces-0.0.20}/paces/edits.py +0 -0
  29. {paces-0.0.19 → paces-0.0.20}/paces/evidence.py +0 -0
  30. {paces-0.0.19 → paces-0.0.20}/paces/excerpts.py +0 -0
  31. {paces-0.0.19 → paces-0.0.20}/paces/model.py +0 -0
  32. {paces-0.0.19 → paces-0.0.20}/paces/pose.py +0 -0
  33. {paces-0.0.19 → paces-0.0.20}/paces/projection.py +0 -0
  34. {paces-0.0.19 → paces-0.0.20}/paces/render.py +0 -0
  35. {paces-0.0.19 → paces-0.0.20}/paces/segmentation.py +0 -0
  36. {paces-0.0.19 → paces-0.0.20}/paces/timed_text.py +0 -0
  37. {paces-0.0.19 → paces-0.0.20}/paces/tools.py +0 -0
  38. {paces-0.0.19 → paces-0.0.20}/tests/audio_synth.py +0 -0
  39. {paces-0.0.19 → paces-0.0.20}/tests/data/body_schemas.json +0 -0
  40. {paces-0.0.19 → paces-0.0.20}/tests/data/routine.json +0 -0
  41. {paces-0.0.19 → paces-0.0.20}/tests/poc_fixture.py +0 -0
  42. {paces-0.0.19 → paces-0.0.20}/tests/test_ask.py +0 -0
  43. {paces-0.0.19 → paces-0.0.20}/tests/test_chapters.py +0 -0
  44. {paces-0.0.19 → paces-0.0.20}/tests/test_cli_surface.py +0 -0
  45. {paces-0.0.19 → paces-0.0.20}/tests/test_corrections.py +0 -0
  46. {paces-0.0.19 → paces-0.0.20}/tests/test_cues.py +0 -0
  47. {paces-0.0.19 → paces-0.0.20}/tests/test_derivation.py +0 -0
  48. {paces-0.0.19 → paces-0.0.20}/tests/test_edit_protection_residuals.py +0 -0
  49. {paces-0.0.19 → paces-0.0.20}/tests/test_edits.py +0 -0
  50. {paces-0.0.19 → paces-0.0.20}/tests/test_evidence.py +0 -0
  51. {paces-0.0.19 → paces-0.0.20}/tests/test_evidence_schemas.py +0 -0
  52. {paces-0.0.19 → paces-0.0.20}/tests/test_excerpts.py +0 -0
  53. {paces-0.0.19 → paces-0.0.20}/tests/test_measure.py +0 -0
  54. {paces-0.0.19 → paces-0.0.20}/tests/test_pose.py +0 -0
  55. {paces-0.0.19 → paces-0.0.20}/tests/test_render_media.py +0 -0
  56. {paces-0.0.19 → paces-0.0.20}/tests/test_roundtrip_poc.py +0 -0
  57. {paces-0.0.19 → paces-0.0.20}/tests/test_smoke.py +0 -0
  58. {paces-0.0.19 → paces-0.0.20}/tests/test_vertical_slice.py +0 -0
  59. {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.19
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
- _segment_grid_placed,
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 = _segment_grid_placed({**inputs, "grid": grid})
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 _segment_grid_placed(inputs: Mapping[str, Any]) -> Segmentation:
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
- grid: MetricGrid = inputs["grid"]
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) # guaranteed by the 'grid' fact
548
+ origin = float(grid.origin)
512
549
 
513
- def _place(rows: list[dict], at_units: float) -> tuple[list[SegStep], 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, at_units
568
+ return placed
533
569
 
534
- steps, total_units = _place(inputs["steps"], 0.0)
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(steps),
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:
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "paces"
9
- version = "0.0.19"
9
+ version = "0.0.20"
10
10
  description = "Turn instructional media into structured, interactive learning material"
11
11
  readme = "README.md"
12
12
  license = "MIT"
@@ -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