Spaces:
Paused
Paused
Add ZeroGPU Gradio Space: free on-demand GPU, always-on, no manual start
Browse files- huggingface_space/ : Gradio app with @spaces.GPU, same two-section workflow
(create profile / generate), tuning sliders, chunked long-script stitching
- DEPLOY.md: ZeroGPU as recommended always-on free-GPU option; notes why an
automatic Colab backend isn't possible
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- DEPLOY.md +37 -9
- huggingface_space/README.md +26 -0
- huggingface_space/app.py +245 -0
- huggingface_space/requirements.txt +5 -0
DEPLOY.md
CHANGED
|
@@ -1,14 +1,42 @@
|
|
| 1 |
# π Running Voice Clone Studio online
|
| 2 |
|
| 3 |
-
GitHub stores the code; it doesn't run it. Below are
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
| | π€
|
| 7 |
-
|---|---|---|
|
| 8 |
-
| Cost | Free
|
| 9 |
-
| Speed | Slow (~min/clip
|
| 10 |
-
| Stays online? | β
Always-on
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
---
|
| 14 |
|
|
|
|
| 1 |
# π Running Voice Clone Studio online
|
| 2 |
|
| 3 |
+
GitHub stores the code; it doesn't run it. Below are ready-to-use ways to run the
|
| 4 |
+
app online. The files for all of them are already in this repo.
|
| 5 |
+
|
| 6 |
+
| | β HF Space (ZeroGPU) | π€ HF Space (Docker, CPU) | βΆοΈ Google Colab |
|
| 7 |
+
|---|---|---|---|
|
| 8 |
+
| Cost | Free | Free | Free |
|
| 9 |
+
| Speed | **Fast** (on-demand GPU) | Slow (~min/clip) | Fast (GPU) |
|
| 10 |
+
| Stays online? | β
Always-on | β
Always-on | β Only while notebook runs |
|
| 11 |
+
| Manual start each time? | β No | β No | β
Yes |
|
| 12 |
+
| UI | Gradio | Your custom Flask UI | Your custom Flask UI |
|
| 13 |
+
| Files | `huggingface_space/` | `Dockerfile` + `deploy/huggingface/` | `notebooks/` |
|
| 14 |
+
|
| 15 |
+
> **Recommended: ZeroGPU.** Always-on, free GPU, nothing to launch. The only
|
| 16 |
+
> trade-off is it uses a Gradio UI instead of the custom Flask one. This is the
|
| 17 |
+
> closest thing to "free GPU hosting with nothing to start."
|
| 18 |
+
>
|
| 19 |
+
> **Why there's no automatic Colab option:** Google Colab is an interactive
|
| 20 |
+
> notebook β it can't stay running on its own and hands out a new URL each run, so
|
| 21 |
+
> it always requires manually starting it. ZeroGPU replaces that need.
|
| 22 |
+
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
## Option β β Hugging Face Space with ZeroGPU (free GPU, always-on)
|
| 26 |
+
|
| 27 |
+
1. Create a free account at **https://huggingface.co**.
|
| 28 |
+
2. **New βΈ Space** β **SDK: Gradio**, name it, Create.
|
| 29 |
+
3. Upload the **contents of the `huggingface_space/` folder** to the Space root
|
| 30 |
+
(`app.py`, `requirements.txt`, `README.md`). Easiest: clone the Space repo,
|
| 31 |
+
copy those 3 files in, commit & push. (The Space's `README.md` must be the one
|
| 32 |
+
from `huggingface_space/` β it has the Gradio + config header.)
|
| 33 |
+
4. **Settings βΈ Hardware βΈ select `ZeroGPU`** (free; may require a quick eligibility
|
| 34 |
+
opt-in / PRO for higher limits, but the free tier works).
|
| 35 |
+
5. The Space builds and starts. First generation downloads the model (~1 min),
|
| 36 |
+
then runs on a GPU. Share the Space URL β done, nothing to launch.
|
| 37 |
+
|
| 38 |
+
> β οΈ Free-Space storage is ephemeral: saved profiles reset on restart. To keep them,
|
| 39 |
+
> add Persistent Storage in Settings or back them with a HF Dataset.
|
| 40 |
|
| 41 |
---
|
| 42 |
|
huggingface_space/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Voice Clone Studio
|
| 3 |
+
emoji: ποΈ
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 4.44.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# Voice Clone Studio β Hugging Face Space (ZeroGPU)
|
| 14 |
+
|
| 15 |
+
Clone a voice and generate speech with Chatterbox TTS, on a **free on-demand GPU**.
|
| 16 |
+
|
| 17 |
+
## How this Space is set up
|
| 18 |
+
- **SDK:** Gradio
|
| 19 |
+
- **Hardware:** **ZeroGPU** (select it in *Settings βΈ Hardware βΈ ZeroGPU*). The
|
| 20 |
+
`@spaces.GPU` decorator on `generate()` requests a GPU only while generating.
|
| 21 |
+
|
| 22 |
+
## Notes
|
| 23 |
+
- Storage on a free Space is **ephemeral** β saved voice profiles reset when the
|
| 24 |
+
Space restarts/rebuilds. For permanent profiles, attach Persistent Storage
|
| 25 |
+
(Settings) or back them with a Hugging Face Dataset.
|
| 26 |
+
- First generation downloads the model (~1 min). After that, GPU generation is fast.
|
huggingface_space/app.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Voice Clone Studio β Gradio app for Hugging Face Spaces (ZeroGPU).
|
| 3 |
+
|
| 4 |
+
Free on-demand GPU via the `spaces` library: the @GPU-decorated generate()
|
| 5 |
+
function gets a GPU allocated for its duration. Runs on CPU locally as a no-op.
|
| 6 |
+
|
| 7 |
+
Same two-section workflow as the Flask app:
|
| 8 |
+
β Create Voice Profile (upload + name + optional transcript, save/delete)
|
| 9 |
+
β‘ Generate Audio (pick profile, paste script, tune, generate, download)
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import os
|
| 13 |
+
import re
|
| 14 |
+
import glob
|
| 15 |
+
import uuid
|
| 16 |
+
import shutil
|
| 17 |
+
from datetime import datetime
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
import numpy as np
|
| 21 |
+
import soundfile as sf
|
| 22 |
+
import gradio as gr
|
| 23 |
+
|
| 24 |
+
# ZeroGPU decorator β real GPU on HF Spaces, transparent no-op locally.
|
| 25 |
+
try:
|
| 26 |
+
import spaces
|
| 27 |
+
|
| 28 |
+
gpu = spaces.GPU(duration=120)
|
| 29 |
+
except Exception: # not on ZeroGPU / package missing -> run inline
|
| 30 |
+
def gpu(fn):
|
| 31 |
+
return fn
|
| 32 |
+
|
| 33 |
+
# --------------------------------------------------------------------------- #
|
| 34 |
+
PROFILE_DIR = "profiles"
|
| 35 |
+
OUTPUT_DIR = "outputs"
|
| 36 |
+
for _d in (PROFILE_DIR, OUTPUT_DIR):
|
| 37 |
+
os.makedirs(_d, exist_ok=True)
|
| 38 |
+
|
| 39 |
+
ALLOWED = {"wav", "mp3", "flac", "ogg", "m4a"}
|
| 40 |
+
CHUNK_MAX = 280
|
| 41 |
+
GAP_SECONDS = 0.35
|
| 42 |
+
|
| 43 |
+
_model = None
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def get_model():
|
| 47 |
+
global _model
|
| 48 |
+
if _model is None:
|
| 49 |
+
from chatterbox.tts import ChatterboxTTS
|
| 50 |
+
|
| 51 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 52 |
+
_model = ChatterboxTTS.from_pretrained(device=device)
|
| 53 |
+
return _model
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# ---- profile helpers ------------------------------------------------------ #
|
| 57 |
+
def _safe(name: str) -> str:
|
| 58 |
+
return re.sub(r"[^A-Za-z0-9_-]+", "_", (name or "").strip()).strip("_")
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def profile_audio_path(name: str):
|
| 62 |
+
safe = _safe(name)
|
| 63 |
+
if not safe:
|
| 64 |
+
return None
|
| 65 |
+
for ext in ALLOWED:
|
| 66 |
+
p = os.path.join(PROFILE_DIR, f"{safe}.{ext}")
|
| 67 |
+
if os.path.exists(p):
|
| 68 |
+
return p
|
| 69 |
+
return None
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def list_profile_names():
|
| 73 |
+
names, seen = [], set()
|
| 74 |
+
for p in sorted(glob.glob(os.path.join(PROFILE_DIR, "*.*"))):
|
| 75 |
+
base, ext = os.path.splitext(os.path.basename(p))
|
| 76 |
+
if ext.lstrip(".").lower() in ALLOWED and base not in seen:
|
| 77 |
+
seen.add(base)
|
| 78 |
+
names.append(base)
|
| 79 |
+
return names
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def save_profile(name, audio_path, transcript):
|
| 83 |
+
safe = _safe(name)
|
| 84 |
+
if not safe:
|
| 85 |
+
return gr.update(), gr.update(), "β Please enter a profile name."
|
| 86 |
+
if not audio_path:
|
| 87 |
+
return gr.update(), gr.update(), "β Please upload a reference clip."
|
| 88 |
+
ext = os.path.splitext(audio_path)[1].lstrip(".").lower()
|
| 89 |
+
if ext not in ALLOWED:
|
| 90 |
+
ext = "wav"
|
| 91 |
+
for old in glob.glob(os.path.join(PROFILE_DIR, safe + ".*")):
|
| 92 |
+
try:
|
| 93 |
+
os.remove(old)
|
| 94 |
+
except OSError:
|
| 95 |
+
pass
|
| 96 |
+
shutil.copy(audio_path, os.path.join(PROFILE_DIR, f"{safe}.{ext}"))
|
| 97 |
+
if (transcript or "").strip():
|
| 98 |
+
with open(os.path.join(PROFILE_DIR, f"{safe}.txt"), "w", encoding="utf-8") as fh:
|
| 99 |
+
fh.write(transcript.strip())
|
| 100 |
+
names = list_profile_names()
|
| 101 |
+
return (
|
| 102 |
+
gr.update(choices=names, value=safe),
|
| 103 |
+
gr.update(choices=names, value=safe),
|
| 104 |
+
f"β
Saved voice profile β{safe}β.",
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def delete_profile(name):
|
| 109 |
+
safe = _safe(name)
|
| 110 |
+
for f in glob.glob(os.path.join(PROFILE_DIR, safe + ".*")):
|
| 111 |
+
try:
|
| 112 |
+
os.remove(f)
|
| 113 |
+
except OSError:
|
| 114 |
+
pass
|
| 115 |
+
names = list_profile_names()
|
| 116 |
+
val = names[0] if names else None
|
| 117 |
+
return (
|
| 118 |
+
gr.update(choices=names, value=val),
|
| 119 |
+
gr.update(choices=names, value=val),
|
| 120 |
+
(f"π Deleted β{safe}β." if safe else ""),
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
# ---- text chunking -------------------------------------------------------- #
|
| 125 |
+
def split_text(text, max_len=CHUNK_MAX):
|
| 126 |
+
text = " ".join(text.split())
|
| 127 |
+
chunks, cur = [], ""
|
| 128 |
+
for s in re.split(r"(?<=[.!?])\s+", text):
|
| 129 |
+
if not s:
|
| 130 |
+
continue
|
| 131 |
+
if len(s) > max_len:
|
| 132 |
+
pieces = []
|
| 133 |
+
for part in re.split(r"(?<=[,;:])\s+", s):
|
| 134 |
+
while len(part) > max_len:
|
| 135 |
+
pieces.append(part[:max_len])
|
| 136 |
+
part = part[max_len:]
|
| 137 |
+
if part:
|
| 138 |
+
pieces.append(part)
|
| 139 |
+
else:
|
| 140 |
+
pieces = [s]
|
| 141 |
+
for p in pieces:
|
| 142 |
+
if cur and len(cur) + len(p) + 1 > max_len:
|
| 143 |
+
chunks.append(cur)
|
| 144 |
+
cur = p
|
| 145 |
+
else:
|
| 146 |
+
cur = (cur + " " + p).strip()
|
| 147 |
+
if cur:
|
| 148 |
+
chunks.append(cur)
|
| 149 |
+
return chunks or [text]
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def recent_outputs(limit=10):
|
| 153 |
+
files = sorted(
|
| 154 |
+
glob.glob(os.path.join(OUTPUT_DIR, "*.wav")),
|
| 155 |
+
key=os.path.getmtime,
|
| 156 |
+
reverse=True,
|
| 157 |
+
)
|
| 158 |
+
return files[:limit]
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
# ---- generation (runs on GPU via ZeroGPU) --------------------------------- #
|
| 162 |
+
@gpu
|
| 163 |
+
def generate(profile, text, cfg_weight, temperature, exaggeration,
|
| 164 |
+
progress=gr.Progress()):
|
| 165 |
+
if not profile:
|
| 166 |
+
raise gr.Error("Choose a saved voice profile (create one in tab β ).")
|
| 167 |
+
if not (text or "").strip():
|
| 168 |
+
raise gr.Error("Paste or type a script to speak.")
|
| 169 |
+
ref = profile_audio_path(profile)
|
| 170 |
+
if not ref:
|
| 171 |
+
raise gr.Error(f"Voice profile '{profile}' not found.")
|
| 172 |
+
|
| 173 |
+
model = get_model()
|
| 174 |
+
chunks = split_text(text)
|
| 175 |
+
gap = torch.zeros(int(GAP_SECONDS * model.sr))
|
| 176 |
+
pieces = []
|
| 177 |
+
for i, ch in enumerate(progress.tqdm(chunks, desc="Generating")):
|
| 178 |
+
wav = model.generate(
|
| 179 |
+
ch,
|
| 180 |
+
audio_prompt_path=ref,
|
| 181 |
+
exaggeration=exaggeration,
|
| 182 |
+
cfg_weight=cfg_weight,
|
| 183 |
+
temperature=temperature,
|
| 184 |
+
)
|
| 185 |
+
pieces.append(wav.squeeze(0).cpu())
|
| 186 |
+
if i < len(chunks) - 1:
|
| 187 |
+
pieces.append(gap)
|
| 188 |
+
|
| 189 |
+
full = torch.cat(pieces).numpy()
|
| 190 |
+
slug = re.sub(r"[^a-zA-Z0-9]+", "-", text.strip().lower())[:32].strip("-") or "clip"
|
| 191 |
+
out = os.path.join(OUTPUT_DIR, f"{slug}-{datetime.now():%Y%m%d-%H%M%S}-{uuid.uuid4().hex[:6]}.wav")
|
| 192 |
+
sf.write(out, full, model.sr)
|
| 193 |
+
return (model.sr, full), recent_outputs()
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
# --------------------------------------------------------------------------- #
|
| 197 |
+
# UI
|
| 198 |
+
# --------------------------------------------------------------------------- #
|
| 199 |
+
def build_ui():
|
| 200 |
+
names = list_profile_names()
|
| 201 |
+
init = names[0] if names else None
|
| 202 |
+
with gr.Blocks(title="Voice Clone Studio", theme=gr.themes.Soft()) as demo:
|
| 203 |
+
gr.Markdown(
|
| 204 |
+
"# ποΈ Voice Clone Studio\n"
|
| 205 |
+
"Clone a voice and generate speech β powered by Chatterbox TTS on a free GPU."
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
with gr.Tab("β Create Voice Profile"):
|
| 209 |
+
with gr.Row():
|
| 210 |
+
with gr.Column():
|
| 211 |
+
p_name = gr.Textbox(label="Profile name", placeholder="e.g. gayan")
|
| 212 |
+
p_audio = gr.Audio(label="Reference audio (10β60 s of clear speech)", type="filepath")
|
| 213 |
+
p_trans = gr.Textbox(label="Transcript / notes (optional)", lines=3,
|
| 214 |
+
placeholder="Optional β Chatterbox clones from the audio alone.")
|
| 215 |
+
p_save = gr.Button("πΎ Save voice profile", variant="primary")
|
| 216 |
+
p_msg = gr.Markdown()
|
| 217 |
+
with gr.Column():
|
| 218 |
+
gr.Markdown("### Your voice profiles")
|
| 219 |
+
del_dd = gr.Dropdown(label="Saved profiles", choices=names, value=init)
|
| 220 |
+
del_btn = gr.Button("π Delete selected")
|
| 221 |
+
|
| 222 |
+
with gr.Tab("β‘ Generate Audio"):
|
| 223 |
+
gen_dd = gr.Dropdown(label="Choose voice profile", choices=names, value=init)
|
| 224 |
+
g_text = gr.Textbox(label="Script", lines=8,
|
| 225 |
+
placeholder="Paste your script. Long scripts are split and stitched into one trackβ¦")
|
| 226 |
+
with gr.Accordion("βοΈ Voice tuning (lower = cleaner / less artifacts)", open=False):
|
| 227 |
+
cfg = gr.Slider(0, 1, value=0.3, step=0.05, label="CFG weight")
|
| 228 |
+
temp = gr.Slider(0.05, 1.2, value=0.7, step=0.05, label="Temperature")
|
| 229 |
+
exag = gr.Slider(0.25, 1, value=0.5, step=0.05, label="Exaggeration")
|
| 230 |
+
g_btn = gr.Button("Generate Audio", variant="primary")
|
| 231 |
+
g_audio = gr.Audio(label="Output", type="numpy", autoplay=True)
|
| 232 |
+
g_hist = gr.Files(label="Recent generations (click to download)", value=recent_outputs())
|
| 233 |
+
|
| 234 |
+
# refresh dropdown choices when this tab gains focus is automatic via events below
|
| 235 |
+
p_save.click(save_profile, [p_name, p_audio, p_trans], [gen_dd, del_dd, p_msg])
|
| 236 |
+
del_btn.click(delete_profile, [del_dd], [gen_dd, del_dd, p_msg])
|
| 237 |
+
g_btn.click(generate, [gen_dd, g_text, cfg, temp, exag], [g_audio, g_hist])
|
| 238 |
+
|
| 239 |
+
return demo
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
demo = build_ui()
|
| 243 |
+
|
| 244 |
+
if __name__ == "__main__":
|
| 245 |
+
demo.queue().launch()
|
huggingface_space/requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.44.0
|
| 2 |
+
spaces
|
| 3 |
+
chatterbox-tts>=0.1.7
|
| 4 |
+
numpy<2
|
| 5 |
+
soundfile
|