generated from dellevin/template
1262 lines
64 KiB
HTML
1262 lines
64 KiB
HTML
<!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>UVR 人声分离</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: 480px; 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; justify-content: space-between; margin-bottom: 2px; }
|
||
.header h1 { font-size: 18px; font-weight: 700; color: #1a1a1a; letter-spacing: -0.01em; }
|
||
.settings-icon {
|
||
width: 32px; height: 32px; border: none; background: transparent;
|
||
color: #999; font-size: 15px; cursor: pointer; border-radius: 8px;
|
||
display: flex; align-items: center; justify-content: center; transition: all 0.15s;
|
||
}
|
||
.settings-icon:hover { background: #f5f5f5; color: #333; }
|
||
.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; flex: 1; min-width: 0;
|
||
}
|
||
.card-pair { display: flex; gap: 8px; margin-bottom: 8px; }
|
||
.card-pair .section-card { margin-bottom: 0; }
|
||
.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; }
|
||
.section-title .save-btn {
|
||
margin-left: auto; padding: 3px 10px; border: 1px solid #e0e0e0;
|
||
border-radius: 5px; background: #fff; color: #666; font-size: 11px;
|
||
cursor: pointer; transition: all 0.15s;
|
||
}
|
||
.section-title .save-btn:hover { background: #f0f0f0; color: #333; }
|
||
|
||
.input-group { margin-bottom: 10px; position: relative; }
|
||
.input-group:last-child { margin-bottom: 0; }
|
||
.input-group label { display: block; font-size: 12px; font-weight: 500; margin-bottom: 4px; color: #555; }
|
||
.input-group .input-wrap { position: relative; }
|
||
.input-group .input-wrap i {
|
||
position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
|
||
color: #c0c0c0; font-size: 12px; pointer-events: none;
|
||
}
|
||
.input-group input, .input-group select {
|
||
width: 100%; padding: 7px 10px 7px 32px;
|
||
border: 1px solid #e0e0e0; border-radius: 6px;
|
||
background: #fff; color: #333; font-size: 13px;
|
||
outline: none; transition: all 0.15s ease;
|
||
}
|
||
.input-group input::placeholder { color: #c0c0c0; }
|
||
.input-group input:focus, .input-group select:focus {
|
||
border-color: #0078d4; box-shadow: 0 0 0 2px rgba(0,120,212,0.06);
|
||
}
|
||
.input-group.no-icon input, .input-group.no-icon select { padding-left: 10px; }
|
||
.path-row { display: flex; gap: 6px; align-items: flex-end; }
|
||
.path-row select { width: 90px; flex-shrink: 0; padding: 7px 6px; font-size: 12px; }
|
||
.path-row input { flex: 1; padding-left: 10px !important; }
|
||
|
||
/* 上传区 */
|
||
.upload-area {
|
||
border: 2px dashed #e0e0e0; border-radius: 10px;
|
||
padding: 24px 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: 24px; color: #ccc; margin-bottom: 6px; 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; }
|
||
|
||
/* 参数 */
|
||
.slider-group { margin-bottom: 10px; }
|
||
.slider-group:last-child { margin-bottom: 0; }
|
||
.slider-label {
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
font-size: 12px; font-weight: 500; margin-bottom: 4px; color: #555;
|
||
}
|
||
.slider-value { font-weight: 600; color: #0078d4; font-size: 12px; }
|
||
input[type="range"] {
|
||
width: 100%; height: 4px; -webkit-appearance: none; appearance: none;
|
||
background: #e0e0e0; border-radius: 2px; outline: none;
|
||
}
|
||
input[type="range"]::-webkit-slider-thumb {
|
||
-webkit-appearance: none; width: 14px; height: 14px;
|
||
background: #0078d4; border-radius: 50%; cursor: pointer;
|
||
}
|
||
.param-desc { font-size: 11px; color: #aaa; margin-top: 2px; line-height: 1.4; }
|
||
|
||
/* 复选框 */
|
||
.checkbox-group {
|
||
display: flex; align-items: center; gap: 10px;
|
||
margin-bottom: 10px; font-size: 12px; color: #555; cursor: pointer;
|
||
padding: 8px 10px; background: #fff; border: 1px solid #e9ecef;
|
||
border-radius: 6px; transition: all 0.15s ease;
|
||
}
|
||
.checkbox-group:hover { background: #f8f9fa; border-color: #dee2e6; }
|
||
.checkbox-group:last-child { margin-bottom: 0; }
|
||
.checkbox-group input[type="checkbox"] {
|
||
width: 18px; height: 18px; accent-color: #0078d4; cursor: pointer;
|
||
}
|
||
.checkbox-group label { cursor: pointer; flex: 1; }
|
||
|
||
/* 架构切换 */
|
||
.arch-tabs {
|
||
display: flex; gap: 4px; margin-bottom: 12px;
|
||
}
|
||
.arch-tab {
|
||
flex: 1; padding: 8px 0; text-align: center;
|
||
border: 1px solid #e0e0e0; border-radius: 6px;
|
||
font-size: 12px; font-weight: 600; color: #999;
|
||
cursor: pointer; transition: all 0.15s;
|
||
background: #fff;
|
||
}
|
||
.arch-tab.active {
|
||
border-color: #0078d4; color: #0078d4;
|
||
background: #f0f7ff;
|
||
}
|
||
.arch-tab:hover:not(.active) { background: #fafafa; color: #666; }
|
||
|
||
/* 架构参数区域 */
|
||
.arch-params {
|
||
margin-top: 12px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid #e9ecef;
|
||
}
|
||
|
||
.arch-params-label {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: #0078d4;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
margin-bottom: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.arch-params-label i { font-size: 10px; }
|
||
|
||
.arch-params .input-group {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.arch-params .input-group:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.arch-params .param-desc {
|
||
font-size: 11px;
|
||
color: #999;
|
||
margin-top: 4px;
|
||
margin-bottom: 10px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
/* 按钮 */
|
||
.btn {
|
||
padding: 7px 14px; border: none; border-radius: 6px; font-size: 12px;
|
||
font-weight: 600; cursor: pointer; transition: all 0.12s ease;
|
||
display: inline-flex; align-items: center; gap: 5px;
|
||
}
|
||
.btn-primary { background: #0078d4; color: #fff; }
|
||
.btn-primary:hover:not(:disabled) { background: #006cbd; }
|
||
.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }
|
||
.btn-outline { background: #fff; color: #666; border: 1px solid #e0e0e0; }
|
||
.btn-outline:hover { background: #f5f5f5; color: #333; }
|
||
.btn-sm { padding: 5px 10px; font-size: 11px; }
|
||
.btn-block { width: 100%; justify-content: center; }
|
||
|
||
.action-btn {
|
||
width: 100%; padding: 10px 18px;
|
||
background: linear-gradient(135deg, #0078d4, #005fa3);
|
||
color: #fff; 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;
|
||
margin-top: 14px; box-shadow: 0 2px 8px rgba(0,120,212,0.2);
|
||
}
|
||
.action-btn:hover:not(:disabled) {
|
||
background: linear-gradient(135deg, #006cbd, #004e8a);
|
||
box-shadow: 0 4px 12px rgba(0,120,212,0.3); transform: translateY(-1px);
|
||
}
|
||
.action-btn:active:not(:disabled) { transform: translateY(0); }
|
||
.action-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
|
||
|
||
/* 右侧面板 */
|
||
.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;
|
||
}
|
||
|
||
/* 进度条 */
|
||
.progress-bar {
|
||
width: 100%; height: 8px; background: #f0f0f0; border-radius: 4px;
|
||
overflow: hidden; margin: 12px 0;
|
||
}
|
||
.progress-fill {
|
||
height: 100%; background: linear-gradient(90deg, #0078d4, #0a7a1a);
|
||
border-radius: 4px; transition: width 0.3s;
|
||
}
|
||
.status-text { font-size: 12px; color: #999; margin: 8px 0; }
|
||
|
||
/* 空状态 */
|
||
.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; }
|
||
|
||
/* 音轨行 */
|
||
.stem-row {
|
||
display: flex; align-items: center; gap: 12px;
|
||
padding: 12px 14px; border: 1px solid #f0f0f0;
|
||
border-radius: 8px; margin-bottom: 8px; background: #fafafa;
|
||
}
|
||
.stem-icon {
|
||
width: 36px; height: 36px; border-radius: 8px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 16px; color: #fff;
|
||
}
|
||
.stem-icon.vocals { background: linear-gradient(135deg, #0078d4, #005fa3); }
|
||
.stem-icon.instrumental { background: linear-gradient(135deg, #0a7a1a, #065f12); }
|
||
.stem-icon.other { background: linear-gradient(135deg, #f57c00, #e65100); }
|
||
.stem-info { flex: 1; }
|
||
.stem-name { font-size: 14px; font-weight: 600; color: #333; }
|
||
.stem-filename { font-size: 11px; color: #999; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.stem-actions { display: flex; gap: 6px; }
|
||
.stem-btn {
|
||
width: 32px; height: 32px; border: 1px solid #e0e0e0; border-radius: 6px;
|
||
background: #fff; color: #666; cursor: pointer; display: flex;
|
||
align-items: center; justify-content: center; font-size: 13px;
|
||
transition: all 0.15s;
|
||
}
|
||
.stem-btn:hover { background: #f0f0f0; color: #333; border-color: #ccc; }
|
||
.stem-btn.play-btn:hover { color: #0078d4; border-color: #0078d4; }
|
||
|
||
/* 顶部操作栏 */
|
||
.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; }
|
||
|
||
/* 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; }
|
||
}
|
||
|
||
/* 设置弹窗 */
|
||
.modal-overlay {
|
||
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||
background: rgba(0,0,0,0.4); z-index: 100;
|
||
justify-content: center; align-items: flex-start; padding-top: 80px;
|
||
}
|
||
.modal-overlay.show { display: flex; }
|
||
.settings-modal {
|
||
background: #fff; border-radius: 12px; width: 440px;
|
||
box-shadow: 0 12px 40px rgba(0,0,0,0.2); overflow: hidden;
|
||
}
|
||
.modal-header {
|
||
padding: 16px 20px; border-bottom: 1px solid #eee;
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
}
|
||
.modal-header h3 { font-size: 15px; font-weight: 600; color: #333; }
|
||
.modal-close {
|
||
width: 28px; height: 28px; border: none; background: #f5f5f5;
|
||
border-radius: 6px; cursor: pointer; font-size: 14px; color: #999;
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.modal-close:hover { background: #eee; color: #333; }
|
||
.modal-body { padding: 20px; }
|
||
.modal-footer {
|
||
padding: 12px 20px; border-top: 1px solid #eee;
|
||
display: flex; justify-content: flex-end; gap: 8px;
|
||
}
|
||
.modal-save-btn {
|
||
padding: 7px 20px; font-size: 12px; border: none; border-radius: 6px;
|
||
background: #0078d4; color: #fff; cursor: pointer; font-weight: 600;
|
||
transition: all 0.15s;
|
||
}
|
||
.modal-save-btn:hover { background: #006cbd; }
|
||
</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-microphone" style="color:#0078d4;font-size:16px;margin-right:6px;"></i>UVR 人声分离</h1>
|
||
<button class="settings-icon" onclick="openSettings()"><i class="fas fa-wrench"></i></button>
|
||
</div>
|
||
<p class="subtitle">基于 Ultimate Vocal Remover 的音频源分离</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>
|
||
<input type="file" id="audioInput" accept=".wav,.flac,.ogg,.mp3,.aac,.m4a,.wma,.aiff,.opus">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模型 & 参数(合并卡片) -->
|
||
<div class="section-card">
|
||
<div class="section-title"><i class="fas fa-wrench"></i> 模型 & 参数
|
||
<button class="save-btn" onclick="saveConfig()"><i class="fas fa-check"></i> 保存</button>
|
||
</div>
|
||
|
||
<div class="input-group">
|
||
<label>选择架构</label>
|
||
<select id="modelArchSelect" onchange="switchArch(this)" style="width:100%;padding:8px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:13px;background:#fff;color:#333;outline:none;transition:all 0.15s ease;">
|
||
<option value="MDX-Net">MDX-Net</option>
|
||
<option value="VR Arc">VR Arc</option>
|
||
<option value="Demucs">Demucs</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="input-group">
|
||
<label>选择模型</label>
|
||
<select id="modelSelect" style="width:100%;padding:8px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:13px;background:#fff;color:#333;outline:none;transition:all 0.15s ease;">
|
||
<option value="">-- 请先扫描模型目录 --</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div id="selectedModelInfo" style="display: none; margin-top: 10px; padding: 10px; background: #f0f7ff; border: 1px solid #b3d4fc; border-radius: 6px; font-size: 12px;">
|
||
<div style="display:flex;align-items:center;gap:6px;">
|
||
<i class="fas fa-check-circle" style="color:#2e7d32;font-size:14px;"></i>
|
||
<span id="selectedModelName" style="font-weight:600;color:#1a1a1a;"></span>
|
||
</div>
|
||
<div id="selectedModelPath" style="color:#666;font-size:11px;margin-top:4px;padding-left:20px;word-break:break-all;"></div>
|
||
</div>
|
||
|
||
<!-- 架构参数区域 -->
|
||
<div id="archParamsContainer">
|
||
<!-- Demucs 参数 -->
|
||
<div id="demucsParams" class="arch-params">
|
||
<div class="arch-params-label"><i class="fas fa-cog"></i> Demucs 参数</div>
|
||
<div class="input-group">
|
||
<label>Choose Stem(选择音轨)</label>
|
||
<select id="demucsStemSelect" onchange="updateStemOnlyState()" style="width:100%;padding:7px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:13px;background:#fff;color:#333;outline:none;">
|
||
<option value="All Stems">All Stems(所有音轨)</option>
|
||
<option value="Vocals">Vocals(人声)</option>
|
||
<option value="Bass">Bass(贝斯)</option>
|
||
<option value="Drums">Drums(鼓点)</option>
|
||
<option value="Other">Other(其他)</option>
|
||
</select>
|
||
<div class="param-desc">全部分轨 - 提取所有可用分分轨</div>
|
||
</div>
|
||
<div class="input-group">
|
||
<label>Segment(分割参数大小)</label>
|
||
<input type="text" id="demucsSegment" placeholder="如 Default、1、2、4"
|
||
list="demucsSegmentOptions"
|
||
style="width:100%;padding:7px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:13px;background:#fff;color:#333;outline:none;">
|
||
<datalist id="demucsSegmentOptions">
|
||
<option value="Default">
|
||
<option value="1"><option value="2"><option value="3">
|
||
<option value="4"><option value="5"><option value="6">
|
||
<option value="7"><option value="8"><option value="10">
|
||
<option value="12"><option value="15"><option value="20">
|
||
<option value="30"><option value="40"><option value="50">
|
||
</datalist>
|
||
<div class="param-desc">较小的尺寸占用资源较少。较大的尺寸效果可能更好。</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MDX-Net 参数 -->
|
||
<div id="mdxParams" class="arch-params" style="display: none;">
|
||
<div class="arch-params-label"><i class="fas fa-cog"></i> MDX-Net 参数</div>
|
||
<div class="input-group">
|
||
<label>Segment(分割参数大小)</label>
|
||
<input type="text" id="mdxSegmentSize" placeholder="如 Default、256、512、1024"
|
||
list="mdxSegmentOptions"
|
||
style="width:100%;padding:7px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:13px;background:#fff;color:#333;outline:none;">
|
||
<datalist id="mdxSegmentOptions">
|
||
<option value="Default">
|
||
<option value="128"><option value="256"><option value="512">
|
||
<option value="1024"><option value="2048"><option value="4096">
|
||
</datalist>
|
||
<div class="param-desc">默认大小为 256。音质可能会因你的选择而有所不同。</div>
|
||
</div>
|
||
<div class="input-group">
|
||
<label>Overlap(重叠度)</label>
|
||
<input type="text" id="mdxOverlap" placeholder="如 0.250(范围 0.10 ~ 0.99)"
|
||
list="mdxOverlapOptions"
|
||
style="width:100%;padding:7px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:13px;background:#fff;color:#333;outline:none;">
|
||
<datalist id="mdxOverlapOptions">
|
||
<option value="0.100"><option value="0.250"><option value="0.500"><option value="0.750"><option value="0.990">
|
||
</datalist>
|
||
<div class="param-desc">较高的值可能会带来更好的效果,但也会导致处理时间延长。</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- VR Arc 参数 -->
|
||
<div id="vrParams" class="arch-params" style="display: none;">
|
||
<div class="arch-params-label"><i class="fas fa-cog"></i> VR Arc 参数</div>
|
||
<div class="input-group">
|
||
<label>Window Size(窗口大小)</label>
|
||
<input type="text" id="vrWindowSize" placeholder="如 1024、512、320"
|
||
list="vrWindowSizeOptions"
|
||
style="width:100%;padding:7px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:13px;background:#fff;color:#333;outline:none;">
|
||
<datalist id="vrWindowSizeOptions">
|
||
<option value="1024"><option value="512"><option value="320">
|
||
</datalist>
|
||
<div class="param-desc">选择窗口大小,以平衡音质与处理速度。</div>
|
||
</div>
|
||
<div class="input-group">
|
||
<label>Aggression(分离强度)</label>
|
||
<input type="text" id="vrAggression" placeholder="如 5(范围 -100 ~ 100)"
|
||
list="vrAggressionOptions"
|
||
style="width:100%;padding:7px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:13px;background:#fff;color:#333;outline:none;">
|
||
<datalist id="vrAggressionOptions">
|
||
<option value="5"><option value="10"><option value="0"><option value="-5"><option value="100"><option value="-100">
|
||
</datalist>
|
||
<div class="param-desc">数值越大,提取越深入。对于人声和伴奏,通常设置为 5。</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 通用参数 -->
|
||
<div class="section-card">
|
||
<div class="section-title"><i class="fas fa-th"></i> 通用参数
|
||
<button class="save-btn" onclick="saveConfig()"><i class="fas fa-check"></i> 保存</button>
|
||
</div>
|
||
|
||
<div style="display:flex;flex-direction:column;gap:8px;">
|
||
<label style="display:flex;align-items:center;gap:8px;font-size:13px;color:#333;cursor:pointer;">
|
||
<input type="checkbox" id="isGpu" {{ 'checked' if config.is_gpu else '' }}
|
||
style="width:16px;height:16px;accent-color:#0078d4;cursor:pointer;">
|
||
<span>使用 GPU 加速</span>
|
||
<span id="gpuStatus" style="font-size:11px;color:#999;margin-left:auto;"></span>
|
||
</label>
|
||
|
||
<label style="display:flex;align-items:center;gap:8px;font-size:13px;color:#333;cursor:pointer;">
|
||
<input type="checkbox" id="isNormalize" {{ 'checked' if config.is_normalization else '' }}
|
||
style="width:16px;height:16px;accent-color:#0078d4;cursor:pointer;">
|
||
<span>音频归一化</span>
|
||
</label>
|
||
|
||
<label id="primaryOnlyRow" style="display:flex;align-items:center;gap:8px;font-size:13px;color:#333;cursor:pointer;">
|
||
<input type="checkbox" id="isPrimaryOnly" onchange="if(this.checked)document.getElementById('isSecondaryOnly').checked=false"
|
||
style="width:16px;height:16px;accent-color:#0078d4;cursor:pointer;">
|
||
<span>仅输出主音轨</span>
|
||
</label>
|
||
|
||
<label id="secondaryOnlyRow" style="display:flex;align-items:center;gap:8px;font-size:13px;color:#333;cursor:pointer;">
|
||
<input type="checkbox" id="isSecondaryOnly" onchange="if(this.checked)document.getElementById('isPrimaryOnly').checked=false"
|
||
style="width:16px;height:16px;accent-color:#0078d4;cursor:pointer;">
|
||
<span>仅输出副音轨</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="input-group" style="margin-top: 12px;">
|
||
<label>输出格式</label>
|
||
<select id="saveFormat" style="width:100%;padding:7px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:13px;background:#fff;color:#333;outline:none;">
|
||
<option value="wav">WAV</option>
|
||
<option value="flac">FLAC</option>
|
||
<option value="mp3">MP3</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 开始分离 -->
|
||
<button class="action-btn" id="startBtn" onclick="startSeparate()" disabled>
|
||
<i class="fas fa-play"></i> 开始分离
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:结果面板 -->
|
||
<div class="right-panel">
|
||
<div class="right-header">
|
||
<h2><i class="fas fa-list" style="margin-right:6px;color:#999;"></i>分离结果</h2>
|
||
</div>
|
||
<div class="right-body" id="rightBody">
|
||
<div class="empty-state" id="emptyState">
|
||
<i class="fas fa-exchange-alt"></i>
|
||
<p>上传音频并选择模型后开始分离</p>
|
||
</div>
|
||
<div id="progressSection" style="display: none;">
|
||
<div class="status-text" id="statusText">正在处理...</div>
|
||
<div class="progress-bar"><div class="progress-fill" id="progressFill" style="width:0%"></div></div>
|
||
</div>
|
||
<div id="resultSection" style="display: none;">
|
||
<div class="top-actions">
|
||
<button class="top-btn primary" onclick="downloadAll()"><i class="fas fa-folder-plus"></i> 打包下载</button>
|
||
<button class="top-btn" onclick="cleanupTask()"><i class="fas fa-trash"></i> 清理</button>
|
||
</div>
|
||
<div id="stemsList"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 设置弹窗 -->
|
||
<div class="modal-overlay" id="settingsModal">
|
||
<div class="settings-modal">
|
||
<div class="modal-header">
|
||
<h3><i class="fas fa-wrench" style="margin-right:6px;color:#999;"></i>UVR 设置</h3>
|
||
<button class="modal-close" onclick="closeSettings()"><i class="fas fa-times-circle"></i></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="input-group no-icon">
|
||
<label>项目路径</label>
|
||
<input type="text" id="uvrPath" placeholder="UVR 项目路径" value="{{ config.uvr_project_path|default('') }}">
|
||
</div>
|
||
<div style="border-top:1px solid #eee;margin:16px 0 12px;padding-top:12px;">
|
||
<label style="font-size:11px;font-weight:600;color:#999;text-transform:uppercase;letter-spacing:0.06em;display:block;margin-bottom:10px;">模型目录</label>
|
||
</div>
|
||
<div class="input-group">
|
||
<label>Demucs 模型目录</label>
|
||
<div class="path-row">
|
||
<select id="demucsModelDirMode">
|
||
<option value="absolute">绝对路径</option>
|
||
<option value="relative">相对路径</option>
|
||
</select>
|
||
<input type="text" id="demucsModelDir" placeholder="含 Demucs 模型的文件夹路径">
|
||
</div>
|
||
</div>
|
||
<div class="input-group">
|
||
<label>VR Arc 模型目录</label>
|
||
<div class="path-row">
|
||
<select id="vrModelDirMode">
|
||
<option value="absolute">绝对路径</option>
|
||
<option value="relative">相对路径</option>
|
||
</select>
|
||
<input type="text" id="vrModelDir" placeholder="含 VR Arc 模型的文件夹路径">
|
||
</div>
|
||
</div>
|
||
<div class="input-group">
|
||
<label>MDX-Net 模型目录</label>
|
||
<div class="path-row">
|
||
<select id="mdxModelDirMode">
|
||
<option value="absolute">绝对路径</option>
|
||
<option value="relative">相对路径</option>
|
||
</select>
|
||
<input type="text" id="mdxModelDir" placeholder="含 MDX-Net 模型的文件夹路径">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-outline" onclick="closeSettings()">取消</button>
|
||
<button class="modal-save-btn" onclick="saveSettings()">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<audio id="previewAudio" style="display:none;"></audio>
|
||
|
||
<script>
|
||
// ── 状态 ──────────────────────────────────────────────────────────────
|
||
var currentTaskId = null;
|
||
var pollTimer = null;
|
||
var selectedModelPath = '';
|
||
var currentArch = 'Demucs';
|
||
var scannedModels = [];
|
||
|
||
// ── 设置弹窗 ──────────────────────────────────────────────────────────
|
||
function openSettings() {
|
||
var dirs = window._savedModelDirs || {};
|
||
var dirModes = window._savedModelDirModes || {};
|
||
var uvrPath = document.getElementById('uvrPath');
|
||
|
||
if (uvrPath) uvrPath.value = window._savedUvrPath || '';
|
||
|
||
['demucs', 'vr', 'mdx'].forEach(function(k) {
|
||
var mode = document.getElementById(k + 'ModelDirMode');
|
||
var dir = document.getElementById(k + 'ModelDir');
|
||
if (mode) mode.value = dirModes[k] || 'absolute';
|
||
if (dir) dir.value = dirs[k] || '';
|
||
});
|
||
|
||
document.getElementById('settingsModal').classList.add('show');
|
||
}
|
||
|
||
function closeSettings() {
|
||
document.getElementById('settingsModal').classList.remove('show');
|
||
}
|
||
|
||
function saveSettings() {
|
||
var uvrPath = document.getElementById('uvrPath');
|
||
window._savedUvrPath = uvrPath ? uvrPath.value.trim() : '';
|
||
|
||
window._savedModelDirs = {};
|
||
window._savedModelDirModes = {};
|
||
['demucs', 'vr', 'mdx'].forEach(function(k) {
|
||
var dir = document.getElementById(k + 'ModelDir');
|
||
var mode = document.getElementById(k + 'ModelDirMode');
|
||
window._savedModelDirs[k] = dir ? dir.value.trim() : '';
|
||
window._savedModelDirModes[k] = mode ? mode.value : 'absolute';
|
||
});
|
||
|
||
updateModelDirDisplay();
|
||
closeSettings();
|
||
saveConfig();
|
||
}
|
||
|
||
function updateModelDirDisplay() {
|
||
var dirs = window._savedModelDirs || {};
|
||
var key = archDirKeys[currentArch] || currentArch;
|
||
var dir = dirs[key] || '';
|
||
var modelDirText = document.getElementById('modelDirText');
|
||
var modelArchSelect = document.getElementById('modelArchSelect');
|
||
|
||
if (modelDirText) {
|
||
modelDirText.textContent = dir || '未配置模型目录';
|
||
modelDirText.style.color = dir ? '#2e7d32' : '#999';
|
||
}
|
||
|
||
if (modelArchSelect) {
|
||
modelArchSelect.value = currentArch;
|
||
}
|
||
}
|
||
|
||
// 架构 → 对应的模型目录 config key
|
||
var archDirKeys = {
|
||
'Demucs': 'demucs',
|
||
'VR Arc': 'vr',
|
||
'MDX-Net': 'mdx'
|
||
};
|
||
|
||
// ── Toast ─────────────────────────────────────────────────────────────
|
||
function showMessage(text, type, duration) {
|
||
type = type || 'info'; duration = duration || 3000;
|
||
var c = document.getElementById('toastContainer');
|
||
var t = document.createElement('div');
|
||
t.className = 'toast ' + type; t.textContent = text;
|
||
c.appendChild(t);
|
||
setTimeout(function() { t.style.opacity = '0'; t.style.transition = 'opacity 0.3s'; setTimeout(function() { t.remove(); }, 300); }, duration);
|
||
}
|
||
|
||
// ── 架构切换 ──────────────────────────────────────────────────────────
|
||
function switchArch(el) {
|
||
// 支持从select元素或tab元素调用
|
||
if (el.tagName === 'SELECT') {
|
||
currentArch = el.value;
|
||
} else {
|
||
document.querySelectorAll('.arch-tab').forEach(function(t) { t.classList.remove('active'); });
|
||
el.classList.add('active');
|
||
currentArch = el.dataset.arch;
|
||
}
|
||
|
||
// 同步架构选择器
|
||
var modelArchSelect = document.getElementById('modelArchSelect');
|
||
if (modelArchSelect) {
|
||
modelArchSelect.value = currentArch;
|
||
}
|
||
|
||
// 切换架构特异参数面板
|
||
var demucsParams = document.getElementById('demucsParams');
|
||
var mdxParams = document.getElementById('mdxParams');
|
||
var vrParams = document.getElementById('vrParams');
|
||
if (demucsParams) demucsParams.style.display = currentArch === 'Demucs' ? 'block' : 'none';
|
||
if (mdxParams) mdxParams.style.display = currentArch === 'MDX-Net' ? 'block' : 'none';
|
||
if (vrParams) vrParams.style.display = currentArch === 'VR Arc' ? 'block' : 'none';
|
||
|
||
// 主/副音轨复选框始终显示
|
||
var primaryOnlyRow = document.getElementById('primaryOnlyRow');
|
||
var secondaryOnlyRow = document.getElementById('secondaryOnlyRow');
|
||
if (primaryOnlyRow) primaryOnlyRow.style.display = 'flex';
|
||
if (secondaryOnlyRow) secondaryOnlyRow.style.display = 'flex';
|
||
|
||
// 更新主/副音轨复选框状态
|
||
updateStemOnlyState();
|
||
|
||
// MDX-Net/VR Arc 不支持 GPU(需要 cuDNN+ONNX Runtime CUDA)
|
||
var gpuCb = document.getElementById('isGpu');
|
||
if (gpuCb) {
|
||
if (currentArch !== 'Demucs') {
|
||
gpuCb.checked = false;
|
||
gpuCb.disabled = true;
|
||
} else {
|
||
gpuCb.disabled = false;
|
||
}
|
||
}
|
||
|
||
// 恢复该架构保存的模型目录
|
||
updateModelDirDisplay();
|
||
|
||
// 清空模型选择
|
||
selectedModelPath = '';
|
||
var selectedModelInfo = document.getElementById('selectedModelInfo');
|
||
if (selectedModelInfo) selectedModelInfo.style.display = 'none';
|
||
scannedModels = [];
|
||
var modelSelect = document.getElementById('modelSelect');
|
||
if (modelSelect) {
|
||
modelSelect.innerHTML = '<option value="">-- 请先扫描模型目录 --</option>';
|
||
}
|
||
updateStartBtn();
|
||
// 自动扫描
|
||
autoScanModels();
|
||
}
|
||
|
||
function updateStemOnlyState() {
|
||
var demucsStemSelect = document.getElementById('demucsStemSelect');
|
||
var allStems = currentArch === 'Demucs' && demucsStemSelect && demucsStemSelect.value === 'All Stems';
|
||
var pri = document.getElementById('isPrimaryOnly');
|
||
var sec = document.getElementById('isSecondaryOnly');
|
||
if (pri) { pri.disabled = allStems; if (allStems) pri.checked = false; }
|
||
if (sec) { sec.disabled = allStems; if (allStems) sec.checked = false; }
|
||
}
|
||
|
||
function autoScanModels() {
|
||
var dirs = window._savedModelDirs || {};
|
||
var key = archDirKeys[currentArch] || currentArch;
|
||
var dir = dirs[key] || '';
|
||
if (!dir) return;
|
||
fetch('/uvr-sep/scan-models', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({model_dir: dir, arch_type: currentArch})
|
||
})
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
if (!data.success || !data.models || !data.models.length) return;
|
||
scannedModels = data.models;
|
||
var sel = document.getElementById('modelSelect');
|
||
if (!sel) return;
|
||
sel.innerHTML = '<option value="">-- 请选择模型 --</option>';
|
||
scannedModels.forEach(function(m, idx) {
|
||
var opt = document.createElement('option');
|
||
opt.value = idx;
|
||
opt.textContent = m.name + (m.has_meta ? ' [' + m.primary_stem + ']' : '');
|
||
sel.appendChild(opt);
|
||
});
|
||
// 自动选中已保存的模型
|
||
var saved = (window._savedSelectedModels || {})[currentArch] || '';
|
||
if (saved) {
|
||
for (var i = 0; i < scannedModels.length; i++) {
|
||
if (scannedModels[i].name === saved) {
|
||
sel.value = String(i);
|
||
onModelSelect(sel);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
})
|
||
.catch(function() {});
|
||
}
|
||
|
||
// ── 上传 ──────────────────────────────────────────────────────────────
|
||
var uploadArea = document.getElementById('uploadArea');
|
||
var audioInput = document.getElementById('audioInput');
|
||
if (uploadArea && audioInput) {
|
||
uploadArea.addEventListener('dragover', function(e) { e.preventDefault(); uploadArea.classList.add('dragover'); });
|
||
uploadArea.addEventListener('dragleave', function() { uploadArea.classList.remove('dragover'); });
|
||
uploadArea.addEventListener('drop', function(e) {
|
||
e.preventDefault(); uploadArea.classList.remove('dragover');
|
||
if (e.dataTransfer.files.length) { audioInput.files = e.dataTransfer.files; showFileInfo(e.dataTransfer.files[0]); }
|
||
});
|
||
audioInput.addEventListener('change', function() { if (audioInput.files.length) showFileInfo(audioInput.files[0]); });
|
||
}
|
||
|
||
function showFileInfo(file) {
|
||
var uploadLabel = document.getElementById('uploadLabel');
|
||
if (uploadLabel) uploadLabel.innerHTML = '<span class="filename">' + file.name + '</span>';
|
||
updateStartBtn();
|
||
}
|
||
|
||
function updateStartBtn() {
|
||
var hasFile = audioInput && audioInput.files && audioInput.files.length > 0;
|
||
var hasModel = selectedModelPath !== '';
|
||
var startBtn = document.getElementById('startBtn');
|
||
if (startBtn) startBtn.disabled = !(hasFile && hasModel);
|
||
}
|
||
|
||
// ── 扫描模型 ──────────────────────────────────────────────────────────
|
||
function scanModels() {
|
||
var dirs = window._savedModelDirs || {};
|
||
var key = archDirKeys[currentArch] || currentArch;
|
||
var dir = dirs[key] || '';
|
||
if (!dir) { showMessage('请先在设置中配置' + currentArch + '模型目录', 'error'); return; }
|
||
fetch('/uvr-sep/scan-models', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({model_dir: dir, arch_type: currentArch})
|
||
})
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
if (!data.success) { showMessage(data.error, 'error'); return; }
|
||
scannedModels = data.models || [];
|
||
if (!scannedModels.length) {
|
||
showMessage('未找到 ' + currentArch + ' 模型文件', 'info');
|
||
return;
|
||
}
|
||
var sel = document.getElementById('modelSelect');
|
||
if (!sel) return;
|
||
sel.innerHTML = '<option value="">-- 请选择模型 --</option>';
|
||
scannedModels.forEach(function(m, idx) {
|
||
var opt = document.createElement('option');
|
||
opt.value = idx;
|
||
opt.textContent = m.name + (m.has_meta ? ' [' + m.primary_stem + ']' : '');
|
||
sel.appendChild(opt);
|
||
});
|
||
selectedModelPath = '';
|
||
var selectedModelInfo = document.getElementById('selectedModelInfo');
|
||
if (selectedModelInfo) selectedModelInfo.style.display = 'none';
|
||
updateStartBtn();
|
||
})
|
||
.catch(function(e) { showMessage('扫描失败: ' + e, 'error'); });
|
||
}
|
||
|
||
function onModelSelect(sel) {
|
||
var idx = parseInt(sel.value);
|
||
if (isNaN(idx) || idx < 0 || idx >= scannedModels.length) {
|
||
selectedModelPath = '';
|
||
var selectedModelInfo = document.getElementById('selectedModelInfo');
|
||
if (selectedModelInfo) selectedModelInfo.style.display = 'none';
|
||
updateStartBtn();
|
||
return;
|
||
}
|
||
var m = scannedModels[idx];
|
||
selectedModelPath = m.path;
|
||
var selectedModelInfo = document.getElementById('selectedModelInfo');
|
||
var selectedModelName = document.getElementById('selectedModelName');
|
||
var selectedModelPathEl = document.getElementById('selectedModelPath');
|
||
if (selectedModelInfo) selectedModelInfo.style.display = 'block';
|
||
if (selectedModelName) selectedModelName.textContent = m.name;
|
||
if (selectedModelPathEl) selectedModelPathEl.textContent = m.path;
|
||
updateStartBtn();
|
||
}
|
||
|
||
// ── 开始分离 ──────────────────────────────────────────────────────────
|
||
function startSeparate() {
|
||
if (!audioInput || !audioInput.files || !audioInput.files.length) { showMessage('请上传音频文件', 'error'); return; }
|
||
if (!selectedModelPath) { showMessage('请选择模型', 'error'); return; }
|
||
|
||
var formData = new FormData();
|
||
formData.append('audio', audioInput.files[0]);
|
||
formData.append('model_path', selectedModelPath);
|
||
formData.append('process_method', currentArch);
|
||
|
||
var isPrimaryOnly = document.getElementById('isPrimaryOnly');
|
||
var isSecondaryOnly = document.getElementById('isSecondaryOnly');
|
||
var isGpu = document.getElementById('isGpu');
|
||
var isNormalize = document.getElementById('isNormalize');
|
||
|
||
formData.append('is_primary_stem_only', isPrimaryOnly && isPrimaryOnly.checked ? '1' : '0');
|
||
formData.append('is_secondary_stem_only', isSecondaryOnly && isSecondaryOnly.checked ? '1' : '0');
|
||
formData.append('is_gpu', isGpu && isGpu.checked ? '1' : '0');
|
||
formData.append('is_normalization', isNormalize && isNormalize.checked ? '1' : '0');
|
||
|
||
// 架构特异参数
|
||
if (currentArch === 'Demucs') {
|
||
var demucsStemSelect = document.getElementById('demucsStemSelect');
|
||
var demucsSegment = document.getElementById('demucsSegment');
|
||
formData.append('demucs_stems', demucsStemSelect ? demucsStemSelect.value : 'All Stems');
|
||
formData.append('segment', demucsSegment ? demucsSegment.value : 'Default');
|
||
} else if (currentArch === 'MDX-Net') {
|
||
var mdxSegmentSize = document.getElementById('mdxSegmentSize');
|
||
var mdxOverlap = document.getElementById('mdxOverlap');
|
||
formData.append('segment', mdxSegmentSize ? mdxSegmentSize.value : 'Default');
|
||
formData.append('mdx_overlap', mdxOverlap ? mdxOverlap.value : '0.250');
|
||
} else if (currentArch === 'VR Arc') {
|
||
var vrWindowSize = document.getElementById('vrWindowSize');
|
||
var vrAggression = document.getElementById('vrAggression');
|
||
formData.append('vr_window_size', vrWindowSize ? vrWindowSize.value : '1024');
|
||
formData.append('vr_aggression', vrAggression ? vrAggression.value : '5');
|
||
}
|
||
|
||
var startBtn = document.getElementById('startBtn');
|
||
var emptyState = document.getElementById('emptyState');
|
||
var resultSection = document.getElementById('resultSection');
|
||
var progressSection = document.getElementById('progressSection');
|
||
var progressFill = document.getElementById('progressFill');
|
||
var statusText = document.getElementById('statusText');
|
||
|
||
if (startBtn) startBtn.disabled = true;
|
||
if (emptyState) emptyState.style.display = 'none';
|
||
if (resultSection) resultSection.style.display = 'none';
|
||
if (progressSection) progressSection.style.display = 'block';
|
||
if (progressFill) progressFill.style.width = '0%';
|
||
if (statusText) statusText.textContent = '正在上传并初始化...';
|
||
|
||
fetch('/uvr-sep/separate', { method: 'POST', body: formData })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
if (!data.success) {
|
||
showMessage(data.error, 'error');
|
||
if (progressSection) progressSection.style.display = 'none';
|
||
if (emptyState) emptyState.style.display = '';
|
||
if (startBtn) startBtn.disabled = false;
|
||
return;
|
||
}
|
||
currentTaskId = data.task_id;
|
||
if (statusText) statusText.textContent = '正在分离...';
|
||
pollStatus();
|
||
})
|
||
.catch(function(e) {
|
||
showMessage('请求失败: ' + e, 'error');
|
||
if (progressSection) progressSection.style.display = 'none';
|
||
if (emptyState) emptyState.style.display = '';
|
||
if (startBtn) startBtn.disabled = false;
|
||
});
|
||
}
|
||
|
||
// ── 轮询状态 ──────────────────────────────────────────────────────────
|
||
function pollStatus() {
|
||
if (!currentTaskId) return;
|
||
if (pollTimer) clearInterval(pollTimer);
|
||
pollTimer = setInterval(function() {
|
||
fetch('/uvr-sep/status/' + currentTaskId)
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
if (!data.success) return;
|
||
var progressFill = document.getElementById('progressFill');
|
||
var statusText = document.getElementById('statusText');
|
||
var startBtn = document.getElementById('startBtn');
|
||
var progressSection = document.getElementById('progressSection');
|
||
var emptyState = document.getElementById('emptyState');
|
||
|
||
if (progressFill) progressFill.style.width = data.progress + '%';
|
||
var devInfo = data.device ? ' [' + data.device + ']' : '';
|
||
if (data.status === 'done') {
|
||
clearInterval(pollTimer); pollTimer = null;
|
||
if (statusText) statusText.textContent = '分离完成!' + devInfo;
|
||
showResults(data.stems);
|
||
if (startBtn) startBtn.disabled = false;
|
||
} else if (data.status === 'error') {
|
||
clearInterval(pollTimer); pollTimer = null;
|
||
showMessage('分离失败: ' + data.error, 'error', 5000);
|
||
if (progressSection) progressSection.style.display = 'none';
|
||
if (emptyState) emptyState.style.display = '';
|
||
if (startBtn) startBtn.disabled = false;
|
||
} else {
|
||
if (statusText) statusText.textContent = '正在分离... ' + data.progress + '%' + devInfo;
|
||
}
|
||
})
|
||
.catch(function() {});
|
||
}, 1000);
|
||
}
|
||
|
||
// ── 显示结果 ──────────────────────────────────────────────────────────
|
||
function showResults(stems) {
|
||
var list = document.getElementById('stemsList');
|
||
if (!list) return;
|
||
list.innerHTML = '';
|
||
stems.forEach(function(s) {
|
||
var iconClass = 'other', icon = 'fa-exchange-alt';
|
||
var name = s.stem.toLowerCase();
|
||
if (name === 'vocals' || name === 'vocal') { iconClass = 'vocals'; icon = 'fa-microphone'; }
|
||
else if (name === 'instrumental' || name === 'no vocals') { iconClass = 'instrumental'; icon = 'fa-folder'; }
|
||
var row = document.createElement('div');
|
||
row.className = 'stem-row';
|
||
row.innerHTML =
|
||
'<div class="stem-icon ' + iconClass + '"><i class="fas ' + icon + '"></i></div>' +
|
||
'<div class="stem-info"><div class="stem-name">' + s.stem + '</div><div class="stem-filename">' + s.filename + '</div></div>' +
|
||
'<div class="stem-actions">' +
|
||
'<button class="stem-btn play-btn" onclick="playStem(\'' + currentTaskId + '\',\'' + s.stem + '\')"><i class="fas fa-play"></i></button>' +
|
||
'<button class="stem-btn" onclick="downloadStem(\'' + currentTaskId + '\',\'' + s.stem + '\')"><i class="fas fa-folder-plus"></i></button>' +
|
||
'</div>';
|
||
list.appendChild(row);
|
||
});
|
||
var progressSection = document.getElementById('progressSection');
|
||
var resultSection = document.getElementById('resultSection');
|
||
if (progressSection) progressSection.style.display = 'none';
|
||
if (resultSection) resultSection.style.display = 'block';
|
||
}
|
||
|
||
function playStem(taskId, stem) {
|
||
var audio = document.getElementById('previewAudio');
|
||
if (audio) {
|
||
audio.src = '/uvr-sep/download/' + encodeURIComponent(taskId) + '/' + encodeURIComponent(stem);
|
||
audio.play();
|
||
}
|
||
}
|
||
|
||
function downloadStem(taskId, stem) {
|
||
window.open('/uvr-sep/download/' + encodeURIComponent(taskId) + '/' + encodeURIComponent(stem));
|
||
}
|
||
|
||
function downloadAll() { if (currentTaskId) window.open('/uvr-sep/download-all/' + currentTaskId); }
|
||
|
||
function cleanupTask() {
|
||
if (!currentTaskId) return;
|
||
fetch('/uvr-sep/cleanup/' + currentTaskId, { method: 'POST' })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
if (data.success) {
|
||
showMessage('已清理临时文件', 'success');
|
||
var resultSection = document.getElementById('resultSection');
|
||
var emptyState = document.getElementById('emptyState');
|
||
if (resultSection) resultSection.style.display = 'none';
|
||
if (emptyState) emptyState.style.display = '';
|
||
currentTaskId = null;
|
||
}
|
||
});
|
||
}
|
||
|
||
// ── 保存配置 ──────────────────────────────────────────────────────────
|
||
function saveConfig() {
|
||
var modelDirs = window._savedModelDirs || {};
|
||
var dirModes = window._savedModelDirModes || {};
|
||
var uvrPath = document.getElementById('uvrPath');
|
||
var isGpu = document.getElementById('isGpu');
|
||
var isNormalize = document.getElementById('isNormalize');
|
||
var isPrimaryOnly = document.getElementById('isPrimaryOnly');
|
||
var isSecondaryOnly = document.getElementById('isSecondaryOnly');
|
||
var saveFormat = document.getElementById('saveFormat');
|
||
var demucsStemSelect = document.getElementById('demucsStemSelect');
|
||
var demucsSegment = document.getElementById('demucsSegment');
|
||
var mdxSegmentSize = document.getElementById('mdxSegmentSize');
|
||
var mdxOverlap = document.getElementById('mdxOverlap');
|
||
var vrWindowSize = document.getElementById('vrWindowSize');
|
||
var vrAggression = document.getElementById('vrAggression');
|
||
|
||
var cfg = {
|
||
uvr_project_path: uvrPath ? uvrPath.value.trim() : '',
|
||
demucs_model_dir: modelDirs['demucs'] || '',
|
||
vr_model_dir: modelDirs['vr'] || '',
|
||
mdx_model_dir: modelDirs['mdx'] || '',
|
||
demucs_model_dir_mode: dirModes['demucs'] || 'absolute',
|
||
vr_model_dir_mode: dirModes['vr'] || 'absolute',
|
||
mdx_model_dir_mode: dirModes['mdx'] || 'absolute',
|
||
arch_type: currentArch,
|
||
is_gpu: isGpu ? isGpu.checked : false,
|
||
is_normalization: isNormalize ? isNormalize.checked : false,
|
||
is_primary_stem_only: isPrimaryOnly ? isPrimaryOnly.checked : false,
|
||
is_secondary_stem_only: isSecondaryOnly ? isSecondaryOnly.checked : false,
|
||
save_format: saveFormat ? saveFormat.value : 'wav',
|
||
demucs_stems: demucsStemSelect ? demucsStemSelect.value : 'All Stems',
|
||
demucs_segment: demucsSegment ? demucsSegment.value : 'Default',
|
||
mdx_segment_size: mdxSegmentSize ? mdxSegmentSize.value : 'Default',
|
||
mdx_overlap: mdxOverlap ? parseFloat(mdxOverlap.value) : 0.25,
|
||
vr_window_size: vrWindowSize ? parseInt(vrWindowSize.value) : 1024,
|
||
vr_aggression: vrAggression ? parseInt(vrAggression.value) : 5,
|
||
};
|
||
|
||
// 保存选中模型(按架构区分)
|
||
var archKey = {'Demucs':'demucs_selected_model','MDX-Net':'mdx_selected_model','VR Arc':'vr_selected_model'};
|
||
cfg.demucs_selected_model = window._savedSelectedModels ? (window._savedSelectedModels['Demucs'] || '') : '';
|
||
cfg.mdx_selected_model = window._savedSelectedModels ? (window._savedSelectedModels['MDX-Net'] || '') : '';
|
||
cfg.vr_selected_model = window._savedSelectedModels ? (window._savedSelectedModels['VR Arc'] || '') : '';
|
||
|
||
// 更新当前架构的值
|
||
if (selectedModelPath && archKey[currentArch]) {
|
||
var modelSelect = document.getElementById('modelSelect');
|
||
if (modelSelect && modelSelect.value) {
|
||
var idx = parseInt(modelSelect.value);
|
||
if (!isNaN(idx) && idx >= 0 && idx < scannedModels.length) {
|
||
cfg[archKey[currentArch]] = scannedModels[idx].name;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 同步到内存
|
||
if (!window._savedSelectedModels) window._savedSelectedModels = {};
|
||
window._savedSelectedModels[currentArch] = cfg[archKey[currentArch]] || '';
|
||
|
||
fetch('/uvr-sep/config', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(cfg)
|
||
})
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
if (data.success) showMessage('配置已保存', 'success');
|
||
else showMessage('保存失败', 'error');
|
||
})
|
||
.catch(function(e) { showMessage('保存失败: ' + e, 'error'); });
|
||
}
|
||
|
||
// ── 初始化 ────────────────────────────────────────────────────────────
|
||
function initPage() {
|
||
var cfg = {
|
||
uvr_project_path: {{ config.uvr_project_path|default("")|tojson }},
|
||
arch_type: {{ config.arch_type|default("Demucs")|tojson }},
|
||
model_dir_mode: {{ config.model_dir_mode|default("absolute")|tojson }},
|
||
demucs_model_dir: {{ config.demucs_model_dir|default("")|tojson }},
|
||
vr_model_dir: {{ config.vr_model_dir|default("")|tojson }},
|
||
mdx_model_dir: {{ config.mdx_model_dir|default("")|tojson }},
|
||
demucs_segment: {{ config.demucs_segment|default("Default")|tojson }},
|
||
demucs_stems: {{ config.demucs_stems|default("All Stems")|tojson }},
|
||
mdx_segment_size: {{ config.mdx_segment_size|default("Default")|tojson }},
|
||
mdx_overlap: {{ config.mdx_overlap|default(0.25) }},
|
||
vr_window_size: {{ config.vr_window_size|default(1024) }},
|
||
vr_aggression: {{ config.vr_aggression|default(5) }},
|
||
save_format: {{ config.save_format|default("wav")|tojson }},
|
||
is_gpu: {{ 'true' if config.is_gpu else 'false' }},
|
||
is_normalization: {{ 'true' if config.is_normalization else 'false' }},
|
||
is_primary_stem_only: {{ 'true' if config.is_primary_stem_only else 'false' }},
|
||
is_secondary_stem_only: {{ 'true' if config.is_secondary_stem_only else 'false' }},
|
||
demucs_selected_model: {{ config.demucs_selected_model|default("")|tojson }},
|
||
mdx_selected_model: {{ config.mdx_selected_model|default("")|tojson }},
|
||
vr_selected_model: {{ config.vr_selected_model|default("")|tojson }},
|
||
demucs_model_dir_mode: {{ config.demucs_model_dir_mode|default("absolute")|tojson }},
|
||
vr_model_dir_mode: {{ config.vr_model_dir_mode|default("absolute")|tojson }},
|
||
mdx_model_dir_mode: {{ config.mdx_model_dir_mode|default("absolute")|tojson }},
|
||
};
|
||
|
||
// 保存各架构的模型目录
|
||
window._savedModelDirs = {
|
||
'demucs': cfg.demucs_model_dir,
|
||
'vr': cfg.vr_model_dir,
|
||
'mdx': cfg.mdx_model_dir
|
||
};
|
||
|
||
window._savedModelDirModes = {
|
||
'demucs': cfg.demucs_model_dir_mode,
|
||
'vr': cfg.vr_model_dir_mode,
|
||
'mdx': cfg.mdx_model_dir_mode
|
||
};
|
||
|
||
window._savedUvrPath = cfg.uvr_project_path;
|
||
|
||
// 保存各架构的已选模型
|
||
window._savedSelectedModels = {
|
||
'Demucs': cfg.demucs_selected_model,
|
||
'MDX-Net': cfg.mdx_selected_model,
|
||
'VR Arc': cfg.vr_selected_model
|
||
};
|
||
|
||
// 恢复架构选择
|
||
currentArch = cfg.arch_type;
|
||
document.querySelectorAll('.arch-tab').forEach(function(t) {
|
||
t.classList.toggle('active', t.dataset.arch === currentArch);
|
||
});
|
||
|
||
// 切换面板
|
||
var demucsParams = document.getElementById('demucsParams');
|
||
var mdxParams = document.getElementById('mdxParams');
|
||
var vrParams = document.getElementById('vrParams');
|
||
if (demucsParams) demucsParams.style.display = currentArch === 'Demucs' ? 'block' : 'none';
|
||
if (mdxParams) mdxParams.style.display = currentArch === 'MDX-Net' ? 'block' : 'none';
|
||
if (vrParams) vrParams.style.display = currentArch === 'VR Arc' ? 'block' : 'none';
|
||
|
||
// 主/副音轨复选框始终显示
|
||
var primaryOnlyRow = document.getElementById('primaryOnlyRow');
|
||
var secondaryOnlyRow = document.getElementById('secondaryOnlyRow');
|
||
if (primaryOnlyRow) primaryOnlyRow.style.display = 'flex';
|
||
if (secondaryOnlyRow) secondaryOnlyRow.style.display = 'flex';
|
||
|
||
// 恢复模型目录显示
|
||
updateModelDirDisplay();
|
||
|
||
// 恢复 Demucs 参数
|
||
var demucsStemSel = document.getElementById('demucsStemSelect');
|
||
if (demucsStemSel) {
|
||
for (var i = 0; i < demucsStemSel.options.length; i++) {
|
||
if (demucsStemSel.options[i].value === cfg.demucs_stems) { demucsStemSel.selectedIndex = i; break; }
|
||
}
|
||
}
|
||
var demucsSegSel = document.getElementById('demucsSegment');
|
||
if (demucsSegSel) demucsSegSel.value = cfg.demucs_segment;
|
||
|
||
// 恢复 MDX 参数
|
||
var mdxSegSel = document.getElementById('mdxSegmentSize');
|
||
if (mdxSegSel) mdxSegSel.value = cfg.mdx_segment_size;
|
||
var mdxOverlap = document.getElementById('mdxOverlap');
|
||
if (mdxOverlap) mdxOverlap.value = cfg.mdx_overlap.toFixed(3);
|
||
|
||
// 恢复 VR 参数
|
||
var vrWinSel = document.getElementById('vrWindowSize');
|
||
if (vrWinSel) vrWinSel.value = cfg.vr_window_size;
|
||
var vrAggression = document.getElementById('vrAggression');
|
||
if (vrAggression) vrAggression.value = cfg.vr_aggression;
|
||
|
||
// 恢复通用参数
|
||
var isGpu = document.getElementById('isGpu');
|
||
var isNormalize = document.getElementById('isNormalize');
|
||
var isPrimaryOnly = document.getElementById('isPrimaryOnly');
|
||
var isSecondaryOnly = document.getElementById('isSecondaryOnly');
|
||
if (isGpu) isGpu.checked = cfg.is_gpu;
|
||
if (isNormalize) isNormalize.checked = cfg.is_normalization;
|
||
if (isPrimaryOnly) isPrimaryOnly.checked = cfg.is_primary_stem_only;
|
||
if (isSecondaryOnly) isSecondaryOnly.checked = cfg.is_secondary_stem_only;
|
||
var formatSel = document.getElementById('saveFormat');
|
||
if (formatSel) {
|
||
for (var i = 0; i < formatSel.options.length; i++) {
|
||
if (formatSel.options[i].value === cfg.save_format) { formatSel.selectedIndex = i; break; }
|
||
}
|
||
}
|
||
|
||
// 更新主/副音轨复选框状态(All Stems 时禁用)
|
||
updateStemOnlyState();
|
||
|
||
// MDX-Net/VR Arc 禁用 GPU
|
||
if (isGpu) {
|
||
if (currentArch !== 'Demucs') { isGpu.checked = false; isGpu.disabled = true; }
|
||
}
|
||
|
||
// 自动扫描当前架构的模型目录
|
||
autoScanModels();
|
||
|
||
// 检测 GPU 状态
|
||
var gpuStatus = document.getElementById('gpuStatus');
|
||
if (gpuStatus) {
|
||
fetch('/uvr-sep/gpu-status').then(function(r){return r.json();}).then(function(d){
|
||
if (d.torch_cuda) {
|
||
gpuStatus.style.color = '#2e7d32';
|
||
gpuStatus.textContent = '✓ PyTorch CUDA 可用' + (d.gpu_name ? ' (' + d.gpu_name + ')' : '');
|
||
} else if (d.cuda_available) {
|
||
gpuStatus.style.color = '#f57c00';
|
||
gpuStatus.textContent = '⚠ PyTorch 无 CUDA (需重装 torch)';
|
||
} else {
|
||
gpuStatus.style.color = '#c62828';
|
||
gpuStatus.textContent = '✗ CUDA 不可用';
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// DOM加载完成后初始化
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', initPage);
|
||
} else {
|
||
initPage();
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|