paces 0.0.21__tar.gz → 0.0.23__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.21 → paces-0.0.23}/PKG-INFO +1 -1
- {paces-0.0.21 → paces-0.0.23}/paces/measure.py +3 -0
- {paces-0.0.21 → paces-0.0.23}/paces/segmenters.py +116 -31
- {paces-0.0.21 → paces-0.0.23}/pyproject.toml +1 -1
- {paces-0.0.21 → paces-0.0.23}/tests/test_ask.py +2 -1
- {paces-0.0.21 → paces-0.0.23}/tests/test_segment.py +136 -1
- {paces-0.0.21 → paces-0.0.23}/.gitignore +0 -0
- {paces-0.0.21 → paces-0.0.23}/LICENSE +0 -0
- {paces-0.0.21 → paces-0.0.23}/README.md +0 -0
- {paces-0.0.21 → paces-0.0.23}/docs/README.md +0 -0
- {paces-0.0.21 → paces-0.0.23}/docs/alignment/README.md +0 -0
- {paces-0.0.21 → paces-0.0.23}/docs/poc-reference/README.md +0 -0
- {paces-0.0.21 → paces-0.0.23}/docs/poc-reference/artifacts/clips.json +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/__init__.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/__main__.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/ask.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/bodies/__init__.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/bodies/_base.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/bodies/derivation.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/bodies/document.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/bodies/signal.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/bodies/step.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/corrections.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/cues.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/data/ask/ask.css +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/data/ask/ask.html +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/data/ask/ask.js +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/derivation.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/edits.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/evidence.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/excerpts.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/model.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/pose.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/projection.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/render.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/segmentation.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/timed_text.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/paces/tools.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/audio_synth.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/data/body_schemas.json +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/data/routine.json +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/poc_fixture.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/test_chapters.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/test_cli_surface.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/test_corrections.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/test_cues.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/test_derivation.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/test_edit_protection_residuals.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/test_edits.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/test_evidence.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/test_evidence_schemas.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/test_excerpts.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/test_measure.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/test_pose.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/test_render_media.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/test_roundtrip_poc.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/test_smoke.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/tests/test_vertical_slice.py +0 -0
- {paces-0.0.21 → paces-0.0.23}/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.23
|
|
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,6 +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
|
+
check_step_durations,
|
|
48
49
|
place_on_grid,
|
|
49
50
|
register,
|
|
50
51
|
)
|
|
@@ -308,6 +309,8 @@ def _segment_grid_measured(inputs: Mapping[str, Any]) -> Segmentation:
|
|
|
308
309
|
partial: MetricGrid | None = inputs["grid"]
|
|
309
310
|
unit = partial.unit if partial is not None else DFLT_UNIT
|
|
310
311
|
subdivisions = partial.subdivisions if partial is not None else DFLT_SUBDIVISIONS
|
|
312
|
+
# bad durations fail here, before the costly measurement, not after it
|
|
313
|
+
check_step_durations(inputs["steps"], who="grid-measured")
|
|
311
314
|
total_units = sum(row["duration"] for row in inputs["steps"])
|
|
312
315
|
measurement = measure_grid(
|
|
313
316
|
inputs["media"],
|
|
@@ -39,7 +39,7 @@ from __future__ import annotations
|
|
|
39
39
|
import importlib
|
|
40
40
|
import math
|
|
41
41
|
import time
|
|
42
|
-
from collections.abc import Mapping, Sequence
|
|
42
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
43
43
|
from dataclasses import dataclass, field
|
|
44
44
|
from pathlib import Path
|
|
45
45
|
from typing import Any, Callable
|
|
@@ -131,18 +131,31 @@ def _slugify(text: str) -> str:
|
|
|
131
131
|
return slug or "step"
|
|
132
132
|
|
|
133
133
|
|
|
134
|
-
def unique_slug_ids(
|
|
134
|
+
def unique_slug_ids(
|
|
135
|
+
names: Sequence[str], *, fallback: str = "step", taken: Iterable[str] = ()
|
|
136
|
+
) -> list[str]:
|
|
135
137
|
"""One unique, git-readable id per name; unnamed ones get ``{fallback}-{i}``.
|
|
136
138
|
|
|
139
|
+
An id already in ``taken`` (or produced earlier in this call) gets a
|
|
140
|
+
``-2``, ``-3``… suffix — the first one free. ``taken`` is only read.
|
|
141
|
+
|
|
137
142
|
>>> unique_slug_ids(["Turn", "", "Turn"], fallback="cue")
|
|
138
143
|
['turn', 'cue-2', 'turn-2']
|
|
144
|
+
>>> unique_slug_ids(["Turn", "Turn", "Turn 2"])
|
|
145
|
+
['turn', 'turn-2', 'turn-2-2']
|
|
146
|
+
>>> unique_slug_ids(["Turn"], taken={"turn"})
|
|
147
|
+
['turn-2']
|
|
139
148
|
"""
|
|
140
|
-
|
|
149
|
+
used = set(taken)
|
|
141
150
|
ids = []
|
|
142
151
|
for index, name in enumerate(names):
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
152
|
+
base = _slugify(name) if name else f"{fallback}-{index + 1}"
|
|
153
|
+
candidate, n = base, 1
|
|
154
|
+
while candidate in used:
|
|
155
|
+
n += 1
|
|
156
|
+
candidate = f"{base}-{n}"
|
|
157
|
+
used.add(candidate)
|
|
158
|
+
ids.append(candidate)
|
|
146
159
|
return ids
|
|
147
160
|
|
|
148
161
|
|
|
@@ -150,29 +163,104 @@ def _normalize_steps(steps: Sequence | None) -> list[dict]:
|
|
|
150
163
|
"""Accept names, (name, duration) pairs, or mappings; emit uniform dicts.
|
|
151
164
|
|
|
152
165
|
Uniform shape: ``{id, name, duration (float units or None), spans, children}``.
|
|
166
|
+
Ids are unique across the whole tree, as a document requires. An id the
|
|
167
|
+
caller gave is kept as given, and two equal ones are a ``ValueError``
|
|
168
|
+
naming both paths. A generated one is the name's slug, suffixed ``-2``,
|
|
169
|
+
``-3``… when taken; generation runs breadth-first (every top-level row,
|
|
170
|
+
then each level of children), so editing children never renames a parent.
|
|
171
|
+
A generated id still follows position among same-named steps — give
|
|
172
|
+
repeated steps that must keep their identity across edits explicit ids.
|
|
173
|
+
|
|
174
|
+
>>> [r["id"] for r in _normalize_steps(["Turn", "Turn", {"name": "x", "id": "turn-2"}])]
|
|
175
|
+
['turn', 'turn-3', 'turn-2']
|
|
153
176
|
"""
|
|
177
|
+
rows = _step_rows(steps, where="steps")
|
|
178
|
+
|
|
179
|
+
def _paths(rows: list[dict], where: str):
|
|
180
|
+
for i, row in enumerate(rows):
|
|
181
|
+
yield f"{where}[{i}]", row
|
|
182
|
+
yield from _paths(row["children"], f"{where}[{i}].children")
|
|
183
|
+
|
|
184
|
+
explicit: dict[str, str] = {}
|
|
185
|
+
for path, row in _paths(rows, "steps"):
|
|
186
|
+
if row["id"] is None:
|
|
187
|
+
continue
|
|
188
|
+
if row["id"] in explicit:
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"duplicate step id {row['id']!r} at {explicit[row['id']]} and {path}"
|
|
191
|
+
)
|
|
192
|
+
explicit[row["id"]] = path
|
|
193
|
+
|
|
194
|
+
taken = set(explicit)
|
|
195
|
+
level = rows
|
|
196
|
+
while level:
|
|
197
|
+
pending = [row for row in level if row["id"] is None]
|
|
198
|
+
ids = unique_slug_ids(
|
|
199
|
+
[row.get("name") or "step" for row in pending], taken=taken
|
|
200
|
+
)
|
|
201
|
+
for row, step_id in zip(pending, ids):
|
|
202
|
+
row["id"] = step_id
|
|
203
|
+
taken.update(ids)
|
|
204
|
+
level = [child for row in level for child in row["children"]]
|
|
205
|
+
return rows
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _walk_rows(rows: list[dict]):
|
|
209
|
+
"""Every row of a normalised step tree, parents before children."""
|
|
210
|
+
for row in rows:
|
|
211
|
+
yield row
|
|
212
|
+
yield from _walk_rows(row["children"])
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _undated_ids(rows: list[dict]) -> list[str]:
|
|
216
|
+
"""Ids of the (nested) normalised rows that have no duration."""
|
|
217
|
+
return [row["id"] for row in _walk_rows(rows) if row["duration"] is None]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def check_step_durations(rows: list[dict], *, who: str = "place_on_grid") -> None:
|
|
221
|
+
"""Raise ``ValueError`` unless every (nested) row has a positive, finite
|
|
222
|
+
duration — the precondition for placing a step list on a grid. ``rows``
|
|
223
|
+
are normalised step rows (what :func:`segment` hands a segmenter in
|
|
224
|
+
``inputs["steps"]``), not the raw forms :func:`segment` accepts."""
|
|
225
|
+
undated = _undated_ids(rows)
|
|
226
|
+
if undated:
|
|
227
|
+
raise ValueError(f"{who} needs a duration for every step: {undated}")
|
|
228
|
+
bad = [
|
|
229
|
+
row["id"]
|
|
230
|
+
for row in _walk_rows(rows)
|
|
231
|
+
if not (math.isfinite(row["duration"]) and row["duration"] > 0)
|
|
232
|
+
]
|
|
233
|
+
if bad:
|
|
234
|
+
raise ValueError(f"{who} needs positive, finite durations: {bad}")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _step_rows(steps: Sequence | None, *, where: str) -> list[dict]:
|
|
154
238
|
if steps is None:
|
|
155
239
|
return []
|
|
156
240
|
rows: list[dict] = []
|
|
157
241
|
for i, item in enumerate(steps):
|
|
242
|
+
is_sequence = isinstance(item, Sequence) and not isinstance(item, bytes)
|
|
158
243
|
if isinstance(item, str):
|
|
159
244
|
row = {"name": item}
|
|
160
245
|
elif isinstance(item, Mapping):
|
|
161
246
|
row = dict(item)
|
|
162
|
-
elif
|
|
247
|
+
elif is_sequence and len(item) == 2:
|
|
163
248
|
name, duration = item
|
|
164
249
|
row = {"name": name, "duration": duration}
|
|
165
250
|
else:
|
|
251
|
+
got = f"a {len(item)}-item sequence" if is_sequence else type(item).__name__
|
|
166
252
|
raise TypeError(
|
|
167
|
-
f"
|
|
168
|
-
f"mapping — got {
|
|
253
|
+
f"{where}[{i}]: expected a name, a (name, duration) pair, or a "
|
|
254
|
+
f"mapping — got {got}"
|
|
169
255
|
)
|
|
170
|
-
row
|
|
256
|
+
row["id"] = row.get("id")
|
|
171
257
|
row["duration"] = (
|
|
172
258
|
None if row.get("duration") is None else float(row["duration"])
|
|
173
259
|
)
|
|
174
260
|
row["spans"] = [tuple(map(float, s)) for s in row.get("spans", ())]
|
|
175
|
-
row["children"] =
|
|
261
|
+
row["children"] = _step_rows(
|
|
262
|
+
row.get("children") or row.get("steps"), where=f"{where}[{i}].children"
|
|
263
|
+
)
|
|
176
264
|
rows.append(row)
|
|
177
265
|
return rows
|
|
178
266
|
|
|
@@ -296,10 +384,10 @@ def _facts(
|
|
|
296
384
|
pass
|
|
297
385
|
if steps:
|
|
298
386
|
facts.add("steps")
|
|
299
|
-
if
|
|
300
|
-
facts.add("steps.durations")
|
|
301
|
-
if any(row["spans"] for row in steps):
|
|
302
|
-
facts.add("steps.spans")
|
|
387
|
+
if not _undated_ids(steps):
|
|
388
|
+
facts.add("steps.durations") # children too: placement needs them all
|
|
389
|
+
if any(row["spans"] for row in _walk_rows(steps)):
|
|
390
|
+
facts.add("steps.spans") # like steps.durations: the whole tree
|
|
303
391
|
if boundaries:
|
|
304
392
|
facts.add("boundaries")
|
|
305
393
|
if grid is not None and grid.tempo_bpm is not None and grid.origin is not None:
|
|
@@ -371,6 +459,11 @@ def segment(
|
|
|
371
459
|
chapters = _normalize_chapters(metadata)
|
|
372
460
|
subtitles, subtitles_problem = _subtitle_cues(metadata)
|
|
373
461
|
problem_flags = (subtitles_problem,) if subtitles_problem else ()
|
|
462
|
+
undated = _undated_ids(step_rows)
|
|
463
|
+
if undated and len(undated) < sum(1 for _ in _walk_rows(step_rows)):
|
|
464
|
+
# durations were given but not for all: say which, so a refused or
|
|
465
|
+
# unplaced result never just asks for what the caller supplied
|
|
466
|
+
problem_flags += (f"steps-undated: {undated}",)
|
|
374
467
|
transcript_cues = normalize_timed_text(transcript)
|
|
375
468
|
# fail fast: a malformed log raises before any segmenter runs
|
|
376
469
|
correction_log = (
|
|
@@ -412,9 +505,15 @@ def segment(
|
|
|
412
505
|
raise ValueError(f"unknown segmenter {segmenter!r}; registered: {known}")
|
|
413
506
|
missing_facts = capability.needs - facts
|
|
414
507
|
if missing_facts:
|
|
508
|
+
undated_note = (
|
|
509
|
+
f"; steps without a duration: {undated}"
|
|
510
|
+
if "steps.durations" in missing_facts and undated
|
|
511
|
+
else ""
|
|
512
|
+
)
|
|
415
513
|
raise ValueError(
|
|
416
514
|
f"segmenter {capability.name!r} needs inputs you did not "
|
|
417
|
-
f"provide: {sorted(missing_facts)} (facts present:
|
|
515
|
+
f"provide: {sorted(missing_facts)} (facts present: "
|
|
516
|
+
f"{sorted(facts)}){undated_note}"
|
|
418
517
|
)
|
|
419
518
|
else:
|
|
420
519
|
candidates = [
|
|
@@ -516,11 +615,6 @@ def place_on_grid(steps: Sequence, grid: MetricGrid | Mapping) -> Segmentation:
|
|
|
516
615
|
([((1.0, 5.0),), ((5.0, 13.0),)], (1.0, 5.0, 13.0))
|
|
517
616
|
"""
|
|
518
617
|
|
|
519
|
-
def _all_rows(rows: list[dict]):
|
|
520
|
-
for row in rows:
|
|
521
|
-
yield row
|
|
522
|
-
yield from _all_rows(row["children"])
|
|
523
|
-
|
|
524
618
|
grid = _normalize_grid(grid)
|
|
525
619
|
rows = _normalize_steps(steps)
|
|
526
620
|
if grid is None:
|
|
@@ -534,16 +628,7 @@ def place_on_grid(steps: Sequence, grid: MetricGrid | Mapping) -> Segmentation:
|
|
|
534
628
|
raise ValueError(f"place_on_grid needs a grid with {' and '.join(missing)}")
|
|
535
629
|
if not rows:
|
|
536
630
|
raise ValueError("place_on_grid needs at least one step")
|
|
537
|
-
|
|
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}")
|
|
631
|
+
check_step_durations(rows)
|
|
547
632
|
spu = seconds_per_unit(grid)
|
|
548
633
|
origin = float(grid.origin)
|
|
549
634
|
|
|
@@ -456,7 +456,8 @@ def test_js_repeated_names_are_separate_rows():
|
|
|
456
456
|
"""verse/chorus/verse: each occurrence is marked on its own, and replays."""
|
|
457
457
|
names = ["verse", "chorus", "verse"]
|
|
458
458
|
proposal = propose(segment(boundaries=CUTS, steps=names))
|
|
459
|
-
|
|
459
|
+
# ids are unique now (#47), but rows stay the key: names still repeat
|
|
460
|
+
assert [s["id"] for s in proposal["steps"]] == ["verse", "chorus", "verse-2"]
|
|
460
461
|
answers = _answers_after(proposal, [{"type": "absent", "index": 2, "on": True}])
|
|
461
462
|
assert [(c["verb"], c["anchorT"]) for c in answers["corrections"]] == [
|
|
462
463
|
("absent", "25")
|
|
@@ -8,7 +8,13 @@ import pytest
|
|
|
8
8
|
|
|
9
9
|
from paces.model import MetricGrid
|
|
10
10
|
from paces.projection import to_document
|
|
11
|
-
from paces.segmenters import
|
|
11
|
+
from paces.segmenters import (
|
|
12
|
+
Capability,
|
|
13
|
+
Segmentation,
|
|
14
|
+
capabilities,
|
|
15
|
+
register,
|
|
16
|
+
segment,
|
|
17
|
+
)
|
|
12
18
|
|
|
13
19
|
GRID = {"unit": "eight", "subdivisions": 8, "tempoBpm": "129.2", "origin": "51.2"}
|
|
14
20
|
#: The POC routine as (name, eights) pairs — kickoff's "common real case".
|
|
@@ -231,3 +237,132 @@ def test_resolve_treats_a_numeric_zero_origin_as_an_origin():
|
|
|
231
237
|
doc = to_document(segment(steps=[("a", 1)], grid=GRID), doc_id="d", title="t")
|
|
232
238
|
doc = doc.model_copy(update={"metric": doc.metric.model_copy(update={"origin": 0})})
|
|
233
239
|
assert resolve(doc)["steps"][0]["start_s"] == 0.0
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_duplicate_step_names_get_unique_ids_tree_wide():
|
|
243
|
+
"""Edits, locks and the evidence layer address steps by id (#47)."""
|
|
244
|
+
steps = [
|
|
245
|
+
("Turn", 2),
|
|
246
|
+
("Turn", 2),
|
|
247
|
+
{"name": "Kick", "duration": 4, "children": [("Turn", 2), ("Turn", 2)]},
|
|
248
|
+
]
|
|
249
|
+
seg = segment(steps=steps, grid=GRID)
|
|
250
|
+
ids = [seg.steps[0].id, seg.steps[1].id, *(c.id for c in seg.steps[2].children)]
|
|
251
|
+
assert ids == ["turn", "turn-2", "turn-3", "turn-4"]
|
|
252
|
+
doc = to_document(seg, doc_id="d", title="t")
|
|
253
|
+
from paces.model import validate_document
|
|
254
|
+
|
|
255
|
+
assert not [i for i in validate_document(doc) if "duplicate step id" in i]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def test_an_explicit_step_id_is_kept_and_never_reused():
|
|
259
|
+
seg = segment(
|
|
260
|
+
steps=[("Turn", 1), {"name": "x", "id": "turn", "duration": 1}], grid=GRID
|
|
261
|
+
)
|
|
262
|
+
assert [s.id for s in seg.steps] == ["turn-2", "turn"]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_a_step_that_is_neither_name_pair_nor_mapping_is_named_in_the_error():
|
|
266
|
+
with pytest.raises(TypeError, match=r"steps\[1\].*3-item sequence"):
|
|
267
|
+
segment(steps=[("a", 1), ("b", 1, "extra")], grid=GRID)
|
|
268
|
+
with pytest.raises(TypeError, match=r"steps\[0\]\.children\[0\]"):
|
|
269
|
+
segment(steps=[{"name": "p", "duration": 1, "children": [3]}], grid=GRID)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_bytes_is_not_a_name_duration_pair():
|
|
273
|
+
with pytest.raises(TypeError, match=r"steps\[0\].*got bytes"):
|
|
274
|
+
segment(steps=[b"ab"], grid=GRID)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_duplicate_explicit_ids_name_both_paths():
|
|
278
|
+
steps = [
|
|
279
|
+
{"name": "a", "id": "x", "duration": 1},
|
|
280
|
+
("b", 1),
|
|
281
|
+
{"name": "c", "duration": 2, "children": [("d", 1), {"name": "e", "id": "x"}]},
|
|
282
|
+
]
|
|
283
|
+
with pytest.raises(
|
|
284
|
+
ValueError, match=r"'x' at steps\[0\] and steps\[2\]\.children\[1\]"
|
|
285
|
+
):
|
|
286
|
+
segment(steps=steps, grid=GRID)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_generated_ids_are_breadth_first_so_children_never_rename_parents():
|
|
290
|
+
before = segment(steps=[{"name": "Turn", "duration": 2}, ("Turn", 2)], grid=GRID)
|
|
291
|
+
after = segment(
|
|
292
|
+
steps=[{"name": "Turn", "duration": 2, "children": [("Turn", 2)]}, ("Turn", 2)],
|
|
293
|
+
grid=GRID,
|
|
294
|
+
)
|
|
295
|
+
assert [s.id for s in before.steps] == [s.id for s in after.steps]
|
|
296
|
+
assert [s.id for s in after.steps] == ["turn", "turn-2"]
|
|
297
|
+
assert after.steps[0].children[0].id == "turn-3"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def test_partly_undated_steps_are_named_in_a_flag():
|
|
301
|
+
"""The refused state's 'try:' must not read as if no durations were given."""
|
|
302
|
+
steps = [("a", 2), {"name": "p", "duration": 2, "children": ["u"]}]
|
|
303
|
+
seg = segment(steps=steps, grid=GRID)
|
|
304
|
+
assert "steps-undated: ['u']" in seg.flags
|
|
305
|
+
assert not [f for f in segment(steps=["a"], grid=GRID).flags if "undated" in f]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _forbid_measuring(monkeypatch):
|
|
309
|
+
import paces.measure
|
|
310
|
+
|
|
311
|
+
def _measure_grid(*args, **kwargs):
|
|
312
|
+
raise AssertionError("measure_grid must not be called")
|
|
313
|
+
|
|
314
|
+
monkeypatch.setattr(paces.measure, "measure_grid", _measure_grid)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def test_an_undated_child_keeps_placement_from_being_selected(tmp_path, monkeypatch):
|
|
318
|
+
"""'steps.durations' covers children, so no placing segmenter is chosen —
|
|
319
|
+
and grid-measured is never asked to measure for a list it cannot place."""
|
|
320
|
+
steps = [{"name": "p", "duration": 2, "children": ["undated"]}]
|
|
321
|
+
seg = segment(steps=steps, grid=GRID)
|
|
322
|
+
assert seg.method != "grid-placed"
|
|
323
|
+
media = tmp_path / "song.wav"
|
|
324
|
+
media.write_bytes(b"")
|
|
325
|
+
_forbid_measuring(monkeypatch)
|
|
326
|
+
with pytest.raises(
|
|
327
|
+
ValueError,
|
|
328
|
+
match=r"needs inputs you did not provide: \['steps\.durations'\]"
|
|
329
|
+
r".*steps without a duration: \['undated'\]",
|
|
330
|
+
):
|
|
331
|
+
segment(str(media), steps=steps, segmenter="grid-measured")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@pytest.mark.parametrize("bad", [0, -1, float("nan")])
|
|
335
|
+
def test_grid_measured_rejects_bad_durations_before_measuring(
|
|
336
|
+
bad, tmp_path, monkeypatch
|
|
337
|
+
):
|
|
338
|
+
pytest.importorskip("mixing")
|
|
339
|
+
pytest.importorskip("librosa")
|
|
340
|
+
media = tmp_path / "song.wav"
|
|
341
|
+
media.write_bytes(b"")
|
|
342
|
+
_forbid_measuring(monkeypatch)
|
|
343
|
+
steps = [("a", 1), {"name": "p", "duration": 2, "children": [("c", bad)]}]
|
|
344
|
+
with pytest.raises(ValueError, match=r"positive, finite durations: \['c'\]"):
|
|
345
|
+
segment(str(media), steps=steps, segmenter="grid-measured")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _spans_reader(inputs):
|
|
349
|
+
return Segmentation(boundaries=(0.0, 1.0))
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def test_the_spans_fact_reads_the_whole_step_tree():
|
|
353
|
+
"""A span on a child counts, as a duration on a child does (#51): a
|
|
354
|
+
segmenter that needs ``steps.spans`` is selectable from a nested span."""
|
|
355
|
+
needs_spans = Capability(
|
|
356
|
+
name="needs-spans",
|
|
357
|
+
gives="segmentation",
|
|
358
|
+
summary="test double",
|
|
359
|
+
target=f"{__name__}:_spans_reader",
|
|
360
|
+
needs=frozenset({"steps", "steps.spans"}),
|
|
361
|
+
)
|
|
362
|
+
catalog = {"needs-spans": needs_spans}
|
|
363
|
+
nested = [
|
|
364
|
+
{"name": "p", "duration": 2, "children": [{"name": "c", "spans": [(1, 2)]}]}
|
|
365
|
+
]
|
|
366
|
+
assert segment(steps=nested, catalog=catalog).method == "needs-spans"
|
|
367
|
+
refused = segment(steps=[("p", 2)], catalog=catalog)
|
|
368
|
+
assert refused.method == "" and "no-signal" in refused.flags
|
|
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
|