Pelku commited on
Commit
c4aa5d2
·
verified ·
1 Parent(s): 2797ff6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +116 -55
app.py CHANGED
@@ -1,70 +1,131 @@
 
1
  import io
2
  import torch
3
- import scipy.io.wavfile
 
 
4
  import gradio as gr
5
- import spaces
6
  from pocket_tts import TTSModel
7
 
8
- print("Initializing Kyutai Pocket-TTS for ZeroGPU...")
9
- model = None
10
-
11
- def get_model():
12
- global model
13
- if model is None:
14
- model = TTSModel.load_model()
15
- return model
16
-
17
- VOICE_LIST = [
18
- "stuart_bell", "alba", "fantine", "bill_boerst",
19
- "caro_davy", "peter_yearsley", "azelma", "cosette",
20
- "eponine", "javert", "marius", "jean", "jane",
21
- "anna", "charles", "eve", "george", "mary",
22
- "michael", "paul", "vera"
23
  ]
24
 
25
- # Cache voice states so we don't recompute voice prompt embeddings every request
26
- voice_state_cache = {}
 
 
 
 
27
 
28
- @spaces.GPU
29
- def generate_tts(text: str, voice: str):
30
- clean_text = text.strip()
31
- if not clean_text:
32
- return None
33
-
34
- m = get_model()
35
- clean_voice = voice.replace("pocket-", "").replace("-", "_")
36
-
37
- # Get or compute voice state for the voice prompt
38
- if clean_voice not in voice_state_cache:
 
 
 
39
  try:
40
- voice_state_cache[clean_voice] = m.get_state_for_audio_prompt(clean_voice)
 
41
  except Exception:
42
- # If string voice identifier is passed directly or with fallback
43
- voice_state_cache[clean_voice] = m.get_state_for_audio_prompt(f"voices/{clean_voice}.wav") if hasattr(m, "get_state_for_audio_prompt") else {}
44
 
45
- model_state = voice_state_cache[clean_voice]
 
 
 
 
 
46
 
47
- # Generate audio
48
- audio = m.generate_audio(model_state, clean_text)
49
-
50
- if isinstance(audio, torch.Tensor):
51
- audio_np = audio.detach().cpu().numpy()
 
 
 
 
 
 
 
 
 
 
52
  else:
53
- audio_np = audio
54
-
55
- sample_rate = getattr(m, "sample_rate", 24000)
56
- return (sample_rate, audio_np)
57
-
58
- demo = gr.Interface(
59
- fn=generate_tts,
60
- inputs=[
61
- gr.Textbox(label="Text to speak", value="Hello! This is Kyutai Pocket TTS running smoothly."),
62
- gr.Dropdown(choices=VOICE_LIST, value="stuart_bell", label="Voice"),
63
- ],
64
- outputs=gr.Audio(label="Generated Audio", type="numpy"),
65
- title="Kyutai Pocket-TTS ZeroGPU API",
66
- api_name="predict"
67
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  if __name__ == "__main__":
70
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
1
+ import os
2
  import io
3
  import torch
4
+ import torchaudio
5
+ import numpy as np
6
+ import scipy.io.wavfile as wavfile
7
  import gradio as gr
 
8
  from pocket_tts import TTSModel
9
 
10
+ # Load the official Kyutai Pocket-TTS model on CUDA GPU if available, else CPU
11
+ device = "cuda" if torch.cuda.is_available() else "cpu"
12
+ print(f"Loading Pocket-TTS model on device: {device}...")
13
+ model = TTSModel.from_pretrained("kyutai/pocket-tts-v0.1", device=device)
14
+
15
+ # Available voices in Kyutai Pocket-TTS
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 to phase vocoder: {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
+ # Clamp speed between 0.5x and 2.0x
67
+ speed_factor = max(0.5, min(2.0, float(speed) if speed else 1.0))
68
+
69
+ # 1. Generate audio using Kyutai Pocket-TTS model
70
+ with torch.no_grad():
71
+ audio_tensor = model.generate(text=clean_text, voice=clean_voice)
72
+
73
+ # Convert to 1D float numpy array
74
+ if isinstance(audio_tensor, torch.Tensor):
75
+ audio_np = audio_tensor.cpu().float().numpy().squeeze()
76
  else:
77
+ audio_np = np.array(audio_tensor, dtype=np.float32).squeeze()
78
+
79
+ sample_rate = getattr(model, "sample_rate", 24000)
80
+
81
+ # 2. Adjust speed with pitch preservation
82
+ if abs(speed_factor - 1.0) >= 0.02:
83
+ audio_np = change_speed_pitch_preserved(audio_np, sample_rate, speed_factor)
84
+
85
+ # Normalize audio to prevent clipping
86
+ max_val = np.max(np.abs(audio_np))
87
+ if max_val > 0:
88
+ audio_np = (audio_np / max_val) * 0.95
89
+
90
+ # Return in Gradio (sample_rate, numpy_array) format
91
+ int16_audio = (audio_np * 32767).astype(np.int16)
92
+ return (sample_rate, int16_audio)
93
+
94
+ # --- Gradio UI & API Interface ---
95
+ with gr.Blocks(title="Kyutai Pocket-TTS Server") as demo:
96
+ gr.Markdown("# 🎙️ Kyutai Pocket-TTS Server with Speed Control")
97
+
98
+ with gr.Row():
99
+ with gr.Column():
100
+ text_input = gr.Textbox(
101
+ label="Text to Synthesize",
102
+ placeholder="Enter text to speak...",
103
+ lines=4,
104
+ value="The quick brown fox jumps over the lazy dog."
105
+ )
106
+ voice_input = gr.Dropdown(
107
+ label="Voice",
108
+ choices=VOICES,
109
+ value="alba"
110
+ )
111
+ speed_slider = gr.Slider(
112
+ label="Speed Multiplier",
113
+ minimum=0.5,
114
+ maximum=2.0,
115
+ step=0.05,
116
+ value=1.0
117
+ )
118
+ generate_btn = gr.Button("Generate Speech", variant="primary")
119
+
120
+ with gr.Column():
121
+ audio_output = gr.Audio(label="Synthesized Audio", type="numpy")
122
+
123
+ generate_btn.click(
124
+ fn=synthesize,
125
+ inputs=[text_input, voice_input, speed_slider],
126
+ outputs=audio_output,
127
+ api_name="predict"
128
+ )
129
 
130
  if __name__ == "__main__":
131
+ demo.queue().launch(server_name="0.0.0.0", server_port=7860)