| """ |
| Standalone Higgs Audio V2 Tokenizer — ONNX inference. |
| |
| Encodes audio to discrete codec codes and decodes codes back to audio, |
| using the four exported ONNX sub-models: |
| |
| acoustic_encoder.onnx — DAC encoder: (1,1,T_24k) → (1,256,T_frames) |
| semantic_encoder.onnx — HuBERT encoder: (1,T_16k) → (1,768,T_frames) |
| quantizer_encoder.onnx — RVQ encode: (1,256,T)+(1,768,T) → (8,1,T) |
| higgs_decoder.onnx — DAC decode: (8,1,T) → (1,1,T_24k) |
| |
| No PyTorch, no transformers required at runtime — only onnxruntime. |
| |
| Usage: |
| # Round-trip encode → decode (sanity check) |
| python higgs_inference.py --input speech.wav --output reconstructed.wav |
| |
| # Encode only → save codes as .npy |
| python higgs_inference.py --input speech.wav --encode-only --codes-out codes.npy |
| |
| # Decode only from saved codes |
| python higgs_inference.py --decode-only --codes-in codes.npy --output decoded.wav |
| |
| # Use a different models directory |
| python higgs_inference.py --models-dir path/to/higgs/models --input speech.wav --output out.wav |
| """ |
|
|
| import argparse |
| import sys |
| from pathlib import Path |
|
|
| import numpy as np |
|
|
| |
| |
| |
| SR_24K = 24_000 |
| SR_16K = 16_000 |
| HOP_LENGTH = 960 |
| D_ACOUSTIC = 256 |
| D_SEMANTIC = 768 |
| N_CODEBOOKS = 8 |
| CODEBOOK_SIZE = 1024 |
| DEFAULT_MODELS_DIR = Path(__file__).parent / "higgs" / "models" |
|
|
|
|
| |
| |
| |
|
|
| def load_wav(path: str, target_sr: int) -> np.ndarray: |
| """Load an audio file, resample to target_sr, mix to mono. |
| |
| Returns float32 array in [-1, 1], shape (T,). |
| Tries torchaudio first (accurate), falls back to scipy. |
| """ |
| path = str(path) |
|
|
| |
| try: |
| import torchaudio, torch |
| wav, sr = torchaudio.load(path) |
| if sr != target_sr: |
| wav = torchaudio.functional.resample(wav, sr, target_sr) |
| if wav.shape[0] > 1: |
| wav = wav.mean(0, keepdim=True) |
| return wav.squeeze(0).numpy().astype(np.float32) |
| except ImportError: |
| pass |
|
|
| |
| from scipy.io import wavfile |
| from scipy.signal import resample_poly |
| from math import gcd |
| sr, data = wavfile.read(path) |
| if data.dtype == np.int16: |
| data = data.astype(np.float32) / 32768.0 |
| elif data.dtype == np.int32: |
| data = data.astype(np.float32) / 2147483648.0 |
| elif data.dtype != np.float32: |
| data = data.astype(np.float32) |
| if data.ndim > 1: |
| data = data.mean(axis=1) |
| if sr != target_sr: |
| g = gcd(target_sr, sr) |
| data = resample_poly(data, target_sr // g, sr // g).astype(np.float32) |
| return data |
|
|
|
|
| def save_wav(path: str, waveform: np.ndarray, sr: int = SR_24K): |
| """Save float32 numpy waveform as 16-bit PCM WAV.""" |
| path = str(path) |
| try: |
| import soundfile as sf |
| sf.write(path, waveform, sr, subtype="PCM_16") |
| return |
| except ImportError: |
| pass |
| from scipy.io import wavfile |
| pcm = np.clip(waveform, -1.0, 1.0) |
| wavfile.write(path, sr, (pcm * 32767).astype(np.int16)) |
|
|
|
|
| |
| |
| |
|
|
| class HiggsOnnxSessions: |
| """Holds the four Higgs ONNX InferenceSession objects.""" |
|
|
| MODEL_FILES = { |
| "acoustic_encoder": "acoustic_encoder.onnx", |
| "semantic_encoder": "semantic_encoder.onnx", |
| "quantizer_encoder": "quantizer_encoder.onnx", |
| "higgs_decoder": "higgs_decoder.onnx", |
| } |
|
|
| def __init__(self, models_dir: str, provider: str = "CPUExecutionProvider"): |
| import onnxruntime as ort |
| models_dir = Path(models_dir) |
| opts = ort.SessionOptions() |
| opts.log_severity_level = 3 |
|
|
| self.sessions = {} |
| for key, filename in self.MODEL_FILES.items(): |
| p = models_dir / filename |
| if not p.exists(): |
| raise FileNotFoundError( |
| f"Missing: {p}\n" |
| f"Export Higgs models first:\n" |
| f" cd OmniVoice && python optimize.py --higgs-only" |
| ) |
| self.sessions[key] = ort.InferenceSession( |
| str(p), sess_options=opts, providers=[provider] |
| ) |
| print(f"Loaded {len(self.sessions)} Higgs ONNX sessions from {models_dir}") |
|
|
| def __getitem__(self, key): |
| return self.sessions[key] |
|
|
|
|
| |
| |
| |
|
|
| _ORT_TO_NP = { |
| "tensor(float16)": np.float16, |
| "tensor(float)": np.float32, |
| "tensor(double)": np.float64, |
| "tensor(int64)": np.int64, |
| "tensor(int32)": np.int32, |
| } |
|
|
|
|
| def _cast_feed(feed: dict, sess) -> dict: |
| """Auto-cast numpy arrays to the dtype expected by the ONNX session. |
| |
| OnnxFloatToFloat16 converts all float inputs/outputs to float16. Feeding |
| float32 to such a model raises INVALID_ARGUMENT. This inspects the session |
| input metadata and casts each value to the declared type. |
| """ |
| type_map = {inp.name: _ORT_TO_NP.get(inp.type) for inp in sess.get_inputs()} |
| out = {} |
| for k, v in feed.items(): |
| tgt = type_map.get(k) |
| if tgt is not None and isinstance(v, np.ndarray) and v.dtype != tgt: |
| v = v.astype(tgt) |
| out[k] = v |
| return out |
|
|
|
|
| |
| |
| |
|
|
| def encode(sessions: HiggsOnnxSessions, wav_path: str) -> np.ndarray: |
| """Encode an audio file to RVQ codec codes. |
| |
| Pipeline: |
| wav_path (24 kHz) → acoustic_encoder → (1, 256, T_a) |
| wav_path (16 kHz) → semantic_encoder → (1, 768, T_s) |
| align T_s → T_a (linear interpolation along time axis) |
| concat channels → quantizer_encoder → (8, 1, T_a) int64 |
| |
| Returns: |
| codes: np.ndarray shape (8, 1, T_a) dtype int64 |
| T_a is the number of codec frames (25 per second of 24 kHz audio). |
| """ |
| |
| wav24 = load_wav(wav_path, SR_24K) |
| wav16 = load_wav(wav_path, SR_16K) |
|
|
| dur = len(wav24) / SR_24K |
| print(f" Audio: {dur:.2f}s " |
| f"({len(wav24)} samples @ {SR_24K} Hz, " |
| f"{len(wav16)} samples @ {SR_16K} Hz)") |
|
|
| |
| waveform_24k = wav24[None, None, :] |
| _ae_sess = sessions["acoustic_encoder"] |
| acoustic_feat = _ae_sess.run( |
| ["acoustic_features"], |
| _cast_feed({"waveform_24k": waveform_24k}, _ae_sess) |
| )[0] |
| T_a = acoustic_feat.shape[2] |
| print(f" acoustic_encoder → {acoustic_feat.shape} ({T_a} frames, {T_a/dur:.1f} fps)") |
|
|
| |
| waveform_16k = wav16[None, :] |
| _se_sess = sessions["semantic_encoder"] |
| semantic_feat = _se_sess.run( |
| ["semantic_features"], |
| _cast_feed({"waveform_16k": waveform_16k}, _se_sess) |
| )[0] |
| T_s = semantic_feat.shape[2] |
| print(f" semantic_encoder → {semantic_feat.shape} ({T_s} frames, {T_s/dur:.1f} fps)") |
|
|
| |
| |
| |
| |
| if T_s != T_a: |
| T = min(T_a, T_s) |
| acoustic_feat = acoustic_feat[:, :, :T] |
| semantic_feat = semantic_feat[:, :, :T] |
| print(f" [warn] T_a={T_a} != T_s={T_s}, trimmed both to T={T}") |
|
|
| |
| _qe_sess = sessions["quantizer_encoder"] |
| codes = _qe_sess.run( |
| ["codes"], |
| _cast_feed({ |
| "acoustic_features": acoustic_feat, |
| "semantic_features": semantic_feat, |
| }, _qe_sess) |
| )[0] |
| print(f" quantizer_encoder → {codes.shape} " |
| f"({N_CODEBOOKS} codebooks × {T_a} frames) " |
| f"value range [{codes.min()}, {codes.max()}]") |
|
|
| return codes |
|
|
|
|
| |
| |
| |
|
|
| def decode(sessions: HiggsOnnxSessions, codes: np.ndarray) -> np.ndarray: |
| """Decode RVQ codec codes back to a waveform. |
| |
| Args: |
| codes: np.ndarray shape (8, 1, T_frames) dtype int64 |
| |
| Returns: |
| waveform: np.ndarray shape (T_samples,) float32 at 24 kHz |
| """ |
| if codes.ndim != 3 or codes.shape[0] != N_CODEBOOKS: |
| raise ValueError( |
| f"codes must have shape (8, 1, T_frames), got {codes.shape}" |
| ) |
| if codes.dtype != np.int64: |
| codes = codes.astype(np.int64) |
|
|
| T_frames = codes.shape[2] |
| expected_dur = T_frames * HOP_LENGTH / SR_24K |
|
|
| _hd_sess = sessions["higgs_decoder"] |
| waveform = _hd_sess.run( |
| ["waveform_24k"], |
| _cast_feed({"codes": codes}, _hd_sess) |
| )[0] |
|
|
| waveform = waveform.squeeze() |
| actual_dur = len(waveform) / SR_24K |
| print(f" higgs_decoder → {waveform.shape} " |
| f"({actual_dur:.2f}s expected {expected_dur:.2f}s)") |
|
|
| return waveform |
|
|
|
|
| |
| |
| |
|
|
| def encode_decode(sessions: HiggsOnnxSessions, wav_path: str) -> tuple: |
| """Encode audio to codes then decode back to waveform. |
| |
| Returns: |
| codes: np.ndarray (8, 1, T_frames) int64 |
| waveform: np.ndarray (T_samples,) float32 |
| """ |
| print("[Encode]") |
| codes = encode(sessions, wav_path) |
| print("[Decode]") |
| waveform = decode(sessions, codes) |
| return codes, waveform |
|
|
|
|
| |
| |
| |
|
|
| def main(): |
| parser = argparse.ArgumentParser( |
| description="Higgs Audio V2 Tokenizer — standalone ONNX encode/decode" |
| ) |
| parser.add_argument( |
| "--models-dir", default=str(DEFAULT_MODELS_DIR), |
| help=f"Higgs ONNX models directory (default: {DEFAULT_MODELS_DIR})" |
| ) |
| parser.add_argument( |
| "--input", "-i", default=None, |
| help="Input audio file to encode (.wav or any format torchaudio/scipy supports)" |
| ) |
| parser.add_argument( |
| "--output", "-o", default="output.wav", |
| help="Output WAV file for decoded audio (default: output.wav)" |
| ) |
| parser.add_argument( |
| "--encode-only", action="store_true", |
| help="Only encode — save codes to --codes-out, do not decode" |
| ) |
| parser.add_argument( |
| "--decode-only", action="store_true", |
| help="Only decode — load codes from --codes-in, skip encoding" |
| ) |
| parser.add_argument( |
| "--codes-out", default="codes.npy", |
| help="Path to save encoded codes as .npy (default: codes.npy)" |
| ) |
| parser.add_argument( |
| "--codes-in", default=None, |
| help="Path to load codes .npy for --decode-only mode" |
| ) |
| parser.add_argument( |
| "--cuda", action="store_true", |
| help="Use CUDAExecutionProvider instead of CPU" |
| ) |
| args = parser.parse_args() |
|
|
| provider = "CUDAExecutionProvider" if args.cuda else "CPUExecutionProvider" |
|
|
| |
| if args.encode_only and args.decode_only: |
| parser.error("--encode-only and --decode-only are mutually exclusive") |
| if not args.decode_only and args.input is None: |
| parser.error("--input is required unless --decode-only is used") |
| if args.decode_only and args.codes_in is None: |
| parser.error("--codes-in is required when using --decode-only") |
|
|
| |
| sessions = HiggsOnnxSessions(args.models_dir, provider) |
|
|
| |
| if args.encode_only: |
| print(f"\nEncoding: {args.input}") |
| codes = encode(sessions, args.input) |
| np.save(args.codes_out, codes) |
| print(f"\nSaved codes → {args.codes_out} shape={codes.shape} dtype={codes.dtype}") |
|
|
| |
| elif args.decode_only: |
| print(f"\nLoading codes from: {args.codes_in}") |
| codes = np.load(args.codes_in) |
| print(f" codes shape={codes.shape} dtype={codes.dtype}") |
| print(f"\nDecoding...") |
| waveform = decode(sessions, codes) |
| save_wav(args.output, waveform, SR_24K) |
| print(f"Saved WAV → {args.output} ({len(waveform)/SR_24K:.2f}s @ {SR_24K} Hz)") |
|
|
| |
| else: |
| |
| print(f"\nRound-trip encode → decode") |
| print(f" Input : {args.input}") |
| print(f" Output: {args.output}") |
| codes, waveform = encode_decode(sessions, args.input) |
|
|
| |
| np.save(args.codes_out, codes) |
| print(f"\nSaved codes → {args.codes_out}") |
|
|
| save_wav(args.output, waveform, SR_24K) |
| dur_in = len(load_wav(args.input, SR_24K)) / SR_24K |
| dur_out = len(waveform) / SR_24K |
| print(f"Saved WAV → {args.output} ({dur_out:.2f}s, input was {dur_in:.2f}s)") |
| print(f"\nReconstruction note: codec compression (RVQ, 8 codebooks × 1024 entries)") |
| print(f"at 25 fps introduces mild quality loss — this is expected.") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|