paces 0.0.10__tar.gz → 0.0.12__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.10 → paces-0.0.12}/PKG-INFO +66 -4
- {paces-0.0.10 → paces-0.0.12}/README.md +62 -3
- {paces-0.0.10 → paces-0.0.12}/docs/README.md +3 -3
- {paces-0.0.10 → paces-0.0.12}/docs/alignment/README.md +1 -1
- {paces-0.0.10 → paces-0.0.12}/paces/__init__.py +30 -0
- paces-0.0.12/paces/bodies/__init__.py +111 -0
- paces-0.0.12/paces/bodies/_base.py +24 -0
- paces-0.0.12/paces/bodies/derivation.py +76 -0
- paces-0.0.12/paces/bodies/document.py +118 -0
- paces-0.0.12/paces/bodies/signal.py +122 -0
- paces-0.0.12/paces/bodies/step.py +155 -0
- {paces-0.0.10 → paces-0.0.12}/paces/edits.py +5 -3
- paces-0.0.12/paces/evidence.py +1302 -0
- {paces-0.0.10 → paces-0.0.12}/paces/model.py +3 -2
- {paces-0.0.10 → paces-0.0.12}/pyproject.toml +21 -2
- paces-0.0.12/tests/data/body_schemas.json +746 -0
- paces-0.0.12/tests/test_evidence.py +684 -0
- paces-0.0.12/tests/test_evidence_schemas.py +94 -0
- {paces-0.0.10 → paces-0.0.12}/.gitignore +0 -0
- {paces-0.0.10 → paces-0.0.12}/LICENSE +0 -0
- {paces-0.0.10 → paces-0.0.12}/docs/poc-reference/README.md +0 -0
- {paces-0.0.10 → paces-0.0.12}/docs/poc-reference/artifacts/clips.json +0 -0
- {paces-0.0.10 → paces-0.0.12}/paces/__main__.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/paces/derivation.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/paces/excerpts.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/paces/measure.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/paces/pose.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/paces/projection.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/paces/render.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/paces/segmenters.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/paces/tools.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/tests/audio_synth.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/tests/data/routine.json +0 -0
- {paces-0.0.10 → paces-0.0.12}/tests/poc_fixture.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/tests/test_chapters.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/tests/test_cli_surface.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/tests/test_derivation.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/tests/test_edit_protection_residuals.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/tests/test_edits.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/tests/test_excerpts.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/tests/test_measure.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/tests/test_pose.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/tests/test_render_media.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/tests/test_roundtrip_poc.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/tests/test_segment.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/tests/test_smoke.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/tests/test_vertical_slice.py +0 -0
- {paces-0.0.10 → paces-0.0.12}/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.12
|
|
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.
|
|
@@ -13,7 +13,7 @@ a dance, plus a hand-written HTML aide-mémoire, plus a paragraph of steering pr
|
|
|
13
13
|
deployed interactive practice page: <https://thorwhalen.com/que_calor_dance/>.
|
|
14
14
|
|
|
15
15
|
The user now wants that generalised into a library, integrated with the **reelee** /
|
|
16
|
-
`video_gen` fleet. Your job is to research, design and build it. **This folder exists so you
|
|
16
|
+
`av` (formerly `video_gen`) fleet. Your job is to research, design and build it. **This folder exists so you
|
|
17
17
|
do not start from scratch.** It records what was built, the parameters that were expensive to
|
|
18
18
|
find, the failures and what each one teaches, the user's own framing of the generalisation,
|
|
19
19
|
and an inventory of what already exists in the fleet.
|
|
@@ -28,7 +28,7 @@ Nothing here is a design you must follow. It is evidence and framing. Argue with
|
|
|
28
28
|
| 2 | **`03-design-brief.md`** | The user's own framing: the parse→AST→render metaphor they explicitly asked to have recorded, the three generalisation axes, and the constraints the POC discovered. |
|
|
29
29
|
| 3 | **`04-reelee-core.md`** | What reelee is, what a reelee "genre" is, and what to reuse. **This determines the package boundary**, so read it before deciding anything structural. |
|
|
30
30
|
| 4 | **`07-annotation-model.md`** | The proposed shape of the AST — the contract between analysis and rendering. |
|
|
31
|
-
| 5 | **`05-fleet-inventory.md`** | What already exists across `
|
|
31
|
+
| 5 | **`05-fleet-inventory.md`** | What already exists across `av`, and honestly which parts are stubs. |
|
|
32
32
|
| 6 | **`02-technical-recipes.md`** | Every technique with working parameters. Reference, not narrative — come back to it when implementing. |
|
|
33
33
|
| 7 | **`06-surfaces-and-conventions.md`** | House style: architecture-first seams, qh, py2mcp, storage, frontend, deploy. |
|
|
34
34
|
| 8 | **`08-naming-candidates.md`** | PyPI-verified name options, and a better word for the "subject" axis. |
|
|
@@ -37,7 +37,7 @@ Nothing here is a design you must follow. It is evidence and framing. Argue with
|
|
|
37
37
|
| — | **`adr/`** | The decisions. **`0003` `video + segmenter`** — read it before designing the analysis phase. `0002` the name. `0001` the alignment engine (intent) — note that much of what you'd otherwise build already exists in `muvid`, `mixing` and `kodokan`. |
|
|
38
38
|
| — | **`alignment/`** | The research behind that ADR: one file per method family, prepared so you don't start from a literature search. |
|
|
39
39
|
| — | **`KICKOFF.md`** | A paste-ready prompt to start a fresh session on this. |
|
|
40
|
-
| — | **`REGISTRATION.md`** | **One command still pending** to register `paces` in the `
|
|
40
|
+
| — | **`REGISTRATION.md`** | **One command still pending** to register `paces` in the `av` group, and why `priv pkg add-package` is deliberately deferred. |
|
|
41
41
|
| — | **`poc-reference/`** | The actual scripts and data. Read `poc-reference/README.md` first — several of those files are recorded dead ends. |
|
|
42
42
|
|
|
43
43
|
## The one-paragraph version
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Alignment research
|
|
2
2
|
|
|
3
3
|
*What this folder is for: preparation for a possible dedicated **alignment tool** in the
|
|
4
|
-
`video_gen` / reelee fleet — the thing that answers "given some artifacts and some media,
|
|
4
|
+
`av` (formerly `video_gen`) / reelee fleet — the thing that answers "given some artifacts and some media,
|
|
5
5
|
which span of the media does each artifact correspond to?". The decision to build it is
|
|
6
6
|
recorded as an intent in `../adr/0001-alignment-engine-as-a-fleet-package.md`; **read that
|
|
7
7
|
first**, then come here for the method-by-method detail.*
|
|
@@ -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)
|