apolinario commited on
Commit
2c50831
·
1 Parent(s): ff50d21

Add Voice Changer, Voice Isolator, and Sound Effects tabs

Browse files
Files changed (1) hide show
  1. index.html +554 -1
index.html CHANGED
@@ -799,6 +799,18 @@ pre {
799
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
800
  Voice Design
801
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
802
  <button class="nav-item" data-page="voices" onclick="showPage('voices', this)">
803
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
804
  Voice Library
@@ -1000,6 +1012,222 @@ pre {
1000
  </div>
1001
  </div>
1002
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1003
  <!-- Voice Library Page -->
1004
  <div class="page" id="page-voices">
1005
  <div class="page-header">
@@ -1175,7 +1403,7 @@ result = client.<span class="f">predict</span>(
1175
  </div>
1176
  </div>
1177
 
1178
- <div class="footer">Powered by <a href="https://huggingface.co/spaces/k2-fsa/OmniVoice" target="_blank">OmniVoice</a>, <a href="https://huggingface.co/spaces/Qwen/Qwen3-TTS" target="_blank">Qwen3-TTS</a> & <a href="https://huggingface.co/spaces/CohereLabs/cohere-transcribe-03-2026" target="_blank">Cohere Transcribe</a></div>
1179
  </div>
1180
  </div>
1181
  </div>
@@ -2016,6 +2244,331 @@ window.showApiTab = function(tab, el) {
2016
  el.classList.add("active");
2017
  };
2018
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2019
  // ── Text ──
2020
 
2021
  const textInput = document.getElementById("text-input");
 
799
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
800
  Voice Design
801
  </button>
802
+ <button class="nav-item" data-page="changer" onclick="showPage('changer', this)">
803
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="9" r="2.5"/><path d="M3 17a3 3 0 0 1 3-3"/><circle cx="18" cy="9" r="2.5"/><path d="M21 17a3 3 0 0 0-3-3"/><path d="M8 19h8"/><polyline points="14 17 16 19 14 21"/><polyline points="10 17 8 19 10 21"/></svg>
804
+ Voice Changer
805
+ </button>
806
+ <button class="nav-item" data-page="isolate" onclick="showPage('isolate', this)">
807
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="8" x2="12" y2="16"/><line x1="9" y1="10" x2="9" y2="14"/><line x1="15" y1="10" x2="15" y2="14"/><path d="M6 6c-3 3-3 9 0 12"/><path d="M18 6c3 3 3 9 0 12"/></svg>
808
+ Voice Isolator
809
+ </button>
810
+ <button class="nav-item" data-page="sfx" onclick="showPage('sfx', this)">
811
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 C 12 8 13 11 21 12 C 13 13 12 16 12 21 C 12 16 11 13 3 12 C 11 11 12 8 12 3 Z"/></svg>
812
+ Sound Effects
813
+ </button>
814
  <button class="nav-item" data-page="voices" onclick="showPage('voices', this)">
815
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
816
  Voice Library
 
1012
  </div>
1013
  </div>
1014
 
1015
+ <!-- Voice Changer Page -->
1016
+ <div class="page" id="page-changer">
1017
+ <div class="page-header">
1018
+ <div class="page-title">Voice Changer</div>
1019
+ <div class="page-desc">Transform speech into any target voice. Powered by Seed-VC.</div>
1020
+ </div>
1021
+
1022
+ <div class="setting-label" style="margin-bottom:8px">Source audio</div>
1023
+ <div class="upload-area" id="vc-source-upload" style="margin-bottom:16px">
1024
+ <div id="vc-source-label">Drop source audio or click to upload<div class="upload-area-sub">The speech you want to convert</div></div>
1025
+ <input type="file" id="vc-source-input" accept="audio/*">
1026
+ </div>
1027
+
1028
+ <div class="setting-label" style="margin-bottom:8px">Target voice</div>
1029
+ <div class="upload-area" id="vc-target-upload" style="margin-bottom:16px">
1030
+ <div id="vc-target-label">Drop target voice or click to upload<div class="upload-area-sub">Short clip of the voice to convert to</div></div>
1031
+ <input type="file" id="vc-target-input" accept="audio/*">
1032
+ </div>
1033
+
1034
+ <div class="settings-accordion">
1035
+ <button class="settings-toggle" onclick="this.classList.toggle('open');this.nextElementSibling.classList.toggle('open')">
1036
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
1037
+ Advanced Settings
1038
+ </button>
1039
+ <div class="settings-body">
1040
+ <div class="settings-row">
1041
+ <div class="setting-group">
1042
+ <div class="setting-label">Diffusion Steps</div>
1043
+ <div class="setting-value">
1044
+ <input type="range" id="vc-steps" min="4" max="50" value="10">
1045
+ <span class="setting-num" id="vc-steps-val">10</span>
1046
+ </div>
1047
+ </div>
1048
+ <div class="setting-group">
1049
+ <div class="setting-label">Similarity</div>
1050
+ <div class="setting-value">
1051
+ <input type="range" id="vc-sim" min="0" max="100" value="70">
1052
+ <span class="setting-num" id="vc-sim-val">0.7</span>
1053
+ </div>
1054
+ </div>
1055
+ </div>
1056
+ </div>
1057
+ </div>
1058
+
1059
+ <div class="generate-bar">
1060
+ <button class="btn-generate" id="vc-btn" onclick="voiceChange()" disabled>
1061
+ <span class="btn-text">Convert Voice</span>
1062
+ <div class="spinner"></div>
1063
+ </button>
1064
+ </div>
1065
+
1066
+ <div class="status-bar" id="vc-status"></div>
1067
+ <div class="progress-track" id="vc-progress-bar"><div class="progress-fill" id="vc-progress-fill"></div></div>
1068
+
1069
+ <div class="player-card" id="vc-result">
1070
+ <div class="player-top">
1071
+ <button class="play-btn" onclick="togglePlayer('vc')">
1072
+ <svg id="vc-play-icon" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
1073
+ <svg id="vc-pause-icon" viewBox="0 0 24 24" fill="currentColor" style="display:none"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
1074
+ </button>
1075
+ <div class="player-info">
1076
+ <div class="player-title" id="vc-player-title">Converted audio</div>
1077
+ <div class="player-meta" id="vc-player-meta"></div>
1078
+ </div>
1079
+ <div style="display:flex;gap:6px">
1080
+ <button class="icon-btn" onclick="downloadResult('vc')" title="Download">
1081
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
1082
+ </button>
1083
+ </div>
1084
+ </div>
1085
+ <audio id="vc-audio-player" class="player-audio"></audio>
1086
+ </div>
1087
+ </div>
1088
+
1089
+ <!-- Voice Isolator Page -->
1090
+ <div class="page" id="page-isolate">
1091
+ <div class="page-header">
1092
+ <div class="page-title">Voice Isolator</div>
1093
+ <div class="page-desc">Clean and enhance noisy audio. Powered by Resemble Enhance.</div>
1094
+ </div>
1095
+
1096
+ <div class="upload-area" id="iso-upload" style="margin-bottom:16px">
1097
+ <div id="iso-upload-label">Drop noisy audio or click to upload<div class="upload-area-sub">MP3, WAV, FLAC, M4A &middot; speech with background noise</div></div>
1098
+ <input type="file" id="iso-file-input" accept="audio/*">
1099
+ </div>
1100
+
1101
+ <div class="settings-accordion">
1102
+ <button class="settings-toggle" onclick="this.classList.toggle('open');this.nextElementSibling.classList.toggle('open')">
1103
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
1104
+ Advanced Settings
1105
+ </button>
1106
+ <div class="settings-body">
1107
+ <div class="settings-row">
1108
+ <div class="setting-group">
1109
+ <div class="setting-label">NFE Steps</div>
1110
+ <div class="setting-value">
1111
+ <input type="range" id="iso-nfe" min="1" max="128" value="64">
1112
+ <span class="setting-num" id="iso-nfe-val">64</span>
1113
+ </div>
1114
+ </div>
1115
+ <div class="setting-group">
1116
+ <div class="setting-label">Prior Tau</div>
1117
+ <div class="setting-value">
1118
+ <input type="range" id="iso-tau" min="1" max="100" value="50">
1119
+ <span class="setting-num" id="iso-tau-val">0.5</span>
1120
+ </div>
1121
+ </div>
1122
+ </div>
1123
+ <label style="display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text);cursor:pointer;margin-top:12px">
1124
+ <input type="checkbox" id="iso-denoise-only" style="accent-color:var(--accent)">
1125
+ Denoise only (skip enhancement)
1126
+ </label>
1127
+ </div>
1128
+ </div>
1129
+
1130
+ <div class="generate-bar">
1131
+ <button class="btn-generate" id="iso-btn" onclick="voiceIsolate()" disabled>
1132
+ <span class="btn-text">Enhance Audio</span>
1133
+ <div class="spinner"></div>
1134
+ </button>
1135
+ </div>
1136
+
1137
+ <div class="status-bar" id="iso-status"></div>
1138
+ <div class="progress-track" id="iso-progress-bar"><div class="progress-fill" id="iso-progress-fill"></div></div>
1139
+
1140
+ <div class="player-card" id="iso-result">
1141
+ <div class="player-top">
1142
+ <button class="play-btn" onclick="togglePlayer('iso')">
1143
+ <svg id="iso-play-icon" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
1144
+ <svg id="iso-pause-icon" viewBox="0 0 24 24" fill="currentColor" style="display:none"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
1145
+ </button>
1146
+ <div class="player-info">
1147
+ <div class="player-title" id="iso-player-title">Enhanced audio</div>
1148
+ <div class="player-meta" id="iso-player-meta"></div>
1149
+ </div>
1150
+ <div style="display:flex;gap:6px">
1151
+ <button class="icon-btn" onclick="downloadResult('iso')" title="Download">
1152
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
1153
+ </button>
1154
+ </div>
1155
+ </div>
1156
+ <audio id="iso-audio-player" class="player-audio"></audio>
1157
+ </div>
1158
+ </div>
1159
+
1160
+ <!-- Sound Effects Page -->
1161
+ <div class="page" id="page-sfx">
1162
+ <div class="page-header">
1163
+ <div class="page-title">Sound Effects</div>
1164
+ <div class="page-desc">Generate sound effects from a text prompt. Powered by Stable Audio 3.</div>
1165
+ </div>
1166
+
1167
+ <div class="editor">
1168
+ <textarea id="sfx-prompt" placeholder="e.g. dog barking, thunder cracking, footsteps on gravel, lo-fi vinyl crackle..."></textarea>
1169
+ <div class="editor-footer">
1170
+ <div class="editor-left">
1171
+ <span class="editor-stat"><span id="sfx-char-count">0</span> chars</span>
1172
+ </div>
1173
+ </div>
1174
+ </div>
1175
+
1176
+ <div class="settings-accordion">
1177
+ <button class="settings-toggle" onclick="this.classList.toggle('open');this.nextElementSibling.classList.toggle('open')">
1178
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
1179
+ Advanced Settings
1180
+ </button>
1181
+ <div class="settings-body">
1182
+ <div class="settings-row">
1183
+ <div class="setting-group">
1184
+ <div class="setting-label">Duration</div>
1185
+ <div class="setting-value">
1186
+ <input type="range" id="sfx-duration" min="1" max="30" value="5">
1187
+ <span class="setting-num" id="sfx-duration-val">5s</span>
1188
+ </div>
1189
+ </div>
1190
+ <div class="setting-group">
1191
+ <div class="setting-label">Inference Steps</div>
1192
+ <div class="setting-value">
1193
+ <input type="range" id="sfx-steps" min="4" max="32" value="8">
1194
+ <span class="setting-num" id="sfx-steps-val">8</span>
1195
+ </div>
1196
+ </div>
1197
+ </div>
1198
+ </div>
1199
+ </div>
1200
+
1201
+ <div class="generate-bar">
1202
+ <button class="btn-generate" id="sfx-btn" onclick="soundEffect()" disabled>
1203
+ <span class="btn-text">Generate Sound</span>
1204
+ <div class="spinner"></div>
1205
+ </button>
1206
+ </div>
1207
+
1208
+ <div class="status-bar" id="sfx-status"></div>
1209
+ <div class="progress-track" id="sfx-progress-bar"><div class="progress-fill" id="sfx-progress-fill"></div></div>
1210
+
1211
+ <div class="player-card" id="sfx-result">
1212
+ <div class="player-top">
1213
+ <button class="play-btn" onclick="togglePlayer('sfx')">
1214
+ <svg id="sfx-play-icon" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
1215
+ <svg id="sfx-pause-icon" viewBox="0 0 24 24" fill="currentColor" style="display:none"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
1216
+ </button>
1217
+ <div class="player-info">
1218
+ <div class="player-title" id="sfx-player-title">Generated sound</div>
1219
+ <div class="player-meta" id="sfx-player-meta"></div>
1220
+ </div>
1221
+ <div style="display:flex;gap:6px">
1222
+ <button class="icon-btn" onclick="downloadResult('sfx')" title="Download">
1223
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
1224
+ </button>
1225
+ </div>
1226
+ </div>
1227
+ <audio id="sfx-audio-player" class="player-audio"></audio>
1228
+ </div>
1229
+ </div>
1230
+
1231
  <!-- Voice Library Page -->
1232
  <div class="page" id="page-voices">
1233
  <div class="page-header">
 
1403
  </div>
1404
  </div>
1405
 
1406
+ <div class="footer">Powered by <a href="https://huggingface.co/spaces/k2-fsa/OmniVoice" target="_blank">OmniVoice</a>, <a href="https://huggingface.co/spaces/Qwen/Qwen3-TTS" target="_blank">Qwen3-TTS</a>, <a href="https://huggingface.co/spaces/Plachta/Seed-VC" target="_blank">Seed-VC</a>, <a href="https://huggingface.co/spaces/multimodalart/resemble-enhance-zerogpu" target="_blank">Resemble Enhance</a>, <a href="https://huggingface.co/spaces/stabilityai/stable-audio-3" target="_blank">Stable Audio 3</a> & <a href="https://huggingface.co/spaces/CohereLabs/cohere-transcribe-03-2026" target="_blank">Cohere Transcribe</a></div>
1407
  </div>
1408
  </div>
1409
  </div>
 
2244
  el.classList.add("active");
2245
  };
2246
 
2247
+ // ── Shared gradio helpers (for Voice Changer / Isolator / SFX) ──
2248
+
2249
+ const SEED_VC_BASE = "https://plachta-seed-vc.hf.space/gradio_api";
2250
+ const RESEMBLE_BASE = "https://multimodalart-resemble-enhance-zerogpu.hf.space/gradio_api";
2251
+ const STABLE_AUDIO_BASE = "https://stabilityai-stable-audio-3.hf.space/gradio_api";
2252
+
2253
+ const resultUrls = {};
2254
+
2255
+ function showStatusOn(elId, type, msg) {
2256
+ const el = document.getElementById(elId);
2257
+ el.className = `status-bar visible ${type}`;
2258
+ el.innerHTML = `<div class="status-text">${msg}</div>`;
2259
+ }
2260
+
2261
+ function showStatusCtaOn(elId, errMsg) {
2262
+ const el = document.getElementById(elId);
2263
+ const isQuota = errMsg.includes("GPU quota");
2264
+ const isFree = errMsg.includes("free GPU quota");
2265
+ let html = `<div class="status-text">${errMsg}</div>`;
2266
+ if (isQuota) {
2267
+ html += isFree
2268
+ ? `<a class="status-cta upgrade" href="https://huggingface.co/subscribe/pro" target="_blank">🤗 Upgrade to HF Pro &mdash; 25 min/day GPU quota</a>`
2269
+ : `<a class="status-cta credits" href="https://huggingface.co/settings/billing" target="_blank">Buy additional GPU credits &mdash; $1 per 10 min</a>`;
2270
+ const m = errMsg.match(/Try again in ([\d:]+)/);
2271
+ if (m && m[1] !== "0:00:00") html += `<div style="font-size:11px;color:var(--text-muted);margin-top:2px">Or wait ${m[1]} for your quota to reset</div>`;
2272
+ }
2273
+ el.className = "status-bar visible error";
2274
+ el.innerHTML = html;
2275
+ }
2276
+
2277
+ function wireUploadArea(areaId, inputId, labelId, onFile) {
2278
+ const area = document.getElementById(areaId);
2279
+ const inp = document.getElementById(inputId);
2280
+ area.addEventListener("click", e => { if (e.target.tagName !== "INPUT") inp.click(); });
2281
+ area.addEventListener("dragover", e => { e.preventDefault(); area.classList.add("dragover"); });
2282
+ area.addEventListener("dragleave", () => area.classList.remove("dragover"));
2283
+ area.addEventListener("drop", e => {
2284
+ e.preventDefault();
2285
+ area.classList.remove("dragover");
2286
+ if (e.dataTransfer.files[0]) { onFile(e.dataTransfer.files[0]); document.getElementById(labelId).textContent = e.dataTransfer.files[0].name; area.classList.add("has-file"); }
2287
+ });
2288
+ inp.addEventListener("change", () => {
2289
+ if (inp.files[0]) { onFile(inp.files[0]); document.getElementById(labelId).textContent = inp.files[0].name; area.classList.add("has-file"); }
2290
+ });
2291
+ }
2292
+
2293
+ function setupPlayer(prefix) {
2294
+ const el = document.getElementById(`${prefix}-audio-player`);
2295
+ el.addEventListener("play", () => {
2296
+ document.getElementById(`${prefix}-play-icon`).style.display = "none";
2297
+ document.getElementById(`${prefix}-pause-icon`).style.display = "block";
2298
+ });
2299
+ el.addEventListener("pause", () => {
2300
+ document.getElementById(`${prefix}-play-icon`).style.display = "block";
2301
+ document.getElementById(`${prefix}-pause-icon`).style.display = "none";
2302
+ });
2303
+ }
2304
+
2305
+ window.togglePlayer = function(prefix) {
2306
+ const el = document.getElementById(`${prefix}-audio-player`);
2307
+ el.paused ? el.play() : el.pause();
2308
+ };
2309
+
2310
+ window.downloadResult = async function(prefix) {
2311
+ const url = resultUrls[prefix];
2312
+ if (!url) return;
2313
+ const blob = await fetch(url).then(r => r.blob());
2314
+ const a = document.createElement("a");
2315
+ a.href = URL.createObjectURL(blob);
2316
+ a.download = `zerolabs_${prefix}.wav`;
2317
+ a.click();
2318
+ URL.revokeObjectURL(a.href);
2319
+ };
2320
+
2321
+ // Upload a file to a Space's /upload, return a FileData ref ready for /queue/join data array
2322
+ async function uploadFile(base, file) {
2323
+ const form = new FormData();
2324
+ form.append("files", file);
2325
+ const r = await fetch(`${base}/upload`, {
2326
+ method: "POST",
2327
+ headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {},
2328
+ body: form
2329
+ });
2330
+ if (!r.ok) throw new Error(`Upload failed: ${r.status}`);
2331
+ const [path] = await r.json();
2332
+ return { path, meta: { _type: "gradio.FileData" }, orig_name: file.name };
2333
+ }
2334
+
2335
+ // Run a queue/join + queue/data SSE call. Returns { output, duration }.
2336
+ // statusElId/progressFillId are optional and used for UI updates.
2337
+ async function gradioCall({ base, fnIndex, data, statusElId, progressFillId, processingMsg }) {
2338
+ const session = crypto.randomUUID();
2339
+ if (statusElId) showStatusOn(statusElId, "info", "Joining queue...");
2340
+
2341
+ const joinRes = await fetch(`${base}/queue/join`, {
2342
+ method: "POST",
2343
+ headers: {
2344
+ "Content-Type": "application/json",
2345
+ ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {})
2346
+ },
2347
+ body: JSON.stringify({ data, fn_index: fnIndex, session_hash: session })
2348
+ });
2349
+ if (!joinRes.ok) throw new Error(`Queue join failed: ${joinRes.status}`);
2350
+
2351
+ const streamRes = await fetch(`${base}/queue/data?session_hash=${session}`, {
2352
+ headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {}
2353
+ });
2354
+
2355
+ const reader = streamRes.body.getReader();
2356
+ const decoder = new TextDecoder();
2357
+ let buffer = "";
2358
+
2359
+ while (true) {
2360
+ const { done, value } = await reader.read();
2361
+ if (done) break;
2362
+ buffer += decoder.decode(value, { stream: true });
2363
+ const lines = buffer.split("\n\n");
2364
+ buffer = lines.pop();
2365
+ for (const line of lines) {
2366
+ if (!line.startsWith("data: ")) continue;
2367
+ const evt = JSON.parse(line.slice(6));
2368
+ if (statusElId) {
2369
+ if (evt.msg === "estimation") showStatusOn(statusElId, "info", `In queue (position ${evt.rank})...`);
2370
+ else if (evt.msg === "process_starts") {
2371
+ showStatusOn(statusElId, "info", processingMsg || "Processing...");
2372
+ if (progressFillId) document.getElementById(progressFillId).style.width = "10%";
2373
+ }
2374
+ else if (evt.msg === "log") showStatusOn(statusElId, "info", evt.log);
2375
+ else if (evt.msg === "progress" && evt.progress_data?.length) {
2376
+ const p = evt.progress_data[0];
2377
+ if (p.length && progressFillId) {
2378
+ document.getElementById(progressFillId).style.width = `${Math.round((p.index / p.length) * 90) + 10}%`;
2379
+ showStatusOn(statusElId, "info", p.desc || `Step ${p.index}/${p.length}`);
2380
+ }
2381
+ }
2382
+ }
2383
+ if (evt.msg === "process_completed") {
2384
+ if (progressFillId) document.getElementById(progressFillId).style.width = "100%";
2385
+ if (evt.success) return { output: evt.output, duration: evt.output.duration || 0 };
2386
+ throw new Error(evt.output?.error || "Generation failed");
2387
+ }
2388
+ }
2389
+ }
2390
+ throw new Error("Stream ended without completion");
2391
+ }
2392
+
2393
+ // Pull a URL out of a gradio Audio output (object {url} or {path} ref).
2394
+ function audioUrlFromOutput(out, base) {
2395
+ if (!out) return null;
2396
+ if (typeof out === "string") return out;
2397
+ if (out.url) return out.url;
2398
+ if (out.path) return `${base}/file=${out.path}`;
2399
+ return null;
2400
+ }
2401
+
2402
+ // ── Voice Changer (Seed-VC) ──
2403
+
2404
+ let vcSource = null, vcTarget = null;
2405
+ const vcSteps = document.getElementById("vc-steps");
2406
+ const vcSim = document.getElementById("vc-sim");
2407
+ vcSteps.addEventListener("input", () => document.getElementById("vc-steps-val").textContent = vcSteps.value);
2408
+ vcSim.addEventListener("input", () => document.getElementById("vc-sim-val").textContent = (vcSim.value / 100).toFixed(2));
2409
+
2410
+ function updateVcBtn() {
2411
+ document.getElementById("vc-btn").disabled = !(vcSource && vcTarget);
2412
+ }
2413
+ wireUploadArea("vc-source-upload", "vc-source-input", "vc-source-label", f => { vcSource = f; updateVcBtn(); });
2414
+ wireUploadArea("vc-target-upload", "vc-target-input", "vc-target-label", f => { vcTarget = f; updateVcBtn(); });
2415
+ setupPlayer("vc");
2416
+
2417
+ window.voiceChange = async function() {
2418
+ if (!vcSource || !vcTarget) return;
2419
+ const btn = document.getElementById("vc-btn");
2420
+ const progressBar = document.getElementById("vc-progress-bar");
2421
+ const progressFill = document.getElementById("vc-progress-fill");
2422
+ const result = document.getElementById("vc-result");
2423
+
2424
+ btn.disabled = true; btn.classList.add("loading");
2425
+ result.classList.remove("visible");
2426
+ progressBar.classList.add("visible");
2427
+ progressFill.style.width = "0%";
2428
+
2429
+ try {
2430
+ showStatusOn("vc-status", "info", "Uploading audio...");
2431
+ const [src, tgt] = await Promise.all([uploadFile(SEED_VC_BASE, vcSource), uploadFile(SEED_VC_BASE, vcTarget)]);
2432
+
2433
+ const { output, duration } = await gradioCall({
2434
+ base: SEED_VC_BASE,
2435
+ fnIndex: 3,
2436
+ data: [src, tgt, parseInt(vcSteps.value), 1.0, 0.0, vcSim.value / 100, 0.9, 1.0, 1.0, false, false],
2437
+ statusElId: "vc-status",
2438
+ progressFillId: "vc-progress-fill",
2439
+ processingMsg: "Converting voice..."
2440
+ });
2441
+
2442
+ // Seed-VC outputs: [Stream Output (HLS), Full Output (wav)]. Use the full one.
2443
+ const url = audioUrlFromOutput(output.data[1], SEED_VC_BASE);
2444
+ if (!url) throw new Error("No audio returned");
2445
+ resultUrls.vc = url;
2446
+ document.getElementById("vc-audio-player").src = url;
2447
+ document.getElementById("vc-player-meta").textContent = `${vcSource.name} → ${vcTarget.name} · ${duration.toFixed(1)}s`;
2448
+ result.classList.add("visible");
2449
+ showStatusOn("vc-status", "success", `Converted in ${duration.toFixed(1)}s`);
2450
+ updateUsageTracker(duration * 0.5);
2451
+ } catch (err) {
2452
+ showStatusCtaOn("vc-status", err.message);
2453
+ } finally {
2454
+ btn.disabled = false; btn.classList.remove("loading");
2455
+ setTimeout(() => { progressBar.classList.remove("visible"); updateVcBtn(); }, 2000);
2456
+ }
2457
+ };
2458
+
2459
+ // ── Voice Isolator (Resemble Enhance) ──
2460
+
2461
+ let isoFile = null;
2462
+ const isoNfe = document.getElementById("iso-nfe");
2463
+ const isoTau = document.getElementById("iso-tau");
2464
+ isoNfe.addEventListener("input", () => document.getElementById("iso-nfe-val").textContent = isoNfe.value);
2465
+ isoTau.addEventListener("input", () => document.getElementById("iso-tau-val").textContent = (isoTau.value / 100).toFixed(2));
2466
+
2467
+ wireUploadArea("iso-upload", "iso-file-input", "iso-upload-label", f => {
2468
+ isoFile = f;
2469
+ document.getElementById("iso-btn").disabled = false;
2470
+ });
2471
+ setupPlayer("iso");
2472
+
2473
+ window.voiceIsolate = async function() {
2474
+ if (!isoFile) return;
2475
+ const btn = document.getElementById("iso-btn");
2476
+ const progressBar = document.getElementById("iso-progress-bar");
2477
+ const progressFill = document.getElementById("iso-progress-fill");
2478
+ const result = document.getElementById("iso-result");
2479
+ const denoiseOnly = document.getElementById("iso-denoise-only").checked;
2480
+
2481
+ btn.disabled = true; btn.classList.add("loading");
2482
+ result.classList.remove("visible");
2483
+ progressBar.classList.add("visible");
2484
+ progressFill.style.width = "0%";
2485
+
2486
+ try {
2487
+ showStatusOn("iso-status", "info", "Uploading audio...");
2488
+ const fileRef = await uploadFile(RESEMBLE_BASE, isoFile);
2489
+
2490
+ const { output, duration } = await gradioCall({
2491
+ base: RESEMBLE_BASE,
2492
+ fnIndex: 2,
2493
+ data: [fileRef, "Midpoint", parseInt(isoNfe.value), isoTau.value / 100, denoiseOnly],
2494
+ statusElId: "iso-status",
2495
+ progressFillId: "iso-progress-fill",
2496
+ processingMsg: "Enhancing audio..."
2497
+ });
2498
+
2499
+ // Outputs: [Denoised, Enhanced]. Pick enhanced normally; denoised if denoise-only.
2500
+ const idx = denoiseOnly ? 0 : 1;
2501
+ const url = audioUrlFromOutput(output.data[idx], RESEMBLE_BASE);
2502
+ if (!url) throw new Error("No audio returned");
2503
+ resultUrls.iso = url;
2504
+ document.getElementById("iso-audio-player").src = url;
2505
+ document.getElementById("iso-player-title").textContent = denoiseOnly ? "Denoised audio" : "Enhanced audio";
2506
+ document.getElementById("iso-player-meta").textContent = `${isoFile.name} · ${duration.toFixed(1)}s`;
2507
+ result.classList.add("visible");
2508
+ showStatusOn("iso-status", "success", `Done in ${duration.toFixed(1)}s`);
2509
+ updateUsageTracker(duration * 0.5);
2510
+ } catch (err) {
2511
+ showStatusCtaOn("iso-status", err.message);
2512
+ } finally {
2513
+ btn.disabled = false; btn.classList.remove("loading");
2514
+ setTimeout(() => { progressBar.classList.remove("visible"); }, 2000);
2515
+ }
2516
+ };
2517
+
2518
+ // ── Sound Effects (Stable Audio 3) ──
2519
+
2520
+ const sfxPrompt = document.getElementById("sfx-prompt");
2521
+ const sfxDuration = document.getElementById("sfx-duration");
2522
+ const sfxSteps = document.getElementById("sfx-steps");
2523
+ sfxPrompt.addEventListener("input", () => {
2524
+ document.getElementById("sfx-char-count").textContent = sfxPrompt.value.length;
2525
+ document.getElementById("sfx-btn").disabled = !sfxPrompt.value.trim();
2526
+ });
2527
+ sfxDuration.addEventListener("input", () => document.getElementById("sfx-duration-val").textContent = `${sfxDuration.value}s`);
2528
+ sfxSteps.addEventListener("input", () => document.getElementById("sfx-steps-val").textContent = sfxSteps.value);
2529
+ setupPlayer("sfx");
2530
+
2531
+ window.soundEffect = async function() {
2532
+ const prompt = sfxPrompt.value.trim();
2533
+ if (!prompt) return;
2534
+ const btn = document.getElementById("sfx-btn");
2535
+ const progressBar = document.getElementById("sfx-progress-bar");
2536
+ const progressFill = document.getElementById("sfx-progress-fill");
2537
+ const result = document.getElementById("sfx-result");
2538
+
2539
+ btn.disabled = true; btn.classList.add("loading");
2540
+ result.classList.remove("visible");
2541
+ progressBar.classList.add("visible");
2542
+ progressFill.style.width = "0%";
2543
+
2544
+ try {
2545
+ const { output, duration } = await gradioCall({
2546
+ base: STABLE_AUDIO_BASE,
2547
+ fnIndex: 3,
2548
+ // /infer params: variant_key, prompt, duration, steps, cfg_scale, sampler_type, seed
2549
+ data: ["medium", prompt, parseInt(sfxDuration.value), parseInt(sfxSteps.value), 1.0, "pingpong", 0],
2550
+ statusElId: "sfx-status",
2551
+ progressFillId: "sfx-progress-fill",
2552
+ processingMsg: "Generating sound..."
2553
+ });
2554
+
2555
+ const url = audioUrlFromOutput(output.data[0], STABLE_AUDIO_BASE);
2556
+ if (!url) throw new Error("No audio returned");
2557
+ resultUrls.sfx = url;
2558
+ document.getElementById("sfx-audio-player").src = url;
2559
+ document.getElementById("sfx-player-title").textContent = prompt.slice(0, 60) + (prompt.length > 60 ? "..." : "");
2560
+ document.getElementById("sfx-player-meta").textContent = `${sfxDuration.value}s · ${duration.toFixed(1)}s gen`;
2561
+ result.classList.add("visible");
2562
+ showStatusOn("sfx-status", "success", `Generated in ${duration.toFixed(1)}s`);
2563
+ updateUsageTracker(duration * 0.5);
2564
+ } catch (err) {
2565
+ showStatusCtaOn("sfx-status", err.message);
2566
+ } finally {
2567
+ btn.disabled = false; btn.classList.remove("loading");
2568
+ setTimeout(() => { progressBar.classList.remove("visible"); }, 2000);
2569
+ }
2570
+ };
2571
+
2572
  // ── Text ──
2573
 
2574
  const textInput = document.getElementById("text-input");