Hamed744 commited on
Commit
f5cf77c
·
verified ·
1 Parent(s): 195e789

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +443 -454
index.html CHANGED
@@ -9,106 +9,100 @@
9
 
10
  :root {
11
  --app-font: 'Vazirmatn', sans-serif;
12
- --app-bg: #F0F4F8; /* Slightly lighter app background */
13
  --panel-bg: #FFFFFF;
14
- --panel-border: #DEE7F0; /* Softer panel border */
15
- --text-primary: #0A0F1A; /* Darker primary text for better contrast */
16
- --text-secondary: #5A687F; /* Slightly adjusted secondary text */
17
- --accent-primary: #3B82F6;
18
- --accent-primary-hover: #2563EB;
19
- --accent-secondary: #10B981;
20
  --accent-secondary-hover: #059669;
21
- --input-bg: #F7FAFC; /* Lighter input background */
22
- --input-border: #CFD8E3;
23
  --input-border-focus: var(--accent-primary);
24
-
25
- --radius-card: 28px; /* Slightly larger card radius */
26
- --radius-input: 16px; /* Slightly larger input radius */
27
-
28
- --shadow-subtle: 0 2px 6px rgba(20, 26, 36, 0.03);
29
- --shadow-medium: 0 5px 15px -3px rgba(20, 26, 36, 0.07), 0 3px 8px -4px rgba(20,26,36,0.04);
30
- --shadow-strong: 0 10px 25px -5px rgba(20, 26, 36, 0.1), 0 8px 15px -8px rgba(20,26,36,0.06);
31
- --shadow-interactive: 0 0 0 3px color-mix(in srgb, var(--accent-primary) 20%, transparent);
32
 
33
- --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
34
- --transition-bounce: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
 
 
 
 
 
35
  }
36
 
37
- *, *::before, *::after {
38
- box-sizing: border-box;
39
- -webkit-font-smoothing: antialiased;
40
- -moz-osx-font-smoothing: grayscale;
 
 
 
41
  }
42
 
 
43
  body {
44
  font-family: var(--app-font);
45
  direction: rtl;
46
  background-color: var(--app-bg);
47
  color: var(--text-primary);
48
  font-size: 16px;
49
- line-height: 1.8;
50
  margin: 0;
51
- padding: 3rem 0;
52
  min-height: 100vh;
 
 
53
  display: flex;
54
  justify-content: center;
55
  align-items: flex-start;
56
- overflow-x: hidden; /* Prevent horizontal scroll from animations */
57
  }
58
 
59
- .container {
60
- max-width: 780px;
61
- width: 92%;
62
  margin: 0 auto;
63
- opacity: 0;
64
- transform: translateY(20px);
65
- animation: fadeInSlideUp 0.6s 0.2s ease-out forwards;
66
  }
67
 
68
- @keyframes fadeInSlideUp {
69
- to {
70
- opacity: 1;
71
- transform: translateY(0);
72
- }
73
- }
74
-
75
- .app-header {
76
- padding: 0.5rem 0 2.5rem 0;
77
- text-align: center;
78
- margin-bottom: 2.5rem;
79
- }
80
- .app-header h1 {
81
- font-size: 2.6em; /* Reduced size */
82
- font-weight: 900;
83
- margin:0 0 0.75rem 0;
84
- background: linear-gradient(60deg, var(--accent-primary), var(--accent-secondary));
85
  -webkit-background-clip: text;
86
  -webkit-text-fill-color: transparent;
87
- letter-spacing: -0.8px;
88
  }
89
- .app-header p {
90
- font-size: 1.15em;
91
- color: var(--text-secondary);
92
- margin-top:0;
93
- opacity: 0.9;
94
  font-weight: 400;
95
  }
96
 
97
- .main-content {
98
  padding: 2.5rem; /* Adjusted padding */
99
- background-color: var(--panel-bg);
100
- border-radius: var(--radius-card);
101
- box-shadow: var(--shadow-strong);
102
  border: 1px solid var(--panel-border);
 
103
  }
104
 
105
- .form-group { margin-bottom: 2rem; }
 
106
  .label-with-info {
107
  display: flex;
108
  align-items: center;
109
- justify-content: space-between; /* Align info icon to the right */
110
  gap: 0.5rem;
111
- margin-bottom: 0.8rem; /* Reduced margin */
112
  }
113
  .label-with-info label {
114
  margin-bottom: 0;
@@ -118,24 +112,25 @@
118
  display: inline-flex;
119
  align-items: center;
120
  justify-content: center;
121
- width: 24px;
122
- height: 24px;
123
  border-radius: 50%;
124
- background-color: color-mix(in srgb, var(--app-bg) 50%, var(--panel-border));
125
  border: 1px solid var(--panel-border);
126
  color: var(--text-secondary);
127
  font-size: 0.9em;
128
  font-weight: 700;
129
- cursor: help; /* Changed cursor */
130
  position: relative;
131
  transition: var(--transition-smooth);
132
  user-select: none;
133
  }
134
- .info-tooltip-icon:hover, .info-tooltip-icon.active {
135
  background-color: var(--accent-primary);
136
  color: white;
137
  border-color: var(--accent-primary);
138
  transform: scale(1.1);
 
139
  }
140
  .tooltip-text {
141
  visibility: hidden;
@@ -143,16 +138,16 @@
143
  background-color: var(--text-primary);
144
  color: #fff;
145
  text-align: right;
146
- border-radius: calc(var(--radius-input) - 4px); /* Smaller radius for tooltip */
147
- padding: 12px 18px;
148
  position: absolute;
149
  z-index: 10;
150
- bottom: 140%;
151
  left: 50%;
152
- transform: translateX(-50%) translateY(10px); /* Initial position for animation */
153
  opacity: 0;
154
- transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s ease;
155
- font-size: 0.85em;
156
  font-weight: 400;
157
  line-height: 1.6;
158
  box-shadow: var(--shadow-medium);
@@ -162,211 +157,199 @@
162
  position: absolute;
163
  top: 100%;
164
  left: 50%;
165
- margin-left: -6px;
166
- border-width: 6px;
167
  border-style: solid;
168
  border-color: var(--text-primary) transparent transparent transparent;
169
  }
170
  .info-tooltip-icon.active .tooltip-text {
171
  visibility: visible;
172
  opacity: 1;
173
- transform: translateX(-50%) translateY(0px);
174
  }
175
 
176
 
177
- label {
178
- display: block;
179
- font-weight: 700;
180
- color: var(--text-primary);
181
- font-size: 1.05em;
182
- margin-bottom: 0.8rem;
183
  }
184
 
185
- textarea, input[type="text"] {
186
- width: 100%;
187
- padding: 0.9rem 1.1rem;
188
- border-radius: var(--radius-input);
189
- border: 1px solid var(--input-border);
190
  background-color: var(--input-bg);
191
  color: var(--text-primary);
192
- box-shadow: var(--shadow-subtle) inset;
193
- font-family: var(--app-font);
194
- font-size: 1rem;
195
- box-sizing: border-box;
196
  transition: var(--transition-smooth);
197
  }
198
- textarea:focus, input[type="text"]:focus {
199
- outline: none;
200
- border-color: var(--input-border-focus);
201
- box-shadow: var(--shadow-interactive), var(--shadow-subtle) inset;
202
  background-color: #fff;
203
  }
204
- textarea { min-height: 130px; resize: vertical; }
205
 
206
  .char-counter-wrapper {
207
  font-size: 0.85em;
208
  color: var(--text-secondary);
209
- text-align: left; /* English numbers are LTR */
210
  margin-top: 0.5rem;
211
  padding: 0 0.2rem;
212
  }
213
- .char-counter-wrapper .limit-exceeded {
214
- color: #E53E3E; /* Red for error */
215
- font-weight: 600;
216
- }
217
 
218
 
219
- #selected-speaker-display { text-align: center; margin-top: 0.8rem; position: relative; }
220
- #selected-speaker-card {
221
- display: inline-flex;
222
- align-items: center;
223
- background: linear-gradient(135deg, var(--input-bg) 0%, #fff 90%);
224
- border-radius: var(--radius-card);
225
- padding: 0.9rem 1.1rem;
226
- box-shadow: var(--shadow-medium);
 
227
  border: 1px solid var(--panel-border);
228
- transition: var(--transition-bounce); /* Use bounce transition */
229
- position: relative;
230
- margin-bottom: 1rem;
231
- cursor: pointer;
232
  }
233
  #selected-speaker-card:hover {
234
- transform: translateY(-5px) scale(1.03);
235
  box-shadow: var(--shadow-strong);
236
  }
237
- #selected-speaker-card.speaker-changed { /* Animation for speaker change */
238
- animation: pulseHighlight 0.7s ease-out;
239
- }
240
- @keyframes pulseHighlight {
241
- 0% { transform: scale(1); box-shadow: var(--shadow-medium); }
242
- 50% { transform: scale(1.05); box-shadow: 0 0 20px color-mix(in srgb, var(--accent-secondary) 40%, transparent); }
243
- 100% { transform: scale(1); box-shadow: var(--shadow-medium); }
244
- }
245
-
246
- #selected-speaker-card img {
247
- width: 70px; height: 70px; /* Slightly smaller */
248
- border-radius: 50%;
249
- object-fit: cover;
250
- margin-left: 18px;
251
- border: 3px solid var(--accent-secondary);
252
- box-shadow: 0 0 12px -2px color-mix(in srgb, var(--accent-secondary) 50%, transparent);
253
- background-color: #e0e0e0;
254
  transition: var(--transition-smooth);
255
  }
256
  #selected-speaker-card:hover img {
257
- transform: scale(1.08) rotate(3deg);
258
- border-color: color-mix(in srgb, var(--accent-secondary) 70%, var(--accent-primary) 30%);
259
  }
260
- #selected-speaker-info h3 { margin: 0; font-size: 1.3em; font-weight: 800; color: var(--text-primary); }
261
  #selected-speaker-info p { margin: 4px 0 0; color: var(--text-secondary); font-size: 0.88em; font-weight: 500; }
262
-
263
- #change-speaker-btn {
264
- display: inline-flex;
265
  align-items: center;
266
  justify-content: center;
267
- margin: 0 auto;
268
- padding: 10px 20px;
269
- border-radius: var(--radius-input);
270
- background: linear-gradient(45deg, var(--accent-primary), var(--accent-primary-hover));
271
- border: none;
272
- color: #fff;
273
- cursor: pointer;
274
- font-family: var(--app-font); font-weight: 600;
275
- font-size: 1rem;
276
- transition: var(--transition-smooth), transform 0.15s ease-out;
277
- box-shadow: 0 4px 10px -2px color-mix(in srgb, var(--accent-primary) 35%, transparent), var(--shadow-subtle);
278
- }
279
- #change-speaker-btn:hover {
280
  background: linear-gradient(45deg, var(--accent-primary-hover), var(--accent-primary));
281
- transform: translateY(-3px) scale(1.04);
282
- box-shadow: 0 6px 12px -3px color-mix(in srgb, var(--accent-primary) 45%, transparent), var(--shadow-medium);
 
 
 
 
 
 
 
 
 
 
283
  }
284
  #change-speaker-btn svg {
285
- width: 1.1em;
286
  height: 1.1em;
