Ezmary commited on
Commit
3d68d9a
·
verified ·
1 Parent(s): 7aa72ea

Upload wordpress_export (7).html

Browse files
Files changed (1) hide show
  1. wordpress_export (7).html +518 -0
wordpress_export (7).html ADDED
@@ -0,0 +1,518 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>تولید صدای هوشمند با هوش مصنوعی | AI Sada</title>
7
+ <meta name="description" content="با AI Sada، متن فارسی خود را به صدایی طبیعی و با کیفیت استودیویی تبدیل کنید.">
8
+ <style>
9
+ @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800;900&display=swap');
10
+
11
+ :root {
12
+ --app-font: 'Vazirmatn', sans-serif; --app-bg: #F8F9FC; --panel-bg: #FFFFFF;
13
+ --panel-border: #EAEFF7; --input-bg: #F6F8FB; --input-border: #E1E7EF;
14
+ --text-primary: #1A202C; --text-secondary: #626F86; --text-tertiary: #8A94A6;
15
+ --accent-primary: #4A6CFA; --accent-primary-hover: #3553D6; --accent-primary-glow: rgba(74, 108, 250, 0.25);
16
+ --accent-secondary: #0FD4A8; --accent-secondary-hover: #0DA986; --accent-secondary-glow: rgba(15, 212, 168, 0.2);
17
+ --shadow-sm: 0 1px 2px 0 rgba(26, 32, 44, 0.03); --shadow-md: 0 4px 6px -1px rgba(26, 32, 44, 0.05), 0 2px 4px -2px rgba(26, 32, 44, 0.04);
18
+ --shadow-lg: 0 10px 15px -3px rgba(26, 32, 44, 0.06), 0 4px 6px -4px rgba(26, 32, 44, 0.05);
19
+ --shadow-xl: 0 20px 25px -5px rgba(26, 32, 44, 0.07), 0 8px 10px -6px rgba(26, 32, 44, 0.05);
20
+ --radius-card: 24px; --radius-btn: 14px; --radius-input: 12px;
21
+ --transition-fast: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
22
+ --transition-smooth: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
23
+ --glass-bg: rgba(255, 255, 255, 0.75);
24
+ --glass-border: rgba(255, 255, 255, 0.5);
25
+ --glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1);
26
+ }
27
+
28
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
29
+ @keyframes modalZoomIn { from { opacity: 0; transform: scale(0.9) translateY(20px); } to { opacity: 1; transform: scale(1) translateY(0); } }
30
+ @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
31
+ @keyframes rotate-loader-orbital { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
32
+ @keyframes orbit-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
33
+ @keyframes satellite-pulse-1 { from { transform: scale(0.7) translateX(-50%); opacity: 0.6; } to { transform: scale(1.1) translateX(-50%); opacity: 1; } }
34
+ @keyframes satellite-pulse-2 { from { transform: scale(0.7) translateY(-50%); opacity: 0.6; } to { transform: scale(1.1) translateY(-50%); opacity: 1; } }
35
+ @keyframes satellite-pulse-3 { from { transform: scale(0.7) translateX(50%); opacity: 0.6; } to { transform: scale(1.1) translateX(50%); opacity: 1; } }
36
+
37
+ body {
38
+ font-family: var(--app-font); direction: rtl; background-color: var(--app-bg);
39
+ color: var(--text-primary); margin: 0; padding: 0; min-height: 100vh;
40
+ }
41
+ .page-wrapper { max-width: 820px; width: 92%; margin: 0 auto; padding: 2.5rem 0; }
42
+ .app-container { width: 100%; margin: 0 auto; margin-bottom: 5rem; }
43
+ .app-header { padding: 0.5rem 0 1rem 0; text-align: center; margin-bottom: 1.5rem; animation: fadeIn 0.8s 0.2s ease-out backwards; }
44
+ .app-header h1 { font-size: 2.5em; 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; }
45
+ .app-header p { font-size: 1.05em; color: var(--text-secondary); margin-top: 0; opacity: 0.9; }
46
+
47
+ /* Glass Navigation */
48
+ .glass-nav-container { display: flex; justify-content: center; margin-bottom: 2rem; position: sticky; top: 10px; z-index: 100; }
49
+ .glass-nav { background: var(--glass-bg); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid var(--glass-border); border-radius: 20px; padding: 0.5rem; display: flex; gap: 0.5rem; box-shadow: var(--glass-shadow); overflow-x: auto; max-width: 100%; white-space: nowrap; }
50
+ .glass-nav::-webkit-scrollbar { display: none; }
51
+ .nav-item { background: transparent; border: none; padding: 0.7rem 1.2rem; border-radius: 14px; color: var(--text-secondary); font-family: var(--app-font); font-weight: 700; font-size: 0.95em; cursor: pointer; transition: all 0.3s ease; position: relative; }
52
+ .nav-item.active { background: #fff; color: var(--accent-primary); box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
53
+ .nav-item .badge { font-size: 0.6em; background: #FFC107; color: #000; padding: 2px 6px; border-radius: 6px; position: absolute; top: 2px; left: 2px; }
54
+
55
+ /* Tabs Content */
56
+ .content-tab { display: none; animation: fadeIn 0.5s ease-out; }
57
+ .content-tab.active { display: block; }
58
+
59
+ /* Common Styles (From Your Original Code) */
60
+ .main-content { padding: 3rem; background-color: var(--panel-bg); border-radius: var(--radius-card); box-shadow: var(--shadow-xl); border: 1px solid var(--panel-border); }
61
+ .form-group { margin-bottom: 2.2rem; }
62
+ label { display: block; font-weight: 700; color: var(--text-primary); font-size: 1.1em; margin-bottom: 1rem; }
63
+ textarea, input[type="text"], input[type="email"] { width: 100%; padding: 1rem 1.2rem; border-radius: var(--radius-input); border: 1px solid var(--input-border); background-color: var(--input-bg); color: var(--text-primary); font-family: var(--app-font); font-size: 1rem; box-sizing: border-box; transition: var(--transition-smooth); }
64
+ textarea:focus, input:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-primary-glow); background-color: var(--panel-bg); }
65
+ .generate-btn { display: flex; align-items: center; justify-content: center; gap: 10px; width: 100%; padding: 1.1rem 1.5rem; font-size: 1.25em; font-weight: 800; background: linear-gradient(95deg, var(--accent-secondary) 0%, var(--accent-primary) 100%); color: #fff; border: none; border-radius: var(--radius-btn); cursor: pointer; transition: all 0.3s ease; box-shadow: 0 6px 12px -3px var(--accent-primary-glow); position: relative; overflow: hidden; }
66
+ .generate-btn:hover:not(:disabled) { transform: translateY(-5px); box-shadow: 0 8px 20px -4px var(--accent-primary-glow); }
67
+ .generate-btn:disabled { background: var(--text-tertiary); cursor: not-allowed; }
68
+ .spinner { width: 20px; height: 20px; border: 3px solid rgba(255, 255, 255, 0.4); border-top-color: #fff; border-radius: 50%; animation: spin 0.8s linear infinite; display: none;}
69
+ .output-section { margin-top: 3rem; display: flex; align-items: center; justify-content: center; flex-direction: column; min-height: 220px; padding: 2rem; background-color: var(--input-bg); border-radius: var(--radius-card); border: 2px dashed var(--input-border); transition: var(--transition-smooth); }
70
+ .output-section.has-content { background-color: var(--panel-bg); border: 1px solid var(--panel-border); box-shadow: var(--shadow-lg); padding: 0; min-height: auto; }
71
+ .status-message { font-weight: 500; color: var(--text-secondary); text-align: center; }
72
+ .status-message.error { color: #e53e3e; background-color: #fed7d7; padding: 1rem; border-radius: 12px; }
73
+
74
+ /* Specifics for TTS (Your Code) */
75
+ #standard-view .char-counter-wrapper { font-size: 0.85em; color: var(--text-tertiary); text-align: left; margin-top: 0.75rem; }
76
+ #standard-view #char-count { font-weight: 600; color: var(--accent-primary); }
77
+ #standard-view #selected-speaker-card { display: inline-flex; align-items: center; background: linear-gradient(135deg, var(--input-bg) 0%, var(--panel-bg) 100%); border-radius: 50px; padding: 0.75rem 0.75rem 0.75rem 1.5rem; box-shadow: var(--shadow-md); border: 1px solid var(--panel-border); cursor: pointer; margin-bottom: 1.5rem; width: 100%; max-width: 350px; justify-content: space-between; }
78
+ #standard-view #selected-speaker-card img { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; margin-left: 1rem; border: 3px solid var(--accent-secondary); }
79
+ #standard-view #change-speaker-btn { display: inline-flex; padding: 12px 24px; border-radius: 14px; background: var(--panel-bg); border: 1px solid var(--input-border); cursor: pointer; font-weight: 600; }
80
+ .slider-container { display: flex; align-items: center; gap: 1.5rem; }
81
+ input[type="range"] { flex-grow: 1; -webkit-appearance: none; width: 100%; height: 6px; background: var(--input-border); border-radius: 3px; outline: none; }
82
+ input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 24px; height: 24px; background: #fff; border-radius: 50%; cursor: pointer; border: 4px solid var(--accent-primary); margin-top: -9px; box-shadow: var(--shadow-md); }
83
+ .temperature-value { font-weight: 700; background-color: var(--input-bg); padding: 0.5rem 1rem; border-radius: 8px; border: 1px solid var(--input-border); min-width: 45px; text-align: center; color: var(--accent-primary); }
84
+
85
+ /* New Voice Changer Styles (Matches Your Theme) */
86
+ .vc-model-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 1rem; max-height: 300px; overflow-y: auto; padding: 5px; }
87
+ .vc-model-item { background: var(--input-bg); border: 2px solid transparent; border-radius: 16px; padding: 0.8rem; text-align: center; cursor: pointer; transition: all 0.2s; display: flex; flex-direction: column; align-items: center; }
88
+ .vc-model-item:hover { transform: translateY(-3px); }
89
+ .vc-model-item.selected { border-color: var(--accent-primary); background: #fff; box-shadow: 0 5px 15px rgba(74, 108, 250, 0.2); }
90
+ .vc-model-item img { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; margin-bottom: 0.5rem; border: 2px solid #fff; box-shadow: var(--shadow-sm); }
91
+ .vc-model-item span { font-size: 0.85em; font-weight: 700; color: var(--text-primary); }
92
+
93
+ .upload-area-vc { border: 2px dashed var(--input-border); border-radius: var(--radius-card); padding: 2rem; text-align: center; cursor: pointer; background: var(--input-bg); transition: all 0.3s; }
94
+ .upload-area-vc:hover { border-color: var(--accent-primary); background: #fff; }
95
+ .file-status-bar { display: none; align-items: center; justify-content: space-between; background: #e6fffa; border: 1px solid #b2f5ea; padding: 0.8rem 1rem; border-radius: 12px; margin-top: 1rem; color: #047481; font-weight: 600; }
96
+
97
+ /* Podcast Section */
98
+ .coming-soon-wrapper { text-align: center; padding: 4rem 2rem; background: linear-gradient(135deg, #1A202C, #2D3748); border-radius: var(--radius-card); color: white; position: relative; overflow: hidden; }
99
+ .coming-soon-wrapper::before { content: ''; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%); animation: spin 15s linear infinite; }
100
+
101
+ /* Headers & Auth */
102
+ .header-actions { margin-top: 1rem; text-align: center; }
103
+ #user-status-container { padding: 0.75rem 1.5rem; background-color: var(--input-bg); border-radius: var(--radius-btn); display: none; align-items: center; justify-content: center; gap: 0.5rem; border: 1px solid var(--panel-border); max-width: 320px; margin: 0 auto; }
104
+ #user-status-container .user-sub-status.status-paid { background-color: var(--accent-secondary-glow); color: var(--accent-secondary-hover); }
105
+ #login-check-btn { background: var(--input-bg); border: 1px solid var(--panel-border); color: var(--text-primary); font-weight: 600; padding: 0.75rem 1.5rem; border-radius: var(--radius-btn); cursor: pointer; }
106
+ #login-check-btn:hover { background: var(--accent-primary); color: white; }
107
+
108
+ /* Loader */
109
+ .orbital-loader { width: 110px; height: 110px; position: relative; animation: rotate-loader-orbital 10s linear infinite; }
110
+ .orbit { position: absolute; top: 50%; left: 50%; border: 2px dashed rgba(74, 108, 250, 0.35); border-radius: 50%; }
111
+ .orbit:nth-child(1) { width: 35px; height: 35px; margin: -17.5px 0 0 -17.5px; animation: orbit-spin 2.8s linear infinite reverse; }
112
+ .orbit:nth-child(2) { width: 65px; height: 65px; margin: -32.5px 0 0 -32.5px; animation: orbit-spin 3.8s linear infinite; }
113
+ .orbit:nth-child(3) { width: 95px; height: 95px; margin: -47.5px 0 0 -47.5px; animation: orbit-spin 4.8s linear infinite reverse; }
114
+ .satellite { position: absolute; width: 10px; height: 10px; border-radius: 50%; background-color: var(--accent-primary); }
115
+ .orbit:nth-child(1) .satellite { top: -5px; left: 50%; }
116
+
117
+ /* Audio Player */
118
+ .simple-player-container { width: 100%; display: flex; flex-direction: column; gap: 1rem; align-items: center; }
119
+ .play-controls { display: flex; gap: 1rem; width: 100%; align-items: center; }
120
+ .play-pause-btn-simple { background: linear-gradient(135deg, var(--accent-secondary), var(--accent-primary)); color: white; width: 50px; height: 50px; border-radius: 50%; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; }
121
+ .audio-download-btn-new { display: inline-flex; align-items: center; justify-content: center; gap: 10px; width: 100%; padding: 0.9rem; background: linear-gradient(95deg, var(--accent-secondary), var(--accent-primary)); color: white; border-radius: 14px; text-decoration: none; font-weight: 700; margin-top: 1rem; }
122
+
123
+ /* Modals */
124
+ .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(5px); display: none; align-items: center; justify-content: center; z-index: 1000; }
125
+ .modal-overlay.visible { display: flex; }
126
+ .modal-dialog { background: #fff; padding: 2rem; border-radius: 24px; width: 90%; max-width: 500px; position: relative; }
127
+ .close-modal-btn { position: absolute; top: 1rem; left: 1rem; font-size: 2rem; background: none; border: none; cursor: pointer; line-height: 1; }
128
+
129
+ @media(max-width: 600px) { .vc-model-grid { grid-template-columns: repeat(3, 1fr); } }
130
+ </style>
131
+ </head>
132
+ <body>
133
+
134
+ <div class="page-wrapper">
135
+ <div class="app-container">
136
+
137
+ <!-- Header -->
138
+ <header class="app-header">
139
+ <h1>هوش مصنوعی آلفا صدا</h1>
140
+ <p>کیفیت استودیو، قدرت هوش مصنوعی. متن فارسی را به صدایی فراتر از انتظار تبدیل کنید.</p>
141
+ <div class="header-actions">
142
+ <div id="user-status-container">
143
+ <span id="user-email-display" class="user-email"></span>
144
+ <div id="user-status-details">
145
+ <span id="user-sub-status-display" class="user-sub-status"></span>
146
+ <button id="logout-btn" class="logout-btn">خروج</button>
147
+ </div>
148
+ </div>
149
+ <button id="login-check-btn">ورود / ثبت نام</button>
150
+ </div>
151
+ </header>
152
+
153
+ <!-- Glass Navigation -->
154
+ <div class="glass-nav-container">
155
+ <nav class="glass-nav">
156
+ <button class="nav-item active" onclick="switchTab('tts')">تبدیل متن به صدا</button>
157
+ <button class="nav-item" onclick="switchTab('vc')">تغییر صدا (AI)</button>
158
+ <button class="nav-item" onclick="switchTab('podcast')">ساخت پادکست <span class="badge">بزودی</span></button>
159
+ </nav>
160
+ </div>
161
+
162
+ <!-- TAB 1: Text to Speech (YOUR EXACT ORIGINAL CODE) -->
163
+ <div id="standard-view" class="content-tab active">
164
+ <main class="main-content">
165
+ <form id="standard-tts-form" onsubmit="return false;">
166
+ <div class="form-group"><label for="text-input-standard">📝 متن اصلی</label><textarea id="text-input-standard" rows="5" placeholder="متن خود را برای تبدیل به گفتار اینجا وارد کنید..."></textarea><div class="char-counter-wrapper"><span id="char-count">0</span> / <span id="char-max">50000</span> نویسه</div></div>
167
+ <div class="form-group"><label for="prompt-input-standard">🗣️ توصیف لحن و احساس (اختیاری)</label><input type="text" id="prompt-input-standard" placeholder="مثال: با لحنی آرام و قصه‌گو"></div>
168
+ <div class="form-group">
169
+ <label>🎤 گوینده منتخب</label>
170
+ <div id="selected-speaker-display">
171
+ <div id="selected-speaker-card" title="برای تغییر گوینده کلیک کنید">
172
+ <img id="selected-speaker-img" src="" alt="عکس گوینده">
173
+ <div id="selected-speaker-info">
174
+ <h3 id="selected-speaker-name"></h3>
175
+ <p id="selected-speaker-desc"></p>
176
+ </div>
177
+ </div>
178
+ <button type="button" id="change-speaker-btn">تغییر گوینده</button>
179
+ </div>
180
+ </div>
181
+ <div class="form-group"><div class="label-with-info"><label for="temperature-slider-standard">🌡️ خلاقیت و پویایی صدا</label><div class="info-icon" id="temp-info-icon" role="button" tabindex="0" aria-label="اطلاعات بیشتر">!</div></div><div class="slider-container"><input type="range" id="temperature-slider-standard" class="temperature-slider" min="0.1" max="1.5" step="0.05" value="0.9"><span id="temperature-value-standard" class="temperature-value">0.9</span></div></div>
182
+ <p id="credit-status-message"></p>
183
+ <button type="submit" id="generate-btn-standard" class="generate-btn"><span class="btn-text">✨ خلق صدا با آلفا</span><div class="spinner"></div></button>
184
+ </form>
185
+ <div id="output-section-standard" class="output-section">
186
+ <div id="status-message-standard" class="status-message">صدای تولید شده در اینجا ظاهر خواهد شد.</div>
187
+ <div id="loading-animation-wrapper-standard" class="loading-animation-wrapper"><div class="orbital-loader"><div class="orbit"><div class="satellite"></div></div><div class="orbit"><div class="satellite"></div></div><div class="orbit"><div class="satellite"></div></div></div><p class="loading-text">در حال پردازش هوشمند و تولید صدا...</p></div>
188
+ <div id="audio-player-content-standard" class="audio-player-content"></div>
189
+ </div>
190
+ </main>
191
+ </div>
192
+
193
+ <!-- TAB 2: Voice Changer (New & Complete) -->
194
+ <div id="voice-clone-view-tab" class="content-tab">
195
+ <main class="main-content">
196
+ <div style="text-align:center; margin-bottom:2rem;">
197
+ <h2 style="font-size:1.5em;margin-bottom:0.5rem;color:var(--text-primary);">تغییر صدای جادویی</h2>
198
+ <p style="color:var(--text-secondary);">صدای خود را به صدای خوانندگان و مشاهیر تبدیل کنید.</p>
199
+ </div>
200
+
201
+ <div class="form-group">
202
+ <label>۱. انتخاب مدل صدا:</label>
203
+ <div class="vc-model-grid" id="vc-models-container">
204
+ <!-- Models loaded by JS -->
205
+ </div>
206
+ </div>
207
+
208
+ <div class="form-group">
209
+ <label>۲. آپلود صدای خودتان:</label>
210
+ <div class="upload-area-vc" onclick="document.getElementById('vc-file-input').click()">
211
+ <div style="font-size:2.5rem;margin-bottom:1rem;color:var(--accent-primary);">🎤</div>
212
+ <p>برای انتخاب فایل کلیک کنید (۳ تا ۹ ثانیه)</p>
213
+ <input type="file" id="vc-file-input" accept="audio/*" style="display:none;" onchange="handleVcFile(this)">
214
+ </div>
215
+ <div id="vc-file-status" class="file-status-bar">
216
+ <span id="vc-filename-display"></span>
217
+ <span>✅ آماده</span>
218
+ </div>
219
+ </div>
220
+
221
+ <button id="vc-generate-btn" class="generate-btn" onclick="startVoiceConversion()">
222
+ <span class="btn-text">شروع تغییر صدا</span>
223
+ <div class="spinner"></div>
224
+ </button>
225
+
226
+ <div id="vc-output-section" class="output-section">
227
+ <div id="vc-status-message" class="status-message">فایل خروجی اینجا نمایش داده می‌شود.</div>
228
+ <div id="vc-loader" style="display:none; flex-direction:column; align-items:center; gap:1rem;">
229
+ <div class="orbital-loader"><div class="orbit"><div class="satellite"></div></div><div class="orbit"><div class="satellite"></div></div></div>
230
+ <p style="font-weight:700; color:var(--accent-primary);">در حال پردازش در سرور ابری...</p>
231
+ </div>
232
+ <div id="vc-result-container" style="width:100%; display:none; text-align:center;">
233
+ <p style="color:#047481; font-weight:700; margin-bottom:1rem;">تغییر صدا با موفقیت انجام شد!</p>
234
+ <audio id="vc-audio-player" controls style="width:100%; margin-bottom:1rem;"></audio>
235
+ <a id="vc-download-btn" href="#" class="audio-download-btn-new" download>دانلود فایل نهایی</a>
236
+ </div>
237
+ </div>
238
+ </main>
239
+ </div>
240
+
241
+ <!-- TAB 3: Podcast (Coming Soon) -->
242
+ <div id="podcast-view" class="content-tab">
243
+ <div class="coming-soon-wrapper">
244
+ <h2>استودیو ساخت پادکست</h2>
245
+ <p>به زودی... سناریو بدهید، پادکست چند نفره تحویل بگیرید.</p>
246
+ <div style="font-size:4rem; margin-top:2rem;">🎙️</div>
247
+ </div>
248
+ </div>
249
+
250
+ <audio id="hidden-audio-player" style="display: none;"></audio>
251
+ </div>
252
+
253
+ <!-- Modals -->
254
+ <div id="speaker-modal" class="modal-overlay"><div class="modal-dialog"><div class="modal-header"><h2>گالری گویندگان</h2><button type="button" class="close-modal-btn" data-modal-id="speaker-modal">×</button></div><div id="speaker-grid"></div></div></div>
255
+ <div id="info-modal" class="modal-overlay"><div class="modal-dialog"><div class="modal-header"><h2>راهنما</h2><button class="close-modal-btn" data-modal-id="info-modal">×</button></div><p>این پارامتر خلاقیت را تنظیم میکند.</p></div></div>
256
+
257
+ <div id="email-modal" class="modal-overlay">
258
+ <div class="modal-dialog" style="max-width: 400px; text-align:center;">
259
+ <div class="modal-header"><h2 id="email-modal-title">ورود / ثبت نام</h2><button class="close-modal-btn" data-modal-id="email-modal">×</button></div>
260
+ <form id="email-form" onsubmit="return false;">
261
+ <input type="email" id="login-email-input" required placeholder="example@gmail.com" style="width:100%; padding:1rem; border-radius:12px; border:1px solid #ddd; margin-bottom:1rem;">
262
+ <button type="submit" id="send-code-btn" class="generate-btn">ارسال کد تایید</button>
263
+ </form>
264
+ <form id="code-form" onsubmit="return false;" style="display: none;">
265
+ <input type="text" id="code-input" required placeholder="123456" style="width:100%; padding:1rem; border-radius:12px; border:1px solid #ddd; margin-bottom:1rem; text-align:center; letter-spacing:5px;">
266
+ <button type="submit" id="verify-code-btn" class="generate-btn">تایید و ورود</button>
267
+ <button type="button" id="back-to-email-btn" style="background:none; border:none; margin-top:10px; color:#888;">بازگشت</button>
268
+ </form>
269
+ </div>
270
+ </div>
271
+
272
+ <input type="hidden" id="selected_speaker_id_storage" value="Charon">
273
+
274
+ </div>
275
+
276
+ <script>
277
+ document.addEventListener('DOMContentLoaded', () => {
278
+
279
+ // --- Config ---
280
+ const PROXY_URL = '/tts/proxy.php';
281
+ let currentUser = { email: null, status: 'free', fingerprint: null };
282
+ let selectedVcModel = null;
283
+
284
+ // --- Tab Logic ---
285
+ window.switchTab = (tabId) => {
286
+ document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
287
+ document.querySelectorAll('.content-tab').forEach(el => el.classList.remove('active'));
288
+
289
+ const btns = document.querySelectorAll('.nav-item');
290
+ if(tabId === 'tts') { btns[0].classList.add('active'); document.getElementById('standard-view').classList.add('active'); }
291
+ if(tabId === 'vc') { btns[1].classList.add('active'); document.getElementById('voice-clone-view-tab').classList.add('active'); initVcModels(); }
292
+ if(tabId === 'podcast') { btns[2].classList.add('active'); document.getElementById('podcast-view').classList.add('active'); }
293
+ };
294
+
295
+ // --- Original TTS Logic Variables ---
296
+ const generationButtons = document.querySelectorAll('#generate-btn-standard');
297
+ const creditStatusMessage = document.getElementById('credit-status-message');
298
+ const userStatusContainer = document.getElementById('user-status-container');
299
+ const userEmailDisplay = document.getElementById('user-email-display');
300
+ const userSubStatusDisplay = document.getElementById('user-sub-status-display');
301
+ const loginCheckBtn = document.getElementById('login-check-btn');
302
+ const emailModal = document.getElementById('email-modal');
303
+ const speakerModal = document.getElementById('speaker-modal');
304
+ const infoModal = document.getElementById('info-modal');
305
+ const speakerGridInModal = document.getElementById('speaker-grid');
306
+ const selectedSpeakerIdStorage = document.getElementById('selected_speaker_id_storage');
307
+ const selectedSpeakerImg = document.getElementById('selected-speaker-img');
308
+ const selectedSpeakerName = document.getElementById('selected-speaker-name');
309
+ const selectedSpeakerDesc = document.getElementById('selected-speaker-desc');
310
+ const mainAudioPlayer = document.getElementById('hidden-audio-player');
311
+
312
+ const speakers = [ { id: "Charon", name: "شهاب (مرد)", desc: "صدایی قدرمند و رسا", imgUrl: "https://uploadkon.ir/uploads/a18705_25IMG-۲۰۲۵۰۷۰۵-۱۱۰۵۴۹.jpg" }, { id: "Zephyr", name: "آوا (زن)", desc: "لطیف و دلنشین", imgUrl: "https://uploadkon.ir/uploads/029605_25IMG-۲۰۲۵۰۷۰۵-۱۱۱۲۵۲.jpg" }, { id: "Achird", name: "نوید (مرد)", desc: "جوان و پرانرژی", imgUrl: "https://uploadkon.ir/uploads/697e05_25IMG-۲۰۲۵۰۶۰۹-۰۶۴۶۳۷.jpg" }, { id: "Zubenelgenubi", name: "آرمان (مرد)", desc: "گرم و صمیمی", imgUrl: "https://uploadkon.ir/uploads/a8a705_25IMG-۲۰۲۵۰۷۰۵-۱۱۱۶۲۹.jpg" }, { id: "Vindemiatrix", name: "مهسا (زن)", desc: "باوقار و رسمی", imgUrl: "https://uploadkon.ir/uploads/d74d05_25IMG-۲۰۲۵۰۷۰۵-۱۱۱۸۳۸.jpg" } ];
313
+
314
+ // --- VC Models ---
315
+ const vcModels = [
316
+ { id: 'shadmehr', name: 'شادمهر عقیلی', img: 'https://app.puzzley.net/uploads/user/Jydo/%D8%AA%D8%BA%DB%8C%D8%B1%20%D8%B5%D8%AF%D8%A7%20%D8%A8%D8%A7%20%D9%87%D9%88%D8%B4%20%D9%85%D8%B5%D9%86%D9%88%D8%B9%DB%8C/1000188203.jpg?_t=1725334498', ref: 'https://uploadkon.ir/uploads/55c918_25شادمهر-قوی-2-.mp3' },
317
+ { id: 'moein', name: 'معین', img: 'https://app.puzzley.net/uploads/user/Jydo/%D8%AA%D8%BA%DB%8C%D8%B1%20%D8%B5%D8%AF%D8%A7%20%D8%A8%D8%A7%20%D9%87%D9%88%D8%B4%20%D9%85%D8%B5%D9%86%D9%88%D8%B9%DB%8C/5dbc55de-d6ab-442f-9a00-da874521cc0b.jpg?_t=1725334795', ref: 'https://uploadkon.ir/uploads/f8bb17_25معین-2-.mp3' },
318
+ { id: 'billie', name: 'بیلی آیلیش', img: 'https://app.puzzley.net/uploads/user/Jydo/%D8%AA%D8%BA%DB%8C%D8%B1%20%D8%B5%D8%AF%D8%A7%20%D8%A8%D8%A7%20%D9%87%D9%88%D8%B4%20%D9%85%D8%B5%D9%86%D9%88%D8%B9%DB%8C/1551c598-f02f-4ced-a037-33d2d7317edd.jpg?_t=1726723022', ref: 'https://uploadkon.ir/uploads/c21018_25بیلی-آیلیش-2-.mp3' },
319
+ { id: 'chavoshi', name: 'محسن چاوشی', img: 'https://app.puzzley.net/uploads/user/Jydo/%D8%AA%D8%BA%DB%8C%D8%B1%20%D8%B5%D8%AF%D8%A7%20%D8%A8%D8%A7%20%D9%87%D9%88%D8%B4%20%D9%85%D8%B5%D9%86%D9%88%D8%B9%DB%8C/c52eefb1-071e-40ea-9bc2-e20a7c29cb81.jpg?_t=1726907812', ref: 'https://uploadkon.ir/uploads/7ca518_25محسن-چاووشی-3-2-.mp3' }
320
+ ];
321
+
322
+ // --- Helper Functions ---
323
+ async function getBrowserFingerprint() { return 'fp_' + Math.floor(Math.random() * 10000000).toString(16); }
324
+ function splitTextIntoChunks(text, maxChunkLength = 2500) { const chunks = []; let remainingText = text.trim(); if (remainingText.length <= maxChunkLength) return [remainingText]; while (remainingText.length > 0) { if (remainingText.length <= maxChunkLength) { chunks.push(remainingText); break; } let chunkCandidate = remainingText.substring(0, maxChunkLength); let splitIndex = -1; const delimiters = ['\n', '.', '؟', '!', '؛', '،', ' ']; for (const delimiter of delimiters) { const lastIndex = chunkCandidate.lastIndexOf(delimiter); if (lastIndex !== -1) { splitIndex = lastIndex + 1; break; } } if (splitIndex === -1) splitIndex = maxChunkLength; chunks.push(remainingText.substring(0, splitIndex).trim()); remainingText = remainingText.substring(splitIndex).trim(); } return chunks.filter(chunk => chunk.length > 0); }
325
+ async function mergeAudioBlobs(blobs) { if (!blobs || blobs.length === 0) return null; if (blobs.length === 1) return blobs[0]; const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const decodedBuffers = await Promise.all(blobs.map(blob => blob.arrayBuffer().then(buffer => audioContext.decodeAudioData(buffer)))); const totalLength = decodedBuffers.reduce((acc, buffer) => acc + buffer.length, 0); const firstBuffer = decodedBuffers[0]; const finalBuffer = audioContext.createBuffer(firstBuffer.numberOfChannels, totalLength, firstBuffer.sampleRate); let offset = 0; for (const buffer of decodedBuffers) { for (let channel = 0; channel < buffer.numberOfChannels; channel++) { finalBuffer.copyToChannel(buffer.getChannelData(channel), channel, offset); } offset += buffer.length; } return bufferToWave(finalBuffer); }
326
+ function bufferToWave(abuffer) { const numOfChan = abuffer.numberOfChannels, length = abuffer.length * numOfChan * 2 + 44, buffer = new ArrayBuffer(length), view = new DataView(buffer), channels = []; let i, sample, offset = 0, pos = 0; const setUint16 = (data) => { view.setUint16(pos, data, true); pos += 2; }; const setUint32 = (data) => { view.setUint32(pos, data, true); pos += 4; }; setUint32(0x46464952); setUint32(length - 8); setUint32(0x45564157); setUint32(0x20746d66); setUint32(16); setUint16(1); setUint16(numOfChan); setUint32(abuffer.sampleRate); setUint32(abuffer.sampleRate * 2 * numOfChan); setUint16(numOfChan * 2); setUint16(16); setUint32(0x61746164); setUint32(length - pos - 4); for (i = 0; i < abuffer.numberOfChannels; i++) { channels.push(abuffer.getChannelData(i)); } while (pos < length) { for (i = 0; i < numOfChan; i++) { sample = Math.max(-1, Math.min(1, channels[i][offset])); sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767) | 0; view.setInt16(pos, sample, true); pos += 2; } offset++; } return new Blob([view], { type: 'audio/wav' }); }
327
+ const formatTime = (s) => { if (isNaN(s) || s < 0) return '0:00'; const m = Math.floor(s / 60); return `${m}:${Math.floor(s % 60).toString().padStart(2, '0')}`; };
328
+
329
+ // --- Auth Logic ---
330
+ async function checkUserStatus(email) { if (!email) { updateUIForUserState({ status: 'free', email: null }); return; } try { const response = await fetch('/tts/check_status.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email }) }); const result = await response.json(); updateUIForUserState({ ...result, email: email }); } catch (error) { console.error('Error checking user status:', error); updateUIForUserState({ status: 'free', email: email }); } }
331
+ function updateUIForUserState(userData) { currentUser = { ...currentUser, ...userData }; const isLoggedIn = !!currentUser.email; if (isLoggedIn) { localStorage.setItem('userEmail', currentUser.email); userEmailDisplay.textContent = currentUser.email; loginCheckBtn.style.display = 'none'; userStatusContainer.style.display = 'inline-flex'; if (currentUser.status === 'paid') { userSubStatusDisplay.textContent = `اشتراک ویژه تا ${currentUser.expires_at}`; userSubStatusDisplay.className = 'user-sub-status status-paid'; } else { userSubStatusDisplay.textContent = 'کاربر رایگان'; userSubStatusDisplay.className = 'user-sub-status status-free'; } } else { localStorage.removeItem('userEmail'); currentUser.email = null; userStatusContainer.style.display = 'none'; loginCheckBtn.style.display = 'inline-block'; } }
332
+
333
+ // --- UI Interactions ---
334
+ const updateSelectedSpeakerDisplay = (speakerId) => { const speaker = speakers.find(s => s.id === speakerId) || speakers[0]; selectedSpeakerImg.src = speaker.imgUrl; selectedSpeakerName.textContent = speaker.name; selectedSpeakerDesc.textContent = speaker.desc; selectedSpeakerIdStorage.value = speaker.id; };
335
+ const createSpeakerCardsInModal = () => { speakerGridInModal.innerHTML = ''; speakers.forEach((speaker) => { const card = document.createElement('label'); card.className = 'speaker-card'; card.innerHTML = `<input type="radio" name="modal_speaker_selection" value="${speaker.id}" ${speaker.id === selectedSpeakerIdStorage.value ? 'checked' : ''}><div class="speaker-visual"><img src="${speaker.imgUrl}" alt="${speaker.name}" loading="lazy"></div><div class="speaker-name">${speaker.name}</div>`; card.addEventListener('click', () => { updateSelectedSpeakerDisplay(speaker.id); document.getElementById('speaker-modal').classList.remove('visible'); }); speakerGridInModal.appendChild(card); }); };
336
+
337
+ function createPlayerInstance(containerId) { const playerContent = document.getElementById(containerId); if (!playerContent) return; playerContent.innerHTML = `<div class="simple-player-container"><div class="play-controls"><button type="button" class="play-pause-btn-simple" aria-label="Play/Pause"><svg viewBox="0 0 24 24" class="play-icon"><path d="M8 5v14l11-7z"></path></svg><svg viewBox="0 0 24 24" class="pause-icon" style="display:none;"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"></path></svg></button><div class="progress-wrapper"><span class="time-display current-time">0:00</span><input type="range" class="audio-progress-bar" value="0" min="0" max="100" step="0.1"><span class="time-display total-time">0:00</span></div></div><a href="#" class="audio-download-btn-new" download="ai_sada_output.wav" title="دانلود فایل صوتی"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19.35 10.04A7.49 7.49 0 0 0 12 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 0 0 0 14a6 6 0 0 0 6 6h13a5 5 0 0 0 5-5c0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z"></path></svg><span>دانلود فایل صوتی</span></a></div>`; const playPauseBtn = playerContent.querySelector('.play-pause-btn-simple'), progressBar = playerContent.querySelector('.audio-progress-bar'); playPauseBtn.addEventListener('click', () => { mainAudioPlayer.paused ? mainAudioPlayer.play() : mainAudioPlayer.pause(); }); progressBar.addEventListener('input', () => { if (!isNaN(mainAudioPlayer.duration)) mainAudioPlayer.currentTime = (progressBar.value / 100) * mainAudioPlayer.duration; }); }
338
+ const updatePlayerUI = () => { const isPlaying = !(mainAudioPlayer.paused || mainAudioPlayer.ended), { currentTime, duration } = mainAudioPlayer; document.querySelectorAll('.simple-player-container').forEach(player => { const playIcon = player.querySelector('.play-icon'), pauseIcon = player.querySelector('.pause-icon'), currentTimeEl = player.querySelector('.current-time'), totalTimeEl = player.querySelector('.total-time'), progressBar = player.querySelector('.audio-progress-bar'); if (playIcon) playIcon.style.display = isPlaying ? 'none' : 'block'; if (pauseIcon) pauseIcon.style.display = isPlaying ? 'block' : 'none'; if (currentTimeEl) currentTimeEl.textContent = formatTime(currentTime); if (totalTimeEl) totalTimeEl.textContent = isFinite(duration) ? formatTime(duration) : '0:00'; if (progressBar) progressBar.value = isFinite(duration) && duration > 0 ? (currentTime / duration) * 100 : 0; }); };
339
+
340
+ // --- Standard TTS Logic (Exact) ---
341
+ (function() {
342
+ const form = document.getElementById('standard-tts-form'); if (!form) return;
343
+ const textInput = form.querySelector('#text-input-standard'), promptInput = form.querySelector('#prompt-input-standard'), tempSlider = form.querySelector('#temperature-slider-standard'), tempValueSpan = form.querySelector('#temperature-value-standard'), generateBtn = form.querySelector('#generate-btn-standard'), btnText = generateBtn.querySelector('.btn-text'), btnSpinner = generateBtn.querySelector('.spinner'), outputSection = document.getElementById('output-section-standard'), statusMessage = outputSection.querySelector('#status-message-standard'), loadingAnimation = outputSection.querySelector('#loading-animation-wrapper-standard'), playerContent = outputSection.querySelector('#audio-player-content-standard'), charCount = form.querySelector('#char-count'), charMax = form.querySelector('#char-max'), MAX_CHARS = 50000;
344
+ charMax.textContent = MAX_CHARS.toLocaleString('fa-IR');
345
+ textInput.addEventListener('input', () => { const len = textInput.value.length; charCount.textContent = len.toLocaleString('fa-IR'); charCount.style.color = len > MAX_CHARS ? 'red' : 'var(--accent-primary)'; });
346
+ tempSlider.addEventListener('input', () => tempValueSpan.textContent = tempSlider.value);
347
+ const showLoadingState = () => { generateBtn.disabled = true; btnSpinner.style.display = 'inline-block'; btnText.textContent = 'در حال پردازش...'; playerContent.style.display = 'none'; outputSection.classList.remove('has-content'); statusMessage.style.display = 'none'; loadingAnimation.style.display = 'flex'; mainAudioPlayer.src = ''; };
348
+ const showResultState = (isSuccess, msg = '') => { loadingAnimation.style.display = 'none'; if (isSuccess) { playerContent.style.display = 'block'; outputSection.classList.add('has-content'); } else { statusMessage.innerHTML = msg || 'خطا.'; statusMessage.style.display = 'block'; statusMessage.classList.add('error'); } generateBtn.disabled = false; btnSpinner.style.display = 'none'; btnText.textContent = '✨ خلق صدا با آلفا'; };
349
+ form.addEventListener('submit', async () => {
350
+ if (generateBtn.disabled) return;
351
+ if (!currentUser.email) { alert('برای تولید صدا، لطفا ابتدا وارد حساب کاربری خود شوید.'); loginCheckBtn.click(); return; }
352
+ if (!textInput.value.trim()) { showResultState(false, '<b>خطا:</b> متن ورودی خالی است.'); return; }
353
+ showLoadingState();
354
+ const textChunks = splitTextIntoChunks(textInput.value), allAudioBlobs = []; let hasError = false;
355
+ for (let i = 0; i < textChunks.length; i++) {
356
+ btnText.textContent = `در حال پردازش بخش ${i + 1} از ${textChunks.length}...`;
357
+ try {
358
+ const response = await fetch('/tts/proxy.php?endpoint=generate', {
359
+ method: 'POST',
360
+ headers: {'Content-Type': 'application/json'},
361
+ body: JSON.stringify({ text: textChunks[i], prompt: promptInput.value, speaker: selectedSpeakerIdStorage.value, temperature: parseFloat(tempSlider.value), email: currentUser.email, fingerprint: currentUser.fingerprint })
362
+ });
363
+ if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || `خطای سرور (${response.status})`); }
364
+ const blob = await response.blob();
365
+ allAudioBlobs.push(blob);
366
+ } catch (error) { showResultState(false, `<b>خطا:</b> ${error.message}`); hasError = true; break; }
367
+ }
368
+ if (!hasError && allAudioBlobs.length > 0) {
369
+ try {
370
+ const finalBlob = await mergeAudioBlobs(allAudioBlobs);
371
+ if (finalBlob) {
372
+ const audioUrl = URL.createObjectURL(finalBlob);
373
+ mainAudioPlayer.src = audioUrl;
374
+ const downloadLink = playerContent.querySelector('.audio-download-btn-new');
375
+ if (downloadLink) downloadLink.href = audioUrl;
376
+ showResultState(true);
377
+ } else throw new Error("ادغام ناموفق");
378
+ } catch (error) { showResultState(false, `<b>خطا:</b> ${error.message}`); }
379
+ }
380
+ });
381
+ createPlayerInstance('audio-player-content-standard');
382
+ if (statusMessage) statusMessage.style.display = 'block';
383
+ })();
384
+
385
+ // --- Voice Changer Logic (New & Complete) ---
386
+ function initVcModels() {
387
+ const grid = document.getElementById('vc-models-container');
388
+ if(!grid || grid.children.length > 0) return;
389
+ vcModels.forEach(m => {
390
+ const div = document.createElement('div');
391
+ div.className = 'vc-model-item';
392
+ div.onclick = () => { document.querySelectorAll('.vc-model-item').forEach(i=>i.classList.remove('selected')); div.classList.add('selected'); selectedVcModel = m; };
393
+ div.innerHTML = `<img src="${m.img}"><span>${m.name}</span>`;
394
+ grid.appendChild(div);
395
+ });
396
+ }
397
+
398
+ window.handleVcFile = (input) => {
399
+ if(input.files && input.files[0]) {
400
+ document.getElementById('vc-filename-display').textContent = input.files[0].name;
401
+ document.getElementById('vc-file-status').style.display = 'flex';
402
+ }
403
+ };
404
+
405
+ window.startVoiceConversion = async () => {
406
+ if(!currentUser.email) { document.getElementById('email-modal').classList.add('visible'); return; }
407
+ if(!selectedVcModel) return alert('مدل را انتخاب کنید');
408
+ const fileInput = document.getElementById('vc-file-input');
409
+ if(!fileInput.files[0]) return alert('فایل را آپلود کنید');
410
+
411
+ const btn = document.getElementById('vc-generate-btn');
412
+ const loader = document.getElementById('vc-loader');
413
+ const resultDiv = document.getElementById('vc-result-container');
414
+ const statusMsg = document.getElementById('vc-status-message');
415
+ const outputSec = document.getElementById('vc-output-section');
416
+
417
+ btn.disabled = true;
418
+ btn.querySelector('.spinner').style.display = 'inline-block';
419
+ loader.style.display = 'flex';
420
+ statusMsg.style.display = 'none';
421
+ resultDiv.style.display = 'none';
422
+ outputSec.classList.remove('has-content');
423
+
424
+ try {
425
+ // Fetch Ref
426
+ const refBlob = await fetch(selectedVcModel.ref).then(r=>r.blob());
427
+ const formData = new FormData();
428
+ formData.append('email', currentUser.email);
429
+ formData.append('fingerprint', currentUser.fingerprint);
430
+ formData.append('source_audio', fileInput.files[0]);
431
+ formData.append('ref_audio', refBlob, 'ref.wav');
432
+
433
+ const upRes = await fetch('/tts/proxy.php?endpoint=vc-upload', { method: 'POST', body: formData });
434
+ if(!upRes.ok) throw new Error('خطا در آپلود');
435
+ const { job_id } = await upRes.json();
436
+
437
+ const poll = setInterval(async () => {
438
+ try {
439
+ const stRes = await fetch('/tts/proxy.php?endpoint=vc-status', { method: 'POST', body: JSON.stringify({job_id}) });
440
+ const stData = await stRes.json();
441
+ if(stData.status === 'completed') {
442
+ clearInterval(poll);
443
+ const url = `https://ezmary-sada.hf.space/download/${stData.filename}`;
444
+ document.getElementById('vc-audio-player').src = url;
445
+ document.getElementById('vc-download-btn').href = url;
446
+ loader.style.display = 'none';
447
+ resultDiv.style.display = 'block';
448
+ outputSec.classList.add('has-content');
449
+ btn.disabled = false;
450
+ btn.querySelector('.spinner').style.display = 'none';
451
+ } else if(stData.status === 'failed') {
452
+ throw new Error('خطا در پردازش');
453
+ }
454
+ } catch(e) { clearInterval(poll); alert(e.message); loader.style.display='none'; statusMsg.style.display='block'; btn.disabled=false; }
455
+ }, 3000);
456
+ } catch(e) { alert(e.message); loader.style.display='none'; statusMsg.style.display='block'; btn.disabled=false; }
457
+ };
458
+
459
+ // --- Init ---
460
+ async function initializeApp() {
461
+ currentUser.fingerprint = await getBrowserFingerprint();
462
+ setupEventListeners();
463
+ const storedEmail = localStorage.getItem('userEmail');
464
+ await checkUserStatus(storedEmail);
465
+ updateSelectedSpeakerDisplay(selectedSpeakerIdStorage.value || speakers[0].id);
466
+ }
467
+
468
+ function setupEventListeners() {
469
+ loginCheckBtn.addEventListener('click', () => { document.getElementById('email-modal').classList.add('visible'); });
470
+ document.getElementById('logout-btn').addEventListener('click', () => { localStorage.removeItem('userEmail'); location.reload(); });
471
+ document.querySelectorAll('.close-modal-btn').forEach(b => b.addEventListener('click', () => b.closest('.modal-overlay').classList.remove('visible')));
472
+ ['timeupdate', 'play', 'pause'].forEach(e => mainAudioPlayer.addEventListener(e, updatePlayerUI));
473
+ document.getElementById('selected-speaker-card').addEventListener('click', () => { createSpeakerCardsInModal(); document.getElementById('speaker-modal').classList.add('visible'); });
474
+ document.getElementById('change-speaker-btn').addEventListener('click', (e) => { e.stopPropagation(); createSpeakerCardsInModal(); document.getElementById('speaker-modal').classList.add('visible'); });
475
+ document.getElementById('temp-info-icon').addEventListener('click', () => document.getElementById('info-modal').classList.add('visible'));
476
+
477
+ // Email Auth Handlers
478
+ const emailForm = document.getElementById('email-form');
479
+ const codeForm = document.getElementById('code-form');
480
+ emailForm.addEventListener('submit', async (e) => {
481
+ e.preventDefault();
482
+ const email = document.getElementById('login-email-input').value;
483
+ const btn = document.getElementById('send-code-btn');
484
+ btn.disabled = true; btn.textContent = '...';
485
+ try {
486
+ const res = await fetch('/tts/send_code.php', { method: 'POST', body: JSON.stringify({email}) });
487
+ const d = await res.json();
488
+ if(d.status === 'success') { emailForm.style.display='none'; codeForm.style.display='block'; }
489
+ else alert(d.message);
490
+ } catch(e) { alert('خطا'); }
491
+ btn.disabled = false; btn.textContent = 'ارسال کد تایید';
492
+ });
493
+ codeForm.addEventListener('submit', async (e) => {
494
+ e.preventDefault();
495
+ const email = document.getElementById('login-email-input').value;
496
+ const code = document.getElementById('code-input').value;
497
+ const btn = document.getElementById('verify-code-btn');
498
+ btn.disabled = true; btn.textContent = '...';
499
+ try {
500
+ const res = await fetch('/tts/verify_code.php', { method: 'POST', body: JSON.stringify({email, code}) });
501
+ const d = await res.json();
502
+ if(d.status === 'success') {
503
+ localStorage.setItem('userEmail', email);
504
+ checkUserStatus(email);
505
+ document.getElementById('email-modal').classList.remove('visible');
506
+ } else alert(d.message);
507
+ } catch(e) { alert('خطا'); }
508
+ btn.disabled = false; btn.textContent = 'تایید';
509
+ });
510
+ document.getElementById('back-to-email-btn').addEventListener('click', () => { codeForm.style.display='none'; emailForm.style.display='block'; });
511
+ }
512
+
513
+ initializeApp();
514
+ });
515
+ </script>
516
+
517
+ </body>
518
+ </html>