Pelku commited on
Commit
b8095c5
·
verified ·
1 Parent(s): f48d7c3

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +159 -134
  2. requirements.txt +9 -8
app.py CHANGED
@@ -1,135 +1,160 @@
1
- import os
2
- import io
3
- import torch
4
- import torchaudio
5
- import numpy as np
6
- import gradio as gr
7
- import pocket_tts
8
- from pocket_tts import TTSModel
9
-
10
- # 1. Load the Pocket-TTS model using the official pocket_tts API
11
- print("Loading Kyutai Pocket-TTS model...")
12
- model = TTSModel.load_model()
13
- print("Pocket-TTS model loaded successfully!")
14
-
15
- # Official Kyutai Pocket-TTS voice list
16
- VOICES = [
17
- "alba",
18
- "marius",
19
- "jaime",
20
- "stuart",
21
- "kelly",
22
- "leo",
23
- "carla",
24
- "serena"
25
- ]
26
-
27
- def change_speed_pitch_preserved(audio_np: np.ndarray, sample_rate: int, speed: float) -> np.ndarray:
28
- """
29
- Adjusts speech speed cleanly while preserving natural human voice pitch and formants.
30
- """
31
- if abs(speed - 1.0) < 0.02:
32
- return audio_np
33
-
34
- try:
35
- # High quality DSP time-stretching using torchaudio / sox tempo
36
- tensor = torch.from_numpy(audio_np).float()
37
- if tensor.dim() == 1:
38
- tensor = tensor.unsqueeze(0)
39
-
40
- # 'tempo -s' applies SoX's speech-optimized WSOLA with phase alignment
41
- effects = [["tempo", "-s", str(speed)]]
42
- stretched_tensor, _ = torchaudio.sox_effects.apply_effects_tensor(
43
- tensor, sample_rate, effects
44
- )
45
- return stretched_tensor.squeeze(0).numpy()
46
- except Exception as e:
47
- print(f"SoX tempo stretch fallback: {e}")
48
- try:
49
- import librosa
50
- return librosa.effects.time_stretch(audio_np.astype(np.float32), rate=speed)
51
- except Exception:
52
- return audio_np
53
-
54
- def synthesize(text: str, voice: str, speed: float = 1.0):
55
- """
56
- Generates audio from Pocket-TTS and applies clean server-side speed adjustment.
57
- """
58
- if not text or not text.strip():
59
- raise gr.Error("Text prompt cannot be empty.")
60
-
61
- clean_text = text.strip()
62
- clean_voice = voice.lower().replace("pocket-", "").replace("-", "_").strip()
63
- if clean_voice not in VOICES:
64
- clean_voice = "alba"
65
-
66
- speed_factor = max(0.5, min(2.0, float(speed) if speed else 1.0))
67
-
68
- # 1. Prepare voice state & stream chunks
69
- voice_state = model.get_voice_state(clean_voice)
70
- generation_state = model.get_state_for_audio_generation(clean_text, voice_state)
71
-
72
- audio_chunks = []
73
- for chunk in model.generate_audio_stream(generation_state):
74
- if isinstance(chunk, torch.Tensor):
75
- audio_chunks.append(chunk.detach().cpu().float().numpy().squeeze())
76
- else:
77
- audio_chunks.append(np.array(chunk, dtype=np.float32).squeeze())
78
-
79
- if not audio_chunks:
80
- raise gr.Error("No audio was generated by the model.")
81
-
82
- audio_np = np.concatenate(audio_chunks)
83
- sample_rate = getattr(model, "sample_rate", 24000)
84
-
85
- # 2. Adjust speed with pitch preservation
86
- if abs(speed_factor - 1.0) >= 0.02:
87
- audio_np = change_speed_pitch_preserved(audio_np, sample_rate, speed_factor)
88
-
89
- # Normalize audio to prevent clipping
90
- max_val = np.max(np.abs(audio_np))
91
- if max_val > 0:
92
- audio_np = (audio_np / max_val) * 0.95
93
-
94
- # Return in Gradio (sample_rate, numpy_int16_array) format
95
- int16_audio = (audio_np * 32767).astype(np.int16)
96
- return (sample_rate, int16_audio)
97
-
98
- # --- Gradio UI & API Interface ---
99
- with gr.Blocks(title="Kyutai Pocket-TTS Server") as demo:
100
- gr.Markdown("# 🎙️ Kyutai Pocket-TTS Server with Speed Control")
101
-
102
- with gr.Row():
103
- with gr.Column():
104
- text_input = gr.Textbox(
105
- label="Text to Synthesize",
106
- placeholder="Enter text to speak...",
107
- lines=4,
108
- value="The quick brown fox jumps over the lazy dog."
109
- )
110
- voice_input = gr.Dropdown(
111
- label="Voice",
112
- choices=VOICES,
113
- value="alba"
114
- )
115
- speed_slider = gr.Slider(
116
- label="Speed Multiplier",
117
- minimum=0.5,
118
- maximum=2.0,
119
- step=0.05,
120
- value=1.0
121
- )
122
- generate_btn = gr.Button("Generate Speech", variant="primary")
123
-
124
- with gr.Column():
125
- audio_output = gr.Audio(label="Synthesized Audio", type="numpy")
126
-
127
- generate_btn.click(
128
- fn=synthesize,
129
- inputs=[text_input, voice_input, speed_slider],
130
- outputs=audio_output,
131
- api_name="predict"
132
- )
133
-
134
- if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  demo.queue().launch(server_name="0.0.0.0", server_port=7860)
 
