Serve the document from a Django backend, split into three tiers
Step 9. The tier split was the work; Django was the easy half.
Tier 1 — the authored scene — is the document, and it is addressed as
independently versioned leaves rather than saved whole, so one vertex drag
cannot clobber a collaborator's keying. `domain/leaf` is the document as
path -> value; `domain/wire` puts it on the wire as transit, because JSON
has neither integer map keys nor keywords and a save would quietly turn
`{0 v}` into `{"0" v}`.
Tier 2 — the dense channel blocks — is content-addressed by a hash over
every input, with the detector version inside every key through the
analysis the block descriptor names. `flow/address`'s `block-knobs` is the
invalidation table, and `address-test` does not trust it: it re-freezes the
take once per knob and asserts the biconditional, that a block's bytes
changed if and only if its key changed. That found `brow-pos` not depending
on `contour-avg` — the brow ring is smoothed, the raise is not.
Tier 3 — frames and audio — is served by the hash of its bytes out of the
same store. A manifest now names frames and carries a URL for each, so the
frame layout stopped being a shared secret between a shell script and a
ClojureScript namespace, and the `?v=` cache-buster went with it: a blob's
name is the hash of its contents, so a stale copy is not a thing that can
happen. The synthetic take's `audio.wav` moved to `static/arthur/` — an
asset the project owns, not an extraction that churns.
The server verifies rather than trusting a name it was handed: it
recomputes every key from the descriptor stored beside it, refuses an
analysis that declares no detector version, and refuses a document naming
blocks it does not hold. It hashes the descriptor TEXT, because JS prints
an integral double as `1` and Python as `1.0`, and a scheme where both ends
re-render the numbers disagrees on the first parameter that happens to be
whole.
Two loose ends from step 8 closed on the way. `pack` no longer takes a
`(track, frame)` predicate whose call sites each re-derived a feature from
an index — every track names the feature it follows, which deleted five
hand-maintained mappings. And `:dev-http` is gone: Django serves the page,
shadow-cljs only builds into the staticfiles tree.
227 CLJS tests, 31 Django tests, green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b6517f837a
commit
9cd5243983
61 changed files with 4694 additions and 269 deletions
|
|
@ -2,16 +2,20 @@
|
|||
|
||||
Self-contained. You should not need any prior conversation to execute this.
|
||||
|
||||
**Implementation status (2026-09-27):** steps 0–7 are in the CLJS frontend.
|
||||
Step 6 reads extracted footage from the manifest, detects landmarks with local
|
||||
MediaPipe assets at full source cadence, and runs the same freeze path as the
|
||||
synthetic take. The scene time map can sample the frozen roto at a lower picture
|
||||
fps without changing source analysis, duration or audio. Step 7 adds dense
|
||||
eyelids, shared gaze, brows and pixel-derived teeth. Step 8's data model now
|
||||
has stable feature identity, feature-level presence, explicit eye pairs and
|
||||
shared parameter definitions. A manifest can now supply known feature absence
|
||||
intervals through measurement and freeze. Automatic per-feature detection,
|
||||
parameter editing and scoped regeneration remain.
|
||||
**Implementation status (2026-09-28):** steps 0–9 are in. Step 6 reads extracted
|
||||
footage, detects landmarks with local MediaPipe assets at full source cadence, and
|
||||
runs the same freeze path as the synthetic take. The scene time map can sample the
|
||||
frozen roto at a lower picture fps without changing source analysis, duration or
|
||||
audio. Step 7 adds dense eyelids, shared gaze, brows and pixel-derived teeth.
|
||||
Step 8's data model has stable feature identity, feature-level presence, explicit
|
||||
eye pairs and shared parameter definitions; a manifest can supply known feature
|
||||
absence intervals through measurement and freeze. Step 9 adds the Django backend,
|
||||
the three-tier split, content-addressed tier 2 with the detector version inside
|
||||
every key, leaf addressing for tier 1, and project load/save that round-trips.
|
||||
|
||||
**Still open.** Step 8's parameter UI and scoped regeneration, and automatic
|
||||
per-feature detection. Everything under "Out, and do not build it" below, which
|
||||
step 9 did not touch.
|
||||
|
||||
## What arthur is
|
||||
|
||||
|
|
@ -59,17 +63,40 @@ arthur/
|
|||
src/arthur/** namespace root stays arthur.* whatever the dir is called
|
||||
test/arthur/**
|
||||
static/arthur/js/ shadow-cljs output, collected by Django staticfiles
|
||||
static/arthur/audio.wav the synthetic take's clock. NOT extract.sh's output —
|
||||
that is tier 3 and lives in the blob store
|
||||
var/blobs/ the content-addressed blob store: tiers 2 and 3. Gitignored
|
||||
docs/
|
||||
js/ index.html serve.py extract.sh the old tool — see "the oracle"
|
||||
```
|
||||
|
||||
The namespaces step 9 added, since the list under **Namespaces** in
|
||||
`docs/architecture.md` predates them:
|
||||
|
||||
```
|
||||
domain/sha256.cljs SHA-256, synchronous and pure, byte-compatible with hashlib
|
||||
domain/canon.cljs the one canonical text for a descriptor, so hashing it means
|
||||
something
|
||||
domain/leaf.cljs leaf addressing: the document as path -> value
|
||||
domain/wire.cljs transit for tier 1, base64 for tier 2
|
||||
domain/project.cljs clip <-> the document and blocks that travel
|
||||
flow/address.cljs tier-2 keys, and the invalidation table they are built from
|
||||
fx/http.cljs the only namespace that talks to the server
|
||||
events/project.cljs save and open
|
||||
```
|
||||
|
||||
`clips` is a naming call, not a constraint — it is the Django app holding
|
||||
Project, Clip, Footage, Analysis, Leaf and Revision. Rename in one line if
|
||||
something fits better.
|
||||
|
||||
Dev runs two processes: Django serves the page, `shadow-cljs watch app` rebuilds
|
||||
into `static/arthur/js`. Set `:output-dir "../static/arthur/js"` in
|
||||
`shadow-cljs.edn`.
|
||||
Dev runs two processes and they do not talk to each other: Django serves the page,
|
||||
`shadow-cljs watch app` rebuilds into `static/arthur/js`, which is already
|
||||
`:output-dir` in `shadow-cljs.edn`. `:dev-http` is gone.
|
||||
|
||||
```sh
|
||||
mise exec -- python manage.py runserver 8778 # from the repo root
|
||||
cd frontend && mise exec -- npx shadow-cljs watch app
|
||||
```
|
||||
|
||||
## Toolchain
|
||||
|
||||
|
|
@ -352,10 +379,50 @@ group. Retain source measurements so a setting change can regenerate affected
|
|||
channels without re-detecting footage. Time-varying parameter values and all
|
||||
parameter controls are deferred to the UI pass.
|
||||
|
||||
### 9 — backend
|
||||
Django project, the `clips` app, models for Project/Clip/Footage/Analysis/Leaf,
|
||||
and project load/save. Round-tripping a project through the server is the proof
|
||||
the model serialises.
|
||||
### 9 — backend — DONE
|
||||
Django project, the `clips` app, models for
|
||||
Project/Clip/Footage/FootageFrame/Analysis/Block/Leaf/Revision/Blob, and project
|
||||
load/save. Round-tripping a project through the server is the proof the model
|
||||
serialises.
|
||||
|
||||
Django was the easy half; the tier split was the work. What it came to:
|
||||
|
||||
**Tier 2 keys are content addresses over every input**, and the detector version
|
||||
is in every one of them, through the analysis id that each block descriptor names.
|
||||
`flow/address`'s `block-knobs` is the invalidation table, and it is not trusted:
|
||||
`address-test` re-freezes the take once per knob and asserts the biconditional —
|
||||
a block's bytes changed if and only if its key changed. That test found two
|
||||
things reading the code would not have. `brow-pos` does not depend on
|
||||
`contour-avg`, because the brow RING is smoothed and the raise is not. And the
|
||||
first version of the test was itself wrong: a 3% perturbation of `gaze-gain`
|
||||
moves every sample inside the grid cell `quantize-snap` had already rounded it
|
||||
into, so the bytes came out identical and the knob looked like an input the block
|
||||
did not have.
|
||||
|
||||
**The server verifies what it is handed.** It recomputes every key from the
|
||||
descriptor stored beside it and refuses a mismatch, refuses an analysis that does
|
||||
not declare a detector version, and refuses a document naming blocks it does not
|
||||
hold. It hashes the descriptor TEXT rather than re-rendering it from parsed
|
||||
values, because JS prints an integral double as `1` and Python as `1.0` — a
|
||||
scheme where both sides re-render breaks on the first parameter whose value
|
||||
happens to be whole.
|
||||
|
||||
**Tier 3 is served by hash.** `extract.sh` still decodes; `manage.py
|
||||
ingest_bundle` hashes the result into the blob store, by hard link. The manifest
|
||||
the client receives now carries a URL per frame, so the frame layout stopped being
|
||||
a shared secret between a shell script and a ClojureScript namespace. The
|
||||
cache-busting `?v=` on every frame URL went with it: a blob's name is the hash of
|
||||
its bytes, so a stale copy is not a thing that can happen.
|
||||
|
||||
**Leaf addressing exists**, with conditional writes and a monotonic project
|
||||
version, so the sync design has nothing to retrofit. The socket, presence and
|
||||
broadcasts are still out of scope.
|
||||
|
||||
Two loose ends from step 8 closed on the way. `pack` no longer takes a
|
||||
`(track, frame)` predicate whose call sites each derived a feature from an index —
|
||||
every track names the feature it follows, which deleted five hand-maintained
|
||||
mappings and handed `flow/address` the same list for its observation digest. And
|
||||
the `presence-check` binding in a `let` nobody read is now an ordinary `doseq`.
|
||||
|
||||
**Output is not in this plan.** The `.take` writer in `js/take.js` was for
|
||||
driving an Animator Pro render script and it is not where this is going: the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue