Spaces:
Running on Zero
Running on Zero
| import base64 | |
| import json | |
| import os | |
| import shutil | |
| import tempfile | |
| import threading | |
| import time | |
| from pathlib import Path | |
| import gradio as gr | |
| import spaces | |
| VOICE_API_TOKEN = os.getenv("VOICE_API_TOKEN", "").strip() | |
| MODEL_NAME = os.getenv("F5_MODEL", "F5TTS_v1_Base") | |
| started_at = time.time() | |
| model_lock = threading.Lock() | |
| f5_model = None | |
| def check_key(api_key: str) -> None: | |
| if VOICE_API_TOKEN and api_key != VOICE_API_TOKEN: | |
| raise gr.Error("invalid api_key") | |
| def decode_audio_b64(audio_base64: str, filename: str) -> Path: | |
| if "," in audio_base64[:128]: | |
| audio_base64 = audio_base64.split(",", 1)[1] | |
| suffix = Path(filename or "reference.wav").suffix or ".wav" | |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix) | |
| tmp_path = Path(tmp.name) | |
| try: | |
| tmp.write(base64.b64decode(audio_base64)) | |
| tmp.close() | |
| return tmp_path | |
| except Exception: | |
| tmp.close() | |
| tmp_path.unlink(missing_ok=True) | |
| raise gr.Error("invalid base64 audio") | |
| def get_model(): | |
| global f5_model | |
| with model_lock: | |
| if f5_model is not None: | |
| return f5_model | |
| from f5_tts.api import F5TTS | |
| f5_model = F5TTS(model=MODEL_NAME, device="cpu") | |
| return f5_model | |
| def health(api_key: str = "") -> str: | |
| check_key(api_key) | |
| return json.dumps( | |
| { | |
| "ok": True, | |
| "model": MODEL_NAME, | |
| "model_loaded": f5_model is not None, | |
| "uptime_seconds": round(time.time() - started_at, 3), | |
| }, | |
| ensure_ascii=False, | |
| ) | |
| def zerogpu_probe() -> str: | |
| return "ok" | |
| def clone_b64( | |
| api_key: str, | |
| ref_audio_base64: str, | |
| ref_filename: str, | |
| ref_text: str, | |
| gen_text: str, | |
| speed: float = 1.0, | |
| nfe_step: int = 32, | |
| ) -> str: | |
| check_key(api_key) | |
| speed = float(speed) | |
| nfe_step = int(nfe_step) | |
| if not ref_text.strip(): | |
| raise gr.Error("ref_text is required") | |
| if not gen_text.strip(): | |
| raise gr.Error("gen_text is required") | |
| if len(gen_text) > 1000: | |
| raise gr.Error("gen_text is too long for free CPU; max 1000 characters") | |
| if speed < 0.5 or speed > 2.0: | |
| raise gr.Error("speed must be between 0.5 and 2.0") | |
| if nfe_step < 8 or nfe_step > 64: | |
| raise gr.Error("nfe_step must be between 8 and 64") | |
| ref_path = decode_audio_b64(ref_audio_base64, ref_filename or "reference.wav") | |
| output_dir = Path(tempfile.mkdtemp(prefix="f5_clone_")) | |
| output_wav = output_dir / "output.wav" | |
| output_spec = output_dir / "output.png" | |
| try: | |
| model = get_model() | |
| try: | |
| model.infer( | |
| ref_file=str(ref_path), | |
| ref_text=ref_text, | |
| gen_text=gen_text, | |
| file_wave=str(output_wav), | |
| file_spec=str(output_spec), | |
| speed=speed, | |
| nfe_step=nfe_step, | |
| seed=None, | |
| ) | |
| except TypeError: | |
| model.infer( | |
| ref_file=str(ref_path), | |
| ref_text=ref_text, | |
| gen_text=gen_text, | |
| file_wave=str(output_wav), | |
| file_spec=str(output_spec), | |
| speed=speed, | |
| seed=None, | |
| ) | |
| if not output_wav.exists(): | |
| raise gr.Error("voice clone produced no output file") | |
| audio_base64 = base64.b64encode(output_wav.read_bytes()).decode("ascii") | |
| return json.dumps( | |
| { | |
| "audio_base64": audio_base64, | |
| "mime_type": "audio/wav", | |
| "model": MODEL_NAME, | |
| }, | |
| ensure_ascii=False, | |
| ) | |
| finally: | |
| ref_path.unlink(missing_ok=True) | |
| shutil.rmtree(output_dir, ignore_errors=True) | |
| with gr.Blocks(title="F5 Voice Clone API") as demo: | |
| gr.Markdown("# F5 Voice Clone API") | |
| gr.Markdown("Use `clone_b64` from n8n. Reference audio and output audio are base64 for simple JSON automation.") | |
| api_key = gr.Textbox(label="api_key", type="password") | |
| with gr.Tab("Health"): | |
| health_out = gr.Textbox(label="result") | |
| gr.Button("Check").click(health, inputs=[api_key], outputs=[health_out], api_name="health") | |
| gr.Button("ZeroGPU probe", visible=False).click( | |
| zerogpu_probe, | |
| inputs=[], | |
| outputs=[health_out], | |
| api_name="zerogpu_probe", | |
| ) | |
| with gr.Tab("Clone"): | |
| ref_audio_base64 = gr.Textbox(label="ref_audio_base64", lines=5) | |
| ref_filename = gr.Textbox(label="ref_filename", value="reference.wav") | |
| ref_text = gr.Textbox(label="ref_text", lines=3) | |
| gen_text = gr.Textbox(label="gen_text", lines=4) | |
| speed = gr.Number(label="speed", value=1.0) | |
| nfe_step = gr.Number(label="nfe_step", value=32, precision=0) | |
| result = gr.Textbox(label="result", lines=8) | |
| gr.Button("Clone").click( | |
| clone_b64, | |
| inputs=[api_key, ref_audio_base64, ref_filename, ref_text, gen_text, speed, nfe_step], | |
| outputs=[result], | |
| api_name="clone_b64", | |
| ) | |
| demo.queue(default_concurrency_limit=1).launch() | |