akhaliq HF Staff commited on
Commit
ea25969
·
1 Parent(s): 75cc0e6

feat: add interactive client-side audio trimmer for voice references

Browse files
Files changed (1) hide show
  1. index.html +546 -18
index.html CHANGED
@@ -1024,6 +1024,171 @@
1024
  display: none; /* Only show toggle trigger on desktop */
1025
  }
1026
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1027
  </style>
1028
  </head>
1029
  <body>
@@ -1049,6 +1214,48 @@
1049
  </div>
1050
  </div>
1051
  <input type="file" id="audio-file" class="hidden-input" accept="audio/*">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1052
  </div>
1053
 
1054
  <!-- Settings Dashboard Parameters -->
@@ -1380,6 +1587,12 @@
1380
  let selectedAudioFile = null;
1381
  let selectedAudioFilename = "";
1382
 
 
 
 
 
 
 
1383
  // UI nodes
1384
  const sidebar = document.getElementById("app-sidebar");
1385
  const sidebarOverlay = document.getElementById("sidebar-overlay");
@@ -1421,6 +1634,19 @@
1421
  const playerDownload = document.getElementById("player-download");
1422
  const speedButtons = document.querySelectorAll(".btn-speed");
1423
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1424
  // Guide togglers
1425
  const guideToggle = document.getElementById("guide-toggle");
1426
  const guideBody = document.getElementById("guide-body");
@@ -1532,29 +1758,206 @@
1532
  function handleUploadedFile(file) {
1533
  selectedAudioFile = file;
1534
  selectedAudioFilename = file.name;
1535
- renderUploadedUI(file.name);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1536
  }
1537
 
1538
- function renderUploadedUI(filename) {
1539
- dropzone.innerHTML = `
1540
- <div class="file-capsule">
1541
- <div class="file-info">
1542
- <span>🎵</span>
1543
- <span style="overflow: hidden; text-overflow: ellipsis; max-width: 190px;">${filename}</span>
1544
- </div>
1545
- <button type="button" class="btn-clear" id="btn-clear-upload" title="Remove voice file">✕</button>
1546
- </div>
1547
- `;
1548
- document.getElementById("btn-clear-upload").addEventListener("click", (e) => {
1549
- e.stopPropagation();
1550
- clearUploadedFile();
1551
- });
 
 
 
 
 
 
 
 
 
 
1552
  }
1553
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1554
  function clearUploadedFile() {
 
1555
  selectedAudioFile = null;
1556
  selectedAudioFilename = "";
 
1557
  audioFileInput.value = "";
 
 
 
1558
  dropzone.innerHTML = `
1559
  <span class="upload-icon">📤</span>
1560
  <div class="upload-text">
@@ -1564,7 +1967,7 @@
1564
  `;
1565
  }
1566
 
1567
- // Fetch preloaded voices as File
1568
  async function loadExampleVoice(voicePath, originalFilename) {
1569
  try {
1570
  updateStatus("Buffering reference voice...", "info");
@@ -1575,7 +1978,7 @@
1575
  selectedAudioFile = new File([blob], originalFilename, { type: blob.type || "audio/mpeg" });
1576
  selectedAudioFilename = originalFilename;
1577
 
1578
- renderUploadedUI(originalFilename);
1579
  updateStatus("Reference voice mapped", "success", false);
1580
  setTimeout(hideStatus, 1500);
1581
  } catch (err) {
@@ -1584,6 +1987,90 @@
1584
  }
1585
  }
1586
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1587
  // Dynamically build Example Capsules
1588
  const examplesContainer = document.getElementById("examples-container");
