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
|
|
@ -18,15 +18,20 @@ manifest that makes the frames the backend's to serve.
|
|||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
from unittest import skipUnless
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from clips import blobs
|
||||
from clips.models import Analysis, Block, Blob, Clip, Footage, Leaf, Project, Revision
|
||||
from clips import blobs, extraction
|
||||
from clips.models import Analysis, Block, Blob, Clip, Footage, Leaf, Project, Revision, Source
|
||||
|
||||
BLOB_DIR = tempfile.mkdtemp(prefix="arthur-test-blobs-")
|
||||
|
||||
|
|
@ -215,6 +220,29 @@ class Tier2Tests(TestCase):
|
|||
# duplicate it on disk.
|
||||
self.assertEqual(1, Blob.objects.filter(block_data_for__isnull=False).distinct().count())
|
||||
|
||||
def test_an_analysis_reopens_its_three_source_blocks(self):
|
||||
analysis = self.register_analysis()
|
||||
keys = []
|
||||
for role in ("source/dense", "source/detected", "source/crops"):
|
||||
descriptor = block_descriptor(analysis, role=role)
|
||||
key = key_for(descriptor)
|
||||
self.assertEqual(201, self.post("/api/blocks", {
|
||||
"key": key, "descriptor": descriptor, "data": "AA==",
|
||||
}).status_code)
|
||||
keys.append(key)
|
||||
response = self.client.put(
|
||||
f"/api/analyses/{analysis}", json.dumps({"source_blocks": keys}),
|
||||
content_type="application/json")
|
||||
self.assertEqual(200, response.status_code, response.content)
|
||||
self.assertEqual(set(keys), set(self.client.get(
|
||||
f"/api/analyses/{analysis}").json()["source_blocks"]))
|
||||
self.assertEqual(200, self.client.put(
|
||||
f"/api/analyses/{analysis}", json.dumps({"source_blocks": keys}),
|
||||
content_type="application/json").status_code)
|
||||
self.assertEqual(400, self.client.put(
|
||||
f"/api/analyses/{analysis}", json.dumps({"source_blocks": keys[:2]}),
|
||||
content_type="application/json").status_code)
|
||||
|
||||
|
||||
@override_settings(BLOB_ROOT=BLOB_DIR)
|
||||
class DocumentTests(TestCase):
|
||||
|
|
@ -476,3 +504,70 @@ class PageTests(TestCase):
|
|||
self.assertNotEqual("unknown", report["version"])
|
||||
self.assertTrue(report["model"].startswith("sha256:"))
|
||||
self.assertIn("+", report["version"])
|
||||
|
||||
|
||||
@skipUnless(shutil.which("ffmpeg") and shutil.which("ffprobe"), "ffmpeg is required")
|
||||
@override_settings(BLOB_ROOT=BLOB_DIR)
|
||||
class UploadTests(TestCase):
|
||||
def test_frame_decode_reports_live_progress(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
frames = root / "frames"
|
||||
frames.mkdir()
|
||||
job = Mock(progress=0)
|
||||
|
||||
class FakeProcess:
|
||||
returncode = 0
|
||||
calls = 0
|
||||
|
||||
def poll(self):
|
||||
self.calls += 1
|
||||
if self.calls == 1:
|
||||
(root / "frames.progress").write_text("frame=2\nprogress=continue\n")
|
||||
return None
|
||||
return 0
|
||||
|
||||
def wait(self):
|
||||
return 0
|
||||
|
||||
with patch("clips.extraction.subprocess.Popen", return_value=FakeProcess()), \
|
||||
patch("clips.extraction.time.sleep"):
|
||||
extraction._decode_frames(job, root / "source.mp4", frames,
|
||||
{"reported_frames": 4, "duration": 1, "fps": 4},
|
||||
root)
|
||||
self.assertEqual(30, job.progress)
|
||||
job.save.assert_called_once_with(update_fields=["progress", "updated"])
|
||||
|
||||
def test_uploaded_video_extracts_to_reopenable_footage(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "four-frames.mp4"
|
||||
subprocess.run([
|
||||
"ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
|
||||
"-f", "lavfi", "-i", "color=c=red:s=64x48:r=4:d=1",
|
||||
"-c:v", "mpeg4", str(path),
|
||||
], check=True, capture_output=True)
|
||||
payload = path.read_bytes()
|
||||
|
||||
uploaded = self.client.post("/api/sources", {
|
||||
"file": SimpleUploadedFile("four-frames.mp4", payload, content_type="video/mp4")})
|
||||
self.assertEqual(201, uploaded.status_code, uploaded.content)
|
||||
source_id = uploaded.json()["id"]
|
||||
self.assertEqual(4, uploaded.json()["probe"]["reported_frames"])
|
||||
self.assertEqual(1, Source.objects.count())
|
||||
again = self.client.post("/api/sources", {
|
||||
"file": SimpleUploadedFile("same-video.mp4", payload, content_type="video/mp4")})
|
||||
self.assertEqual(200, again.status_code, again.content)
|
||||
self.assertEqual(source_id, again.json()["id"])
|
||||
|
||||
with patch("clips.extraction.enqueue", side_effect=extraction.run):
|
||||
queued = self.client.post("/api/extractions", json.dumps({
|
||||
"source": source_id, "settings": {},
|
||||
}), content_type="application/json")
|
||||
self.assertIn(queued.status_code, (200, 202), queued.content)
|
||||
job = self.client.get(f"/api/extractions/{queued.json()['key']}").json()
|
||||
self.assertEqual("done", job["state"], job)
|
||||
footage = self.client.get(f"/api/footage/{job['footage']}").json()
|
||||
self.assertEqual((4, 64, 48), (footage["frames"], footage["width"], footage["height"]))
|
||||
self.assertEqual(4, len(footage["urls"]))
|
||||
self.assertEqual(200, self.client.get(footage["urls"][0]).status_code)
|
||||
self.assertEqual(200, self.client.get(footage["audio"]).status_code)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue