File size: 11,384 Bytes
b4fb9f0 6f3c0ac b4fb9f0 6f3c0ac b4fb9f0 6f3c0ac b4fb9f0 6f3c0ac | 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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 | """
BlueCodec β speech autoencoder demo
Upload audio, encode it into BlueCodec's 24-dimensional ~86 Hz latent
space, decode it back to a waveform, and inspect what happened in
between: the reconstructed audio, the waveform comparison, the latent
representation itself, and the numbers behind the compression.
"""
# `spaces` must be imported before torch (or anything that touches CUDA) so
# its ZeroGPU shims are installed first. This Space runs on ZeroGPU hardware,
# which requires at least one @spaces.GPU-decorated function.
import spaces
import os
import time
import tempfile
import urllib.request
import numpy as np
import torch
import torchaudio
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
import gradio as gr
from bluecodec import BlueCodec
# --------------------------------------------------------------------------
# Config
# --------------------------------------------------------------------------
MODEL_ID = "notmax123/blue-codec"
TARGET_SR = 44100
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
EXAMPLE_DIR = "examples_cache"
EXAMPLE_URL = (
"https://github.com/thewh1teagle/phonikud-chatterbox/releases/"
"download/asset-files-v1/female1.wav"
)
EXAMPLE_PATH = os.path.join(EXAMPLE_DIR, "female1.wav")
NAVY = "#0B0F1A"
SURFACE = "#111827"
BLUE = "#3B9EFF"
VIOLET = "#8B7CFF"
TEXT = "#E6EBF5"
MUTED = "#7C89A3"
LINE = "#22304A"
# --------------------------------------------------------------------------
# Model loading (once, at import time)
# --------------------------------------------------------------------------
_codec = None
_load_error = None
try:
_codec = BlueCodec.from_pretrained(MODEL_ID, device=DEVICE)
except Exception as exc: # noqa: BLE001 - surface any load failure in the UI
_load_error = str(exc)
def get_codec():
return _codec, _load_error
def ensure_example():
"""Best-effort download of a sample clip so the Space ships with an example."""
try:
os.makedirs(EXAMPLE_DIR, exist_ok=True)
if not os.path.exists(EXAMPLE_PATH):
urllib.request.urlretrieve(EXAMPLE_URL, EXAMPLE_PATH)
return EXAMPLE_PATH
except Exception: # noqa: BLE001 - examples are optional, never block startup
return None
# --------------------------------------------------------------------------
# Plotting helpers
# --------------------------------------------------------------------------
def _style_ax(ax):
ax.set_facecolor(SURFACE)
for spine in ax.spines.values():
spine.set_color(LINE)
spine.set_linewidth(0.8)
ax.tick_params(colors=MUTED, labelsize=8)
ax.xaxis.label.set_color(MUTED)
ax.yaxis.label.set_color(MUTED)
ax.grid(color=LINE, linewidth=0.5, alpha=0.6)
ax.set_axisbelow(True)
def make_waveform_figure(original: np.ndarray, reconstructed: np.ndarray, sr: int):
t_o = np.arange(len(original)) / sr
t_r = np.arange(len(reconstructed)) / sr
fig, axes = plt.subplots(2, 1, figsize=(9, 4), sharex=True, facecolor=NAVY)
axes[0].plot(t_o, original, color=BLUE, linewidth=0.6)
axes[0].set_ylabel("Original")
axes[1].plot(t_r, reconstructed, color=VIOLET, linewidth=0.6)
axes[1].set_ylabel("Reconstructed")
axes[1].set_xlabel("Time (s)")
for ax in axes:
_style_ax(ax)
ax.set_ylim(-1.05, 1.05)
fig.tight_layout()
plt.close(fig)
return fig
def latent_to_matrix(latents: torch.Tensor) -> np.ndarray:
"""Return latents as a (24, T) numpy array regardless of axis order."""
arr = latents.detach().float().cpu().numpy()
arr = np.squeeze(arr)
if arr.ndim != 2:
raise ValueError(f"Unexpected latent shape: {tuple(latents.shape)}")
if arr.shape[0] > arr.shape[1]:
arr = arr.T
return arr
def make_latent_figure(latent_matrix: np.ndarray):
cmap = LinearSegmentedColormap.from_list(
"bluecodec", [NAVY, BLUE, VIOLET, "#F2F5FF"]
)
fig, ax = plt.subplots(figsize=(9, 2.8), facecolor=NAVY)
im = ax.imshow(latent_matrix, aspect="auto", cmap=cmap, interpolation="nearest")
ax.set_ylabel("Latent dim")
ax.set_xlabel("Frame (~86 Hz)")
_style_ax(ax)
ax.grid(False)
cbar = fig.colorbar(im, ax=ax, fraction=0.025, pad=0.02)
cbar.ax.tick_params(colors=MUTED, labelsize=7)
cbar.outline.set_edgecolor(LINE)
fig.tight_layout()
plt.close(fig)
return fig
# --------------------------------------------------------------------------
# Core inference
# --------------------------------------------------------------------------
@spaces.GPU(duration=60)
def process(audio_path, progress=gr.Progress(track_tqdm=False)):
if audio_path is None:
raise gr.Error("Upload or record a clip first.")
codec, err = get_codec()
if codec is None:
raise gr.Error(f"BlueCodec failed to load on startup: {err}")
progress(0.05, desc="Loading audio")
wav, sr = torchaudio.load(audio_path, backend="soundfile")
channels_in = wav.shape[0]
if channels_in > 1:
wav = wav.mean(dim=0, keepdim=True)
if sr != TARGET_SR:
wav = torchaudio.functional.resample(wav, sr, TARGET_SR)
wav = wav.to(DEVICE)
duration_s = wav.shape[-1] / TARGET_SR
progress(0.25, desc="Encoding to latents")
t0 = time.perf_counter()
with torch.inference_mode():
latents = codec.encode(wav)
t1 = time.perf_counter()
progress(0.6, desc="Decoding back to audio")
with torch.inference_mode():
recon = codec.decode(latents)
t2 = time.perf_counter()
encode_s, decode_s = t1 - t0, t2 - t1
total_s = t2 - t0
progress(0.9, desc="Rendering plots")
recon_cpu = recon.detach().float().cpu().clamp(-1, 1)
out_path = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
torchaudio.save(out_path, recon_cpu, TARGET_SR)
orig_np = wav.detach().float().cpu().numpy()[0]
recon_np = recon_cpu.numpy()[0]
n_samples = orig_np.shape[-1]
original_bits = n_samples * 16 # reference: 16-bit PCM, mono
latent_bits = latents.numel() * 32 # latents assumed float32
ratio = original_bits / latent_bits if latent_bits else float("nan")
rtf = duration_s / total_s if total_s > 0 else float("nan")
stats_md = f"""
| | |
|---|---|
| Duration | {duration_s:.2f} s |
| Channels | mono ({"downmixed from " + str(channels_in) if channels_in > 1 else "native"}) |
| Device | {DEVICE.upper()} |
| Latent shape | {tuple(latents.shape)} |
| Latent rate | ~86 Hz Γ 24 dims |
| Encode time | {encode_s * 1000:.0f} ms |
| Decode time | {decode_s * 1000:.0f} ms |
| Compression ratio | {ratio:.1f}Γ vs 16-bit PCM |
| Real-time factor | {rtf:.1f}Γ |
"""
wave_fig = make_waveform_figure(orig_np, recon_np, TARGET_SR)
latent_fig = make_latent_figure(latent_to_matrix(latents))
return out_path, stats_md, wave_fig, latent_fig
# --------------------------------------------------------------------------
# UI
# --------------------------------------------------------------------------
CUSTOM_CSS = f"""
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=IBM+Plex+Sans:wght@400;500&display=swap');
.gradio-container {{
background: {NAVY} !important;
color: {TEXT} !important;
font-family: 'IBM Plex Sans', sans-serif !important;
}}
#bc-hero h1 {{
font-family: 'Space Grotesk', sans-serif !important;
font-weight: 700 !important;
font-size: 2.15rem !important;
letter-spacing: -0.01em;
color: {TEXT} !important;
margin-bottom: 0.2rem !important;
}}
#bc-hero p {{
color: {MUTED} !important;
font-size: 1.02rem;
max-width: 62ch;
}}
#bc-section-label h3 {{
color: {MUTED} !important;
font-family: 'Space Grotesk', sans-serif !important;
font-weight: 500 !important;
font-size: 0.95rem !important;
margin: 0.6rem 0 0.3rem 0 !important;
}}
.bc-panel {{
background: {SURFACE} !important;
border: 1px solid {LINE} !important;
border-radius: 6px !important;
box-shadow: none !important;
}}
.bc-stats table {{
color: {TEXT} !important;
font-family: 'Space Grotesk', sans-serif !important;
width: 100%;
border-collapse: collapse;
}}
.bc-stats table, .bc-stats th, .bc-stats td {{
border: none !important;
}}
.bc-stats tr {{
border-bottom: 1px solid {LINE} !important;
}}
.bc-stats td:first-child {{
color: {MUTED} !important;
font-size: 0.82rem;
font-family: 'IBM Plex Sans', sans-serif !important;
}}
.bc-stats td:last-child {{
text-align: right;
font-variant-numeric: tabular-nums;
}}
#bc-cta {{
background: {BLUE} !important;
border: none !important;
color: #051220 !important;
font-weight: 600 !important;
}}
#bc-cta:hover {{
background: {VIOLET} !important;
}}
#bc-footer, #bc-footer p {{
color: {MUTED} !important;
font-size: 0.85rem;
}}
#bc-footer a {{
color: {BLUE} !important;
}}
"""
with gr.Blocks(title="BlueCodec β speech autoencoder") as demo:
gr.Markdown(
"# BlueCodec\n"
"Upload a voice clip and pass it through a 24-dimensional latent "
"bottleneck at roughly 86 frames per second, then back out as audio. "
"Everything in between β the reconstructed sound, the waveforms, and "
"the latent representation itself β is rendered below.",
elem_id="bc-hero",
)
if _load_error:
gr.Markdown(
f"**Model failed to load at startup:** `{_load_error}`\n\n"
"The interface below will report this error again if you try to run it.",
)
with gr.Row():
with gr.Column(scale=1):
audio_in = gr.Audio(
sources=["upload", "microphone"],
type="filepath",
label="Original audio",
elem_classes=["bc-panel"],
)
run_btn = gr.Button("Encode β decode", variant="primary", elem_id="bc-cta")
example_path = ensure_example()
if example_path:
gr.Examples(
examples=[[example_path]],
inputs=[audio_in],
label="Try a sample clip",
)
with gr.Column(scale=1):
audio_out = gr.Audio(label="Reconstructed audio", elem_classes=["bc-panel"])
stats_out = gr.Markdown(elem_classes=["bc-panel", "bc-stats"])
gr.Markdown("### Waveform: original vs. reconstructed", elem_id="bc-section-label")
wave_plot = gr.Plot(elem_classes=["bc-panel"])
gr.Markdown("### Latent representation (24 dims Γ ~86 Hz)", elem_id="bc-section-label")
latent_plot = gr.Plot(elem_classes=["bc-panel"])
gr.Markdown(
"This Space runs the BlueCodec autoencoder. Model weights are hosted at "
"[notmax123/blue-codec](https://huggingface.co/notmax123/blue-codec) on "
"Hugging Face, and the source lives at "
"[maxmelichov/blue-codec](https://github.com/maxmelichov/blue-codec) on GitHub. "
"The compression ratio shown is measured against 16-bit PCM at the same "
"sample rate, and assumes float32 latents.",
elem_id="bc-footer",
)
run_btn.click(
fn=process,
inputs=[audio_in],
outputs=[audio_out, stats_out, wave_plot, latent_plot],
)
if __name__ == "__main__":
demo.queue().launch(theme=gr.themes.Base(), css=CUSTOM_CSS) |