paces
Turn instructional media into structured, interactive learning material. Put it through its paces.
Take a video of someone teaching something — a dance routine, a kata, a
recipe — plus, optionally, notes and a steering prompt. paces segments it
into named steps, builds a structured step document (an AST for
step-by-step instruction), and renders that into learning material: a
practice page with counts and deep links today, other guides later.
pip install paces
Quick example
from paces import segment, to_document, render_html
seg = segment(
"https://youtu.be/q_TUyxUhoEw",
steps=[
("Mise en place", 2),
("Pas pieds pointe et ronde", 6),
("Soleil avec les bras", 4),
("Déhanchés", 8),
],
grid={"unit": "eight", "subdivisions": 8, "tempoBpm": "129.2", "origin": "51.2"},
)
doc = to_document(
seg,
doc_id="que-calor",
title="Chorégraphie Que Calor",
source="https://youtu.be/q_TUyxUhoEw",
)
open("page.html", "w").write(render_html(doc))
The page lists every step with its counts, links each one back into the video (both the at-tempo run-through and the slow breakdown, when both are known), and — because the document carries a metric grid — includes a count-along transport that paces you through the routine at the measured tempo.
Same thing from the shell:
paces segment VIDEO_URL --steps steps.json --grid grid.json --output seg.json
paces to-document seg.json --source VIDEO_URL --title "My routine" --output document.json
paces suggest-excerpts document.json --output document.json # mark each block's loop window
paces derive document.json --media routine.mp4 # real loop clips + gifs + posters
paces render document.json --output page.html
derive (pip install paces[media]) cuts a loopable mp4, a palette-quality
gif and a poster for every excerpt-bearing span, writes them to media/ next
to the document, and the practice page embeds them as looping clips. Crop
recipes persist in a hand-overridable document.recipes.json sidecar; the
subject_locator= seam (default: no crop) is where pose-based auto-crop
plugs in. Design record: docs/adr/0005-media-derivation.md.
Auto-crop to the people in frame with pip install paces[pose], then
paces derive doc.json --media routine.mp4 --subject-locator paces.pose:rtmlib_pose
(or subject_locator=paces.pose.rtmlib_pose from Python). It probes each
excerpt window at ~5 fps and reports every person it sees; the crop policy
stays in the core, so two people in frame get one box around both. The extra
itself is rtmlib (Apache-2.0, pure Python)
and onnxruntime (MIT), with model weights downloaded on first use. Detection is
YOLOX (Apache-2.0); what is barred from every extra here — rather than
quarantined into one — is the ultralytics distribution, which is AGPL-3.0.
What it pulls in is a different question, and worth stating plainly: rtmlib
requires opencv, and opencv's bundled FFmpeg is GPL-3.0-or-later on macOS
wheels of the versions measured (4.12.0.88 / 4.13.0.92) (built --enable-gpl
with libx264/libx265) though LGPL-2.1-or-later on manylinux and Windows — a
per-version fact, not a per-platform one: the 5.0.0.93 macOS x86_64 wheel ships
no FFmpeg at all. This is measured from the shipped binaries — the wheels' own
LICENSE-3RD-PARTY.txt never mentions x264. paces[media] already brings such
a wheel, so [pose] adds a second copy rather than a higher tier. Worse:
rtmlib's own metadata requires both opencv-python and
opencv-contrib-python, unpinned, so a plain pip install paces[media,pose]
ends up with two distributions owning one cv2 — harmless until either is
uninstalled, at which point the survivor's cv2 can be left with files
missing (issue #20). rtmlib only calls plain cv2 APIs (VideoCapture,
dnn.readNetFromONNX, drawing helpers — nothing contrib-only), so [media]'s
opencv-contrib-python already covers it; to keep the closure
single-provider, install in two steps instead of one:
pip install "paces[media]" # opencv-contrib-python, the fleet's one cv2
pip install --no-deps "rtmlib>=0.0.16,<0.1" # skip rtmlib's own opencv-* re-declaration
pip install onnxruntime tqdm # rtmlib's other real deps (numpy already arrives via opencv)
paces.pose.check_pose_requirements() reports what you have, and names the
repair command if both providers are already present.
How it thinks
Analysis and rendering are separate phases with a serialisable document between them — like a parser emitting an AST and a backend interpreting it. Renderers depend on the document, never on the analyser.
Segmentation is a seam, not a stage. segment(media, segmenter=...) —
segmenters are registered capabilities, the default follows from what is
present, and "the user typed the boundaries" is a first-class segmenter, not
a fallback. A segmenter that cannot name steps returns honest unnamed
boundaries (flags: ['naming-abstained']) rather than inventing names.
The document keeps what the learner actually counts. A dance step lasts
"4 eights", not "14.86 seconds" — seconds are derived from the metric grid
(tempo + origin), never stored. A step can have several source spans (the
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 store, and the document is a
projection of it. The document is derivable from the store; the store is
not derivable from the document.
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 |
|---|---|
| cut media into steps | segment(media, steps=..., grid=...) → Segmentation |
| explicit/human boundaries | segment(media, boundaries=[...], steps=[names]) |
| keep a human's corrections across re-runs | segment(..., corrections=[split(t), merge(t), pin((s, e)), forbid(t, (s, e)), absent(t)]) — from paces.corrections; a time-keyed log, replayed as constraints, anything it could not honour flagged |
| use the video's own chapters | segment(media, metadata=<yt-dlp info.json>) |
| measure the grid from the media | segment(local_media, steps=[(name, counts), ...]) — no grid needed; tempo + structure measured, origin estimated and flagged (pip install paces[audio]) |
| protect edits from regeneration | apply_edits(doc, patches, by="user:you") + merge_regenerated(committed, fresh) |
| 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) |
| what segmenters exist | capabilities() / paces list-segmenters |
| add a segmenter | register(Capability(name=..., gives="segmentation", target="mymod:fn", needs={...})) — a new file, nothing edited |
Status
Young and moving. The document schema is validated by round-tripping a real
proof of concept (an interactive dance-practice
page) through it — see
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.
Metadata
Release files for paces 0.0.16
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| paces-0.0.16.tar.gz | 174.3 kB | Without provenance | Details for paces-0.0.16.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| paces-0.0.16-py3-none-any.whl | Python 3 | none | any | Details for paces-0.0.16-py3-none-any.whl |
Total release size: 287.3 kB
Release files / paces-0.0.16.tar.gz
| Download URL | paces-0.0.16.tar.gz |
|---|---|
| Size | 174.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8d1049eb177632f43c5cac24afa147a3ccb7d7154e6b331b2633349988041e87
|
|
BLAKE2b-256 checksum How to use checksums |
eab14c54816863d4cb9441f40b88506e57dbc443544ee9120a30e04ead6227ef
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / paces-0.0.16-py3-none-any.whl
| Download URL | paces-0.0.16-py3-none-any.whl |
|---|---|
| Size | 113.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b286523a6bc4af3499ac93e93fed9e2392e42c527ff3e4c87ca14efb74e3bea3
|
|
BLAKE2b-256 checksum How to use checksums |
dee10829b52ec105513aa4a1bea0319d21e01f6754b99d1c535aadd29dee8255
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for paces 0.0.16
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.