paces 0.0.9__tar.gz → 0.0.11__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.9 → paces-0.0.11}/PKG-INFO +66 -4
- {paces-0.0.9 → paces-0.0.11}/README.md +62 -3
- {paces-0.0.9 → paces-0.0.11}/paces/__init__.py +30 -0
- paces-0.0.11/paces/bodies/__init__.py +111 -0
- paces-0.0.11/paces/bodies/_base.py +24 -0
- paces-0.0.11/paces/bodies/derivation.py +76 -0
- paces-0.0.11/paces/bodies/document.py +118 -0
- paces-0.0.11/paces/bodies/signal.py +122 -0
- paces-0.0.11/paces/bodies/step.py +155 -0
- {paces-0.0.9 → paces-0.0.11}/paces/edits.py +110 -5
- paces-0.0.11/paces/evidence.py +1302 -0
- {paces-0.0.9 → paces-0.0.11}/paces/model.py +89 -4
- {paces-0.0.9 → paces-0.0.11}/pyproject.toml +21 -2
- paces-0.0.11/tests/data/body_schemas.json +746 -0
- paces-0.0.11/tests/test_edit_protection_residuals.py +254 -0
- paces-0.0.11/tests/test_evidence.py +684 -0
- paces-0.0.11/tests/test_evidence_schemas.py +94 -0
- {paces-0.0.9 → paces-0.0.11}/.gitignore +0 -0
- {paces-0.0.9 → paces-0.0.11}/LICENSE +0 -0
- {paces-0.0.9 → paces-0.0.11}/docs/README.md +0 -0
- {paces-0.0.9 → paces-0.0.11}/docs/alignment/README.md +0 -0
- {paces-0.0.9 → paces-0.0.11}/docs/poc-reference/README.md +0 -0
- {paces-0.0.9 → paces-0.0.11}/docs/poc-reference/artifacts/clips.json +0 -0
- {paces-0.0.9 → paces-0.0.11}/paces/__main__.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/paces/derivation.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/paces/excerpts.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/paces/measure.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/paces/pose.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/paces/projection.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/paces/render.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/paces/segmenters.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/paces/tools.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/tests/audio_synth.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/tests/data/routine.json +0 -0
- {paces-0.0.9 → paces-0.0.11}/tests/poc_fixture.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/tests/test_chapters.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/tests/test_cli_surface.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/tests/test_derivation.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/tests/test_edits.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/tests/test_excerpts.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/tests/test_measure.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/tests/test_pose.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/tests/test_render_media.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/tests/test_roundtrip_poc.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/tests/test_segment.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/tests/test_smoke.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/tests/test_vertical_slice.py +0 -0
- {paces-0.0.9 → paces-0.0.11}/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.11
|
|
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
|
|
@@ -20,6 +20,7 @@ Requires-Dist: argcomplete>=3; extra == 'cli'
|
|
|
20
20
|
Requires-Dist: cw<0.2,>=0.1.1; extra == 'cli'
|
|
21
21
|
Provides-Extra: dev
|
|
22
22
|
Requires-Dist: cw<0.2,>=0.1.1; extra == 'dev'
|
|
23
|
+
Requires-Dist: lacing>=0.0.40; extra == 'dev'
|
|
23
24
|
Requires-Dist: mixing[audio,beats]>=0.0.39; extra == 'dev'
|
|
24
25
|
Requires-Dist: numba>=0.59; extra == 'dev'
|
|
25
26
|
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
@@ -29,6 +30,8 @@ Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'dev'
|
|
|
29
30
|
Provides-Extra: docs
|
|
30
31
|
Requires-Dist: sphinx-rtd-theme>=1.0; extra == 'docs'
|
|
31
32
|
Requires-Dist: sphinx>=6.0; extra == 'docs'
|
|
33
|
+
Provides-Extra: lacing
|
|
34
|
+
Requires-Dist: lacing>=0.0.40; extra == 'lacing'
|
|
32
35
|
Provides-Extra: media
|
|
33
36
|
Requires-Dist: mixing>=0.0.39; extra == 'media'
|
|
34
37
|
Provides-Extra: pose
|
|
@@ -152,6 +155,64 @@ run-through and the breakdown are the same step seen twice). Uncertainty is
|
|
|
152
155
|
content (`OpenQuestion`), and human edits are protected from regeneration
|
|
153
156
|
(`Lock`).
|
|
154
157
|
|
|
158
|
+
## The evidence layer
|
|
159
|
+
|
|
160
|
+
The document is the *contract*; the machine evidence behind it — the
|
|
161
|
+
speech/music split, the metric grid, the beats, the transcript, the step
|
|
162
|
+
candidates, the crop recipes, and the lineage between them — lives in a
|
|
163
|
+
[`lacing`](https://github.com/thorwhalen/lacing) store, and the document is a
|
|
164
|
+
**projection** of it. The document is derivable from the store; the store is
|
|
165
|
+
not derivable from the document.
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from lacing import MemoryStore # pip install 'paces[lacing]'
|
|
169
|
+
from paces import segment, from_store, to_store
|
|
170
|
+
from paces.model import dumps_document
|
|
171
|
+
|
|
172
|
+
grid = {"unit": "eight", "subdivisions": 8, "tempoBpm": "129.2", "origin": "51.2"}
|
|
173
|
+
asset_sha256 = "a" * 64 # lacing.hash_file(video) in real use
|
|
174
|
+
seg = segment(None, steps=[("Mise en place", 2), ("Déhanchés", 8)], grid=grid)
|
|
175
|
+
|
|
176
|
+
store = MemoryStore() # or SqliteStore('project.annot')
|
|
177
|
+
guide = dict(doc_id="que-calor", title="Que Calor", domain="dance",
|
|
178
|
+
source="https://youtu.be/q_TUyxUhoEw")
|
|
179
|
+
|
|
180
|
+
write = to_store(seg, store=store, asset_id=asset_sha256, **guide)
|
|
181
|
+
|
|
182
|
+
dumps_document(from_store(store, asset_id=asset_sha256)) == dumps_document(write.document)
|
|
183
|
+
# True — the projection is exact
|
|
184
|
+
|
|
185
|
+
to_store(seg, store=store, asset_id=asset_sha256, **guide).written
|
|
186
|
+
# 0 — a re-run of the same analysis writes nothing
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The store is injected, never constructed for you: `MemoryStore()` in tests, a
|
|
190
|
+
`SqliteStore` for a project sidecar, any `dol` store that conforms. Annotation
|
|
191
|
+
ids are derived (`uuid5`) from the evidence they stand for, so a re-derivation
|
|
192
|
+
*is* the same annotation — which is what keeps `was_derived_from` lineage
|
|
193
|
+
resolving and lets `write.document`'s `Origin.annotationId` be committed. Rows
|
|
194
|
+
whose value digest did not change are left completely alone, so freshness does
|
|
195
|
+
not fire on a no-op re-run.
|
|
196
|
+
|
|
197
|
+
`to_store` is a **re-derivation** of the guide, not a merge into it: a step
|
|
198
|
+
that no longer exists is dropped rather than left to be resurrected by the
|
|
199
|
+
next projection. Pruning only ever touches this asset, this `doc_id`, and the
|
|
200
|
+
guide's own tiers. Evidence *about the asset* — the speech/music split, the
|
|
201
|
+
beats, the transcript — is shared by every guide over that asset and is never
|
|
202
|
+
pruned, so one guide can't delete what another's lineage points at; a
|
|
203
|
+
re-measure adds a row under a new content-derived key and the old one stays.
|
|
204
|
+
`prune=False` opts out of the rest.
|
|
205
|
+
|
|
206
|
+
Analysis a `Segmentation` cannot carry rides along as keywords: `passes=`
|
|
207
|
+
(the speech/music split), `beats=`, `transcript=`, `cues=`, `recipes=`. What
|
|
208
|
+
stays on the document and never flows back into the store: `locks`,
|
|
209
|
+
`questions`, `artifacts`, and span `excerpt` windows — those are human
|
|
210
|
+
decisions, not measurements. Full table and rationale in
|
|
211
|
+
`docs/07-annotation-model.md` §6.
|
|
212
|
+
|
|
213
|
+
`import paces` does not import `lacing`: the core stays pydantic-only, and
|
|
214
|
+
`paces.to_store` resolves the extra on first use.
|
|
215
|
+
|
|
155
216
|
## The pieces
|
|
156
217
|
|
|
157
218
|
| you want | reach for |
|
|
@@ -164,6 +225,7 @@ content (`OpenQuestion`), and human edits are protected from regeneration
|
|
|
164
225
|
| the committed artifact | `to_document(seg, ...)` → `StepDocument` |
|
|
165
226
|
| real clips/gifs/posters for the page | `derive_document(doc, media=..., doc_path=...)` / `paces derive` (`pip install paces[media]`) |
|
|
166
227
|
| auto-crop those clips to the people in frame | `derive(..., subject_locator=paces.pose.rtmlib_pose)` / `--subject-locator paces.pose:rtmlib_pose` (`pip install paces[pose]`) |
|
|
228
|
+
| persist the analysis behind a document | `to_store(seg, store=..., asset_id=...)` / `from_store(store, asset_id=...)` (`pip install paces[lacing]`) |
|
|
167
229
|
| a practice page | `render_html(doc)` |
|
|
168
230
|
| wall-clock times from counts | `resolve(doc)` |
|
|
169
231
|
| sanity checks | `validate_document(doc)` |
|
|
@@ -175,6 +237,6 @@ content (`OpenQuestion`), and human edits are protected from regeneration
|
|
|
175
237
|
Young and moving. The document schema is validated by round-tripping a real
|
|
176
238
|
proof of concept ([an interactive dance-practice
|
|
177
239
|
page](https://thorwhalen.com/que_calor_dance/)) through it — see
|
|
178
|
-
`tests/test_roundtrip_poc.py`. Media derivation (auto-cropped looping clips)
|
|
179
|
-
intrinsic segmenters (scene/beat/speech
|
|
180
|
-
are designed (see `docs/`) and arrive next.
|
|
240
|
+
`tests/test_roundtrip_poc.py`. Media derivation (auto-cropped looping clips)
|
|
241
|
+
and the evidence layer have landed; intrinsic segmenters (scene/beat/speech
|
|
242
|
+
detection) are designed (see `docs/`) and arrive next.
|
|
@@ -114,6 +114,64 @@ run-through and the breakdown are the same step seen twice). Uncertainty is
|
|
|
114
114
|
content (`OpenQuestion`), and human edits are protected from regeneration
|
|
115
115
|
(`Lock`).
|
|
116
116
|
|
|
117
|
+
## The evidence layer
|
|
118
|
+
|
|
119
|
+
The document is the *contract*; the machine evidence behind it — the
|
|
120
|
+
speech/music split, the metric grid, the beats, the transcript, the step
|
|
121
|
+
candidates, the crop recipes, and the lineage between them — lives in a
|
|
122
|
+
[`lacing`](https://github.com/thorwhalen/lacing) store, and the document is a
|
|
123
|
+
**projection** of it. The document is derivable from the store; the store is
|
|
124
|
+
not derivable from the document.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from lacing import MemoryStore # pip install 'paces[lacing]'
|
|
128
|
+
from paces import segment, from_store, to_store
|
|
129
|
+
from paces.model import dumps_document
|
|
130
|
+
|
|
131
|
+
grid = {"unit": "eight", "subdivisions": 8, "tempoBpm": "129.2", "origin": "51.2"}
|
|
132
|
+
asset_sha256 = "a" * 64 # lacing.hash_file(video) in real use
|
|
133
|
+
seg = segment(None, steps=[("Mise en place", 2), ("Déhanchés", 8)], grid=grid)
|
|
134
|
+
|
|
135
|
+
store = MemoryStore() # or SqliteStore('project.annot')
|
|
136
|
+
guide = dict(doc_id="que-calor", title="Que Calor", domain="dance",
|
|
137
|
+
source="https://youtu.be/q_TUyxUhoEw")
|
|
138
|
+
|
|
139
|
+
write = to_store(seg, store=store, asset_id=asset_sha256, **guide)
|
|
140
|
+
|
|
141
|
+
dumps_document(from_store(store, asset_id=asset_sha256)) == dumps_document(write.document)
|
|
142
|
+
# True — the projection is exact
|
|
143
|
+
|
|
144
|
+
to_store(seg, store=store, asset_id=asset_sha256, **guide).written
|
|
145
|
+
# 0 — a re-run of the same analysis writes nothing
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The store is injected, never constructed for you: `MemoryStore()` in tests, a
|
|
149
|
+
`SqliteStore` for a project sidecar, any `dol` store that conforms. Annotation
|
|
150
|
+
ids are derived (`uuid5`) from the evidence they stand for, so a re-derivation
|
|
151
|
+
*is* the same annotation — which is what keeps `was_derived_from` lineage
|
|
152
|
+
resolving and lets `write.document`'s `Origin.annotationId` be committed. Rows
|
|
153
|
+
whose value digest did not change are left completely alone, so freshness does
|
|
154
|
+
not fire on a no-op re-run.
|
|
155
|
+
|
|
156
|
+
`to_store` is a **re-derivation** of the guide, not a merge into it: a step
|
|
157
|
+
that no longer exists is dropped rather than left to be resurrected by the
|
|
158
|
+
next projection. Pruning only ever touches this asset, this `doc_id`, and the
|
|
159
|
+
guide's own tiers. Evidence *about the asset* — the speech/music split, the
|
|
160
|
+
beats, the transcript — is shared by every guide over that asset and is never
|
|
161
|
+
pruned, so one guide can't delete what another's lineage points at; a
|
|
162
|
+
re-measure adds a row under a new content-derived key and the old one stays.
|
|
163
|
+
`prune=False` opts out of the rest.
|
|
164
|
+
|
|
165
|
+
Analysis a `Segmentation` cannot carry rides along as keywords: `passes=`
|
|
166
|
+
(the speech/music split), `beats=`, `transcript=`, `cues=`, `recipes=`. What
|
|
167
|
+
stays on the document and never flows back into the store: `locks`,
|
|
168
|
+
`questions`, `artifacts`, and span `excerpt` windows — those are human
|
|
169
|
+
decisions, not measurements. Full table and rationale in
|
|
170
|
+
`docs/07-annotation-model.md` §6.
|
|
171
|
+
|
|
172
|
+
`import paces` does not import `lacing`: the core stays pydantic-only, and
|
|
173
|
+
`paces.to_store` resolves the extra on first use.
|
|
174
|
+
|
|
117
175
|
## The pieces
|
|
118
176
|
|
|
119
177
|
| you want | reach for |
|
|
@@ -126,6 +184,7 @@ content (`OpenQuestion`), and human edits are protected from regeneration
|
|
|
126
184
|
| the committed artifact | `to_document(seg, ...)` → `StepDocument` |
|
|
127
185
|
| real clips/gifs/posters for the page | `derive_document(doc, media=..., doc_path=...)` / `paces derive` (`pip install paces[media]`) |
|
|
128
186
|
| auto-crop those clips to the people in frame | `derive(..., subject_locator=paces.pose.rtmlib_pose)` / `--subject-locator paces.pose:rtmlib_pose` (`pip install paces[pose]`) |
|
|
187
|
+
| persist the analysis behind a document | `to_store(seg, store=..., asset_id=...)` / `from_store(store, asset_id=...)` (`pip install paces[lacing]`) |
|
|
129
188
|
| a practice page | `render_html(doc)` |
|
|
130
189
|
| wall-clock times from counts | `resolve(doc)` |
|
|
131
190
|
| sanity checks | `validate_document(doc)` |
|
|
@@ -137,6 +196,6 @@ content (`OpenQuestion`), and human edits are protected from regeneration
|
|
|
137
196
|
Young and moving. The document schema is validated by round-tripping a real
|
|
138
197
|
proof of concept ([an interactive dance-practice
|
|
139
198
|
page](https://thorwhalen.com/que_calor_dance/)) through it — see
|
|
140
|
-
`tests/test_roundtrip_poc.py`. Media derivation (auto-cropped looping clips)
|
|
141
|
-
intrinsic segmenters (scene/beat/speech
|
|
142
|
-
are designed (see `docs/`) and arrive next.
|
|
199
|
+
`tests/test_roundtrip_poc.py`. Media derivation (auto-cropped looping clips)
|
|
200
|
+
and the evidence layer have landed; intrinsic segmenters (scene/beat/speech
|
|
201
|
+
detection) are designed (see `docs/`) and arrive next.
|
|
@@ -116,6 +116,36 @@ __all__ = [
|
|
|
116
116
|
# editing & regeneration
|
|
117
117
|
"apply_edits",
|
|
118
118
|
"merge_regenerated",
|
|
119
|
+
# the evidence layer (issue #4) — resolved lazily, needs [lacing]
|
|
120
|
+
"to_store",
|
|
121
|
+
"from_store",
|
|
122
|
+
"StoreWrite",
|
|
119
123
|
# rendering
|
|
120
124
|
"render_html",
|
|
121
125
|
]
|
|
126
|
+
|
|
127
|
+
#: The evidence layer's public names and where they live. Resolved on first
|
|
128
|
+
#: attribute access rather than imported here, so ``import paces`` keeps the
|
|
129
|
+
#: pydantic-only core of ADR-0004 — installing ``paces`` must not pull
|
|
130
|
+
#: ``lacing`` in, and asking for ``paces.to_store`` without the ``[lacing]``
|
|
131
|
+
#: extra must fail with an install line rather than an AttributeError.
|
|
132
|
+
_LAZY = {
|
|
133
|
+
"to_store": "paces.evidence",
|
|
134
|
+
"from_store": "paces.evidence",
|
|
135
|
+
"StoreWrite": "paces.evidence",
|
|
136
|
+
"recipes_from_store": "paces.evidence",
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def __getattr__(name: str):
|
|
141
|
+
"""Resolve the evidence layer on demand (PEP 562)."""
|
|
142
|
+
module_name = _LAZY.get(name)
|
|
143
|
+
if module_name is None:
|
|
144
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
145
|
+
import importlib
|
|
146
|
+
|
|
147
|
+
return getattr(importlib.import_module(module_name), name)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def __dir__() -> list[str]:
|
|
151
|
+
return sorted({*globals(), *_LAZY})
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""The body schemas paces registers with ``lacing`` — the evidence layer's vocabulary.
|
|
2
|
+
|
|
3
|
+
Importing this package registers every URI paces owns into
|
|
4
|
+
:mod:`lacing.schema`'s registry. That registry is **one flat, fleet-wide
|
|
5
|
+
dict**, ``register_body_schema`` is last-write-wins and silent, and
|
|
6
|
+
``annot://schema/beat/v1`` is already ``reelee``'s *narrative* beat — so
|
|
7
|
+
every URI paces owns is prefixed ``guide-`` (the thing it describes: a step
|
|
8
|
+
guide), and the one URI paces does not own, ``annot://schema/word/v1``, is
|
|
9
|
+
reused from :mod:`lacing.bodies` unchanged. The reasoning is issue #4's
|
|
10
|
+
decision comment and ``docs/07-annotation-model.md §6.3``.
|
|
11
|
+
|
|
12
|
+
Tier names are *not* prefixed: tiers are store-local, so ``step``,
|
|
13
|
+
``step.sub``, ``cue``, ``beat`` cost nothing and stay exactly as
|
|
14
|
+
``docs/07 §6.3`` wrote them.
|
|
15
|
+
|
|
16
|
+
Two rules hold across every body here:
|
|
17
|
+
|
|
18
|
+
- **No interval in a body.** Annotations are standoff; the interval lives on
|
|
19
|
+
``Annotation.reference``. A body that wants to say *when* says it by being
|
|
20
|
+
attached somewhere, never by carrying a number.
|
|
21
|
+
- **No floats where the document would refuse one.** Times, durations and
|
|
22
|
+
box coordinates cross as decimal strings, matching the document's
|
|
23
|
+
no-floats wire (``docs/07 §6.5``). ``confidence`` is the one exception the
|
|
24
|
+
document itself already makes.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from paces.bodies.derivation import (
|
|
28
|
+
RECIPE_BODY_SCHEMA_URI,
|
|
29
|
+
RECIPE_TIER,
|
|
30
|
+
GuideRecipeBodyV1,
|
|
31
|
+
)
|
|
32
|
+
from paces.bodies.document import (
|
|
33
|
+
DOC_BODY_SCHEMA_URI,
|
|
34
|
+
DOC_TIER,
|
|
35
|
+
SOURCE_BODY_SCHEMA_URI,
|
|
36
|
+
SOURCE_TIER,
|
|
37
|
+
GuideDocBodyV1,
|
|
38
|
+
GuideSourceBodyV1,
|
|
39
|
+
)
|
|
40
|
+
from paces.bodies.signal import (
|
|
41
|
+
BEAT_BODY_SCHEMA_URI,
|
|
42
|
+
BEAT_TIER,
|
|
43
|
+
GRID_BODY_SCHEMA_URI,
|
|
44
|
+
GRID_TIER,
|
|
45
|
+
PASS_BODY_SCHEMA_URI,
|
|
46
|
+
PASS_TIER,
|
|
47
|
+
WORD_BODY_SCHEMA_URI,
|
|
48
|
+
WORD_TIER,
|
|
49
|
+
GuideBeatBodyV1,
|
|
50
|
+
GuideGridBodyV1,
|
|
51
|
+
GuidePassBodyV1,
|
|
52
|
+
)
|
|
53
|
+
from paces.bodies.step import (
|
|
54
|
+
CUE_BODY_SCHEMA_URI,
|
|
55
|
+
CUE_TIER,
|
|
56
|
+
STEP_BODY_SCHEMA_URI,
|
|
57
|
+
STEP_TIER,
|
|
58
|
+
SUB_STEP_TIER,
|
|
59
|
+
GuideCueBodyV1,
|
|
60
|
+
GuideStepBodyV1,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
#: Every ``body_schema_uri`` paces *owns*. ``word/v1`` is deliberately absent:
|
|
64
|
+
#: it is lacing's, reused rather than claimed. Pinned by
|
|
65
|
+
#: ``tests/test_evidence_schemas.py`` against a committed JSON Schema snapshot,
|
|
66
|
+
#: so a rename or a field change fails the build (issue #4).
|
|
67
|
+
PACES_BODY_SCHEMA_URIS = (
|
|
68
|
+
DOC_BODY_SCHEMA_URI,
|
|
69
|
+
SOURCE_BODY_SCHEMA_URI,
|
|
70
|
+
PASS_BODY_SCHEMA_URI,
|
|
71
|
+
STEP_BODY_SCHEMA_URI,
|
|
72
|
+
CUE_BODY_SCHEMA_URI,
|
|
73
|
+
GRID_BODY_SCHEMA_URI,
|
|
74
|
+
BEAT_BODY_SCHEMA_URI,
|
|
75
|
+
RECIPE_BODY_SCHEMA_URI,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
__all__ = [
|
|
79
|
+
"PACES_BODY_SCHEMA_URIS",
|
|
80
|
+
# document envelope
|
|
81
|
+
"DOC_TIER",
|
|
82
|
+
"DOC_BODY_SCHEMA_URI",
|
|
83
|
+
"GuideDocBodyV1",
|
|
84
|
+
"SOURCE_TIER",
|
|
85
|
+
"SOURCE_BODY_SCHEMA_URI",
|
|
86
|
+
"GuideSourceBodyV1",
|
|
87
|
+
# steps and cues
|
|
88
|
+
"STEP_TIER",
|
|
89
|
+
"SUB_STEP_TIER",
|
|
90
|
+
"STEP_BODY_SCHEMA_URI",
|
|
91
|
+
"GuideStepBodyV1",
|
|
92
|
+
"CUE_TIER",
|
|
93
|
+
"CUE_BODY_SCHEMA_URI",
|
|
94
|
+
"GuideCueBodyV1",
|
|
95
|
+
# measured signal
|
|
96
|
+
"PASS_TIER",
|
|
97
|
+
"PASS_BODY_SCHEMA_URI",
|
|
98
|
+
"GuidePassBodyV1",
|
|
99
|
+
"GRID_TIER",
|
|
100
|
+
"GRID_BODY_SCHEMA_URI",
|
|
101
|
+
"GuideGridBodyV1",
|
|
102
|
+
"BEAT_TIER",
|
|
103
|
+
"BEAT_BODY_SCHEMA_URI",
|
|
104
|
+
"GuideBeatBodyV1",
|
|
105
|
+
"WORD_TIER",
|
|
106
|
+
"WORD_BODY_SCHEMA_URI",
|
|
107
|
+
# derivation
|
|
108
|
+
"RECIPE_TIER",
|
|
109
|
+
"RECIPE_BODY_SCHEMA_URI",
|
|
110
|
+
"GuideRecipeBodyV1",
|
|
111
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Shared conventions for every paces body schema.
|
|
2
|
+
|
|
3
|
+
One base class and one type alias, so the three rules that make a body
|
|
4
|
+
round-trippable are stated once rather than re-typed per module: frozen,
|
|
5
|
+
``extra="forbid"`` (lacing non-negotiable #3), and decimal strings where the
|
|
6
|
+
document would refuse a float.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Annotated
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
#: Exact decimal number as a string ("231.3") — the document's wire form for
|
|
16
|
+
#: any quantity, mirrored here so a body never carries a float the document
|
|
17
|
+
#: itself would reject (``docs/07 §6.5``).
|
|
18
|
+
Decimal = Annotated[str, Field(pattern=r"^-?\d+(\.\d+)?$")]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BodyBase(BaseModel):
|
|
22
|
+
"""Frozen, closed-world body model — lacing validates against it by URI."""
|
|
23
|
+
|
|
24
|
+
model_config = {"frozen": True, "extra": "forbid"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Body schema for derivation recipes — the POC's ``crops.json``, promoted.
|
|
2
|
+
|
|
3
|
+
``annot://schema/guide-recipe/v1``, tier ``derivation``
|
|
4
|
+
(``SYMBOLIC_ASSOCIATION`` under ``step``): the *parameters* a derived file
|
|
5
|
+
was made with, so a re-run is stable and a human override survives it.
|
|
6
|
+
``docs/07 §6.2``'s split, stated once: the document's
|
|
7
|
+
:class:`~paces.model.ArtifactRef` says **what** a file is; this says **how**
|
|
8
|
+
it was made.
|
|
9
|
+
|
|
10
|
+
The recipe's ``window`` is not a field — it is the annotation's reference
|
|
11
|
+
interval, which is exactly what a window is. Everything else is
|
|
12
|
+
:class:`paces.derivation.CropRecipe` as it already exists on disk, with the
|
|
13
|
+
box's coordinates crossing as decimal strings; the sidecar is committed, so
|
|
14
|
+
it has always been float-free and the store keeps it that way.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from lacing.schema import register_body_schema
|
|
20
|
+
from pydantic import Field
|
|
21
|
+
|
|
22
|
+
from paces.bodies._base import BodyBase, Decimal
|
|
23
|
+
|
|
24
|
+
RECIPE_TIER = "derivation"
|
|
25
|
+
RECIPE_BODY_SCHEMA_URI = "annot://schema/guide-recipe/v1"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GuideRecipeBodyV1(BodyBase):
|
|
29
|
+
"""One span's derivation parameters, keyed by the span address."""
|
|
30
|
+
|
|
31
|
+
doc_id: str = Field(..., description="Which guide this recipe belongs to.")
|
|
32
|
+
span_key: str = Field(
|
|
33
|
+
...,
|
|
34
|
+
description=(
|
|
35
|
+
"The span address ``{step_id}/{source}/{role}/{start}`` — the same "
|
|
36
|
+
"key ``paces.derivation`` writes into the committed sidecar, so "
|
|
37
|
+
"the two layers join without a translation table."
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
step_id: str = Field(..., description="The step this recipe derives media for.")
|
|
41
|
+
box: list[Decimal] | None = Field(
|
|
42
|
+
None,
|
|
43
|
+
description=(
|
|
44
|
+
"The resolved crop box (x, y, w, h) in pixels, as decimal "
|
|
45
|
+
"strings. None means full frame — a recorded decision, not a gap."
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
frame_width: int = Field(..., description="Source frame width the box is in.")
|
|
49
|
+
frame_height: int = Field(..., description="Source frame height the box is in.")
|
|
50
|
+
source_asset_id: str | None = Field(
|
|
51
|
+
None, description="Hash of the media cut from. None = identity unverified."
|
|
52
|
+
)
|
|
53
|
+
locator: str = Field(
|
|
54
|
+
..., description="Which subject locator resolved the box ('full_frame', ...)."
|
|
55
|
+
)
|
|
56
|
+
params: dict[str, Decimal] = Field(
|
|
57
|
+
default_factory=dict,
|
|
58
|
+
description=(
|
|
59
|
+
"Policy parameters the box was resolved under (aspect, pad). A "
|
|
60
|
+
"change here re-locates an unlocked entry."
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
locked: bool = Field(
|
|
64
|
+
False, description="A human set this box; reconcile must not re-derive it."
|
|
65
|
+
)
|
|
66
|
+
media_digest: str | None = Field(
|
|
67
|
+
None,
|
|
68
|
+
description=(
|
|
69
|
+
"What the stored media was actually cut with (box + window + "
|
|
70
|
+
"source hash). A hand-edited box or a re-timed window changes it "
|
|
71
|
+
"and forces a re-encode instead of serving stale bytes."
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
register_body_schema(RECIPE_BODY_SCHEMA_URI, GuideRecipeBodyV1)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Body schemas for the document envelope and its sources.
|
|
2
|
+
|
|
3
|
+
- ``annot://schema/guide-doc/v1`` (tier ``document``) — the one annotation
|
|
4
|
+
that says *which guide this is*: id, title, language, domain, plus the
|
|
5
|
+
analysis run's own report (``method``, ``flags``). Everything ordered or
|
|
6
|
+
timed hangs off the other tiers.
|
|
7
|
+
- ``annot://schema/guide-source/v1`` (tier ``source``) — one per
|
|
8
|
+
:class:`~paces.model.Source`.
|
|
9
|
+
|
|
10
|
+
Neither body carries an interval; both annotations attach as point
|
|
11
|
+
annotations at tick 0, which is lacing's spelling of "about the asset, not
|
|
12
|
+
about a region of it".
|
|
13
|
+
|
|
14
|
+
What is deliberately **not** here: ``locks``, ``questions``, ``artifacts``
|
|
15
|
+
and span ``excerpt`` windows. Those are document-layer records — a human
|
|
16
|
+
edit, an open question, a built file, a hand-picked loop — and
|
|
17
|
+
``docs/07 §6.0`` is explicit that nothing flows document → store. The
|
|
18
|
+
evidence layer holds what analysis produced; the document holds what people
|
|
19
|
+
did to it.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from lacing.schema import register_body_schema
|
|
27
|
+
from pydantic import Field
|
|
28
|
+
|
|
29
|
+
from paces.bodies._base import BodyBase, Decimal
|
|
30
|
+
|
|
31
|
+
DOC_TIER = "document"
|
|
32
|
+
DOC_BODY_SCHEMA_URI = "annot://schema/guide-doc/v1"
|
|
33
|
+
|
|
34
|
+
SOURCE_TIER = "source"
|
|
35
|
+
SOURCE_BODY_SCHEMA_URI = "annot://schema/guide-source/v1"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class GuideDocBodyV1(BodyBase):
|
|
39
|
+
"""The guide's envelope: identity, presentation language, and the run."""
|
|
40
|
+
|
|
41
|
+
doc_id: str = Field(
|
|
42
|
+
...,
|
|
43
|
+
description=(
|
|
44
|
+
"Stable slug of the guide within a project. Distinct from the "
|
|
45
|
+
"annotation id, and the field that lets one store hold several "
|
|
46
|
+
"guides over the same asset."
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
title: str = Field(..., description="Human title of the guide.")
|
|
50
|
+
lang: str = Field("en", description="BCP-47 language tag of the guide's prose.")
|
|
51
|
+
domain: str = Field(
|
|
52
|
+
"generic", description="Domain slug: dance | recipe | workout | ..."
|
|
53
|
+
)
|
|
54
|
+
schema_version: str = Field(
|
|
55
|
+
..., description="Version of the StepDocument schema this was projected at."
|
|
56
|
+
)
|
|
57
|
+
credits: str | None = Field(
|
|
58
|
+
None, description="Attribution line, when there is one."
|
|
59
|
+
)
|
|
60
|
+
method: str = Field(
|
|
61
|
+
"",
|
|
62
|
+
description=(
|
|
63
|
+
"Which segmentation capability produced the steps "
|
|
64
|
+
"(``Segmentation.method``). Empty when unrecorded."
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
flags: list[str] = Field(
|
|
68
|
+
default_factory=list,
|
|
69
|
+
description=(
|
|
70
|
+
"The run's honesty flags (``Segmentation.flags``) — "
|
|
71
|
+
"'naming-abstained', 'origin-estimated: ...', and friends. "
|
|
72
|
+
"Evidence about the run, never projected into the document."
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
attrs: dict[str, Any] = Field(
|
|
76
|
+
default_factory=dict, description="Namespaced open bag, as on the document."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class GuideSourceBodyV1(BodyBase):
|
|
81
|
+
"""One input the guide's spans refer into."""
|
|
82
|
+
|
|
83
|
+
doc_id: str = Field(
|
|
84
|
+
...,
|
|
85
|
+
description=(
|
|
86
|
+
"Which guide this source belongs to — one store can hold several "
|
|
87
|
+
"guides over the same asset, and a projection has to read back "
|
|
88
|
+
"only its own."
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
ordinal: int = Field(
|
|
92
|
+
..., description="Position in the document's source list, 0-based."
|
|
93
|
+
)
|
|
94
|
+
source_id: str = Field(
|
|
95
|
+
..., description="Slug of the source within the guide. Not the annotation id."
|
|
96
|
+
)
|
|
97
|
+
kind: str = Field(..., description="video | audio | image | document | url.")
|
|
98
|
+
uri: str = Field(..., description="Where the source lives.")
|
|
99
|
+
asset_id: str | None = Field(
|
|
100
|
+
None, description="Content hash of a local copy, if any."
|
|
101
|
+
)
|
|
102
|
+
duration_s: Decimal | None = Field(
|
|
103
|
+
None, description="Source duration in seconds, as a decimal string."
|
|
104
|
+
)
|
|
105
|
+
title: str | None = Field(None, description="Source title.")
|
|
106
|
+
attribution: str | None = Field(None, description="Who made it.")
|
|
107
|
+
rights: str | None = Field(
|
|
108
|
+
None,
|
|
109
|
+
description=(
|
|
110
|
+
"Usage rights. First-class in this domain (``docs/07 §8.7``): a "
|
|
111
|
+
"guide that restyles someone's choreography has to say so."
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
attrs: dict[str, Any] = Field(default_factory=dict, description="Open bag.")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
register_body_schema(DOC_BODY_SCHEMA_URI, GuideDocBodyV1)
|
|
118
|
+
register_body_schema(SOURCE_BODY_SCHEMA_URI, GuideSourceBodyV1)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Body schemas for what was measured off the media: passes, the grid, beats.
|
|
2
|
+
|
|
3
|
+
- ``annot://schema/guide-pass/v1`` (tier ``source.pass``) — *"[45 s, 215 s]
|
|
4
|
+
is a music pass; [220 s, 520 s] is speech"*. The speech/music split
|
|
5
|
+
``paces.measure`` gets from ``mixing.find_segments``, which is what made
|
|
6
|
+
the POC's dual timestamps possible in the first place.
|
|
7
|
+
- ``annot://schema/guide-grid/v1`` (tier ``grid``) — the metric grid, **one**
|
|
8
|
+
annotation over the region it governs.
|
|
9
|
+
- ``annot://schema/guide-beat/v1`` (tier ``beat``) — one point annotation per
|
|
10
|
+
*measured* beat.
|
|
11
|
+
|
|
12
|
+
Why the grid is one annotation and the beats are separate: paces' grid is
|
|
13
|
+
closed form (``tempo_bpm`` + ``origin`` + ``subdivisions``), so materialising
|
|
14
|
+
N beats *from* it would be a denormalised cache with no invalidation story —
|
|
15
|
+
the same argument ``docs/07 §7`` uses to refuse materialising Allen
|
|
16
|
+
relations. Beat annotations are therefore written only when a producer hands
|
|
17
|
+
over times it actually measured, and they carry a ``was_derived_from`` edge
|
|
18
|
+
to the grid so a re-measure invalidates them.
|
|
19
|
+
|
|
20
|
+
Transcripts reuse lacing's own ``annot://schema/word/v1`` unchanged, on tier
|
|
21
|
+
``transcript.word``; the URIs and tier name live here so every evidence tier
|
|
22
|
+
is named in one place.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import lacing.bodies # noqa: F401 — registers the word/v1 paces reuses
|
|
28
|
+
from lacing.schema import register_body_schema
|
|
29
|
+
from pydantic import Field
|
|
30
|
+
|
|
31
|
+
from paces.bodies._base import BodyBase, Decimal
|
|
32
|
+
|
|
33
|
+
PASS_TIER = "source.pass"
|
|
34
|
+
PASS_BODY_SCHEMA_URI = "annot://schema/guide-pass/v1"
|
|
35
|
+
|
|
36
|
+
GRID_TIER = "grid"
|
|
37
|
+
GRID_BODY_SCHEMA_URI = "annot://schema/guide-grid/v1"
|
|
38
|
+
|
|
39
|
+
BEAT_TIER = "beat"
|
|
40
|
+
BEAT_BODY_SCHEMA_URI = "annot://schema/guide-beat/v1"
|
|
41
|
+
|
|
42
|
+
#: Transcripts are lacing's, not paces'. Named here so the tier table is
|
|
43
|
+
#: complete in one file; NOT registered here, and NOT in
|
|
44
|
+
#: ``PACES_BODY_SCHEMA_URIS`` — ``lacing.bodies.word`` owns it.
|
|
45
|
+
WORD_TIER = "transcript.word"
|
|
46
|
+
WORD_BODY_SCHEMA_URI = "annot://schema/word/v1"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class GuidePassBodyV1(BodyBase):
|
|
50
|
+
"""A stretch of one source with a single character (music, speech, ...)."""
|
|
51
|
+
|
|
52
|
+
label: str = Field(
|
|
53
|
+
...,
|
|
54
|
+
description=(
|
|
55
|
+
"What this stretch is: 'music' | 'speech' | '' when the detector "
|
|
56
|
+
"found a segment it would not name."
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
detector: str = Field(
|
|
60
|
+
"",
|
|
61
|
+
description=(
|
|
62
|
+
"What produced the split ('mixing.find_segments:speech_music'). "
|
|
63
|
+
"Empty when the caller supplied passes without saying."
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class GuideGridBodyV1(BodyBase):
|
|
69
|
+
"""How the domain unit relates to wall-clock time, over the region it governs.
|
|
70
|
+
|
|
71
|
+
The document's ``metric`` is projected straight back out of this — one
|
|
72
|
+
grid, one source of truth, no second copy in the document envelope.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
doc_id: str = Field(
|
|
76
|
+
...,
|
|
77
|
+
description=(
|
|
78
|
+
"Which guide this grid governs. The grid is per-guide, not "
|
|
79
|
+
"per-asset: two guides can count the same video differently."
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
unit: str = Field(..., description="The domain unit counted: eight | bar | rep.")
|
|
83
|
+
subdivisions: int = Field(1, ge=1, description="Beats per unit (8 for an 8-count).")
|
|
84
|
+
tempo_bpm: Decimal | None = Field(
|
|
85
|
+
None,
|
|
86
|
+
description=(
|
|
87
|
+
"Tempo as a decimal string. None spells 'tempo unknown' — never '0'."
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
origin: Decimal | None = Field(
|
|
91
|
+
None, description="Seconds into ``origin_source`` where unit 0 starts."
|
|
92
|
+
)
|
|
93
|
+
origin_source: str | None = Field(
|
|
94
|
+
None, description="Which source ``origin`` is measured in."
|
|
95
|
+
)
|
|
96
|
+
beat_count: int | None = Field(
|
|
97
|
+
None,
|
|
98
|
+
description=(
|
|
99
|
+
"How many beats were actually tracked, when the grid was measured "
|
|
100
|
+
"rather than supplied. None means nobody counted."
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
method: str = Field(
|
|
104
|
+
"",
|
|
105
|
+
description=(
|
|
106
|
+
"Which capability produced the grid ('grid-measured', "
|
|
107
|
+
"'grid-placed'). Empty when the caller supplied it outright."
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class GuideBeatBodyV1(BodyBase):
|
|
113
|
+
"""One measured beat. The time is the reference's point interval."""
|
|
114
|
+
|
|
115
|
+
index: int = Field(
|
|
116
|
+
..., description="Position in the measured beat sequence, 0-based."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
register_body_schema(PASS_BODY_SCHEMA_URI, GuidePassBodyV1)
|
|
121
|
+
register_body_schema(GRID_BODY_SCHEMA_URI, GuideGridBodyV1)
|
|
122
|
+
register_body_schema(BEAT_BODY_SCHEMA_URI, GuideBeatBodyV1)
|