"""Mimi Neural Audio Codec - Hugging Face Space A Gradio demo for the Kyutai Mimi neural audio codec (1.1 kbps @ 12.5 Hz). Supports encoding audio to compact token representations and decoding back. Paper: http://kyutai.org/Moshi.pdf Model: gcxrightsify/mimi on Hugging Face """ import gradio as gr import numpy as np import torch import torchaudio import tempfile import json import os # HF token for private model access (set as Space secret) HF_TOKEN = os.environ.get("HF_TOKEN") # ============================================================================= # Mimi Engine (standalone, no sunalink dependency) # ============================================================================= MIMI_DOWNSAMPLE_FACTOR = 1920 # 24kHz @ 12.5 codes/sec CONTEXT_FRAMES = 8 # Left context for chunked processing CROSSFADE_SAMPLES = 240 # 10ms @ 24kHz class MimiCodec: """Minimal Mimi codec wrapper for the Space.""" def __init__(self): self._model = None self._feature_extractor = None self._device = None self._sampling_rate = None def _ensure_loaded(self): if self._model is None: from transformers import MimiModel, AutoFeatureExtractor self._device = torch.device("cuda" if torch.cuda.is_available() else "cpu") self._model = MimiModel.from_pretrained("gcxrightsify/mimi", token=HF_TOKEN) self._feature_extractor = AutoFeatureExtractor.from_pretrained("gcxrightsify/mimi", token=HF_TOKEN) self._model.to(self._device) self._model.eval() self._sampling_rate = self._feature_extractor.sampling_rate print(f"Mimi loaded on {self._device}") def encode(self, audio: np.ndarray, sample_rate: int) -> dict: """Encode audio to Mimi tokens.""" self._ensure_loaded() # Convert to mono if stereo if audio.ndim == 2: audio = audio.mean(axis=0) # Resample if needed (high-quality Kaiser-windowed sinc interpolation) if sample_rate != self._sampling_rate: audio_torch = torch.from_numpy(audio).float() resampler = torchaudio.transforms.Resample( orig_freq=sample_rate, new_freq=self._sampling_rate, resampling_method="sinc_interp_kaiser", lowpass_filter_width=64, # More taps = better quality ) audio = resampler(audio_torch).numpy() # Prepare input inputs = self._feature_extractor( raw_audio=audio, sampling_rate=self._sampling_rate, return_tensors="pt" ) input_values = inputs["input_values"].to(self._device) # Encode with chunking for memory efficiency import math n_samples = input_values.shape[-1] total_frames = math.ceil(n_samples / MIMI_DOWNSAMPLE_FACTOR) pad = total_frames * MIMI_DOWNSAMPLE_FACTOR - n_samples if pad: input_values = torch.nn.functional.pad(input_values, (0, pad)) chunk_length = 2.0 # seconds chunk_frames = max(1, int(chunk_length * self._sampling_rate / MIMI_DOWNSAMPLE_FACTOR)) chunk_codes = [] for start_f in range(0, total_frames, chunk_frames): end_f = min(start_f + chunk_frames, total_frames) ctx_f = min(CONTEXT_FRAMES, start_f) seg = input_values[ :, :, (start_f - ctx_f) * MIMI_DOWNSAMPLE_FACTOR: end_f * MIMI_DOWNSAMPLE_FACTOR ] with torch.no_grad(): codes = self._model.encode(seg).audio_codes[0] chunk_codes.append(codes[:, ctx_f:]) audio_codes = torch.cat(chunk_codes, dim=-1).cpu().numpy() return { "codes": audio_codes.tolist(), "shape": list(audio_codes.shape), "sample_rate": self._sampling_rate, "original_samples": n_samples, } def decode(self, encoded: dict) -> tuple[np.ndarray, int]: """Decode Mimi tokens back to audio.""" self._ensure_loaded() audio_codes = np.array(encoded["codes"], dtype=np.int64) audio_codes_tensor = torch.from_numpy(audio_codes).to(self._device) chunk_length = 2.0 chunk_frames = max(1, int(chunk_length * self._sampling_rate / MIMI_DOWNSAMPLE_FACTOR)) total_frames = audio_codes_tensor.shape[-1] decoded_chunks = [] for start_f in range(0, total_frames, chunk_frames): end_f = min(start_f + chunk_frames, total_frames) ctx_f = min(CONTEXT_FRAMES, start_f) seg_codes = audio_codes_tensor[:, start_f - ctx_f:end_f] with torch.no_grad(): decoded = self._model.decode(seg_codes.unsqueeze(0)).audio_values if ctx_f == 0: decoded_chunks.append(decoded) continue cut = ctx_f * MIMI_DOWNSAMPLE_FACTOR - CROSSFADE_SAMPLES decoded = decoded[..., cut:] fade_in = torch.linspace(0.0, 1.0, CROSSFADE_SAMPLES, device=decoded.device) prev = decoded_chunks[-1] prev[..., -CROSSFADE_SAMPLES:] = ( prev[..., -CROSSFADE_SAMPLES:] * (1.0 - fade_in) + decoded[..., :CROSSFADE_SAMPLES] * fade_in ) decoded_chunks.append(decoded[..., CROSSFADE_SAMPLES:]) audio_values = torch.cat(decoded_chunks, dim=-1) # Trim to original length original_samples = encoded.get("original_samples", audio_values.shape[-1]) audio_values = audio_values[..., :original_samples] audio_array = audio_values.squeeze().cpu().numpy() return audio_array, encoded["sample_rate"] # Global codec instance (load at startup to avoid first-request latency) codec = MimiCodec() codec._ensure_loaded() # ============================================================================= # Gradio Interface Functions # ============================================================================= def process_audio(audio_input): """Encode and decode audio, return comparison stats.""" if audio_input is None: return None, None, None, "Please upload an audio file." sample_rate, audio_data = audio_input # Convert to float32 normalized if audio_data.dtype == np.int16: audio_data = audio_data.astype(np.float32) / 32768.0 elif audio_data.dtype == np.int32: audio_data = audio_data.astype(np.float32) / 2147483648.0 # Ensure correct shape (samples,) or (channels, samples) if audio_data.ndim == 2 and audio_data.shape[0] > audio_data.shape[1]: audio_data = audio_data.T # (samples, channels) -> (channels, samples) try: # Encode encoded = codec.encode(audio_data, sample_rate) # Decode reconstructed, out_sr = codec.decode(encoded) # Calculate stats codes_array = np.array(encoded["codes"]) n_codebooks, n_frames = codes_array.shape original_bytes = audio_data.size * 4 # float32 compressed_bits = n_codebooks * n_frames * 11 # 2048-entry codebook = 11 bits compressed_bytes = compressed_bits / 8 compression_ratio = original_bytes / compressed_bytes duration = len(audio_data.flatten()) / sample_rate if audio_data.ndim == 1 else audio_data.shape[-1] / sample_rate bitrate = compressed_bits / duration / 1000 # kbps stats = f"""## Compression Statistics | Metric | Value | |--------|-------| | Original size | {original_bytes:,} bytes | | Compressed size | {int(compressed_bytes):,} bytes | | Compression ratio | {compression_ratio:.1f}x | | Bitrate | {bitrate:.2f} kbps | | Duration | {duration:.2f}s | | Codebooks | {n_codebooks} | | Frames | {n_frames} | | Frame rate | {n_frames / duration:.1f} Hz | """ # Save tokens for download tokens_json = json.dumps(encoded, indent=2) with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: f.write(tokens_json) tokens_path = f.name return (out_sr, reconstructed), tokens_path, stats, "" except Exception as e: return None, None, None, f"Error: {str(e)}" def decode_from_tokens(tokens_file): """Decode audio from uploaded token JSON file.""" if tokens_file is None: return None, "Please upload a tokens JSON file." try: with open(tokens_file.name, 'r') as f: encoded = json.load(f) audio, sr = codec.decode(encoded) return (sr, audio), "" except Exception as e: return None, f"Error decoding: {str(e)}" # ============================================================================= # Gradio App # ============================================================================= with gr.Blocks(title="Mimi Audio Codec", theme=gr.themes.Soft()) as demo: gr.Markdown(""" # Mimi Neural Audio Codec A demo of [Kyutai's Mimi](https://huggingface.co/gcxrightsify/mimi) neural audio codec - achieving **1.1 kbps** compression at 12.5 Hz frame rate. Mimi uses a causal architecture with 8 RVQ codebooks (2048 entries each), making it suitable for real-time streaming applications. **Paper:** [Moshi: A Speech-Text Foundation Model for Real-Time Dialogue](http://kyutai.org/Moshi.pdf) """) with gr.Tab("Encode & Decode"): with gr.Row(): with gr.Column(): audio_input = gr.Audio( label="Input Audio", type="numpy", sources=["upload", "microphone"], ) encode_btn = gr.Button("Encode & Decode", variant="primary") with gr.Column(): audio_output = gr.Audio(label="Reconstructed Audio", type="numpy") tokens_download = gr.File(label="Download Tokens (JSON)") stats_output = gr.Markdown(label="Statistics") error_output = gr.Markdown(label="Errors", visible=True) encode_btn.click( fn=process_audio, inputs=[audio_input], outputs=[audio_output, tokens_download, stats_output, error_output], ) with gr.Tab("Decode from Tokens"): gr.Markdown("Upload a previously saved tokens JSON file to decode back to audio.") with gr.Row(): tokens_upload = gr.File(label="Upload Tokens JSON", file_types=[".json"]) decode_btn = gr.Button("Decode", variant="primary") decoded_audio = gr.Audio(label="Decoded Audio", type="numpy") decode_error = gr.Markdown() decode_btn.click( fn=decode_from_tokens, inputs=[tokens_upload], outputs=[decoded_audio, decode_error], ) with gr.Tab("About"): gr.Markdown(""" ## About Mimi Mimi is a neural audio codec developed by Kyutai as part of the Moshi project. Key features: - **Ultra-low bitrate**: 1.1 kbps (compared to 6-24 kbps for other neural codecs) - **Causal architecture**: Suitable for real-time streaming - **12.5 Hz frame rate**: 1920 samples per frame at 24 kHz - **8 RVQ codebooks**: Each with 2048 entries (11 bits per code) ### Technical Details | Parameter | Value | |-----------|-------| | Sample rate | 24,000 Hz | | Frame rate | 12.5 Hz | | Codebooks | 8 | | Codebook size | 2048 | | Bits per frame | 88 (8 x 11) | | Bitrate | 1.1 kbps | ### How It Works 1. **Encoder**: Transforms audio into continuous embeddings using causal convolutions and transformers 2. **Quantizer**: Converts embeddings to discrete codes using Residual Vector Quantization (RVQ) 3. **Decoder**: Reconstructs audio from quantized embeddings The causal architecture means Mimi can process audio in real-time without looking ahead, making it ideal for live streaming and interactive applications. ### Part of Sunalink This Space demonstrates the Mimi codec as used in [Sunalink](https://github.com/rightsify/sunalink), a multi-modal compression engine supporting neural and classical codecs. """) if __name__ == "__main__": demo.launch()