import { transcribeAudio } from '../../utils/chatTranscribe' /** * Synchronous speech-to-text for mobile memo fields. * * POST /api/mobile/transcribe-text * Body: multipart form — 'audio' file (webm/ogg/mp3/m4a/wav/aac/mp4), optional 'lang' (de|en|es, default auto) * Returns: { status: 200, transcript } — the caller shows/edits the text BEFORE it is * persisted anywhere. This differs from /api/mobile/transcribe, which queues the job and * appends "[VOICE: …]" to an existing R_Request asynchronously. * * Fail-soft: when whisper.cpp/ffmpeg are unavailable (dev Mac) the transcript is '' with * status 500 so the UI can fall back to typed text. */ const MAX_BYTES = 25 * 1024 * 1024 export default defineEventHandler(async (event) => { const formData = await readMultipartFormData(event) if (!formData) { return { status: 400, transcript: '', message: 'Multipart form data required' } } let audioBuffer: Buffer | null = null let audioExt = 'webm' let lang: string | undefined for (const part of formData) { if (part.name === 'audio') { audioBuffer = part.data // Never trust the client filename — only take a whitelisted extension from it. const extMatch = /\.(webm|ogg|oga|mp3|m4a|wav|aac|mp4)$/i.exec(part.filename || '') audioExt = extMatch ? extMatch[1].toLowerCase() : 'webm' } else if (part.name === 'lang') { const l = part.data.toString().trim().toLowerCase().slice(0, 2) if (['de', 'en', 'es'].includes(l)) lang = l } } if (!audioBuffer || audioBuffer.length === 0) { return { status: 400, transcript: '', message: 'Audio file required' } } if (audioBuffer.length > MAX_BYTES) { return { status: 413, transcript: '', message: 'Audio too large (max 25 MB)' } } const transcript = await transcribeAudio(audioBuffer, `memo.${audioExt}`, lang) if (!transcript) { return { status: 500, transcript: '', message: 'Transkription nicht verfügbar' } } return { status: 200, transcript } })