This commit is contained in:
DelLevin-Home
2026-06-17 21:05:01 +08:00
parent 835e22b6dc
commit d94a9bad1b
13 changed files with 464 additions and 38 deletions

8
flask-dev-api/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

25
flask-dev-api/.idea/flask-dev-api.iml generated Normal file
View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="Flask">
<option name="enabled" value="true" />
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.12" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/templates" />
</list>
</option>
</component>
</module>

View File

@@ -0,0 +1,23 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="9">
<item index="0" class="java.lang.String" itemvalue="sentence_transformers" />
<item index="1" class="java.lang.String" itemvalue="fast_langdetect" />
<item index="2" class="java.lang.String" itemvalue="jieba_fast" />
<item index="3" class="java.lang.String" itemvalue="rotary_embedding_torch" />
<item index="4" class="java.lang.String" itemvalue="ko_pron" />
<item index="5" class="java.lang.String" itemvalue="pyopenjtalk" />
<item index="6" class="java.lang.String" itemvalue="g2p_en" />
<item index="7" class="java.lang.String" itemvalue="huggingface_hub" />
<item index="8" class="java.lang.String" itemvalue="x_transformers" />
</list>
</value>
</option>
</inspection_tool>
</profile>
</component>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
flask-dev-api/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
</project>

8
flask-dev-api/.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/flask-dev-api.iml" filepath="$PROJECT_DIR$/.idea/flask-dev-api.iml" />
</modules>
</component>
</project>

6
flask-dev-api/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

15
flask-dev-api/.idea/webResources.xml generated Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="WebResourcesPaths">
<contentEntries>
<entry url="file://$PROJECT_DIR$">
<entryData>
<resourceRoots>
<path value="file://$PROJECT_DIR$/config" />
<path value="file://$PROJECT_DIR$/static" />
</resourceRoots>
</entryData>
</entry>
</contentEntries>
</component>
</project>

View File

@@ -15,7 +15,7 @@ from blueprints import pin_tu_bp, base64_bp, down_video_bp, fen_ci_bp, content_t
from utils.stats_db import init_db, record_call, get_total_count, get_avg_duration, get_daily_counts, get_monthly_counts, get_available_years, get_available_months, get_daily_counts_by_month, get_hourly_counts, get_endpoint_stats, get_today_count, get_yesterday_count, get_month_count, get_success_rate, get_duration_distribution, get_slowest_endpoints, get_week_compare, get_recent_calls, get_active_days from utils.stats_db import init_db, record_call, get_total_count, get_avg_duration, get_daily_counts, get_monthly_counts, get_available_years, get_available_months, get_daily_counts_by_month, get_hourly_counts, get_endpoint_stats, get_today_count, get_yesterday_count, get_month_count, get_success_rate, get_duration_distribution, get_slowest_endpoints, get_week_compare, get_recent_calls, get_active_days
from utils.stats_db import ALLOWED_TABLES, get_table_info, get_table_rows, get_table_count, insert_row, update_row, delete_rows, clear_table from utils.stats_db import ALLOWED_TABLES, get_table_info, get_table_rows, get_table_count, insert_row, update_row, delete_rows, clear_table
TOOL_COUNT = 19 TOOL_COUNT = 18
def create_app(): def create_app():

View File

@@ -1,7 +1,7 @@
{ {
"threshold": -41, "threshold": -41,
"min_length": 5000, "min_length": 5000,
"min_interval": 300, "min_interval": 500,
"hop_size": 10, "hop_size": 10,
"max_sil_kept": 1000 "max_sil_kept": 1000
} }

View File

@@ -4,13 +4,13 @@
"model_dir_mode": "absolute", "model_dir_mode": "absolute",
"index_dir": "E:\\AI\\RVC\\rvc_con\\index", "index_dir": "E:\\AI\\RVC\\rvc_con\\index",
"index_dir_mode": "absolute", "index_dir_mode": "absolute",
"last_model": "", "last_model": "bubu-sing.pth",
"last_index": "", "last_index": "E:\\AI\\RVC\\rvc_con\\index\\bubu-sing_v2.index",
"f0_up_key": 0, "f0_up_key": 0,
"f0_method": "rmvpe", "f0_method": "rmvpe",
"index_rate": 0.75, "index_rate": 1,
"filter_radius": 3, "filter_radius": 3,
"resample_sr": 22050, "resample_sr": 32000,
"rms_mix_rate": 0.25, "rms_mix_rate": 0.5,
"protect": 0.33 "protect": 0.37
} }

