When Medical Transcribe fails (wrong IAM permissions, region not supported), automatically falls back to Standard Transcribe instead of returning an error. Logs the specific failure reason.
176 lines
6.7 KiB
JavaScript
176 lines
6.7 KiB
JavaScript
// ============================================================
|
|
// TRANSCRIBE-AWS.JS — Amazon Transcribe Streaming
|
|
// No S3 required. Audio streams directly to AWS.
|
|
//
|
|
// Audio conversion: ffmpeg converts browser WebM/Opus → PCM 16kHz
|
|
// which is the most reliable format for AWS Transcribe. If ffmpeg
|
|
// is not installed, falls back to sending ogg-opus directly.
|
|
//
|
|
// Env vars:
|
|
// TRANSCRIBE_PROVIDER=aws — use this instead of Whisper
|
|
// AWS_TRANSCRIBE_MEDICAL=true — use Transcribe Medical (better
|
|
// accuracy for clinical dictation)
|
|
// AWS_TRANSCRIBE_SPECIALTY=PRIMARYCARE — medical specialty
|
|
// Options: PRIMARYCARE, CARDIOLOGY, NEUROLOGY, ONCOLOGY,
|
|
// RADIOLOGY, UROLOGY
|
|
// AWS_BEDROCK_REGION — reused for Transcribe region
|
|
// AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY — reused credentials
|
|
// ============================================================
|
|
|
|
const { spawn } = require('child_process');
|
|
|
|
const CHUNK_SIZE = 32768; // 32 KB per audio chunk
|
|
|
|
// Convert any browser audio (WebM, OGG, etc.) to raw PCM s16le 16kHz mono
|
|
// using ffmpeg. Returns a Buffer of raw PCM bytes.
|
|
// Throws if ffmpeg is not installed.
|
|
function convertToPCM(inputBuffer) {
|
|
return new Promise(function(resolve, reject) {
|
|
var ff = spawn('ffmpeg', [
|
|
'-i', 'pipe:0', // read from stdin
|
|
'-f', 's16le', // raw signed 16-bit little-endian PCM
|
|
'-ar', '16000', // 16 kHz — ideal for speech recognition
|
|
'-ac', '1', // mono
|
|
'-acodec', 'pcm_s16le',
|
|
'pipe:1' // write to stdout
|
|
]);
|
|
|
|
var out = [];
|
|
var errBuf = [];
|
|
|
|
ff.stdout.on('data', function(d) { out.push(d); });
|
|
ff.stderr.on('data', function(d) { errBuf.push(d); }); // ffmpeg logs to stderr, not an error
|
|
|
|
ff.on('close', function(code) {
|
|
if (code !== 0) {
|
|
reject(new Error('ffmpeg conversion failed (code ' + code + '): ' + Buffer.concat(errBuf).toString().slice(-200)));
|
|
return;
|
|
}
|
|
resolve(Buffer.concat(out));
|
|
});
|
|
|
|
ff.on('error', function(err) {
|
|
reject(new Error('ffmpeg not found. Install ffmpeg on the server: ' + err.message));
|
|
});
|
|
|
|
ff.stdin.write(inputBuffer);
|
|
ff.stdin.end();
|
|
});
|
|
}
|
|
|
|
async function* makeAudioStream(buffer) {
|
|
for (var i = 0; i < buffer.length; i += CHUNK_SIZE) {
|
|
yield { AudioEvent: { AudioChunk: buffer.slice(i, i + CHUNK_SIZE) } };
|
|
await new Promise(function(r) { setTimeout(r, 0); });
|
|
}
|
|
}
|
|
|
|
async function transcribeWithAWS(audioBuffer, mimeType) {
|
|
var TranscribeModule = require('@aws-sdk/client-transcribe-streaming');
|
|
var TranscribeStreamingClient = TranscribeModule.TranscribeStreamingClient;
|
|
|
|
var region = process.env.AWS_BEDROCK_REGION || process.env.AWS_REGION || 'us-east-1';
|
|
var credentials = process.env.AWS_ACCESS_KEY_ID ? {
|
|
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
|
|
} : undefined;
|
|
|
|
var client = new TranscribeStreamingClient({ region: region, credentials: credentials });
|
|
|
|
// Try ffmpeg conversion to PCM (most reliable with AWS Transcribe).
|
|
// Falls back to ogg-opus passthrough if ffmpeg is not installed.
|
|
var audioToSend = audioBuffer;
|
|
var mediaEncoding = 'ogg-opus';
|
|
var sampleRate = 48000;
|
|
|
|
try {
|
|
audioToSend = await convertToPCM(audioBuffer);
|
|
mediaEncoding = 'pcm';
|
|
sampleRate = 16000;
|
|
console.log('[Transcribe] ffmpeg conversion OK — PCM 16kHz, ' + audioToSend.length + ' bytes');
|
|
} catch (ffErr) {
|
|
console.warn('[Transcribe] ffmpeg unavailable, sending ogg-opus directly:', ffErr.message);
|
|
// For WAV input, try pcm anyway without ffmpeg
|
|
if (mimeType && mimeType.indexOf('wav') !== -1) {
|
|
mediaEncoding = 'pcm';
|
|
sampleRate = 16000;
|
|
}
|
|
}
|
|
|
|
var useMedical = process.env.AWS_TRANSCRIBE_MEDICAL === 'true';
|
|
var specialty = process.env.AWS_TRANSCRIBE_SPECIALTY || 'PRIMARYCARE';
|
|
var transcript = '';
|
|
|
|
// Helper to collect transcript from a result stream
|
|
async function collectTranscript(resultStream) {
|
|
var text = '';
|
|
for await (var event of resultStream) {
|
|
if (event.TranscriptEvent && event.TranscriptEvent.Transcript) {
|
|
var results = event.TranscriptEvent.Transcript.Results || [];
|
|
for (var i = 0; i < results.length; i++) {
|
|
var r = results[i];
|
|
if (!r.IsPartial && r.Alternatives && r.Alternatives[0]) {
|
|
text += r.Alternatives[0].Transcript + ' ';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return text;
|
|
}
|
|
|
|
if (useMedical) {
|
|
try {
|
|
var StartMedicalStreamTranscriptionCommand = TranscribeModule.StartMedicalStreamTranscriptionCommand;
|
|
var medCmd = new StartMedicalStreamTranscriptionCommand({
|
|
LanguageCode: 'en-US',
|
|
MediaSampleRateHertz: sampleRate,
|
|
MediaEncoding: mediaEncoding,
|
|
Specialty: specialty,
|
|
Type: 'DICTATION',
|
|
AudioStream: makeAudioStream(audioToSend)
|
|
});
|
|
var medResp = await client.send(medCmd);
|
|
transcript = await collectTranscript(medResp.TranscriptResultStream);
|
|
console.log('[Transcribe] Medical transcription OK (' + transcript.length + ' chars)');
|
|
} catch (medErr) {
|
|
console.warn('[Transcribe] Medical failed, falling back to standard:', medErr.message);
|
|
// Fallback to standard Transcribe
|
|
var client2 = new TranscribeStreamingClient({ region: region, credentials: credentials });
|
|
var StartStreamTranscriptionCommand = TranscribeModule.StartStreamTranscriptionCommand;
|
|
var cmd = new StartStreamTranscriptionCommand({
|
|
LanguageCode: 'en-US',
|
|
MediaSampleRateHertz: sampleRate,
|
|
MediaEncoding: mediaEncoding,
|
|
AudioStream: makeAudioStream(audioToSend)
|
|
});
|
|
var resp = await client2.send(cmd);
|
|
transcript = await collectTranscript(resp.TranscriptResultStream);
|
|
console.log('[Transcribe] Standard fallback OK (' + transcript.length + ' chars)');
|
|
}
|
|
} else {
|
|
var StartStreamTranscriptionCommand = TranscribeModule.StartStreamTranscriptionCommand;
|
|
var cmd = new StartStreamTranscriptionCommand({
|
|
LanguageCode: 'en-US',
|
|
MediaSampleRateHertz: sampleRate,
|
|
MediaEncoding: mediaEncoding,
|
|
AudioStream: makeAudioStream(audioToSend)
|
|
});
|
|
var resp = await client.send(cmd);
|
|
transcript = await collectTranscript(resp.TranscriptResultStream);
|
|
console.log('[Transcribe] Standard transcription OK (' + transcript.length + ' chars)');
|
|
}
|
|
|
|
return transcript.trim();
|
|
}
|
|
|
|
// Check if AWS Transcribe is available and configured
|
|
function isAWSTranscribeConfigured() {
|
|
try {
|
|
require('@aws-sdk/client-transcribe-streaming');
|
|
return !!(process.env.AWS_BEDROCK_REGION || process.env.AWS_REGION);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
module.exports = { transcribeWithAWS, isAWSTranscribeConfigured };
|