Arabi32 commited on
Commit
ce47ec4
ยท
verified ยท
1 Parent(s): 843f4c2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +219 -772
app.py CHANGED
@@ -6,39 +6,37 @@ from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
6
  from fastapi.middleware.cors import CORSMiddleware
7
  from huggingface_hub import HfApi, snapshot_download
8
 
9
- # โ”€โ”€ Env & Configuration โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
10
  os.environ["COQUI_TOS_AGREED"] = "1"
11
 
12
- # Directory structure for easy syncing
13
- DATA_DIR = "data"
14
- VOICE_LIB = os.path.join(DATA_DIR, "voice_library")
15
- OUTPUT_DIR = os.path.join(DATA_DIR, "outputs")
16
  HISTORY_FILE = os.path.join(DATA_DIR, "history.json")
17
 
18
  for d in [DATA_DIR, VOICE_LIB, OUTPUT_DIR]:
19
  os.makedirs(d, exist_ok=True)
20
 
21
- # Hugging Face Dataset Sync Logic
22
  HF_TOKEN = os.environ.get("HF_TOKEN")
23
- HF_DATASET_REPO = os.environ.get("HF_DATASET_REPO") # e.g., "username/xtts-data"
24
 
25
  def pull_data_from_hf():
26
- """ุชุญู…ูŠู„ ุงู„ุจูŠุงู†ุงุช ุงู„ู…ุญููˆุธุฉ ู…ู† ุงู„ู€ Dataset ุนู†ุฏ ุจุฏุก ุชุดุบูŠู„ ุงู„ุชุทุจูŠู‚"""
27
  if HF_TOKEN and HF_DATASET_REPO:
28
  try:
29
- print(f"[*] Downloading data from dataset: {HF_DATASET_REPO}...")
30
  snapshot_download(
31
  repo_id=HF_DATASET_REPO,
32
  repo_type="dataset",
33
  local_dir=DATA_DIR,
34
  token=HF_TOKEN
35
  )
36
- print("[โœ“] Data loaded successfully.")
37
  except Exception as e:
38
- print(f"[!] Warning: Could not download data from HF: {e}")
39
 
40
  def push_data_to_hf():
41
- """ุฑูุน ุงู„ุจูŠุงู†ุงุช ุฅู„ู‰ ุงู„ู€ Dataset ููŠ ุงู„ุฎู„ููŠุฉ ู„ุญูุธู‡ุง ุฏุงุฆู…ู‹ุง"""
42
  if HF_TOKEN and HF_DATASET_REPO:
43
  try:
44
  api = HfApi()
@@ -47,27 +45,18 @@ def push_data_to_hf():
47
  repo_id=HF_DATASET_REPO,
48
  repo_type="dataset",
49
  token=HF_TOKEN,
50
- commit_message=f"Auto-sync at {time.time()}"
51
  )
52
- print("[โœ“] Data synced to Hugging Face Dataset.")
53
  except Exception as e:
54
- print(f"[!] Sync error: {e}")
55
 
56
  def background_sync():
57
- """ุชุดุบูŠู„ ุงู„ู…ุฒุงู…ู†ุฉ ููŠ Thread ู…ู†ูุตู„ ู„ูƒูŠ ู„ุง ูŠุชูˆู‚ู ุงู„ุฎุงุฏู…"""
58
  threading.Thread(target=push_data_to_hf, daemon=True).start()
59
 
60
- # Load initial data
61
  pull_data_from_hf()
62
 
