apolinario commited on
Commit
0935499
·
1 Parent(s): 6bad670

Mic recording option on every audio input (Voice Changer src/tgt, Isolator, Transcribe)

Browse files
Files changed (1) hide show
  1. 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() {