/* app.js — VoiceScript Full frontend logic: upload, record, transcribe, translate, timestamps */ const API_URL = ""; // ── Global state ─────────────────────────────────────────── let mediaRecorder = null; let audioChunks = []; let timerInterval = null; let secondsElapsed = 0; let currentMode = "transcribe"; let lastTranscript = ""; let lastSegments = []; let timestampsShowing = false; // Loading messages that rotate while processing const loadingMessages = [ "Starting beast mode pipeline...", "Converting audio format...", "Demucs isolating vocals from background...", "Whisper AI transcribing clean vocals...", "Almost there — processing final segments...", "Finalizing transcript..." ]; let loadingMsgInterval = null; // ═══════════════════════════════════════════════════════════ // INIT — runs on page load // ═══════════════════════════════════════════════════════════ async function loadLanguages() { try { const response = await fetch(`${API_URL}/languages`); const data = await response.json(); if (data.success) { const select = document.getElementById("lang-select"); const sorted = Object.entries(data.languages).sort((a, b) => a[1].localeCompare(b[1])); sorted.forEach(([code, name]) => { const opt = document.createElement("option"); opt.value = code; opt.textContent = name; select.appendChild(opt); }); } } catch (e) { console.log("Languages not loaded yet — server may be starting"); } } loadLanguages(); // ═══════════════════════════════════════════════════════════ // MODE TOGGLE (Transcribe / Any Language → English) // ═══════════════════════════════════════════════════════════ function setMode(mode) { currentMode = mode; document.getElementById("mode-transcribe").classList.toggle("active", mode === "transcribe"); document.getElementById("mode-translate").classList.toggle("active", mode === "translate_to_english"); } // ═══════════════════════════════════════════════════════════ // FILE UPLOAD // ═══════════════════════════════════════════════════════════ document.getElementById("audio-file-input").addEventListener("change", function () { const file = this.files[0]; if (file) { document.getElementById("file-name-display").textContent = file.name; document.getElementById("drop-text").textContent = "File selected:"; } }); async function uploadAndTranscribe() { const fileInput = document.getElementById("audio-file-input"); const file = fileInput.files[0]; if (!file) { alert("Please select an audio file first!"); return; } const formData = new FormData(); formData.append("audio", file); formData.append("mode", currentMode); showLoading(); try { const response = await fetch(`${API_URL}/transcribe`, { method: "POST", body: formData, }); const result = await response.json(); handleResult(result); } catch (error) { showError("Could not connect to backend. Is the server running? (python backend/app.py)"); } } // ═══════════════════════════════════════════════════════════ // MICROPHONE RECORDING // ═══════════════════════════════════════════════════════════ async function startRecording() { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const mimeType = MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/ogg"; mediaRecorder = new MediaRecorder(stream, { mimeType }); audioChunks = []; mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) audioChunks.push(event.data); }; mediaRecorder.onstop = async () => { const audioBlob = new Blob(audioChunks, { type: mimeType }); await sendAudioToBackend(audioBlob); stream.getTracks().forEach(track => track.stop()); }; mediaRecorder.start(); document.body.classList.add("recording"); document.getElementById("record-status").textContent = "Recording..."; document.getElementById("start-btn").disabled = true; document.getElementById("stop-btn").disabled = false; startTimer(); } catch (error) { showError("Microphone access denied. Please allow mic access in your browser."); } } function stopRecording() { if (mediaRecorder && mediaRecorder.state !== "inactive") { mediaRecorder.stop(); } document.body.classList.remove("recording"); document.getElementById("record-status").textContent = "Processing..."; document.getElementById("start-btn").disabled = false; document.getElementById("stop-btn").disabled = true; stopTimer(); showLoading(); } async function sendAudioToBackend(audioBlob) { const formData = new FormData(); const ext = audioBlob.type.includes("ogg") ? "ogg" : "webm"; formData.append("audio", audioBlob, `recording.${ext}`); formData.append("mode", currentMode); try { const response = await fetch(`${API_URL}/transcribe`, { method: "POST", body: formData, }); const result = await response.json(); handleResult(result); } catch (error) { showError("Could not connect to backend. Is the server running?"); } document.getElementById("record-status").textContent = "Ready to record"; } // ═══════════════════════════════════════════════════════════ // RESULT HANDLING // ═══════════════════════════════════════════════════════════ function handleResult(result) { hideLoading(); if (result.success) { lastSegments = result.segments || []; showTranscript( result.transcript, result.duration, result.word_count, result.detected_language_name ); } else { showError(result.error); } } function showTranscript(text, duration, wordCount, detectedLang) { // Show result section document.getElementById("results-section").classList.add("visible"); document.getElementById("result-box").classList.add("visible"); document.getElementById("error-box").classList.remove("visible"); // Set plain transcript text document.getElementById("transcript-text").textContent = text; document.getElementById("transcript-text").style.display = "block"; // Reset timestamps view document.getElementById("timestamps-view").style.display = "none"; timestampsShowing = false; // Word count + duration meta let meta = `${wordCount || text.trim().split(/\s+/).length} words`; if (duration) meta += ` · ${duration}s audio`; document.getElementById("word-count").textContent = meta; // Language badge const langBadge = document.getElementById("lang-badge"); if (detectedLang && detectedLang !== "Unknown") { langBadge.textContent = "🌐 " + detectedLang + " detected"; langBadge.style.display = "inline-block"; } else { langBadge.style.display = "none"; } // Timestamps button — show only if we have segments const tsBtn = document.getElementById("timestamps-btn"); if (lastSegments.length > 0) { tsBtn.style.display = "inline-block"; tsBtn.textContent = "Show Timestamps"; tsBtn.classList.remove("active"); } else { tsBtn.style.display = "none"; } // Store for translation lastTranscript = text; document.getElementById("translated-box").classList.remove("visible"); document.getElementById("translate-panel").classList.add("visible"); document.getElementById("results-section").scrollIntoView({ behavior: "smooth" }); } function showError(message) { document.getElementById("results-section").classList.add("visible"); document.getElementById("result-box").classList.remove("visible"); document.getElementById("error-text").textContent = message; document.getElementById("error-box").classList.add("visible"); document.getElementById("results-section").scrollIntoView({ behavior: "smooth" }); } function showLoading() { document.getElementById("results-section").classList.add("visible"); document.getElementById("loading-state").classList.add("visible"); document.getElementById("result-box").classList.remove("visible"); document.getElementById("error-box").classList.remove("visible"); let msgIndex = 0; const msgEl = document.getElementById("loading-msg"); if (msgEl) msgEl.textContent = loadingMessages[0]; loadingMsgInterval = setInterval(() => { msgIndex = (msgIndex + 1) % loadingMessages.length; if (msgEl) msgEl.textContent = loadingMessages[msgIndex]; }, 4000); } function hideLoading() { document.getElementById("loading-state").classList.remove("visible"); if (loadingMsgInterval) { clearInterval(loadingMsgInterval); loadingMsgInterval = null; } } function clearResults() { document.getElementById("results-section").classList.remove("visible"); document.getElementById("result-box").classList.remove("visible"); document.getElementById("error-box").classList.remove("visible"); document.getElementById("loading-state").classList.remove("visible"); document.getElementById("transcript-text").textContent = ""; document.getElementById("translate-panel").classList.remove("visible"); document.getElementById("translated-box").classList.remove("visible"); document.getElementById("timestamps-view").innerHTML = ""; lastTranscript = ""; lastSegments = []; timestampsShowing = false; } // ═══════════════════════════════════════════════════════════ // TIMESTAMPS // ═══════════════════════════════════════════════════════════ function toggleTimestamps() { timestampsShowing = !timestampsShowing; const btn = document.getElementById("timestamps-btn"); const plainView = document.getElementById("transcript-text"); const tsView = document.getElementById("timestamps-view"); if (timestampsShowing) { // Build the timestamped lines tsView.innerHTML = ""; lastSegments.forEach(seg => { const line = document.createElement("div"); line.className = "ts-line"; line.innerHTML = `${seg.start_fmt}${seg.text}`; tsView.appendChild(line); }); plainView.style.display = "none"; tsView.style.display = "flex"; btn.textContent = "Hide Timestamps"; btn.classList.add("active"); } else { plainView.style.display = "block"; tsView.style.display = "none"; btn.textContent = "Show Timestamps"; btn.classList.remove("active"); } } // ═══════════════════════════════════════════════════════════ // TRANSLATION // ═══════════════════════════════════════════════════════════ async function translateTranscript() { const langSelect = document.getElementById("lang-select"); const targetLang = langSelect.value; if (!targetLang) { alert("Please select a language first!"); return; } if (!lastTranscript) { alert("No transcript to translate. Please transcribe audio first."); return; } const btn = document.getElementById("translate-btn"); btn.textContent = "Translating..."; btn.disabled = true; try { const response = await fetch(`${API_URL}/translate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: lastTranscript, target_language: targetLang }) }); const result = await response.json(); if (result.success) { document.getElementById("translated-text").textContent = result.translated; document.getElementById("translated-lang-label").textContent = "Translated to " + result.language; document.getElementById("translated-box").classList.add("visible"); document.getElementById("translated-box").scrollIntoView({ behavior: "smooth" }); } else { alert("Translation failed: " + result.error); } } catch (error) { alert("Could not connect to translation service. Is the server running?"); } btn.textContent = "Translate"; btn.disabled = false; } function copyTranslation() { const text = document.getElementById("translated-text").textContent; navigator.clipboard.writeText(text).then(() => { const btns = document.querySelectorAll(".btn-copy"); if (btns[1]) { btns[1].textContent = "Copied!"; setTimeout(() => btns[1].textContent = "Copy", 2000); } }); } // ═══════════════════════════════════════════════════════════ // UTILITIES // ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════ // EXPORT FUNCTIONS — TXT, SRT, PDF // ═══════════════════════════════════════════════════════════ function downloadFile(filename, content, mimeType) { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function exportTXT() { if (!lastTranscript) { alert("No transcript to export!"); return; } const now = new Date(); const dateStr = now.toLocaleDateString("en-IN", { day:"2-digit", month:"short", year:"numeric" }); const words = lastTranscript.trim().split(/\s+/).length; const content = [ "VoiceScript — Transcript Export", "================================", "Date : " + dateStr, "Words : " + words, "", "TRANSCRIPT", "----------", lastTranscript, "", "--------------------------------", "Exported by VoiceScript · AI Speech Recognition", "https://huggingface.co/spaces/Tusharz/VoiceScript" ].join("\n"); downloadFile("VoiceScript_Transcript.txt", content, "text/plain"); } function secondsToSRT(totalSeconds) { const h = Math.floor(totalSeconds / 3600); const m = Math.floor((totalSeconds % 3600) / 60); const s = Math.floor(totalSeconds % 60); return String(h).padStart(2,"0") + ":" + String(m).padStart(2,"0") + ":" + String(s).padStart(2,"0") + ",000"; } function exportSRT() { if (!lastTranscript) { alert("No transcript to export!"); return; } let srtContent = ""; if (lastSegments && lastSegments.length > 0) { lastSegments.forEach(function(seg, i) { srtContent += (i + 1) + "\n" + secondsToSRT(seg.start) + " --> " + secondsToSRT(seg.end) + "\n" + seg.text + "\n\n"; }); } else { const words = lastTranscript.trim().split(/\s+/); const size = 10; let idx = 1; for (let i = 0; i < words.length; i += size) { const chunk = words.slice(i, i + size).join(" "); srtContent += idx + "\n" + secondsToSRT((idx-1)*5) + " --> " + secondsToSRT(idx*5) + "\n" + chunk + "\n\n"; idx++; } } downloadFile("VoiceScript_Subtitles.srt", srtContent, "text/plain"); } function exportPDF() { if (!lastTranscript) { alert("No transcript to export!"); return; } const now = new Date(); const dateStr = now.toLocaleDateString("en-IN", { day:"2-digit", month:"long", year:"numeric" }); const words = lastTranscript.trim().split(/\s+/).length; const printHTML = "