Add video upload, extraction progress, and reusable analysis sources
This commit is contained in:
parent
690de21fa4
commit
686f897401
24 changed files with 927 additions and 137 deletions
106
clips/views.py
106
clips/views.py
|
|
@ -27,15 +27,17 @@ import hashlib
|
|||
import json
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
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
|
||||
from . import blobs, extraction
|
||||
from .models import Analysis, Block, Blob, Clip, Extraction, Footage, Leaf, Project, Revision, Source
|
||||
|
||||
KEY_LENGTH = 71 # "sha256:" + 64 hex
|
||||
|
||||
|
|
@ -155,8 +157,71 @@ def detector(request):
|
|||
# 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.
|
||||
# frame instead, so uploaded video and command-line bundles produce the same
|
||||
# footage response without the client knowing where either stored its frames.
|
||||
|
||||
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def sources(request):
|
||||
if request.method == "GET":
|
||||
return JsonResponse({"sources": [
|
||||
{"id": str(row.id), "filename": row.filename, "probe": row.probe}
|
||||
for row in Source.objects.order_by("-created")[:100]
|
||||
]})
|
||||
upload = request.FILES.get("file")
|
||||
if upload is None:
|
||||
return JsonResponse({"error": "upload a video as the file field"}, status=400)
|
||||
try:
|
||||
digest, size = blobs.write_stream(upload.chunks())
|
||||
facts = extraction.probe(blobs.path_for(digest))
|
||||
blob, _ = Blob.objects.get_or_create(
|
||||
digest=digest, defaults={"size": size,
|
||||
"media_type": upload.content_type or "video/mp4"})
|
||||
row, created = Source.objects.get_or_create(
|
||||
blob=blob, defaults={"filename": Path(upload.name).name[:255], "probe": facts})
|
||||
return JsonResponse({"id": str(row.id), "digest": digest,
|
||||
"filename": row.filename, "probe": row.probe,
|
||||
"created": created}, status=201 if created else 200)
|
||||
except (ValueError, OSError) as exc:
|
||||
return JsonResponse({"error": str(exc)}, status=400)
|
||||
|
||||
|
||||
def _extraction_json(row):
|
||||
return {"key": row.key, "source": str(row.source_id), "state": row.state,
|
||||
"progress": row.progress, "error": row.error,
|
||||
"footage": str(row.footage_id) if row.footage_id else None}
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
def extractions(request):
|
||||
try:
|
||||
data = _body(request)
|
||||
source_id = data.get("source")
|
||||
if not source_id:
|
||||
raise Bad("an extraction needs a source id")
|
||||
try:
|
||||
source = Source.objects.get(id=UUID(str(source_id)))
|
||||
except (ValueError, ValidationError, Source.DoesNotExist):
|
||||
raise Bad("no such source", status=404)
|
||||
settings = data.get("settings") or {}
|
||||
if settings != {}:
|
||||
raise Bad("extraction currently keeps the source frame rate; settings must be empty")
|
||||
key = extraction.extraction_key(source, settings)
|
||||
row, _ = Extraction.objects.get_or_create(
|
||||
key=key, defaults={"source": source, "settings": settings})
|
||||
if row.state != "done":
|
||||
extraction.enqueue(key)
|
||||
return JsonResponse(_extraction_json(row), status=202 if row.state != "done" else 200)
|
||||
except Bad as exc:
|
||||
return _error(exc)
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def extraction_detail(request, key):
|
||||
try:
|
||||
return JsonResponse(_extraction_json(Extraction.objects.get(key=key)))
|
||||
except Extraction.DoesNotExist:
|
||||
return JsonResponse({"error": "no such extraction"}, status=404)
|
||||
|
||||
|
||||
def _footage_json(footage: Footage, urls=True):
|
||||
|
|
@ -254,6 +319,39 @@ def analyses(request):
|
|||
return _error(exc)
|
||||
|
||||
|
||||
@require_http_methods(["GET", "PUT"])
|
||||
def analysis_detail(request, key):
|
||||
try:
|
||||
row = Analysis.objects.get(key=key)
|
||||
except Analysis.DoesNotExist:
|
||||
return JsonResponse({"error": "no such analysis"}, status=404)
|
||||
if request.method == "GET":
|
||||
return JsonResponse({
|
||||
"key": row.key, "descriptor": row.descriptor,
|
||||
"detector": row.detector, "version": row.version,
|
||||
"footage": str(row.footage_id) if row.footage_id else None,
|
||||
"source_blocks": sorted(row.source_blocks.values_list("key", flat=True)),
|
||||
})
|
||||
try:
|
||||
keys = _body(request).get("source_blocks")
|
||||
roles = {"source/dense", "source/detected", "source/crops"}
|
||||
if not isinstance(keys, list) or len(keys) != len(roles) or len(set(keys)) != len(roles):
|
||||
raise Bad("an analysis needs one block for each source role")
|
||||
blocks = list(Block.objects.filter(key__in=keys))
|
||||
if (len(blocks) != len(roles) or {b.role for b in blocks} != roles
|
||||
or any(b.analysis_id != key for b in blocks)):
|
||||
raise Bad("source blocks must have distinct source roles and name this analysis")
|
||||
with transaction.atomic():
|
||||
row = Analysis.objects.select_for_update().get(key=key)
|
||||
current = set(row.source_blocks.values_list("key", flat=True))
|
||||
if current and current != set(keys):
|
||||
raise Bad("the source blocks of an analysis are immutable", status=409)
|
||||
row.source_blocks.set(blocks)
|
||||
return JsonResponse({"key": key, "source_blocks": sorted(keys)})
|
||||
except Bad as exc:
|
||||
return _error(exc)
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
def blocks_missing(request):
|
||||
"""Which of these keys the server does not have.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue