fix
This commit is contained in:
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user