Files changed (1) hide show
  1. app.py +208 -50
app.py CHANGED
@@ -1,63 +1,221 @@
1
- import gradio as gr
 
 
 
 
 
2
  import os
3
- import subprocess
4
  import tempfile
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- MODEL_FILE = "en_US-lessac-medium.onnx"
7
- CONFIG_FILE = "en_US-lessac-medium.onnx.json"
8
 
9
- MODEL_URL = "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx"
10
- CONFIG_URL = "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx.json"
11
 
12
- def download_file(url, filename):
13
- if not os.path.exists(filename):
14
- result = subprocess.run(
15
- ["wget", "-O", filename, url],
16
- capture_output=True,
17
- text=True
 
18
  )
19
- if result.returncode != 0:
20
- raise gr.Error("فشل تحميل الملف: " + filename + "\n" + result.stderr)
21
 
22
- def generate_audio(text):
23
- if not text or not text.strip():
24
- raise gr.Error("اكتبي نص أولًا.")
25
 
26
- download_file(MODEL_URL, MODEL_FILE)
27
- download_file(CONFIG_URL, CONFIG_FILE)
 
 
 
 
28
 
29
- output_file = tempfile.NamedTemporaryFile(delete=False, suffix=".wav").name
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
- command = f'echo "{text}" | piper --model {MODEL_FILE} --config {CONFIG_FILE} --output_file {output_file}'
 
 
 
32
 
33
- result = subprocess.run(
34
- command,
35
- shell=True,
36
- capture_output=True,
37
- text=True
38
  )
39
 
40
- if result.returncode != 0:
41
- raise gr.Error("Piper فشل في توليد الصوت:\n" + result.stderr)
42
-
43
- if not os.path.exists(output_file) or os.path.getsize(output_file) < 1000:
44
- raise gr.Error("تم إنشاء ملف صوت لكنه فارغ أو صغير جدًا.")
45
-
46
- return output_file
47
-
48
- demo = gr.Interface(
49
- fn=generate_audio,
50
- inputs=gr.Textbox(
51
- label="Text",
52
- value="Hello Thuraya. Welcome to Under the Palm Tree.",
53
- lines=3
54
- ),
55
- outputs=gr.Audio(
56
- label="Audio",
57
- type="filepath",
58
- autoplay=True
59
- ),
60
- title="Piper TTS Test"
61
- )
62
-
63
- demo.launch()
 
1
+ # ============================================================
2
+ # Hugging Face Space - Bilingual TTS with Piper
3
+ # ============================================================
4
+
5
+ from __future__ import annotations
6
+
7
  import os
8
+ import wave
9
  import tempfile
10
+ from functools import lru_cache
11
+ from pathlib import Path
12
+
13
+ import gradio as gr
14
+ from huggingface_hub import hf_hub_download
15
+ from piper import PiperVoice, SynthesisConfig
16
+
17
+ # ------------------------------------------------------------
18
+ # Config
19
+ # ------------------------------------------------------------
20
+
21
+ REPO_ID = "rhasspy/piper-voices"
22
+
23
+ # إضافة المزيد من الأصوات (عربي، بريطاني، أمريكي)
24
+ VOICE_PRESETS = {
25
+ "Woman (British)": {
26
+ "label": "Woman (British) — Alba",
27
+ "model": "en/en_GB/alba/medium/en_GB-alba-medium.onnx",
28
+ "config": "en/en_GB/alba/medium/en_GB-alba-medium.onnx.json",
29
+ },
30
+ "Man (British)": {
31
+ "label": "Man (British) — Alan",
32
+ "model": "en/en_GB/alan/medium/en_GB-alan-medium.onnx",
33
+ "config": "en/en_GB/alan/medium/en_GB-alan-medium.onnx.json",
34
+ },
35
+ "Child (British)": {
36
+ "label": "Child (British, approximate) — Aru",
37
+ "model": "en/en_GB/aru/medium/en_GB-aru-medium.onnx",
38
+ "config": "en/en_GB/aru/medium/en_GB-aru-medium.onnx.json",
39
+ },
40
+ "Woman (US English)": {
41
+ "label": "Woman (US English) — Lessac",
42
+ "model": "en/en_US/lessac/medium/en_US-lessac-medium.onnx",
43
+ "config": "en/en_US/lessac/medium/en_US-lessac-medium.onnx.json",
44
+ },
45
+ "Man (Arabic)": {
46
+ "label": "Man (Arabic) — Khalil",
47
+ "model": "ar/ar_JO/khalil/medium/ar_JO-khalil-medium.onnx",
48
+ "config": "ar/ar_JO/khalil/medium/ar_JO-khalil-medium.onnx.json",
49
+ }
50
+ }
51
+
52
+ DEFAULT_TEXT = "Hello. Welcome to our text to speech demo."
53
+
54
+ CUSTOM_CSS = """
55
+ footer {display:none !important;}
56
+ .gradio-container {
57
+ max-width: 980px !important;
58
+ }
59
+ .hero {
60
+ text-align: center;
61
+ margin-bottom: 10px;
62
+ }
63
+ .note {
64
+ font-size: 0.95rem;
65
+ color: #666;
66
+ }
67
+ """
68
+
69
+ # ------------------------------------------------------------
70
+ # Voice loading
71
+ # ------------------------------------------------------------
72
+
73
+ def download_voice_files(preset_name: str) -> tuple[str, str]:
74
+ preset = VOICE_PRESETS[preset_name]
75
+ model_path = hf_hub_download(
76
+ repo_id=REPO_ID,
77
+ filename=preset["model"],
78
+ repo_type="model",
79
+ )
80
+ config_path = hf_hub_download(
81
+ repo_id=REPO_ID,
82
+ filename=preset["config"],
83
+ repo_type="model",
84
+ )
85
+ return model_path, config_path
86
+
87
+ @lru_cache(maxsize=8)
88
+ def get_voice(preset_name: str) -> PiperVoice:
89
+ model_path, _ = download_voice_files(preset_name)
90
+ return PiperVoice.load(model_path)
91
+
92
+ # ------------------------------------------------------------
93
+ # TTS
94
+ # ------------------------------------------------------------
95
+
96
+ def synthesize_tts(
97
+ text: str,
98
+ preset_name: str,
99
+ speed: float,
100
+ volume: float,
101
+ sentence_silence: float,
102
+ ) -> tuple[str | None, str]:
103
+ text = (text or "").strip()
104
+ if not text:
105
+ return None, "Please enter some text."
106
 
