Automatic Speech Recognition
NeMo
Core ML
PyTorch
English
speech
audio
Transducer
TDT
FastConformer
Conformer
NeMo
hf-asr-leaderboard
Eval Results (legacy)
Instructions to use FluidInference/parakeet-ctc-110m-coreml with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- NeMo
How to use FluidInference/parakeet-ctc-110m-coreml with NeMo:
import nemo.collections.asr as nemo_asr asr_model = nemo_asr.models.ASRModel.from_pretrained("FluidInference/parakeet-ctc-110m-coreml") transcriptions = asr_model.transcribe(["file.wav"]) - Notebooks
- Google Colab
- Kaggle
Commit ·
20c78b6
1
Parent(s): a7fc9b6
add config.json, remove non-CTC scripts
Browse files- Restore config.json (model metadata: vocab_size, token IDs)
- Remove convert/parakeet-tdt-v2-0.6b/ (TDT 0.6b, not CTC)
- Remove scripts/run_benchmarks.py (general FluidAudio, not CTC)
- Keep convert/parakeet-tdt-ctc-110m/ (CTC conversion scripts)
- config.json +7 -0
- convert/parakeet-tdt-v2-0.6b/coreml/compare-components.py +0 -958
- convert/parakeet-tdt-v2-0.6b/coreml/compile_modelc.py +0 -91
- convert/parakeet-tdt-v2-0.6b/coreml/convert-parakeet.py +0 -619
- convert/parakeet-tdt-v2-0.6b/coreml/individual_components.py +0 -229
- convert/parakeet-tdt-v2-0.6b/coreml/quantize_coreml.py +0 -1207
- convert/parakeet-tdt-v2-0.6b/coreml/speech_to_text_streaming_infer_rnnt.py +0 -341
- scripts/run_benchmarks.py +0 -273
config.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"bos_token_id": 1,
|
| 3 |
+
"eos_token_id": 2,
|
| 4 |
+
"nemo_model_type": "parakeet",
|
| 5 |
+
"pad_token_id": 0,
|
| 6 |
+
"vocab_size": 1024,
|
| 7 |
+
}
|
convert/parakeet-tdt-v2-0.6b/coreml/compare-components.py
DELETED
|
@@ -1,958 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Compare Parakeet TDT v2 Torch vs CoreML components on a fixed 15s window.
|
| 3 |
-
|
| 4 |
-
Writes numeric diffs to the specified output directory (metadata.json) and
|
| 5 |
-
saves plots under a repo-tracked directory: plots/<script-name>/.
|
| 6 |
-
"""
|
| 7 |
-
from __future__ import annotations
|
| 8 |
-
|
| 9 |
-
import json
|
| 10 |
-
import time
|
| 11 |
-
from dataclasses import dataclass
|
| 12 |
-
from pathlib import Path
|
| 13 |
-
from typing import Dict, Optional, Tuple
|
| 14 |
-
|
| 15 |
-
import coremltools as ct
|
| 16 |
-
import numpy as np
|
| 17 |
-
import soundfile as sf
|
| 18 |
-
import torch
|
| 19 |
-
import typer
|
| 20 |
-
|
| 21 |
-
import nemo.collections.asr as nemo_asr
|
| 22 |
-
|
| 23 |
-
# Optional plotting
|
| 24 |
-
try:
|
| 25 |
-
import matplotlib
|
| 26 |
-
matplotlib.use("Agg")
|
| 27 |
-
import matplotlib.pyplot as plt
|
| 28 |
-
HAS_MPL = True
|
| 29 |
-
except Exception:
|
| 30 |
-
HAS_MPL = False
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
@dataclass
|
| 34 |
-
class ValidationSettings:
|
| 35 |
-
audio_path: Optional[Path]
|
| 36 |
-
seconds: float
|
| 37 |
-
seed: Optional[int]
|
| 38 |
-
rtol: float
|
| 39 |
-
atol: float
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def _compute_length(seconds: float, sample_rate: int) -> int:
|
| 43 |
-
return int(round(seconds * sample_rate))
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
def _prepare_audio(
|
| 47 |
-
validation_audio: Optional[Path],
|
| 48 |
-
sample_rate: int,
|
| 49 |
-
max_samples: int,
|
| 50 |
-
seed: Optional[int],
|
| 51 |
-
) -> torch.Tensor:
|
| 52 |
-
if validation_audio is None:
|
| 53 |
-
if seed is not None:
|
| 54 |
-
torch.manual_seed(seed)
|
| 55 |
-
return torch.randn(1, max_samples, dtype=torch.float32)
|
| 56 |
-
|
| 57 |
-
data, sr = sf.read(str(validation_audio), dtype="float32")
|
| 58 |
-
if sr != sample_rate:
|
| 59 |
-
raise typer.BadParameter(
|
| 60 |
-
f"Validation audio sample rate {sr} does not match model rate {sample_rate}"
|
| 61 |
-
)
|
| 62 |
-
if data.ndim > 1:
|
| 63 |
-
data = data[:, 0]
|
| 64 |
-
if data.size == 0:
|
| 65 |
-
raise typer.BadParameter("Validation audio is empty")
|
| 66 |
-
if data.size < max_samples:
|
| 67 |
-
data = np.pad(data, (0, max_samples - data.size))
|
| 68 |
-
elif data.size > max_samples:
|
| 69 |
-
data = data[:max_samples]
|
| 70 |
-
return torch.from_numpy(data).unsqueeze(0).to(dtype=torch.float32)
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
def _np(x: torch.Tensor, dtype=None) -> np.ndarray:
|
| 74 |
-
arr = x.detach().cpu().numpy()
|
| 75 |
-
if dtype is not None:
|
| 76 |
-
return arr.astype(dtype, copy=False)
|
| 77 |
-
return arr
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def _to_t(x) -> torch.Tensor:
|
| 81 |
-
if isinstance(x, torch.Tensor):
|
| 82 |
-
return x.detach().cpu()
|
| 83 |
-
elif isinstance(x, np.ndarray):
|
| 84 |
-
# Ensure a separate tensor (avoid shared memory weirdness)
|
| 85 |
-
return torch.from_numpy(np.array(x, copy=True))
|
| 86 |
-
else:
|
| 87 |
-
return torch.tensor(x)
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
def _max_diffs(a, b, rtol: float, atol: float) -> Tuple[float, float, bool]:
|
| 91 |
-
# Use NumPy for comparisons to avoid invoking the PyTorch C-API in contexts
|
| 92 |
-
# where the GIL may not be held (which can trigger PyEval_SaveThread errors).
|
| 93 |
-
na = np.array(a, dtype=np.float32, copy=True)
|
| 94 |
-
nb = np.array(b, dtype=np.float32, copy=True)
|
| 95 |
-
if na.size == 0:
|
| 96 |
-
return 0.0, 0.0, True
|
| 97 |
-
diff = np.abs(na - nb)
|
| 98 |
-
max_abs = float(diff.max())
|
| 99 |
-
denom = np.maximum(np.abs(na), np.abs(nb))
|
| 100 |
-
with np.errstate(divide="ignore", invalid="ignore"):
|
| 101 |
-
rel = np.where(denom == 0.0, 0.0, diff / denom)
|
| 102 |
-
max_rel = float(rel.max())
|
| 103 |
-
ok = bool(np.allclose(na, nb, rtol=rtol, atol=atol))
|
| 104 |
-
return max_abs, max_rel, ok
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
def _plot_line(x_ref: np.ndarray, x_ml: np.ndarray, title: str, path: Path, also_delta: bool = False):
|
| 108 |
-
if not HAS_MPL:
|
| 109 |
-
return None
|
| 110 |
-
if also_delta:
|
| 111 |
-
fig, axes = plt.subplots(2, 1, figsize=(8, 5), sharex=True)
|
| 112 |
-
axes[0].plot(x_ref, label="torch", linewidth=1)
|
| 113 |
-
axes[0].plot(x_ml, label="coreml", linewidth=1, alpha=0.8)
|
| 114 |
-
axes[0].set_title(title)
|
| 115 |
-
axes[0].legend()
|
| 116 |
-
delta = np.asarray(x_ref) - np.asarray(x_ml)
|
| 117 |
-
axes[1].plot(delta, color="C3", linewidth=1)
|
| 118 |
-
axes[1].set_title("Delta (torch - coreml)")
|
| 119 |
-
axes[1].set_xlabel("time/step")
|
| 120 |
-
plt.tight_layout()
|
| 121 |
-
plt.savefig(path)
|
| 122 |
-
plt.close(fig)
|
| 123 |
-
else:
|
| 124 |
-
plt.figure(figsize=(8, 3))
|
| 125 |
-
plt.plot(x_ref, label="torch", linewidth=1)
|
| 126 |
-
plt.plot(x_ml, label="coreml", linewidth=1, alpha=0.8)
|
| 127 |
-
plt.title(title)
|
| 128 |
-
plt.legend()
|
| 129 |
-
plt.tight_layout()
|
| 130 |
-
plt.savefig(path)
|
| 131 |
-
plt.close()
|
| 132 |
-
return str(path.name)
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
def _plot_image(img: np.ndarray, title: str, path: Path, vmin=None, vmax=None):
|
| 136 |
-
if not HAS_MPL:
|
| 137 |
-
return None
|
| 138 |
-
plt.figure(figsize=(6, 4))
|
| 139 |
-
plt.imshow(img, aspect='auto', origin='lower', interpolation='nearest', vmin=vmin, vmax=vmax)
|
| 140 |
-
plt.title(title)
|
| 141 |
-
plt.colorbar(shrink=0.8)
|
| 142 |
-
plt.tight_layout()
|
| 143 |
-
plt.savefig(path)
|
| 144 |
-
plt.close()
|
| 145 |
-
return str(path.name)
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
def _plot_mel_composite(
|
| 149 |
-
mel_torch: np.ndarray,
|
| 150 |
-
mel_coreml: np.ndarray,
|
| 151 |
-
path: Path,
|
| 152 |
-
vmin=None,
|
| 153 |
-
vmax=None,
|
| 154 |
-
):
|
| 155 |
-
"""Create a single PNG with mel torch, mel coreml, abs diff heatmap, and mean-over-time curves with delta."""
|
| 156 |
-
if not HAS_MPL:
|
| 157 |
-
return None
|
| 158 |
-
mel_torch = np.asarray(mel_torch)
|
| 159 |
-
mel_coreml = np.asarray(mel_coreml)
|
| 160 |
-
absdiff = np.abs(mel_torch - mel_coreml)
|
| 161 |
-
mean_t = mel_torch.mean(axis=0)
|
| 162 |
-
mean_c = mel_coreml.mean(axis=0)
|
| 163 |
-
delta = mean_t - mean_c
|
| 164 |
-
|
| 165 |
-
fig = plt.figure(figsize=(12, 8))
|
| 166 |
-
gs = fig.add_gridspec(2, 2, height_ratios=[1, 1])
|
| 167 |
-
ax1 = fig.add_subplot(gs[0, 0])
|
| 168 |
-
im1 = ax1.imshow(mel_torch, aspect='auto', origin='lower', interpolation='nearest', vmin=vmin, vmax=vmax)
|
| 169 |
-
ax1.set_title("Mel (Torch)")
|
| 170 |
-
fig.colorbar(im1, ax=ax1, shrink=0.8)
|
| 171 |
-
|
| 172 |
-
ax2 = fig.add_subplot(gs[0, 1])
|
| 173 |
-
im2 = ax2.imshow(mel_coreml, aspect='auto', origin='lower', interpolation='nearest', vmin=vmin, vmax=vmax)
|
| 174 |
-
ax2.set_title("Mel (CoreML)")
|
| 175 |
-
fig.colorbar(im2, ax=ax2, shrink=0.8)
|
| 176 |
-
|
| 177 |
-
ax3 = fig.add_subplot(gs[1, 0])
|
| 178 |
-
im3 = ax3.imshow(absdiff, aspect='auto', origin='lower', interpolation='nearest')
|
| 179 |
-
ax3.set_title("Mel |diff|")
|
| 180 |
-
fig.colorbar(im3, ax=ax3, shrink=0.8)
|
| 181 |
-
|
| 182 |
-
ax4 = fig.add_subplot(gs[1, 1])
|
| 183 |
-
ax4.plot(mean_t, label="torch", linewidth=1)
|
| 184 |
-
ax4.plot(mean_c, label="coreml", linewidth=1, alpha=0.8)
|
| 185 |
-
ax4.plot(delta, label="delta", linewidth=1, color="C3")
|
| 186 |
-
ax4.set_title("Mel mean over time + delta")
|
| 187 |
-
ax4.legend()
|
| 188 |
-
|
| 189 |
-
plt.tight_layout()
|
| 190 |
-
plt.savefig(path)
|
| 191 |
-
plt.close(fig)
|
| 192 |
-
return str(path.name)
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
def _plot_latency_bars(
|
| 196 |
-
labels,
|
| 197 |
-
torch_means,
|
| 198 |
-
torch_stds,
|
| 199 |
-
coreml_means,
|
| 200 |
-
coreml_stds,
|
| 201 |
-
path: Path,
|
| 202 |
-
):
|
| 203 |
-
if not HAS_MPL:
|
| 204 |
-
return None
|
| 205 |
-
x = np.arange(len(labels))
|
| 206 |
-
width = 0.35
|
| 207 |
-
fig, ax = plt.subplots(figsize=(8, 4))
|
| 208 |
-
b1 = ax.bar(x - width/2, torch_means, width, yerr=torch_stds, label="torch", color="C0", alpha=0.9)
|
| 209 |
-
b2 = ax.bar(x + width/2, coreml_means, width, yerr=coreml_stds, label="coreml", color="C1", alpha=0.9)
|
| 210 |
-
ax.set_xticks(x, labels, rotation=15)
|
| 211 |
-
ax.set_ylabel("latency (ms)")
|
| 212 |
-
ax.set_title("Component latency (15s window inputs)")
|
| 213 |
-
ax.legend()
|
| 214 |
-
# Add value labels on bars
|
| 215 |
-
def _annotate(bars):
|
| 216 |
-
for bar in bars:
|
| 217 |
-
h = bar.get_height()
|
| 218 |
-
if np.isnan(h):
|
| 219 |
-
continue
|
| 220 |
-
ax.annotate(f"{h:.0f}",
|
| 221 |
-
xy=(bar.get_x() + bar.get_width()/2, h),
|
| 222 |
-
xytext=(0, 3), textcoords="offset points",
|
| 223 |
-
ha='center', va='bottom', fontsize=8)
|
| 224 |
-
_annotate(b1)
|
| 225 |
-
_annotate(b2)
|
| 226 |
-
plt.tight_layout()
|
| 227 |
-
plt.savefig(path)
|
| 228 |
-
plt.close(fig)
|
| 229 |
-
return str(path.name)
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
def _plot_speedup_bars(labels, torch_means, coreml_means, path: Path):
|
| 233 |
-
if not HAS_MPL:
|
| 234 |
-
return None
|
| 235 |
-
speedup = []
|
| 236 |
-
for t, c in zip(torch_means, coreml_means):
|
| 237 |
-
if c and c > 0:
|
| 238 |
-
speedup.append(float(t) / float(c))
|
| 239 |
-
else:
|
| 240 |
-
speedup.append(np.nan)
|
| 241 |
-
x = np.arange(len(labels))
|
| 242 |
-
fig, ax = plt.subplots(figsize=(8, 4))
|
| 243 |
-
bars = ax.bar(x, speedup, color="C2")
|
| 244 |
-
ax.set_xticks(x, labels, rotation=15)
|
| 245 |
-
ax.set_ylabel("torch/coreml speedup")
|
| 246 |
-
ax.set_title("CoreML speedup vs Torch (higher is better)")
|
| 247 |
-
ax.axhline(1.0, color="gray", linestyle="--", linewidth=1)
|
| 248 |
-
# Add value labels
|
| 249 |
-
for bar in bars:
|
| 250 |
-
h = bar.get_height()
|
| 251 |
-
if np.isnan(h):
|
| 252 |
-
continue
|
| 253 |
-
ax.annotate(f"{h:.2f}",
|
| 254 |
-
xy=(bar.get_x() + bar.get_width()/2, h),
|
| 255 |
-
xytext=(0, 3), textcoords="offset points",
|
| 256 |
-
ha='center', va='bottom', fontsize=8)
|
| 257 |
-
plt.tight_layout()
|
| 258 |
-
plt.savefig(path)
|
| 259 |
-
plt.close(fig)
|
| 260 |
-
return str(path.name)
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
@app.command()
|
| 268 |
-
def compare(
|
| 269 |
-
output_dir: Path = typer.Option(Path("parakeet_coreml"), help="Directory containing mlpackages + metadata.json"),
|
| 270 |
-
nemo_path: Optional[Path] = typer.Option(None, "--nemo-path", exists=True, resolve_path=True, help="Path to .nemo checkpoint"),
|
| 271 |
-
model_id: str = typer.Option("nvidia/parakeet-tdt-0.6b-v2", "--model-id", help="HF model id if --nemo-path omitted"),
|
| 272 |
-
validation_audio: Optional[Path] = typer.Option(None, exists=True, resolve_path=True, help="15s, 16kHz wav for validation (defaults to audio/yc_first_minute_16k_15s.wav if present)"),
|
| 273 |
-
seed: Optional[int] = typer.Option(None, help="Random seed for synthetic input when audio is not provided"),
|
| 274 |
-
rtol: float = typer.Option(1e-3, help="Relative tolerance for comparisons"),
|
| 275 |
-
atol: float = typer.Option(1e-4, help="Absolute tolerance for comparisons"),
|
| 276 |
-
runs: int = typer.Option(10, help="Timed runs per model for latency measurement"),
|
| 277 |
-
warmup: int = typer.Option(3, help="Warmup runs before timing (compilation, caches)"),
|
| 278 |
-
symbol_steps: int = typer.Option(
|
| 279 |
-
32,
|
| 280 |
-
help="Number of sequential decoder steps to validate with streaming U=1 inputs",
|
| 281 |
-
),
|
| 282 |
-
) -> None:
|
| 283 |
-
"""Run Torch vs CoreML comparisons and update metadata.json with plots and diffs."""
|
| 284 |
-
output_dir.mkdir(parents=True, exist_ok=True)
|
| 285 |
-
if symbol_steps < 1:
|
| 286 |
-
raise typer.BadParameter("symbol_steps must be >= 1")
|
| 287 |
-
|
| 288 |
-
meta_path = output_dir / "metadata.json"
|
| 289 |
-
exported_meta: Dict[str, object] = {}
|
| 290 |
-
if meta_path.exists():
|
| 291 |
-
try:
|
| 292 |
-
exported_meta = json.loads(meta_path.read_text())
|
| 293 |
-
except Exception:
|
| 294 |
-
exported_meta = {}
|
| 295 |
-
exported_max_u = int(exported_meta.get("max_symbol_steps", 1))
|
| 296 |
-
if exported_max_u != 1:
|
| 297 |
-
typer.echo(
|
| 298 |
-
f"Note: CoreML export reports max_symbol_steps={exported_max_u}; "
|
| 299 |
-
"comparison still drives decoder step-wise with U=1 inputs."
|
| 300 |
-
)
|
| 301 |
-
if nemo_path is not None:
|
| 302 |
-
typer.echo(f"Loading NeMo model from {nemo_path}…")
|
| 303 |
-
asr_model = nemo_asr.models.EncDecRNNTBPEModel.restore_from(str(nemo_path), map_location="cpu")
|
| 304 |
-
else:
|
| 305 |
-
typer.echo(f"Downloading NeMo model via {model_id}…")
|
| 306 |
-
asr_model = nemo_asr.models.EncDecRNNTBPEModel.from_pretrained(model_id, map_location="cpu")
|
| 307 |
-
asr_model.eval()
|
| 308 |
-
|
| 309 |
-
sample_rate = int(asr_model.cfg.preprocessor.sample_rate)
|
| 310 |
-
max_samples = _compute_length(15.0, sample_rate)
|
| 311 |
-
default_audio = (Path(__file__).parent / "audio" / "yc_first_minute_16k_15s.wav").resolve()
|
| 312 |
-
chosen_audio = validation_audio if validation_audio is not None else (default_audio if default_audio.exists() else None)
|
| 313 |
-
if chosen_audio is not None and validation_audio is None:
|
| 314 |
-
typer.echo(f"Using default validation audio: {chosen_audio}")
|
| 315 |
-
|
| 316 |
-
audio_tensor = _prepare_audio(chosen_audio, sample_rate, max_samples, seed)
|
| 317 |
-
audio_length = torch.tensor([max_samples], dtype=torch.int32)
|
| 318 |
-
|
| 319 |
-
asr_model.decoder._rnnt_export = True
|
| 320 |
-
# Disable fused loss/WER computation for simpler joint inference
|
| 321 |
-
asr_model.joint.set_fuse_loss_wer(False)
|
| 322 |
-
# Important: ensure the joint returns raw logits (not log-softmax)
|
| 323 |
-
# RNNTJoint applies log_softmax on CPU by default when `log_softmax is None`.
|
| 324 |
-
# Our exported CoreML joint emits pre-softmax logits, so make the Torch
|
| 325 |
-
# reference do the same to avoid systematic offsets in comparisons/plots.
|
| 326 |
-
try:
|
| 327 |
-
# Some versions expose this as a plain attribute
|
| 328 |
-
asr_model.joint.log_softmax = False
|
| 329 |
-
except Exception:
|
| 330 |
-
pass
|
| 331 |
-
|
| 332 |
-
# Generate reference outputs directly from NeMo model components
|
| 333 |
-
with torch.inference_mode():
|
| 334 |
-
# Preprocessor - direct NeMo call
|
| 335 |
-
mel_ref, mel_length_ref = asr_model.preprocessor(
|
| 336 |
-
input_signal=audio_tensor,
|
| 337 |
-
length=audio_length.to(dtype=torch.long)
|
| 338 |
-
)
|
| 339 |
-
mel_length_ref = mel_length_ref.to(dtype=torch.int32)
|
| 340 |
-
|
| 341 |
-
# Encoder - direct NeMo call
|
| 342 |
-
encoder_ref, encoder_length_ref = asr_model.encoder(
|
| 343 |
-
audio_signal=mel_ref,
|
| 344 |
-
length=mel_length_ref.to(dtype=torch.long)
|
| 345 |
-
)
|
| 346 |
-
encoder_length_ref = encoder_length_ref.to(dtype=torch.int32)
|
| 347 |
-
|
| 348 |
-
vocab_size = int(asr_model.tokenizer.vocab_size)
|
| 349 |
-
num_extra = int(asr_model.joint.num_extra_outputs)
|
| 350 |
-
decoder_hidden = int(asr_model.decoder.pred_hidden)
|
| 351 |
-
decoder_layers = int(asr_model.decoder.pred_rnn_layers)
|
| 352 |
-
blank_id = int(asr_model.decoder.blank_idx)
|
| 353 |
-
|
| 354 |
-
blank_targets = torch.tensor([[blank_id]], dtype=torch.int32)
|
| 355 |
-
blank_target_lengths = torch.tensor([1], dtype=torch.int32)
|
| 356 |
-
blank_targets_long = blank_targets.to(dtype=torch.long)
|
| 357 |
-
blank_target_lengths_long = blank_target_lengths.to(dtype=torch.long)
|
| 358 |
-
|
| 359 |
-
def _decoder_rollout_torch(num_steps: int) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 360 |
-
outputs = []
|
| 361 |
-
h_state = torch.zeros(decoder_layers, 1, decoder_hidden, dtype=torch.float32)
|
| 362 |
-
c_state = torch.zeros(decoder_layers, 1, decoder_hidden, dtype=torch.float32)
|
| 363 |
-
state = [h_state, c_state]
|
| 364 |
-
with torch.inference_mode():
|
| 365 |
-
for _ in range(num_steps):
|
| 366 |
-
y, _, new_state = asr_model.decoder(
|
| 367 |
-
targets=blank_targets_long,
|
| 368 |
-
target_length=blank_target_lengths_long,
|
| 369 |
-
states=state,
|
| 370 |
-
)
|
| 371 |
-
outputs.append(y.detach())
|
| 372 |
-
state = [new_state[0].detach(), new_state[1].detach()]
|
| 373 |
-
if outputs:
|
| 374 |
-
decoder_seq = torch.cat(outputs, dim=-1)
|
| 375 |
-
else:
|
| 376 |
-
decoder_seq = torch.zeros(1, decoder_hidden, 0, dtype=torch.float32)
|
| 377 |
-
return decoder_seq, state[0], state[1]
|
| 378 |
-
|
| 379 |
-
decoder_ref, h_ref, c_ref = _decoder_rollout_torch(symbol_steps)
|
| 380 |
-
|
| 381 |
-
with torch.inference_mode():
|
| 382 |
-
logits_ref = asr_model.joint(
|
| 383 |
-
encoder_outputs=encoder_ref,
|
| 384 |
-
decoder_outputs=decoder_ref,
|
| 385 |
-
)
|
| 386 |
-
|
| 387 |
-
# Convert tensors to numpy for CoreML
|
| 388 |
-
def _np32(x):
|
| 389 |
-
return np.array(x.detach().cpu().numpy(), dtype=np.float32, copy=True)
|
| 390 |
-
|
| 391 |
-
# Prepare plot dir (write to repo-tracked plots/<script-name>/)
|
| 392 |
-
plots_root = Path(__file__).parent / "plots"
|
| 393 |
-
plots_dir = plots_root / Path(__file__).stem
|
| 394 |
-
plots_dir.mkdir(parents=True, exist_ok=True)
|
| 395 |
-
|
| 396 |
-
encoder_np = _np32(encoder_ref)
|
| 397 |
-
decoder_ref_np = _np32(decoder_ref)
|
| 398 |
-
|
| 399 |
-
summary: Dict[str, object] = {
|
| 400 |
-
"requested": True,
|
| 401 |
-
"status": "ok",
|
| 402 |
-
"atol": atol,
|
| 403 |
-
"rtol": rtol,
|
| 404 |
-
"symbol_steps": int(symbol_steps),
|
| 405 |
-
"audio_path": None if validation_audio is None else str(validation_audio),
|
| 406 |
-
"components": {},
|
| 407 |
-
}
|
| 408 |
-
|
| 409 |
-
# Preprocessor
|
| 410 |
-
pre = ct.models.MLModel(str(output_dir / "parakeet_preprocessor.mlpackage"), compute_units=ct.ComputeUnit.CPU_AND_NE)
|
| 411 |
-
t0 = time.perf_counter()
|
| 412 |
-
pre_out = pre.predict({"audio_signal": _np32(audio_tensor), "audio_length": _np32(audio_length).astype(np.int32)})
|
| 413 |
-
t1 = time.perf_counter()
|
| 414 |
-
pre_first_ms = (t1 - t0) * 1000.0
|
| 415 |
-
mel_ml = np.array(pre_out["mel"], dtype=np.float32, copy=True)
|
| 416 |
-
mel_len_ml = np.array(pre_out["mel_length"], dtype=np.int32, copy=True)
|
| 417 |
-
pre_atol, pre_rtol = max(atol, 1.0), max(rtol, 1e-2)
|
| 418 |
-
a_mel, r_mel, ok_mel = _max_diffs(_np32(mel_ref), mel_ml, pre_rtol, pre_atol)
|
| 419 |
-
ok_len = int(_np32(mel_length_ref).astype(np.int32)[0]) == int(np.array(mel_len_ml).astype(np.int32)[0])
|
| 420 |
-
mel_t = _np32(mel_ref)[0]
|
| 421 |
-
mel_c = mel_ml[0]
|
| 422 |
-
vmin = float(min(mel_t.min(), mel_c.min()))
|
| 423 |
-
vmax = float(max(mel_t.max(), mel_c.max()))
|
| 424 |
-
pre_plots = {
|
| 425 |
-
"mel_composite.png": _plot_mel_composite(mel_t, mel_c, plots_dir / "mel_composite.png", vmin=vmin, vmax=vmax),
|
| 426 |
-
}
|
| 427 |
-
# Latency measurements: Torch and CoreML
|
| 428 |
-
def _time_coreml(model: ct.models.MLModel, inputs: Dict[str, np.ndarray]) -> Tuple[float, float]:
|
| 429 |
-
# Warmup
|
| 430 |
-
for _ in range(max(0, warmup)):
|
| 431 |
-
_ = model.predict(inputs)
|
| 432 |
-
times = []
|
| 433 |
-
for _ in range(max(1, runs)):
|
| 434 |
-
t0 = time.perf_counter()
|
| 435 |
-
_ = model.predict(inputs)
|
| 436 |
-
t1 = time.perf_counter()
|
| 437 |
-
times.append((t1 - t0) * 1000.0)
|
| 438 |
-
arr = np.array(times, dtype=np.float64)
|
| 439 |
-
return float(arr.mean()), float(arr.std(ddof=1) if arr.size > 1 else 0.0)
|
| 440 |
-
|
| 441 |
-
def _time_torch(fn, *args, **kwargs) -> Tuple[float, float]:
|
| 442 |
-
with torch.inference_mode():
|
| 443 |
-
for _ in range(max(0, warmup)):
|
| 444 |
-
_ = fn(*args, **kwargs)
|
| 445 |
-
times = []
|
| 446 |
-
for _ in range(max(1, runs)):
|
| 447 |
-
t0 = time.perf_counter()
|
| 448 |
-
_ = fn(*args, **kwargs)
|
| 449 |
-
t1 = time.perf_counter()
|
| 450 |
-
times.append((t1 - t0) * 1000.0)
|
| 451 |
-
arr = np.array(times, dtype=np.float64)
|
| 452 |
-
return float(arr.mean()), float(arr.std(ddof=1) if arr.size > 1 else 0.0)
|
| 453 |
-
|
| 454 |
-
pre_torch_ms_mean, pre_torch_ms_std = _time_torch(
|
| 455 |
-
asr_model.preprocessor, input_signal=audio_tensor, length=audio_length.to(dtype=torch.long)
|
| 456 |
-
)
|
| 457 |
-
pre_coreml_ms_mean, pre_coreml_ms_std = _time_coreml(
|
| 458 |
-
pre,
|
| 459 |
-
{"audio_signal": _np32(audio_tensor), "audio_length": _np32(audio_length).astype(np.int32)},
|
| 460 |
-
)
|
| 461 |
-
seconds = 15.0
|
| 462 |
-
pre_coreml_rtf = float(pre_coreml_ms_mean / (seconds * 1000.0)) if pre_coreml_ms_mean > 0 else None
|
| 463 |
-
pre_torch_rtf = float(pre_torch_ms_mean / (seconds * 1000.0)) if pre_torch_ms_mean > 0 else None
|
| 464 |
-
|
| 465 |
-
summary["components"]["preprocessor"] = {
|
| 466 |
-
"mel": {"max_abs": a_mel, "max_rel": r_mel, "match": bool(ok_mel)},
|
| 467 |
-
"length_match": bool(ok_len),
|
| 468 |
-
"latency": {
|
| 469 |
-
"runs": int(runs),
|
| 470 |
-
"warmup": int(warmup),
|
| 471 |
-
"coreml_first_ms": pre_first_ms,
|
| 472 |
-
"torch_ms": {"mean": pre_torch_ms_mean, "std": pre_torch_ms_std},
|
| 473 |
-
"coreml_ms": {"mean": pre_coreml_ms_mean, "std": pre_coreml_ms_std},
|
| 474 |
-
"rtf": {"torch": pre_torch_rtf, "coreml": pre_coreml_rtf},
|
| 475 |
-
},
|
| 476 |
-
"plots": {k: v for k, v in pre_plots.items() if v},
|
| 477 |
-
}
|
| 478 |
-
|
| 479 |
-
# Encoder
|
| 480 |
-
enc = ct.models.MLModel(str(output_dir / "parakeet_encoder.mlpackage"), compute_units=ct.ComputeUnit.CPU_AND_NE)
|
| 481 |
-
t0 = time.perf_counter()
|
| 482 |
-
enc_out = enc.predict({"mel": _np32(mel_ref), "mel_length": _np32(mel_length_ref).astype(np.int32)})
|
| 483 |
-
t1 = time.perf_counter()
|
| 484 |
-
enc_first_ms = (t1 - t0) * 1000.0
|
| 485 |
-
enc_ml = np.array(enc_out["encoder"], dtype=np.float32, copy=True)
|
| 486 |
-
enc_len_ml = np.array(enc_out["encoder_length"], dtype=np.int32, copy=True)
|
| 487 |
-
a_enc, r_enc, ok_enc = _max_diffs(_np32(encoder_ref), enc_ml, max(rtol, 5e-3), max(atol, 5e-2))
|
| 488 |
-
ok_enc_len = int(_np32(encoder_length_ref).astype(np.int32)[0]) == int(np.array(enc_len_ml).astype(np.int32)[0])
|
| 489 |
-
enc_t = _np32(encoder_ref)[0]
|
| 490 |
-
enc_c = enc_ml[0]
|
| 491 |
-
enc_plots = {
|
| 492 |
-
"encoder_time_l2.png": _plot_line(
|
| 493 |
-
np.linalg.norm(enc_t, axis=0), # L2 norm over features (D) for each time step
|
| 494 |
-
np.linalg.norm(enc_c, axis=0), # enc_t shape is (D, T), so axis=0 is features
|
| 495 |
-
"Encoder L2 over time",
|
| 496 |
-
plots_dir / "encoder_time_l2.png",
|
| 497 |
-
also_delta=True,
|
| 498 |
-
),
|
| 499 |
-
}
|
| 500 |
-
enc_torch_ms_mean, enc_torch_ms_std = _time_torch(
|
| 501 |
-
asr_model.encoder, audio_signal=mel_ref, length=mel_length_ref.to(dtype=torch.long)
|
| 502 |
-
)
|
| 503 |
-
enc_coreml_ms_mean, enc_coreml_ms_std = _time_coreml(
|
| 504 |
-
enc, {"mel": _np32(mel_ref), "mel_length": _np32(mel_length_ref).astype(np.int32)}
|
| 505 |
-
)
|
| 506 |
-
enc_coreml_rtf = float(enc_coreml_ms_mean / (seconds * 1000.0)) if enc_coreml_ms_mean > 0 else None
|
| 507 |
-
enc_torch_rtf = float(enc_torch_ms_mean / (seconds * 1000.0)) if enc_torch_ms_mean > 0 else None
|
| 508 |
-
|
| 509 |
-
summary["components"]["encoder"] = {
|
| 510 |
-
"encoder": {"max_abs": a_enc, "max_rel": r_enc, "match": bool(ok_enc)},
|
| 511 |
-
"length_match": bool(ok_enc_len),
|
| 512 |
-
"latency": {
|
| 513 |
-
"runs": int(runs),
|
| 514 |
-
"warmup": int(warmup),
|
| 515 |
-
"coreml_first_ms": enc_first_ms,
|
| 516 |
-
"torch_ms": {"mean": enc_torch_ms_mean, "std": enc_torch_ms_std},
|
| 517 |
-
"coreml_ms": {"mean": enc_coreml_ms_mean, "std": enc_coreml_ms_std},
|
| 518 |
-
"rtf": {"torch": enc_torch_rtf, "coreml": enc_coreml_rtf},
|
| 519 |
-
},
|
| 520 |
-
"plots": {k: v for k, v in enc_plots.items() if v},
|
| 521 |
-
}
|
| 522 |
-
|
| 523 |
-
# Decoder (sequential U=1 rollout)
|
| 524 |
-
dec = ct.models.MLModel(str(output_dir / "parakeet_decoder.mlpackage"), compute_units=ct.ComputeUnit.CPU_AND_NE)
|
| 525 |
-
|
| 526 |
-
zero_state_np = np.zeros((decoder_layers, 1, decoder_hidden), dtype=np.float32)
|
| 527 |
-
blank_targets_np = np.array(blank_targets.detach().cpu().numpy(), dtype=np.int32, copy=True)
|
| 528 |
-
blank_target_lengths_np = np.array(blank_target_lengths.detach().cpu().numpy(), dtype=np.int32, copy=True)
|
| 529 |
-
|
| 530 |
-
def _decoder_rollout_coreml(num_steps: int) -> Tuple[np.ndarray, np.ndarray, np.ndarray, float]:
|
| 531 |
-
outputs = []
|
| 532 |
-
h_np = zero_state_np.copy()
|
| 533 |
-
c_np = zero_state_np.copy()
|
| 534 |
-
first_ms: Optional[float] = None
|
| 535 |
-
for i in range(num_steps):
|
| 536 |
-
t0_i = time.perf_counter() if i == 0 else None
|
| 537 |
-
res = dec.predict(
|
| 538 |
-
{
|
| 539 |
-
"targets": blank_targets_np,
|
| 540 |
-
"target_length": blank_target_lengths_np,
|
| 541 |
-
"h_in": h_np,
|
| 542 |
-
"c_in": c_np,
|
| 543 |
-
}
|
| 544 |
-
)
|
| 545 |
-
if t0_i is not None:
|
| 546 |
-
t1_i = time.perf_counter()
|
| 547 |
-
first_ms = (t1_i - t0_i) * 1000.0
|
| 548 |
-
outputs.append(np.array(res["decoder"], dtype=np.float32, copy=True))
|
| 549 |
-
h_np = np.array(res["h_out"], dtype=np.float32, copy=True)
|
| 550 |
-
c_np = np.array(res["c_out"], dtype=np.float32, copy=True)
|
| 551 |
-
if outputs:
|
| 552 |
-
decoder_seq = np.concatenate(outputs, axis=-1)
|
| 553 |
-
else:
|
| 554 |
-
decoder_seq = np.zeros((1, decoder_hidden, 0), dtype=np.float32)
|
| 555 |
-
return decoder_seq, h_np, c_np, (0.0 if first_ms is None else float(first_ms))
|
| 556 |
-
|
| 557 |
-
dec_ml, h_ml, c_ml, dec_first_ms = _decoder_rollout_coreml(symbol_steps)
|
| 558 |
-
h_ref_np = _np32(h_ref)
|
| 559 |
-
c_ref_np = _np32(c_ref)
|
| 560 |
-
|
| 561 |
-
a_dec, r_dec, ok_dec = _max_diffs(decoder_ref_np, dec_ml, max(rtol, 1e-2), max(atol, 1e-1))
|
| 562 |
-
a_h, r_h, ok_h = _max_diffs(h_ref_np, h_ml, max(rtol, 1e-2), max(atol, 2.5e-1))
|
| 563 |
-
a_c, r_c, ok_c = _max_diffs(c_ref_np, c_ml, max(rtol, 5e-2), max(atol, 1.5e0))
|
| 564 |
-
|
| 565 |
-
dec_t = decoder_ref_np[0]
|
| 566 |
-
dec_c = dec_ml[0]
|
| 567 |
-
dec_plots = {
|
| 568 |
-
"decoder_steps_l2.png": _plot_line(
|
| 569 |
-
np.linalg.norm(dec_t, axis=0),
|
| 570 |
-
np.linalg.norm(dec_c, axis=0),
|
| 571 |
-
"Decoder L2 over steps",
|
| 572 |
-
plots_dir / "decoder_steps_l2.png",
|
| 573 |
-
also_delta=True,
|
| 574 |
-
),
|
| 575 |
-
}
|
| 576 |
-
|
| 577 |
-
def _time_decoder_coreml() -> Tuple[float, float]:
|
| 578 |
-
for _ in range(max(0, warmup)):
|
| 579 |
-
_decoder_rollout_coreml(symbol_steps)
|
| 580 |
-
times = []
|
| 581 |
-
for _ in range(max(1, runs)):
|
| 582 |
-
t0 = time.perf_counter()
|
| 583 |
-
_decoder_rollout_coreml(symbol_steps)
|
| 584 |
-
t1 = time.perf_counter()
|
| 585 |
-
times.append((t1 - t0) * 1000.0)
|
| 586 |
-
arr = np.array(times, dtype=np.float64)
|
| 587 |
-
return float(arr.mean()), float(arr.std(ddof=1) if arr.size > 1 else 0.0)
|
| 588 |
-
|
| 589 |
-
dec_torch_ms_mean, dec_torch_ms_std = _time_torch(lambda: _decoder_rollout_torch(symbol_steps))
|
| 590 |
-
dec_coreml_ms_mean, dec_coreml_ms_std = _time_decoder_coreml()
|
| 591 |
-
dec_coreml_rtf = float(dec_coreml_ms_mean / (seconds * 1000.0)) if dec_coreml_ms_mean > 0 else None
|
| 592 |
-
dec_torch_rtf = float(dec_torch_ms_mean / (seconds * 1000.0)) if dec_torch_ms_mean > 0 else None
|
| 593 |
-
|
| 594 |
-
summary["components"]["decoder"] = {
|
| 595 |
-
"decoder": {"max_abs": a_dec, "max_rel": r_dec, "match": bool(ok_dec)},
|
| 596 |
-
"h_out": {"max_abs": a_h, "max_rel": r_h, "match": bool(ok_h)},
|
| 597 |
-
"c_out": {"max_abs": a_c, "max_rel": r_c, "match": bool(ok_c)},
|
| 598 |
-
"latency": {
|
| 599 |
-
"runs": int(runs),
|
| 600 |
-
"warmup": int(warmup),
|
| 601 |
-
"coreml_first_ms": dec_first_ms,
|
| 602 |
-
"torch_ms": {"mean": dec_torch_ms_mean, "std": dec_torch_ms_std},
|
| 603 |
-
"coreml_ms": {"mean": dec_coreml_ms_mean, "std": dec_coreml_ms_std},
|
| 604 |
-
"rtf": {"torch": dec_torch_rtf, "coreml": dec_coreml_rtf},
|
| 605 |
-
},
|
| 606 |
-
"plots": {k: v for k, v in dec_plots.items() if v},
|
| 607 |
-
}
|
| 608 |
-
|
| 609 |
-
# Joint (sequential U=1 rollouts)
|
| 610 |
-
j = ct.models.MLModel(str(output_dir / "parakeet_joint.mlpackage"), compute_units=ct.ComputeUnit.CPU_AND_NE)
|
| 611 |
-
|
| 612 |
-
def _joint_rollout_coreml(decoder_seq_np: np.ndarray) -> Tuple[np.ndarray, float]:
|
| 613 |
-
logits_steps = []
|
| 614 |
-
first_ms: Optional[float] = None
|
| 615 |
-
for u in range(decoder_seq_np.shape[2]):
|
| 616 |
-
dec_slice = decoder_seq_np[:, :, u : u + 1]
|
| 617 |
-
t0_u = time.perf_counter() if u == 0 else None
|
| 618 |
-
res = j.predict({"encoder": encoder_np, "decoder": dec_slice})
|
| 619 |
-
if t0_u is not None:
|
| 620 |
-
t1_u = time.perf_counter()
|
| 621 |
-
first_ms = (t1_u - t0_u) * 1000.0
|
| 622 |
-
logits_steps.append(np.array(res["logits"], dtype=np.float32, copy=True))
|
| 623 |
-
if not logits_steps:
|
| 624 |
-
raise RuntimeError("No decoder steps provided for joint rollout")
|
| 625 |
-
return np.concatenate(logits_steps, axis=2), (0.0 if first_ms is None else float(first_ms))
|
| 626 |
-
|
| 627 |
-
logits_ml, joint_first_ms = _joint_rollout_coreml(decoder_ref_np)
|
| 628 |
-
logits_ref_np = _np32(logits_ref)
|
| 629 |
-
a_j, r_j, ok_j = _max_diffs(logits_ref_np, logits_ml, max(rtol, 1e-2), max(atol, 1e-1))
|
| 630 |
-
joint_plots = {}
|
| 631 |
-
if HAS_MPL:
|
| 632 |
-
lt = logits_ref_np[0, 0, 0, :]
|
| 633 |
-
lc = logits_ml[0, 0, 0, :]
|
| 634 |
-
top_idx = np.argsort(-np.abs(lt))[:50]
|
| 635 |
-
path = plots_dir / "joint_top50.png"
|
| 636 |
-
plt.figure(figsize=(8, 3))
|
| 637 |
-
plt.plot(lt[top_idx], label="torch")
|
| 638 |
-
plt.plot(lc[top_idx], label="coreml", alpha=0.8)
|
| 639 |
-
plt.title("Joint logits (t=0,u=0) top-50 |torch|")
|
| 640 |
-
plt.legend(); plt.tight_layout(); plt.savefig(path); plt.close()
|
| 641 |
-
joint_plots["joint_top50.png"] = str(path.name)
|
| 642 |
-
|
| 643 |
-
# Delta-over-time visualization (fix u=0; summarize over vocab)
|
| 644 |
-
jt = logits_ref_np[0, :, 0, :]
|
| 645 |
-
jc = logits_ml[0, :, 0, :]
|
| 646 |
-
l2_t = np.linalg.norm(jt, axis=1)
|
| 647 |
-
l2_c = np.linalg.norm(jc, axis=1)
|
| 648 |
-
path2 = plots_dir / "joint_time_l2.png"
|
| 649 |
-
_plot_line(l2_t, l2_c, "Joint L2 over time (u=0)", path2, also_delta=True)
|
| 650 |
-
joint_plots["joint_time_l2.png"] = str(path2.name)
|
| 651 |
-
|
| 652 |
-
def _time_joint_coreml() -> Tuple[float, float]:
|
| 653 |
-
for _ in range(max(0, warmup)):
|
| 654 |
-
_joint_rollout_coreml(decoder_ref_np)
|
| 655 |
-
times = []
|
| 656 |
-
for _ in range(max(1, runs)):
|
| 657 |
-
t0 = time.perf_counter()
|
| 658 |
-
_joint_rollout_coreml(decoder_ref_np)
|
| 659 |
-
t1 = time.perf_counter()
|
| 660 |
-
times.append((t1 - t0) * 1000.0)
|
| 661 |
-
arr = np.array(times, dtype=np.float64)
|
| 662 |
-
return float(arr.mean()), float(arr.std(ddof=1) if arr.size > 1 else 0.0)
|
| 663 |
-
|
| 664 |
-
joint_torch_ms_mean, joint_torch_ms_std = _time_torch(
|
| 665 |
-
asr_model.joint, encoder_outputs=encoder_ref, decoder_outputs=decoder_ref
|
| 666 |
-
)
|
| 667 |
-
joint_coreml_ms_mean, joint_coreml_ms_std = _time_joint_coreml()
|
| 668 |
-
joint_coreml_rtf = float(joint_coreml_ms_mean / (seconds * 1000.0)) if joint_coreml_ms_mean > 0 else None
|
| 669 |
-
joint_torch_rtf = float(joint_torch_ms_mean / (seconds * 1000.0)) if joint_torch_ms_mean > 0 else None
|
| 670 |
-
|
| 671 |
-
summary["components"]["joint"] = {
|
| 672 |
-
"logits": {"max_abs": a_j, "max_rel": r_j, "match": bool(ok_j)},
|
| 673 |
-
"latency": {
|
| 674 |
-
"runs": int(runs),
|
| 675 |
-
"warmup": int(warmup),
|
| 676 |
-
"coreml_first_ms": joint_first_ms,
|
| 677 |
-
"torch_ms": {"mean": joint_torch_ms_mean, "std": joint_torch_ms_std},
|
| 678 |
-
"coreml_ms": {"mean": joint_coreml_ms_mean, "std": joint_coreml_ms_std},
|
| 679 |
-
"rtf": {"torch": joint_torch_rtf, "coreml": joint_coreml_rtf},
|
| 680 |
-
},
|
| 681 |
-
"plots": joint_plots,
|
| 682 |
-
}
|
| 683 |
-
|
| 684 |
-
# Fused components
|
| 685 |
-
# 1) Mel+Encoder fused vs separate
|
| 686 |
-
mel_enc_plots = {}
|
| 687 |
-
try:
|
| 688 |
-
mel_enc = ct.models.MLModel(str(output_dir / "parakeet_mel_encoder.mlpackage"), compute_units=ct.ComputeUnit.CPU_AND_NE)
|
| 689 |
-
t0 = time.perf_counter()
|
| 690 |
-
mel_enc_out = mel_enc.predict({
|
| 691 |
-
"audio_signal": _np32(audio_tensor),
|
| 692 |
-
"audio_length": _np32(audio_length).astype(np.int32),
|
| 693 |
-
})
|
| 694 |
-
t1 = time.perf_counter()
|
| 695 |
-
mel_enc_first_ms = (t1 - t0) * 1000.0
|
| 696 |
-
mel_enc_ml = np.array(mel_enc_out["encoder"], dtype=np.float32, copy=True)
|
| 697 |
-
mel_enc_len_ml = np.array(mel_enc_out["encoder_length"], dtype=np.int32, copy=True)
|
| 698 |
-
# Compare fused output vs Torch reference encoder
|
| 699 |
-
a_melenc, r_melenc, ok_melenc = _max_diffs(_np32(encoder_ref), mel_enc_ml, max(rtol, 5e-3), max(atol, 5e-2))
|
| 700 |
-
ok_melenc_len = int(_np32(encoder_length_ref).astype(np.int32)[0]) == int(mel_enc_len_ml.astype(np.int32)[0])
|
| 701 |
-
# Also compare fused vs separate CoreML pipeline (pre -> enc)
|
| 702 |
-
a_melenc_vs_sep, r_melenc_vs_sep, ok_melenc_vs_sep = _max_diffs(enc_ml, mel_enc_ml, max(rtol, 5e-3), max(atol, 5e-2))
|
| 703 |
-
|
| 704 |
-
# Plots: L2 over time (fused vs torch)
|
| 705 |
-
enc_t_ref = _np32(encoder_ref)[0]
|
| 706 |
-
enc_c_fused = mel_enc_ml[0]
|
| 707 |
-
mel_enc_plots["mel_encoder_time_l2.png"] = _plot_line(
|
| 708 |
-
np.linalg.norm(enc_t_ref, axis=0),
|
| 709 |
-
np.linalg.norm(enc_c_fused, axis=0),
|
| 710 |
-
"Mel+Encoder (fused) L2 over time",
|
| 711 |
-
plots_dir / "mel_encoder_time_l2.png",
|
| 712 |
-
also_delta=True,
|
| 713 |
-
)
|
| 714 |
-
|
| 715 |
-
# Latency: fused CoreML vs separate (CoreML pre + CoreML enc)
|
| 716 |
-
mel_enc_coreml_ms_mean, mel_enc_coreml_ms_std = _time_coreml(
|
| 717 |
-
mel_enc,
|
| 718 |
-
{"audio_signal": _np32(audio_tensor), "audio_length": _np32(audio_length).astype(np.int32)},
|
| 719 |
-
)
|
| 720 |
-
sep_coreml_ms_mean = float(pre_coreml_ms_mean + enc_coreml_ms_mean)
|
| 721 |
-
sep_coreml_ms_std = float((pre_coreml_ms_std ** 2 + enc_coreml_ms_std ** 2) ** 0.5)
|
| 722 |
-
# Torch baseline (separate torch pre + enc)
|
| 723 |
-
sep_torch_ms_mean = float(pre_torch_ms_mean + enc_torch_ms_mean)
|
| 724 |
-
sep_torch_ms_std = float((pre_torch_ms_std ** 2 + enc_torch_ms_std ** 2) ** 0.5)
|
| 725 |
-
|
| 726 |
-
mel_enc_coreml_rtf = float(mel_enc_coreml_ms_mean / (seconds * 1000.0)) if mel_enc_coreml_ms_mean > 0 else None
|
| 727 |
-
sep_coreml_rtf = float(sep_coreml_ms_mean / (seconds * 1000.0)) if sep_coreml_ms_mean > 0 else None
|
| 728 |
-
sep_torch_rtf = float(sep_torch_ms_mean / (seconds * 1000.0)) if sep_torch_ms_mean > 0 else None
|
| 729 |
-
|
| 730 |
-
summary["components"]["mel_encoder"] = {
|
| 731 |
-
"encoder": {"max_abs": a_melenc, "max_rel": r_melenc, "match": bool(ok_melenc)},
|
| 732 |
-
"length_match": bool(ok_melenc_len),
|
| 733 |
-
"vs_separate_coreml": {"max_abs": a_melenc_vs_sep, "max_rel": r_melenc_vs_sep, "match": bool(ok_melenc_vs_sep)},
|
| 734 |
-
"latency": {
|
| 735 |
-
"runs": int(runs),
|
| 736 |
-
"warmup": int(warmup),
|
| 737 |
-
"fused_coreml_first_ms": mel_enc_first_ms,
|
| 738 |
-
"fused_coreml_ms": {"mean": mel_enc_coreml_ms_mean, "std": mel_enc_coreml_ms_std},
|
| 739 |
-
"separate_coreml_ms": {"mean": sep_coreml_ms_mean, "std": sep_coreml_ms_std},
|
| 740 |
-
"separate_torch_ms": {"mean": sep_torch_ms_mean, "std": sep_torch_ms_std},
|
| 741 |
-
"rtf": {"fused_coreml": mel_enc_coreml_rtf, "separate_coreml": sep_coreml_rtf, "separate_torch": sep_torch_rtf},
|
| 742 |
-
},
|
| 743 |
-
"plots": {k: v for k, v in mel_enc_plots.items() if v},
|
| 744 |
-
}
|
| 745 |
-
except Exception as e:
|
| 746 |
-
summary["components"]["mel_encoder_error"] = str(e)
|
| 747 |
-
|
| 748 |
-
# 2) JointDecision fused vs CPU PyTorch post-processing
|
| 749 |
-
jd_plots = {}
|
| 750 |
-
try:
|
| 751 |
-
# Fused CoreML joint decision
|
| 752 |
-
jd = ct.models.MLModel(str(output_dir / "parakeet_joint_decision.mlpackage"), compute_units=ct.ComputeUnit.CPU_AND_NE)
|
| 753 |
-
def _joint_decision_rollout_coreml(decoder_seq_np: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray, float]:
|
| 754 |
-
token_ids = []
|
| 755 |
-
token_probs = []
|
| 756 |
-
durations = []
|
| 757 |
-
first_ms: Optional[float] = None
|
| 758 |
-
for u in range(decoder_seq_np.shape[2]):
|
| 759 |
-
dec_slice = decoder_seq_np[:, :, u : u + 1]
|
| 760 |
-
t0_u = time.perf_counter() if u == 0 else None
|
| 761 |
-
res = jd.predict({"encoder": encoder_np, "decoder": dec_slice})
|
| 762 |
-
if t0_u is not None:
|
| 763 |
-
t1_u = time.perf_counter()
|
| 764 |
-
first_ms = (t1_u - t0_u) * 1000.0
|
| 765 |
-
token_ids.append(np.array(res["token_id"], dtype=np.int32, copy=True))
|
| 766 |
-
token_probs.append(np.array(res["token_prob"], dtype=np.float32, copy=True))
|
| 767 |
-
durations.append(np.array(res["duration"], dtype=np.int32, copy=True))
|
| 768 |
-
if not token_ids:
|
| 769 |
-
raise RuntimeError("No decoder steps provided for joint decision rollout")
|
| 770 |
-
return (
|
| 771 |
-
np.concatenate(token_ids, axis=2),
|
| 772 |
-
np.concatenate(token_probs, axis=2),
|
| 773 |
-
np.concatenate(durations, axis=2),
|
| 774 |
-
(0.0 if first_ms is None else float(first_ms)),
|
| 775 |
-
)
|
| 776 |
-
|
| 777 |
-
token_id_ml, token_prob_ml, duration_ml, jd_first_ms = _joint_decision_rollout_coreml(decoder_ref_np)
|
| 778 |
-
|
| 779 |
-
# CPU PyTorch decision using Torch logits
|
| 780 |
-
vocab_with_blank = int(vocab_size) + 1
|
| 781 |
-
with torch.inference_mode():
|
| 782 |
-
logits_t = logits_ref
|
| 783 |
-
token_logits_t = logits_t[..., :vocab_with_blank]
|
| 784 |
-
duration_logits_t = logits_t[..., -num_extra:] if num_extra > 0 else None
|
| 785 |
-
token_ids_t = torch.argmax(token_logits_t, dim=-1).to(dtype=torch.int32)
|
| 786 |
-
token_probs_all_t = torch.softmax(token_logits_t, dim=-1)
|
| 787 |
-
token_prob_t = torch.gather(
|
| 788 |
-
token_probs_all_t, dim=-1, index=token_ids_t.long().unsqueeze(-1)
|
| 789 |
-
).squeeze(-1)
|
| 790 |
-
if duration_logits_t is not None and duration_logits_t.numel() > 0:
|
| 791 |
-
duration_t = torch.argmax(duration_logits_t, dim=-1).to(dtype=torch.int32)
|
| 792 |
-
else:
|
| 793 |
-
duration_t = torch.zeros_like(token_ids_t, dtype=torch.int32)
|
| 794 |
-
|
| 795 |
-
# Also derive CPU decision from CoreML joint logits for "separate" path
|
| 796 |
-
token_logits_c = _to_t(logits_ml)[..., :vocab_with_blank]
|
| 797 |
-
duration_logits_c = _to_t(logits_ml)[..., -num_extra:] if num_extra > 0 else None
|
| 798 |
-
token_ids_c = torch.argmax(token_logits_c, dim=-1).to(dtype=torch.int32)
|
| 799 |
-
token_probs_all_c = torch.softmax(token_logits_c, dim=-1)
|
| 800 |
-
token_prob_c = torch.gather(
|
| 801 |
-
token_probs_all_c, dim=-1, index=token_ids_c.long().unsqueeze(-1)
|
| 802 |
-
).squeeze(-1)
|
| 803 |
-
if duration_logits_c is not None and duration_logits_c.numel() > 0:
|
| 804 |
-
duration_c = torch.argmax(duration_logits_c, dim=-1).to(dtype=torch.int32)
|
| 805 |
-
else:
|
| 806 |
-
duration_c = torch.zeros_like(token_ids_c, dtype=torch.int32)
|
| 807 |
-
|
| 808 |
-
# Compare fused outputs to CPU PyTorch decisions
|
| 809 |
-
a_tid_t, r_tid_t, ok_tid_t = _max_diffs(_np(token_ids_t), token_id_ml, 0.0, 0.0)
|
| 810 |
-
a_tprob_t, r_tprob_t, ok_tprob_t = _max_diffs(_np(token_prob_t), token_prob_ml, max(rtol, 1e-2), max(atol, 1e-1))
|
| 811 |
-
a_dur_t, r_dur_t, ok_dur_t = _max_diffs(_np(duration_t), duration_ml, 0.0, 0.0)
|
| 812 |
-
|
| 813 |
-
a_tid_c, r_tid_c, ok_tid_c = _max_diffs(_np(token_ids_c), token_id_ml, 0.0, 0.0)
|
| 814 |
-
a_tprob_c, r_tprob_c, ok_tprob_c = _max_diffs(_np(token_prob_c), token_prob_ml, max(rtol, 1e-2), max(atol, 1e-1))
|
| 815 |
-
a_dur_c, r_dur_c, ok_dur_c = _max_diffs(_np(duration_c), duration_ml, 0.0, 0.0)
|
| 816 |
-
|
| 817 |
-
# Plots: token_prob over time for u=0 (fused vs torch CPU)
|
| 818 |
-
if HAS_MPL:
|
| 819 |
-
prob_t = _np(token_prob_t)[0, :, 0]
|
| 820 |
-
prob_ml = token_prob_ml[0, :, 0]
|
| 821 |
-
jd_plots["joint_decision_prob_u0.png"] = _plot_line(
|
| 822 |
-
prob_t,
|
| 823 |
-
prob_ml,
|
| 824 |
-
"JointDecision token_prob (u=0)",
|
| 825 |
-
plots_dir / "joint_decision_prob_u0.png",
|
| 826 |
-
also_delta=True,
|
| 827 |
-
)
|
| 828 |
-
|
| 829 |
-
# Agreement heatmap for token_id
|
| 830 |
-
agree = (_np(token_ids_t)[0] == token_id_ml[0]).astype(np.float32)
|
| 831 |
-
jd_plots["joint_decision_token_agree.png"] = _plot_image(
|
| 832 |
-
agree,
|
| 833 |
-
"token_id agreement (torch CPU vs fused)",
|
| 834 |
-
plots_dir / "joint_decision_token_agree.png",
|
| 835 |
-
vmin=0.0,
|
| 836 |
-
vmax=1.0,
|
| 837 |
-
)
|
| 838 |
-
|
| 839 |
-
# Latency: fused CoreML vs separate (CoreML joint + CPU PyTorch decision)
|
| 840 |
-
def _time_joint_decision_coreml() -> Tuple[float, float]:
|
| 841 |
-
for _ in range(max(0, warmup)):
|
| 842 |
-
_joint_decision_rollout_coreml(decoder_ref_np)
|
| 843 |
-
times = []
|
| 844 |
-
for _ in range(max(1, runs)):
|
| 845 |
-
t0 = time.perf_counter()
|
| 846 |
-
_joint_decision_rollout_coreml(decoder_ref_np)
|
| 847 |
-
t1 = time.perf_counter()
|
| 848 |
-
times.append((t1 - t0) * 1000.0)
|
| 849 |
-
arr = np.array(times, dtype=np.float64)
|
| 850 |
-
return float(arr.mean()), float(arr.std(ddof=1) if arr.size > 1 else 0.0)
|
| 851 |
-
|
| 852 |
-
jd_coreml_ms_mean, jd_coreml_ms_std = _time_joint_decision_coreml()
|
| 853 |
-
|
| 854 |
-
# Time CPU post-processing only (Torch) on top of CoreML or Torch logits. Use Torch logits.
|
| 855 |
-
def _decision_torch_call():
|
| 856 |
-
with torch.inference_mode():
|
| 857 |
-
tl = logits_ref
|
| 858 |
-
tl_token = tl[..., :vocab_with_blank]
|
| 859 |
-
tl_ids = torch.argmax(tl_token, dim=-1)
|
| 860 |
-
tl_probs = torch.softmax(tl_token, dim=-1)
|
| 861 |
-
_ = torch.gather(tl_probs, -1, tl_ids.long().unsqueeze(-1)).squeeze(-1)
|
| 862 |
-
if num_extra > 0:
|
| 863 |
-
_ = torch.argmax(tl[..., -num_extra:], dim=-1)
|
| 864 |
-
return None
|
| 865 |
-
|
| 866 |
-
jd_decision_torch_ms_mean, jd_decision_torch_ms_std = _time_torch(lambda: _decision_torch_call())
|
| 867 |
-
sep_joint_plus_cpu_ms_mean = float(joint_coreml_ms_mean + jd_decision_torch_ms_mean)
|
| 868 |
-
sep_joint_plus_cpu_ms_std = float((joint_coreml_ms_std ** 2 + jd_decision_torch_ms_std ** 2) ** 0.5)
|
| 869 |
-
jd_coreml_rtf = float(jd_coreml_ms_mean / (seconds * 1000.0)) if jd_coreml_ms_mean > 0 else None
|
| 870 |
-
sep_joint_cpu_rtf = float(sep_joint_plus_cpu_ms_mean / (seconds * 1000.0)) if sep_joint_plus_cpu_ms_mean > 0 else None
|
| 871 |
-
|
| 872 |
-
summary["components"]["joint_decision"] = {
|
| 873 |
-
"vs_torch_cpu": {
|
| 874 |
-
"token_id": {"max_abs": a_tid_t, "max_rel": r_tid_t, "match": bool(ok_tid_t)},
|
| 875 |
-
"token_prob": {"max_abs": a_tprob_t, "max_rel": r_tprob_t, "match": bool(ok_tprob_t)},
|
| 876 |
-
"duration": {"max_abs": a_dur_t, "max_rel": r_dur_t, "match": bool(ok_dur_t)},
|
| 877 |
-
},
|
| 878 |
-
"vs_coreml_joint_cpu": {
|
| 879 |
-
"token_id": {"max_abs": a_tid_c, "max_rel": r_tid_c, "match": bool(ok_tid_c)},
|
| 880 |
-
"token_prob": {"max_abs": a_tprob_c, "max_rel": r_tprob_c, "match": bool(ok_tprob_c)},
|
| 881 |
-
"duration": {"max_abs": a_dur_c, "max_rel": r_dur_c, "match": bool(ok_dur_c)},
|
| 882 |
-
},
|
| 883 |
-
"latency": {
|
| 884 |
-
"runs": int(runs),
|
| 885 |
-
"warmup": int(warmup),
|
| 886 |
-
"fused_coreml_first_ms": jd_first_ms,
|
| 887 |
-
"fused_coreml_ms": {"mean": jd_coreml_ms_mean, "std": jd_coreml_ms_std},
|
| 888 |
-
"separate_joint_coreml_plus_cpu_ms": {"mean": sep_joint_plus_cpu_ms_mean, "std": sep_joint_plus_cpu_ms_std},
|
| 889 |
-
"rtf": {"fused_coreml": jd_coreml_rtf, "separate_joint_coreml_plus_cpu": sep_joint_cpu_rtf},
|
| 890 |
-
},
|
| 891 |
-
"plots": {k: v for k, v in jd_plots.items() if v},
|
| 892 |
-
}
|
| 893 |
-
except Exception as e:
|
| 894 |
-
summary["components"]["joint_decision_error"] = str(e)
|
| 895 |
-
|
| 896 |
-
# Latency overview plots (saved alongside component plots)
|
| 897 |
-
latency_plots = {}
|
| 898 |
-
labels = ["preprocessor", "encoder", "decoder", "joint"]
|
| 899 |
-
torch_means = [pre_torch_ms_mean, enc_torch_ms_mean, dec_torch_ms_mean, joint_torch_ms_mean]
|
| 900 |
-
torch_stds = [pre_torch_ms_std, enc_torch_ms_std, dec_torch_ms_std, joint_torch_ms_std]
|
| 901 |
-
coreml_means = [pre_coreml_ms_mean, enc_coreml_ms_mean, dec_coreml_ms_mean, joint_coreml_ms_mean]
|
| 902 |
-
coreml_stds = [pre_coreml_ms_std, enc_coreml_ms_std, dec_coreml_ms_std, joint_coreml_ms_std]
|
| 903 |
-
lat_path = plots_dir / "latency_summary.png"
|
| 904 |
-
spd_path = plots_dir / "latency_speedup.png"
|
| 905 |
-
latency_plots["latency_summary.png"] = _plot_latency_bars(
|
| 906 |
-
labels, torch_means, torch_stds, coreml_means, coreml_stds, lat_path
|
| 907 |
-
)
|
| 908 |
-
latency_plots["latency_speedup.png"] = _plot_speedup_bars(
|
| 909 |
-
labels, torch_means, coreml_means, spd_path
|
| 910 |
-
)
|
| 911 |
-
|
| 912 |
-
# Fused vs separate latency summary
|
| 913 |
-
fused_labels = ["mel+encoder", "joint_decision"]
|
| 914 |
-
fused_baseline_means = [
|
| 915 |
-
float(pre_torch_ms_mean + enc_torch_ms_mean),
|
| 916 |
-
float(joint_coreml_ms_mean + jd_decision_torch_ms_mean if 'jd_coreml_ms_mean' in locals() else joint_coreml_ms_mean),
|
| 917 |
-
]
|
| 918 |
-
fused_coreml_means = [
|
| 919 |
-
float(mel_enc_coreml_ms_mean if 'mel_enc_coreml_ms_mean' in locals() else np.nan),
|
| 920 |
-
float(jd_coreml_ms_mean if 'jd_coreml_ms_mean' in locals() else np.nan),
|
| 921 |
-
]
|
| 922 |
-
fused_latency_path = plots_dir / "latency_fused_vs_separate.png"
|
| 923 |
-
fused_speedup_path = plots_dir / "latency_fused_speedup.png"
|
| 924 |
-
latency_plots["latency_fused_vs_separate.png"] = _plot_latency_bars(
|
| 925 |
-
fused_labels, fused_baseline_means, [0, 0], fused_coreml_means, [0, 0], fused_latency_path
|
| 926 |
-
)
|
| 927 |
-
latency_plots["latency_fused_speedup.png"] = _plot_speedup_bars(
|
| 928 |
-
fused_labels, fused_baseline_means, fused_coreml_means, fused_speedup_path
|
| 929 |
-
)
|
| 930 |
-
|
| 931 |
-
all_ok = (
|
| 932 |
-
summary["components"]["preprocessor"]["mel"]["match"]
|
| 933 |
-
and summary["components"]["preprocessor"]["length_match"]
|
| 934 |
-
and summary["components"]["encoder"]["encoder"]["match"]
|
| 935 |
-
and summary["components"]["encoder"]["length_match"]
|
| 936 |
-
and summary["components"]["decoder"]["decoder"]["match"]
|
| 937 |
-
and summary["components"]["decoder"]["h_out"]["match"]
|
| 938 |
-
and summary["components"]["decoder"]["c_out"]["match"]
|
| 939 |
-
and summary["components"]["joint"]["logits"]["match"]
|
| 940 |
-
)
|
| 941 |
-
summary["status"] = "ok" if all_ok else "mismatch"
|
| 942 |
-
|
| 943 |
-
# Update metadata.json
|
| 944 |
-
meta_path = output_dir / "metadata.json"
|
| 945 |
-
try:
|
| 946 |
-
meta = json.loads(meta_path.read_text())
|
| 947 |
-
except Exception:
|
| 948 |
-
meta = {}
|
| 949 |
-
meta["validation"] = summary
|
| 950 |
-
meta_path.write_text(json.dumps(meta, indent=2))
|
| 951 |
-
|
| 952 |
-
typer.echo(f"Validation {'passed' if all_ok else 'mismatched'}. Updated {meta_path}")
|
| 953 |
-
if HAS_MPL:
|
| 954 |
-
typer.echo(f"Saved plots to {plots_dir}")
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
if __name__ == "__main__":
|
| 958 |
-
app()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
convert/parakeet-tdt-v2-0.6b/coreml/compile_modelc.py
DELETED
|
@@ -1,91 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Compile Core ML packages into ``.mlmodelc`` bundles via ``xcrun``.
|
| 3 |
-
|
| 4 |
-
This script walks through the default Parakeet CoreML directories, finds
|
| 5 |
-
all ``*.mlpackage`` bundles, and compiles each of them with
|
| 6 |
-
``xcrun coremlcompiler`` into ``./compiled`` while preserving the
|
| 7 |
-
relative directory structure.
|
| 8 |
-
"""
|
| 9 |
-
from __future__ import annotations
|
| 10 |
-
|
| 11 |
-
import shutil
|
| 12 |
-
import subprocess
|
| 13 |
-
import sys
|
| 14 |
-
from pathlib import Path
|
| 15 |
-
|
| 16 |
-
BASE_DIR = Path(__file__).resolve().parent
|
| 17 |
-
OUTPUT_ROOT = BASE_DIR / "compiled"
|
| 18 |
-
SOURCE_DIRS = [BASE_DIR / "parakeet_coreml", BASE_DIR / "parakeet_coreml_quantized"]
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def ensure_coremlcompiler() -> None:
|
| 22 |
-
"""Ensure ``xcrun coremlcompiler`` is available for the active Xcode."""
|
| 23 |
-
xcrun_path = shutil.which("xcrun")
|
| 24 |
-
if xcrun_path is None:
|
| 25 |
-
print("Error: 'xcrun' not found on PATH. Install Xcode command line tools.", file=sys.stderr)
|
| 26 |
-
sys.exit(1)
|
| 27 |
-
|
| 28 |
-
try:
|
| 29 |
-
subprocess.run([
|
| 30 |
-
xcrun_path,
|
| 31 |
-
"--find",
|
| 32 |
-
"coremlcompiler",
|
| 33 |
-
], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
| 34 |
-
except subprocess.CalledProcessError:
|
| 35 |
-
print("Error: 'coremlcompiler' not found via xcrun. Check your Xcode installation.", file=sys.stderr)
|
| 36 |
-
sys.exit(1)
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def gather_packages() -> list[Path]:
|
| 40 |
-
"""Return a list of all ``*.mlpackage`` bundles under the source dirs."""
|
| 41 |
-
packages: list[Path] = []
|
| 42 |
-
for source in SOURCE_DIRS:
|
| 43 |
-
if not source.exists():
|
| 44 |
-
print(f"Warning: {source.relative_to(BASE_DIR)} does not exist; skipping", file=sys.stderr)
|
| 45 |
-
continue
|
| 46 |
-
packages.extend(source.rglob("*.mlpackage"))
|
| 47 |
-
return packages
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def compile_package(package: Path) -> None:
|
| 51 |
-
"""Compile a single ``.mlpackage`` bundle using ``xcrun coremlcompiler``."""
|
| 52 |
-
relative_pkg = package.relative_to(BASE_DIR)
|
| 53 |
-
output_dir = OUTPUT_ROOT / relative_pkg.parent
|
| 54 |
-
output_dir.mkdir(parents=True, exist_ok=True)
|
| 55 |
-
output_path = output_dir / f"{package.stem}.mlmodelc"
|
| 56 |
-
|
| 57 |
-
if output_path.exists():
|
| 58 |
-
shutil.rmtree(output_path)
|
| 59 |
-
|
| 60 |
-
cmd = [
|
| 61 |
-
"xcrun",
|
| 62 |
-
"coremlcompiler",
|
| 63 |
-
"compile",
|
| 64 |
-
str(package),
|
| 65 |
-
str(output_dir),
|
| 66 |
-
]
|
| 67 |
-
|
| 68 |
-
print(f"Compiling {relative_pkg} -> {output_path.relative_to(BASE_DIR)}")
|
| 69 |
-
subprocess.run(cmd, check=True)
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
def main() -> None:
|
| 73 |
-
ensure_coremlcompiler()
|
| 74 |
-
packages = gather_packages()
|
| 75 |
-
|
| 76 |
-
if not packages:
|
| 77 |
-
print("No .mlpackage bundles found to compile.")
|
| 78 |
-
return
|
| 79 |
-
|
| 80 |
-
for package in packages:
|
| 81 |
-
try:
|
| 82 |
-
compile_package(package)
|
| 83 |
-
except subprocess.CalledProcessError as exc:
|
| 84 |
-
print(f"Failed to compile {package}: {exc}", file=sys.stderr)
|
| 85 |
-
sys.exit(exc.returncode)
|
| 86 |
-
|
| 87 |
-
print(f"Finished compiling {len(packages)} package(s) into {OUTPUT_ROOT.relative_to(BASE_DIR)}.")
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
if __name__ == "__main__":
|
| 91 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
convert/parakeet-tdt-v2-0.6b/coreml/convert-parakeet.py
DELETED
|
@@ -1,619 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""CLI for exporting Parakeet TDT v2 components to CoreML."""
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import json
|
| 6 |
-
from dataclasses import asdict
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
from typing import Dict, Optional, Tuple
|
| 9 |
-
|
| 10 |
-
import coremltools as ct
|
| 11 |
-
import numpy as np
|
| 12 |
-
import soundfile as sf
|
| 13 |
-
import torch
|
| 14 |
-
import typer
|
| 15 |
-
|
| 16 |
-
import nemo.collections.asr as nemo_asr
|
| 17 |
-
|
| 18 |
-
from individual_components import (
|
| 19 |
-
DecoderWrapper,
|
| 20 |
-
EncoderWrapper,
|
| 21 |
-
ExportSettings,
|
| 22 |
-
JointWrapper,
|
| 23 |
-
JointDecisionWrapper,
|
| 24 |
-
JointDecisionSingleStep,
|
| 25 |
-
PreprocessorWrapper,
|
| 26 |
-
MelEncoderWrapper,
|
| 27 |
-
_coreml_convert,
|
| 28 |
-
)
|
| 29 |
-
|
| 30 |
-
DEFAULT_MODEL_ID = "nvidia/parakeet-tdt-0.6b-v2"
|
| 31 |
-
AUTHOR = "Fluid Inference"
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
def _compute_length(seconds: float, sample_rate: int) -> int:
|
| 35 |
-
return int(round(seconds * sample_rate))
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
def _prepare_audio(
|
| 39 |
-
validation_audio: Optional[Path],
|
| 40 |
-
sample_rate: int,
|
| 41 |
-
max_samples: int,
|
| 42 |
-
seed: Optional[int],
|
| 43 |
-
) -> torch.Tensor:
|
| 44 |
-
if validation_audio is None:
|
| 45 |
-
if seed is not None:
|
| 46 |
-
torch.manual_seed(seed)
|
| 47 |
-
audio = torch.randn(1, max_samples, dtype=torch.float32)
|
| 48 |
-
return audio
|
| 49 |
-
|
| 50 |
-
data, sr = sf.read(str(validation_audio), dtype="float32")
|
| 51 |
-
if sr != sample_rate:
|
| 52 |
-
raise typer.BadParameter(
|
| 53 |
-
f"Validation audio sample rate {sr} does not match model rate {sample_rate}"
|
| 54 |
-
)
|
| 55 |
-
|
| 56 |
-
if data.ndim > 1:
|
| 57 |
-
data = data[:, 0]
|
| 58 |
-
|
| 59 |
-
if data.size == 0:
|
| 60 |
-
raise typer.BadParameter("Validation audio is empty")
|
| 61 |
-
|
| 62 |
-
if data.size < max_samples:
|
| 63 |
-
pad_width = max_samples - data.size
|
| 64 |
-
data = np.pad(data, (0, pad_width))
|
| 65 |
-
elif data.size > max_samples:
|
| 66 |
-
data = data[:max_samples]
|
| 67 |
-
|
| 68 |
-
audio = torch.from_numpy(data).unsqueeze(0).to(dtype=torch.float32)
|
| 69 |
-
return audio
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
def _save_mlpackage(model: ct.models.MLModel, path: Path, description: str) -> None:
|
| 73 |
-
# Ensure iOS 17+ target for MLProgram ops and ANE readiness
|
| 74 |
-
try:
|
| 75 |
-
model.minimum_deployment_target = ct.target.iOS17
|
| 76 |
-
except Exception:
|
| 77 |
-
pass
|
| 78 |
-
model.short_description = description
|
| 79 |
-
model.author = AUTHOR
|
| 80 |
-
path.parent.mkdir(parents=True, exist_ok=True)
|
| 81 |
-
model.save(str(path))
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
def _tensor_shape(tensor: torch.Tensor) -> Tuple[int, ...]:
|
| 85 |
-
return tuple(int(dim) for dim in tensor.shape)
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
def _parse_compute_units(name: str) -> ct.ComputeUnit:
|
| 89 |
-
"""Parse a human-friendly compute units string into ct.ComputeUnit.
|
| 90 |
-
|
| 91 |
-
Accepted (case-insensitive): ALL, CPU_ONLY, CPU_AND_GPU, CPU_AND_NE.
|
| 92 |
-
"""
|
| 93 |
-
normalized = str(name).strip().upper()
|
| 94 |
-
mapping = {
|
| 95 |
-
"ALL": ct.ComputeUnit.ALL,
|
| 96 |
-
"CPU_ONLY": ct.ComputeUnit.CPU_ONLY,
|
| 97 |
-
"CPU_AND_GPU": ct.ComputeUnit.CPU_AND_GPU,
|
| 98 |
-
"CPU_AND_NE": ct.ComputeUnit.CPU_AND_NE,
|
| 99 |
-
"CPU_AND_NEURALENGINE": ct.ComputeUnit.CPU_AND_NE,
|
| 100 |
-
}
|
| 101 |
-
if normalized not in mapping:
|
| 102 |
-
raise typer.BadParameter(
|
| 103 |
-
f"Unknown compute units '{name}'. Choose from: " + ", ".join(mapping.keys())
|
| 104 |
-
)
|
| 105 |
-
return mapping[normalized]
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
def _parse_compute_precision(name: Optional[str]) -> Optional[ct.precision]:
|
| 109 |
-
"""Parse compute precision string into ct.precision or None.
|
| 110 |
-
|
| 111 |
-
Accepted (case-insensitive): FLOAT32, FLOAT16. If None/empty, returns None (tool default).
|
| 112 |
-
"""
|
| 113 |
-
if name is None:
|
| 114 |
-
return None
|
| 115 |
-
normalized = str(name).strip().upper()
|
| 116 |
-
if normalized == "":
|
| 117 |
-
return None
|
| 118 |
-
mapping = {
|
| 119 |
-
"FLOAT32": ct.precision.FLOAT32,
|
| 120 |
-
"FLOAT16": ct.precision.FLOAT16,
|
| 121 |
-
}
|
| 122 |
-
if normalized not in mapping:
|
| 123 |
-
raise typer.BadParameter(
|
| 124 |
-
f"Unknown compute precision '{name}'. Choose from: " + ", ".join(mapping.keys())
|
| 125 |
-
)
|
| 126 |
-
return mapping[normalized]
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
# Validation logic removed; use compare-compnents.py for comparisons.
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
# Fixed export choices: CPU_ONLY + FP32, min target iOS17
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
@app.command()
|
| 139 |
-
def convert(
|
| 140 |
-
nemo_path: Optional[Path] = typer.Option(
|
| 141 |
-
None,
|
| 142 |
-
"--nemo-path",
|
| 143 |
-
exists=True,
|
| 144 |
-
resolve_path=True,
|
| 145 |
-
help="Path to parakeet-tdt-0.6b-v2 .nemo checkpoint (skip to auto-download)",
|
| 146 |
-
),
|
| 147 |
-
model_id: str = typer.Option(
|
| 148 |
-
DEFAULT_MODEL_ID,
|
| 149 |
-
"--model-id",
|
| 150 |
-
help="Model identifier to download when --nemo-path is omitted",
|
| 151 |
-
),
|
| 152 |
-
output_dir: Path = typer.Option(Path("parakeet_coreml"), help="Directory where mlpackages and metadata will be written"),
|
| 153 |
-
preprocessor_cu: str = typer.Option(
|
| 154 |
-
"CPU_ONLY",
|
| 155 |
-
"--preprocessor-cu",
|
| 156 |
-
help="Compute units for preprocessor (default CPU_ONLY)",
|
| 157 |
-
),
|
| 158 |
-
mel_encoder_cu: str = typer.Option(
|
| 159 |
-
"CPU_ONLY",
|
| 160 |
-
"--mel-encoder-cu",
|
| 161 |
-
help="Compute units for fused mel+encoder (default CPU_ONLY)",
|
| 162 |
-
),
|
| 163 |
-
compute_precision: Optional[str] = typer.Option(
|
| 164 |
-
None,
|
| 165 |
-
"--compute-precision",
|
| 166 |
-
help="Export precision: FLOAT32 (default) or FLOAT16 to shrink non-quantized weights.",
|
| 167 |
-
),
|
| 168 |
-
) -> None:
|
| 169 |
-
"""Export all Parakeet sub-modules to CoreML with a fixed 15-second window."""
|
| 170 |
-
# Runtime CoreML contract keeps U=1 so the prediction net matches the streaming decoder.
|
| 171 |
-
export_settings = ExportSettings(
|
| 172 |
-
output_dir=output_dir,
|
| 173 |
-
compute_units=ct.ComputeUnit.CPU_ONLY, # Default: CPU-only for all components
|
| 174 |
-
deployment_target=ct.target.iOS17, # iOS 17+ features and kernels
|
| 175 |
-
compute_precision=_parse_compute_precision(compute_precision),
|
| 176 |
-
max_audio_seconds=15.0,
|
| 177 |
-
max_symbol_steps=1,
|
| 178 |
-
)
|
| 179 |
-
|
| 180 |
-
typer.echo("Export configuration:")
|
| 181 |
-
typer.echo(asdict(export_settings))
|
| 182 |
-
|
| 183 |
-
output_dir.mkdir(parents=True, exist_ok=True)
|
| 184 |
-
pre_cu = _parse_compute_units(preprocessor_cu)
|
| 185 |
-
melenc_cu = _parse_compute_units(mel_encoder_cu)
|
| 186 |
-
|
| 187 |
-
if nemo_path is not None:
|
| 188 |
-
typer.echo(f"Loading NeMo model from {nemo_path}…")
|
| 189 |
-
asr_model = nemo_asr.models.EncDecRNNTBPEModel.restore_from(
|
| 190 |
-
str(nemo_path), map_location="cpu"
|
| 191 |
-
)
|
| 192 |
-
checkpoint_meta = {
|
| 193 |
-
"type": "file",
|
| 194 |
-
"path": str(nemo_path),
|
| 195 |
-
}
|
| 196 |
-
else:
|
| 197 |
-
typer.echo(f"Downloading NeMo model via {model_id}…")
|
| 198 |
-
asr_model = nemo_asr.models.EncDecRNNTBPEModel.from_pretrained(
|
| 199 |
-
model_id, map_location="cpu"
|
| 200 |
-
)
|
| 201 |
-
checkpoint_meta = {
|
| 202 |
-
"type": "pretrained",
|
| 203 |
-
"model_id": model_id,
|
| 204 |
-
}
|
| 205 |
-
asr_model.eval()
|
| 206 |
-
|
| 207 |
-
sample_rate = int(asr_model.cfg.preprocessor.sample_rate)
|
| 208 |
-
max_samples = _compute_length(export_settings.max_audio_seconds, sample_rate)
|
| 209 |
-
# Prefer a bundled 15s 16kHz audio if available
|
| 210 |
-
default_audio = (Path(__file__).parent / "audio" / "yc_first_minute_16k_15s.wav").resolve()
|
| 211 |
-
if not default_audio.exists():
|
| 212 |
-
raise typer.BadParameter(f"Expected 15s trace audio at {default_audio}; add the file to proceed.")
|
| 213 |
-
typer.echo(f"Using trace audio: {default_audio}")
|
| 214 |
-
audio_tensor = _prepare_audio(default_audio, sample_rate, max_samples, seed=None)
|
| 215 |
-
audio_length = torch.tensor([max_samples], dtype=torch.int32)
|
| 216 |
-
|
| 217 |
-
preprocessor = PreprocessorWrapper(asr_model.preprocessor.eval())
|
| 218 |
-
encoder = EncoderWrapper(asr_model.encoder.eval())
|
| 219 |
-
decoder = DecoderWrapper(asr_model.decoder.eval())
|
| 220 |
-
joint = JointWrapper(asr_model.joint.eval())
|
| 221 |
-
|
| 222 |
-
decoder_export_flag = getattr(asr_model.decoder, "_rnnt_export", False)
|
| 223 |
-
asr_model.decoder._rnnt_export = True
|
| 224 |
-
|
| 225 |
-
try:
|
| 226 |
-
with torch.inference_mode():
|
| 227 |
-
mel_ref, mel_length_ref = preprocessor(audio_tensor, audio_length)
|
| 228 |
-
mel_length_ref = mel_length_ref.to(dtype=torch.int32)
|
| 229 |
-
encoder_ref, encoder_length_ref = encoder(mel_ref, mel_length_ref)
|
| 230 |
-
encoder_length_ref = encoder_length_ref.to(dtype=torch.int32)
|
| 231 |
-
|
| 232 |
-
# Clone Tensors to drop the inference tensor flag before tracing
|
| 233 |
-
mel_ref = mel_ref.clone()
|
| 234 |
-
mel_length_ref = mel_length_ref.clone()
|
| 235 |
-
encoder_ref = encoder_ref.clone()
|
| 236 |
-
encoder_length_ref = encoder_length_ref.clone()
|
| 237 |
-
|
| 238 |
-
vocab_size = int(asr_model.tokenizer.vocab_size)
|
| 239 |
-
num_extra = int(asr_model.joint.num_extra_outputs)
|
| 240 |
-
decoder_hidden = int(asr_model.decoder.pred_hidden)
|
| 241 |
-
decoder_layers = int(asr_model.decoder.pred_rnn_layers)
|
| 242 |
-
|
| 243 |
-
targets = torch.full(
|
| 244 |
-
(1, export_settings.max_symbol_steps),
|
| 245 |
-
fill_value=asr_model.decoder.blank_idx,
|
| 246 |
-
dtype=torch.int32,
|
| 247 |
-
)
|
| 248 |
-
target_lengths = torch.tensor(
|
| 249 |
-
[export_settings.max_symbol_steps], dtype=torch.int32
|
| 250 |
-
)
|
| 251 |
-
zero_state = torch.zeros(
|
| 252 |
-
decoder_layers,
|
| 253 |
-
1,
|
| 254 |
-
decoder_hidden,
|
| 255 |
-
dtype=torch.float32,
|
| 256 |
-
)
|
| 257 |
-
|
| 258 |
-
with torch.inference_mode():
|
| 259 |
-
decoder_ref, h_ref, c_ref = decoder(targets, target_lengths, zero_state, zero_state)
|
| 260 |
-
joint_ref = joint(encoder_ref, decoder_ref)
|
| 261 |
-
|
| 262 |
-
decoder_ref = decoder_ref.clone()
|
| 263 |
-
h_ref = h_ref.clone()
|
| 264 |
-
c_ref = c_ref.clone()
|
| 265 |
-
joint_ref = joint_ref.clone()
|
| 266 |
-
|
| 267 |
-
typer.echo("Tracing and converting preprocessor…")
|
| 268 |
-
# Ensure tracing happens on CPU explicitly
|
| 269 |
-
preprocessor = preprocessor.cpu()
|
| 270 |
-
audio_tensor = audio_tensor.cpu()
|
| 271 |
-
audio_length = audio_length.cpu()
|
| 272 |
-
traced_preprocessor = torch.jit.trace(
|
| 273 |
-
preprocessor, (audio_tensor, audio_length), strict=False
|
| 274 |
-
)
|
| 275 |
-
traced_preprocessor.eval()
|
| 276 |
-
preprocessor_inputs = [
|
| 277 |
-
# Allow variable-length audio up to the fixed 15s window using RangeDim
|
| 278 |
-
ct.TensorType(
|
| 279 |
-
name="audio_signal",
|
| 280 |
-
shape=(1, ct.RangeDim(1, max_samples)),
|
| 281 |
-
dtype=np.float32,
|
| 282 |
-
),
|
| 283 |
-
ct.TensorType(name="audio_length", shape=(1,), dtype=np.int32),
|
| 284 |
-
]
|
| 285 |
-
preprocessor_outputs = [
|
| 286 |
-
ct.TensorType(name="mel", dtype=np.float32),
|
| 287 |
-
ct.TensorType(name="mel_length", dtype=np.int32),
|
| 288 |
-
]
|
| 289 |
-
# Preprocessor compute units (parametrized; default CPU_ONLY)
|
| 290 |
-
preprocessor_model = _coreml_convert(
|
| 291 |
-
traced_preprocessor,
|
| 292 |
-
preprocessor_inputs,
|
| 293 |
-
preprocessor_outputs,
|
| 294 |
-
export_settings,
|
| 295 |
-
compute_units_override=pre_cu,
|
| 296 |
-
)
|
| 297 |
-
preprocessor_path = output_dir / "parakeet_preprocessor.mlpackage"
|
| 298 |
-
_save_mlpackage(
|
| 299 |
-
preprocessor_model,
|
| 300 |
-
preprocessor_path,
|
| 301 |
-
"Parakeet preprocessor (15 s window)",
|
| 302 |
-
)
|
| 303 |
-
|
| 304 |
-
typer.echo("Tracing and converting encoder…")
|
| 305 |
-
traced_encoder = torch.jit.trace(
|
| 306 |
-
encoder, (mel_ref, mel_length_ref), strict=False
|
| 307 |
-
)
|
| 308 |
-
traced_encoder.eval()
|
| 309 |
-
encoder_inputs = [
|
| 310 |
-
ct.TensorType(name="mel", shape=_tensor_shape(mel_ref), dtype=np.float32),
|
| 311 |
-
ct.TensorType(name="mel_length", shape=(1,), dtype=np.int32),
|
| 312 |
-
]
|
| 313 |
-
encoder_outputs = [
|
| 314 |
-
ct.TensorType(name="encoder", dtype=np.float32),
|
| 315 |
-
ct.TensorType(name="encoder_length", dtype=np.int32),
|
| 316 |
-
]
|
| 317 |
-
# Encoder: CPU only
|
| 318 |
-
encoder_model = _coreml_convert(
|
| 319 |
-
traced_encoder,
|
| 320 |
-
encoder_inputs,
|
| 321 |
-
encoder_outputs,
|
| 322 |
-
export_settings,
|
| 323 |
-
compute_units_override=ct.ComputeUnit.CPU_ONLY,
|
| 324 |
-
)
|
| 325 |
-
encoder_path = output_dir / "parakeet_encoder.mlpackage"
|
| 326 |
-
_save_mlpackage(
|
| 327 |
-
encoder_model,
|
| 328 |
-
encoder_path,
|
| 329 |
-
"Parakeet encoder (15 s window)",
|
| 330 |
-
)
|
| 331 |
-
|
| 332 |
-
# Optional fused export: Preprocessor + Encoder
|
| 333 |
-
typer.echo("Tracing and converting fused mel+encoder…")
|
| 334 |
-
mel_encoder = MelEncoderWrapper(preprocessor, encoder)
|
| 335 |
-
traced_mel_encoder = torch.jit.trace(
|
| 336 |
-
mel_encoder, (audio_tensor, audio_length), strict=False
|
| 337 |
-
)
|
| 338 |
-
traced_mel_encoder.eval()
|
| 339 |
-
mel_encoder_inputs = [
|
| 340 |
-
# Keep fixed 15s window for fused Mel+Encoder
|
| 341 |
-
ct.TensorType(name="audio_signal", shape=(1, max_samples), dtype=np.float32),
|
| 342 |
-
ct.TensorType(name="audio_length", shape=(1,), dtype=np.int32),
|
| 343 |
-
]
|
| 344 |
-
mel_encoder_outputs = [
|
| 345 |
-
ct.TensorType(name="encoder", dtype=np.float32),
|
| 346 |
-
ct.TensorType(name="encoder_length", dtype=np.int32),
|
| 347 |
-
]
|
| 348 |
-
# Fused mel+encoder compute units (parametrized; default CPU_ONLY)
|
| 349 |
-
mel_encoder_model = _coreml_convert(
|
| 350 |
-
traced_mel_encoder,
|
| 351 |
-
mel_encoder_inputs,
|
| 352 |
-
mel_encoder_outputs,
|
| 353 |
-
export_settings,
|
| 354 |
-
compute_units_override=melenc_cu,
|
| 355 |
-
)
|
| 356 |
-
mel_encoder_path = output_dir / "parakeet_mel_encoder.mlpackage"
|
| 357 |
-
_save_mlpackage(
|
| 358 |
-
mel_encoder_model,
|
| 359 |
-
mel_encoder_path,
|
| 360 |
-
"Parakeet fused Mel+Encoder (15 s window)",
|
| 361 |
-
)
|
| 362 |
-
|
| 363 |
-
typer.echo("Tracing and converting decoder…")
|
| 364 |
-
traced_decoder = torch.jit.trace(
|
| 365 |
-
decoder,
|
| 366 |
-
(targets, target_lengths, zero_state, zero_state),
|
| 367 |
-
strict=False,
|
| 368 |
-
)
|
| 369 |
-
traced_decoder.eval()
|
| 370 |
-
decoder_inputs = [
|
| 371 |
-
ct.TensorType(name="targets", shape=_tensor_shape(targets), dtype=np.int32),
|
| 372 |
-
ct.TensorType(name="target_length", shape=(1,), dtype=np.int32),
|
| 373 |
-
ct.TensorType(name="h_in", shape=_tensor_shape(zero_state), dtype=np.float32),
|
| 374 |
-
ct.TensorType(name="c_in", shape=_tensor_shape(zero_state), dtype=np.float32),
|
| 375 |
-
]
|
| 376 |
-
decoder_outputs = [
|
| 377 |
-
ct.TensorType(name="decoder", dtype=np.float32),
|
| 378 |
-
ct.TensorType(name="h_out", dtype=np.float32),
|
| 379 |
-
ct.TensorType(name="c_out", dtype=np.float32),
|
| 380 |
-
]
|
| 381 |
-
# Decoder: CPU only
|
| 382 |
-
decoder_model = _coreml_convert(
|
| 383 |
-
traced_decoder,
|
| 384 |
-
decoder_inputs,
|
| 385 |
-
decoder_outputs,
|
| 386 |
-
export_settings,
|
| 387 |
-
compute_units_override=ct.ComputeUnit.CPU_ONLY,
|
| 388 |
-
)
|
| 389 |
-
decoder_path = output_dir / "parakeet_decoder.mlpackage"
|
| 390 |
-
_save_mlpackage(
|
| 391 |
-
decoder_model,
|
| 392 |
-
decoder_path,
|
| 393 |
-
"Parakeet decoder (RNNT prediction network)",
|
| 394 |
-
)
|
| 395 |
-
|
| 396 |
-
typer.echo("Tracing and converting joint…")
|
| 397 |
-
traced_joint = torch.jit.trace(
|
| 398 |
-
joint,
|
| 399 |
-
(encoder_ref, decoder_ref),
|
| 400 |
-
strict=False,
|
| 401 |
-
)
|
| 402 |
-
traced_joint.eval()
|
| 403 |
-
joint_inputs = [
|
| 404 |
-
ct.TensorType(name="encoder", shape=_tensor_shape(encoder_ref), dtype=np.float32),
|
| 405 |
-
ct.TensorType(name="decoder", shape=_tensor_shape(decoder_ref), dtype=np.float32),
|
| 406 |
-
]
|
| 407 |
-
joint_outputs = [
|
| 408 |
-
ct.TensorType(name="logits", dtype=np.float32),
|
| 409 |
-
]
|
| 410 |
-
# Joint: CPU only
|
| 411 |
-
joint_model = _coreml_convert(
|
| 412 |
-
traced_joint,
|
| 413 |
-
joint_inputs,
|
| 414 |
-
joint_outputs,
|
| 415 |
-
export_settings,
|
| 416 |
-
compute_units_override=ct.ComputeUnit.CPU_ONLY,
|
| 417 |
-
)
|
| 418 |
-
joint_path = output_dir / "parakeet_joint.mlpackage"
|
| 419 |
-
_save_mlpackage(
|
| 420 |
-
joint_model,
|
| 421 |
-
joint_path,
|
| 422 |
-
"Parakeet joint network (RNNT)",
|
| 423 |
-
)
|
| 424 |
-
|
| 425 |
-
# Joint + decision head (split logits, softmax, argmax)
|
| 426 |
-
typer.echo("Tracing and converting joint decision head…")
|
| 427 |
-
vocab_size = int(asr_model.tokenizer.vocab_size)
|
| 428 |
-
num_extra = int(asr_model.joint.num_extra_outputs)
|
| 429 |
-
joint_decision = JointDecisionWrapper(joint, vocab_size=vocab_size, num_extra=num_extra)
|
| 430 |
-
traced_joint_decision = torch.jit.trace(
|
| 431 |
-
joint_decision,
|
| 432 |
-
(encoder_ref, decoder_ref),
|
| 433 |
-
strict=False,
|
| 434 |
-
)
|
| 435 |
-
traced_joint_decision.eval()
|
| 436 |
-
joint_decision_inputs = [
|
| 437 |
-
ct.TensorType(name="encoder", shape=_tensor_shape(encoder_ref), dtype=np.float32),
|
| 438 |
-
ct.TensorType(name="decoder", shape=_tensor_shape(decoder_ref), dtype=np.float32),
|
| 439 |
-
]
|
| 440 |
-
joint_decision_outputs = [
|
| 441 |
-
ct.TensorType(name="token_id", dtype=np.int32),
|
| 442 |
-
ct.TensorType(name="token_prob", dtype=np.float32),
|
| 443 |
-
ct.TensorType(name="duration", dtype=np.int32),
|
| 444 |
-
]
|
| 445 |
-
# JointDecision: CPU only
|
| 446 |
-
joint_decision_model = _coreml_convert(
|
| 447 |
-
traced_joint_decision,
|
| 448 |
-
joint_decision_inputs,
|
| 449 |
-
joint_decision_outputs,
|
| 450 |
-
export_settings,
|
| 451 |
-
compute_units_override=ct.ComputeUnit.CPU_ONLY,
|
| 452 |
-
)
|
| 453 |
-
joint_decision_path = output_dir / "parakeet_joint_decision.mlpackage"
|
| 454 |
-
_save_mlpackage(
|
| 455 |
-
joint_decision_model,
|
| 456 |
-
joint_decision_path,
|
| 457 |
-
"Parakeet joint + decision head (split, softmax, argmax)",
|
| 458 |
-
)
|
| 459 |
-
|
| 460 |
-
# Single-step JointDecision for [1,1024,1] x [1,640,1] -> [1,1,1]
|
| 461 |
-
typer.echo("Tracing and converting single-step joint decision…")
|
| 462 |
-
jd_single = JointDecisionSingleStep(joint, vocab_size=vocab_size, num_extra=num_extra)
|
| 463 |
-
# Create single-step slices from refs
|
| 464 |
-
enc_step = encoder_ref[:, :, :1].contiguous()
|
| 465 |
-
dec_step = decoder_ref[:, :, :1].contiguous()
|
| 466 |
-
traced_jd_single = torch.jit.trace(
|
| 467 |
-
jd_single,
|
| 468 |
-
(enc_step, dec_step),
|
| 469 |
-
strict=False,
|
| 470 |
-
)
|
| 471 |
-
traced_jd_single.eval()
|
| 472 |
-
jd_single_inputs = [
|
| 473 |
-
ct.TensorType(name="encoder_step", shape=(1, enc_step.shape[1], 1), dtype=np.float32),
|
| 474 |
-
ct.TensorType(name="decoder_step", shape=(1, dec_step.shape[1], 1), dtype=np.float32),
|
| 475 |
-
]
|
| 476 |
-
jd_single_outputs = [
|
| 477 |
-
ct.TensorType(name="token_id", dtype=np.int32),
|
| 478 |
-
ct.TensorType(name="token_prob", dtype=np.float32),
|
| 479 |
-
ct.TensorType(name="duration", dtype=np.int32),
|
| 480 |
-
]
|
| 481 |
-
# Single-step JointDecision: CPU only
|
| 482 |
-
jd_single_model = _coreml_convert(
|
| 483 |
-
traced_jd_single,
|
| 484 |
-
jd_single_inputs,
|
| 485 |
-
jd_single_outputs,
|
| 486 |
-
export_settings,
|
| 487 |
-
compute_units_override=ct.ComputeUnit.CPU_ONLY,
|
| 488 |
-
)
|
| 489 |
-
jd_single_path = output_dir / "parakeet_joint_decision_single_step.mlpackage"
|
| 490 |
-
_save_mlpackage(
|
| 491 |
-
jd_single_model,
|
| 492 |
-
jd_single_path,
|
| 493 |
-
"Parakeet single-step joint decision (current frame)",
|
| 494 |
-
)
|
| 495 |
-
|
| 496 |
-
metadata: Dict[str, object] = {
|
| 497 |
-
"model_id": model_id,
|
| 498 |
-
"sample_rate": sample_rate,
|
| 499 |
-
"max_audio_seconds": export_settings.max_audio_seconds,
|
| 500 |
-
"max_audio_samples": max_samples,
|
| 501 |
-
"max_symbol_steps": export_settings.max_symbol_steps,
|
| 502 |
-
"vocab_size": vocab_size,
|
| 503 |
-
"joint_extra_outputs": num_extra,
|
| 504 |
-
"checkpoint": checkpoint_meta,
|
| 505 |
-
"coreml": {
|
| 506 |
-
"compute_units": export_settings.compute_units.name,
|
| 507 |
-
"compute_precision": (
|
| 508 |
-
export_settings.compute_precision.name
|
| 509 |
-
if export_settings.compute_precision is not None
|
| 510 |
-
else "FLOAT32"
|
| 511 |
-
),
|
| 512 |
-
},
|
| 513 |
-
"components": {
|
| 514 |
-
"preprocessor": {
|
| 515 |
-
"inputs": {
|
| 516 |
-
"audio_signal": list(_tensor_shape(audio_tensor)),
|
| 517 |
-
"audio_length": [1],
|
| 518 |
-
},
|
| 519 |
-
"outputs": {
|
| 520 |
-
"mel": list(_tensor_shape(mel_ref)),
|
| 521 |
-
"mel_length": [1],
|
| 522 |
-
},
|
| 523 |
-
"path": preprocessor_path.name,
|
| 524 |
-
},
|
| 525 |
-
"encoder": {
|
| 526 |
-
"inputs": {
|
| 527 |
-
"mel": list(_tensor_shape(mel_ref)),
|
| 528 |
-
"mel_length": [1],
|
| 529 |
-
},
|
| 530 |
-
"outputs": {
|
| 531 |
-
"encoder": list(_tensor_shape(encoder_ref)),
|
| 532 |
-
"encoder_length": [1],
|
| 533 |
-
},
|
| 534 |
-
"path": encoder_path.name,
|
| 535 |
-
},
|
| 536 |
-
"mel_encoder": {
|
| 537 |
-
"inputs": {
|
| 538 |
-
"audio_signal": [1, max_samples],
|
| 539 |
-
"audio_length": [1],
|
| 540 |
-
},
|
| 541 |
-
"outputs": {
|
| 542 |
-
"encoder": list(_tensor_shape(encoder_ref)),
|
| 543 |
-
"encoder_length": [1],
|
| 544 |
-
},
|
| 545 |
-
"path": mel_encoder_path.name,
|
| 546 |
-
},
|
| 547 |
-
"decoder": {
|
| 548 |
-
"inputs": {
|
| 549 |
-
"targets": list(_tensor_shape(targets)),
|
| 550 |
-
"target_length": [1],
|
| 551 |
-
"h_in": list(_tensor_shape(zero_state)),
|
| 552 |
-
"c_in": list(_tensor_shape(zero_state)),
|
| 553 |
-
},
|
| 554 |
-
"outputs": {
|
| 555 |
-
"decoder": list(_tensor_shape(decoder_ref)),
|
| 556 |
-
"h_out": list(_tensor_shape(h_ref)),
|
| 557 |
-
"c_out": list(_tensor_shape(c_ref)),
|
| 558 |
-
},
|
| 559 |
-
"path": decoder_path.name,
|
| 560 |
-
},
|
| 561 |
-
"joint": {
|
| 562 |
-
"inputs": {
|
| 563 |
-
"encoder": list(_tensor_shape(encoder_ref)),
|
| 564 |
-
"decoder": list(_tensor_shape(decoder_ref)),
|
| 565 |
-
},
|
| 566 |
-
"outputs": {
|
| 567 |
-
"logits": list(_tensor_shape(joint_ref)),
|
| 568 |
-
},
|
| 569 |
-
"path": joint_path.name,
|
| 570 |
-
},
|
| 571 |
-
"joint_decision": {
|
| 572 |
-
"inputs": {
|
| 573 |
-
"encoder": list(_tensor_shape(encoder_ref)),
|
| 574 |
-
"decoder": list(_tensor_shape(decoder_ref)),
|
| 575 |
-
},
|
| 576 |
-
"outputs": {
|
| 577 |
-
"token_id": [
|
| 578 |
-
_tensor_shape(encoder_ref)[0],
|
| 579 |
-
_tensor_shape(encoder_ref)[1],
|
| 580 |
-
_tensor_shape(decoder_ref)[1],
|
| 581 |
-
],
|
| 582 |
-
"token_prob": [
|
| 583 |
-
_tensor_shape(encoder_ref)[0],
|
| 584 |
-
_tensor_shape(encoder_ref)[1],
|
| 585 |
-
_tensor_shape(decoder_ref)[1],
|
| 586 |
-
],
|
| 587 |
-
"duration": [
|
| 588 |
-
_tensor_shape(encoder_ref)[0],
|
| 589 |
-
_tensor_shape(encoder_ref)[1],
|
| 590 |
-
_tensor_shape(decoder_ref)[1],
|
| 591 |
-
],
|
| 592 |
-
},
|
| 593 |
-
"path": joint_decision_path.name,
|
| 594 |
-
},
|
| 595 |
-
"joint_decision_single_step": {
|
| 596 |
-
"inputs": {
|
| 597 |
-
"encoder_step": [1, _tensor_shape(encoder_ref)[2-1], 1],
|
| 598 |
-
"decoder_step": [1, _tensor_shape(decoder_ref)[2-1], 1],
|
| 599 |
-
},
|
| 600 |
-
"outputs": {
|
| 601 |
-
"token_id": [1, 1, 1],
|
| 602 |
-
"token_prob": [1, 1, 1],
|
| 603 |
-
"duration": [1, 1, 1],
|
| 604 |
-
},
|
| 605 |
-
"path": jd_single_path.name,
|
| 606 |
-
},
|
| 607 |
-
},
|
| 608 |
-
}
|
| 609 |
-
|
| 610 |
-
metadata_path = output_dir / "metadata.json"
|
| 611 |
-
metadata_path.write_text(json.dumps(metadata, indent=2))
|
| 612 |
-
typer.echo(f"Export complete. Metadata written to {metadata_path}")
|
| 613 |
-
|
| 614 |
-
finally:
|
| 615 |
-
asr_model.decoder._rnnt_export = decoder_export_flag
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
if __name__ == "__main__":
|
| 619 |
-
app()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
convert/parakeet-tdt-v2-0.6b/coreml/individual_components.py
DELETED
|
@@ -1,229 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Export Parakeet TDT v2 RNNT components into CoreML and validate outputs."""
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from dataclasses import dataclass
|
| 6 |
-
from pathlib import Path
|
| 7 |
-
from typing import Optional, Tuple
|
| 8 |
-
|
| 9 |
-
import coremltools as ct
|
| 10 |
-
import torch
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
@dataclass
|
| 14 |
-
class ExportSettings:
|
| 15 |
-
output_dir: Path
|
| 16 |
-
compute_units: ct.ComputeUnit
|
| 17 |
-
deployment_target: Optional[ct.target.iOS17]
|
| 18 |
-
compute_precision: Optional[ct.precision]
|
| 19 |
-
max_audio_seconds: float
|
| 20 |
-
max_symbol_steps: int
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
@dataclass
|
| 24 |
-
class ValidationSettings:
|
| 25 |
-
audio_path: Optional[Path]
|
| 26 |
-
seconds: float
|
| 27 |
-
seed: Optional[int]
|
| 28 |
-
rtol: float
|
| 29 |
-
atol: float
|
| 30 |
-
skip: bool
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
@dataclass
|
| 34 |
-
class ValidationDiff:
|
| 35 |
-
name: str
|
| 36 |
-
max_abs_diff: float
|
| 37 |
-
max_rel_diff: float
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
@dataclass
|
| 41 |
-
class ValidationResult:
|
| 42 |
-
source: str
|
| 43 |
-
audio_num_samples: int
|
| 44 |
-
audio_seconds: float
|
| 45 |
-
token_length: int
|
| 46 |
-
atol: float
|
| 47 |
-
rtol: float
|
| 48 |
-
diffs: Tuple[ValidationDiff, ...]
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
class PreprocessorWrapper(torch.nn.Module):
|
| 52 |
-
def __init__(self, module: torch.nn.Module) -> None:
|
| 53 |
-
super().__init__()
|
| 54 |
-
self.module = module
|
| 55 |
-
|
| 56 |
-
def forward(self, audio_signal: torch.Tensor, length: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 57 |
-
mel, mel_length = self.module(input_signal=audio_signal, length=length.to(dtype=torch.long))
|
| 58 |
-
return mel, mel_length
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
class EncoderWrapper(torch.nn.Module):
|
| 62 |
-
def __init__(self, module: torch.nn.Module) -> None:
|
| 63 |
-
super().__init__()
|
| 64 |
-
self.module = module
|
| 65 |
-
|
| 66 |
-
def forward(self, features: torch.Tensor, length: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 67 |
-
encoded, encoded_lengths = self.module(audio_signal=features, length=length.to(dtype=torch.long))
|
| 68 |
-
return encoded, encoded_lengths
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
class DecoderWrapper(torch.nn.Module):
|
| 72 |
-
def __init__(self, module: torch.nn.Module) -> None:
|
| 73 |
-
super().__init__()
|
| 74 |
-
self.module = module
|
| 75 |
-
|
| 76 |
-
def forward(
|
| 77 |
-
self,
|
| 78 |
-
targets: torch.Tensor,
|
| 79 |
-
target_lengths: torch.Tensor,
|
| 80 |
-
h_in: torch.Tensor,
|
| 81 |
-
c_in: torch.Tensor,
|
| 82 |
-
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 83 |
-
state = [h_in, c_in]
|
| 84 |
-
decoder_output, _, new_state = self.module(
|
| 85 |
-
targets=targets.to(dtype=torch.long),
|
| 86 |
-
target_length=target_lengths.to(dtype=torch.long),
|
| 87 |
-
states=state,
|
| 88 |
-
)
|
| 89 |
-
return decoder_output, new_state[0], new_state[1]
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
class JointWrapper(torch.nn.Module):
|
| 93 |
-
def __init__(self, module: torch.nn.Module) -> None:
|
| 94 |
-
super().__init__()
|
| 95 |
-
self.module = module
|
| 96 |
-
|
| 97 |
-
def forward(self, encoder_outputs: torch.Tensor, decoder_outputs: torch.Tensor) -> torch.Tensor:
|
| 98 |
-
# Input: encoder_outputs [B, D, T], decoder_outputs [B, D, U]
|
| 99 |
-
# Transpose to match what projection layers expect
|
| 100 |
-
encoder_outputs = encoder_outputs.transpose(1, 2) # [B, T, D]
|
| 101 |
-
decoder_outputs = decoder_outputs.transpose(1, 2) # [B, U, D]
|
| 102 |
-
|
| 103 |
-
# Apply projections
|
| 104 |
-
enc_proj = self.module.enc(encoder_outputs) # [B, T, 640]
|
| 105 |
-
dec_proj = self.module.pred(decoder_outputs) # [B, U, 640]
|
| 106 |
-
|
| 107 |
-
# Explicit broadcasting along T and U to avoid converter ambiguity
|
| 108 |
-
x = enc_proj.unsqueeze(2) + dec_proj.unsqueeze(1) # [B, T, U, 640]
|
| 109 |
-
x = self.module.joint_net[0](x) # ReLU
|
| 110 |
-
x = self.module.joint_net[1](x) # Dropout (no-op in eval)
|
| 111 |
-
out = self.module.joint_net[2](x) # Linear -> logits [B, T, U, 8198]
|
| 112 |
-
return out
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
class MelEncoderWrapper(torch.nn.Module):
|
| 116 |
-
"""Fused wrapper: waveform -> mel -> encoder.
|
| 117 |
-
|
| 118 |
-
Inputs:
|
| 119 |
-
- audio_signal: [B, S]
|
| 120 |
-
- audio_length: [B]
|
| 121 |
-
|
| 122 |
-
Outputs:
|
| 123 |
-
- encoder: [B, D, T_enc]
|
| 124 |
-
- encoder_length: [B]
|
| 125 |
-
"""
|
| 126 |
-
def __init__(self, preprocessor: PreprocessorWrapper, encoder: EncoderWrapper) -> None:
|
| 127 |
-
super().__init__()
|
| 128 |
-
self.preprocessor = preprocessor
|
| 129 |
-
self.encoder = encoder
|
| 130 |
-
|
| 131 |
-
def forward(self, audio_signal: torch.Tensor, audio_length: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 132 |
-
mel, mel_length = self.preprocessor(audio_signal, audio_length)
|
| 133 |
-
encoded, enc_len = self.encoder(mel, mel_length.to(dtype=torch.int32))
|
| 134 |
-
return encoded, enc_len
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
class JointDecisionWrapper(torch.nn.Module):
|
| 138 |
-
"""Joint + decision head: outputs label id, label prob, duration frames.
|
| 139 |
-
|
| 140 |
-
Splits joint logits into token logits and duration logits, applies softmax
|
| 141 |
-
over tokens, argmax for both heads, and gathers probability of the chosen token.
|
| 142 |
-
|
| 143 |
-
Inputs:
|
| 144 |
-
- encoder_outputs: [B, D, T]
|
| 145 |
-
- decoder_outputs: [B, D, U]
|
| 146 |
-
|
| 147 |
-
Returns:
|
| 148 |
-
- token_id: [B, T, U] int32
|
| 149 |
-
- token_prob: [B, T, U] float32
|
| 150 |
-
- duration: [B, T, U] int32 (frames; duration buckets as defined by the model)
|
| 151 |
-
"""
|
| 152 |
-
def __init__(self, joint: JointWrapper, vocab_size: int, num_extra: int) -> None:
|
| 153 |
-
super().__init__()
|
| 154 |
-
self.joint = joint
|
| 155 |
-
self.vocab_with_blank = int(vocab_size) + 1
|
| 156 |
-
self.num_extra = int(num_extra)
|
| 157 |
-
|
| 158 |
-
def forward(self, encoder_outputs: torch.Tensor, decoder_outputs: torch.Tensor):
|
| 159 |
-
logits = self.joint(encoder_outputs, decoder_outputs)
|
| 160 |
-
token_logits = logits[..., : self.vocab_with_blank]
|
| 161 |
-
duration_logits = logits[..., -self.num_extra :]
|
| 162 |
-
|
| 163 |
-
# Token selection
|
| 164 |
-
token_ids = torch.argmax(token_logits, dim=-1).to(dtype=torch.int32)
|
| 165 |
-
token_probs_all = torch.softmax(token_logits, dim=-1)
|
| 166 |
-
# gather expects int64 (long) indices; cast only for gather
|
| 167 |
-
token_prob = torch.gather(
|
| 168 |
-
token_probs_all, dim=-1, index=token_ids.long().unsqueeze(-1)
|
| 169 |
-
).squeeze(-1)
|
| 170 |
-
|
| 171 |
-
# Duration prediction (duration bucket argmax → frame increments)
|
| 172 |
-
duration = torch.argmax(duration_logits, dim=-1).to(dtype=torch.int32)
|
| 173 |
-
return token_ids, token_prob, duration
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
class JointDecisionSingleStep(torch.nn.Module):
|
| 177 |
-
"""Single-step variant for streaming: encoder_step [1, 1024, 1] -> [1,1,1].
|
| 178 |
-
|
| 179 |
-
Inputs:
|
| 180 |
-
- encoder_step: [B=1, D=1024, T=1]
|
| 181 |
-
- decoder_step: [B=1, D=640, U=1]
|
| 182 |
-
|
| 183 |
-
Returns:
|
| 184 |
-
- token_id: [1, 1, 1] int32
|
| 185 |
-
- token_prob: [1, 1, 1] float32
|
| 186 |
-
- duration: [1, 1, 1] int32
|
| 187 |
-
"""
|
| 188 |
-
def __init__(self, joint: JointWrapper, vocab_size: int, num_extra: int) -> None:
|
| 189 |
-
super().__init__()
|
| 190 |
-
self.joint = joint
|
| 191 |
-
self.vocab_with_blank = int(vocab_size) + 1
|
| 192 |
-
self.num_extra = int(num_extra)
|
| 193 |
-
|
| 194 |
-
def forward(self, encoder_step: torch.Tensor, decoder_step: torch.Tensor):
|
| 195 |
-
# Reuse JointWrapper which expects [B, D, T] and [B, D, U]
|
| 196 |
-
logits = self.joint(encoder_step, decoder_step) # [1, 1, 1, V+extra]
|
| 197 |
-
token_logits = logits[..., : self.vocab_with_blank]
|
| 198 |
-
duration_logits = logits[..., -self.num_extra :]
|
| 199 |
-
|
| 200 |
-
token_ids = torch.argmax(token_logits, dim=-1, keepdim=False).to(dtype=torch.int32)
|
| 201 |
-
token_probs_all = torch.softmax(token_logits, dim=-1)
|
| 202 |
-
token_prob = torch.gather(
|
| 203 |
-
token_probs_all, dim=-1, index=token_ids.long().unsqueeze(-1)
|
| 204 |
-
).squeeze(-1)
|
| 205 |
-
duration = torch.argmax(duration_logits, dim=-1, keepdim=False).to(dtype=torch.int32)
|
| 206 |
-
return token_ids, token_prob, duration
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
def _coreml_convert(
|
| 210 |
-
traced: torch.jit.ScriptModule,
|
| 211 |
-
inputs,
|
| 212 |
-
outputs,
|
| 213 |
-
settings: ExportSettings,
|
| 214 |
-
compute_units_override: Optional[ct.ComputeUnit] = None,
|
| 215 |
-
) -> ct.models.MLModel:
|
| 216 |
-
cu = compute_units_override if compute_units_override is not None else settings.compute_units
|
| 217 |
-
kwargs = {
|
| 218 |
-
"convert_to": "mlprogram",
|
| 219 |
-
"inputs": inputs,
|
| 220 |
-
"outputs": outputs,
|
| 221 |
-
"compute_units": cu,
|
| 222 |
-
}
|
| 223 |
-
print("Converting:", traced.__class__.__name__)
|
| 224 |
-
print("Conversion kwargs:", kwargs)
|
| 225 |
-
if settings.deployment_target is not None:
|
| 226 |
-
kwargs["minimum_deployment_target"] = settings.deployment_target
|
| 227 |
-
if settings.compute_precision is not None:
|
| 228 |
-
kwargs["compute_precision"] = settings.compute_precision
|
| 229 |
-
return ct.convert(traced, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
convert/parakeet-tdt-v2-0.6b/coreml/quantize_coreml.py
DELETED
|
@@ -1,1207 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Quantize CoreML mlpackages and compare quality, compression, latency, and compile time.
|
| 3 |
-
|
| 4 |
-
This script focuses on the fused models:
|
| 5 |
-
- parakeet_mel_encoder.mlpackage (waveform -> encoder)
|
| 6 |
-
- parakeet_joint_decision.mlpackage (joint + softmax/argmax)
|
| 7 |
-
|
| 8 |
-
It also quantizes the rest of the components in the directory for completeness.
|
| 9 |
-
|
| 10 |
-
Variants tried by default (examples):
|
| 11 |
-
- int8-linear (per-channel)
|
| 12 |
-
- palettize 6-bit (Mel-only)
|
| 13 |
-
- jd-only variants (int8 and palette)
|
| 14 |
-
- prune + int8
|
| 15 |
-
|
| 16 |
-
Outputs:
|
| 17 |
-
- A new directory per variant under <output_root>/<variant>/ with quantized mlpackages
|
| 18 |
-
- quantization_summary.json with aggregate metrics (baseline vs each variant)
|
| 19 |
-
- Plots saved under <output_root>/plots/ and mirrored to <repo>/plots/quantize/<compute_units_lower>/
|
| 20 |
-
- fused_quality.png / fused_latency.png / fused_compression.png / fused_size.png (latency chart also shows compile time)
|
| 21 |
-
- all_components_quality.png / all_components_latency.png / all_components_compression.png / all_components_size.png (latency chart also shows compile time)
|
| 22 |
-
|
| 23 |
-
Notes:
|
| 24 |
-
- Uses CoreMLTools optimize.coreml linear/palettize/prune for MLProgram models.
|
| 25 |
-
- Keeps the fixed 15-second window shapes as per context/coreml_component_io.md.
|
| 26 |
-
- Run via `uv run` to ensure reproducible environment.
|
| 27 |
-
- Sets minimum deployment target to iOS 17 for all outputs and loads models with `compute_units=ALL` by default to enable ANE.
|
| 28 |
-
- Tracks offline compile time (mlpackage->mlmodelc) and includes that metric in plots.
|
| 29 |
-
"""
|
| 30 |
-
from __future__ import annotations
|
| 31 |
-
|
| 32 |
-
import json
|
| 33 |
-
import platform
|
| 34 |
-
import shutil
|
| 35 |
-
import subprocess
|
| 36 |
-
import time
|
| 37 |
-
from dataclasses import dataclass
|
| 38 |
-
from pathlib import Path
|
| 39 |
-
from typing import Dict, List, Optional, Set, Tuple
|
| 40 |
-
|
| 41 |
-
import numpy as np
|
| 42 |
-
import soundfile as sf
|
| 43 |
-
import typer
|
| 44 |
-
|
| 45 |
-
import coremltools as ct
|
| 46 |
-
from coremltools.optimize.coreml import (
|
| 47 |
-
OptimizationConfig,
|
| 48 |
-
OpLinearQuantizerConfig,
|
| 49 |
-
OpPalettizerConfig,
|
| 50 |
-
OpThresholdPrunerConfig,
|
| 51 |
-
linear_quantize_weights,
|
| 52 |
-
palettize_weights,
|
| 53 |
-
prune_weights,
|
| 54 |
-
)
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
# Optional plotting
|
| 58 |
-
try:
|
| 59 |
-
import matplotlib
|
| 60 |
-
matplotlib.use("Agg")
|
| 61 |
-
import matplotlib.pyplot as plt
|
| 62 |
-
HAS_MPL = True
|
| 63 |
-
except Exception:
|
| 64 |
-
HAS_MPL = False
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
BASE_DIR = Path(__file__).resolve().parent
|
| 68 |
-
BYTES_IN_MB = 1024 * 1024
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
DISPLAY_LABEL_OVERRIDES = {
|
| 72 |
-
"mel6bit-palettize": "mel6bit",
|
| 73 |
-
"enc6bit-palettize": "enc6bit",
|
| 74 |
-
"int8-linear": "int8 linear",
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
# When no explicit components are provided, we derive the set of
|
| 79 |
-
# components to quantize from the selected variants' whitelists.
|
| 80 |
-
# If any selected variant has no whitelist (i.e., applies globally),
|
| 81 |
-
# we quantize all components.
|
| 82 |
-
DEFAULT_TARGET_COMPONENTS: Tuple[str, str] = ()
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
# Formatting helpers -------------------------------------------------------
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
def _format_labels(labels: List[str]) -> List[str]:
|
| 89 |
-
formatted: List[str] = []
|
| 90 |
-
for label in labels:
|
| 91 |
-
pretty = DISPLAY_LABEL_OVERRIDES.get(label, label)
|
| 92 |
-
pretty = pretty.replace("_", " ")
|
| 93 |
-
pretty = pretty.replace("-", "\n")
|
| 94 |
-
formatted.append(pretty)
|
| 95 |
-
return formatted
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
def _friendly_component_name(name: str) -> str:
|
| 99 |
-
return name.replace("_", " ").title()
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
def _get_metric_series(
|
| 103 |
-
metrics: Dict[str, Dict[str, List[float]]],
|
| 104 |
-
component: str,
|
| 105 |
-
key: str,
|
| 106 |
-
length: int,
|
| 107 |
-
) -> List[float]:
|
| 108 |
-
values = metrics.get(component, {}).get(key)
|
| 109 |
-
if values is None:
|
| 110 |
-
return [float("nan")] * length
|
| 111 |
-
if len(values) >= length:
|
| 112 |
-
return list(values[:length])
|
| 113 |
-
padded = list(values)
|
| 114 |
-
padded.extend([float("nan")] * (length - len(values)))
|
| 115 |
-
return padded
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
def _plot_bar_rows(
|
| 119 |
-
out_path: Path,
|
| 120 |
-
display_labels: List[str],
|
| 121 |
-
rows: List[Dict[str, object]],
|
| 122 |
-
title_suffix: str,
|
| 123 |
-
) -> None:
|
| 124 |
-
if not HAS_MPL:
|
| 125 |
-
return
|
| 126 |
-
if not rows or not display_labels:
|
| 127 |
-
return
|
| 128 |
-
|
| 129 |
-
n_labels = len(display_labels)
|
| 130 |
-
fig_width = max(8.0, 1.3 * n_labels)
|
| 131 |
-
fig_height = max(2.6 * len(rows), 3.0)
|
| 132 |
-
|
| 133 |
-
fig, axes = plt.subplots(len(rows), 1, figsize=(fig_width, fig_height), squeeze=False)
|
| 134 |
-
axes = axes.flatten()
|
| 135 |
-
x = np.arange(n_labels)
|
| 136 |
-
|
| 137 |
-
for ax, row in zip(axes, rows):
|
| 138 |
-
values = np.asarray(row.get("values", [float("nan")] * n_labels), dtype=np.float64)
|
| 139 |
-
color = row.get("color")
|
| 140 |
-
bars = ax.bar(x, values, width=0.6, color=color)
|
| 141 |
-
ax.set_title(str(row.get("title", "")))
|
| 142 |
-
ax.set_xticks(x)
|
| 143 |
-
ax.set_xticklabels(display_labels, rotation=25, ha="right")
|
| 144 |
-
ylim = row.get("ylim")
|
| 145 |
-
if isinstance(ylim, tuple) and len(ylim) == 2:
|
| 146 |
-
ax.set_ylim(ylim)
|
| 147 |
-
ax.grid(axis="y", linestyle="--", alpha=0.3)
|
| 148 |
-
# Add value labels for bars
|
| 149 |
-
for bar in bars:
|
| 150 |
-
h = bar.get_height()
|
| 151 |
-
if np.isnan(h):
|
| 152 |
-
continue
|
| 153 |
-
ax.annotate(
|
| 154 |
-
f"{h:.2f}",
|
| 155 |
-
xy=(bar.get_x() + bar.get_width() / 2, h),
|
| 156 |
-
xytext=(0, 3),
|
| 157 |
-
textcoords="offset points",
|
| 158 |
-
ha="center",
|
| 159 |
-
va="bottom",
|
| 160 |
-
fontsize=8,
|
| 161 |
-
)
|
| 162 |
-
|
| 163 |
-
if title_suffix:
|
| 164 |
-
fig.suptitle(title_suffix, fontsize=12)
|
| 165 |
-
fig.tight_layout(rect=(0, 0, 1, 0.95))
|
| 166 |
-
else:
|
| 167 |
-
fig.tight_layout()
|
| 168 |
-
|
| 169 |
-
out_path.parent.mkdir(parents=True, exist_ok=True)
|
| 170 |
-
plt.savefig(out_path)
|
| 171 |
-
plt.close(fig)
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
def _plot_fused_category_charts(
|
| 175 |
-
plot_dir: Path,
|
| 176 |
-
labels: List[str],
|
| 177 |
-
mel_quality: List[float],
|
| 178 |
-
mel_latency_ms: List[float],
|
| 179 |
-
mel_compression: List[float],
|
| 180 |
-
mel_size_mb: List[float],
|
| 181 |
-
jd_acc: List[float],
|
| 182 |
-
jd_latency_ms: List[float],
|
| 183 |
-
jd_compression: List[float],
|
| 184 |
-
jd_size_mb: List[float],
|
| 185 |
-
mel_compile_ms: Optional[List[float]] = None,
|
| 186 |
-
jd_compile_ms: Optional[List[float]] = None,
|
| 187 |
-
title_suffix: str = "",
|
| 188 |
-
) -> List[Path]:
|
| 189 |
-
if not HAS_MPL:
|
| 190 |
-
return []
|
| 191 |
-
outputs: List[Path] = []
|
| 192 |
-
display = _format_labels(labels)
|
| 193 |
-
prefix = f"Fused Components — {title_suffix}" if title_suffix else "Fused Components"
|
| 194 |
-
|
| 195 |
-
quality_path = plot_dir / "fused_quality.png"
|
| 196 |
-
_plot_bar_rows(
|
| 197 |
-
quality_path,
|
| 198 |
-
display,
|
| 199 |
-
[
|
| 200 |
-
{
|
| 201 |
-
"title": "MelEncoder quality (1 - norm err)",
|
| 202 |
-
"values": mel_quality,
|
| 203 |
-
"color": "C0",
|
| 204 |
-
"ylim": (0.0, 1.05),
|
| 205 |
-
},
|
| 206 |
-
{
|
| 207 |
-
"title": "JointDecision token-id match rate",
|
| 208 |
-
"values": jd_acc,
|
| 209 |
-
"color": "C0",
|
| 210 |
-
"ylim": (0.0, 1.05),
|
| 211 |
-
},
|
| 212 |
-
],
|
| 213 |
-
f"{prefix} — Quality",
|
| 214 |
-
)
|
| 215 |
-
outputs.append(quality_path)
|
| 216 |
-
|
| 217 |
-
compression_path = plot_dir / "fused_compression.png"
|
| 218 |
-
_plot_bar_rows(
|
| 219 |
-
compression_path,
|
| 220 |
-
display,
|
| 221 |
-
[
|
| 222 |
-
{
|
| 223 |
-
"title": "MelEncoder compression ratio",
|
| 224 |
-
"values": mel_compression,
|
| 225 |
-
"color": "C2",
|
| 226 |
-
},
|
| 227 |
-
{
|
| 228 |
-
"title": "JointDecision compression ratio",
|
| 229 |
-
"values": jd_compression,
|
| 230 |
-
"color": "C2",
|
| 231 |
-
},
|
| 232 |
-
],
|
| 233 |
-
f"{prefix} — Compression",
|
| 234 |
-
)
|
| 235 |
-
outputs.append(compression_path)
|
| 236 |
-
|
| 237 |
-
size_path = plot_dir / "fused_size.png"
|
| 238 |
-
_plot_bar_rows(
|
| 239 |
-
size_path,
|
| 240 |
-
display,
|
| 241 |
-
[
|
| 242 |
-
{
|
| 243 |
-
"title": "MelEncoder size (MB)",
|
| 244 |
-
"values": mel_size_mb,
|
| 245 |
-
"color": "C4",
|
| 246 |
-
},
|
| 247 |
-
{
|
| 248 |
-
"title": "JointDecision size (MB)",
|
| 249 |
-
"values": jd_size_mb,
|
| 250 |
-
"color": "C4",
|
| 251 |
-
},
|
| 252 |
-
],
|
| 253 |
-
f"{prefix} — Size",
|
| 254 |
-
)
|
| 255 |
-
outputs.append(size_path)
|
| 256 |
-
|
| 257 |
-
latency_rows = [
|
| 258 |
-
{
|
| 259 |
-
"title": "MelEncoder latency (ms)",
|
| 260 |
-
"values": mel_latency_ms,
|
| 261 |
-
"color": "C1",
|
| 262 |
-
},
|
| 263 |
-
{
|
| 264 |
-
"title": "JointDecision latency (ms)",
|
| 265 |
-
"values": jd_latency_ms,
|
| 266 |
-
"color": "C1",
|
| 267 |
-
},
|
| 268 |
-
]
|
| 269 |
-
if mel_compile_ms is not None and jd_compile_ms is not None:
|
| 270 |
-
latency_rows.extend(
|
| 271 |
-
[
|
| 272 |
-
{
|
| 273 |
-
"title": "MelEncoder compile (ms)",
|
| 274 |
-
"values": mel_compile_ms,
|
| 275 |
-
"color": "C3",
|
| 276 |
-
},
|
| 277 |
-
{
|
| 278 |
-
"title": "JointDecision compile (ms)",
|
| 279 |
-
"values": jd_compile_ms,
|
| 280 |
-
"color": "C3",
|
| 281 |
-
},
|
| 282 |
-
]
|
| 283 |
-
)
|
| 284 |
-
|
| 285 |
-
latency_path = plot_dir / "fused_latency.png"
|
| 286 |
-
_plot_bar_rows(
|
| 287 |
-
latency_path,
|
| 288 |
-
display,
|
| 289 |
-
latency_rows,
|
| 290 |
-
f"{prefix} — Latency",
|
| 291 |
-
)
|
| 292 |
-
outputs.append(latency_path)
|
| 293 |
-
|
| 294 |
-
return outputs
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
def _plot_all_component_category_charts(
|
| 298 |
-
plot_dir: Path,
|
| 299 |
-
labels: List[str],
|
| 300 |
-
metrics: Dict[str, Dict[str, List[float]]],
|
| 301 |
-
title_suffix: str = "",
|
| 302 |
-
) -> List[Path]:
|
| 303 |
-
if not HAS_MPL:
|
| 304 |
-
return []
|
| 305 |
-
outputs: List[Path] = []
|
| 306 |
-
display = _format_labels(labels)
|
| 307 |
-
prefix = f"Component Breakdown — {title_suffix}" if title_suffix else "Component Breakdown"
|
| 308 |
-
comp_order = [
|
| 309 |
-
"preprocessor",
|
| 310 |
-
"encoder",
|
| 311 |
-
"mel_encoder",
|
| 312 |
-
"decoder",
|
| 313 |
-
"joint",
|
| 314 |
-
"joint_decision",
|
| 315 |
-
]
|
| 316 |
-
n = len(labels)
|
| 317 |
-
|
| 318 |
-
quality_rows: List[Dict[str, object]] = []
|
| 319 |
-
for comp in comp_order:
|
| 320 |
-
friendly = _friendly_component_name(comp)
|
| 321 |
-
if comp == "joint_decision":
|
| 322 |
-
values = _get_metric_series(metrics, comp, "acc", n)
|
| 323 |
-
title = f"{friendly} token-id match rate"
|
| 324 |
-
else:
|
| 325 |
-
values = _get_metric_series(metrics, comp, "quality", n)
|
| 326 |
-
title = f"{friendly} quality (1 - norm err)"
|
| 327 |
-
quality_rows.append({"title": title, "values": values, "color": "C0", "ylim": (0.0, 1.05)})
|
| 328 |
-
|
| 329 |
-
compression_rows: List[Dict[str, object]] = []
|
| 330 |
-
for comp in comp_order:
|
| 331 |
-
friendly = _friendly_component_name(comp)
|
| 332 |
-
compression_rows.append(
|
| 333 |
-
{
|
| 334 |
-
"title": f"{friendly} compression ratio",
|
| 335 |
-
"values": _get_metric_series(metrics, comp, "compression", n),
|
| 336 |
-
"color": "C2",
|
| 337 |
-
}
|
| 338 |
-
)
|
| 339 |
-
|
| 340 |
-
size_rows: List[Dict[str, object]] = []
|
| 341 |
-
for comp in comp_order:
|
| 342 |
-
friendly = _friendly_component_name(comp)
|
| 343 |
-
size_rows.append(
|
| 344 |
-
{
|
| 345 |
-
"title": f"{friendly} size (MB)",
|
| 346 |
-
"values": _get_metric_series(metrics, comp, "size_mb", n),
|
| 347 |
-
"color": "C4",
|
| 348 |
-
}
|
| 349 |
-
)
|
| 350 |
-
|
| 351 |
-
latency_rows: List[Dict[str, object]] = []
|
| 352 |
-
for comp in comp_order:
|
| 353 |
-
friendly = _friendly_component_name(comp)
|
| 354 |
-
latency_rows.append(
|
| 355 |
-
{
|
| 356 |
-
"title": f"{friendly} latency (ms)",
|
| 357 |
-
"values": _get_metric_series(metrics, comp, "latency_ms", n),
|
| 358 |
-
"color": "C1",
|
| 359 |
-
}
|
| 360 |
-
)
|
| 361 |
-
|
| 362 |
-
compile_rows: List[Dict[str, object]] = []
|
| 363 |
-
for comp in comp_order:
|
| 364 |
-
friendly = _friendly_component_name(comp)
|
| 365 |
-
compile_rows.append(
|
| 366 |
-
{
|
| 367 |
-
"title": f"{friendly} compile (ms)",
|
| 368 |
-
"values": _get_metric_series(metrics, comp, "compile_ms", n),
|
| 369 |
-
"color": "C3",
|
| 370 |
-
}
|
| 371 |
-
)
|
| 372 |
-
|
| 373 |
-
quality_path = plot_dir / "all_components_quality.png"
|
| 374 |
-
_plot_bar_rows(quality_path, display, quality_rows, f"{prefix} — Quality")
|
| 375 |
-
outputs.append(quality_path)
|
| 376 |
-
|
| 377 |
-
compression_path = plot_dir / "all_components_compression.png"
|
| 378 |
-
_plot_bar_rows(compression_path, display, compression_rows, f"{prefix} — Compression")
|
| 379 |
-
outputs.append(compression_path)
|
| 380 |
-
|
| 381 |
-
size_path = plot_dir / "all_components_size.png"
|
| 382 |
-
_plot_bar_rows(size_path, display, size_rows, f"{prefix} — Size")
|
| 383 |
-
outputs.append(size_path)
|
| 384 |
-
|
| 385 |
-
latency_path = plot_dir / "all_components_latency.png"
|
| 386 |
-
_plot_bar_rows(latency_path, display, latency_rows, f"{prefix} — Latency")
|
| 387 |
-
outputs.append(latency_path)
|
| 388 |
-
|
| 389 |
-
compile_path = plot_dir / "all_components_compile.png"
|
| 390 |
-
_plot_bar_rows(compile_path, display, compile_rows, f"{prefix} — Compile")
|
| 391 |
-
outputs.append(compile_path)
|
| 392 |
-
|
| 393 |
-
return outputs
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
@dataclass
|
| 400 |
-
class VariantConfig:
|
| 401 |
-
name: str
|
| 402 |
-
# List of (op_kind, opt_config) steps applied in order; op_kind in {'linear','palettize','prune'}
|
| 403 |
-
steps: List[Tuple[str, OptimizationConfig]]
|
| 404 |
-
category: str
|
| 405 |
-
whitelist: Optional[List[str]] = None # component names to apply; others copied
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
def _dir_size_bytes(path: Path) -> int:
|
| 409 |
-
total = 0
|
| 410 |
-
for p in path.rglob("*"):
|
| 411 |
-
if p.is_file():
|
| 412 |
-
try:
|
| 413 |
-
total += p.stat().st_size
|
| 414 |
-
except OSError:
|
| 415 |
-
pass
|
| 416 |
-
return total
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
def _load_metadata(input_dir: Path) -> Dict[str, object]:
|
| 420 |
-
meta_path = input_dir / "metadata.json"
|
| 421 |
-
if not meta_path.exists():
|
| 422 |
-
raise typer.BadParameter(f"Expected metadata.json in {input_dir}")
|
| 423 |
-
return json.loads(meta_path.read_text())
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
def _prepare_audio(
|
| 427 |
-
seconds: float,
|
| 428 |
-
sample_rate: int,
|
| 429 |
-
audio_path: Optional[Path],
|
| 430 |
-
) -> Tuple[np.ndarray, np.ndarray]:
|
| 431 |
-
max_samples = int(round(seconds * sample_rate))
|
| 432 |
-
if audio_path is None:
|
| 433 |
-
# random but deterministic sample
|
| 434 |
-
rng = np.random.default_rng(1234)
|
| 435 |
-
audio = rng.standard_normal(size=(1, max_samples), dtype=np.float32).astype(np.float32)
|
| 436 |
-
else:
|
| 437 |
-
data, sr = sf.read(str(audio_path), dtype="float32")
|
| 438 |
-
if sr != sample_rate:
|
| 439 |
-
raise typer.BadParameter(
|
| 440 |
-
f"Validation audio sample rate {sr} != expected {sample_rate}"
|
| 441 |
-
)
|
| 442 |
-
if data.ndim > 1:
|
| 443 |
-
data = data[:, 0]
|
| 444 |
-
if data.size < max_samples:
|
| 445 |
-
data = np.pad(data, (0, max_samples - data.size))
|
| 446 |
-
elif data.size > max_samples:
|
| 447 |
-
data = data[:max_samples]
|
| 448 |
-
audio = data.reshape(1, -1).astype(np.float32, copy=False)
|
| 449 |
-
length = np.array([max_samples], dtype=np.int32)
|
| 450 |
-
return audio, length
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
def _predict_latency(model: ct.models.MLModel, inputs: Dict[str, np.ndarray], runs: int = 10, warmup: int = 3) -> Tuple[float, float]:
|
| 454 |
-
# Warmup
|
| 455 |
-
for _ in range(max(0, warmup)):
|
| 456 |
-
_ = model.predict(inputs)
|
| 457 |
-
# Timed runs
|
| 458 |
-
times: List[float] = []
|
| 459 |
-
for _ in range(max(1, runs)):
|
| 460 |
-
t0 = time.perf_counter()
|
| 461 |
-
_ = model.predict(inputs)
|
| 462 |
-
t1 = time.perf_counter()
|
| 463 |
-
times.append((t1 - t0) * 1000.0) # ms
|
| 464 |
-
arr = np.array(times, dtype=np.float64)
|
| 465 |
-
return float(arr.mean()), float(arr.std(ddof=1) if arr.size > 1 else 0.0)
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
def _max_abs_rel(a: np.ndarray, b: np.ndarray) -> Tuple[float, float]:
|
| 469 |
-
a = np.asarray(a, dtype=np.float32)
|
| 470 |
-
b = np.asarray(b, dtype=np.float32)
|
| 471 |
-
if a.size == 0:
|
| 472 |
-
return 0.0, 0.0
|
| 473 |
-
diff = np.abs(a - b)
|
| 474 |
-
max_abs = float(diff.max())
|
| 475 |
-
denom = np.maximum(np.abs(a), np.abs(b))
|
| 476 |
-
with np.errstate(divide="ignore", invalid="ignore"):
|
| 477 |
-
rel = np.where(denom == 0.0, 0.0, diff / denom)
|
| 478 |
-
max_rel = float(rel.max())
|
| 479 |
-
return max_abs, max_rel
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
def _save_mlpackage(model: ct.models.MLModel, path: Path, description: str) -> None:
|
| 483 |
-
path.parent.mkdir(parents=True, exist_ok=True)
|
| 484 |
-
# Ensure iOS 17 target for proper MLProgram ops (e.g., blockwise shift/scale)
|
| 485 |
-
try:
|
| 486 |
-
model.minimum_deployment_target = ct.target.iOS17
|
| 487 |
-
except Exception:
|
| 488 |
-
pass
|
| 489 |
-
model.short_description = description
|
| 490 |
-
model.author = "Fluid Inference"
|
| 491 |
-
model.save(str(path))
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
def _offline_compile_time_ms(model_path: Path) -> float:
|
| 495 |
-
"""Compile mlpackage -> mlmodelc and return wall time in ms (host offline compile).
|
| 496 |
-
|
| 497 |
-
Returns NaN on failure.
|
| 498 |
-
"""
|
| 499 |
-
compiled_dir: Optional[Path] = None
|
| 500 |
-
expected_dir = model_path.with_suffix(".mlmodelc")
|
| 501 |
-
try:
|
| 502 |
-
# Delete any prior compile artifact in-place so we can measure a fresh build
|
| 503 |
-
if expected_dir.exists():
|
| 504 |
-
shutil.rmtree(expected_dir, ignore_errors=True)
|
| 505 |
-
|
| 506 |
-
t0 = time.perf_counter()
|
| 507 |
-
compiled_path = ct.utils.compile_model(str(model_path))
|
| 508 |
-
t1 = time.perf_counter()
|
| 509 |
-
compiled_dir = Path(compiled_path)
|
| 510 |
-
return (t1 - t0) * 1000.0
|
| 511 |
-
except Exception:
|
| 512 |
-
return float("nan")
|
| 513 |
-
finally:
|
| 514 |
-
if (
|
| 515 |
-
compiled_dir is not None
|
| 516 |
-
and compiled_dir.exists()
|
| 517 |
-
and compiled_dir == expected_dir
|
| 518 |
-
):
|
| 519 |
-
shutil.rmtree(compiled_dir, ignore_errors=True)
|
| 520 |
-
|
| 521 |
-
def _chip_spec_string(compute_units: str) -> str:
|
| 522 |
-
try:
|
| 523 |
-
chip = subprocess.check_output(["sysctl", "-n", "machdep.cpu.brand_string"]).decode().strip()
|
| 524 |
-
except Exception:
|
| 525 |
-
chip = platform.processor() or platform.machine()
|
| 526 |
-
mac_ver = platform.mac_ver()[0] or platform.platform()
|
| 527 |
-
return f"Host: {chip} • macOS {mac_ver} • CoreMLTools {ct.__version__} • ComputeUnits={compute_units} • Min Target: iOS17"
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
def _quantize_dir(
|
| 531 |
-
input_dir: Path,
|
| 532 |
-
output_dir: Path,
|
| 533 |
-
variant: VariantConfig,
|
| 534 |
-
global_whitelist: Optional[Set[str]] = None,
|
| 535 |
-
) -> Dict[str, str]:
|
| 536 |
-
"""Quantize all mlpackages in input_dir into output_dir using given variant config.
|
| 537 |
-
|
| 538 |
-
Returns a map of component name -> saved relative path.
|
| 539 |
-
"""
|
| 540 |
-
meta = _load_metadata(input_dir)
|
| 541 |
-
comps = meta.get("components", {})
|
| 542 |
-
saved: Dict[str, str] = {}
|
| 543 |
-
for name, cfg in comps.items():
|
| 544 |
-
src_name = cfg.get("path")
|
| 545 |
-
if not src_name:
|
| 546 |
-
continue
|
| 547 |
-
src_path = input_dir / src_name
|
| 548 |
-
if not src_path.exists():
|
| 549 |
-
continue
|
| 550 |
-
dst_path = output_dir / src_name
|
| 551 |
-
# Use CPU+GPU for preprocessor to avoid NE preprocessor input size issues; others use CPU+NE
|
| 552 |
-
cu = ct.ComputeUnit.CPU_AND_GPU if name == "preprocessor" else ct.ComputeUnit.CPU_AND_NE
|
| 553 |
-
base_model = ct.models.MLModel(str(src_path), compute_units=cu)
|
| 554 |
-
# Target iOS17 when running optimizations so the right ops are chosen
|
| 555 |
-
try:
|
| 556 |
-
base_model.minimum_deployment_target = ct.target.iOS17
|
| 557 |
-
except Exception:
|
| 558 |
-
pass
|
| 559 |
-
|
| 560 |
-
if name == "decoder":
|
| 561 |
-
typer.echo(f"[{variant.name}] Skipping decoder quantization; copying baseline: {src_name}")
|
| 562 |
-
_save_mlpackage(base_model, dst_path, "Baseline copy (decoder quantization disabled) - decoder")
|
| 563 |
-
saved[name] = dst_path.name
|
| 564 |
-
continue
|
| 565 |
-
|
| 566 |
-
skip_reasons: List[str] = []
|
| 567 |
-
if variant.whitelist is not None and name not in variant.whitelist:
|
| 568 |
-
skip_reasons.append("not targeted by variant")
|
| 569 |
-
if global_whitelist is not None and name not in global_whitelist:
|
| 570 |
-
skip_reasons.append("not in requested components")
|
| 571 |
-
|
| 572 |
-
if skip_reasons:
|
| 573 |
-
reason = "; ".join(skip_reasons)
|
| 574 |
-
typer.echo(f"[{variant.name}] Skipping {name} ({reason}); copying baseline: {src_name}")
|
| 575 |
-
_save_mlpackage(base_model, dst_path, f"Baseline copy ({reason}) - {name}")
|
| 576 |
-
saved[name] = dst_path.name
|
| 577 |
-
continue
|
| 578 |
-
|
| 579 |
-
typer.echo(f"[{variant.name}] Quantizing {name}: {src_name}")
|
| 580 |
-
|
| 581 |
-
try:
|
| 582 |
-
q_model = base_model
|
| 583 |
-
for step_kind, step_cfg in variant.steps:
|
| 584 |
-
if step_kind == 'linear':
|
| 585 |
-
q_model = linear_quantize_weights(q_model, step_cfg)
|
| 586 |
-
elif step_kind == 'palettize':
|
| 587 |
-
q_model = palettize_weights(q_model, step_cfg)
|
| 588 |
-
elif step_kind == 'prune':
|
| 589 |
-
q_model = prune_weights(q_model, step_cfg)
|
| 590 |
-
else:
|
| 591 |
-
raise ValueError(f"Unknown variant step: {step_kind}")
|
| 592 |
-
except Exception as e:
|
| 593 |
-
# If quantization fails (e.g., unsupported op), fall back to copying baseline.
|
| 594 |
-
typer.echo(f" ! Failed to quantize {name} with {variant.name}: {e}. Copying baseline.")
|
| 595 |
-
_save_mlpackage(base_model, dst_path, f"Baseline copy (failed to quantize) - {name}")
|
| 596 |
-
else:
|
| 597 |
-
_save_mlpackage(q_model, dst_path, f"{variant.name} quantized - {name}")
|
| 598 |
-
saved[name] = dst_path.name
|
| 599 |
-
# Persist a variant metadata shim
|
| 600 |
-
out_meta = {
|
| 601 |
-
"variant": variant.name,
|
| 602 |
-
"base_dir": str(input_dir.resolve()),
|
| 603 |
-
"components": saved,
|
| 604 |
-
}
|
| 605 |
-
(output_dir / "quantization_metadata.json").write_text(json.dumps(out_meta, indent=2))
|
| 606 |
-
return saved
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
@app.command()
|
| 610 |
-
def quantize(
|
| 611 |
-
input_dir: Path = typer.Option(Path("parakeet_coreml"), help="Directory containing baseline mlpackages + metadata.json"),
|
| 612 |
-
output_root: Path = typer.Option(Path("parakeet_coreml_quantized"), help="Root output dir for quantized variants"),
|
| 613 |
-
validation_audio: Optional[Path] = typer.Option(None, exists=True, resolve_path=True, help="Optional 15s, 16kHz wav for evaluation (defaults to bundled audio if present)"),
|
| 614 |
-
compute_units: str = typer.Option("CPU_AND_NE", help="Compute units for evaluation of non-preprocessor models. Preprocessor is forced to CPU_AND_GPU."),
|
| 615 |
-
runs: int = typer.Option(10, help="Timed runs per model for latency measurement"),
|
| 616 |
-
categories: Optional[List[str]] = typer.Option(
|
| 617 |
-
None,
|
| 618 |
-
"--category",
|
| 619 |
-
"-c",
|
| 620 |
-
help="Only run quantization variants in these categories (e.g., linear, mel-palettize). Can be repeated.",
|
| 621 |
-
),
|
| 622 |
-
components: Optional[List[str]] = typer.Option(
|
| 623 |
-
None,
|
| 624 |
-
"--component",
|
| 625 |
-
"-m",
|
| 626 |
-
help="Component names to quantize. Defaults to mel_encoder and joint_decision. Use 'all' to keep every component enabled.",
|
| 627 |
-
),
|
| 628 |
-
) -> None:
|
| 629 |
-
"""Quantize models, then compare quality, compression, latency, and compile time.
|
| 630 |
-
|
| 631 |
-
Variants include int8-linear (per-channel/per-tensor/block), palettization (6-bit),
|
| 632 |
-
jd-only probes, and prune+int8. Baseline is the pre-converted models.
|
| 633 |
-
"""
|
| 634 |
-
meta = _load_metadata(input_dir)
|
| 635 |
-
sr = int(meta.get("sample_rate", 16000))
|
| 636 |
-
seconds = float(meta.get("max_audio_seconds", 15.0))
|
| 637 |
-
|
| 638 |
-
components_meta = meta.get("components", {})
|
| 639 |
-
component_lookup = {name.lower(): name for name in components_meta.keys()}
|
| 640 |
-
# Defer computing component_filter until after variant/category selection so
|
| 641 |
-
# that, by default, we quantize exactly the components targeted by the
|
| 642 |
-
# selected variants (instead of a hard-coded subset).
|
| 643 |
-
component_filter: Optional[Set[str]] = None
|
| 644 |
-
|
| 645 |
-
# Default audio if present
|
| 646 |
-
default_audio = (BASE_DIR / "audio" / "yc_first_minute_16k_15s.wav").resolve()
|
| 647 |
-
audio_path = validation_audio if validation_audio is not None else (default_audio if default_audio.exists() else None)
|
| 648 |
-
if audio_path is not None and validation_audio is None:
|
| 649 |
-
typer.echo(f"Using default validation audio: {audio_path}")
|
| 650 |
-
audio, audio_len = _prepare_audio(seconds, sr, audio_path)
|
| 651 |
-
|
| 652 |
-
# Load baseline models and helpers for inputs
|
| 653 |
-
# Baseline models for input preparation
|
| 654 |
-
# Force preprocessor to CPU+GPU and all other components to CPU+NE for evaluation
|
| 655 |
-
pre_base = ct.models.MLModel(str(input_dir / "parakeet_preprocessor.mlpackage"), compute_units=ct.ComputeUnit.CPU_AND_GPU)
|
| 656 |
-
enc_base = ct.models.MLModel(str(input_dir / "parakeet_encoder.mlpackage"), compute_units=ct.ComputeUnit.CPU_AND_NE)
|
| 657 |
-
mel_encoder_base = ct.models.MLModel(str(input_dir / "parakeet_mel_encoder.mlpackage"), compute_units=ct.ComputeUnit.CPU_AND_NE)
|
| 658 |
-
decoder_base = ct.models.MLModel(str(input_dir / "parakeet_decoder.mlpackage"), compute_units=ct.ComputeUnit.CPU_AND_NE)
|
| 659 |
-
joint_decision_base = ct.models.MLModel(str(input_dir / "parakeet_joint_decision.mlpackage"), compute_units=ct.ComputeUnit.CPU_AND_NE)
|
| 660 |
-
joint_base = ct.models.MLModel(str(input_dir / "parakeet_joint.mlpackage"), compute_units=ct.ComputeUnit.CPU_AND_NE)
|
| 661 |
-
|
| 662 |
-
# Prepare typical inputs once using baseline models
|
| 663 |
-
pre_out = pre_base.predict({"audio_signal": audio, "audio_length": audio_len})
|
| 664 |
-
mel_ref = np.array(pre_out["mel"], dtype=np.float32, copy=True)
|
| 665 |
-
mel_len = np.array(pre_out["mel_length"], dtype=np.int32, copy=True)
|
| 666 |
-
|
| 667 |
-
enc_out = enc_base.predict({"mel": mel_ref, "mel_length": mel_len})
|
| 668 |
-
encoder_ref = np.array(enc_out["encoder"], dtype=np.float32, copy=True)
|
| 669 |
-
encoder_len = np.array(enc_out["encoder_length"], dtype=np.int32, copy=True)
|
| 670 |
-
|
| 671 |
-
# Decoder inputs from metadata
|
| 672 |
-
dec_in = meta["components"]["decoder"]["inputs"]
|
| 673 |
-
targets_shape = tuple(int(x) for x in dec_in["targets"]) # e.g., (1, 256)
|
| 674 |
-
h_shape = tuple(int(x) for x in dec_in["h_in"]) # e.g., (2, 1, 640)
|
| 675 |
-
# Use zeros as targets for reproducibility without needing blank idx
|
| 676 |
-
targets = np.zeros(targets_shape, dtype=np.int32)
|
| 677 |
-
target_length = np.array([targets_shape[1]], dtype=np.int32)
|
| 678 |
-
h0 = np.zeros(h_shape, dtype=np.float32)
|
| 679 |
-
c0 = np.zeros(h_shape, dtype=np.float32)
|
| 680 |
-
dec_out = decoder_base.predict({
|
| 681 |
-
"targets": targets,
|
| 682 |
-
"target_length": target_length,
|
| 683 |
-
"h_in": h0,
|
| 684 |
-
"c_in": c0,
|
| 685 |
-
})
|
| 686 |
-
decoder_ref = np.array(dec_out["decoder"], dtype=np.float32, copy=True)
|
| 687 |
-
|
| 688 |
-
# Baseline sizes per component
|
| 689 |
-
pre_base_size = _dir_size_bytes(input_dir / "parakeet_preprocessor.mlpackage")
|
| 690 |
-
enc_base_size = _dir_size_bytes(input_dir / "parakeet_encoder.mlpackage")
|
| 691 |
-
mel_base_size = _dir_size_bytes(input_dir / "parakeet_mel_encoder.mlpackage")
|
| 692 |
-
dec_base_size = _dir_size_bytes(input_dir / "parakeet_decoder.mlpackage")
|
| 693 |
-
joint_base_size = _dir_size_bytes(input_dir / "parakeet_joint.mlpackage")
|
| 694 |
-
jd_base_size = _dir_size_bytes(input_dir / "parakeet_joint_decision.mlpackage")
|
| 695 |
-
|
| 696 |
-
# Baseline latencies
|
| 697 |
-
pre_base_inputs = {"audio_signal": audio, "audio_length": audio_len}
|
| 698 |
-
enc_base_inputs = {"mel": mel_ref, "mel_length": mel_len}
|
| 699 |
-
mel_base_inputs = {"audio_signal": audio, "audio_length": audio_len}
|
| 700 |
-
dec_base_inputs = {"targets": targets, "target_length": target_length, "h_in": h0, "c_in": c0}
|
| 701 |
-
joint_base_inputs = {"encoder": encoder_ref, "decoder": decoder_ref}
|
| 702 |
-
jd_base_inputs = {"encoder": encoder_ref, "decoder": decoder_ref}
|
| 703 |
-
pre_base_ms, _ = _predict_latency(pre_base, pre_base_inputs, runs=runs)
|
| 704 |
-
enc_base_ms, _ = _predict_latency(enc_base, enc_base_inputs, runs=runs)
|
| 705 |
-
mel_base_ms, _ = _predict_latency(mel_encoder_base, mel_base_inputs, runs=runs)
|
| 706 |
-
dec_base_ms, _ = _predict_latency(decoder_base, dec_base_inputs, runs=runs)
|
| 707 |
-
joint_base_ms, _ = _predict_latency(joint_base, joint_base_inputs, runs=runs)
|
| 708 |
-
jd_base_ms, _ = _predict_latency(joint_decision_base, jd_base_inputs, runs=runs)
|
| 709 |
-
# Cache baseline joint logits for comparisons
|
| 710 |
-
logits_base = np.array(joint_base.predict(joint_base_inputs)["logits"], dtype=np.float32, copy=True)
|
| 711 |
-
|
| 712 |
-
# Variants
|
| 713 |
-
variants: List[VariantConfig] = [
|
| 714 |
-
VariantConfig(
|
| 715 |
-
name="int8-linear",
|
| 716 |
-
steps=[(
|
| 717 |
-
"linear",
|
| 718 |
-
OptimizationConfig(global_config=OpLinearQuantizerConfig(mode="linear", granularity="per_channel")),
|
| 719 |
-
)],
|
| 720 |
-
category="linear",
|
| 721 |
-
),
|
| 722 |
-
# 6-bit palettization for MelEncoder only
|
| 723 |
-
VariantConfig(
|
| 724 |
-
name="mel6bit-palettize",
|
| 725 |
-
steps=[(
|
| 726 |
-
"palettize",
|
| 727 |
-
OptimizationConfig(
|
| 728 |
-
global_config=OpPalettizerConfig(mode="kmeans", nbits=6)
|
| 729 |
-
),
|
| 730 |
-
)],
|
| 731 |
-
category="mel-palettize",
|
| 732 |
-
whitelist=["mel_encoder"],
|
| 733 |
-
),
|
| 734 |
-
# 6-bit palettization for Encoder only
|
| 735 |
-
VariantConfig(
|
| 736 |
-
name="enc6bit-palettize",
|
| 737 |
-
steps=[(
|
| 738 |
-
"palettize",
|
| 739 |
-
OptimizationConfig(
|
| 740 |
-
global_config=OpPalettizerConfig(mode="kmeans", nbits=6)
|
| 741 |
-
),
|
| 742 |
-
)],
|
| 743 |
-
category="encoder-palettize",
|
| 744 |
-
whitelist=["encoder"],
|
| 745 |
-
),
|
| 746 |
-
# (removed) Global palettization variants
|
| 747 |
-
]
|
| 748 |
-
|
| 749 |
-
available_categories = {variant.category for variant in variants}
|
| 750 |
-
category_lookup = {cat.lower(): cat for cat in available_categories}
|
| 751 |
-
selected_categories: Set[str]
|
| 752 |
-
if categories:
|
| 753 |
-
normalized_categories = [cat.strip().lower() for cat in categories if cat.strip()]
|
| 754 |
-
if normalized_categories:
|
| 755 |
-
invalid_categories = [cat for cat in normalized_categories if cat not in category_lookup]
|
| 756 |
-
if invalid_categories:
|
| 757 |
-
available_str = ", ".join(sorted(available_categories)) or "none"
|
| 758 |
-
bad = ", ".join(sorted(set(invalid_categories)))
|
| 759 |
-
raise typer.BadParameter(
|
| 760 |
-
f"Unknown category (--category): {bad}. Available categories: {available_str}."
|
| 761 |
-
)
|
| 762 |
-
selected_categories = {category_lookup[cat] for cat in normalized_categories}
|
| 763 |
-
variants = [variant for variant in variants if variant.category in selected_categories]
|
| 764 |
-
else:
|
| 765 |
-
selected_categories = available_categories
|
| 766 |
-
else:
|
| 767 |
-
selected_categories = available_categories
|
| 768 |
-
|
| 769 |
-
if not variants:
|
| 770 |
-
typer.echo("No quantization variants matched the requested categories; nothing to do.")
|
| 771 |
-
raise typer.Exit(code=0)
|
| 772 |
-
|
| 773 |
-
typer.echo("Running variant categories: " + ", ".join(sorted(selected_categories)))
|
| 774 |
-
|
| 775 |
-
# Resolve the component whitelist now that variants are known.
|
| 776 |
-
if components:
|
| 777 |
-
normalized_components = [comp.strip().lower() for comp in components if comp.strip()]
|
| 778 |
-
if any(comp == "all" for comp in normalized_components):
|
| 779 |
-
component_filter = None
|
| 780 |
-
else:
|
| 781 |
-
resolved: List[str] = []
|
| 782 |
-
invalid: List[str] = []
|
| 783 |
-
for comp in normalized_components:
|
| 784 |
-
match = component_lookup.get(comp)
|
| 785 |
-
if match is None:
|
| 786 |
-
invalid.append(comp)
|
| 787 |
-
else:
|
| 788 |
-
resolved.append(match)
|
| 789 |
-
if invalid:
|
| 790 |
-
available = ", ".join(sorted(component_lookup.values())) or "none"
|
| 791 |
-
bad = ", ".join(sorted(set(invalid)))
|
| 792 |
-
raise typer.BadParameter(
|
| 793 |
-
f"Unknown component(s) for --component: {bad}. Available components: {available}."
|
| 794 |
-
)
|
| 795 |
-
component_filter = set(resolved)
|
| 796 |
-
else:
|
| 797 |
-
# Derive from selected variants' whitelists
|
| 798 |
-
derived: Set[str] = set()
|
| 799 |
-
has_global = False
|
| 800 |
-
for v in variants:
|
| 801 |
-
if v.whitelist is None:
|
| 802 |
-
has_global = True
|
| 803 |
-
break
|
| 804 |
-
derived.update(v.whitelist)
|
| 805 |
-
component_filter = None if has_global or not derived else derived
|
| 806 |
-
|
| 807 |
-
if component_filter is None:
|
| 808 |
-
typer.echo("Quantizing components: all components (derived)")
|
| 809 |
-
else:
|
| 810 |
-
typer.echo("Quantizing components: " + ", ".join(sorted(component_filter)) + " (derived)")
|
| 811 |
-
|
| 812 |
-
# Aggregate results (baseline + variants)
|
| 813 |
-
summary: Dict[str, Dict[str, object]] = {}
|
| 814 |
-
variants_names: List[str] = []
|
| 815 |
-
# Build arrays including baseline as first label
|
| 816 |
-
fused_labels: List[str] = ["baseline"]
|
| 817 |
-
mel_quality_scores: List[float] = [1.0]
|
| 818 |
-
mel_latency_ms: List[float] = [mel_base_ms]
|
| 819 |
-
mel_compression: List[float] = [1.0]
|
| 820 |
-
mel_size_mb: List[float] = [float(mel_base_size) / BYTES_IN_MB]
|
| 821 |
-
# Offline compile time (host) for fused models
|
| 822 |
-
mel_compile_ms: List[float] = [_offline_compile_time_ms(input_dir / "parakeet_mel_encoder.mlpackage")]
|
| 823 |
-
jd_accuracy: List[float] = [1.0]
|
| 824 |
-
jd_latency_ms: List[float] = [jd_base_ms]
|
| 825 |
-
jd_compression: List[float] = [1.0]
|
| 826 |
-
jd_size_mb: List[float] = [float(jd_base_size) / BYTES_IN_MB]
|
| 827 |
-
jd_compile_ms: List[float] = [_offline_compile_time_ms(input_dir / "parakeet_joint_decision.mlpackage")]
|
| 828 |
-
|
| 829 |
-
# For the all-components chart, collect per-component metrics similarly
|
| 830 |
-
all_metrics: Dict[str, Dict[str, List[float]]] = {
|
| 831 |
-
"preprocessor": {
|
| 832 |
-
"quality": [1.0],
|
| 833 |
-
"compression": [1.0],
|
| 834 |
-
"latency_ms": [pre_base_ms],
|
| 835 |
-
"compile_ms": [_offline_compile_time_ms(input_dir / "parakeet_preprocessor.mlpackage")],
|
| 836 |
-
"size_mb": [float(pre_base_size) / BYTES_IN_MB],
|
| 837 |
-
},
|
| 838 |
-
"encoder": {
|
| 839 |
-
"quality": [1.0],
|
| 840 |
-
"compression": [1.0],
|
| 841 |
-
"latency_ms": [enc_base_ms],
|
| 842 |
-
"compile_ms": [_offline_compile_time_ms(input_dir / "parakeet_encoder.mlpackage")],
|
| 843 |
-
"size_mb": [float(enc_base_size) / BYTES_IN_MB],
|
| 844 |
-
},
|
| 845 |
-
"mel_encoder": {
|
| 846 |
-
"quality": [1.0],
|
| 847 |
-
"compression": [1.0],
|
| 848 |
-
"latency_ms": [mel_base_ms],
|
| 849 |
-
"compile_ms": [mel_compile_ms[0]],
|
| 850 |
-
"size_mb": [float(mel_base_size) / BYTES_IN_MB],
|
| 851 |
-
},
|
| 852 |
-
"decoder": {
|
| 853 |
-
"quality": [1.0],
|
| 854 |
-
"compression": [1.0],
|
| 855 |
-
"latency_ms": [dec_base_ms],
|
| 856 |
-
"compile_ms": [_offline_compile_time_ms(input_dir / "parakeet_decoder.mlpackage")],
|
| 857 |
-
"size_mb": [float(dec_base_size) / BYTES_IN_MB],
|
| 858 |
-
},
|
| 859 |
-
"joint": {
|
| 860 |
-
"quality": [1.0],
|
| 861 |
-
"compression": [1.0],
|
| 862 |
-
"latency_ms": [joint_base_ms],
|
| 863 |
-
"compile_ms": [_offline_compile_time_ms(input_dir / "parakeet_joint.mlpackage")],
|
| 864 |
-
"size_mb": [float(joint_base_size) / BYTES_IN_MB],
|
| 865 |
-
},
|
| 866 |
-
"joint_decision": {
|
| 867 |
-
"acc": [1.0],
|
| 868 |
-
"compression": [1.0],
|
| 869 |
-
"latency_ms": [jd_base_ms],
|
| 870 |
-
"compile_ms": [jd_compile_ms[0]],
|
| 871 |
-
"size_mb": [float(jd_base_size) / BYTES_IN_MB],
|
| 872 |
-
},
|
| 873 |
-
}
|
| 874 |
-
|
| 875 |
-
# Populate baseline entry
|
| 876 |
-
summary["baseline"] = {
|
| 877 |
-
"components": {
|
| 878 |
-
"preprocessor": {
|
| 879 |
-
"quality": 1.0,
|
| 880 |
-
"latency_ms": pre_base_ms,
|
| 881 |
-
"size_bytes": float(pre_base_size),
|
| 882 |
-
"size_mb": float(pre_base_size) / BYTES_IN_MB,
|
| 883 |
-
"compression_ratio": 1.0,
|
| 884 |
-
"compile_ms": all_metrics["preprocessor"]["compile_ms"][0],
|
| 885 |
-
},
|
| 886 |
-
"encoder": {
|
| 887 |
-
"quality": 1.0,
|
| 888 |
-
"latency_ms": enc_base_ms,
|
| 889 |
-
"size_bytes": float(enc_base_size),
|
| 890 |
-
"size_mb": float(enc_base_size) / BYTES_IN_MB,
|
| 891 |
-
"compression_ratio": 1.0,
|
| 892 |
-
"compile_ms": all_metrics["encoder"]["compile_ms"][0],
|
| 893 |
-
},
|
| 894 |
-
"mel_encoder": {
|
| 895 |
-
"quality": 1.0,
|
| 896 |
-
"latency_ms": mel_base_ms,
|
| 897 |
-
"size_bytes": float(mel_base_size),
|
| 898 |
-
"size_mb": float(mel_base_size) / BYTES_IN_MB,
|
| 899 |
-
"compression_ratio": 1.0,
|
| 900 |
-
"compile_ms": all_metrics["mel_encoder"]["compile_ms"][0],
|
| 901 |
-
},
|
| 902 |
-
"decoder": {
|
| 903 |
-
"quality": 1.0,
|
| 904 |
-
"latency_ms": dec_base_ms,
|
| 905 |
-
"size_bytes": float(dec_base_size),
|
| 906 |
-
"size_mb": float(dec_base_size) / BYTES_IN_MB,
|
| 907 |
-
"compression_ratio": 1.0,
|
| 908 |
-
"compile_ms": all_metrics["decoder"]["compile_ms"][0],
|
| 909 |
-
},
|
| 910 |
-
"joint": {
|
| 911 |
-
"quality": 1.0,
|
| 912 |
-
"latency_ms": joint_base_ms,
|
| 913 |
-
"size_bytes": float(joint_base_size),
|
| 914 |
-
"size_mb": float(joint_base_size) / BYTES_IN_MB,
|
| 915 |
-
"compression_ratio": 1.0,
|
| 916 |
-
"compile_ms": all_metrics["joint"]["compile_ms"][0],
|
| 917 |
-
},
|
| 918 |
-
"joint_decision": {
|
| 919 |
-
"acc": 1.0,
|
| 920 |
-
"latency_ms": jd_base_ms,
|
| 921 |
-
"size_bytes": float(jd_base_size),
|
| 922 |
-
"size_mb": float(jd_base_size) / BYTES_IN_MB,
|
| 923 |
-
"compression_ratio": 1.0,
|
| 924 |
-
"compile_ms": all_metrics["joint_decision"]["compile_ms"][0],
|
| 925 |
-
},
|
| 926 |
-
}
|
| 927 |
-
}
|
| 928 |
-
|
| 929 |
-
for var in variants:
|
| 930 |
-
variants_names.append(var.name)
|
| 931 |
-
out_dir = output_root / var.name
|
| 932 |
-
out_dir_exists = out_dir.exists()
|
| 933 |
-
out_dir.mkdir(parents=True, exist_ok=True)
|
| 934 |
-
|
| 935 |
-
expected_components = []
|
| 936 |
-
for comp_cfg in meta.get("components", {}).values():
|
| 937 |
-
rel = comp_cfg.get("path")
|
| 938 |
-
if rel:
|
| 939 |
-
expected_components.append(out_dir / rel)
|
| 940 |
-
|
| 941 |
-
missing = [p for p in expected_components if not p.exists()]
|
| 942 |
-
if out_dir_exists and not missing:
|
| 943 |
-
typer.echo(f"[{var.name}] Output already present at {out_dir}; skipping quantization step.")
|
| 944 |
-
else:
|
| 945 |
-
if out_dir_exists and missing:
|
| 946 |
-
missing_names = ", ".join(sorted(p.name for p in missing)) or "unknown"
|
| 947 |
-
typer.echo(f"[{var.name}] Output directory exists but is incomplete (missing: {missing_names}). Re-quantizing.")
|
| 948 |
-
shutil.rmtree(out_dir, ignore_errors=True)
|
| 949 |
-
out_dir.mkdir(parents=True, exist_ok=True)
|
| 950 |
-
_quantize_dir(input_dir, out_dir, var, component_filter)
|
| 951 |
-
|
| 952 |
-
# Load quantized models and pre-compute offline compile time
|
| 953 |
-
pre_q_path = out_dir / "parakeet_preprocessor.mlpackage"
|
| 954 |
-
enc_q_path = out_dir / "parakeet_encoder.mlpackage"
|
| 955 |
-
mel_q_path = out_dir / "parakeet_mel_encoder.mlpackage"
|
| 956 |
-
dec_q_path = out_dir / "parakeet_decoder.mlpackage"
|
| 957 |
-
joint_q_path = out_dir / "parakeet_joint.mlpackage"
|
| 958 |
-
jd_q_path = out_dir / "parakeet_joint_decision.mlpackage"
|
| 959 |
-
|
| 960 |
-
pre_compile_ms = _offline_compile_time_ms(pre_q_path)
|
| 961 |
-
enc_compile_ms = _offline_compile_time_ms(enc_q_path)
|
| 962 |
-
mel_compile_q_ms = _offline_compile_time_ms(mel_q_path)
|
| 963 |
-
dec_compile_ms = _offline_compile_time_ms(dec_q_path)
|
| 964 |
-
joint_compile_ms = _offline_compile_time_ms(joint_q_path)
|
| 965 |
-
jd_compile_q_ms = _offline_compile_time_ms(jd_q_path)
|
| 966 |
-
|
| 967 |
-
# Match compute units for quantized artifacts: preprocessor on CPU+GPU; others on CPU+NE
|
| 968 |
-
pre_q = ct.models.MLModel(str(pre_q_path), compute_units=ct.ComputeUnit.CPU_AND_GPU)
|
| 969 |
-
enc_q = ct.models.MLModel(str(enc_q_path), compute_units=ct.ComputeUnit.CPU_AND_NE)
|
| 970 |
-
mel_q = ct.models.MLModel(str(mel_q_path), compute_units=ct.ComputeUnit.CPU_AND_NE)
|
| 971 |
-
dec_q = ct.models.MLModel(str(dec_q_path), compute_units=ct.ComputeUnit.CPU_AND_NE)
|
| 972 |
-
joint_q = ct.models.MLModel(str(joint_q_path), compute_units=ct.ComputeUnit.CPU_AND_NE)
|
| 973 |
-
jd_q = ct.models.MLModel(str(jd_q_path), compute_units=ct.ComputeUnit.CPU_AND_NE)
|
| 974 |
-
|
| 975 |
-
# Preprocessor quality vs baseline
|
| 976 |
-
pre_q_out = pre_q.predict(pre_base_inputs)
|
| 977 |
-
mel_q_in = np.array(pre_q_out["mel"], dtype=np.float32, copy=True)
|
| 978 |
-
a_pre, r_pre = _max_abs_rel(mel_ref, mel_q_in)
|
| 979 |
-
l2_pre_ref = float(np.linalg.norm(mel_ref))
|
| 980 |
-
l2_pre_err = float(np.linalg.norm(mel_ref - mel_q_in))
|
| 981 |
-
pre_norm_err = (l2_pre_err / (l2_pre_ref + 1e-8)) if l2_pre_ref > 0 else 0.0
|
| 982 |
-
pre_quality = float(max(0.0, 1.0 - pre_norm_err))
|
| 983 |
-
pre_q_ms, _ = _predict_latency(pre_q, pre_base_inputs, runs=runs)
|
| 984 |
-
pre_q_size = _dir_size_bytes(out_dir / "parakeet_preprocessor.mlpackage")
|
| 985 |
-
all_metrics["preprocessor"]["quality"].append(pre_quality)
|
| 986 |
-
all_metrics["preprocessor"]["latency_ms"].append(pre_q_ms)
|
| 987 |
-
all_metrics["preprocessor"]["compression"].append(float(pre_base_size) / float(pre_q_size if pre_q_size > 0 else 1))
|
| 988 |
-
all_metrics["preprocessor"].setdefault("compile_ms", []).append(pre_compile_ms)
|
| 989 |
-
all_metrics["preprocessor"]["size_mb"].append(float(pre_q_size) / BYTES_IN_MB)
|
| 990 |
-
|
| 991 |
-
# Encoder quality vs baseline (feed baseline mel to both)
|
| 992 |
-
enc_q_out = enc_q.predict({"mel": mel_ref, "mel_length": mel_len})
|
| 993 |
-
encoder_q = np.array(enc_q_out["encoder"], dtype=np.float32, copy=True)
|
| 994 |
-
l2_enc_ref = float(np.linalg.norm(encoder_ref))
|
| 995 |
-
l2_enc_err = float(np.linalg.norm(encoder_ref - encoder_q))
|
| 996 |
-
enc_norm_err = (l2_enc_err / (l2_enc_ref + 1e-8)) if l2_enc_ref > 0 else 0.0
|
| 997 |
-
enc_quality = float(max(0.0, 1.0 - enc_norm_err))
|
| 998 |
-
enc_q_ms, _ = _predict_latency(enc_q, enc_base_inputs, runs=runs)
|
| 999 |
-
enc_q_size = _dir_size_bytes(out_dir / "parakeet_encoder.mlpackage")
|
| 1000 |
-
all_metrics["encoder"]["quality"].append(enc_quality)
|
| 1001 |
-
all_metrics["encoder"]["latency_ms"].append(enc_q_ms)
|
| 1002 |
-
all_metrics["encoder"]["compression"].append(float(enc_base_size) / float(enc_q_size if enc_q_size > 0 else 1))
|
| 1003 |
-
all_metrics["encoder"].setdefault("compile_ms", []).append(enc_compile_ms)
|
| 1004 |
-
all_metrics["encoder"]["size_mb"].append(float(enc_q_size) / BYTES_IN_MB)
|
| 1005 |
-
|
| 1006 |
-
# MelEncoder quality
|
| 1007 |
-
mel_q_out = mel_q.predict(mel_base_inputs)
|
| 1008 |
-
enc_q_fused = np.array(mel_q_out["encoder"], dtype=np.float32, copy=True)
|
| 1009 |
-
a_mel, r_mel = _max_abs_rel(encoder_ref, enc_q_fused)
|
| 1010 |
-
# Normalize error into a [0,1] quality score: 1 / (1 + normalized L2)
|
| 1011 |
-
# Use relative measure derived from L2 norms as more stable than max.
|
| 1012 |
-
l2_ref = float(np.linalg.norm(encoder_ref))
|
| 1013 |
-
l2_err = float(np.linalg.norm(encoder_ref - enc_q_fused))
|
| 1014 |
-
norm_err = (l2_err / (l2_ref + 1e-8)) if l2_ref > 0 else 0.0
|
| 1015 |
-
mel_quality = float(max(0.0, 1.0 - norm_err))
|
| 1016 |
-
mel_q_ms, _ = _predict_latency(mel_q, mel_base_inputs, runs=runs)
|
| 1017 |
-
mel_q_size = _dir_size_bytes(out_dir / "parakeet_mel_encoder.mlpackage")
|
| 1018 |
-
mel_ratio = float(mel_base_size) / float(mel_q_size if mel_q_size > 0 else 1)
|
| 1019 |
-
mel_size_mb.append(float(mel_q_size) / BYTES_IN_MB)
|
| 1020 |
-
all_metrics["mel_encoder"]["quality"].append(mel_quality)
|
| 1021 |
-
all_metrics["mel_encoder"]["latency_ms"].append(mel_q_ms)
|
| 1022 |
-
all_metrics["mel_encoder"]["compression"].append(float(mel_base_size) / float(mel_q_size if mel_q_size > 0 else 1))
|
| 1023 |
-
all_metrics["mel_encoder"].setdefault("compile_ms", []).append(mel_compile_q_ms)
|
| 1024 |
-
all_metrics["mel_encoder"]["size_mb"].append(float(mel_q_size) / BYTES_IN_MB)
|
| 1025 |
-
|
| 1026 |
-
# JointDecision quality: token-id and duration match rates
|
| 1027 |
-
jd_base_out = joint_decision_base.predict(jd_base_inputs)
|
| 1028 |
-
token_id_base = np.array(jd_base_out["token_id"], dtype=np.int32, copy=True)
|
| 1029 |
-
duration_base = np.array(jd_base_out["duration"], dtype=np.int32, copy=True)
|
| 1030 |
-
token_prob_base = np.array(jd_base_out["token_prob"], dtype=np.float32, copy=True)
|
| 1031 |
-
|
| 1032 |
-
jd_q_out = jd_q.predict(jd_base_inputs)
|
| 1033 |
-
token_id_q = np.array(jd_q_out["token_id"], dtype=np.int32, copy=True)
|
| 1034 |
-
duration_q = np.array(jd_q_out["duration"], dtype=np.int32, copy=True)
|
| 1035 |
-
token_prob_q = np.array(jd_q_out["token_prob"], dtype=np.float32, copy=True)
|
| 1036 |
-
|
| 1037 |
-
# Accuracy metrics
|
| 1038 |
-
id_match = float((token_id_q == token_id_base).mean())
|
| 1039 |
-
dur_match = float((duration_q == duration_base).mean())
|
| 1040 |
-
# Aggregate a single "accuracy" number as token-id match rate (primary)
|
| 1041 |
-
jd_acc = id_match
|
| 1042 |
-
jd_q_ms, _ = _predict_latency(jd_q, jd_base_inputs, runs=runs)
|
| 1043 |
-
jd_q_size = _dir_size_bytes(out_dir / "parakeet_joint_decision.mlpackage")
|
| 1044 |
-
jd_ratio = float(jd_base_size) / float(jd_q_size if jd_q_size > 0 else 1)
|
| 1045 |
-
jd_size_mb.append(float(jd_q_size) / BYTES_IN_MB)
|
| 1046 |
-
all_metrics["joint_decision"].setdefault("acc", []).append(jd_acc)
|
| 1047 |
-
all_metrics["joint_decision"]["latency_ms"].append(jd_q_ms)
|
| 1048 |
-
all_metrics["joint_decision"]["compression"].append(float(jd_base_size) / float(jd_q_size if jd_q_size > 0 else 1))
|
| 1049 |
-
all_metrics["joint_decision"].setdefault("compile_ms", []).append(jd_compile_q_ms)
|
| 1050 |
-
all_metrics["joint_decision"]["size_mb"].append(float(jd_q_size) / BYTES_IN_MB)
|
| 1051 |
-
|
| 1052 |
-
# Decoder quality vs baseline
|
| 1053 |
-
dec_q_out = dec_q.predict(dec_base_inputs)
|
| 1054 |
-
decoder_q = np.array(dec_q_out["decoder"], dtype=np.float32, copy=True)
|
| 1055 |
-
l2_dec_ref = float(np.linalg.norm(decoder_ref))
|
| 1056 |
-
l2_dec_err = float(np.linalg.norm(decoder_ref - decoder_q))
|
| 1057 |
-
dec_norm_err = (l2_dec_err / (l2_dec_ref + 1e-8)) if l2_dec_ref > 0 else 0.0
|
| 1058 |
-
dec_quality = float(max(0.0, 1.0 - dec_norm_err))
|
| 1059 |
-
dec_q_ms, _ = _predict_latency(dec_q, dec_base_inputs, runs=runs)
|
| 1060 |
-
dec_q_size = _dir_size_bytes(out_dir / "parakeet_decoder.mlpackage")
|
| 1061 |
-
all_metrics["decoder"]["quality"].append(dec_quality)
|
| 1062 |
-
all_metrics["decoder"]["latency_ms"].append(dec_q_ms)
|
| 1063 |
-
all_metrics["decoder"]["compression"].append(float(dec_base_size) / float(dec_q_size if dec_q_size > 0 else 1))
|
| 1064 |
-
all_metrics["decoder"].setdefault("compile_ms", []).append(dec_compile_ms)
|
| 1065 |
-
all_metrics["decoder"]["size_mb"].append(float(dec_q_size) / BYTES_IN_MB)
|
| 1066 |
-
|
| 1067 |
-
# Joint quality vs baseline (compare logits)
|
| 1068 |
-
joint_q_out = joint_q.predict(joint_base_inputs)
|
| 1069 |
-
logits_q = np.array(joint_q_out["logits"], dtype=np.float32, copy=True)
|
| 1070 |
-
l2_joint_ref = float(np.linalg.norm(logits_base))
|
| 1071 |
-
l2_joint_err = float(np.linalg.norm(logits_base - logits_q))
|
| 1072 |
-
joint_norm_err = (l2_joint_err / (l2_joint_ref + 1e-8)) if l2_joint_ref > 0 else 0.0
|
| 1073 |
-
joint_quality = float(max(0.0, 1.0 - joint_norm_err))
|
| 1074 |
-
joint_q_ms, _ = _predict_latency(joint_q, joint_base_inputs, runs=runs)
|
| 1075 |
-
joint_q_size = _dir_size_bytes(out_dir / "parakeet_joint.mlpackage")
|
| 1076 |
-
all_metrics["joint"]["quality"].append(joint_quality)
|
| 1077 |
-
all_metrics["joint"]["latency_ms"].append(joint_q_ms)
|
| 1078 |
-
all_metrics["joint"]["compression"].append(float(joint_base_size) / float(joint_q_size if joint_q_size > 0 else 1))
|
| 1079 |
-
all_metrics["joint"].setdefault("compile_ms", []).append(joint_compile_ms)
|
| 1080 |
-
all_metrics["joint"]["size_mb"].append(float(joint_q_size) / BYTES_IN_MB)
|
| 1081 |
-
|
| 1082 |
-
# Decoder deltas for JSON
|
| 1083 |
-
a_dec, r_dec = _max_abs_rel(decoder_ref, decoder_q)
|
| 1084 |
-
# Joint deltas for JSON
|
| 1085 |
-
a_joint, r_joint = _max_abs_rel(logits_base, logits_q)
|
| 1086 |
-
|
| 1087 |
-
# Store metrics
|
| 1088 |
-
summary[var.name] = {
|
| 1089 |
-
"components": {
|
| 1090 |
-
"preprocessor": {
|
| 1091 |
-
"quality": pre_quality,
|
| 1092 |
-
"latency_ms": pre_q_ms,
|
| 1093 |
-
"size_bytes": float(pre_q_size),
|
| 1094 |
-
"size_mb": float(pre_q_size) / BYTES_IN_MB,
|
| 1095 |
-
"compression_ratio": float(pre_base_size) / float(pre_q_size if pre_q_size > 0 else 1),
|
| 1096 |
-
"max_abs": a_pre,
|
| 1097 |
-
"max_rel": r_pre,
|
| 1098 |
-
"compile_ms": pre_compile_ms,
|
| 1099 |
-
},
|
| 1100 |
-
"encoder": {
|
| 1101 |
-
"quality": enc_quality,
|
| 1102 |
-
"latency_ms": enc_q_ms,
|
| 1103 |
-
"size_bytes": float(enc_q_size),
|
| 1104 |
-
"size_mb": float(enc_q_size) / BYTES_IN_MB,
|
| 1105 |
-
"compression_ratio": float(enc_base_size) / float(enc_q_size if enc_q_size > 0 else 1),
|
| 1106 |
-
"compile_ms": enc_compile_ms,
|
| 1107 |
-
},
|
| 1108 |
-
"mel_encoder": {
|
| 1109 |
-
"quality": mel_quality,
|
| 1110 |
-
"latency_ms": mel_q_ms,
|
| 1111 |
-
"size_bytes": float(mel_q_size),
|
| 1112 |
-
"size_mb": float(mel_q_size) / BYTES_IN_MB,
|
| 1113 |
-
"compression_ratio": mel_ratio,
|
| 1114 |
-
"max_abs": a_mel,
|
| 1115 |
-
"max_rel": r_mel,
|
| 1116 |
-
"compile_ms": mel_compile_q_ms,
|
| 1117 |
-
},
|
| 1118 |
-
"decoder": {
|
| 1119 |
-
"quality": dec_quality,
|
| 1120 |
-
"latency_ms": dec_q_ms,
|
| 1121 |
-
"size_bytes": float(dec_q_size),
|
| 1122 |
-
"size_mb": float(dec_q_size) / BYTES_IN_MB,
|
| 1123 |
-
"compression_ratio": float(dec_base_size) / float(dec_q_size if dec_q_size > 0 else 1),
|
| 1124 |
-
"max_abs": a_dec,
|
| 1125 |
-
"max_rel": r_dec,
|
| 1126 |
-
"compile_ms": dec_compile_ms,
|
| 1127 |
-
},
|
| 1128 |
-
"joint": {
|
| 1129 |
-
"quality": joint_quality,
|
| 1130 |
-
"latency_ms": joint_q_ms,
|
| 1131 |
-
"size_bytes": float(joint_q_size),
|
| 1132 |
-
"size_mb": float(joint_q_size) / BYTES_IN_MB,
|
| 1133 |
-
"compression_ratio": float(joint_base_size) / float(joint_q_size if joint_q_size > 0 else 1),
|
| 1134 |
-
"max_abs": a_joint,
|
| 1135 |
-
"max_rel": r_joint,
|
| 1136 |
-
"compile_ms": joint_compile_ms,
|
| 1137 |
-
},
|
| 1138 |
-
"joint_decision": {
|
| 1139 |
-
"acc": jd_acc,
|
| 1140 |
-
"duration_match": dur_match,
|
| 1141 |
-
"prob_mae": float(np.mean(np.abs(token_prob_q - token_prob_base))),
|
| 1142 |
-
"latency_ms": jd_q_ms,
|
| 1143 |
-
"size_bytes": float(jd_q_size),
|
| 1144 |
-
"size_mb": float(jd_q_size) / BYTES_IN_MB,
|
| 1145 |
-
"compression_ratio": jd_ratio,
|
| 1146 |
-
"compile_ms": jd_compile_q_ms,
|
| 1147 |
-
},
|
| 1148 |
-
}
|
| 1149 |
-
}
|
| 1150 |
-
|
| 1151 |
-
fused_labels.append(var.name)
|
| 1152 |
-
mel_quality_scores.append(mel_quality)
|
| 1153 |
-
mel_latency_ms.append(mel_q_ms)
|
| 1154 |
-
mel_compression.append(mel_ratio)
|
| 1155 |
-
jd_accuracy.append(jd_acc)
|
| 1156 |
-
jd_latency_ms.append(jd_q_ms)
|
| 1157 |
-
jd_compression.append(jd_ratio)
|
| 1158 |
-
mel_compile_ms.append(mel_compile_q_ms)
|
| 1159 |
-
jd_compile_ms.append(jd_compile_q_ms)
|
| 1160 |
-
|
| 1161 |
-
# Write summary JSON
|
| 1162 |
-
out_root = output_root
|
| 1163 |
-
out_root.mkdir(parents=True, exist_ok=True)
|
| 1164 |
-
(out_root / "quantization_summary.json").write_text(json.dumps(summary, indent=2))
|
| 1165 |
-
|
| 1166 |
-
# Plot
|
| 1167 |
-
plot_dir = out_root / "plots"
|
| 1168 |
-
title_suffix = _chip_spec_string(compute_units)
|
| 1169 |
-
fused_paths = _plot_fused_category_charts(
|
| 1170 |
-
plot_dir,
|
| 1171 |
-
fused_labels,
|
| 1172 |
-
mel_quality_scores,
|
| 1173 |
-
mel_latency_ms,
|
| 1174 |
-
mel_compression,
|
| 1175 |
-
mel_size_mb,
|
| 1176 |
-
jd_accuracy,
|
| 1177 |
-
jd_latency_ms,
|
| 1178 |
-
jd_compression,
|
| 1179 |
-
jd_size_mb,
|
| 1180 |
-
mel_compile_ms,
|
| 1181 |
-
jd_compile_ms,
|
| 1182 |
-
title_suffix,
|
| 1183 |
-
)
|
| 1184 |
-
component_paths = _plot_all_component_category_charts(
|
| 1185 |
-
plot_dir,
|
| 1186 |
-
fused_labels,
|
| 1187 |
-
all_metrics,
|
| 1188 |
-
title_suffix,
|
| 1189 |
-
)
|
| 1190 |
-
|
| 1191 |
-
typer.echo(f"Wrote summary JSON: {out_root / 'quantization_summary.json'}")
|
| 1192 |
-
if HAS_MPL:
|
| 1193 |
-
all_plot_paths = fused_paths + component_paths
|
| 1194 |
-
repo_plot_dir = BASE_DIR / "plots" / "quantize" / compute_units.lower()
|
| 1195 |
-
repo_plot_dir.mkdir(parents=True, exist_ok=True)
|
| 1196 |
-
for path in all_plot_paths:
|
| 1197 |
-
typer.echo(f"Wrote plot: {path}")
|
| 1198 |
-
if path.exists():
|
| 1199 |
-
dest = repo_plot_dir / path.name
|
| 1200 |
-
shutil.copy2(path, dest)
|
| 1201 |
-
typer.echo(f"Mirrored plot: {dest}")
|
| 1202 |
-
else:
|
| 1203 |
-
typer.echo("matplotlib unavailable; skipped plotting.")
|
| 1204 |
-
|
| 1205 |
-
|
| 1206 |
-
if __name__ == "__main__":
|
| 1207 |
-
app()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
convert/parakeet-tdt-v2-0.6b/coreml/speech_to_text_streaming_infer_rnnt.py
DELETED
|
@@ -1,341 +0,0 @@
|
|
| 1 |
-
"""Streaming inference helper that stitches Parakeet CoreML components using the RNNT greedy loop from Nemo."""
|
| 2 |
-
from __future__ import annotations
|
| 3 |
-
|
| 4 |
-
import argparse
|
| 5 |
-
import json
|
| 6 |
-
import logging
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
from typing import Iterable, List, Optional, Sequence
|
| 9 |
-
|
| 10 |
-
import coremltools as ct
|
| 11 |
-
import librosa
|
| 12 |
-
import numpy as np
|
| 13 |
-
import torch
|
| 14 |
-
|
| 15 |
-
from parakeet_components import CoreMLModelBundle
|
| 16 |
-
|
| 17 |
-
LOGGER = logging.getLogger("parakeet_streaming")
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
class BatchedHyps:
|
| 21 |
-
"""Minimal port of Nemo's batched hypothesis buffer."""
|
| 22 |
-
|
| 23 |
-
def __init__(
|
| 24 |
-
self,
|
| 25 |
-
batch_size: int,
|
| 26 |
-
init_length: int,
|
| 27 |
-
device: torch.device,
|
| 28 |
-
float_dtype: torch.dtype,
|
| 29 |
-
) -> None:
|
| 30 |
-
if init_length <= 0:
|
| 31 |
-
raise ValueError("init_length must be > 0")
|
| 32 |
-
self._max_length = init_length
|
| 33 |
-
self.current_lengths = torch.zeros(batch_size, device=device, dtype=torch.long)
|
| 34 |
-
self.transcript = torch.zeros((batch_size, self._max_length), device=device, dtype=torch.long)
|
| 35 |
-
self.timestamps = torch.zeros((batch_size, self._max_length), device=device, dtype=torch.long)
|
| 36 |
-
self.scores = torch.zeros(batch_size, device=device, dtype=float_dtype)
|
| 37 |
-
self.last_timestamp = torch.full((batch_size,), -1, device=device, dtype=torch.long)
|
| 38 |
-
self.last_timestamp_lasts = torch.zeros(batch_size, device=device, dtype=torch.long)
|
| 39 |
-
self._batch_indices = torch.arange(batch_size, device=device)
|
| 40 |
-
self._ones = torch.ones_like(self._batch_indices)
|
| 41 |
-
|
| 42 |
-
def add_results(
|
| 43 |
-
self,
|
| 44 |
-
active_mask: torch.Tensor,
|
| 45 |
-
labels: torch.Tensor,
|
| 46 |
-
time_indices: torch.Tensor,
|
| 47 |
-
scores: torch.Tensor,
|
| 48 |
-
) -> None:
|
| 49 |
-
self.scores = torch.where(active_mask, self.scores + scores, self.scores)
|
| 50 |
-
self.transcript[self._batch_indices, self.current_lengths] = labels
|
| 51 |
-
self.timestamps[self._batch_indices, self.current_lengths] = time_indices
|
| 52 |
-
torch.where(
|
| 53 |
-
torch.logical_and(active_mask, self.last_timestamp == time_indices),
|
| 54 |
-
self.last_timestamp_lasts + 1,
|
| 55 |
-
self.last_timestamp_lasts,
|
| 56 |
-
out=self.last_timestamp_lasts,
|
| 57 |
-
)
|
| 58 |
-
torch.where(
|
| 59 |
-
torch.logical_and(active_mask, self.last_timestamp != time_indices),
|
| 60 |
-
self._ones,
|
| 61 |
-
self.last_timestamp_lasts,
|
| 62 |
-
out=self.last_timestamp_lasts,
|
| 63 |
-
)
|
| 64 |
-
torch.where(active_mask, time_indices, self.last_timestamp, out=self.last_timestamp)
|
| 65 |
-
self.current_lengths += active_mask
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
class CoreMLStreamingDecoder:
|
| 69 |
-
"""Use exported decoder and joint CoreML models with Nemo's greedy RNNT loop."""
|
| 70 |
-
|
| 71 |
-
def __init__(
|
| 72 |
-
self,
|
| 73 |
-
decoder_model: ct.models.MLModel,
|
| 74 |
-
joint_model: ct.models.MLModel,
|
| 75 |
-
*,
|
| 76 |
-
vocab_size: int,
|
| 77 |
-
blank_id: int,
|
| 78 |
-
num_layers: int,
|
| 79 |
-
hidden_size: int,
|
| 80 |
-
durations: Sequence[int] = (0, 1, 2, 3, 4),
|
| 81 |
-
max_symbols: int = 10,
|
| 82 |
-
device: torch.device = torch.device("cpu"),
|
| 83 |
-
) -> None:
|
| 84 |
-
self.decoder_model = decoder_model
|
| 85 |
-
self.joint_model = joint_model
|
| 86 |
-
self.vocab_size = vocab_size
|
| 87 |
-
self.blank_id = blank_id
|
| 88 |
-
self.num_layers = num_layers
|
| 89 |
-
self.hidden_size = hidden_size
|
| 90 |
-
self.durations = torch.tensor(durations, dtype=torch.long, device=device)
|
| 91 |
-
self.max_symbols = max_symbols
|
| 92 |
-
self.device = device
|
| 93 |
-
|
| 94 |
-
def _predict_decoder(self, labels: torch.Tensor, h_in: np.ndarray, c_in: np.ndarray) -> tuple[torch.Tensor, np.ndarray, np.ndarray]:
|
| 95 |
-
outputs = self.decoder_model.predict(
|
| 96 |
-
{
|
| 97 |
-
"targets": np.array([labels.cpu().numpy()], dtype=np.int32),
|
| 98 |
-
"target_lengths": np.array([labels.numel()], dtype=np.int32),
|
| 99 |
-
"h_in": h_in,
|
| 100 |
-
"c_in": c_in,
|
| 101 |
-
}
|
| 102 |
-
)
|
| 103 |
-
decoder_output = torch.from_numpy(outputs["decoder_output"]).to(self.device)
|
| 104 |
-
return decoder_output, outputs["h_out"], outputs["c_out"]
|
| 105 |
-
|
| 106 |
-
def _predict_joint(self, encoder_frame: torch.Tensor, decoder_output: torch.Tensor) -> torch.Tensor:
|
| 107 |
-
outputs = self.joint_model.predict(
|
| 108 |
-
{
|
| 109 |
-
"encoder_outputs": encoder_frame.unsqueeze(1).cpu().numpy().astype(np.float32),
|
| 110 |
-
"decoder_outputs": decoder_output.cpu().numpy().astype(np.float32),
|
| 111 |
-
}
|
| 112 |
-
)
|
| 113 |
-
logits = torch.from_numpy(outputs["logits"]).to(self.device)
|
| 114 |
-
return logits.squeeze(1).squeeze(1)
|
| 115 |
-
|
| 116 |
-
def decode(self, encoder_output: torch.Tensor, encoder_lengths: torch.Tensor) -> List[List[int]]:
|
| 117 |
-
batch_size, max_time, _ = encoder_output.shape
|
| 118 |
-
encoder_output = encoder_output.to(self.device)
|
| 119 |
-
encoder_lengths = encoder_lengths.to(self.device)
|
| 120 |
-
|
| 121 |
-
float_dtype = encoder_output.dtype
|
| 122 |
-
batch_indices = torch.arange(batch_size, device=self.device)
|
| 123 |
-
labels = torch.full((batch_size,), fill_value=self.blank_id, device=self.device, dtype=torch.long)
|
| 124 |
-
time_indices = torch.zeros_like(labels)
|
| 125 |
-
safe_time_indices = torch.zeros_like(labels)
|
| 126 |
-
time_indices_current = torch.zeros_like(labels)
|
| 127 |
-
last_timesteps = encoder_lengths - 1
|
| 128 |
-
active_mask = encoder_lengths > 0
|
| 129 |
-
advance_mask = torch.empty_like(active_mask)
|
| 130 |
-
active_mask_prev = torch.empty_like(active_mask)
|
| 131 |
-
became_inactive = torch.empty_like(active_mask)
|
| 132 |
-
|
| 133 |
-
hyps = BatchedHyps(
|
| 134 |
-
batch_size=batch_size,
|
| 135 |
-
init_length=max_time * self.max_symbols if self.max_symbols else max_time,
|
| 136 |
-
device=self.device,
|
| 137 |
-
float_dtype=float_dtype,
|
| 138 |
-
)
|
| 139 |
-
|
| 140 |
-
h_in = np.zeros((self.num_layers, batch_size, self.hidden_size), dtype=np.float32)
|
| 141 |
-
c_in = np.zeros((self.num_layers, batch_size, self.hidden_size), dtype=np.float32)
|
| 142 |
-
|
| 143 |
-
while active_mask.any():
|
| 144 |
-
active_mask_prev.copy_(active_mask)
|
| 145 |
-
decoder_output, h_in, c_in = self._predict_decoder(labels, h_in, c_in)
|
| 146 |
-
logits = self._predict_joint(encoder_output[batch_indices, safe_time_indices], decoder_output)
|
| 147 |
-
|
| 148 |
-
scores, labels = logits[:, : self.vocab_size].max(dim=-1)
|
| 149 |
-
duration_indices = logits[:, self.vocab_size : self.vocab_size + len(self.durations)].argmax(dim=-1)
|
| 150 |
-
durations = self.durations[duration_indices]
|
| 151 |
-
|
| 152 |
-
blank_mask = labels == self.blank_id
|
| 153 |
-
durations.masked_fill_(torch.logical_and(durations == 0, blank_mask), 1)
|
| 154 |
-
time_indices_current.copy_(time_indices)
|
| 155 |
-
time_indices += durations
|
| 156 |
-
torch.minimum(time_indices, last_timesteps, out=safe_time_indices)
|
| 157 |
-
torch.less(time_indices, encoder_lengths, out=active_mask)
|
| 158 |
-
torch.logical_and(active_mask, blank_mask, out=advance_mask)
|
| 159 |
-
|
| 160 |
-
while advance_mask.any():
|
| 161 |
-
torch.where(advance_mask, time_indices, time_indices_current, out=time_indices_current)
|
| 162 |
-
logits = self._predict_joint(encoder_output[batch_indices, safe_time_indices], decoder_output)
|
| 163 |
-
more_scores, more_labels = logits[:, : self.vocab_size].max(dim=-1)
|
| 164 |
-
labels = torch.where(advance_mask, more_labels, labels)
|
| 165 |
-
scores = torch.where(advance_mask, more_scores, scores)
|
| 166 |
-
duration_indices = logits[:, self.vocab_size : self.vocab_size + len(self.durations)].argmax(dim=-1)
|
| 167 |
-
durations = self.durations[duration_indices]
|
| 168 |
-
blank_mask = labels == self.blank_id
|
| 169 |
-
durations.masked_fill_(torch.logical_and(durations == 0, blank_mask), 1)
|
| 170 |
-
torch.where(advance_mask, time_indices + durations, time_indices, out=time_indices)
|
| 171 |
-
torch.minimum(time_indices, last_timesteps, out=safe_time_indices)
|
| 172 |
-
torch.less(time_indices, encoder_lengths, out=active_mask)
|
| 173 |
-
torch.logical_and(active_mask, blank_mask, out=advance_mask)
|
| 174 |
-
|
| 175 |
-
torch.ne(active_mask, active_mask_prev, out=became_inactive)
|
| 176 |
-
hyps.add_results(active_mask, labels, time_indices_current, scores)
|
| 177 |
-
|
| 178 |
-
if self.max_symbols is not None:
|
| 179 |
-
force_blank = torch.logical_and(
|
| 180 |
-
active_mask,
|
| 181 |
-
torch.logical_and(
|
| 182 |
-
torch.logical_and(labels != self.blank_id, hyps.last_timestamp_lasts >= self.max_symbols),
|
| 183 |
-
hyps.last_timestamp == time_indices,
|
| 184 |
-
),
|
| 185 |
-
)
|
| 186 |
-
time_indices += force_blank
|
| 187 |
-
torch.minimum(time_indices, last_timesteps, out=safe_time_indices)
|
| 188 |
-
torch.less(time_indices, encoder_lengths, out=active_mask)
|
| 189 |
-
|
| 190 |
-
results: List[List[int]] = []
|
| 191 |
-
for hyp in hyps.transcript:
|
| 192 |
-
tokens = [int(token) for token in hyp.tolist() if 0 < token < self.vocab_size]
|
| 193 |
-
results.append(tokens)
|
| 194 |
-
return results
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
class StreamingTranscriber:
|
| 198 |
-
def __init__(
|
| 199 |
-
self,
|
| 200 |
-
bundle: CoreMLModelBundle,
|
| 201 |
-
*,
|
| 202 |
-
blank_id: Optional[int] = None,
|
| 203 |
-
num_layers: int = 2,
|
| 204 |
-
hidden_size: int = 640,
|
| 205 |
-
durations: Sequence[int] = (0, 1, 2, 3, 4),
|
| 206 |
-
) -> None:
|
| 207 |
-
self.preprocessor = ct.models.MLModel(str(bundle.preprocessor), compute_units=ct.ComputeUnit.CPU_ONLY)
|
| 208 |
-
self.encoder = ct.models.MLModel(str(bundle.encoder), compute_units=ct.ComputeUnit.CPU_ONLY)
|
| 209 |
-
self.decoder = ct.models.MLModel(str(bundle.decoder), compute_units=ct.ComputeUnit.CPU_ONLY)
|
| 210 |
-
self.joint = ct.models.MLModel(str(bundle.joint), compute_units=ct.ComputeUnit.CPU_ONLY)
|
| 211 |
-
self.tokenizer = self._load_tokenizer(bundle.tokenizer)
|
| 212 |
-
|
| 213 |
-
vocab_size = max(self.tokenizer.keys()) + 1
|
| 214 |
-
if blank_id is None:
|
| 215 |
-
blank_id = vocab_size - 1
|
| 216 |
-
self.decoder_helper = CoreMLStreamingDecoder(
|
| 217 |
-
self.decoder,
|
| 218 |
-
self.joint,
|
| 219 |
-
vocab_size=vocab_size,
|
| 220 |
-
blank_id=blank_id,
|
| 221 |
-
num_layers=num_layers,
|
| 222 |
-
hidden_size=hidden_size,
|
| 223 |
-
durations=durations,
|
| 224 |
-
)
|
| 225 |
-
self.blank_id = blank_id
|
| 226 |
-
|
| 227 |
-
@staticmethod
|
| 228 |
-
def _load_tokenizer(tokenizer_path: Optional[Path]) -> dict[int, str]:
|
| 229 |
-
if tokenizer_path is None:
|
| 230 |
-
raise ValueError("Tokenizer JSON is required")
|
| 231 |
-
with Path(tokenizer_path).open() as f:
|
| 232 |
-
data = json.load(f)
|
| 233 |
-
return {int(k): v for k, v in data.items()}
|
| 234 |
-
|
| 235 |
-
def _tokens_to_text(self, tokens: Iterable[int]) -> str:
|
| 236 |
-
pieces: List[str] = []
|
| 237 |
-
for token in tokens:
|
| 238 |
-
piece = self.tokenizer.get(token)
|
| 239 |
-
if piece is None:
|
| 240 |
-
continue
|
| 241 |
-
if piece.startswith("▁"):
|
| 242 |
-
if pieces:
|
| 243 |
-
pieces.append(" ")
|
| 244 |
-
pieces.append(piece[1:])
|
| 245 |
-
else:
|
| 246 |
-
pieces.append(piece)
|
| 247 |
-
return "".join(pieces).strip()
|
| 248 |
-
|
| 249 |
-
def _preprocess(self, audio: np.ndarray) -> tuple[np.ndarray, int]:
|
| 250 |
-
audio_2d = audio.reshape(1, -1).astype(np.float32)
|
| 251 |
-
length = np.array([audio_2d.shape[-1]], dtype=np.int32)
|
| 252 |
-
outputs = self.preprocessor.predict({
|
| 253 |
-
"audio_signal": audio_2d,
|
| 254 |
-
"audio_length": length,
|
| 255 |
-
})
|
| 256 |
-
return outputs["melspectrogram"], int(outputs["melspectrogram_length"][0])
|
| 257 |
-
|
| 258 |
-
def _encode(self, mel: np.ndarray, mel_length: int) -> tuple[torch.Tensor, torch.Tensor]:
|
| 259 |
-
outputs = self.encoder.predict({
|
| 260 |
-
"melspectrogram": mel.astype(np.float32),
|
| 261 |
-
"melspectrogram_length": np.array([mel_length], dtype=np.int32),
|
| 262 |
-
})
|
| 263 |
-
encoder_output = outputs["encoder_output"]
|
| 264 |
-
if encoder_output.ndim == 3:
|
| 265 |
-
encoder_output = np.transpose(encoder_output, (0, 2, 1))
|
| 266 |
-
length = torch.tensor(outputs["encoder_output_length"], dtype=torch.long)
|
| 267 |
-
return torch.from_numpy(encoder_output.astype(np.float32)), length
|
| 268 |
-
|
| 269 |
-
def transcribe(self, audio_path: Path) -> str:
|
| 270 |
-
audio, _ = librosa.load(str(audio_path), sr=16000)
|
| 271 |
-
mel, mel_length = self._preprocess(audio)
|
| 272 |
-
encoder_output, encoder_length = self._encode(mel, mel_length)
|
| 273 |
-
token_ids = self.decoder_helper.decode(encoder_output, encoder_length)[0]
|
| 274 |
-
return self._tokens_to_text(token_ids)
|
| 275 |
-
|
| 276 |
-
def transcribe_many(self, audio_paths: Sequence[Path]) -> List[str]:
|
| 277 |
-
results: List[str] = []
|
| 278 |
-
for path in audio_paths:
|
| 279 |
-
LOGGER.info("Transcribing %s", path)
|
| 280 |
-
results.append(self.transcribe(path))
|
| 281 |
-
return results
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
def _resolve_bundle(args: argparse.Namespace) -> CoreMLModelBundle:
|
| 285 |
-
base = Path(args.model_dir) if args.model_dir else None
|
| 286 |
-
if base is None and not all([args.preprocessor, args.encoder, args.decoder, args.joint, args.tokenizer]):
|
| 287 |
-
raise ValueError("Either --model-dir or explicit model paths are required")
|
| 288 |
-
return CoreMLModelBundle(
|
| 289 |
-
preprocessor=Path(args.preprocessor) if args.preprocessor else base / "Melspectrogram.mlpackage",
|
| 290 |
-
encoder=Path(args.encoder) if args.encoder else base / "ParakeetEncoder.mlpackage",
|
| 291 |
-
decoder=Path(args.decoder) if args.decoder else base / "ParakeetDecoder.mlpackage",
|
| 292 |
-
joint=Path(args.joint) if args.joint else base / "RNNTJoint.mlpackage",
|
| 293 |
-
tokenizer=Path(args.tokenizer) if args.tokenizer else base / "tokenizer.json",
|
| 294 |
-
)
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
def _build_parser() -> argparse.ArgumentParser:
|
| 298 |
-
parser = argparse.ArgumentParser(description="Streaming RNNT inference with CoreML components")
|
| 299 |
-
parser.add_argument("--model-dir", type=Path, help="Directory containing exported CoreML models")
|
| 300 |
-
parser.add_argument("--preprocessor", type=Path, help="Path to the preprocessor .mlpackage")
|
| 301 |
-
parser.add_argument("--encoder", type=Path, help="Path to the encoder .mlpackage")
|
| 302 |
-
parser.add_argument("--decoder", type=Path, help="Path to the decoder .mlpackage")
|
| 303 |
-
parser.add_argument("--joint", type=Path, help="Path to the joint .mlpackage")
|
| 304 |
-
parser.add_argument("--tokenizer", type=Path, help="Path to tokenizer JSON")
|
| 305 |
-
parser.add_argument("audio", nargs="+", help="Audio files to transcribe")
|
| 306 |
-
parser.add_argument("--blank-id", type=int, help="Blank token id")
|
| 307 |
-
parser.add_argument("--num-layers", type=int, default=2, help="Prediction network layer count")
|
| 308 |
-
parser.add_argument("--hidden-size", type=int, default=640, help="Prediction network hidden size")
|
| 309 |
-
parser.add_argument("--durations", type=int, nargs="+", default=[0, 1, 2, 3, 4], help="RNNT duration bucket values")
|
| 310 |
-
parser.add_argument("--verbose", "-v", action="count", default=0, help="Increase log verbosity")
|
| 311 |
-
return parser
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
def _configure_logging(verbosity: int) -> None:
|
| 315 |
-
level = logging.WARNING - (10 * verbosity)
|
| 316 |
-
logging.basicConfig(level=max(logging.DEBUG, level), format="[%(levelname)s] %(message)s")
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
def main(argv: Optional[Sequence[str]] = None) -> None:
|
| 320 |
-
parser = _build_parser()
|
| 321 |
-
args = parser.parse_args(argv)
|
| 322 |
-
_configure_logging(args.verbose)
|
| 323 |
-
|
| 324 |
-
try:
|
| 325 |
-
bundle = _resolve_bundle(args)
|
| 326 |
-
transcriber = StreamingTranscriber(
|
| 327 |
-
bundle,
|
| 328 |
-
blank_id=args.blank_id,
|
| 329 |
-
num_layers=args.num_layers,
|
| 330 |
-
hidden_size=args.hidden_size,
|
| 331 |
-
durations=tuple(args.durations),
|
| 332 |
-
)
|
| 333 |
-
transcripts = transcriber.transcribe_many([Path(p) for p in args.audio])
|
| 334 |
-
for path, text in zip(args.audio, transcripts):
|
| 335 |
-
print(f"{path}: {text}")
|
| 336 |
-
except ValueError as exc:
|
| 337 |
-
parser.error(str(exc))
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
if __name__ == "__main__": # pragma: no cover
|
| 341 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/run_benchmarks.py
DELETED
|
@@ -1,273 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
FluidAudio Benchmark Suite
|
| 4 |
-
|
| 5 |
-
Runs ASR, VAD, and Diarization benchmarks and saves results to JSON.
|
| 6 |
-
Compare results against Documentation/Benchmarks.md baselines.
|
| 7 |
-
|
| 8 |
-
Usage:
|
| 9 |
-
python run_benchmarks.py # Run all benchmarks
|
| 10 |
-
python run_benchmarks.py --quick # Quick smoke test
|
| 11 |
-
python run_benchmarks.py --asr-only # ASR benchmark only
|
| 12 |
-
python run_benchmarks.py --vad-only # VAD benchmark only
|
| 13 |
-
python run_benchmarks.py --diar-only # Diarization only
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
import argparse
|
| 17 |
-
import json
|
| 18 |
-
import subprocess
|
| 19 |
-
import sys
|
| 20 |
-
from datetime import datetime
|
| 21 |
-
from pathlib import Path
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
# Baseline values from Documentation/Benchmarks.md
|
| 25 |
-
BASELINES = {
|
| 26 |
-
"asr": {
|
| 27 |
-
"wer_percent": 5.8,
|
| 28 |
-
"rtfx_min": 200, # M4 Pro: ~210x
|
| 29 |
-
"description": "LibriSpeech test-clean, Parakeet TDT 0.6B"
|
| 30 |
-
},
|
| 31 |
-
"vad": {
|
| 32 |
-
"f1_percent": 85.0,
|
| 33 |
-
"rtfx_min": 500,
|
| 34 |
-
"description": "VOiCES dataset, Silero VAD"
|
| 35 |
-
},
|
| 36 |
-
"diarization": {
|
| 37 |
-
"der_percent": 17.7,
|
| 38 |
-
"rtfx_min": 1.0,
|
| 39 |
-
"description": "AMI SDM, pyannote-based"
|
| 40 |
-
}
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def run_command(cmd: list[str], output_file: Path | None = None) -> tuple[int, str]:
|
| 45 |
-
"""Run a command and optionally save output."""
|
| 46 |
-
print(f"Running: {' '.join(cmd)}")
|
| 47 |
-
|
| 48 |
-
result = subprocess.run(
|
| 49 |
-
cmd,
|
| 50 |
-
capture_output=True,
|
| 51 |
-
text=True
|
| 52 |
-
)
|
| 53 |
-
|
| 54 |
-
output = result.stdout + result.stderr
|
| 55 |
-
|
| 56 |
-
if output_file:
|
| 57 |
-
output_file.write_text(output)
|
| 58 |
-
|
| 59 |
-
return result.returncode, output
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
def build_release() -> bool:
|
| 63 |
-
"""Build the project in release mode."""
|
| 64 |
-
print("\n" + "=" * 60)
|
| 65 |
-
print("Building release...")
|
| 66 |
-
print("=" * 60)
|
| 67 |
-
|
| 68 |
-
returncode, _ = run_command(["swift", "build", "-c", "release"])
|
| 69 |
-
|
| 70 |
-
if returncode != 0:
|
| 71 |
-
print("ERROR: Build failed!")
|
| 72 |
-
return False
|
| 73 |
-
|
| 74 |
-
print("Build successful.")
|
| 75 |
-
return True
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
def run_asr_benchmark(output_dir: Path, quick: bool = False) -> dict | None:
|
| 79 |
-
"""Run ASR benchmark on LibriSpeech test-clean."""
|
| 80 |
-
print("\n" + "=" * 60)
|
| 81 |
-
print("ASR Benchmark (LibriSpeech test-clean)")
|
| 82 |
-
print("=" * 60)
|
| 83 |
-
|
| 84 |
-
max_files = "100" if quick else "all"
|
| 85 |
-
output_json = output_dir / f"asr_results.json"
|
| 86 |
-
|
| 87 |
-
cmd = [
|
| 88 |
-
"swift", "run", "-c", "release", "fluidaudio", "asr-benchmark",
|
| 89 |
-
"--subset", "test-clean",
|
| 90 |
-
"--max-files", max_files,
|
| 91 |
-
"--output", str(output_json)
|
| 92 |
-
]
|
| 93 |
-
|
| 94 |
-
returncode, output = run_command(cmd, output_dir / "asr_log.txt")
|
| 95 |
-
|
| 96 |
-
if returncode != 0:
|
| 97 |
-
print(f"ERROR: ASR benchmark failed!")
|
| 98 |
-
return None
|
| 99 |
-
|
| 100 |
-
if output_json.exists():
|
| 101 |
-
return json.loads(output_json.read_text())
|
| 102 |
-
|
| 103 |
-
return None
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
def run_vad_benchmark(output_dir: Path, quick: bool = False) -> dict | None:
|
| 107 |
-
"""Run VAD benchmark."""
|
| 108 |
-
print("\n" + "=" * 60)
|
| 109 |
-
print("VAD Benchmark")
|
| 110 |
-
print("=" * 60)
|
| 111 |
-
|
| 112 |
-
dataset = "mini50" if quick else "voices-subset"
|
| 113 |
-
output_json = output_dir / f"vad_results.json"
|
| 114 |
-
|
| 115 |
-
cmd = [
|
| 116 |
-
"swift", "run", "-c", "release", "fluidaudio", "vad-benchmark",
|
| 117 |
-
"--dataset", dataset,
|
| 118 |
-
"--all-files",
|
| 119 |
-
"--threshold", "0.5",
|
| 120 |
-
"--output", str(output_json)
|
| 121 |
-
]
|
| 122 |
-
|
| 123 |
-
returncode, output = run_command(cmd, output_dir / "vad_log.txt")
|
| 124 |
-
|
| 125 |
-
if returncode != 0:
|
| 126 |
-
print(f"ERROR: VAD benchmark failed!")
|
| 127 |
-
return None
|
| 128 |
-
|
| 129 |
-
if output_json.exists():
|
| 130 |
-
return json.loads(output_json.read_text())
|
| 131 |
-
|
| 132 |
-
return None
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
def run_diarization_benchmark(output_dir: Path, quick: bool = False) -> dict | None:
|
| 136 |
-
"""Run diarization benchmark on AMI SDM."""
|
| 137 |
-
print("\n" + "=" * 60)
|
| 138 |
-
print("Diarization Benchmark (AMI SDM)")
|
| 139 |
-
print("=" * 60)
|
| 140 |
-
|
| 141 |
-
output_json = output_dir / f"diarization_results.json"
|
| 142 |
-
|
| 143 |
-
cmd = [
|
| 144 |
-
"swift", "run", "-c", "release", "fluidaudio", "diarization-benchmark",
|
| 145 |
-
"--auto-download",
|
| 146 |
-
"--output", str(output_json)
|
| 147 |
-
]
|
| 148 |
-
|
| 149 |
-
if quick:
|
| 150 |
-
cmd.extend(["--single-file", "ES2004a"])
|
| 151 |
-
|
| 152 |
-
returncode, output = run_command(cmd, output_dir / "diarization_log.txt")
|
| 153 |
-
|
| 154 |
-
if returncode != 0:
|
| 155 |
-
print(f"ERROR: Diarization benchmark failed!")
|
| 156 |
-
return None
|
| 157 |
-
|
| 158 |
-
if output_json.exists():
|
| 159 |
-
return json.loads(output_json.read_text())
|
| 160 |
-
|
| 161 |
-
return None
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
def compare_results(results: dict) -> None:
|
| 165 |
-
"""Compare results against baselines."""
|
| 166 |
-
print("\n" + "=" * 60)
|
| 167 |
-
print("Results vs Baselines (Documentation/Benchmarks.md)")
|
| 168 |
-
print("=" * 60)
|
| 169 |
-
|
| 170 |
-
if "asr" in results and results["asr"]:
|
| 171 |
-
asr = results["asr"]
|
| 172 |
-
baseline = BASELINES["asr"]
|
| 173 |
-
wer = asr.get("wer", asr.get("average_wer", 0)) * 100
|
| 174 |
-
rtfx = asr.get("rtfx", asr.get("median_rtfx", 0))
|
| 175 |
-
|
| 176 |
-
wer_status = "✓" if wer <= baseline["wer_percent"] * 1.1 else "✗"
|
| 177 |
-
rtfx_status = "✓" if rtfx >= baseline["rtfx_min"] * 0.8 else "✗"
|
| 178 |
-
|
| 179 |
-
print(f"\nASR ({baseline['description']}):")
|
| 180 |
-
print(f" WER: {wer:.1f}% (baseline: {baseline['wer_percent']}%) {wer_status}")
|
| 181 |
-
print(f" RTFx: {rtfx:.1f}x (baseline: {baseline['rtfx_min']}x+) {rtfx_status}")
|
| 182 |
-
|
| 183 |
-
if "vad" in results and results["vad"]:
|
| 184 |
-
vad = results["vad"]
|
| 185 |
-
baseline = BASELINES["vad"]
|
| 186 |
-
f1 = vad.get("f1_score", 0)
|
| 187 |
-
rtfx = vad.get("rtfx", 0)
|
| 188 |
-
|
| 189 |
-
f1_status = "✓" if f1 >= baseline["f1_percent"] * 0.9 else "✗"
|
| 190 |
-
rtfx_status = "✓" if rtfx >= baseline["rtfx_min"] * 0.5 else "✗"
|
| 191 |
-
|
| 192 |
-
print(f"\nVAD ({baseline['description']}):")
|
| 193 |
-
print(f" F1: {f1:.1f}% (baseline: {baseline['f1_percent']}%+) {f1_status}")
|
| 194 |
-
print(f" RTFx: {rtfx:.1f}x (baseline: {baseline['rtfx_min']}x+) {rtfx_status}")
|
| 195 |
-
|
| 196 |
-
if "diarization" in results and results["diarization"]:
|
| 197 |
-
diar = results["diarization"]
|
| 198 |
-
baseline = BASELINES["diarization"]
|
| 199 |
-
der = diar.get("der", diar.get("average_der", 0)) * 100
|
| 200 |
-
rtfx = diar.get("rtfx", diar.get("average_rtfx", 0))
|
| 201 |
-
|
| 202 |
-
der_status = "✓" if der <= baseline["der_percent"] * 1.2 else "✗"
|
| 203 |
-
rtfx_status = "✓" if rtfx >= baseline["rtfx_min"] else "✗"
|
| 204 |
-
|
| 205 |
-
print(f"\nDiarization ({baseline['description']}):")
|
| 206 |
-
print(f" DER: {der:.1f}% (baseline: {baseline['der_percent']}%) {der_status}")
|
| 207 |
-
print(f" RTFx: {rtfx:.1f}x (baseline: {baseline['rtfx_min']}x+) {rtfx_status}")
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
def main():
|
| 211 |
-
parser = argparse.ArgumentParser(description="FluidAudio Benchmark Suite")
|
| 212 |
-
parser.add_argument("--quick", action="store_true", help="Quick smoke test with smaller datasets")
|
| 213 |
-
parser.add_argument("--asr-only", action="store_true", help="Run ASR benchmark only")
|
| 214 |
-
parser.add_argument("--vad-only", action="store_true", help="Run VAD benchmark only")
|
| 215 |
-
parser.add_argument("--diar-only", action="store_true", help="Run diarization benchmark only")
|
| 216 |
-
parser.add_argument("--output-dir", type=str, help="Output directory for results")
|
| 217 |
-
args = parser.parse_args()
|
| 218 |
-
|
| 219 |
-
# Determine which benchmarks to run
|
| 220 |
-
run_all = not (args.asr_only or args.vad_only or args.diar_only)
|
| 221 |
-
|
| 222 |
-
# Setup output directory
|
| 223 |
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 224 |
-
if args.output_dir:
|
| 225 |
-
output_dir = Path(args.output_dir)
|
| 226 |
-
else:
|
| 227 |
-
output_dir = Path("benchmark-results") / timestamp
|
| 228 |
-
|
| 229 |
-
output_dir.mkdir(parents=True, exist_ok=True)
|
| 230 |
-
|
| 231 |
-
print("=" * 60)
|
| 232 |
-
print("FluidAudio Benchmark Suite")
|
| 233 |
-
print("=" * 60)
|
| 234 |
-
print(f"Mode: {'Quick' if args.quick else 'Full'}")
|
| 235 |
-
print(f"Output: {output_dir}")
|
| 236 |
-
print(f"Time: {timestamp}")
|
| 237 |
-
|
| 238 |
-
# Build first
|
| 239 |
-
if not build_release():
|
| 240 |
-
sys.exit(1)
|
| 241 |
-
|
| 242 |
-
results = {}
|
| 243 |
-
|
| 244 |
-
# Run benchmarks
|
| 245 |
-
if run_all or args.asr_only:
|
| 246 |
-
results["asr"] = run_asr_benchmark(output_dir, args.quick)
|
| 247 |
-
|
| 248 |
-
if run_all or args.vad_only:
|
| 249 |
-
results["vad"] = run_vad_benchmark(output_dir, args.quick)
|
| 250 |
-
|
| 251 |
-
if run_all or args.diar_only:
|
| 252 |
-
results["diarization"] = run_diarization_benchmark(output_dir, args.quick)
|
| 253 |
-
|
| 254 |
-
# Save combined results
|
| 255 |
-
combined_output = output_dir / "benchmark_results.json"
|
| 256 |
-
combined_output.write_text(json.dumps({
|
| 257 |
-
"timestamp": timestamp,
|
| 258 |
-
"mode": "quick" if args.quick else "full",
|
| 259 |
-
"baselines": BASELINES,
|
| 260 |
-
"results": results
|
| 261 |
-
}, indent=2))
|
| 262 |
-
|
| 263 |
-
# Compare against baselines
|
| 264 |
-
compare_results(results)
|
| 265 |
-
|
| 266 |
-
print("\n" + "=" * 60)
|
| 267 |
-
print("Benchmark complete!")
|
| 268 |
-
print("=" * 60)
|
| 269 |
-
print(f"Results saved to: {combined_output}")
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
if __name__ == "__main__":
|
| 273 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|