287
- margin-right: 0.5em;
288
  fill: currentColor;
289
  }
290
 
291
 
292
  /* --- مودال گالری گویندگان --- */
293
- #speaker-modal {
294
- position: fixed; top: 0; left: 0; width: 100%; height: 100%;
295
- background-color: rgba(10, 15, 26, 0.5); /* Darker backdrop */
296
- backdrop-filter: blur(8px) saturate(160%);
297
- display: none; align-items: center; justify-content: center;
298
- z-index: 1000; opacity: 0;
299
- transition: opacity 0.3s ease;
300
  }
301
  #speaker-modal.visible { display: flex; opacity: 1; }
302
- .modal-content {
303
- background: var(--panel-bg);
304
- padding: 2rem;
305
- border-radius: var(--radius-card);
306
- width: 90%; max-width: 700px; /* Slightly narrower */
307
- max-height: 85vh; overflow-y: auto;
308
- transform: scale(0.92) translateY(15px);
309
- transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.35s cubic-bezier(0.4, 0, 0.2, 1);
310
  border: 1px solid var(--panel-border);
311
  box-shadow: var(--shadow-strong);
312
  opacity: 0;
313
  }
314
  #speaker-modal.visible .modal-content { transform: scale(1) translateY(0); opacity: 1;}
315
- .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--panel-border); }
316
- .modal-header h2 { margin: 0; font-size: 1.6em; font-weight: 800; color: var(--accent-primary);}
317
- .close-modal-btn { background: none; border: none; font-size: 2.5rem; cursor: pointer; color: var(--text-secondary); transition: var(--transition-smooth); line-height: 1; padding:0; }
318
- .close-modal-btn:hover { color: var(--accent-primary); transform: rotate(90deg) scale(1.1); }
319
-
320
- #speaker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 1.2rem; }
321
  @media (min-width: 640px) { #speaker-grid { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); } }
322
  .speaker-card { cursor: pointer; transition: var(--transition-smooth); text-align: center; position: relative;}
323
- .speaker-card .speaker-visual {
324
- border: 2px solid transparent; /* Thinner border */
325
- border-radius: calc(var(--radius-card) - 4px);
326
- overflow: hidden;
327
- box-shadow: var(--shadow-subtle);
328
- position: relative;
329
- background-color: var(--input-bg);
330
- transition: var(--transition-smooth);
331
- padding: 6px; /* Reduced padding */
332
- }
333
- .speaker-card:hover .speaker-visual {
334
- transform: translateY(-5px) scale(1.04);
335
- box-shadow: var(--shadow-medium);
336
- border-color: color-mix(in srgb, var(--accent-primary) 30%, transparent);
337
  }
338
  .speaker-card input[type="radio"] { display: none; }
339
- .speaker-card img {
340
- width: 100%; height: 115px; /* Adjusted height */
341
- object-fit: cover; display: block;
342
- background-color: #e0e0e0;
343
  transition: transform 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
344
- border-radius: calc(var(--radius-card) - 12px);
345
  }
346
- .speaker-card:hover img { transform: scale(1.08); }
347
  .speaker-card .speaker-name { padding: 0.8rem 0.4rem 0.1rem; font-weight: 600; font-size: 0.9em; color: var(--text-secondary); transition: color 0.2s; }
348
-
349
- .speaker-card input[type="radio"]:checked + .speaker-visual {
350
- border-color: var(--accent-secondary);
351
- box-shadow: 0 0 18px -4px color-mix(in srgb, var(--accent-secondary) 60%, transparent);
352
- background: linear-gradient(145deg, var(--accent-secondary), var(--accent-primary));
353
  }
354
  .speaker-card input[type="radio"]:checked + .speaker-visual img {
355
- border: 2px solid white;
356
- transform: scale(1.05);
357
  }
358
- .speaker-card input[type="radio"]:checked + .speaker-visual .speaker-name {
359
- color: #fff;
360
- font-weight: 700;
361
  }
362
 
363
  /* --- Slider & Button & Output --- */
364
- .slider-container { display: flex; align-items: center; gap: 1.2rem; }
365
- input[type="range"] {
366
- flex-grow: 1; -webkit-appearance: none; appearance: none;
367
- width: 100%; height: 8px; /* Thinner slider */
368
- background: color-mix(in srgb, var(--panel-border) 60%, var(--app-bg));
369
- border-radius: 4px; outline: none;
370
  cursor: pointer;
371
  transition: background 0.2s;
372
  }
@@ -375,25 +358,25 @@
375
  height: 8px;
376
  border-radius: 4px;
377
  }
378
- input[type="range"]::-webkit-slider-thumb {
379
- -webkit-appearance: none; appearance: none;
380
- width: 22px; height: 22px;
381
- background: #fff;
382
- border-radius: 50%; cursor: pointer;
383
- border: 3px solid var(--accent-primary);
384
- box-shadow: 0 2px 6px rgba(0,0,0,0.15);
385
- margin-top: -7px;
386
  transition: transform 0.2s ease, box-shadow 0.2s ease;
387
  }
388
- input[type="range"]::-webkit-slider-thumb:hover {
389
- transform: scale(1.2);
390
- box-shadow: 0 3px 8px color-mix(in srgb, var(--accent-primary) 30%, transparent);
391
  }
392
- input[type="range"]::-moz-range-thumb {
393
- width: 22px; height: 22px;
394
- background: #fff;
395
- border-radius: 50%; cursor: pointer;
396
- border: 3px solid var(--accent-primary);
397
  box-shadow: 0 2px 6px rgba(0,0,0,0.15);
398
  }
399
  input[type="range"]::-moz-range-track {
@@ -402,142 +385,154 @@
402
  border-radius: 4px;
403
  border: none;
404
  }
405
- #temperature-value {
406
- font-weight: 700; background-color: var(--input-bg);
407
- padding: 0.5rem 1rem;
408
- border-radius: 10px;
409
- border: 1px solid var(--panel-border);
410
- min-width: 45px; text-align: center;
411
- color: var(--accent-primary);
412
- font-size: 1rem;
413
  box-shadow: var(--shadow-subtle);
414
- transition: var(--transition-smooth);
415
- }
416
- input[type="range"]:focus + #temperature-value {
417
- border-color: var(--accent-primary);
418
- box-shadow: var(--shadow-interactive);
419
  }
420
-
421
- #generate-btn {
422
  width: 100%; padding: 1rem 1.5rem; /* Adjusted padding */
423
- font-size: 1.25em; font-weight: 800;
424
- font-family: var(--app-font);
425
- background: linear-gradient(95deg, var(--accent-secondary) 0%, var(--accent-primary) 100%);
426
- color: #fff;
427
- border: none;
428
- border-radius: var(--radius-input);
429
- cursor: pointer;
430
- transition: var(--transition-smooth), transform 0.15s ease-out;
431
- box-shadow: 0 5px 15px -4px color-mix(in srgb, var(--accent-primary) 40%, transparent), 0 5px 15px -4px color-mix(in srgb, var(--accent-secondary) 30%, transparent);
432
  position: relative;
433
  overflow: hidden;
434
- letter-spacing: 0.3px;
435
  }
436
- #generate-btn::before {
437
  content: ''; position: absolute;
438
- top: 0; left: -150%; width: 60%; height: 100%;
439
  background: linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.25) 50%, rgba(255,255,255,0) 100%);
440
- transform: skewX(-20deg);
441
- transition: left 0.75s cubic-bezier(0.23, 1, 0.32, 1);
442
  }
443
- #generate-btn:hover::before { left: 150%; }
444
- #generate-btn:hover:not(:disabled) {
445
- transform: translateY(-3px) scale(1.02);
446
- box-shadow: 0 8px 20px -5px color-mix(in srgb, var(--accent-primary) 50%, transparent), 0 8px 20px -5px color-mix(in srgb, var(--accent-secondary) 40%, transparent);
447
  }
448
  #generate-btn:active:not(:disabled) {
449
- transform: translateY(-1px) scale(0.99);
450
  }
451
- #generate-btn:disabled {
452
- background: #B8C2CC;
453
- cursor: not-allowed; box-shadow: none; color: #DEE7F0;
454
  transform: none;
455
  }
456
  #generate-btn:disabled::before { display: none; }
457
- #generate-btn svg.loader-icon { /* Style for loader icon in button */
458
- display:inline-block; margin-left: 0.5em; width:1.1em; height:1.1em;
459
  vertical-align: middle;
460
- animation: spin 1s linear infinite;
461
  }
462
- @keyframes spin { to { transform: rotate(360deg); } }
463
-
464
 
465
- #output-section {
466
- margin-top: 3rem;
467
- padding: 2rem;
468
- background-color: var(--input-bg);
469
- border-radius: var(--radius-card);
470
- min-height: 200px;
471
- display: flex; align-items: center; justify-content: center;
472
- flex-direction: column; gap: 1.2rem;
473
- border: 2px dashed var(--panel-border);
474
- transition: all 0.4s ease;
475
  position: relative;
476
- box-shadow: var(--shadow-subtle) inset;
477
  }
478
- #output-section.has-content {
479
- background-color: #fff;
480
  border-style: solid;
481
  border-color: var(--accent-secondary);
482
- box-shadow: 0 0 25px -5px color-mix(in srgb, var(--accent-secondary) 20%, transparent);
483
- padding: 1.5rem;
484
  }
485
  #output-section.has-content #status-message,
486
  #output-section.has-content #loading-animation-wrapper {
487
- display: none !important;
488
  }
489
 
490
- #status-message { font-weight: 500; color: var(--text-secondary); text-align: center; font-size: 1.1em; }
491
  #audio-player { width: 100%; margin-top: 0; display: none; border-radius: 10px; box-shadow: var(--shadow-medium); }
492
  #output-section.has-content #audio-player {
493
- margin-top: 0;
494
  }
495
- #audio-player::-webkit-media-controls-panel { background-color: #fff; border-radius: 10px; padding: 5px; box-shadow: var(--shadow-subtle) inset;}
496
- #audio-player::-webkit-media-controls-play-button { color: var(--accent-primary); background-color: color-mix(in srgb, var(--accent-primary) 10%, transparent); border-radius: 50%; margin: 5px;}
497
- #audio-player::-webkit-media-controls-current-time-display,
 
498
  #audio-player::-webkit-media-controls-time-remaining-display { color: var(--text-secondary); font-weight: 500; }
499
- #audio-player::-webkit-media-controls-timeline { background-color: color-mix(in srgb, var(--panel-border) 70%, var(--app-bg)); border-radius: 5px; margin: 0 10px;}
 
500
 
501
  /* --- انیمیشن پردازش --- */
502
  #loading-animation-wrapper {
503
- display: none;
504
  flex-direction: column;
505
  align-items: center;
506
  justify-content: center;
507
  gap: 1.8rem; /* Adjusted gap */
508
  width: 100%;
509
- min-height: 150px; /* Ensure enough space */
510
  }
511
- .wave-loader {
512
- display: flex;
513
- align-items: flex-end; /* Align to bottom for wave effect */
514
- height: 60px; /* Height of the loader */
515
- }
516
- .wave-loader .bar {
517
- width: 10px; /* Width of each bar */
518
- height: 10px; /* Initial height */
519
- margin: 0 4px; /* Spacing between bars */
520
- border-radius: 5px; /* Rounded bars */
521
- background-color: var(--accent-primary);
522
- animation: wave-animate 1.2s infinite ease-in-out;
523
- box-shadow: 0 0 8px color-mix(in srgb, var(--accent-primary) 50%, transparent);
524
  }
