generated from dellevin/template
11111
This commit is contained in:
566
flask-dev-api/templates/audio_slicer.html
Normal file
566
flask-dev-api/templates/audio_slicer.html
Normal file
@@ -0,0 +1,566 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='font-awesome.css') }}">
|
||||
<title>Audio Slicer 音频分割</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background: #f5f5f5; color: #333;
|
||||
height: 100vh; overflow: hidden; display: flex;
|
||||
}
|
||||
/* 左侧面板 */
|
||||
.left-panel {
|
||||
width: 460px; flex-shrink: 0;
|
||||
display: flex; flex-direction: column;
|
||||
border-right: 1px solid #e8e8e8;
|
||||
background: #fff; height: 100vh; overflow-y: auto;
|
||||
}
|
||||
.left-panel::-webkit-scrollbar { width: 4px; }
|
||||
.left-panel::-webkit-scrollbar-thumb { background: #ddd; border-radius: 2px; }
|
||||
.input-section { padding: 20px 24px 28px; }
|
||||
|
||||
.header { display: flex; align-items: center; margin-bottom: 2px; }
|
||||
.header h1 { font-size: 18px; font-weight: 700; color: #1a1a1a; letter-spacing: -0.01em; }
|
||||
.subtitle { color: #aaa; font-size: 12px; margin-bottom: 16px; }
|
||||
|
||||
.section-card {
|
||||
background: #fafafa; border: 1px solid #f0f0f0;
|
||||
border-radius: 10px; padding: 14px 16px; margin-bottom: 12px;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 11px; font-weight: 600; color: #999;
|
||||
text-transform: uppercase; letter-spacing: 0.06em;
|
||||
margin-bottom: 12px; display: flex; align-items: center; gap: 6px;
|
||||
}
|
||||
.section-title i { font-size: 11px; }
|
||||
|
||||
/* 上传区 */
|
||||
.upload-area {
|
||||
border: 2px dashed #e0e0e0; border-radius: 10px;
|
||||
padding: 28px 16px; text-align: center; cursor: pointer;
|
||||
transition: all 0.2s; background: #fff;
|
||||
}
|
||||
.upload-area:hover, .upload-area.dragover {
|
||||
border-color: #0078d4; background: #f0f7ff;
|
||||
}
|
||||
.upload-area i { font-size: 28px; color: #ccc; margin-bottom: 8px; display: block; }
|
||||
.upload-area .label { font-size: 13px; color: #999; }
|
||||
.upload-area .filename { font-size: 13px; color: #0078d4; font-weight: 600; margin-top: 4px; }
|
||||
.upload-area input[type="file"] { display: none; }
|
||||
|
||||
/* 音频信息 */
|
||||
.audio-info {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.info-item {
|
||||
background: #fff; border: 1px solid #f0f0f0; border-radius: 6px;
|
||||
padding: 8px 10px; font-size: 12px;
|
||||
}
|
||||
.info-item .info-label { color: #999; font-size: 11px; }
|
||||
.info-item .info-value { font-weight: 600; color: #333; margin-top: 2px; }
|
||||
|
||||
/* 参数 */
|
||||
.param-row {
|
||||
display: flex; align-items: center; gap: 10px; margin-bottom: 10px;
|
||||
}
|
||||
.param-row:last-child { margin-bottom: 0; }
|
||||
.param-label {
|
||||
font-size: 12px; color: #666; width: 100px; flex-shrink: 0;
|
||||
}
|
||||
.param-row input[type="range"] { flex: 1; accent-color: #0078d4; }
|
||||
.param-val {
|
||||
width: 60px; text-align: center; padding: 4px 6px;
|
||||
border: 1px solid #e0e0e0; border-radius: 4px;
|
||||
font-size: 12px; font-weight: 600; color: #333;
|
||||
}
|
||||
.param-unit { font-size: 11px; color: #999; width: 24px; }
|
||||
|
||||
/* 按钮 */
|
||||
.action-btn {
|
||||
width: 100%; padding: 10px 18px;
|
||||
border: none; border-radius: 8px;
|
||||
font-size: 14px; font-weight: 600; cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,120,212,0.2);
|
||||
}
|
||||
.action-btn:hover { transform: translateY(-1px); }
|
||||
.action-btn:active { transform: translateY(0); }
|
||||
.action-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
|
||||
.action-btn.analyze {
|
||||
background: linear-gradient(135deg, #0078d4, #005fa3); color: #fff;
|
||||
}
|
||||
.action-btn.slice {
|
||||
background: linear-gradient(135deg, #0a7a1a, #065f12); color: #fff;
|
||||
}
|
||||
.btn-row { display: flex; gap: 8px; margin-top: 12px; }
|
||||
.btn-row .action-btn { flex: 1; }
|
||||
|
||||
/* 右侧面板 */
|
||||
.right-panel {
|
||||
flex: 1; background: #fff;
|
||||
display: flex; flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
.right-header {
|
||||
padding: 20px 24px 16px;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
.right-header h2 { font-size: 15px; font-weight: 600; color: #333; }
|
||||
.right-body {
|
||||
flex: 1; overflow-y: auto; padding: 16px 24px 24px;
|
||||
}
|
||||
|
||||
/* 预览表格 */
|
||||
.preview-table {
|
||||
width: 100%; border-collapse: collapse; font-size: 13px;
|
||||
}
|
||||
.preview-table th {
|
||||
text-align: left; padding: 8px 12px; color: #999;
|
||||
font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;
|
||||
font-size: 11px; border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
.preview-table td {
|
||||
padding: 8px 12px; border-bottom: 1px solid #f0f0f0; color: #555;
|
||||
}
|
||||
.preview-table tr:hover td { background: #fafafa; }
|
||||
|
||||
/* 切片行 */
|
||||
.slice-row { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.slice-row:last-child { border-bottom: none; }
|
||||
.slice-idx { width: 36px; font-size: 13px; font-weight: 700; color: #999; text-align: center; }
|
||||
.slice-name { flex: 1; font-size: 13px; color: #333; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.slice-dur { font-size: 12px; color: #999; width: 60px; text-align: right; }
|
||||
.slice-actions { display: flex; gap: 6px; }
|
||||
.slice-btn {
|
||||
width: 30px; height: 30px; border: 1px solid #e0e0e0; border-radius: 6px;
|
||||
background: #fff; color: #666; cursor: pointer; display: flex;
|
||||
align-items: center; justify-content: center; font-size: 12px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.slice-btn:hover { background: #f0f0f0; color: #333; border-color: #ccc; }
|
||||
.slice-btn.play-btn:hover { color: #0078d4; border-color: #0078d4; }
|
||||
|
||||
/* 进度条 */
|
||||
.progress-bar {
|
||||
width: 100%; height: 6px; background: #f0f0f0; border-radius: 3px;
|
||||
overflow: hidden; margin: 12px 0;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%; background: linear-gradient(90deg, #0078d4, #0a7a1a);
|
||||
border-radius: 3px; transition: width 0.3s;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
justify-content: center; height: 100%; color: #ccc;
|
||||
}
|
||||
.empty-state i { font-size: 48px; margin-bottom: 16px; }
|
||||
.empty-state p { font-size: 14px; }
|
||||
|
||||
/* 顶部操作栏 */
|
||||
.top-actions {
|
||||
display: flex; gap: 8px; margin-bottom: 16px;
|
||||
}
|
||||
.top-btn {
|
||||
padding: 7px 14px; border: 1px solid #e0e0e0; border-radius: 6px;
|
||||
background: #fff; color: #666; font-size: 12px; font-weight: 600;
|
||||
cursor: pointer; display: flex; align-items: center; gap: 5px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.top-btn:hover { background: #f0f0f0; color: #333; border-color: #ccc; }
|
||||
.top-btn.primary { background: #0078d4; color: #fff; border-color: #0078d4; }
|
||||
.top-btn.primary:hover { background: #006cbd; }
|
||||
|
||||
.status-text { font-size: 12px; color: #999; margin: 8px 0; }
|
||||
|
||||
/* Toast */
|
||||
.toast-container { position: fixed; top: 20px; right: 20px; z-index: 200; display: flex; flex-direction: column; gap: 8px; }
|
||||
.toast {
|
||||
padding: 10px 16px; border-radius: 8px; font-size: 13px; font-weight: 500;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15); animation: slideIn 0.3s ease;
|
||||
max-width: 360px;
|
||||
}
|
||||
.toast.success { background: #e8f5e9; color: #2e7d32; border: 1px solid #c8e6c9; }
|
||||
.toast.error { background: #fce4ec; color: #c62828; border: 1px solid #f8bbd0; }
|
||||
.toast.info { background: #e3f2fd; color: #1565c0; border: 1px solid #bbdefb; }
|
||||
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body { flex-direction: column; height: auto; overflow: auto; }
|
||||
.left-panel { width: 100% !important; height: auto; border-right: none; border-bottom: 1px solid #e8e8e8; overflow-y: visible; }
|
||||
.right-panel { height: auto; min-height: 50vh; }
|
||||
.input-section { padding: 14px 16px 20px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
<div class="left-panel">
|
||||
<div class="input-section">
|
||||
<div class="header">
|
||||
<h1><i class="fas fa-scissors" style="color:#0078d4;font-size:16px;margin-right:6px;"></i>Audio Slicer</h1>
|
||||
</div>
|
||||
<p class="subtitle">基于静音检测的智能音频分割</p>
|
||||
|
||||
<!-- 上传 -->
|
||||
<div class="section-card">
|
||||
<div class="section-title"><i class="fas fa-file"></i> 上传音频</div>
|
||||
<div class="upload-area" id="uploadArea" onclick="document.getElementById('audioInput').click()">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
<div class="label" id="uploadLabel">点击或拖拽音频文件到此处</div>
|
||||
<div class="filename" id="uploadFilename" style="display:none;"></div>
|
||||
<input type="file" id="audioInput" accept=".wav,.flac,.ogg,.mp3,.aac,.m4a,.wma,.aiff,.opus">
|
||||
</div>
|
||||
<div class="audio-info" id="audioInfo" style="display:none;">
|
||||
<div class="info-item">
|
||||
<div class="info-label">采样率</div>
|
||||
<div class="info-value" id="infoSr">-</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">声道数</div>
|
||||
<div class="info-value" id="infoCh">-</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">时长</div>
|
||||
<div class="info-value" id="infoDur">-</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">文件名</div>
|
||||
<div class="info-value" id="infoName" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 参数 -->
|
||||
<div class="section-card">
|
||||
<div class="section-title">
|
||||
<i class="fas fa-wrench"></i> 分割参数
|
||||
<button class="slice-btn" style="margin-left:auto;" onclick="saveConfig()" title="保存参数"><i class="fas fa-check"></i></button>
|
||||
</div>
|
||||
<div class="param-row">
|
||||
<span class="param-label">静音阈值</span>
|
||||
<input type="range" id="pThreshold" min="-80" max="-10" value="{{ config.threshold }}" oninput="syncParam(this, 'vThreshold')">
|
||||
<input type="number" class="param-val" id="vThreshold" value="{{ config.threshold }}" onchange="syncSlider(this, 'pThreshold')">
|
||||
<span class="param-unit">dB</span>
|
||||
</div>
|
||||
<div class="param-row">
|
||||
<span class="param-label">最短片段</span>
|
||||
<input type="range" id="pMinLength" min="1000" max="30000" step="500" value="{{ config.min_length }}" oninput="syncParam(this, 'vMinLength')">
|
||||
<input type="number" class="param-val" id="vMinLength" value="{{ config.min_length }}" onchange="syncSlider(this, 'pMinLength')">
|
||||
<span class="param-unit">ms</span>
|
||||
</div>
|
||||
<div class="param-row">
|
||||
<span class="param-label">最短静音</span>
|
||||
<input type="range" id="pMinInterval" min="50" max="3000" step="50" value="{{ config.min_interval }}" oninput="syncParam(this, 'vMinInterval')">
|
||||
<input type="number" class="param-val" id="vMinInterval" value="{{ config.min_interval }}" onchange="syncSlider(this, 'pMinInterval')">
|
||||
<span class="param-unit">ms</span>
|
||||
</div>
|
||||
<div class="param-row">
|
||||
<span class="param-label">帧长</span>
|
||||
<input type="range" id="pHopSize" min="5" max="100" step="5" value="{{ config.hop_size }}" oninput="syncParam(this, 'vHopSize')">
|
||||
<input type="number" class="param-val" id="vHopSize" value="{{ config.hop_size }}" onchange="syncSlider(this, 'pHopSize')">
|
||||
<span class="param-unit">ms</span>
|
||||
</div>
|
||||
<div class="param-row">
|
||||
<span class="param-label">保留静音</span>
|
||||
<input type="range" id="pMaxSilKept" min="100" max="10000" step="100" value="{{ config.max_sil_kept }}" oninput="syncParam(this, 'vMaxSilKept')">
|
||||
<input type="number" class="param-val" id="vMaxSilKept" value="{{ config.max_sil_kept }}" onchange="syncSlider(this, 'pMaxSilKept')">
|
||||
<span class="param-unit">ms</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="btn-row">
|
||||
<button class="action-btn analyze" id="btnAnalyze" onclick="doAnalyze()" disabled>
|
||||
<i class="fas fa-search"></i> 分析
|
||||
</button>
|
||||
<button class="action-btn slice" id="btnSlice" onclick="doSlice()" disabled>
|
||||
<i class="fas fa-scissors"></i> 开始切割
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-panel">
|
||||
<div class="right-header">
|
||||
<h2 id="rightTitle"><i class="fas fa-check-circle" style="color:#0078d4;margin-right:6px;"></i>切片结果</h2>
|
||||
</div>
|
||||
<div class="right-body" id="rightBody">
|
||||
<div class="empty-state" id="emptyState">
|
||||
<i class="fas fa-scissors"></i>
|
||||
<p>上传音频文件后开始分析</p>
|
||||
</div>
|
||||
<div id="resultArea" style="display:none;">
|
||||
<div class="top-actions" id="topActions" style="display:none;">
|
||||
<button class="top-btn primary" onclick="downloadAll()">
|
||||
<i class="fas fa-folder-plus"></i> 打包下载全部
|
||||
</button>
|
||||
</div>
|
||||
<div class="status-text" id="statusText"></div>
|
||||
<div class="progress-bar" id="progressWrap" style="display:none;">
|
||||
<div class="progress-fill" id="progressFill" style="width:0%;"></div>
|
||||
</div>
|
||||
<div id="sliceList"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let taskId = null;
|
||||
let pollTimer = null;
|
||||
|
||||
function showMessage(text, type, duration) {
|
||||
type = type || 'info'; duration = duration || 3000;
|
||||
var t = document.createElement('div');
|
||||
t.className = 'toast ' + type;
|
||||
t.textContent = text;
|
||||
document.getElementById('toastContainer').appendChild(t);
|
||||
setTimeout(function() { t.style.opacity = '0'; t.style.transition = 'opacity 0.3s'; setTimeout(function(){ t.remove(); }, 300); }, duration);
|
||||
}
|
||||
|
||||
// 参数同步
|
||||
function syncParam(slider, valId) {
|
||||
document.getElementById(valId).value = slider.value;
|
||||
}
|
||||
function syncSlider(input, sliderId) {
|
||||
document.getElementById(sliderId).value = input.value;
|
||||
}
|
||||
|
||||
function getSettings() {
|
||||
return {
|
||||
threshold: parseInt(document.getElementById('vThreshold').value),
|
||||
min_length: parseInt(document.getElementById('vMinLength').value),
|
||||
min_interval: parseInt(document.getElementById('vMinInterval').value),
|
||||
hop_size: parseInt(document.getElementById('vHopSize').value),
|
||||
max_sil_kept: parseInt(document.getElementById('vMaxSilKept').value),
|
||||
};
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
try {
|
||||
var res = await fetch('/audio-slicer/config', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(getSettings())
|
||||
});
|
||||
var data = await res.json();
|
||||
if (data.success) showMessage('参数已保存', 'success');
|
||||
} catch (e) {
|
||||
showMessage('保存失败: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function formatDur(sec) {
|
||||
if (sec < 60) return sec.toFixed(1) + 's';
|
||||
var m = Math.floor(sec / 60);
|
||||
var s = (sec % 60).toFixed(1);
|
||||
return m + 'm ' + s + 's';
|
||||
}
|
||||
|
||||
// 上传区拖拽
|
||||
var uploadArea = document.getElementById('uploadArea');
|
||||
uploadArea.addEventListener('dragover', function(e) { e.preventDefault(); this.classList.add('dragover'); });
|
||||
uploadArea.addEventListener('dragleave', function() { this.classList.remove('dragover'); });
|
||||
uploadArea.addEventListener('drop', function(e) {
|
||||
e.preventDefault(); this.classList.remove('dragover');
|
||||
var files = e.dataTransfer.files;
|
||||
if (files.length > 0) uploadFile(files[0]);
|
||||
});
|
||||
document.getElementById('audioInput').addEventListener('change', function() {
|
||||
if (this.files.length > 0) uploadFile(this.files[0]);
|
||||
});
|
||||
|
||||
async function uploadFile(file) {
|
||||
var fd = new FormData();
|
||||
fd.append('audio', file);
|
||||
document.getElementById('uploadLabel').textContent = '上传中...';
|
||||
document.getElementById('uploadFilename').style.display = 'none';
|
||||
document.getElementById('btnAnalyze').disabled = true;
|
||||
document.getElementById('btnSlice').disabled = true;
|
||||
|
||||
try {
|
||||
var res = await fetch('/audio-slicer/upload', { method: 'POST', body: fd });
|
||||
var data = await res.json();
|
||||
if (!data.success) {
|
||||
showMessage(data.error, 'error');
|
||||
document.getElementById('uploadLabel').textContent = '点击或拖拽音频文件到此处';
|
||||
return;
|
||||
}
|
||||
taskId = data.task_id;
|
||||
var info = data.info;
|
||||
document.getElementById('uploadLabel').textContent = '已选择文件';
|
||||
document.getElementById('uploadFilename').textContent = data.filename;
|
||||
document.getElementById('uploadFilename').style.display = 'block';
|
||||
document.getElementById('audioInfo').style.display = 'grid';
|
||||
document.getElementById('infoSr').textContent = info.sample_rate + ' Hz';
|
||||
document.getElementById('infoCh').textContent = info.channels === 1 ? '单声道' : info.channels === 2 ? '立体声' : info.channels + '声道';
|
||||
document.getElementById('infoDur').textContent = formatDur(info.duration);
|
||||
document.getElementById('infoName').textContent = data.filename;
|
||||
document.getElementById('btnAnalyze').disabled = false;
|
||||
// 重置右侧面板
|
||||
document.getElementById('emptyState').style.display = 'flex';
|
||||
document.getElementById('resultArea').style.display = 'none';
|
||||
document.getElementById('sliceList').innerHTML = '';
|
||||
document.getElementById('topActions').style.display = 'none';
|
||||
} catch (e) {
|
||||
showMessage('上传失败: ' + e.message, 'error');
|
||||
document.getElementById('uploadLabel').textContent = '点击或拖拽音频文件到此处';
|
||||
}
|
||||
}
|
||||
|
||||
async function doAnalyze() {
|
||||
if (!taskId) return;
|
||||
var btn = document.getElementById('btnAnalyze');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-refresh"></i> 分析中...';
|
||||
document.getElementById('emptyState').style.display = 'none';
|
||||
document.getElementById('resultArea').style.display = 'block';
|
||||
document.getElementById('statusText').textContent = '正在分析音频...';
|
||||
document.getElementById('sliceList').innerHTML = '';
|
||||
|
||||
try {
|
||||
var params = Object.assign({ task_id: taskId }, getSettings());
|
||||
var res = await fetch('/audio-slicer/analyze', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(params)
|
||||
});
|
||||
var data = await res.json();
|
||||
if (!data.success) {
|
||||
showMessage(data.error, 'error');
|
||||
document.getElementById('statusText').textContent = '分析失败: ' + data.error;
|
||||
return;
|
||||
}
|
||||
document.getElementById('statusText').textContent = '分析完成: 检测到 ' + data.count + ' 个片段';
|
||||
renderPreview(data.preview, data.sample_rate);
|
||||
document.getElementById('btnSlice').disabled = false;
|
||||
} catch (e) {
|
||||
showMessage('分析失败: ' + e.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-search"></i> 分析';
|
||||
}
|
||||
}
|
||||
|
||||
function renderPreview(preview, sr) {
|
||||
var html = '<table class="preview-table"><thead><tr>'
|
||||
+ '<th>#</th><th>时长</th><th>采样数</th></tr></thead><tbody>';
|
||||
for (var i = 0; i < preview.length; i++) {
|
||||
var p = preview[i];
|
||||
html += '<tr><td>' + (i + 1) + '</td><td>' + formatDur(p.duration) + '</td><td>' + p.samples.toLocaleString() + '</td></tr>';
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
document.getElementById('sliceList').innerHTML = html;
|
||||
}
|
||||
|
||||
async function doSlice() {
|
||||
if (!taskId) return;
|
||||
var btn = document.getElementById('btnSlice');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-refresh"></i> 切割中...';
|
||||
document.getElementById('btnAnalyze').disabled = true;
|
||||
document.getElementById('progressWrap').style.display = 'block';
|
||||
document.getElementById('progressFill').style.width = '0%';
|
||||
document.getElementById('statusText').textContent = '正在切割...';
|
||||
document.getElementById('topActions').style.display = 'none';
|
||||
document.getElementById('sliceList').innerHTML = '';
|
||||
|
||||
try {
|
||||
var res = await fetch('/audio-slicer/slice', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ task_id: taskId })
|
||||
});
|
||||
var data = await res.json();
|
||||
if (!data.success) {
|
||||
showMessage(data.error, 'error');
|
||||
return;
|
||||
}
|
||||
// 开始轮询
|
||||
startPoll();
|
||||
} catch (e) {
|
||||
showMessage('请求失败: ' + e.message, 'error');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-scissors"></i> 开始切割';
|
||||
document.getElementById('btnAnalyze').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function startPoll() {
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
pollTimer = setInterval(async function() {
|
||||
try {
|
||||
var res = await fetch('/audio-slicer/status/' + taskId);
|
||||
var data = await res.json();
|
||||
if (!data.success) return;
|
||||
document.getElementById('progressFill').style.width = data.progress + '%';
|
||||
document.getElementById('statusText').textContent = '切割进度: ' + data.progress + '%';
|
||||
|
||||
if (data.status === 'done') {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
document.getElementById('statusText').textContent = '切割完成! 共 ' + data.count + ' 个片段';
|
||||
document.getElementById('progressWrap').style.display = 'none';
|
||||
renderSlices(data.slices);
|
||||
document.getElementById('topActions').style.display = 'flex';
|
||||
document.getElementById('btnSlice').disabled = false;
|
||||
document.getElementById('btnSlice').innerHTML = '<i class="fas fa-scissors"></i> 开始切割';
|
||||
document.getElementById('btnAnalyze').disabled = false;
|
||||
} else if (data.status === 'error') {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
document.getElementById('statusText').textContent = '切割失败: ' + data.error;
|
||||
document.getElementById('progressWrap').style.display = 'none';
|
||||
document.getElementById('btnSlice').disabled = false;
|
||||
document.getElementById('btnSlice').innerHTML = '<i class="fas fa-scissors"></i> 开始切割';
|
||||
document.getElementById('btnAnalyze').disabled = false;
|
||||
}
|
||||
} catch (e) {}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function renderSlices(slices) {
|
||||
var html = '';
|
||||
for (var i = 0; i < slices.length; i++) {
|
||||
var s = slices[i];
|
||||
html += '<div class="slice-row">'
|
||||
+ '<span class="slice-idx">' + (i + 1) + '</span>'
|
||||
+ '<span class="slice-name" title="' + s.filename + '">' + s.filename + '</span>'
|
||||
+ '<span class="slice-dur">' + formatDur(s.duration) + '</span>'
|
||||
+ '<div class="slice-actions">'
|
||||
+ '<button class="slice-btn play-btn" onclick="playSlice(' + i + ')" title="播放"><i class="fas fa-play"></i></button>'
|
||||
+ '<button class="slice-btn" onclick="downloadSlice(' + i + ')" title="下载"><i class="fas fa-file"></i></button>'
|
||||
+ '</div></div>';
|
||||
}
|
||||
document.getElementById('sliceList').innerHTML = html;
|
||||
}
|
||||
|
||||
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) {
|
||||
var a = document.createElement('a');
|
||||
a.href = '/audio-slicer/download/' + taskId + '/' + index;
|
||||
a.download = '';
|
||||
a.click();
|
||||
}
|
||||
|
||||
function downloadAll() {
|
||||
var a = document.createElement('a');
|
||||
a.href = '/audio-slicer/download-all/' + taskId;
|
||||
a.download = '';
|
||||
a.click();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user