Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
170 commits
Select commit Hold shift + click to select a range
b6d85cf
whisper: read encoder dims, alignment heads and mel params from the c…
merceod Sep 18, 2026
daeb373
whisper: add a native batched audio encoder
merceod Sep 18, 2026
58f651a
whisper: add timestamp decoding rules as a staged per-row state
merceod Sep 18, 2026
992fdb3
whisper: capture the encoder per batch size, detect language, apply t…
merceod Sep 18, 2026
9d897dc
whisper: native encoder, language detection walks, initial_prompt and…
merceod Sep 18, 2026
237acc1
qwen3_asr: add the model package
merceod Sep 18, 2026
75090c1
qwen3_asr: read the AuT and Qwen3 configs from the checkpoint
merceod Sep 18, 2026
25c6459
qwen3_asr: packed AuT encoder and Qwen3 decoder submodules
merceod Sep 18, 2026
aa23710
qwen3_asr: add the Qwen3-ASR model graph and prompt
merceod Sep 18, 2026
881f05b
registry: add whisper_large_v3_turbo, qwen3_asr and qwen3_asr_realtime
merceod Sep 18, 2026
a854e22
cli: default configs and next steps for the ASR models
merceod Sep 18, 2026
96bcc22
configs: add whisper_large_v3_turbo
merceod Sep 18, 2026
4ecff30
configs: add qwen3_asr
merceod Sep 18, 2026
891929d
configs: add qwen3_asr_realtime
merceod Sep 18, 2026
f9927d7
benchmark: register the new ASR model types
merceod Sep 18, 2026
94847e8
benchmark: accept the new ASR model keys in asr_eval
merceod Sep 18, 2026
b0eb897
benchmark: build the shared LibriSpeech ASR set from openslr
merceod Sep 18, 2026
187e09b
benchmark: protocol ASR benchmark over /v1/audio/transcriptions
merceod Sep 18, 2026
a68d071
benchmark: streaming partial-latency benchmark over /v1/realtime
merceod Sep 18, 2026
e0f5d26
benchmark: reference-implementation transcripts for WER parity
merceod Sep 18, 2026
cfeb68d
docs: Whisper turbo/v3 and Qwen3-ASR rows in the model table
merceod Sep 18, 2026
4d5e1c8
tests: Whisper config, encoder parity, state machine and wiring
merceod Sep 18, 2026
498b48b
tests: pin Whisper timestamp rules to HF's logits processor
merceod Sep 18, 2026
3875c39
tests: Qwen3-ASR prompt, submodules, state machine and wiring
merceod Sep 18, 2026
f58fe38
tests: GPU parity for Whisper on real weights
merceod Sep 18, 2026
a2af50a
tests: GPU parity for Qwen3-ASR on real weights
merceod Sep 18, 2026
e0468ce
api: add the Qwen3-ASR transcription adapter
merceod Sep 18, 2026
412ddd5
tests: cover the Qwen3-ASR OpenAI adapter
merceod Sep 18, 2026
08041b9
whisper: carry the timestamp rule state as a tensor and advance it in…
merceod Sep 18, 2026
779cb29
whisper: route the rule state with the token instead of a host history
merceod Sep 18, 2026
5557aef
whisper: loop the timestamp rule state back through the decode walk
merceod Sep 18, 2026
ac6b3f4
tests: advance reproduces rule_state token by token
merceod Sep 18, 2026
ea97347
tests: routed timestamp state in the decoder and the state machine
merceod Sep 18, 2026
3cf1870
api: report Qwen3-ASR's detected language as an ISO code
merceod Sep 18, 2026
4993fac
tests: Qwen3-ASR adapter maps language names to codes
merceod Sep 18, 2026
e626090
benchmark: realtime bench speaks vLLM's WebSocket dialect and reports…
merceod Sep 18, 2026
b641019
whisper: opt the submodules out of the engine's torch.compile
merceod Sep 18, 2026
9061a35
qwen3_asr: opt the submodules out of the engine's torch.compile
merceod Sep 18, 2026
5182d79
benchmark: realtime bench tolerates close frames
merceod Sep 18, 2026
e162500
whisper: send the encoder samples and make the log-mel the encoder's …
merceod Sep 18, 2026
3d5b6b3
whisper: encoder submodule pads the window and makes the log-mel batc…
merceod Sep 18, 2026
fdcf954
qwen3_asr: send the encoder samples, the prompt only needs the frame …
merceod Sep 18, 2026
2a4d966
qwen3_asr: encoder submodule makes the log-mel on the GPU
merceod Sep 18, 2026
20a6dd1
tests: Whisper encoder takes samples and the tiny config uses a 1 s w…
merceod Sep 18, 2026
4ec2325
tests: Qwen3-ASR encoder takes samples
merceod Sep 18, 2026
c447815
tests: Whisper GPU parity checks the submodule's log-mel against the …
merceod Sep 18, 2026
196b06d
benchmark: vLLM realtime final text is the joined deltas
merceod Sep 18, 2026
acb7a8a
whisper: word timestamps from the alignment heads (median filter, DTW…
merceod Sep 18, 2026
1485a3c
whisper: name the align walk
merceod Sep 18, 2026
2980300
whisper: teacher-forced cross-attention pass on the served decoder we…
merceod Sep 18, 2026
efb87f5
whisper: align walk in the decoder and a float32 log-mel under the en…
merceod Sep 18, 2026
7a543ae
qwen3_asr: log-mel stays float32 under the engine's cast
merceod Sep 18, 2026
7024eb2
whisper: run the align walk after decoding when word timestamps are r…
merceod Sep 18, 2026
855e5d5
tests: word-timestamp alignment against HF attentions, DTW and word g…
merceod Sep 18, 2026
6499b8d
tests: align walk through the state machine and the decoder submodule
merceod Sep 18, 2026
e205911
tests: Whisper GPU parity checks word starts against HF token timestamps
merceod Sep 18, 2026
484c06c
docs: speech recognition page (Whisper and Qwen3-ASR scaffold, walks,…
merceod Sep 18, 2026
ddffe45
docs: link the speech recognition page
merceod Sep 18, 2026
89361ea
whisper: select the prefill's last rows from the row lengths (a one-t…
merceod Sep 18, 2026
8dd6774
tests: decoder prefill selects each request's last row itself
merceod Sep 18, 2026
0d68a6c
benchmark: realtime bench keeps the final transcripts
merceod Sep 18, 2026
41bff96
tests: word-timestamp parity reads HF's output as a mapping
merceod Sep 18, 2026
5fe93e7
tests: word-timestamp parity compares starts with HF's previous-token…
merceod Sep 18, 2026
1e9b11e
docs: serve the ASR API process with one OpenMP thread
merceod Sep 18, 2026
45f95f6
docs: serve the ASR API process with a passive OpenMP wait policy
merceod Sep 18, 2026
e91bf19
benchmark: keep the transcript text from vLLM's realtime chunk lines
merceod Sep 18, 2026
a67b1ba
whisper: deliver the decode loop's tokens in one message at the end
merceod Sep 21, 2026
5f7d98d
tests: the Whisper decode loop accumulates its tokens for the client
merceod Sep 21, 2026
9c052bb
qwen3_asr: deliver the decode loop's tokens in one message at the end
merceod Sep 21, 2026
22c9592
docs: ASR tokens reach the client once per window
merceod Sep 21, 2026
6774586
benchmark: reword a data split comment
merceod Sep 28, 2026
2ef39cc
docs: plainer wording on the ASR page
merceod Sep 28, 2026
46b3fcd
docs: plainer wording in the ASR model entries
merceod Sep 28, 2026
7f649ab
qwen3_asr: reword config comments
merceod Sep 28, 2026
5fb27e2
qwen3_asr: reword comments and the audio count error
merceod Sep 28, 2026
09aaf53
qwen3_asr: reword submodule comments
merceod Sep 28, 2026
b412469
registry: reword the ASR entries
merceod Sep 28, 2026
04f9820
whisper: reword encoder comments
merceod Sep 28, 2026
4b0c38a
whisper: reword a timestamp rule comment
merceod Sep 28, 2026
6e1635b
whisper: reword the walk comments
merceod Sep 28, 2026
ee97a19
whisper: reword submodule comments
merceod Sep 28, 2026
02881ac
whisper: reword model comments and the audio count error
merceod Sep 28, 2026
99e8a73
tests: reword parity test comments
merceod Sep 28, 2026
fd7332a
tests: reword timestamp test docstrings
merceod Sep 28, 2026
1a3002b
tests: reword Whisper test comments
merceod Sep 28, 2026
322af1e
tests: match the new audio count message
merceod Sep 28, 2026
be76e1c
whisper: use PagedKVConfig after the engine rename
merceod Oct 6, 2026
7997dd7
qwen3_asr: use PagedKVConfig after the engine rename
merceod Oct 6, 2026
01a6ab5
components: check the ASR request kwargs once
merceod Oct 6, 2026
8b74e19
whisper: prompt length per walk, state keyed by the handle, greedy de…
merceod Oct 6, 2026
66a8c0e
whisper: check request kwargs and the concurrency cap in the config
merceod Oct 6, 2026
4543462
qwen3_asr: check request kwargs and the concurrency cap in the config
merceod Oct 6, 2026
3182aa2
api: Qwen3-ASR adapter streams through stream_text
merceod Oct 6, 2026
578ac16
cli: keep kokoro in the OpenAI example list
merceod Oct 6, 2026
bab4291
configs: cap whisper_large_v3_turbo at 64 concurrent requests
merceod Oct 6, 2026
2626cb0
configs: cap whisper_large at 64 concurrent requests
merceod Oct 6, 2026
1adcbf5
configs: cap whisper_large_tp2 at 64 concurrent requests
merceod Oct 6, 2026
0f5f951
configs: cap qwen3_asr at 64 requests and give it 2048 KV pages
merceod Oct 6, 2026
ae09247
configs: cap qwen3_asr_realtime at 64 concurrent requests
merceod Oct 6, 2026
ba8ead3
docs: the concurrency caps on the ASR page
merceod Oct 6, 2026
86956d9
tests: Whisper state by handle, prompt length per walk, greedy detect…
merceod Oct 6, 2026
fd98c3f
tests: Qwen3-ASR stream_text, cap and kwargs checks
merceod Oct 6, 2026
0d70c25
tests: the request kwargs checker
merceod Oct 6, 2026
ed80181
registry: keep main's waypoint and Qwen3-TTS entries beside the ASR ones
merceod Oct 6, 2026
58d632a
move language and task computation into the process_prompt
NSagan271 Oct 6, 2026
7536f93
engine: pre_plan as a serve-config key, off for whisper turbo
NSagan271 Oct 6, 2026
dc614a8
whisper: align with the detected language
vasilevklart Oct 8, 2026
3818a65
whisper: count the sink page in the cross cache
vasilevklart Oct 8, 2026
767a787
asr: bound seed to int64
vasilevklart Oct 8, 2026
ace45bb
qwen3_asr: no compression-ratio retries
vasilevklart Oct 8, 2026
66dc67b
api: stop long-form windows on disconnect
vasilevklart Oct 2, 2026
2cb0a7b
qwen3_asr: refuse <|audio_pad|> in the prompt
vasilevklart Oct 8, 2026
b4a7692
whisper: keep the align walk within 448 positions
vasilevklart Oct 8, 2026
974e5cb
docs: Qwen3-ASR windowing, ASR route list
vasilevklart Oct 8, 2026
b36f484
ci: run the ASR tests in CPU Core
vasilevklart Oct 8, 2026
8e16585
Merge main into model/asr
vasilevklart Oct 8, 2026
230ad2f
shm: don't send from reused host buffers
vasilevklart Oct 8, 2026
90ded70
shm: drop host copies after the step's sends
vasilevklart Oct 9, 2026
5ea6af2
Merge main into model/asr
vasilevklart Oct 9, 2026
84e927a
Merge origin/main into model/asr
dghosef Oct 17, 2026
b3e8248
Merge origin/main into model/asr
dghosef Oct 20, 2026
12ddf77
API server: wake non-streaming handlers on completion instead of a 5 …
dghosef Oct 17, 2026
a33d315
Conductor and API message thread: wake on a message instead of a fixe…
dghosef Oct 17, 2026
8a710f6
whisper: count the decode captures' padding pages in the startup chec…
dghosef Oct 20, 2026
82fe498
test: register the woken request under its submission key; stub commu…
dghosef Oct 20, 2026
5f21188
qwen3_asr: import order after the accelerator-graph rename
dghosef Oct 20, 2026
38ab1d2
qwen3_asr: collapse repetition loops in the transcript, as the refere…
dghosef Oct 20, 2026
c1cffc8
transcriptions: parallel long form decodes the tail a timed window le…
dghosef Oct 20, 2026
0e61337
test: the tick-exception stub communicator answers poll_for_messages …
dghosef Oct 20, 2026
a1c237e
ci: test/asr needs a GPU and real weights; not part of CPU Core
dghosef Oct 20, 2026
f4ba498
transcriptions: no tail pass once the client is gone; streamed parall…
dghosef Oct 20, 2026
601e127
whisper: back to main's decoder-cache sizing; padding rows hold no pages
dghosef Oct 20, 2026
2f0778a
qwen3_asr: collapse repetition loops in the finished transcript only
dghosef Oct 20, 2026
cdbb473
API server, conductor: guard the waits between passes; a testable wak…
dghosef Oct 20, 2026
a886a0d
docs: Qwen3-ASR long-file capacity, Whisper's cap check, the pre_plan…
dghosef Oct 20, 2026
9bfb139
Merge origin/main into model/asr
dghosef Oct 20, 2026
13cb3f2
test: worker-metrics run-loop fake sets _enable_pre_plan, which this …
dghosef Oct 20, 2026
b112666
qwen3_asr: splice the audio embeddings in by slice, not by a device mask
dghosef Oct 21, 2026
5118369
request_kwargs: an integer field that overflows (inf, 1e400) is a Val…
dghosef Oct 21, 2026
0e0d62d
whisper: numba is part of the asr extra; warn without it, compile at …
dghosef Oct 21, 2026
0707ac6
asr benchmarks and parity tests: data under data/asr, not an undocume…
dghosef Oct 21, 2026
2cea7ca
worker: resolve_enable_pre_plan, tested directly; a non-boolean pre_p…
dghosef Oct 21, 2026
d08a300
qwen3_asr: ASR_TEXT_TAG defined once, in a module the API server impo…
dghosef Oct 21, 2026
5c62b05
whisper: word timestamps on a TP-sharded decoder are a 400 at ingest
dghosef Oct 21, 2026
7d26005
transcriptions: submit a parallel window's tail as soon as that windo…
dghosef Oct 21, 2026
6a62e79
qwen3_asr: the per-request window cap follows max_audio_seconds inste…
dghosef Oct 21, 2026
1face27
docs: Whisper delivers a window's transcript at loop end; qwen3_asr_r…
dghosef Oct 21, 2026
2c077f7
Merge main into model/asr
vasilevklart Oct 24, 2026
246d285
whisper, qwen3_asr: decode lengths on main's per-request loop caps
dghosef Oct 24, 2026
8ee690b
whisper: split zh/ja/th/lo/my/yue words per character, as openai-whisper
dghosef Oct 24, 2026
cf65a75
qwen3_asr: decode tokens go to the client inline, not accumulated to …
dghosef Oct 24, 2026
5c1dbb4
docs/asr: serve ASR on the shared-memory arena
dghosef Oct 24, 2026
f78e7c0
docs/asr: drop the unverified thread-pool hangs claim
dghosef Oct 24, 2026
580bfcd
whisper, qwen3_asr: cap the API server's torch threads at 4 (PREPROCE…
dghosef Oct 24, 2026
2fc4745
docs/asr: Qwen3-ASR long-file capacity from a measured run
dghosef Oct 24, 2026
383eed7
api server, conductor: drop the early wake between message-loop passes
dghosef Oct 24, 2026
48b8e69
whisper: merge punctuation as openai-whisper's merge_punctuations
dghosef Oct 24, 2026
0292f1f
test/asr: compare the served model's greedy text to the reference
dghosef Oct 24, 2026
c2d1e69
whisper: stage prefill's rule state and row lengths through a pinned …
dghosef Oct 24, 2026
4b6cae8
qwen3_asr: check_stop_batched takes the engine's step_walk
dghosef Oct 24, 2026
dc5b770
whisper, qwen3_asr: fixes from the clean review of this round
dghosef Oct 24, 2026
5b95432
whisper tests, docs/asr: fixes from the GPU run; drop the OMP_WAIT_PO…
dghosef Oct 24, 2026
0a479b7
qwen3_asr: cap the shipped config at 12 concurrent requests, and docu…
dghosef Oct 24, 2026
b2b793c
test/asr: run the Qwen3-ASR SDK reference in a subprocess with its ow…
dghosef Oct 26, 2026
509382b
Merge origin/main into model/asr
dghosef Oct 27, 2026
2b74310
Read pre_plan through the knobs: block (R1.1)
dghosef Oct 27, 2026
9acb78e
Qwen3-ASR's LLM node is a TokenStepNode (R1.2)
dghosef Oct 27, 2026
d5d92f8
One serving.rst knobs row, listing pre_plan (R2.1); base-default word…
dghosef Oct 27, 2026
7b11999
Parallel transcription tail cut off the event loop (P3)
dghosef Oct 27, 2026
e3c824b
Parallel transcription tail: only the cut leaves the event loop (R3.1)
dghosef Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
305 changes: 305 additions & 0 deletions benchmark/asr_bench.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
"""ASR benchmark per the workspace protocol: RTFx, latency and WER vs. any
OpenAI-compatible transcription server.

One client measures every engine (mstar, vLLM, TensorRT-LLM, SGLang-Omni,
faster-whisper) through ``POST /v1/audio/transcriptions`` on the same files,
built once by ``benchmark/asr_data.py``. For each concurrency level the whole
set is sent closed-loop (at most ``c`` requests in flight), ``--repeats``
times after a warmup pass, and the run reports:

RTFx audio seconds transcribed per wall-clock second (median over repeats)
latency per-request end-to-end p50 / p95 (ms) of the median run
WER on the transcripts of the last run, whisper-normalized
long-form wall time and RTFx of the ~10 min file at concurrency 1

Results go to ``--output-json`` with the engine label, git SHA and GPU
clocks, and a markdown row for the PR table is printed.

python -m benchmark.asr_bench --url http://localhost:8000 --model whisper_large_v3_turbo \\
--system "M* <sha>" --data-dir data/asr \\
--concurrency 1 8 32 --long-form --output-json results/asr_mstar.json
"""

from __future__ import annotations

import argparse
import asyncio
import json
import os
import statistics
import subprocess
import sys
import time
import wave
from dataclasses import dataclass, field
from pathlib import Path

import aiohttp


@dataclass
class Utterance:
uid: str
path: Path
duration: float
reference: str
audio: bytes = field(default_factory=bytes, repr=False)


def load_set(directory: Path) -> list[Utterance]:
refs = {}
for line in (directory / "references.tsv").read_text().splitlines():
if line.strip():
uid, text = line.split("\t", 1)
refs[uid] = text
items: list[Utterance] = []
for uid, text in refs.items():
path = directory / f"{uid}.wav"
with wave.open(str(path), "rb") as wf:
duration = wf.getnframes() / wf.getframerate()
items.append(Utterance(uid, path, duration, text, path.read_bytes()))
return items


# --------------------------------------------------------------------------
# client
# --------------------------------------------------------------------------


async def transcribe(
session: aiohttp.ClientSession, base_url: str, model: str, utt: Utterance,
language: str | None, extra: dict[str, str],
) -> tuple[str | None, float, str | None]:
"""One ``/v1/audio/transcriptions`` call: (text, seconds, error)."""
form = aiohttp.FormData()
form.add_field("model", model)
form.add_field("response_format", "json")
form.add_field("temperature", "0")
if language:
form.add_field("language", language)
for key, value in extra.items():
form.add_field(key, value)
form.add_field("file", utt.audio, filename=utt.path.name, content_type="audio/wav")
t0 = time.perf_counter()
try:
async with session.post(f"{base_url}/v1/audio/transcriptions", data=form) as resp:
body = await resp.read()
if resp.status != 200:
return None, time.perf_counter() - t0, f"HTTP {resp.status}: {body[:200]!r}"
text = json.loads(body).get("text", "")
return text, time.perf_counter() - t0, None
except Exception as exc: # noqa: BLE001 — recorded per request
return None, time.perf_counter() - t0, repr(exc)


async def run_set(
base_url: str, model: str, items: list[Utterance], concurrency: int,
language: str | None, extra: dict[str, str], timeout_s: float,
) -> dict:
sem = asyncio.Semaphore(concurrency)
results: dict[str, tuple[str | None, float, str | None]] = {}

async def one(session, utt):
async with sem:
results[utt.uid] = await transcribe(session, base_url, model, utt, language, extra)

connector = aiohttp.TCPConnector(limit=max(64, concurrency + 8))
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=timeout_s), connector=connector,
) as session:
t0 = time.perf_counter()
await asyncio.gather(*(one(session, utt) for utt in items))
wall = time.perf_counter() - t0

