apolinario commited on
Commit
d35ed81
·
1 Parent(s): 7837aa2

OpenAudioLabs: voice cloning UI with HF OAuth

Browse files
Files changed (3) hide show
  1. README.md +6 -4
  2. index.html +736 -18
  3. style.css +0 -28
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
  title: OpenAudioLabs
3
- emoji: 🔥
4
  colorFrom: gray
5
- colorTo: pink
6
  sdk: static
7
  pinned: false
 
 
 
 
8
  ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
  title: OpenAudioLabs
3
+ emoji: 🎙
4
  colorFrom: gray
5
+ colorTo: purple
6
  sdk: static
7
  pinned: false
8
+ hf_oauth: true
9
+ hf_oauth_expiration_minutes: 480
10
+ hf_oauth_scopes:
11
+ - read-billing
12
  ---
 
 
index.html CHANGED
@@ -1,19 +1,737 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>OpenAudioLabs</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0a0a0f;
10
+ --surface: #14141f;
11
+ --surface2: #1e1e2e;
12
+ --border: #2a2a3e;
13
+ --text: #e0e0e8;
14
+ --text-dim: #8888a0;
15
+ --accent: #7c5cfc;
16
+ --accent-hover: #9070ff;
17
+ --danger: #ff4466;
18
+ --success: #44cc88;
19
+ }
20
+
21
+ * { margin: 0; padding: 0; box-sizing: border-box; }
22
+
23
+ body {
24
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
25
+ background: var(--bg);
26
+ color: var(--text);
27
+ min-height: 100vh;
28
+ }
29
+
30
+ header {
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: space-between;
34
+ padding: 16px 32px;
35
+ border-bottom: 1px solid var(--border);
36
+ background: var(--surface);
37
+ }
38
+
39
+ .logo {
40
+ font-size: 20px;
41
+ font-weight: 700;
42
+ letter-spacing: -0.5px;
43
+ }
44
+
45
+ .logo span { color: var(--accent); }
46
+
47
+ .user-info {
48
+ display: flex;
49
+ align-items: center;
50
+ gap: 10px;
51
+ font-size: 14px;
52
+ color: var(--text-dim);
53
+ }
54
+
55
+ .user-info img {
56
+ width: 28px;
57
+ height: 28px;
58
+ border-radius: 50%;
59
+ }
60
+
61
+ .btn {
62
+ padding: 8px 20px;
63
+ border: none;
64
+ border-radius: 6px;
65
+ font-size: 14px;
66
+ font-weight: 500;
67
+ cursor: pointer;
68
+ transition: all 0.15s;
69
+ }
70
+
71
+ .btn-primary {
72
+ background: var(--accent);
73
+ color: white;
74
+ }
75
+
76
+ .btn-primary:hover { background: var(--accent-hover); }
77
+
78
+ .btn-primary:disabled {
79
+ opacity: 0.5;
80
+ cursor: not-allowed;
81
+ }
82
+
83
+ .btn-outline {
84
+ background: transparent;
85
+ color: var(--text);
86
+ border: 1px solid var(--border);
87
+ }
88
+
89
+ .btn-outline:hover { border-color: var(--accent); }
90
+
91
+ .btn-hf {
92
+ background: #ff9d00;
93
+ color: #000;
94
+ font-weight: 600;
95
+ display: inline-flex;
96
+ align-items: center;
97
+ gap: 8px;
98
+ }
99
+
100
+ .btn-hf:hover { background: #ffb340; }
101
+
102
+ /* Landing */
103
+ #landing {
104
+ display: flex;
105
+ flex-direction: column;
106
+ align-items: center;
107
+ justify-content: center;
108
+ min-height: 80vh;
109
+ text-align: center;
110
+ gap: 24px;
111
+ }
112
+
113
+ #landing h1 {
114
+ font-size: 48px;
115
+ font-weight: 700;
116
+ letter-spacing: -1px;
117
+ }
118
+
119
+ #landing h1 span { color: var(--accent); }
120
+
121
+ #landing p {
122
+ color: var(--text-dim);
123
+ font-size: 18px;
124
+ max-width: 500px;
125
+ }
126
+
127
+ /* App */
128
+ #app { display: none; }
129
+
130
+ .main {
131
+ max-width: 800px;
132
+ margin: 0 auto;
133
+ padding: 32px;
134
+ }
135
+
136
+ .section {
137
+ background: var(--surface);
138
+ border: 1px solid var(--border);
139
+ border-radius: 12px;
140
+ padding: 24px;
141
+ margin-bottom: 24px;
142
+ }
143
+
144
+ .section-title {
145
+ font-size: 14px;
146
+ font-weight: 600;
147
+ text-transform: uppercase;
148
+ letter-spacing: 1px;
149
+ color: var(--text-dim);
150
+ margin-bottom: 16px;
151
+ }
152
+
153
+ /* Upload */
154
+ .upload-zone {
155
+ border: 2px dashed var(--border);
156
+ border-radius: 8px;
157
+ padding: 32px;
158
+ text-align: center;
159
+ cursor: pointer;
160
+ transition: all 0.15s;
161
+ color: var(--text-dim);
162
+ }
163
+
164
+ .upload-zone:hover, .upload-zone.dragover {
165
+ border-color: var(--accent);
166
+ color: var(--text);
167
+ }
168
+
169
+ .upload-zone.has-file {
170
+ border-color: var(--success);
171
+ border-style: solid;
172
+ color: var(--success);
173
+ }
174
+
175
+ .upload-zone input { display: none; }
176
+
177
+ /* Text input */
178
+ textarea {
179
+ width: 100%;
180
+ min-height: 100px;
181
+ background: var(--surface2);
182
+ border: 1px solid var(--border);
183
+ border-radius: 8px;
184
+ padding: 12px;
185
+ color: var(--text);
186
+ font-family: inherit;
187
+ font-size: 15px;
188
+ resize: vertical;
189
+ }
190
+
191
+ textarea:focus {
192
+ outline: none;
193
+ border-color: var(--accent);
194
+ }
195
+
196
+ .char-count {
197
+ text-align: right;
198
+ font-size: 12px;
199
+ color: var(--text-dim);
200
+ margin-top: 4px;
201
+ }
202
+
203
+ /* Controls row */
204
+ .controls {
205
+ display: flex;
206
+ gap: 12px;
207
+ align-items: center;
208
+ margin-top: 16px;
209
+ }
210
+
211
+ select {
212
+ padding: 8px 12px;
213
+ background: var(--surface2);
214
+ border: 1px solid var(--border);
215
+ border-radius: 6px;
216
+ color: var(--text);
217
+ font-size: 14px;
218
+ }
219
+
220
+ select:focus {
221
+ outline: none;
222
+ border-color: var(--accent);
223
+ }
224
+
225
+ /* Status */
226
+ .status {
227
+ margin-top: 16px;
228
+ padding: 12px;
229
+ border-radius: 8px;
230
+ font-size: 14px;
231
+ display: none;
232
+ }
233
+
234
+ .status.visible { display: block; }
235
+ .status.info { background: #1a1a3e; color: #8888ff; }
236
+ .status.success { background: #1a3e2a; color: var(--success); }
237
+ .status.error { background: #3e1a1a; color: var(--danger); }
238
+
239
+ /* Audio result */
240
+ .result {
241
+ margin-top: 16px;
242
+ display: none;
243
+ }
244
+
245
+ .result.visible { display: block; }
246
+
247
+ .result audio {
248
+ width: 100%;
249
+ margin-bottom: 12px;
250
+ }
251
+
252
+ .result-actions {
253
+ display: flex;
254
+ gap: 8px;
255
+ }
256
+
257
+ /* API Docs */
258
+ .tabs {
259
+ display: flex;
260
+ gap: 4px;
261
+ margin-bottom: 16px;
262
+ }
263
+
264
+ .tab {
265
+ padding: 6px 14px;
266
+ border-radius: 6px;
267
+ font-size: 13px;
268
+ cursor: pointer;
269
+ color: var(--text-dim);
270
+ background: transparent;
271
+ border: 1px solid transparent;
272
+ transition: all 0.15s;
273
+ }
274
+
275
+ .tab.active {
276
+ color: var(--text);
277
+ background: var(--surface2);
278
+ border-color: var(--border);
279
+ }
280
+
281
+ .tab:hover { color: var(--text); }
282
+
283
+ pre {
284
+ background: var(--bg);
285
+ border: 1px solid var(--border);
286
+ border-radius: 8px;
287
+ padding: 16px;
288
+ overflow-x: auto;
289
+ font-size: 13px;
290
+ line-height: 1.5;
291
+ }
292
+
293
+ code { color: #c0c0d8; }
294
+ .code-comment { color: #6a6a8a; }
295
+ .code-string { color: #88cc88; }
296
+ .code-key { color: #cc88cc; }
297
+
298
+ /* Progress bar */
299
+ .progress-bar {
300
+ width: 100%;
301
+ height: 4px;
302
+ background: var(--surface2);
303
+ border-radius: 2px;
304
+ margin-top: 8px;
305
+ overflow: hidden;
306
+ display: none;
307
+ }
308
+
309
+ .progress-bar.visible { display: block; }
310
+
311
+ .progress-fill {
312
+ height: 100%;
313
+ background: var(--accent);
314
+ border-radius: 2px;
315
+ transition: width 0.3s;
316
+ width: 0%;
317
+ }
318
+
319
+ @media (max-width: 600px) {
320
+ header { padding: 12px 16px; }
321
+ .main { padding: 16px; }
322
+ #landing h1 { font-size: 32px; }
323
+ .controls { flex-direction: column; align-items: stretch; }
324
+ }
325
+ </style>
326
+ </head>
327
+ <body>
328
+
329
+ <header>
330
+ <div class="logo">Open<span>Audio</span>Labs</div>
331
+ <div id="header-right">
332
+ <button class="btn btn-hf" id="login-btn-header" style="display:none" onclick="login()">
333
+ 🤗 Sign in with Hugging Face
334
+ </button>
335
+ <div class="user-info" id="user-info" style="display:none">
336
+ <img id="user-avatar" src="" alt="">
337
+ <span id="user-name"></span>
338
+ <button class="btn btn-outline" onclick="logout()" style="font-size:12px;padding:4px 10px">Sign out</button>
339
+ </div>
340
+ </div>
341
+ </header>
342
+
343
+ <div id="landing">
344
+ <h1>Open<span>Audio</span>Labs</h1>
345
+ <p>Voice cloning powered by open-source models on Hugging Face. Sign in with your HF account to get started.</p>
346
+ <button class="btn btn-hf" onclick="login()" style="font-size:16px;padding:12px 28px">
347
+ 🤗 Sign in with Hugging Face
348
+ </button>
349
+ <p style="font-size:13px;color:var(--text-dim)">Uses your HF ZeroGPU quota. Free tier included with every account.</p>
350
+ </div>
351
+
352
+ <div id="app">
353
+ <div class="main">
354
+
355
+ <div class="section">
356
+ <div class="section-title">Reference Voice</div>
357
+ <div class="upload-zone" id="upload-zone">
358
+ <div id="upload-label">Drop an audio file here or click to upload<br><span style="font-size:12px">MP3, WAV, FLAC supported</span></div>
359
+ <input type="file" id="file-input" accept="audio/*">
360
+ </div>
361
+ </div>
362
+
363
+ <div class="section">
364
+ <div class="section-title">Text to Synthesize</div>
365
+ <textarea id="text-input" placeholder="Type the text you want to synthesize..."></textarea>
366
+ <div class="char-count"><span id="char-count">0</span> characters</div>
367
+
368
+ <div class="controls">
369
+ <select id="lang-select">
370
+ <option value="Auto">Auto-detect</option>
371
+ <option value="English">English</option>
372
+ <option value="Chinese">Chinese</option>
373
+ <option value="French">French</option>
374
+ <option value="German">German</option>
375
+ <option value="Spanish">Spanish</option>
376
+ <option value="Japanese">Japanese</option>
377
+ <option value="Korean">Korean</option>
378
+ <option value="Portuguese">Portuguese</option>
379
+ <option value="Italian">Italian</option>
380
+ <option value="Dutch">Dutch</option>
381
+ <option value="Russian">Russian</option>
382
+ <option value="Arabic">Arabic</option>
383
+ <option value="Hindi">Hindi</option>
384
+ </select>
385
+ <button class="btn btn-primary" id="generate-btn" onclick="generate()" disabled>Generate</button>
386
+ </div>
387
+
388
+ <div class="status" id="status"></div>
389
+ <div class="progress-bar" id="progress-bar"><div class="progress-fill" id="progress-fill"></div></div>
390
+
391
+ <div class="result" id="result">
392
+ <audio id="audio-player" controls></audio>
393
+ <div class="result-actions">
394
+ <button class="btn btn-outline" onclick="downloadAudio()">Download WAV</button>
395
+ </div>
396
+ </div>
397
+ </div>
398
+
399
+ <div class="section">
400
+ <div class="section-title">API Usage</div>
401
+ <p style="color:var(--text-dim);font-size:14px;margin-bottom:16px">Use your HF token to call the OmniVoice API directly. The key trick: file objects need <code>meta: {"_type": "gradio.FileData"}</code>.</p>
402
+
403
+ <div class="tabs">
404
+ <div class="tab active" onclick="showTab('curl')">curl</div>
405
+ <div class="tab" onclick="showTab('javascript')">JavaScript</div>
406
+ <div class="tab" onclick="showTab('python')">Python</div>
407
+ </div>
408
+
409
+ <div id="tab-curl">
410
+ <pre><code><span class="code-comment"># 1. Upload reference audio</span>
411
+ UPLOAD_PATH=$(curl -s -X POST \
412
+ <span class="code-string">"https://k2-fsa-omnivoice.hf.space/gradio_api/upload"</span> \
413
+ -H <span class="code-string">"Authorization: Bearer $HF_TOKEN"</span> \
414
+ -F <span class="code-string">"files=@voice.mp3"</span> | jq -r <span class="code-string">'.[0]'</span>)
415
+
416
+ <span class="code-comment"># 2. Generate (queue/join + queue/data)</span>
417
+ SESSION=$(uuidgen)
418
+ curl -s -X POST \
419
+ <span class="code-string">"https://k2-fsa-omnivoice.hf.space/gradio_api/queue/join"</span> \
420
+ -H <span class="code-string">"Content-Type: application/json"</span> \
421
+ -H <span class="code-string">"Authorization: Bearer $HF_TOKEN"</span> \
422
+ -d <span class="code-string">'{
423
+ "data": [
424
+ "Your text here", "English",
425
+ {"path": "'$UPLOAD_PATH'",
426
+ "meta": {"_type": "gradio.FileData"},
427
+ "orig_name": "voice.mp3"},
428
+ "", "", 32, 2.0, true, 1.0, 0, true, true
429
+ ],
430
+ "fn_index": 0,
431
+ "session_hash": "'$SESSION'"
432
+ }'</span>
433
+
434
+ <span class="code-comment"># 3. Stream result</span>
435
+ curl -N <span class="code-string">"https://k2-fsa-omnivoice.hf.space/gradio_api/queue/data?session_hash=$SESSION"</span> \
436
+ -H <span class="code-string">"Authorization: Bearer $HF_TOKEN"</span></code></pre>
437
+ </div>
438
+
439
+ <div id="tab-javascript" style="display:none">
440
+ <pre><code><span class="code-key">const</span> BASE = <span class="code-string">"https://k2-fsa-omnivoice.hf.space/gradio_api"</span>;
441
+ <span class="code-key">const</span> TOKEN = <span class="code-string">"hf_..."</span>;
442
+ <span class="code-key">const</span> SESSION = crypto.randomUUID();
443
+
444
+ <span class="code-comment">// 1. Upload reference audio</span>
445
+ <span class="code-key">const</span> form = <span class="code-key">new</span> FormData();
446
+ form.append(<span class="code-string">"files"</span>, audioFile);
447
+ <span class="code-key">const</span> [uploadPath] = <span class="code-key">await</span> fetch(`${BASE}/upload`, {
448
+ method: <span class="code-string">"POST"</span>,
449
+ headers: { Authorization: <span class="code-string">`Bearer ${TOKEN}`</span> },
450
+ body: form
451
+ }).then(r => r.json());
452
+
453
+ <span class="code-comment">// 2. Join queue</span>
454
+ <span class="code-key">await</span> fetch(`${BASE}/queue/join`, {
455
+ method: <span class="code-string">"POST"</span>,
456
+ headers: {
457
+ <span class="code-string">"Content-Type"</span>: <span class="code-string">"application/json"</span>,
458
+ Authorization: <span class="code-string">`Bearer ${TOKEN}`</span>
459
+ },
460
+ body: JSON.stringify({
461
+ data: [
462
+ <span class="code-string">"Your text"</span>, <span class="code-string">"English"</span>,
463
+ { path: uploadPath,
464
+ meta: { _type: <span class="code-string">"gradio.FileData"</span> },
465
+ orig_name: <span class="code-string">"voice.mp3"</span> },
466
+ <span class="code-string">""</span>, <span class="code-string">""</span>, 32, 2.0, true, 1.0, 0, true, true
467
+ ],
468
+ fn_index: 0,
469
+ session_hash: SESSION
470
+ })
471
+ });
472
+
473
+ <span class="code-comment">// 3. Stream results</span>
474
+ <span class="code-key">const</span> res = <span class="code-key">await</span> fetch(
475
+ `${BASE}/queue/data?session_hash=${SESSION}`,
476
+ { headers: { Authorization: <span class="code-string">`Bearer ${TOKEN}`</span> } }
477
+ );
478
+ <span class="code-comment">// Parse SSE stream for process_completed message</span></code></pre>
479
+ </div>
480
+
481
+ <div id="tab-python" style="display:none">
482
+ <pre><code><span class="code-key">from</span> gradio_client <span class="code-key">import</span> Client, handle_file
483
+
484
+ client = Client(<span class="code-string">"k2-fsa/OmniVoice"</span>)
485
+
486
+ result = client.predict(
487
+ text=<span class="code-string">"Your text here"</span>,
488
+ lang=<span class="code-string">"English"</span>,
489
+ ref_aud=handle_file(<span class="code-string">"voice.mp3"</span>),
490
+ ref_text=<span class="code-string">""</span>,
491
+ instruct=<span class="code-string">""</span>,
492
+ ns=32, gs=2.0, dn=True,
493
+ sp=1.0, du=0, pp=True, po=True,
494
+ api_name=<span class="code-string">"/_clone_fn"</span>
495
+ )
496
+
497
+ <span class="code-key">print</span>(result) <span class="code-comment"># (audio_path, status)</span></code></pre>
498
+ </div>
499
+ </div>
500
+
501
+ </div>
502
+ </div>
503
+
504
+ <script type="module">
505
+ import { oauthLoginUrl, oauthHandleRedirectIfPresent } from "https://esm.sh/@huggingface/hub@0.20.0";
506
+
507
+ const OMNIVOICE_BASE = "https://k2-fsa-omnivoice.hf.space/gradio_api";
508
+
509
+ let accessToken = null;
510
+ let uploadedFilePath = null;
511
+ let uploadedFileName = null;
512
+ let resultAudioUrl = null;
513
+
514
+ // --- OAuth ---
515
+
516
+ async function initOAuth() {
517
+ const result = await oauthHandleRedirectIfPresent();
518
+ if (result) {
519
+ accessToken = result.accessToken;
520
+ showApp(result.userInfo);
521
+ }
522
+ }
523
+
524
+ window.login = async function() {
525
+ const url = await oauthLoginUrl({ scopes: "openid profile read-billing" });
526
+ window.location.href = url;
527
+ };
528
+
529
+ window.logout = function() {
530
+ accessToken = null;
531
+ document.getElementById("app").style.display = "none";
532
+ document.getElementById("landing").style.display = "flex";
533
+ document.getElementById("user-info").style.display = "none";
534
+ document.getElementById("login-btn-header").style.display = "none";
535
+ };
536
+
537
+ function showApp(userInfo) {
538
+ document.getElementById("landing").style.display = "none";
539
+ document.getElementById("app").style.display = "block";
540
+ document.getElementById("login-btn-header").style.display = "none";
541
+ document.getElementById("user-info").style.display = "flex";
542
+ document.getElementById("user-avatar").src = userInfo.avatarUrl;
543
+ document.getElementById("user-name").textContent = userInfo.name;
544
+ }
545
+
546
+ // --- File Upload ---
547
+
548
+ const uploadZone = document.getElementById("upload-zone");
549
+ const fileInput = document.getElementById("file-input");
550
+ const uploadLabel = document.getElementById("upload-label");
551
+
552
+ uploadZone.addEventListener("click", () => fileInput.click());
553
+ uploadZone.addEventListener("dragover", e => { e.preventDefault(); uploadZone.classList.add("dragover"); });
554
+ uploadZone.addEventListener("dragleave", () => uploadZone.classList.remove("dragover"));
555
+ uploadZone.addEventListener("drop", e => {
556
+ e.preventDefault();
557
+ uploadZone.classList.remove("dragover");
558
+ if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);
559
+ });
560
+ fileInput.addEventListener("change", () => { if (fileInput.files.length) handleFile(fileInput.files[0]); });
561
+
562
+ async function handleFile(file) {
563
+ uploadLabel.textContent = `Uploading ${file.name}...`;
564
+ uploadedFileName = file.name;
565
+
566
+ const form = new FormData();
567
+ form.append("files", file);
568
+
569
+ try {
570
+ const res = await fetch(`${OMNIVOICE_BASE}/upload`, {
571
+ method: "POST",
572
+ headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {},
573
+ body: form
574
+ });
575
+ const paths = await res.json();
576
+ uploadedFilePath = paths[0];
577
+ uploadZone.classList.add("has-file");
578
+ uploadLabel.textContent = `${file.name}`;
579
+ updateGenerateBtn();
580
+ } catch (err) {
581
+ uploadLabel.textContent = `Upload failed: ${err.message}`;
582
+ uploadZone.classList.remove("has-file");
583
+ }
584
+ }
585
+
586
+ // --- Text Input ---
587
+
588
+ const textInput = document.getElementById("text-input");
589
+ const charCount = document.getElementById("char-count");
590
+
591
+ textInput.addEventListener("input", () => {
592
+ charCount.textContent = textInput.value.length;
593
+ updateGenerateBtn();
594
+ });
595
+
596
+ function updateGenerateBtn() {
597
+ document.getElementById("generate-btn").disabled = !(uploadedFilePath && textInput.value.trim());
598
+ }
599
+
600
+ // --- Generate ---
601
+
602
+ window.generate = async function() {
603
+ const text = textInput.value.trim();
604
+ const lang = document.getElementById("lang-select").value;
605
+ const btn = document.getElementById("generate-btn");
606
+ const status = document.getElementById("status");
607
+ const progressBar = document.getElementById("progress-bar");
608
+ const progressFill = document.getElementById("progress-fill");
609
+ const result = document.getElementById("result");
610
+
611
+ btn.disabled = true;
612
+ btn.textContent = "Generating...";
613
+ result.classList.remove("visible");
614
+ showStatus("info", "Joining queue...");
615
+ progressBar.classList.add("visible");
616
+ progressFill.style.width = "0%";
617
+
618
+ const sessionHash = crypto.randomUUID();
619
+
620
+ try {
621
+ // Join queue
622
+ const joinRes = await fetch(`${OMNIVOICE_BASE}/queue/join`, {
623
+ method: "POST",
624
+ headers: {
625
+ "Content-Type": "application/json",
626
+ ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {})
627
+ },
628
+ body: JSON.stringify({
629
+ data: [
630
+ text, lang,
631
+ { path: uploadedFilePath, meta: { _type: "gradio.FileData" }, orig_name: uploadedFileName },
632
+ "", "", 32, 2.0, true, 1.0, 0, true, true
633
+ ],
634
+ fn_index: 0,
635
+ session_hash: sessionHash
636
+ })
637
+ });
638
+
639
+ if (!joinRes.ok) throw new Error(`Queue join failed: ${joinRes.status}`);
640
+
641
+ // Stream results
642
+ const streamRes = await fetch(
643
+ `${OMNIVOICE_BASE}/queue/data?session_hash=${sessionHash}`,
644
+ { headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {} }
645
+ );
646
+
647
+ const reader = streamRes.body.getReader();
648
+ const decoder = new TextDecoder();
649
+ let buffer = "";
650
+
651
+ while (true) {
652
+ const { done, value } = await reader.read();
653
+ if (done) break;
654
+
655
+ buffer += decoder.decode(value, { stream: true });
656
+ const lines = buffer.split("\n\n");
657
+ buffer = lines.pop();
658
+
659
+ for (const line of lines) {
660
+ if (!line.startsWith("data: ")) continue;
661
+ const data = JSON.parse(line.slice(6));
662
+
663
+ if (data.msg === "estimation") {
664
+ showStatus("info", `In queue (position ${data.rank})...`);
665
+ } else if (data.msg === "process_starts") {
666
+ showStatus("info", "Processing...");
667
+ progressFill.style.width = "10%";
668
+ } else if (data.msg === "log") {
669
+ showStatus("info", data.log);
670
+ } else if (data.msg === "progress" && data.progress_data?.length) {
671
+ const p = data.progress_data[0];
672
+ if (p.length) {
673
+ const pct = Math.round((p.index / p.length) * 90) + 10;
674
+ progressFill.style.width = `${pct}%`;
675
+ const desc = p.desc || `Step ${p.index}/${p.length}`;
676
+ showStatus("info", desc);
677
+ }
678
+ } else if (data.msg === "process_completed") {
679
+ progressFill.style.width = "100%";
680
+ if (data.success) {
681
+ const audioData = data.output.data[0];
682
+ resultAudioUrl = audioData.url;
683
+ document.getElementById("audio-player").src = resultAudioUrl;
684
+ result.classList.add("visible");
685
+ showStatus("success", `Done in ${data.output.duration?.toFixed(1)}s`);
686
+ } else {
687
+ const errMsg = data.output?.error || "Generation failed";
688
+ showStatus("error", errMsg);
689
+ }
690
+ }
691
+ }
692
+ }
693
+ } catch (err) {
694
+ showStatus("error", err.message);
695
+ } finally {
696
+ btn.disabled = false;
697
+ btn.textContent = "Generate";
698
+ setTimeout(() => {
699
+ progressBar.classList.remove("visible");
700
+ updateGenerateBtn();
701
+ }, 2000);
702
+ }
703
+ };
704
+
705
+ function showStatus(type, msg) {
706
+ const el = document.getElementById("status");
707
+ el.className = `status visible ${type}`;
708
+ el.textContent = msg;
709
+ }
710
+
711
+ // --- Download ---
712
+
713
+ window.downloadAudio = async function() {
714
+ if (!resultAudioUrl) return;
715
+ const res = await fetch(resultAudioUrl);
716
+ const blob = await res.blob();
717
+ const a = document.createElement("a");
718
+ a.href = URL.createObjectURL(blob);
719
+ a.download = "openaudio_output.wav";
720
+ a.click();
721
+ URL.revokeObjectURL(a.href);
722
+ };
723
+
724
+ // --- Tabs ---
725
+
726
+ window.showTab = function(tab) {
727
+ document.querySelectorAll('[id^="tab-"]').forEach(el => el.style.display = "none");
728
+ document.getElementById(`tab-${tab}`).style.display = "block";
729
+ document.querySelectorAll(".tab").forEach(el => el.classList.remove("active"));
730
+ event.target.classList.add("active");
731
+ };
732
+
733
+ // --- Init ---
734
+ initOAuth();
735
+ </script>
736
+ </body>
737
  </html>
style.css DELETED
@@ -1,28 +0,0 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
28
- }