63
- # Kill leftover processes on port 7860 (HF default port)
64
- try:
65
- subprocess.run(["fuser", "-k", "7860/tcp"],
66
- stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
67
- except:
68
- pass
69
-
70
- # โ”€โ”€ Install & Load TTS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
71
  try:
72
  from TTS.api import TTS
73
  except ImportError:
@@ -77,800 +66,258 @@ except ImportError:
77
  import torch
78
  device = "cuda" if torch.cuda.is_available() else "cpu"
79
 
80
- if "xtts_engine" not in globals():
81
- print(f"[*] Loading XTTS v2 on {device.upper()} โ€ฆ")
82
- xtts_engine = TTS("tts_models/multilingual/multi-dataset/xtts_v2").to(device)
83
- print("[โœ“] Model ready.")
84
 
85
- # โ”€โ”€ History helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
86
  def load_history():
87
  if os.path.exists(HISTORY_FILE):
88
  try:
89
- return json.load(open(HISTORY_FILE))
90
- except:
91
- pass
92
  return []
93
 
94
  def save_history(h):
95
  with open(HISTORY_FILE, "w", encoding="utf-8") as f:
96
  json.dump(h, f, ensure_ascii=False, indent=2)
97
 
98
- # โ”€โ”€ FastAPI app โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
99
- app = FastAPI(title="XTTS Studio")
100
- app.add_middleware(CORSMiddleware, allow_origins=["*"],
101
- allow_methods=["*"], allow_headers=["*"])
102
 
103
- # โ”€โ”€ Supported languages โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
104
  LANGUAGES = {
105
- "ar": "ุงู„ุนุฑุจูŠุฉ", "en": "English", "es": "Espaรฑol",
106
- "fr": "Franรงais", "de": "Deutsch", "it": "Italiano",
107
- "pt": "Portuguรชs","ru": "ะ ัƒััะบะธะน", "zh-cn": "ไธญๆ–‡",
108
- "ja": "ๆ—ฅๆœฌ่ชž", "ko": "ํ•œ๊ตญ์–ด", "tr": "Tรผrkรงe",
109
- "nl": "Nederlands","pl": "Polski", "cs": "ฤŒeลกtina",
110
- "hi": "เคนเคฟเคจเฅเคฆเฅ€",
111
  }
112
 
113
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
114
- # HTML / React Frontend
115
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
116
- HTML = r"""<!DOCTYPE html>
117
  <html lang="ar" dir="rtl">
118
  <head>
119
- <meta charset="UTF-8">
120
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
121
- <title>XTTS Voice Studio</title>
122
- <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=IBM+Plex+Sans+Arabic:wght@300;400;600;700&display=swap" rel="stylesheet">
123
- <script src="https://cdn.tailwindcss.com"></script>
124
- <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
125
- <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
126
- <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
127
- <style>
128
- :root {
129
- --bg: #0b0c0f;
130
- --surface: #13151a;
131
- --border: #1f2330;
132
- --amber: #f5a623;
133
- --amber-dim:#a06a10;
134
- --green: #3ddc84;
135
- --red: #ff5252;
136
- --text: #e8eaf0;
137
- --muted: #6b7280;
138
- --mono: 'IBM Plex Mono', monospace;
139
- --sans: 'IBM Plex Sans Arabic', sans-serif;
140
- }
141
- * { box-sizing: border-box; }
142
- body {
143
- margin: 0;
144
- background: var(--bg);
145
- color: var(--text);
146
- font-family: var(--sans);
147
- min-height: 100vh;
148
- }
149
- ::-webkit-scrollbar { width: 5px; }
150
- ::-webkit-scrollbar-track { background: var(--surface); }
151
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
152
- body::before {
153
- content: "";
154
- position: fixed; inset: 0; pointer-events: none; z-index: 0;
155
- background:
156
- repeating-linear-gradient(0deg, transparent, transparent 39px,
157
- rgba(255,255,255,.02) 40px);
158
- }
159
- .card {
160
- background: var(--surface);
161
- border: 1px solid var(--border);
162
- border-radius: 12px;
163
- }
164
- .amber { color: var(--amber); }
165
- .tag {
166
- font-family: var(--mono);
167
- font-size: 10px;
168
- letter-spacing: .12em;
169
- text-transform: uppercase;
170
- color: var(--muted);
171
- }
172
- input[type=range] {
173
- -webkit-appearance: none;
174
- width: 100%; height: 3px;
175
- background: var(--border);
176
- border-radius: 2px;
177
- outline: none;
178
- }
179
- input[type=range]::-webkit-slider-thumb {
180
- -webkit-appearance: none;
181
- width: 14px; height: 14px;
182
- background: var(--amber);
183
- border-radius: 50%;
184
- cursor: pointer;
185
- transition: transform .15s;
186
- }
187
- input[type=range]::-webkit-slider-thumb:hover { transform: scale(1.3); }
188
- select {
189
- background: var(--bg);
190
- border: 1px solid var(--border);
191
- color: var(--text);
192
- border-radius: 6px;
193
- padding: 6px 10px;
194
- font-family: var(--sans);
195
- outline: none;
196
- cursor: pointer;
197
- }
198
- select:focus { border-color: var(--amber); }
199
- textarea {
200
- background: var(--bg);
201
- border: 1px solid var(--border);
202
- color: var(--text);
203
- border-radius: 8px;
204
- font-family: var(--sans);
205
- resize: vertical;
206
- outline: none;
207
- transition: border-color .2s;
208
- padding: 12px;
209
- width: 100%;
210
- }
211
- textarea:focus { border-color: var(--amber); }
212
- .file-drop {
213
- border: 2px dashed var(--border);
214
- border-radius: 8px;
215
- padding: 16px;
216
- text-align: center;
217
- cursor: pointer;
218
- transition: border-color .2s, background .2s;
219
- position: relative;
220
- }
221
- .file-drop:hover, .file-drop.active {
222
- border-color: var(--amber);
223
- background: rgba(245,166,35,.05);
224
- }
225
- .file-drop input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
226
- .btn-primary {
227
- background: var(--amber);
228
- color: #000;
229
- font-weight: 700;
230
- border: none;
231
- border-radius: 8px;
232
- padding: 14px 24px;
233
- cursor: pointer;
234
- font-family: var(--sans);
235
- font-size: 15px;
236
- width: 100%;
237
- transition: opacity .2s, transform .1s;
238
- }
239
- .btn-primary:hover { opacity: .9; }
240
- .btn-primary:active { transform: scale(.98); }
241
- .btn-primary:disabled { opacity: .4; cursor: not-allowed; }
242
- .btn-ghost {
243
- background: transparent;
244
- border: 1px solid var(--border);
245
- color: var(--muted);
246
- border-radius: 6px;
247
- padding: 6px 12px;
248
- cursor: pointer;
249
- font-size: 12px;
250
- transition: color .2s, border-color .2s;
251
- }
252
- .btn-ghost:hover { color: var(--text); border-color: var(--muted); }
253
- .badge {
254
- display: inline-flex; align-items: center; gap: 4px;
255
- background: rgba(245,166,35,.12);
256
- border: 1px solid rgba(245,166,35,.3);
257
- color: var(--amber);
258
- border-radius: 20px;
259
- padding: 2px 10px;
260
- font-size: 11px;
261
- font-family: var(--mono);
262
- }
263
- .badge.green {
264
- background: rgba(61,220,132,.1);
265
- border-color: rgba(61,220,132,.3);
266
- color: var(--green);
267
- }
268
- .badge.red {
269
- background: rgba(255,82,82,.1);
270
- border-color: rgba(255,82,82,.3);
271
- color: var(--red);
272
- }
273
- .pulse { animation: pulse 1.5s infinite; }
274
- @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
275
- .waveform {
276
- display: flex; align-items: center; gap: 3px;
277
- height: 28px;
278
- }
279
- .waveform span {
280
- flex: 1;
281
- background: var(--amber);
282
- border-radius: 2px;
283
- animation: wave 1s ease-in-out infinite;
284
- opacity: .7;
285
- }
286
- @keyframes wave { 0%,100%{height:4px} 50%{height:24px} }
287
- .waveform span:nth-child(2){animation-delay:.1s}
288
- .waveform span:nth-child(3){animation-delay:.2s}
289
- .waveform span:nth-child(4){animation-delay:.3s}
290
- .waveform span:nth-child(5){animation-delay:.2s}
291
- .waveform span:nth-child(6){animation-delay:.1s}
292
- .waveform span:nth-child(7){animation-delay:.05s}
293
- .tab { cursor:pointer; padding:8px 16px; border-radius:6px;
294
- font-size:13px; color:var(--muted); transition:all .2s; }
295
- .tab.active { background:rgba(245,166,35,.15); color:var(--amber); }
296
- .tab:hover:not(.active) { color:var(--text); }
297
- audio { width:100%; accent-color:var(--amber); }
298
- audio::-webkit-media-controls-panel { background:var(--surface); }
299
- .history-row {
300
- display:flex; align-items:center; gap:12px;
301
- padding:10px 14px; border-radius:8px;
302
- border:1px solid var(--border);
303
- background:var(--bg);
304
- transition:border-color .2s;
305
- }
306
- .history-row:hover { border-color: var(--amber-dim); }
307
- .param-row {
308
- display:grid; grid-template-columns:140px 1fr 48px;
309
- align-items:center; gap:12px;
310
- }
311
- .param-label { font-size:12px; color:var(--muted); font-family:var(--mono); }
312
- .param-val { font-size:13px; color:var(--amber); font-family:var(--mono); text-align:right; }
313
- .voice-card {
314
- padding:10px 14px; border-radius:8px;
315
- border:1px solid var(--border);
316
- background:var(--bg); cursor:pointer;
317
- transition:all .2s;
318
- display:flex; align-items:center; justify-content:space-between;
319
- }
320
- .voice-card:hover { border-color:var(--amber-dim); }
321
- .voice-card.selected {
322
- border-color:var(--amber);
323
- background:rgba(245,166,35,.06);
324
- }
325
- </style>
326
  </head>
327
  <body>
328
- <div id="root"></div>
329
- <script type="text/babel">
330
- const { useState, useEffect, useRef, useCallback } = React;
331
-
332
- const fmt = v => parseFloat(v).toFixed(2);
333
- const apiPost = (url, body) =>
334
- fetch(url, { method:"POST", body }).then(r => {
335
- if (!r.ok) return r.json().then(e => { throw new Error(e.detail || "Server error"); });
336
- return r.json();
337
- });
338
-
339
- function Slider({ label, min, max, step, value, onChange, hint }) {
340
- return (
341
- <div className="param-row">
342
- <span className="param-label">{label}</span>
343
- <input type="range" min={min} max={max} step={step}
344
- value={value} onChange={e => onChange(parseFloat(e.target.value))} />
345
- <span className="param-val">{fmt(value)}</span>
346
- </div>
347
- );
348
- }
349
-
350
- function FileZone({ label, file, onFile }) {
351
- const [active, setActive] = useState(false);
352
- return (
353
- <div>
354
- <div className="tag mb-1">{label}</div>
355
- <div className={`file-drop ${active?"active":""}`}
356
- onDragOver={e=>{e.preventDefault();setActive(true)}}
357
- onDragLeave={()=>setActive(false)}
358
- onDrop={e=>{e.preventDefault();setActive(false);onFile(e.dataTransfer.files[0]);}}>
359
- <input type="file" accept="audio/*"
360
- onChange={e=>onFile(e.target.files[0])} />
361
- {file
362
- ? <span style={{color:"var(--green)",fontSize:12}}>โœ“ {file.name}</span>
363
- : <span style={{color:"var(--muted)",fontSize:12}}>ุงุณุญุจ ู…ู„ูุงู‹ ุฃูˆ ุงู†ู‚ุฑ ู„ู„ุงุฎุชูŠุงุฑ</span>}
364
- </div>
365
- </div>
366
- );
367
- }
368
-
369
- function WaveAnim() {
370
- return (
371
- <div className="waveform">
372
- {[1,2,3,4,5,6,7].map(i=><span key={i}/>)}
373
- </div>
374
- );
375
- }
376
-
377
- function App() {
378
- const [tab, setTab] = useState("generate");
379
- const [text, setText] = useState("");
380
- const [lang, setLang] = useState("ar");
381
- const [file1, setFile1] = useState(null);
382
- const [file2, setFile2] = useState(null);
383
- const [temperature, setTemp] = useState(0.75);
384
- const [speed, setSpeed] = useState(1.0);
385
- const [topK, setTopK] = useState(50);
386
- const [topP, setTopP] = useState(0.85);
387
- const [repPenalty, setRepPenalty] = useState(5.0);
388
- const [splitText, setSplitText] = useState(true);
389
- const [status, setStatus] = useState("idle");
390
- const [statusMsg, setStatusMsg] = useState("");
391
- const [audioUrl, setAudioUrl] = useState(null);
392
- const [audioFilename, setAudioFilename] = useState(null);
393
- const [history, setHistory] = useState([]);
394
- const [voices, setVoices] = useState([]);
395
- const [selVoice, setSelVoice] = useState(null);
396
- const [saveName, setSaveName] = useState("");
397
- const [saveStatus, setSaveStatus] = useState("");
398
-
399
- const languages = LANGUAGES_JSON;
400
-
401
- useEffect(() => {
402
- fetch("/history").then(r=>r.json()).then(setHistory).catch(()=>{});
403
- fetch("/voices").then(r=>r.json()).then(setVoices).catch(()=>{});
404
- }, []);
405
-
406
- const generate = async () => {
407
- if (!text.trim()) return setStatusMsg("ุฃุฏุฎู„ ุงู„ู†ุต ุฃูˆู„ุงู‹.");
408
- const hasRef = file1 || selVoice;
409
- if (!hasRef) return setStatusMsg("ูŠุฌุจ ุชุญุฏูŠุฏ ุนูŠู†ุฉ ุตูˆุชูŠุฉ ู…ุฑุฌุนูŠุฉ ุฃูˆ ุงุฎุชูŠุงุฑ ุตูˆุช ู…ู† ุงู„ู…ูƒุชุจุฉ.");
410
- setStatus("running"); setStatusMsg(""); setAudioUrl(null);
411
-
412
- const fd = new FormData();
413
- fd.append("text", text);
414
- fd.append("language", lang);
415
- fd.append("temperature", temperature);
416
- fd.append("speed", speed);
417
- fd.append("top_k", topK);
418
- fd.append("top_p", topP);
419
- fd.append("repetition_penalty", repPenalty);
420
- fd.append("enable_text_splitting", splitText);
421
- if (file1) fd.append("files", file1);
422
- if (file2) fd.append("files", file2);
423
- if (selVoice) fd.append("voice_name", selVoice);
424
-
425
- try {
426
- const data = await apiPost("/generate", fd);
427
- setAudioUrl(`/audio/${data.filename}`);
428
- setAudioFilename(data.filename);
429
- setStatus("done");
430
- fetch("/history").then(r=>r.json()).then(setHistory).catch(()=>{});
431
- } catch(e) {
432
- setStatus("error");
433
- setStatusMsg(e.message);
434
- }
435
- };
436
-
437
- const saveVoice = async () => {
438
- if (!saveName.trim() || !file1) return;
439
- const fd = new FormData();
440
- fd.append("name", saveName.trim());
441
- fd.append("file", file1);
442
- if (file2) fd.append("file2", file2);
443
- try {
444
- await apiPost("/voices/save", fd);
445
- setSaveStatus("โœ“ ุชู… ุงู„ุญูุธ");
446
- fetch("/voices").then(r=>r.json()).then(setVoices).catch(()=>{});
447
- setTimeout(()=>setSaveStatus(""),2000);
448
- } catch(e) { setSaveStatus("ุฎุทุฃ: " + e.message); }
449
- };
450
-
451
- const deleteVoice = async name => {
452
- await fetch(`/voices/${name}`, {method:"DELETE"});
453
- setVoices(v => v.filter(x=>x!==name));
454
- if (selVoice===name) setSelVoice(null);
455
- };
456
-
457
- const isRTL = ["ar","fa","he","ur"].includes(lang);
458
-
459
- return (
460
- <div style={{maxWidth:720,margin:"0 auto",padding:"24px 16px",position:"relative",zIndex:1}}>
461
- <div style={{marginBottom:28,textAlign:"center"}}>
462
- <div className="tag" style={{marginBottom:6}}>XTTS V2 MULTILINGUAL</div>
463
- <h1 style={{margin:0,fontSize:26,fontWeight:700,letterSpacing:"-.02em"}}>
464
- <span className="amber">Voice</span> Studio
465
- </h1>
466
- <div style={{marginTop:8,display:"flex",justifyContent:"center",gap:6}}>
467
- <span className={`badge ${device==="cuda"?"green":"red"}`}>
468
- โ— {device.toUpperCase()}
469
- </span>
470
- <span className="badge">Hugging Face Space</span>
471
- </div>
472
- </div>
473
-
474
- <div style={{display:"flex",gap:4,marginBottom:20,
475
- background:"var(--surface)",border:"1px solid var(--border)",
476
- borderRadius:8,padding:4}}>
477
- {["generate","library","history"].map(t=>(
478
- <div key={t} className={`tab${tab===t?" active":""}`}
479
- onClick={()=>setTab(t)} style={{flex:1,textAlign:"center"}}>
480
- {t==="generate"?"โšก ุชูˆู„ูŠุฏ":t==="library"?"๐Ÿ“š ู…ูƒุชุจุฉ ุงู„ุฃุตูˆุงุช":"๐Ÿ•˜ ุงู„ุณุฌู„"}
481
- </div>
482
- ))}
483
- </div>
484
-
485
- {tab==="generate" && (
486
- <div style={{display:"flex",flexDirection:"column",gap:14}}>
487
- <div className="card" style={{padding:16}}>
488
- <div style={{display:"flex",justifyContent:"space-between",
489
- alignItems:"center",marginBottom:10}}>
490
- <div className="tag">ุงู„ู†ุต ุงู„ู…ุฑุงุฏ ุชุญูˆูŠู„ู‡</div>
491
- <select value={lang} onChange={e=>setLang(e.target.value)}>
492
- {Object.entries(languages).map(([k,v])=>(
493
- <option key={k} value={k}>{v}</option>
494
- ))}
495
- </select>
496
- </div>
497
- <textarea
498
- dir={isRTL?"rtl":"ltr"}
499
- rows={5}
500
- placeholder={isRTL?"ุฃุฏุฎู„ ุงู„ู†ุต ู‡ู†ุงโ€ฆ":"Enter text hereโ€ฆ"}
501
- value={text}
502
- onChange={e=>setText(e.target.value)}
503
- />
504
- <div style={{textAlign:"left",fontSize:11,color:"var(--muted)",marginTop:4,
505
- fontFamily:"var(--mono)"}}>
506
- {text.length} chars
507
- </div>
508
- </div>
509
-
510
- <div className="card" style={{padding:16}}>
511
- <div className="tag" style={{marginBottom:10}}>ุงู„ู…ุฑุฌุน ุงู„ุตูˆุชูŠ</div>
512
- <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:12}}>
513
- <FileZone label="ุนูŠู†ุฉ 1 (ู…ุทู„ูˆุจุฉ)" file={file1} onFile={setFile1} />
514
- <FileZone label="ุนูŠู†ุฉ 2 (ุงุฎุชูŠุงุฑูŠุฉ)" file={file2} onFile={setFile2} />
515
- </div>
516
- {voices.length>0 && (
517
- <div style={{marginTop:12}}>
518
- <div className="tag" style={{marginBottom:8}}>ุฃูˆ ู…ู† ุงู„ู…ูƒุชุจุฉ</div>
519
- <div style={{display:"flex",flexWrap:"wrap",gap:8}}>
520
- {voices.map(v=>(
521
- <div key={v}
522
- style={{padding:"6px 14px",borderRadius:20,cursor:"pointer",
523
- fontSize:12,border:"1px solid",
524
- borderColor:selVoice===v?"var(--amber)":"var(--border)",
525
- color:selVoice===v?"var(--amber)":"var(--muted)",
526
- background:selVoice===v?"rgba(245,166,35,.08)":"transparent",
527
- transition:"all .2s"}}
528
- onClick={()=>setSelVoice(selVoice===v?null:v)}>
529
- {v}
530
- </div>
531
- ))}
532
  </div>
533
- </div>
534
- )}
535
- {file1 && (
536
- <div style={{marginTop:12,display:"flex",gap:8,alignItems:"center"}}>
537
- <input type="text"
538
- placeholder="ุงุณู… ุงู„ุตูˆุช ู„ู„ุญูุธโ€ฆ"
539
- value={saveName} onChange={e=>setSaveName(e.target.value)}
540
- style={{flex:1,background:"var(--bg)",border:"1px solid var(--border)",
541
- color:"var(--text)",borderRadius:6,padding:"6px 10px",
542
- fontSize:12,fontFamily:"var(--sans)",outline:"none"}} />
543
- <button className="btn-ghost" onClick={saveVoice}
544
- style={{whiteSpace:"nowrap"}}>
545
- ุญูุธ ููŠ ุงู„ู…ูƒุชุจุฉ
546
- </button>
547
- {saveStatus && <span style={{fontSize:12,color:"var(--green)"}}>{saveStatus}</span>}
548
- </div>
549
- )}
550
- </div>
551
-
552
- <div className="card" style={{padding:16}}>
553
- <details>
554
- <summary style={{cursor:"pointer",listStyle:"none",display:"flex",
555
- justifyContent:"space-between",alignItems:"center",
556
- userSelect:"none"}}>
557
- <div className="tag">ุงู„ู…ุนุงู…ู„ุงุช ุงู„ู…ุชู‚ุฏู…ุฉ</div>
558
- <span style={{fontSize:11,color:"var(--muted)"}}>ุงู†ู‚ุฑ ู„ู„ุชูˆุณูŠุน โ–พ</span>
559
- </summary>
560
- <div style={{marginTop:14,display:"flex",flexDirection:"column",gap:14}}>
561
- <Slider label="temperature" min={0.05} max={1.0} step={0.05}
562
- value={temperature} onChange={setTemp}
563
- hint="ุงู„ุชู†ูˆุน ุงู„ุนุงุทููŠ ููŠ ุงู„ุตูˆุช" />
564
- <Slider label="speed" min={0.5} max={2.0} step={0.05}
565
- value={speed} onChange={setSpeed}
566
- hint="ุณุฑุนุฉ ุงู„ูƒู„ุงู…" />
567
- <Slider label="top_k" min={1} max={100} step={1}
568
- value={topK} onChange={setTopK}
569
- hint="ุฃุนู„ู‰ k ุงุญุชู…ุงู„ุงุช" />
570
- <Slider label="top_p" min={0.1} max={1.0} step={0.05}
571
- value={topP} onChange={setTopP}
572
- hint="ุฃุนู„ู‰ p ุงุญุชู…ุงู„ุงุช" />
573
- <Slider label="rep_penalty" min={1.0} max={10.0} step={0.5}
574
- value={repPenalty} onChange={setRepPenalty}
575
- hint="ุนู‚ูˆุจุฉ ุงู„ุชูƒุฑุงุฑ" />
576
- <div style={{display:"flex",alignItems:"center",gap:10,paddingTop:4}}>
577
- <label style={{display:"flex",alignItems:"center",gap:8,
578
- cursor:"pointer",fontSize:12,color:"var(--muted)"}}>
579
- <input type="checkbox" checked={splitText}
580
- onChange={e=>setSplitText(e.target.checked)}
581
- style={{accentColor:"var(--amber)",width:14,height:14}} />
582
- ุชู‚ุณูŠู… ุงู„ู†ุต ุงู„ุชู„ู‚ุงุฆูŠ (ู„ู„ู†ุตูˆุต ุงู„ุทูˆูŠู„ุฉ)
583
- </label>
584
- </div>
585
- </div>
586
- </details>
587
- </div>
588
-
589
- {statusMsg && (
590
- <div style={{padding:"10px 14px",borderRadius:8,fontSize:13,
591
- background:"rgba(255,82,82,.08)",border:"1px solid rgba(255,82,82,.3)",
592
- color:"var(--red)"}}>
593
- {statusMsg}
594
- </div>
595
- )}
596
-
597
- <button className="btn-primary" onClick={generate}
598
- disabled={status==="running"}>
599
- {status==="running"
600
- ? <span style={{display:"flex",alignItems:"center",justifyContent:"center",gap:10}}>
601
- <WaveAnim/> ุฌุงุฑูŠ ุงู„ุชูˆู„ูŠุฏโ€ฆ
602
- </span>
603
- : "โšก ุชูˆู„ูŠุฏ ุงู„ุตูˆุช"}
604
- </button>
605
-
606
- {audioUrl && (
607
- <div className="card" style={{padding:16}}>
608
- <div style={{display:"flex",justifyContent:"space-between",
609
- alignItems:"center",marginBottom:12}}>
610
- <div style={{display:"flex",alignItems:"center",gap:8}}>
611
- <span className="badge green">โœ“ ุชู… ุงู„ุชูˆู„ูŠุฏ</span>
612
- </div>
613
- <a href={audioUrl} download={audioFilename}
614
- style={{textDecoration:"none"}}>
615
- <button className="btn-ghost">โฌ‡ ุชุญู…ูŠู„</button>
616
- </a>
617
- </div>
618
- <audio src={audioUrl} controls autoPlay />
619
- </div>
620
- )}
621
- </div>
622
- )}
623
-
624
- {tab==="library" && (
625
- <div style={{display:"flex",flexDirection:"column",gap:12}}>
626
- <div className="card" style={{padding:16}}>
627
- <div className="tag" style={{marginBottom:8}}>ุฅุถุงูุฉ ุตูˆุช ุฌุฏูŠุฏ ู„ู„ู…ูƒุชุจุฉ</div>
628
- <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:12,marginBottom:12}}>
629
- <FileZone label="ุนูŠู†ุฉ 1" file={file1} onFile={setFile1} />
630
- <FileZone label="ุนูŠู†ุฉ 2 (ุงุฎุชูŠุงุฑูŠ)" file={file2} onFile={setFile2} />
631
- </div>
632
- <div style={{display:"flex",gap:8}}>
633
- <input type="text"
634
- placeholder="ุงุณู… ุงู„ุตูˆุชโ€ฆ"
635
- value={saveName} onChange={e=>setSaveName(e.target.value)}
636
- style={{flex:1,background:"var(--bg)",border:"1px solid var(--border)",
637
- color:"var(--text)",borderRadius:6,padding:"8px 12px",
638
- fontSize:13,fontFamily:"var(--sans)",outline:"none"}} />
639
- <button className="btn-primary" style={{width:"auto",padding:"8px 20px"}}
640
- onClick={saveVoice}>
641
- ุญูุธ
642
- </button>
643
- </div>
644
- {saveStatus && <div style={{marginTop:8,fontSize:12,
645
- color:"var(--green)"}}>{saveStatus}</div>}
646
- </div>
647
-
648
- {voices.length===0
649
- ? <div style={{textAlign:"center",color:"var(--muted)",
650
- padding:"40px 0",fontSize:14}}>
651
- ู„ุง ุชูˆุฌุฏ ุฃุตูˆุงุช ู…ุญููˆุธุฉ ุจุนุฏ.
652
- </div>
653
- : voices.map(v=>(
654
- <div key={v} className="voice-card"
655
- onClick={()=>{setSelVoice(selVoice===v?null:v);setTab("generate");}}>
656
- <div>
657
- <div style={{fontSize:14,fontWeight:600}}>{v}</div>
658
- <div style={{fontSize:11,color:"var(--muted)",marginTop:2,
659
- fontFamily:"var(--mono)"}}>
660
- {selVoice===v?"โ— ู…ุญุฏุฏ":"ุงู†ู‚ุฑ ู„ู„ุงุณุชุฎุฏุงู…"}
661
- </div>
662
- </div>
663
- <button className="btn-ghost"
664
- onClick={e=>{e.stopPropagation();deleteVoice(v);}}>
665
- ุญุฐู
666
- </button>
667
- </div>
668
- ))}
669
- </div>
670
- )}
671
-
672
- {tab==="history" && (
673
- <div style={{display:"flex",flexDirection:"column",gap:10}}>
674
- {history.length===0
675
- ? <div style={{textAlign:"center",color:"var(--muted)",
676
- padding:"40px 0",fontSize:14}}>
677
- ุงู„ุณุฌู„ ูุงุฑุบ.
678
- </div>
679
- : [...history].reverse().map((h,i)=>(
680
- <div key={i} className="history-row">
681
- <div style={{flex:1,minWidth:0}}>
682
- <div style={{fontSize:12,color:"var(--text)",
683
- overflow:"hidden",textOverflow:"ellipsis",
684
- whiteSpace:"nowrap",direction:"rtl"}}>
685
- {h.text}
686
- </div>
687
- <div style={{display:"flex",gap:8,marginTop:4,flexWrap:"wrap"}}>
688
- <span className="badge">{h.language}</span>
689
- <span style={{fontSize:10,color:"var(--muted)",
690
- fontFamily:"var(--mono)"}}>
691
- {new Date(h.ts*1000).toLocaleString("ar-EG")}
692
- </span>
693
- </div>
694
- </div>
695
- <div style={{display:"flex",gap:6,flexShrink:0}}>
696
- <button className="btn-ghost"
697
- onClick={()=>{setAudioUrl(`/audio/${h.filename}`);
698
- setAudioFilename(h.filename);
699
- setTab("generate");
700
- setStatus("done");}}>
701
- โ–ถ
702
- </button>
703
- <a href={`/audio/${h.filename}`} download={h.filename}
704
- style={{textDecoration:"none"}}>
705
- <button className="btn-ghost">โฌ‡</button>
706
- </a>
707
- </div>
708
- </div>
709
- ))}
710
- </div>
711
- )}
712
-
713
- <div style={{marginTop:32,textAlign:"center",fontSize:11,
714
- color:"var(--muted)",fontFamily:"var(--mono)"}}>
715
- XTTS v2 ยท Coqui TTS ยท {device.toUpperCase()} inference
716
- </div>
717
- </div>
718
- );
719
- }
720
-
721
- ReactDOM.createRoot(document.getElementById("root")).render(<App/>);
722
- </script>
723
  </body>
724
  </html>
725
  """
726
 
727
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•๏ฟฝ๏ฟฝ๏ฟฝโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
728
- # Routes
729
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
730
-
731
  @app.get("/", response_class=HTMLResponse)
732
  async def ui():
733
- page = HTML \
734
- .replace("LANGUAGES_JSON", json.dumps(LANGUAGES, ensure_ascii=False)) \
735
- .replace('device="{device}"', f'device="{device}"') \
736
- .replace("{device}", device)
737
- page = page.replace(
738
- 'const { useState, useEffect, useRef, useCallback } = React;',
739
- f'const {{ useState, useEffect, useRef, useCallback }} = React;\n'
740
- f'const device = "{device}";'
741
- )
742
- return page
743
 
744
  @app.post("/generate")
745
  async def generate(
746
- text: str = Form(...),
747
- language: str = Form("ar"),
748
- temperature: float = Form(0.75),
749
- speed: float = Form(1.0),
750
- top_k: int = Form(50),
751
- top_p: float = Form(0.85),
752
- repetition_penalty: float = Form(5.0),
753
- enable_text_splitting: bool = Form(True),
754
- voice_name: str = Form(None),
755
- files: list[UploadFile] = File(default=[]),
756
  ):
757
- if not text.strip():
758
- raise HTTPException(400, "ุงู„ู†ุต ูุงุฑุบ.")
759
-
760
  ref_paths = []
761
- tmp_files = []
762
-
763
- for f in files:
764
- path = f"/tmp/ref_{uuid.uuid4().hex}_{f.filename}"
765
- with open(path, "wb") as buf:
766
- shutil.copyfileobj(f.file, buf)
767
- ref_paths.append(path)
768
- tmp_files.append(path)
769
-
770
- if voice_name:
771
- lib_dir = os.path.join(VOICE_LIB, voice_name)
772
- if os.path.isdir(lib_dir):
773
- ref_paths += [
774
- os.path.join(lib_dir, fn)
775
- for fn in os.listdir(lib_dir)
776
- if fn.lower().endswith((".wav",".mp3",".flac",".ogg"))
777
- ]
778
-
779
- if not ref_paths:
780
- raise HTTPException(400, "ูŠุฌุจ ุชุญุฏูŠุฏ ุนูŠู†ุฉ ุตูˆุชูŠุฉ ู…ุฑุฌุนูŠุฉ.")
781
-
782
- out_name = f"gen_{uuid.uuid4().hex[:8]}.wav"
783
- out_path = os.path.join(OUTPUT_DIR, out_name)
784
 
785
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
786
  xtts_engine.tts_to_file(
787
  text=text,
788
  speaker_wav=ref_paths,
789
  language=language,
790
  file_path=out_path,
791
- temperature=float(temperature),
792
- speed=float(speed),
793
- top_k=int(top_k),
794
- top_p=float(top_p),
795
- repetition_penalty=float(repetition_penalty),
796
- enable_text_splitting=bool(enable_text_splitting),
797
  )
798
- finally:
799
- for p in tmp_files:
800
- try: os.remove(p)
801
- except: pass
802
-
803
- hist = load_history()
804
- hist.append({
805
- "filename": out_name,
806
- "text": text[:120],
807
- "language": language,
808
- "ts": int(time.time()),
809
- "params": dict(temperature=temperature, speed=speed,
810
- top_k=top_k, top_p=top_p,
811
- repetition_penalty=repetition_penalty),
812
- })
813
- save_history(hist)
814
-
815
- # Sync after generating and saving history
816
- background_sync()
817
 
818
- return {"filename": out_name}
 
 
 
 
 
 
 
 
 
819
 
820
  @app.get("/audio/{filename}")
821
- def get_audio(filename: str):
822
- path = os.path.join(OUTPUT_DIR, filename)
823
- if not os.path.exists(path):
824
- raise HTTPException(404, "File not found.")
825
- return FileResponse(path, media_type="audio/wav")
826
 
827
  @app.get("/history")
828
- def get_history():
829
- return JSONResponse(load_history())
830
 
831
  @app.get("/voices")
832
- def list_voices():
833
- if not os.path.isdir(VOICE_LIB):
834
- return []
835
- return [d for d in os.listdir(VOICE_LIB)
836
- if os.path.isdir(os.path.join(VOICE_LIB, d))]
837
 
838
  @app.post("/voices/save")
839
- async def save_voice(
840
- name: str = Form(...),
841
- file: UploadFile = File(...),
842
- file2: UploadFile = File(default=None),
843
- ):
844
- safe = name.strip().replace("/", "_").replace("..", "_")
845
- lib_dir = os.path.join(VOICE_LIB, safe)
846
- os.makedirs(lib_dir, exist_ok=True)
847
- for f in ([file, file2] if file2 else [file]):
848
- dest = os.path.join(lib_dir, f.filename)
849
- with open(dest, "wb") as buf:
850
- shutil.copyfileobj(f.file, buf)
851
-
852
- # Sync after saving a new voice
853
  background_sync()
854
-
855
- return {"name": safe}
856
-
857
- @app.delete("/voices/{name}")
858
- def delete_voice(name: str):
859
- lib_dir = os.path.join(VOICE_LIB, name)
860
- if os.path.isdir(lib_dir):
861
- shutil.rmtree(lib_dir)
862
- # Sync after deleting a voice
863
- background_sync()
864
- return {"deleted": name}
865
-
866
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
867
- # Launch
868
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
869
- def _run():
870
- nest_asyncio.apply()
871
- # Hugging Face Spaces Default Port is 7860
872
- uvicorn.run(app, host="0.0.0.0", port=7860, log_level="warning")
873
 
 
874
  if __name__ == "__main__":
875
- print("[*] Starting XTTS Studio on Hugging Face Spaces...")
876
- _run()
 
6
  from fastapi.middleware.cors import CORSMiddleware
7
  from huggingface_hub import HfApi, snapshot_download
8
 
9
+ # โ”€โ”€ ุงู„ุจูŠุฆุฉ ูˆุงู„ุชูƒูˆูŠู† โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
10
  os.environ["COQUI_TOS_AGREED"] = "1"
11
 
12
+ DATA_DIR = "data"
13
+ VOICE_LIB = os.path.join(DATA_DIR, "voice_library")
14
+ OUTPUT_DIR = os.path.join(DATA_DIR, "outputs")
 
15
  HISTORY_FILE = os.path.join(DATA_DIR, "history.json")
16
 
17
  for d in [DATA_DIR, VOICE_LIB, OUTPUT_DIR]:
18
  os.makedirs(d, exist_ok=True)
19
 
 
20
  HF_TOKEN = os.environ.get("HF_TOKEN")
21
+ HF_DATASET_REPO = os.environ.get("HF_DATASET_REPO")
22
 
23
  def pull_data_from_hf():
24
+ """ุชุญู…ูŠู„ ุงู„ุจูŠุงู†ุงุช ู…ู† Dataset ุนู†ุฏ ุงู„ุชุดุบูŠู„"""
25
  if HF_TOKEN and HF_DATASET_REPO:
26
  try:
27
+ print(f"[*] ุฌุงุฑูŠ ุชุญู…ูŠู„ ุงู„ุจูŠุงู†ุงุช ู…ู†: {HF_DATASET_REPO}...")
28
  snapshot_download(
29
  repo_id=HF_DATASET_REPO,
30
  repo_type="dataset",
31
  local_dir=DATA_DIR,
32
  token=HF_TOKEN
33
  )
34
+ print("[โœ“] ุชู… ุงู„ุชุญู…ูŠู„ ุจู†ุฌุงุญ.")
35
  except Exception as e:
36
+ print(f"[!] ุชู†ุจูŠู‡: ู„ู… ูŠุชู… ู…ุฒุงู…ู†ุฉ ุงู„ุจูŠุงู†ุงุช (ู‚ุฏ ุชูƒูˆู† ุงู„ู…ุณุงุญุฉ ุฌุฏูŠุฏุฉ): {e}")
37
 
38
  def push_data_to_hf():
39
+ """ุฑูุน ุงู„ุจูŠุงู†ุงุช ุฅู„ู‰ Dataset ููŠ ุงู„ุฎู„ููŠุฉ"""
40
  if HF_TOKEN and HF_DATASET_REPO:
41
  try:
42
  api = HfApi()
 
45
  repo_id=HF_DATASET_REPO,
46
  repo_type="dataset",
47
  token=HF_TOKEN,
48
+ commit_message=f"Auto-sync {time.strftime('%Y-%m-%d %H:%M:%S')}"
49
  )
 
