From 41429206ac3d2c7044d79b014999a472a44e8bfb Mon Sep 17 00:00:00 2001 From: Chandu Date: Sat, 25 Jul 2026 11:55:40 -0400 Subject: [PATCH] Fix speech-to-text with Groq Whisper --- certs/localhost.crt | 17 ++++++++++ package-lock.json | 54 ++++++++++++++++++++++++++++++- package.json | 2 ++ script.js | 22 ++++++++++--- scripts/generate-cert.js | 21 ++++++++++++ server.js | 70 ++++++++++++++++++++++++++++------------ 6 files changed, 160 insertions(+), 26 deletions(-) create mode 100644 certs/localhost.crt create mode 100644 scripts/generate-cert.js diff --git a/certs/localhost.crt b/certs/localhost.crt new file mode 100644 index 0000000..43ff0c6 --- /dev/null +++ b/certs/localhost.crt @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICyTCCAbGgAwIBAgIJFHb8UETKGEeoMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV +BAMTCWxvY2FsaG9zdDAeFw0yNjA3MDUxNzI5MjRaFw0yNzA3MDUxNzI5MjRaMBQx +EjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAKuxx7CoAy1uKr888vjsoCvNaHz7o6BUTr6+HT3dAMAQuUIRhhNup/yLW6k/ +UbpBdJMPQmdu5bUen8UgsRosn/zm9aK8hg+7Zqsm3DGYgcG380lFbFLil/00mamy +jOi8qJD5V9gxFyi8g5yyE0PeAmJzEaslRztl3kzWWPrMaTniZ3lL9d1OujDLr0Z3 +LAFOiJWQroKK3ZCNNe4TcFburqvAg9CBG2m8EoL++XDtOKzIbj5xroQvniiU9he4 +5NjjNBhsunTTNw/PEazPqqCORWbmYKRD8PG330Iq1OlIo1foI/fjG7afwo8MwtCH +pzxcjV3Hc9dpLysmalwBK0ZmTl0CAwEAAaMeMBwwGgYDVR0RBBMwEYIJbG9jYWxo +b3N0hwR/AAABMA0GCSqGSIb3DQEBBQUAA4IBAQCWj7KBzjn3LOJOhXbF01m65JxU +r3d+4UgrvKQJ5dtMVq2r2HU4i9mOs+C50rNDlqZl5pD99xW/YbtM07nQMj7KLrm3 +WiKjxgxtCLcNhAZtmwbjk218gFp0WHSPJ/2gspUNkbYmoZTx8BAmjw1uC04PdF1q +upx50bAISYTGwQNCDB118i2hEikvcm6cicrfnBKu1r+rvhLAODrbc1VEHh3dy+PZ +2ac/dmTrSvqUJnDHr+8v6u2tbPewJq+6WQ3qHKDmVbEW2OIhA9G0toTA+WhwWd7q +syLQcvGMJY8+pQl+QGACY2xisRtOzExewYBB7FDu4X9fpheIW1xreN9dKzk3 +-----END CERTIFICATE----- diff --git a/package-lock.json b/package-lock.json index 12529de..82363c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,8 @@ "google-tts-api": "^2.0.1" }, "devDependencies": { - "puppeteer": "^25.3.0" + "puppeteer": "^25.3.0", + "selfsigned": "^2.1.0" } }, "node_modules/@puppeteer/browsers": { @@ -48,6 +49,26 @@ } } }, + "node_modules/@types/node": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -920,6 +941,16 @@ "node": ">= 0.6" } }, + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1096,6 +1127,20 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", @@ -1291,6 +1336,13 @@ "dev": true, "license": "MIT" }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index a76cac1..8c1373d 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,8 @@ "main": "server.js", "scripts": { "start": "node server.js", + "start:https": "SSL_KEY_PATH=certs/localhost.key SSL_CERT_PATH=certs/localhost.crt node server.js", + "generate-cert": "node scripts/generate-cert.js", "check:permissions": "node scripts/permission-check.js" }, "dependencies": { diff --git a/script.js b/script.js index 91ab760..f53f3f8 100644 --- a/script.js +++ b/script.js @@ -411,12 +411,23 @@ async function transcribeAudio(blob) { body: blob }); + const data = await response.json().catch(() => null); if (!response.ok) { - throw new Error('Transcription failed'); + const errorMessage = data?.error || 'Voice transcription failed'; + throw new Error(errorMessage); + } + + const transcript = data?.transcript?.trim() || ''; + if (!transcript) { + sourceText.value = 'No speech detected'; + if (charCount) { + charCount.textContent = 0; + } + showStatus('No speech detected', 'error'); + voiceStatus.textContent = '⚠️ No speech detected'; + return; } - const data = await response.json(); - const transcript = data.transcript || ''; sourceText.value = transcript; if (charCount) { charCount.textContent = transcript.length; @@ -428,8 +439,9 @@ async function transcribeAudio(blob) { await translateText(); } catch (error) { console.error('Voice transcription error:', error); - showStatus('Voice transcription failed', 'error'); - voiceStatus.textContent = '❌ Voice transcription failed'; + const errorMessage = error?.message || 'Voice transcription failed'; + showStatus(errorMessage, 'error'); + voiceStatus.textContent = `❌ ${errorMessage}`; } } diff --git a/scripts/generate-cert.js b/scripts/generate-cert.js new file mode 100644 index 0000000..fb5c83b --- /dev/null +++ b/scripts/generate-cert.js @@ -0,0 +1,21 @@ +const fs = require('fs'); +const path = require('path'); +const selfsigned = require('selfsigned'); + +const outDir = path.resolve(__dirname, '..', 'certs'); +fs.mkdirSync(outDir, { recursive: true }); + +const attrs = [{ name: 'commonName', value: 'localhost' }]; +const opts = { days: 365, keySize: 2048, extensions: [{ name: 'subjectAltName', altNames: [{ type: 2, value: 'localhost' }, { type: 7, ip: '127.0.0.1' }] }] }; +const pems = selfsigned.generate(attrs, opts); + +const keyPath = path.join(outDir, 'localhost.key'); +const certPath = path.join(outDir, 'localhost.crt'); + +fs.writeFileSync(keyPath, pems.private, { mode: 0o600 }); +fs.writeFileSync(certPath, pems.cert, { mode: 0o644 }); + +console.log('Wrote key:', keyPath); +console.log('Wrote cert:', certPath); +console.log('To start HTTPS server using these certs run:'); +console.log(' SSL_KEY_PATH=' + keyPath + ' SSL_CERT_PATH=' + certPath + ' npm start'); diff --git a/server.js b/server.js index 17d9c8a..e31c1f7 100644 --- a/server.js +++ b/server.js @@ -7,6 +7,7 @@ const googleTTS = require('google-tts-api'); require('dotenv').config(); const HUGGINGFACE_API_TOKEN = process.env.HUGGINGFACEHUB_API_TOKEN || process.env.HF_API_TOKEN; +const GROQ_API_KEY = process.env.GROQ_API_KEY; const nllbLangMap = { 'en': 'eng_Latn', 'es': 'spa_Latn', @@ -77,7 +78,7 @@ function loadSslOptions() { // Generate a temporary self-signed certificate for localhost console.warn('No SSL cert/key provided. Generating a temporary self-signed certificate for https://localhost:' + SSL_PORT); const attrs = [{ name: 'commonName', value: 'localhost' }]; - const pems = selfsigned.generate(attrs, { days: 365 }); + const pems = selfsigned.generate(attrs, { days: 365, keySize: 2048 }); return { key: pems.private, cert: pems.cert }; } @@ -332,7 +333,25 @@ const translateViaLibreTranslate = async (text, sourceLang, targetLang) => { } }; -// Speech-to-text endpoint using Hugging Face Whisper +function getTranscriptionFilename(contentType) { + const mimeType = (contentType || 'audio/webm').split(';')[0].trim().toLowerCase(); + + if (mimeType.includes('mp4') || mimeType.includes('m4a') || mimeType.includes('aac')) { + return 'audio.mp4'; + } + + if (mimeType.includes('webm')) { + return 'audio.webm'; + } + + if (mimeType.includes('wav')) { + return 'audio.wav'; + } + + return 'audio.webm'; +} + +// Speech-to-text endpoint using Groq Whisper app.post('/api/transcribe', async (req, res) => { try { const audioBuffer = req.body; @@ -340,29 +359,40 @@ app.post('/api/transcribe', async (req, res) => { return res.status(400).json({ error: 'No audio provided' }); } - if (!HUGGINGFACE_API_TOKEN) { - return res.status(500).json({ error: 'Hugging Face token not configured' }); + if (!GROQ_API_KEY) { + return res.status(503).json({ error: 'Speech-to-text is temporarily unavailable: GROQ API key is not configured.' }); } - const contentType = req.headers['content-type'] || 'application/octet-stream'; - const response = await axios.post( - 'https://api-inference.huggingface.co/models/openai/whisper-large-v3', - audioBuffer, - { - headers: { - Authorization: `Bearer ${HUGGINGFACE_API_TOKEN}`, - 'Content-Type': contentType - }, - timeout: 60000 - } - ); + const contentType = req.headers['content-type'] || 'audio/webm'; + const mimeType = contentType.split(';')[0].trim().toLowerCase(); + const filename = getTranscriptionFilename(contentType); + const formData = new FormData(); - const data = response.data; - const transcript = Array.isArray(data) ? data[0]?.text : data?.text || ''; - res.json({ transcript }); + formData.append('file', new Blob([audioBuffer], { type: mimeType }), filename); + formData.append('model', 'whisper-large-v3-turbo'); + formData.append('response_format', 'json'); + + const response = await fetch('https://api.groq.com/openai/v1/audio/transcriptions', { + method: 'POST', + headers: { + Authorization: `Bearer ${GROQ_API_KEY}` + }, + body: formData + }); + + const data = await response.json().catch(() => null); + + if (!response.ok) { + const errorMessage = data?.error?.message || data?.error || 'Speech-to-text failed'; + console.error('Groq speech-to-text error:', errorMessage); + return res.status(response.status).json({ error: errorMessage }); + } + + const transcript = data?.text || ''; + return res.json({ transcript }); } catch (error) { console.error('Speech-to-text error:', error.message || error); - res.status(500).json({ error: 'Speech-to-text failed' }); + return res.status(500).json({ error: 'Speech-to-text failed' }); } });