BoxOfColors commited on
Commit
4aa0f7d
·
1 Parent(s): 02a1f95
Files changed (1) hide show
  1. app.py +140 -80
app.py CHANGED
@@ -785,16 +785,24 @@ def _splice_and_save(new_wav, seg_idx, meta, slot_id):
785
  else:
786
  full_wav = full_wav[:n_total]
787
 
788
- # Save new audio
 
 
 
789
  tmp_dir = os.path.dirname(meta["audio_path"])
790
- audio_path = meta["audio_path"] # overwrite in-place
 
 
 
791
  if stereo:
792
  torchaudio.save(audio_path, torch.from_numpy(np.ascontiguousarray(full_wav)), sr)
793
  else:
794
  torchaudio.save(audio_path, torch.from_numpy(np.ascontiguousarray(full_wav)).unsqueeze(0), sr)
795
 
796
- # Re-mux video
797
- video_path = meta["video_path"] # overwrite in-place
 
 
798
  if model == "hunyuan":
799
  # HunyuanFoley uses its own merge_audio_video
800
  _hf_path = str(Path("HunyuanVideo-Foley").resolve())
@@ -1082,8 +1090,11 @@ _REGIONS_CDN = "https://cdnjs.cloudflare.com/ajax/libs/wavesurfer.js/7.8.7/pl
1082
 
1083
  def _build_waveform_html(audio_path: str, segments: list, slot_id: str,
1084
  hidden_input_id: str) -> str:
1085
- """Return a self-contained HTML block with a WaveSurfer waveform,
1086
- segment boundary markers, a play/pause button, and a download link.
 
 
 
1087
 
1088
  Clicking a region shows a small popup near the cursor with a
1089
  "Regenerate" button. Clicking elsewhere dismisses the popup.
@@ -1114,10 +1125,7 @@ def _build_waveform_html(audio_path: str, segments: list, slot_id: str,
1114
  style="background:#1a1a1a;border-radius:8px;padding:10px;margin-top:6px;position:relative;">
1115
  <div id="wf_{slot_id}" style="width:100%;min-height:80px;"></div>
1116
  <div style="display:flex;align-items:center;gap:8px;margin-top:6px;">
1117
- <button id="wf_playbtn_{slot_id}" onclick="wf_toggle_{slot_id}()"
1118
- style="background:#333;color:#eee;border:1px solid #555;border-radius:4px;
1119
- padding:3px 10px;font-size:12px;cursor:pointer;">&#9654; Play</button>
1120
- <span style="color:#888;font-size:11px;">Click a segment to regenerate</span>
1121
  <a href="{data_uri}" download="audio_{slot_id}.wav"
1122
  style="margin-left:auto;background:#333;color:#eee;border:1px solid #555;
1123
  border-radius:4px;padding:3px 10px;font-size:12px;text-decoration:none;">
@@ -1142,9 +1150,17 @@ def _build_waveform_html(audio_path: str, segments: list, slot_id: str,
1142
  </div>
1143
  <script>
1144
  (function() {{
1145
- // Guard against double-init on Gradio re-renders
1146
- if (window["_wf_init_{slot_id}"]) return;
1147
- window["_wf_init_{slot_id}"] = true;
 
 
 
 
 
 
 
 
1148
 
1149
  let _pendingSegIdx_{slot_id} = null;
1150
 
@@ -1160,10 +1176,14 @@ def _build_waveform_html(audio_path: str, segments: list, slot_id: str,
1160
  if (el) {{
1161
  const input = el.querySelector('input, textarea');
1162
  if (input) {{
1163
- const setter =
1164
- Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set ||
1165
- Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
1166
- setter.call(input, '{slot_id}|' + idx);
 
 
 
 
1167
  input.dispatchEvent(new Event('input', {{ bubbles: true }}));
1168
  }}
1169
  }}
@@ -1178,12 +1198,9 @@ def _build_waveform_html(audio_path: str, segments: list, slot_id: str,
1178
  'Seg ' + (idx+1) + ' (' + segs[idx][0].toFixed(2) + 's \u2013 ' + segs[idx][1].toFixed(2) + 's)';
1179
  if (popup) {{
1180
  popup.style.display = 'block';
1181
- // Position near cursor, keep inside viewport
1182
  const vw = window.innerWidth, vh = window.innerHeight;
1183
- let x = mouseX + 10, y = mouseY + 10;
1184
- popup.style.left = x + 'px';
1185
- popup.style.top = y + 'px';
1186
- // nudge back if off screen
1187
  requestAnimationFrame(function() {{
1188
  const r = popup.getBoundingClientRect();
1189
  if (r.right > vw - 8) popup.style.left = (vw - r.width - 8) + 'px';
@@ -1198,15 +1215,7 @@ def _build_waveform_html(audio_path: str, segments: list, slot_id: str,
1198
  _pendingSegIdx_{slot_id} = null;
1199
  }}
1200
 
1201
- // Wire the Regenerate button
1202
- document.addEventListener('DOMContentLoaded', function() {{
1203
- const btn = document.getElementById('wf_regen_btn_{slot_id}');
1204
- if (btn) btn.addEventListener('click', function(e) {{
1205
- e.stopPropagation();
1206
- if (_pendingSegIdx_{slot_id} !== null) fireRegen(_pendingSegIdx_{slot_id});
1207
- }});
1208
- }});
1209
- // Also wire immediately in case DOM already loaded
1210
  (function tryWireBtn() {{
1211
  const btn = document.getElementById('wf_regen_btn_{slot_id}');
1212
  if (btn) {{
@@ -1227,79 +1236,130 @@ def _build_waveform_html(audio_path: str, segments: list, slot_id: str,
1227
  }}
1228
  }}, true);
1229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1230
  function loadWS() {{
1231
- if (!window.WaveSurfer || !window.WaveSurfer.Regions) {{
1232
  setTimeout(loadWS, 200);
1233
  return;
1234
  }}
1235
- const RegionsPlugin = window.WaveSurfer.Regions.create();
 
 
 
 
 
 
 
 
 
1236
  const ws = WaveSurfer.create({{
1237
- container: '#wf_{slot_id}',
1238
- waveColor: '#4a9eff',
1239
- progressColor:'#1a5fa8',
1240
- height: 80,
1241
- barWidth: 2,
1242
- barGap: 1,
1243
- barRadius: 2,
1244
- backend: 'WebAudio',
1245
- url: '{data_uri}',
1246
- plugins: [RegionsPlugin],
1247
  }});
1248
- window["_wf_ws_{slot_id}"] = ws;
1249
- window["wf_toggle_{slot_id}"] = function() {{ ws.playPause(); }};
1250
-
1251
- const segments = {segs_json};
1252
- const colors = {json.dumps(colors)};
1253
 
 
 
1254
  ws.on('ready', function() {{
1255
- segments.forEach(function(seg, idx) {{
1256
- RegionsPlugin.addRegion({{
1257
- id: 'seg_' + idx,
1258
- start: seg[0],
1259
- end: seg[1],
1260
- color: colors[idx % colors.length],
1261
- drag: false,
1262
- resize: false,
1263
- content: 'Seg ' + (idx + 1),
 
 
 
 
 
1264
  }});
1265
- }});
1266
- }});
1267
-
1268
- RegionsPlugin.on('region-clicked', function(region, e) {{
1269
- e.stopPropagation();
1270
- const idx = parseInt(region.id.replace('seg_', ''));
1271
- showPopup(idx, e.clientX, e.clientY);
1272
  }});
1273
 
1274
- ws.on('play', function() {{
1275
- const b = document.getElementById('wf_playbtn_{slot_id}');
1276
- if (b) b.textContent = '\u23f8 Pause';
1277
- }});
1278
- ws.on('pause', function() {{
1279
- const b = document.getElementById('wf_playbtn_{slot_id}');
1280
- if (b) b.textContent = '\u25b6 Play';
1281
- }});
1282
- ws.on('finish', function() {{
1283
- const b = document.getElementById('wf_playbtn_{slot_id}');
1284
- if (b) b.textContent = '\u25b6 Play';
1285
- }});
1286
  }}
1287
 
1288
- if (!document.getElementById('wavesurfer_script')) {{
 
 
 
 
 
 
 
 
 
1289
  const s = document.createElement('script');
1290
  s.id = 'wavesurfer_script';
1291
  s.src = '{_WAVESURFER_CDN}';
1292
  s.onload = function() {{
 
 
 
1293
  const r = document.createElement('script');
1294
  r.id = 'wavesurfer_regions_script';
1295
  r.src = '{_REGIONS_CDN}';
1296
- r.onload = loadWS;
1297
  document.head.appendChild(r);
1298
  }};
1299
  document.head.appendChild(s);
1300
- }} else {{
1301
- loadWS();
1302
  }}
 
 
1303
  }})();
1304
  </script>
1305
  """
 