525
- .wave-loader .bar:nth-child(2) { animation-delay: 0.1s; background-color: var(--accent-secondary); box-shadow: 0 0 8px color-mix(in srgb, var(--accent-secondary) 50%, transparent); }
526
- .wave-loader .bar:nth-child(3) { animation-delay: 0.2s; }
527
- .wave-loader .bar:nth-child(4) { animation-delay: 0.3s; background-color: var(--accent-secondary); box-shadow: 0 0 8px color-mix(in srgb, var(--accent-secondary) 50%, transparent); }
528
- .wave-loader .bar:nth-child(5) { animation-delay: 0.4s; }
 
 
 
 
 
 
529
 
530
- @keyframes wave-animate {
531
- 0%, 100% { height: 10px; opacity: 0.6; }
532
- 50% { height: 50px; opacity: 1; }
 
 
 
533
  }
 
 
 
534
 
535
- #loading-text {
536
- font-size: 1.2em;
537
- font-weight: 700;
538
- color: var(--text-primary);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  text-align: center;
540
- background: linear-gradient(60deg, var(--accent-primary), var(--accent-secondary));
541
  -webkit-background-clip: text;
542
  -webkit-text-fill-color: transparent;
543
  }
@@ -547,22 +542,22 @@
547
  <body>
548
  <div class="container">
549
  <header class="app-header">
550
- <h1>آلفا TTS: صدای هوشمند شما</h1>
551
- <p>تجربه‌ای نوین در تبدیل متن به گفتار با هوش مصنوعی پیشرفته</p>
552
  </header>
553
 
554
  <main class="main-content">
555
  <form id="tts-form">
556
  <div class="form-group">
557
  <label for="text-input">📝 متن مورد نظر شما</label>
558
- <textarea id="text-input" rows="5" placeholder="متن خود را اینجا وارد کنید..."></textarea>
559
  <div class="char-counter-wrapper">
560
- <span id="char-count">0</span> / <span id="max-char-count">1500</span>
561
  </div>
562
  </div>
563
  <div class="form-group">
564
  <label for="prompt-input">🗣️ توصیف سبک و لحن گفتار (اختیاری)</label>
565
- <input type="text" id="prompt-input" value="با صدای طبیعی و روان." placeholder="مثال: با لحنی رسمی و موقر، یا شاد و کودکانه">
566
  </div>
567
 
568
  <div class="form-group">
@@ -578,22 +573,22 @@
578
  <button type="button" id="change-speaker-btn">
579
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
580
  <path d="M12 2.5a5.5 5.5 0 0 1 5.5 5.5c0 1.826-.889 3.442-2.257 4.494.04.054.082.106.126.158A7.5 7.5 0 0 1 21.75 18H2.25a7.5 7.5 0 0 1 6.381-5.348c.044-.052.087-.104.127-.158A5.502 5.502 0 0 1 6.5 8a5.5 5.5 0 0 1 5.5-5.5Zm3.018 9.589A7.502 7.502 0 0 0 12 11.5a7.502 7.502 0 0 0-3.018.589C6.44 12.51 5.018 13.56 4.135 14.997a.75.75 0 0 0 .614.853A10.51 10.51 0 0 0 12 16.5a10.51 10.51 0 0 0 7.251-.65.75.75 0 0 0 .614-.853 9.998 9.998 0 0 0-4.847-2.408Z" />
581
- </svg>
582
  تغییر گوینده
583
  </button>
584
  </div>
585
  </div>
586
-
587
  <div class="form-group">
588
  <div class="label-with-info">
589
- <label for="temperature-slider">🌡️ میزان خلاقیت صدا</label>
590
  <div class="info-tooltip-icon" id="temp-info-icon" role="button" tabindex="0" aria-label="اطلاعات بیشتر">!
591
  <span class="tooltip-text" id="temp-tooltip-text">مقادیر بالاتر منجر به صدایی متنوع‌تر و گاهی غیرمنتظره‌تر می‌شود، در حالی که مقادیر پایین‌تر صدایی پایدارتر و قابل پیش‌بینی‌تر تولید می‌کنند. (محدوده: 0.1 تا 1.5)</span>
592
  </div>
593
  </div>
594
  <div class="slider-container">
595
- <input type="range" id="temperature-slider" min="0.1" max="1.5" step="0.05" value="0.9" aria-describedby="temperature-value">
596
- <span id="temperature-value" role="status" aria-live="polite">0.9</span>
597
  </div>
598
  </div>
599
 
@@ -603,12 +598,10 @@
603
  <div id="output-section">
604
  <div id="status-message">صدای تولید شده در اینجا نمایش داده خواهد شد.</div>
605
  <div id="loading-animation-wrapper">
606
- <div class="wave-loader">
607
- <div class="bar"></div>
608
- <div class="bar"></div>
609
- <div class="bar"></div>
610
- <div class="bar"></div>
611
- <div class="bar"></div>
612
  </div>
613
  <p id="loading-text">در حال پردازش هوشمند و تولید صدا...</p>
614
  </div>
@@ -620,74 +613,73 @@
620
  <div id="speaker-modal">
621
  <div class="modal-content">
622
  <div class="modal-header">
623
- <h2>گالری گویندگان آلفا</h2>
624
  <button type="button" class="close-modal-btn" aria-label="بستن مودال">×</button>
625
  </div>
626
- <div id="speaker-grid"></div>
 
 
627
  </div>
628
  </div>
629
-
630
  <input type="hidden" id="selected_speaker_id_storage" value="Charon">
631
 
632
  <script>
633
  document.addEventListener('DOMContentLoaded', () => {
634
- const HF_SPACE_URL = "https://hamed744-ttspro.hf.space";
635
  const JOIN_QUEUE_URL = `${HF_SPACE_URL}/gradio_api/queue/join`;
636
  const GET_DATA_URL_BASE = `${HF_SPACE_URL}/gradio_api/queue/data`;
637
  const FILE_URL_BASE = `${HF_SPACE_URL}/gradio_api/file=`;
638
  const FN_INDEX = 1;
639
- const MAX_TEXT_CHARS = 1500;
640
 
641
  const speakers = [
642
- { id: "Charon", name: "شهاب (مرد)", desc: "صدایی قدرتمند و رسا" },
643
- { id: "Zephyr", name: "آوا (زن)", desc: "لطیف و دلنشین" },
644
- { id: "Achird", name: "نوید (مرد)", desc: "جوان و پرانرژی" },
645
- { id: "Zubenelgenubi", name: "رویا (زن)", desc: "گرم و صمیمی" },
646
- { id: "Vindemiatrix", name: "کیان (مرد)", desc: "باوقار و رسمی" },
647
- { id: "Sadachbia", name: "پریسا (زن)", desc: "شاداب و پویا" },
648
- { id: "Sadaltager", name: "آرش (مرد)", desc: "مطمئن و تاثیرگذار" },
649
- { id: "Sulafat", name: "شبنم (زن)", desc: "آرام و متین" },
650
- { id: "Laomedeia", name: "سهیل (مرد)", desc: "دوستانه و گیرا" },
651
- { id: "Achernar", name: "مریم (زن)", desc: "حرفه‌ای و واضح" },
652
- { id: "Alnilam", name: "بهرام (مرد)", desc: "حماسی و نافذ" },
653
- { id: "Schedar", name: "نگار (زن)", desc: "مهربان و شیرین" },
654
- { id: "Gacrux", name: "فرید (مرد)", desc: "پخته و قابل اعتماد" },
655
- { id: "Pulcherrima", name: "سارا (زن)", desc: "جذاب و مدرن" },
656
- { id: "Umbriel", name: "مانی (مرد)", desc: "خلاق و متفاوت" },
657
- { id: "Algieba", name: "آناهیتا (زن)", desc: "با اصالت و شیک" },
658
- { id: "Despina", name: "دلنواز (زن)", desc: "هنری و احساسی" },
659
- { id: "Erinome", name: "رسا (مرد)", desc: "شفاف و گویا" },
660
- { id: "Algenib", name: "امید (مرد)", desc: "انگیزه بخش و مثبت" },
661
- { id: "Rasalthgeti", name: "الهه (زن)", desc: "اسرارآمیز و فریبنده" },
662
- { id: "Orus", name: "بردیا (مرد)", desc: "ورزشی و پرهیجان" },
663
- { id: "Aoede", name: "ترانه (زن)", desc: "موزیکال و خوش‌آهنگ" },
664
- { id: "Callirrhoe", name: "نیما (مرد)", desc: "روایتگر و قصه‌گو" },
665
- { id: "Autonoe", name: "هستی (زن)", desc: "طبیعی و خودمانی" },
666
- { id: "Enceladus", name: "کامیار (مرد)", desc: "مصمم و جدی" },
667
- { id: "Iapetus", name: "ستاره (زن)", desc: "درخشان و گیرا" },
668
- { id: "Puck", name: "پویا (مرد)", desc: "بازیگوش و سرزنده" },
669
- { id: "Kore", name: "مهتاب (زن)", desc: "نجواگر و آرامش‌بخش" },
670
- { id: "Fenrir", name: "سام (مرد)", desc: "جسور و بی‌باک" },
671
  { id: "Leda", name: "لیدا (زن)", desc: "کلاسیک و باوقار" }
672
  ];
673
-
674
  const form = document.getElementById('tts-form');
675
  const textInput = document.getElementById('text-input');
676
- const charCountSpan = document.getElementById('char-count');
677
- const maxCharSpan = document.getElementById('max-char-count');
678
  const promptInput = document.getElementById('prompt-input');
679
  const tempSlider = document.getElementById('temperature-slider');
680
  const tempValueSpan = document.getElementById('temperature-value');
681
  const generateBtn = document.getElementById('generate-btn');
682
-
683
  const outputSection = document.getElementById('output-section');
684
  const statusMessage = document.getElementById('status-message');
685
  const audioPlayer = document.getElementById('audio-player');
686
  const loadingAnimationWrapper = document.getElementById('loading-animation-wrapper');
687
-
688
  const selectedSpeakerIdStorage = document.getElementById('selected_speaker_id_storage');
689
  const speakerModal = document.getElementById('speaker-modal');
690
- const changeSpeakerBtn = document.getElementById('change-speaker-btn');
691
  const selectedSpeakerCard = document.getElementById('selected-speaker-card');
692
  const closeModalBtn = document.querySelector('.close-modal-btn');
693
  const speakerGridInModal = document.getElementById('speaker-grid');
@@ -697,48 +689,46 @@
697
  const tempInfoIcon = document.getElementById('temp-info-icon');
698
 
699
  // Character Counter
700
- if (maxCharSpan) maxCharSpan.textContent = MAX_TEXT_CHARS;
 
 
 
 
 
701
  textInput.addEventListener('input', () => {
702
  const currentLength = textInput.value.length;
703
  charCountSpan.textContent = currentLength;
704
- if (currentLength > MAX_TEXT_CHARS) {
705
- charCountSpan.classList.add('limit-exceeded');
706
- textInput.setAttribute('aria-invalid', 'true');
707
  } else {
708
- charCountSpan.classList.remove('limit-exceeded');
709
- textInput.setAttribute('aria-invalid', 'false');
710
  }
711
  });
712
-
 
713
  function getSpeakerById(id) {
714
- return speakers.find(s => s.id === id) || speakers[0];
715
  }
716
-
717
- function getImageUrl(speaker, index, size = 'thumb') {
718
  const gender = speaker.name.includes('(مرد)') ? 'men' : 'women';
719
- const imageIndex = (index * 7 + speaker.id.length * 3 + 5) % 100;
720
- let portraitSizePath = 'thumb/';
721
- if (size === 'large') portraitSizePath = '';
 
 
722
  return `https://randomuser.me/api/portraits/${portraitSizePath}${gender}/${imageIndex}.jpg`;
723
  }
724
 
725
  function updateSelectedSpeakerDisplay(speakerId) {
726
  const speaker = getSpeakerById(speakerId);
727
- const speakerIndex = speakers.findIndex(s => s.id === speaker.id);
728
  selectedSpeakerImgDisplay.src = getImageUrl(speaker, speakerIndex, 'large');
729
  selectedSpeakerImgDisplay.alt = `عکس گوینده ${speaker.name}`;
730
  selectedSpeakerNameDisplay.textContent = speaker.name;
731
  selectedSpeakerDescDisplay.textContent = speaker.desc || "گوینده منتخب شما";
732
  selectedSpeakerIdStorage.value = speaker.id;
733
-
734
- // Add animation class if it's not the initial load
735
- if (updateSelectedSpeakerDisplay.hasRunOnce) {
736
- selectedSpeakerCard.classList.add('speaker-changed');
737
- setTimeout(() => selectedSpeakerCard.classList.remove('speaker-changed'), 700);
738
- }
739
- updateSelectedSpeakerDisplay.hasRunOnce = true;
740
  }
741
- updateSelectedSpeakerDisplay.hasRunOnce = false; // Initialize flag
742
 
743
  function createSpeakerCardsInModal() {
744
  speakerGridInModal.innerHTML = '';
@@ -747,7 +737,7 @@
747
  cardLabel.className = 'speaker-card';
748
  cardLabel.setAttribute('for', `modal-speaker-${speaker.id}`);
749
  const isChecked = speaker.id === selectedSpeakerIdStorage.value ? 'checked' : '';
750
-
751
  cardLabel.innerHTML = `
752
  <input type="radio" name="modal_speaker_selection" value="${speaker.id}" id="modal-speaker-${speaker.id}" ${isChecked}>
753
  <div class="speaker-visual">
@@ -755,81 +745,77 @@
755
  <div class="speaker-name">${speaker.name}</div>
756
  </div>
757
  `;
758
-
759
  cardLabel.addEventListener('click', (e) => {
 
760
  if (e.target.name !== "modal_speaker_selection") {
761
  const radio = cardLabel.querySelector('input[type="radio"]');
762
- if(radio) radio.checked = true;
763
  }
764
  updateSelectedSpeakerDisplay(speaker.id);
765
- setTimeout(() => speakerModal.classList.remove('visible'), 200);
 
766
  });
 
767
  speakerGridInModal.appendChild(cardLabel);
768
  });
769
  }
