jiaaom commited on
Commit
660180f
·
1 Parent(s): 2df4526

feat: add web ui and idiot-proof launch script

Browse files
Files changed (3) hide show
  1. README.md +6 -0
  2. launch-web-app.sh +35 -0
  3. web-app/app.py +174 -0
README.md CHANGED
@@ -19,6 +19,12 @@ CosyVoice3 SFT fine-tune for a Mandarin-speaking talking flower character. Outpu
19
 
20
  This bundle is self-contained — no separate CosyVoice repository clone required.
21
 
 
 
 
 
 
 
22
  ### Option 1: Using `uv` (Recommended)
23
  This is the fastest and most reliable way to run the model, leveraging a fully locked and isolated environment.
24
 
 
19
 
20
  This bundle is self-contained — no separate CosyVoice repository clone required.
21
 
22
+ ### Launching the Web UI
23
+ To easily launch the interactive web interface, use the included shell script:
24
+ ```bash
25
+ ./launch-web-app.sh
26
+ ```
27
+
28
  ### Option 1: Using `uv` (Recommended)
29
  This is the fastest and most reliable way to run the model, leveraging a fully locked and isolated environment.
30
 
launch-web-app.sh ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # A foolproof script to launch the Talking Flower Web UI
3
+ # It ensures dependencies are installed via `uv` and starts the Gradio app.
4
+
5
+ set -e # Exit on error
6
+
7
+ echo "🌸 Starting Talking Flower Web UI..."
8
+ echo ""
9
+
10
+ # Check if uv is installed
11
+ if ! command -v uv &> /dev/null; then
12
+ echo "❌ Error: 'uv' is not installed."
13
+ echo "Please install it first: pip install uv"
14
+ exit 1
15
+ fi
16
+
17
+ # Change to the directory where this script is located
18
+ cd "$(dirname "$0")"
19
+
20
+ # Optional: Default environment variables, overridden if already set
21
+ export GRADIO_SERVER_NAME="${GRADIO_SERVER_NAME:-0.0.0.0}"
22
+ export GRADIO_SERVER_PORT="${GRADIO_SERVER_PORT:-6112}"
23
+
24
+ echo "📦 Syncing dependencies using uv..."
25
+ uv sync
26
+
27
+ echo ""
28
+ echo "🚀 Launching the Web Application..."
29
+ echo "🌐 The UI will be available on all network interfaces (0.0.0.0) on port ${GRADIO_SERVER_PORT}"
30
+ echo "Local access: http://localhost:${GRADIO_SERVER_PORT}"
31
+ echo "Network access: http://<your-machine-ip>:${GRADIO_SERVER_PORT}"
32
+ echo ""
33
+
34
+ # Run the app
35
+ uv run web-app/app.py
web-app/app.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import time
4
+ import random
5
+ import torch
6
+ import soundfile as sf
7
+ import gradio as gr
8
+
9
+ # Add the parent directory to sys.path so we can import cosyvoice and transformers
10
+ _HERE = os.path.dirname(os.path.abspath(__file__))
11
+ _PARENT = os.path.dirname(_HERE)
12
+ sys.path.insert(0, _PARENT)
13
+ sys.path.insert(0, os.path.join(_PARENT, "third_party/Matcha-TTS"))
14
+
15
+ import onnxruntime
16
+ import transformers
17
+
18
+ # Monkey patch same as inference.py to avoid missing files errors in HF slim-bundle
19
+ _original_inference_session = onnxruntime.InferenceSession
20
+
21
+ def _maybe_inference_session(path_or_bytes, *args, **kwargs):
22
+ if isinstance(path_or_bytes, str) and not os.path.exists(path_or_bytes):
23
+ return None
24
+ return _original_inference_session(path_or_bytes, *args, **kwargs)
25
+
26
+ onnxruntime.InferenceSession = _maybe_inference_session
27
+
28
+ _original_qwen2_from_pretrained = transformers.Qwen2ForCausalLM.from_pretrained
29
+
30
+ def _qwen2_from_pretrained_or_config(pretrained_model_name_or_path, *args, **kwargs):
31
+ weight_files = ('model.safetensors', 'pytorch_model.bin',
32
+ 'model.safetensors.index.json', 'pytorch_model.bin.index.json')
33
+ if (isinstance(pretrained_model_name_or_path, str)
34
+ and os.path.isdir(pretrained_model_name_or_path)
35
+ and not any(os.path.exists(os.path.join(pretrained_model_name_or_path, f))
36
+ for f in weight_files)):
37
+ config = transformers.Qwen2Config.from_pretrained(pretrained_model_name_or_path)
38
+ return transformers.Qwen2ForCausalLM(config)
39
+ return _original_qwen2_from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
40
+
41
+ transformers.Qwen2ForCausalLM.from_pretrained = _qwen2_from_pretrained_or_config
42
+
43
+ from cosyvoice.cli.cosyvoice import CosyVoice3
44
+ from cosyvoice.utils.common import set_all_random_seed
45
+
46
+ MODEL_DIR = _PARENT
47
+ INSTRUCT = "You are a helpful assistant.<|endofprompt|>"
48
+ SPK_ID = "TalkingFlower"
49
+
50
+ # Global model instance (lazy load)
51
+ model = None
52
+
53
+ def load_model():
54
+ global model
55
+ if model is None:
56
+ model = CosyVoice3(MODEL_DIR)
57
+ return model
58
+
59
+ def remove_tail_click(audio, sr, search_s=0.20, burst_thresh=0.05,
60
+ silence_thresh=0.02, win_ms=5, fade_ms=3):
61
+ ch = audio[0]
62
+ win_n = int(sr * win_ms / 1000)
63
+ search_n = min(int(sr * search_s), ch.shape[0])
64
+ fade_n = int(sr * fade_ms / 1000)
65
+ tail = ch[-search_n:]
66
+ n_wins = search_n // win_n
67
+ rms = [tail[i * win_n:(i + 1) * win_n].pow(2).mean().sqrt().item()
68
+ for i in range(n_wins)]
69
+ if rms[-1] < burst_thresh:
70
+ return audio
71
+ cut_win = next((i for i in range(n_wins - 2, -1, -1)
72
+ if rms[i] < silence_thresh), None)
73
+ if cut_win is None:
74
+ return audio
75
+ cut = ch.shape[0] - search_n + cut_win * win_n
76
+ out = audio.clone()
77
+ out[0, cut:] = 0.0
78
+ if fade_n > 0 and cut >= fade_n:
79
+ out[0, cut - fade_n:cut] *= torch.linspace(1.0, 0.0, fade_n)
80
+ return out
81
+
82
+ def generate_audio(text, seed, speed):
83
+ if not text:
84
+ return None, "Please enter some text."
85
+
86
+ set_all_random_seed(seed)
87
+
88
+ try:
89
+ model = load_model()
90
+
91
+ for output in model.inference_sft(
92
+ INSTRUCT + text,
93
+ spk_id=SPK_ID,
94
+ stream=False,
95
+ speed=speed,
96
+ text_frontend=False,
97
+ ):
98
+ audio = remove_tail_click(output["tts_speech"], model.sample_rate)
99
+
100
+ output_path = os.path.join(_HERE, "output.wav")
101
+ sf.write(output_path, audio.squeeze(0).cpu().numpy(), model.sample_rate)
102
+ return output_path, f"Success! (Seed: {seed})"
103
+
104
+ except Exception as e:
105
+ return None, f"Error: {str(e)}"
106
+
107
+ # Gradio UI Theme & Setup (inspired by Talking-Flower)
108
+ custom_css = """
109
+ #main-container { max-width: 900px; margin: auto; }
110
+ .wonder-card { border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
111
+ .header { text-align: center; margin-bottom: 2rem; }
112
+ .model-arch { background-color: #f8f9fa; padding: 1rem; border-radius: 8px; border-left: 4px solid #f472b6; }
113
+ """
114
+
115
+ with gr.Blocks(title="Talking Flower TTS", css=custom_css) as demo:
116
+ with gr.Column(elem_id="main-container"):
117
+ gr.Markdown("<div class='header'><h1>🌸 Talking Flower Web UI</h1><p>A CosyVoice3-powered TTS interface mimicking the original Talking-Flower.</p></div>")
118
+
119
+ with gr.Row():
120
+ with gr.Column(scale=5):
121
+ text_input = gr.Textbox(
122
+ label="Talking Flower will say:",
123
+ lines=3,
124
+ placeholder="Support Chinese, English and Japanese...",
125
+ value="你好呀!我是会说话的花朵,很高兴认识你!",
126
+ elem_classes="wonder-card"
127
+ )
128
+
129
+ with gr.Row():
130
+ seed_input = gr.Slider(
131
+ label="Random Seed (For Reproducibility)",
132
+ minimum=0, maximum=100000, step=1, value=42,
133
+ elem_classes="wonder-card"
134
+ )
135
+ speed_input = gr.Slider(
136
+ label="Speech Speed",
137
+ minimum=0.5, maximum=2.0, step=0.1, value=1.0,
138
+ elem_classes="wonder-card"
139
+ )
140
+
141
+ generate_btn = gr.Button("🌸 Speak!", variant="primary", elem_classes="wonder-card")
142
+
143
+ with gr.Column(scale=3):
144
+ audio_output = gr.Audio(
145
+ label="输出音频 (Generated Audio)",
146
+ type="filepath",
147
+ interactive=False, # Enables native download button
148
+ elem_classes="wonder-card"
149
+ )
150
+ status_output = gr.Textbox(label="Status", interactive=False, elem_classes="wonder-card")
151
+
152
+ gr.Markdown("---")
153
+ gr.Markdown("### 🧠 Model Architecture (CosyVoice 3 Sub-Models)")
154
+ gr.HTML("""
155
+ <div class="model-arch">
156
+ <p>This repository uses a three-stage cascade architecture for zero-shot and supervised text-to-speech:</p>
157
+ <ol>
158
+ <li><strong>LLM (Language Model) <code>llm.pt</code></strong>: A Qwen2-0.5B based transformer that autoregressively generates semantic speech tokens from the input text and instruction.</li>
159
+ <li><strong>Flow Matching <code>flow.pt</code></strong>: A conditional flow-matching network that translates the discrete semantic tokens into continuous Mel-spectrogram features, conditioned on speaker embeddings.</li>
160
+ <li><strong>HIFT (HiFi-GAN Vocoder) <code>hift.pt</code></strong>: A high-fidelity generative adversarial network that converts the Mel-spectrograms into the final raw audio waveform.</li>
161
+ </ol>
162
+ </div>
163
+ """)
164
+
165
+ generate_btn.click(
166
+ fn=generate_audio,
167
+ inputs=[text_input, seed_input, speed_input],
168
+ outputs=[audio_output, status_output]
169
+ )
170
+
171
+ if __name__ == "__main__":
172
+ port = int(os.environ.get("GRADIO_SERVER_PORT", 6112))
173
+ host = os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0")
174
+ demo.launch(server_name=host, server_port=port)