50
  except Exception as e:
51
+ print(f"[!] ุฎุทุฃ ููŠ ุงู„ู…ุฒุงู…ู†ุฉ ุงู„ุฎู„ููŠุฉ: {e}")
52
 
53
  def background_sync():
 
54
  threading.Thread(target=push_data_to_hf, daemon=True).start()
55
 
56
+ # ุชู†ููŠุฐ ุงู„ุณุญุจ ุงู„ุฃูˆู„ูŠ
57
  pull_data_from_hf()
58
 
59
+ # โ”€โ”€ ุฅุนุฏุงุฏ ู…ุญุฑูƒ TTS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
 
 
 
 
 
60
  try:
61
  from TTS.api import TTS
62
  except ImportError:
 
66
  import torch
67
  device = "cuda" if torch.cuda.is_available() else "cpu"
68
 
69
+ print(f"[*] ุฌุงุฑูŠ ุชุญู…ูŠู„ ู†ู…ูˆุฐุฌ XTTS v2 ุนู„ู‰ {device.upper()}...")
70
+ xtts_engine = TTS("tts_models/multilingual/multi-dataset/xtts_v2").to(device)
71
+ print("[โœ“] ุงู„ู†ู…ูˆุฐุฌ ุฌุงู‡ุฒ ู„ู„ุงุณุชุฎุฏุงู….")
 