770
-
771
  function openSpeakerModal() {
772
- createSpeakerCardsInModal();
773
  speakerModal.classList.add('visible');
774
- document.body.style.overflow = 'hidden'; // Prevent background scroll
775
- setTimeout(() => {
776
  const firstFocusable = speakerModal.querySelector('input[type="radio"]:checked, input[type="radio"], .close-modal-btn');
777
  if (firstFocusable) firstFocusable.focus();
778
- else closeModalBtn.focus();
779
- }, 50);
780
- }
781
- function closeSpeakerModal() {
782
- speakerModal.classList.remove('visible');
783
- document.body.style.overflow = ''; // Restore background scroll
784
  }
785
 
786
- changeSpeakerBtn.addEventListener('click', openSpeakerModal);
787
  selectedSpeakerCard.addEventListener('click', openSpeakerModal);
788
 
789
- closeModalBtn.addEventListener('click', closeSpeakerModal);
790
  speakerModal.addEventListener('click', (e) => {
791
- if (e.target === speakerModal) {
792
- closeSpeakerModal();
793
  }
794
  });
795
  document.addEventListener('keydown', (e) => {
796
  if (e.key === 'Escape' && speakerModal.classList.contains('visible')) {
797
- closeSpeakerModal();
798
  }
799
  });
800
 
801
- tempSlider.addEventListener('input', () => {
802
- tempValueSpan.textContent = tempSlider.value;
803
- });
804
 
 
805
  tempInfoIcon.addEventListener('click', (e) => {
806
  e.stopPropagation();
807
  tempInfoIcon.classList.toggle('active');
808
- tempInfoIcon.setAttribute('aria-expanded', tempInfoIcon.classList.contains('active').toString());
809
  });
810
- tempInfoIcon.addEventListener('keydown', (e) => {
811
  if (e.key === 'Enter' || e.key === ' ') {
812
  e.preventDefault();
813
  tempInfoIcon.classList.toggle('active');
814
- tempInfoIcon.setAttribute('aria-expanded', tempInfoIcon.classList.contains('active').toString());
815
  }
816
  });
817
  document.addEventListener('click', (e) => {
818
  if (!tempInfoIcon.contains(e.target) && tempInfoIcon.classList.contains('active')) {
819
  tempInfoIcon.classList.remove('active');
820
- tempInfoIcon.setAttribute('aria-expanded', 'false');
821
  }
822
  });
