mate-workload-stt 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ # Python
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+
10
+ # uv
11
+ uv.lock
12
+
13
+ # Node / Cloudflare Worker
14
+ worker/node_modules/
15
+ worker/.wrangler/
16
+ worker/dist/
17
+
18
+ # Results (local benchmark output)
19
+ results/
20
+
21
+ # Secrets / local config
22
+ .env
23
+ *.env.local
24
+
25
+ # OS
26
+ .DS_Store
27
+ Thumbs.db
28
+
29
+ # Internal planning notes
30
+ BENCH_NOTES.md
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: mate-workload-stt
3
+ Version: 0.1.0
4
+ Summary: Speech-to-text workload plugin for mate-bench
5
+ Project-URL: Homepage, https://github.com/T0nd3/mate-bench
6
+ Project-URL: Repository, https://github.com/T0nd3/mate-bench
7
+ Author-email: Benjamin Fäuster <benjamin.faeuster@web.de>
8
+ License: MIT
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Topic :: System :: Benchmark
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: mate-bench<0.2,>=0.1
16
+ Description-Content-Type: text/markdown
17
+
18
+ # mate-workload-stt
19
+
20
+ Speech-to-text workload plugin for [mate-bench](https://github.com/T0nd3/mate-bench).
21
+
22
+ Benchmarks STT transcription speed and accuracy using LibriSpeech test-clean audio clips
23
+ with [faster-whisper](https://github.com/SYSTRAN/faster-whisper).
24
+
25
+ ## Metrics
26
+
27
+ | Metric | Description |
28
+ |--------|-------------|
29
+ | `rtf` | Real-Time Factor — `processing_time / audio_duration` (lower is better) |
30
+ | `wer` | Word Error Rate — edit distance / total reference words (lower is better) |
31
+ | `total_audio_seconds` | Total audio processed per run |
32
+
33
+ ## Profiles
34
+
35
+ | Profile | Clips | Audio | Model |
36
+ |---------|-------|-------|-------|
37
+ | `quick` | 5 | ~50 s | whisper-large-v3 |
38
+ | `standard` | 20 | ~200 s | whisper-large-v3 |
39
+
40
+ ## Usage
41
+
42
+ ```bash
43
+ mate-bench run stt --profile quick
44
+ mate-bench run stt --profile standard
45
+ ```
46
+
47
+ ## Test data
48
+
49
+ [LibriSpeech test-clean](https://openslr.org/12) (Panayotov et al., 2015).
50
+ Licensed [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
@@ -0,0 +1,33 @@
1
+ # mate-workload-stt
2
+
3
+ Speech-to-text workload plugin for [mate-bench](https://github.com/T0nd3/mate-bench).
4
+
5
+ Benchmarks STT transcription speed and accuracy using LibriSpeech test-clean audio clips
6
+ with [faster-whisper](https://github.com/SYSTRAN/faster-whisper).
7
+
8
+ ## Metrics
9
+
10
+ | Metric | Description |
11
+ |--------|-------------|
12
+ | `rtf` | Real-Time Factor — `processing_time / audio_duration` (lower is better) |
13
+ | `wer` | Word Error Rate — edit distance / total reference words (lower is better) |
14
+ | `total_audio_seconds` | Total audio processed per run |
15
+
16
+ ## Profiles
17
+
18
+ | Profile | Clips | Audio | Model |
19
+ |---------|-------|-------|-------|
20
+ | `quick` | 5 | ~50 s | whisper-large-v3 |
21
+ | `standard` | 20 | ~200 s | whisper-large-v3 |
22
+
23
+ ## Usage
24
+
25
+ ```bash
26
+ mate-bench run stt --profile quick
27
+ mate-bench run stt --profile standard
28
+ ```
29
+
30
+ ## Test data
31
+
32
+ [LibriSpeech test-clean](https://openslr.org/12) (Panayotov et al., 2015).
33
+ Licensed [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
@@ -0,0 +1,35 @@
1
+ [project]
2
+ name = "mate-workload-stt"
3
+ version = "0.1.0"
4
+ description = "Speech-to-text workload plugin for mate-bench"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = {text = "MIT"}
8
+ authors = [{name = "Benjamin Fäuster", email = "benjamin.faeuster@web.de"}]
9
+ classifiers = [
10
+ "License :: OSI Approved :: MIT License",
11
+ "Programming Language :: Python :: 3",
12
+ "Programming Language :: Python :: 3.11",
13
+ "Programming Language :: Python :: 3.12",
14
+ "Topic :: System :: Benchmark",
15
+ ]
16
+ dependencies = [
17
+ "mate-bench>=0.1,<0.2",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/T0nd3/mate-bench"
22
+ Repository = "https://github.com/T0nd3/mate-bench"
23
+
24
+ [project.entry-points."mate_bench.workload"]
25
+ stt = "mate_workload_stt:SttWorkload"
26
+
27
+ [dependency-groups]
28
+ dev = ["pytest>=8.0", "ruff>=0.4"]
29
+
30
+ [build-system]
31
+ requires = ["hatchling"]
32
+ build-backend = "hatchling.build"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/mate_workload_stt"]
@@ -0,0 +1,123 @@
1
+ """Speech-to-text workload plugin for mate-bench.
2
+
3
+ Benchmarks STT transcription speed (RTF) and accuracy (WER) using
4
+ LibriSpeech test-clean audio clips with a faster-whisper backend.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from mate_bench._utils import sha256_file
14
+ from mate_bench.paths import TEST_SETS_DIR
15
+ from mate_bench.plugin import (
16
+ EnginePlugin,
17
+ Measurement,
18
+ Mode,
19
+ PluginManifest,
20
+ ProfileConfig,
21
+ TestSetSpec,
22
+ )
23
+
24
+ from ._download import fetch_clips, fetch_manifest
25
+ from ._measure import measure
26
+ from ._profiles import PROFILES, TEST_SETS
27
+
28
+ __all__ = ["SttWorkload"]
29
+
30
+ _STT_DIR = TEST_SETS_DIR / "stt"
31
+
32
+
33
+ class SttWorkload:
34
+ name = "stt"
35
+ manifest = PluginManifest(requires_mate_bench=">=0.1,<0.2", api_version=1)
36
+ profiles: dict[str, ProfileConfig] = PROFILES
37
+ test_sets: dict[str, TestSetSpec] = TEST_SETS
38
+
39
+ def estimate_download(self, profile: str) -> int:
40
+ return PROFILES[profile].download_size_bytes
41
+
42
+ def estimate_vram(self, profile: str) -> int:
43
+ return int(PROFILES[profile].vram_required_gb * 1024**3)
44
+
45
+ def estimate_runtime(self, profile: str) -> int:
46
+ return PROFILES[profile].estimated_runtime_seconds
47
+
48
+ def required_models(self, profile: str) -> list[str]:
49
+ return [PROFILES[profile].reference_engine_config["model"]]
50
+
51
+ def _manifest_path(self, test_set_id: str) -> Path:
52
+ spec = TEST_SETS[test_set_id]
53
+ name = spec.url.rsplit("/", 1)[-1]
54
+ return _STT_DIR / "manifests" / name
55
+
56
+ def setup_closed(self, profile: str) -> None:
57
+ spec = TEST_SETS[PROFILES[profile].test_set_id]
58
+ cached_manifest = fetch_manifest(spec.url, spec.sha256, _STT_DIR)
59
+ fetch_clips(cached_manifest, _STT_DIR / "clips")
60
+
61
+ def setup_open(self, profile: str, user_inputs: dict[str, Any]) -> None:
62
+ raise NotImplementedError("open mode not supported for stt workload")
63
+
64
+ def _load_clips_and_paths(self, profile: str) -> tuple[list[dict], dict[str, Path]]:
65
+ test_set_id = PROFILES[profile].test_set_id
66
+ spec = TEST_SETS[test_set_id]
67
+ cached_manifest = fetch_manifest(spec.url, spec.sha256, _STT_DIR)
68
+ clip_paths = fetch_clips(cached_manifest, _STT_DIR / "clips")
69
+ return cached_manifest["clips"], clip_paths
70
+
71
+ def run(
72
+ self,
73
+ profile: str,
74
+ mode: Mode,
75
+ engine: EnginePlugin,
76
+ runs: int,
77
+ warmup_runs: int,
78
+ ) -> Measurement:
79
+ if mode != Mode.CLOSED:
80
+ raise NotImplementedError("open mode not supported for stt workload")
81
+
82
+ cfg = PROFILES[profile].reference_engine_config
83
+ model = cfg["model"]
84
+ clips, clip_paths = self._load_clips_and_paths(profile)
85
+
86
+ median_stats, std_dev_stats, throttling_detected = measure(
87
+ engine, # type: ignore[arg-type]
88
+ model,
89
+ clips,
90
+ clip_paths,
91
+ runs,
92
+ warmup_runs,
93
+ )
94
+
95
+ return Measurement(
96
+ runs=runs,
97
+ warmup_runs=warmup_runs,
98
+ median=median_stats,
99
+ std_dev=std_dev_stats,
100
+ vram_peak_gb=0.0,
101
+ throttling_detected=throttling_detected,
102
+ )
103
+
104
+ def test_set_hash(self, profile: str) -> str:
105
+ path = self._manifest_path(PROFILES[profile].test_set_id)
106
+ if not path.exists():
107
+ self.setup_closed(profile)
108
+ return sha256_file(path)
109
+
110
+ def cleanup(self, profile: str) -> None:
111
+ test_set_id = PROFILES[profile].test_set_id
112
+ spec = TEST_SETS[test_set_id]
113
+ name = spec.url.rsplit("/", 1)[-1]
114
+ manifest_path = _STT_DIR / "manifests" / name
115
+ if manifest_path.exists():
116
+ with manifest_path.open() as f:
117
+ manifest = json.load(f)
118
+ for clip in manifest.get("clips", []):
119
+ fname = clip["url"].rsplit("/", 1)[-1]
120
+ p = _STT_DIR / "clips" / fname
121
+ if p.exists():
122
+ p.unlink()
123
+ manifest_path.unlink()
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import urllib.request
6
+ from pathlib import Path
7
+
8
+
9
+ def _sha256_file(path: Path) -> str:
10
+ h = hashlib.sha256()
11
+ with path.open("rb") as f:
12
+ for chunk in iter(lambda: f.read(65536), b""):
13
+ h.update(chunk)
14
+ return f"sha256:{h.hexdigest()}"
15
+
16
+
17
+ def _download(url: str, dst: Path) -> None:
18
+ dst.parent.mkdir(parents=True, exist_ok=True)
19
+ with urllib.request.urlopen(url) as resp, dst.open("wb") as f:
20
+ while chunk := resp.read(65536):
21
+ f.write(chunk)
22
+
23
+
24
+ def fetch_manifest(url: str, expected_sha256: str, cache_dir: Path) -> dict:
25
+ """Download and cache the test-set manifest JSON; verify hash."""
26
+ name = url.rsplit("/", 1)[-1]
27
+ dst = cache_dir / "manifests" / name
28
+ if not dst.exists():
29
+ _download(url, dst)
30
+ if expected_sha256 != "sha256:PENDING":
31
+ actual = _sha256_file(dst)
32
+ if actual != expected_sha256:
33
+ dst.unlink()
34
+ raise ValueError(f"Manifest hash mismatch: expected {expected_sha256}, got {actual}")
35
+ with dst.open() as f:
36
+ return json.load(f)
37
+
38
+
39
+ def fetch_clips(manifest: dict, clips_dir: Path) -> dict[str, Path]:
40
+ """Download all clips referenced in the manifest; return {clip_id: local_path}."""
41
+ clips_dir.mkdir(parents=True, exist_ok=True)
42
+ result: dict[str, Path] = {}
43
+ for clip in manifest["clips"]:
44
+ clip_id = clip["id"]
45
+ fname = clip["url"].rsplit("/", 1)[-1]
46
+ dst = clips_dir / fname
47
+ if not dst.exists():
48
+ _download(clip["url"], dst)
49
+ actual = _sha256_file(dst)
50
+ if actual != clip["sha256"]:
51
+ dst.unlink()
52
+ raise ValueError(
53
+ f"Clip {clip_id} hash mismatch: expected {clip['sha256']}, got {actual}"
54
+ )
55
+ result[clip_id] = dst
56
+ return result
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ import statistics
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Any, Protocol
7
+
8
+
9
+ class _TranscribeResult(Protocol):
10
+ text: str
11
+ audio_duration_s: float
12
+ processing_time_s: float
13
+
14
+
15
+ class _SttEngine(Protocol):
16
+ def transcribe(self, audio_path: Path, model: str) -> _TranscribeResult: ...
17
+
18
+
19
+ @dataclass
20
+ class _RunStats:
21
+ total_audio_s: float
22
+ total_processing_s: float
23
+ rtf: float
24
+ wer: float
25
+
26
+
27
+ def _word_edit_distance(ref: list[str], hyp: list[str]) -> int:
28
+ m, n = len(ref), len(hyp)
29
+ dp = list(range(n + 1))
30
+ for i in range(1, m + 1):
31
+ prev = dp[:]
32
+ dp[0] = i
33
+ for j in range(1, n + 1):
34
+ if ref[i - 1] == hyp[j - 1]:
35
+ dp[j] = prev[j - 1]
36
+ else:
37
+ dp[j] = 1 + min(prev[j], dp[j - 1], prev[j - 1])
38
+ return dp[n]
39
+
40
+
41
+ def corpus_wer(references: list[str], hypotheses: list[str]) -> float:
42
+ """Word Error Rate over a corpus (lower is better)."""
43
+ total_errors = 0
44
+ total_words = 0
45
+ for ref, hyp in zip(references, hypotheses, strict=False):
46
+ r = ref.upper().split()
47
+ h = hyp.upper().split()
48
+ total_errors += _word_edit_distance(r, h)
49
+ total_words += len(r)
50
+ return total_errors / max(total_words, 1)
51
+
52
+
53
+ def _aggregate_run(
54
+ results: list[_TranscribeResult],
55
+ references: list[str],
56
+ ) -> _RunStats:
57
+ total_audio = sum(r.audio_duration_s for r in results)
58
+ total_proc = sum(r.processing_time_s for r in results)
59
+ rtf = total_proc / total_audio if total_audio > 0 else 0.0
60
+ wer = corpus_wer(references, [r.text for r in results])
61
+ return _RunStats(
62
+ total_audio_s=total_audio,
63
+ total_processing_s=total_proc,
64
+ rtf=rtf,
65
+ wer=wer,
66
+ )
67
+
68
+
69
+ def measure(
70
+ engine: _SttEngine,
71
+ model: str,
72
+ clips: list[dict[str, Any]],
73
+ clip_paths: dict[str, Path],
74
+ runs: int,
75
+ warmup_runs: int,
76
+ ) -> tuple[dict[str, Any], dict[str, Any], bool]:
77
+ """Run the STT benchmark loop; return (median, std_dev, throttling_detected)."""
78
+ references = [c["reference"] for c in clips]
79
+ all_stats: list[_RunStats] = []
80
+
81
+ for i in range(warmup_runs + runs):
82
+ run_results: list[_TranscribeResult] = []
83
+ for clip in clips:
84
+ path = clip_paths[clip["id"]]
85
+ result = engine.transcribe(path, model)
86
+ run_results.append(result)
87
+
88
+ if i >= warmup_runs:
89
+ all_stats.append(_aggregate_run(run_results, references))
90
+
91
+ rtf_values = [s.rtf for s in all_stats]
92
+ wer_values = [s.wer for s in all_stats]
93
+
94
+ median_rtf = statistics.median(rtf_values) if rtf_values else 0.0
95
+ median_wer = statistics.median(wer_values) if wer_values else 0.0
96
+ std_rtf = statistics.stdev(rtf_values) if len(rtf_values) > 1 else 0.0
97
+ std_wer = statistics.stdev(wer_values) if len(wer_values) > 1 else 0.0
98
+
99
+ cv = std_rtf / median_rtf if median_rtf > 0 else 0.0
100
+ throttling_detected = cv > 0.15
101
+
102
+ median_stats = {
103
+ "rtf": median_rtf,
104
+ "wer": median_wer,
105
+ "total_audio_seconds": all_stats[0].total_audio_s if all_stats else 0.0,
106
+ }
107
+ std_dev_stats = {"rtf": std_rtf, "wer": std_wer}
108
+
109
+ return median_stats, std_dev_stats, throttling_detected
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from mate_bench.plugin import ProfileConfig, TestSetSpec
4
+
5
+ # Public R2 CDN
6
+ CDN_BASE = "https://pub-f27eb09940c14a8dac6ae7fe10e789f3.r2.dev"
7
+
8
+ # SHA256 values are filled in by scripts/prepare_stt_testset.py after upload.
9
+ TEST_SETS: dict[str, TestSetSpec] = {
10
+ "stt-librispeech-quick-v1": TestSetSpec(
11
+ id="stt-librispeech-quick-v1",
12
+ url=f"{CDN_BASE}/stt/stt-librispeech-quick-v1.json",
13
+ sha256="sha256:420a49113d77e4e72a1740eeb9fff9b1918def6821c994b1d2c7e59990b8adc3",
14
+ size_bytes=754300,
15
+ license="CC BY 4.0",
16
+ source="LibriSpeech test-clean (Panayotov et al., 2015) — openslr.org/12",
17
+ ),
18
+ "stt-librispeech-standard-v1": TestSetSpec(
19
+ id="stt-librispeech-standard-v1",
20
+ url=f"{CDN_BASE}/stt/stt-librispeech-standard-v1.json",
21
+ sha256="sha256:66a200da99544095c68b90fff5287ed4460b61c854ef29a29524373973a2915b",
22
+ size_bytes=3184083,
23
+ license="CC BY 4.0",
24
+ source="LibriSpeech test-clean (Panayotov et al., 2015) — openslr.org/12",
25
+ ),
26
+ }
27
+
28
+ PROFILES: dict[str, ProfileConfig] = {
29
+ "quick": ProfileConfig(
30
+ name="quick",
31
+ description="5 LibriSpeech clips (~50 s audio) with whisper-large-v3 (~3 min, ~3 GB VRAM)",
32
+ test_set_id="stt-librispeech-quick-v1",
33
+ reference_engine_config={"engine": "faster-whisper", "model": "large-v3"},
34
+ vram_required_gb=3.0,
35
+ download_size_bytes=1_500_000_000, # large-v3 ~1.5 GB
36
+ estimated_runtime_seconds=180,
37
+ ),
38
+ "standard": ProfileConfig(
39
+ name="standard",
40
+ description="20 LibriSpeech clips (~200 s audio) with whisper-large-v3 (~8 min, ~3 GB VRAM)",
41
+ test_set_id="stt-librispeech-standard-v1",
42
+ reference_engine_config={"engine": "faster-whisper", "model": "large-v3"},
43
+ vram_required_gb=3.0,
44
+ download_size_bytes=1_500_000_000,
45
+ estimated_runtime_seconds=480,
46
+ ),
47
+ }
File without changes
@@ -0,0 +1,23 @@
1
+ from pathlib import Path
2
+
3
+ MOCK_CLIPS = [
4
+ {
5
+ "id": "1089-134686-0000",
6
+ "url": "https://example.com/stt/clips/1089-134686-0000.flac",
7
+ "sha256": "sha256:aabbcc",
8
+ "duration_s": 11.4,
9
+ "reference": "HE HOPED THERE WOULD BE STEW FOR DINNER TURNIPS AND CARROTS",
10
+ },
11
+ {
12
+ "id": "1221-135766-0001",
13
+ "url": "https://example.com/stt/clips/1221-135766-0001.flac",
14
+ "sha256": "sha256:ddeeff",
15
+ "duration_s": 6.3,
16
+ "reference": "AFTER EARLY NIGHTFALL THE YELLOW LAMPS WOULD LIGHT UP",
17
+ },
18
+ ]
19
+
20
+ MOCK_CLIP_PATHS: dict[str, Path] = {
21
+ "1089-134686-0000": Path("/tmp/1089-134686-0000.flac"),
22
+ "1221-135766-0001": Path("/tmp/1221-135766-0001.flac"),
23
+ }
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import MagicMock
4
+
5
+ import pytest
6
+
7
+ from mate_workload_stt._measure import corpus_wer, measure
8
+
9
+ from .fixtures import MOCK_CLIP_PATHS, MOCK_CLIPS
10
+
11
+ # ── corpus_wer ────────────────────────────────────────────────────────────────
12
+
13
+
14
+ class TestCorpusWer:
15
+ def test_perfect_match(self):
16
+ assert corpus_wer(["HELLO WORLD"], ["HELLO WORLD"]) == 0.0
17
+
18
+ def test_one_substitution_of_two(self):
19
+ result = corpus_wer(["HELLO WORLD"], ["HELLO EARTH"])
20
+ assert result == pytest.approx(0.5)
21
+
22
+ def test_full_deletion(self):
23
+ result = corpus_wer(["ONE TWO THREE"], [""])
24
+ assert result == pytest.approx(1.0)
25
+
26
+ def test_empty_reference(self):
27
+ assert corpus_wer([""], [""]) == 0.0
28
+
29
+ def test_case_insensitive(self):
30
+ assert corpus_wer(["hello world"], ["HELLO WORLD"]) == 0.0
31
+
32
+ def test_multi_clip_corpus(self):
33
+ refs = ["ONE TWO", "THREE FOUR"]
34
+ hyps = ["ONE TWO", "THREE FIVE"]
35
+ # 0 errors + 1 error over 4 total words = 0.25
36
+ assert corpus_wer(refs, hyps) == pytest.approx(0.25)
37
+
38
+
39
+ # ── measure ───────────────────────────────────────────────────────────────────
40
+
41
+
42
+ def _make_engine(processing_time: float, text: str) -> MagicMock:
43
+ result = MagicMock()
44
+ result.text = text
45
+ result.audio_duration_s = 10.0
46
+ result.processing_time_s = processing_time
47
+ engine = MagicMock()
48
+ engine.transcribe.return_value = result
49
+ return engine
50
+
51
+
52
+ class TestMeasure:
53
+ def test_rtf_is_correct(self):
54
+ engine = _make_engine(
55
+ processing_time=1.0, text="HE HOPED THERE WOULD BE STEW FOR DINNER TURNIPS AND CARROTS"
56
+ )
57
+ median, _std_dev, _throttling = measure(
58
+ engine, "large-v3", MOCK_CLIPS, MOCK_CLIP_PATHS, runs=1, warmup_runs=0
59
+ )
60
+ # processing_time=1.0, audio_duration=10.0 per clip → RTF = 0.1
61
+ assert median["rtf"] == pytest.approx(0.1)
62
+
63
+ def test_warmup_excluded(self):
64
+ call_count = 0
65
+ engine = MagicMock()
66
+
67
+ def side_effect(path, model):
68
+ nonlocal call_count
69
+ call_count += 1
70
+ r = MagicMock()
71
+ r.text = "HE HOPED THERE WOULD BE STEW FOR DINNER TURNIPS AND CARROTS"
72
+ r.audio_duration_s = 10.0
73
+ r.processing_time_s = 1.0
74
+ return r
75
+
76
+ engine.transcribe.side_effect = side_effect
77
+ measure(engine, "large-v3", MOCK_CLIPS, MOCK_CLIP_PATHS, runs=1, warmup_runs=2)
78
+ # 2 warmup + 1 run, each with 2 clips → 6 total calls
79
+ assert call_count == 6
80
+
81
+ def test_no_throttling_stable_run(self):
82
+ engine = _make_engine(1.0, "HE HOPED THERE WOULD BE STEW FOR DINNER TURNIPS AND CARROTS")
83
+ _, _, throttling = measure(
84
+ engine, "large-v3", MOCK_CLIPS, MOCK_CLIP_PATHS, runs=3, warmup_runs=0
85
+ )
86
+ assert throttling is False