latencies = sorted(r[1] for r in results.values())
errors = {uid: r[2] for uid, r in results.items() if r[2]}
audio_seconds = sum(u.duration for u in items if u.uid not in errors)
return {
"wall_s": wall,
"audio_s": audio_seconds,
"rtfx": audio_seconds / wall if wall > 0 else 0.0,
"p50_ms": 1000 * statistics.median(latencies) if latencies else None,
"p95_ms": 1000 * _percentile(latencies, 0.95) if latencies else None,
"errors": errors,
"hypotheses": {uid: (r[0] or "") for uid, r in results.items()},
}


def _percentile(sorted_values: list[float], q: float) -> float:
if not sorted_values:
return float("nan")
idx = min(len(sorted_values) - 1, max(0, round(q * (len(sorted_values) - 1))))
return sorted_values[idx]


# --------------------------------------------------------------------------
# WER
# --------------------------------------------------------------------------


def make_normalizer(kind: str):
"""``whisper``: the EnglishTextNormalizer every Whisper WER in the
literature uses (numbers, spellings, punctuation); ``basic``: uppercase,
strip punctuation (``benchmark/asr_eval.py``'s choice)."""
if kind == "whisper":
try:
from transformers.models.whisper.english_normalizer import EnglishTextNormalizer

spelling = {}
try:
from huggingface_hub import hf_hub_download

