Skip to main content

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])
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.12

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for paces 0.0.12
File Size Uploaded
paces-0.0.12.tar.gz 151.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for paces 0.0.12
File Interpreter ABI Platform
paces-0.0.12-py3-none-any.whl Python 3 none any Details

Total release size: 248.2 kB

Release files / paces-0.0.12.tar.gz

Download URL paces-0.0.12.tar.gz
Size 151.8 kB
Tags Source
SHA-256 checksum
How to use checksums
dae328272515bf5873ba959cc5c13aa13d83216e11403afd6d161f334b284a1e
BLAKE2b-256 checksum
How to use checksums
93d448e79f24426d2665d4f18b53b151cc9b8ebcc34c3fcf90d545929e351680
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","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.12-py3-none-any.whl

Download URL paces-0.0.12-py3-none-any.whl
Size 96.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4d1618d1c64d93df71d2205b4d12465c4cc90410a5209b0f8b3e51a6fefce2f3
BLAKE2b-256 checksum
How to use checksums
dfb8ec59866e896730ebb3185b0c339ca0f4729de5280fe847fdde27d3f6a180
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","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 history Release notifications | RSS feed

This release

0.0.12 This release

2 release files

0.0.9

2 release files

0.0.8

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page