Files
python_script/flask-dev-api/templates/uvr_sep.html
DelLevin-Home 05e733e872 22
2026-06-17 10:43:33 +08:00

1478 lines
75 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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: 50%; 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; min-height: 0;
}
/* 日志区域 */
.log-area {
flex: 1; flex-shrink: 0; display: flex; flex-direction: column;
background: #1e1e1e; color: #d4d4d4; border-top: 1px solid #3e3e3e;
min-height: 0;
}
.log-header {
display: flex; align-items: center; justify-content: space-between;
padding: 6px 10px; background: #2d2d2d; border-bottom: 1px solid #3e3e3e;
font-size: 11px; color: #888; flex-shrink: 0;
}
.log-header i { margin-right: 4px; }
.log-clear-btn {
background: none; border: 1px solid #555; color: #999;
border-radius: 4px; padding: 2px 8px; font-size: 10px;
cursor: pointer; transition: all 0.15s;
}
.log-clear-btn:hover { background: #3e3e3e; color: #ccc; border-color: #777; }
.log-content {
flex: 1; overflow-y: auto; padding: 6px 10px;
font-size: 11px; font-family: Consolas, "Courier New", monospace; line-height: 1.6;
}
.log-content::-webkit-scrollbar { width: 4px; }
.log-content::-webkit-scrollbar-thumb { background: #555; border-radius: 2px; }
/* 进度条 */
.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 { width: 180px; flex-shrink: 0; }
.stem-name { font-size: 14px; font-weight: 600; color: #333; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.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; }
/* 音频播放器 */
.stem-player {
display: none; align-items: center; gap: 8px;
flex: 1; min-width: 0;
}
.stem-player.active { display: flex; }
.player-btn {
width: 26px; height: 26px; border-radius: 50%; border: none;
background: #0078d4; color: #fff; cursor: pointer; display: flex;
align-items: center; justify-content: center; font-size: 10px;
flex-shrink: 0; transition: all 0.15s;
}
.player-btn:hover { background: #006cbd; }
.player-btn.playing { background: #e53935; }
.player-btn.playing:hover { background: #c62828; }
.player-progress {
width: 200px; flex-shrink: 0; height: 4px;
-webkit-appearance: none; appearance: none;
background: #e0e0e0; border-radius: 2px; outline: none; cursor: pointer;
}
.player-progress::-webkit-slider-thumb {
-webkit-appearance: none; width: 12px; height: 12px;
background: #0078d4; border-radius: 50%; cursor: pointer;
}
.player-time {
font-size: 11px; color: #999; flex-shrink: 0;
font-variant-numeric: tabular-nums; min-width: 70px; text-align: right;
}
/* 顶部操作栏 */
.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" onchange="onModelSelect(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="">-- 请先扫描模型目录 --</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>
<select id="demucsSegment" onchange="toggleCustom(this,'demucsSegmentCustom')" style="width:100%;padding:7px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:13px;background:#fff;color:#333;outline:none;">
<option value="Default">Default</option>
<option value="1">1</option><option value="2">2</option><option value="3">3</option>
<option value="4">4</option><option value="5">5</option><option value="6">6</option>
<option value="7">7</option><option value="8">8</option><option value="10">10</option>
<option value="12">12</option><option value="15">15</option><option value="20">20</option>
<option value="30">30</option><option value="40">40</option><option value="50">50</option>
<option value="__custom__">自定义...</option>
</select>
<input type="text" id="demucsSegmentCustom" placeholder="输入自定义值" style="display:none;margin-top:4px;width:100%;padding:7px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:13px;background:#fff;color:#333;outline:none;">
<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>
<select id="mdxSegmentSize" onchange="toggleCustom(this,'mdxSegmentSizeCustom')" style="width:100%;padding:7px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:13px;background:#fff;color:#333;outline:none;">
<option value="Default">Default</option>
<option value="128">128</option><option value="256">256</option>
<option value="512">512</option><option value="1024">1024</option>
<option value="2048">2048</option><option value="4096">4096</option>
<option value="__custom__">自定义...</option>
</select>
<input type="text" id="mdxSegmentSizeCustom" placeholder="输入自定义值" style="display:none;margin-top:4px;width:100%;padding:7px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:13px;background:#fff;color:#333;outline:none;">
<div class="param-desc">默认大小为 256。音质可能会因你的选择而有所不同。</div>
</div>
<div class="input-group">
<label>Overlap重叠度</label>
<select id="mdxOverlap" onchange="toggleCustom(this,'mdxOverlapCustom')" style="width:100%;padding:7px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:13px;background:#fff;color:#333;outline:none;">
<option value="0.100">0.100</option><option value="0.250">0.250</option>
<option value="0.500">0.500</option><option value="0.750">0.750</option>
<option value="0.990">0.990</option>
<option value="__custom__">自定义...</option>
</select>
<input type="text" id="mdxOverlapCustom" placeholder="输入自定义值,如 0.350" style="display:none;margin-top:4px;width:100%;padding:7px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:13px;background:#fff;color:#333;outline:none;">
<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>
<select id="vrWindowSize" onchange="toggleCustom(this,'vrWindowSizeCustom')" style="width:100%;padding:7px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:13px;background:#fff;color:#333;outline:none;">
<option value="320">320</option>
<option value="512">512</option>
<option value="1024">1024</option>
<option value="2048">2048</option>
<option value="4096">4096</option>
<option value="__custom__">自定义...</option>
</select>
<input type="text" id="vrWindowSizeCustom" placeholder="输入自定义值" style="display:none;margin-top:4px;width:100%;padding:7px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:13px;background:#fff;color:#333;outline:none;">
<div class="param-desc">选择窗口大小,以平衡音质与处理速度。</div>
</div>
<div class="input-group">
<label>Aggression分离强度</label>
<select id="vrAggression" onchange="toggleCustom(this,'vrAggressionCustom')" style="width:100%;padding:7px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:13px;background:#fff;color:#333;outline:none;">
<option value="-100">-100</option><option value="-50">-50</option>
<option value="-10">-10</option><option value="-5">-5</option>
<option value="0">0</option><option value="5">5</option>
<option value="10">10</option><option value="50">50</option>
<option value="100">100</option>
<option value="__custom__">自定义...</option>
</select>
<input type="text" id="vrAggressionCustom" placeholder="输入自定义值,如 20" style="display:none;margin-top:4px;width:100%;padding:7px 10px;border:1px solid #e0e0e0;border-radius:6px;font-size:13px;background:#fff;color:#333;outline:none;">
<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 class="log-area" id="logArea">
<div class="log-header">
<span><i class="fas fa-code"></i> 控制台日志</span>
<button class="log-clear-btn" onclick="clearLogs()" title="清除日志">清除</button>
</div>
<div class="log-content" id="logContent"></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 toggleCustom(sel, customId) {
var customEl = document.getElementById(customId);
if (!customEl) return;
customEl.style.display = sel.value === '__custom__' ? 'block' : 'none';
if (sel.value === '__custom__') customEl.focus();
}
function getSelectValue(selectId, customId) {
var sel = document.getElementById(selectId);
if (!sel) return '';
if (sel.value === '__custom__') {
var custom = document.getElementById(customId);
return custom ? custom.value.trim() : '';
}
return sel.value;
}
function restoreCustomSelect(selectId, customId, value) {
var sel = document.getElementById(selectId);
if (!sel) return;
// Try matching a preset option
for (var i = 0; i < sel.options.length; i++) {
if (sel.options[i].value === value) { sel.selectedIndex = i; return; }
}
// No match — use custom
sel.value = '__custom__';
var custom = document.getElementById(customId);
if (custom) { custom.style.display = 'block'; custom.value = value; }
}
// ── 设置弹窗 ──────────────────────────────────────────────────────────
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');
formData.append('demucs_stems', demucsStemSelect ? demucsStemSelect.value : 'All Stems');
formData.append('segment', getSelectValue('demucsSegment', 'demucsSegmentCustom') || 'Default');
} else if (currentArch === 'MDX-Net') {
formData.append('segment', getSelectValue('mdxSegmentSize', 'mdxSegmentSizeCustom') || 'Default');
formData.append('mdx_overlap', getSelectValue('mdxOverlap', 'mdxOverlapCustom') || '0.250');
} else if (currentArch === 'VR Arc') {
formData.append('vr_window_size', getSelectValue('vrWindowSize', 'vrWindowSizeCustom') || '1024');
formData.append('vr_aggression', getSelectValue('vrAggression', 'vrAggressionCustom') || '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 = '正在上传并初始化...';
var logContent = document.getElementById('logContent');
if (logContent) logContent.innerHTML = '';
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 renderLogs(logs) {
var el = document.getElementById('logContent');
if (!el || !logs || !logs.length) return;
el.innerHTML = logs.map(function(l) { return l.replace(/</g, '&lt;'); }).join('<br>');
el.scrollTop = el.scrollHeight;
}
function clearLogs() {
var el = document.getElementById('logContent');
if (el) el.innerHTML = '';
if (currentTaskId) {
fetch('/uvr-sep/clear-logs/' + currentTaskId, { method: 'POST' });
}
}
// ── 轮询状态 ──────────────────────────────────────────────────────────
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 + '%';
if (data.logs) renderLogs(data.logs);
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 safeStem = s.stem.replace(/'/g, "\\'");
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 style="display:flex;align-items:center;gap:10px;margin-left:auto;">' +
'<div class="stem-player active" id="player_' + safeStem + '">' +
'<button class="player-btn" id="playerBtn_' + safeStem + '" onclick="playStem(\'' + currentTaskId + '\',\'' + safeStem + '\')" title="播放"><i class="fas fa-play"></i></button>' +
'<input type="range" class="player-progress" id="progress_' + safeStem + '" min="0" max="100" value="0" step="0.1"' +
' onmousedown="startSeeking(\'' + safeStem + '\')"' +
' onmouseup="doSeek(\'' + safeStem + '\')"' +
' ontouchstart="startSeeking(\'' + safeStem + '\')"' +
' ontouchend="doSeek(\'' + safeStem + '\')">' +
'<span class="player-time" id="time_' + safeStem + '">0:00 / 0:00</span>' +
'</div>' +
'<div class="stem-actions">' +
'<button class="stem-btn" onclick="downloadStem(\'' + currentTaskId + '\',\'' + safeStem + '\')" title="下载"><i class="fas fa-folder-plus"></i></button>' +
'</div>' +
'</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';
}
// ── 音频播放控制 ─────────────────────────────────────────────────────
var currentStem = '';
var playTimer = null;
var seeking = false;
function playStem(taskId, stem) {
var audio = document.getElementById('previewAudio');
if (!audio) return;
// 切换音轨
if (currentStem !== stem) {
stopCurrentStem();
currentStem = stem;
audio.src = '/uvr-sep/download/' + encodeURIComponent(taskId) + '/' + encodeURIComponent(stem);
}
// 播放/暂停切换
if (audio.paused) {
audio.play();
} else {
audio.pause();
}
}
function stopCurrentStem() {
var audio = document.getElementById('previewAudio');
if (!audio) return;
audio.pause();
audio.removeAttribute('src');
if (playTimer) { clearInterval(playTimer); playTimer = null; }
if (currentStem) {
var btn = document.getElementById('playerBtn_' + currentStem);
var prog = document.getElementById('progress_' + currentStem);
var time = document.getElementById('time_' + currentStem);
if (btn) { btn.className = 'player-btn'; btn.innerHTML = '<i class="fas fa-play"></i>'; }
if (prog) prog.value = 0;
if (time) time.textContent = '0:00 / 0:00';
}
currentStem = '';
}
// audio 事件
(function() {
var audio = document.getElementById('previewAudio');
if (!audio) return;
audio.addEventListener('play', function() {
if (!currentStem) return;
var btn = document.getElementById('playerBtn_' + currentStem);
if (btn) { btn.className = 'player-btn playing'; btn.innerHTML = '<i class="fas fa-pause"></i>'; }
if (playTimer) clearInterval(playTimer);
playTimer = setInterval(updatePlayProgress, 200);
});
audio.addEventListener('pause', function() {
if (!currentStem) return;
var btn = document.getElementById('playerBtn_' + currentStem);
if (btn) { btn.className = 'player-btn'; btn.innerHTML = '<i class="fas fa-play"></i>'; }
if (playTimer) { clearInterval(playTimer); playTimer = null; }
});
audio.addEventListener('ended', function() {
if (!currentStem) return;
var btn = document.getElementById('playerBtn_' + currentStem);
var prog = document.getElementById('progress_' + currentStem);
var time = document.getElementById('time_' + currentStem);
if (btn) { btn.className = 'player-btn'; btn.innerHTML = '<i class="fas fa-play"></i>'; }
if (prog) prog.value = 100;
if (time) time.textContent = formatPlayerTime(audio.duration) + ' / ' + formatPlayerTime(audio.duration);
if (playTimer) { clearInterval(playTimer); playTimer = null; }
});
audio.addEventListener('loadedmetadata', function() {
updatePlayProgress();
});
})();
function updatePlayProgress() {
var audio = document.getElementById('previewAudio');
if (!audio || !currentStem || isNaN(audio.duration)) return;
var prog = document.getElementById('progress_' + currentStem);
var time = document.getElementById('time_' + currentStem);
if (prog && !seeking) {
prog.value = audio.currentTime / audio.duration * 100;
}
if (time) {
time.textContent = formatPlayerTime(audio.currentTime) + ' / ' + formatPlayerTime(audio.duration);
}
}
function startSeeking(stem) {
if (stem === currentStem) seeking = true;
}
function doSeek(stem) {
seeking = false;
var audio = document.getElementById('previewAudio');
if (!audio || stem !== currentStem || isNaN(audio.duration)) return;
var prog = document.getElementById('progress_' + stem);
if (prog) {
audio.currentTime = parseFloat(prog.value) / 100 * audio.duration;
}
}
function formatPlayerTime(sec) {
if (isNaN(sec)) return '0:00';
var m = Math.floor(sec / 60);
var s = Math.floor(sec % 60);
return m + ':' + (s < 10 ? '0' : '') + s;
}
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 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: getSelectValue('demucsSegment', 'demucsSegmentCustom') || 'Default',
mdx_segment_size: getSelectValue('mdxSegmentSize', 'mdxSegmentSizeCustom') || 'Default',
mdx_overlap: parseFloat(getSelectValue('mdxOverlap', 'mdxOverlapCustom')) || 0.25,
vr_window_size: parseInt(getSelectValue('vrWindowSize', 'vrWindowSizeCustom')) || 1024,
vr_aggression: parseInt(getSelectValue('vrAggression', 'vrAggressionCustom')) || 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; }
}
}
restoreCustomSelect('demucsSegment', 'demucsSegmentCustom', cfg.demucs_segment);
// 恢复 MDX 参数
restoreCustomSelect('mdxSegmentSize', 'mdxSegmentSizeCustom', cfg.mdx_segment_size);
restoreCustomSelect('mdxOverlap', 'mdxOverlapCustom', cfg.mdx_overlap.toFixed(3));
// 恢复 VR 参数
restoreCustomSelect('vrWindowSize', 'vrWindowSizeCustom', String(cfg.vr_window_size));
restoreCustomSelect('vrAggression', 'vrAggressionCustom', String(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>