fix(downloads): GIF review round 1 — shape, HDR, caps, audio, copy
- A GIF squares its pixels before the size, so an anamorphic source keeps
its shown shape (a 720x480 DVD at 16:9 was 480x320).
- An editor GIF of an HDR upload is tonemapped, as a clip GIF is.
- The length caps are judged in whole milliseconds: a range exactly at a
cap is no longer refused for a float hair over it.
- A GIF is made of a video only: an audio file is refused with its reason
and the editor offers no GIF for it.
- A GIF clip keeps no audio track: the track neither splits its cache key
nor fails it.
- The editor's GIF notes keep their live region mounted; one size reads
as one ("about 1 MB"), none is shown with nothing kept; the size choices
show their unit; a shared picture that fails to load says so.
- Copy: the two-sentence GIF notes keep their last stop (a copy-rule test
now holds it); the English warns of "only a still picture".
This commit is contained in:
@@ -3,12 +3,15 @@ to the GIF's limits at enqueue, and made by the worker with real ffmpeg."""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.config import settings
|
||||
from app.downloads import clip as clipmod
|
||||
from app.downloads import edit as editmod
|
||||
from app.downloads import pipeline, service
|
||||
from app.downloads import probe as probemod
|
||||
from app.downloads import gif as gifmod
|
||||
from app.downloads import upload as uploadmod
|
||||
from app.downloads.models import DownloadJob, MediaAsset
|
||||
@@ -63,6 +66,84 @@ def test_a_gif_edit_it_cannot_make_is_refused_with_its_reason(
|
||||
assert e.value.reason == reason
|
||||
|
||||
|
||||
@needs_ffmpeg
|
||||
def test_a_gif_edit_of_exactly_thirty_seconds_is_allowed(db, make_user, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(settings, "files_root", str(tmp_path / "root"))
|
||||
u = make_user()
|
||||
source = _source(db, u, tmp_path)
|
||||
# Ends kept to the millisecond whose float difference lands a hair over 30 s.
|
||||
assert 32.02 - 2.02 > gifmod.MAX_S
|
||||
job = service.enqueue_edit(
|
||||
db, u.id, source, {"trim": {"start_s": 2.02, "end_s": 32.02}, "gif": {"side": 480}}
|
||||
)
|
||||
assert job.edit_spec["gif"] == {"side": 480}
|
||||
|
||||
|
||||
@needs_ffmpeg
|
||||
def test_a_gif_is_made_of_a_video_never_of_audio(db, make_user, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(settings, "files_root", str(tmp_path / "root"))
|
||||
u = make_user()
|
||||
src = tmp_path / "in.m4a"
|
||||
subprocess.run(
|
||||
[
|
||||
"ffmpeg", "-y", "-loglevel", "error",
|
||||
"-f", "lavfi", "-i", "sine=frequency=440:duration=10", "-c:a", "aac", str(src),
|
||||
],
|
||||
check=True,
|
||||
) # fmt: skip
|
||||
source = uploadmod.ingest_upload(
|
||||
db, u.id, filename="in.m4a", src_path=src, size=src.stat().st_size
|
||||
)
|
||||
assert db.get(MediaAsset, source.asset_id).media_kind == "audio"
|
||||
spec = {"trim": {"start_s": 1, "end_s": 5}}
|
||||
with pytest.raises(service.EditError) as e:
|
||||
service.enqueue_edit(db, u.id, source, {**spec, "gif": {"side": 480}})
|
||||
assert e.value.reason == "gif_not_video"
|
||||
# The same range as audio is an edit as it always was.
|
||||
assert service.enqueue_edit(db, u.id, source, spec).edit_spec["trim"]["end_s"] == 5
|
||||
|
||||
|
||||
@needs_ffmpeg
|
||||
def test_an_hdr_source_is_tonemapped_before_its_gif_palette(db, make_user, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(settings, "files_root", str(tmp_path / "root"))
|
||||
u = make_user()
|
||||
src = tmp_path / "hlg.mp4"
|
||||
subprocess.run(
|
||||
[
|
||||
"ffmpeg", "-y", "-loglevel", "error",
|
||||
"-f", "lavfi", "-i", "testsrc2=size=640x360:rate=10:duration=4",
|
||||
"-vf", "setparams=color_trc=arib-std-b67:color_primaries=bt2020:colorspace=bt2020nc",
|
||||
"-c:v", "libx264", "-pix_fmt", "yuv420p", str(src),
|
||||
],
|
||||
check=True,
|
||||
) # fmt: skip
|
||||
source = uploadmod.ingest_upload(
|
||||
db, u.id, filename="hlg.mp4", src_path=src, size=src.stat().st_size
|
||||
)
|
||||
# The upload keeps its HLG tag, as an HDR phone video's does.
|
||||
placed = Path(settings.files_root) / db.get(MediaAsset, source.asset_id).rel_path
|
||||
assert clipmod.is_hdr(probemod.probe_json(placed) or {})
|
||||
job = service.enqueue_edit(
|
||||
db, u.id, source, {"trim": {"start_s": 1, "end_s": 3}, "gif": {"side": 320}}
|
||||
)
|
||||
asset = db.get(MediaAsset, job.asset_id)
|
||||
job.status, asset.status = "running", "downloading"
|
||||
db.commit()
|
||||
seen: dict = {}
|
||||
build = gifmod.build_cmd
|
||||
|
||||
def spy(*a, **k):
|
||||
seen["pre"] = k["pre"]
|
||||
return build(*a, **k)
|
||||
|
||||
monkeypatch.setattr(pipeline.gifmod, "build_cmd", spy)
|
||||
pipeline._process_edit(job.id, asset.id)
|
||||
|
||||
assert "tonemap" in seen["pre"]
|
||||
db.expire_all()
|
||||
assert db.get(DownloadJob, job.id).status == "done"
|
||||
|
||||
|
||||
@needs_ffmpeg
|
||||
def test_a_gif_edit_is_made_by_the_worker_and_kept_as_an_image_file(
|
||||
db, make_user, tmp_path, monkeypatch
|
||||
|
||||
Reference in New Issue
Block a user