File size: 5,712 Bytes
05ed0af
 
 
 
5573de1
 
05ed0af
 
 
 
 
0653705
5573de1
05ed0af
 
 
 
 
 
 
5573de1
 
 
 
 
 
 
 
 
 
 
8f1ca63
 
 
5573de1
 
05ed0af
 
 
 
5573de1
 
 
 
 
 
 
 
 
 
 
05ed0af
 
5573de1
2c906c0
 
05ed0af
 
 
 
 
3613873
0653705
3613873
 
0653705
 
 
 
e70677e
0653705
 
e70677e
 
05ed0af
2c906c0
 
 
0653705
3613873
 
 
 
0653705
 
 
f7e1789
 
0653705
 
 
2c906c0
05ed0af
 
 
 
 
 
 
 
 
 
 
 
 
 
209575e
 
 
 
 
3613873
34eacbd
209575e
40faa21
209575e
 
 
 
 
 
 
 
 
 
 
 
 
 
5573de1
209575e
 
5573de1
209575e
05ed0af
db38ccb
 
 
 
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
"""PyHarp front end for the original IndexTTS2 inference implementation."""

from __future__ import annotations

import spaces  # imported first, as ZeroGPU requires

import sys
import uuid
from pathlib import Path

import gradio as gr
import soundfile as sf
from pyharp import Channels, ModelCard, SampleRate, Subcategory, build_endpoint

ROOT = Path(__file__).resolve().parent
sys.path.append(str(ROOT / "indextts"))

from indextts.infer_v2 import IndexTTS2  # noqa: E402
from tools.download_files import download_model_from_huggingface  # noqa: E402

model_card = ModelCard(
    name="IndexTTS2",
    description=(
        "Zero-shot speech synthesis with a voice reference and optional emotion "
        "reference, as 22.05 kHz mono speech. Repository: https://github.com/index-tts/index-tts "
        "Paper: https://arxiv.org/abs/2506.21619 Released under the bilibili Model Use "
        "License, which forbids using the model or its output to train or "
        "improve another AI model, except a non-commercial one."
    ),
    author="Siyi Zhou, Yiquan Zhou, Yi He, Xun Zhou, Jinchao Wang, Wei Deng, Jingchen Shu",
    tags=[Subcategory.TEXT_TO_SPEECH, SampleRate(22050), Channels(1), 'voice-cloning',
          'emotion-control',
          'domain:speech',
],
)

MODEL_DIR = ROOT / "checkpoints"
OUTPUT_DIR = ROOT / "outputs"
OUTPUT_DIR.mkdir(exist_ok=True)

# pyharp runs process_fn in a worker, which imports this file as __mp_main__ and starts with the
# server, so the model is downloaded and loaded there alone
if __name__ != "__main__":
    download_model_from_huggingface(str(MODEL_DIR), str(MODEL_DIR / "hf_cache"))
    tts = IndexTTS2(
        model_dir=str(MODEL_DIR),
        cfg_path=str(MODEL_DIR / "config.yaml"),
        use_fp16=False,
        use_deepspeed=False,
        use_cuda_kernel=False,
    )


def process_fn(text, voice_reference, emotion_reference, emotion_weight):
    # Checked before the GPU is reserved, so a request missing an input spends none of the
    # caller's ZeroGPU allowance
    text = (text or "").strip()
    if not text:
        raise gr.Error("Please enter text to synthesize.")
    if not voice_reference:
        raise gr.Error("Please upload a voice reference recording.")
    if gpu_seconds(text, voice_reference, emotion_reference, emotion_weight) > MAX_GPU_SECONDS:
        raise gr.Error(
            f"The text is {len(text)} characters long, more than one request can speak in the "
            f"{MAX_GPU_SECONDS // 60} minutes it may run. Split it."
        )
    for reference in (voice_reference, emotion_reference):
        if reference:
            try:
                info = sf.info(reference)
            except Exception as exc:
                raise gr.Error(f"Could not read the reference recording: {exc}") from exc
            if info.frames == 0:
                raise gr.Error("The reference recording is empty.")

    return speak(text, voice_reference, emotion_reference, emotion_weight)


# The text is spoken a segment at a time, so the cost grows with its length
# PyHARP stops a job that runs past its timeout_s, 15 minutes by default, so a request whose
# reservation would exceed that is refused rather than started. A shorter one is left to
# ZeroGPU, which admits it when the caller's own allowance covers it.
MAX_GPU_SECONDS = 900


def gpu_seconds(text, voice_reference, emotion_reference, emotion_weight):
    # Measured at about 12 s for 70 characters, 29 s for 550 and 54 s for 1000, padded
    return 15 + 0.07 * len(text)


@spaces.GPU(duration=gpu_seconds)
def speak(text, voice_reference, emotion_reference, emotion_weight):
    output_path = OUTPUT_DIR / f"harp_{uuid.uuid4().hex}.wav"
    try:
        return tts.infer(
            spk_audio_prompt=voice_reference,
            text=text,
            output_path=str(output_path),
            emo_audio_prompt=emotion_reference or None,
            emo_alpha=float(emotion_weight) * 0.8 if emotion_reference else 1.0,
            max_text_tokens_per_segment=120,
        )
    except Exception as exc:
        raise gr.Error(f"IndexTTS2 synthesis failed: {exc}") from exc


# The processing worker imports this file, so the app must not be built there
if __name__ == "__main__":
    with gr.Blocks(title="IndexTTS2") as demo:
        input_components = [
            gr.Textbox(label="Text to Synthesize", lines=3).set_info(
                "Chinese or English text to synthesize."
            ),
            gr.Audio(type="filepath", label="Voice Reference").set_info(
                "Required voice sample, of which the model uses up to 15 seconds."
            ),
            gr.Audio(type="filepath", label="Emotion Reference")
            .harp_required(False)
            .set_info("Optional. Leave empty to follow the voice reference."),
            gr.Slider(0.0, 1.0, value=0.8, step=0.05, label="Emotion Weight").set_info(
                "Strength of the optional emotion reference."
            ),
        ]
        output_components = [
            gr.Audio(type="filepath", label="Synthesized Speech").set_info(
                "The text spoken in the reference voice."
            ),
        ]
        build_endpoint(
            model_card=model_card,
            input_components=input_components,
            output_components=output_components,
            process_fn=process_fn,
        )

    # Server-side rendering puts a Node proxy in front of every request, and Gradio's launch
    # check of the page through it can time out while the model loads, which crashes the app.
    # HARP calls the API alone, so nothing is lost without it.
    demo.queue(default_concurrency_limit=1).launch(show_error=True, pwa=True, ssr_mode=False)