/** Voice recording + API helpers for PitchFight AI Phase 7. */ let mediaRecorder = null; let audioChunks = []; let recordTimerInterval = null; let recordStartMs = 0; let currentRecordMode = null; // "pitch" | "turn" const MIME_CANDIDATES = [ "audio/webm;codecs=opus", "audio/webm", "audio/ogg;codecs=opus", "audio/mp4", ]; function pickMimeType() { if (!window.MediaRecorder) return ""; for (const m of MIME_CANDIDATES) { if (MediaRecorder.isTypeSupported(m)) return m; } return ""; } function mimeToFormat(mime) { if (!mime) return "webm"; if (mime.includes("ogg")) return "ogg"; if (mime.includes("mp4")) return "m4a"; return "webm"; } export function blobToBase64(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => { const dataUrl = reader.result || ""; const base64 = String(dataUrl).split(",")[1] || ""; resolve(base64); }; reader.onerror = reject; reader.readAsDataURL(blob); }); } async function apiPost(path, body) { const response = await fetch(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(data.error || data.detail || response.statusText); } return data; } export async function sendVoicePitch(base64, format) { return apiPost("/api/voice-pitch", { audio: base64, audio_format: format }); } export async function sendVoiceTurn(sessionId, base64, format) { return apiPost("/api/voice-turn", { session_id: sessionId, audio: base64, audio_format: format, }); } function formatTimer(seconds) { const m = Math.floor(seconds / 60); const s = seconds % 60; return `${m}:${String(s).padStart(2, "0")}`; } function _retryModeActive() { const overlay = document.getElementById("retry-overlay"); return overlay && !overlay.hidden; } function _dealModeActive() { const dealScreen = document.getElementById("screen-deal"); return dealScreen?.classList.contains("active"); } function _voiceContext() { if (_dealModeActive()) return "deal"; if (_retryModeActive()) return "retry"; return "battle"; } function setRecordingUI(active, mode) { const pitchBtn = document.getElementById("btn-voice-pitch-record"); const turnBtn = document.getElementById("btn-voice-turn-record"); const retryBtn = document.getElementById("btn-retry-voice-record"); const dealBtn = document.getElementById("btn-deal-voice-record"); const pitchStatus = document.getElementById("voice-pitch-status"); const turnStatus = document.getElementById("voice-turn-status"); const retryStatus = document.getElementById("retry-voice-status"); const dealStatus = document.getElementById("deal-voice-status"); const pitchTimer = document.getElementById("voice-pitch-timer"); const turnTimer = document.getElementById("voice-turn-timer"); const retryTimer = document.getElementById("retry-voice-timer"); const dealTimer = document.getElementById("deal-voice-timer"); const ctx = _voiceContext(); const retryRecording = active && mode === "turn" && ctx === "retry"; const dealRecording = active && mode === "turn" && ctx === "deal"; [pitchBtn, turnBtn, retryBtn, dealBtn].forEach((btn) => btn?.classList.remove("recording")); if (active && mode === "pitch" && pitchBtn) pitchBtn.classList.add("recording"); if (active && mode === "turn" && ctx === "battle" && turnBtn) turnBtn.classList.add("recording"); if (retryRecording && retryBtn) retryBtn.classList.add("recording"); if (dealRecording && dealBtn) dealBtn.classList.add("recording"); if (pitchStatus) pitchStatus.textContent = active && mode === "pitch" ? "Recording…" : ""; if (turnStatus) turnStatus.textContent = active && mode === "turn" && ctx === "battle" ? "Recording…" : ""; if (retryStatus) retryStatus.textContent = retryRecording ? "Recording…" : ""; if (dealStatus) dealStatus.textContent = dealRecording ? "Recording…" : ""; if (!active) { if (pitchTimer) pitchTimer.textContent = "0:00"; if (turnTimer) turnTimer.textContent = "0:00"; if (retryTimer) retryTimer.textContent = "0:00"; if (dealTimer) dealTimer.textContent = "0:00"; } } function startTimer(el) { recordStartMs = Date.now(); clearInterval(recordTimerInterval); recordTimerInterval = setInterval(() => { const sec = Math.floor((Date.now() - recordStartMs) / 1000); if (el) el.textContent = formatTimer(sec); }, 250); } function stopTimer() { clearInterval(recordTimerInterval); recordTimerInterval = null; } async function startRecording(mode) { if (mediaRecorder?.state === "recording") return; currentRecordMode = mode; const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const mime = pickMimeType(); mediaRecorder = mime ? new MediaRecorder(stream, { mimeType: mime }) : new MediaRecorder(stream); audioChunks = []; mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) audioChunks.push(e.data); }; mediaRecorder.start(200); let timerEl = document.getElementById("voice-pitch-timer"); if (mode === "turn") { const ctx = _voiceContext(); timerEl = ctx === "deal" ? document.getElementById("deal-voice-timer") : ctx === "retry" ? document.getElementById("retry-voice-timer") : document.getElementById("voice-turn-timer"); } startTimer(timerEl); setRecordingUI(true, mode); } async function stopRecording() { if (!mediaRecorder || mediaRecorder.state !== "recording") { return { blob: null, format: "webm", mode: currentRecordMode }; } const mode = currentRecordMode; const mime = mediaRecorder.mimeType || "audio/webm"; const format = mimeToFormat(mime); return new Promise((resolve) => { mediaRecorder.onstop = () => { stopTimer(); setRecordingUI(false, mode); mediaRecorder.stream.getTracks().forEach((t) => t.stop()); const blob = new Blob(audioChunks, { type: mime }); audioChunks = []; resolve({ blob, format, mode }); }; mediaRecorder.stop(); }); } async function _handleTurnRecording(handlers, onComplete) { if (mediaRecorder?.state === "recording" && currentRecordMode === "turn") { const { blob, format } = await stopRecording(); if (!blob?.size) throw new Error("No audio captured."); const base64 = await blobToBase64(blob); const sessionId = handlers.getSessionId?.(); if (!sessionId) throw new Error("No active session."); const data = await sendVoiceTurn(sessionId, base64, format); if (data.error) throw new Error(data.error); onComplete?.(data); } else { await startRecording("turn"); } } export function initVoiceUI(handlers = {}) { const { onPitchComplete, onTurnComplete, onRetryTurnComplete, onDealTurnComplete, onError, } = handlers; const resolveTurnComplete = (data) => { const ctx = handlers.getUiMode?.() || _voiceContext(); if (ctx === "deal") onDealTurnComplete?.(data); else if (ctx === "retry") (onRetryTurnComplete ?? onTurnComplete)?.(data); else onTurnComplete?.(data); }; document.getElementById("btn-voice-pitch-record")?.addEventListener("click", async () => { try { if (mediaRecorder?.state === "recording" && currentRecordMode === "pitch") { const { blob, format } = await stopRecording(); if (!blob?.size) throw new Error("No audio captured."); const base64 = await blobToBase64(blob); const data = await sendVoicePitch(base64, format); if (data.error) throw new Error(data.error); onPitchComplete?.(data); } else { await startRecording("pitch"); } } catch (err) { onError?.(err.message || String(err)); } }); document.getElementById("btn-voice-pitch-cancel")?.addEventListener("click", async () => { if (mediaRecorder?.state === "recording") { mediaRecorder.onstop = () => { stopTimer(); setRecordingUI(false, "pitch"); mediaRecorder?.stream?.getTracks().forEach((t) => t.stop()); }; mediaRecorder.stop(); } }); document.getElementById("btn-voice-turn-record")?.addEventListener("click", async () => { try { await _handleTurnRecording(handlers, resolveTurnComplete); } catch (err) { onError?.(err.message || String(err)); } }); document.getElementById("btn-retry-voice-record")?.addEventListener("click", async () => { try { await _handleTurnRecording(handlers, resolveTurnComplete); } catch (err) { onError?.(err.message || String(err)); } }); document.getElementById("btn-deal-voice-record")?.addEventListener("click", async () => { try { await _handleTurnRecording(handlers, resolveTurnComplete); } catch (err) { onError?.(err.message || String(err)); } }); document.getElementById("btn-voice-turn-cancel")?.addEventListener("click", async () => { if (mediaRecorder?.state === "recording") { mediaRecorder.onstop = () => { stopTimer(); setRecordingUI(false, "turn"); mediaRecorder?.stream?.getTracks().forEach((t) => t.stop()); }; mediaRecorder.stop(); } document.getElementById("voice-turn-preview")?.setAttribute("hidden", ""); }); } export function startVoicePitchRecording() { return startRecording("pitch"); } export function stopVoicePitchRecording() { return stopRecording(); } export function startVoiceTurnRecording() { return startRecording("turn"); } export function stopVoiceTurnRecording() { return stopRecording(); }