Spaces:
Runtime error
Runtime error
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# %cd /content/omnivoice-colab
|
| 2 |
+
import subprocess
|
| 3 |
+
# Clone the repository
|
| 4 |
+
subprocess.run(["git", "clone", "https://github.com/k2-fsa/OmniVoice.git"])
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
import logging
|
| 12 |
+
import tempfile
|
| 13 |
+
from typing import Any, Dict
|
| 14 |
+
|
| 15 |
+
import gradio as gr
|
| 16 |
+
import numpy as np
|
| 17 |
+
import torch
|
| 18 |
+
import scipy.io.wavfile as wavfile
|
| 19 |
+
import re
|
| 20 |
+
import os
|
| 21 |
+
import uuid
|
| 22 |
+
temp_audio_dir="./Omni_Audio"
|
| 23 |
+
os.makedirs(temp_audio_dir, exist_ok=True)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# ---------------------------------------------------------------------------
|
| 27 |
+
# Setup path to import subtitle_maker from /content/omnivoice-colab/OmniVoice/
|
| 28 |
+
OmniVoice_path = f"{os.getcwd()}/OmniVoice/"
|
| 29 |
+
sys.path.append(OmniVoice_path)
|
| 30 |
+
from subtitle import subtitle_maker
|
| 31 |
+
|
| 32 |
+
# Attempt to import Whisper's supported language dict to filter unsupported languages
|
| 33 |
+
try:
|
| 34 |
+
from subtitle import LANGUAGE_CODE as WHISPER_LANGUAGE_CODE
|
| 35 |
+
except ImportError:
|
| 36 |
+
WHISPER_LANGUAGE_CODE = None
|
| 37 |
+
|
| 38 |
+
from omnivoice import OmniVoice, OmniVoiceGenerationConfig
|
| 39 |
+
from omnivoice.utils.lang_map import LANG_NAMES, lang_display_name
|
| 40 |
+
|
| 41 |
+
# ---------------------------------------------------------------------------
|
| 42 |
+
# Logging Setup
|
| 43 |
+
# ---------------------------------------------------------------------------
|
| 44 |
+
logging.basicConfig(
|
| 45 |
+
level=logging.WARNING,
|
| 46 |
+
format="%(asctime)s %(name)s %(levelname)s: %(message)s",
|
| 47 |
+
)
|
| 48 |
+
logging.getLogger("omnivoice").setLevel(logging.DEBUG)
|
| 49 |
+
|
| 50 |
+
# ---------------------------------------------------------------------------
|
| 51 |
+
# Model Loading (Global Scope)
|
| 52 |
+
# ---------------------------------------------------------------------------
|
| 53 |
+
print("Loading model from k2-fsa/OmniVoice to cuda ...")
|
| 54 |
+
model = OmniVoice.from_pretrained(
|
| 55 |
+
"k2-fsa/OmniVoice",
|
| 56 |
+
device_map="cuda",
|
| 57 |
+
dtype=torch.float16,
|
| 58 |
+
load_asr=False,
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# from hf_mirror import download_model
|
| 62 |
+
# try:
|
| 63 |
+
# model = OmniVoice.from_pretrained(
|
| 64 |
+
# "k2-fsa/OmniVoice",
|
| 65 |
+
# device_map="cuda",
|
| 66 |
+
# dtype=torch.float16,
|
| 67 |
+
# load_asr=False,
|
| 68 |
+
# )
|
| 69 |
+
# except Exception as e:
|
| 70 |
+
# omnivoice_model_path=download_model(
|
| 71 |
+
# "k2-fsa/OmniVoice",
|
| 72 |
+
# download_folder="./OmniVoice_Model",
|
| 73 |
+
# redownload=False,
|
| 74 |
+
# workers=6,
|
| 75 |
+
# use_snapshot=False,
|
| 76 |
+
# )
|
| 77 |
+
|
| 78 |
+
# model = OmniVoice.from_pretrained(
|
| 79 |
+
# omnivoice_model_path,
|
| 80 |
+
# device_map="cuda",
|
| 81 |
+
# dtype=torch.float16,
|
| 82 |
+
# load_asr=False,
|
| 83 |
+
# )
|
| 84 |
+
|
| 85 |
+
sampling_rate = model.sampling_rate
|
| 86 |
+
print("Model loaded successfully!")
|
| 87 |
+
|
| 88 |
+
# ---------------------------------------------------------------------------
|
| 89 |
+
# Event Tags & JS Functions
|
| 90 |
+
# ---------------------------------------------------------------------------
|
| 91 |
+
EVENT_TAGS = [
|
| 92 |
+
"[laughter]", "[sigh]", "[confirmation-en]", "[question-en]",
|
| 93 |
+
"[question-ah]", "[question-oh]", "[question-ei]", "[question-yi]",
|
| 94 |
+
"[surprise-ah]", "[surprise-oh]", "[surprise-wa]", "[surprise-yo]",
|
| 95 |
+
"[dissatisfaction-hnn]"
|
| 96 |
+
]
|
| 97 |
+
|
| 98 |
+
# JS for Voice Clone Tab Textbox
|
| 99 |
+
INSERT_TAG_JS_VC = """
|
| 100 |
+
(tag_val, current_text) => {
|
| 101 |
+
const textarea = document.querySelector('#vc_textbox textarea');
|
| 102 |
+
if (!textarea) return current_text + " " + tag_val;
|
| 103 |
+
const start = textarea.selectionStart;
|
| 104 |
+
const end = textarea.selectionEnd;
|
| 105 |
+
let prefix = " ";
|
| 106 |
+
let suffix = " ";
|
| 107 |
+
if (!current_text) return tag_val;
|
| 108 |
+
if (start === 0) prefix = "";
|
| 109 |
+
else if (current_text[start - 1] === ' ') prefix = "";
|
| 110 |
+
if (end < current_text.length && current_text[end] === ' ') suffix = "";
|
| 111 |
+
return current_text.slice(0, start) + prefix + tag_val + suffix + current_text.slice(end);
|
| 112 |
+
}
|
| 113 |
+
"""
|
| 114 |
+
|
| 115 |
+
# JS for Voice Design Tab Textbox
|
| 116 |
+
INSERT_TAG_JS_VD = """
|
| 117 |
+
(tag_val, current_text) => {
|
| 118 |
+
const textarea = document.querySelector('#vd_textbox textarea');
|
| 119 |
+
if (!textarea) return current_text + " " + tag_val;
|
| 120 |
+
const start = textarea.selectionStart;
|
| 121 |
+
const end = textarea.selectionEnd;
|
| 122 |
+
let prefix = " ";
|
| 123 |
+
let suffix = " ";
|
| 124 |
+
if (!current_text) return tag_val;
|
| 125 |
+
if (start === 0) prefix = "";
|
| 126 |
+
else if (current_text[start - 1] === ' ') prefix = "";
|
| 127 |
+
if (end < current_text.length && current_text[end] === ' ') suffix = "";
|
| 128 |
+
return current_text.slice(0, start) + prefix + tag_val + suffix + current_text.slice(end);
|
| 129 |
+
}
|
| 130 |
+
"""
|
| 131 |
+
|
| 132 |
+
# ---------------------------------------------------------------------------
|
| 133 |
+
# UI Configurations & Language Mappings
|
| 134 |
+
# ---------------------------------------------------------------------------
|
| 135 |
+
_ALL_LANGUAGES = ["Auto"] + sorted(lang_display_name(n) for n in LANG_NAMES)
|
| 136 |
+
|
| 137 |
+
_CATEGORIES = {
|
| 138 |
+
"Gender": ["Male", "Female"],
|
| 139 |
+
"Age": ["Child", "Teenager", "Young Adult", "Middle-aged", "Elderly"],
|
| 140 |
+
"Pitch": ["Very Low Pitch", "Low Pitch", "Moderate Pitch", "High Pitch", "Very High Pitch"],
|
| 141 |
+
"Style": ["Whisper"],
|
| 142 |
+
"English Accent": [
|
| 143 |
+
"American Accent", "Australian Accent", "British Accent", "Chinese Accent",
|
| 144 |
+
"Canadian Accent", "Indian Accent", "Korean Accent", "Portuguese Accent",
|
| 145 |
+
"Russian Accent", "Japanese Accent"
|
| 146 |
+
],
|
| 147 |
+
"Chinese Dialect": [
|
| 148 |
+
"Henan Dialect", "Shaanxi Dialect", "Sichuan Dialect", "Guizhou Dialect",
|
| 149 |
+
"Yunnan Dialect", "Guilin Dialect", "Jinan Dialect", "Shijiazhuang Dialect",
|
| 150 |
+
"Gansu Dialect", "Ningxia Dialect", "Qingdao Dialect", "Northeast Dialect"
|
| 151 |
+
],
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
DIALECT_MAP = {
|
| 155 |
+
"Henan Dialect": "河南话", "Shaanxi Dialect": "陕西话", "Sichuan Dialect": "四川话",
|
| 156 |
+
"Guizhou Dialect": "贵州话", "Yunnan Dialect": "云南话", "Guilin Dialect": "桂林话",
|
| 157 |
+
"Jinan Dialect": "济南话", "Shijiazhuang Dialect": "石家庄话", "Gansu Dialect": "甘肃话",
|
| 158 |
+
"Ningxia Dialect": "宁夏话", "Qingdao Dialect": "青岛话", "Northeast Dialect": "东北话",
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
_ATTR_INFO = {
|
| 162 |
+
"English Accent": "Only effective for English speech.",
|
| 163 |
+
"Chinese Dialect": "Only effective for Chinese speech.",
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
# ---------------------------------------------------------------------------
|
| 167 |
+
# Core Logic & Helpers
|
| 168 |
+
# ---------------------------------------------------------------------------
|
| 169 |
+
def _is_whisper_supported(lang):
|
| 170 |
+
"""Check if the selected language is supported by Whisper to save processing time."""
|
| 171 |
+
if not lang or lang == "Auto":
|
| 172 |
+
return True
|
| 173 |
+
|
| 174 |
+
if WHISPER_LANGUAGE_CODE is None:
|
| 175 |
+
return True
|
| 176 |
+
|
| 177 |
+
supported_langs = [str(k).lower() for k in WHISPER_LANGUAGE_CODE.keys()] + \
|
| 178 |
+
[str(v).lower() for v in WHISPER_LANGUAGE_CODE.values()]
|
| 179 |
+
|
| 180 |
+
lang_lower = lang.lower()
|
| 181 |
+
for w_lang in supported_langs:
|
| 182 |
+
if w_lang in lang_lower or lang_lower in w_lang:
|
| 183 |
+
return True
|
| 184 |
+
|
| 185 |
+
return False
|
| 186 |
+
|
| 187 |
+
def generate_subtitles_if_needed(wav_path, lang, want_subs):
|
| 188 |
+
"""Generates Subtitles only if user requested them and language is supported."""
|
| 189 |
+
if not want_subs:
|
| 190 |
+
return None, None, None
|
| 191 |
+
|
| 192 |
+
if not _is_whisper_supported(lang):
|
| 193 |
+
logging.warning(f"Language '{lang}' is likely unsupported by Whisper. Skipping subtitle generation.")
|
| 194 |
+
return None, None, None
|
| 195 |
+
|
| 196 |
+
try:
|
| 197 |
+
whisper_lang = lang if (lang and lang != "Auto") else None
|
| 198 |
+
whisper_results = subtitle_maker(wav_path, whisper_lang)
|
| 199 |
+
if whisper_results and len(whisper_results) > 3:
|
| 200 |
+
return whisper_results[1], whisper_results[2], whisper_results[3]
|
| 201 |
+
except Exception as e:
|
| 202 |
+
logging.warning(f"Subtitle generation failed: {e}")
|
| 203 |
+
|
| 204 |
+
return None, None, None
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def tts_file_name(text, language="en"):
|
| 208 |
+
global temp_audio_dir
|
| 209 |
+
|
| 210 |
+
# --- Clean text ---
|
| 211 |
+
clean_text = re.sub(r'[^a-zA-Z\s]', '', text) # keep only letters + spaces
|
| 212 |
+
clean_text = clean_text.lower().strip().replace(" ", "_")
|
| 213 |
+
|
| 214 |
+
if not clean_text:
|
| 215 |
+
clean_text = "audio"
|
| 216 |
+
|
| 217 |
+
# --- Truncate ---
|
| 218 |
+
truncated = clean_text[:20]
|
| 219 |
+
|
| 220 |
+
# --- Clean language ---
|
| 221 |
+
lang = re.sub(r'\s+', '_', language.strip().lower()) if language else "unknown"
|
| 222 |
+
|
| 223 |
+
# --- Random suffix ---
|
| 224 |
+
rand = uuid.uuid4().hex[:8].upper()
|
| 225 |
+
|
| 226 |
+
# --- Final filename ---
|
| 227 |
+
return f"{temp_audio_dir}/{truncated}_{lang}_{rand}.wav"
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def _gen_core(
|
| 231 |
+
text, language, ref_audio, instruct, num_step, guidance_scale,
|
| 232 |
+
denoise, speed, duration, preprocess_prompt, postprocess_output, mode, ref_text=None
|
| 233 |
+
):
|
| 234 |
+
"""Core Text-to-Speech Generation Logic"""
|
| 235 |
+
if not text or not text.strip():
|
| 236 |
+
return None, "Please enter the text to synthesize."
|
| 237 |
+
|
| 238 |
+
if mode == "clone" and ref_audio and not ref_text:
|
| 239 |
+
try:
|
| 240 |
+
whisper_lang = language if (language and language != "Auto") else None
|
| 241 |
+
whisper_results = subtitle_maker(ref_audio, whisper_lang)
|
| 242 |
+
if whisper_results and len(whisper_results) > 7:
|
| 243 |
+
ref_text = whisper_results[7]
|
| 244 |
+
except Exception as e:
|
| 245 |
+
logging.warning(f"Fallback transcription failed: {e}")
|
| 246 |
+
|
| 247 |
+
gen_config = OmniVoiceGenerationConfig(
|
| 248 |
+
num_step=int(num_step or 32),
|
| 249 |
+
guidance_scale=float(guidance_scale) if guidance_scale is not None else 2.0,
|
| 250 |
+
denoise=bool(denoise) if denoise is not None else True,
|
| 251 |
+
preprocess_prompt=bool(preprocess_prompt),
|
| 252 |
+
postprocess_output=bool(postprocess_output),
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
lang = language if (language and language != "Auto") else None
|
| 256 |
+
kw: Dict[str, Any] = dict(text=text.strip(), language=lang, generation_config=gen_config)
|
| 257 |
+
|
| 258 |
+
if speed is not None and float(speed) != 1.0:
|
| 259 |
+
kw["speed"] = float(speed)
|
| 260 |
+
if duration is not None and float(duration) > 0:
|
| 261 |
+
kw["duration"] = float(duration)
|
| 262 |
+
|
| 263 |
+
if mode == "clone":
|
| 264 |
+
if not ref_audio:
|
| 265 |
+
return None, "Please upload a reference audio."
|
| 266 |
+
kw["voice_clone_prompt"] = model.create_voice_clone_prompt(ref_audio=ref_audio, ref_text=ref_text)
|
| 267 |
+
if mode == "design":
|
| 268 |
+
if instruct and instruct.strip():
|
| 269 |
+
kw["instruct"] = instruct.strip()
|
| 270 |
+
|
| 271 |
+
try:
|
| 272 |
+
audio = model.generate(**kw)
|
| 273 |
+
except Exception as e:
|
| 274 |
+
return None, f"Error: {type(e).__name__}: {e}"
|
| 275 |
+
|
| 276 |
+
waveform = audio[0].squeeze(0).numpy()
|
| 277 |
+
waveform = (waveform * 32767).astype(np.int16)
|
| 278 |
+
return (sampling_rate, waveform), "Done."
|
| 279 |
+
|
| 280 |
+
# ---------------------------------------------------------------------------
|
| 281 |
+
# Gradio UI Construction
|
| 282 |
+
# ---------------------------------------------------------------------------
|
| 283 |
+
theme = gr.themes.Soft(font=["Inter", "Arial", "sans-serif"])
|
| 284 |
+
css = """
|
| 285 |
+
.gradio-container {max-width: 100% !important; font-size: 16px !important;}
|
| 286 |
+
.gradio-container h1 {font-size: 1.5em !important;}
|
| 287 |
+
.gradio-container .prose {font-size: 1.1em !important;}
|
| 288 |
+
.compact-audio audio {height: 60px !important;}
|
| 289 |
+
.compact-audio .waveform {min-height: 80px !important;}
|
| 290 |
+
|
| 291 |
+
/* CSS for Event Tags */
|
| 292 |
+
.tag-container {
|
| 293 |
+
display: flex !important;
|
| 294 |
+
flex-wrap: wrap !important;
|
| 295 |
+
gap: 8px !important;
|
| 296 |
+
margin-top: 5px !important;
|
| 297 |
+
margin-bottom: 10px !important;
|
| 298 |
+
border: none !important;
|
| 299 |
+
background: transparent !important;
|
| 300 |
+
}
|
| 301 |
+
.tag-btn {
|
| 302 |
+
min-width: fit-content !important;
|
| 303 |
+
width: auto !important;
|
| 304 |
+
height: 32px !important;
|
| 305 |
+
font-size: 13px !important;
|
| 306 |
+
background: #eef2ff !important;
|
| 307 |
+
border: 1px solid #c7d2fe !important;
|
| 308 |
+
color: #3730a3 !important;
|
| 309 |
+
border-radius: 6px !important;
|
| 310 |
+
padding: 0 10px !important;
|
| 311 |
+
margin: 0 !important;
|
| 312 |
+
box-shadow: none !important;
|
| 313 |
+
}
|
| 314 |
+
.tag-btn:hover {
|
| 315 |
+
background: #c7d2fe !important;
|
| 316 |
+
transform: translateY(-1px);
|
| 317 |
+
}
|
| 318 |
+
"""
|
| 319 |
+
|
| 320 |
+
def _lang_dropdown(label="Language (optional)", value="Auto"):
|
| 321 |
+
return gr.Dropdown(
|
| 322 |
+
label=label, choices=_ALL_LANGUAGES, value=value,
|
| 323 |
+
allow_custom_value=False, interactive=True,
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
def _gen_settings():
|
| 327 |
+
with gr.Accordion("Generation Settings (optional)", open=False):
|
| 328 |
+
sp = gr.Slider(0.5, 1.5, value=1.0, step=0.05, label="Speed", info="1.0 = normal. >1 faster, <1 slower.")
|
| 329 |
+
du = gr.Number(value=None, label="Duration (seconds)", info="Set a fixed duration to override speed.")
|
| 330 |
+
ns = gr.Slider(4, 64, value=32, step=1, label="Inference Steps", info="Lower = faster, higher = better quality.")
|
| 331 |
+
dn = gr.Checkbox(label="Denoise", value=True)
|
| 332 |
+
gs = gr.Slider(0.0, 4.0, value=2.0, step=0.1, label="Guidance Scale (CFG)")
|
| 333 |
+
pp = gr.Checkbox(label="Preprocess Prompt", value=True, info="Applies silence removal and trims reference audio.")
|
| 334 |
+
po = gr.Checkbox(label="Postprocess Output", value=True, info="Removes long silences from generated audio.")
|
| 335 |
+
return ns, gs, dn, sp, du, pp, po
|
| 336 |
+
|
| 337 |
+
with gr.Blocks(theme=theme, css=css, title="OmniVoice Demo") as demo:
|
| 338 |
+
gr.HTML("""
|
| 339 |
+
<div style="text-align: center; margin: 20px auto; max-width: 800px;">
|
| 340 |
+
<h1 style="font-size: 2.5em; margin-bottom: 5px;">🎙️ OmniVoice Multilingual </h1>
|
| 341 |
+
<p>State-of-the-art text-to-speech model for 600+ languages, supporting Voice Clone and Voice Design.</p>
|
| 342 |
+
</div>
|
| 343 |
+
""")
|
| 344 |
+
|
| 345 |
+
with gr.Tabs():
|
| 346 |
+
# ==============================================================
|
| 347 |
+
# Voice Clone Tab
|
| 348 |
+
# ==============================================================
|
| 349 |
+
with gr.TabItem("Voice Clone"):
|
| 350 |
+
with gr.Row():
|
| 351 |
+
with gr.Column(scale=1):
|
| 352 |
+
# Added elem_id for JS hook
|
| 353 |
+
vc_text = gr.Textbox(label="Text to Synthesize", lines=4, placeholder="Enter the text to synthesize...", elem_id="vc_textbox")
|
| 354 |
+
|
| 355 |
+
# Tag Buttons for Voice Clone
|
| 356 |
+
with gr.Row(elem_classes=["tag-container"]):
|
| 357 |
+
for tag in EVENT_TAGS:
|
| 358 |
+
btn = gr.Button(tag, elem_classes=["tag-btn"])
|
| 359 |
+
btn.click(
|
| 360 |
+
fn=None,
|
| 361 |
+
inputs=[btn, vc_text],
|
| 362 |
+
outputs=vc_text,
|
| 363 |
+
js=INSERT_TAG_JS_VC
|
| 364 |
+
)
|
| 365 |
+
|
| 366 |
+
with gr.Row():
|
| 367 |
+
vc_lang = _lang_dropdown("Language (optional)")
|
| 368 |
+
vc_want_subs = gr.Checkbox(label="Want Subtitles ?", value=False)
|
| 369 |
+
vc_ref_audio = gr.Audio(label="Reference Audio (3–10 seconds audio)", type="filepath", elem_classes="compact-audio")
|
| 370 |
+
|
| 371 |
+
vc_ref_text = gr.Textbox(
|
| 372 |
+
label="Reference Text", lines=2,
|
| 373 |
+
placeholder="Auto-transcribed upon audio upload. You can manually edit it if Whisper gets it wrong."
|
| 374 |
+
)
|
| 375 |
+
|
| 376 |
+
vc_btn = gr.Button("Generate", variant="primary")
|
| 377 |
+
vc_ns, vc_gs, vc_dn, vc_sp, vc_du, vc_pp, vc_po = _gen_settings()
|
| 378 |
+
|
| 379 |
+
with gr.Column(scale=1):
|
| 380 |
+
vc_audio = gr.Audio(label="Output Audio", type="numpy")
|
| 381 |
+
vc_status = gr.Textbox(label="Status", lines=1)
|
| 382 |
+
|
| 383 |
+
with gr.Accordion("Download files", open=False):
|
| 384 |
+
vc_out_wav = gr.File(label="Generated Audio (WAV)")
|
| 385 |
+
vc_out_custom_srt = gr.File(label="Sentence Level SRT")
|
| 386 |
+
vc_out_word_srt = gr.File(label="Word Level SRT")
|
| 387 |
+
vc_out_shorts_srt = gr.File(label="Shorts SRT")
|
| 388 |
+
|
| 389 |
+
def _auto_transcribe(audio_path, lang):
|
| 390 |
+
if not audio_path:
|
| 391 |
+
return gr.update(value="")
|
| 392 |
+
try:
|
| 393 |
+
whisper_lang = lang if lang != "Auto" else None
|
| 394 |
+
whisper_results = subtitle_maker(audio_path, whisper_lang)
|
| 395 |
+
if whisper_results and len(whisper_results) > 7:
|
| 396 |
+
return gr.update(value=whisper_results[7])
|
| 397 |
+
except Exception as e:
|
| 398 |
+
logging.warning(f"Auto-transcription failed: {e}")
|
| 399 |
+
return gr.update(value="")
|
| 400 |
+
|
| 401 |
+
vc_ref_audio.change(
|
| 402 |
+
fn=_auto_transcribe,
|
| 403 |
+
inputs=[vc_ref_audio, vc_lang],
|
| 404 |
+
outputs=[vc_ref_text]
|
| 405 |
+
)
|
| 406 |
+
|
| 407 |
+
def _clone_fn(text, lang, ref_aud, ref_text, want_subs, ns, gs, dn, sp, du, pp, po):
|
| 408 |
+
res = _gen_core(text, lang, ref_aud, None, ns, gs, dn, sp, du, pp, po, mode="clone", ref_text=ref_text)
|
| 409 |
+
if res[0] is None:
|
| 410 |
+
return None, res[1], None, None, None, None
|
| 411 |
+
|
| 412 |
+
audio_tuple, status = res
|
| 413 |
+
sr, waveform = audio_tuple
|
| 414 |
+
# tmp_wav = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
|
| 415 |
+
tmp_wav=tts_file_name(text, language=lang)
|
| 416 |
+
wavfile.write(tmp_wav, sr, waveform)
|
| 417 |
+
|
| 418 |
+
c_srt, w_srt, s_srt = generate_subtitles_if_needed(tmp_wav, lang, want_subs)
|
| 419 |
+
|
| 420 |
+
return audio_tuple, status, tmp_wav, c_srt, w_srt, s_srt
|
| 421 |
+
|
| 422 |
+
vc_btn.click(
|
| 423 |
+
_clone_fn,
|
| 424 |
+
inputs=[vc_text, vc_lang, vc_ref_audio, vc_ref_text, vc_want_subs, vc_ns, vc_gs, vc_dn, vc_sp, vc_du, vc_pp, vc_po],
|
| 425 |
+
outputs=[vc_audio, vc_status, vc_out_wav, vc_out_custom_srt, vc_out_word_srt, vc_out_shorts_srt],
|
| 426 |
+
)
|
| 427 |
+
|
| 428 |
+
# ==============================================================
|
| 429 |
+
# Voice Design Tab
|
| 430 |
+
# ==============================================================
|
| 431 |
+
with gr.TabItem("Voice Design"):
|
| 432 |
+
with gr.Row():
|
| 433 |
+
with gr.Column(scale=1):
|
| 434 |
+
# Added elem_id for JS hook
|
| 435 |
+
vd_text = gr.Textbox(label="Text to Synthesize", lines=4, placeholder="Enter the text to synthesize...", elem_id="vd_textbox")
|
| 436 |
+
|
| 437 |
+
# Tag Buttons for Voice Design
|
| 438 |
+
with gr.Row(elem_classes=["tag-container"]):
|
| 439 |
+
for tag in EVENT_TAGS:
|
| 440 |
+
btn = gr.Button(tag, elem_classes=["tag-btn"])
|
| 441 |
+
btn.click(
|
| 442 |
+
fn=None,
|
| 443 |
+
inputs=[btn, vd_text],
|
| 444 |
+
outputs=vd_text,
|
| 445 |
+
js=INSERT_TAG_JS_VD
|
| 446 |
+
)
|
| 447 |
+
|
| 448 |
+
with gr.Row():
|
| 449 |
+
vd_lang = _lang_dropdown(value='Auto')
|
| 450 |
+
vd_want_subs = gr.Checkbox(label="Want Subtitles ?", value=False)
|
| 451 |
+
vd_btn = gr.Button("Generate", variant="primary")
|
| 452 |
+
with gr.Accordion("Character Voice Design", open=False):
|
| 453 |
+
vd_groups = []
|
| 454 |
+
for _cat, _choices in _CATEGORIES.items():
|
| 455 |
+
default_val = "Auto"
|
| 456 |
+
if _cat == "Gender":
|
| 457 |
+
default_val = "Female"
|
| 458 |
+
elif _cat == "Age":
|
| 459 |
+
default_val = "Young Adult"
|
| 460 |
+
|
| 461 |
+
vd_groups.append(
|
| 462 |
+
gr.Dropdown(label=_cat, choices=["Auto"] + _choices, value=default_val, info=_ATTR_INFO.get(_cat))
|
| 463 |
+
)
|
| 464 |
+
|
| 465 |
+
vd_ns, vd_gs, vd_dn, vd_sp, vd_du, vd_pp, vd_po = _gen_settings()
|
| 466 |
+
|
| 467 |
+
with gr.Column(scale=1):
|
| 468 |
+
vd_audio = gr.Audio(label="Output Audio", type="numpy")
|
| 469 |
+
vd_status = gr.Textbox(label="Status", lines=1)
|
| 470 |
+
|
| 471 |
+
with gr.Accordion("Download files", open=False):
|
| 472 |
+
vd_out_wav = gr.File(label="Generated Audio (WAV)")
|
| 473 |
+
vd_out_custom_srt = gr.File(label="Sentence Level SRT")
|
| 474 |
+
vd_out_word_srt = gr.File(label="Word Level SRT")
|
| 475 |
+
vd_out_shorts_srt = gr.File(label="Shorts SRT")
|
| 476 |
+
|
| 477 |
+
def _build_instruct(groups):
|
| 478 |
+
selected = [g for g in groups if g and g != "Auto"]
|
| 479 |
+
if not selected: return None
|
| 480 |
+
return ", ".join([DIALECT_MAP.get(v, v) for v in selected])
|
| 481 |
+
|
| 482 |
+
def _design_fn(text, lang, want_subs, ns, gs, dn, sp, du, pp, po, *groups):
|
| 483 |
+
instruct = _build_instruct(groups)
|
| 484 |
+
res = _gen_core(text, lang, None, instruct, ns, gs, dn, sp, du, pp, po, mode="design")
|
| 485 |
+
if res[0] is None:
|
| 486 |
+
return None, res[1], None, None, None, None
|
| 487 |
+
|
| 488 |
+
audio_tuple, status = res
|
| 489 |
+
sr, waveform = audio_tuple
|
| 490 |
+
tmp_wav=tts_file_name(text, language=lang)
|
| 491 |
+
# tmp_wav = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
|
| 492 |
+
wavfile.write(tmp_wav, sr, waveform)
|
| 493 |
+
|
| 494 |
+
c_srt, w_srt, s_srt = generate_subtitles_if_needed(tmp_wav, lang, want_subs)
|
| 495 |
+
|
| 496 |
+
return audio_tuple, status, tmp_wav, c_srt, w_srt, s_srt
|
| 497 |
+
|
| 498 |
+
vd_btn.click(
|
| 499 |
+
_design_fn,
|
| 500 |
+
inputs=[vd_text, vd_lang, vd_want_subs, vd_ns, vd_gs, vd_dn, vd_sp, vd_du, vd_pp, vd_po] + vd_groups,
|
| 501 |
+
outputs=[vd_audio, vd_status, vd_out_wav, vd_out_custom_srt, vd_out_word_srt, vd_out_shorts_srt],
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
if __name__ == "__main__":
|
| 505 |
+
demo.queue().launch(share=True, debug=True)
|