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.
Files changed (48) hide show
  1. {paces-0.0.10 → paces-0.0.12}/PKG-INFO +66 -4
  2. {paces-0.0.10 → paces-0.0.12}/README.md +62 -3
  3. {paces-0.0.10 → paces-0.0.12}/docs/README.md +3 -3
  4. {paces-0.0.10 → paces-0.0.12}/docs/alignment/README.md +1 -1
  5. {paces-0.0.10 → paces-0.0.12}/paces/__init__.py +30 -0
  6. paces-0.0.12/paces/bodies/__init__.py +111 -0
  7. paces-0.0.12/paces/bodies/_base.py +24 -0
  8. paces-0.0.12/paces/bodies/derivation.py +76 -0
  9. paces-0.0.12/paces/bodies/document.py +118 -0
  10. paces-0.0.12/paces/bodies/signal.py +122 -0
  11. paces-0.0.12/paces/bodies/step.py +155 -0
  12. {paces-0.0.10 → paces-0.0.12}/paces/edits.py +5 -3
  13. paces-0.0.12/paces/evidence.py +1302 -0
  14. {paces-0.0.10 → paces-0.0.12}/paces/model.py +3 -2
  15. {paces-0.0.10 → paces-0.0.12}/pyproject.toml +21 -2
  16. paces-0.0.12/tests/data/body_schemas.json +746 -0
  17. paces-0.0.12/tests/test_evidence.py +684 -0
  18. paces-0.0.12/tests/test_evidence_schemas.py +94 -0
  19. {paces-0.0.10 → paces-0.0.12}/.gitignore +0 -0
  20. {paces-0.0.10 → paces-0.0.12}/LICENSE +0 -0
  21. {paces-0.0.10 → paces-0.0.12}/docs/poc-reference/README.md +0 -0
  22. {paces-0.0.10 → paces-0.0.12}/docs/poc-reference/artifacts/clips.json +0 -0
  23. {paces-0.0.10 → paces-0.0.12}/paces/__main__.py +0 -0
  24. {paces-0.0.10 → paces-0.0.12}/paces/derivation.py +0 -0
  25. {paces-0.0.10 → paces-0.0.12}/paces/excerpts.py +0 -0
  26. {paces-0.0.10 → paces-0.0.12}/paces/measure.py +0 -0
  27. {paces-0.0.10 → paces-0.0.12}/paces/pose.py +0 -0
  28. {paces-0.0.10 → paces-0.0.12}/paces/projection.py +0 -0
  29. {paces-0.0.10 → paces-0.0.12}/paces/render.py +0 -0
  30. {paces-0.0.10 → paces-0.0.12}/paces/segmenters.py +0 -0
  31. {paces-0.0.10 → paces-0.0.12}/paces/tools.py +0 -0
  32. {paces-0.0.10 → paces-0.0.12}/tests/audio_synth.py +0 -0
  33. {paces-0.0.10 → paces-0.0.12}/tests/data/routine.json +0 -0
  34. {paces-0.0.10 → paces-0.0.12}/tests/poc_fixture.py +0 -0
  35. {paces-0.0.10 → paces-0.0.12}/tests/test_chapters.py +0 -0
  36. {paces-0.0.10 → paces-0.0.12}/tests/test_cli_surface.py +0 -0
  37. {paces-0.0.10 → paces-0.0.12}/tests/test_derivation.py +0 -0
  38. {paces-0.0.10 → paces-0.0.12}/tests/test_edit_protection_residuals.py +0 -0
  39. {paces-0.0.10 → paces-0.0.12}/tests/test_edits.py +0 -0
  40. {paces-0.0.10 → paces-0.0.12}/tests/test_excerpts.py +0 -0
  41. {paces-0.0.10 → paces-0.0.12}/tests/test_measure.py +0 -0
  42. {paces-0.0.10 → paces-0.0.12}/tests/test_pose.py +0 -0
  43. {paces-0.0.10 → paces-0.0.12}/tests/test_render_media.py +0 -0
  44. {paces-0.0.10 → paces-0.0.12}/tests/test_roundtrip_poc.py +0 -0
  45. {paces-0.0.10 → paces-0.0.12}/tests/test_segment.py +0 -0
  46. {paces-0.0.10 → paces-0.0.12}/tests/test_smoke.py +0 -0
  47. {paces-0.0.10 → paces-0.0.12}/tests/test_vertical_slice.py +0 -0
  48. {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.10
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 detection), and the evidence layer
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 detection), and the evidence layer
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 `video_gen`, and honestly which parts are stubs. |
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 `video_gen` group, and why `priv pkg add-package` is deliberately deferred. |
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)