Sada8888 commited on
Commit
513e9e9
·
verified ·
1 Parent(s): 10ededa

Create templates/shabihsazi-sada.html

Browse files
Files changed (1) hide show
  1. templates/shabihsazi-sada.html +1458 -0
templates/shabihsazi-sada.html ADDED
@@ -0,0 +1,1458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>شبیه‌ساز اختصاصی صدا - OmniVoice Clone API</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800;900&display=swap');
9
+ :root {
10
+ --app-font: 'Vazirmatn', sans-serif;
11
+ --app-bg: #F8F9FC;
12
+ --panel-bg: #FFFFFF;
13
+ --panel-border: #EAEFF7;
14
+ --input-bg: #F6F8FB;
15
+ --input-border: #E1E7EF;
16
+ --text-primary: #1A202C;
17
+ --text-secondary: #626F86;
18
+ --text-tertiary: #8A94A6;
19
+ --accent-primary: #4A6CFA; /* آبی رویال پرمیوم آلفا */
20
+ --accent-primary-hover: #3553D6;
21
+ --accent-primary-glow: rgba(74, 108, 250, 0.25);
22
+ --accent-secondary: #0FD4A8; /* سبز نعنایی متمایل به فیروزه‌ای آلفا */
23
+ --accent-secondary-hover: #0DA986;
24
+ --accent-secondary-glow: rgba(15, 212, 168, 0.2);
25
+ --waveform-color-active: var(--accent-primary);
26
+ --waveform-color-inactive: #D0D9E6;
27
+ --waveform-dashed-line-color: #E0E4E9;
28
+ --shadow-sm: 0 1px 2px 0 rgba(26, 32, 44, 0.03);
29
+ --shadow-md: 0 4px 6px -1px rgba(26, 32, 44, 0.05), 0 2px 4px -2px rgba(26, 32, 44, 0.04);
30
+ --shadow-lg: 0 10px 15px -3px rgba(26, 32, 44, 0.06), 0 4px 6px -4px rgba(26, 32, 44, 0.05);
31
+ --shadow-xl: 0 20px 25px -5px rgba(26, 32, 44, 0.07), 0 8px 10px -6px rgba(26, 32, 44, 0.05);
32
+ --radius-card: 24px;
33
+ --radius-btn: 14px;
34
+ --radius-input: 12px;
35
+ --transition-fast: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
36
+ --transition-smooth: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
37
+ }
38
+
39
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
40
+ @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
41
+ @keyframes rotate-loader-orbital { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
42
+ @keyframes orbit-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
43
+ @keyframes satellite-pulse-1 { from { transform: scale(0.7) translateX(-50%); opacity: 0.6; } to { transform: scale(1.1) translateX(-50%); opacity: 1; } }
44
+ @keyframes satellite-pulse-2 { from { transform: scale(0.7) translateY(-50%); opacity: 0.6; } to { transform: scale(1.1) translateY(-50%); opacity: 1; } }
45
+ @keyframes satellite-pulse-3 { from { transform: scale(0.7) translateX(50%); opacity: 0.6; } to { transform: scale(1.1) translateX(50%); opacity: 1; } }
46
+ @keyframes sound-wave { 0% { height: 2px; } 25% { height: 10px; } 50% { height: 5px; } 100% { height: 2px; } }
47
+ @keyframes scaleInRef { from { transform: scale(0.96); opacity: 0; } to { transform: scale(1); opacity: 1; } }
48
+ @keyframes pulseRefIcon { 0% { transform: scale(1); opacity: 0.8; } 100% { transform: scale(1.15); opacity: 1; filter: drop-shadow(0 0 4px var(--accent-primary-glow)); } }
49
+ @keyframes badge-fade-in { from { opacity: 0; transform: translateY(-10px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
50
+
51
+ html { scroll-behavior: smooth; }
52
+ body {
53
+ font-family: var(--app-font);
54
+ direction: rtl;
55
+ background-color: var(--app-bg);
56
+ color: var(--text-primary);
57
+ font-size: 16px;
58
+ line-height: 1.8;
59
+ margin: 0;
60
+ padding: 2.5rem 0;
61
+ min-height: 100vh;
62
+ -webkit-font-smoothing: antialiased;
63
+ display: flex;
64
+ justify-content: center;
65
+ align-items: flex-start;
66
+ overflow-x: hidden;
67
+ background-image: radial-gradient(var(--text-tertiary) 0.5px, transparent 0.5px);
68
+ background-size: 20px 20px;
69
+ background-position: -10px -10px;
70
+ }
71
+
72
+ .app-container { max-width: 820px; width: 92%; margin: 0 auto; }
73
+ .app-header { padding: 0.5rem 0 2.5rem 0; text-align: center; margin-bottom: 1.5rem; animation: fadeIn 0.8s 0.2s ease-out backwards; }
74
+ .app-header h1 { font-size: 2.3em; font-weight: 900; margin: 0 0 0.8rem 0; background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -1px; }
75
+ .app-header p { font-size: 1.05em; color: var(--text-secondary); margin-top: 0; opacity: 0.9; font-weight: 400; line-height: 1.7; }
76
+
77
+ .main-content { position: relative; padding: 3rem; background-color: var(--panel-bg); border-radius: var(--radius-card); box-shadow: var(--shadow-xl); border: 1px solid var(--panel-border); animation: fadeIn 0.8s 0.4s ease-out backwards; }
78
+ .form-group { margin-bottom: 2.2rem; }
79
+ label { display: block; font-weight: 700; color: var(--text-primary); font-size: 1.1em; margin-bottom: 1rem; }
80
+
81
+ textarea, input[type="text"] {
82
+ width: 100%;
83
+ padding: 1rem 1.2rem;
84
+ border-radius: var(--radius-input);
85
+ border: 1px solid var(--input-border);
86
+ background-color: var(--input-bg);
87
+ color: var(--text-primary);
88
+ box-shadow: var(--shadow-sm) inset;
89
+ font-family: var(--app-font);
90
+ font-size: 1rem;
91
+ box-sizing: border-box;
92
+ transition: var(--transition-smooth);
93
+ }
94
+ textarea:focus, input[type="text"]:focus {
95
+ outline: none;
96
+ border-color: var(--accent-primary);
97
+ box-shadow: 0 0 0 3px var(--accent-primary-glow), var(--shadow-sm) inset;
98
+ background-color: var(--panel-bg);
99
+ }
100
+ textarea { min-height: 120px; resize: vertical; }
101
+
102
+ /* شمارشگر کاراکتر */
103
+ .char-counter {
104
+ text-align: left;
105
+ font-size: 0.85rem;
106
+ color: var(--text-secondary);
107
+ margin-top: 0.5rem;
108
+ font-weight: 500;
109
+ }
110
+
111
+ /* استایل سفارشی بارگذاری فایل */
112
+ .upload-zone {
113
+ border: 2px dashed var(--input-border);
114
+ background-color: var(--input-bg);
115
+ border-radius: var(--radius-card);
116
+ padding: 2.5rem 1.5rem;
117
+ text-align: center;
118
+ cursor: pointer;
119
+ transition: var(--transition-smooth);
120
+ display: flex;
121
+ flex-direction: column;
122
+ align-items: center;
123
+ gap: 0.75rem;
124
+ }
125
+ .upload-zone:hover {
126
+ border-color: var(--accent-primary);
127
+ background-color: rgba(74, 108, 250, 0.04);
128
+ }
129
+ .upload-icon {
130
+ font-size: 2.5rem;
131
+ line-height: 1;
132
+ transition: var(--transition-smooth);
133
+ }
134
+ .upload-zone:hover .upload-icon {
135
+ transform: translateY(-4px) scale(1.05);
136
+ }
137
+ .upload-text {
138
+ font-weight: 700;
139
+ color: var(--text-primary);
140
+ font-size: 1.05rem;
141
+ }
142
+ .upload-sub {
143
+ font-size: 0.85rem;
144
+ color: var(--text-secondary);
145
+ }
146
+
147
+ /* پلیر فایل مرجع بارگذاری شده */
148
+ .ref-audio-container {
149
+ position: relative;
150
+ background-color: var(--panel-bg);
151
+ border: 2px solid var(--panel-border);
152
+ border-radius: var(--radius-card);
153
+ padding: 1.5rem;
154
+ display: flex;
155
+ flex-direction: column;
156
+ gap: 1rem;
157
+ box-shadow: var(--shadow-md);
158
+ animation: scaleInRef 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) both;
159
+ }
160
+ .btn-remove-ref {
161
+ position: absolute;
162
+ top: 14px;
163
+ left: 14px;
164
+ background: rgba(239, 68, 68, 0.1);
165
+ color: #ef4444;
166
+ border: none;
167
+ border-radius: 50%;
168
+ width: 32px;
169
+ height: 32px;
170
+ display: flex;
171
+ align-items: center;
172
+ justify-content: center;
173
+ cursor: pointer;
174
+ transition: var(--transition-fast);
175
+ padding: 0;
176
+ z-index: 10;
177
+ }
178
+ .btn-remove-ref:hover {
179
+ background: #ef4444;
180
+ color: #fff;
181
+ transform: rotate(90deg);
182
+ }
183
+ .btn-remove-ref svg {
184
+ width: 18px;
185
+ height: 18px;
186
+ fill: currentColor;
187
+ }
188
+ .ref-audio-info {
189
+ display: flex;
190
+ align-items: center;
191
+ gap: 1rem;
192
+ padding-left: 2rem;
193
+ }
194
+ .ref-audio-icon-animated {
195
+ font-size: 1.8rem;
196
+ animation: pulseRefIcon 1.5s ease-in-out infinite alternate;
197
+ }
198
+ .ref-audio-details {
199
+ display: flex;
200
+ flex-direction: column;
201
+ gap: 0.1rem;
202
+ }
203
+ .ref-file-name {
204
+ font-weight: 700;
205
+ color: var(--text-primary);
206
+ font-size: 0.95rem;
207
+ word-break: break-all;
208
+ }
209
+ .ref-file-size {
210
+ font-size: 0.8rem;
211
+ color: var(--text-secondary);
212
+ }
213
+ .ref-audio-controls {
214
+ display: flex;
215
+ align-items: center;
216
+ gap: 1rem;
217
+ background-color: var(--input-bg);
218
+ padding: 0.75rem 1rem;
219
+ border-radius: var(--radius-input);
220
+ direction: ltr;
221
+ }
222
+ .ref-play-btn {
223
+ background: var(--accent-primary);
224
+ color: #fff;
225
+ border: none;
226
+ width: 36px;
227
+ height: 36px;
228
+ border-radius: 50%;
229
+ display: flex;
230
+ align-items: center;
231
+ justify-content: center;
232
+ cursor: pointer;
233
+ transition: var(--transition-fast);
234
+ padding: 0;
235
+ flex-shrink: 0;
236
+ }
237
+ .ref-play-btn:hover {
238
+ background: var(--accent-primary-hover);
239
+ transform: scale(1.05);
240
+ }
241
+ .ref-play-btn svg {
242
+ width: 18px;
243
+ height: 18px;
244
+ fill: currentColor;
245
+ }
246
+ .ref-progress-container {
247
+ flex-grow: 1;
248
+ height: 6px;
249
+ background-color: var(--waveform-color-inactive);
250
+ border-radius: 3px;
251
+ position: relative;
252
+ cursor: pointer;
253
+ }
254
+ .ref-progress-bar {
255
+ height: 100%;
256
+ background-color: var(--accent-primary);
257
+ border-radius: 3px;
258
+ width: 0%;
259
+ transition: width 0.1s linear;
260
+ }
261
+ .ref-audio-time {
262
+ font-size: 0.8rem;
263
+ color: var(--text-secondary);
264
+ font-variant-numeric: tabular-nums;
265
+ min-width: 75px;
266
+ text-align: left;
267
+ font-family: var(--app-font);
268
+ }
269
+
270
+ .generate-btn {
271
+ display: flex;
272
+ align-items: center;
273
+ justify-content: center;
274
+ gap: 10px;
275
+ width: 100%;
276
+ padding: 1.1rem 1.5rem;
277
+ font-size: 1.25em;
278
+ font-weight: 800;
279
+ background: linear-gradient(95deg, var(--accent-secondary) 0%, var(--accent-primary) 100%);
280
+ color: #fff;
281
+ border: none;
282
+ border-radius: var(--radius-btn);
283
+ cursor: pointer;
284
+ transition: all 0.3s ease;
285
+ box-shadow: 0 6px 12px -3px var(--accent-primary-glow), 0 6px 12px -3px var(--accent-secondary-glow);
286
+ position: relative;
287
+ overflow: hidden;
288
+ letter-spacing: 0.5px;
289
+ }
290
+ .generate-btn:hover:not(:disabled) {
291
+ transform: translateY(-5px) scale(1.02);
292
+ box-shadow: 0 8px 20px -4px var(--accent-primary-glow), 0 8px 20px -4px var(--accent-secondary-glow);
293
+ }
294
+ .generate-btn:disabled {
295
+ background: var(--text-tertiary);
296
+ cursor: not-allowed;
297
+ box-shadow: none;
298
+ color: rgba(255,255,255,0.7);
299
+ }
300
+ .generate-btn .spinner {
301
+ width: 20px;
302
+ height: 20px;
303
+ border: 3px solid rgba(255, 255, 255, 0.4);
304
+ border-top-color: #fff;
305
+ border-radius: 50%;
306
+ animation: spin 0.8s linear infinite;
307
+ display: none;
308
+ }
309
+
310
+ .output-section {
311
+ margin-top: 3rem;
312
+ display: flex;
313
+ align-items: center;
314
+ justify-content: center;
315
+ flex-direction: column;
316
+ min-height: 220px;
317
+ position: relative;
318
+ box-sizing: border-box;
319
+ padding: 2rem;
320
+ background-color: var(--input-bg);
321
+ border-radius: var(--radius-card);
322
+ border: 2px dashed var(--input-border);
323
+ box-shadow: var(--shadow-sm) inset;
324
+ transition: var(--transition-smooth);
325
+ }
326
+ .output-section.has-content { background-color: transparent; border-color: transparent; box-shadow: none; padding: 0; min-height: auto; }
327
+ .status-message { font-weight: 500; color: var(--text-secondary); text-align: center; font-size: 1.1em; }
328
+
329
+ /* لودر چرخان اختصاصی آلفا */
330
+ .loading-animation-wrapper { display: none; flex-direction: column; align-items: center; justify-content: center; gap: 1.8rem; width: 100%; }
331
+ .orbital-loader { width: 110px; height: 110px; position: relative; animation: rotate-loader-orbital 10s linear infinite; }
332
+ .orbit { position: absolute; top: 50%; left: 50%; border: 2px dashed rgba(74, 108, 250, 0.35); border-radius: 50%; transform-origin: center center; }
333
+ .orbit:nth-child(1) { width: 35px; height: 35px; margin: -17.5px 0 0 -17.5px; animation: orbit-spin 2.8s linear infinite reverse; }
334
+ .orbit:nth-child(2) { width: 65px; height: 65px; margin: -32.5px 0 0 -32.5px; animation: orbit-spin 3.8s linear infinite; }
335
+ .orbit:nth-child(3) { width: 95px; height: 95px; margin: -47.5px 0 0 -47.5px; animation: orbit-spin 4.8s linear infinite reverse; }
336
+ .orbit .satellite { position: absolute; width: 10px; height: 10px; border-radius: 50%; background-color: var(--accent-primary); box-shadow: 0 0 8px var(--accent-primary), 0 0 12px var(--accent-secondary); }
337
+ .orbit:nth-child(1) .satellite { top: -5px; left: 50%; animation: satellite-pulse-1 1.4s ease-in-out infinite alternate; }
338
+ .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; }
339
+ .orbit:nth-child(3) .satellite { bottom: -5px; right: 50%; animation: satellite-pulse-3 1.4s 0.4s 0.2s ease-in-out infinite alternate;}
340
+ .loading-text { font-size: 1.2em; font-weight: 700; color: var(--text-primary); text-align: center; background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
341
+
342
+ /* پلیر صوتی پیشرفته به همراه افکت ویوفر صوتی */
343
+ .audio-player-content { display: none; width: 100%; padding: 1.5rem; box-sizing: border-box; flex-direction: column; gap: 1.2rem; animation: fadeIn 0.5s ease-out; background-color: var(--panel-bg); border: 1px solid var(--panel-border); box-shadow: var(--shadow-lg); border-radius: var(--radius-card); }
344
+ .audio-waveform-container { display: flex; align-items: center; gap: 1rem; width: 100%; margin-bottom: 1rem; }
345
+ .audio-time { font-size: 0.9em; color: var(--text-secondary); min-width: 40px; text-align: center; font-variant-numeric: tabular-nums; user-select: none; }
346
+ .audio-waveform { flex-grow: 1; height: 60px; position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center; margin-bottom: 0.5rem; }
347
+ .audio-waveform-canvas { display: block; max-width: 100%; height: 100%; width: 100%; }
348
+ .audio-waveform-dashed-line { position: absolute; top: 50%; left: 0; width: 100%; height: 1px; background-image: linear-gradient(to right, var(--waveform-dashed-line-color) 33%, transparent 0%); background-position: center; background-size: 10px 1px; z-index: 1; }
349
+ .audio-controls-group { display: flex; justify-content: center; align-items: center; gap: 1.5rem; margin-bottom: 1rem; }
350
+ .audio-skip-btn, .audio-play-pause-btn-large, .audio-volume-btn { background: none; border: none; cursor: pointer; padding: 8px; transition: transform 0.2s, opacity 0.2s; color: var(--text-secondary); }
351
+ .audio-skip-btn:hover, .audio-play-pause-btn-large:hover, .audio-volume-btn:hover { color: var(--accent-primary); opacity: 0.9; }
352
+ .audio-skip-btn:active, .audio-play-pause-btn-large:active, .audio-volume-btn:active { transform: scale(0.9); }
353
+ .audio-skip-btn svg { width: 28px; height: 28px; fill: currentColor; }
354
+ .audio-play-pause-btn-large { padding: 0; width: 50px; height: 50px; }
355
+ .audio-play-pause-btn-large svg { width: 38px; height: 38px; fill: currentColor; }
356
+ .audio-utility-controls { display: flex; align-items: center; justify-content: space-between; width: 100%; }
357
+ .audio-volume-btn svg { width: 24px; height: 24px; fill: currentColor; }
358
+ .audio-speed-btn { font-family: var(--app-font); font-size: 0.9em; font-weight: 600; background-color: var(--panel-bg); border: 1px solid var(--panel-border); border-radius: 8px; min-width: 40px; text-align: center; color: var(--text-primary); box-shadow: var(--shadow-sm); cursor: pointer; padding: 2px 8px; }
359
+ .audio-speed-btn:hover { background-color: var(--input-bg); }
360
+
361
+ /* کانتینر دانلود تمام قد و متمرکز با استایل ۳بعدی */
362
+ .download-wrapper { width: 100%; box-sizing: border-box; display: flex; flex-direction: column; align-items: center; justify-content: center; }
363
+ .beautiful-download-btn {
364
+ display: flex;
365
+ align-items: center;
366
+ justify-content: center;
367
+ gap: 12px;
368
+ width: 100%;
369
+ box-sizing: border-box;
370
+ margin: 1.5rem auto 0.5rem auto;
371
+ padding: 1.1rem 1.5rem;
372
+ background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); /* دکمه دانلود آبی پرمیوم آلفا */
373
+ color: #fff;
374
+ border: none;
375
+ border-radius: var(--radius-btn);
376
+ font-family: var(--app-font);
377
+ font-weight: 800;
378
+ font-size: 1.25em;
379
+ cursor: pointer;
380
+ text-decoration: none;
381
+ position: relative;
382
+ overflow: hidden;
383
+ box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.1), 0 2px 4px -1px rgba(37, 99, 235, 0.06);
384
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
385
+ border-bottom: 4px solid #1d4ed8;
386
+ text-align: center;
387
+ }
388
+ .beautiful-download-btn:hover {
389
+ transform: translateY(-2px);
390
+ box-shadow: 0 10px 15px -3px rgba(37, 99, 235, 0.2), 0 4px 6px -2px rgba(37, 99, 235, 0.1);
391
+ filter: brightness(1.05);
392
+ }
393
+ .beautiful-download-btn:active {
394
+ transform: translateY(2px);
395
+ border-bottom-width: 0px;
396
+ margin-top: calc(1.5rem + 4px);
397
+ }
398
+ .beautiful-download-btn svg { width: 24px; height: 24px; fill: currentColor; }
399
+
400
+ /* استایل وضعیت اشتراک و توست اطلاعیه */
401
+ #subscription-status-badge {
402
+ display: none;
403
+ padding: 6px 16px;
404
+ border-radius: 20px;
405
+ font-size: 0.85em;
406
+ font-weight: 700;
407
+ margin-top: 1.25rem;
408
+ letter-spacing: 0.5px;
409
+ animation: badge-fade-in 0.6s 0.3s ease-out backwards;
410
+ }
411
+ #subscription-status-badge.free-badge {
412
+ background: linear-gradient(45deg, #6c757d, #495057);
413
+ color: white;
414
+ box-shadow: 0 4px 10px rgba(108, 117, 125, 0.3);
415
+ cursor: pointer;
416
+ }
417
+ #subscription-status-badge.paid-badge {
418
+ background: linear-gradient(45deg, #FFC107, #ffca2c);
419
+ color: #333;
420
+ box-shadow: 0 4px 10px rgba(255, 193, 7, 0.3);
421
+ }
422
+ .toast-notification {
423
+ position: fixed;
424
+ bottom: 25px;
425
+ left: 50%;
426
+ transform: translate(-50%, 120px);
427
+ background: linear-gradient(135deg, rgba(26, 32, 44, 0.95), rgba(49, 58, 77, 0.95));
428
+ backdrop-filter: blur(8px);
429
+ color: #FFFFFF;
430
+ padding: 1rem 1.5rem;
431
+ border-radius: var(--radius-btn);
432
+ box-shadow: var(--shadow-xl);
433
+ z-index: 1100;
434
+ font-size: 0.95rem;
435
+ font-weight: 500;
436
+ max-width: 90%;
437
+ width: 450px;
438
+ text-align: center;
439
+ border: 1px solid rgba(255, 255, 255, 0.1);
440
+ opacity: 0;
441
+ visibility: hidden;
442
+ transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
443
+ }
444
+ .toast-notification.show {
445
+ opacity: 1;
446
+ visibility: visible;
447
+ transform: translate(-50%, 0);
448
+ }
449
+
450
+ /* استایل اختصاصی پاپ‌آ‌پ‌های شیک محدودیت آلفا مطابق نمونه تصویر */
451
+ .modal-overlay {
452
+ display: none;
453
+ position: fixed;
454
+ top: 0;
455
+ left: 0;
456
+ width: 100%;
457
+ height: 100%;
458
+ background-color: rgba(26, 32, 44, 0.4);
459
+ backdrop-filter: blur(8px);
460
+ z-index: 2000;
461
+ align-items: center;
462
+ justify-content: center;
463
+ animation: fadeIn 0.3s ease-out;
464
+ }
465
+ .modal-overlay.active {
466
+ display: flex;
467
+ }
468
+ .modal-content-custom {
469
+ background-color: var(--panel-bg);
470
+ padding: 2.5rem 2rem;
471
+ border-radius: 32px;
472
+ box-shadow: var(--shadow-xl);
473
+ border: 1px solid var(--panel-border);
474
+ width: 90%;
475
+ max-width: 440px;
476
+ text-align: center;
477
+ animation: scaleInRef 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) both;
478
+ display: flex;
479
+ flex-direction: column;
480
+ align-items: center;
481
+ box-sizing: border-box;
482
+ }
483
+ .modal-icon-wrapper {
484
+ width: 100px;
485
+ height: 100px;
486
+ margin-bottom: 1.5rem;
487
+ background: linear-gradient(135deg, #3b82f6 0%, #0fd4a8 100%);
488
+ border-radius: 50%;
489
+ display: flex;
490
+ align-items: center;
491
+ justify-content: center;
492
+ color: white;
493
+ box-shadow: 0 10px 20px rgba(74, 108, 250, 0.2);
494
+ }
495
+ .modal-icon-wrapper svg {
496
+ width: 44px;
497
+ height: 44px;
498
+ }
499
+ .modal-title-custom {
500
+ font-size: 1.6rem;
501
+ font-weight: 800;
502
+ margin: 0 0 1.2rem 0;
503
+ background: linear-gradient(90deg, #2563eb, #09a17e);
504
+ -webkit-background-clip: text;
505
+ -webkit-text-fill-color: transparent;
506
+ }
507
+ .modal-text-custom {
508
+ font-size: 0.95rem;
509
+ color: var(--text-secondary);
510
+ line-height: 1.8;
511
+ margin: 0 0 2rem 0;
512
+ text-align: justify;
513
+ text-justify: inter-word;
514
+ }
515
+ .modal-btn-primary {
516
+ display: flex;
517
+ align-items: center;
518
+ justify-content: center;
519
+ width: 100%;
520
+ padding: 1.1rem;
521
+ font-size: 1.15rem;
522
+ font-weight: 700;
523
+ background: linear-gradient(90deg, #3b82f6 0%, #0fd4a8 100%);
524
+ color: #fff;
525
+ border: none;
526
+ border-radius: var(--radius-btn);
527
+ cursor: pointer;
528
+ transition: var(--transition-smooth);
529
+ box-shadow: 0 4px 15px rgba(74, 108, 250, 0.2);
530
+ }
531
+ .modal-btn-primary:hover {
532
+ transform: translateY(-2px);
533
+ box-shadow: 0 6px 20px rgba(74, 108, 250, 0.35);
534
+ }
535
+ .modal-btn-secondary {
536
+ background: none;
537
+ border: none;
538
+ color: var(--text-secondary);
539
+ font-family: var(--app-font);
540
+ font-size: 0.95rem;
541
+ font-weight: 600;
542
+ cursor: pointer;
543
+ margin-top: 1.5rem;
544
+ transition: var(--transition-fast);
545
+ }
546
+ .modal-btn-secondary:hover {
547
+ color: var(--text-primary);
548
+ }
549
+
550
+ @media (max-width: 600px) {
551
+ body { padding: 1.5rem 0; }
552
+ .main-content { padding: 1.5rem; }
553
+ .app-header h1 { font-size: 1.8em; }
554
+ }
555
+ </style>
556
+ </head>
557
+ <body>
558
+ <div class="app-container">
559
+ <header class="app-header">
560
+ <h1>سامانه شبیه‌ساز صدا آلفا</h1>
561
+ <p>با یک فایل صوتی کوتاه از هر شخصی متن رو به همون صدا تبدیل کنید</p>
562
+ <div id="subscription-status-badge" style="display: inline-block;"></div>
563
+ </header>
564
+
565
+ <main class="main-content">
566
+ <form id="voice-clone-form" onsubmit="return false;">
567
+ <div class="form-group">
568
+ <label for="inputText">📝 متن مورد نظر برای تبدیل به گفتار</label>
569
+ <textarea id="inputText" rows="5" placeholder="متنی را که می‌خواهید با صدای شبیه‌سازی‌شده خوانده شود، بنویسید..."></textarea>
570
+ <div id="char-counter" class="char-counter"><span id="current-char-count" style="color: var(--accent-primary); font-weight: 700; font-family: var(--app-font);">۰</span> / ۱۰,۰۰۰ نویسه</div>
571
+ </div>
572
+
573
+ <div class="form-group">
574
+ <label>🎤 فایل صوتی مرجع (۳ تا ۳۰ ثانیه)</label>
575
+
576
+ <!-- بخش آپلود فایل -->
577
+ <div id="upload-zone" class="upload-zone" onclick="document.getElementById('refAudio').click()">
578
+ <div class="upload-icon">🎵</div>
579
+ <div class="upload-text">برای انتخاب فایل صوتی کلیک کنید</div>
580
+ <div class="upload-sub">یا فایل خود را بکشید و در این قسمت رها کنید (فرمت MP3، WAV و غیره)</div>
581
+ </div>
582
+ <input type="file" id="refAudio" accept="audio/*" style="display: none;">
583
+
584
+ <!-- پلیر زیبای فایل صوتی مرجع جایگزین باکس آپلود -->
585
+ <div id="ref-audio-player-container" class="ref-audio-container" style="display: none;">
586
+ <button type="button" id="btn-remove-ref" class="btn-remove-ref" title="حذف فایل و انتخاب مجدد">
587
+ <svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
588
+ </button>
589
+ <div class="ref-audio-info">
590
+ <div class="ref-audio-icon-animated">🔊</div>
591
+ <div class="ref-audio-details">
592
+ <span id="ref-file-name" class="ref-file-name">نام فایل</span>
593
+ <span id="ref-file-size" class="ref-file-size">اندازه فایل</span>
594
+ </div>
595
+ </div>
596
+ <div class="ref-audio-controls">
597
+ <button type="button" id="btnPlayPauseRef" class="ref-play-btn" onclick="togglePlayRef()">
598
+ <svg viewBox="0 0 24 24" id="play-icon-ref"><path d="M8 5v14l11-7z"/></svg>
599
+ <svg viewBox="0 0 24 24" id="pause-icon-ref" style="display:none;"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
600
+ </button>
601
+ <div class="ref-progress-container" onclick="seekRefAudio(event)">
602
+ <div id="ref-progress-bar" class="ref-progress-bar"></div>
603
+ </div>
604
+ <span id="ref-audio-time" class="ref-audio-time">0:00 / 0:00</span>
605
+ </div>
606
+ <audio id="refAudioPlayer" style="display: none;"></audio>
607
+ </div>
608
+ </div>
609
+
610
+ <button type="submit" id="btnGenerate" onclick="generateClone()" class="generate-btn">
611
+ <span id="btnText">✨ تولید صدا</span>
612
+ <div class="spinner" id="btnSpinner"></div>
613
+ </button>
614
+ </form>
615
+
616
+ <div id="output-section" class="output-section">
617
+ <!-- پیام وضعیت اولیه -->
618
+ <div id="status-message-box" class="status-message">فایل صوتی شبیه‌سازی‌شده در اینجا ظاهر خواهد شد.</div>
619
+
620
+ <!-- لودر مدور زمان پردازش -->
621
+ <div id="loading-animation-wrapper" class="loading-animation-wrapper">
622
+ <div class="orbital-loader">
623
+ <div class="orbit"><div class="satellite"></div></div>
624
+ <div class="orbit"><div class="satellite"></div></div>
625
+ <div class="orbit"><div class="satellite"></div></div>
626
+ </div>
627
+ <p id="statusMessage" class="loading-text">در حال بررسی ارتباط...</p>
628
+ </div>
629
+
630
+ <!-- کانتینر پلیر صوتی پیشرفته همراه با ویوفر -->
631
+ <div id="resultContainer" class="audio-player-content">
632
+ <div class="audio-waveform-container">
633
+ <span id="audio-current-time" class="audio-time">0:00</span>
634
+ <div class="audio-waveform">
635
+ <canvas id="audio-canvas" class="audio-waveform-canvas"></canvas>
636
+ <div class="audio-waveform-dashed-line"></div>
637
+ </div>
638
+ <span id="audio-total-time" class="audio-time">0:00</span>
639
+ </div>
640
+
641
+ <div class="audio-controls-group">
642
+ <button type="button" class="audio-skip-btn backward" onclick="skipAudio(-5)" title="پرش به عقب">
643
+ <svg viewBox="0 0 24 24"><path d="M11 16V8l-4 4 4 4zm4-12v16l7-8-7-8z"></path></svg>
644
+ </button>
645
+ <button type="button" id="btnPlayPause" class="audio-play-pause-btn-large" onclick="togglePlay()">
646
+ <svg viewBox="0 0 24 24" id="play-icon"><path d="M8 5v14l11-7z"></path></svg>
647
+ <svg viewBox="0 0 24 24" id="pause-icon" style="display:none;"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"></path></svg>
648
+ </button>
649
+ <button type="button" class="audio-skip-btn forward" onclick="skipAudio(5)" title="پرش به جلو">
650
+ <svg viewBox="0 0 24 24"><path d="M13 16V8l4 4-4 4zM9 4v16L2 12l7-8z"></path></svg>
651
+ </button>
652
+ </div>
653
+
654
+ <div class="audio-utility-controls">
655
+ <button type="button" id="btnMute" class="audio-volume-btn" onclick="toggleMute()" title="قطع/وصل صدا">
656
+ <svg viewBox="0 0 24 24" id="volume-high-icon"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"></path></svg>
657
+ <svg viewBox="0 0 24 24" id="volume-mute-icon" style="display:none;"><path d="M7 9v6h4l5 5V4L11 9H7zM16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zM19 12c0 .94-.23 1.82-.68 2.6L19 14.88c.45-.88.7-1.88.7-2.88 0-4.01-2.99-7.14-7-8.05v2.06c2.89.86 5 3.54 5 6.71zM4.55 4L2 6.55 9.45 14H7v6h4l5 5V14.55l4.05 4.05L22 18 12 8 4.55 4z"></path></svg>
658
+ </button>
659
+ <button type="button" id="btnSpeed" class="audio-speed-btn" onclick="toggleSpeed()" title="سرعت پخش">1x</button>
660
+ </div>
661
+ </div>
662
+
663
+ <div id="download-wrapper-container" class="download-wrapper" style="display: none;">
664
+ <a id="btnDownload" download="cloned_speech.wav" class="beautiful-download-btn">
665
+ <svg viewBox="0 0 24 24"><path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z"></path></svg>
666
+ دانلود فایل صوتی
667
+ </a>
668
+ </div>
669
+ </div>
670
+ </main>
671
+
672
+ <audio id="audioPlayer" style="display: none;"></audio>
673
+ <div id="toast-notification" class="toast-notification"></div>
674
+
675
+ <!-- پاپ‌آپ شیک محدودیت تعداد کاراکتر آلفا دقیقا مشابه تصویر ارسالی -->
676
+ <div class="modal-overlay" id="char-limit-modal">
677
+ <div class="modal-content-custom">
678
+ <div class="modal-icon-wrapper">
679
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
680
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
681
+ <polyline points="14 2 14 8 20 8"></polyline>
682
+ <line x1="16" y1="13" x2="8" y2="13"></line>
683
+ <line x1="16" y1="17" x2="8" y2="17"></line>
684
+ <line x1="10" y1="9" x2="8" y2="9"></line>
685
+ </svg>
686
+ </div>
687
+ <h3 class="modal-title-custom">محدودیت تعداد کاراکتر</h3>
688
+ <p class="modal-text-custom">کاربر گرامی، در نسخه رایگان شما تنها مجاز به تبدیل متن‌های کوتاه (زیر ۵۰۰ کاراکتر) هستید. متن شما طولانی‌تر از حد مجاز است. با ارتقاء به نسخه کامل همه بخش‌های برنامه برای شما در دسترس خواهد بود و امکان تبدیل متن به صدا همزمان تا ۱۰ هزار کاراکتر امکان پذیر است.</p>
689
+ <button type="button" class="modal-btn-primary" id="modal-upgrade-btn">ارتقا به نسخه نامحدود</button>
690
+ <button type="button" class="modal-btn-secondary" id="modal-close-btn">متوجه شدم، متن را کوتاه می‌کنم</button>
691
+ </div>
692
+ </div>
693
+
694
+ <!-- پاپ‌آپ شیک محدودیت استفاده روزانه آلفا با تِم هشدار پرمیوم -->
695
+ <div class="modal-overlay" id="daily-limit-modal">
696
+ <div class="modal-content-custom">
697
+ <div class="modal-icon-wrapper" style="background: linear-gradient(135deg, #3b82f6 0%, #0fd4a8 100%); box-shadow: 0 10px 20px rgba(74, 108, 250, 0.2);">
698
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
699
+ <circle cx="12" cy="12" r="10"></circle>
700
+ <polyline points="12 6 12 12 16 14"></polyline>
701
+ </svg>
702
+ </div>
703
+ <h3 class="modal-title-custom">محدودیت استفاده روزانه</h3>
704
+ <p class="modal-text-custom">کاربر گرامی، شما به سقف ۲ بار شبیه‌سازی رایگان روزانه خود رسیده‌اید. با ارتقاء به نسخه کامل، بدون هیچ محدودیتی می‌توانید به صورت نامحدود از شبیه‌ساز صدا و تمامی ابزارهای پیشرفته آلفا استفاده کنید.</p>
705
+ <button type="button" class="modal-btn-primary" id="modal-daily-upgrade-btn">ارتقا به نسخه نامحدود</button>
706
+ <button type="button" class="modal-btn-secondary" id="modal-daily-close-btn">متوجه شدم، بازگشت</button>
707
+ </div>
708
+ </div>
709
+ </div>
710
+
711
+ <script>
712
+ let userSubscriptionStatus = 'free';
713
+ const PREMIUM_PAGE_ID = '1149636';
714
+
715
+ // پیاده‌سازی سیستم مدیریت کش IndexedDB محلی
716
+ const voiceDB = {
717
+ db: null,
718
+ init() {
719
+ return new Promise((resolve, reject) => {
720
+ const request = indexedDB.open('VoiceCloneDB_V1', 1);
721
+ request.onupgradeneeded = event => {
722
+ const db = event.target.result;
723
+ if (!db.objectStoreNames.contains('voiceState')) {
724
+ db.createObjectStore('voiceState');
725
+ }
726
+ };
727
+ request.onsuccess = event => {
728
+ this.db = event.target.result;
729
+ resolve();
730
+ };
731
+ request.onerror = event => {
732
+ console.error('IndexedDB error:', event.target.errorCode);
733
+ reject(event.target.errorCode);
734
+ };
735
+ });
736
+ },
737
+ save(key, value) {
738
+ return new Promise((resolve, reject) => {
739
+ if (!this.db) return reject("DB not initialized");
740
+ const transaction = this.db.transaction(['voiceState'], 'readwrite');
741
+ const store = transaction.objectStore('voiceState');
742
+ const request = store.put(value, key);
743
+ request.onsuccess = () => resolve();
744
+ request.onerror = event => reject(event.target.error);
745
+ });
746
+ },
747
+ get(key) {
748
+ return new Promise((resolve, reject) => {
749
+ if (!this.db) return reject("DB not initialized");
750
+ const transaction = this.db.transaction(['voiceState'], 'readonly');
751
+ const store = transaction.objectStore('voiceState');
752
+ const request = store.get(key);
753
+ request.onsuccess = () => resolve(request.result);
754
+ request.onerror = event => reject(event.target.error);
755
+ });
756
+ },
757
+ delete(key) {
758
+ return new Promise((resolve, reject) => {
759
+ if (!this.db) return reject("DB not initialized");
760
+ const transaction = this.db.transaction(['voiceState'], 'readwrite');
761
+ const store = transaction.objectStore('voiceState');
762
+ const request = store.delete(key);
763
+ request.onsuccess = () => resolve();
764
+ request.onerror = event => reject(event.target.error);
765
+ });
766
+ }
767
+ };
768
+
769
+ // بازگردانی وضعیت پروژه قبلی در صورت وجود در دیتابیس
770
+ async function loadStoredState() {
771
+ try {
772
+ // بازیابی و نمایش متن ورودی
773
+ const storedText = await voiceDB.get('inputText');
774
+ if (storedText) {
775
+ const inputText = document.getElementById('inputText');
776
+ inputText.value = storedText;
777
+ document.getElementById('current-char-count').textContent = storedText.length.toLocaleString('fa-IR');
778
+ }
779
+
780
+ // بازیابی و نمایش فایل صوتی مرجع گوینده
781
+ const storedRef = await voiceDB.get('refAudio');
782
+ if (storedRef && storedRef.blob) {
783
+ const reconstructedFile = new File([storedRef.blob], storedRef.name, { type: storedRef.blob.type });
784
+ window.restoredRefAudioFile = reconstructedFile;
785
+
786
+ document.getElementById('ref-file-name').textContent = storedRef.name;
787
+ document.getElementById('ref-file-size').textContent = `${(storedRef.size / 1024 / 1024).toFixed(2)} مگابایت`;
788
+
789
+ const objectURL = URL.createObjectURL(reconstructedFile);
790
+ refAudioPlayer.src = objectURL;
791
+
792
+ document.getElementById('upload-zone').style.display = 'none';
793
+ document.getElementById('ref-audio-player-container').style.display = 'flex';
794
+ }
795
+
796
+ // بازیابی و نمایش فایل صوتی شبیه‌سازی‌شده (خروجی نهایی)
797
+ const storedOutput = await voiceDB.get('outputAudio');
798
+ if (storedOutput && storedOutput.blob) {
799
+ const objectURL = URL.createObjectURL(storedOutput.blob);
800
+ audioPlayer.src = objectURL;
801
+ document.getElementById("btnDownload").href = objectURL;
802
+ audioPeaks = storedOutput.peaks || [];
803
+
804
+ setUIState('success');
805
+ drawWaveform();
806
+ }
807
+ } catch(e) {
808
+ console.error("Error loading stored state from IndexedDB:", e);
809
+ }
810
+ }
811
+
812
+ // ارتباط با پنجره والد جهت دریافت وضعیت کاربری
813
+ window.addEventListener('message', (event) => {
814
+ if (event.data?.type === 'USER_DATA_RESPONSE') {
815
+ if (event.data.error || !event.data.payload) {
816
+ updateUIForSubscriptionStatus('free');
817
+ return;
818
+ }
819
+ try {
820
+ const userObject = JSON.parse(event.data.payload);
821
+ updateUIForSubscriptionStatus(isUserPaid(userObject) ? 'paid' : 'free');
822
+ } catch (e) {
823
+ updateUIForSubscriptionStatus('free');
824
+ }
825
+ }
826
+ });
827
+
828
+ function isUserPaid(userObject) {
829
+ return userObject?.isLogin && userObject.accessible_pages?.some(p => p == PREMIUM_PAGE_ID);
830
+ }
831
+
832
+ function updateUIForSubscriptionStatus(status) {
833
+ userSubscriptionStatus = status;
834
+ const badge = document.getElementById('subscription-status-badge');
835
+ if (status === 'paid') {
836
+ badge.textContent = 'نسخه نامحدود';
837
+ badge.className = 'paid-badge';
838
+ } else {
839
+ badge.textContent = 'نسخه رایگان (ارتقا)';
840
+ badge.className = 'free-badge';
841
+ }
842
+ badge.style.display = 'inline-block';
843
+ }
844
+
845
+ function showToast(message) {
846
+ const toast = document.getElementById('toast-notification');
847
+ toast.textContent = message;
848
+ toast.classList.add('show');
849
+ setTimeout(() => {
850
+ toast.classList.remove('show');
851
+ }, 5000);
852
+ }
853
+
854
+ function getTodayString() {
855
+ const today = new Date();
856
+ return today.toISOString().split('T')[0];
857
+ }
858
+
859
+ function checkFreeUserUsage() {
860
+ const todayStr = getTodayString();
861
+ let usage = localStorage.getItem('alpha_voice_free_usage');
862
+ if (usage) {
863
+ try {
864
+ usage = JSON.parse(usage);
865
+ if (usage.date !== todayStr) {
866
+ return 0;
867
+ }
868
+ return usage.count || 0;
869
+ } catch (e) {
870
+ return 0;
871
+ }
872
+ }
873
+ return 0;
874
+ }
875
+
876
+ function incrementFreeUserUsage() {
877
+ const todayStr = getTodayString();
878
+ let currentCount = checkFreeUserUsage();
879
+ localStorage.setItem('alpha_voice_free_usage', JSON.stringify({
880
+ date: todayStr,
881
+ count: currentCount + 1
882
+ }));
883
+ }
884
+
885
+ // بررسی و اجرای مطمئن بعد از آماده‌سازی دام و فعال‌سازی کش محلی
886
+ async function initApp() {
887
+ parent.postMessage({ type: 'REQUEST_USER_DATA' }, '*');
888
+
889
+ const badge = document.getElementById('subscription-status-badge');
890
+ if (badge) {
891
+ badge.addEventListener('click', () => {
892
+ if (userSubscriptionStatus !== 'paid') {
893
+ parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM' }, '*');
894
+ }
895
+ });
896
+ }
897
+
898
+ // هندلر کلیک دکمه‌های پاپ‌آ‌پ کاراکتر آلفا
899
+ const upgradeBtn = document.getElementById('modal-upgrade-btn');
900
+ if (upgradeBtn) {
901
+ upgradeBtn.addEventListener('click', () => {
902
+ document.getElementById('char-limit-modal').classList.remove('active');
903
+ parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM' }, '*');
904
+ });
905
+ }
906
+
907
+ const closeBtn = document.getElementById('modal-close-btn');
908
+ if (closeBtn) {
909
+ closeBtn.addEventListener('click', () => {
910
+ document.getElementById('char-limit-modal').classList.remove('active');
911
+ });
912
+ }
913
+
914
+ // هندلر کلیک دکمه‌های پاپ‌آپ محدودیت استفاده روزانه آلفا
915
+ const dailyUpgradeBtn = document.getElementById('modal-daily-upgrade-btn');
916
+ if (dailyUpgradeBtn) {
917
+ dailyUpgradeBtn.addEventListener('click', () => {
918
+ document.getElementById('daily-limit-modal').classList.remove('active');
919
+ parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM' }, '*');
920
+ });
921
+ }
922
+
923
+ const dailyCloseBtn = document.getElementById('modal-daily-close-btn');
924
+ if (dailyCloseBtn) {
925
+ dailyCloseBtn.addEventListener('click', () => {
926
+ document.getElementById('daily-limit-modal').classList.remove('active');
927
+ });
928
+ }
929
+
930
+ // مدیریت کلیک دکمه دانلود جهت ارسال فایل صوتی به والد (اپلیکیشن اندروید)
931
+ const downloadBtn = document.getElementById('btnDownload');
932
+ if (downloadBtn) {
933
+ downloadBtn.addEventListener('click', async (e) => {
934
+ e.preventDefault();
935
+ const audioUrl = audioPlayer.src;
936
+ if (!audioUrl) return;
937
+
938
+ try {
939
+ const response = await fetch(audioUrl);
940
+ const blob = await response.blob();
941
+ parent.postMessage({
942
+ type: 'PROCESS_AND_DOWNLOAD_AUDIO',
943
+ blob: blob
944
+ }, '*');
945
+ } catch (err) {
946
+ console.error("Error sending audio blob to parent:", err);
947
+ // بازگشت به حالت دانلود عادی مرورگر در صورت بروز خطا
948
+ const link = document.createElement('a');
949
+ link.href = audioUrl;
950
+ link.download = 'cloned_speech.wav';
951
+ link.click();
952
+ }
953
+ });
954
+ }
955
+
956
+ // راه‌اندازی فرآیند لود مجدد وضعیت از دیتابیس
957
+ try {
958
+ await voiceDB.init();
959
+ await loadStoredState();
960
+ } catch(e) {
961
+ console.error("IndexedDB initialization failed:", e);
962
+ }
963
+ }
964
+
965
+ if (document.readyState === 'loading') {
966
+ document.addEventListener('DOMContentLoaded', initApp);
967
+ } else {
968
+ initApp();
969
+ }
970
+
971
+ let currentRunId = '';
972
+ let pollInterval = null;
973
+ let emptyActionCounter = 0;
974
+
975
+ let audioContext = null;
976
+ let audioPeaks = [];
977
+ const audioPlayer = document.getElementById("audioPlayer");
978
+ const refAudioPlayer = document.getElementById("refAudioPlayer");
979
+
980
+ // مدیریت شمارش زنده نویسه‌ها و ذخیره‌سازی همزمان متن در دیتابیس
981
+ document.getElementById('inputText').addEventListener('input', function() {
982
+ const count = this.value.length;
983
+ document.getElementById('current-char-count').textContent = count.toLocaleString('fa-IR');
984
+ voiceDB.save('inputText', this.value).catch(err => console.error(err));
985
+ });
986
+
987
+ // مدیریت افزودن فایل صوتی مرجع و ذخیره فایل در کش محلی
988
+ document.getElementById('refAudio').addEventListener('change', function(e) {
989
+ const file = e.target.files[0];
990
+ if (file) {
991
+ document.getElementById('ref-file-name').textContent = file.name;
992
+ document.getElementById('ref-file-size').textContent = `${(file.size / 1024 / 1024).toFixed(2)} مگابایت`;
993
+
994
+ const objectURL = URL.createObjectURL(file);
995
+ refAudioPlayer.src = objectURL;
996
+
997
+ document.getElementById('upload-zone').style.display = 'none';
998
+ document.getElementById('ref-audio-player-container').style.display = 'flex';
999
+
1000
+ // حذف فایل شبیه‌سازی قبلی جهت جلوگیری از تداخل
1001
+ window.restoredRefAudioFile = null;
1002
+
1003
+ voiceDB.save('refAudio', {
1004
+ blob: file,
1005
+ name: file.name,
1006
+ size: file.size
1007
+ }).catch(err => console.error("Error saving ref file to IndexedDB:", err));
1008
+ }
1009
+ });
1010
+
1011
+ // مدیریت دکمه حذف فایل صوتی مرجع و پاکسازی دیتابیس
1012
+ document.getElementById('btn-remove-ref').addEventListener('click', function(e) {
1013
+ e.stopPropagation();
1014
+ refAudioPlayer.pause();
1015
+ refAudioPlayer.src = '';
1016
+ document.getElementById('refAudio').value = '';
1017
+ window.restoredRefAudioFile = null;
1018
+
1019
+ document.getElementById('play-icon-ref').style.display = 'block';
1020
+ document.getElementById('pause-icon-ref').style.display = 'none';
1021
+ document.getElementById('ref-progress-bar').style.width = '0%';
1022
+ document.getElementById('ref-audio-time').textContent = '0:00 / 0:00';
1023
+
1024
+ document.getElementById('upload-zone').style.display = 'flex';
1025
+ document.getElementById('ref-audio-player-container').style.display = 'none';
1026
+
1027
+ voiceDB.delete('refAudio').catch(err => console.error(err));
1028
+ });
1029
+
1030
+ // توابع کنترل پلیر صوتی مرجع
1031
+ function togglePlayRef() {
1032
+ if (refAudioPlayer.paused) {
1033
+ refAudioPlayer.play();
1034
+ document.getElementById('play-icon-ref').style.display = 'none';
1035
+ document.getElementById('pause-icon-ref').style.display = 'block';
1036
+ } else {
1037
+ refAudioPlayer.pause();
1038
+ document.getElementById('play-icon-ref').style.display = 'block';
1039
+ document.getElementById('pause-icon-ref').style.display = 'none';
1040
+ }
1041
+ }
1042
+
1043
+ refAudioPlayer.addEventListener('timeupdate', () => {
1044
+ if (refAudioPlayer.duration) {
1045
+ const progress = (refAudioPlayer.currentTime / refAudioPlayer.duration) * 100;
1046
+ document.getElementById('ref-progress-bar').style.width = `${progress}%`;
1047
+ document.getElementById('ref-audio-time').textContent = `${formatTime(refAudioPlayer.currentTime)} / ${formatTime(refAudioPlayer.duration)}`;
1048
+ }
1049
+ });
1050
+
1051
+ refAudioPlayer.addEventListener('loadedmetadata', () => {
1052
+ document.getElementById('ref-audio-time').textContent = `0:00 / ${formatTime(refAudioPlayer.duration)}`;
1053
+ });
1054
+
1055
+ refAudioPlayer.addEventListener('ended', () => {
1056
+ document.getElementById('play-icon-ref').style.display = 'block';
1057
+ document.getElementById('pause-icon-ref').style.display = 'none';
1058
+ document.getElementById('ref-progress-bar').style.width = '0%';
1059
+ });
1060
+
1061
+ function seekRefAudio(event) {
1062
+ const container = event.currentTarget;
1063
+ const rect = container.getBoundingClientRect();
1064
+ const clickX = event.clientX - rect.left;
1065
+ const percentage = clickX / rect.width;
1066
+ refAudioPlayer.currentTime = (refAudioPlayer.duration || 0) * percentage;
1067
+ }
1068
+
1069
+ // پشتیبانی از Drag & Drop برای باکس آپلود
1070
+ const uploadZone = document.getElementById('upload-zone');
1071
+ ['dragenter', 'dragover'].forEach(eventName => {
1072
+ uploadZone.addEventListener(eventName, (e) => { e.preventDefault(); uploadZone.style.borderColor = 'var(--accent-primary)'; }, false);
1073
+ });
1074
+ ['dragleave', 'drop'].forEach(eventName => {
1075
+ uploadZone.addEventListener(eventName, (e) => { e.preventDefault(); uploadZone.style.borderColor = 'var(--input-border)'; }, false);
1076
+ });
1077
+ uploadZone.addEventListener('drop', (e) => {
1078
+ const dt = e.dataTransfer;
1079
+ const files = dt.files;
1080
+ if(files.length > 0) {
1081
+ document.getElementById('refAudio').files = files;
1082
+ const event = new Event('change');
1083
+ document.getElementById('refAudio').dispatchEvent(event);
1084
+ }
1085
+ });
1086
+
1087
+ async function uploadFileToDocker(file, suffix) {
1088
+ const formData = new FormData();
1089
+ const ext = file.name.split('.').pop().toLowerCase();
1090
+ formData.append('run_id', `${currentRunId}_${suffix}`);
1091
+ formData.append('ext', ext);
1092
+ formData.append('file', file);
1093
+
1094
+ const response = await fetch('/api/webhook/upload', { method: 'POST', body: formData });
1095
+ if (!response.ok) throw new Error("خطا در آپلود فایل مرجع.");
1096
+ return ext;
1097
+ }
1098
+
1099
+ function safeBase64Encode(str) {
1100
+ return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => {
1101
+ return String.fromCharCode('0x' + p1);
1102
+ }));
1103
+ }
1104
+
1105
+ function setUIState(state) {
1106
+ const btn = document.getElementById("btnGenerate");
1107
+ const btnText = document.getElementById("btnText");
1108
+ const btnSpinner = document.getElementById("btnSpinner");
1109
+ const outputSection = document.getElementById("output-section");
1110
+ const statusMessageBox = document.getElementById("status-message-box");
1111
+ const loadingWrapper = document.getElementById("loading-animation-wrapper");
1112
+ const resultContainer = document.getElementById("resultContainer");
1113
+ const downloadWrapper = document.getElementById("download-wrapper-container");
1114
+
1115
+ if (state === 'loading') {
1116
+ btn.disabled = true;
1117
+ btnText.innerText = "منتظر بمانید...";
1118
+ btnSpinner.style.display = "inline-block";
1119
+
1120
+ outputSection.classList.remove("has-content");
1121
+ statusMessageBox.style.display = "none";
1122
+ loadingWrapper.style.display = "flex";
1123
+ resultContainer.style.display = "none";
1124
+ downloadWrapper.style.display = "none";
1125
+ } else if (state === 'success') {
1126
+ btn.disabled = false;
1127
+ btnText.innerText = "✨ تولید صدا";
1128
+ btnSpinner.style.display = "none";
1129
+
1130
+ outputSection.classList.add("has-content");
1131
+ statusMessageBox.style.display = "none";
1132
+ loadingWrapper.style.display = "none";
1133
+ resultContainer.style.display = "flex";
1134
+ downloadWrapper.style.display = "flex";
1135
+ } else {
1136
+ btn.disabled = false;
1137
+ btnText.innerText = "✨ تولید صدا";
1138
+ btnSpinner.style.display = "none";
1139
+
1140
+ outputSection.classList.remove("has-content");
1141
+ statusMessageBox.style.display = "block";
1142
+ loadingWrapper.style.display = "none";
1143
+ resultContainer.style.display = "none";
1144
+ downloadWrapper.style.display = "none";
1145
+ }
1146
+ }
1147
+
1148
+ // توابع ترسیم و بهینه‌سازی نمودار صوتی فرکانسی
1149
+ async function processAudioForWaveform(url) {
1150
+ try {
1151
+ if (!audioContext) {
1152
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
1153
+ }
1154
+ const response = await fetch(url);
1155
+ const arrayBuffer = await response.arrayBuffer();
1156
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
1157
+
1158
+ const data = audioBuffer.getChannelData(0);
1159
+ const samples = 45;
1160
+ const peaks = [];
1161
+ const sampleSize = Math.floor(data.length / samples);
1162
+ for (let i = 0; i < samples; i++) {
1163
+ let max = 0;
1164
+ const start = i * sampleSize;
1165
+ for (let j = 0; j < sampleSize; j++) {
1166
+ const val = Math.abs(data[start + j]);
1167
+ if (val > max) max = val;
1168
+ }
1169
+ peaks.push(Math.min(1, Math.max(0, max * 1.5)));
1170
+ }
1171
+ audioPeaks = peaks;
1172
+ drawWaveform();
1173
+ } catch(e) {
1174
+ console.error("Error processing waveform:", e);
1175
+ }
1176
+ }
1177
+
1178
+ function drawWaveform() {
1179
+ const canvas = document.getElementById('audio-canvas');
1180
+ if (!canvas || audioPeaks.length === 0) return;
1181
+ const ctx = canvas.getContext('2d');
1182
+ const dpr = window.devicePixelRatio || 1;
1183
+ canvas.width = canvas.offsetWidth * dpr;
1184
+ canvas.height = canvas.offsetHeight * dpr;
1185
+ ctx.scale(dpr, dpr);
1186
+
1187
+ const width = canvas.offsetWidth;
1188
+ const height = canvas.offsetHeight;
1189
+ ctx.clearRect(0, 0, width, height);
1190
+
1191
+ const barWidth = 3;
1192
+ const barGap = 2;
1193
+ const totalBarWidth = barWidth + barGap;
1194
+ const numBars = Math.floor(width / totalBarWidth);
1195
+ const offset = (width - numBars * totalBarWidth) / 2;
1196
+
1197
+ const progress = audioPlayer.duration ? audioPlayer.currentTime / audioPlayer.duration : 0;
1198
+
1199
+ ctx.fillStyle = "#D0D9E6";
1200
+ for (let i = 0; i < numBars; i++) {
1201
+ const idx = Math.floor((i / numBars) * audioPeaks.length);
1202
+ const barH = (audioPeaks[idx] || 0) * height;
1203
+ ctx.fillRect(offset + i * totalBarWidth, (height - barH) / 2, barWidth, barH);
1204
+ }
1205
+
1206
+ const activeFillEnd = progress * width;
1207
+ if (activeFillEnd > 0) {
1208
+ ctx.save();
1209
+ ctx.beginPath();
1210
+ ctx.rect(0, 0, activeFillEnd, height);
1211
+ ctx.clip();
1212
+ ctx.fillStyle = "var(--accent-primary)";
1213
+ for (let i = 0; i < numBars; i++) {
1214
+ const idx = Math.floor((i / numBars) * audioPeaks.length);
1215
+ const barH = (audioPeaks[idx] || 0) * height;
1216
+ ctx.fillRect(offset + i * totalBarWidth, (height - barH) / 2, barWidth, barH);
1217
+ }
1218
+ ctx.restore();
1219
+ }
1220
+ }
1221
+
1222
+ function formatTime(s) {
1223
+ if (isNaN(s) || s < 0) return '0:00';
1224
+ const m = Math.floor(s / 60);
1225
+ return `${m}:${Math.floor(s % 60).toString().padStart(2, '0')}`;
1226
+ }
1227
+
1228
+ // هندلرهای پلیر صوتی سفارشی خروجی
1229
+ audioPlayer.addEventListener('timeupdate', () => {
1230
+ document.getElementById('audio-current-time').textContent = formatTime(audioPlayer.currentTime);
1231
+ drawWaveform();
1232
+ });
1233
+
1234
+ audioPlayer.addEventListener('loadedmetadata', () => {
1235
+ document.getElementById('audio-total-time').textContent = formatTime(audioPlayer.duration);
1236
+ drawWaveform();
1237
+ });
1238
+
1239
+ audioPlayer.addEventListener('ended', () => {
1240
+ document.getElementById('play-icon').style.display = 'block';
1241
+ document.getElementById('pause-icon').style.display = 'none';
1242
+ });
1243
+
1244
+ window.addEventListener('resize', drawWaveform);
1245
+
1246
+ // بیدارباش صوتی هنگام بازگشت کاربر به برنامه جهت رفع هنگی و تاخیر پلیر صوتی در گوشی
1247
+ document.addEventListener('visibilitychange', () => {
1248
+ if (document.visibilityState === 'visible') {
1249
+ // فعال‌سازی مجدد موتور پردازش صوتی مرورگر در صورت خواب رفتن توسط سیستم‌عامل
1250
+ if (audioContext && audioContext.state === 'suspended') {
1251
+ audioContext.resume();
1252
+ }
1253
+ drawWaveform();
1254
+ }
1255
+ });
1256
+
1257
+ function togglePlay() {
1258
+ // بیدار کردن فوری موتور صوتی دقیقاً در لحظه لمس دکمه پخش
1259
+ if (audioContext && audioContext.state === 'suspended') {
1260
+ audioContext.resume();
1261
+ }
1262
+ if (audioPlayer.paused) {
1263
+ audioPlayer.play();
1264
+ document.getElementById('play-icon').style.display = 'none';
1265
+ document.getElementById('pause-icon').style.display = 'block';
1266
+ } else {
1267
+ audioPlayer.pause();
1268
+ document.getElementById('play-icon').style.display = 'block';
1269
+ document.getElementById('pause-icon').style.display = 'none';
1270
+ }
1271
+ }
1272
+
1273
+ function skipAudio(seconds) {
1274
+ audioPlayer.currentTime = Math.max(0, Math.min(audioPlayer.duration || 0, audioPlayer.currentTime + seconds));
1275
+ }
1276
+
1277
+ function toggleMute() {
1278
+ audioPlayer.muted = !audioPlayer.muted;
1279
+ document.getElementById('volume-high-icon').style.display = audioPlayer.muted ? 'none' : 'block';
1280
+ document.getElementById('volume-mute-icon').style.display = audioPlayer.muted ? 'block' : 'none';
1281
+ }
1282
+
1283
+ const speeds = [1, 1.25, 1.5, 2, 0.75];
1284
+ let currentSpeedIndex = 0;
1285
+ function toggleSpeed() {
1286
+ currentSpeedIndex = (currentSpeedIndex + 1) % speeds.length;
1287
+ const newSpeed = speeds[currentSpeedIndex];
1288
+ audioPlayer.playbackRate = newSpeed;
1289
+ document.getElementById('btnSpeed').textContent = `${newSpeed}x`;
1290
+ }
1291
+
1292
+ function startPolling(runId) {
1293
+ const statusMessage = document.getElementById("statusMessage");
1294
+ const btnDownload = document.getElementById("btnDownload");
1295
+
1296
+ if (pollInterval) clearInterval(pollInterval);
1297
+ emptyActionCounter = 0;
1298
+
1299
+ pollInterval = setInterval(async () => {
1300
+ try {
1301
+ // بررسی مستقیم آماده بودن فایل صوتی با فرمت wav
1302
+ try {
1303
+ const audioCheck = await fetch(`/static/images/${runId}.wav`, { method: 'HEAD' });
1304
+ if (audioCheck.ok) {
1305
+ clearInterval(pollInterval);
1306
+
1307
+ const finalUrl = `/static/images/${runId}.wav`;
1308
+ audioPlayer.src = finalUrl;
1309
+ btnDownload.href = finalUrl;
1310
+
1311
+ await processAudioForWaveform(finalUrl);
1312
+ setUIState('success');
1313
+
1314
+ // ذخیره‌سازی فایل صوتی ساخته شده نهایی در دیتابیس
1315
+ try {
1316
+ const audioRes = await fetch(finalUrl);
1317
+ const audioBlob = await audioRes.blob();
1318
+ await voiceDB.save('outputAudio', {
1319
+ blob: audioBlob,
1320
+ runId: runId,
1321
+ peaks: audioPeaks
1322
+ });
1323
+ } catch(err) {
1324
+ console.error("Error storing output file inside IndexedDB:", err);
1325
+ }
1326
+ return;
1327
+ }
1328
+ } catch(e) {}
1329
+
1330
+ // بررسی وضعیت رانرها و لاگ سرور
1331
+ const activeRes = await fetch('/api/actions/active');
1332
+ const activeData = await activeRes.json();
1333
+ const hasActiveActions = activeData.status === 'success' && activeData.actions.length > 0;
1334
+
1335
+ const statusRes = await fetch(`/api/status/${runId}`);
1336
+ const statusData = await statusRes.json();
1337
+
1338
+ if (statusData.status === 'ready') {
1339
+ const urlLower = statusData.url.toLowerCase();
1340
+ if (urlLower.endsWith('.png') || urlLower.endsWith('.jpg') || urlLower.endsWith('.jpeg')) {
1341
+
1342
+ if (hasActiveActions) {
1343
+ emptyActionCounter = 0;
1344
+ statusMessage.innerText = "تلاش قبلی با محدودیت کارت گرافیک مواجه شد. تخصیص رانر جدید...";
1345
+ return;
1346
+ } else {
1347
+ emptyActionCounter++;
1348
+ if (emptyActionCounter < 4) {
1349
+ statusMessage.innerText = "در حال بازنشانی درخواست روی موقعیت کاری جدید ...";
1350
+ return;
1351
+ }
1352
+ }
1353
+
1354
+ clearInterval(pollInterval);
1355
+ statusMessage.innerText = "پردازش صدا با محدودیت کارت گرافیک روبرو شد. لطفاً دوباره تلاش کنید.";
1356
+ setUIState('idle');
1357
+ }
1358
+ } else {
1359
+ if (hasActiveActions) {
1360
+ emptyActionCounter = 0;
1361
+ statusMessage.innerText = "در حال شبیه‌سازی گام‌به‌گام فرکانس‌های صوتی گوینده...";
1362
+ } else {
1363
+ statusMessage.innerText = "در انتظار تخصیص کانال پردازشی آزاد و ایجاد مخزن کار جدید...";
1364
+ }
1365
+ }
1366
+ } catch (e) {
1367
+ console.error("Error polling:", e);
1368
+ }
1369
+ }, 4000);
1370
+ }
1371
+
1372
+ window.generateClone = async function() {
1373
+ const text = document.getElementById("inputText").value.trim();
1374
+ let refAudioFile = document.getElementById("refAudio").files[0];
1375
+ const refTextVal = "";
1376
+ const statusMessage = document.getElementById("statusMessage");
1377
+
1378
+ const language = "Persian";
1379
+ const hfToken = "";
1380
+
1381
+ // در صورتی که فیلد آپلود فیزیکی خالی باشد، از فایلی که قبلاً کش شده استفاده می‌شود
1382
+ if (!refAudioFile && window.restoredRefAudioFile) {
1383
+ refAudioFile = window.restoredRefAudioFile;
1384
+ }
1385
+
1386
+ if (!text) {
1387
+ alert("لطفاً ابتدا متنی را برای تبدیل به گفتار وارد کنید.");
1388
+ return;
1389
+ }
1390
+ if (!refAudioFile) {
1391
+ alert("لطفاً فایل صوتی گوینده مرجع را وارد کنید.");
1392
+ return;
1393
+ }
1394
+
1395
+ // بررسی محدودیت‌های نسخه رایگان
1396
+ if (userSubscriptionStatus !== 'paid') {
1397
+ if (text.length > 500) {
1398
+ // فعال کردن پاپ‌آپ شیک محدودیت کاراکتر به جای توست ساده
1399
+ document.getElementById('char-limit-modal').classList.add('active');
1400
+ return;
1401
+ }
1402
+
1403
+ const dailyCount = checkFreeUserUsage();
1404
+ if (dailyCount >= 2) {
1405
+ // فعال کردن پاپ‌آپ شیک محدودیت استفاده روزانه
1406
+ document.getElementById('daily-limit-modal').classList.add('active');
1407
+ return;
1408
+ }
1409
+ }
1410
+
1411
+ setUIState('loading');
1412
+ currentRunId = 'voice' + Math.random().toString(36).substring(2, 14);
1413
+
1414
+ try {
1415
+ statusMessage.innerText = "۱/۳: در حال آپلود فایل صوتی مرجع گوینده...";
1416
+ const audExt = await uploadFileToDocker(refAudioFile, 'voice_ref');
1417
+
1418
+ statusMessage.innerText = "۲/۳: بسته‌بندی پکت و ثبت نوبت صف...";
1419
+
1420
+ const b64Prompt = safeBase64Encode(text);
1421
+ const b64RefText = safeBase64Encode(refTextVal || "");
1422
+
1423
+ const voicePayload = `VOICECONFIG_userRunId_${currentRunId}_audExt_${audExt}_lang_${language}_refText_${b64RefText}_prompt_${b64Prompt}`;
1424
+
1425
+ const response = await fetch('/api/generate', {
1426
+ method: 'POST',
1427
+ headers: { 'Content-Type': 'application/json' },
1428
+ body: JSON.stringify({
1429
+ prompt: voicePayload,
1430
+ width: 1024,
1431
+ height: 1024,
1432
+ action_name: 'shabihsazi-sada',
1433
+ hf_token: hfToken
1434
+ })
1435
+ });
1436
+
1437
+ const data = await response.json();
1438
+
1439
+ if (data.status === 'success') {
1440
+ // کسر اعتبار روزانه در صورت موفقیت‌آمیز بودن نوبت صف برای کاربر رایگان
1441
+ if (userSubscriptionStatus !== 'paid') {
1442
+ incrementFreeUserUsage();
1443
+ }
1444
+ statusMessage.innerText = "۳/۳: درخواست با موفقیت به صف پردازش افزوده شد.";
1445
+ startPolling(data.run_id);
1446
+ } else {
1447
+ throw new Error(data.message || 'خطا در صف‌بندی درخواست.');
1448
+ }
1449
+
1450
+ } catch (error) {
1451
+ console.error("An error occurred:", error);
1452
+ alert(`خطا: ${error.message}`);
1453
+ setUIState('idle');
1454
+ }
1455
+ };
1456
+ </script>
1457
+ </body>
1458
+ </html>