785
  else:
786
  full_wav = full_wav[:n_total]
787
 
788
+ # Save new audio — use a new timestamped filename so Gradio / the browser
789
+ # treats it as a genuinely different file and reloads the video player.
790
+ import time as _time
791
+ _ts = int(_time.time() * 1000)
792
  tmp_dir = os.path.dirname(meta["audio_path"])
793
+ _base = os.path.splitext(os.path.basename(meta["audio_path"]))[0]
794
+ # Strip any previous timestamp suffix before adding a new one
795
+ _base_clean = _base.rsplit("_regen_", 1)[0]
796
+ audio_path = os.path.join(tmp_dir, f"{_base_clean}_regen_{_ts}.wav")
797
  if stereo:
798
  torchaudio.save(audio_path, torch.from_numpy(np.ascontiguousarray(full_wav)), sr)
799
  else:
800
  torchaudio.save(audio_path, torch.from_numpy(np.ascontiguousarray(full_wav)).unsqueeze(0), sr)
801
 
802
+ # Re-mux into a new video file so the browser is forced to reload it
803
+ _vid_base = os.path.splitext(os.path.basename(meta["video_path"]))[0]
804
+ _vid_base_clean = _vid_base.rsplit("_regen_", 1)[0]
805
+ video_path = os.path.join(tmp_dir, f"{_vid_base_clean}_regen_{_ts}.mp4")
806
  if model == "hunyuan":
807
  # HunyuanFoley uses its own merge_audio_video
808
  _hf_path = str(Path("HunyuanVideo-Foley").resolve())
 
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 WaveSurfer waveform (display only),
1094
+ segment boundary markers, and a download link.
1095
+
1096
+ The waveform is SILENT — no audio plays from it. The playhead tracks the
1097
+ Gradio <video> element in the same slot via its `timeupdate` event.
1098
 
1099
  Clicking a region shows a small popup near the cursor with a
1100
  "Regenerate" button. Clicking elsewhere dismisses the popup.
 
1125
  style="background:#1a1a1a;border-radius:8px;padding:10px;margin-top:6px;position:relative;">
1126
  <div id="wf_{slot_id}" style="width:100%;min-height:80px;"></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 &nbsp;|&nbsp; Playhead syncs to video</span>
 
 
 
1129
  <a href="{data_uri}" download="audio_{slot_id}.wav"
1130
  style="margin-left:auto;background:#333;color:#eee;border:1px solid #555;
1131
  border-radius:4px;padding:3px 10px;font-size:12px;text-decoration:none;">
 
1150
  </div>
1151
  <script>
