Fix speech-to-text with Groq Whisper

This commit is contained in:
Chandu
2026-07-25 11:55:40 -04:00
parent 7a56c88907
commit 41429206ac
6 changed files with 160 additions and 26 deletions
+17
View File
@@ -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-----
+53 -1
View File
@@ -16,7 +16,8 @@
"google-tts-api": "^2.0.1" "google-tts-api": "^2.0.1"
}, },
"devDependencies": { "devDependencies": {
"puppeteer": "^25.3.0" "puppeteer": "^25.3.0",
"selfsigned": "^2.1.0"
} }
}, },
"node_modules/@puppeteer/browsers": { "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": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -920,6 +941,16 @@
"node": ">= 0.6" "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": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -1096,6 +1127,20 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT" "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": { "node_modules/send": {
"version": "0.19.2", "version": "0.19.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
@@ -1291,6 +1336,13 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+2
View File
@@ -4,6 +4,8 @@
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
"start": "node server.js", "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" "check:permissions": "node scripts/permission-check.js"
}, },
"dependencies": { "dependencies": {
+17 -5
View File
@@ -411,12 +411,23 @@ async function transcribeAudio(blob) {
body: blob body: blob
}); });
const data = await response.json().catch(() => null);
if (!response.ok) { 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; sourceText.value = transcript;
if (charCount) { if (charCount) {
charCount.textContent = transcript.length; charCount.textContent = transcript.length;
@@ -428,8 +439,9 @@ async function transcribeAudio(blob) {
await translateText(); await translateText();
} catch (error) { } catch (error) {
console.error('Voice transcription error:', error); console.error('Voice transcription error:', error);
showStatus('Voice transcription failed', 'error'); const errorMessage = error?.message || 'Voice transcription failed';
voiceStatus.textContent = '❌ Voice transcription failed'; showStatus(errorMessage, 'error');
voiceStatus.textContent = `${errorMessage}`;
} }
} }
+21
View File
@@ -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');
+48 -18
View File
@@ -7,6 +7,7 @@ const googleTTS = require('google-tts-api');
require('dotenv').config(); require('dotenv').config();
const HUGGINGFACE_API_TOKEN = process.env.HUGGINGFACEHUB_API_TOKEN || process.env.HF_API_TOKEN; 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 = { const nllbLangMap = {
'en': 'eng_Latn', 'en': 'eng_Latn',
'es': 'spa_Latn', 'es': 'spa_Latn',
@@ -77,7 +78,7 @@ function loadSslOptions() {
// Generate a temporary self-signed certificate for localhost // 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); 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 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 }; 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) => { app.post('/api/transcribe', async (req, res) => {
try { try {
const audioBuffer = req.body; const audioBuffer = req.body;
@@ -340,29 +359,40 @@ app.post('/api/transcribe', async (req, res) => {
return res.status(400).json({ error: 'No audio provided' }); return res.status(400).json({ error: 'No audio provided' });
} }
if (!HUGGINGFACE_API_TOKEN) { if (!GROQ_API_KEY) {
return res.status(500).json({ error: 'Hugging Face token not configured' }); 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 contentType = req.headers['content-type'] || 'audio/webm';
const response = await axios.post( const mimeType = contentType.split(';')[0].trim().toLowerCase();
'https://api-inference.huggingface.co/models/openai/whisper-large-v3', const filename = getTranscriptionFilename(contentType);
audioBuffer, const formData = new FormData();
{
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: { headers: {
Authorization: `Bearer ${HUGGINGFACE_API_TOKEN}`, Authorization: `Bearer ${GROQ_API_KEY}`
'Content-Type': contentType
}, },
timeout: 60000 body: formData
} });
);
const data = response.data; const data = await response.json().catch(() => null);
const transcript = Array.isArray(data) ? data[0]?.text : data?.text || '';
res.json({ transcript }); 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) { } catch (error) {
console.error('Speech-to-text error:', error.message || 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' });
} }
}); });