Binary file not shown.

View File

@@ -280,6 +280,56 @@
position: absolute; top: 0; left: 0; width: 100%; height: 100%; position: absolute; top: 0; left: 0; width: 100%; height: 100%;
border-radius: 8px; pointer-events: none; border-radius: 8px; pointer-events: none;
} }
#segmentOverlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
border-radius: 8px; pointer-events: none; z-index: 1;
}
.seg-gray {
position: absolute; top: 0; height: 100%;
background: rgba(0,0,0,0.18); pointer-events: none;
}
#playCursor {
position: absolute; top: 0; height: 100%; width: 10px;
transform: translateX(-5px); z-index: 5;
pointer-events: auto; cursor: col-resize;
display: none;
}
#playCursor::after {
content: ''; position: absolute; left: 50%; top: 0; bottom: 0;
width: 2px; transform: translateX(-50%);
background: #e53935; box-shadow: 0 0 4px rgba(229,57,53,0.4);
pointer-events: none;
}
#cursorHandle {
position: absolute; left: 50%; top: -6px; transform: translateX(-50%);
width: 12px; height: 12px; border-radius: 50%;
background: #e53935; border: 2px solid #fff;
box-shadow: 0 1px 4px rgba(0,0,0,0.25);
pointer-events: none;
}
.player-controls {
display: flex; align-items: center; gap: 10px;
padding: 6px 12px; margin-top: 4px;
background: #fff; border: 1px solid #e0e0e0; border-radius: 8px;
font-size: 12px; color: #555;
}
.player-controls button {
width: 30px; height: 30px; border: none; border-radius: 50%;
background: #0078d4; color: #fff; cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: 12px; transition: background 0.15s;
}
.player-controls button:hover { background: #005fa3; }
.player-controls .p-time { font-variant-numeric: tabular-nums; min-width: 36px; text-align: center; }
.player-controls input[type="range"] {
flex: 1; height: 4px; -webkit-appearance: none; appearance: none;
background: #e0e0e0; border-radius: 2px; outline: none; cursor: pointer;
}
.player-controls input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 14px; height: 14px; border-radius: 50%;
background: #0078d4; cursor: pointer;
}
.cut-marker { .cut-marker {
position: absolute; top: 0; width: 24px; height: 100%; position: absolute; top: 0; width: 24px; height: 100%;
transform: translateX(-50%); z-index: 2; cursor: grab; transform: translateX(-50%); z-index: 2; cursor: grab;
@@ -431,6 +481,12 @@
</div> </div>
<div id="timelineArea" style="display:none;"> <div id="timelineArea" style="display:none;">
<div class="timeline-wrap" id="timelineBar"></div> <div class="timeline-wrap" id="timelineBar"></div>
<div class="player-controls" id="playerControls" style="display:none;">
<button onclick="playerToggle()" title="播放/暂停"><i id="playerIcon" class="fas fa-play"></i></button>
<span class="p-time" id="playerTime">0:00</span>
<input type="range" id="playerSeekBar" min="0" max="1000" value="0" step="1">
<span class="p-time" id="playerTotal">0:00</span>
</div>
<div id="redoWrap" style="display:none;text-align:center;margin-top:8px;"> <div id="redoWrap" style="display:none;text-align:center;margin-top:8px;">
<button class="action-btn slice" style="width:auto;padding:8px 20px;font-size:13px;" onclick="redoManualSlice()"> <button class="action-btn slice" style="width:auto;padding:8px 20px;font-size:13px;" onclick="redoManualSlice()">
<i class="fas fa-redo"></i> 切割点已更改,点击重新切割 <i class="fas fa-redo"></i> 切割点已更改,点击重新切割
@@ -466,6 +522,14 @@
let manualSegChanged = false; // 用户是否手动调整过勾选 let manualSegChanged = false; // 用户是否手动调整过勾选
let manualSegInit = false; // 新建预览时重置勾选 let manualSegInit = false; // 新建预览时重置勾选
// 播放器状态
var playerAudio = null;
var playerPlaying = false;
var _playerRAF = null;
var _playerStartSec = 0;
var _playerEndSec = 0;
var _playerMode = 'full'; // 'full' 全曲 | 'seg' 片段
function showMessage(text, type, duration) { function showMessage(text, type, duration) {
type = type || 'info'; duration = duration || 3000; type = type || 'info'; duration = duration || 3000;
var t = document.createElement('div'); var t = document.createElement('div');
@@ -648,14 +712,6 @@
document.getElementById('sliceList').innerHTML = html; document.getElementById('sliceList').innerHTML = html;
} }
function playPreviewSeg(index) {
if (!taskId || !window._previewRanges || !window._previewRanges[index]) return;
if (currentAudio) { currentAudio.pause(); currentAudio = null; }
var r = window._previewRanges[index];
var audio = new Audio('/audio-slicer/preview-range/' + taskId + '?start=' + r[0] + '&end=' + r[1]);
audio.play();
currentAudio = audio;
}
function toggleSegment(idx) { function toggleSegment(idx) {
var pos = selectedSegments.indexOf(idx); var pos = selectedSegments.indexOf(idx);
@@ -696,6 +752,7 @@
if (el) el.textContent = '已选 ' + selectedSegments.length + '/' + total; if (el) el.textContent = '已选 ' + selectedSegments.length + '/' + total;
var btnSlice = document.getElementById('btnSlice'); var btnSlice = document.getElementById('btnSlice');
if (btnSlice) btnSlice.disabled = selectedSegments.length === 0; if (btnSlice) btnSlice.disabled = selectedSegments.length === 0;
_updateSegmentOverlay();
} }
async function doSlice() { async function doSlice() {
@@ -790,13 +847,8 @@
document.getElementById('sliceList').innerHTML = html; document.getElementById('sliceList').innerHTML = html;
} }
var currentAudio = null; var currentAudio = null;
function playSlice(index) {
if (currentAudio) { currentAudio.pause(); currentAudio = null; }
var audio = new Audio('/audio-slicer/download/' + taskId + '/' + index);
audio.play();
currentAudio = audio;
}
function downloadSlice(index) { function downloadSlice(index) {
var a = document.createElement('a'); var a = document.createElement('a');
@@ -902,8 +954,6 @@
// 新预览时重置勾选 // 新预览时重置勾选
if (manualSegInit) { if (manualSegInit) {
manualSegInit = false; manualSegInit = false;
manualSegSelected = [];
manualSegChanged = false;
} }
var html = ''; var html = '';
@@ -918,8 +968,8 @@
} }
var boundaries = [0].concat(cutPoints).concat([audioDuration]); var boundaries = [0].concat(cutPoints).concat([audioDuration]);
// 确保勾选数组长度匹配 // 仅初始化时全选;用户已手动调整勾选则保留
if (manualSegSelected.length !== boundaries.length - 1) { if (manualSegSelected.length !== boundaries.length - 1 && !manualSegChanged) {
manualSegSelected = []; manualSegSelected = [];
for (var j = 0; j < boundaries.length - 1; j++) manualSegSelected.push(j); for (var j = 0; j < boundaries.length - 1; j++) manualSegSelected.push(j);
} }
@@ -965,13 +1015,6 @@
markCutPointsChanged(); markCutPointsChanged();
} }
var previewAudio = null;
function playPreview(start, end) {
if (!taskId) return;
if (previewAudio) { previewAudio.pause(); previewAudio = null; }
previewAudio = new Audio('/audio-slicer/preview-range/' + taskId + '?start=' + start + '&end=' + end);
previewAudio.play();
}
function toggleManualSeg(idx) { function toggleManualSeg(idx) {
var pos = manualSegSelected.indexOf(idx); var pos = manualSegSelected.indexOf(idx);
@@ -988,6 +1031,7 @@
if (row) row.className = 'seg-row' + (pos < 0 ? ' selected' : ' dimmed'); if (row) row.className = 'seg-row' + (pos < 0 ? ' selected' : ' dimmed');
updateManualSegCount(); updateManualSegCount();
updateManualBtn(); updateManualBtn();
_updateSegmentOverlay();
} }
function selectManualSegments() { function selectManualSegments() {
@@ -995,6 +1039,7 @@
for (var i = 0; i < cutPoints.length + 1; i++) manualSegSelected.push(i); for (var i = 0; i < cutPoints.length + 1; i++) manualSegSelected.push(i);
manualSegChanged = true; manualSegChanged = true;
renderCutList(); renderCutList();
_updateSegmentOverlay();
} }
@@ -1030,7 +1075,9 @@
bar.className = readOnly ? 'readonly' : ''; bar.className = readOnly ? 'readonly' : '';
var html = '<canvas id="waveformCanvas"></canvas>'; var html = '<canvas id="waveformCanvas"></canvas>'
+ '<div id="segmentOverlay"></div>'
+ '<div id="playCursor"><div id="cursorHandle"></div></div>';
// 时间刻度(在波形条上方) // 时间刻度(在波形条上方)
var step = Math.max(1, Math.ceil(audioDuration / 10)); var step = Math.max(1, Math.ceil(audioDuration / 10));
for (var t = 0; t <= audioDuration; t += step) { for (var t = 0; t <= audioDuration; t += step) {
@@ -1050,20 +1097,62 @@
// 绘制波形 // 绘制波形
if (waveformPeaks) drawWaveform(); if (waveformPeaks) drawWaveform();
_updateSegmentOverlay();
if (readOnly) return; // 只读模式不绑定交互事件 // 播放控制条
var pc = document.getElementById('playerControls');
if (pc) { pc.style.display = audioDuration > 0 ? '' : 'none'; }
var pt = document.getElementById('playerTotal');
if (pt) pt.textContent = formatTime(audioDuration);
_showCursorAtCurrent();
// 时间轴空白区域点击 → 添加切割点 // 绑定事件(只绑一次)
if (!bar._bound) { if (!bar._bound) {
bar._bound = true; bar._bound = true;
// 时间轴点击:只读模式 → seek手动模式 → 添加切割点
bar.addEventListener('click', function(e) { bar.addEventListener('click', function(e) {
if (bar._suppressClick) { bar._suppressClick = false; return; } if (bar._suppressClick) { bar._suppressClick = false; return; }
if (bar.classList.contains('readonly')) return; if (e.target.closest('.cut-marker') || e.target.closest('#playCursor')) return;
if (e.target.closest('.cut-marker')) return;
var rect = bar.getBoundingClientRect(); var rect = bar.getBoundingClientRect();
var sec = (e.clientX - rect.left) / rect.width * audioDuration; var sec = (e.clientX - rect.left) / rect.width * audioDuration;
if (bar.classList.contains('readonly')) {
playerSeekTo(sec);
return;
}
// 手动模式:不允许在灰色片段内添加切割点
var ranges = _getSegmentRanges();
var sel = _getSelectedSet();
if (ranges.length && sel.length) {
for (var i = 0; i < ranges.length; i++) {
if (sec >= ranges[i][0] && sec <= ranges[i][1]) {
if (sel.indexOf(i) < 0) return;
break;
}
}
}
if (sec > 0.5 && sec < audioDuration - 0.5) addCutPoint(sec); if (sec > 0.5 && sec < audioDuration - 0.5) addCutPoint(sec);
}); });
// 播放游标拖拽
_bindCursorDrag(bar);
// 进度条拖拽 seek
var seekBar = document.getElementById('playerSeekBar');
if (seekBar) {
seekBar.addEventListener('input', function() {
var sec = (parseFloat(this.value) / 1000) * audioDuration;
var cursor = document.getElementById('playCursor');
var timeEl = document.getElementById('playerTime');
if (cursor) { cursor.style.display = 'block'; cursor.style.left = (sec / audioDuration * 100).toFixed(2) + '%'; }
if (timeEl) timeEl.textContent = formatTime(sec);
if (playerAudio) {
var rel = sec - _playerStartSec;
var dur = _playerEndSec - _playerStartSec;
if (rel >= 0 && rel <= dur) playerAudio.currentTime = rel;
}
});
}
} }
// 为每个标记绑定拖动 // 为每个标记绑定拖动
@@ -1099,6 +1188,245 @@
} }
} }
// ── 片段灰色覆盖(取消选中的片段变灰) ──
function _getSegmentRanges() {
// 手动模式:从 cutPoints 计算
if (cutMode === 'manual') {
if (cutPoints.length > 0) {
var b = [0].concat(cutPoints.slice().sort(function(a,b){return a-b;})).concat([audioDuration]);
var ranges = [];
for (var i = 0; i < b.length - 1; i++) ranges.push([b[i], b[i+1]]);
return ranges;
}
return [];
}
// 自动模式:用预览区间
if (window._previewRanges && window._previewRanges.length) {
return window._previewRanges;
}
return [];
}
function _getSelectedSet() {
if (manualSegSelected && manualSegSelected.length > 0 && cutPoints.length > 0) {
return manualSegSelected;
}
return selectedSegments;
}
function _updateSegmentOverlay() {
var overlay = document.getElementById('segmentOverlay');
if (!overlay || audioDuration <= 0) return;
var ranges = _getSegmentRanges();
if (!ranges.length) { overlay.innerHTML = ''; return; }
var sel = _getSelectedSet();
var html = '';
for (var i = 0; i < ranges.length; i++) {
if (sel.indexOf(i) < 0) {
var left = (ranges[i][0] / audioDuration * 100).toFixed(2);
var width = ((ranges[i][1] - ranges[i][0]) / audioDuration * 100).toFixed(2);
html += '<div class="seg-gray" style="left:' + left + '%;width:' + width + '%;"></div>';
}
}
overlay.innerHTML = html;
}
// ── 播放器系统 ──
function _showCursorAtCurrent() {
var cursor = document.getElementById('playCursor');
if (!cursor) return;
if (playerAudio) {
var sec = _playerStartSec + playerAudio.currentTime;
cursor.style.left = (sec / audioDuration * 100).toFixed(2) + '%';
}
}
function _playerTick() {
var cursor = document.getElementById('playCursor');
var timeEl = document.getElementById('playerTime');
if (!playerAudio || !cursor) return;
var sec = _playerStartSec + playerAudio.currentTime;
cursor.style.left = (sec / audioDuration * 100).toFixed(2) + '%';
if (timeEl) timeEl.textContent = formatTime(sec);
var seekBar = document.getElementById('playerSeekBar');
if (seekBar && audioDuration > 0) seekBar.value = (sec / audioDuration * 1000).toFixed(0);
_playerRAF = requestAnimationFrame(_playerTick);
}
function playerStartTick() {
playerStopTick();
var cursor = document.getElementById('playCursor');
if (cursor) cursor.style.display = 'block';
_playerRAF = requestAnimationFrame(_playerTick);
}
function playerStopTick() {
if (_playerRAF) { cancelAnimationFrame(_playerRAF); _playerRAF = null; }
}
function playerSeekTo(sec) {
sec = Math.max(0, Math.min(audioDuration, sec));
var cursor = document.getElementById('playCursor');
var timeEl = document.getElementById('playerTime');
if (cursor) {
cursor.style.display = 'block';
cursor.style.left = (sec / audioDuration * 100).toFixed(2) + '%';
}
if (timeEl) timeEl.textContent = formatTime(sec);
if (playerAudio && !playerAudio.paused) {
var rel = sec - _playerStartSec;
if (rel >= 0 && rel <= _playerEndSec - _playerStartSec) {
playerAudio.currentTime = rel;
}
}
}
function playerToggle() {
if (playerPlaying) {
playerPause();
} else {
playerPlay();
}
}
function playerPlay() {
if (!taskId || audioDuration <= 0) return;
if (playerAudio && !playerAudio.paused) return;
// 恢复暂停的音频
if (playerAudio && playerAudio.paused && playerAudio.currentTime > 0 && playerAudio.duration > 0) {
playerAudio.play();
playerPlaying = true;
playerStartTick();
_updatePlayerIcon();
return;
}
// 新建全曲播放
_playerStopAll();
_playerMode = 'full';
_playerStartSec = 0;
_playerEndSec = audioDuration;
playerAudio = new Audio('/audio-slicer/preview-range/' + taskId + '?start=0&end=' + audioDuration);
playerAudio.addEventListener('ended', function() {
playerPlaying = false;
playerStopTick();
_updatePlayerIcon();
});
playerAudio.play();
playerPlaying = true;
playerStartTick();
_updatePlayerIcon();
}
function playerPause() {
if (playerAudio) playerAudio.pause();
playerPlaying = false;
playerStopTick();
_updatePlayerIcon();
}
function _playerStopAll() {
playerStopTick();
if (playerAudio) {
playerAudio.pause();
playerAudio.src = '';
playerAudio = null;
}
playerPlaying = false;
_updatePlayerIcon();
if (currentAudio) { currentAudio.pause(); currentAudio = null; }
}
function _updatePlayerIcon() {
var icon = document.getElementById('playerIcon');
if (icon) icon.className = playerPlaying ? 'fas fa-pause' : 'fas fa-play';
}
// 播放片段(自动模式列表按钮)
function playPreviewSeg(index) {
if (!taskId || !window._previewRanges || !window._previewRanges[index]) return;
_playerStopAll();
var r = window._previewRanges[index];
_playerMode = 'seg';
_playerStartSec = r[0];
_playerEndSec = r[1];
playerAudio = new Audio('/audio-slicer/preview-range/' + taskId + '?start=' + r[0] + '&end=' + r[1]);
playerAudio.addEventListener('ended', function() {
playerPlaying = false;
playerStopTick();
_updatePlayerIcon();
});
playerAudio.play();
playerPlaying = true;
playerStartTick();
_updatePlayerIcon();
}
// 播放片段(手动模式列表按钮)
function playPreview(start, end) {
if (!taskId) return;
_playerStopAll();
_playerMode = 'seg';
_playerStartSec = start;
_playerEndSec = end;
playerAudio = new Audio('/audio-slicer/preview-range/' + taskId + '?start=' + start + '&end=' + end);
playerAudio.addEventListener('ended', function() {
playerPlaying = false;
playerStopTick();
_updatePlayerIcon();
});
playerAudio.play();
playerPlaying = true;
playerStartTick();
_updatePlayerIcon();
}
// 播放已切片结果
function playSlice(index) {
_playerStopAll();
var audio = new Audio('/audio-slicer/download/' + taskId + '/' + index);
audio.play();
currentAudio = audio;
}
// 游标拖拽
function _bindCursorDrag(bar) {
var cursor = document.getElementById('playCursor');
if (!cursor) return;
var dragging = false;
cursor.addEventListener('pointerdown', function(e) {
e.preventDefault();
e.stopPropagation();
dragging = true;
cursor.setPointerCapture(e.pointerId);
});
cursor.addEventListener('pointermove', function(e) {
if (!dragging) return;
var rect = bar.getBoundingClientRect();
var sec = (e.clientX - rect.left) / rect.width * audioDuration;
sec = Math.max(0, Math.min(audioDuration, sec));
cursor.style.left = (sec / audioDuration * 100).toFixed(2) + '%';
var timeEl = document.getElementById('playerTime');
if (timeEl) timeEl.textContent = formatTime(sec);
// 同步 audio currentTime
if (playerAudio) {
var rel = sec - _playerStartSec;
var dur = _playerEndSec - _playerStartSec;
if (rel >= 0 && rel <= dur) {
playerAudio.currentTime = rel;
}
}
});
cursor.addEventListener('pointerup', function(e) {
if (!dragging) return;
dragging = false;
cursor.releasePointerCapture(e.pointerId);
bar._suppressClick = true;
});
}
async function loadWaveform() { async function loadWaveform() {
if (!taskId) return; if (!taskId) return;
try { try {