File size: 6,960 Bytes
be6318c | 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 | """
Khmer/Multilingual TTS Dataset Generator — built on openbmb/VoxCPM2.
Upload a JSON file of [{"id": ..., "text": ...}, ...] (up to 100 items),
batch-generate speech for each entry, and download a zip containing:
- audio/<id>.wav for every item
- metadata.csv and metadata.json (id, text, filename, status)
Designed for preparing paired text/speech datasets for ASR & TTS training.
"""
import csv
import json
import os
import tempfile
import traceback
import zipfile
import gradio as gr
import soundfile as sf
MODEL = None
MAX_ITEMS = 100
def load_model():
"""Lazy-load VoxCPM2 once and cache it across requests."""
global MODEL
if MODEL is None:
from voxcpm import VoxCPM
MODEL = VoxCPM.from_pretrained("openbmb/VoxCPM2", load_denoiser=False)
return MODEL
def parse_json_file(path):
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, list):
raise ValueError("The JSON file must contain a list of objects, e.g. "
'[{"id": 1, "text": "..."}, ...]')
items = []
seen_ids = set()
for i, entry in enumerate(data):
if not isinstance(entry, dict) or "id" not in entry or "text" not in entry:
raise ValueError(f"Item #{i} is missing 'id' or 'text': {entry}")
item_id = entry["id"]
text = str(entry["text"]).strip()
if not text:
raise ValueError(f"Item id={item_id} has empty text")
if item_id in seen_ids:
raise ValueError(f"Duplicate id found: {item_id}")
seen_ids.add(item_id)
items.append({"id": item_id, "text": text})
return items
def build_zip(work_dir, audio_dir, metadata):
csv_path = os.path.join(work_dir, "metadata.csv")
with open(csv_path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["id", "text", "filename", "status"])
writer.writeheader()
writer.writerows(metadata)
json_path = os.path.join(work_dir, "metadata.json")
with open(json_path, "w", encoding="utf-8") as f:
json.dump(metadata, f, ensure_ascii=False, indent=2)
zip_path = os.path.join(work_dir, "tts_dataset.zip")
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
for fname in sorted(os.listdir(audio_dir)):
zf.write(os.path.join(audio_dir, fname), arcname=f"audio/{fname}")
zf.write(csv_path, arcname="metadata.csv")
zf.write(json_path, arcname="metadata.json")
return zip_path
def generate_batch(json_file, voice_description, cfg_value, inference_timesteps,
limit, progress=gr.Progress()):
if json_file is None:
raise gr.Error("Upload a JSON file first.")
try:
items = parse_json_file(json_file)
except Exception as e:
raise gr.Error(f"Could not read JSON: {e}")
if len(items) == 0:
raise gr.Error("The JSON file is empty.")
if len(items) > MAX_ITEMS:
raise gr.Error(
f"Found {len(items)} items — please split into batches of "
f"{MAX_ITEMS} or fewer."
)
if limit and int(limit) > 0:
items = items[: int(limit)]
progress(0, desc="Loading VoxCPM2 (first run only, can take a while)...")
model = load_model()
sample_rate = getattr(model.tts_model, "sample_rate", 48000)
work_dir = tempfile.mkdtemp(prefix="tts_batch_")
audio_dir = os.path.join(work_dir, "audio")
os.makedirs(audio_dir, exist_ok=True)
metadata = []
rows = []
total = len(items)
for i, item in enumerate(items):
item_id = item["id"]
text = item["text"]
progress(i / total, desc=f"id={item_id} ({i + 1}/{total})")
prompt_text = f"({voice_description.strip()}){text}" if voice_description.strip() else text
status = "ok"
filename = ""
try:
wav = model.generate(
text=prompt_text,
cfg_value=float(cfg_value),
inference_timesteps=int(inference_timesteps),
)
filename = f"{item_id}.wav"
sf.write(os.path.join(audio_dir, filename), wav, sample_rate)
except Exception as e:
status = f"error: {e}"
traceback.print_exc()
metadata.append({"id": item_id, "text": text, "filename": filename, "status": status})
rows.append([item_id, text, filename, status])
yield rows, None, f"Processed {i + 1}/{total}"
zip_path = build_zip(work_dir, audio_dir, metadata)
ok_count = sum(1 for m in metadata if m["status"] == "ok")
yield rows, zip_path, f"Done — {ok_count}/{total} generated successfully."
with gr.Blocks(title="TTS Dataset Generator (VoxCPM2)") as demo:
gr.Markdown(
"""
# 🗣️ Text → Speech Dataset Generator
Built on [`openbmb/VoxCPM2`](https://huggingface.co/openbmb/VoxCPM2) (2B params, 30 languages
including Khmer). Upload a JSON file shaped like `[{"id": 1, "text": "..."}, ...]`
(up to 100 items) and generate a downloadable dataset: one `.wav` per item plus
`metadata.csv` / `metadata.json` mapping id → text → filename — ready for ASR/TTS training.
⚠️ **This Space runs on free CPU hardware**, so generation is slow — expect roughly
tens of seconds per sentence. Test with a small **limit** first before running a full
batch of 100, and keep this tab open while it runs.
"""
)
with gr.Row():
with gr.Column():
json_input = gr.File(label="Upload JSON file", file_types=[".json"], type="filepath")
voice_description = gr.Textbox(
label="Voice description (optional, applied to every item)",
placeholder="e.g. a calm young woman, clear and steady voice",
)
with gr.Row():
cfg_value = gr.Slider(0.5, 4.0, value=2.0, step=0.1, label="CFG value")
inference_timesteps = gr.Slider(4, 30, value=10, step=1, label="Inference timesteps")
limit = gr.Number(
label="Limit (0 = process all items — use a small number to test first)",
value=5, precision=0,
)
generate_btn = gr.Button("Generate batch", variant="primary")
with gr.Column():
status_box = gr.Textbox(label="Status", interactive=False)
results_table = gr.Dataframe(
headers=["id", "text", "filename", "status"],
label="Results",
wrap=True,
)
download_file = gr.File(label="Download dataset (.zip: audio/ + metadata.csv + metadata.json)")
generate_btn.click(
generate_batch,
inputs=[json_input, voice_description, cfg_value, inference_timesteps, limit],
outputs=[results_table, download_file, status_box],
)
demo.queue(max_size=10).launch()
|