Spaces:
Running on Zero
Running on Zero
Commit Β·
41d322f
1
Parent(s): fcc4220
Fix waveform: popup via postMessage, regen, playhead, Too many args
Browse files
app.py
CHANGED
|
@@ -1125,17 +1125,6 @@ def _build_waveform_html(audio_path: str, segments: list, slot_id: str,
|
|
| 1125 |
canvas {{ display:block; }}
|
| 1126 |
#cv {{ position:absolute; top:0; left:0; width:100%; height:100%; }}
|
| 1127 |
#cvp {{ position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; }}
|
| 1128 |
-
#popup {{
|
| 1129 |
-
display:none; position:fixed; z-index:9999;
|
| 1130 |
-
background:#2a2a2a; border:1px solid #555; border-radius:6px;
|
| 1131 |
-
padding:8px 12px; box-shadow:0 4px 16px rgba(0,0,0,.5);
|
| 1132 |
-
font-family:sans-serif;
|
| 1133 |
-
}}
|
| 1134 |
-
#popuplbl {{ color:#ccc; font-size:11px; margin-bottom:6px; white-space:nowrap; }}
|
| 1135 |
-
#regenbtn {{
|
| 1136 |
-
background:#1d6fa5; color:#fff; border:none; border-radius:4px;
|
| 1137 |
-
padding:5px 14px; font-size:12px; cursor:pointer; width:100%;
|
| 1138 |
-
}}
|
| 1139 |
</style>
|
| 1140 |
</head>
|
| 1141 |
<body>
|
|
@@ -1143,71 +1132,38 @@ def _build_waveform_html(audio_path: str, segments: list, slot_id: str,
|
|
| 1143 |
<canvas id="cv"></canvas>
|
| 1144 |
<canvas id="cvp"></canvas>
|
| 1145 |
</div>
|
| 1146 |
-
<div id="popup">
|
| 1147 |
-
<div id="popuplbl"></div>
|
| 1148 |
-
<button id="regenbtn">⟳ Regenerate</button>
|
| 1149 |
-
</div>
|
| 1150 |
<script>
|
| 1151 |
(function() {{
|
| 1152 |
const SLOT_ID = '{slot_id}';
|
| 1153 |
const segments = {segs_json};
|
| 1154 |
const segColors = {json.dumps(seg_colors)};
|
| 1155 |
let audioDuration = 0;
|
| 1156 |
-
let pendingIdx = null;
|
| 1157 |
-
|
| 1158 |
-
// ββ Popup ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1159 |
-
const popup = document.getElementById('popup');
|
| 1160 |
-
const popuplbl= document.getElementById('popuplbl');
|
| 1161 |
-
const regenbtn= document.getElementById('regenbtn');
|
| 1162 |
|
|
|
|
|
|
|
|
|
|
| 1163 |
function showPopup(idx, mx, my) {{
|
| 1164 |
-
|
| 1165 |
-
|
| 1166 |
-
|
| 1167 |
-
|
| 1168 |
-
|
| 1169 |
-
|
| 1170 |
-
|
| 1171 |
-
|
| 1172 |
-
|
| 1173 |
-
}})
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
// Directly trigger Gradio hidden input in parent document (same-origin iframe)
|
| 1181 |
-
try {{
|
| 1182 |
-
const par = window.parent.document;
|
| 1183 |
-
const el = par.getElementById('{hidden_input_id}');
|
| 1184 |
-
if (el) {{
|
| 1185 |
-
const input = el.querySelector('input, textarea');
|
| 1186 |
-
if (input) {{
|
| 1187 |
-
const desc = Object.getOwnPropertyDescriptor(
|
| 1188 |
-
window.parent.HTMLInputElement.prototype, 'value')
|
| 1189 |
-
|| Object.getOwnPropertyDescriptor(
|
| 1190 |
-
window.parent.HTMLTextAreaElement.prototype, 'value');
|
| 1191 |
-
if (desc && desc.set) desc.set.call(input, SLOT_ID+'|'+pendingIdx);
|
| 1192 |
-
else input.value = SLOT_ID+'|'+pendingIdx;
|
| 1193 |
-
input.dispatchEvent(new window.parent.Event('input', {{bubbles:true}}));
|
| 1194 |
-
}}
|
| 1195 |
-
}}
|
| 1196 |
-
// Update status label in parent
|
| 1197 |
-
const lbl = par.getElementById('wf_seglabel_{slot_id}');
|
| 1198 |
-
if (lbl && segments[pendingIdx])
|
| 1199 |
-
lbl.textContent = 'Regenerating Seg '+(pendingIdx+1)+
|
| 1200 |
-
' ('+segments[pendingIdx][0].toFixed(2)+'s \u2013 '+
|
| 1201 |
-
segments[pendingIdx][1].toFixed(2)+'s)\u2026';
|
| 1202 |
-
}} catch(err) {{
|
| 1203 |
-
console.error('[wf iframe] regen trigger failed:', err);
|
| 1204 |
-
}}
|
| 1205 |
-
hidePopup();
|
| 1206 |
}}
|
| 1207 |
-
}}
|
| 1208 |
-
|
| 1209 |
-
|
| 1210 |
-
}}
|
| 1211 |
|
| 1212 |
// ββ Canvas waveform ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1213 |
const cv = document.getElementById('cv');
|
|
@@ -1289,14 +1245,29 @@ def _build_waveform_html(audio_path: str, segments: list, slot_id: str,
|
|
| 1289 |
ctx.restore();
|
| 1290 |
}}
|
| 1291 |
|
| 1292 |
-
// Poll parent for video time
|
| 1293 |
-
|
| 1294 |
try {{
|
| 1295 |
-
const
|
| 1296 |
-
|
| 1297 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1298 |
}}
|
| 1299 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1300 |
}}, 50);
|
| 1301 |
|
| 1302 |
// ββ Decode audio βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -1375,7 +1346,6 @@ def _make_output_slots(tab_prefix: str) -> tuple:
|
|
| 1375 |
vids.append(gr.Video(label=f"Generation {i+1} β Video"))
|
| 1376 |
waveforms.append(gr.HTML(
|
| 1377 |
value="<p style='color:#888;font-size:12px'>Generate audio to see waveform.</p>",
|
| 1378 |
-
label=f"Generation {i+1} β Waveform",
|
| 1379 |
))
|
| 1380 |
# Hidden textbox: JS writes "<slot_id>|<seg_idx>" here to trigger regen
|
| 1381 |
regen_triggers.append(gr.Textbox(
|
|
@@ -1449,7 +1419,85 @@ _SLOT_CSS = """
|
|
| 1449 |
}
|
| 1450 |
"""
|
| 1451 |
|
| 1452 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1453 |
gr.Markdown(
|
| 1454 |
"# Generate Audio for Video\n"
|
| 1455 |
"Choose a model and upload a video to generate synchronized audio.\n\n"
|
|
|
|
| 1125 |
canvas {{ display:block; }}
|
| 1126 |
#cv {{ position:absolute; top:0; left:0; width:100%; height:100%; }}
|
| 1127 |
#cvp {{ position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1128 |
</style>
|
| 1129 |
</head>
|
| 1130 |
<body>
|
|
|
|
| 1132 |
<canvas id="cv"></canvas>
|
| 1133 |
<canvas id="cvp"></canvas>
|
| 1134 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1135 |
<script>
|
| 1136 |
(function() {{
|
| 1137 |
const SLOT_ID = '{slot_id}';
|
| 1138 |
const segments = {segs_json};
|
| 1139 |
const segColors = {json.dumps(seg_colors)};
|
| 1140 |
let audioDuration = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1141 |
|
| 1142 |
+
// ββ Popup via postMessage to parent global listener βββββββββββββββββ
|
| 1143 |
+
// The parent page (Gradio) has a global window.addEventListener('message',...)
|
| 1144 |
+
// set up via gr.Blocks(js=...) that handles popup show/hide and regen trigger.
|
| 1145 |
function showPopup(idx, mx, my) {{
|
| 1146 |
+
// Convert iframe-local coords to parent page coords
|
| 1147 |
+
try {{
|
| 1148 |
+
const fr = window.frameElement ? window.frameElement.getBoundingClientRect() : {{left:0,top:0}};
|
| 1149 |
+
window.parent.postMessage({{
|
| 1150 |
+
type:'wf_popup', action:'show',
|
| 1151 |
+
slot_id: SLOT_ID, seg_idx: idx,
|
| 1152 |
+
t0: segments[idx][0], t1: segments[idx][1],
|
| 1153 |
+
x: mx + fr.left, y: my + fr.top
|
| 1154 |
+
}}, '*');
|
| 1155 |
+
}} catch(e) {{
|
| 1156 |
+
window.parent.postMessage({{
|
| 1157 |
+
type:'wf_popup', action:'show',
|
| 1158 |
+
slot_id: SLOT_ID, seg_idx: idx,
|
| 1159 |
+
t0: segments[idx][0], t1: segments[idx][1],
|
| 1160 |
+
x: mx, y: my
|
| 1161 |
+
}}, '*');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1162 |
}}
|
| 1163 |
+
}}
|
| 1164 |
+
function hidePopup() {{
|
| 1165 |
+
window.parent.postMessage({{type:'wf_popup', action:'hide'}}, '*');
|
| 1166 |
+
}}
|
| 1167 |
|
| 1168 |
// ββ Canvas waveform ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1169 |
const cv = document.getElementById('cv');
|
|
|
|
| 1245 |
ctx.restore();
|
| 1246 |
}}
|
| 1247 |
|
| 1248 |
+
// Poll parent for video time β find the video in the same wf_container slot
|
| 1249 |
+
function findSlotVideo() {{
|
| 1250 |
try {{
|
| 1251 |
+
const par = window.parent.document;
|
| 1252 |
+
// Walk up from our iframe to find wf_container_{slot_id}, then find its sibling video
|
| 1253 |
+
const container = par.getElementById('wf_container_{slot_id}');
|
| 1254 |
+
if (!container) return par.querySelector('video');
|
| 1255 |
+
// The video is inside the same gr.Group β walk up to find it
|
| 1256 |
+
let node = container.parentElement;
|
| 1257 |
+
while (node && node !== par.body) {{
|
| 1258 |
+
const v = node.querySelector('video');
|
| 1259 |
+
if (v) return v;
|
| 1260 |
+
node = node.parentElement;
|
| 1261 |
}}
|
| 1262 |
+
return null;
|
| 1263 |
+
}} catch(e) {{ return null; }}
|
| 1264 |
+
}}
|
| 1265 |
+
|
| 1266 |
+
setInterval(function() {{
|
| 1267 |
+
const vid = findSlotVideo();
|
| 1268 |
+
if (vid && vid.duration && isFinite(vid.duration) && audioDuration > 0) {{
|
| 1269 |
+
drawPlayhead(vid.currentTime / vid.duration);
|
| 1270 |
+
}}
|
| 1271 |
}}, 50);
|
| 1272 |
|
| 1273 |
// ββ Decode audio βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 1346 |
vids.append(gr.Video(label=f"Generation {i+1} β Video"))
|
| 1347 |
waveforms.append(gr.HTML(
|
| 1348 |
value="<p style='color:#888;font-size:12px'>Generate audio to see waveform.</p>",
|
|
|
|
| 1349 |
))
|
| 1350 |
# Hidden textbox: JS writes "<slot_id>|<seg_idx>" here to trigger regen
|
| 1351 |
regen_triggers.append(gr.Textbox(
|
|
|
|
| 1419 |
}
|
| 1420 |
"""
|
| 1421 |
|
| 1422 |
+
_GLOBAL_JS = """
|
| 1423 |
+
() => {
|
| 1424 |
+
// Global postMessage handler for waveform iframe events.
|
| 1425 |
+
// Runs once on page load (Gradio js= parameter).
|
| 1426 |
+
// Handles: popup open/close relay, regen trigger.
|
| 1427 |
+
if (window._wf_global_listener) return; // already registered
|
| 1428 |
+
window._wf_global_listener = true;
|
| 1429 |
+
|
| 1430 |
+
// Shared popup element created once and reused across all slots
|
| 1431 |
+
let _popup = null;
|
| 1432 |
+
let _pendingSlot = null, _pendingIdx = null;
|
| 1433 |
+
|
| 1434 |
+
function ensurePopup() {
|
| 1435 |
+
if (_popup) return _popup;
|
| 1436 |
+
_popup = document.createElement('div');
|
| 1437 |
+
_popup.style.cssText = 'display:none;position:fixed;z-index:99999;' +
|
| 1438 |
+
'background:#2a2a2a;border:1px solid #555;border-radius:6px;' +
|
| 1439 |
+
'padding:8px 12px;box-shadow:0 4px 16px rgba(0,0,0,.5);font-family:sans-serif;';
|
| 1440 |
+
_popup.innerHTML =
|
| 1441 |
+
'<div id="_wf_popup_lbl" style="color:#ccc;font-size:11px;margin-bottom:6px;white-space:nowrap;"></div>' +
|
| 1442 |
+
'<button id="_wf_popup_btn" style="background:#1d6fa5;color:#fff;border:none;' +
|
| 1443 |
+
'border-radius:4px;padding:5px 14px;font-size:12px;cursor:pointer;width:100%;">⟳ Regenerate</button>';
|
| 1444 |
+
document.body.appendChild(_popup);
|
| 1445 |
+
document.getElementById('_wf_popup_btn').onclick = function(e) {
|
| 1446 |
+
e.stopPropagation();
|
| 1447 |
+
if (_pendingSlot !== null && _pendingIdx !== null) {
|
| 1448 |
+
fireRegen(_pendingSlot, _pendingIdx);
|
| 1449 |
+
}
|
| 1450 |
+
hidePopup();
|
| 1451 |
+
};
|
| 1452 |
+
document.addEventListener('click', function() { hidePopup(); }, true);
|
| 1453 |
+
return _popup;
|
| 1454 |
+
}
|
| 1455 |
+
|
| 1456 |
+
function hidePopup() {
|
| 1457 |
+
if (_popup) _popup.style.display = 'none';
|
| 1458 |
+
_pendingSlot = null; _pendingIdx = null;
|
| 1459 |
+
}
|
| 1460 |
+
|
| 1461 |
+
function fireRegen(slot_id, idx) {
|
| 1462 |
+
const hidden_id = 'regen_trigger_' + slot_id;
|
| 1463 |
+
const el = document.getElementById(hidden_id);
|
| 1464 |
+
if (!el) { console.warn('[wf global] regen el not found:', hidden_id); return; }
|
| 1465 |
+
const input = el.querySelector('input, textarea');
|
| 1466 |
+
if (!input) { console.warn('[wf global] regen input not found in', hidden_id); return; }
|
| 1467 |
+
const desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')
|
| 1468 |
+
|| Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value');
|
| 1469 |
+
if (desc && desc.set) desc.set.call(input, slot_id + '|' + idx);
|
| 1470 |
+
else input.value = slot_id + '|' + idx;
|
| 1471 |
+
input.dispatchEvent(new Event('input', {bubbles: true}));
|
| 1472 |
+
// Update status label
|
| 1473 |
+
const lbl = document.getElementById('wf_seglabel_' + slot_id);
|
| 1474 |
+
if (lbl) lbl.textContent = 'Regenerating Seg ' + (idx + 1) + '...';
|
| 1475 |
+
}
|
| 1476 |
+
|
| 1477 |
+
window.addEventListener('message', function(e) {
|
| 1478 |
+
const d = e.data;
|
| 1479 |
+
if (!d || d.type !== 'wf_popup') return;
|
| 1480 |
+
const p = ensurePopup();
|
| 1481 |
+
if (d.action === 'hide') { hidePopup(); return; }
|
| 1482 |
+
// action === 'show'
|
| 1483 |
+
_pendingSlot = d.slot_id;
|
| 1484 |
+
_pendingIdx = d.seg_idx;
|
| 1485 |
+
const lbl = document.getElementById('_wf_popup_lbl');
|
| 1486 |
+
if (lbl) lbl.textContent = 'Seg ' + (d.seg_idx + 1) +
|
| 1487 |
+
' (' + d.t0.toFixed(2) + 's β ' + d.t1.toFixed(2) + 's)';
|
| 1488 |
+
p.style.display = 'block';
|
| 1489 |
+
p.style.left = (d.x + 10) + 'px';
|
| 1490 |
+
p.style.top = (d.y + 10) + 'px';
|
| 1491 |
+
requestAnimationFrame(function() {
|
| 1492 |
+
const r = p.getBoundingClientRect();
|
| 1493 |
+
if (r.right > window.innerWidth - 8) p.style.left = (window.innerWidth - r.width - 8) + 'px';
|
| 1494 |
+
if (r.bottom > window.innerHeight - 8) p.style.top = (window.innerHeight - r.height - 8) + 'px';
|
| 1495 |
+
});
|
| 1496 |
+
});
|
| 1497 |
+
}
|
| 1498 |
+
"""
|
| 1499 |
+
|
| 1500 |
+
with gr.Blocks(title="Generate Audio for Video", css=_SLOT_CSS, js=_GLOBAL_JS) as demo:
|
| 1501 |
gr.Markdown(
|
| 1502 |
"# Generate Audio for Video\n"
|
| 1503 |
"Choose a model and upload a video to generate synchronized audio.\n\n"
|