Fix speech-to-text with Groq Whisper
This commit is contained in:
@@ -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-----
|
||||||
Generated
+53
-1
@@ -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",
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
@@ -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' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user