fix
This commit is contained in:
@@ -138,7 +138,10 @@ speakBtn.addEventListener('click', () => {
|
|||||||
autoSpeak();
|
autoSpeak();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Speech recognition ────────────────────────────────────────────────────────
|
// ── Device detection ──────────────────────────────────────────────────────────
|
||||||
|
const isMobile = /Android|iPhone|iPad/i.test(navigator.userAgent);
|
||||||
|
|
||||||
|
// ── Speech recognition (desktop only) ────────────────────────────────────────
|
||||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||||
let recognition = null;
|
let recognition = null;
|
||||||
|
|
||||||
@@ -201,27 +204,45 @@ let audioChunks = [];
|
|||||||
let audioBlob = null;
|
let audioBlob = null;
|
||||||
|
|
||||||
async function startRecording() {
|
async function startRecording() {
|
||||||
if (!SpeechRecognition) {
|
|
||||||
alert('Spracherkennung wird in diesem Browser nicht unterstützt. Bitte nutze Chrome oder Edge.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stopAudio();
|
stopAudio();
|
||||||
audioBlob = null;
|
audioBlob = null;
|
||||||
audioChunks = [];
|
audioChunks = [];
|
||||||
downloadBtn.disabled = true;
|
downloadBtn.disabled = true;
|
||||||
|
|
||||||
// Start SpeechRecognition first — triggers mic permission dialog on Android
|
|
||||||
transcriptBox.contentEditable = 'false';
|
transcriptBox.contentEditable = 'false';
|
||||||
|
|
||||||
|
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();
|
recognition = createRecognition();
|
||||||
state.isRecording = true;
|
state.isRecording = true;
|
||||||
try { recognition.start(); } catch (_) {}
|
try { recognition.start(); } catch (_) {}
|
||||||
|
|
||||||
clearTimeout(recordingTimer);
|
|
||||||
recordingTimer = setTimeout(stopRecording, MAX_RECORD_SECONDS * 1000);
|
|
||||||
|
|
||||||
// MediaRecorder for audio download — desktop only; on mobile it conflicts with SpeechRecognition
|
|
||||||
const isMobile = /Android|iPhone|iPad/i.test(navigator.userAgent);
|
|
||||||
if (!isMobile) {
|
|
||||||
try {
|
try {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
|
const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
|
||||||
@@ -238,6 +259,9 @@ async function startRecording() {
|
|||||||
console.warn('MediaRecorder unavailable:', e);
|
console.warn('MediaRecorder unavailable:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearTimeout(recordingTimer);
|
||||||
|
recordingTimer = setTimeout(stopRecording, MAX_RECORD_SECONDS * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopRecording() {
|
function stopRecording() {
|
||||||
@@ -248,12 +272,45 @@ function stopRecording() {
|
|||||||
recognition = null;
|
recognition = null;
|
||||||
}
|
}
|
||||||
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
||||||
mediaRecorder.stop();
|
mediaRecorder.stop(); // onstop handles the rest (incl. transcribeAudio on mobile)
|
||||||
mediaRecorder = null;
|
mediaRecorder = null;
|
||||||
}
|
}
|
||||||
recordBtn.classList.remove('recording');
|
recordBtn.classList.remove('recording');
|
||||||
|
if (!isMobile) {
|
||||||
recordHint.textContent = 'Tippen zum Aufnehmen';
|
recordHint.textContent = 'Tippen zum Aufnehmen';
|
||||||
transcriptBox.contentEditable = 'true';
|
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());
|
recordBtn.addEventListener('click', () => state.isRecording ? stopRecording() : startRecording());
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const path = require("path");
|
|||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 8083;
|
const PORT = process.env.PORT || 8083;
|
||||||
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
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.json());
|
||||||
app.use(express.static(path.join(__dirname, "public")));
|
app.use(express.static(path.join(__dirname, "public")));
|
||||||
@@ -14,6 +15,42 @@ const LEVEL_DESCRIPTIONS = {
|
|||||||
B2: "выше среднего (B2): детально анализируй смысл, структуру предложений, стиль и естественность речи, предлагай альтернативные формулировки",
|
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) => {
|
app.post("/api/check", async (req, res) => {
|
||||||
const { question, answer, level } = req.body;
|
const { question, answer, level } = req.body;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user