Sub-project 2: ingest & tagging / review tools #1
@@ -0,0 +1,94 @@
|
||||
from tools.probe import Probe
|
||||
from tools.tagging import Tags, derive_tags
|
||||
|
||||
|
||||
def test_video_and_audio_is_av():
|
||||
p = Probe(
|
||||
streams=[
|
||||
{
|
||||
"codec_type": "video",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"disposition": {"attached_pic": 0},
|
||||
},
|
||||
{"codec_type": "audio"},
|
||||
],
|
||||
format={"duration": "12.5"},
|
||||
)
|
||||
t = derive_tags(p)
|
||||
assert isinstance(t, Tags)
|
||||
assert t.mode == "av"
|
||||
assert t.resolution == "1920x1080"
|
||||
assert t.duration_s == 13 # round(12.5)
|
||||
|
||||
|
||||
def test_audio_only():
|
||||
p = Probe(streams=[{"codec_type": "audio"}], format={"duration": "30.0"})
|
||||
t = derive_tags(p)
|
||||
assert t.mode == "audio" and t.resolution == "" and t.duration_s == 30
|
||||
|
||||
|
||||
def test_cover_art_stays_audio():
|
||||
p = Probe(
|
||||
streams=[
|
||||
{"codec_type": "audio", "duration": "30.0"},
|
||||
{
|
||||
"codec_type": "video",
|
||||
"width": 600,
|
||||
"height": 600,
|
||||
"disposition": {"attached_pic": 1},
|
||||
},
|
||||
],
|
||||
format={"duration": "30.0"},
|
||||
)
|
||||
t = derive_tags(p)
|
||||
assert t.mode == "audio" and t.resolution == "" and t.duration_s == 30
|
||||
|
||||
|
||||
def test_video_only():
|
||||
p = Probe(
|
||||
streams=[
|
||||
{
|
||||
"codec_type": "video",
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"disposition": {"attached_pic": 0},
|
||||
}
|
||||
],
|
||||
format={"duration": "5.0"},
|
||||
)
|
||||
t = derive_tags(p)
|
||||
assert t.mode == "video" and t.resolution == "1280x720"
|
||||
|
||||
|
||||
def test_duration_falls_back_to_longest_stream():
|
||||
p = Probe(
|
||||
streams=[
|
||||
{"codec_type": "audio", "duration": "10.0"},
|
||||
{
|
||||
"codec_type": "video",
|
||||
"width": 640,
|
||||
"height": 480,
|
||||
"duration": "42.4",
|
||||
"disposition": {"attached_pic": 0},
|
||||
},
|
||||
],
|
||||
format={},
|
||||
)
|
||||
t = derive_tags(p)
|
||||
assert t.duration_s == 42 # round(42.4), longest stream
|
||||
|
||||
|
||||
def test_duration_never_negative_and_zero_default():
|
||||
p = Probe(streams=[{"codec_type": "audio"}], format={})
|
||||
t = derive_tags(p)
|
||||
assert t.duration_s == 0
|
||||
|
||||
|
||||
def test_video_without_dimensions_yields_empty_resolution():
|
||||
p = Probe(
|
||||
streams=[{"codec_type": "video", "disposition": {"attached_pic": 0}}],
|
||||
format={"duration": "3.0"},
|
||||
)
|
||||
t = derive_tags(p)
|
||||
assert t.mode == "video" and t.resolution == ""
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Mechanical tagging: derive mode/duration_s/resolution from a Probe.
|
||||
|
||||
Honors the cover-art guard (spec §5.1): an embedded album-art image in an audio
|
||||
file shows up as a video stream with disposition.attached_pic == 1; it must NOT
|
||||
make the file 'av'.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
from tools.probe import Probe
|
||||
|
||||
|
||||
@dataclass
|
||||
class Tags:
|
||||
mode: str
|
||||
duration_s: int
|
||||
resolution: str
|
||||
|
||||
|
||||
def _is_real_video(stream) -> bool:
|
||||
if stream.get("codec_type") != "video":
|
||||
return False
|
||||
return stream.get("disposition", {}).get("attached_pic", 0) != 1
|
||||
|
||||
|
||||
def _parse_duration(value) -> float:
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def derive_tags(probe: Probe) -> Tags:
|
||||
real_video = [s for s in probe.streams if _is_real_video(s)]
|
||||
has_audio = any(s.get("codec_type") == "audio" for s in probe.streams)
|
||||
|
||||
if real_video and has_audio:
|
||||
mode = "av"
|
||||
elif real_video:
|
||||
mode = "video"
|
||||
else:
|
||||
mode = "audio"
|
||||
|
||||
resolution = ""
|
||||
if real_video:
|
||||
v = real_video[0]
|
||||
w, h = v.get("width"), v.get("height")
|
||||
if w and h:
|
||||
resolution = f"{w}x{h}"
|
||||
|
||||
if "duration" in probe.format:
|
||||
seconds = _parse_duration(probe.format.get("duration"))
|
||||
else:
|
||||
seconds = max(
|
||||
(_parse_duration(s.get("duration")) for s in probe.streams),
|
||||
default=0.0,
|
||||
)
|
||||
|
||||
# Round half up (math.floor(x + 0.5)) rather than Python's banker's
|
||||
# rounding, so a 12.5s clip becomes 13s as the spec intends.
|
||||
duration_s = max(0, math.floor(seconds + 0.5))
|
||||
return Tags(mode=mode, duration_s=duration_s, resolution=resolution)
|
||||
Reference in New Issue
Block a user