Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
| 1 | `segmenter=` on `segment()` | auto-select from present facts (`grid-placed` / `explicit`) | the strategies catalogued in `docs/alignment/07` (novelty-k, align-to-steps); `kodokan.segment`; `mixing.audio.find_segments` |
| 2 | `renderer=` on `tools.render()` | `"html"` practice page (stdlib) | the POC page (metronome+clips) at `docs/poc-reference/render/`; PDF/deck per `docs/03 §2.3` |
| 3 | `grid=` values on `segment()` | caller-supplied tempo/origin; measured from local media when absent (`grid-measured`: mixing speech/music split + beat_grid, origin estimated + flagged) | better origin fitting (issue #2 follow-ons; `kodokan.estimate_period`) |
| 4 | `store=` on `evidence.to_store()` / `from_store()` | none — injected, always: `lacing.MemoryStore` in tests, `SqliteStore` for a project sidecar | any `dol` MutableMapping conforming to `lacing.IntervalAnnotationStore` |

Surface for v1: **CLI** (`argh` over `tools._dispatch_funcs`). MCP (`py2mcp`
string refs), HTTP (`qh`), shipped skills, frontend: questions answered — none
Expand All @@ -26,6 +27,19 @@ template internals, CLI parsing, storage (v1 reads/writes explicit paths).
by `resolve()`, never stored.
- A segmenter may never return a step count it has no evidence for
(honesty states in `paces/segment.py` docstring).
- Named next steps: evidence layer → lacing (`docs/07 §6.3`); `paces/genre.py`
registering `step_by_step` into `nw` (ADR-0004); intrinsic segmenter
combinators when a third intrinsic segmenter needs them (ADR-0003).
- The evidence layer (`paces/evidence.py` + `paces/bodies/`, issue #4) owns
the lacing boundary. Its rules, in one line each: intervals live on
`Annotation.reference`, never in a body; annotation ids are `uuid5` over a
stable evidence key, so a re-derivation *is* the same row; an unchanged
value digest leaves the row completely alone; `locks`, `questions`,
`artifacts` and span `excerpt` windows are document-layer and never flow
into the store.
- **Body schema URIs are a flat, fleet-wide namespace and
`register_body_schema` overwrites silently.** Every URI paces owns is
prefixed `guide-`; `word/v1` is lacing's and reused. Before adding one,
check the fleet (`rg 'annot://schema/'` across `$PP`) and pin it in
`tests/test_evidence_schemas.py`.
- Named next steps: `paces/genre.py` registering `step_by_step` into `nw`
(ADR-0004); intrinsic segmenter combinators when a third intrinsic
segmenter needs them (ADR-0003); a CLI/MCP surface over `to_store`
(needs no core change — the store's on-disk shape is the open question).
65 changes: 62 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,64 @@ run-through and the breakdown are the same step seen twice). Uncertainty is
content (`OpenQuestion`), and human edits are protected from regeneration
(`Lock`).

## The evidence layer

The document is the *contract*; the machine evidence behind it — the
speech/music split, the metric grid, the beats, the transcript, the step
candidates, the crop recipes, and the lineage between them — lives in a
[`lacing`](https://github.com/thorwhalen/lacing) store, and the document is a
**projection** of it. The document is derivable from the store; the store is
not derivable from the document.

```python
from lacing import MemoryStore # pip install 'paces[lacing]'
from paces import segment, from_store, to_store
from paces.model import dumps_document

grid = {"unit": "eight", "subdivisions": 8, "tempoBpm": "129.2", "origin": "51.2"}
asset_sha256 = "a" * 64 # lacing.hash_file(video) in real use
seg = segment(None, steps=[("Mise en place", 2), ("Déhanchés", 8)], grid=grid)

store = MemoryStore() # or SqliteStore('project.annot')
guide = dict(doc_id="que-calor", title="Que Calor", domain="dance",
source="https://youtu.be/q_TUyxUhoEw")

write = to_store(seg, store=store, asset_id=asset_sha256, **guide)

dumps_document(from_store(store, asset_id=asset_sha256)) == dumps_document(write.document)
# True — the projection is exact

to_store(seg, store=store, asset_id=asset_sha256, **guide).written
# 0 — a re-run of the same analysis writes nothing
```

The store is injected, never constructed for you: `MemoryStore()` in tests, a
`SqliteStore` for a project sidecar, any `dol` store that conforms. Annotation
ids are derived (`uuid5`) from the evidence they stand for, so a re-derivation
*is* the same annotation — which is what keeps `was_derived_from` lineage
resolving and lets `write.document`'s `Origin.annotationId` be committed. Rows
whose value digest did not change are left completely alone, so freshness does
not fire on a no-op re-run.

`to_store` is a **re-derivation** of the guide, not a merge into it: a step
that no longer exists is dropped rather than left to be resurrected by the
next projection. Pruning only ever touches this asset, this `doc_id`, and the
guide's own tiers. Evidence *about the asset* — the speech/music split, the
beats, the transcript — is shared by every guide over that asset and is never
pruned, so one guide can't delete what another's lineage points at; a
re-measure adds a row under a new content-derived key and the old one stays.
`prune=False` opts out of the rest.

Analysis a `Segmentation` cannot carry rides along as keywords: `passes=`
(the speech/music split), `beats=`, `transcript=`, `cues=`, `recipes=`. What
stays on the document and never flows back into the store: `locks`,
`questions`, `artifacts`, and span `excerpt` windows — those are human
decisions, not measurements. Full table and rationale in
`docs/07-annotation-model.md` §6.

`import paces` does not import `lacing`: the core stays pydantic-only, and
`paces.to_store` resolves the extra on first use.

## The pieces

| you want | reach for |
Expand All @@ -126,6 +184,7 @@ content (`OpenQuestion`), and human edits are protected from regeneration
| the committed artifact | `to_document(seg, ...)` → `StepDocument` |
| real clips/gifs/posters for the page | `derive_document(doc, media=..., doc_path=...)` / `paces derive` (`pip install paces[media]`) |
| 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]`) |
| persist the analysis behind a document | `to_store(seg, store=..., asset_id=...)` / `from_store(store, asset_id=...)` (`pip install paces[lacing]`) |
| a practice page | `render_html(doc)` |
| wall-clock times from counts | `resolve(doc)` |
| sanity checks | `validate_document(doc)` |
Expand All @@ -137,6 +196,6 @@ content (`OpenQuestion`), and human edits are protected from regeneration
Young and moving. The document schema is validated by round-tripping a real
proof of concept ([an interactive dance-practice
page](https://thorwhalen.com/que_calor_dance/)) through it — see
`tests/test_roundtrip_poc.py`. Media derivation (auto-cropped looping clips),
intrinsic segmenters (scene/beat/speech detection), and the evidence layer
are designed (see `docs/`) and arrive next.
`tests/test_roundtrip_poc.py`. Media derivation (auto-cropped looping clips)
and the evidence layer have landed; intrinsic segmenters (scene/beat/speech
detection) are designed (see `docs/`) and arrive next.
Loading
Loading