1152
  (function() {{
1153
+ // Each time this HTML is injected (new audio or regen), destroy any previous
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
  let _pendingSegIdx_{slot_id} = null;
1166
 
 
1176
  if (el) {{
1177
  const input = el.querySelector('input, textarea');
1178
  if (input) {{
1179
+ const nativeSetter =
1180
+ Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value') ||
1181
+ Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value');
1182
+ if (nativeSetter && nativeSetter.set) {{
1183
+ nativeSetter.set.call(input, '{slot_id}|' + idx);
1184
+ }} else {{
1185
+ input.value = '{slot_id}|' + idx;
1186
+ }}
1187
  input.dispatchEvent(new Event('input', {{ bubbles: true }}));
1188
  }}
1189
  }}
 
1198
  'Seg ' + (idx+1) + ' (' + segs[idx][0].toFixed(2) + 's \u2013 ' + segs[idx][1].toFixed(2) + 's)';
1199
  if (popup) {{
1200
  popup.style.display = 'block';
 
1201
  const vw = window.innerWidth, vh = window.innerHeight;
1202
+ popup.style.left = (mouseX + 10) + 'px';
1203
+ popup.style.top = (mouseY + 10) + 'px';
 
 
1204
  requestAnimationFrame(function() {{
1205
  const r = popup.getBoundingClientRect();
1206
  if (r.right > vw - 8) popup.style.left = (vw - r.width - 8) + 'px';
 
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) {{
 
1236
  }}
1237
  }}, true);
1238
 
1239
+ // -----------------------------------------------------------------------
1240
+ // Find the <video> element that lives in the same Gradio slot as this
1241
+ // waveform. Walk up the DOM from the waveform container until we find
1242
+ // a parent that also contains a <video> tag.
1243
+ // -----------------------------------------------------------------------
1244
+ function findSlotVideo() {{
1245
+ const wfEl = document.getElementById('wf_container_{slot_id}');
1246
+ if (!wfEl) return null;
1247
+ let node = wfEl.parentElement;
1248
+ while (node && node !== document.body) {{
1249
+ const vid = node.querySelector('video');
1250
+ if (vid) return vid;
1251
+ node = node.parentElement;
1252
+ }}
1253
+ return null;
1254
+ }}
1255
+
1256
+ function attachVideoSync(ws) {{
1257
+ // Poll until the video element appears (Gradio may render it after the HTML)
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
+ const progress = video.currentTime / video.duration;
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
  function loadWS() {{
1279
+ if (!window.WaveSurfer) {{
1280
  setTimeout(loadWS, 200);
1281
  return;
1282
  }}
1283
+
1284
+ // Use MediaElement backend so WaveSurfer renders the waveform from the
1285
+ // audio data but we keep the backing <audio> muted — no sound output.
1286
+ let plugins = [];
1287
+ let regionsPlugin = null;
1288
+ if (window.WaveSurfer.Regions) {{
1289
+ regionsPlugin = window.WaveSurfer.Regions.create();
1290
+ plugins = [regionsPlugin];
1291
+ }}
1292
+
1293
  const ws = WaveSurfer.create({{
1294
+ container: '#wf_{slot_id}',
1295
+ waveColor: '#4a9eff',
1296
+ progressColor: '#1a5fa8',
1297
+ height: 80,
1298
+ barWidth: 2,
1299
+ barGap: 1,
1300
+ barRadius: 2,
1301
+ interact: false,
1302
+ url: '{data_uri}',
1303
+ plugins: plugins,
1304
  }});
 
 
 
 
 
1305
 
1306
+ // Silence the WaveSurfer audio immediately and on ready
1307
+ ws.setVolume(0);
1308
  ws.on('ready', function() {{
1309
+ ws.setVolume(0);
1310
+ const segments = {segs_json};
1311
+ const colors = {json.dumps(colors)};
1312
+ if (regionsPlugin) {{
1313
+ segments.forEach(function(seg, idx) {{
1314
+ regionsPlugin.addRegion({{
1315
+ id: 'seg_' + idx,
1316
+ start: seg[0],
1317
+ end: seg[1],
1318
+ color: colors[idx % colors.length],
1319
+ drag: false,
1320
+ resize: false,
1321
+ content: 'Seg ' + (idx + 1),
1322
+ }});
1323
  }});
1324
+ regionsPlugin.on('region-clicked', function(region, e) {{
1325
+ e.stopPropagation();
1326
+ const idx = parseInt(region.id.replace('seg_', ''));
1327
+ showPopup(idx, e.clientX, e.clientY);
1328
+ }});
1329
+ }}
1330
+ attachVideoSync(ws);
1331
  }});
1332
 
1333
+ window["_wf_ws_{slot_id}"] = ws;
 
 
 
 
 
 
 
 
 
 
 
1334
  }}
1335
 
1336
+ // Load WaveSurfer + Regions scripts once (idempotent)
1337
+ function loadScripts(cb) {{
1338
+ if (window.WaveSurfer) {{ cb(); return; }}
1339
+ if (document.getElementById('wavesurfer_script')) {{
1340
+ // Script tag exists but WaveSurfer not yet defined — wait
1341
+ const t = setInterval(function() {{
1342
+ if (window.WaveSurfer) {{ clearInterval(t); cb(); }}
1343
+ }}, 100);
1344
+ return;
1345
+ }}
1346
  const s = document.createElement('script');
1347
  s.id = 'wavesurfer_script';
1348
  s.src = '{_WAVESURFER_CDN}';
1349
  s.onload = function() {{
1350
+ if (document.getElementById('wavesurfer_regions_script')) {{
1351
+ cb(); return;
1352
+ }}
1353
  const r = document.createElement('script');
1354
  r.id = 'wavesurfer_regions_script';
1355
  r.src = '{_REGIONS_CDN}';
1356
+ r.onload = cb;
1357
  document.head.appendChild(r);
1358
  }};
1359
  document.head.appendChild(s);
 
 
1360
  }}
1361
+
1362
+ loadScripts(loadWS);
1363
  }})();
1364
  </script>
1365
  """