"""The API's implementation. Two things in here are load-bearing and neither is Django. THE SERVER VERIFIES EVERY TIER-2 KEY IT IS HANDED. A key is the sha256 of a canonical descriptor, and this recomputes it and refuses a mismatch. That is what makes content addressing a property of the system rather than a convention in the client: nothing can store bytes under a name that does not describe them. It hashes THE TEXT IT WAS SENT rather than re-rendering the descriptor from parsed values, and that is the honest arrangement rather than a shortcut. JS prints an integral double as `1` and Python prints `1.0`, so a scheme where both sides re-render the numbers would disagree on the first parameter whose value happens to be whole — and the failure would be an upload that 409s with nothing wrong. The bytes are the contract; the schema on top of them is a convention, and the two fields this file actually reads out of that schema are checked separately. AND IT REFUSES A BLOCK WHOSE ANALYSIS IT DOES NOT KNOW. Every block descriptor names an analysis, and every analysis declares a detector and a VERSION. So the chain from a stored block to the model version that produced it cannot be broken by a client that forgot a step — which is the whole point of docs/architecture.md's insistence that the cache key include the detector version. A model upgrade that silently reused old landmarks would otherwise present as "the tool got worse", with no event to attach it to. """ import hashlib import json from functools import lru_cache from pathlib import Path from django.conf import settings from django.db import transaction from django.http import FileResponse, HttpResponse, JsonResponse from django.shortcuts import render from django.views.decorators.http import require_http_methods from . import blobs from .models import Analysis, Block, Blob, Clip, Footage, Leaf, Project, Revision KEY_LENGTH = 71 # "sha256:" + 64 hex # --------------------------------------------------------------------------- # helpers def _body(request): try: return json.loads(request.body or b"{}") except json.JSONDecodeError as exc: raise Bad(f"the request body is not JSON: {exc}") from exc class Bad(Exception): """A 400 with a message, raised where the problem is noticed.""" def __init__(self, message, status=400, **detail): super().__init__(message) self.message = message self.status = status self.detail = detail def _error(exc: Bad): return JsonResponse({"error": exc.message, **exc.detail}, status=exc.status) def _check_key(key, descriptor): """The verification. A key is the sha256 of the descriptor stored beside it.""" if not isinstance(key, str) or len(key) != KEY_LENGTH or not key.startswith("sha256:"): raise Bad(f"not a content address: {key!r}") if not isinstance(descriptor, str) or not descriptor: raise Bad("a key without its descriptor addresses nothing") actual = hashlib.sha256(descriptor.encode("utf-8")).hexdigest() if actual != key[7:]: raise Bad( "the key is not the hash of its descriptor", status=409, expected=f"sha256:{actual}", given=key, ) try: return json.loads(descriptor) except json.JSONDecodeError as exc: raise Bad(f"the descriptor is not canonical JSON: {exc}") from exc def _blob(b64, media_type="application/octet-stream"): import base64 digest, size = blobs.write(base64.b64decode(b64)) blob, _ = Blob.objects.get_or_create( digest=digest, defaults={"size": size, "media_type": media_type} ) return blob # --------------------------------------------------------------------------- # the page def page(request): """The host page. This replaced `frontend/public/index.html` at step 9, and `:dev-http` in shadow-cljs.edn went away with it.""" return render(request, "clips/index.html") # --------------------------------------------------------------------------- # the detector # # WHY THE SERVER ANSWERS THIS. The analysis key has to include the detector # version, and a version string in the client is a string somebody has to remember # to bump. The server serves the model, so it can hash the model — and then the # version is a fact about the bytes that produced the landmarks rather than a # claim about them. @lru_cache(maxsize=4) def _model_digest(path: str, mtime: float) -> str: return blobs.digest_file(Path(path)) def _package_version() -> str: pkg = Path(settings.BASE_DIR) / "frontend" / "package.json" try: deps = json.loads(pkg.read_text())["dependencies"] return deps["@mediapipe/tasks-vision"].lstrip("^~") except Exception: return "unknown" @require_http_methods(["GET"]) def detector(request): model = Path(settings.BASE_DIR) / "frontend" / "public" / "mediapipe" / "face_landmarker.task" if not model.exists(): # Honest rather than fatal: detection will fail at the MediaPipe boundary # with a better message than this one could give, and an analysis stamped # "unknown" is a take somebody can still look at and re-freeze later. return JsonResponse({"detector": "mediapipe", "version": "unknown", "model": None}) digest = _model_digest(str(model), model.stat().st_mtime) return JsonResponse( { "detector": "mediapipe", # The package version AND the model's own hash. Either alone can change # while the other does not, and both change the landmarks. "version": f"{_package_version()}+{digest[:16]}", "model": f"sha256:{digest}", } ) # --------------------------------------------------------------------------- # tier 3: footage # # THE MANIFEST NOW CARRIES URLS. It used to carry a directory and the loader built # `frames/0001.png` itself, which quietly made the frame layout a shared secret # between a shell script and a ClojureScript namespace. The server names every # frame instead, so the frames can move into the blob store — or later be uploaded # from the browser by wasm ffmpeg — without the client learning anything new. def _footage_json(footage: Footage, urls=True): out = { "id": str(footage.id), "label": footage.label or footage.source, "source": footage.source, "fps": footage.fps, "frames": footage.frames, "width": footage.width, "height": footage.height, "footage": f"sha256:{footage.digest}", "audio": f"/blob/{footage.audio.digest}", "feature-absence": footage.feature_absence or {}, } if urls: out["urls"] = [f"/blob/{f.blob.digest}" for f in footage.frame_set.select_related("blob")] return out @require_http_methods(["GET"]) def footage_list(request): return JsonResponse( {"footage": [_footage_json(f, urls=False) for f in Footage.objects.all()]} ) @require_http_methods(["GET"]) def footage_detail(request, footage_id): try: footage = Footage.objects.select_related("audio").get(id=footage_id) except Footage.DoesNotExist: return JsonResponse({"error": "no such footage"}, status=404) return JsonResponse(_footage_json(footage)) @require_http_methods(["GET"]) def blob(request, digest): """Raw bytes, immutable. `immutable` is not optimism here, it is the definition: the name IS the hash of the content, so a cached copy cannot be stale. That is what makes serving 600 frames out of this cheap enough to do on every load. """ try: row = Blob.objects.get(digest=digest) path = blobs.path_for(digest) except (Blob.DoesNotExist, ValueError): return JsonResponse({"error": "no such blob"}, status=404) response = FileResponse(open(path, "rb"), content_type=row.media_type) response["Cache-Control"] = "public, max-age=31536000, immutable" response["ETag"] = f'"{digest}"' return response # --------------------------------------------------------------------------- # tier 2: analyses and blocks @require_http_methods(["POST"]) def analyses(request): """Register an analysis artifact's identity. Idempotent: the same inputs are the same key are the same row.""" try: data = _body(request) key = data.get("key") descriptor = data.get("descriptor") parsed = _check_key(key, descriptor) for field in ("detector", "version"): if not parsed.get(field): raise Bad( f"the descriptor does not declare a {field}: a cache key that " "omits the detector version lets a model upgrade silently reuse " "old landmarks", missing=field, ) footage = None if data.get("footage"): digest = str(data["footage"]).removeprefix("sha256:") footage = Footage.objects.filter(digest=digest).first() if footage is None: raise Bad("the analysis names footage this server does not have", footage=data["footage"]) row, created = Analysis.objects.get_or_create( key=key, defaults={ "descriptor": descriptor, "detector": parsed["detector"], "version": str(parsed["version"]), "footage": footage, }, ) return JsonResponse({"key": row.key, "created": created}, status=201 if created else 200) except Bad as exc: return _error(exc) @require_http_methods(["POST"]) def blocks_missing(request): """Which of these keys the server does not have. The return on content addressing, as one request: a save uploads the blocks that are new and nothing else, so re-saving a document after a knob-free edit moves kilobytes. """ try: keys = _body(request).get("keys") or [] if not isinstance(keys, list): raise Bad("keys must be a list") have = set(Block.objects.filter(key__in=keys).values_list("key", flat=True)) return JsonResponse({"missing": [k for k in keys if k not in have]}) except Bad as exc: return _error(exc) @require_http_methods(["POST"]) def blocks(request): """Store one dense block: its bytes, its optional absence mask, and the descriptor its key is the hash of.""" try: data = _body(request) key = data.get("key") descriptor = data.get("descriptor") parsed = _check_key(key, descriptor) role = parsed.get("role") if not role: raise Bad("a block's descriptor names its role") if not parsed.get("layout", {}).get("type"): raise Bad( "a block's descriptor must say what its elements are: an Int16Array " "and a Float32Array over the same bytes are both valid readings and " "only one of them is the block" ) analysis_key = parsed.get("analysis") analysis = Analysis.objects.filter(key=analysis_key).first() if analysis is None: raise Bad( "this block names an analysis the server does not know; register the " "analysis first, so that every stored block can name the detector " "version that produced it", analysis=analysis_key, ) if not data.get("data"): raise Bad("a block with no bytes") with transaction.atomic(): row, created = Block.objects.get_or_create( key=key, defaults={ "descriptor": descriptor, "role": role, "analysis": analysis, "data": _blob(data["data"]), "state": _blob(data["state"]) if data.get("state") else None, }, ) return JsonResponse({"key": row.key, "created": created}, status=201 if created else 200) except Bad as exc: return _error(exc) @require_http_methods(["GET"]) def block_detail(request, key): import base64 try: row = Block.objects.select_related("data", "state").get(key=key) except Block.DoesNotExist: return JsonResponse({"error": "no such block"}, status=404) out = { "key": row.key, "descriptor": row.descriptor, "data": base64.b64encode(blobs.read(row.data.digest)).decode("ascii"), } if row.state_id: out["state"] = base64.b64encode(blobs.read(row.state.digest)).decode("ascii") response = JsonResponse(out) response["Cache-Control"] = "public, max-age=31536000, immutable" return response # --------------------------------------------------------------------------- # tier 1: projects, clips, leaves def _project_json(project: Project): leaves = list(project.leaves.all()) clips = [] for clip in project.clips.all(): prefix = f"clip/{clip.cid}/" clips.append( { "cid": clip.cid, "name": clip.name, "footage": str(clip.footage_id) if clip.footage_id else None, "analysis": clip.analysis_id, "blocks": sorted(clip.blocks.values_list("key", flat=True)), "leaves": {leaf.path: leaf.value for leaf in leaves if leaf.path.startswith(prefix)}, } ) return { "id": str(project.id), "name": project.name, "seq": project.seq, "palette": project.palette, "clips": clips, } @require_http_methods(["GET", "POST"]) def projects(request): if request.method == "GET": return JsonResponse( { "projects": [ {"id": str(p.id), "name": p.name, "seq": p.seq, "updated": p.updated.isoformat()} for p in Project.objects.all()[:100] ] } ) try: data = _body(request) project = Project.objects.create(name=data.get("name") or "untitled") return JsonResponse(_project_json(project), status=201) except Bad as exc: return _error(exc) @require_http_methods(["GET", "PUT"]) def project_detail(request, project_id): try: project = Project.objects.get(id=project_id) except Project.DoesNotExist: return JsonResponse({"error": "no such project"}, status=404) if request.method == "GET": return JsonResponse(_project_json(project)) try: return _save(project, _body(request)) except Bad as exc: return _error(exc) @transaction.atomic def _save(project: Project, data): """A whole-document save: one clip's leaves replace that clip's leaves. SCOPED BY CLIP, not by project. A payload that carries clip `a` does not disturb clip `b`'s leaves, because a save is not the only way the document changes — a single-leaf conditional write is — and a save that cleared everything it did not mention would be a save that undoes a collaborator. A leaf whose value is unchanged keeps its VERSION. That is what makes the entity tag mean something: a save of a document where one channel moved invalidates one leaf's etag, not all four hundred. """ if data.get("name"): project.name = data["name"] if data.get("palette"): project.palette = data["palette"] written, removed, unchanged = [], [], [] for spec in data.get("clips") or []: cid = spec.get("cid") if not cid: raise Bad("every clip in a save names its cid") leaves = spec.get("leaves") or {} prefix = f"clip/{cid}/" for path in leaves: if not path.startswith(prefix): raise Bad( f"leaf {path!r} is not addressed to clip {cid!r}", clip=cid, path=path, ) keys = spec.get("blocks") or [] have = set(Block.objects.filter(key__in=keys).values_list("key", flat=True)) if missing := [k for k in keys if k not in have]: # Referential integrity across the tiers, enforced where it can be: # a document that names blocks the server does not hold would load # into a blank stage on any other machine. raise Bad( "this clip names tier-2 blocks the server does not have; upload them " "before saving the document that points at them", status=409, missing=missing, ) analysis = Analysis.objects.filter(key=spec.get("analysis")).first() footage = None if spec.get("footage"): footage = Footage.objects.filter(id=spec["footage"]).first() clip, _ = Clip.objects.update_or_create( project=project, cid=cid, defaults={"name": spec.get("name") or "", "analysis": analysis, "footage": footage}, ) clip.blocks.set(Block.objects.filter(key__in=keys)) existing = {leaf.path: leaf for leaf in project.leaves.filter(path__startswith=prefix)} for path, value in leaves.items(): leaf = existing.get(path) if leaf is None: Leaf.objects.create(project=project, path=path, value=value) written.append(path) elif leaf.value != value: leaf.value = value leaf.version += 1 leaf.save(update_fields=["value", "version", "updated"]) written.append(path) else: unchanged.append(path) for path, leaf in existing.items(): if path not in leaves: leaf.delete() removed.append(path) seq = project.seq + 1 project.seq = seq project.save() return JsonResponse( { "id": str(project.id), "seq": seq, "written": sorted(written), "removed": sorted(removed), "unchanged": len(unchanged), } ) @require_http_methods(["GET", "PUT"]) def leaf_detail(request, project_id, leaf_path): """One leaf, conditionally. `If-Match` and a 409 whose body carries the CURRENT value, so the client can offer keep-mine / take-theirs. A PUT that replaced unconditionally is the bug docs/architecture.md calls out in tl: the loser's work disappears silently, and for a painted cel that is the class of bug that ends trust in a tool. """ try: project = Project.objects.get(id=project_id) except Project.DoesNotExist: return JsonResponse({"error": "no such project"}, status=404) leaf = project.leaves.filter(path=leaf_path).first() if request.method == "GET": if leaf is None: return JsonResponse({"error": "no such leaf"}, status=404) response = JsonResponse({"path": leaf.path, "value": leaf.value, "version": leaf.version}) response["ETag"] = leaf.etag return response try: data = _body(request) except Bad as exc: return _error(exc) if "value" not in data: return _error(Bad("a leaf write carries a value")) match = request.headers.get("If-Match") if leaf is None: # ANY `If-Match` on a leaf that does not exist is a failed precondition, # `*` included: RFC 7232 gives `*` the meaning "the resource must already # exist", which is exactly the write a client makes when it believes it is # editing something. Creating it instead would turn "somebody deleted this # node" into a silent resurrection. if match: return JsonResponse( {"error": "no such leaf", "path": leaf_path}, status=409 ) leaf = Leaf.objects.create(project=project, path=leaf_path, value=data["value"]) else: if match and match not in ("*", leaf.etag): response = JsonResponse( { "error": "stale write", "path": leaf.path, "version": leaf.version, "value": leaf.value, }, status=409, ) response["ETag"] = leaf.etag return response leaf.value = data["value"] leaf.version += 1 leaf.save(update_fields=["value", "version", "updated"]) seq = project.bump() response = JsonResponse({"path": leaf.path, "version": leaf.version, "seq": seq}) response["ETag"] = leaf.etag return response @require_http_methods(["GET", "POST"]) def revisions(request, project_id): """Mark a version: one snapshot of the authored layer, with a summary.""" try: project = Project.objects.get(id=project_id) except Project.DoesNotExist: return JsonResponse({"error": "no such project"}, status=404) if request.method == "GET": return JsonResponse( { "revisions": [ {"seq": r.seq, "author": r.author, "summary": r.summary, "created": r.created.isoformat(), "leaves": len(r.document)} for r in project.revisions.all()[:100] ] } ) data = json.loads(request.body or b"{}") revision = Revision.objects.create( project=project, seq=project.seq, author=data.get("author") or "", summary=data.get("summary") or "", document={leaf.path: leaf.value for leaf in project.leaves.all()}, ) return JsonResponse({"seq": revision.seq, "leaves": len(revision.document)}, status=201)