Spaces:
Running on Zero
Running on Zero
mobile friendly UI with gradio server
#3
by akhaliq HF Staff - opened
- .gitignore +1 -0
- README.md +1 -1
- app.py +312 -525
- index.html +842 -0
.gitignore
CHANGED
|
@@ -1,3 +1,4 @@
|
|
| 1 |
__pycache__/
|
| 2 |
*.py[cod]
|
| 3 |
.cache/
|
|
|
|
|
|
| 1 |
__pycache__/
|
| 2 |
*.py[cod]
|
| 3 |
.cache/
|
| 4 |
+
rendered/
|
README.md
CHANGED
|
@@ -4,7 +4,7 @@ emoji: 🔊
|
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
python_version: 3.12
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.20.0
|
| 8 |
python_version: 3.12
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
app.py
CHANGED
|
@@ -1,9 +1,8 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
import logging
|
| 4 |
-
import
|
| 5 |
import re
|
| 6 |
-
import sys
|
|
|
|
| 7 |
import threading
|
| 8 |
import time
|
| 9 |
from dataclasses import dataclass
|
|
@@ -12,118 +11,130 @@ from pathlib import Path
|
|
| 12 |
|
| 13 |
import gradio as gr
|
| 14 |
import numpy as np
|
|
|
|
| 15 |
import torch
|
| 16 |
from audiotsm import wsola
|
| 17 |
from audiotsm.io.array import ArrayReader, ArrayWriter
|
|
|
|
| 18 |
from scipy.signal import resample_poly
|
| 19 |
-
|
| 20 |
-
try:
|
| 21 |
-
import spaces
|
| 22 |
-
except ImportError: # Local smoke tests do not require the ZeroGPU shim.
|
| 23 |
-
class _Spaces:
|
| 24 |
-
@staticmethod
|
| 25 |
-
def GPU(*args, **kwargs):
|
| 26 |
-
def decorate(function):
|
| 27 |
-
return function
|
| 28 |
-
return decorate
|
| 29 |
-
spaces = _Spaces()
|
| 30 |
-
|
| 31 |
-
|
| 32 |
ROOT = Path(__file__).resolve().parent
|
| 33 |
RUNTIME = ROOT / "runtime"
|
| 34 |
WEB_RUNTIME = ROOT / "web_runtime"
|
|
|
|
| 35 |
sys.path.insert(0, str(RUNTIME))
|
| 36 |
sys.path.insert(0, str(ROOT))
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
mimetypes.add_type("text/javascript", ".mjs")
|
| 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 |
def edge_fade(waveform: np.ndarray, sample_rate: int, milliseconds: float = 5.0) -> np.ndarray:
|
| 120 |
-
frames = min(round(sample_rate * milliseconds / 1000.0), waveform.size // 2)
|
| 121 |
-
if frames <= 0:
|
| 122 |
-
return waveform
|
| 123 |
-
output = waveform.copy()
|
| 124 |
-
ramp = np.linspace(0.0, 1.0, frames, endpoint=True, dtype=np.float32)
|
| 125 |
-
output[:frames] *= ramp
|
| 126 |
-
output[-frames:] *= ramp[::-1]
|
| 127 |
return output
|
| 128 |
|
| 129 |
|
|
@@ -173,429 +184,205 @@ def pitch_shift_speech(
|
|
| 173 |
|
| 174 |
|
| 175 |
class Engine:
|
| 176 |
-
def __init__(self, spec: ModelSpec) -> None:
|
| 177 |
-
if not spec.config.is_file():
|
| 178 |
-
raise FileNotFoundError(f"Missing release configuration for {spec.label}")
|
| 179 |
-
if not spec.checkpoint.is_file():
|
| 180 |
-
raise FileNotFoundError(f"Missing release weights for {spec.label}")
|
| 181 |
-
self.spec = spec
|
| 182 |
-
self.hps = utils.get_hparams_from_file(str(spec.config))
|
| 183 |
-
self.model = SynthesizerTrn(
|
| 184 |
-
len(symbols),
|
| 185 |
-
self.hps.data.filter_length // 2 + 1,
|
| 186 |
-
self.hps.train.segment_size // self.hps.data.hop_length,
|
| 187 |
-
**self.hps.model,
|
| 188 |
-
).eval()
|
| 189 |
-
root_logger = logging.getLogger()
|
| 190 |
-
previous_level = root_logger.level
|
| 191 |
-
try:
|
| 192 |
-
root_logger.setLevel(logging.WARNING)
|
| 193 |
-
utils.load_checkpoint(str(spec.checkpoint), self.model, None)
|
| 194 |
-
finally:
|
| 195 |
-
root_logger.setLevel(previous_level)
|
| 196 |
-
self.device = torch.device("cpu")
|
| 197 |
-
self.sample_rate = int(self.hps.data.sampling_rate)
|
| 198 |
-
self.lock = threading.Lock()
|
| 199 |
-
|
| 200 |
-
def tokens(self, text: str) -> tuple[torch.Tensor, torch.Tensor]:
|
| 201 |
-
phonemes = run_vits_frontend(text).phoneme_text
|
| 202 |
-
sequence = cleaned_text_to_sequence(phonemes)
|
| 203 |
-
if self.hps.data.add_blank:
|
| 204 |
-
sequence = commons.intersperse(sequence, 0)
|
| 205 |
-
if not sequence:
|
| 206 |
-
raise ValueError("The phoneme frontend produced no speakable tokens.")
|
| 207 |
-
tokens = torch.LongTensor(sequence).to(self.device).unsqueeze(0)
|
| 208 |
-
lengths = torch.LongTensor([tokens.size(1)]).to(self.device)
|
| 209 |
-
return tokens, lengths
|
| 210 |
-
|
| 211 |
-
@torch.inference_mode()
|
| 212 |
-
def synthesize(
|
| 213 |
-
self,
|
| 214 |
-
text: str,
|
| 215 |
-
speed: float,
|
| 216 |
-
variation: float,
|
| 217 |
-
pitch_steps: float,
|
| 218 |
-
seed: int,
|
| 219 |
-
) -> tuple[int, np.ndarray]:
|
| 220 |
-
chunks = split_text(text)
|
| 221 |
-
waveforms: list[np.ndarray] = []
|
| 222 |
-
with self.lock:
|
| 223 |
-
self.device = torch.device("cuda")
|
| 224 |
-
self.model.to(self.device)
|
| 225 |
-
try:
|
| 226 |
-
for index, chunk in enumerate(chunks):
|
| 227 |
-
if index:
|
| 228 |
-
waveforms.append(
|
| 229 |
-
np.zeros(
|
| 230 |
-
round(self.sample_rate * boundary_pause_seconds(chunks[index - 1])),
|
| 231 |
-
dtype=np.float32,
|
| 232 |
-
)
|
| 233 |
-
)
|
| 234 |
-
tokens, lengths = self.tokens(chunk)
|
| 235 |
-
torch.manual_seed(seed + index)
|
| 236 |
-
torch.cuda.manual_seed_all(seed + index)
|
| 237 |
-
waveform = self.model.infer(
|
| 238 |
-
tokens,
|
| 239 |
-
lengths,
|
| 240 |
-
noise_scale=variation,
|
| 241 |
-
noise_scale_w=0.8,
|
| 242 |
-
length_scale=1.0 / speed,
|
| 243 |
-
max_len=4000,
|
| 244 |
-
)[0][0, 0].float().cpu().numpy()
|
| 245 |
-
waveforms.append(edge_fade(waveform, self.sample_rate))
|
| 246 |
-
finally:
|
| 247 |
-
self.model.to("cpu")
|
| 248 |
-
self.device = torch.device("cpu")
|
| 249 |
-
torch.cuda.empty_cache()
|
| 250 |
audio = np.concatenate(waveforms)
|
| 251 |
if abs(pitch_steps) >= 0.01:
|
| 252 |
audio = pitch_shift_speech(audio, pitch_steps)
|
| 253 |
return self.sample_rate, pcm16(audio)
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
ENGINES = {label: Engine(spec) for label, spec in SPECS.items()}
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
def validate(
|
| 260 |
-
text: str,
|
| 261 |
-
speed: float,
|
| 262 |
-
variation: float,
|
| 263 |
-
pitch_steps: float,
|
| 264 |
-
seed: int,
|
| 265 |
-
) -> tuple[str, float, float, float, int]:
|
| 266 |
-
text = " ".join((text or "").split())
|
| 267 |
-
if not text:
|
| 268 |
-
raise gr.Error("Enter something for Inflect to say.")
|
| 269 |
-
return text, float(speed), float(variation), float(pitch_steps), int(seed)
|
| 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 |
-
f"Nano `{nano_wall:.2f}s / {nano_seconds:.2f}s audio`"
|
| 328 |
-
)
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
CSS = """
|
| 332 |
-
:root{color-scheme:dark;--paper:#111412;--panel:#181c19;--panel-2:#141815;--ink:#f3f4ef;--muted:#9ea8a0;--line:#303732;--signal:#2486ff;--signal-hover:#52a0ff;--signal-soft:#15263b}
|
| 333 |
-
html,body,.gradio-container,.dark{background:var(--paper)!important;color:var(--ink)!important;color-scheme:dark!important}
|
| 334 |
-
body,.gradio-container,.gradio-container button,.gradio-container input,.gradio-container textarea{font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif!important}
|
| 335 |
-
.gradio-container{width:min(100%,1640px)!important;max-width:none!important;margin:auto!important;padding:0 clamp(14px,2.2vw,34px) 54px!important;box-sizing:border-box!important}
|
| 336 |
-
.gradio-container>.main{padding:0!important}
|
| 337 |
-
.gradio-container *{box-sizing:border-box}
|
| 338 |
-
.hero-wrap,.hero-wrap.block,.hero-wrap>.wrap{padding:0!important;border:0!important;box-shadow:none!important;background:transparent!important}
|
| 339 |
-
.hero{padding:18px 2px 13px;border-bottom:1px solid var(--line);margin-bottom:2px}
|
| 340 |
-
.hero-top{display:flex;align-items:center;justify-content:space-between;gap:20px}
|
| 341 |
-
.eyebrow{color:var(--signal);font:700 11px/1 ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:.13em;text-transform:uppercase}
|
| 342 |
-
.links{display:flex;align-items:center;gap:18px;font-size:12px;font-weight:700}
|
| 343 |
-
.links a{color:var(--muted)!important;text-decoration:none}.links a:hover{color:var(--ink)!important}
|
| 344 |
-
.hero h1{max-width:1040px;margin:15px 0 8px;color:var(--ink);font:650 clamp(40px,4.5vw,56px)/.96 Inter,ui-sans-serif,sans-serif;letter-spacing:-.055em}
|
| 345 |
-
.hero>p{max-width:1000px;margin:0;color:var(--muted)!important;font-size:13px;line-height:1.48}
|
| 346 |
-
.model-facts{display:flex;align-items:stretch;margin-top:13px;border-top:1px solid var(--line)}
|
| 347 |
-
.model-fact{flex:1;padding:8px 24px 0 0;color:var(--muted)!important;font-size:11px;line-height:1.3}
|
| 348 |
-
.model-fact+.model-fact{padding-left:20px;border-left:1px solid var(--line)}
|
| 349 |
-
.model-fact strong{display:block;margin-bottom:2px;color:var(--ink);font-size:14px;line-height:1.2}
|
| 350 |
-
.runtime-tabs{margin:14px 10px 0!important}
|
| 351 |
-
.runtime-tabs>.tab-container,.runtime-tabs .tab-container{display:grid!important;grid-template-columns:repeat(2,minmax(0,1fr))!important;gap:0!important;height:auto!important;overflow:hidden!important;border:1px solid var(--line)!important;border-radius:2px!important;background:var(--panel-2)!important}
|
| 352 |
-
.runtime-tabs>.tab-container button,.runtime-tabs .tab-container button{display:flex!important;min-height:54px!important;padding:9px 14px!important;flex-direction:column!important;align-items:flex-start!important;justify-content:center!important;border:0!important;border-radius:0!important;background:transparent!important;color:var(--ink)!important;font-size:13px!important;font-weight:800!important;line-height:1.2!important;letter-spacing:.005em!important;transition:background .16s ease,color .16s ease!important}
|
| 353 |
-
.runtime-tabs>.tab-container button+button,.runtime-tabs .tab-container button+button{border-left:1px solid var(--line)!important}
|
| 354 |
-
.runtime-tabs>.tab-container button::after,.runtime-tabs .tab-container button::after{margin-top:3px;color:var(--muted);font-size:10px;font-weight:550;letter-spacing:.01em}
|
| 355 |
-
.runtime-tabs>.tab-container button:nth-child(1)::after,.runtime-tabs .tab-container button:nth-child(1)::after{content:"Hugging Face ZeroGPU"}
|
| 356 |
-
.runtime-tabs>.tab-container button:nth-child(2)::after,.runtime-tabs .tab-container button:nth-child(2)::after{content:"WebGPU / WASM · private"}
|
| 357 |
-
.runtime-tabs>.tab-container button:hover,.runtime-tabs .tab-container button:hover{background:#1b242d!important;color:var(--ink)!important}
|
| 358 |
-
.runtime-tabs>.tab-container button.selected,.runtime-tabs .tab-container button.selected{background:var(--signal-soft)!important;color:#fff!important;box-shadow:inset 0 -2px 0 var(--signal)!important}
|
| 359 |
-
.runtime-tabs>.tab-container button.selected::after,.runtime-tabs .tab-container button.selected::after{color:#a9c7e8!important}
|
| 360 |
-
.runtime-tabs>.tabitem,.runtime-tabs .tabitem{padding-top:0!important;border:0!important;background:transparent!important}
|
| 361 |
-
.mode-banner{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:16px;margin:10px 0;padding:9px 11px;border:1px solid var(--line);border-left:2px solid var(--signal);border-radius:2px;background:var(--panel-2)}
|
| 362 |
-
.mode-banner strong{display:block;margin-bottom:2px;color:var(--ink)!important;font-size:12px}
|
| 363 |
-
.mode-banner span{color:var(--muted)!important;font-size:11px;line-height:1.45}
|
| 364 |
-
.mode-badge{padding:5px 7px;border:1px solid #356ea8;border-radius:2px;color:#dceaff!important;font:750 9px/1 ui-monospace,SFMono-Regular,Consolas,monospace!important;letter-spacing:.08em;text-transform:uppercase;white-space:nowrap}
|
| 365 |
-
.browser-frame-shell{margin:0;padding:0;border:1px solid var(--line);border-radius:2px;background:var(--paper);overflow:hidden}
|
| 366 |
-
.browser-frame{display:block;width:100%;height:580px;border:0;background:var(--paper);transition:height .18s ease}
|
| 367 |
-
.browser-help{margin:9px 2px 0!important;color:var(--muted)!important;font-size:11px!important;line-height:1.5!important}
|
| 368 |
-
.workbench{padding:0 0 4px!important;border:0!important;background:transparent!important}
|
| 369 |
-
.workbench.block,.workbench>.wrap{border:0!important;box-shadow:none!important;background:transparent!important}
|
| 370 |
-
.control-row{align-items:stretch!important;gap:18px!important;margin-top:6px!important;padding:12px 14px 11px!important;border:1px solid var(--line)!important;border-radius:3px!important;background:var(--panel-2)!important}
|
| 371 |
-
.control-row>div{min-width:0!important}
|
| 372 |
-
.control-row .form{height:100%!important;border:0!important;background:transparent!important}
|
| 373 |
-
.control-row label>span:first-child{font-size:12px!important;font-weight:750!important;letter-spacing:.015em!important}
|
| 374 |
-
.model-switch .wrap{display:grid!important;grid-template-columns:repeat(2,minmax(0,1fr))!important;gap:0!important;padding:0!important;overflow:hidden!important;border:1px solid var(--line)!important;border-radius:2px!important;background:#101411!important}
|
| 375 |
-
.model-switch .wrap label{min-height:46px!important;margin:0!important;padding:10px 12px!important;border:0!important;border-radius:0!important;background:transparent!important}
|
| 376 |
-
.model-switch .wrap label+label{border-left:1px solid var(--line)!important}
|
| 377 |
-
.model-switch .wrap label:has(input:checked){background:var(--signal-soft)!important;box-shadow:inset 0 -2px 0 var(--signal)!important}
|
| 378 |
-
.model-switch .wrap label span{font-size:12px!important;font-weight:750!important}
|
| 379 |
-
.action-row{justify-content:center!important;gap:12px!important;margin:12px auto 0!important;max-width:840px!important}
|
| 380 |
-
.action-row button{min-height:48px!important}
|
| 381 |
-
.comparison{margin-top:22px!important;padding-top:20px!important;border-top:1px solid var(--line)!important}
|
| 382 |
-
.comparison-title{margin-bottom:9px!important}
|
| 383 |
-
.gradio-container .block,.gradio-container .form{border-radius:3px!important;background:var(--panel-2)!important;border-color:var(--line)!important;color:var(--ink)!important}
|
| 384 |
-
.gradio-container .prose,.gradio-container .markdown-body,.gradio-container label,.gradio-container span,.gradio-container p,.gradio-container h1,.gradio-container h2,.gradio-container h3{color:var(--ink)!important}
|
| 385 |
-
.gradio-container label{background:transparent!important}
|
| 386 |
-
.gradio-container .wrap{background:var(--panel-2)!important}
|
| 387 |
-
.gradio-container select{min-height:48px!important;background:#101411!important;color:var(--ink)!important;border-color:var(--line)!important;text-align:left!important}
|
| 388 |
-
.gradio-container input,.gradio-container textarea{background:#101411!important;color:var(--ink)!important;border-color:var(--line)!important}
|
| 389 |
-
.prompt-input textarea{font-size:16px!important;line-height:1.55!important;min-height:104px!important;padding:15px!important}
|
| 390 |
-
.gradio-container button:not(.primary){background:var(--panel-2)!important;color:var(--ink)!important;border-color:var(--line)!important}
|
| 391 |
-
.gradio-container button:not(.primary):hover{border-color:#527aa6!important;background:#1b242d!important}
|
| 392 |
-
button.primary{min-height:46px!important;border:0!important;border-radius:1px!important;background:var(--signal)!important;color:#fff!important;font-weight:800!important}
|
| 393 |
-
button.primary *{color:#fff!important}button.primary:hover{background:var(--signal-hover)!important}
|
| 394 |
-
.advanced{margin:10px 0 3px!important;background:#131814!important;border-left:2px solid #344138!important}
|
| 395 |
-
.advanced>button{font-size:12px!important;font-weight:750!important;letter-spacing:.01em!important}
|
| 396 |
-
.output-audio{margin-top:8px!important}
|
| 397 |
-
.outputmeta{padding:11px 13px!important;background:var(--signal-soft)!important;border:1px solid #294e78!important}
|
| 398 |
-
.outputmeta *{color:#dceaff!important}
|
| 399 |
-
.helper-copy{margin:8px 2px 4px!important;color:var(--muted)!important;font-size:12px!important}
|
| 400 |
-
.fineprint{color:var(--muted)!important;font-size:12px;line-height:1.55;border-top:1px solid var(--line);padding-top:16px;margin-top:22px}
|
| 401 |
-
footer{display:none!important}
|
| 402 |
-
@media(max-width:760px){
|
| 403 |
-
html,body{overflow-x:hidden!important}.gradio-container{width:100%!important;min-width:0!important;padding:0 14px 38px!important}
|
| 404 |
-
.hero{padding-top:22px}.hero-top{align-items:flex-start;flex-direction:column;gap:12px}.links{gap:14px;font-size:11px}
|
| 405 |
-
.hero h1{margin-top:18px;font-size:42px}.model-facts{display:grid;grid-template-columns:1fr 1fr}.model-fact{padding:11px 10px 9px 0}.model-fact+.model-fact{padding-left:10px}.model-fact:nth-child(3){padding-left:0;border-left:0;border-top:1px solid var(--line)}.model-fact:nth-child(4){border-top:1px solid var(--line)}
|
| 406 |
-
.runtime-tabs{margin-left:0!important;margin-right:0!important}.runtime-tabs>.tab-container button,.runtime-tabs .tab-container button{min-height:54px!important;padding-inline:10px!important}.runtime-tabs>.tab-container button::after,.runtime-tabs .tab-container button::after{font-size:9px}.mode-banner{grid-template-columns:1fr}.mode-badge{justify-self:start}.browser-frame{height:980px}
|
| 407 |
-
.workbench{min-width:0!important;padding-top:18px!important}.control-row,.action-row,.comparison .row{flex-direction:column!important;min-width:0!important}
|
| 408 |
-
.control-row>*,.action-row>*,.comparison .row>*{width:100%!important;min-width:0!important}.gradio-container .wrap,.gradio-container .form,.gradio-container .block{min-width:0!important}
|
| 409 |
-
}
|
| 410 |
-
"""
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
PROMPTS = [
|
| 414 |
-
"Wait, are you actually being for real? I thought the train left twenty minutes ago!",
|
| 415 |
-
"Does punctuation work? Yes: commas, semicolons, and periods should create sensible boundaries.",
|
| 416 |
-
"Professor Nguyen mailed the fluorescent poster to Reykjavik on Thursday.",
|
| 417 |
-
"Although the weather changed without warning, the musicians finished packing before anyone opened the enormous glass door.",
|
| 418 |
-
]
|
| 419 |
-
|
| 420 |
-
with gr.Blocks(title="Inflect v2 · local speech playground") as demo:
|
| 421 |
-
gr.HTML("""
|
| 422 |
-
<header class="hero">
|
| 423 |
-
<div class="hero-top">
|
| 424 |
-
<span class="eyebrow">Inflect v2 · choose where inference runs</span>
|
| 425 |
-
<nav class="links">
|
| 426 |
-
<a href="https://huggingface.co/owensong/Inflect-Micro-v2">Micro model</a>
|
| 427 |
-
<a href="https://huggingface.co/owensong/Inflect-Nano-v2">Nano model</a>
|
| 428 |
-
<a href="https://github.com/owenawsong/Inflect">GitHub</a>
|
| 429 |
-
</nav>
|
| 430 |
-
</div>
|
| 431 |
-
<h1>Small models.<br>Complete speech.</h1>
|
| 432 |
-
<p>Generate 24 kHz English speech with the published Inflect-Micro-v2 and Inflect-Nano-v2 checkpoints. Choose hosted ZeroGPU inference or run the ONNX models privately in this browser.</p>
|
| 433 |
-
<div class="model-facts">
|
| 434 |
-
<div class="model-fact"><strong>9.36M · Micro</strong>quality-first model</div>
|
| 435 |
-
<div class="model-fact"><strong>3.96M · Nano</strong>footprint-first model</div>
|
| 436 |
-
<div class="model-fact"><strong>24 kHz WAV</strong>complete waveform output</div>
|
| 437 |
-
<div class="model-fact"><strong>Local stack</strong>text normalization through audio</div>
|
| 438 |
-
</div>
|
| 439 |
-
</header>
|
| 440 |
-
""", elem_classes="hero-wrap")
|
| 441 |
-
with gr.Tabs(elem_classes="runtime-tabs"):
|
| 442 |
-
with gr.Tab("Hosted GPU", id="zerogpu"):
|
| 443 |
-
gr.HTML("""
|
| 444 |
-
<div class="mode-banner">
|
| 445 |
-
<span><strong>ZeroGPU inference</strong>Nothing downloads to your device. Hugging Face runs each request; free-tier quota and queue limits apply.</span>
|
| 446 |
-
<span class="mode-badge">Server mode</span>
|
| 447 |
-
</div>
|
| 448 |
-
""")
|
| 449 |
-
with gr.Column(elem_classes="workbench"):
|
| 450 |
-
text = gr.Textbox(
|
| 451 |
-
value=PROMPTS[0],
|
| 452 |
-
lines=4,
|
| 453 |
-
max_lines=14,
|
| 454 |
-
label="Text",
|
| 455 |
-
placeholder="Write a sentence, paragraph, or script...",
|
| 456 |
-
elem_classes="prompt-input",
|
| 457 |
-
)
|
| 458 |
-
with gr.Row(equal_height=True, elem_classes="control-row"):
|
| 459 |
-
model = gr.Radio(
|
| 460 |
-
choices=list(SPECS),
|
| 461 |
-
value="Inflect Micro v2",
|
| 462 |
-
label="Model",
|
| 463 |
-
info="Micro prioritizes quality; Nano prioritizes footprint.",
|
| 464 |
-
scale=4,
|
| 465 |
-
elem_classes="model-switch",
|
| 466 |
-
)
|
| 467 |
-
speed = gr.Slider(
|
| 468 |
-
0.7,
|
| 469 |
-
1.35,
|
| 470 |
-
value=1.0,
|
| 471 |
-
step=0.01,
|
| 472 |
-
label="Speaking speed",
|
| 473 |
-
info="1.00 is the trained default.",
|
| 474 |
-
scale=4,
|
| 475 |
-
)
|
| 476 |
-
variation = gr.Slider(
|
| 477 |
-
0.0,
|
| 478 |
-
1.0,
|
| 479 |
-
value=0.667,
|
| 480 |
-
step=0.001,
|
| 481 |
-
label="Delivery variation",
|
| 482 |
-
info="Lower is steadier. Higher gives each take more variation.",
|
| 483 |
-
scale=5,
|
| 484 |
-
)
|
| 485 |
-
with gr.Accordion(
|
| 486 |
-
"Advanced controls",
|
| 487 |
-
open=False,
|
| 488 |
-
elem_classes="advanced",
|
| 489 |
-
):
|
| 490 |
-
with gr.Row():
|
| 491 |
-
pitch = gr.Slider(
|
| 492 |
-
-2.0,
|
| 493 |
-
2.0,
|
| 494 |
-
value=0.0,
|
| 495 |
-
step=0.25,
|
| 496 |
-
label="Pitch shift (semitones)",
|
| 497 |
-
info="Changes the finished voice pitch without changing speaking speed.",
|
| 498 |
-
)
|
| 499 |
-
seed = gr.Number(
|
| 500 |
-
value=0,
|
| 501 |
-
precision=0,
|
| 502 |
-
label="Repeatable seed",
|
| 503 |
-
info="Use the same seed and settings to reproduce a take.",
|
| 504 |
-
)
|
| 505 |
-
with gr.Row(elem_classes="action-row"):
|
| 506 |
-
generate = gr.Button("Generate selected model", variant="primary")
|
| 507 |
-
compare = gr.Button("Compare Micro and Nano")
|
| 508 |
-
audio = gr.Audio(label="Generated 24 kHz WAV", elem_classes="output-audio")
|
| 509 |
-
status = gr.Markdown("Ready.", elem_classes="outputmeta")
|
| 510 |
-
gr.Markdown(
|
| 511 |
-
"The first request can include ZeroGPU queue and model-transfer time. "
|
| 512 |
-
"Use the audio player's menu to download the final WAV.",
|
| 513 |
-
elem_classes="helper-copy",
|
| 514 |
-
)
|
| 515 |
-
gr.Examples(
|
| 516 |
-
[[prompt] for prompt in PROMPTS],
|
| 517 |
-
inputs=text,
|
| 518 |
-
label="Try a prepared prompt",
|
| 519 |
-
)
|
| 520 |
-
with gr.Column(elem_classes="comparison"):
|
| 521 |
-
gr.Markdown(
|
| 522 |
-
"### Direct model comparison\n"
|
| 523 |
-
"The same text, seed, speed, variation, and pitch are sent to both models.",
|
| 524 |
-
elem_classes="comparison-title",
|
| 525 |
-
)
|
| 526 |
-
with gr.Row():
|
| 527 |
-
micro_audio = gr.Audio(label="Micro v2 · 9.36M · quality first")
|
| 528 |
-
nano_audio = gr.Audio(label="Nano v2 · 3.96M · footprint first")
|
| 529 |
-
compare_status = gr.Markdown("", elem_classes="outputmeta")
|
| 530 |
-
gr.HTML("""<p class="fineprint">Fixed English voice · English-only frontend · no voice cloning · no reference audio upload. Uncommon names, abbreviations, and long passages can still fail.</p>""")
|
| 531 |
-
with gr.Tab("This browser", id="webgpu"):
|
| 532 |
-
gr.HTML("""
|
| 533 |
-
<div class="mode-banner">
|
| 534 |
-
<span><strong>Browser-local inference</strong>Your text and generated audio stay in this tab. WebGPU is preferred; compatible browsers fall back to WASM.</span>
|
| 535 |
-
<span class="mode-badge">No queue · no quota</span>
|
| 536 |
-
</div>
|
| 537 |
-
<div class="browser-frame-shell">
|
| 538 |
-
<iframe
|
| 539 |
-
class="browser-frame"
|
| 540 |
-
src="/gradio_api/file=web_runtime/index.html?embed=1"
|
| 541 |
-
title="Inflect browser-local WebGPU runtime"
|
| 542 |
-
allow="autoplay; webgpu"
|
| 543 |
-
loading="eager"
|
| 544 |
-
></iframe>
|
| 545 |
-
</div>
|
| 546 |
-
<p class="browser-help">The first browser-local run downloads the selected ONNX model and compiles it on your device. Streaming and complete-audio modes are both available inside this panel.</p>
|
| 547 |
-
""", js_on_load="""
|
| 548 |
-
const frame = element.querySelector('.browser-frame');
|
| 549 |
-
if (frame) {
|
| 550 |
-
if (element.__inflectResizeHandler) {
|
| 551 |
-
window.removeEventListener('message', element.__inflectResizeHandler);
|
| 552 |
-
}
|
| 553 |
-
if (element.__inflectLoadHandler) {
|
| 554 |
-
frame.removeEventListener('load', element.__inflectLoadHandler);
|
| 555 |
-
}
|
| 556 |
-
const applyHeight = (height) => {
|
| 557 |
-
const measured = Number(height);
|
| 558 |
-
if (Number.isFinite(measured) && measured > 0) {
|
| 559 |
-
const responsiveFloor = frame.clientWidth < 1024 ? 980 : 560;
|
| 560 |
-
frame.style.height = `${Math.max(responsiveFloor, Math.min(2200, measured + 2))}px`;
|
| 561 |
-
}
|
| 562 |
-
};
|
| 563 |
-
const measureFrame = () => {
|
| 564 |
-
try {
|
| 565 |
-
const doc = frame.contentDocument;
|
| 566 |
-
applyHeight(Math.max(
|
| 567 |
-
doc?.body?.scrollHeight || 0,
|
| 568 |
-
doc?.documentElement?.scrollHeight || 0,
|
| 569 |
-
));
|
| 570 |
-
} catch (_) {
|
| 571 |
-
// The embedded runtime also reports its height by postMessage.
|
| 572 |
-
}
|
| 573 |
-
};
|
| 574 |
-
const resizeHandler = (event) => {
|
| 575 |
-
if (
|
| 576 |
-
event.origin !== window.location.origin ||
|
| 577 |
-
event.source !== frame.contentWindow ||
|
| 578 |
-
event.data?.type !== 'inflect-web-resize'
|
| 579 |
-
) {
|
| 580 |
-
return;
|
| 581 |
-
}
|
| 582 |
-
applyHeight(event.data.height);
|
| 583 |
-
};
|
| 584 |
-
element.__inflectResizeHandler = resizeHandler;
|
| 585 |
-
element.__inflectLoadHandler = measureFrame;
|
| 586 |
-
window.addEventListener('message', resizeHandler);
|
| 587 |
-
frame.addEventListener('load', measureFrame);
|
| 588 |
-
[100, 500, 1500].forEach((delay) => window.setTimeout(measureFrame, delay));
|
| 589 |
-
}
|
| 590 |
-
""")
|
| 591 |
-
generate.click(synthesize_one, [text, model, speed, variation, pitch, seed], [audio, status], concurrency_limit=1, api_name="synthesize")
|
| 592 |
-
text.submit(synthesize_one, [text, model, speed, variation, pitch, seed], [audio, status], concurrency_limit=1)
|
| 593 |
-
compare.click(synthesize_both, [text, speed, variation, pitch, seed], [micro_audio, nano_audio, compare_status], concurrency_limit=1, api_name="compare")
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
if __name__ == "__main__":
|
| 597 |
-
demo.queue(default_concurrency_limit=1).launch(
|
| 598 |
-
theme=gr.themes.Base(),
|
| 599 |
-
css=CSS,
|
| 600 |
-
allowed_paths=[str(WEB_RUNTIME)],
|
| 601 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import logging
|
| 2 |
+
import os
|
| 3 |
import re
|
| 4 |
+
import sys
|
| 5 |
+
import tempfile
|
| 6 |
import threading
|
| 7 |
import time
|
| 8 |
from dataclasses import dataclass
|
|
|
|
| 11 |
|
| 12 |
import gradio as gr
|
| 13 |
import numpy as np
|
| 14 |
+
import soundfile as sf
|
| 15 |
import torch
|
| 16 |
from audiotsm import wsola
|
| 17 |
from audiotsm.io.array import ArrayReader, ArrayWriter
|
| 18 |
+
from fastapi.responses import HTMLResponse, FileResponse
|
| 19 |
from scipy.signal import resample_poly
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
import spaces
|
| 23 |
+
except ImportError: # Local smoke tests do not require the ZeroGPU shim.
|
| 24 |
+
class _Spaces:
|
| 25 |
+
@staticmethod
|
| 26 |
+
def GPU(*args, **kwargs):
|
| 27 |
+
def decorate(function):
|
| 28 |
+
return function
|
| 29 |
+
return decorate
|
| 30 |
+
spaces = _Spaces()
|
| 31 |
+
|
| 32 |
+
|
| 33 |
ROOT = Path(__file__).resolve().parent
|
| 34 |
RUNTIME = ROOT / "runtime"
|
| 35 |
WEB_RUNTIME = ROOT / "web_runtime"
|
| 36 |
+
STATIC_URL = "/web_runtime"
|
| 37 |
sys.path.insert(0, str(RUNTIME))
|
| 38 |
sys.path.insert(0, str(ROOT))
|
| 39 |
|
| 40 |
+
from gradio import Server
|
| 41 |
+
from gradio.data_classes import FileData
|
| 42 |
+
|
| 43 |
+
# Register ONNX Runtime / model asset MIME types. Windows (and some containers)
|
| 44 |
+
# do not map these by default, and FileResponse would otherwise serve the
|
| 45 |
+
# browser runtime's modules with a wrong content type.
|
| 46 |
+
import mimetypes
|
| 47 |
mimetypes.add_type("text/javascript", ".mjs")
|
| 48 |
+
mimetypes.add_type("application/wasm", ".wasm")
|
| 49 |
+
mimetypes.add_type("application/octet-stream", ".onnx")
|
| 50 |
+
mimetypes.add_type("application/json", ".config.json")
|
| 51 |
+
|
| 52 |
+
app = Server()
|
| 53 |
+
|
| 54 |
+
import commons
|
| 55 |
+
import utils
|
| 56 |
+
from inflect_vits_frontend import run_vits_frontend
|
| 57 |
+
from models import SynthesizerTrn
|
| 58 |
+
from text import cleaned_text_to_sequence
|
| 59 |
+
from text.symbols import symbols
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@dataclass(frozen=True)
|
| 63 |
+
class ModelSpec:
|
| 64 |
+
label: str
|
| 65 |
+
short_label: str
|
| 66 |
+
params: str
|
| 67 |
+
checkpoint: Path
|
| 68 |
+
config: Path
|
| 69 |
+
revision: str
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
SPECS = {
|
| 73 |
+
"Inflect Micro v2": ModelSpec(
|
| 74 |
+
label="Inflect Micro v2",
|
| 75 |
+
short_label="Micro",
|
| 76 |
+
params="9.36M",
|
| 77 |
+
checkpoint=ROOT / "models" / "micro" / "model.pth",
|
| 78 |
+
config=ROOT / "models" / "micro" / "config.json",
|
| 79 |
+
revision="3eede065",
|
| 80 |
+
),
|
| 81 |
+
"Inflect Nano v2": ModelSpec(
|
| 82 |
+
label="Inflect Nano v2",
|
| 83 |
+
short_label="Nano",
|
| 84 |
+
params="3.96M",
|
| 85 |
+
checkpoint=ROOT / "models" / "nano" / "model.pth",
|
| 86 |
+
config=ROOT / "models" / "nano" / "config.json",
|
| 87 |
+
revision="bfca4684",
|
| 88 |
+
),
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def split_text(text: str, limit: int = 280) -> list[str]:
|
| 93 |
+
normalized = " ".join(text.split())
|
| 94 |
+
sentences = [
|
| 95 |
+
part.strip()
|
| 96 |
+
for part in re.split(r"(?<=[.!?;:])\s+", normalized)
|
| 97 |
+
if part.strip()
|
| 98 |
+
]
|
| 99 |
+
chunks: list[str] = []
|
| 100 |
+
for sentence in sentences or [normalized]:
|
| 101 |
+
while len(sentence) > limit:
|
| 102 |
+
search = sentence[: limit + 1]
|
| 103 |
+
punctuation = max(search.rfind(mark) for mark in (",", ";", ":"))
|
| 104 |
+
split_at = (
|
| 105 |
+
punctuation + 1
|
| 106 |
+
if punctuation >= limit // 2
|
| 107 |
+
else sentence.rfind(" ", 0, limit + 1)
|
| 108 |
+
)
|
| 109 |
+
if split_at < limit // 2:
|
| 110 |
+
split_at = limit
|
| 111 |
+
chunks.append(sentence[:split_at].strip())
|
| 112 |
+
sentence = sentence[split_at:].strip()
|
| 113 |
+
if sentence:
|
| 114 |
+
chunks.append(sentence)
|
| 115 |
+
return chunks
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def boundary_pause_seconds(chunk: str) -> float:
|
| 119 |
+
ending = chunk.rstrip()[-1:] if chunk.strip() else ""
|
| 120 |
+
return {
|
| 121 |
+
"?": 0.28,
|
| 122 |
+
"!": 0.24,
|
| 123 |
+
".": 0.22,
|
| 124 |
+
";": 0.16,
|
| 125 |
+
":": 0.13,
|
| 126 |
+
",": 0.09,
|
| 127 |
+
}.get(ending, 0.08)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
def edge_fade(waveform: np.ndarray, sample_rate: int, milliseconds: float = 5.0) -> np.ndarray:
|
| 131 |
+
frames = min(round(sample_rate * milliseconds / 1000.0), waveform.size // 2)
|
| 132 |
+
if frames <= 0:
|
| 133 |
+
return waveform
|
| 134 |
+
output = waveform.copy()
|
| 135 |
+
ramp = np.linspace(0.0, 1.0, frames, endpoint=True, dtype=np.float32)
|
| 136 |
+
output[:frames] *= ramp
|
| 137 |
+
output[-frames:] *= ramp[::-1]
|
| 138 |
return output
|
| 139 |
|
| 140 |
|
|
|
|
| 184 |
|
| 185 |
|
| 186 |
class Engine:
|
| 187 |
+
def __init__(self, spec: ModelSpec) -> None:
|
| 188 |
+
if not spec.config.is_file():
|
| 189 |
+
raise FileNotFoundError(f"Missing release configuration for {spec.label}")
|
| 190 |
+
if not spec.checkpoint.is_file():
|
| 191 |
+
raise FileNotFoundError(f"Missing release weights for {spec.label}")
|
| 192 |
+
self.spec = spec
|
| 193 |
+
self.hps = utils.get_hparams_from_file(str(spec.config))
|
| 194 |
+
self.model = SynthesizerTrn(
|
| 195 |
+
len(symbols),
|
| 196 |
+
self.hps.data.filter_length // 2 + 1,
|
| 197 |
+
self.hps.train.segment_size // self.hps.data.hop_length,
|
| 198 |
+
**self.hps.model,
|
| 199 |
+
).eval()
|
| 200 |
+
root_logger = logging.getLogger()
|
| 201 |
+
previous_level = root_logger.level
|
| 202 |
+
try:
|
| 203 |
+
root_logger.setLevel(logging.WARNING)
|
| 204 |
+
utils.load_checkpoint(str(spec.checkpoint), self.model, None)
|
| 205 |
+
finally:
|
| 206 |
+
root_logger.setLevel(previous_level)
|
| 207 |
+
self.device = torch.device("cpu")
|
| 208 |
+
self.sample_rate = int(self.hps.data.sampling_rate)
|
| 209 |
+
self.lock = threading.Lock()
|
| 210 |
+
|
| 211 |
+
def tokens(self, text: str) -> tuple[torch.Tensor, torch.Tensor]:
|
| 212 |
+
phonemes = run_vits_frontend(text).phoneme_text
|
| 213 |
+
sequence = cleaned_text_to_sequence(phonemes)
|
| 214 |
+
if self.hps.data.add_blank:
|
| 215 |
+
sequence = commons.intersperse(sequence, 0)
|
| 216 |
+
if not sequence:
|
| 217 |
+
raise ValueError("The phoneme frontend produced no speakable tokens.")
|
| 218 |
+
tokens = torch.LongTensor(sequence).to(self.device).unsqueeze(0)
|
| 219 |
+
lengths = torch.LongTensor([tokens.size(1)]).to(self.device)
|
| 220 |
+
return tokens, lengths
|
| 221 |
+
|
| 222 |
+
@torch.inference_mode()
|
| 223 |
+
def synthesize(
|
| 224 |
+
self,
|
| 225 |
+
text: str,
|
| 226 |
+
speed: float,
|
| 227 |
+
variation: float,
|
| 228 |
+
pitch_steps: float,
|
| 229 |
+
seed: int,
|
| 230 |
+
) -> tuple[int, np.ndarray]:
|
| 231 |
+
chunks = split_text(text)
|
| 232 |
+
waveforms: list[np.ndarray] = []
|
| 233 |
+
with self.lock:
|
| 234 |
+
self.device = torch.device("cuda")
|
| 235 |
+
self.model.to(self.device)
|
| 236 |
+
try:
|
| 237 |
+
for index, chunk in enumerate(chunks):
|
| 238 |
+
if index:
|
| 239 |
+
waveforms.append(
|
| 240 |
+
np.zeros(
|
| 241 |
+
round(self.sample_rate * boundary_pause_seconds(chunks[index - 1])),
|
| 242 |
+
dtype=np.float32,
|
| 243 |
+
)
|
| 244 |
+
)
|
| 245 |
+
tokens, lengths = self.tokens(chunk)
|
| 246 |
+
torch.manual_seed(seed + index)
|
| 247 |
+
torch.cuda.manual_seed_all(seed + index)
|
| 248 |
+
waveform = self.model.infer(
|
| 249 |
+
tokens,
|
| 250 |
+
lengths,
|
| 251 |
+
noise_scale=variation,
|
| 252 |
+
noise_scale_w=0.8,
|
| 253 |
+
length_scale=1.0 / speed,
|
| 254 |
+
max_len=4000,
|
| 255 |
+
)[0][0, 0].float().cpu().numpy()
|
| 256 |
+
waveforms.append(edge_fade(waveform, self.sample_rate))
|
| 257 |
+
finally:
|
| 258 |
+
self.model.to("cpu")
|
| 259 |
+
self.device = torch.device("cpu")
|
| 260 |
+
torch.cuda.empty_cache()
|
| 261 |
audio = np.concatenate(waveforms)
|
| 262 |
if abs(pitch_steps) >= 0.01:
|
| 263 |
audio = pitch_shift_speech(audio, pitch_steps)
|
| 264 |
return self.sample_rate, pcm16(audio)
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
ENGINES = {label: Engine(spec) for label, spec in SPECS.items()}
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def validate(
|
| 271 |
+
text: str,
|
| 272 |
+
speed: float,
|
| 273 |
+
variation: float,
|
| 274 |
+
pitch_steps: float,
|
| 275 |
+
seed: int,
|
| 276 |
+
) -> tuple[str, float, float, float, int]:
|
| 277 |
+
text = " ".join((text or "").split())
|
| 278 |
+
if not text:
|
| 279 |
+
raise gr.Error("Enter something for Inflect to say.")
|
| 280 |
+
return text, float(speed), float(variation), float(pitch_steps), int(seed)
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
def render_wav(sample_rate: int, samples: np.ndarray) -> FileData:
|
| 284 |
+
"""Persist PCM16 samples to a WAV and return a Gradio FileData handle.
|
| 285 |
+
|
| 286 |
+
The return-type annotation is what tells gradio.Server which output
|
| 287 |
+
component to serialize, so the JS client receives a {url, ...} blob
|
| 288 |
+
rather than the function's return value being dropped.
|
| 289 |
+
"""
|
| 290 |
+
handle, out_path = tempfile.mkstemp(suffix=".wav", prefix="inflect_")
|
| 291 |
+
os.close(handle)
|
| 292 |
+
sf.write(out_path, samples, sample_rate, subtype="PCM_16")
|
| 293 |
+
return FileData(path=out_path)
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
@app.api(concurrency_limit=1)
|
| 297 |
+
@spaces.GPU(duration=120)
|
| 298 |
+
def synthesize(
|
| 299 |
+
text: str,
|
| 300 |
+
model_name: str,
|
| 301 |
+
speed: float,
|
| 302 |
+
variation: float,
|
| 303 |
+
pitch_steps: float,
|
| 304 |
+
seed: int,
|
| 305 |
+
) -> tuple[FileData, str]:
|
| 306 |
+
text, speed, variation, pitch_steps, seed = validate(
|
| 307 |
+
text, speed, variation, pitch_steps, seed
|
| 308 |
+
)
|
| 309 |
+
engine = ENGINES[model_name]
|
| 310 |
+
started = time.perf_counter()
|
| 311 |
+
sample_rate, samples = engine.synthesize(text, speed, variation, pitch_steps, seed)
|
| 312 |
+
seconds = len(samples) / sample_rate
|
| 313 |
+
wall = time.perf_counter() - started
|
| 314 |
+
rtf = wall / seconds
|
| 315 |
+
chunks = len(split_text(text))
|
| 316 |
+
status = (
|
| 317 |
+
f"**{model_name}** · fixed English voice \n"
|
| 318 |
+
f"`{engine.spec.params}` parameters · `{seconds:.2f}s` audio · "
|
| 319 |
+
f"`{wall:.2f}s` generation · `RTF {rtf:.3f}` · "
|
| 320 |
+
f"`{len(text)}` characters · `{chunks}` chunk{'s' if chunks != 1 else ''} · "
|
| 321 |
+
f"pitch `{pitch_steps:+.2f} st` · "
|
| 322 |
+
f"seed `{seed}` · weights `{engine.spec.revision}`"
|
| 323 |
+
)
|
| 324 |
+
return render_wav(sample_rate, samples), status
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
@app.api(concurrency_limit=1)
|
| 328 |
+
@spaces.GPU(duration=120)
|
| 329 |
+
def compare(
|
| 330 |
+
text: str,
|
| 331 |
+
speed: float,
|
| 332 |
+
variation: float,
|
| 333 |
+
pitch_steps: float,
|
| 334 |
+
seed: int,
|
| 335 |
+
) -> tuple[FileData, FileData, str]:
|
| 336 |
+
text, speed, variation, pitch_steps, seed = validate(
|
| 337 |
+
text, speed, variation, pitch_steps, seed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
)
|
| 339 |
+
started = time.perf_counter()
|
| 340 |
+
micro_sr, micro_pcm = ENGINES["Inflect Micro v2"].synthesize(
|
| 341 |
+
text, speed, variation, pitch_steps, seed
|
| 342 |
+
)
|
| 343 |
+
micro_wall = time.perf_counter() - started
|
| 344 |
+
started = time.perf_counter()
|
| 345 |
+
nano_sr, nano_pcm = ENGINES["Inflect Nano v2"].synthesize(
|
| 346 |
+
text, speed, variation, pitch_steps, seed
|
| 347 |
+
)
|
| 348 |
+
nano_wall = time.perf_counter() - started
|
| 349 |
+
micro_seconds = len(micro_pcm) / micro_sr
|
| 350 |
+
nano_seconds = len(nano_pcm) / nano_sr
|
| 351 |
+
status = (
|
| 352 |
+
f"Same text and seed `{seed}` · "
|
| 353 |
+
f"Micro `{micro_wall:.2f}s / {micro_seconds:.2f}s audio` · "
|
| 354 |
+
f"Nano `{nano_wall:.2f}s / {nano_seconds:.2f}s audio`"
|
| 355 |
+
)
|
| 356 |
+
return render_wav(micro_sr, micro_pcm), render_wav(nano_sr, nano_pcm), status
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
@app.middleware("http")
|
| 360 |
+
async def _cross_origin_isolation(request, call_next):
|
| 361 |
+
"""Enable crossOriginIsolated so the browser ONNX runtime can use WebGPU.
|
| 362 |
+
|
| 363 |
+
COEP `credentialless` (rather than `require-corp`) keeps the Gradio JS
|
| 364 |
+
client module from the CDN loadable while still enabling shared memory.
|
| 365 |
+
"""
|
| 366 |
+
response = await call_next(request)
|
| 367 |
+
response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
|
| 368 |
+
response.headers["Cross-Origin-Embedder-Policy"] = "credentialless"
|
| 369 |
+
return response
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
@app.get(STATIC_URL + "/{path:path}")
|
| 373 |
+
async def serve_web_runtime(path: str):
|
| 374 |
+
full = (WEB_RUNTIME / path).resolve()
|
| 375 |
+
if not full.is_file():
|
| 376 |
+
return HTMLResponse("Not found", status_code=404)
|
| 377 |
+
return FileResponse(str(full))
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
@app.get("/")
|
| 381 |
+
async def homepage():
|
| 382 |
+
html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
|
| 383 |
+
with open(html_path, "r", encoding="utf-8") as f:
|
| 384 |
+
return HTMLResponse(f.read())
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
if __name__ == "__main__":
|
| 388 |
+
app.launch(show_error=True)
|
index.html
ADDED
|
@@ -0,0 +1,842 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Inflect v2 · studio</title>
|
| 7 |
+
<link rel="icon" href="/web_runtime/favicon.svg" type="image/svg+xml" />
|
| 8 |
+
<style>
|
| 9 |
+
:root{
|
| 10 |
+
color-scheme:dark;
|
| 11 |
+
--bg:#000000;--bg-2:#08090a;
|
| 12 |
+
--surface:#16181a;--surface-2:#1c1e20;--inset:#101213;
|
| 13 |
+
--well:#0c0d0e;
|
| 14 |
+
--ink:#f5f5f7;--muted:#98989d;--dim:#6c6c70;
|
| 15 |
+
--line:#2a2c2e;--line-soft:#232527;--line-faint:#1d1f21;
|
| 16 |
+
--accent:#0a84ff;--accent-press:#3a9bff;--accent-glow:rgba(10,132,255,.4);
|
| 17 |
+
--teal:#30d4c2;--amber:#ff9f0a;--green:#30d158;--warn:#ff453a;
|
| 18 |
+
--shadow:0 1px 3px rgba(0,0,0,.4),0 8px 28px rgba(0,0,0,.5);
|
| 19 |
+
--shadow-lg:0 2px 8px rgba(0,0,0,.45),0 24px 60px rgba(0,0,0,.6);
|
| 20 |
+
--ring:0 0 0 4px rgba(10,132,255,.22);
|
| 21 |
+
}
|
| 22 |
+
*{box-sizing:border-box}
|
| 23 |
+
html,body{margin:0;padding:0}
|
| 24 |
+
body{
|
| 25 |
+
background:
|
| 26 |
+
radial-gradient(90% 60% at 50% -8%,rgba(10,132,255,.08),transparent 60%),
|
| 27 |
+
var(--bg);
|
| 28 |
+
color:var(--ink);
|
| 29 |
+
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display",Inter,ui-sans-serif,"Segoe UI",sans-serif;
|
| 30 |
+
line-height:1.5;-webkit-font-smoothing:antialiased;letter-spacing:-.003em;
|
| 31 |
+
min-height:100vh;display:flex;flex-direction:column;
|
| 32 |
+
}
|
| 33 |
+
.mono{font-family:ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,monospace}
|
| 34 |
+
|
| 35 |
+
/* ---- top bar ---- */
|
| 36 |
+
.topbar{
|
| 37 |
+
display:flex;align-items:center;gap:16px;padding:13px clamp(16px,4vw,40px);
|
| 38 |
+
background:rgba(0,0,0,.62);backdrop-filter:saturate(180%) blur(20px);
|
| 39 |
+
border-bottom:1px solid var(--line-soft);position:sticky;top:0;z-index:20;
|
| 40 |
+
}
|
| 41 |
+
.brand{display:flex;align-items:center;gap:11px}
|
| 42 |
+
.logo{width:32px;height:32px;border-radius:9px;flex-shrink:0;background:linear-gradient(140deg,#2bd4c4,#0a7a6e);display:flex;align-items:center;justify-content:center;box-shadow:0 2px 10px rgba(43,212,196,.35)}
|
| 43 |
+
.logo svg{width:17px;height:17px}
|
| 44 |
+
.brand .name{font-weight:600;font-size:15px;letter-spacing:-.01em;line-height:1.05}
|
| 45 |
+
.brand .name small{display:block;font-weight:400;font-size:11px;color:var(--muted);margin-top:1px}
|
| 46 |
+
.spacer{flex:1}
|
| 47 |
+
.meters{display:flex;align-items:center;gap:16px}
|
| 48 |
+
.meter{display:flex;flex-direction:column;gap:2px;align-items:flex-end}
|
| 49 |
+
.meter .lab{font:600 9px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--dim)}
|
| 50 |
+
.meter .val{font:600 12px/1 var(--mono);color:var(--ink);font-variant-numeric:tabular-nums}
|
| 51 |
+
.meter .val.accent{color:var(--teal)}
|
| 52 |
+
.led{width:9px;height:9px;border-radius:50%;background:var(--dim);transition:background .2s,box-shadow .2s}
|
| 53 |
+
.led.on{background:var(--teal);box-shadow:0 0 10px rgba(48,212,194,.8)}
|
| 54 |
+
.led.busy{background:var(--accent);box-shadow:0 0 10px var(--accent-glow);animation:pulse 1s infinite}
|
| 55 |
+
@keyframes pulse{50%{opacity:.4}}
|
| 56 |
+
.segctrl{display:inline-flex;background:rgba(120,120,128,.18);border-radius:10px;padding:2px;gap:2px}
|
| 57 |
+
.segctrl button{appearance:none;border:0;background:transparent;color:var(--muted);cursor:pointer;font:inherit;font-size:13px;font-weight:550;padding:6px 16px;border-radius:8px;transition:color .18s,background .18s,box-shadow .18s}
|
| 58 |
+
.segctrl button.active{background:var(--surface-2);color:var(--ink);box-shadow:0 1px 3px rgba(0,0,0,.4),0 0 0 1px var(--line)}
|
| 59 |
+
.link{color:var(--muted);font-size:13px;font-weight:450;text-decoration:none;padding:7px 9px;border-radius:8px;transition:color .15s,background .15s}
|
| 60 |
+
.link:hover{color:var(--ink);background:rgba(255,255,255,.06)}
|
| 61 |
+
|
| 62 |
+
/* ---- shell ---- */
|
| 63 |
+
.shell{flex:1;display:flex;flex-direction:column;min-height:0}
|
| 64 |
+
.pane{display:none;flex:1;min-height:0;flex-direction:column}
|
| 65 |
+
.pane.active{display:flex}
|
| 66 |
+
.stage-scroll{flex:1;overflow-y:auto}
|
| 67 |
+
.page{max-width:820px;margin:0 auto;padding:28px clamp(16px,4vw,40px) 80px;width:100%}
|
| 68 |
+
|
| 69 |
+
/* ---- cards ---- */
|
| 70 |
+
.card{background:var(--surface);border:1px solid var(--line-soft);border-radius:18px;box-shadow:var(--shadow);overflow:hidden}
|
| 71 |
+
.card+.card{margin-top:16px}
|
| 72 |
+
.card-pad{padding:20px 22px}
|
| 73 |
+
.card-head{display:flex;align-items:center;gap:11px;padding:15px 20px;border-bottom:1px solid var(--line-soft);background:linear-gradient(180deg,rgba(255,255,255,.02),transparent)}
|
| 74 |
+
.card-head .tag{font:600 10px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}
|
| 75 |
+
.card-head .ic{width:26px;height:26px;border-radius:7px;background:var(--inset);border:1px solid var(--line);display:flex;align-items:center;justify-content:center;color:var(--teal)}
|
| 76 |
+
.card-head .grow{flex:1}
|
| 77 |
+
|
| 78 |
+
.label{font-size:13px;font-weight:600;color:var(--ink);margin-bottom:9px;display:block;letter-spacing:-.01em}
|
| 79 |
+
textarea,input[type="text"],input[type="number"]{
|
| 80 |
+
width:100%;background:var(--well);color:var(--ink);border:1px solid var(--line);border-radius:12px;
|
| 81 |
+
padding:14px 15px;font-size:16px;font-family:inherit;outline:none;transition:border-color .15s,box-shadow .15s;line-height:1.5;
|
| 82 |
+
}
|
| 83 |
+
textarea::placeholder,input::placeholder{color:var(--dim)}
|
| 84 |
+
textarea:focus,input:focus{border-color:var(--accent);box-shadow:var(--ring)}
|
| 85 |
+
textarea{min-height:108px;resize:vertical}
|
| 86 |
+
.examples{display:flex;flex-wrap:wrap;gap:8px;margin-top:11px}
|
| 87 |
+
.examples button{background:var(--inset);border:1px solid var(--line-soft);border-radius:100px;color:var(--muted);font-size:12px;padding:6px 13px;cursor:pointer;transition:all .15s;font-family:inherit}
|
| 88 |
+
.examples button:hover{color:var(--teal);border-color:var(--teal);background:rgba(48,212,194,.08)}
|
| 89 |
+
|
| 90 |
+
/* segmented (model) */
|
| 91 |
+
.segmented{display:flex;background:var(--inset);border:1px solid var(--line-soft);border-radius:12px;padding:3px;gap:3px}
|
| 92 |
+
.segmented label{flex:1;position:relative}
|
| 93 |
+
.segmented input{position:absolute;opacity:0;width:0;height:0}
|
| 94 |
+
.segmented label span{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:10px 10px;border-radius:9px;cursor:pointer;transition:background .15s,box-shadow .15s;text-align:center}
|
| 95 |
+
.segmented label strong{font-size:13px;font-weight:550;color:var(--muted);transition:color .15s}
|
| 96 |
+
.segmented label small{font-size:11px;color:var(--dim);margin-top:2px;font-weight:400;transition:color .15s}
|
| 97 |
+
.segmented label input:checked+span{background:var(--surface-2);box-shadow:0 0 0 1px var(--line),0 1px 3px rgba(0,0,0,.4)}
|
| 98 |
+
.segmented label input:checked+span strong{color:var(--ink)}
|
| 99 |
+
.segmented label input:checked+span small{color:var(--muted)}
|
| 100 |
+
|
| 101 |
+
/* grouped rows */
|
| 102 |
+
.group{background:var(--inset);border:1px solid var(--line-soft);border-radius:12px;overflow:hidden}
|
| 103 |
+
.row{padding:14px 16px;display:flex;flex-direction:column;gap:10px}
|
| 104 |
+
.row+.row{border-top:1px solid var(--line-faint)}
|
| 105 |
+
.row .top{display:flex;justify-content:space-between;align-items:center}
|
| 106 |
+
.row .top label{font-size:15px;font-weight:500;color:var(--ink);margin:0}
|
| 107 |
+
.row .top .val{font-size:14px;color:var(--muted);font-variant-numeric:tabular-nums;font-weight:500}
|
| 108 |
+
input[type="range"]{-webkit-appearance:none;appearance:none;width:100%;height:4px;background:var(--line);border-radius:4px;outline:none}
|
| 109 |
+
input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;width:26px;height:26px;border-radius:50%;background:var(--surface-2);cursor:pointer;border:0;box-shadow:0 0 0 1px var(--line),0 2px 8px rgba(0,0,0,.5);transition:transform .1s}
|
| 110 |
+
input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.08)}
|
| 111 |
+
input[type="range"]::-webkit-slider-thumb:active{transform:scale(1.12);box-shadow:0 0 0 1px var(--accent),var(--ring)}
|
| 112 |
+
input[type="range"]::-moz-range-thumb{width:22px;height:22px;border:0;border-radius:50%;background:var(--surface-2);cursor:pointer;box-shadow:0 0 0 1px var(--line),0 2px 8px rgba(0,0,0,.5)}
|
| 113 |
+
.row small{font-size:12px;color:var(--muted);margin-top:-2px;line-height:1.4}
|
| 114 |
+
|
| 115 |
+
/* advanced disclosure */
|
| 116 |
+
details.adv{margin-top:16px;border:1px solid var(--line-soft);border-radius:12px;background:var(--surface-2);overflow:hidden}
|
| 117 |
+
details.adv summary{list-style:none;padding:14px 16px;cursor:pointer;display:flex;align-items:center;gap:10px;font-size:15px;font-weight:500;color:var(--ink)}
|
| 118 |
+
details.adv summary::-webkit-details-marker{display:none}
|
| 119 |
+
details.adv summary .chev{margin-left:auto;color:var(--muted);transition:transform .22s ease;font-size:13px}
|
| 120 |
+
details.adv[open] summary .chev{transform:rotate(90deg)}
|
| 121 |
+
details.adv summary .dot-i{width:7px;height:7px;border-radius:50%;background:var(--dim)}
|
| 122 |
+
details.adv .body{padding:0 16px 16px;display:grid;grid-template-columns:1fr;gap:0}
|
| 123 |
+
details.adv .body .group{border:1px solid var(--line-soft)}
|
| 124 |
+
|
| 125 |
+
/* actions */
|
| 126 |
+
.actions{display:flex;gap:12px;margin-top:18px;align-items:stretch}
|
| 127 |
+
.btn{display:flex;align-items:center;justify-content:center;gap:9px;border-radius:14px;cursor:pointer;font:inherit;font-size:16px;font-weight:560;border:0;transition:background .15s,transform .05s,box-shadow .15s}
|
| 128 |
+
.btn:active{transform:scale(.985)}
|
| 129 |
+
.btn:disabled{opacity:.4;cursor:not-allowed}
|
| 130 |
+
.btn.primary{flex:1;min-height:52px;background:var(--accent);color:#fff;box-shadow:0 1px 2px rgba(10,132,255,.4),0 6px 20px rgba(10,132,255,.3)}
|
| 131 |
+
.btn.primary:hover{background:var(--accent-press)}
|
| 132 |
+
.btn.secondary{background:var(--surface-2);color:var(--ink);min-height:52px;padding:0 20px;border:1px solid var(--line)}
|
| 133 |
+
.btn.secondary:hover{background:#26292b}
|
| 134 |
+
.kbd{font-size:11px;font-weight:500;opacity:.75;padding:2px 6px;border-radius:5px;background:rgba(255,255,255,.16)}
|
| 135 |
+
|
| 136 |
+
/* status */
|
| 137 |
+
.status{margin-top:14px;font-size:13px;color:var(--muted);line-height:1.55;min-height:1.2em;display:flex;align-items:center;gap:9px}
|
| 138 |
+
.status .dot{width:8px;height:8px;border-radius:50%;background:var(--dim);flex-shrink:0;transition:background .2s,box-shadow .2s}
|
| 139 |
+
.status.ok .dot{background:var(--green);box-shadow:0 0 0 3px rgba(48,209,88,.2)}
|
| 140 |
+
.status.busy .dot{background:var(--accent);animation:pulse 1s infinite}
|
| 141 |
+
.status.err .dot{background:var(--warn);box-shadow:0 0 0 3px rgba(255,69,58,.2)}
|
| 142 |
+
.status .txt{min-width:0}
|
| 143 |
+
.status .txt b{color:var(--ink);font-weight:600}
|
| 144 |
+
.status .txt code{font-family:var(--mono);font-size:12px;background:var(--inset);padding:1px 5px;border-radius:5px;color:var(--muted)}
|
| 145 |
+
.queue{font-size:12px;color:var(--accent);white-space:nowrap}
|
| 146 |
+
.busy-spin{display:inline-block;width:13px;height:13px;border:2.5px solid var(--accent);border-top-color:transparent;border-radius:50%;animation:spin .7s linear infinite}
|
| 147 |
+
@keyframes spin{to{transform:rotate(360deg)}}
|
| 148 |
+
|
| 149 |
+
/* ---- player ---- */
|
| 150 |
+
.player{display:none}
|
| 151 |
+
.player.show{display:block;animation:rise .42s cubic-bezier(.22,1,.36,1)}
|
| 152 |
+
@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
| 153 |
+
.np{display:flex;align-items:center;gap:13px;margin-bottom:16px}
|
| 154 |
+
.np-badge{width:44px;height:44px;border-radius:11px;flex-shrink:0;background:linear-gradient(140deg,#34d8c8,#0a7a6e);display:flex;align-items:center;justify-content:center;color:#fff;box-shadow:0 3px 12px rgba(14,143,130,.45);transition:box-shadow .25s}
|
| 155 |
+
.np-badge.playing{box-shadow:0 3px 12px rgba(14,143,130,.6),0 0 0 5px rgba(48,212,196,.18)}
|
| 156 |
+
.np-text{min-width:0}
|
| 157 |
+
.np-text .title{font-size:16px;font-weight:600;letter-spacing:-.01em}
|
| 158 |
+
.np-text .sub{font-size:12.5px;color:var(--muted);font-variant-numeric:tabular-nums}
|
| 159 |
+
|
| 160 |
+
.scrub-row{display:flex;align-items:center;gap:13px;margin-bottom:12px}
|
| 161 |
+
.tc{font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums;min-width:38px;text-align:center;font-weight:500}
|
| 162 |
+
.tc.now{color:var(--accent)}
|
| 163 |
+
.wave{position:relative;flex:1;min-width:0;height:72px;background:var(--well);border:1px solid var(--line-faint);border-radius:10px;overflow:hidden;cursor:pointer}
|
| 164 |
+
.wave canvas{position:absolute;inset:0;width:100%;height:100%;display:block}
|
| 165 |
+
.wave .ph{position:absolute;top:0;bottom:0;width:2px;background:var(--accent);box-shadow:0 0 10px var(--accent-glow);pointer-events:none;transform:translateX(-1px);border-radius:2px}
|
| 166 |
+
.wave .center{position:absolute;left:0;right:0;top:50%;height:1px;background:var(--line-faint);pointer-events:none}
|
| 167 |
+
.seek{width:100%}
|
| 168 |
+
|
| 169 |
+
.tctrl{display:flex;align-items:center;gap:11px;margin-bottom:6px}
|
| 170 |
+
.tbtn{width:30px;height:30px;border-radius:50%;border:0;background:transparent;color:var(--muted);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:color .15s,background .15s;font-size:14px}
|
| 171 |
+
.tbtn:hover{color:var(--ink);background:var(--inset)}
|
| 172 |
+
.tbtn:disabled{opacity:.35;cursor:not-allowed}
|
| 173 |
+
.tbtn.play{width:50px;height:50px;background:var(--accent);color:#fff;font-size:17px;box-shadow:0 2px 10px var(--accent-glow)}
|
| 174 |
+
.tbtn.play:hover{background:var(--accent-press)}
|
| 175 |
+
.tbtn.play:disabled{background:var(--line);color:var(--dim);box-shadow:none}
|
| 176 |
+
|
| 177 |
+
/* meters */
|
| 178 |
+
.meterbars{display:flex;gap:4px;align-items:flex-end;height:30px;margin-top:6px;padding:0 2px}
|
| 179 |
+
.meterbars .bar{flex:1;background:var(--line);border-radius:2px;min-height:3px;transition:height .06s linear,background .1s}
|
| 180 |
+
.meterbars .bar.lev{background:linear-gradient(180deg,var(--teal),#0a7a6e)}
|
| 181 |
+
.meterbars .bar.peak{background:var(--amber)}
|
| 182 |
+
|
| 183 |
+
/* karaoke */
|
| 184 |
+
.script{margin-top:16px;padding:16px 4px 4px;font-size:19px;line-height:1.85;letter-spacing:.001em;color:var(--muted);max-width:680px}
|
| 185 |
+
.script .empty{font-size:14px;color:var(--dim)}
|
| 186 |
+
.karaoke .chunk{display:inline;color:#5c5d61;transition:color .25s ease,background-color .18s ease;border-radius:3px;padding:0 1px}
|
| 187 |
+
.karaoke .chunk.spoken{color:#8a8b90}
|
| 188 |
+
.karaoke .chunk.speaking{color:var(--ink)}
|
| 189 |
+
.karaoke .chunk.speaking::after{content:"";display:inline-block;width:3px;height:1.1em;background:var(--accent);margin-left:2px;vertical-align:-3px;border-radius:1px;animation:blink 1.05s steps(2) infinite}
|
| 190 |
+
@keyframes blink{50%{opacity:.25}}
|
| 191 |
+
|
| 192 |
+
/* tools */
|
| 193 |
+
.tools{display:flex;align-items:center;gap:14px;margin-top:14px;flex-wrap:wrap}
|
| 194 |
+
a.dl{display:inline-flex;align-items:center;gap:7px;padding:8px 14px;border-radius:100px;background:var(--surface-2);color:var(--ink);font-size:13px;font-weight:500;text-decoration:none;border:1px solid var(--line);transition:all .15s}
|
| 195 |
+
a.dl:hover{background:rgba(10,132,255,.12);color:var(--accent);border-color:var(--accent)}
|
| 196 |
+
.chip{font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums}
|
| 197 |
+
|
| 198 |
+
/* compare */
|
| 199 |
+
.cmp-wrap{display:none}
|
| 200 |
+
.cmp-wrap.show{display:block;animation:rise .42s cubic-bezier(.22,1,.36,1)}
|
| 201 |
+
.cmp-title{font-size:13px;font-weight:600;color:var(--muted);margin:30px 0 12px;display:flex;align-items:center;gap:9px}
|
| 202 |
+
.cmp-title .line{flex:1;height:1px;background:var(--line-soft)}
|
| 203 |
+
.compare-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
|
| 204 |
+
.track{background:var(--surface);border:1px solid var(--line-soft);border-radius:16px;box-shadow:var(--shadow);overflow:hidden}
|
| 205 |
+
.track .th{padding:14px 16px 0;display:flex;align-items:center;gap:9px}
|
| 206 |
+
.track .th .dot{width:9px;height:9px;border-radius:50%}
|
| 207 |
+
.track .th .nm{font-size:14px;font-weight:600}
|
| 208 |
+
.track .th .nm small{display:block;font-size:11px;color:var(--muted);font-weight:400;margin-top:1px}
|
| 209 |
+
.track .tb{padding:10px 16px 16px}
|
| 210 |
+
.mini-wave{position:relative;height:50px;background:var(--well);border:1px solid var(--line-faint);border-radius:8px;overflow:hidden;cursor:pointer;margin-bottom:10px}
|
| 211 |
+
.mini-wave canvas{position:absolute;inset:0;width:100%;height:100%}
|
| 212 |
+
.mini-wave .ph{position:absolute;top:0;bottom:0;width:2px;background:var(--accent);pointer-events:none;transform:translateX(-1px)}
|
| 213 |
+
.mini-ctrl{display:flex;align-items:center;gap:10px}
|
| 214 |
+
.mini-play{width:34px;height:34px;border-radius:50%;border:0;background:var(--accent);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .15s,transform .1s;font-size:12px;box-shadow:0 2px 8px var(--accent-glow)}
|
| 215 |
+
.mini-play:hover{background:var(--accent-press)}
|
| 216 |
+
.mini-play:active{transform:scale(.94)}
|
| 217 |
+
.mini-play:disabled{background:var(--line);box-shadow:none;cursor:not-allowed}
|
| 218 |
+
.mini-tc{font-size:11.5px;color:var(--muted);font-variant-numeric:tabular-nums;white-space:nowrap}
|
| 219 |
+
.mini-tc .now{color:var(--accent)}
|
| 220 |
+
.mini-seek{flex:1;min-width:0}
|
| 221 |
+
.mini-dl{font-size:12px;padding:6px 11px;border-radius:100px}
|
| 222 |
+
|
| 223 |
+
/* browser pane */
|
| 224 |
+
.browser-pane{flex:1;display:flex;flex-direction:column;min-height:0}
|
| 225 |
+
.browser-head{padding:20px clamp(16px,4vw,40px) 0;max-width:820px;margin:0 auto;width:100%}
|
| 226 |
+
.browser-head h2{margin:0 0 5px;font-size:17px;font-weight:600}
|
| 227 |
+
.browser-head p{margin:0;color:var(--muted);font-size:13px;max-width:640px;line-height:1.5}
|
| 228 |
+
.browser-frame-shell{flex:1;min-height:0;margin:16px clamp(16px,4vw,40px);border:1px solid var(--line-soft);border-radius:18px;background:#000;overflow:hidden;box-shadow:var(--shadow-lg)}
|
| 229 |
+
.browser-frame{display:block;width:100%;height:580px;border:0;background:#000}
|
| 230 |
+
.browser-help{margin:0 clamp(16px,4vw,40px) 28px;color:var(--muted);font-size:12px;line-height:1.55;max-width:820px;margin-left:auto;margin-right:auto}
|
| 231 |
+
|
| 232 |
+
.fineprint{margin-top:30px;font-size:11.5px;color:var(--dim);line-height:1.6;text-align:center;max-width:560px;margin-left:auto;margin-right:auto}
|
| 233 |
+
.hidden{display:none!important}
|
| 234 |
+
|
| 235 |
+
@media(max-width:680px){
|
| 236 |
+
.page{padding:18px 14px 56px}
|
| 237 |
+
.meters{display:none}
|
| 238 |
+
.compare-grid{grid-template-columns:1fr}
|
| 239 |
+
details.adv .body{grid-template-columns:1fr}
|
| 240 |
+
.browser-frame{height:1000px}
|
| 241 |
+
.actions{flex-direction:column}
|
| 242 |
+
.btn.secondary{padding:0 20px;min-height:48px}
|
| 243 |
+
.card-pad{padding:18px 16px}
|
| 244 |
+
.script{font-size:17px;line-height:1.8}
|
| 245 |
+
.tctrl{flex-wrap:wrap}
|
| 246 |
+
a.dl{margin-left:0}
|
| 247 |
+
.np-badge{width:40px;height:40px}
|
| 248 |
+
}
|
| 249 |
+
@media(max-width:520px){
|
| 250 |
+
.topbar{padding:11px 14px;gap:10px}
|
| 251 |
+
.brand .name small{display:none}
|
| 252 |
+
.link:not(.always){display:none}
|
| 253 |
+
.segctrl button{padding:6px 12px;font-size:12px}
|
| 254 |
+
.scrub-row{flex-wrap:nowrap;gap:9px}
|
| 255 |
+
.wave{height:56px}
|
| 256 |
+
.tc{min-width:34px;font-size:11px}
|
| 257 |
+
.tbtn{width:28px;height:28px}
|
| 258 |
+
.tbtn.play{width:46px;height:46px}
|
| 259 |
+
.mini-seek{display:none}
|
| 260 |
+
.mini-tc{font-size:11px}
|
| 261 |
+
.meterbars{height:24px}
|
| 262 |
+
}
|
| 263 |
+
@media(max-width:380px){
|
| 264 |
+
.brand .name{font-size:14px}
|
| 265 |
+
.logo{width:28px;height:28px}
|
| 266 |
+
.segctrl button{padding:6px 10px}
|
| 267 |
+
}
|
| 268 |
+
</style>
|
| 269 |
+
</head>
|
| 270 |
+
<body>
|
| 271 |
+
|
| 272 |
+
<header class="topbar">
|
| 273 |
+
<div class="brand">
|
| 274 |
+
<div class="logo" aria-hidden="true">
|
| 275 |
+
<svg viewBox="0 0 24 24" fill="none"><path d="M9 18V6l10-2v12" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/><circle cx="7" cy="18" r="2.4" fill="#fff"/><circle cx="17" cy="16" r="2.4" fill="#fff"/></svg>
|
| 276 |
+
</div>
|
| 277 |
+
<div class="name">Inflect v2<small>speech studio</small></div>
|
| 278 |
+
</div>
|
| 279 |
+
<div class="spacer"></div>
|
| 280 |
+
<div class="meters">
|
| 281 |
+
<div class="meter"><span class="lab">Rate</span><span class="val" id="m-sr">24 kHz</span></div>
|
| 282 |
+
<div class="meter"><span class="lab">RTF</span><span class="val accent" id="m-rtf">—</span></div>
|
| 283 |
+
<div class="led" id="status-led" title="Idle"></div>
|
| 284 |
+
</div>
|
| 285 |
+
<div class="segctrl" role="tablist">
|
| 286 |
+
<button class="active" id="tab-hosted" role="tab">Hosted</button>
|
| 287 |
+
<button id="tab-browser" role="tab">Browser</button>
|
| 288 |
+
</div>
|
| 289 |
+
<a class="link always" href="https://github.com/owenawsong/Inflect" target="_blank" rel="noopener">GitHub</a>
|
| 290 |
+
</header>
|
| 291 |
+
|
| 292 |
+
<div class="shell">
|
| 293 |
+
<!-- HOSTED -->
|
| 294 |
+
<section id="pane-hosted" class="pane active">
|
| 295 |
+
<div class="stage-scroll">
|
| 296 |
+
<div class="page">
|
| 297 |
+
|
| 298 |
+
<div class="card">
|
| 299 |
+
<div class="card-head"><span class="ic"><svg width="15" height="15" viewBox="0 0 24 24" fill="none"><path d="M9 18V6l10-2v12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle cx="7" cy="18" r="2" fill="currentColor"/><circle cx="17" cy="16" r="2" fill="currentColor"/></svg></span><span class="tag">Script</span></div>
|
| 300 |
+
<div class="card-pad">
|
| 301 |
+
<label class="label" for="text">What should Inflect say?</label>
|
| 302 |
+
<textarea id="text" placeholder="Write a sentence, paragraph, or script…">Wait, are you actually being for real? I thought the train left twenty minutes ago!</textarea>
|
| 303 |
+
<div class="examples" id="examples"></div>
|
| 304 |
+
</div>
|
| 305 |
+
</div>
|
| 306 |
+
|
| 307 |
+
<div class="card">
|
| 308 |
+
<div class="card-head"><span class="ic"><svg width="15" height="15" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.8"/><path d="M7 10c2-1.5 8-1.5 10 0M8 13c1.6-1 6.4-1 8 0" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></span><span class="tag">Voice & Delivery</span></div>
|
| 309 |
+
<div class="card-pad">
|
| 310 |
+
<label class="label">Voice model</label>
|
| 311 |
+
<div class="segmented" id="model-seg">
|
| 312 |
+
<label><input type="radio" name="model" value="Inflect Micro v2" checked><span><strong>Micro v2</strong><small>Higher quality · 9.36M</small></span></label>
|
| 313 |
+
<label><input type="radio" name="model" value="Inflect Nano v2"><span><strong>Nano v2</strong><small>Lighter · 3.96M</small></span></label>
|
| 314 |
+
</div>
|
| 315 |
+
<details class="adv">
|
| 316 |
+
<summary><span class="dot-i"></span><span>Advanced</span><span class="chev">▸</span></summary>
|
| 317 |
+
<div class="body">
|
| 318 |
+
<div class="group">
|
| 319 |
+
<div class="row">
|
| 320 |
+
<div class="top"><label for="speed">Speaking speed</label><span class="val" id="speed-val">1.00×</span></div>
|
| 321 |
+
<input type="range" id="speed" min="0.7" max="1.35" step="0.01" value="1.0">
|
| 322 |
+
</div>
|
| 323 |
+
<div class="row">
|
| 324 |
+
<div class="top"><label for="variation">Delivery variation</label><span class="val" id="variation-val">Steady</span></div>
|
| 325 |
+
<input type="range" id="variation" min="0" max="1" step="0.001" value="0.667">
|
| 326 |
+
<small>Lower is steadier; higher gives each take more character.</small>
|
| 327 |
+
</div>
|
| 328 |
+
<div class="row">
|
| 329 |
+
<div class="top"><label for="pitch">Pitch shift</label><span class="val" id="pitch-val">0 st</span></div>
|
| 330 |
+
<input type="range" id="pitch" min="-2" max="2" step="0.25" value="0">
|
| 331 |
+
</div>
|
| 332 |
+
<div class="row">
|
| 333 |
+
<div class="top"><label for="seed">Repeatable seed</label><span class="val" id="seed-val">0</span></div>
|
| 334 |
+
<input type="number" id="seed" value="0" step="1" inputmode="numeric">
|
| 335 |
+
</div>
|
| 336 |
+
</div>
|
| 337 |
+
</div>
|
| 338 |
+
</details>
|
| 339 |
+
<div class="actions">
|
| 340 |
+
<button class="btn primary" id="generate">Generate<span class="kbd">⌘↵</span></button>
|
| 341 |
+
<button class="btn secondary" id="compare-btn">Compare models</button>
|
| 342 |
+
</div>
|
| 343 |
+
<div class="status" id="status"><span class="dot"></span><span class="txt" id="status-txt">Ready.</span><span class="queue hidden" id="queue"></span></div>
|
| 344 |
+
</div>
|
| 345 |
+
</div>
|
| 346 |
+
|
| 347 |
+
<!-- player -->
|
| 348 |
+
<div class="card player" id="player">
|
| 349 |
+
<div class="card-head"><span class="ic"><svg width="15" height="15" viewBox="0 0 24 24" fill="none"><path d="M4 12h3l3-7 4 14 3-7h3" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="tag">Playback</span><div class="grow"></div><span class="chip" id="take-spec">—</span></div>
|
| 350 |
+
<div class="card-pad">
|
| 351 |
+
<div class="np">
|
| 352 |
+
<div class="np-badge" id="np-badge"><svg width="19" height="19" viewBox="0 0 24 24" fill="none"><path d="M9 18V6l10-2v12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle cx="7" cy="18" r="2.2" fill="currentColor"/><circle cx="17" cy="16" r="2.2" fill="currentColor"/></svg></div>
|
| 353 |
+
<div class="np-text"><div class="title" id="np-title">Inflect Micro v2</div><div class="sub" id="np-sub">—</div></div>
|
| 354 |
+
</div>
|
| 355 |
+
<div class="scrub-row">
|
| 356 |
+
<span class="tc now" id="now">0:00</span>
|
| 357 |
+
<div class="wave" id="wave"><canvas id="wave-canvas"></canvas><div class="center"></div><div class="ph" id="playhead" style="left:0%"></div></div>
|
| 358 |
+
<span class="tc" id="dur">0:00</span>
|
| 359 |
+
</div>
|
| 360 |
+
<input type="range" class="seek" id="seek" min="0" max="1000" value="0" disabled aria-label="Seek">
|
| 361 |
+
<div class="tctrl">
|
| 362 |
+
<button class="tbtn play" id="play" title="Play" disabled>▶</button>
|
| 363 |
+
<button class="tbtn" id="restart" title="Restart" disabled>⏮</button>
|
| 364 |
+
<button class="tbtn" id="stop" title="Stop" disabled>⏹</button>
|
| 365 |
+
<div style="flex:1"></div>
|
| 366 |
+
<a class="dl" id="download" download="inflect_v2.wav">Download</a>
|
| 367 |
+
</div>
|
| 368 |
+
<div class="meterbars" id="meters" aria-hidden="true"></div>
|
| 369 |
+
<div class="script"><div class="karaoke fade" id="karaoke"><div class="empty">Your text will appear here, highlighting as it's spoken.</div></div></div>
|
| 370 |
+
</div>
|
| 371 |
+
</div>
|
| 372 |
+
|
| 373 |
+
<!-- compare (shown after Compare models is used) -->
|
| 374 |
+
<div class="cmp-wrap" id="cmp-wrap">
|
| 375 |
+
<div class="cmp-title"><span>Model comparison</span><span class="line"></span><span class="chip" id="cmp-chip">same text · same seed</span></div>
|
| 376 |
+
<div class="compare-grid">
|
| 377 |
+
<div class="track">
|
| 378 |
+
<div class="th"><span class="dot" style="background:#30d4c2"></span><span class="nm">Micro v2<small>9.36M · quality first</small></span><div style="flex:1"></div><a class="dl mini-dl" id="micro-dl" download="inflect_micro.wav">WAV</a></div>
|
| 379 |
+
<div class="tb">
|
| 380 |
+
<div class="mini-wave" id="micro-wave"><canvas id="micro-canvas"></canvas><div class="ph" id="micro-ph" style="left:0%"></div></div>
|
| 381 |
+
<div class="mini-ctrl">
|
| 382 |
+
<button class="mini-play" data-mini="micro" disabled>▶</button>
|
| 383 |
+
<span class="mini-tc"><span class="now" id="micro-now">0:00</span> / <span id="micro-dur">0:00</span></span>
|
| 384 |
+
<input type="range" id="micro-seek" class="mini-seek" min="0" max="1000" value="0" disabled aria-label="Micro seek">
|
| 385 |
+
</div>
|
| 386 |
+
</div>
|
| 387 |
+
</div>
|
| 388 |
+
<div class="track">
|
| 389 |
+
<div class="th"><span class="dot" style="background:#ff9f0a"></span><span class="nm">Nano v2<small>3.96M · footprint first</small></span><div style="flex:1"></div><a class="dl mini-dl" id="nano-dl" download="inflect_nano.wav">WAV</a></div>
|
| 390 |
+
<div class="tb">
|
| 391 |
+
<div class="mini-wave" id="nano-wave"><canvas id="nano-canvas"></canvas><div class="ph" id="nano-ph" style="left:0%"></div></div>
|
| 392 |
+
<div class="mini-ctrl">
|
| 393 |
+
<button class="mini-play" data-mini="nano" disabled>▶</button>
|
| 394 |
+
<span class="mini-tc"><span class="now" id="nano-now">0:00</span> / <span id="nano-dur">0:00</span></span>
|
| 395 |
+
<input type="range" id="nano-seek" class="mini-seek" min="0" max="1000" value="0" disabled aria-label="Nano seek">
|
| 396 |
+
</div>
|
| 397 |
+
</div>
|
| 398 |
+
</div>
|
| 399 |
+
</div>
|
| 400 |
+
</div>
|
| 401 |
+
|
| 402 |
+
<p class="fineprint">Fixed English voice · English-only · no voice cloning or reference audio. Uncommon names and long passages can still stumble.</p>
|
| 403 |
+
</div>
|
| 404 |
+
</div>
|
| 405 |
+
</section>
|
| 406 |
+
|
| 407 |
+
<!-- BROWSER -->
|
| 408 |
+
<section id="pane-browser" class="pane">
|
| 409 |
+
<div class="browser-pane">
|
| 410 |
+
<div class="browser-head">
|
| 411 |
+
<h2>Run it in your browser</h2>
|
| 412 |
+
<p>Your text and audio stay on this device. WebGPU is preferred; compatible browsers fall back to WASM. No queue, no server quota.</p>
|
| 413 |
+
</div>
|
| 414 |
+
<div class="browser-frame-shell">
|
| 415 |
+
<iframe class="browser-frame" id="webgpu-frame" src="/web_runtime/index.html?embed=1" title="Inflect browser-local runtime" allow="autoplay; webgpu" loading="eager"></iframe>
|
| 416 |
+
</div>
|
| 417 |
+
<p class="browser-help">The first run downloads the selected ONNX model and compiles it on your device. Streaming and complete-audio modes are both available inside this panel.</p>
|
| 418 |
+
</div>
|
| 419 |
+
</section>
|
| 420 |
+
</div>
|
| 421 |
+
|
| 422 |
+
<script type="module">
|
| 423 |
+
import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
|
| 424 |
+
|
| 425 |
+
const $ = (id) => document.getElementById(id);
|
| 426 |
+
const fmtTime = (s) => { if (!Number.isFinite(s) || s < 0) s = 0; const m = Math.floor(s/60); return `${m}:${Math.floor(s%60).toString().padStart(2,"0")}`; };
|
| 427 |
+
const variationLabel = (v) => v < 0.2 ? "Very steady" : v < 0.45 ? "Steady" : v < 0.75 ? "Natural" : v < 0.92 ? "Expressive" : "Very expressive";
|
| 428 |
+
|
| 429 |
+
/* prompts */
|
| 430 |
+
const PROMPTS = [
|
| 431 |
+
"Wait, are you actually being for real? I thought the train left twenty minutes ago!",
|
| 432 |
+
"Does punctuation work? Yes: commas, semicolons, and periods should create sensible boundaries.",
|
| 433 |
+
"Professor Nguyen mailed the fluorescent poster to Reykjavik on Thursday.",
|
| 434 |
+
"Although the weather changed without warning, the musicians finished packing before anyone opened the enormous glass door.",
|
| 435 |
+
];
|
| 436 |
+
const exEl = $("examples");
|
| 437 |
+
PROMPTS.forEach((p) => { const b=document.createElement("button"); b.type="button"; b.textContent=p.length>42?p.slice(0,40)+"…":p; b.title=p; b.addEventListener("click",()=>{ $("text").value=p; }); exEl.appendChild(b); });
|
| 438 |
+
|
| 439 |
+
/* tabs */
|
| 440 |
+
const setTab = (which) => { const b = which === "browser";
|
| 441 |
+
$("tab-hosted").classList.toggle("active",!b); $("tab-browser").classList.toggle("active",b);
|
| 442 |
+
$("pane-hosted").classList.toggle("active",!b); $("pane-browser").classList.toggle("active",b);
|
| 443 |
+
};
|
| 444 |
+
$("tab-hosted").addEventListener("click",()=>setTab("hosted"));
|
| 445 |
+
$("tab-browser").addEventListener("click",()=>setTab("browser"));
|
| 446 |
+
|
| 447 |
+
/* readouts */
|
| 448 |
+
$("speed").addEventListener("input",()=>{ $("speed-val").textContent=parseFloat($("speed").value).toFixed(2)+"×"; });
|
| 449 |
+
$("speed-val").textContent="1.00×";
|
| 450 |
+
$("variation").addEventListener("input",()=>{ $("variation-val").textContent=variationLabel(parseFloat($("variation").value)); });
|
| 451 |
+
$("variation-val").textContent=variationLabel(parseFloat($("variation").value));
|
| 452 |
+
$("pitch").addEventListener("input",()=>{ $("pitch-val").textContent=parseFloat($("pitch").value).toFixed(2)+" st"; });
|
| 453 |
+
$("pitch-val").textContent="0.00 st";
|
| 454 |
+
$("seed").addEventListener("input",(e)=>{ $("seed-val").textContent=e.target.value||"0"; });
|
| 455 |
+
|
| 456 |
+
/* ---- theme the embedded browser runtime ---- */
|
| 457 |
+
(function(){
|
| 458 |
+
const frame = $("webgpu-frame");
|
| 459 |
+
const THEME = `
|
| 460 |
+
:root{color-scheme:dark}
|
| 461 |
+
html,body{background:#07080a !important;color:#f5f5f7 !important}
|
| 462 |
+
body{background:#07080a !important;color:#f5f5f7 !important;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text",Inter,ui-sans-serif,sans-serif !important}
|
| 463 |
+
#root{background:#07080a !important;color:#f5f5f7 !important}
|
| 464 |
+
.surface-panel, .bg-white{background:#16181a !important;border-color:#2a2c2e !important}
|
| 465 |
+
.bg-slate-50, .bg-slate-50\\/80, .embedded-sidebar{background:#0e0f10 !important}
|
| 466 |
+
.bg-slate-100{background:#1c1e20 !important}
|
| 467 |
+
.border-slate-200, .border-slate-300{border-color:#2a2c2e !important}
|
| 468 |
+
.border-blue-600{border-color:#0a84ff !important}
|
| 469 |
+
.text-slate-950, .text-slate-900{color:#f5f5f7 !important}
|
| 470 |
+
.text-slate-700{color:#d1d1d6 !important}
|
| 471 |
+
.text-slate-600{color:#98989d !important}
|
| 472 |
+
.text-slate-500{color:#86868b !important}
|
| 473 |
+
.text-slate-400{color:#6c6c70 !important}
|
| 474 |
+
.hover\\:text-blue-600:hover{color:#4d9bff !important}
|
| 475 |
+
.text-blue-600{color:#4d9bff !important}
|
| 476 |
+
.speech-input{background:#0c0d0e !important;border-color:#2a2c2e !important;color:#f5f5f7 !important}
|
| 477 |
+
.speech-input:focus{border-color:#0a84ff !important;box-shadow:0 0 0 4px rgba(10,132,255,.22) !important}
|
| 478 |
+
.speech-input::placeholder{color:#6c6c70 !important}
|
| 479 |
+
textarea, input[type="text"]{background:#0c0d0e !important;color:#f5f5f7 !important;border-color:#2a2c2e !important}
|
| 480 |
+
.model-option-selected{background:rgba(10,132,255,.16) !important;border-color:#0a84ff !important;color:#f5f5f7 !important}
|
| 481 |
+
.segmented-selected{background:#0a84ff !important}
|
| 482 |
+
.h-full.bg-blue-500{background:#0a84ff !important}
|
| 483 |
+
input[type="range"]::-webkit-slider-thumb{background:#1c1e20 !important}
|
| 484 |
+
input[type="range"]{background:#2a2c2e !important}
|
| 485 |
+
:root{
|
| 486 |
+
--dt-bg:#0c0d0e; --dt-surface-selected:#1c2126; --dt-border:#2a2c2e;
|
| 487 |
+
--dt-text:#e5e5ea; --dt-text-muted:#86868b; --dt-accent:#0a84ff;
|
| 488 |
+
--dt-live:#30d158; --dt-paused:#ff9f0a; --dt-tree-hover:#1c1e20;
|
| 489 |
+
--dt-json-string:#30d158; --dt-json-number:#64d2ff; --dt-json-boolean:#ff9f0a;
|
| 490 |
+
--dt-json-null:#86868b; --dt-json-key:#64d2ff; --dt-json-tag:#5ac8fa;
|
| 491 |
+
--dt-json-preview:#86868b; --dt-json-arrow:#86868b; --dt-diff-changed:#64d2ff;
|
| 492 |
+
}
|
| 493 |
+
[style*="#f9fafb"]{background:#0c0d0e !important}
|
| 494 |
+
[style*="#ffffff"]{background:#16181a !important}
|
| 495 |
+
[style*="#f3f4f6"]{background:#0e0f10 !important;border-color:#2a2c2e !important}
|
| 496 |
+
[style*="#e5e7eb"]{border-color:#2a2c2e !important}
|
| 497 |
+
[style*="#111827"]{color:#f5f5f7 !important}
|
| 498 |
+
[style*="#6b7280"]{color:#98989d !important}
|
| 499 |
+
[style*="#dc2626"]{color:#ff453a !important}
|
| 500 |
+
::-webkit-scrollbar-thumb{background:#2a2c2e !important}
|
| 501 |
+
|
| 502 |
+
/* ADVANCED sections hidden by default; shown when body.studio-adv-open */
|
| 503 |
+
body:not(.studio-adv-open) .voice-controls{display:none !important}
|
| 504 |
+
body:not(.studio-adv-open) .playback-setting{display:none !important}
|
| 505 |
+
body:not(.studio-adv-open) .session-status{display:none !important}
|
| 506 |
+
body:not(.studio-adv-open) .embedded-logs{display:none !important}
|
| 507 |
+
/* Compute backend has no stable class — hide by JS tagging via .studio-adv-section */
|
| 508 |
+
.studio-adv-section{display:none !important}
|
| 509 |
+
body.studio-adv-open .studio-adv-section{display:flex !important}
|
| 510 |
+
|
| 511 |
+
/* compact model picker */
|
| 512 |
+
.model-picker{gap:8px !important}
|
| 513 |
+
.model-options-grid{grid-template-columns:1fr 1fr !important;gap:8px !important}
|
| 514 |
+
.model-option{padding:9px 11px !important;border-radius:10px !important}
|
| 515 |
+
.model-option .text-sm{font-size:13px !important}
|
| 516 |
+
.model-option .text-xs{font-size:11px !important}
|
| 517 |
+
.model-option>div:nth-child(3){display:none !important}
|
| 518 |
+
.model-option>div.mt-3{display:none !important}
|
| 519 |
+
.model-option .h-2.w-2{height:7px !important;width:7px !important}
|
| 520 |
+
|
| 521 |
+
/* kill empty space: stop compose/sidebar from stretching */
|
| 522 |
+
.embedded-compose, .embedded-sidebar{align-self:start !important;flex:0 0 auto !important}
|
| 523 |
+
.embedded-panel{align-items:start !important}
|
| 524 |
+
.embedded-grid{align-items:start !important;gap:0 !important}
|
| 525 |
+
.embedded-compose{gap:14px !important;padding:18px !important}
|
| 526 |
+
.embedded-sidebar{padding:18px !important;gap:14px !important}
|
| 527 |
+
/* hide empty bottom blocks */
|
| 528 |
+
.embedded-compose>div.border-t.border-slate-200.pt-6:empty,
|
| 529 |
+
.embedded-compose>div.border-t.border-slate-200.pt-6:not(:has(*)),
|
| 530 |
+
.embedded-compose>.flex.w-full:empty,
|
| 531 |
+
.embedded-compose>.flex.w-full:not(:has(*)){display:none !important}
|
| 532 |
+
.embedded-compose>.flex.w-full:not(:has(a:not(.hidden)):not(button)){display:none !important}
|
| 533 |
+
.embedded-compose>div:last-child:not(.flex):empty{display:none !important}
|
| 534 |
+
.section-label{font-size:11px !important;letter-spacing:.1em !important;text-transform:uppercase;color:#86868b !important}
|
| 535 |
+
.embedded-compose>div:first-child>h2,
|
| 536 |
+
.embedded-compose .text-lg{font-size:15px !important;margin-top:2px !important}
|
| 537 |
+
.embedded-compose .font-mono.text-\\[11px\\]{display:none !important}
|
| 538 |
+
.embedded-compose .flex.items-end.justify-between{margin-bottom:2px !important}
|
| 539 |
+
|
| 540 |
+
/* Advanced toggle (injected) */
|
| 541 |
+
.studio-adv-toggle{
|
| 542 |
+
appearance:none;border:1px solid #2a2c2e;background:#1c1e20;color:#f5f5f7;
|
| 543 |
+
border-radius:12px;padding:13px 15px;width:100%;display:flex;align-items:center;gap:10px;
|
| 544 |
+
font:inherit;font-size:14px;font-weight:550;cursor:pointer;margin-top:14px;
|
| 545 |
+
}
|
| 546 |
+
.studio-adv-toggle:hover{background:#26292b;border-color:#3a3d40}
|
| 547 |
+
.studio-adv-toggle .dot{width:7px;height:7px;border-radius:50%;background:#6c6c70;flex-shrink:0}
|
| 548 |
+
.studio-adv-toggle .chev{margin-left:auto;color:#98989d;transition:transform .22s ease}
|
| 549 |
+
body.studio-adv-open .studio-adv-toggle .chev{transform:rotate(90deg)}
|
| 550 |
+
|
| 551 |
+
@media(max-width:680px){
|
| 552 |
+
.dt-inspector-pane, .dt-panel, .dt-panel-wide{display:none !important}
|
| 553 |
+
.p-5{padding:1rem !important} .p-4{padding:0.85rem !important}
|
| 554 |
+
.speech-input{min-height:140px !important;font-size:16px !important}
|
| 555 |
+
.embedded-compose{padding:14px !important;gap:12px !important}
|
| 556 |
+
.embedded-sidebar{padding:14px !important;gap:12px !important}
|
| 557 |
+
.studio-adv-toggle{margin-top:12px;padding:12px 13px;font-size:13px}
|
| 558 |
+
}
|
| 559 |
+
`;
|
| 560 |
+
const tag = () => {
|
| 561 |
+
let doc;
|
| 562 |
+
try { doc = frame.contentDocument; } catch(_) { return false; }
|
| 563 |
+
if (!doc || !doc.getElementById("root")) return false;
|
| 564 |
+
if (doc.getElementById("inflect-studio-theme")) return true;
|
| 565 |
+
const s = doc.createElement("style");
|
| 566 |
+
s.id = "inflect-studio-theme";
|
| 567 |
+
s.textContent = THEME;
|
| 568 |
+
(doc.head || doc.documentElement).appendChild(s);
|
| 569 |
+
if (doc.documentElement){ doc.documentElement.style.background="#07080a"; doc.documentElement.style.color="#f5f5f7"; }
|
| 570 |
+
if (doc.body){ doc.body.style.background="#07080a"; doc.body.style.color="#f5f5f7"; }
|
| 571 |
+
return true;
|
| 572 |
+
};
|
| 573 |
+
const tagAdv = () => {
|
| 574 |
+
let doc;
|
| 575 |
+
try { doc = frame.contentDocument; } catch(_) { return; }
|
| 576 |
+
if (!doc) return;
|
| 577 |
+
// tag advanced sections by their label text (Compute backend, Voice controls, Runtime, etc.)
|
| 578 |
+
doc.querySelectorAll(".section-label").forEach((lab) => {
|
| 579 |
+
const t = (lab.textContent || "").trim();
|
| 580 |
+
if (["Compute backend","Runtime","Playback mode","Voice controls","Logs","Activity","Details"].includes(t)) {
|
| 581 |
+
let n = lab.parentElement;
|
| 582 |
+
if (n) {
|
| 583 |
+
n.classList.add("studio-adv-section");
|
| 584 |
+
if (n.parentElement && /flex|grid/.test(n.parentElement.className)) n.parentElement.classList.add("studio-adv-section");
|
| 585 |
+
}
|
| 586 |
+
}
|
| 587 |
+
});
|
| 588 |
+
// also tag the Auto/WebGPU/WASM grid (no label relationship) by being a sibling of Compute backend label
|
| 589 |
+
doc.querySelectorAll(".grid.grid-cols-3").forEach((g) => {
|
| 590 |
+
const prev = g.previousElementSibling;
|
| 591 |
+
if (prev && (prev.textContent || "").trim() === "Compute backend") {
|
| 592 |
+
g.classList.add("studio-adv-section");
|
| 593 |
+
const w = g.closest(".flex.flex-col"); if (w) w.classList.add("studio-adv-section");
|
| 594 |
+
}
|
| 595 |
+
});
|
| 596 |
+
// inject the Advanced toggle if not present
|
| 597 |
+
if (!doc.getElementById("studio-adv-toggle")) {
|
| 598 |
+
const sb = doc.querySelector(".embedded-sidebar") || doc.querySelector(".embedded-compose");
|
| 599 |
+
if (sb) {
|
| 600 |
+
const b = doc.createElement("button");
|
| 601 |
+
b.id = "studio-adv-toggle"; b.type = "button"; b.className = "studio-adv-toggle";
|
| 602 |
+
b.innerHTML = '<span class="dot"></span><span>Advanced</span><span class="chev">▸</span>';
|
| 603 |
+
b.addEventListener("click", () => {
|
| 604 |
+
const open = doc.body.classList.toggle("studio-adv-open");
|
| 605 |
+
b.setAttribute("aria-expanded", String(open));
|
| 606 |
+
try { frame.contentWindow.postMessage({type:"inflect-web-resize",height:Math.max(doc.body.scrollHeight,doc.documentElement.scrollHeight)}, "*"); } catch (_) {}
|
| 607 |
+
});
|
| 608 |
+
sb.appendChild(b);
|
| 609 |
+
}
|
| 610 |
+
}
|
| 611 |
+
};
|
| 612 |
+
const tick = () => {
|
| 613 |
+
if (!tag()) return; // iframe not ready yet
|
| 614 |
+
tagAdv();
|
| 615 |
+
// re-tag on DOM mutations (foldkit re-renders)
|
| 616 |
+
let doc;
|
| 617 |
+
try { doc = frame.contentDocument; } catch(_) { return; }
|
| 618 |
+
if (!doc._studioMO) {
|
| 619 |
+
try {
|
| 620 |
+
const mo = new (doc.defaultView.MutationObserver || MutationObserver)(() => tagAdv());
|
| 621 |
+
mo.observe(doc.getElementById("root"), { childList: true, subtree: true });
|
| 622 |
+
doc._studioMO = mo;
|
| 623 |
+
} catch (_) {}
|
| 624 |
+
}
|
| 625 |
+
};
|
| 626 |
+
frame.addEventListener("load", tick);
|
| 627 |
+
[60, 200, 600, 1500, 3000, 5000, 8000].forEach((d) => window.setTimeout(tick, d));
|
| 628 |
+
})();
|
| 629 |
+
|
| 630 |
+
/* responsive browser frame height */
|
| 631 |
+
(function(){ const frame=$("webgpu-frame"); const apply=(h)=>{ const n=Number(h); if(Number.isFinite(n)&&n>0){ const floor=frame.clientWidth<1024?1020:560; frame.style.height=`${Math.max(floor,Math.min(2400,n+2))}px`; } }; const measure=()=>{ try{ const d=frame.contentDocument; apply(Math.max(d?.body?.scrollHeight||0,d?.documentElement?.scrollHeight||0)); }catch(_){} }; window.addEventListener("message",(ev)=>{ if(ev.source!==frame.contentWindow) return; if(ev.data?.type==="inflect-web-resize") apply(ev.data.height); }); frame.addEventListener("load",measure); [120,500,1000,1800,3000].forEach((d)=>setTimeout(measure,d)); })();
|
| 632 |
+
|
| 633 |
+
/* client */
|
| 634 |
+
let client=null;
|
| 635 |
+
const getClient = async () => { if(client) return client; client=await Client.connect(window.location.origin); return client; };
|
| 636 |
+
const inputs = () => ({
|
| 637 |
+
text: $("text").value,
|
| 638 |
+
speed: parseFloat($("speed").value),
|
| 639 |
+
variation: parseFloat($("variation").value),
|
| 640 |
+
pitch_steps: parseFloat($("pitch").value),
|
| 641 |
+
seed: parseInt($("seed").value||"0",10),
|
| 642 |
+
});
|
| 643 |
+
const activeModel = () => document.querySelector('input[name="model"]:checked').value;
|
| 644 |
+
|
| 645 |
+
/* chunking (mirrors app.py) */
|
| 646 |
+
function splitText(text, limit=280){
|
| 647 |
+
const normalized=text.split(/\s+/).filter(Boolean).join(" "); if(!normalized) return [];
|
| 648 |
+
const sentences=normalized.match(/[^.!?;:]+[.!?;:]*/g)||[normalized]; const chunks=[];
|
| 649 |
+
for(let s of sentences){ s=s.trim(); if(!s) continue;
|
| 650 |
+
while(s.length>limit){ let cut=-1; for(const m of [",",";",":"]){ const idx=s.slice(0,limit+1).lastIndexOf(m); if(idx>=limit/2){ cut=idx+1; break; } }
|
| 651 |
+
if(cut<limit/2){ const sp=s.slice(0,limit+1).lastIndexOf(" "); cut=sp>0?sp:limit; }
|
| 652 |
+
chunks.push(s.slice(0,cut).trim()); s=s.slice(cut).trim(); }
|
| 653 |
+
if(s) chunks.push(s);
|
| 654 |
+
} return chunks;
|
| 655 |
+
}
|
| 656 |
+
function boundaryPause(chunk){ const end=chunk.trim().slice(-1); return ({ "?":0.28,"!":0.24,".":0.22,";":0.16,":":0.13,",":0.09 })[end] ?? 0.08; }
|
| 657 |
+
function chunkTimings(chunks,totalSec){
|
| 658 |
+
const lens=chunks.map(c=>c.length); const pauses=chunks.slice(0,-1).map(c=>boundaryPause(c));
|
| 659 |
+
const lensAdj=lens.map((l,i)=>l+(i<lens.length-1?pauses[i]*40:0));
|
| 660 |
+
const sum=lensAdj.reduce((a,b)=>a+b,0)||1; let t=0;
|
| 661 |
+
return chunks.map((c,i)=>{ const dur=(lensAdj[i]/sum)*totalSec; const start=t; t+=dur; if(i<chunks.length-1) t+=pauses[i]; return {text:c,start,dur}; });
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
/* decode + waveform */
|
| 665 |
+
async function fetchArray(url){ const r=await fetch(url); if(!r.ok) throw new Error(`audio fetch ${r.status}`); return r.arrayBuffer(); }
|
| 666 |
+
function wavBlobUrl(buf){ return URL.createObjectURL(new Blob([buf],{type:"audio/wav"})); }
|
| 667 |
+
async function decodeWav(buf){ const ctx=new (window.AudioContext||window.webkitAudioContext)(); const d=await ctx.decodeAudioData(buf.slice(0)); ctx.close(); return d; }
|
| 668 |
+
function peaksFrom(samples, buckets){
|
| 669 |
+
const out=new Float32Array(buckets*2); const step=Math.floor(samples.length/buckets)||1;
|
| 670 |
+
for(let i=0;i<buckets;i++){ const s=i*step; const e=Math.min(s+step,samples.length); let mn=1,mx=-1;
|
| 671 |
+
for(let j=s;j<e;j++){ const v=samples[j]; if(v<mn)mn=v; if(v>mx)mx=v; }
|
| 672 |
+
out[i*2]=mn; out[i*2+1]=mx; }
|
| 673 |
+
return out;
|
| 674 |
+
}
|
| 675 |
+
function drawWave(canvas, samples, progress, playedColor, baseColor){
|
| 676 |
+
const dpr=window.devicePixelRatio||1; const w=canvas.clientWidth, h=canvas.clientHeight;
|
| 677 |
+
canvas.width=Math.max(1,w*dpr); canvas.height=Math.max(1,h*dpr);
|
| 678 |
+
const ctx=canvas.getContext("2d"); ctx.setTransform(dpr,0,0,dpr,0,0); ctx.clearRect(0,0,w,h);
|
| 679 |
+
const buckets=Math.max(40,Math.floor(w*1.4)); const peaks=peaksFrom(samples,buckets);
|
| 680 |
+
const bw=Math.max(1,w/peaks.length*0.55); const mid=h/2; const prog=Math.max(0,Math.min(1,progress||0));
|
| 681 |
+
for(let i=0;i<peaks.length;i++){ const x=(i/peaks.length)*w; const played=(i/peaks.length)<=prog;
|
| 682 |
+
ctx.fillStyle=played?playedColor:baseColor; const top=mid-peaks[i*2+1]*mid*0.92; const bot=mid-peaks[i*2]*mid*0.92;
|
| 683 |
+
ctx.fillRect(x, top, bw, Math.max(1,bot-top));
|
| 684 |
+
}
|
| 685 |
+
}
|
| 686 |
+
const drawMain=(c,s,p)=>drawWave(c,s,p,"#0a84ff","#2a2c2e");
|
| 687 |
+
const drawMiniC=(c,s,p)=>drawWave(c,s,p,"#0a84ff","#2a2c2e");
|
| 688 |
+
|
| 689 |
+
/* player state */
|
| 690 |
+
let current=null; let raf=null;
|
| 691 |
+
const waveCanvas=$("wave-canvas"); const playhead=$("playhead"); const karaoke=$("karaoke");
|
| 692 |
+
const dl=$("download");
|
| 693 |
+
const statusEl=$("status"); const statusTxt=$("status-txt"); const queueEl=$("queue"); const led=$("status-led");
|
| 694 |
+
const genBtn=$("generate"); const cmpBtn=$("compare-btn");
|
| 695 |
+
const playBtn=$("play"); const stopBtn=$("stop"); const restartBtn=$("restart"); const seek=$("seek"); const player=$("player");
|
| 696 |
+
|
| 697 |
+
/* meters */
|
| 698 |
+
const metersEl=$("meters"); const METER_BARS=24; for(let i=0;i<METER_BARS;i++){ const b=document.createElement("div"); b.className="bar"; b.style.height="3px"; metersEl.appendChild(b); }
|
| 699 |
+
let meterRAF=null;
|
| 700 |
+
function runMeters(audio){ cancelAnimationFrame(meterRAF); const bars=[...metersEl.children];
|
| 701 |
+
const tick=()=>{ const lvl=audio.paused?0:0.3+0.45*Math.abs(Math.sin(audio.currentTime*8))+0.2*Math.random();
|
| 702 |
+
bars.forEach((b,i)=>{ const t=i/bars.length; const on=t<=lvl; b.classList.toggle("lev",on&&t<0.85); b.classList.toggle("peak",on&&t>=0.85); b.style.height=on?`${3+t*26}px`:"3px"; });
|
| 703 |
+
meterRAF=requestAnimationFrame(tick);
|
| 704 |
+
}; tick();
|
| 705 |
+
const off=()=>{ cancelAnimationFrame(meterRAF); bars.forEach(b=>{ b.classList.remove("lev","peak"); b.style.height="3px"; }); };
|
| 706 |
+
audio.addEventListener("pause",off,{once:true}); audio.addEventListener("ended",off,{once:true});
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
function renderMarkdown(md){ let s=(md||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
| 710 |
+
s=s.replace(/\*\*([^*]+)\*\*/g,"<b>$1</b>").replace(/`([^`]+)`/g,"<code>$1</code>"); return s.replace(/\n/g,"<br>"); }
|
| 711 |
+
function setBusy(b,msg){ genBtn.disabled=b; cmpBtn.disabled=b; led.classList.toggle("busy",b); led.classList.toggle("on",!b&¤t!=null);
|
| 712 |
+
statusEl.classList.toggle("busy",b); statusEl.classList.remove("ok","err");
|
| 713 |
+
if(b){ statusTxt.innerHTML=`<span class="busy-spin"></span> ${msg||"Generating…"}`; queueEl.classList.remove("hidden"); queueEl.textContent=""; } }
|
| 714 |
+
function setStatus(md,kind){ statusEl.classList.remove("busy","ok","err"); if(kind) statusEl.classList.add(kind); statusTxt.innerHTML=renderMarkdown(md); }
|
| 715 |
+
function setError(msg){ statusEl.classList.remove("busy","ok"); statusEl.classList.add("err"); statusTxt.innerHTML=`<span style="color:var(--warn)">${msg}</span>`; led.classList.remove("busy","on"); }
|
| 716 |
+
|
| 717 |
+
function renderKaraoke(c){ karaoke.innerHTML=""; if(!c||!c.chunks.length){ karaoke.innerHTML='<div class="empty">Your text will appear here, highlighting as it\'s spoken.</div>'; return; }
|
| 718 |
+
c.chunks.forEach((t,i)=>{ const sp=document.createElement("span"); sp.className="chunk"; sp.dataset.index=i; sp.textContent=t+" "; karaoke.appendChild(sp); }); karaoke.classList.add("in"); }
|
| 719 |
+
|
| 720 |
+
function tick(current){
|
| 721 |
+
const a=current.audio; const t=a.currentTime; const p=current.totalSec?Math.min(1,t/current.totalSec):0;
|
| 722 |
+
playhead.style.left=`${p*100}%`; drawMain(waveCanvas,current.samples,p);
|
| 723 |
+
$("now").textContent=fmtTime(t); seek.value=String(Math.round(p*1000));
|
| 724 |
+
const spans=karaoke.querySelectorAll(".chunk"); let active=-1;
|
| 725 |
+
for(let i=0;i<current.timings.length;i++){ const seg=current.timings[i]; if(t>=seg.start && t<seg.start+seg.dur){ active=i; break; } if(t>=seg.start) active=i; }
|
| 726 |
+
spans.forEach((sp,i)=>{ sp.classList.remove("speaking"); sp.style.background="";
|
| 727 |
+
const seg=current.timings[i]; const done=seg && t>=seg.start+seg.dur;
|
| 728 |
+
sp.classList.toggle("spoken",!!done); if(i===active){ sp.classList.add("speaking"); const rel=Math.max(0,t-seg.start)/Math.max(0.001,seg.dur);
|
| 729 |
+
sp.style.background=`linear-gradient(90deg,rgba(10,132,255,.18) ${Math.min(100,rel*100)}%,transparent ${Math.min(100,rel*100)}%)`; }
|
| 730 |
+
});
|
| 731 |
+
raf=requestAnimationFrame(()=>tick(current));
|
| 732 |
+
}
|
| 733 |
+
function startPlayback(c){ if(!c||!c.audio) return; c.audio.play().then(()=>{ playBtn.textContent="❚❚"; $("np-badge").classList.add("playing"); led.classList.add("on"); cancelAnimationFrame(raf); raf=requestAnimationFrame(()=>tick(c)); runMeters(c.audio); }).catch(()=>{}); }
|
| 734 |
+
function pausePlayback(c){ if(!c||!c.audio) return; c.audio.pause(); playBtn.textContent="▶"; $("np-badge").classList.remove("playing"); cancelAnimationFrame(raf); }
|
| 735 |
+
function stopPlayback(c){ if(!c||!c.audio) return; c.audio.pause(); c.audio.currentTime=0; playBtn.textContent="▶"; $("np-badge").classList.remove("playing"); cancelAnimationFrame(raf); $("now").textContent="0:00"; seek.value="0"; playhead.style.left="0%"; drawMain(waveCanvas,c.samples,0); }
|
| 736 |
+
|
| 737 |
+
function setupPlayer(c){
|
| 738 |
+
current=c; renderKaraoke(c); drawMain(waveCanvas,c.samples,0);
|
| 739 |
+
c.audio=new Audio(c.url); const a=c.audio;
|
| 740 |
+
$("dur").textContent=fmtTime(c.totalSec); $("now").textContent="0:00"; seek.value="0"; seek.disabled=false;
|
| 741 |
+
playBtn.disabled=false; stopBtn.disabled=false; restartBtn.disabled=false; playBtn.textContent="▶";
|
| 742 |
+
$("np-title").textContent=activeModel(); $("np-sub").textContent=`${c.totalSec.toFixed(2)}s · ${c.chunks.length} segment${c.chunks.length!==1?"s":""}`;
|
| 743 |
+
$("take-spec").textContent=`${c.totalSec.toFixed(2)}s · ${c.chunks.length} seg · ${c.samples.length} smp`;
|
| 744 |
+
dl.href=c.url; player.classList.add("show"); led.classList.add("on");
|
| 745 |
+
a.addEventListener("loadedmetadata",()=>{ $("dur").textContent=fmtTime(a.duration); });
|
| 746 |
+
a.addEventListener("ended",()=>{ playBtn.textContent="▶"; $("np-badge").classList.remove("playing"); cancelAnimationFrame(raf); $("now").textContent=fmtTime(c.totalSec); seek.value="1000"; playhead.style.left="100%"; drawMain(waveCanvas,c.samples,1); karaoke.querySelectorAll(".chunk").forEach(s=>{ s.classList.remove("speaking"); s.classList.add("spoken"); }); });
|
| 747 |
+
}
|
| 748 |
+
playBtn.addEventListener("click",()=>{ if(!current) return; if(current.audio.paused) startPlayback(current); else pausePlayback(current); });
|
| 749 |
+
stopBtn.addEventListener("click",()=>{ if(current) stopPlayback(current); });
|
| 750 |
+
restartBtn.addEventListener("click",()=>{ if(!current) return; current.audio.currentTime=0; if(current.audio.paused) startPlayback(current); });
|
| 751 |
+
seek.addEventListener("input",()=>{ if(!current) return; const t=(parseFloat(seek.value)/1000)*current.totalSec; current.audio.currentTime=t; $("now").textContent=fmtTime(t); playhead.style.left=`${(t/current.totalSec)*100}%`; drawMain(waveCanvas,current.samples,t/current.totalSec); });
|
| 752 |
+
$("wave").addEventListener("click",(e)=>{ if(!current) return; const r=$("wave").getBoundingClientRect(); const t=((e.clientX-r.left)/r.width)*current.totalSec; current.audio.currentTime=t; $("now").textContent=fmtTime(t); });
|
| 753 |
+
|
| 754 |
+
/* generate */
|
| 755 |
+
/* Auto-retry transient ZeroGPU failures (hardware ECC / queue blips) up to 2x. */
|
| 756 |
+
const TRANSIENT = /CUDA|ECC|GPU|queue|timeout|connection|fetch|network|503|502|500/i;
|
| 757 |
+
async function withRetry(fn, label){
|
| 758 |
+
let lastErr;
|
| 759 |
+
for (let attempt = 0; attempt < 3; attempt++) {
|
| 760 |
+
try { return await fn(); }
|
| 761 |
+
catch (e) {
|
| 762 |
+
lastErr = e;
|
| 763 |
+
const msg = String(e?.message || e);
|
| 764 |
+
const transient = TRANSIENT.test(msg);
|
| 765 |
+
if (!transient || attempt === 2) throw e;
|
| 766 |
+
const wait = 1200 * (attempt + 1);
|
| 767 |
+
statusTxt.innerHTML = `<span class="busy-spin"></span> ${label} hit a transient GPU error — retrying in ${Math.round(wait/1000)}s…`;
|
| 768 |
+
led.classList.add("busy");
|
| 769 |
+
await new Promise((r) => setTimeout(r, wait));
|
| 770 |
+
}
|
| 771 |
+
}
|
| 772 |
+
throw lastErr;
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
async function doGenerate(){
|
| 776 |
+
setBusy(true,"Generating on ZeroGPU…"); queueEl.textContent="";
|
| 777 |
+
$("cmp-wrap").classList.remove("show");
|
| 778 |
+
try{
|
| 779 |
+
await withRetry(async () => {
|
| 780 |
+
const c=await getClient(); const res=await c.predict("/synthesize",{ ...inputs(), model_name: activeModel() });
|
| 781 |
+
const fileObj=res.data[0]; const statusText=res.data[1]||"Done.";
|
| 782 |
+
const url=(fileObj && (fileObj.url || fileObj.path)) || (typeof fileObj==="string"?fileObj:""); if(!url) throw new Error("server returned no audio url");
|
| 783 |
+
const buf=await fetchArray(url); const blobUrl=wavBlobUrl(buf); const decoded=await decodeWav(buf);
|
| 784 |
+
const text=$("text").value; const chunks=splitText(text); const timings=chunkTimings(chunks,decoded.duration);
|
| 785 |
+
setupPlayer({ url:blobUrl, totalSec:decoded.duration, chunks, timings, samples:decoded.getChannelData(0), audio:null });
|
| 786 |
+
setStatus(statusText,"ok"); queueEl.classList.add("hidden");
|
| 787 |
+
const rtfM=statusText.match(/RTF\s*([0-9.]+)/); if(rtfM) $("m-rtf").textContent=parseFloat(rtfM[1]).toFixed(3);
|
| 788 |
+
});
|
| 789 |
+
setBusy(false); startPlayback(current);
|
| 790 |
+
} catch(e){ setError(`Error: ${String(e?.message||e)}`); setBusy(false); }
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
/* compare */
|
| 794 |
+
let cmp={ micro:null, nano:null };
|
| 795 |
+
const cmpCanvas={ micro:$("micro-canvas"), nano:$("nano-canvas") };
|
| 796 |
+
async function doCompare(){
|
| 797 |
+
setBusy(true,"Comparing on ZeroGPU…"); queueEl.textContent="";
|
| 798 |
+
$("cmp-wrap").classList.add("show");
|
| 799 |
+
for(const k of ["micro","nano"]){ if(cmp[k]?.audio) cmp[k].audio.pause(); cmp[k]=null;
|
| 800 |
+
$(`${k}-now`).textContent="0:00"; $(`${k}-dur`).textContent="0:00"; $(`${k}-seek`).value="0"; $(`${k}-seek`).disabled=true;
|
| 801 |
+
const btn=document.querySelector(`.mini-play[data-mini="${k}"]`); btn.disabled=true; btn.textContent="▶"; $(`${k}-ph`).style.left="0%";
|
| 802 |
+
drawMiniC(cmpCanvas[k], new Float32Array(40),0); }
|
| 803 |
+
try{
|
| 804 |
+
const [microObj,nanoObj,statusText] = await withRetry(async () => {
|
| 805 |
+
const c=await getClient(); const res=await c.predict("/compare",{ ...inputs() });
|
| 806 |
+
return res.data;
|
| 807 |
+
}, "Compare");
|
| 808 |
+
for(const [key,obj,durId,nowId,seekId,dlId,phId,cv] of [
|
| 809 |
+
["micro",microObj,"micro-dur","micro-now","micro-seek","micro-dl","micro-ph",cmpCanvas.micro],
|
| 810 |
+
["nano",nanoObj,"nano-dur","nano-now","nano-seek","nano-dl","nano-ph",cmpCanvas.nano],
|
| 811 |
+
]){
|
| 812 |
+
const url=(obj && (obj.url || obj.path)) || (typeof obj==="string"?obj:""); if(!url) throw new Error(`server returned no ${key} url`);
|
| 813 |
+
const buf=await fetchArray(url); const blobUrl=wavBlobUrl(buf); const decoded=await decodeWav(buf); const samples=decoded.getChannelData(0);
|
| 814 |
+
$(dlId).href=blobUrl; $(durId).textContent=fmtTime(decoded.duration); $(seekId).value="0"; $(seekId).disabled=false; drawMiniC(cv,samples,0);
|
| 815 |
+
const audio=new Audio(blobUrl); const btn=document.querySelector(`.mini-play[data-mini="${key}"]`); btn.disabled=false;
|
| 816 |
+
const upd=()=>{ $(nowId).textContent=fmtTime(audio.currentTime); const p=audio.currentTime/decoded.duration; $(seekId).value=String(Math.round(p*1000)); $(phId).style.left=`${p*100}%`; drawMiniC(cv,samples,p); };
|
| 817 |
+
audio.addEventListener("timeupdate",upd);
|
| 818 |
+
audio.addEventListener("ended",()=>{ btn.textContent="▶"; $(phId).style.left="100%"; drawMiniC(cv,samples,1); });
|
| 819 |
+
const sk=$(seekId); if(sk._seek) sk.removeEventListener("input",sk._seek); const onSeek=()=>{ audio.currentTime=(parseFloat(sk.value)/1000)*decoded.duration; }; sk._seek=onSeek; sk.addEventListener("input",onSeek);
|
| 820 |
+
cmp[key]={ audio, totalSec:decoded.duration, samples, canvas:cv };
|
| 821 |
+
}
|
| 822 |
+
setStatus(statusText,"ok"); queueEl.classList.add("hidden"); setBusy(false);
|
| 823 |
+
} catch(e){ setError(`Error: ${String(e?.message||e)}`); setBusy(false); }
|
| 824 |
+
}
|
| 825 |
+
document.querySelectorAll(".mini-play").forEach((btn)=>{ btn.addEventListener("click",()=>{ const key=btn.dataset.mini; const item=cmp[key]; if(!item||!item.audio) return;
|
| 826 |
+
if(item.audio.paused){ const other=key==="micro"?"nano":"micro"; if(cmp[other]?.audio && !cmp[other].audio.paused){ cmp[other].audio.pause(); document.querySelector(`.mini-play[data-mini="${other}"]`).textContent="▶"; } item.audio.play(); btn.textContent="❚❚"; }
|
| 827 |
+
else { item.audio.pause(); btn.textContent="▶"; }
|
| 828 |
+
}); });
|
| 829 |
+
[ $("micro-wave"), $("nano-wave") ].forEach((el,i)=>{ const key=i?"nano":"micro"; el.addEventListener("click",(e)=>{ const item=cmp[key]; if(!item) return; const r=el.getBoundingClientRect(); item.audio.currentTime=((e.clientX-r.left)/r.width)*item.totalSec; }); });
|
| 830 |
+
|
| 831 |
+
genBtn.addEventListener("click",doGenerate);
|
| 832 |
+
cmpBtn.addEventListener("click",doCompare);
|
| 833 |
+
$("text").addEventListener("keydown",(e)=>{ if(e.key==="Enter"&&(e.metaKey||e.ctrlKey)){ e.preventDefault(); doGenerate(); } });
|
| 834 |
+
|
| 835 |
+
window.addEventListener("resize",()=>{ if(current) drawMain(waveCanvas,current.samples, current.audio?current.audio.currentTime/current.totalSec:0); for(const k of ["micro","nano"]){ if(cmp[k]) drawMiniC(cmpCanvas[k],cmp[k].samples, cmp[k].audio?cmp[k].audio.currentTime/cmp[k].totalSec:0); } });
|
| 836 |
+
|
| 837 |
+
drawMain(waveCanvas,new Float32Array(200),0);
|
| 838 |
+
drawMiniC(cmpCanvas.micro,new Float32Array(40),0);
|
| 839 |
+
drawMiniC(cmpCanvas.nano,new Float32Array(40),0);
|
| 840 |
+
</script>
|
| 841 |
+
</body>
|
| 842 |
+
</html>
|