path = hf_hub_download("openai/whisper-large-v3-turbo", "normalizer.json", local_files_only=True)
spelling = json.loads(Path(path).read_text())
except Exception: # noqa: BLE001 — spelling map is optional
pass
return EnglishTextNormalizer(spelling)
except ImportError:
print("transformers unavailable; falling back to the basic normalizer", file=sys.stderr)
import re

def basic(s: str) -> str:
return re.sub(r"\s+", " ", re.sub(r"[^\w\s']", " ", s.upper())).strip()

return basic


def word_error_rate(references: list[str], hypotheses: list[str], normalize) -> float:
import jiwer

refs = [normalize(r) for r in references]
hyps = [normalize(h) for h in hypotheses]
pairs = [(r, h) for r, h in zip(refs, hyps, strict=True) if r]
return jiwer.wer([r for r, _ in pairs], [h for _, h in pairs])


# --------------------------------------------------------------------------
# environment record
# --------------------------------------------------------------------------


def _sh(cmd: list[str]) -> str | None:
try:
return subprocess.run(cmd, capture_output=True, text=True, timeout=20, check=False).stdout.strip() or None
except Exception: # noqa: BLE001
return None


def environment_record() -> dict:
return {
"git_sha": _sh(["git", "rev-parse", "--short", "HEAD"]),
"hostname": _sh(["hostname"]),
"gpu": _sh(["nvidia-smi", "--query-gpu=name,driver_version,clocks.max.sm,clocks.sm,memory.total",
"--format=csv,noheader"]),
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
}


