Skip to main content
Glama
README.md
# splicedeck

[![checks](https://github.com/ihuzaifashoukat/splicedeck/actions/workflows/checks.yml/badge.svg)](https://github.com/ihuzaifashoukat/splicedeck/actions/workflows/checks.yml)
[![licence: Apache-2.0](https://img.shields.io/badge/licence-Apache--2.0-blue.svg)](LICENSE)
[![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://www.python.org/downloads/)
[![runtime dependencies: 0](https://img.shields.io/badge/runtime%20dependencies-0-brightgreen.svg)](pyproject.toml)

**A video editor an AI agent drives, running on your own machine.**

One call dresses a cut in a template: motion graphics, overlays, captions and a beat
grid to cut against. One pass over a source gives you both a cleaned long-form master
and vertical clips. And it remembers how each client, channel or show likes to be cut,
so the next edit starts where the last one ended.

There is no timeline to drag and no account to create. Nothing is uploaded.

---

> ### Status: the pipeline runs end to end, and memory reaches the cut
>
> A source becomes a delivered file today. Measured on the reference machine
> (Windows 11, Python 3.13, ffmpeg 8.1.2) against a real 223 MB `.mov`:
>
> ```
> inspect   2.6 s     draft  27 ms     splice  27 ms     verify  42 ms
> deliver   157 s  ->  1920×1080 h264 + aac, -23.0 LUFS, decodes clean
> ```
>
> `python -m pytest` reports **1425 passed, 2 skipped** in about four minutes.
> Thirty-one verbs reach the CLI and eighteen of them reach an MCP server, both
> generated from one table so they cannot drift apart.
>
> One headline feature does **not** work yet. Cutting by quoting needs a speech
> binary that no manifest can currently obtain. Read
> [What does not work](#what-does-not-work) before you plan around it.

---

## Install

You need Python 3.12 or newer and ffmpeg 8.x on your `PATH`. splicedeck neither installs
nor bundles ffmpeg, and [`docs/first-run.md`](docs/first-run.md) §4 explains why that is
deliberate.

The setup script asks where the workspace goes, offers to install ffmpeg after showing
you the exact command, scaffolds everything and writes an MCP config:

```bash
curl -fsSLO https://raw.githubusercontent.com/ihuzaifashoukat/splicedeck/main/install.sh
less install.sh && bash install.sh
```

```powershell
irm https://raw.githubusercontent.com/ihuzaifashoukat/splicedeck/main/install.ps1 -OutFile install.ps1
notepad install.ps1; powershell -ExecutionPolicy Bypass -File install.ps1
```

Read it before you run it. A `curl | bash` one-liner would be a poor advertisement for a
project whose README is largely about a threat model.

If you would rather do it yourself, or want the tests:

```bash
uv tool install splicedeck                    # or: pipx install splicedeck
git clone https://github.com/ihuzaifashoukat/splicedeck.git && cd splicedeck
python -m venv .venv
.venv/Scripts/python -m pip install -e ".[dev]"   # Windows
.venv/bin/python  -m pip install -e ".[dev]"      # macOS, Linux
```

**Not on PyPI yet.** There is no release, so `uv tool install splicedeck` will 404 until
the first tag is pushed. Until then use the script, a clone, or
`uv tool install "git+https://github.com/ihuzaifashoukat/splicedeck.git"`.

[`docs/install.md`](docs/install.md) has every route, the per-platform ffmpeg commands,
the environment variables, and a prompt you can paste into an AI agent to have it install
and wire up splicedeck for you.

## Try it

The workspace root is whichever directory you run in, and `spd init` scaffolds one:

```bash
mkdir my-edit && cd my-edit
spd init                                      # bookmarks/ casebook/ elements/ ledger/ media/ profiles/ templates/
mkdir -p casebook/parties/demo
spd ready                                     # what is present, and what each gap blocks
```

`init` never overwrites. Re-running it after you have edited a profile fills in
whatever is missing and leaves your edits alone. The files it writes are byte-identical
to the ones this repository ships, and `python -m checks.starter --check` enforces that.

Then put your footage under `media/` and cut:

```bash
spd inspect --path media/your-file.mov               # mints a source handle
spd draft --party demo --source s1 --bookmark baseline --profile wide-1080
spd apply --sheet c1 --template clean-master         # overlays, motion, beat grid
spd splice --sheet c1 --source a --in_ticks 0 --out_ticks 900000 \
           --source_in_ticks 0 --cause manual
spd verify --sheet c1
spd deliver --sheet c1
```

Sources must live inside the workspace. A path with a drive letter in it is refused
`PATH_OUTSIDE_WORKSPACE` before anything is read.

A party is created by a human, by hand, on purpose. `draft` refuses `UNKNOWN_PARTY`
until `casebook/parties/<name>/` exists.

To drive it from an MCP-capable assistant, register the server:

```json
{"mcpServers": {"splicedeck": {
  "command": "C:\\src\\splicedeck\\.venv\\Scripts\\python.exe",
  "args": ["-m", "splicedeck.surface.mcp"],
  "cwd": "C:\\src\\splicedeck"}}}
```

`cwd` must be the workspace, because the workspace root is the working directory and
nothing else discovers it. `python -m splicedeck.surface.mcp --tools` prints the
generated tool list and exits, which is how you tell a broken server from a broken host
configuration. [`docs/mcp.md`](docs/mcp.md) is the full guide.

## Templates: the look in one call

`apply` dresses a cut sheet in a named template. It places the overlays, writes the
beat grid the agent then cuts against, and records on the sheet which template it used.

Four ship today:

| Template | What it is |
|---|---|
| `clean-master` | A calm talking-head master carrying one lower-third and no beat grid |
| `quick-beat` | A fast-cut vertical: three beat slots with a pulsing accent on each |
| `bold-run` | A promo look: full-bleed intro and outro cards around two beat slots |
| `bare-mark` | One small mark on screen and nothing else |

A template's overlays animate when the optional motion tier is installed, and fall back
to a still imprint when it is not. Six animated compositions ship in
[`scenes/`](scenes/README.md), written for this project and licensed with it.

Slots are enforced. `verify` refuses to pass a sheet with an unfilled slot, and a cut
landing outside a slot's tolerance is refused `SLOT_TOO_TIGHT` with the nearest legal
edges returned as ready-to-send calls. That is what lets an agent hit a rhythm it
cannot see.

You can write your own. `spd compose --kind template` validates and writes a
hand-authored template or element card. It is deliberately CLI-only: the MCP server may
not write to `templates/`, and [`docs/templates.md`](docs/templates.md) §4 explains the
reasoning rather than treating it as an oversight.

## Why memory

An edit is a thousand small judgements and almost all of them repeat. How long to hold
after a punchline. Whether this speaker's filler is noise or personality. How big
captions have to be on a phone at arm's length. A stateless tool makes you re-supply
that context every session, which is why "AI editing" so often produces something
technically correct and tonally wrong.

Here, a decision you make once is recorded and reused:

```
subtitle.size_px = 74
  when {surface: vertical, frame: 1080x1920}
  set by  a render you shipped and kept, 2026-08-02
  before  66
```

That record lives in your repository as reviewable text. You can read the diff, correct
a bad entry by editing a line, and `git revert` a change that made the edits worse. It
is a behaviour changelog, kept in the same place as everything else you version.

Two rules keep it trustworthy:

- Nothing durable is written by the model. A record describes something a *human* did:
  shipped a render and kept it, restored a moment the cut removed. The agent can point
  at what happened; it cannot compose what gets remembered.
- Every write passes a human gate. No preference is learned silently.

That loop runs today. `spd set`, `ship`, `keep`, `restore` and `discard` append acts to
a party's hash-chained ledger and stage a proposal from each one; an act cannot be
appended to a chain that does not verify. `spd review` then asks for the value **blind**,
showing the bounds and the shipped cut but never the number, and a matching answer
becomes a sealed case and a regenerated `findings.lock.txt`. The next `draft` resolves
against it: the bookmark opens the settings, the casebook overrides the ones a human
settled, and the sheet records which lock it read.

## Local first, and complete

A fresh clone with no API keys and no cloud account produces a finished, delivered file
on your own machine. That is the baseline, not a degraded mode.

Cloud services can be switched on where they genuinely help, such as a hosted speech API
for difficult audio or diarisation, but nothing becomes required and no deliverable
depends on one. ffmpeg does the work as a child process. It is never vendored and never
linked.

The project also refuses to guess about your hardware. Encoder support is proven by
test-encoding rather than by reading a feature list, because feature lists lie. On the
development machine `ffmpeg -encoders` advertises an NVIDIA encoder that fails at
runtime, while the Intel one that actually works goes unmentioned in every guide.

## What works

- **One analysis pass, two deliverables.** Transcription and analysis run once per
  source. The long-form master and the vertical clips both read the same results.
- **Frame-accurate cutting with no audio drift.** Audio stays PCM until the mux and is
  encoded once. The delivered samples are byte-identical to a reference assembly built
  in Python over 45 and 120 joins. Measured, not asserted.
- **Templates and motion in one call**, with a beat grid the agent cuts against and a
  still fallback when the motion tier is absent.
- **Captions that stay legible.** Size and contrast floors are enforced by the renderer,
  and text that would land under a platform's own interface is refused rather than
  drawn. Glyphs are shaped and rasterised by a pure-stdlib TrueType parser, so imprints
  are byte-reproducible and committed as goldens.
- **Vertical framing that admits uncertainty.** When the subject cannot be tracked
  confidently it declines to auto-frame and says why. A confidently wrong crop is worse
  than an honest refusal, because nobody reviews the one that looked fine.
- **Rights that hold up.** Music, effects and stock footage carry a record of where they
  came from and what the terms permit. A delivery refuses to run if any asset lacks one.
- **Typed refusals that carry their own correction.** A refusal arrives with
  `retry_with`, a list of ready-to-send calls. There are 102 codes, each with a
  construction site and a test proving it is reachable.

## What does not work

Stated plainly, because a status section that omits this is the reason the last one was
worthless.

| Not working | Why | Blocks |
|---|---|---|
| Cutting by quoting | `splicedeck/listen/fetchable.toml` pins both download entries at a host that does not resolve, with placeholder all-zero digests. No route obtains the speech binary, including placing it by hand. | `hear`, `quote`, captions from speech |
| Subject tracking by model | No detector is pinned or shipped ([`docs/framing.md`](docs/framing.md) F5). The boundary is settled — a CLI child process, never an imported extension — but which binary fills it is not. | `watch --subject largest` at the model tier |
| Cancelling from an MCP host | The stdio loop is single-threaded, so nothing can arrive during a `tools/call`. | `cancel` over MCP. The CLI and Ctrl-C are unaffected. |

[`CHANGELOG.md`](CHANGELOG.md) carries the same list, and the two are meant to stay in
sync.

Two tiers below the model one do work. `subject: "centre"` is geometric and needs
nothing. `SPD_SIGHT_LOCATOR=reduce` selects a weights-free locator that finds the subject
by temporal-median background subtraction in pure stdlib Python, no numpy and no compiled
extension anywhere.

Checked against a face detector on the reference master, that locator's median agreed to
within 0.1% of frame width. On the same footage it then reported certainty 0.26 and fitted
no path at all, because a speaker who barely moves against a static background leaves
nothing for background subtraction to hold onto. Both of those are the right answer: the
arithmetic is sound, and the honest limit of a weights-free tier is a hole rather than a
centred guess ([`docs/framing.md`](docs/framing.md) §7). Footage with a moving subject
tracks fine.

## How you drive it

Through an MCP server and Skills, so any MCP-capable assistant can use it, plus a CLI
exposing exactly the same verbs. Both surfaces are generated from
`splicedeck/surface/verbs.py`, and `python -m checks.golden --check` fails the build if
they drift apart.

The server speaks five protocol revisions, `2024-11-05` through `2026-07-28`, and
answers both the `initialize` handshake and `server/discover`.

Failures are typed. A refusal carries its own correction as ready-to-send calls rather
than prose an agent has to interpret, so recovery is one turn:

```json
{"ok": false, "verb": "draft", "refused": "BOOKMARK_UNKNOWN",
 "plain": "No bookmark by that name is shipped.",
 "needs_human": false,
 "retry_with": [{"verb": "draft", "args": {"bookmark": "baseline", "party": "demo",
   "profile": "wide-1080", "situation": "default", "source": "s1"}}]}
```

### Skills

Four skills teach an agent the verb order, the traps between verbs, and how to turn a
refusal into the next correct call. They live in
[`.claude/skills/`](.claude/skills), and a clone picks them up with no install at all.

| Skill | Fires when |
|---|---|
| `cutting-a-deliverable` | Turning a source into a delivered file |
| `cutting-vertical-clips` | Carving a 9:16 clip and keeping the subject in frame |
| `recovering-from-a-refusal` | Any `ok: false`, or an `spd` command exiting 1 |
| `contributing-to-splicedeck` | Editing this codebase, or when two documents disagree |

This repository is also a Claude Code plugin and its own marketplace:

```bash
claude plugin marketplace add ihuzaifashoukat/splicedeck
claude plugin install splicedeck@splicedeck
```

Or install the skills into any of the agents the `skills` CLI supports, including
Codex, Cursor, OpenCode, Antigravity, Cline, Gemini CLI, Zed and Windsurf:

```bash
npx skills add ihuzaifashoukat/splicedeck            # add --list to look first
```

Both routes ship the skills only. They do not register the MCP server, because the
server needs an absolute interpreter path and a `cwd` that neither a plugin nor a skill
installer can know. `install.sh` writes that for you, and
[`docs/mcp.md`](docs/mcp.md) has it by hand.

Every other agent runtime reads [`AGENTS.md`](AGENTS.md).

## Design

The specification is written before the code, deliberately.

| Document | What it settles |
|---|---|
| [`AGENTS.md`](AGENTS.md) | The contract every contributor and agent works under |
| [`docs/architecture.md`](docs/architecture.md) | The map: runtimes, packages, data flow |
| [`docs/first-run.md`](docs/first-run.md) | Clone to delivered file, and the Windows traps |
| [`docs/install.md`](docs/install.md) | Every install route, and a prompt for an AI agent |
| [`docs/mcp.md`](docs/mcp.md) | Driving splicedeck from an assistant |
| [`docs/cut-sheet.md`](docs/cut-sheet.md) | The core artifact: integer-timed, diffable, human-readable |
| [`docs/templates.md`](docs/templates.md) | Templates, slots, and what `apply` and `compose` do |
| [`docs/casebook.md`](docs/casebook.md) | How memory is stored, resolved and gated |
| [`docs/security.md`](docs/security.md) | The threat model, and why memory is an attack surface |
| [`docs/bookmarks.md`](docs/bookmarks.md) | Styles, and the axes they are points in |
| [`docs/agent-surface.md`](docs/agent-surface.md) | The verb table and the refusal catalogue |
| [`docs/roadmap.md`](docs/roadmap.md) | The feature areas, and what each has to prove |

Persistent memory in an agent is a security surface, not just a feature. Anything an
attacker can write into it outlives the conversation that planted it. If you read one
document, read [`docs/security.md`](docs/security.md).

## Non-goals

Assembling a film from many sources. Generating video or music. A timeline GUI.
Real-time collaboration. A hosted service. Automatically choosing which moments become
clips, since it presents candidates and waits for a person.

## Requirements

Python 3.12 or newer, and ffmpeg 8.x on your `PATH`. No compiled Python extension is
used on any default path, so there is no build step and no platform runtime to install
first. Windows, macOS and Linux; CI covers Ubuntu and Windows, and macOS is untested by
machine.

The motion tier additionally needs Node and an `npm install` inside `scenes/`. It is
optional, and a delivery without it falls back to still imprints.

## Contributing

Issues and design critique are welcome. [`CONTRIBUTING.md`](CONTRIBUTING.md) is the
front door: setup, the checks to run, how to add a verb or a refusal code, and the
things that get a pull request rejected regardless of merit. Read
[`AGENTS.md`](AGENTS.md) first. The twelve hard rules are load-bearing, and a change
that breaks one is rejected on that basis alone.

By taking part you agree to the [Code of Conduct](CODE_OF_CONDUCT.md).

## Security

Please do not open a public issue for a vulnerability. [`SECURITY.md`](SECURITY.md) has
the reporting route and what is in scope.

## Licence

[Apache-2.0](LICENSE). Copyright 2026 Huzaifa Shoukat.

TDQS

B3.3/5.0

Scored across 18 tools

Disambiguation4/5

Most tools have clearly distinct purposes (e.g., inspect reads media, draft starts a cut sheet, splice adds segments, hear transcribes). A few pairs like 'splice' and 'place' could be confused in the abstract, but descriptions differentiate them. Overall, an agent can reliably distinguish tools.

Naming Consistency5/5

All tool names are single-word verbs in lowercase, following a highly consistent pattern. Names like 'splice', 'carve', and 'trial' are domain-appropriate and uniform, with no mixing of conventions.

Tool Count5/5

18 tools cover a comprehensive media editing workflow—ingest, editing, verification, delivery, and auxiliary operations. The count feels well-scoped for the domain, with each tool serving a necessary function and no redundancy.

Completeness4/5

The toolset covers the core editing lifecycle well (from source intake to final delivery). Minor gaps exist, such as a lack of tools for deleting segments, managing sources, or listing projects, but these are not critical to the main editing flow.

Maintenance

ActivityMaintained
ResponsivenessNo issues