1589
  EXAMPLES.forEach((ex) => {
@@ -1716,13 +2203,54 @@
1716
  const seed = parseInt(inputSeed.value);
1717
 
1718
  try {
 
 
 
 
 
 
 
1719
  btnGenerate.disabled = true;
1720
  btnGenerate.innerHTML = `<div class="alert-spinner"></div> Synthesizing...`;
1721
  updateStatus("Checking models & processing queues...", "info");
1722
 
1723
  let uploadedFileData = null;
1724
  if (selectedAudioFile) {
1725
- uploadedFileData = handle_file(selectedAudioFile);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1726
  }
1727
 
1728
  // Execute Gradio Client request
 
1024
  display: none; /* Only show toggle trigger on desktop */
1025
  }
1026
  }
1027
+
1028
+ /* ── Premium Audio Trimmer Widget ── */
1029
+ .audio-trimmer-container {
1030
+ display: flex;
1031
+ flex-direction: column;
1032
+ gap: 12px;
1033
+ background: rgba(255, 255, 255, 0.02);
1034
+ border: 1px solid rgba(60, 208, 162, 0.15);
1035
+ border-radius: var(--radius-md);
1036
+ padding: 14px;
1037
+ margin-top: 10px;
1038
+ transition: var(--transition);
1039
+ }
1040
+
1041
+ .trimmer-header {
1042
+ display: flex;
1043
+ justify-content: space-between;
1044
+ align-items: center;
1045
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
1046
+ padding-bottom: 8px;
1047
+ }
1048
+
1049
+ .trimmer-file-info {
1050
+ display: flex;
1051
+ align-items: center;
1052
+ gap: 6px;
1053
+ overflow: hidden;
1054
+ }
1055
+
1056
+ .trimmer-file-icon {
1057
+ font-size: 1.1rem;
1058
+ }
1059
+
1060
+ .trimmer-filename {
1061
+ font-size: 0.8rem;
1062
+ color: var(--text-primary);
1063
+ font-weight: 500;
1064
+ white-space: nowrap;
1065
+ overflow: hidden;
1066
+ text-overflow: ellipsis;
1067
+ max-width: 170px;
1068
+ }
1069
+
1070
+ .btn-clear-trimmer {
1071
+ background: transparent;
1072
+ border: none;
1073
+ color: var(--text-secondary);
1074
+ cursor: pointer;
1075
+ font-size: 0.9rem;
1076
+ padding: 4px;
1077
+ border-radius: 50%;
1078
+ display: flex;
1079
+ align-items: center;
1080
+ justify-content: center;
1081
+ transition: var(--transition);
1082
+ }
1083
+
1084
+ .btn-clear-trimmer:hover {
1085
+ color: #ef4444;
1086
+ background: rgba(239, 68, 68, 0.1);
1087
+ }
1088
+
1089
+ .trimmer-waveform-box {
1090
+ position: relative;
1091
+ width: 100%;
1092
+ height: 64px;
1093
+ background: rgba(0, 0, 0, 0.3);
1094
+ border: 1px solid rgba(255, 255, 255, 0.05);
1095
+ border-radius: var(--radius-sm);
1096
+ overflow: hidden;
1097
+ }
1098
+
1099
+ .trimmer-canvas {
1100
+ width: 100%;
1101
+ height: 100%;
1102
+ display: block;
1103
+ }
1104
+
1105
+ .trimmer-duration-info {
1106
+ display: flex;
1107
+ justify-content: space-between;
1108
+ align-items: center;
1109
+ font-size: 0.75rem;
1110
+ margin-bottom: -4px;
1111
+ }
1112
+
1113
+ .duration-title {
1114
+ color: var(--text-secondary);
1115
+ }
1116
+
1117
+ .duration-val {
1118
+ color: var(--accent-mint);
1119
+ font-family: monospace;
1120
+ font-weight: 700;
1121
+ }
1122
+
1123
+ .trim-slider-group {
1124
+ display: flex;
1125
+ flex-direction: column;
1126
+ gap: 4px;
1127
+ }
1128
+
1129
+ .trim-slider-meta {
1130
+ display: flex;
1131
+ justify-content: space-between;
1132
+ align-items: center;
1133
+ font-size: 0.75rem;
1134
+ color: var(--text-secondary);
1135
+ }
1136
+
1137
+ .trim-value-label {
1138
+ color: var(--accent-mint);
1139
+ font-family: monospace;
1140
+ font-weight: 600;
1141
+ }
1142
+
1143
+ .trimmer-controls {
1144
+ display: flex;
1145
+ gap: 8px;
1146
+ margin-top: 4px;
1147
+ }
1148
+
1149
+ .btn-trimmer-control {
1150
+ flex: 1;
1151
+ padding: 8px 12px;
1152
+ font-size: 0.75rem;
1153
+ font-weight: 600;
1154
+ border-radius: var(--radius-sm);
1155
+ cursor: pointer;
1156
+ transition: var(--transition);
1157
+ display: flex;
1158
+ align-items: center;
1159
+ justify-content: center;
1160
+ gap: 6px;
1161
+ }
1162
+
1163
+ .btn-trimmer-play {
1164
+ background: rgba(60, 208, 162, 0.1);
1165
+ border: 1px solid var(--accent-mint);
1166
+ color: var(--accent-mint);
1167
+ }
1168
+
1169
+ .btn-trimmer-play:hover {
1170
+ background: var(--accent-mint);
1171
+ color: #000;
1172
+ box-shadow: 0 0 10px var(--accent-mint-glow);
1173
+ }
1174
+
1175
+ .btn-trimmer-play.playing {
1176
+ background: #ef4444;
1177
+ border-color: #ef4444;
1178
+ color: #fff;
1179
+ box-shadow: 0 0 10px rgba(239, 68, 68, 0.4);
1180
+ }
1181
+
1182
+ .btn-trimmer-reset {
1183
+ background: rgba(255, 255, 255, 0.05);
1184
+ border: 1px solid rgba(255, 255, 255, 0.1);
1185
+ color: var(--text-secondary);
1186
+ }
1187
+
1188
+ .btn-trimmer-reset:hover {
1189
+ background: rgba(255, 255, 255, 0.1);
1190
+ color: var(--text-primary);
1191
+ }
1192
  </style>
1193
  </head>
1194
  <body>
 
1214
  </div>
1215
  </div>
1216
  <input type="file" id="audio-file" class="hidden-input" accept="audio/*">
1217
+
1218
+ <!-- Premium Audio Trimmer Widget -->
1219
+ <div id="audio-trimmer-container" class="audio-trimmer-container" style="display: none;">
1220
+ <div class="trimmer-header">
1221
+ <div class="trimmer-file-info">
1222
+ <span class="trimmer-file-icon">🎵</span>
1223
+ <span id="trimmer-filename" class="trimmer-filename">audio.wav</span>
1224
+ </div>
1225
+ <button type="button" class="btn-clear-trimmer" id="btn-clear-trimmer" title="Remove audio reference">✕</button>
1226
+ </div>
1227
+
1228
+ <div class="trimmer-waveform-box">
1229
+ <canvas id="trimmer-canvas" class="trimmer-canvas"></canvas>
1230
+ </div>
1231
+
1232
+ <div class="trimmer-duration-info">
1233
+ <span class="duration-title">Trimmed Duration</span>
1234
+ <span id="trimmer-duration-val" class="duration-val">0.0s / 0.0s</span>
1235
+ </div>
1236
+
1237
+ <!-- Range sliders -->
1238
+ <div class="trim-slider-group">
1239
+ <div class="trim-slider-meta">
1240
+ <span>Start Trim</span>
1241
+ <span class="trim-value-label" id="trim-start-val">0.0s</span>
1242
+ </div>
1243
+ <input type="range" id="trim-start" class="trim-range-input" min="0" max="100" step="0.1" value="0">
1244
+ </div>
1245
+
1246
+ <div class="trim-slider-group">
1247
+ <div class="trim-slider-meta">
1248
+ <span>End Trim</span>
1249
+ <span class="trim-value-label" id="trim-end-val">100.0s</span>
1250
+ </div>
1251
+ <input type="range" id="trim-end" class="trim-range-input" min="0" max="100" step="0.1" value="100">
1252
+ </div>
1253
+
1254
+ <div class="trimmer-controls">
1255
+ <button type="button" id="btn-trimmer-play" class="btn-trimmer-control btn-trimmer-play" title="Play trimmed section">▶ Play Trimmed</button>
1256
+ <button type="button" id="btn-trimmer-reset" class="btn-trimmer-control btn-trimmer-reset" title="Reset trim selection">🔄 Reset</button>
1257
+ </div>
1258
+ </div>
1259
  </div>
1260
 
1261
  <!-- Settings Dashboard Parameters -->
 
1587
  let selectedAudioFile = null;
1588
  let selectedAudioFilename = "";
1589
 
1590
+ // Trimmer state variables
1591
+ let trimmerAudioContext = null;
1592
+ let originalAudioBuffer = null;
1593
+ let trimmerAudioSource = null;
1594
+ let isTrimmerPlaying = false;
1595
+
1596
  // UI nodes
1597
  const sidebar = document.getElementById("app-sidebar");
1598
  const sidebarOverlay = document.getElementById("sidebar-overlay");
 
1634
  const playerDownload = document.getElementById("player-download");
1635
  const speedButtons = document.querySelectorAll(".btn-speed");
1636
 
1637
+ // Trimmer UI elements
1638
+ const trimmerContainer = document.getElementById("audio-trimmer-container");
1639
+ const trimmerFilename = document.getElementById("trimmer-filename");
1640
+ const btnClearTrimmer = document.getElementById("btn-clear-trimmer");
1641
+ const trimmerCanvas = document.getElementById("trimmer-canvas");
1642
+ const labelTrimmerDuration = document.getElementById("trimmer-duration-val");
1643
+ const sliderTrimStart = document.getElementById("trim-start");
1644
+ const valTrimStart = document.getElementById("trim-start-val");
1645
+ const sliderTrimEnd = document.getElementById("trim-end");
1646
+ const valTrimEnd = document.getElementById("trim-end-val");
1647
+ const btnTrimmerPlay = document.getElementById("btn-trimmer-play");
1648
+ const btnTrimmerReset = document.getElementById("btn-trimmer-reset");
1649
+
1650
  // Guide togglers
1651
  const guideToggle = document.getElementById("guide-toggle");
1652
  const guideBody = document.getElementById("guide-body");
 
1758
  function handleUploadedFile(file) {
1759
  selectedAudioFile = file;
1760
  selectedAudioFilename = file.name;
1761
+ loadAudioIntoTrimmer(file);
1762
+ }
1763
+
1764
+ // --- 16-bit PCM WAV Encoder Utility ---
1765
+ function bufferToWav(buffer) {
1766
+ let numOfChan = buffer.numberOfChannels,
1767
+ length = buffer.length * numOfChan * 2 + 44,
1768
+ bufferArr = new ArrayBuffer(length),
1769
+ view = new DataView(bufferArr),
1770
+ channels = [], i, sample,
1771
+ offset = 0,
1772
+ pos = 0;
1773
+
1774
+ function setUint16(data) {
1775
+ view.setUint16(pos, data, true);
1776
+ pos += 2;
1777
+ }
1778
+
1779
+ function setUint32(data) {
1780
+ view.setUint32(pos, data, true);
1781
+ pos += 4;
1782
+ }
1783
+
1784
+ setUint32(0x46464952); // "RIFF"
1785
+ setUint32(length - 8);
1786
+ setUint32(0x45564157); // "WAVE"
1787
+
1788
+ setUint32(0x20746d66); // "fmt "
1789
+ setUint32(16);
1790
+ setUint16(1);
1791
+ setUint16(numOfChan);
1792
+ setUint32(buffer.sampleRate);
1793
+ setUint32(buffer.sampleRate * 2 * numOfChan);
1794
+ setUint16(numOfChan * 2);
1795
+ setUint16(16);
1796
+
1797
+ setUint32(0x61746164); // "data"
1798
+ setUint32(length - pos - 4);
1799
+
1800
+ for (i = 0; i < numOfChan; i++) {
1801
+ channels.push(buffer.getChannelData(i));
1802
+ }
1803
+
1804
+ while (pos < length) {
1805
+ for (i = 0; i < numOfChan; i++) {
1806
+ sample = Math.max(-1, Math.min(1, channels[i][offset]));
1807
+ sample = (sample < 0 ? sample * 0x8000 : sample * 0x7FFF);
1808
+ view.setInt16(pos, sample, true);
1809
+ pos += 2;
1810
+ }
1811
+ offset++;
1812
+ }
1813
+
1814
+ return new Blob([bufferArr], { type: "audio/wav" });
1815
+ }
1816
+
1817
+ // --- Waveform Rendering ---
1818
+ function drawWaveform(buffer, canvas, startTime, endTime) {
1819
+ const ctx = canvas.getContext("2d");
1820
+ const width = canvas.width;
1821
+ const height = canvas.height;
1822
+ ctx.clearRect(0, 0, width, height);
1823
+
1824
+ const data = buffer.getChannelData(0);
1825
+ const step = Math.ceil(data.length / width);
1826
+ const amp = height / 2;
1827
+ const duration = buffer.duration;
1828
+ const startPercent = startTime / duration;
1829
+ const endPercent = endTime / duration;
1830
+
1831
+ for (let i = 0; i < width; i++) {
1832
+ let min = 1.0;
1833
+ let max = -1.0;
1834
+ for (let j = 0; j < step; j++) {
1835
+ const idx = i * step + j;
1836
+ if (idx >= data.length) break;
1837
+ const datum = data[idx];
1838
+ if (datum < min) min = datum;
1839
+ if (datum > max) max = datum;
1840
+ }
1841
+
1842
+ const x = i;
1843
+ const y = amp;
1844
+ const h = Math.max(2, (max - min) * amp * 0.85);
1845
+ const currentPercent = i / width;
1846
+ const isSelected = currentPercent >= startPercent && currentPercent <= endPercent;
1847
+
1848
+ if (isSelected) {
1849
+ ctx.fillStyle = "#3cd0a2"; // var(--accent-mint)
1850
+ } else {
1851
+ ctx.fillStyle = "rgba(255, 255, 255, 0.2)";
1852
+ }
1853
+
1854
+ ctx.fillRect(x, y - h / 2, 2, h);
1855
+ }
1856
  }
1857
 
1858
+ // --- Trimmer Web Audio Previewing ---
1859
+ function playTrimmedSlice(buffer, startTime, endTime, onEndedCallback) {
1860
+ if (!trimmerAudioContext) {
1861
+ trimmerAudioContext = new (window.AudioContext || window.webkitAudioContext)();
1862
+ }
1863
+
1864
+ stopTrimmerPlay();
1865
+
1866
+ if (trimmerAudioContext.state === "suspended") {
1867
+ trimmerAudioContext.resume();
1868
+ }
1869
+
1870
+ const duration = endTime - startTime;
1871
+ trimmerAudioSource = trimmerAudioContext.createBufferSource();
1872
+ trimmerAudioSource.buffer = buffer;
1873
+ trimmerAudioSource.connect(trimmerAudioContext.destination);
1874
+
1875
+ trimmerAudioSource.start(0, startTime, duration);
1876
+ isTrimmerPlaying = true;
1877
+
1878
+ trimmerAudioSource.onended = () => {
1879
+ isTrimmerPlaying = false;
1880
+ if (onEndedCallback) onEndedCallback();
1881
+ };
1882
  }
1883
 
1884
+ // --- Stop Preview Playback ---
1885
+ function stopTrimmerPlay() {
1886
+ if (trimmerAudioSource) {
1887
+ try {
1888
+ trimmerAudioSource.stop();
1889
+ } catch (e) {}
1890
+ trimmerAudioSource = null;
1891
+ }
1892
+ isTrimmerPlaying = false;
1893
+ }
1894
+
1895
+ // --- Ingest and Initialize Trimmer ---
1896
+ function loadAudioIntoTrimmer(file) {
1897
+ stopTrimmerPlay();
1898
+ btnTrimmerPlay.innerText = "▶ Play Trimmed";
1899
+ btnTrimmerPlay.classList.remove("playing");
1900
+
1901
+ const reader = new FileReader();
1902
+ reader.onload = async (e) => {
1903
+ const arrayBuffer = e.target.result;
1904
+ try {
1905
+ updateStatus("Decoding audio file for editor...", "info");
1906
+ if (!trimmerAudioContext) {
1907
+ trimmerAudioContext = new (window.AudioContext || window.webkitAudioContext)();
1908
+ }
1909
+
1910
+ originalAudioBuffer = await trimmerAudioContext.decodeAudioData(arrayBuffer);
1911
+ const duration = originalAudioBuffer.duration;
1912
+
1913
+ // Update UI views
1914
+ dropzone.style.display = "none";
1915
+ trimmerContainer.style.display = "flex";
1916
+ trimmerFilename.innerText = selectedAudioFilename;
1917
+
1918
+ // Configure ranges
1919
+ sliderTrimStart.min = 0;
1920
+ sliderTrimStart.max = duration;
1921
+ sliderTrimStart.step = 0.1;
1922
+ sliderTrimStart.value = 0;
1923
+
1924
+ sliderTrimEnd.min = 0;
1925
+ sliderTrimEnd.max = duration;
1926
+ sliderTrimEnd.step = 0.1;
1927
+ sliderTrimEnd.value = duration;
1928
+
1929
+ valTrimStart.innerText = "0.0s";
1930
+ valTrimEnd.innerText = duration.toFixed(1) + "s";
1931
+ labelTrimmerDuration.innerText = duration.toFixed(1) + "s / " + duration.toFixed(1) + "s";
1932
+
1933
+ // Draw waveform
1934
+ setTimeout(() => {
1935
+ trimmerCanvas.width = trimmerCanvas.clientWidth || 250;
1936
+ trimmerCanvas.height = trimmerCanvas.clientHeight || 64;
1937
+ drawWaveform(originalAudioBuffer, trimmerCanvas, 0, duration);
1938
+ }, 50);
1939
+
1940
+ updateStatus("Audio loaded successfully", "success", false);
1941
+ setTimeout(hideStatus, 1500);
1942
+
1943
+ } catch (err) {
1944
+ console.error(err);
1945
+ updateStatus("Failed to decode audio file for editing", "error", false);
1946
+ }
1947
+ };
1948
+ reader.readAsArrayBuffer(file);
1949
+ }
1950
+
1951
+ // --- Clear Loaded Audio State ---
1952
  function clearUploadedFile() {
1953
+ stopTrimmerPlay();
1954
  selectedAudioFile = null;
1955
  selectedAudioFilename = "";
1956
+ originalAudioBuffer = null;
1957
  audioFileInput.value = "";
1958
+
1959
+ trimmerContainer.style.display = "none";
1960
+ dropzone.style.display = "flex";
1961
  dropzone.innerHTML = `
1962
  <span class="upload-icon">📤</span>
1963
  <div class="upload-text">
 
1967
  `;
1968
  }
1969
 
1970
+ // --- Fetch and Buffer Example Voice Clips ---
1971
  async function loadExampleVoice(voicePath, originalFilename) {
1972
  try {
1973
  updateStatus("Buffering reference voice...", "info");
 
1978
  selectedAudioFile = new File([blob], originalFilename, { type: blob.type || "audio/mpeg" });
1979
  selectedAudioFilename = originalFilename;
1980
 
1981
+ loadAudioIntoTrimmer(selectedAudioFile);
1982
  updateStatus("Reference voice mapped", "success", false);
1983
  setTimeout(hideStatus, 1500);
1984
  } catch (err) {
 
1987
  }
1988
  }
1989
 
1990
+ // --- Trimmer Range Slider Event Handlers ---
1991
+ function handleTrimSliderChange() {
1992
+ if (!originalAudioBuffer) return;
1993
+
1994
+ let start = parseFloat(sliderTrimStart.value);
1995
+ let end = parseFloat(sliderTrimEnd.value);
1996
+ const duration = originalAudioBuffer.duration;
1997
+
1998
+ // Maintain at least 0.5s duration
1999
+ if (start >= end - 0.5) {
2000
+ start = Math.max(0, end - 0.5);
2001
+ sliderTrimStart.value = start;
2002
+ }
2003
+ if (end <= start + 0.5) {
2004
+ end = Math.min(duration, start + 0.5);
2005
+ sliderTrimEnd.value = end;
2006
+ }
2007
+
2008
+ valTrimStart.innerText = start.toFixed(1) + "s";
2009
+ valTrimEnd.innerText = end.toFixed(1) + "s";
2010
+
2011
+ const trimmedDur = end - start;
2012
+ labelTrimmerDuration.innerText = trimmedDur.toFixed(1) + "s / " + duration.toFixed(1) + "s";
2013
+
2014
+ drawWaveform(originalAudioBuffer, trimmerCanvas, start, end);
2015
+
2016
+ // Stop playing if they change selection
2017
+ if (isTrimmerPlaying) {
2018
+ stopTrimmerPlay();
2019
+ btnTrimmerPlay.innerText = "▶ Play Trimmed";
2020
+ btnTrimmerPlay.classList.remove("playing");
2021
+ }
2022
+ }
2023
+
2024
+ sliderTrimStart.addEventListener("input", handleTrimSliderChange);
2025
+ sliderTrimEnd.addEventListener("input", handleTrimSliderChange);
2026
+
2027
+ // --- Play Trimmed Trigger ---
2028
+ btnTrimmerPlay.addEventListener("click", () => {
2029
+ if (!originalAudioBuffer) return;
2030
+
2031
+ if (isTrimmerPlaying) {
2032
+ stopTrimmerPlay();
2033
+ btnTrimmerPlay.innerText = "▶ Play Trimmed";
2034
+ btnTrimmerPlay.classList.remove("playing");
2035
+ } else {
2036
+ btnTrimmerPlay.innerText = "⏸ Stop";
2037
+ btnTrimmerPlay.classList.add("playing");
2038
+
2039
+ const start = parseFloat(sliderTrimStart.value);
2040
+ const end = parseFloat(sliderTrimEnd.value);
2041
+
2042
+ playTrimmedSlice(originalAudioBuffer, start, end, () => {
2043
+ btnTrimmerPlay.innerText = "▶ Play Trimmed";
2044
+ btnTrimmerPlay.classList.remove("playing");
2045
+ });
2046
+ }
2047
+ });
2048
+
2049
+ // --- Reset Trim Trigger ---
2050
+ btnTrimmerReset.addEventListener("click", () => {
2051
+ if (!originalAudioBuffer) return;
2052
+
2053
+ stopTrimmerPlay();
2054
+ btnTrimmerPlay.innerText = "▶ Play Trimmed";
2055
+ btnTrimmerPlay.classList.remove("playing");
2056
+
2057
+ const duration = originalAudioBuffer.duration;
2058
+ sliderTrimStart.value = 0;
2059
+ sliderTrimEnd.value = duration;
2060
+
2061
+ valTrimStart.innerText = "0.0s";
2062
+ valTrimEnd.innerText = duration.toFixed(1) + "s";
2063
+ labelTrimmerDuration.innerText = duration.toFixed(1) + "s / " + duration.toFixed(1) + "s";
2064
+
2065
+ drawWaveform(originalAudioBuffer, trimmerCanvas, 0, duration);
2066
+ });
2067
+
2068
+ // --- Clear Trimmer Click ---
2069
+ btnClearTrimmer.addEventListener("click", (e) => {
2070
+ e.stopPropagation();
2071
+ clearUploadedFile();
2072
+ });
2073
+
2074
  // Dynamically build Example Capsules
2075
  const examplesContainer = document.getElementById("examples-container");
2076
  EXAMPLES.forEach((ex) => {
 
2203
  const seed = parseInt(inputSeed.value);
2204
 
2205
  try {
2206
+ // Stop preview audio if playing
2207
+ if (isTrimmerPlaying) {
2208
+ stopTrimmerPlay();
2209
+ btnTrimmerPlay.innerText = "▶ Play Trimmed";
2210
+ btnTrimmerPlay.classList.remove("playing");
2211
+ }
2212
+
2213
  btnGenerate.disabled = true;
2214
  btnGenerate.innerHTML = `<div class="alert-spinner"></div> Synthesizing...`;
2215
  updateStatus("Checking models & processing queues...", "info");
2216
 
2217
  let uploadedFileData = null;
2218
  if (selectedAudioFile) {
2219
+ let fileToUpload = selectedAudioFile;
2220
+
2221
+ // If audio has been trimmed, crop the buffer and serialize it to WAV
2222
+ if (originalAudioBuffer) {
2223
+ const start = parseFloat(sliderTrimStart.value);
2224
+ const end = parseFloat(sliderTrimEnd.value);
2225
+
2226
+ // Slicing buffer if trim range is smaller than full duration
2227
+ if (start > 0 || end < originalAudioBuffer.duration) {
2228
+ const sampleRate = originalAudioBuffer.sampleRate;
2229
+ const startSample = Math.floor(start * sampleRate);
2230
+ const endSample = Math.min(originalAudioBuffer.length, Math.floor(end * sampleRate));
2231
+ const trimLength = endSample - startSample;
2232
+
2233
+ if (trimLength > 0) {
2234
+ const trimmedBuffer = trimmerAudioContext.createBuffer(
2235
+ originalAudioBuffer.numberOfChannels,
2236
+ trimLength,
2237
+ sampleRate
2238
+ );
2239
+
2240
+ for (let channel = 0; channel < originalAudioBuffer.numberOfChannels; channel++) {
2241
+ const channelData = originalAudioBuffer.getChannelData(channel);
2242
+ const trimmedChannelData = trimmedBuffer.getChannelData(channel);
2243
+ trimmedChannelData.set(channelData.subarray(startSample, endSample));
2244
+ }
2245
+
2246
+ const wavBlob = bufferToWav(trimmedBuffer);
2247
+ const trimmedName = `trimmed_${selectedAudioFilename.replace(/\.[^/.]+$/, "")}.wav`;
2248
+ fileToUpload = new File([wavBlob], trimmedName, { type: "audio/wav" });
2249
+ }
2250
+ }
2251
+ }
2252
+
2253
+ uploadedFileData = handle_file(fileToUpload);
2254
  }
2255
 
2256
  // Execute Gradio Client request