823
 
 
824
  function showLoadingState() {
825
- outputSection.classList.remove('has-content');
826
- statusMessage.style.display = 'none';
827
  audioPlayer.style.display = 'none';
828
  audioPlayer.src = '';
829
  loadingAnimationWrapper.style.display = 'flex';
830
  generateBtn.disabled = true;
831
  generateBtn.innerHTML = `
832
- <svg class="loader-icon" aria-hidden="true" role="status" fill="currentColor" viewBox="0 0 100 101">
833
  <path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="#E5E7EB"/>
834
  <path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0492C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentColor"/>
835
  </svg>
@@ -838,39 +824,35 @@
838
  }
839
 
840
  function showResultState(isSuccess, message = '') {
841
- loadingAnimationWrapper.style.display = 'none';
842
  if (isSuccess) {
843
  statusMessage.style.display = 'none';
844
  audioPlayer.style.display = 'block';
845
- outputSection.classList.add('has-content');
846
- audioPlayer.focus(); // Focus on player when ready
847
  } else {
848
  statusMessage.textContent = message || 'خطایی رخ داد. لطفاً دوباره تلاش کنید.';
849
  statusMessage.style.display = 'block';
850
  audioPlayer.style.display = 'none';
851
- outputSection.classList.remove('has-content');
852
  }
853
  generateBtn.disabled = false;
854
- generateBtn.innerHTML = '✨ تولید صدا با آلفا';
855
  }
856
 
857
  async function generateAudio(event) {
858
  event.preventDefault();
859
-
860
- const text = textInput.value.trim();
861
- if (!text) {
 
862
  showResultState(false, 'خطا: متن ورودی نمی‌تواند خالی باشد.');
863
- textInput.focus();
864
  return;
865
  }
866
- if (text.length > MAX_TEXT_CHARS) {
867
- showResultState(false, `خطا: طول متن بیش از حد مجاز (${MAX_TEXT_CHARS} کاراکتر) است.`);
868
- textInput.focus();
869
  return;
870
  }
871
-
872
- showLoadingState();
873
-
874
  const promptVal = promptInput.value;
875
  const temperatureVal = parseFloat(tempSlider.value);
876
  const selectedSpeakerVal = selectedSpeakerIdStorage.value;
@@ -895,10 +877,10 @@
895
  console.error("Join Queue Error Body:", errorBody);
896
  throw new Error(`خطا در برقراری ارتباط با سرویس آلفا (${joinQueueResponse.status}). لطفا لحظاتی دیگر تلاش کنید.`);
897
  }
898
-
899
  let finalFilePath = null;
900
  const startTime = Date.now();
901
- const timeoutDuration = 90000; // 90 seconds
902
 
903
  while (Date.now() - startTime < timeoutDuration) {
904
  const dataResponse = await fetch(`${GET_DATA_URL_BASE}?session_hash=${sessionHash}`);
@@ -907,7 +889,7 @@
907
  console.error("Get Data Error Body:", errorBody);
908
  throw new Error(`خطا در دریافت داده از سرویس (${dataResponse.status})`);
909
  }
910
-
911
  const responseText = await dataResponse.text();
912
  const lines = responseText.trim().split('\n');
913
 
@@ -916,28 +898,28 @@
916
  try {
917
  const data = JSON.parse(line.substring(5));
918
  if (data.msg === 'process_generating' || data.msg === 'process_starts') {
919
- // console.log("Processing:", data.msg, data.output?.progress_data?.[0]?.desc);
920
  } else if (data.msg === 'process_completed') {
921
  if (data.success && data.output.data && data.output.data[0] && (data.output.data[0].name || data.output.data[0].path)) {
922
  finalFilePath = data.output.data[0].name || data.output.data[0].path;
923
- } else {
924
- console.error("Invalid server response structure or unsuccessful processing:", data);
925
  throw new Error(data.output?.error || 'خطای ناشناخته در پردازش سرور.');
926
  }
927
- break;
928
  } else if (data.msg === 'queue_full') {
929
  throw new Error('صف پردازش پر است. لطفا کمی بعد تلاش کنید.');
930
  }
931
- } catch (e) {
932
  console.warn("Error parsing JSON from stream:", e, "Line:", line);
933
  }
934
  }
935
- if (finalFilePath) break;
936
- await new Promise(resolve => setTimeout(resolve, 1000));
937
  }
938
-
939
  if (finalFilePath) {
940
- const audioUrl = `${FILE_URL_BASE}${finalFilePath.replace(/\\/g, '/')}`; // Ensure forward slashes
941
  audioPlayer.src = audioUrl;
942
  showResultState(true);
943
  } else if (Date.now() - startTime >= timeoutDuration) {
@@ -951,8 +933,9 @@
951
  showResultState(false, `${error.message}`);
952
  }
953
  }
954
-
955
- updateSelectedSpeakerDisplay(selectedSpeakerIdStorage.value || speakers[0].id);
 
956
  form.addEventListener('submit', generateAudio);
957
 
958
  // Initial state for output section
@@ -960,6 +943,12 @@
960
  loadingAnimationWrapper.style.display = 'none';
961
  audioPlayer.style.display = 'none';
962
  outputSection.classList.remove('has-content');
 
 
 
 
 
 
963
  });
964
  </script>
965
  </body>
 
9
 
10
  :root {
11
  --app-font: 'Vazirmatn', sans-serif;
12
+ --app-bg: #F4F7FC;
13
  --panel-bg: #FFFFFF;
14
+ --panel-border: #E8EEF3;
15
+ --text-primary: #121826;
16
+ --text-secondary: #5C677D;
17
+ --accent-primary: #3B82F6;
18
+ --accent-primary-hover: #2563EB;
19
+ --accent-secondary: #10B981;
20
  --accent-secondary-hover: #059669;
21
+ --input-bg: #F8FAFC;
 
22
  --input-border-focus: var(--accent-primary);
 
 
 
 
 
 
 
 
23
 
24
+ --radius-card: 24px;
25
+ --radius-input: 14px;
26
+ --shadow-subtle: 0 2px 8px rgba(26, 32, 44, 0.04);
27
+ --shadow-medium: 0 6px 16px -2px rgba(26, 32, 44, 0.08), 0 3px 8px -3px rgba(26,32,44,0.05);
28
+ --shadow-strong: 0 10px 25px -5px rgba(26, 32, 44, 0.12), 0 6px 15px -6px rgba(26,32,44,0.08);
29
+ --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
30
+ --transition-bounce: all 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55);
31
  }
32
 
33
+ @keyframes fadeInDown {
34
+ from { opacity: 0; transform: translateY(-20px); }
35
+ to { opacity: 1; transform: translateY(0); }
36
+ }
37
+ @keyframes fadeInUp {
38
+ from { opacity: 0; transform: translateY(20px); }
39
+ to { opacity: 1; transform: translateY(0); }
40
  }
41
 
42
+
43
  body {
44
  font-family: var(--app-font);
45
  direction: rtl;
46
  background-color: var(--app-bg);
47
  color: var(--text-primary);
48
  font-size: 16px;
49
+ line-height: 1.8;
50
  margin: 0;
51
+ padding: 2.5rem 0;
52
  min-height: 100vh;
53
+ -webkit-font-smoothing: antialiased;
54
+ -moz-osx-font-smoothing: grayscale;
55
  display: flex;
56
  justify-content: center;
57
  align-items: flex-start;
58
+ overflow-x: hidden; /* Prevent horizontal scrollbar from animations */
59
  }
60
 
61
+ .container {
62
+ max-width: 780px;
63
+ width: 92%;
64
  margin: 0 auto;
 
 
 
65
  }
66
 
67
+ .app-header {
68
+ padding: 0.5rem 0 3rem 0;
69
+ text-align: center;
70
+ margin-bottom: 2rem;
71
+ animation: fadeInDown 0.8s 0.1s ease-out backwards;
72
+ }
73
+ .app-header h1 {
74
+ font-size: 2.8em; /* Reduced size */
75
+ font-weight: 900;
76
+ margin:0 0 0.75rem 0;
77
+ background: linear-gradient(45deg, var(--accent-primary), var(--accent-secondary));
 
 
 
 
 
 
78
  -webkit-background-clip: text;
79
  -webkit-text-fill-color: transparent;
80
+ letter-spacing: -1px;
81
  }
82
+ .app-header p {
83
+ font-size: 1.2em; /* Slightly reduced */
84
+ color: var(--text-secondary);
85
+ margin-top:0;
86
+ opacity: 0.9; /* Slightly less prominent */
87
  font-weight: 400;
88
  }
89
 
90
+ .main-content {
91
  padding: 2.5rem; /* Adjusted padding */
92
+ background-color: var(--panel-bg);
93
+ border-radius: var(--radius-card);
94
+ box-shadow: var(--shadow-strong);
95
  border: 1px solid var(--panel-border);
96
+ animation: fadeInUp 0.8s 0.3s ease-out backwards;
97
  }
98
 
99
+ .form-group { margin-bottom: 2rem; } /* Adjusted margin */
100
+
101
  .label-with-info {
102
  display: flex;
103
  align-items: center;
 
104
  gap: 0.5rem;
105
+ margin-bottom: 1rem;
106
  }
107
  .label-with-info label {
108
  margin-bottom: 0;
 
112
  display: inline-flex;
113
  align-items: center;
114
  justify-content: center;
115
+ width: 22px;
116
+ height: 22px;
117
  border-radius: 50%;
118
+ background-color: var(--input-bg);
119
  border: 1px solid var(--panel-border);
120
  color: var(--text-secondary);
121
  font-size: 0.9em;
122
  font-weight: 700;
123
+ cursor: pointer;
124
  position: relative;
125
  transition: var(--transition-smooth);
126
  user-select: none;
127
  }
128
+ .info-tooltip-icon:hover, .info-tooltip-icon:focus {
129
  background-color: var(--accent-primary);
130
  color: white;
131
  border-color: var(--accent-primary);
132
  transform: scale(1.1);
133
+ outline: none;
134
  }
135
  .tooltip-text {
136
  visibility: hidden;
 
138
  background-color: var(--text-primary);
139
  color: #fff;
140
  text-align: right;
141
+ border-radius: var(--radius-input);
142
+ padding: 10px 15px;
143
  position: absolute;
144
  z-index: 10;
145
+ bottom: 140%;
146
  left: 50%;
147
+ transform: translateX(-50%) translateY(10px); /* Start slightly lower */
148
  opacity: 0;
149
+ transition: opacity 0.3s, visibility 0.3s, transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
150
+ font-size: 0.88em;
151
  font-weight: 400;
152
  line-height: 1.6;
153
  box-shadow: var(--shadow-medium);
 
157
  position: absolute;
158
  top: 100%;
159
  left: 50%;
160
+ margin-left: -5px;
161
+ border-width: 5px;
162
  border-style: solid;
163
  border-color: var(--text-primary) transparent transparent transparent;
164
  }
165
  .info-tooltip-icon.active .tooltip-text {
166
  visibility: visible;
167
  opacity: 1;
168
+ transform: translateX(-50%) translateY(0); /* Animate to final position */
169
  }
170
 
171
 
172
+ label {
173
+ display: block;
174
+ font-weight: 700;
175
+ color: var(--text-primary);
176
+ font-size: 1.05em; /* Adjusted size */
177
+ margin-bottom: 0.8rem; /* Adjusted margin */
178
  }
179
 
180
+ textarea, input[type="text"] {
181
+ width: 100%;
182
+ padding: 0.9rem 1.1rem; /* Adjusted padding */
183
+ border-radius: var(--radius-input);
184
+ border: 1px solid var(--panel-border);
185
  background-color: var(--input-bg);
186
  color: var(--text-primary);
187
+ box-shadow: var(--shadow-subtle) inset;
188
+ font-family: var(--app-font);
189
+ font-size: 1rem; /* Adjusted size */
190
+ box-sizing: border-box;
191
  transition: var(--transition-smooth);
192
  }
193
+ textarea:focus, input[type="text"]:focus {
194
+ outline: none;
195
+ border-color: var(--input-border-focus);
196
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25), var(--shadow-subtle) inset; /* Slightly adjusted focus shadow */
197
  background-color: #fff;
198
  }
199
+ textarea { min-height: 100px; resize: vertical; } /* Adjusted min-height */
200
 
201
  .char-counter-wrapper {
202
  font-size: 0.85em;
203
  color: var(--text-secondary);
204
+ text-align: left; /* For LTR numbers, but RTL page */
205
  margin-top: 0.5rem;
206
  padding: 0 0.2rem;
207
  }
208
+ #char-count { font-weight: 600; color: var(--accent-primary); }
 
 
 
209
 
210
 
211
+ /* --- نمایش گوینده منتخب --- */
212
+ #selected-speaker-display { text-align: center; margin-top: 1rem; position: relative; }
213
+ #selected-speaker-card {
214
+ display: inline-flex;
215
+ align-items: center;
216
+ background: linear-gradient(135deg, var(--input-bg) 0%, #fff 100%);
217
+ border-radius: var(--radius-card);
218
+ padding: 1rem 1.2rem;
219
+ box-shadow: var(--shadow-medium);
220
  border: 1px solid var(--panel-border);
221
+ transition: var(--transition-bounce); /* Bouncy transition */
222
+ position: relative;
223
+ margin-bottom: 1.2rem;
224
+ cursor: pointer;
225
  }
226
  #selected-speaker-card:hover {
227
+ transform: translateY(-6px) scale(1.03); /* More pronounced hover */
228
  box-shadow: var(--shadow-strong);
229
  }
230
+ #selected-speaker-card img {
231
+ width: 75px; height: 75px; /* Slightly smaller */
232
+ border-radius: 50%;
233
+ object-fit: cover;
234
+ margin-left: 18px; /* Adjusted margin */
235
+ border: 3px solid var(--accent-secondary); /* Thinner border */
236
+ box-shadow: 0 0 12px -2px rgba(16, 185, 129, 0.5); /* Adjusted shadow */
237
+ background-color: #e0e0e0;
 
 
 
 
 
 
 
 
 
238
  transition: var(--transition-smooth);
239
  }
240
  #selected-speaker-card:hover img {
241
+ transform: scale(1.08) rotate(3deg); /* More dynamic image hover */
 
242
  }
243
+ #selected-speaker-info h3 { margin: 0; font-size: 1.35em; font-weight: 800; color: var(--text-primary); }
244
  #selected-speaker-info p { margin: 4px 0 0; color: var(--text-secondary); font-size: 0.88em; font-weight: 500; }
245
+
246
+ #change-speaker-btn {
247
+ display: inline-flex;
248
  align-items: center;
249
  justify-content: center;
250
+ margin: 0 auto;
251
+ padding: 10px 20px; /* Adjusted padding */
252
+ border-radius: var(--radius-input);
 
 
 
 
 
 
 
 
 
 
253
  background: linear-gradient(45deg, var(--accent-primary-hover), var(--accent-primary));
254
+ border: none;
255
+ color: #fff;
256
+ cursor: pointer;
257
+ font-family: var(--app-font); font-weight: 600; /* Adjusted weight */
258
+ font-size: 1em; /* Adjusted size */
259
+ transition: var(--transition-smooth);
260
+ box-shadow: 0 4px 10px -2px rgba(59, 130, 246, 0.35), var(--shadow-subtle); /* Adjusted shadow */
261
+ }
262
+ #change-speaker-btn:hover {
263
+ background: linear-gradient(45deg, var(--accent-primary), var(--accent-primary-hover));
264
+ transform: translateY(-3px) scale(1.05); /* More lift */
265
+ box-shadow: 0 6px 12px -3px rgba(59, 130, 246, 0.45), var(--shadow-medium); /* Stronger shadow */
266
  }
267
  #change-speaker-btn svg {
268
+ width: 1.1em; /* Adjusted size */
269
  height: 1.1em;
270
+ margin-right: 0.5em; /* Adjusted margin */
271
  fill: currentColor;
272
  }
273
 
274
 
275
  /* --- مودال گالری گویندگان --- */
276
+ #speaker-modal {
277
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
278
+ background-color: rgba(18, 24, 38, 0.7); /* Darker backdrop */
279
+ backdrop-filter: blur(8px) saturate(150%); /* Adjusted blur */
280
+ display: none; align-items: center; justify-content: center;
281
+ z-index: 1000; opacity: 0;
282
+ transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1); /* Faster fade */
283
  }
284
  #speaker-modal.visible { display: flex; opacity: 1; }
285
+ .modal-content {
286
+ background: var(--panel-bg);
287
+ padding: 2rem; /* Adjusted padding */
288
+ border-radius: var(--radius-card);
289
+ width: 90%; max-width: 700px; /* Slightly smaller max-width */
290
+ max-height: 85vh; overflow-y: auto; /* Adjusted max-height */
291
+ transform: scale(0.95) translateY(20px); /* Start slightly smaller and lower */
292
+ transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.35s;
293
  border: 1px solid var(--panel-border);
294
  box-shadow: var(--shadow-strong);
295
  opacity: 0;
296
  }
297
  #speaker-modal.visible .modal-content { transform: scale(1) translateY(0); opacity: 1;}
298
+ .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.8rem; padding-bottom: 1rem; border-bottom: 1px solid var(--panel-border); }
299
+ .modal-header h2 { margin: 0; font-size: 1.6em; font-weight: 800; color: var(--accent-primary);} /* Adjusted size */
300
+ .close-modal-btn { background: none; border: none; font-size: 2.5rem; cursor: pointer; color: var(--text-secondary); transition: var(--transition-smooth); line-height: 1; }
301
+ .close-modal-btn:hover { color: var(--accent-primary); transform: rotate(90deg) scale(1.1); } /* Changed rotation */
302
+
303
+ #speaker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 1.2rem; } /* Adjusted gap and minmax */
304
  @media (min-width: 640px) { #speaker-grid { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); } }
305
  .speaker-card { cursor: pointer; transition: var(--transition-smooth); text-align: center; position: relative;}
306
+ .speaker-card .speaker-visual {
307
+ border: 3px solid transparent;
308
+ border-radius: var(--radius-card);
309
+ overflow: hidden;
310
+ box-shadow: var(--shadow-subtle);
311
+ position: relative;
312
+ background-color: var(--input-bg);
313
+ transition: var(--transition-bounce); /* Bouncy transition */
314
+ padding: 6px; /* Adjusted padding */
315
+ }
316
+ .speaker-card:hover .speaker-visual {
317
+ transform: translateY(-5px) scale(1.06); /* More pop */
318
+ box-shadow: var(--shadow-medium);
319
+ border-color: rgba(var(--accent-secondary-rgb), 0.3); /* Subtle border on hover */
320
  }
321
  .speaker-card input[type="radio"] { display: none; }
322
+ .speaker-card img {
323
+ width: 100%; height: 120px; /* Adjusted height */
324
+ object-fit: cover; display: block;
325
+ background-color: #e0e0e0;
326
  transition: transform 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
327
+ border-radius: calc(var(--radius-card) - 12px); /* Adjusted inner radius */
328
  }
329
+ .speaker-card:hover img { transform: scale(1.12); } /* Slightly more zoom */
330
  .speaker-card .speaker-name { padding: 0.8rem 0.4rem 0.1rem; font-weight: 600; font-size: 0.9em; color: var(--text-secondary); transition: color 0.2s; }
331
+ .speaker-card input[type="radio"]:checked + .speaker-visual {
332
+ border-color: var(--accent-secondary);
333
+ box-shadow: 0 0 25px -5px rgba(16, 185, 129, 0.75); /* Stronger glow */
334
+ background: linear-gradient(135deg, var(--accent-secondary-hover), var(--accent-primary)); /* New gradient */
335
+ transform: scale(1.05); /* Pop out selected */
336
  }
337
  .speaker-card input[type="radio"]:checked + .speaker-visual img {
338
+ border: 2px solid white; /* Thinner border */
339
+ transform: scale(1.05); /* Slight zoom for selected image */
340
  }
341
+ .speaker-card input[type="radio"]:checked + .speaker-visual .speaker-name {
342
+ color: #fff;
343
+ font-weight: 700;
344
  }
345
 
346
  /* --- Slider & Button & Output --- */
347
+ .slider-container { display: flex; align-items: center; gap: 1.2rem; } /* Adjusted gap */
348
+ input[type="range"] {
349
+ flex-grow: 1; -webkit-appearance: none; appearance: none;
350
+ width: 100%; height: 8px; /* Thinner track */
351
+ background: #EAF0F6;
352
+ border-radius: 4px; outline: none;
353
  cursor: pointer;
354
  transition: background 0.2s;
355
  }
 
358
  height: 8px;
359
  border-radius: 4px;
360
  }
361
+ input[type="range"]::-webkit-slider-thumb {
362
+ -webkit-appearance: none; appearance: none;
363
+ width: 22px; height: 22px; /* Slightly smaller thumb */
364
+ background: #fff;
365
+ border-radius: 50%; cursor: pointer;
366
+ border: 3px solid var(--accent-primary); /* Thinner border */
367
+ box-shadow: 0 2px 6px rgba(0,0,0,0.15); /* Adjusted shadow */
368
+ margin-top: -7px;
369
  transition: transform 0.2s ease, box-shadow 0.2s ease;
370
  }
371
+ input[type="range"]::-webkit-slider-thumb:hover, input[type="range"]:focus::-webkit-slider-thumb {
372
+ transform: scale(1.2); /* More pronounced hover/focus */
373
+ box-shadow: 0 3px 8px rgba(59, 130, 246, 0.35);
374
  }
375
+ input[type="range"]::-moz-range-thumb {
376
+ width: 22px; height: 22px;
377
+ background: #fff;
378
+ border-radius: 50%; cursor: pointer;
379
+ border: 3px solid var(--accent-primary);
380
  box-shadow: 0 2px 6px rgba(0,0,0,0.15);
381
  }
382
  input[type="range"]::-moz-range-track {
 
385
  border-radius: 4px;
386
  border: none;
387
  }
388
+ #temperature-value {
389
+ font-weight: 700; background-color: var(--input-bg);
390
+ padding: 0.5rem 1rem; /* Adjusted padding */
391
+ border-radius: 8px; /* Adjusted radius */
392
+ border: 1px solid var(--panel-border);
393
+ min-width: 45px; text-align: center; /* Adjusted min-width */
394
+ color: var(--accent-primary);
395
+ font-size: 1em; /* Adjusted size */
396
  box-shadow: var(--shadow-subtle);
 
 
 
 
 
397
  }
398
+
399
+ #generate-btn {
400
  width: 100%; padding: 1rem 1.5rem; /* Adjusted padding */
401
+ font-size: 1.25em; font-weight: 800; /* Adjusted size */
402
+ font-family: var(--app-font);
403
+ background: linear-gradient(95deg, var(--accent-secondary) 0%, var(--accent-primary) 100%);
404
+ color: #fff;
405
+ border: none;
406
+ border-radius: var(--radius-input);
407
+ cursor: pointer;
408
+ transition: var(--transition-smooth), transform 0.15s ease-out;
409
+ box-shadow: 0 5px 15px -4px rgba(59, 130, 246, 0.45), 0 5px 15px -4px rgba(16, 185, 129, 0.35); /* Adjusted shadow */
410
  position: relative;
411
  overflow: hidden;
412
+ letter-spacing: 0.5px;
413
  }
414
+ #generate-btn::before {
415
  content: ''; position: absolute;
416
+ top: 0; left: -180%; width: 80%; height: 100%; /* Adjusted for effect */
417
  background: linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.25) 50%, rgba(255,255,255,0) 100%);
418
+ transform: skewX(-30deg); /* Steeper angle */
419
+ transition: left 0.8s cubic-bezier(0.23, 1, 0.32, 1); /* Slower, smoother shine */
420
  }
421
+ #generate-btn:hover::before { left: 180%; }
422
+ #generate-btn:hover:not(:disabled) {
423
+ transform: translateY(-5px) scale(1.02); /* More lift */
424
+ box-shadow: 0 8px 20px -4px rgba(59, 130, 246, 0.55), 0 8px 20px -4px rgba(16, 185, 129, 0.45); /* Stronger shadow */
425
  }
426
  #generate-btn:active:not(:disabled) {
427
+ transform: translateY(-2px) scale(0.98); /* More pronounced active state */
428
  }
429
+ #generate-btn:disabled {
430
+ background: #B8C2CC;
431
+ cursor: not-allowed; box-shadow: none; color: #E8EEF3;
432
  transform: none;
433
  }
434
  #generate-btn:disabled::before { display: none; }
435
+ #generate-btn svg {
436
+ display:inline-block; margin-left: 0.5em; width:1.2em; height:1.2em;
437
  vertical-align: middle;
 
438
  }
 
 
439
 
440
+ #output-section {
441
+ margin-top: 3rem; /* Adjusted margin */
442
+ padding: 2rem; /* Adjusted padding */
443
+ background-color: var(--input-bg);
444
+ border-radius: var(--radius-card);
445
+ min-height: 200px; /* Adjusted min-height */
446
+ display: flex; align-items: center; justify-content: center;
447
+ flex-direction: column; gap: 1.5rem;
448
+ border: 2px dashed var(--panel-border);
449
+ transition: var(--transition-smooth), border-color 0.5s, box-shadow 0.5s;
450
  position: relative;
451
+ box-shadow: var(--shadow-subtle) inset;
452
  }
453
+ #output-section.has-content {
454
+ background-color: #fff;
455
  border-style: solid;
456
  border-color: var(--accent-secondary);
457
+ box-shadow: 0 0 30px -8px rgba(16, 185, 129, 0.25); /* Soft glow when content is present */
458
+ padding: 1.5rem;
459
  }
460
  #output-section.has-content #status-message,
461
  #output-section.has-content #loading-animation-wrapper {
462
+ display: none !important;
463
  }
464
 
465
+ #status-message { font-weight: 500; color: var(--text-secondary); text-align: center; font-size: 1.1em; } /* Adjusted size */
466
  #audio-player { width: 100%; margin-top: 0; display: none; border-radius: 10px; box-shadow: var(--shadow-medium); }
467
  #output-section.has-content #audio-player {
468
+ margin-top: 0;
469
  }
470
+ /* Customize audio player (browser-specific, may not work everywhere) */
471
+ #audio-player::-webkit-media-controls-panel { background-color: #fdfdff; border-radius: 10px; padding: 5px; box-shadow: var(--shadow-subtle) inset;}
472
+ #audio-player::-webkit-media-controls-play-button { color: var(--accent-primary); background-color: rgba(59, 130, 246, 0.1); border-radius: 50%; margin: 5px;}
473
+ #audio-player::-webkit-media-controls-current-time-display,
474
  #audio-player::-webkit-media-controls-time-remaining-display { color: var(--text-secondary); font-weight: 500; }
475
+ #audio-player::-webkit-media-controls-timeline { background-color: #EAF0F6; border-radius: 5px; margin: 0 10px;}
476
+
477
 
478
  /* --- انیمیشن پردازش --- */
479
  #loading-animation-wrapper {
480
+ display: none;
481
  flex-direction: column;
482
  align-items: center;
483
  justify-content: center;
484
  gap: 1.8rem; /* Adjusted gap */
485
  width: 100%;
486
+ min-height: 150px;
487
  }
488
+ .orbital-loader {
489
+ width: 110px; /* Slightly smaller */
490
+ height: 110px;
491
+ position: relative;
492
+ animation: rotate-loader-orbital 10s linear infinite; /* Faster rotation */
 
 
 
 
 
 
 
 
493
  }
494
+ .orbit {
495
+ position: absolute;
496
+ top: 50%; left: 50%;
497
+ border: 2px dashed rgba(59, 130, 246, 0.25); /* Lighter dash */
498
+ border-radius: 50%;
499
+ transform-origin: center center;
500
+ }
501
+ .orbit:nth-child(1) { width: 35px; height: 35px; margin: -17.5px 0 0 -17.5px; animation: orbit-spin 2.8s linear infinite reverse; }
502
+ .orbit:nth-child(2) { width: 65px; height: 65px; margin: -32.5px 0 0 -32.5px; animation: orbit-spin 3.8s linear infinite; }
503
+ .orbit:nth-child(3) { width: 95px; height: 95px; margin: -47.5px 0 0 -47.5px; animation: orbit-spin 4.8s linear infinite reverse; }
504
 
505
+ .orbit .satellite {
506
+ position: absolute;
507
+ width: 10px; height: 10px; /* Smaller satellites */
508
+ border-radius: 50%;
509
+ background-color: var(--accent-primary);
510
+ box-shadow: 0 0 8px var(--accent-primary), 0 0 12px var(--accent-secondary); /* Adjusted shadow */
511
  }
512
+ .orbit:nth-child(1) .satellite { top: -5px; left: 50%; animation: satellite-pulse-1 1.4s ease-in-out infinite alternate; }
513
+ .orbit:nth-child(2) .satellite { top: 50%; left: -5px; background-color: var(--accent-secondary); animation: satellite-pulse-2 1.4s 0.2s ease-in-out infinite alternate; } /* Added delay */
514
+ .orbit:nth-child(3) .satellite { bottom: -5px; right: 50%; animation: satellite-pulse-3 1.4s 0.4s ease-in-out infinite alternate;} /* Added delay */
515
 
516
+ @keyframes rotate-loader-orbital { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
517
+ @keyframes orbit-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
518
+ @keyframes satellite-pulse-1 {
519
+ from { transform: scale(0.7) translateX(-50%); opacity: 0.6; }
520
+ to { transform: scale(1.1) translateX(-50%); opacity: 1; }
521
+ }
522
+ @keyframes satellite-pulse-2 {
523
+ from { transform: scale(0.7) translateY(-50%); opacity: 0.6; }
524
+ to { transform: scale(1.1) translateY(-50%); opacity: 1; }
525
+ }
526
+ @keyframes satellite-pulse-3 {
527
+ from { transform: scale(0.7) translateX(50%); opacity: 0.6; }
528
+ to { transform: scale(1.1) translateX(50%); opacity: 1; }
529
+ }
530
+ #loading-text {
531
+ font-size: 1.2em; /* Adjusted size */
532
+ font-weight: 700;
533
+ color: var(--text-primary);
534
  text-align: center;
535
+ background: linear-gradient(45deg, var(--accent-primary), var(--accent-secondary));
536
  -webkit-background-clip: text;
537
  -webkit-text-fill-color: transparent;
538
  }
 
542
  <body>
543
  <div class="container">
544
  <header class="app-header">
545
+ <h1>تبدیل متن به صدا با هوش مصنوعی آلفا</h1>
546
+ <p>صدایی نو، تجربه‌ای نوین در تبدیل متن به گفتار با هوش مصنوعی پیشرفته</p>
547
  </header>
548
 
549
  <main class="main-content">
550
  <form id="tts-form">
551
  <div class="form-group">
552
  <label for="text-input">📝 متن مورد نظر شما</label>
553
+ <textarea id="text-input" rows="4" placeholder="متن خود را اینجا وارد کنید..."></textarea>
554
  <div class="char-counter-wrapper">
555
+ <span id="char-count">0</span> / <span id="char-max">1000</span> نویسه
556
  </div>
557
  </div>
558
  <div class="form-group">
559
  <label for="prompt-input">🗣️ توصیف سبک و لحن گفتار (اختیاری)</label>
560
+ <input type="text" id="prompt-input" placeholder="مثال: با لحنی رسمی و موقر، یا شاد و کودکانه">
561
  </div>
562
 
563
  <div class="form-group">
 
573
  <button type="button" id="change-speaker-btn">
574
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
575
  <path d="M12 2.5a5.5 5.5 0 0 1 5.5 5.5c0 1.826-.889 3.442-2.257 4.494.04.054.082.106.126.158A7.5 7.5 0 0 1 21.75 18H2.25a7.5 7.5 0 0 1 6.381-5.348c.044-.052.087-.104.127-.158A5.502 5.502 0 0 1 6.5 8a5.5 5.5 0 0 1 5.5-5.5Zm3.018 9.589A7.502 7.502 0 0 0 12 11.5a7.502 7.502 0 0 0-3.018.589C6.44 12.51 5.018 13.56 4.135 14.997a.75.75 0 0 0 .614.853A10.51 10.51 0 0 0 12 16.5a10.51 10.51 0 0 0 7.251-.65.75.75 0 0 0 .614-.853 9.998 9.998 0 0 0-4.847-2.408Z" />
576
+ </svg>
577
  تغییر گوینده
578
  </button>
579
  </div>
580
  </div>
581
+
582
  <div class="form-group">
583
  <div class="label-with-info">
584
+ <label for="temperature-slider">🌡️ میزان خلاقیت و نوآوری صدا</label>
585
  <div class="info-tooltip-icon" id="temp-info-icon" role="button" tabindex="0" aria-label="اطلاعات بیشتر">!
586
  <span class="tooltip-text" id="temp-tooltip-text">مقادیر بالاتر منجر به صدایی متنوع‌تر و گاهی غیرمنتظره‌تر می‌شود، در حالی که مقادیر پایین‌تر صدایی پایدارتر و قابل پیش‌بینی‌تر تولید می‌کنند. (محدوده: 0.1 تا 1.5)</span>
587
  </div>
588
  </div>
589
  <div class="slider-container">
590
+ <input type="range" id="temperature-slider" min="0.1" max="1.5" step="0.05" value="0.9">
591
+ <span id="temperature-value">0.9</span>
592
  </div>
593
  </div>
594
 
 
598
  <div id="output-section">
599
  <div id="status-message">صدای تولید شده در اینجا نمایش داده خواهد شد.</div>
600
  <div id="loading-animation-wrapper">
601
+ <div class="orbital-loader">
602
+ <div class="orbit"><div class="satellite"></div></div>
603
+ <div class="orbit"><div class="satellite"></div></div>
604
+ <div class="orbit"><div class="satellite"></div></div>
 
 
605
  </div>
606
  <p id="loading-text">در حال پردازش هوشمند و تولید صدا...</p>
607
  </div>
 
613
  <div id="speaker-modal">
614
  <div class="modal-content">
615
  <div class="modal-header">
616
+ <h2>گالری گویندگان آلفا نوا</h2>
617
  <button type="button" class="close-modal-btn" aria-label="بستن مودال">×</button>
618
  </div>
619
+ <div id="speaker-grid">
620
+ <!-- Speaker cards will be generated here by JS -->
621
+ </div>
622
  </div>
623
  </div>
624
+
625
  <input type="hidden" id="selected_speaker_id_storage" value="Charon">
626
 
627
  <script>
628
  document.addEventListener('DOMContentLoaded', () => {
629
+ const HF_SPACE_URL = "https://hamed744-ttspro.hf.space";
630
  const JOIN_QUEUE_URL = `${HF_SPACE_URL}/gradio_api/queue/join`;
631
  const GET_DATA_URL_BASE = `${HF_SPACE_URL}/gradio_api/queue/data`;
632
  const FILE_URL_BASE = `${HF_SPACE_URL}/gradio_api/file=`;
633
  const FN_INDEX = 1;
 
634
 
635
  const speakers = [
636
+ { id: "Charon", name: "شهاب (مرد)", desc: "صدایی قدرتمند و رسا" },
637
+ { id: "Zephyr", name: "آوا (زن)", desc: "لطیف و دلنشین" },
638
+ { id: "Achird", name: "نوید (مرد)", desc: "جوان و پرانرژی" },
639
+ { id: "Zubenelgenubi", name: "رویا (زن)", desc: "گرم و صمیمی" },
640
+ { id: "Vindemiatrix", name: "کیان (مرد)", desc: "باوقار و رسمی" },
641
+ { id: "Sadachbia", name: "پریسا (زن)", desc: "شاداب و پویا" },
642
+ { id: "Sadaltager", name: "آرش (مرد)", desc: "مطمئن و تاثیرگذار" },
643
+ { id: "Sulafat", name: "شبنم (زن)", desc: "آرام و متین" },
644
+ { id: "Laomedeia", name: "سهیل (مرد)", desc: "دوستانه و گیرا" },
645
+ { id: "Achernar", name: "مریم (زن)", desc: "حرفه‌ای و واضح" },
646
+ { id: "Alnilam", name: "بهرام (مرد)", desc: "حماسی و نافذ" },
647
+ { id: "Schedar", name: "نگار (زن)", desc: "مهربان و شیرین" },
648
+ { id: "Gacrux", name: "فرید (مرد)", desc: "پخته و قابل اعتماد" },
649
+ { id: "Pulcherrima", name: "سارا (زن)", desc: "جذاب و مدرن" },
650
+ { id: "Umbriel", name: "مانی (مرد)", desc: "خلاق و متفاوت" },
651
+ { id: "Algieba", name: "آناهیتا (زن)", desc: "با اصالت و شیک" },
652
+ { id: "Despina", name: "دلنواز (زن)", desc: "هنری و احساسی" },
653
+ { id: "Erinome", name: "رسا (مرد)", desc: "شفاف و گویا" },
654
+ { id: "Algenib", name: "امید (مرد)", desc: "انگیزه بخش و مثبت" },
655
+ { id: "Rasalthgeti", name: "الهه (زن)", desc: "اسرارآمیز و فریبنده" },
656
+ { id: "Orus", name: "بردیا (مرد)", desc: "ورزشی و پرهیجان" },
657
+ { id: "Aoede", name: "ترانه (زن)", desc: "موزیکال و خوش‌آهنگ" },
658
+ { id: "Callirrhoe", name: "نیما (مرد)", desc: "روایتگر و قصه‌گو" },
659
+ { id: "Autonoe", name: "هستی (زن)", desc: "طبیعی و خودمانی" },
660
+ { id: "Enceladus", name: "کامیار (مرد)", desc: "مصمم و جدی" },
661
+ { id: "Iapetus", name: "ستاره (زن)", desc: "درخشان و گیرا" },
662
+ { id: "Puck", name: "پویا (مرد)", desc: "بازیگوش و سرزنده" },
663
+ { id: "Kore", name: "مهتاب (زن)", desc: "نجواگر و آرامش‌بخش" },
664
+ { id: "Fenrir", name: "سام (مرد)", desc: "جسور و بی‌باک" },
665
  { id: "Leda", name: "لیدا (زن)", desc: "کلاسیک و باوقار" }
666
  ];
667
+
668
  const form = document.getElementById('tts-form');
669
  const textInput = document.getElementById('text-input');
 
 
670
  const promptInput = document.getElementById('prompt-input');
671
  const tempSlider = document.getElementById('temperature-slider');
672
  const tempValueSpan = document.getElementById('temperature-value');
673
  const generateBtn = document.getElementById('generate-btn');
674
+
675
  const outputSection = document.getElementById('output-section');
676
  const statusMessage = document.getElementById('status-message');
677
  const audioPlayer = document.getElementById('audio-player');
678
  const loadingAnimationWrapper = document.getElementById('loading-animation-wrapper');
679
+
680
  const selectedSpeakerIdStorage = document.getElementById('selected_speaker_id_storage');
681
  const speakerModal = document.getElementById('speaker-modal');
682
+ const changeSpeakerBtn = document.getElementById('change-speaker-btn');
683
  const selectedSpeakerCard = document.getElementById('selected-speaker-card');
684
  const closeModalBtn = document.querySelector('.close-modal-btn');
685
  const speakerGridInModal = document.getElementById('speaker-grid');
 
689
  const tempInfoIcon = document.getElementById('temp-info-icon');
690
 
691
  // Character Counter
692
+ const charCountSpan = document.getElementById('char-count');
693
+ const charMaxSpan = document.getElementById('char-max');
694
+ const MAX_CHARS = 1000; // You can change this
695
+ // textInput.maxLength = MAX_CHARS; // Optional: Enforce limit in HTML
696
+ charMaxSpan.textContent = MAX_CHARS;
697
+
698
  textInput.addEventListener('input', () => {
699
  const currentLength = textInput.value.length;
700
  charCountSpan.textContent = currentLength;
701
+ if (currentLength > MAX_CHARS) {
702
+ charCountSpan.style.color = 'var(--accent-secondary-hover)'; // Or some error color
 
703
  } else {
704
+ charCountSpan.style.color = 'var(--accent-primary)';
 
705
  }
706
  });
707
+
708
+
709
  function getSpeakerById(id) {
710
+ return speakers.find(s => s.id === id) || speakers[0];
711
  }
712
+
713
+ function getImageUrl(speaker, index, size = 'thumb') {
714
  const gender = speaker.name.includes('(مرد)') ? 'men' : 'women';
715
+ // A simple way to get somewhat varied images based on index and ID length
716
+ const imageIndex = (index * 7 + speaker.id.length * 3 + 5) % 100;
717
+ let portraitSizePath = 'thumb/';
718
+ if (size === 'large') portraitSizePath = '';
719
+
720
  return `https://randomuser.me/api/portraits/${portraitSizePath}${gender}/${imageIndex}.jpg`;
721
  }