1
+ import os
2
+ import io
3
+ import spaces
4
+ import torch
5
+ import torchaudio
6
+ import numpy as np
7
+ import gradio as gr
8
+ from pocket_tts import TTSModel
9
+
10
+ # 1. Load the Pocket-TTS model at startup (CPU-only, ~100M params)
11
+ print("Loading Kyutai Pocket-TTS model...")
12
+ tts_model = TTSModel.load_model()
13
+ print("Pocket-TTS model loaded successfully!")
14
+
15
+ # Official Kyutai Pocket-TTS English voice list
16
+ # See: https://huggingface.co/kyutai/tts-voices
17
+ VOICES = [
18
+ "alba",
19
+ "anna",
20
+ "azelma",
21
+ "bill_boerst",
22
+ "caro_davy",
23
+ "charles",
24
+ "cosette",
25
+ "eponine",
26
+ "eve",
27
+ "fantine",
28
+ "george",
29
+ "jane",
30
+ "jean",
31
+ "javert",
32
+ "marius",
33
+ "mary",
34
+ "michael",
35
+ "paul",
36
+ "peter_yearsley",
37
+ "stuart_bell",
38
+ "vera",
39
+ ]
40
+
41
+ # Pre-cache voice states at startup for faster inference
42
+ print("Pre-caching voice states...")
43
+ voice_states = {}
44
+ for voice_name in VOICES:
45
+ try:
46
+ voice_states[voice_name] = tts_model.get_state_for_audio_prompt(voice_name)
47
+ print(f" Cached voice: {voice_name}")
48
+ except Exception as e:
49
+ print(f" Warning: Could not cache voice '{voice_name}': {e}")
50
+ print("Voice states cached!")
51
+
52
+
53
+ def change_speed_pitch_preserved(audio_np: np.ndarray, sample_rate: int, speed: float) -> np.ndarray:
54
+ """
55
+ Adjusts speech speed cleanly while preserving natural human voice pitch and formants.
56
+ """
57
+ if abs(speed - 1.0) < 0.02:
58
+ return audio_np
59
+
60
+ try:
61
+ # High quality DSP time-stretching using torchaudio / sox tempo
62
+ tensor = torch.from_numpy(audio_np).float()
63
+ if tensor.dim() == 1:
64
+ tensor = tensor.unsqueeze(0)
65
+
66
+ # 'tempo -s' applies SoX's speech-optimized WSOLA with phase alignment
67
+ effects = [["tempo", "-s", str(speed)]]
68
+ stretched_tensor, _ = torchaudio.sox_effects.apply_effects_tensor(
69
+ tensor, sample_rate, effects
70
+ )
71
+ return stretched_tensor.squeeze(0).numpy()
72
+ except Exception as e:
73
+ print(f"SoX tempo stretch fallback: {e}")
74
+ try:
75
+ import librosa
76
+ return librosa.effects.time_stretch(audio_np.astype(np.float32), rate=speed)
77
+ except Exception:
78
+ return audio_np
79
+
80
+
81
+ @spaces.GPU
82
+ def synthesize(text: str, voice: str, speed: float = 1.0):
83
+ """
84
+ Generates audio from Pocket-TTS and applies clean server-side speed adjustment.
85
+ """
86
+ if not text or not text.strip():
87
+ raise gr.Error("Text prompt cannot be empty.")
88
+
89
+ clean_text = text.strip()
90
+ clean_voice = voice.lower().strip()
91
+ if clean_voice not in VOICES:
92
+ clean_voice = "alba"
93
+
94
+ speed_factor = max(0.5, min(2.0, float(speed) if speed else 1.0))
95
+
96
+ # 1. Get the cached voice state, or load it on demand
97
+ if clean_voice in voice_states:
98
+ voice_state = voice_states[clean_voice]
99
+ else:
100
+ voice_state = tts_model.get_state_for_audio_prompt(clean_voice)
101
+
102
+ # 2. Generate audio using the official API
103
+ audio_tensor = tts_model.generate_audio(voice_state, clean_text)
104
+
105
+ # Convert to numpy
106
+ audio_np = audio_tensor.numpy().astype(np.float32)
107
+ sample_rate = tts_model.sample_rate
108
+
109
+ # 3. Adjust speed with pitch preservation
110
+ if abs(speed_factor - 1.0) >= 0.02:
111
+ audio_np = change_speed_pitch_preserved(audio_np, sample_rate, speed_factor)
112
+
113
+ # Normalize audio to prevent clipping
114
+ max_val = np.max(np.abs(audio_np))
115
+ if max_val > 0:
116
+ audio_np = (audio_np / max_val) * 0.95
117
+
118
+ # Return in Gradio (sample_rate, numpy_int16_array) format
119
+ int16_audio = (audio_np * 32767).astype(np.int16)
120
+ return (sample_rate, int16_audio)
121
+
122
+
123
+ # --- Gradio UI & API Interface ---
124
+ with gr.Blocks(title="Kyutai Pocket-TTS Server") as demo:
125
+ gr.Markdown("# 🎙️ Kyutai Pocket-TTS Server with Speed Control")
126
+
127
+ with gr.Row():
128
+ with gr.Column():
129
+ text_input = gr.Textbox(
130
+ label="Text to Synthesize",
131
+ placeholder="Enter text to speak...",
132
+ lines=4,
133
+ value="The quick brown fox jumps over the lazy dog."
134
+ )
135
+ voice_input = gr.Dropdown(
136
+ label="Voice",
137
+ choices=VOICES,
138
+ value="alba"
139
+ )
140
+ speed_slider = gr.Slider(
141
+ label="Speed Multiplier",
142
+ minimum=0.5,
143
+ maximum=2.0,
144
+ step=0.05,
145
+ value=1.0
146
+ )
147
+ generate_btn = gr.Button("Generate Speech", variant="primary")
148
+
149
+ with gr.Column():
150
+ audio_output = gr.Audio(label="Synthesized Audio", type="numpy")
151
+
152
+ generate_btn.click(
153
+ fn=synthesize,
154
+ inputs=[text_input, voice_input, speed_slider],
155
+ outputs=audio_output,
156
+ api_name="predict"
157
+ )
158
+
159
+ if __name__ == "__main__":
160
  demo.queue().launch(server_name="0.0.0.0", server_port=7860)
requirements.txt CHANGED
@@ -1,8 +1,9 @@
1
- pocket-tts
2
- torch
3
- torchaudio
4
- gradio>=4.0.0
5
- numpy
6
- scipy
7
- librosa
8
- soundfile
 
 
1
+ pocket-tts
2
+ torch
3
+ torchaudio
4
+ gradio>=4.0.0
5
+ numpy
6
+ scipy
7
+ librosa
8
+ soundfile
9
+ spaces