Files
WhisperLiveKit/benchmarks/h100/generate_figures.py

271 lines
13 KiB
Python

#!/usr/bin/env python3
"""
Generate polished benchmark figures for WhisperLiveKit H100 results.
Reads data from results.json, outputs PNGs to this directory.
Run: python3 benchmarks/h100/generate_figures.py
"""
import json
import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
DIR = os.path.dirname(os.path.abspath(__file__))
DATA = json.load(open(os.path.join(DIR, "results.json")))
# ── Style constants ──
COLORS = {
"whisper": "#d63031",
"qwen_b": "#6c5ce7",
"qwen_s": "#00b894",
"voxtral": "#fdcb6e",
"fw_m5": "#74b9ff",
"mlx_m5": "#55efc4",
"vox_m5": "#ffeaa7",
}
plt.rcParams.update({
"font.family": "sans-serif",
"font.size": 11,
"axes.spines.top": False,
"axes.spines.right": False,
})
def _save(fig, name):
path = os.path.join(DIR, name)
fig.savefig(path, dpi=180, bbox_inches="tight", facecolor="white")
plt.close(fig)
print(f" {name}")
# ──────────────────────────────────────────────────────────
# Figure 1: WER vs RTF scatter — H100 (LibriSpeech clean)
# ──────────────────────────────────────────────────────────
def fig_scatter_clean():
ls = DATA["librispeech_clean"]["systems"]
m5 = DATA["m5_reference"]["systems"]
fig, ax = plt.subplots(figsize=(9, 7.5))
ax.axhspan(0, 10, color="#f0fff0", alpha=0.5, zorder=0)
# M5 (ghost dots)
for k, v in m5.items():
ax.scatter(v["rtf"], v["wer"], s=50, c="silver", marker="o",
alpha=0.22, zorder=2, linewidths=0.4, edgecolors="gray")
# H100 systems — (name, data, color, marker, size, label_x_off, label_y_off)
pts = [
("Whisper large-v3", ls["whisper_large_v3_batch"], COLORS["whisper"], "h", 240, -8, -16),
("Qwen3-ASR 0.6B (batch)", ls["qwen3_0.6b_batch"], COLORS["qwen_b"], "h", 170, 8, 6),
("Qwen3-ASR 1.7B (batch)", ls["qwen3_1.7b_batch"], COLORS["qwen_b"], "h", 240, 8, -16),
("Voxtral 4B (vLLM)", ls["voxtral_4b_vllm_realtime"], COLORS["voxtral"], "D", 260, 8, 6),
("Qwen3 0.6B SimulStream+KV", ls["qwen3_0.6b_simulstream_kv"], COLORS["qwen_s"], "s", 220, 8, 6),
("Qwen3 1.7B SimulStream+KV", ls["qwen3_1.7b_simulstream_kv"], COLORS["qwen_s"], "s", 280, 8, -16),
]
for name, d, color, marker, sz, lx, ly in pts:
ax.scatter(d["rtf"], d["wer"], s=sz, c=color, marker=marker,
edgecolors="white", linewidths=1.5, zorder=5)
ax.annotate(name, (d["rtf"], d["wer"]), fontsize=8.5, fontweight="bold",
xytext=(lx, ly), textcoords="offset points",
arrowprops=dict(arrowstyle="-", color="#aaa", lw=0.5))
ax.set_xlabel("RTF (lower = faster)")
ax.set_ylabel("WER % (lower = better)")
ax.set_title("Speed vs Accuracy — LibriSpeech test-clean (H100 80 GB)",
fontsize=13, fontweight="bold", pad=12)
ax.set_xlim(-0.005, 0.20)
ax.set_ylim(-0.3, 10)
ax.grid(True, alpha=0.12)
legend = [
mpatches.Patch(color=COLORS["whisper"], label="Whisper large-v3"),
mpatches.Patch(color=COLORS["qwen_b"], label="Qwen3-ASR (batch)"),
mpatches.Patch(color=COLORS["qwen_s"], label="Qwen3 SimulStream+KV"),
mpatches.Patch(color=COLORS["voxtral"], label="Voxtral 4B (vLLM)"),
plt.Line2D([0],[0], marker="h", color="w", mfc="gray", ms=8, label="Batch"),
plt.Line2D([0],[0], marker="s", color="w", mfc="gray", ms=8, label="Streaming"),
]
ax.legend(handles=legend, fontsize=8.5, loc="upper right", framealpha=0.85, ncol=2)
_save(fig, "wer_vs_rtf_clean.png")
# ──────────────────────────────────────────────────────────
# Figure 2: ACL6060 conference talks — the realistic test
# ──────────────────────────────────────────────────────────
def fig_scatter_acl6060():
acl = DATA["acl6060"]["systems"]
fig, ax = plt.subplots(figsize=(10, 6.5))
ax.axhspan(0, 15, color="#f0fff0", alpha=0.4, zorder=0)
pts = [
("Voxtral 4B\n(vLLM Realtime)", acl["voxtral_4b_vllm_realtime"], COLORS["voxtral"], "D", 380),
("Qwen3 1.7B\nSimulStream+KV", acl["qwen3_1.7b_simulstream_kv"], COLORS["qwen_s"], "s", 380),
("Qwen3 0.6B\nSimulStream+KV", acl["qwen3_0.6b_simulstream_kv"], COLORS["qwen_s"], "s", 260),
("Whisper large-v3\n(batch)", acl["whisper_large_v3_batch"], COLORS["whisper"], "h", 320),
]
label_off = [(10, -12), (10, 6), (10, 6), (10, 6)]
for (name, d, color, marker, sz), (lx, ly) in zip(pts, label_off):
wer = d["avg_wer"]; rtf = d["avg_rtf"]
ax.scatter(rtf, wer, s=sz, c=color, marker=marker,
edgecolors="white", linewidths=1.5, zorder=5)
ax.annotate(name, (rtf, wer), fontsize=9.5, fontweight="bold",
xytext=(lx, ly), textcoords="offset points",
arrowprops=dict(arrowstyle="-", color="#aaa", lw=0.6))
# Cascade annotation
ax.annotate("Full STT+MT cascade\nRTF 0.15 (real-time)",
xy=(0.151, 1), xytext=(0.25, 4),
fontsize=9, fontstyle="italic", color="#1565c0",
arrowprops=dict(arrowstyle="->", color="#1565c0", lw=1.5),
bbox=dict(boxstyle="round,pad=0.3", fc="#e3f2fd", ec="#90caf9", alpha=0.9))
ax.set_xlabel("RTF (lower = faster)")
ax.set_ylabel("WER % (lower = better)")
ax.set_title("ACL6060 Conference Talks — 5 talks, 58 min (H100 80 GB)",
fontsize=13, fontweight="bold", pad=12)
ax.set_xlim(-0.005, 0.30)
ax.set_ylim(-1, 26)
ax.grid(True, alpha=0.12)
_save(fig, "wer_vs_rtf_acl6060.png")
# ──────────────────────────────────────────────────────────
# Figure 3: Bar chart — WER + RTF side-by-side
# ──────────────────────────────────────────────────────────
def fig_bars():
names = [
"Whisper\nlarge-v3", "Voxtral 4B\n(vLLM)", "Qwen3 0.6B\n(batch)",
"Qwen3 1.7B\n(batch)", "Qwen3 0.6B\nSimulStream", "Qwen3 1.7B\nSimulStream",
]
wer_c = [2.02, 2.71, 2.30, 2.46, 6.44, 8.09]
wer_o = [7.79, 9.26, 6.12, 5.34, 9.27, 9.56]
rtf_c = [0.071, 0.137, 0.065, 0.069, 0.109, 0.117]
fwl = [472, 137, 432, 457, 91, 94] # ms
cols = [COLORS["whisper"], COLORS["voxtral"], COLORS["qwen_b"],
COLORS["qwen_b"], COLORS["qwen_s"], COLORS["qwen_s"]]
cols_l = ["#ff7675", "#ffeaa7", "#a29bfe", "#a29bfe", "#55efc4", "#55efc4"]
x = np.arange(len(names))
fig, axes = plt.subplots(1, 3, figsize=(16, 6))
# WER
ax = axes[0]; w = 0.36
ax.bar(x - w/2, wer_c, w, color=cols, alpha=0.9, edgecolor="white", label="test-clean")
ax.bar(x + w/2, wer_o, w, color=cols_l, alpha=0.65, edgecolor="white", label="test-other")
ax.set_ylabel("WER %"); ax.set_title("Word Error Rate", fontweight="bold")
ax.set_xticks(x); ax.set_xticklabels(names, fontsize=7.5, rotation=25, ha="right")
ax.legend(fontsize=8); ax.grid(axis="y", alpha=0.15)
for i, v in enumerate(wer_c):
ax.text(i - w/2, v + 0.2, f"{v:.1f}", ha="center", fontsize=7, fontweight="bold")
# RTF
ax = axes[1]
ax.bar(x, rtf_c, 0.55, color=cols, alpha=0.9, edgecolor="white")
ax.set_ylabel("RTF (lower = faster)"); ax.set_title("Real-Time Factor (test-clean)", fontweight="bold")
ax.set_xticks(x); ax.set_xticklabels(names, fontsize=7.5, rotation=25, ha="right")
ax.grid(axis="y", alpha=0.15)
for i, v in enumerate(rtf_c):
ax.text(i, v + 0.003, f"{v:.3f}", ha="center", fontsize=8, fontweight="bold")
# First-word latency
ax = axes[2]
ax.bar(x, fwl, 0.55, color=cols, alpha=0.9, edgecolor="white")
ax.set_ylabel("ms"); ax.set_title("First Word Latency", fontweight="bold")
ax.set_xticks(x); ax.set_xticklabels(names, fontsize=7.5, rotation=25, ha="right")
ax.grid(axis="y", alpha=0.15)
for i, v in enumerate(fwl):
ax.text(i, v + 8, f"{v}", ha="center", fontsize=8, fontweight="bold")
fig.suptitle("LibriSpeech Benchmark — H100 80 GB", fontsize=14, fontweight="bold")
plt.tight_layout()
_save(fig, "bars_wer_rtf_latency.png")
# ──────────────────────────────────────────────────────────
# Figure 4: Clean vs Other robustness
# ──────────────────────────────────────────────────────────
def fig_robustness():
models = [
("Whisper large-v3", 2.02, 7.79, COLORS["whisper"], "h", 280),
("Qwen3 0.6B (batch)", 2.30, 6.12, COLORS["qwen_b"], "h", 180),
("Qwen3 1.7B (batch)", 2.46, 5.34, COLORS["qwen_b"], "h", 280),
("Voxtral 4B (vLLM)", 2.71, 9.26, COLORS["voxtral"], "D", 280),
("Qwen3 0.6B\nSimulStream", 6.44, 9.27, COLORS["qwen_s"], "s", 240),
("Qwen3 1.7B\nSimulStream", 8.09, 9.56, COLORS["qwen_s"], "s", 300),
]
# Manual label offsets — carefully placed to avoid overlap
offsets = [(-55, 10), (8, 10), (8, -18), (-55, -18), (-10, 12), (10, -18)]
fig, ax = plt.subplots(figsize=(8.5, 7))
ax.plot([0, 13], [0, 13], "--", color="#ccc", lw=1, zorder=1)
ax.fill_between([0, 13], [0, 13], [13, 13], color="#fff5f5", alpha=0.5, zorder=0)
ax.text(4, 11, "degrades more\non noisy audio", fontsize=9, color="#bbb", fontstyle="italic")
for (name, wc, wo, color, marker, sz), (lx, ly) in zip(models, offsets):
ax.scatter(wc, wo, s=sz, c=color, marker=marker,
edgecolors="white", linewidths=1.5, zorder=5)
ax.annotate(name, (wc, wo), fontsize=8.5, fontweight="bold",
xytext=(lx, ly), textcoords="offset points",
arrowprops=dict(arrowstyle="-", color="#aaa", lw=0.6))
deg = wo - wc
ax.annotate(f"+{deg:.1f}%", (wc, wo), fontsize=7, color="#999",
xytext=(-6, -13), textcoords="offset points")
ax.set_xlabel("WER % on test-clean")
ax.set_ylabel("WER % on test-other")
ax.set_title("Clean vs Noisy Robustness (H100 80 GB)", fontsize=13, fontweight="bold", pad=12)
ax.set_xlim(-0.3, 12); ax.set_ylim(-0.3, 12)
ax.set_aspect("equal"); ax.grid(True, alpha=0.12)
_save(fig, "robustness_clean_vs_other.png")
# ──────────────────────────────────────────────────────────
# Figure 5: ACL6060 per-talk breakdown (Qwen3 vs Voxtral)
# ──────────────────────────────────────────────────────────
def fig_per_talk():
q = DATA["acl6060"]["systems"]["qwen3_1.7b_simulstream_kv"]["per_talk"]
v = DATA["acl6060"]["systems"]["voxtral_4b_vllm_realtime"]["per_talk"]
talks = DATA["acl6060"]["talks"]
fig, ax = plt.subplots(figsize=(9, 5))
x = np.arange(len(talks)); w = 0.35
bars_v = ax.bar(x - w/2, [v[t] for t in talks], w, color=COLORS["voxtral"],
edgecolor="white", label="Voxtral 4B (vLLM)")
bars_q = ax.bar(x + w/2, [q[t] for t in talks], w, color=COLORS["qwen_s"],
edgecolor="white", label="Qwen3 1.7B SimulStream+KV")
for bar in bars_v:
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3,
f"{bar.get_height():.1f}", ha="center", fontsize=8)
for bar in bars_q:
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3,
f"{bar.get_height():.1f}", ha="center", fontsize=8)
ax.set_xlabel("ACL6060 Talk ID")
ax.set_ylabel("WER %")
ax.set_title("Per-Talk WER — ACL6060 Conference Talks (H100 80 GB)",
fontsize=13, fontweight="bold", pad=12)
ax.set_xticks(x); ax.set_xticklabels([f"Talk {t}" for t in talks])
ax.legend(fontsize=9); ax.grid(axis="y", alpha=0.15)
ax.set_ylim(0, 18)
_save(fig, "acl6060_per_talk.png")
if __name__ == "__main__":
print("Generating H100 benchmark figures...")
fig_scatter_clean()
fig_scatter_acl6060()
fig_bars()
fig_robustness()
fig_per_talk()
print("Done!")