722
 
723
  function updateSelectedSpeakerDisplay(speakerId) {
724
  const speaker = getSpeakerById(speakerId);
725
+ const speakerIndex = speakers.findIndex(s => s.id === speaker.id);
726
  selectedSpeakerImgDisplay.src = getImageUrl(speaker, speakerIndex, 'large');
727
  selectedSpeakerImgDisplay.alt = `عکس گوینده ${speaker.name}`;
728
  selectedSpeakerNameDisplay.textContent = speaker.name;
729
  selectedSpeakerDescDisplay.textContent = speaker.desc || "گوینده منتخب شما";
730
  selectedSpeakerIdStorage.value = speaker.id;
 
 
 
 
 
 
 
731
  }
 
732
 
733
  function createSpeakerCardsInModal() {
734
  speakerGridInModal.innerHTML = '';
 
737
  cardLabel.className = 'speaker-card';
738
  cardLabel.setAttribute('for', `modal-speaker-${speaker.id}`);
739
  const isChecked = speaker.id === selectedSpeakerIdStorage.value ? 'checked' : '';
740
+
741
  cardLabel.innerHTML = `
742
  <input type="radio" name="modal_speaker_selection" value="${speaker.id}" id="modal-speaker-${speaker.id}" ${isChecked}>
743
  <div class="speaker-visual">
 
745
  <div class="speaker-name">${speaker.name}</div>
746
  </div>
747
  `;
748
+
749
  cardLabel.addEventListener('click', (e) => {
750
+ // Ensure radio gets checked even if label's child is clicked
751
  if (e.target.name !== "modal_speaker_selection") {
752
  const radio = cardLabel.querySelector('input[type="radio"]');
753
+ if(radio) radio.checked = true;
754
  }
755
  updateSelectedSpeakerDisplay(speaker.id);
756
+ // Add a slight delay for visual feedback before closing
757
+ setTimeout(() => speakerModal.classList.remove('visible'), 200);
758
  });
759
+
760
  speakerGridInModal.appendChild(cardLabel);
761
  });
