Spaces:
Running on Zero
Running on Zero
Commit Β·
504a99f
1
Parent(s): 78d7cea
app.py
CHANGED
|
@@ -1085,26 +1085,14 @@ def _pad_outputs(outputs: list) -> list:
|
|
| 1085 |
# WaveSurfer waveform + segment marker HTML builder #
|
| 1086 |
# ------------------------------------------------------------------ #
|
| 1087 |
|
| 1088 |
-
_WAVESURFER_CDN = "https://cdnjs.cloudflare.com/ajax/libs/wavesurfer.js/7.8.7/wavesurfer.min.js"
|
| 1089 |
-
_REGIONS_CDN = "https://cdnjs.cloudflare.com/ajax/libs/wavesurfer.js/7.8.7/plugins/regions.min.js"
|
| 1090 |
-
|
| 1091 |
def _build_waveform_html(audio_path: str, segments: list, slot_id: str,
|
| 1092 |
hidden_input_id: str) -> str:
|
| 1093 |
-
"""Return a self-contained HTML block with a
|
| 1094 |
segment boundary markers, and a download link.
|
| 1095 |
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
Clicking a region shows a small popup near the cursor with a
|
| 1100 |
-
"Regenerate" button. Clicking elsewhere dismisses the popup.
|
| 1101 |
-
Clicking "Regenerate" fires the hidden Gradio textbox to trigger Python.
|
| 1102 |
-
|
| 1103 |
-
Args:
|
| 1104 |
-
audio_path: absolute path to the .wav file
|
| 1105 |
-
segments: list of (start_s, end_s) tuples
|
| 1106 |
-
slot_id: unique string id for this slot (e.g. "taro_0")
|
| 1107 |
-
hidden_input_id: elem_id of the hidden gr.Textbox to fire
|
| 1108 |
"""
|
| 1109 |
if not audio_path or not os.path.exists(audio_path):
|
| 1110 |
return "<p style='color:#888;font-size:12px'>No audio yet.</p>"
|
|
@@ -1115,15 +1103,20 @@ def _build_waveform_html(audio_path: str, segments: list, slot_id: str,
|
|
| 1115 |
|
| 1116 |
segs_json = json.dumps(segments)
|
| 1117 |
|
| 1118 |
-
|
| 1119 |
-
|
| 1120 |
-
|
| 1121 |
-
|
| 1122 |
|
| 1123 |
return f"""
|
| 1124 |
<div id="wf_container_{slot_id}"
|
| 1125 |
style="background:#1a1a1a;border-radius:8px;padding:10px;margin-top:6px;position:relative;">
|
| 1126 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1127 |
<div style="display:flex;align-items:center;gap:8px;margin-top:6px;">
|
| 1128 |
<span style="color:#888;font-size:11px;">Click a segment to regenerate | Playhead syncs to video</span>
|
| 1129 |
<a href="{data_uri}" download="audio_{slot_id}.wav"
|
|
@@ -1150,260 +1143,207 @@ def _build_waveform_html(audio_path: str, segments: list, slot_id: str,
|
|
| 1150 |
</div>
|
| 1151 |
<script>
|
| 1152 |
(function() {{
|
| 1153 |
-
//
|
| 1154 |
-
// WaveSurfer instance for this slot so we get a fresh waveform.
|
| 1155 |
-
if (window["_wf_ws_{slot_id}"]) {{
|
| 1156 |
-
try {{ window["_wf_ws_{slot_id}"].destroy(); }} catch(e) {{}}
|
| 1157 |
-
window["_wf_ws_{slot_id}"] = null;
|
| 1158 |
-
}}
|
| 1159 |
-
// Remove previous video timeupdate listener if any
|
| 1160 |
if (window["_wf_video_unlisten_{slot_id}"]) {{
|
| 1161 |
try {{ window["_wf_video_unlisten_{slot_id}"](); }} catch(e) {{}}
|
| 1162 |
window["_wf_video_unlisten_{slot_id}"] = null;
|
| 1163 |
}}
|
| 1164 |
|
| 1165 |
-
|
|
|
|
|
|
|
|
|
|
| 1166 |
|
|
|
|
| 1167 |
function fireRegen(idx) {{
|
| 1168 |
-
|
| 1169 |
-
if (popup) popup.style.display = 'none';
|
| 1170 |
const lbl = document.getElementById('wf_seglabel_{slot_id}');
|
| 1171 |
-
const segs = {segs_json};
|
| 1172 |
if (lbl) lbl.textContent = 'Regenerating Seg ' + (idx+1) +
|
| 1173 |
-
' (' +
|
| 1174 |
-
// Trigger Gradio via the hidden textbox
|
| 1175 |
const el = document.getElementById('{hidden_input_id}');
|
| 1176 |
if (el) {{
|
| 1177 |
const input = el.querySelector('input, textarea');
|
| 1178 |
if (input) {{
|
| 1179 |
-
const
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
}} else {{
|
| 1185 |
-
input.value = '{slot_id}|' + idx;
|
| 1186 |
-
}}
|
| 1187 |
-
input.dispatchEvent(new Event('input', {{ bubbles: true }}));
|
| 1188 |
}}
|
| 1189 |
}}
|
| 1190 |
}}
|
| 1191 |
|
| 1192 |
-
function showPopup(idx,
|
| 1193 |
_pendingSegIdx_{slot_id} = idx;
|
| 1194 |
-
const segs = {segs_json};
|
| 1195 |
const popup = document.getElementById('wf_popup_{slot_id}');
|
| 1196 |
const plbl = document.getElementById('wf_popup_label_{slot_id}');
|
| 1197 |
-
if (plbl) plbl.textContent =
|
| 1198 |
-
|
| 1199 |
-
|
| 1200 |
-
|
| 1201 |
-
const
|
| 1202 |
-
popup.style.left
|
| 1203 |
-
popup.style.top
|
| 1204 |
-
|
| 1205 |
-
const r = popup.getBoundingClientRect();
|
| 1206 |
-
if (r.right > vw - 8) popup.style.left = (vw - r.width - 8) + 'px';
|
| 1207 |
-
if (r.bottom > vh - 8) popup.style.top = (vh - r.height - 8) + 'px';
|
| 1208 |
-
}});
|
| 1209 |
-
}}
|
| 1210 |
}}
|
| 1211 |
|
| 1212 |
function hidePopup() {{
|
| 1213 |
-
|
| 1214 |
-
if (popup) popup.style.display = 'none';
|
| 1215 |
_pendingSegIdx_{slot_id} = null;
|
| 1216 |
}}
|
| 1217 |
|
| 1218 |
-
// Wire the Regenerate button (poll until the element exists)
|
| 1219 |
(function tryWireBtn() {{
|
| 1220 |
const btn = document.getElementById('wf_regen_btn_{slot_id}');
|
| 1221 |
-
if (btn) {{
|
| 1222 |
-
|
| 1223 |
-
e.stopPropagation();
|
| 1224 |
-
if (_pendingSegIdx_{slot_id} !== null) fireRegen(_pendingSegIdx_{slot_id});
|
| 1225 |
-
}};
|
| 1226 |
-
}} else {{
|
| 1227 |
-
setTimeout(tryWireBtn, 100);
|
| 1228 |
-
}}
|
| 1229 |
}})();
|
| 1230 |
|
| 1231 |
-
// Dismiss popup on click outside
|
| 1232 |
document.addEventListener('click', function(e) {{
|
| 1233 |
-
const
|
| 1234 |
-
if (
|
| 1235 |
-
if (!popup.contains(e.target)) hidePopup();
|
| 1236 |
-
}}
|
| 1237 |
}}, true);
|
| 1238 |
|
| 1239 |
-
//
|
| 1240 |
-
|
| 1241 |
-
|
| 1242 |
-
|
| 1243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1244 |
function findSlotVideo() {{
|
| 1245 |
-
|
| 1246 |
-
if (!wfEl) return null;
|
| 1247 |
-
let node = wfEl.parentElement;
|
| 1248 |
while (node && node !== document.body) {{
|
| 1249 |
-
const
|
| 1250 |
-
if (
|
| 1251 |
node = node.parentElement;
|
| 1252 |
}}
|
| 1253 |
return null;
|
| 1254 |
}}
|
| 1255 |
|
| 1256 |
-
function attachVideoSync(
|
| 1257 |
-
|
| 1258 |
-
function tryAttach() {{
|
| 1259 |
const video = findSlotVideo();
|
| 1260 |
-
if (!video) {{
|
| 1261 |
-
setTimeout(tryAttach, 300);
|
| 1262 |
-
return;
|
| 1263 |
-
}}
|
| 1264 |
function onTimeUpdate() {{
|
| 1265 |
if (!video.duration || !isFinite(video.duration)) return;
|
| 1266 |
-
|
| 1267 |
-
try {{ ws.seekTo(Math.max(0, Math.min(1, progress))); }} catch(e) {{}}
|
| 1268 |
}}
|
| 1269 |
video.addEventListener('timeupdate', onTimeUpdate);
|
| 1270 |
-
// Store cleanup fn so we can detach when this HTML is replaced
|
| 1271 |
window["_wf_video_unlisten_{slot_id}"] = function() {{
|
| 1272 |
video.removeEventListener('timeupdate', onTimeUpdate);
|
| 1273 |
}};
|
| 1274 |
-
}}
|
| 1275 |
-
tryAttach();
|
| 1276 |
}}
|
| 1277 |
|
| 1278 |
-
|
| 1279 |
-
|
| 1280 |
-
|
| 1281 |
-
|
| 1282 |
-
|
| 1283 |
-
|
| 1284 |
-
|
| 1285 |
-
|
| 1286 |
-
|
| 1287 |
-
|
| 1288 |
-
|
| 1289 |
-
|
| 1290 |
-
|
| 1291 |
-
|
| 1292 |
-
|
| 1293 |
-
|
| 1294 |
-
|
| 1295 |
-
|
| 1296 |
-
|
| 1297 |
-
const byteNums = new Uint8Array(byteChars.length);
|
| 1298 |
-
for (let i = 0; i < byteChars.length; i++) byteNums[i] = byteChars.charCodeAt(i);
|
| 1299 |
-
const blob = new Blob([byteNums], {{type: 'audio/wav'}});
|
| 1300 |
-
const blobUrl = URL.createObjectURL(blob);
|
| 1301 |
-
|
| 1302 |
-
// Wait until the container has a non-zero width before creating WaveSurfer,
|
| 1303 |
-
// otherwise it renders blank (container is 0px when Gradio first injects HTML).
|
| 1304 |
-
const wfContainer = document.getElementById('wf_{slot_id}');
|
| 1305 |
-
function createWS() {{
|
| 1306 |
-
const ws = WaveSurfer.create({{
|
| 1307 |
-
container: '#wf_{slot_id}',
|
| 1308 |
-
waveColor: '#4a9eff',
|
| 1309 |
-
progressColor: '#1a5fa8',
|
| 1310 |
-
height: 80,
|
| 1311 |
-
barWidth: 2,
|
| 1312 |
-
barGap: 1,
|
| 1313 |
-
barRadius: 2,
|
| 1314 |
-
url: blobUrl,
|
| 1315 |
-
plugins: plugins,
|
| 1316 |
-
}});
|
| 1317 |
-
|
| 1318 |
-
// Silence immediately β waveform is display-only, video plays the audio
|
| 1319 |
-
ws.setVolume(0);
|
| 1320 |
-
ws.on('ready', function() {{
|
| 1321 |
-
ws.setVolume(0);
|
| 1322 |
-
ws.pause();
|
| 1323 |
-
const segments = {segs_json};
|
| 1324 |
-
const colors = {json.dumps(colors)};
|
| 1325 |
-
if (regionsPlugin) {{
|
| 1326 |
-
segments.forEach(function(seg, idx) {{
|
| 1327 |
-
regionsPlugin.addRegion({{
|
| 1328 |
-
id: 'seg_' + idx,
|
| 1329 |
-
start: seg[0],
|
| 1330 |
-
end: seg[1],
|
| 1331 |
-
color: colors[idx % colors.length],
|
| 1332 |
-
drag: false,
|
| 1333 |
-
resize: false,
|
| 1334 |
-
content: 'Seg ' + (idx + 1),
|
| 1335 |
-
}});
|
| 1336 |
-
}});
|
| 1337 |
-
regionsPlugin.on('region-clicked', function(region, e) {{
|
| 1338 |
-
e.stopPropagation();
|
| 1339 |
-
const idx = parseInt(region.id.replace('seg_', ''));
|
| 1340 |
-
showPopup(idx, e.clientX, e.clientY);
|
| 1341 |
-
}});
|
| 1342 |
-
}}
|
| 1343 |
-
attachVideoSync(ws);
|
| 1344 |
-
}});
|
| 1345 |
-
|
| 1346 |
-
ws.on('error', function(err) {{
|
| 1347 |
-
console.error('[WaveSurfer {slot_id}] error:', err);
|
| 1348 |
-
}});
|
| 1349 |
-
|
| 1350 |
-
// Also silence if WaveSurfer tries to play for any reason
|
| 1351 |
-
ws.on('play', function() {{ ws.pause(); ws.setVolume(0); }});
|
| 1352 |
-
|
| 1353 |
-
window["_wf_ws_{slot_id}"] = ws;
|
| 1354 |
-
}}
|
| 1355 |
-
|
| 1356 |
-
// Use ResizeObserver: fire createWS as soon as the container has width
|
| 1357 |
-
if (wfContainer && wfContainer.offsetWidth > 0) {{
|
| 1358 |
-
createWS();
|
| 1359 |
-
}} else if (wfContainer) {{
|
| 1360 |
-
const ro = new ResizeObserver(function(entries) {{
|
| 1361 |
-
for (const entry of entries) {{
|
| 1362 |
-
if (entry.contentRect.width > 0) {{
|
| 1363 |
-
ro.disconnect();
|
| 1364 |
-
createWS();
|
| 1365 |
-
break;
|
| 1366 |
-
}}
|
| 1367 |
-
}}
|
| 1368 |
-
}});
|
| 1369 |
-
ro.observe(wfContainer);
|
| 1370 |
-
// Fallback after 1s in case ResizeObserver fires late
|
| 1371 |
-
setTimeout(function() {{
|
| 1372 |
-
if (!window["_wf_ws_{slot_id}"]) {{ ro.disconnect(); createWS(); }}
|
| 1373 |
-
}}, 1000);
|
| 1374 |
-
}} else {{
|
| 1375 |
-
// Container not in DOM yet β wait for it
|
| 1376 |
-
setTimeout(loadWS, 200);
|
| 1377 |
-
}}
|
| 1378 |
}}
|
| 1379 |
|
| 1380 |
-
//
|
| 1381 |
-
|
| 1382 |
-
|
| 1383 |
-
|
| 1384 |
-
|
| 1385 |
-
|
| 1386 |
-
if (window.WaveSurfer) {{ clearInterval(t); cb(); }}
|
| 1387 |
-
}}, 100);
|
| 1388 |
-
return;
|
| 1389 |
-
}}
|
| 1390 |
-
const s = document.createElement('script');
|
| 1391 |
-
s.id = 'wavesurfer_script';
|
| 1392 |
-
s.src = '{_WAVESURFER_CDN}';
|
| 1393 |
-
s.onload = function() {{
|
| 1394 |
-
if (document.getElementById('wavesurfer_regions_script')) {{
|
| 1395 |
-
cb(); return;
|
| 1396 |
-
}}
|
| 1397 |
-
const r = document.createElement('script');
|
| 1398 |
-
r.id = 'wavesurfer_regions_script';
|
| 1399 |
-
r.src = '{_REGIONS_CDN}';
|
| 1400 |
-
r.onload = cb;
|
| 1401 |
-
document.head.appendChild(r);
|
| 1402 |
-
}};
|
| 1403 |
-
document.head.appendChild(s);
|
| 1404 |
}}
|
| 1405 |
-
|
| 1406 |
-
loadScripts(loadWS);
|
| 1407 |
}})();
|
| 1408 |
</script>
|
| 1409 |
"""
|
|
@@ -1486,20 +1426,7 @@ def _update_slot_visibility(n):
|
|
| 1486 |
# GRADIO UI #
|
| 1487 |
# ================================================================== #
|
| 1488 |
|
| 1489 |
-
_SLOT_CSS = ""
|
| 1490 |
-
/* Cap the entire video component (including the loading/processing state)
|
| 1491 |
-
so the waveform below is never pushed out of view */
|
| 1492 |
-
.gradio-video {
|
| 1493 |
-
max-height: 380px !important;
|
| 1494 |
-
overflow: hidden;
|
| 1495 |
-
}
|
| 1496 |
-
.gradio-video video,
|
| 1497 |
-
.gradio-video .video-container,
|
| 1498 |
-
.gradio-video .wrap {
|
| 1499 |
-
max-height: 340px !important;
|
| 1500 |
-
object-fit: contain;
|
| 1501 |
-
}
|
| 1502 |
-
"""
|
| 1503 |
|
| 1504 |
with gr.Blocks(title="Generate Audio for Video", css=_SLOT_CSS) as demo:
|
| 1505 |
gr.Markdown(
|
|
|
|
| 1085 |
# WaveSurfer waveform + segment marker HTML builder #
|
| 1086 |
# ------------------------------------------------------------------ #
|
| 1087 |
|
|
|
|
|
|
|
|
|
|
| 1088 |
def _build_waveform_html(audio_path: str, segments: list, slot_id: str,
|
| 1089 |
hidden_input_id: str) -> str:
|
| 1090 |
+
"""Return a self-contained HTML block with a Canvas waveform (display only),
|
| 1091 |
segment boundary markers, and a download link.
|
| 1092 |
|
| 1093 |
+
Uses Web Audio API + Canvas β no external libraries.
|
| 1094 |
+
The waveform is SILENT. The playhead tracks the Gradio <video> element
|
| 1095 |
+
in the same slot via its timeupdate event.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1096 |
"""
|
| 1097 |
if not audio_path or not os.path.exists(audio_path):
|
| 1098 |
return "<p style='color:#888;font-size:12px'>No audio yet.</p>"
|
|
|
|
| 1103 |
|
| 1104 |
segs_json = json.dumps(segments)
|
| 1105 |
|
| 1106 |
+
seg_colors = ["rgba(100,180,255,0.35)", "rgba(255,160,100,0.35)",
|
| 1107 |
+
"rgba(120,220,140,0.35)", "rgba(220,120,220,0.35)",
|
| 1108 |
+
"rgba(255,220,80,0.35)", "rgba(80,220,220,0.35)",
|
| 1109 |
+
"rgba(255,100,100,0.35)", "rgba(180,255,180,0.35)"]
|
| 1110 |
|
| 1111 |
return f"""
|
| 1112 |
<div id="wf_container_{slot_id}"
|
| 1113 |
style="background:#1a1a1a;border-radius:8px;padding:10px;margin-top:6px;position:relative;">
|
| 1114 |
+
<div style="position:relative;width:100%;height:80px;">
|
| 1115 |
+
<canvas id="wf_canvas_{slot_id}"
|
| 1116 |
+
style="width:100%;height:80px;display:block;border-radius:4px;"></canvas>
|
| 1117 |
+
<canvas id="wf_playhead_{slot_id}"
|
| 1118 |
+
style="position:absolute;top:0;left:0;width:100%;height:80px;pointer-events:none;"></canvas>
|
| 1119 |
+
</div>
|
| 1120 |
<div style="display:flex;align-items:center;gap:8px;margin-top:6px;">
|
| 1121 |
<span style="color:#888;font-size:11px;">Click a segment to regenerate | Playhead syncs to video</span>
|
| 1122 |
<a href="{data_uri}" download="audio_{slot_id}.wav"
|
|
|
|
| 1143 |
</div>
|
| 1144 |
<script>
|
| 1145 |
(function() {{
|
| 1146 |
+
// Clean up any previous listeners for this slot
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1147 |
if (window["_wf_video_unlisten_{slot_id}"]) {{
|
| 1148 |
try {{ window["_wf_video_unlisten_{slot_id}"](); }} catch(e) {{}}
|
| 1149 |
window["_wf_video_unlisten_{slot_id}"] = null;
|
| 1150 |
}}
|
| 1151 |
|
| 1152 |
+
const segments = {segs_json};
|
| 1153 |
+
const segColors = {json.dumps(seg_colors)};
|
| 1154 |
+
let audioDuration = 0;
|
| 1155 |
+
let _pendingSegIdx_{slot_id} = null;
|
| 1156 |
|
| 1157 |
+
// ββ Popup helpers ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1158 |
function fireRegen(idx) {{
|
| 1159 |
+
document.getElementById('wf_popup_{slot_id}').style.display = 'none';
|
|
|
|
| 1160 |
const lbl = document.getElementById('wf_seglabel_{slot_id}');
|
|
|
|
| 1161 |
if (lbl) lbl.textContent = 'Regenerating Seg ' + (idx+1) +
|
| 1162 |
+
' (' + segments[idx][0].toFixed(2) + 's \u2013 ' + segments[idx][1].toFixed(2) + 's)\u2026';
|
|
|
|
| 1163 |
const el = document.getElementById('{hidden_input_id}');
|
| 1164 |
if (el) {{
|
| 1165 |
const input = el.querySelector('input, textarea');
|
| 1166 |
if (input) {{
|
| 1167 |
+
const desc = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')
|
| 1168 |
+
|| Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value');
|
| 1169 |
+
if (desc && desc.set) desc.set.call(input, '{slot_id}|' + idx);
|
| 1170 |
+
else input.value = '{slot_id}|' + idx;
|
| 1171 |
+
input.dispatchEvent(new Event('input', {{bubbles:true}}));
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1172 |
}}
|
| 1173 |
}}
|
| 1174 |
}}
|
| 1175 |
|
| 1176 |
+
function showPopup(idx, mx, my) {{
|
| 1177 |
_pendingSegIdx_{slot_id} = idx;
|
|
|
|
| 1178 |
const popup = document.getElementById('wf_popup_{slot_id}');
|
| 1179 |
const plbl = document.getElementById('wf_popup_label_{slot_id}');
|
| 1180 |
+
if (plbl) plbl.textContent = 'Seg '+(idx+1)+' ('+segments[idx][0].toFixed(2)+'s \u2013 '+segments[idx][1].toFixed(2)+'s)';
|
| 1181 |
+
popup.style.display = 'block';
|
| 1182 |
+
popup.style.left = (mx+10)+'px'; popup.style.top = (my+10)+'px';
|
| 1183 |
+
requestAnimationFrame(function() {{
|
| 1184 |
+
const r=popup.getBoundingClientRect(), vw=window.innerWidth, vh=window.innerHeight;
|
| 1185 |
+
if (r.right>vw-8) popup.style.left=(vw-r.width-8)+'px';
|
| 1186 |
+
if (r.bottom>vh-8) popup.style.top=(vh-r.height-8)+'px';
|
| 1187 |
+
}});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1188 |
}}
|
| 1189 |
|
| 1190 |
function hidePopup() {{
|
| 1191 |
+
document.getElementById('wf_popup_{slot_id}').style.display='none';
|
|
|
|
| 1192 |
_pendingSegIdx_{slot_id} = null;
|
| 1193 |
}}
|
| 1194 |
|
|
|
|
| 1195 |
(function tryWireBtn() {{
|
| 1196 |
const btn = document.getElementById('wf_regen_btn_{slot_id}');
|
| 1197 |
+
if (btn) {{ btn.onclick = function(e) {{ e.stopPropagation(); if (_pendingSegIdx_{slot_id}!==null) fireRegen(_pendingSegIdx_{slot_id}); }}; }}
|
| 1198 |
+
else setTimeout(tryWireBtn, 100);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1199 |
}})();
|
| 1200 |
|
|
|
|
| 1201 |
document.addEventListener('click', function(e) {{
|
| 1202 |
+
const p=document.getElementById('wf_popup_{slot_id}');
|
| 1203 |
+
if (p && p.style.display!=='none' && !p.contains(e.target)) hidePopup();
|
|
|
|
|
|
|
| 1204 |
}}, true);
|
| 1205 |
|
| 1206 |
+
// ββ Canvas waveform drawing ββββββββββββββββββββββββββββββββββββββββ
|
| 1207 |
+
function drawWaveform(channelData, duration) {{
|
| 1208 |
+
audioDuration = duration;
|
| 1209 |
+
const canvas = document.getElementById('wf_canvas_{slot_id}');
|
| 1210 |
+
if (!canvas) return;
|
| 1211 |
+
const dpr = window.devicePixelRatio || 1;
|
| 1212 |
+
const W = canvas.offsetWidth || canvas.parentElement.offsetWidth || 600;
|
| 1213 |
+
const H = 80;
|
| 1214 |
+
canvas.width = W * dpr;
|
| 1215 |
+
canvas.height = H * dpr;
|
| 1216 |
+
const ctx = canvas.getContext('2d');
|
| 1217 |
+
ctx.scale(dpr, dpr);
|
| 1218 |
+
|
| 1219 |
+
// Background
|
| 1220 |
+
ctx.fillStyle = '#1e1e2e';
|
| 1221 |
+
ctx.fillRect(0, 0, W, H);
|
| 1222 |
+
|
| 1223 |
+
// Segment region overlays
|
| 1224 |
+
segments.forEach(function(seg, idx) {{
|
| 1225 |
+
const x1 = (seg[0] / duration) * W;
|
| 1226 |
+
const x2 = (seg[1] / duration) * W;
|
| 1227 |
+
ctx.fillStyle = segColors[idx % segColors.length];
|
| 1228 |
+
ctx.fillRect(x1, 0, x2-x1, H);
|
| 1229 |
+
// Segment label
|
| 1230 |
+
ctx.fillStyle = 'rgba(255,255,255,0.6)';
|
| 1231 |
+
ctx.font = '10px sans-serif';
|
| 1232 |
+
ctx.fillText('Seg '+(idx+1), x1+3, 12);
|
| 1233 |
+
}});
|
| 1234 |
+
|
| 1235 |
+
// Waveform bars
|
| 1236 |
+
const samples = channelData.length;
|
| 1237 |
+
const barW = 2, gap = 1, step = barW + gap;
|
| 1238 |
+
const numBars = Math.floor(W / step);
|
| 1239 |
+
const blockSz = Math.floor(samples / numBars);
|
| 1240 |
+
ctx.fillStyle = '#4a9eff';
|
| 1241 |
+
for (let i = 0; i < numBars; i++) {{
|
| 1242 |
+
let max = 0;
|
| 1243 |
+
const start = i * blockSz;
|
| 1244 |
+
for (let j = 0; j < blockSz; j++) {{
|
| 1245 |
+
const v = Math.abs(channelData[start + j] || 0);
|
| 1246 |
+
if (v > max) max = v;
|
| 1247 |
+
}}
|
| 1248 |
+
const barH = Math.max(1, max * H);
|
| 1249 |
+
ctx.fillRect(i * step, (H - barH) / 2, barW, barH);
|
| 1250 |
+
}}
|
| 1251 |
+
|
| 1252 |
+
// Segment boundary lines
|
| 1253 |
+
segments.forEach(function(seg) {{
|
| 1254 |
+
[seg[0], seg[1]].forEach(function(t) {{
|
| 1255 |
+
const x = (t / duration) * W;
|
| 1256 |
+
ctx.strokeStyle = 'rgba(255,255,255,0.4)';
|
| 1257 |
+
ctx.lineWidth = 1;
|
| 1258 |
+
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
|
| 1259 |
+
}});
|
| 1260 |
+
}});
|
| 1261 |
+
|
| 1262 |
+
// Click handler for segment selection
|
| 1263 |
+
canvas.onclick = function(e) {{
|
| 1264 |
+
const rect = canvas.getBoundingClientRect();
|
| 1265 |
+
const xRel = (e.clientX - rect.left) / rect.width;
|
| 1266 |
+
const tClick = xRel * duration;
|
| 1267 |
+
let hit = -1;
|
| 1268 |
+
segments.forEach(function(seg, idx) {{ if (tClick >= seg[0] && tClick <= seg[1]) hit = idx; }});
|
| 1269 |
+
if (hit >= 0) showPopup(hit, e.clientX, e.clientY);
|
| 1270 |
+
else hidePopup();
|
| 1271 |
+
}};
|
| 1272 |
+
}}
|
| 1273 |
+
|
| 1274 |
+
// ββ Playhead drawing ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1275 |
+
function drawPlayhead(progress) {{
|
| 1276 |
+
const canvas = document.getElementById('wf_playhead_{slot_id}');
|
| 1277 |
+
if (!canvas) return;
|
| 1278 |
+
const dpr = window.devicePixelRatio || 1;
|
| 1279 |
+
const W = canvas.offsetWidth || canvas.parentElement.offsetWidth || 600;
|
| 1280 |
+
const H = 80;
|
| 1281 |
+
if (canvas.width !== W*dpr) {{ canvas.width=W*dpr; canvas.height=H*dpr; }}
|
| 1282 |
+
const ctx = canvas.getContext('2d');
|
| 1283 |
+
ctx.clearRect(0, 0, W*dpr, H*dpr);
|
| 1284 |
+
ctx.scale(dpr, dpr);
|
| 1285 |
+
const x = progress * W;
|
| 1286 |
+
ctx.strokeStyle = '#fff';
|
| 1287 |
+
ctx.lineWidth = 2;
|
| 1288 |
+
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
|
| 1289 |
+
// Reset transform for next call
|
| 1290 |
+
ctx.setTransform(1,0,0,1,0,0);
|
| 1291 |
+
}}
|
| 1292 |
+
|
| 1293 |
+
// ββ Video sync ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1294 |
function findSlotVideo() {{
|
| 1295 |
+
let node = document.getElementById('wf_container_{slot_id}');
|
|
|
|
|
|
|
| 1296 |
while (node && node !== document.body) {{
|
| 1297 |
+
const v = node.querySelector('video');
|
| 1298 |
+
if (v) return v;
|
| 1299 |
node = node.parentElement;
|
| 1300 |
}}
|
| 1301 |
return null;
|
| 1302 |
}}
|
| 1303 |
|
| 1304 |
+
function attachVideoSync() {{
|
| 1305 |
+
(function tryAttach() {{
|
|
|
|
| 1306 |
const video = findSlotVideo();
|
| 1307 |
+
if (!video) {{ setTimeout(tryAttach, 300); return; }}
|
|
|
|
|
|
|
|
|
|
| 1308 |
function onTimeUpdate() {{
|
| 1309 |
if (!video.duration || !isFinite(video.duration)) return;
|
| 1310 |
+
drawPlayhead(video.currentTime / video.duration);
|
|
|
|
| 1311 |
}}
|
| 1312 |
video.addEventListener('timeupdate', onTimeUpdate);
|
|
|
|
| 1313 |
window["_wf_video_unlisten_{slot_id}"] = function() {{
|
| 1314 |
video.removeEventListener('timeupdate', onTimeUpdate);
|
| 1315 |
}};
|
| 1316 |
+
}})();
|
|
|
|
| 1317 |
}}
|
| 1318 |
|
| 1319 |
+
// ββ Decode audio & render βββββββββββββββββββββββββββββββββββββββββ
|
| 1320 |
+
function init() {{
|
| 1321 |
+
const b64str = '{b64}';
|
| 1322 |
+
// decode base64 β ArrayBuffer
|
| 1323 |
+
const bin = atob(b64str);
|
| 1324 |
+
const buf = new Uint8Array(bin.length);
|
| 1325 |
+
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
| 1326 |
+
|
| 1327 |
+
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
| 1328 |
+
if (!AudioCtx) {{ console.warn('No AudioContext'); return; }}
|
| 1329 |
+
const actx = new AudioCtx();
|
| 1330 |
+
actx.decodeAudioData(buf.buffer, function(audioBuffer) {{
|
| 1331 |
+
const channelData = audioBuffer.getChannelData(0);
|
| 1332 |
+
drawWaveform(channelData, audioBuffer.duration);
|
| 1333 |
+
attachVideoSync();
|
| 1334 |
+
actx.close();
|
| 1335 |
+
}}, function(err) {{
|
| 1336 |
+
console.error('[waveform {slot_id}] decodeAudioData error:', err);
|
| 1337 |
+
}});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1338 |
}}
|
| 1339 |
|
| 1340 |
+
// Run after the canvas is in the DOM and has layout
|
| 1341 |
+
if (document.readyState === 'loading') {{
|
| 1342 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 1343 |
+
}} else {{
|
| 1344 |
+
// Small delay to let Gradio finish laying out the component
|
| 1345 |
+
setTimeout(init, 100);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1346 |
}}
|
|
|
|
|
|
|
| 1347 |
}})();
|
| 1348 |
</script>
|
| 1349 |
"""
|
|
|
|
| 1426 |
# GRADIO UI #
|
| 1427 |
# ================================================================== #
|
| 1428 |
|
| 1429 |
+
_SLOT_CSS = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1430 |
|
| 1431 |
with gr.Blocks(title="Generate Audio for Video", css=_SLOT_CSS) as demo:
|
| 1432 |
gr.Markdown(
|