feat: mechanical mode/duration/resolution tagging

Per sub-project-2 plan Task 4 / spec §5.1. derive_tags(Probe) -> Tags with the
attached_pic cover-art guard so an art-bearing audio file stays mode=audio.
Duration falls back to the longest stream when format.duration is absent, and
rounds half-up so 12.5s -> 13s (Python's round() would give 12 via banker's
rounding) to match the spec's stated behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-04 06:23:57 -07:00
parent 19104f94cd
commit 542b5a5641
2 changed files with 159 additions and 0 deletions
+65
View File
@@ -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)