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:
Olive Vaughn 2026-09-28 01:11:41 -04:00
parent b6517f837a
commit 9cd5243983
61 changed files with 4694 additions and 269 deletions

View file

@ -0,0 +1,149 @@
# Generated by Django 5.2.17 on 2026-09-28 04:44
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Blob',
fields=[
('digest', models.CharField(max_length=64, primary_key=True, serialize=False)),
('media_type', models.CharField(default='application/octet-stream', max_length=100)),
('size', models.BigIntegerField()),
('created', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='Project',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(default='untitled', max_length=200)),
('seq', models.PositiveBigIntegerField(default=0)),
('palette', models.CharField(default='arthur/default', max_length=64)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
],
options={
'ordering': ['-updated'],
},
),
migrations.CreateModel(
name='Analysis',
fields=[
('key', models.CharField(max_length=71, primary_key=True, serialize=False)),
('descriptor', models.TextField()),
('detector', models.CharField(max_length=64)),
('version', models.CharField(max_length=64)),
('created', models.DateTimeField(auto_now_add=True)),
('artifact', models.ForeignKey(blank=True, help_text='the dense landmark track, once bake A is uploaded', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='analysis_for', to='clips.blob')),
],
options={
'verbose_name_plural': 'analyses',
},
),
migrations.CreateModel(
name='Block',
fields=[
('key', models.CharField(max_length=71, primary_key=True, serialize=False)),
('descriptor', models.TextField()),
('role', models.CharField(max_length=32)),
('created', models.DateTimeField(auto_now_add=True)),
('analysis', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='blocks', to='clips.analysis')),
('data', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='block_data_for', to='clips.blob')),
('state', models.ForeignKey(blank=True, help_text='the per-track absence mask, when the take has one', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='block_state_for', to='clips.blob')),
],
),
migrations.CreateModel(
name='Footage',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('digest', models.CharField(max_length=64, unique=True)),
('label', models.CharField(blank=True, max_length=200)),
('source', models.CharField(blank=True, max_length=200)),
('fps', models.FloatField()),
('frames', models.PositiveIntegerField()),
('width', models.PositiveIntegerField()),
('height', models.PositiveIntegerField()),
('feature_absence', models.JSONField(blank=True, default=dict)),
('created', models.DateTimeField(auto_now_add=True)),
('audio', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='audio_for', to='clips.blob')),
],
options={
'ordering': ['-created'],
},
),
migrations.AddField(
model_name='analysis',
name='footage',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='analyses', to='clips.footage'),
),
migrations.CreateModel(
name='Revision',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('seq', models.PositiveBigIntegerField()),
('author', models.CharField(blank=True, max_length=200)),
('summary', models.CharField(blank=True, max_length=500)),
('document', models.JSONField(help_text='every leaf of the project, by path')),
('created', models.DateTimeField(auto_now_add=True)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='clips.project')),
],
options={
'ordering': ['-seq'],
},
),
migrations.CreateModel(
name='FootageFrame',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('index', models.PositiveIntegerField(help_text="0-based; source frame index + 1 is the PNG's name")),
('blob', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='frame_for', to='clips.blob')),
('footage', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frame_set', to='clips.footage')),
],
options={
'ordering': ['index'],
'constraints': [models.UniqueConstraint(fields=('footage', 'index'), name='one_blob_per_frame')],
},
),
migrations.CreateModel(
name='Leaf',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('path', models.CharField(max_length=300)),
('value', models.JSONField()),
('version', models.PositiveBigIntegerField(default=1)),
('updated', models.DateTimeField(auto_now=True)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leaves', to='clips.project')),
],
options={
'ordering': ['path'],
'constraints': [models.UniqueConstraint(fields=('project', 'path'), name='one_leaf_per_path')],
},
),
migrations.CreateModel(
name='Clip',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('cid', models.SlugField(max_length=64)),
('name', models.CharField(blank=True, max_length=200)),
('order', models.IntegerField(default=0)),
('analysis', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='clips', to='clips.analysis')),
('blocks', models.ManyToManyField(blank=True, help_text="the tier-2 blocks this clip's channels name", related_name='clips', to='clips.block')),
('footage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='clips', to='clips.footage')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='clips', to='clips.project')),
],
options={
'ordering': ['order', 'cid'],
'constraints': [models.UniqueConstraint(fields=('project', 'cid'), name='one_cid_per_project')],
},
),
]

View file