# --------------------------------------------------------------------------
# main
# --------------------------------------------------------------------------


def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="ASR protocol benchmark over /v1/audio/transcriptions")
p.add_argument("--url", required=True)
p.add_argument("--model", required=True, help="model id sent in the request")
p.add_argument("--system", required=True, help='engine label for the table, e.g. "vLLM 0.29.0"')
p.add_argument("--data-dir", default="data/asr", help="the set benchmark.asr_data built")
p.add_argument("--short-set", default="test_clean_200")
p.add_argument("--concurrency", type=int, nargs="+", default=[1, 8, 32])
p.add_argument("--repeats", type=int, default=3)
p.add_argument("--warmup", type=int, default=8, help="requests sent (and discarded) before timing")
p.add_argument("--language", default="en", help="ISO-639-1 code; '' to let the engine detect")
p.add_argument("--extra", action="append", default=[], help="extra form field key=value (repeatable)")
p.add_argument("--long-form", action="store_true", help="also time the long_form file at concurrency 1")
p.add_argument("--normalizer", choices=["whisper", "basic"], default="whisper")
p.add_argument("--timeout", type=float, default=600.0)
p.add_argument("--output-json", default=None)
return p.parse_args()


async def main() -> None:
args = parse_args()
data_dir = Path(args.data_dir)
items = load_set(data_dir / args.short_set)
language = args.language or None
extra = dict(kv.split("=", 1) for kv in args.extra)
normalize = make_normalizer(args.normalizer)
record: dict = {
"system": args.system, "model": args.model, "url": args.url, "data_dir": str(data_dir),
"num_utterances": len(items), "audio_seconds": sum(u.duration for u in items),
"language": language, "normalizer": args.normalizer, "env": environment_record(),
"concurrency": {},
}

