Files
claude-video/tests/test_whisper.py
T

158 lines
5.5 KiB
Python
Raw Normal View History

2026-06-29 23:16:17 +10:00
"""Whisper auto-chunking: plan, split, and timestamp stitching."""
from __future__ import annotations
import math
import subprocess
from pathlib import Path
import pytest
import whisper
MB = 1024 * 1024
class TestPlanChunks:
def test_under_limit_is_single_chunk(self):
plan = whisper.plan_chunks(total_seconds=600.0, total_bytes=5 * MB, max_bytes=24 * MB)
assert plan == [(0.0, 600.0)]
def test_at_limit_is_single_chunk(self):
plan = whisper.plan_chunks(total_seconds=600.0, total_bytes=24 * MB, max_bytes=24 * MB)
assert plan == [(0.0, 600.0)]
def test_over_limit_splits_into_enough_chunks(self):
# 71 MB against a 24 MB cap → ceil(71/24) = 3 chunks.
plan = whisper.plan_chunks(total_seconds=3600.0, total_bytes=71 * MB, max_bytes=24 * MB)
assert len(plan) == 3
def test_chunks_are_contiguous_and_cover_full_duration(self):
total = 3600.0
plan = whisper.plan_chunks(total_seconds=total, total_bytes=71 * MB, max_bytes=24 * MB)
# Offsets start at 0 and each picks up where the previous ended.
assert plan[0][0] == 0.0
for (off, dur), (next_off, _) in zip(plan, plan[1:]):
assert math.isclose(off + dur, next_off)
last_off, last_dur = plan[-1]
assert math.isclose(last_off + last_dur, total)
def test_each_chunk_estimated_under_limit(self):
total_seconds, total_bytes, cap = 3600.0, 71 * MB, 24 * MB
plan = whisper.plan_chunks(total_seconds, total_bytes, cap)
bytes_per_second = total_bytes / total_seconds
for _off, dur in plan:
assert dur * bytes_per_second <= cap
def test_zero_duration_is_single_chunk(self):
plan = whisper.plan_chunks(total_seconds=0.0, total_bytes=0, max_bytes=24 * MB)
assert plan == [(0.0, 0.0)]
class TestShiftSegments:
def test_adds_offset_to_start_and_end(self):
segs = [{"start": 0.0, "end": 2.5, "text": "hi"}, {"start": 2.5, "end": 4.0, "text": "there"}]
shifted = whisper.shift_segments(segs, 1800.0)
assert shifted == [
{"start": 1800.0, "end": 1802.5, "text": "hi"},
{"start": 1802.5, "end": 1804.0, "text": "there"},
]
def test_zero_offset_is_identity(self):
segs = [{"start": 1.0, "end": 2.0, "text": "x"}]
assert whisper.shift_segments(segs, 0.0) == segs
def test_does_not_mutate_input(self):
segs = [{"start": 0.0, "end": 1.0, "text": "x"}]
whisper.shift_segments(segs, 10.0)
assert segs[0]["start"] == 0.0
def _make_mp3(path: Path, seconds: float) -> None:
"""Synthesize a mono 16k 64k mp3 of a sine tone — mirrors extract_audio's format."""
subprocess.run(
[
"ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
"-f", "lavfi", "-t", str(seconds), "-i", "sine=frequency=440:sample_rate=16000",
"-acodec", "libmp3lame", "-ar", "16000", "-ac", "1", "-b:a", "64k",
str(path),
],
check=True,
)
class TestSplitAudio:
def test_creates_one_file_per_plan_entry(self, tmp_path: Path):
full = tmp_path / "audio.mp3"
_make_mp3(full, 6.0)
plan = [(0.0, 3.0), (3.0, 3.0)]
chunks = whisper.split_audio(full, tmp_path, plan)
assert len(chunks) == 2
for chunk_path, _offset in chunks:
assert chunk_path.exists() and chunk_path.stat().st_size > 0
def test_returns_plan_offsets(self, tmp_path: Path):
full = tmp_path / "audio.mp3"
_make_mp3(full, 6.0)
plan = [(0.0, 3.0), (3.0, 3.0)]
chunks = whisper.split_audio(full, tmp_path, plan)
assert [offset for _path, offset in chunks] == [0.0, 3.0]
def test_chunks_are_smaller_than_full(self, tmp_path: Path):
full = tmp_path / "audio.mp3"
_make_mp3(full, 6.0)
plan = [(0.0, 3.0), (3.0, 3.0)]
chunks = whisper.split_audio(full, tmp_path, plan)
full_size = full.stat().st_size
for chunk_path, _offset in chunks:
assert chunk_path.stat().st_size < full_size
class TestAudioDuration:
def test_reads_duration_of_synthesized_clip(self, tmp_path: Path):
audio = tmp_path / "audio.mp3"
_make_mp3(audio, 5.0)
assert whisper.audio_duration(audio) == pytest.approx(5.0, abs=0.5)
class TestTranscribeChunks:
def test_shifts_and_concatenates_each_chunk(self):
chunks = [(Path("a.mp3"), 0.0), (Path("b.mp3"), 100.0)]
def fake_transcribe(path: Path) -> list[dict]:
return [{"start": 0.0, "end": 2.0, "text": path.stem}]
out = whisper.transcribe_chunks(chunks, fake_transcribe)
assert out == [
{"start": 0.0, "end": 2.0, "text": "a"},
{"start": 100.0, "end": 102.0, "text": "b"},
]
def test_keeps_successful_chunks_when_one_fails(self):
chunks = [(Path("a.mp3"), 0.0), (Path("b.mp3"), 100.0)]
def flaky(path: Path) -> list[dict]:
if path.stem == "b":
raise SystemExit("chunk b failed")
return [{"start": 1.0, "end": 2.0, "text": "a"}]
out = whisper.transcribe_chunks(chunks, flaky)
assert out == [{"start": 1.0, "end": 2.0, "text": "a"}]
def test_raises_when_every_chunk_fails(self):
chunks = [(Path("a.mp3"), 0.0), (Path("b.mp3"), 100.0)]
def always_fail(path: Path) -> list[dict]:
raise SystemExit("boom")
with pytest.raises(SystemExit):
whisper.transcribe_chunks(chunks, always_fail)