Spaces:
Running
Running
Commit ·
0935499
1
Parent(s): 6bad670
Mic recording option on every audio input (Voice Changer src/tgt, Isolator, Transcribe)
Browse files- index.html +112 -0
index.html
CHANGED
|
@@ -2099,6 +2099,7 @@ function handleTranscribeFile(file) {
|
|
| 2099 |
transcribeUpload.classList.add("has-file");
|
| 2100 |
transcribeBtn.disabled = false;
|
| 2101 |
}
|
|
|
|
| 2102 |
|
| 2103 |
window.transcribe = async function() {
|
| 2104 |
if (!transcribeFile) return;
|
|
@@ -2791,6 +2792,109 @@ function wireUploadArea(areaId, inputId, labelId, onFile) {
|
|
| 2791 |
});
|
| 2792 |
}
|
| 2793 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2794 |
function setupPlayer(prefix) {
|
| 2795 |
const el = document.getElementById(`${prefix}-audio-player`);
|
| 2796 |
el.addEventListener("play", () => {
|
|
@@ -2913,6 +3017,8 @@ function updateVcBtn() {
|
|
| 2913 |
}
|
| 2914 |
wireUploadArea("vc-source-upload", "vc-source-input", "vc-source-label", f => { vcSource = f; updateVcBtn(); });
|
| 2915 |
wireUploadArea("vc-target-upload", "vc-target-input", "vc-target-label", f => { vcTarget = f; updateVcBtn(); });
|
|
|
|
|
|
|
| 2916 |
setupPlayer("vc");
|
| 2917 |
|
| 2918 |
window.voiceChange = async function() {
|
|
@@ -2968,6 +3074,12 @@ wireUploadArea("iso-upload", "iso-file-input", "iso-upload-label", f => {
|
|
| 2968 |
isoFile = f;
|
| 2969 |
document.getElementById("iso-btn").disabled = false;
|
| 2970 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2971 |
setupPlayer("iso");
|
| 2972 |
|
| 2973 |
window.voiceIsolate = async function() {
|
|
|
|
| 2099 |
transcribeUpload.classList.add("has-file");
|
| 2100 |
transcribeBtn.disabled = false;
|
| 2101 |
}
|
| 2102 |
+
attachAudioInput("transcribe", handleTranscribeFile);
|
| 2103 |
|
| 2104 |
window.transcribe = async function() {
|
| 2105 |
if (!transcribeFile) return;
|
|
|
|
| 2792 |
});
|
| 2793 |
}
|
| 2794 |
|
| 2795 |
+
// ── Per-input audio recording (mic alternative on every audio upload) ──
|
| 2796 |
+
|
| 2797 |
+
const inputRecorders = {}; // prefix -> { mediaRecorder, stream, chunks, timerInterval, startTime }
|
| 2798 |
+
const inputRecordHandlers = {}; // prefix -> (file) => void
|
| 2799 |
+
|
| 2800 |
+
window.setInputMode = function(prefix, mode) {
|
| 2801 |
+
document.querySelectorAll(`.audio-source-tabs[data-input-prefix="${prefix}"] .audio-source-tab`)
|
| 2802 |
+
.forEach(t => t.classList.toggle("active", t.dataset.mode === mode));
|
| 2803 |
+
const upload = document.getElementById(`${prefix}-upload`);
|
| 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) libraryPane.style.display = mode === "library" ? "" : "none";
|
| 2810 |
+
};
|
| 2811 |
+
|
| 2812 |
+
window.toggleInputRecord = async function(prefix) {
|
| 2813 |
+
const btn = document.getElementById(`${prefix}-record-btn`);
|
| 2814 |
+
const hint = document.getElementById(`${prefix}-hint`);
|
| 2815 |
+
const r = inputRecorders[prefix];
|
| 2816 |
+
if (r && r.mediaRecorder && r.mediaRecorder.state === "recording") {
|
| 2817 |
+
r.mediaRecorder.stop();
|
| 2818 |
+
return;
|
| 2819 |
+
}
|
| 2820 |
+
if (!navigator.mediaDevices?.getUserMedia) { hint.textContent = "Recording not supported in this browser"; return; }
|
| 2821 |
+
let stream;
|
| 2822 |
+
try { stream = await navigator.mediaDevices.getUserMedia({ audio: true }); }
|
| 2823 |
+
catch { hint.textContent = "Microphone access denied"; return; }
|
| 2824 |
+
const mimeType = pickRecorderMimeType();
|
| 2825 |
+
const mr = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream);
|
| 2826 |
+
const chunks = [];
|
| 2827 |
+
mr.ondataavailable = e => { if (e.data?.size) chunks.push(e.data); };
|
| 2828 |
+
const startTime = Date.now();
|
| 2829 |
+
const timerInterval = setInterval(() => {
|
| 2830 |
+
const e = Math.floor((Date.now() - startTime) / 1000);
|
| 2831 |
+
document.getElementById(`${prefix}-timer`).textContent = `${Math.floor(e/60)}:${(e%60).toString().padStart(2,"0")}`;
|
| 2832 |
+
}, 200);
|
| 2833 |
+
inputRecorders[prefix] = { mediaRecorder: mr, stream, chunks, timerInterval, startTime };
|
| 2834 |
+
mr.onstop = () => {
|
| 2835 |
+
clearInterval(timerInterval);
|
| 2836 |
+
btn.classList.remove("recording");
|
| 2837 |
+
const elapsedSec = (Date.now() - startTime) / 1000;
|
| 2838 |
+
const type = mr.mimeType || "audio/webm";
|
| 2839 |
+
const blob = new Blob(chunks, { type });
|
| 2840 |
+
const ext = type.includes("mp4") ? "m4a" : type.includes("ogg") ? "ogg" : "webm";
|
| 2841 |
+
const file = new File([blob], `recording_${Date.now()}.${ext}`, { type });
|
| 2842 |
+
const preview = document.getElementById(`${prefix}-preview-audio`);
|
| 2843 |
+
preview.src = URL.createObjectURL(blob);
|
| 2844 |
+
preview.style.display = "";
|
| 2845 |
+
hint.textContent = `Recorded ${elapsedSec.toFixed(1)}s · click mic to re-record`;
|
| 2846 |
+
stream.getTracks().forEach(t => t.stop());
|
| 2847 |
+
delete inputRecorders[prefix];
|
| 2848 |
+
inputRecordHandlers[prefix]?.(file);
|
| 2849 |
+
};
|
| 2850 |
+
hint.textContent = "Recording… click again to stop";
|
| 2851 |
+
btn.classList.add("recording");
|
| 2852 |
+
mr.start();
|
| 2853 |
+
};
|
| 2854 |
+
|
| 2855 |
+
// Attach an Upload/Record toggle around an existing .upload-area
|
| 2856 |
+
function attachAudioInput(prefix, onFile, opts = {}) {
|
| 2857 |
+
const uploadId = opts.uploadId || `${prefix}-upload`;
|
| 2858 |
+
const area = document.getElementById(uploadId);
|
| 2859 |
+
if (!area) return;
|
| 2860 |
+
inputRecordHandlers[prefix] = onFile;
|
| 2861 |
+
|
| 2862 |
+
const extraTab = opts.includeLibrary ? `
|
| 2863 |
+
<button class="audio-source-tab" data-mode="library" type="button" onclick="setInputMode('${prefix}', 'library')">
|
| 2864 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
| 2865 |
+
Library
|
| 2866 |
+
</button>` : "";
|
| 2867 |
+
|
| 2868 |
+
area.insertAdjacentHTML("beforebegin", `
|
| 2869 |
+
<div class="audio-source-tabs" data-input-prefix="${prefix}">
|
| 2870 |
+
<button class="audio-source-tab active" data-mode="upload" type="button" onclick="setInputMode('${prefix}', 'upload')">
|
| 2871 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
| 2872 |
+
Upload
|
| 2873 |
+
</button>
|
| 2874 |
+
<button class="audio-source-tab" data-mode="record" type="button" onclick="setInputMode('${prefix}', 'record')">
|
| 2875 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 14 0"/><line x1="12" y1="18" x2="12" y2="22"/></svg>
|
| 2876 |
+
Record
|
| 2877 |
+
</button>
|
| 2878 |
+
${extraTab}
|
| 2879 |
+
</div>
|
| 2880 |
+
`);
|
| 2881 |
+
|
| 2882 |
+
area.insertAdjacentHTML("afterend", `
|
| 2883 |
+
<div id="${prefix}-record-pane" style="display:none;margin-bottom:16px">
|
| 2884 |
+
<div class="record-pane">
|
| 2885 |
+
<button class="record-mic-btn" id="${prefix}-record-btn" type="button" onclick="toggleInputRecord('${prefix}')">
|
| 2886 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 14 0"/><line x1="12" y1="18" x2="12" y2="22"/></svg>
|
| 2887 |
+
</button>
|
| 2888 |
+
<div class="record-meta">
|
| 2889 |
+
<div class="record-timer" id="${prefix}-timer">0:00</div>
|
| 2890 |
+
<div class="record-hint" id="${prefix}-hint">Click to record · 5-30s recommended</div>
|
| 2891 |
+
</div>
|
| 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 |
+
|
| 2898 |
function setupPlayer(prefix) {
|
| 2899 |
const el = document.getElementById(`${prefix}-audio-player`);
|
| 2900 |
el.addEventListener("play", () => {
|
|
|
|
| 3017 |
}
|
| 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() {
|
|
|
|
| 3074 |
isoFile = f;
|
| 3075 |
document.getElementById("iso-btn").disabled = false;
|
| 3076 |
});
|
| 3077 |
+
attachAudioInput("iso", f => {
|
| 3078 |
+
isoFile = f;
|
| 3079 |
+
document.getElementById("iso-upload-label").textContent = f.name;
|
| 3080 |
+
document.getElementById("iso-upload").classList.add("has-file");
|
| 3081 |
+
document.getElementById("iso-btn").disabled = false;
|
| 3082 |
+
});
|
| 3083 |
setupPlayer("iso");
|
| 3084 |
|
| 3085 |
window.voiceIsolate = async function() {
|