107
+ try:
108
+ voice = get_voice(preset_name)
109
 
110
+ safe_speed = max(0.6, min(1.6, float(speed)))
111
+ length_scale = 1.0 / safe_speed
112
 
113
+ syn_config = SynthesisConfig(
114
+ volume=float(volume),
115
+ length_scale=length_scale,
116
+ noise_scale=0.667,
117
+ noise_w_scale=0.8,
118
+ sentence_silence=float(sentence_silence),
119
+ normalize_audio=True,
120
  )
 
 
121
 
122
+ tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
123
+ tmp_path = tmp.name
124
+ tmp.close()
125
 
126
+ with wave.open(tmp_path, "wb") as wav_file:
127
+ voice.synthesize_wav(
128
+ text,
129
+ wav_file,
130
+ syn_config=syn_config,
131
+ )
132
 
133
+ return tmp_path, f"Done: {VOICE_PRESETS[preset_name]['label']}"
134
+ except Exception as error:
135
+ return None, f"Error: {error}"
136
+
137
+ # ------------------------------------------------------------
138
+ # Small helpers
139
+ # ------------------------------------------------------------
140
+
141
+ def fill_example_woman() -> tuple[str, str]:
142
+ return ("Good morning. This website now speaks with a British voice.", "Woman (British)")
143
+
144
+ def fill_example_man() -> tuple[str, str]:
145
+ return ("The dictionary, music, and narration can all use British pronunciation.", "Man (British)")
146
+
147
+ def fill_example_child() -> tuple[str, str]:
148
+ return ("Hello. I am the lighter British preset for younger sounding speech.", "Child (British)")
149
+
150
+ def fill_example_arabic() -> tuple[str, str]:
151
+ return ("مرحباً بكم، هذا النص مثال على الصوت العربي الجاهز للاستخدام.", "Man (Arabic)")
152
+
153
+ # ------------------------------------------------------------
154
+ # UI
155
+ # ------------------------------------------------------------
156
+
157
+ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(), title="Bilingual TTS") as demo:
158
+ gr.HTML("""
159
+ <div class="hero">
160
+ <h1>Bilingual TTS (English & Arabic)</h1>
161
+ <p class="note">
162
+ Piper-based Hugging Face Space
163
+ </p>
164
+ </div>
165
+ """)
166
+
167
+ with gr.Row():
168
+ with gr.Column(scale=3):
169
+ text_input = gr.Textbox(
170
+ label="Text",
171
+ value=DEFAULT_TEXT,
172
+ lines=8,
173
+ placeholder="Type the text you want spoken...",
174
+ )
175
+
176
+ with gr.Column(scale=2):
177
+ preset_dropdown = gr.Dropdown(
178
+ choices=list(VOICE_PRESETS.keys()),
179
+ value="Woman (British)",
180
+ label="Voice Preset",
181
+ )
182
+
183
+ speed_slider = gr.Slider(minimum=0.6, maximum=1.6, value=1.0, step=0.05, label="Speed")
184
+ volume_slider = gr.Slider(minimum=0.2, maximum=1.5, value=1.0, step=0.05, label="Volume")
185
+ silence_slider = gr.Slider(minimum=0.0, maximum=1.0, value=0.2, step=0.05, label="Sentence Silence")
186
+
187
+ generate_btn = gr.Button("Generate Speech", variant="primary")
188
+
189
+ with gr.Row():
190
+ woman_btn = gr.Button("Preset: Woman (EN)")
191
+ man_btn = gr.Button("Preset: Man (EN)")
192
+ child_btn = gr.Button("Preset: Child (EN)")
193
+ arabic_btn = gr.Button("Preset: Man (AR)")
194
+
195
+ with gr.Row():
196
+ audio_output = gr.Audio(
197
+ label="Generated Audio",
198
+ type="filepath",
199
+ format="wav",
200
+ autoplay=False,
201
+ interactive=False,
202
+ )
203
+
204
+ status_output = gr.Textbox(
205
+ label="Status",
206
+ interactive=False,
207
+ )
208
 
209
+ woman_btn.click(fn=fill_example_woman, outputs=[text_input, preset_dropdown])
210
+ man_btn.click(fn=fill_example_man, outputs=[text_input, preset_dropdown])
211
+ child_btn.click(fn=fill_example_child, outputs=[text_input, preset_dropdown])
212
+ arabic_btn.click(fn=fill_example_arabic, outputs=[text_input, preset_dropdown])
213
 
214
+ generate_btn.click(
215
+ fn=synthesize_tts,
216
+ inputs=[text_input, preset_dropdown, speed_slider, volume_slider, silence_slider],
217
+ outputs=[audio_output, status_output],
 
218
  )
219
 
220
+ if __name__ == "__main__":
221
+ demo.launch()