From 228391f28080a907f24374f0b23926c97ccc2960 Mon Sep 17 00:00:00 2001 From: balex Date: Thu, 12 Mar 2026 12:33:19 +0100 Subject: [PATCH] fix --- src/public/js/app.js | 93 +++++++++++++++++++++++++++++++++++--------- src/server.js | 37 ++++++++++++++++++ 2 files changed, 112 insertions(+), 18 deletions(-) diff --git a/src/public/js/app.js b/src/public/js/app.js index e27b4a3..a5a94b6 100644 --- a/src/public/js/app.js +++ b/src/public/js/app.js @@ -138,7 +138,10 @@ speakBtn.addEventListener('click', () => { autoSpeak(); }); -// ── Speech recognition ──────────────────────────────────────────────────────── +// ── Device detection ────────────────────────────────────────────────────────── +const isMobile = /Android|iPhone|iPad/i.test(navigator.userAgent); + +// ── Speech recognition (desktop only) ──────────────────────────────────────── const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; let recognition = null; @@ -201,27 +204,45 @@ let audioChunks = []; let audioBlob = null; async function startRecording() { - if (!SpeechRecognition) { - alert('Spracherkennung wird in diesem Browser nicht unterstützt. Bitte nutze Chrome oder Edge.'); - return; - } stopAudio(); audioBlob = null; audioChunks = []; downloadBtn.disabled = true; - - // Start SpeechRecognition first — triggers mic permission dialog on Android transcriptBox.contentEditable = 'false'; - recognition = createRecognition(); - state.isRecording = true; - try { recognition.start(); } catch (_) {} - clearTimeout(recordingTimer); - recordingTimer = setTimeout(stopRecording, MAX_RECORD_SECONDS * 1000); + if (isMobile) { + // ── Mobile: MediaRecorder → Whisper ─────────────────────────────────────── + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + const mimeType = MediaRecorder.isTypeSupported('audio/mp4') ? 'audio/mp4' + : MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' + : ''; + mediaRecorder = new MediaRecorder(stream, mimeType ? { mimeType } : {}); + mediaRecorder.ondataavailable = e => { if (e.data.size > 0) audioChunks.push(e.data); }; + mediaRecorder.onstop = async () => { + stream.getTracks().forEach(t => t.stop()); + const blob = new Blob(audioChunks, { type: mediaRecorder._mimeType || mimeType || 'audio/webm' }); + await transcribeAudio(blob); + }; + mediaRecorder._mimeType = mimeType; // stash for onstop + mediaRecorder.start(); + state.isRecording = true; + recordBtn.classList.add('recording'); + recordHint.textContent = 'Tippen zum Stoppen'; + } catch (e) { + alert('Mikrofon nicht verfügbar: ' + e.message); + return; + } + } else { + // ── Desktop: Web Speech API ──────────────────────────────────────────────── + if (!SpeechRecognition) { + alert('Spracherkennung wird in diesem Browser nicht unterstützt. Bitte nutze Chrome oder Edge.'); + return; + } + recognition = createRecognition(); + state.isRecording = true; + try { recognition.start(); } catch (_) {} - // MediaRecorder for audio download — desktop only; on mobile it conflicts with SpeechRecognition - const isMobile = /Android|iPhone|iPad/i.test(navigator.userAgent); - if (!isMobile) { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') @@ -238,6 +259,9 @@ async function startRecording() { console.warn('MediaRecorder unavailable:', e); } } + + clearTimeout(recordingTimer); + recordingTimer = setTimeout(stopRecording, MAX_RECORD_SECONDS * 1000); } function stopRecording() { @@ -248,12 +272,45 @@ function stopRecording() { recognition = null; } if (mediaRecorder && mediaRecorder.state !== 'inactive') { - mediaRecorder.stop(); + mediaRecorder.stop(); // onstop handles the rest (incl. transcribeAudio on mobile) mediaRecorder = null; } recordBtn.classList.remove('recording'); - recordHint.textContent = 'Tippen zum Aufnehmen'; - transcriptBox.contentEditable = 'true'; + if (!isMobile) { + recordHint.textContent = 'Tippen zum Aufnehmen'; + transcriptBox.contentEditable = 'true'; + } + // On mobile, recordHint and contentEditable are updated inside transcribeAudio +} + +async function transcribeAudio(blob) { + recordBtn.disabled = true; + recordHint.textContent = 'Transkribiere…'; + + try { + const res = await authFetch('api/transcribe', { + method: 'POST', + headers: { 'Content-Type': blob.type || 'audio/webm' }, + body: blob, + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Fehler'); + + const text = (data.transcript || '').trim(); + state.transcript = text; + state.finalTranscript = text; + updateTranscriptBox(text); + checkBtn.disabled = !text; + transcriptBox.contentEditable = 'true'; + } catch (err) { + console.error('Transcription failed:', err); + updateTranscriptBox(''); + recordHint.textContent = 'Fehler – nochmal versuchen'; + } finally { + recordBtn.disabled = false; + if (!state.transcript) recordHint.textContent = 'Tippen zum Aufnehmen'; + else recordHint.textContent = 'Tippen zum Aufnehmen'; + } } recordBtn.addEventListener('click', () => state.isRecording ? stopRecording() : startRecording()); diff --git a/src/server.js b/src/server.js index 38ce7f8..e9021e0 100644 --- a/src/server.js +++ b/src/server.js @@ -4,6 +4,7 @@ const path = require("path"); const app = express(); const PORT = process.env.PORT || 8083; const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; +const OPENAI_API_KEY = process.env.OPENAI_API_KEY; app.use(express.json()); app.use(express.static(path.join(__dirname, "public"))); @@ -14,6 +15,42 @@ const LEVEL_DESCRIPTIONS = { B2: "выше среднего (B2): детально анализируй смысл, структуру предложений, стиль и естественность речи, предлагай альтернативные формулировки", }; +// Audio is sent as raw binary body (Content-Type: audio/*) +app.post("/api/transcribe", express.raw({ type: "audio/*", limit: "25mb" }), async (req, res) => { + if (!req.body || !req.body.length) { + return res.status(400).json({ error: "No audio data" }); + } + if (!OPENAI_API_KEY) { + return res.status(500).json({ error: "OpenAI API key not configured" }); + } + + try { + const mimeType = req.headers["content-type"] || "audio/webm"; + const form = new FormData(); + form.append("file", new Blob([req.body], { type: mimeType }), "audio.webm"); + form.append("model", "whisper-1"); + form.append("language", "de"); + + const response = await fetch("https://api.openai.com/v1/audio/transcriptions", { + method: "POST", + headers: { Authorization: `Bearer ${OPENAI_API_KEY}` }, + body: form, + }); + + if (!response.ok) { + const err = await response.text(); + console.error("Whisper API error:", response.status, err); + return res.status(502).json({ error: "Transcription service error" }); + } + + const data = await response.json(); + res.json({ transcript: data.text || "" }); + } catch (err) { + console.error("Transcribe request failed:", err); + res.status(500).json({ error: "Internal server error" }); + } +}); + app.post("/api/check", async (req, res) => { const { question, answer, level } = req.body;