72
 
73
+ # โ”€โ”€ ุงู„ู…ุณุงุนุฏูˆู† โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
74
  def load_history():
75
  if os.path.exists(HISTORY_FILE):
76
  try:
77
+ with open(HISTORY_FILE, "r", encoding="utf-8") as f:
78
+ return json.load(f)
79
+ except: return []
80
  return []
81
 
82
  def save_history(h):
83
  with open(HISTORY_FILE, "w", encoding="utf-8") as f:
84
  json.dump(h, f, ensure_ascii=False, indent=2)
85
 
86
+ # โ”€โ”€ ุชุทุจูŠู‚ FastAPI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
87
+ app = FastAPI(title="XTTS Studio Pro")
88
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
 
89
 
 
90
  LANGUAGES = {
91
+ "ar": "ุงู„ุนุฑุจูŠุฉ", "en": "English", "es": "Espaรฑol", "fr": "Franรงais",
92
+ "de": "Deutsch", "it": "Italiano", "pt": "Portuguรชs", "ru": "ะ ัƒััะบะธะน",
93
+ "zh-cn": "ไธญๆ–‡", "ja": "ๆ—ฅๆœฌ่ชž", "ko": "ํ•œ๊ตญ์–ด", "tr": "Tรผrkรงe"
 
 
 
94
  }
