Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files- .gitattributes +3 -0
- README.md +43 -6
- __pycache__/app.cpython-314.pyc +0 -0
- __pycache__/core.cpython-314.pyc +0 -0
- app.py +193 -0
- assets/251-118436-0003.wav +3 -0
- assets/5_1812_20170628135856.wav +3 -0
- assets/p226_006.wav +3 -0
- core.py +158 -0
- requirements.txt +5 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
assets/251-118436-0003.wav filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
assets/5_1812_20170628135856.wav filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
assets/p226_006.wav filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,13 +1,50 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.30.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: FocalCodec Quantization Lab
|
| 3 |
+
emoji: ⚡
|
| 4 |
+
colorFrom: blue
|
| 5 |
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.30.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
+
short_description: FocalCodec weights at int4–int32 vs fp4–fp32
|
| 10 |
+
python_version: "3.12"
|
| 11 |
+
startup_duration_timeout: 30m
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# ⚡ FocalCodec 12.5 Hz — weight quantization lab
|
| 15 |
+
|
| 16 |
+
Interactive demo of what happens to [`lucadellalib/focalcodec_12_5hz`](https://huggingface.co/lucadellalib/focalcodec_12_5hz)
|
| 17 |
+
when its **145 M weights** are stored at different numeric precisions:
|
| 18 |
+
|
| 19 |
+
`int4` · `int8` · `int16` · `int32` · `fp4` · `fp8` · `fp16` · `fp32`
|
| 20 |
+
|
| 21 |
+
The token stream is untouched, so the codec still runs at **0.1625 kbps**
|
| 22 |
+
(12.5 tokens/s × 13 bits) at every level — only the size of the network changes
|
| 23 |
+
(72.6 MB at 4-bit down to 581.1 MB at fp32).
|
| 24 |
+
|
| 25 |
+
## Three views
|
| 26 |
+
|
| 27 |
+
| Tab | What it shows |
|
| 28 |
+
| --- | --- |
|
| 29 |
+
| **Compare** | One reconstructed clip + input/reconstruction spectrograms, side by side for all 8 levels |
|
| 30 |
+
| **Playground** | Encode → decode a single clip at one chosen level, with its metrics row |
|
| 31 |
+
| **Benchmark** | SI-SDR, STOI, LSD, stored model size, bitrate and real-time factor for all 8 levels, plus a summary chart |
|
| 32 |
+
|
| 33 |
+
## Method
|
| 34 |
+
|
| 35 |
+
Post-training **fake quantization** of every floating-point tensor: per-output-channel
|
| 36 |
+
absmax scale, round-to-nearest in the target format, dequantize back to fp32, then run
|
| 37 |
+
the untouched FocalCodec forward pass. `fp4` is IEEE-style E2M1, `fp8` is E4M3, `fp16`
|
| 38 |
+
and `fp32` are exact casts. Nothing is fine-tuned or calibrated on data.
|
| 39 |
+
|
| 40 |
+
⚠️ **On SI-SDR:** it is deeply negative at every level, including `fp32`. That is
|
| 41 |
+
expected — at 0.16 kbps FocalCodec does not preserve waveform phase, and the
|
| 42 |
+
[FocalCodec paper](https://arxiv.org/abs/2502.04465) reports SI-SNR ≈ −37 dB for the
|
| 43 |
+
untouched model. Use **STOI** (intelligibility, higher is better) and **LSD**
|
| 44 |
+
(log-spectral distance, lower is better) to compare levels.
|
| 45 |
+
|
| 46 |
+
## Credits
|
| 47 |
+
|
| 48 |
+
Model, method and example audio by
|
| 49 |
+
[Luca Della Libera](https://huggingface.co/lucadellalib) et al. — Apache-2.0.
|
| 50 |
+
[FocalCodec: Low-Bitrate Speech Coding via Focal Modulation Networks](https://arxiv.org/abs/2502.04465), NeurIPS 2025.
|
__pycache__/app.cpython-314.pyc
ADDED
|
Binary file (15.5 kB). View file
|
|
|
__pycache__/core.cpython-314.pyc
ADDED
|
Binary file (13.2 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FocalCodec 12.5 Hz weight-quantization lab (Gradio / ZeroGPU)."""
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import time
|
| 5 |
+
|
| 6 |
+
import spaces # must precede torch: patches torch.cuda for ZeroGPU
|
| 7 |
+
|
| 8 |
+
import gradio as gr
|
| 9 |
+
import soundfile as sf
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
+
import core
|
| 13 |
+
from core import QUANT_LEVELS
|
| 14 |
+
|
| 15 |
+
MODEL_REPO = "lucadellalib/focalcodec_12_5hz"
|
| 16 |
+
HUB_REPO = "lucadellalib/focalcodec"
|
| 17 |
+
MAX_SECONDS = 20
|
| 18 |
+
HEADERS = ["Level", "SI-SDR (dB)", "STOI", "LSD (dB)", "Model size (MB)", "Bitrate (kbps)", "RTF"]
|
| 19 |
+
DATATYPES = ["str", "number", "number", "number", "number", "number", "number"]
|
| 20 |
+
EXAMPLES = [
|
| 21 |
+
["assets/251-118436-0003.wav"],
|
| 22 |
+
["assets/p226_006.wav"],
|
| 23 |
+
["assets/5_1812_20170628135856.wav"],
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
codec = torch.hub.load(HUB_REPO, "focalcodec", config=MODEL_REPO, trust_repo=True)
|
| 27 |
+
codec.eval().requires_grad_(False)
|
| 28 |
+
codec.to("cuda")
|
| 29 |
+
BASE_STATE = {k: v.detach().cpu().clone() for k, v in codec.state_dict().items()}
|
| 30 |
+
SIZES = {level: core.model_size_mb(BASE_STATE, level) for level in QUANT_LEVELS}
|
| 31 |
+
SAMPLE_RATE = codec.sample_rate_input
|
| 32 |
+
assert codec.sample_rate_output == SAMPLE_RATE, "demo assumes a 16/16 kHz codec"
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _estimate_duration(audio_path: str, levels: tuple) -> float:
|
| 36 |
+
try:
|
| 37 |
+
seconds = min(sf.info(audio_path).duration, MAX_SECONDS)
|
| 38 |
+
except Exception:
|
| 39 |
+
seconds = float(MAX_SECONDS)
|
| 40 |
+
return min(240.0, 5.0 + 2.0 * len(levels) + 2.5 * seconds)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@spaces.GPU(duration=_estimate_duration)
|
| 44 |
+
def _encode_decode(audio_path: str, levels: tuple) -> dict:
|
| 45 |
+
"""Reconstruct `audio_path` once per weight-quantization level, scoring each result."""
|
| 46 |
+
ref, sr = core.load_audio(audio_path, SAMPLE_RATE, MAX_SECONDS)
|
| 47 |
+
sig = torch.from_numpy(ref).unsqueeze(0).cuda()
|
| 48 |
+
rows, reconstructed = [], {}
|
| 49 |
+
for level in levels:
|
| 50 |
+
codec.load_state_dict(BASE_STATE if level == "fp32" else core.quantize_state(BASE_STATE, level))
|
| 51 |
+
torch.cuda.synchronize()
|
| 52 |
+
start = time.perf_counter()
|
| 53 |
+
with torch.inference_mode():
|
| 54 |
+
rec = codec.sig_to_sig(sig).squeeze(0).float().cpu().numpy()
|
| 55 |
+
torch.cuda.synchronize()
|
| 56 |
+
rtf = (time.perf_counter() - start) / (len(ref) / sr)
|
| 57 |
+
rows.append(core.evaluate(ref, rec, sr, rtf, SIZES[level], level))
|
| 58 |
+
reconstructed[level] = (sr, rec)
|
| 59 |
+
codec.load_state_dict(BASE_STATE)
|
| 60 |
+
return {"input": (sr, ref), "rows": rows, "audio": reconstructed}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
_CACHE: dict[str, dict] = {}
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def run(audio_path: str, levels: list | tuple) -> dict:
|
| 67 |
+
"""Encode and decode the audio at every requested weight-quantization level.
|
| 68 |
+
|
| 69 |
+
Results are cached by audio content and level set, so the Compare and
|
| 70 |
+
Benchmark tabs share a single GPU pass.
|
| 71 |
+
"""
|
| 72 |
+
if not audio_path:
|
| 73 |
+
raise gr.Error("Provide audio first: upload, record, or pick an example.")
|
| 74 |
+
levels = tuple(levels)
|
| 75 |
+
with open(audio_path, "rb") as fh:
|
| 76 |
+
key = hashlib.md5(fh.read()).hexdigest() + "|" + ",".join(levels)
|
| 77 |
+
if key not in _CACHE:
|
| 78 |
+
if len(_CACHE) >= 6:
|
| 79 |
+
_CACHE.clear()
|
| 80 |
+
_CACHE[key] = _encode_decode(audio_path, levels)
|
| 81 |
+
return _CACHE[key]
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def compare(audio_path: str):
|
| 85 |
+
"""Rebuild the audio at all eight levels and return one player + spectrogram per level."""
|
| 86 |
+
result = run(audio_path, QUANT_LEVELS)
|
| 87 |
+
audio = [result["audio"][level] for level in QUANT_LEVELS]
|
| 88 |
+
plots = [
|
| 89 |
+
core.spectrogram_figure(result["input"][1], result["audio"][level][1], SAMPLE_RATE, level)
|
| 90 |
+
for level in QUANT_LEVELS
|
| 91 |
+
]
|
| 92 |
+
return audio + plots
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def playground(audio_path: str, level: str):
|
| 96 |
+
"""Rebuild the audio at a single quantization level, with its metrics row."""
|
| 97 |
+
result = run(audio_path, [level])
|
| 98 |
+
rec = result["audio"][level]
|
| 99 |
+
return (
|
| 100 |
+
rec,
|
| 101 |
+
core.spectrogram_figure(result["input"][1], rec[1], SAMPLE_RATE, level),
|
| 102 |
+
[result["rows"][0]],
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def benchmark(audio_path: str):
|
| 107 |
+
"""Score every quantization level on one input: metrics table plus summary chart."""
|
| 108 |
+
result = run(audio_path, QUANT_LEVELS)
|
| 109 |
+
return result["rows"], core.metrics_figure(result["rows"])
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
HEADER = f"""# ⚡ FocalCodec 12.5 Hz — weight quantization lab
|
| 113 |
+
|
| 114 |
+
Same checkpoint ([`lucadellalib/focalcodec_12_5hz`](https://huggingface.co/lucadellalib/focalcodec_12_5hz)),
|
| 115 |
+
eight numeric formats for its **{sum(v.numel() for v in BASE_STATE.values()) / 1e6:.0f} M weights**: `int4 … int32` and `fp4 … fp32`.
|
| 116 |
+
The bitrate never changes — **{core.BITRATE_KBPS:g} kbps** (12.5 tokens/s × 13 bits) — only the storage precision of the network itself.
|
| 117 |
+
|
| 118 |
+
**Method:** post-training fake quantization, per-output-channel absmax scaling, round-to-nearest, dequantized back to fp32 for inference. `fp4` = E2M1, `fp8` = E4M3, `fp16`/`fp32` are exact casts. No fine-tuning.
|
| 119 |
+
|
| 120 |
+
⚠️ *SI-SDR sits far below 0 dB at every level: at 0.16 kbps the codec does not preserve waveform phase, and the FocalCodec paper reports SI-SNR ≈ −37 dB for the untouched model. It is an anchor, not a quality score — read **STOI** (intelligibility) and **LSD** (log-spectral distance, lower is better) instead.*
|
| 121 |
+
"""
|
| 122 |
+
|
| 123 |
+
FOOTER = """Audio examples from the [FocalCodec repository](https://github.com/lucadellalib/focalcodec)
|
| 124 |
+
(LibriSpeech, VoiceBank, MagicData). Model and method by Luca Della Libera et al., Apache-2.0."""
|
| 125 |
+
|
| 126 |
+
with gr.Blocks(title="FocalCodec quantization lab", theme=gr.themes.Soft()) as demo:
|
| 127 |
+
gr.Markdown(HEADER)
|
| 128 |
+
|
| 129 |
+
with gr.Tab("Compare"):
|
| 130 |
+
with gr.Row():
|
| 131 |
+
with gr.Column(scale=1, min_width=320):
|
| 132 |
+
cmp_input = gr.Audio(
|
| 133 |
+
sources=["upload", "microphone"], type="filepath", label="Input audio"
|
| 134 |
+
)
|
| 135 |
+
gr.Examples(examples=EXAMPLES, inputs=cmp_input, label="Examples")
|
| 136 |
+
cmp_button = gr.Button("Reconstruct at all 8 levels", variant="primary")
|
| 137 |
+
gr.Markdown(f"Input capped at {MAX_SECONDS} s.")
|
| 138 |
+
with gr.Column(scale=4):
|
| 139 |
+
cmp_audio, cmp_plot = {}, {}
|
| 140 |
+
for chunk in (QUANT_LEVELS[:4], QUANT_LEVELS[4:]):
|
| 141 |
+
with gr.Row():
|
| 142 |
+
for level in chunk:
|
| 143 |
+
with gr.Column():
|
| 144 |
+
gr.Markdown(f"**{level}**")
|
| 145 |
+
cmp_audio[level] = gr.Audio(interactive=False, label=level)
|
| 146 |
+
cmp_plot[level] = gr.Plot(label=f"{level} spectrogram")
|
| 147 |
+
cmp_button.click(
|
| 148 |
+
compare,
|
| 149 |
+
inputs=cmp_input,
|
| 150 |
+
outputs=[cmp_audio[level] for level in QUANT_LEVELS]
|
| 151 |
+
+ [cmp_plot[level] for level in QUANT_LEVELS],
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
with gr.Tab("Playground"):
|
| 155 |
+
with gr.Row():
|
| 156 |
+
with gr.Column(scale=1, min_width=320):
|
| 157 |
+
pg_input = gr.Audio(
|
| 158 |
+
sources=["upload", "microphone"], type="filepath", label="Input audio"
|
| 159 |
+
)
|
| 160 |
+
gr.Examples(examples=EXAMPLES, inputs=pg_input, label="Examples")
|
| 161 |
+
pg_level = gr.Dropdown(
|
| 162 |
+
choices=list(QUANT_LEVELS), value="int8", label="Weight format"
|
| 163 |
+
)
|
| 164 |
+
pg_button = gr.Button("Encode + decode", variant="primary")
|
| 165 |
+
with gr.Column(scale=3):
|
| 166 |
+
pg_output = gr.Audio(interactive=False, label="Reconstruction")
|
| 167 |
+
pg_plot = gr.Plot(label="Input vs reconstruction")
|
| 168 |
+
pg_table = gr.Dataframe(headers=HEADERS, datatype=DATATYPES, interactive=False)
|
| 169 |
+
pg_button.click(
|
| 170 |
+
playground, inputs=[pg_input, pg_level], outputs=[pg_output, pg_plot, pg_table]
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
with gr.Tab("Benchmark"):
|
| 174 |
+
with gr.Row():
|
| 175 |
+
with gr.Column(scale=1, min_width=320):
|
| 176 |
+
bm_input = gr.Audio(
|
| 177 |
+
sources=["upload", "microphone"], type="filepath", label="Input audio"
|
| 178 |
+
)
|
| 179 |
+
gr.Examples(examples=EXAMPLES, inputs=bm_input, label="Examples")
|
| 180 |
+
bm_button = gr.Button("Run all 8 levels", variant="primary")
|
| 181 |
+
with gr.Column(scale=3):
|
| 182 |
+
gr.Markdown(
|
| 183 |
+
"**STOI** ↑ intelligibility · **LSD** ↓ spectral error · "
|
| 184 |
+
"**RTF** = encode+decode time ÷ audio duration · size assumes "
|
| 185 |
+
"the stored weight format."
|
| 186 |
+
)
|
| 187 |
+
bm_table = gr.Dataframe(headers=HEADERS, datatype=DATATYPES, interactive=False)
|
| 188 |
+
bm_plot = gr.Plot(label="Quality and size per level")
|
| 189 |
+
bm_button.click(benchmark, inputs=bm_input, outputs=[bm_table, bm_plot])
|
| 190 |
+
|
| 191 |
+
gr.Markdown(FOOTER)
|
| 192 |
+
|
| 193 |
+
demo.launch(mcp_server=True)
|
assets/251-118436-0003.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:340ee98cdd138b743dcf12d82bc2d83b39b72a413bb6f1d8a975d7d1ae8d7217
|
| 3 |
+
size 705360
|
assets/5_1812_20170628135856.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dc7aaa044b83ba0a88b4089d2914440f82490456ce3b294297847b60a8aa7050
|
| 3 |
+
size 151624
|
assets/p226_006.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:53b9be9b754b28c4907469333a1d938bc3d6ae7e513e7b371c999f337720d095
|
| 3 |
+
size 489980
|
core.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Quantization, audio IO, objective metrics and plots for the FocalCodec lab.
|
| 2 |
+
|
| 3 |
+
Kept free of Gradio / torch.hub so it can be exercised standalone.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import matplotlib
|
| 9 |
+
|
| 10 |
+
matplotlib.use("Agg")
|
| 11 |
+
|
| 12 |
+
import matplotlib.pyplot as plt
|
| 13 |
+
import numpy as np
|
| 14 |
+
import soundfile as sf
|
| 15 |
+
import torch
|
| 16 |
+
from pystoi import stoi
|
| 17 |
+
from scipy.signal import resample_poly, stft
|
| 18 |
+
|
| 19 |
+
QUANT_LEVELS = ("int4", "int8", "int16", "int32", "fp4", "fp8", "fp16", "fp32")
|
| 20 |
+
|
| 21 |
+
TOKEN_RATE_HZ = 12.5
|
| 22 |
+
BITS_PER_TOKEN = 13
|
| 23 |
+
BITRATE_KBPS = TOKEN_RATE_HZ * BITS_PER_TOKEN / 1000.0
|
| 24 |
+
|
| 25 |
+
_BITS = {"int4": 4, "int8": 8, "int16": 16, "int32": 32, "fp4": 4, "fp8": 8, "fp16": 16, "fp32": 32}
|
| 26 |
+
_FP_FORMATS = {"fp4": (2, 1), "fp8": (4, 3)} # (exponent bits, mantissa bits) -> E2M1 / E4M3
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _ref_scale(w: torch.Tensor) -> torch.Tensor:
|
| 30 |
+
"""Per-output-channel absmax, per-tensor for 1-D params."""
|
| 31 |
+
if w.dim() > 1:
|
| 32 |
+
return w.abs().amax(dim=tuple(range(1, w.dim())), keepdim=True)
|
| 33 |
+
return w.abs().amax()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _quant_int(w: torch.Tensor, bits: int) -> torch.Tensor:
|
| 37 |
+
if bits >= 32:
|
| 38 |
+
return w
|
| 39 |
+
qmax = 1 << (bits - 1)
|
| 40 |
+
scale = (_ref_scale(w) / qmax).clamp_min(1e-12)
|
| 41 |
+
return (w / scale).round().clamp(-qmax, qmax - 1) * scale
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _quant_fp(w: torch.Tensor, e_bits: int, m_bits: int) -> torch.Tensor:
|
| 45 |
+
bias = (1 << (e_bits - 1)) - 1
|
| 46 |
+
max_exp = (1 << e_bits) - 1 - bias
|
| 47 |
+
max_norm = (2.0 - 2.0 ** -m_bits) * 2.0**max_exp
|
| 48 |
+
scale = (_ref_scale(w) / max_norm).clamp_min(1e-12)
|
| 49 |
+
x = w / scale
|
| 50 |
+
ax = x.abs()
|
| 51 |
+
exp = torch.floor(torch.log2(ax.clamp_min(1e-30))).clamp(1 - bias, max_exp)
|
| 52 |
+
step = torch.pow(2.0, exp - m_bits)
|
| 53 |
+
return torch.sign(x) * ((ax / step).round() * step) * scale
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def quantize_state(state: dict, level: str) -> dict:
|
| 57 |
+
"""Fake-quantize every float tensor in `state` to `level` (per-channel absmax scaling)."""
|
| 58 |
+
if level == "fp32":
|
| 59 |
+
return dict(state)
|
| 60 |
+
out = {}
|
| 61 |
+
for key, value in state.items():
|
| 62 |
+
if not value.is_floating_point():
|
| 63 |
+
out[key] = value
|
| 64 |
+
elif level == "fp16":
|
| 65 |
+
out[key] = value.half().float()
|
| 66 |
+
elif level.startswith("int"):
|
| 67 |
+
out[key] = _quant_int(value, int(level[3:]))
|
| 68 |
+
else:
|
| 69 |
+
out[key] = _quant_fp(value, *_FP_FORMATS[level])
|
| 70 |
+
return out
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def model_size_mb(state: dict, level: str) -> float:
|
| 74 |
+
return sum(v.numel() for v in state.values()) * _BITS[level] / 8 / 1e6
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def load_audio(path: str, target_sr: int, max_seconds: float | None = None) -> tuple[np.ndarray, int]:
|
| 78 |
+
"""Read `path` as mono float32, resample to `target_sr`, optionally truncate."""
|
| 79 |
+
x, sr = sf.read(path, dtype="float32", always_2d=True)
|
| 80 |
+
x = x.mean(axis=1)
|
| 81 |
+
if sr != target_sr:
|
| 82 |
+
g = np.gcd(sr, target_sr)
|
| 83 |
+
x = resample_poly(x, target_sr // g, sr // g).astype(np.float32)
|
| 84 |
+
sr = target_sr
|
| 85 |
+
if max_seconds is not None and len(x) > int(max_seconds * sr):
|
| 86 |
+
x = x[: int(max_seconds * sr)]
|
| 87 |
+
return x, sr
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def si_sdr(ref: np.ndarray, rec: np.ndarray) -> float:
|
| 91 |
+
ref = ref - ref.mean()
|
| 92 |
+
rec = rec - rec.mean()
|
| 93 |
+
denom = float(ref @ ref) + 1e-12
|
| 94 |
+
target = (float(rec @ ref) / denom) * ref
|
| 95 |
+
noise = rec - target
|
| 96 |
+
return float(10 * np.log10((float(target @ target) + 1e-12) / (float(noise @ noise) + 1e-12)))
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def log_spectral_distance(ref: np.ndarray, rec: np.ndarray, sr: int) -> float:
|
| 100 |
+
"""Frame-averaged log-spectral distance (dB) between reference and reconstruction."""
|
| 101 |
+
_, _, R = stft(ref, fs=sr, nperseg=1024, noverlap=768)
|
| 102 |
+
_, _, S = stft(rec, fs=sr, nperseg=1024, noverlap=768)
|
| 103 |
+
diff = 20 * np.log10(np.abs(R) + 1e-8) - 20 * np.log10(np.abs(S) + 1e-8)
|
| 104 |
+
return float(np.mean(np.sqrt(np.mean(diff**2, axis=0))))
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def evaluate(ref: np.ndarray, rec: np.ndarray, sr: int, rtf: float, size_mb: float, level: str) -> dict:
|
| 108 |
+
n = min(len(ref), len(rec))
|
| 109 |
+
ref, rec = ref[:n], rec[:n]
|
| 110 |
+
return {
|
| 111 |
+
"Level": level,
|
| 112 |
+
"SI-SDR (dB)": round(si_sdr(ref, rec), 2),
|
| 113 |
+
"STOI": round(float(stoi(ref, rec, sr, extended=False)), 3),
|
| 114 |
+
"LSD (dB)": round(log_spectral_distance(ref, rec, sr), 2),
|
| 115 |
+
"Model size (MB)": round(size_mb, 1),
|
| 116 |
+
"Bitrate (kbps)": round(BITRATE_KBPS, 4),
|
| 117 |
+
"RTF": round(rtf, 3),
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def spectrogram_figure(ref: np.ndarray, rec: np.ndarray, sr: int, title: str):
|
| 122 |
+
fig, axes = plt.subplots(1, 2, figsize=(9.0, 2.6), sharey=True)
|
| 123 |
+
for ax, sig, label in zip(axes, (ref, rec), ("Input", "Reconstruction")):
|
| 124 |
+
ax.specgram(sig, NFFT=1024, noverlap=768, Fs=sr, cmap="magma")
|
| 125 |
+
ax.set_title(label, fontsize=9)
|
| 126 |
+
ax.set_xlabel("Time (s)")
|
| 127 |
+
axes[0].set_ylabel("Frequency (Hz)")
|
| 128 |
+
fig.suptitle(title, fontsize=10)
|
| 129 |
+
fig.tight_layout()
|
| 130 |
+
return fig
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def metrics_figure(rows: list[dict]):
|
| 134 |
+
levels = [r["Level"] for r in rows]
|
| 135 |
+
panels = (("SI-SDR (dB)", "#4c72b0"), ("STOI", "#55a868"), ("Model size (MB)", "#c44e52"))
|
| 136 |
+
fig, axes = plt.subplots(1, len(panels), figsize=(11.5, 2.9))
|
| 137 |
+
for ax, (key, color) in zip(axes, panels):
|
| 138 |
+
ax.bar(levels, [r[key] for r in rows], color=color)
|
| 139 |
+
ax.set_title(key, fontsize=9)
|
| 140 |
+
ax.tick_params(axis="x", rotation=45)
|
| 141 |
+
ax.grid(axis="y", alpha=0.3)
|
| 142 |
+
fig.tight_layout()
|
| 143 |
+
return fig
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
if __name__ == "__main__":
|
| 147 |
+
torch.manual_seed(0)
|
| 148 |
+
w = (torch.randn(64, 32) * 0.05).requires_grad_(False)
|
| 149 |
+
for level in QUANT_LEVELS:
|
| 150 |
+
q = quantize_state({"w": w}, level)["w"]
|
| 151 |
+
assert torch.isfinite(q).all(), f"{level} not finite"
|
| 152 |
+
assert q.abs().max() <= w.abs().max() * 1.01 + 1e-9, f"{level} broke the magnitude bound"
|
| 153 |
+
x = np.random.randn(16000).astype(np.float32)
|
| 154 |
+
assert si_sdr(x, x) > 60, "SI-SDR of identical signals should be large"
|
| 155 |
+
assert si_sdr(x, 3 * x) > 60, "SI-SDR must be scale invariant"
|
| 156 |
+
assert abs(evaluate(x, x, 16000, 1.0, 1.0, "fp32")["STOI"] - 1.0) < 1e-3
|
| 157 |
+
assert evaluate(x, x, 16000, 1.0, 1.0, "fp32")["Bitrate (kbps)"] == round(BITRATE_KBPS, 4)
|
| 158 |
+
print("core self-check ok")
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
matplotlib
|
| 2 |
+
pystoi
|
| 3 |
+
safetensors
|
| 4 |
+
scipy
|
| 5 |
+
soundfile
|