762
  }
763
+
764
  function openSpeakerModal() {
765
+ createSpeakerCardsInModal();
766
  speakerModal.classList.add('visible');
767
+ // Focus management for accessibility
768
+ setTimeout(() => {
769
  const firstFocusable = speakerModal.querySelector('input[type="radio"]:checked, input[type="radio"], .close-modal-btn');
770
  if (firstFocusable) firstFocusable.focus();
771
+ else closeModalBtn.focus(); // Fallback
772
+ }, 50); // Allow modal transition to start
 
 
 
 
773
  }
774
 
775
+ changeSpeakerBtn.addEventListener('click', openSpeakerModal);
776
  selectedSpeakerCard.addEventListener('click', openSpeakerModal);
777
 
778
+ closeModalBtn.addEventListener('click', () => speakerModal.classList.remove('visible'));
779
  speakerModal.addEventListener('click', (e) => {
780
+ if (e.target === speakerModal) { // Click on backdrop
781
+ speakerModal.classList.remove('visible');
782
  }
783
  });
784
  document.addEventListener('keydown', (e) => {
785
  if (e.key === 'Escape' && speakerModal.classList.contains('visible')) {
786
+ speakerModal.classList.remove('visible');
787
  }
788
  });
789
 
790
+ tempSlider.addEventListener('input', () => { tempValueSpan.textContent = tempSlider.value; });
 
 
791
 