95
 
96
+ # โ”€โ”€ ูˆุงุฌู‡ุฉ ุงู„ู…ุณุชุฎุฏู… (React) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
97
+ HTML_TEMPLATE = r"""
98
+ <!DOCTYPE html>
 
99
  <html lang="ar" dir="rtl">
100
  <head>
101
+ <meta charset="UTF-8">
102
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
103
+ <title>XTTS Voice Studio</title>
104
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@300;400;600;700&display=swap" rel="stylesheet">
105
+ <script src="https://cdn.tailwindcss.com"></script>
106
+ <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
107
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
108
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
109
+ <style>
110
+ :root { --bg: #0b0c0f; --surface: #13151a; --border: #1f2330; --amber: #f5a623; --text: #e8eaf0; --muted: #6b7280; }
111
+ body { background: var(--bg); color: var(--text); font-family: 'IBM Plex Sans Arabic', sans-serif; }
112
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; }
113
+ .btn-primary { background: var(--amber); color: #000; font-weight: 700; border-radius: 8px; transition: all 0.2s; }
114
+ .btn-primary:hover { opacity: 0.9; transform: translateY(-1px); }
115
+ .btn-ghost { border: 1px solid var(--border); color: var(--muted); border-radius: 6px; transition: all 0.2s; }
116
+ .btn-ghost:hover { border-color: var(--amber); color: var(--text); }
117
+ .tab-active { background: rgba(245,166,35,0.1); color: var(--amber); border: 1px solid var(--amber); }
118
+ audio { width: 100%; border-radius: 8px; filter: invert(90%) hue-rotate(180deg); }
119
+ </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  </head>
121
  <body>
122
+ <div id="root"></div>
123
+ <script type="text/babel">
124
+ const { useState, useEffect } = React;
125
+ const device = "{DEVICE_NAME}";
126
+
127
+ function App() {
128
+ const [tab, setTab] = useState("generate");
129
+ const [text, setText] = useState("");
130
+ const [lang, setLang] = useState("ar");
131
+ const [file1, setFile1] = useState(null);
132
+ const [voices, setVoices] = useState([]);
133
+ const [selVoice, setSelVoice] = useState(null);
134
+ const [loading, setLoading] = useState(false);
135
+ const [audioUrl, setAudioUrl] = useState(null);
136
+ const [history, setHistory] = useState([]);
137
+
138
+ useEffect(() => {
139
+ refreshData();
140
+ }, []);
141
+
142
+ const refreshData = () => {
143
+ fetch("/voices").then(r => r.json()).then(setVoices);
144
+ fetch("/history").then(r => r.json()).then(setHistory);
145
+ };
146
+
147
+ const handleGenerate = async () => {
148
+ if (!text || (!file1 && !selVoice)) return alert("ูŠุฑุฌู‰ ุฅุฏุฎุงู„ ุงู„ู†ุต ูˆุงุฎุชูŠุงุฑ ุตูˆุช ู…ุฑุฌุนูŠ");
149
+ setLoading(true);
150
+ const fd = new FormData();
151
+ fd.append("text", text);
152
+ fd.append("language", lang);
153
+ if (file1) fd.append("files", file1);
154
+ if (selVoice) fd.append("voice_name", selVoice);
155
+
156
+ try {
157
+ const res = await fetch("/generate", { method: "POST", body: fd });
158
+ const data = await res.json();
159
+ setAudioUrl(`/audio/${data.filename}`);
160
+ refreshData();
161
+ } catch (e) { alert("ุญุฏุซ ุฎุทุฃ ุฃุซู†ุงุก ุงู„ุชูˆู„ูŠุฏ"); }
162
+ setLoading(false);
163
+ };
164
+
165
+ return (
166
+ <div className="max-w-3xl mx-auto p-6">
167
+ <header className="text-center mb-8">
168
+ <h1 className="text-3xl font-bold text-[#f5a623]">ุงุณุชูˆุฏูŠูˆ XTTS v2</h1>
169
+ <p className="text-sm text-gray-500 mt-2">ูŠุนู…ู„ ุนู„ู‰: <span className="uppercase text-green-500">{device}</span></p>
170
+ </header>
171
+
172
+ <nav className="flex gap-2 mb-6 card p-1">
173
+ {[{id:"generate", n:"โšก ุชูˆู„ูŠุฏ"}, {id:"library", n:"๐Ÿ“š ุงู„ู…ูƒุชุจุฉ"}, {id:"history", n:"๐Ÿ•˜ ุงู„ุณุฌู„"}].map(t => (
174
+ <button key={t.id} onClick={() => setTab(t.id)} className={`flex-1 py-2 rounded-lg transition ${tab === t.id ? 'tab-active' : 'text-gray-500'}`}>
175
+ {t.n}
176
+ </button>
177
+ ))}
178
+ </nav>
179
+
180
+ {tab === "generate" && (
181
+ <div className="space-y-4">
182
+ <div className="card p-4">
183
+ <div className="flex justify-between mb-2">
184
+ <label className="text-xs text-gray-400 uppercase">ุงู„ู†ุต ุงู„ู…ุฑุงุฏ ุชุญูˆูŠู„ู‡</label>
185
+ <select className="bg-transparent text-sm text-amber-500 outline-none" value={lang} onChange={e => setLang(e.target.value)}>
186
+ {Object.entries({LANGUAGES_JSON}).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
187
+ </select>
188
+ </div>
189
+ <textarea className="w-full bg-[#0b0c0f] border border-[#1f2330] rounded-lg p-3 outline-none focus:border-amber-500 transition" rows="4" value={text} onChange={e => setText(e.target.value)} placeholder="ุงูƒุชุจ ู…ุง ุชุฑูŠุฏ ู‡ู†ุง..."></textarea>
190
+ </div>
191
+
192
+ <div className="card p-4">
193
+ <label className="text-xs text-gray-400 uppercase block mb-3">ุงู„ุตูˆุช ุงู„ู…ุฑุฌุนูŠ</label>
194
+ <input type="file" accept="audio/*" onChange={e => setFile1(e.target.files[0])} className="text-sm text-gray-500 mb-4 block w-full" />
195
+
196
+ {voices.length > 0 && (
197
+ <div className="flex flex-wrap gap-2">
198
+ {voices.map(v => (
199
+ <button key={v} onClick={() => setSelVoice(v === selVoice ? null : v)} className={`px-3 py-1 rounded-full text-xs border ${selVoice === v ? 'border-amber-500 text-amber-500 bg-amber-500/10' : 'border-gray-700 text-gray-500'}`}>
200
+ {v}
201
+ </button>
202
+ ))}
203
+ </div>
204
+ )}
205
+ </div>
206
+
207
+ <button onClick={handleGenerate} disabled={loading} className="btn-primary w-full py-4 text-lg">
208
+ {loading ? "ุฌุงุฑูŠ ุงู„ู…ุนุงู„ุฌุฉ..." : "ุชูˆู„ูŠุฏ ุงู„ุตูˆุช ุงู„ุขู†"}
209
+ </button>
210
+
211
+ {audioUrl && (
212
+ <div className="card p-4 mt-4 animate-pulse">
213
+ <audio src={audioUrl} controls autoPlay />
214
+ <a href={audioUrl} download className="block text-center mt-2 text-xs text-amber-500">ุชุญู…ูŠู„ ุงู„ู…ู„ู ุงู„ู…ูˆู„ุฏ</a>
215
+ </div>
216
+ )}
217
+ </div>
218
+ )}
219
+
220
+ {tab === "history" && (
221
+ <div className="space-y-2">
222
+ {history.length === 0 ? <p className="text-center text-gray-600 py-10">ู„ุง ูŠูˆุฌุฏ ุณุฌู„ ุญุงู„ูŠุงู‹</p> :
223
+ history.slice().reverse().map((h, i) => (
224
+ <div key={i} className="card p-3 flex justify-between items-center">
225
+ <div className="overflow-hidden">
226
+ <p className="text-sm truncate w-64">{h.text}</p>
227
+ <span className="text-[10px] text-gray-600 uppercase">{h.language} โ€ข {new Date(h.ts*1000).toLocaleTimeString()}</span>
228
+ </div>
229
+ <button onClick={() => { setAudioUrl(`/audio/${h.filename}`); setTab("generate"); }} className="btn-ghost px-3 py-1 text-xs">ุชุดุบูŠู„</button>
230
+ </div>
231
+ ))}
232
+ </div>
233
+ )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  </div>
235
+ );
236
+ }
237
+ ReactDOM.createRoot(document.getElementById("root")).render(<App />);
238
+ </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  </body>
240
  </html>
241
  """
