File size: 3,133 Bytes
305fc2a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | #!/usr/bin/env python3
# Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../LICENSE).
"""Write the audio front-end reference for `eg2-cli audiocheck`: for every clip of tests/audio/, the Python front end's
16 kHz waveform and log-mel features (raw float32 files in tests/audio/ref/) and tests/audio-preprocess-cases.json
with the expected counts and the tolerances the Swift front end must meet.
python3 make_audio_preprocess_cases.py [--audio audio] [--out audio-preprocess-cases.json]
Tolerances: lossless files decode identically on both sides and the resampler is the same double-precision code, so
the waveform must match to 1e-6 (0 for files already at 16 kHz); AAC files decode with the same Apple decoder (1e-6);
MP3 files are decoded by libsndfile here (when installed) and by Apple's decoder in Swift, which differ by up to ~3e-5
per sample, which in near-silent frames moves ln(mel + 0.001) by up to ~1 (tolerance 1.0); for the other formats the
features must match to 0.02 (the single-precision FFTs of numpy and Accelerate round differently, ~0.005 worst).
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
import numpy as np
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parent / "python"))
import eg2_audio_frontend as fe # noqa: E402
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--audio", default=str(HERE / "audio"))
ap.add_argument("--out", default=str(HERE / "audio-preprocess-cases.json"))
args = ap.parse_args()
audio = Path(args.audio)
ref = audio / "ref"
ref.mkdir(exist_ok=True)
manifest = json.loads((audio / "manifest.json").read_text(encoding="utf-8"))
cases = []
for it in manifest:
d = fe.decode(audio / it["file"])
wav = fe.resample_16k(fe.to_mono(d.samples), d.sample_rate)
inputs = fe.prepare_audio_inputs(wav)
feats = fe.log_mel(wav)
wave_file = f"ref/{it['name']}.wave16k.f32"
feat_file = f"ref/{it['name']}.features.f32"
wav.astype("<f4").tofile(audio / wave_file)
feats.astype("<f4").tofile(audio / feat_file)
is_mp3 = it["file"].endswith(".mp3")
cases.append(dict(file=f"audio/{it['file']}", sample_rate=d.sample_rate, channels=int(d.samples.shape[1]), n_samples16k=int(len(wav)),
n_frames=inputs.n_frames, n_tokens=inputs.n_tokens, bucket=inputs.bucket,
wave_file=f"audio/{wave_file}", features_file=f"audio/{feat_file}",
wave_tolerance=1e-4 if is_mp3 else (0.0 if d.sample_rate == 16000 and not it["lossy"] else 1e-6),
feature_tolerance=1.0 if is_mp3 else 0.02, python_decoder=d.decoder,
note=("mp3: libsndfile (Python) vs Apple (Swift) decoders differ by ~3e-5 per sample" if is_mp3 else None)))
print(cases[-1], flush=True)
Path(args.out).write_text(json.dumps(cases, indent=1) + "\n", encoding="utf-8")
print(f"{len(cases)} cases -> {args.out}")
if __name__ == "__main__":
main()
|