792
+ // Tooltip functionality
793
  tempInfoIcon.addEventListener('click', (e) => {
794
  e.stopPropagation();
795
  tempInfoIcon.classList.toggle('active');
 
796
  });
797
+ tempInfoIcon.addEventListener('keydown', (e) => {
798
  if (e.key === 'Enter' || e.key === ' ') {
799
  e.preventDefault();
800
  tempInfoIcon.classList.toggle('active');
 
801
  }
802
  });
803
  document.addEventListener('click', (e) => {
804
  if (!tempInfoIcon.contains(e.target) && tempInfoIcon.classList.contains('active')) {
805
  tempInfoIcon.classList.remove('active');
 
806
  }
807
  });
808
 
809
+
810
  function showLoadingState() {
811
+ outputSection.classList.remove('has-content');
812
+ statusMessage.style.display = 'none';
813
  audioPlayer.style.display = 'none';
814
  audioPlayer.src = '';
815
  loadingAnimationWrapper.style.display = 'flex';
816
  generateBtn.disabled = true;
817
  generateBtn.innerHTML = `
818
+ <svg aria-hidden="true" role="status" fill="currentColor" viewBox="0 0 100 101" style="display:inline-block; margin-left: 0.5em; width:1.2em; height:1.2em; vertical-align: middle; animation: spin 1s linear infinite;">
819
  <path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="#E5E7EB"/>
820
  <path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0492C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentColor"/>
821
  </svg>
 
824
  }
825
 
826
  function showResultState(isSuccess, message = '') {
827
+ loadingAnimationWrapper.style.display = 'none';
828
  if (isSuccess) {
829
  statusMessage.style.display = 'none';
830
  audioPlayer.style.display = 'block';
831
+ outputSection.classList.add('has-content');
 
832
  } else {
833
  statusMessage.textContent = message || 'خطایی رخ داد. لطفاً دوباره تلاش کنید.';
834
  statusMessage.style.display = 'block';
835
  audioPlayer.style.display = 'none';
836
+ outputSection.classList.remove('has-content');
837
  }
838
  generateBtn.disabled = false;
839
+ generateBtn.innerHTML = '✨ تولید صدا با آلفا'; // Restored original text
840
  }
841
 
842
  async function generateAudio(event) {
843
  event.preventDefault();
844
+ showLoadingState();
845
+
846
+ const text = textInput.value;
847
+ if (!text.trim()) {
848
  showResultState(false, 'خطا: متن ورودی نمی‌تواند خالی باشد.');
 
849
  return;
850
  }
851
+ if (text.length > MAX_CHARS) {
852
+ showResultState(false, `خطا: طول متن بیش از ${MAX_CHARS} نویسه است.`);
 
853
  return;
854
  }
855
+
 
 
856
  const promptVal = promptInput.value;
857
  const temperatureVal = parseFloat(tempSlider.value);
858
  const selectedSpeakerVal = selectedSpeakerIdStorage.value;
 
877
  console.error("Join Queue Error Body:", errorBody);
878
  throw new Error(`خطا در برقراری ارتباط با سرویس آلفا (${joinQueueResponse.status}). لطفا لحظاتی دیگر تلاش کنید.`);
879
  }
880
+
881
  let finalFilePath = null;
882
  const startTime = Date.now();
883
+ const timeoutDuration = 90000; // 90 seconds timeout
884
 
885
  while (Date.now() - startTime < timeoutDuration) {
886
  const dataResponse = await fetch(`${GET_DATA_URL_BASE}?session_hash=${sessionHash}`);
 
889
  console.error("Get Data Error Body:", errorBody);
890
  throw new Error(`خطا در دریافت داده از سرویس (${dataResponse.status})`);
891
  }
892
+
893
  const responseText = await dataResponse.text();
894
  const lines = responseText.trim().split('\n');
895
 
 
898
  try {
899
  const data = JSON.parse(line.substring(5));
900
  if (data.msg === 'process_generating' || data.msg === 'process_starts') {
901
+ // console.log("Processing:", data.output?.progress_data?.[0]?.desc || data.msg);
902
  } else if (data.msg === 'process_completed') {
903
  if (data.success && data.output.data && data.output.data[0] && (data.output.data[0].name || data.output.data[0].path)) {
904
  finalFilePath = data.output.data[0].name || data.output.data[0].path;
905
+ } else {
906
+ console.error("Invalid server response structure or unsuccessful processing:", data);
907
  throw new Error(data.output?.error || 'خطای ناشناخته در پردازش سرور.');
908
  }
909
+ break; // Exit inner loop once completed
910
  } else if (data.msg === 'queue_full') {
911
  throw new Error('صف پردازش پر است. لطفا کمی بعد تلاش کنید.');
912
  }
913
+ } catch (e) {
914
  console.warn("Error parsing JSON from stream:", e, "Line:", line);
915
  }
916
  }
917
+ if (finalFilePath) break; // Exit outer loop if file path is found
918
+ await new Promise(resolve => setTimeout(resolve, 1000)); // Poll every second
919
  }
920
+
921
  if (finalFilePath) {
922
+ const audioUrl = `${FILE_URL_BASE}${finalFilePath}`;
923
  audioPlayer.src = audioUrl;
924
  showResultState(true);
925
  } else if (Date.now() - startTime >= timeoutDuration) {
 
933
  showResultState(false, `${error.message}`);
934
  }
935
  }
936
+
937
+ // Initial setup
938
+ updateSelectedSpeakerDisplay(selectedSpeakerIdStorage.value || speakers[0].id);
939
  form.addEventListener('submit', generateAudio);
940
 
941
  // Initial state for output section
 
943
  loadingAnimationWrapper.style.display = 'none';
944
  audioPlayer.style.display = 'none';
945
  outputSection.classList.remove('has-content');
946
+
947
+ // Add a keyframe for the spinner SVG in JS to keep it local
948
+ const styleSheet = document.createElement("style")
949
+ styleSheet.type = "text/css"
950
+ styleSheet.innerText = `@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }`;
951
+ document.head.appendChild(styleSheet);
952
  });
953
  </script>
954
  </body>