242
 
243
+ # โ”€โ”€ ุงู„ู…ุณุงุฑุงุช โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
 
244
  @app.get("/", response_class=HTMLResponse)
245
  async def ui():
246
+ content = HTML_TEMPLATE.replace("{LANGUAGES_JSON}", json.dumps(LANGUAGES, ensure_ascii=False))
247
+ content = content.replace("{DEVICE_NAME}", device)
248
+ return content
 
 
 
 
 
 
 
249
 
250
  @app.post("/generate")
251
  async def generate(
252
+ text: str = Form(...),
253
+ language: str = Form("ar"),
254
+ voice_name: str = Form(None),
255
+ files: list[UploadFile] = File(default=[])
 
 
 
 
 
 
256
  ):
 
 
 
257
  ref_paths = []
258
+ temp_dir = f"temp_{uuid.uuid4().hex}"
259
+ os.makedirs(temp_dir, exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
 
261
  try:
262
+ # ุงู„ุชุนุงู…ู„ ู…ุน ุงู„ู…ู„ูุงุช ุงู„ู…ุฑููˆุนุฉ
263
+ for f in files:
264
+ p = os.path.join(temp_dir, f.filename)
265
+ with open(p, "wb") as b: shutil.copyfileobj(f.file, b)
266
+ ref_paths.append(p)
267
+
268
+ # ุงู„ุชุนุงู…ู„ ู…ุน ุงู„ุฃุตูˆุงุช ุงู„ู…ุญููˆุธุฉ
269
+ if voice_name:
270
+ v_path = os.path.join(VOICE_LIB, voice_name)
271
+ if os.path.isdir(v_path):
272
+ ref_paths += [os.path.join(v_path, x) for x in os.listdir(v_path) if x.lower().endswith(('.wav', '.mp3'))]
273
+
274
+ if not ref_paths:
275
+ raise HTTPException(400, "ูŠุฌุจ ุชูˆููŠุฑ ุตูˆุช ู…ุฑุฌุนูŠ")
276
+
277
+ out_name = f"gen_{uuid.uuid4().hex[:8]}.wav"
278
+ out_path = os.path.join(OUTPUT_DIR, out_name)
279
+
280
  xtts_engine.tts_to_file(
281
  text=text,
282
  speaker_wav=ref_paths,
283
  language=language,
284
  file_path=out_path,
285
+ enable_text_splitting=True
 
 
 
 
 
286
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
 
288
+ # ุชุญุฏูŠุซ ุงู„ุณุฌู„
289
+ hist = load_history()
290
+ hist.append({"filename": out_name, "text": text[:100], "language": language, "ts": int(time.time())})
291
+ save_history(hist)
292
+
293
+ background_sync()
294
+ return {"filename": out_name}
295
+
296
+ finally:
297
+ shutil.rmtree(temp_dir, ignore_errors=True)
298
 
299
  @app.get("/audio/{filename}")
300
+ async def get_audio(filename: str):
301
+ p = os.path.join(OUTPUT_DIR, filename)
302
+ if os.path.exists(p): return FileResponse(p)
303
+ raise HTTPException(404)
 
304
 
305
  @app.get("/history")
306
+ def get_history(): return load_history()
 
307
 
308
  @app.get("/voices")
309
+ def list_voices(): return [d for d in os.listdir(VOICE_LIB) if os.path.isdir(os.path.join(VOICE_LIB, d))]
 
 
 
 
310
 
311
  @app.post("/voices/save")
312
+ async def save_voice(name: str = Form(...), file: UploadFile = File(...)):
313
+ v_dir = os.path.join(VOICE_LIB, name.strip())
314
+ os.makedirs(v_dir, exist_ok=True)
315
+ dest = os.path.join(v_dir, file.filename)
316
+ with open(dest, "wb") as b: shutil.copyfileobj(file.file, b)
 
 
 
 
 
 
 
 
 
317
  background_sync()
318
+ return {"status": "saved"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
 
320
+ # โ”€โ”€ ุงู„ุชุดุบูŠู„ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
321
  if __name__ == "__main__":
322
+ nest_asyncio.apply()
323
+ uvicorn.run(app, host="0.0.0.0", port=7860)