Skip to content

Commit cda95fd

Browse files
committed
refactor(audio): one probe_duration_seconds, in common
Adding 02c gave common/ffmpeg_utils.py a duration probe identical to the one 02b had been carrying privately since Voxtral landed. Delete the local copy and import the shared one; get_ffmpeg_paths goes with it, since the helper was its only caller in that module. The three tests that come with it patch the name on the Voxtral module, which is also the assertion that the function is imported rather than redefined: a stale local copy would be the object patched and the test would pass against the wrong code. The None case is covered separately because Voxtral's three-hour cap is a warning rather than a gate, so an unmeasurable file is sent rather than pre-emptively scolded. Also covers the seam between the two audio workstreams that landed in parallel: step 03 learned to read the `Generated using:` header at the same time as 02c learned to write one, and nothing tested that 02c's header resolves to its authority item.
1 parent 9cfbf24 commit cda95fd

2 files changed

Lines changed: 55 additions & 31 deletions

File tree

AI_audio_summary/02b_AI_transcribe_audio_voxtral.py

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import json
1313
import os
1414
import random
15-
import subprocess
1615
import time
1716
from pathlib import Path
1817
from typing import Any, Optional, Tuple
@@ -24,7 +23,7 @@
2423
import sys as _sys
2524
_sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
2625
from common.rate_limiter import QuotaExhaustedError, is_mistral_quota_exhausted
27-
from common.ffmpeg_utils import get_ffmpeg_paths
26+
from common.ffmpeg_utils import probe_duration_seconds
2827
from common.log_redaction import install_credential_redaction
2928

3029
# Credentials ride in Omeka query strings and provider headers; keep them
@@ -57,32 +56,6 @@
5756
]
5857

5958

60-
def probe_duration_seconds(audio_path: Path) -> Optional[float]:
61-
"""Return the media duration in seconds via ffprobe, or ``None``.
62-
63-
Silently returns ``None`` when ffprobe is unavailable or fails — the
64-
duration check is a best-effort warning, not a gate.
65-
"""
66-
paths = get_ffmpeg_paths()
67-
if not paths:
68-
return None
69-
try:
70-
result = subprocess.run(
71-
[
72-
paths.ffprobe, "-v", "error",
73-
"-show_entries", "format=duration",
74-
"-of", "default=noprint_wrappers=1:nokey=1",
75-
str(audio_path),
76-
],
77-
capture_output=True, text=True, timeout=60,
78-
)
79-
if result.returncode != 0:
80-
return None
81-
return float(result.stdout.strip())
82-
except (OSError, subprocess.SubprocessError, ValueError):
83-
return None
84-
85-
8659
class VoxtralTranscriber(TranscriberBase):
8760
def __init__(
8861
self,

tests/test_audio_pipeline.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
"""Audio media discovery, Gemini transcription orchestration, and what step 03
2-
writes back — including which model an ``iwac:transcriptionModel`` annotation is
3-
allowed to name."""
1+
"""Audio media discovery, the Gemini and Voxtral transcription steps, and what
2+
step 03 writes back — including which model an ``iwac:transcriptionModel``
3+
annotation is allowed to name."""
44

55
import importlib.util
66
import io
@@ -29,6 +29,7 @@ def load_script(name, filename):
2929

3030
downloader_module = load_script("audio_media_downloader", "01_omeka_media_downloader.py")
3131
transcription_module = load_script("gemini_audio_transcriber", "02_AI_transcribe_audio.py")
32+
voxtral_module = load_script("voxtral_audio_transcriber", "02b_AI_transcribe_audio_voxtral.py")
3233

3334

3435
def bare_transcriber():
@@ -511,3 +512,53 @@ def test_an_unannotated_write_still_writes_the_transcript():
511512
(written,) = item["bibo:content"]
512513
assert written["@value"] == "transcript"
513514
assert "@annotation" not in written
515+
516+
517+
def bare_voxtral():
518+
"""A Voxtral transcriber with no API key resolved and no network client."""
519+
return voxtral_module.VoxtralTranscriber.__new__(voxtral_module.VoxtralTranscriber)
520+
521+
522+
def warnings_for(duration):
523+
"""Run the length check against a stubbed ffprobe and return what it printed.
524+
525+
Patching the name on the module is also the assertion that the helper is
526+
imported from ``common.ffmpeg_utils`` rather than redefined here: a stale
527+
local copy would be the object patched, and a missing import would raise.
528+
"""
529+
with patch.object(voxtral_module, "probe_duration_seconds", return_value=duration), \
530+
patch.object(voxtral_module, "console") as console:
531+
bare_voxtral()._warn_if_too_long(Path("interview.mp3"))
532+
return [call.args[0] for call in console.print.call_args_list]
533+
534+
535+
def test_file_over_the_three_hour_cap_is_flagged():
536+
warnings = warnings_for(4 * 3600.0)
537+
assert len(warnings) == 1
538+
assert "4.0 h" in warnings[0]
539+
540+
541+
def test_file_within_the_cap_is_not_flagged():
542+
assert warnings_for(2 * 3600.0) == []
543+
544+
545+
def test_unknown_duration_is_not_flagged():
546+
"""Without ffprobe the length is unknown. Voxtral's cap is a soft warning,
547+
not a gate, so an unmeasurable file is sent rather than pre-emptively
548+
scolded — the ``duration and`` short-circuit is what keeps ``None`` quiet.
549+
"""
550+
assert warnings_for(None) == []
551+
552+
553+
def test_the_transcribe_header_resolves_to_its_authority_item():
554+
"""``02c``'s transcripts must be annotatable without ``--model``.
555+
556+
The two workstreams landed in parallel — step 03 learned to read the
557+
``Generated using:`` header while ``02c`` learned to write one — so nothing
558+
covered the seam between them. ``gemini-3.5-transcribe`` is pinned and has
559+
item 113077, which is exactly the case the resolver is meant to catch.
560+
"""
561+
from common.iwac_config import AI_MODEL_ITEMS
562+
563+
assert updater.annotation_key_for("Google gemini-3.5-transcribe") == "gemini-3.5-transcribe"
564+
assert AI_MODEL_ITEMS["gemini-3.5-transcribe"]["item_id"] == 113077

0 commit comments

Comments
 (0)