if args.warmup:
warm = (items * ((args.warmup // len(items)) + 1))[:args.warmup]
await run_set(args.url, args.model, warm, min(8, max(args.concurrency)), language, extra, args.timeout)

for c in args.concurrency:
runs = []
for _ in range(args.repeats):
runs.append(await run_set(args.url, args.model, items, c, language, extra, args.timeout))
rtfx = sorted(r["rtfx"] for r in runs)
median_run = runs[[r["rtfx"] for r in runs].index(rtfx[len(rtfx) // 2])]
last = runs[-1]
wer = word_error_rate(
[u.reference for u in items if u.uid not in last["errors"]],
[last["hypotheses"][u.uid] for u in items if u.uid not in last["errors"]],
normalize,
) if len(last["errors"]) < len(items) else None
record["concurrency"][str(c)] = {
"rtfx_median": rtfx[len(rtfx) // 2],
"rtfx_runs": rtfx,
"wall_s_median": median_run["wall_s"],
"p50_ms": median_run["p50_ms"],
"p95_ms": median_run["p95_ms"],
"wer": wer,
"errors": len(last["errors"]),
"error_samples": list(last["errors"].values())[:3],
}
print(f"[c={c:>3}] RTFx {rtfx[len(rtfx) // 2]:8.1f} p50 {median_run['p50_ms']:8.1f} ms "
f"p95 {median_run['p95_ms']:8.1f} ms WER {100 * wer if wer is not None else float('nan'):5.2f}% "
f"errors {len(last['errors'])}", flush=True)
if c == args.concurrency[-1]:
record["hypotheses"] = last["hypotheses"]

if args.long_form:
long_items = load_set(data_dir / "long_form")
runs = [await run_set(args.url, args.model, long_items, 1, language, extra, args.timeout)
for _ in range(args.repeats)]
walls = sorted(r["wall_s"] for r in runs)
last = runs[-1]
wer = word_error_rate([u.reference for u in long_items],
[last["hypotheses"][u.uid] for u in long_items], normalize) \
if not last["errors"] else None
record["long_form"] = {
"audio_s": last["audio_s"], "wall_s_median": walls[len(walls) // 2],
"rtfx_median": last["audio_s"] / walls[len(walls) // 2] if walls[len(walls) // 2] else None,
"wer": wer, "errors": list(last["errors"].values())[:3],
"hypothesis": last["hypotheses"],
}
print(f"[long-form] {last['audio_s']:.0f} s audio in {walls[len(walls) // 2]:.2f} s "
f"(RTFx {record['long_form']['rtfx_median'] or float('nan'):.1f}) WER "
f"{100 * wer if wer is not None else float('nan'):.2f}%", flush=True)

cells = [args.system]
for c in args.concurrency:
r = record["concurrency"][str(c)]
cells.append(f"{r['rtfx_median']:.0f}")
cells.append(f"{100 * record['concurrency'][str(args.concurrency[0])]['wer']:.2f}%"
if record["concurrency"][str(args.concurrency[0])]["wer"] is not None else "n/a")
cells.append(f"{record['long_form']['rtfx_median']:.0f}" if record.get("long_form") and
record["long_form"]["rtfx_median"] else "n/a")
print("| " + " | ".join(cells) + " | <- system | RTFx@" +
" | RTFx@".join(map(str, args.concurrency)) + " | WER | long-form RTFx")

if args.output_json:
os.makedirs(os.path.dirname(os.path.abspath(args.output_json)), exist_ok=True)
Path(args.output_json).write_text(json.dumps(record, indent=2))
print(f"wrote {args.output_json}")


if __name__ == "__main__":
asyncio.run(main())
Loading
Loading