Spaces:
Running
Running
Commit ·
c71e540
1
Parent(s): 0935499
Voice Changer target: add Library tab to pick from saved voices and presets
Browse files- index.html +70 -3
index.html
CHANGED
|
@@ -728,6 +728,16 @@ pre {
|
|
| 728 |
@keyframes recPulse { 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.55); } 70% { box-shadow: 0 0 0 14px rgba(239, 68, 68, 0); } 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } }
|
| 729 |
.record-timer { font-size: 18px; font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
|
| 730 |
.record-hint { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 731 |
</style>
|
| 732 |
</head>
|
| 733 |
<body>
|
|
@@ -2804,9 +2814,65 @@ window.setInputMode = function(prefix, mode) {
|
|
| 2804 |
if (upload) upload.style.display = mode === "upload" ? "" : "none";
|
| 2805 |
const recordPane = document.getElementById(`${prefix}-record-pane`);
|
| 2806 |
if (recordPane) recordPane.style.display = mode === "record" ? "" : "none";
|
| 2807 |
-
// Optional library pane (Voice Changer target only, added later)
|
| 2808 |
const libraryPane = document.getElementById(`${prefix}-library-pane`);
|
| 2809 |
-
if (libraryPane)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2810 |
};
|
| 2811 |
|
| 2812 |
window.toggleInputRecord = async function(prefix) {
|
|
@@ -2892,6 +2958,7 @@ function attachAudioInput(prefix, onFile, opts = {}) {
|
|
| 2892 |
</div>
|
| 2893 |
<audio id="${prefix}-preview-audio" controls style="display:none;width:100%;margin-top:10px;border-radius:8px"></audio>
|
| 2894 |
</div>
|
|
|
|
| 2895 |
`);
|
| 2896 |
}
|
| 2897 |
|
|
@@ -3018,7 +3085,7 @@ function updateVcBtn() {
|
|
| 3018 |
wireUploadArea("vc-source-upload", "vc-source-input", "vc-source-label", f => { vcSource = f; updateVcBtn(); });
|
| 3019 |
wireUploadArea("vc-target-upload", "vc-target-input", "vc-target-label", f => { vcTarget = f; updateVcBtn(); });
|
| 3020 |
attachAudioInput("vc-source", f => { vcSource = f; document.getElementById("vc-source-label").textContent = f.name; document.getElementById("vc-source-upload").classList.add("has-file"); updateVcBtn(); });
|
| 3021 |
-
attachAudioInput("vc-target", f => { vcTarget = f; document.getElementById("vc-target-label").textContent = f.name; document.getElementById("vc-target-upload").classList.add("has-file"); updateVcBtn(); });
|
| 3022 |
setupPlayer("vc");
|
| 3023 |
|
| 3024 |
window.voiceChange = async function() {
|
|
|
|
| 728 |
@keyframes recPulse { 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.55); } 70% { box-shadow: 0 0 0 14px rgba(239, 68, 68, 0); } 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } }
|
| 729 |
.record-timer { font-size: 18px; font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
|
| 730 |
.record-hint { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
|
| 731 |
+
|
| 732 |
+
.lib-list { display: flex; flex-direction: column; gap: 2px; padding: 6px; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; max-height: 320px; overflow-y: auto; }
|
| 733 |
+
.lib-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 8px; cursor: pointer; transition: background 0.1s; border: 1px solid transparent; }
|
| 734 |
+
.lib-item:hover { background: var(--surface2); }
|
| 735 |
+
.lib-item.selected { background: var(--surface2); border-color: var(--border-hover); }
|
| 736 |
+
.lib-item .voice-avatar { width: 32px; height: 32px; border-radius: 8px; font-size: 14px; }
|
| 737 |
+
.lib-item-meta { display: flex; flex-direction: column; min-width: 0; }
|
| 738 |
+
.lib-item-name { font-size: 13px; font-weight: 500; color: var(--text); }
|
| 739 |
+
.lib-item-tag { font-size: 11px; color: var(--text-muted); }
|
| 740 |
+
.lib-empty { padding: 24px 12px; text-align: center; color: var(--text-muted); font-size: 13px; }
|
| 741 |
</style>
|
| 742 |
</head>
|
| 743 |
<body>
|
|
|
|
| 2814 |
if (upload) upload.style.display = mode === "upload" ? "" : "none";
|
| 2815 |
const recordPane = document.getElementById(`${prefix}-record-pane`);
|
| 2816 |
if (recordPane) recordPane.style.display = mode === "record" ? "" : "none";
|
|
|
|
| 2817 |
const libraryPane = document.getElementById(`${prefix}-library-pane`);
|
| 2818 |
+
if (libraryPane) {
|
| 2819 |
+
libraryPane.style.display = mode === "library" ? "" : "none";
|
| 2820 |
+
if (mode === "library" && prefix === "vc-target") renderVcTargetLibrary();
|
| 2821 |
+
}
|
| 2822 |
+
};
|
| 2823 |
+
|
| 2824 |
+
async function renderVcTargetLibrary() {
|
| 2825 |
+
const list = document.getElementById("vc-target-library-list");
|
| 2826 |
+
if (!list) return;
|
| 2827 |
+
const voices = await getVoices();
|
| 2828 |
+
const usedPresetNames = new Set(voices.filter(v => v.isPreset).map(v => v.fileName.replace(/\.wav$/, "")));
|
| 2829 |
+
const availablePresets = PRESETS.filter(p => !usedPresetNames.has(p.name));
|
| 2830 |
+
|
| 2831 |
+
const userHtml = voices.length ? voices.map(v => `
|
| 2832 |
+
<div class="lib-item" data-id="${v.id}" onclick="pickVcTargetVoice('${v.id}')">
|
| 2833 |
+
<div class="voice-avatar" style="background:${v.color}">${v.name[0].toUpperCase()}</div>
|
| 2834 |
+
<div class="lib-item-meta">
|
| 2835 |
+
<div class="lib-item-name">${v.name}</div>
|
| 2836 |
+
<div class="lib-item-tag">${v.isPreset ? "Preset" : "Your voice"}</div>
|
| 2837 |
+
</div>
|
| 2838 |
+
</div>`).join("") : "";
|
| 2839 |
+
|
| 2840 |
+
const presetHtml = availablePresets.map(p => `
|
| 2841 |
+
<div class="lib-item" data-id="preset_${p.name}" onclick="pickVcTargetPreset('${p.name}')">
|
| 2842 |
+
<div class="voice-avatar" style="background:#d97706">${p.name[0]}</div>
|
| 2843 |
+
<div class="lib-item-meta">
|
| 2844 |
+
<div class="lib-item-name">${p.name.replace("_", " ")}</div>
|
| 2845 |
+
<div class="lib-item-tag">${p.tag}</div>
|
| 2846 |
+
</div>
|
| 2847 |
+
</div>`).join("");
|
| 2848 |
+
|
| 2849 |
+
list.innerHTML = userHtml + presetHtml || `<div class="lib-empty">No voices yet — add one in Voice Clone or Voice Design.</div>`;
|
| 2850 |
+
}
|
| 2851 |
+
|
| 2852 |
+
function markVcTargetSelected(id) {
|
| 2853 |
+
document.querySelectorAll("#vc-target-library-list .lib-item").forEach(el => el.classList.toggle("selected", el.dataset.id === id));
|
| 2854 |
+
}
|
| 2855 |
+
|
| 2856 |
+
window.pickVcTargetVoice = async function(id) {
|
| 2857 |
+
const voices = await getVoices();
|
| 2858 |
+
const v = voices.find(x => x.id === id);
|
| 2859 |
+
if (!v) return;
|
| 2860 |
+
const blob = new Blob([v.audioData], { type: v.audioType || "audio/wav" });
|
| 2861 |
+
vcTarget = new File([blob], v.fileName || `${v.name}.wav`, { type: blob.type });
|
| 2862 |
+
document.getElementById("vc-target-label").textContent = v.name;
|
| 2863 |
+
document.getElementById("vc-target-upload").classList.add("has-file");
|
| 2864 |
+
markVcTargetSelected(id);
|
| 2865 |
+
updateVcBtn();
|
| 2866 |
+
};
|
| 2867 |
+
|
| 2868 |
+
window.pickVcTargetPreset = async function(name) {
|
| 2869 |
+
const res = await fetch(`samples/${name}.wav`);
|
| 2870 |
+
const blob = await res.blob();
|
| 2871 |
+
vcTarget = new File([blob], `${name}.wav`, { type: blob.type || "audio/wav" });
|
| 2872 |
+
document.getElementById("vc-target-label").textContent = name.replace("_", " ");
|
| 2873 |
+
document.getElementById("vc-target-upload").classList.add("has-file");
|
| 2874 |
+
markVcTargetSelected(`preset_${name}`);
|
| 2875 |
+
updateVcBtn();
|
| 2876 |
};
|
| 2877 |
|
| 2878 |
window.toggleInputRecord = async function(prefix) {
|
|
|
|
| 2958 |
</div>
|
| 2959 |
<audio id="${prefix}-preview-audio" controls style="display:none;width:100%;margin-top:10px;border-radius:8px"></audio>
|
| 2960 |
</div>
|
| 2961 |
+
${opts.includeLibrary ? `<div id="${prefix}-library-pane" style="display:none;margin-bottom:16px"><div class="lib-list" id="${prefix}-library-list"></div></div>` : ""}
|
| 2962 |
`);
|
| 2963 |
}
|
| 2964 |
|
|
|
|
| 3085 |
wireUploadArea("vc-source-upload", "vc-source-input", "vc-source-label", f => { vcSource = f; updateVcBtn(); });
|
| 3086 |
wireUploadArea("vc-target-upload", "vc-target-input", "vc-target-label", f => { vcTarget = f; updateVcBtn(); });
|
| 3087 |
attachAudioInput("vc-source", f => { vcSource = f; document.getElementById("vc-source-label").textContent = f.name; document.getElementById("vc-source-upload").classList.add("has-file"); updateVcBtn(); });
|
| 3088 |
+
attachAudioInput("vc-target", f => { vcTarget = f; document.getElementById("vc-target-label").textContent = f.name; document.getElementById("vc-target-upload").classList.add("has-file"); updateVcBtn(); }, { includeLibrary: true });
|
| 3089 |
setupPlayer("vc");
|
| 3090 |
|
| 3091 |
window.voiceChange = async function() {
|