pdnhan's picture
Add files using upload-large-folder tool
5b2d29f verified
Raw History Blame Contribute Delete
5.13 kB
#!/usr/bin/env python3
"""Transcribes FLEURS Vietnamese test clips with the exported ONNX files and reports WER.
Runs the same loop a mobile app does: encoder once, then the merged decoder step by step with its
KV cache (use_cache_branch false on the first step, true after), greedy, prompt
<|startoftranscript|><|vi|><|transcribe|><|notimestamps|>. Writes out/<size>/wer.json (read by publish.py)
and fails when WER exceeds --max-wer.
Usage:
.venv/bin/python -I validate.py tiny [--count 30] [--max-wer 0.4]
"""
import argparse
import csv
import json
import re
import sys
import time
import unicodedata
from pathlib import Path
import jiwer
import numpy as np
import onnxruntime as ort
import soundfile as sf
from transformers import WhisperFeatureExtractor, WhisperTokenizer
HERE = Path(__file__).resolve().parent
FLEURS = HERE / "data"
MAX_TOKENS = 224
def clips(count: int) -> list[tuple[Path, str]]:
rows = []
with open(FLEURS / "fleurs/data/vi_vn/test.tsv", encoding="utf-8") as f:
for row in csv.reader(f, delimiter="\t", quoting=csv.QUOTE_NONE):
wav = next((FLEURS / "vi").glob(f"*/{row[1]}"), None)
if wav is not None:
rows.append((wav, row[2]))
if len(rows) == count:
break
return rows
def normalize(text: str) -> str:
text = unicodedata.normalize("NFC", text.lower())
return re.sub(r"\s+", " ", re.sub(r"[^\w\s]", " ", text)).strip()
class Transcriber:
def __init__(self, repo: Path, quantized: bool):
suffix = "_quantized" if quantized else ""
opts = ort.SessionOptions()
opts.intra_op_num_threads = 8
self.encoder = ort.InferenceSession(str(repo / f"onnx/encoder_model{suffix}.onnx"), opts)
self.decoder = ort.InferenceSession(str(repo / f"onnx/decoder_model_merged{suffix}.onnx"), opts)
self.features = WhisperFeatureExtractor.from_pretrained(repo)
self.tokenizer = WhisperTokenizer.from_pretrained(repo)
names = ["<|startoftranscript|>", "<|vi|>", "<|transcribe|>", "<|notimestamps|>"]
self.prompt = self.tokenizer.convert_tokens_to_ids(names)
self.eot = self.tokenizer.eos_token_id
self.past = [i for i in self.decoder.get_inputs() if i.name.startswith("past_key_values.")]
def __call__(self, audio: np.ndarray) -> str:
feats = self.features(audio, sampling_rate=16000, return_tensors="np").input_features
hidden = self.encoder.run(None, {"input_features": feats.astype(np.float32)})[0]
# Shape of an empty cache: [1, heads, 0, head_dim]; dims come from the graph's symbolic shapes.
cache = {i.name: np.zeros([1, *[d if isinstance(d, int) else 0 for d in i.shape[1:]]], np.float32)
for i in self.past}
for i in self.past:
if not isinstance(i.shape[1], int) or not isinstance(i.shape[3], int):
sys.exit(f"error: cannot size the empty cache for {i.name} {i.shape}")
tokens, ids = list(self.prompt), np.array([self.prompt], np.int64)
outputs = [o.name for o in self.decoder.get_outputs()]
for step in range(MAX_TOKENS):
feed = {"input_ids": ids, "encoder_hidden_states": hidden,
"use_cache_branch": np.array([step > 0]), **cache}
result = dict(zip(outputs, self.decoder.run(None, feed)))
nxt = int(result["logits"][0, -1].argmax())
if nxt == self.eot:
break
tokens.append(nxt)
for name in cache:
present = result[name.replace("past_key_values.", "present.")]
# The encoder (cross-attention) cache only comes out of the first step.
if step == 0 or ".decoder." in name:
cache[name] = present
ids = np.array([[nxt]], np.int64)
return self.tokenizer.decode(tokens, skip_special_tokens=True).strip()
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("size")
parser.add_argument("--count", type=int, default=30)
parser.add_argument("--max-wer", type=float, default=0.4)
args = parser.parse_args()
repo = HERE / "out" / args.size / "repo"
samples = clips(args.count)
failed, results = False, {"clips": len(samples)}
for quantized in (False, True):
run = Transcriber(repo, quantized)
hyps, refs, start = [], [], time.time()
for wav, ref in samples:
audio, rate = sf.read(wav, dtype="float32")
assert rate == 16000, wav
hyps.append(normalize(run(audio)))
refs.append(normalize(ref))
wer = jiwer.wer(refs, hyps)
label = "int8" if quantized else "fp32"
print(f"{args.size} {label}: WER {wer:.3f} on {len(samples)} clips in {time.time() - start:.0f} s")
print(f" ref: {refs[0]}\n hyp: {hyps[0]}")
results[label] = round(wer, 4)
failed |= wer > args.max_wer
(repo.parent / "wer.json").write_text(json.dumps(results) + "\n")
if failed:
sys.exit(f"error: WER above {args.max_wer}")
if __name__ == "__main__":
main()