import spaces import gradio as gr import torch import os, json, time, uuid, datetime, pathlib # torch >= 2.6 defaults torch.load(weights_only=True), which breaks TTS's # pickle-based config loading. Restore the older behavior before TTS imports. _orig_torch_load = torch.load def _patched_torch_load(*args, **kwargs): kwargs.setdefault("weights_only", False) return _orig_torch_load(*args, **kwargs) torch.load = _patched_torch_load os.environ["COQUI_TOS_AGREED"] = "1" from TTS.api import TTS device = "cuda" if torch.cuda.is_available() else "cpu" tts = TTS("tts_models/multilingual/multi-dataset/xtts_v2").to(device) # ── survey storage: one JSON line per event, batched into a PRIVATE dataset repo ── # Events: "generate" (Generate clicked; the survey panel opens at the same moment) # "submit" (survey answered). Nothing else is stored — no text, no audio. SURVEY_REPO = os.environ.get("SURVEY_REPO", "tonyassi/voice-clone-survey") DATA_DIR = pathlib.Path("survey_data"); DATA_DIR.mkdir(exist_ok=True) DATA_FILE = DATA_DIR / f"events-{uuid.uuid4().hex[:8]}.jsonl" scheduler = None if os.environ.get("TOKEN"): from huggingface_hub import CommitScheduler scheduler = CommitScheduler(repo_id=SURVEY_REPO, repo_type="dataset", folder_path=DATA_DIR, path_in_repo="data", every=5, token=os.environ["TOKEN"], private=True) def log_event(event, **fields): row = {"ts": datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds"), "event": event, **fields} lock = scheduler.lock if scheduler else __import__("threading").Lock() with lock: with DATA_FILE.open("a") as f: f.write(json.dumps(row) + "\n") # ── the app itself, unchanged ── @spaces.GPU def clone(text, audio): tts.tts_to_file(text=text, speaker_wav=audio, language="en", file_path="./output.wav") gr.Info("Done. If you have 20 seconds, tell me what you'd want from a better version — the panel on the left.", duration=8) return "./output.wav" def open_survey(): log_event("generate") return gr.Sidebar(open=True) def submit_survey(use, worth, other, email): if not (use or worth or (other or "").strip()): return gr.update(value="Pick one thing or write a line first."), gr.update() log_event("submit", use=use, worth=worth, other=(other or "").strip()[:500], email=(email or "").strip()[:200]) return gr.update(value="Thank you. That helps more than you'd think."), gr.update(interactive=False) CSS = """ .top-line { display:block; text-align:center; padding:10px 14px; background:#0f766e; color:#fff; text-decoration:none; font-weight:600; cursor:pointer; } .top-line:hover { filter:brightness(1.05); } #survey-btn { width:100%; } /* one choice per line in the survey */ #q-use .wrap, #q-worth .wrap { flex-direction: column; align-items: stretch; } #q-use label, #q-worth label { width: 100%; } """ with gr.Blocks(title="Voice Clone", css=CSS, theme=gr.themes.Base(primary_hue="teal", secondary_hue="teal", neutral_hue="slate")) as demo: top = gr.Button("I'm building a better version of this. 20 seconds — tell me what you'd want →", elem_classes=["top-line"], variant="primary") gr.Markdown(""" # Voice Clone by [Tony Assi](https://www.tonyassi.com/) ---