Spaces:
Running on Zero
Running on Zero
File size: 10,976 Bytes
6d1acb0 287541f 6d1acb0 c392a82 9684bdc 6d1acb0 c392a82 6d1acb0 1b2da16 6d1acb0 1b2da16 6d1acb0 1b2da16 6d1acb0 287541f 6d1acb0 287541f 6d1acb0 1b2da16 6d1acb0 1b2da16 6d1acb0 255bed9 6d1acb0 287541f 255bed9 1b2da16 255bed9 1b2da16 6d1acb0 255bed9 287541f 6d1acb0 255bed9 6d1acb0 215ce62 255bed9 ba8c1f3 215ce62 6d1acb0 9684bdc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | """ZONOS3 Mini · a minimal, session-isolated ZeroGPU Space."""
import html
from functools import partial
import json
import os
from pathlib import Path
import tempfile
import spaces # Before any torch or upstream model imports.
import gradio as gr
from config import DEFAULT_TEXT, GRADIO_VERSION, LANGUAGES, MAX_CHARS, MAX_SEED, ROOT, VERSION
from controller import Controller, message
from logging_setup import configure_logging
from starlette.middleware import Middleware
from boot import EntryShell
configure_logging()
if gr.__version__ != GRADIO_VERSION:
raise RuntimeError(f"Use Gradio {GRADIO_VERSION}; this version is part of the UI contract.")
CATALOG = json.loads((ROOT / "voices.json").read_text())["voices"]
UI_ONLY = os.environ.get("ZONOS_UI_ONLY") == "1"
if UI_ONLY:
class PreviewEngine:
def prepare(self, *args):
from requests_schema import InputError
raise InputError("UI preview only: model inference is disabled.")
engine = PreviewEngine()
aligner = None
else:
from engine import load_engine
engine = load_engine() # Synchronous startup download / load / CUDA placement.
from alignment import load_aligner
aligner = load_aligner()
RUNTIME = Path(os.environ.get("ZONOS_RUNTIME_DIR", str(Path(tempfile.gettempdir()) / "zonos3-mini")))
controller = Controller(engine, RUNTIME, [v["name"] for v in CATALOG], aligner=aligner)
ASSETS = ROOT / "assets"
EXAMPLES = json.loads((ROOT / "examples.json").read_text())
DEFAULT_PICKER = json.dumps({"language":"en_us", "voice":"Savannah", "custom":False})
def picker_markup():
langs = "".join(f'<option value="{key}">{html.escape(label)}</option>' for key, label in LANGUAGES.items())
voices = "".join(f'<option value="{html.escape(v["name"], quote=True)}" data-language="{v["language"]}">{html.escape(v["name"])}</option>' for v in CATALOG)
return ('<div class="pickers"><label>language <select data-language aria-label="Text language">' + langs +
'</select></label><span aria-hidden="true">·</span><label><span class="sr">Voice</span>' +
'<select data-voice aria-label="Preset voice">' + voices +
'</select><span data-custom hidden>Your voice clip</span></label></div>')
PICKER_CSS = """
.pickers { display:flex; justify-content:center; align-items:center; flex-wrap:wrap; gap:13px; color:#bdbdbd; font:12px ui-monospace,monospace; }
.pickers label { display:inline-flex; align-items:center; gap:10px; }
.pickers select { color:#ddd; background:#000; border:0; border-radius:0; min-height:44px; max-width:165px; font:13px ui-monospace,monospace; padding:0 7px; }
.pickers select:focus-visible { outline:2px solid #ddd; outline-offset:3px; }
.pickers option { background:#151515; color:#eee; }
.pickers [hidden] { display:none; }
.sr { position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); }
@media(max-width:400px) { .pickers { gap:6px; } .pickers label { gap:4px; } .pickers select { max-width:128px; font-size:16px; } }
"""
def load_example(language):
example = EXAMPLES[language]
return example["text"], json.dumps({"voice":example["voice"], "language":example["language"], "custom":False}), "Preset"
def settings_markup():
buttons = "".join(
f'<button type="button" class="example-button" data-example="{html.escape(code, quote=True)}"><span>{html.escape(item["label"])}</span></button>'
for code, item in EXAMPLES.items()
)
return (ASSETS / "settings.html").read_text().replace("<!-- example-buttons -->", buttons)
theme = gr.themes.Base(primary_hue="neutral", secondary_hue="neutral", neutral_hue="neutral",
font=["Arial", "sans-serif"], font_mono=["ui-monospace", "monospace"]).set(
body_background_fill="#000", body_background_fill_dark="#000",
body_text_color="#eee", body_text_color_dark="#eee",
block_background_fill="#111", block_background_fill_dark="#111",
block_border_color="#333", block_border_color_dark="#333",
input_background_fill="#151515", input_background_fill_dark="#151515",
background_fill_primary="#111", background_fill_primary_dark="#111",
background_fill_secondary="#171717", background_fill_secondary_dark="#171717",
block_label_text_color="#bdbdbd", block_label_text_color_dark="#bdbdbd",
block_title_text_color="#bdbdbd", block_title_text_color_dark="#bdbdbd",
block_label_background_fill="#111", block_label_background_fill_dark="#111",
button_secondary_background_fill="#242424", button_secondary_background_fill_dark="#242424",
button_secondary_background_fill_hover="#333", button_secondary_background_fill_hover_dark="#333",
button_secondary_text_color="#eee", button_secondary_text_color_dark="#eee",
button_secondary_border_color="#444", button_secondary_border_color_dark="#444",
link_text_color="#ccc", link_text_color_dark="#ccc",
)
with gr.Blocks(title="ZONOS3 Mini", fill_width=True, delete_cache=(3600, 86400), analytics_enabled=False) as demo:
gr.HTML('<header class="app-header"><span>ZONOS3-Mini</span><span>text-to-speech · <a href="https://huggingface.co/Zyphra/ZONOS3-Mini" target="_blank" rel="noopener">Zyphra</a></span></header>', elem_id="app-header")
if UI_ONLY:
gr.HTML('<p id="preview-warning">UI preview · inference disabled</p>')
text = gr.HTML(value=DEFAULT_TEXT, html_template=(ASSETS / "script.html").read_text(),
css_template=(ASSETS / "script.css").read_text(),
js_on_load=(ASSETS / "script.js").read_text(), elem_id="script-input")
player = gr.HTML(value="{}", html_template=(ASSETS / "player.html").read_text(),
css_template=(ASSETS / "player.css").read_text(),
js_on_load=(ASSETS / "wave.js").read_text() + (ASSETS / "player.js").read_text(), elem_id="speech-player")
picker = gr.HTML(value=DEFAULT_PICKER, html_template=picker_markup(), css_template=PICKER_CSS,
js_on_load=(ASSETS / "picker.js").read_text(), elem_id="voice-picker")
with gr.Row(elem_id="action-row"):
generate = gr.Button("Generate speech", variant="primary", elem_id="generate-speech")
cancel = gr.Button("Cancel", visible=False, elem_id="cancel-speech")
status = gr.HTML(message(""), elem_id="request-status")
with gr.HTML(value="{}", source="Preset", html_template=settings_markup(),
css_template=(ASSETS / "settings.css").read_text(),
js_on_load=(ASSETS / "settings.js").read_text(), elem_id="voice-options") as settings:
reference = gr.HTML(value="null", html_template=(ASSETS / "reference.html").read_text(),
css_template=(ASSETS / "reference.css").read_text(),
js_on_load=(ASSETS / "reference.js").read_text(), elem_id="reference-clip")
clip = gr.Audio(label="Reference voice", type="filepath", format="wav", visible=False)
# Stable API wire components. The HTML UI passes its state atomically via the
# documented event JS preprocessor; API callers continue using the same inputs.
source = gr.Radio(["Preset", "Voice clip"], value="Preset", label="Voice source", visible=False)
consent = gr.Checkbox(value=False, label="I have permission to use this voice.", visible=False)
seed = gr.Number(value=0, precision=0, minimum=0, maximum=MAX_SEED, label="Seed", visible=False)
randomize = gr.Checkbox(value=True, label="New seed for each take", visible=False)
normalize = gr.Checkbox(value=True, label="Normalize numbers and abbreviations", visible=False)
rate = gr.Slider(0, 30, value=0, step=.5, label="Speaking rate · UTF-8 bytes/second", visible=False)
temperature = gr.Slider(.1, 1.5, value=.75, step=.05, label="Temperature", visible=False)
top_p = gr.Slider(.1, 1, value=.69, step=.01, label="Top-p", visible=False)
top_k = gr.Slider(1, 400, value=80, step=1, label="Top-k", visible=False)
repetition = gr.Slider(1, 4, value=2.4, step=.1, label="Repetition penalty", visible=False)
gr.HTML(f'''<footer class="credits"><p>Model · <a href="https://huggingface.co/Zyphra/ZONOS3-Mini" target="_blank" rel="noopener">Zyphra</a></p><p>UI/UX · <a href="https://huggingface.co/fffiloni" target="_blank" rel="noopener">Sylvain Filoni / fffiloni</a></p><details><summary>About · v{VERSION}</summary><p>Model: Gabriel Clark, Sofian Mejjoute, George Close and Beren Millidge — Zyphra. Weights: Apache 2.0. Official runtime: MIT. <a href="https://github.com/Zyphra/ZONOS3-Mini" target="_blank" rel="noopener">Source and notices</a>. Word synchronization: <a href="https://huggingface.co/Qwen/Qwen3-ForcedAligner-0.6B-hf" target="_blank" rel="noopener">Qwen3 ForcedAligner</a> (Apache 2.0). Japanese and Chinese use character-level timing. Hosted on Hugging Face with Gradio. Independent UI adaptation.</p></details></footer>''', elem_id="app-credits")
def example(language):
script, voice, _ = load_example(language)
return script, voice, gr.HTML(source="Preset")
for code in EXAMPLES:
getattr(settings, "example_" + code)(partial(example, code), outputs=[text, picker, settings], queue=False, api_visibility="private")
event = generate.click(
controller.generate,
[text, picker, source, clip, consent, normalize, seed, randomize, temperature, top_p, top_k, repetition, rate],
[player, status, generate, cancel, seed],
api_name="generate", concurrency_limit=8, concurrency_id="speech_requests", trigger_mode="once",
show_progress="minimal", show_progress_on=[status],
js=(ASSETS / "generate.js").read_text(),
)
finish_ui = """() => {
const status = document.getElementById('status-state');
if (status) status.dataset.busy = 'false';
document.getElementById('speech-player')?.dispatchEvent(new CustomEvent('zonos:activity', {detail:false}));
document.getElementById('generate-speech').disabled = document.getElementById('voice-options')?.dataset.clip === 'true' && document.getElementById('reference-clip')?.dataset.busy === 'true';
}"""
event.then(fn=None, js=finish_ui, api_visibility="private")
event.failure(fn=None, js=finish_ui, api_visibility="private")
cancel.click(controller.cancel, outputs=[status, cancel], queue=False, api_name="cancel", show_progress="hidden")
demo.unload(controller.on_disconnect)
demo.queue(max_size=16, default_concurrency_limit=1, api_open=False)
if __name__ == "__main__":
demo.launch(theme=theme, css=(ASSETS / "style.css").read_text(), allowed_paths=[str(controller.outputs.resolve())],
blocked_paths=[str((ROOT / "vendor").resolve()), str(controller.jobs.directory.resolve())],
max_file_size="25mb", show_error=False, max_threads=40, footer_links=[],
ssr_mode=False, app_kwargs={"middleware":[Middleware(EntryShell, shell_path=ASSETS / "boot.html")]})
|