Add Amazon Transcribe streaming (no S3) with Medical specialty support

- New src/utils/transcribeAWS.js: streams audio directly to AWS
  Transcribe without requiring an S3 bucket
- Supports AWS_TRANSCRIBE_MEDICAL=true for Transcribe Medical
  (better clinical accuracy: drug names, diagnoses, procedures)
- AWS_TRANSCRIBE_SPECIALTY configures specialty (default PRIMARYCARE)
- transcribe.js auto-selects AWS when AWS_BEDROCK_REGION is set,
  or can be forced with TRANSCRIBE_PROVIDER=aws|openai
- Falls back to OpenAI Whisper when AWS is not configured
- Add @aws-sdk/client-transcribe-streaming as optional dependency
- Update .env.example with transcription configuration docs
This commit is contained in:
Daniel Onyejesi 2026-03-25 20:26:10 +00:00
parent e0757310c8
commit 14497b3270
5 changed files with 322 additions and 87 deletions

View file

@ -21,10 +21,25 @@ OPENROUTER_API_KEY=sk-or-v1-your-key
# AZURE_OPENAI_API_VERSION=2024-02-01
# ============================================================
# Whisper (always OpenAI for now)
# TRANSCRIPTION (speech-to-text)
# ============================================================
# Option A: OpenAI Whisper (default if no AWS configured)
OPENAI_API_KEY=sk-your-openai-key
# Option B: Amazon Transcribe (HIPAA eligible, no S3 needed)
# Uses same AWS credentials as Bedrock above.
# Set TRANSCRIBE_PROVIDER=aws to force AWS even if OPENAI_API_KEY is set.
# Leave unset to auto-detect (uses AWS when AWS_BEDROCK_REGION is configured).
# TRANSCRIBE_PROVIDER=aws
# Amazon Transcribe Medical — better accuracy for clinical dictation
# Knows drug names, diagnoses, procedures, SOAP terminology
# HIPAA eligible (ensure your AWS account has a BAA)
# AWS_TRANSCRIBE_MEDICAL=true
# AWS_TRANSCRIBE_SPECIALTY=PRIMARYCARE
# Other options: CARDIOLOGY, NEUROLOGY, ONCOLOGY, RADIOLOGY, UROLOGY
# Optional
ELEVENLABS_API_KEY=

246
package-lock.json generated
View file

@ -1,13 +1,14 @@
{
"name": "pediatric-ai-scribe",
"version": "3.1.0",
"version": "7.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pediatric-ai-scribe",
"version": "3.1.0",
"version": "7.0.0",
"dependencies": {
"@aws-sdk/client-transcribe-streaming": "^3.700.0",
"@marp-team/marp-cli": "^4.3.1",
"@marp-team/marp-core": "^4.3.0",
"@tiptap/core": "^3.20.4",
@ -36,7 +37,8 @@
"speakeasy": "^2.0.0"
},
"optionalDependencies": {
"@aws-sdk/client-bedrock-runtime": "^3.700.0"
"@aws-sdk/client-bedrock-runtime": "^3.700.0",
"@aws-sdk/client-transcribe-streaming": "^3.1017.0"
}
},
"node_modules/@aws-crypto/crc32": {
@ -248,11 +250,67 @@
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/client-transcribe-streaming": {
"version": "3.1017.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-transcribe-streaming/-/client-transcribe-streaming-3.1017.0.tgz",
"integrity": "sha512-7lnA73jWQPP5bkyBwFowLOsNkBpJfC9CcA4EXiCE4ivV0Vah225yURMTuNxhNXBAnszA3pmjB9y/eS5R14+vSg==",
"optional": true,
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "^3.973.24",
"@aws-sdk/credential-provider-node": "^3.972.25",
"@aws-sdk/eventstream-handler-node": "^3.972.11",
"@aws-sdk/middleware-eventstream": "^3.972.8",
"@aws-sdk/middleware-host-header": "^3.972.8",
"@aws-sdk/middleware-logger": "^3.972.8",
"@aws-sdk/middleware-recursion-detection": "^3.972.8",
"@aws-sdk/middleware-sdk-transcribe-streaming": "^3.972.8",
"@aws-sdk/middleware-user-agent": "^3.972.25",
"@aws-sdk/middleware-websocket": "^3.972.13",
"@aws-sdk/region-config-resolver": "^3.972.9",
"@aws-sdk/types": "^3.973.6",
"@aws-sdk/util-endpoints": "^3.996.5",
"@aws-sdk/util-user-agent-browser": "^3.972.8",
"@aws-sdk/util-user-agent-node": "^3.973.11",
"@smithy/config-resolver": "^4.4.13",
"@smithy/core": "^3.23.12",
"@smithy/eventstream-serde-browser": "^4.2.12",
"@smithy/eventstream-serde-config-resolver": "^4.3.12",
"@smithy/eventstream-serde-node": "^4.2.12",
"@smithy/fetch-http-handler": "^5.3.15",
"@smithy/hash-node": "^4.2.12",
"@smithy/invalid-dependency": "^4.2.12",
"@smithy/middleware-content-length": "^4.2.12",
"@smithy/middleware-endpoint": "^4.4.27",
"@smithy/middleware-retry": "^4.4.44",
"@smithy/middleware-serde": "^4.2.15",
"@smithy/middleware-stack": "^4.2.12",
"@smithy/node-config-provider": "^4.3.12",
"@smithy/node-http-handler": "^4.5.0",
"@smithy/protocol-http": "^5.3.12",
"@smithy/smithy-client": "^4.12.7",
"@smithy/types": "^4.13.1",
"@smithy/url-parser": "^4.2.12",
"@smithy/util-base64": "^4.3.2",
"@smithy/util-body-length-browser": "^4.2.2",
"@smithy/util-body-length-node": "^4.2.3",
"@smithy/util-defaults-mode-browser": "^4.3.43",
"@smithy/util-defaults-mode-node": "^4.2.47",
"@smithy/util-endpoints": "^3.3.3",
"@smithy/util-middleware": "^4.2.12",
"@smithy/util-retry": "^4.2.12",
"@smithy/util-utf8": "^4.2.2",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/core": {
"version": "3.973.23",
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.23.tgz",
"integrity": "sha512-aoJncvD1XvloZ9JLnKqTRL9dBy+Szkryoag9VT+V1TqsuUgIxV9cnBVM/hrDi2vE8bDqLiDR8nirdRcCdtJu0w==",
"license": "Apache-2.0",
"version": "3.973.24",
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.24.tgz",
"integrity": "sha512-vvf82RYQu2GidWAuQq+uIzaPz9V0gSCXVqdVzRosgl5rXcspXOpSD3wFreGGW6AYymPr97Z69kjVnLePBxloDw==",
"optional": true,
"dependencies": {
"@aws-sdk/types": "^3.973.6",
@ -274,13 +332,12 @@
}
},
"node_modules/@aws-sdk/credential-provider-env": {
"version": "3.972.21",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.21.tgz",
"integrity": "sha512-BkAfKq8Bd4shCtec1usNz//urPJF/SZy14qJyxkSaRJQ/Vv1gVh0VZSTmS7aE6aLMELkFV5wHHrS9ZcdG8Kxsg==",
"license": "Apache-2.0",
"version": "3.972.22",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.22.tgz",
"integrity": "sha512-cXp0VTDWT76p3hyK5D51yIKEfpf6/zsUvMfaB8CkyqadJxMQ8SbEeVroregmDlZbtG31wkj9ei0WnftmieggLg==",
"optional": true,
"dependencies": {
"@aws-sdk/core": "^3.973.23",
"@aws-sdk/core": "^3.973.24",
"@aws-sdk/types": "^3.973.6",
"@smithy/property-provider": "^4.2.12",
"@smithy/types": "^4.13.1",
@ -291,13 +348,12 @@
}
},
"node_modules/@aws-sdk/credential-provider-http": {
"version": "3.972.23",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.23.tgz",
"integrity": "sha512-4XZ3+Gu5DY8/n8zQFHBgcKTF7hWQl42G6CY9xfXVo2d25FM/lYkpmuzhYopYoPL1ITWkJ2OSBQfYEu5JRfHOhA==",
"license": "Apache-2.0",
"version": "3.972.24",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.24.tgz",
"integrity": "sha512-h694K7+tRuepSRJr09wTvQfaEnjzsKZ5s7fbESrVds02GT/QzViJ94/HCNwM7bUfFxqpPXHxulZfL6Cou0dwPg==",
"optional": true,
"dependencies": {
"@aws-sdk/core": "^3.973.23",
"@aws-sdk/core": "^3.973.24",
"@aws-sdk/types": "^3.973.6",
"@smithy/fetch-http-handler": "^5.3.15",
"@smithy/node-http-handler": "^4.5.0",
@ -313,20 +369,19 @@
}
},
"node_modules/@aws-sdk/credential-provider-ini": {
"version": "3.972.23",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.23.tgz",
"integrity": "sha512-PZLSmU0JFpNCDFReidBezsgL5ji9jOBry8CnZdw4Jj6d0K2z3Ftnp44NXgADqYx5BLMu/ZHujfeJReaDoV+IwQ==",
"license": "Apache-2.0",
"version": "3.972.24",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.24.tgz",
"integrity": "sha512-O46fFmv0RDFWiWEA9/e6oW92BnsyAXuEgTTasxHligjn2RCr9L/DK773m/NoFaL3ZdNAUz8WxgxunleMnHAkeQ==",
"optional": true,
"dependencies": {
"@aws-sdk/core": "^3.973.23",
"@aws-sdk/credential-provider-env": "^3.972.21",
"@aws-sdk/credential-provider-http": "^3.972.23",
"@aws-sdk/credential-provider-login": "^3.972.23",
"@aws-sdk/credential-provider-process": "^3.972.21",
"@aws-sdk/credential-provider-sso": "^3.972.23",
"@aws-sdk/credential-provider-web-identity": "^3.972.23",
"@aws-sdk/nested-clients": "^3.996.13",
"@aws-sdk/core": "^3.973.24",
"@aws-sdk/credential-provider-env": "^3.972.22",
"@aws-sdk/credential-provider-http": "^3.972.24",
"@aws-sdk/credential-provider-login": "^3.972.24",
"@aws-sdk/credential-provider-process": "^3.972.22",
"@aws-sdk/credential-provider-sso": "^3.972.24",
"@aws-sdk/credential-provider-web-identity": "^3.972.24",
"@aws-sdk/nested-clients": "^3.996.14",
"@aws-sdk/types": "^3.973.6",
"@smithy/credential-provider-imds": "^4.2.12",
"@smithy/property-provider": "^4.2.12",
@ -339,14 +394,13 @@
}
},
"node_modules/@aws-sdk/credential-provider-login": {
"version": "3.972.23",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.23.tgz",
"integrity": "sha512-OmE/pSkbMM3dCj1HdOnZ5kXnKK+R/Yz+kbBugraBecp0pGAs21eEURfQRz+1N2gzIHLVyGIP1MEjk/uSrFsngg==",
"license": "Apache-2.0",
"version": "3.972.24",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.24.tgz",
"integrity": "sha512-sIk8oa6AzDoUhxsR11svZESqvzGuXesw62Rl2oW6wguZx8i9cdGCvkFg+h5K7iucUZP8wyWibUbJMc+J66cu5g==",
"optional": true,
"dependencies": {
"@aws-sdk/core": "^3.973.23",
"@aws-sdk/nested-clients": "^3.996.13",
"@aws-sdk/core": "^3.973.24",
"@aws-sdk/nested-clients": "^3.996.14",
"@aws-sdk/types": "^3.973.6",
"@smithy/property-provider": "^4.2.12",
"@smithy/protocol-http": "^5.3.12",
@ -359,18 +413,17 @@
}
},
"node_modules/@aws-sdk/credential-provider-node": {
"version": "3.972.24",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.24.tgz",
"integrity": "sha512-9Jwi7aps3AfUicJyF5udYadPypPpCwUZ6BSKr/QjRbVCpRVS1wc+1Q6AEZ/qz8J4JraeRd247pSzyMQSIHVebw==",
"license": "Apache-2.0",
"version": "3.972.25",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.25.tgz",
"integrity": "sha512-m7dR0Dsva2P+VUpL+VkC0WwiDby5pgmWXkRVDB5rlwv0jXJrQJf7YMtCoM8Wjk0H9jPeCYOxOXXcIgp/qp5Alg==",
"optional": true,
"dependencies": {
"@aws-sdk/credential-provider-env": "^3.972.21",
"@aws-sdk/credential-provider-http": "^3.972.23",
"@aws-sdk/credential-provider-ini": "^3.972.23",
"@aws-sdk/credential-provider-process": "^3.972.21",
"@aws-sdk/credential-provider-sso": "^3.972.23",
"@aws-sdk/credential-provider-web-identity": "^3.972.23",
"@aws-sdk/credential-provider-env": "^3.972.22",
"@aws-sdk/credential-provider-http": "^3.972.24",
"@aws-sdk/credential-provider-ini": "^3.972.24",
"@aws-sdk/credential-provider-process": "^3.972.22",
"@aws-sdk/credential-provider-sso": "^3.972.24",
"@aws-sdk/credential-provider-web-identity": "^3.972.24",
"@aws-sdk/types": "^3.973.6",
"@smithy/credential-provider-imds": "^4.2.12",
"@smithy/property-provider": "^4.2.12",
@ -383,13 +436,12 @@
}
},
"node_modules/@aws-sdk/credential-provider-process": {
"version": "3.972.21",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.21.tgz",
"integrity": "sha512-nRxbeOJ1E1gVA0lNQezuMVndx+ZcuyaW/RB05pUsznN5BxykSlH6KkZ/7Ca/ubJf3i5N3p0gwNO5zgPSCzj+ww==",
"license": "Apache-2.0",
"version": "3.972.22",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.22.tgz",
"integrity": "sha512-Os32s8/4gTZjBk5BtoS/cuTILaj+K72d0dVG7TCJX/fC4598cxwLDmf1AEHEpER5oL3K//yETjvFaz0V8oO5Xw==",
"optional": true,
"dependencies": {
"@aws-sdk/core": "^3.973.23",
"@aws-sdk/core": "^3.973.24",
"@aws-sdk/types": "^3.973.6",
"@smithy/property-provider": "^4.2.12",
"@smithy/shared-ini-file-loader": "^4.4.7",
@ -401,15 +453,32 @@
}
},
"node_modules/@aws-sdk/credential-provider-sso": {
"version": "3.972.23",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.23.tgz",
"integrity": "sha512-APUccADuYPLL0f2htpM8Z4czabSmHOdo4r41W6lKEZdy++cNJ42Radqy6x4TopENzr3hR6WYMyhiuiqtbf/nAA==",
"license": "Apache-2.0",
"version": "3.972.24",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.24.tgz",
"integrity": "sha512-PaFv7snEfypU2yXkpvfyWgddEbDLtgVe51wdZlinhc2doubBjUzJZZpgwuF2Jenl1FBydMhNpMjD6SBUM3qdSA==",
"optional": true,
"dependencies": {
"@aws-sdk/core": "^3.973.23",
"@aws-sdk/nested-clients": "^3.996.13",
"@aws-sdk/token-providers": "3.1014.0",
"@aws-sdk/core": "^3.973.24",
"@aws-sdk/nested-clients": "^3.996.14",
"@aws-sdk/token-providers": "3.1015.0",
"@aws-sdk/types": "^3.973.6",
"@smithy/property-provider": "^4.2.12",
"@smithy/shared-ini-file-loader": "^4.4.7",
"@smithy/types": "^4.13.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": {
"version": "3.1015.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1015.0.tgz",
"integrity": "sha512-3OSD4y110nisRhHzFOjoEeHU4GQL4KpzkX9PxzWaiZe0Yg2+thZKM0Pn9DjYwezH5JYfh/K++xK/SE0IHGrmCQ==",
"optional": true,
"dependencies": {
"@aws-sdk/core": "^3.973.24",
"@aws-sdk/nested-clients": "^3.996.14",
"@aws-sdk/types": "^3.973.6",
"@smithy/property-provider": "^4.2.12",
"@smithy/shared-ini-file-loader": "^4.4.7",
@ -421,14 +490,13 @@
}
},
"node_modules/@aws-sdk/credential-provider-web-identity": {
"version": "3.972.23",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.23.tgz",
"integrity": "sha512-H5JNqtIwOu/feInmMMWcK0dL5r897ReEn7n2m16Dd0DPD9gA2Hg8Cq4UDzZ/9OzaLh/uqBM6seixz0U6Fi2Eag==",
"license": "Apache-2.0",
"version": "3.972.24",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.24.tgz",
"integrity": "sha512-J6H4R1nvr3uBTqD/EeIPAskrBtET4WFfNhpFySr2xW7bVZOXpQfPjrLSIx65jcNjBmLXzWq8QFLdVoGxiGG/SA==",
"optional": true,
"dependencies": {
"@aws-sdk/core": "^3.973.23",
"@aws-sdk/nested-clients": "^3.996.13",
"@aws-sdk/core": "^3.973.24",
"@aws-sdk/nested-clients": "^3.996.14",
"@aws-sdk/types": "^3.973.6",
"@smithy/property-provider": "^4.2.12",
"@smithy/shared-ini-file-loader": "^4.4.7",
@ -519,14 +587,32 @@
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/middleware-user-agent": {
"version": "3.972.24",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.24.tgz",
"integrity": "sha512-dLTWy6IfAMhNiSEvMr07g/qZ54be6pLqlxVblbF6AzafmmGAzMMj8qMoY9B4+YgT+gY9IcuxZslNh03L6PyMCQ==",
"license": "Apache-2.0",
"node_modules/@aws-sdk/middleware-sdk-transcribe-streaming": {
"version": "3.972.8",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-transcribe-streaming/-/middleware-sdk-transcribe-streaming-3.972.8.tgz",
"integrity": "sha512-fBIX+TEwzKnJnQiMjOLUl0480HbKd0enZ77Q5aEVzegT2jHyESEHFWJaB3q64r6xXZYioPxLnuTkDe6xVDjqCA==",
"optional": true,
"dependencies": {
"@aws-sdk/core": "^3.973.23",
"@aws-sdk/types": "^3.973.6",
"@aws-sdk/util-format-url": "^3.972.8",
"@smithy/eventstream-serde-browser": "^4.2.12",
"@smithy/protocol-http": "^5.3.12",
"@smithy/signature-v4": "^5.3.12",
"@smithy/types": "^4.13.1",
"@smithy/uuid": "^1.1.2",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/middleware-user-agent": {
"version": "3.972.25",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.25.tgz",
"integrity": "sha512-QxiMPofvOt8SwSynTOmuZfvvPM1S9QfkESBxB22NMHTRXCJhR5BygLl8IXfC4jELiisQgwsgUby21GtXfX3f/g==",
"optional": true,
"dependencies": {
"@aws-sdk/core": "^3.973.24",
"@aws-sdk/types": "^3.973.6",
"@aws-sdk/util-endpoints": "^3.996.5",
"@smithy/core": "^3.23.12",
@ -564,24 +650,23 @@
}
},
"node_modules/@aws-sdk/nested-clients": {
"version": "3.996.13",
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.13.tgz",
"integrity": "sha512-ptZ1HF4yYHNJX8cgFF+8NdYO69XJKZn7ft0/ynV3c0hCbN+89fAbrLS+fqniU2tW8o9Kfqhj8FUh+IPXb2Qsuw==",
"license": "Apache-2.0",
"version": "3.996.14",
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.14.tgz",
"integrity": "sha512-fSESKvh1VbfjtV3QMnRkCPZWkUbQof6T/DOpiLp33yP2wA+rbwwnZeG3XT3Ekljgw2I8X4XaQPnw+zSR8yxJ5Q==",
"optional": true,
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "^3.973.23",
"@aws-sdk/core": "^3.973.24",
"@aws-sdk/middleware-host-header": "^3.972.8",
"@aws-sdk/middleware-logger": "^3.972.8",
"@aws-sdk/middleware-recursion-detection": "^3.972.8",
"@aws-sdk/middleware-user-agent": "^3.972.24",
"@aws-sdk/middleware-user-agent": "^3.972.25",
"@aws-sdk/region-config-resolver": "^3.972.9",
"@aws-sdk/types": "^3.973.6",
"@aws-sdk/util-endpoints": "^3.996.5",
"@aws-sdk/util-user-agent-browser": "^3.972.8",
"@aws-sdk/util-user-agent-node": "^3.973.10",
"@aws-sdk/util-user-agent-node": "^3.973.11",
"@smithy/config-resolver": "^4.4.13",
"@smithy/core": "^3.23.12",
"@smithy/fetch-http-handler": "^5.3.15",
@ -723,13 +808,12 @@
}
},
"node_modules/@aws-sdk/util-user-agent-node": {
"version": "3.973.10",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.10.tgz",
"integrity": "sha512-E99zeTscCc+pTMfsvnfi6foPpKmdD1cZfOC7/P8UUrjsoQdg9VEWPRD+xdFduKnfPXwcvby58AlO9jwwF6U96g==",
"license": "Apache-2.0",
"version": "3.973.11",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.11.tgz",
"integrity": "sha512-1qdXbXo2s5MMLpUvw00284LsbhtlQ4ul7Zzdn5n+7p4WVgCMLqhxImpHIrjSoc72E/fyc4Wq8dLtUld2Gsh+lA==",
"optional": true,
"dependencies": {
"@aws-sdk/middleware-user-agent": "^3.972.24",
"@aws-sdk/middleware-user-agent": "^3.972.25",
"@aws-sdk/types": "^3.973.6",
"@smithy/node-config-provider": "^4.3.12",
"@smithy/types": "^4.13.1",

View file

@ -35,6 +35,7 @@
"speakeasy": "^2.0.0"
},
"optionalDependencies": {
"@aws-sdk/client-bedrock-runtime": "^3.700.0"
"@aws-sdk/client-bedrock-runtime": "^3.700.0",
"@aws-sdk/client-transcribe-streaming": "^3.1017.0"
}
}

View file

@ -2,21 +2,49 @@ const express = require('express');
const router = express.Router();
const multer = require('multer');
const { whisperClient } = require('../utils/ai');
const { transcribeWithAWS, isAWSTranscribeConfigured } = require('../utils/transcribeAWS');
const { authMiddleware } = require('../middleware/auth');
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 25 * 1024 * 1024 } });
// Determine which provider to use:
// TRANSCRIBE_PROVIDER=aws → always use Amazon Transcribe
// TRANSCRIBE_PROVIDER=openai → always use OpenAI Whisper
// (default) if AWS Bedrock region is set → use AWS Transcribe
// otherwise → use OpenAI Whisper
function getTranscribeProvider() {
var env = process.env.TRANSCRIBE_PROVIDER;
if (env === 'aws') return 'aws';
if (env === 'openai') return 'openai';
// Auto-detect: prefer AWS when Bedrock is already configured
if (isAWSTranscribeConfigured()) return 'aws';
return 'openai';
}
var provider = getTranscribeProvider();
var medical = process.env.AWS_TRANSCRIBE_MEDICAL === 'true';
console.log('🎙️ Transcribe provider:', provider + (provider === 'aws' && medical ? ' (Medical)' : ''));
router.post('/transcribe', authMiddleware, upload.single('audio'), async (req, res) => {
try {
if (!req.file) return res.status(400).json({ error: 'No audio' });
if (!whisperClient) return res.status(400).json({ error: 'Whisper not configured' });
const file = new File([req.file.buffer], 'audio.webm', { type: req.file.mimetype || 'audio/webm' });
const result = await whisperClient.audio.transcriptions.create({
if (provider === 'aws') {
if (!isAWSTranscribeConfigured()) {
return res.status(400).json({ error: 'AWS Transcribe not configured. Set AWS_BEDROCK_REGION.' });
}
var text = await transcribeWithAWS(req.file.buffer, req.file.mimetype || 'audio/webm');
return res.json({ success: true, text: text, provider: 'aws-transcribe' });
}
// OpenAI Whisper
if (!whisperClient) return res.status(400).json({ error: 'Whisper not configured. Set OPENAI_API_KEY.' });
var file = new File([req.file.buffer], 'audio.webm', { type: req.file.mimetype || 'audio/webm' });
var result = await whisperClient.audio.transcriptions.create({
file, model: 'whisper-1', language: 'en',
prompt: 'Medical patient encounter. Pediatric. Clinical terms, diagnoses, medications.'
});
res.json({ success: true, text: result.text });
res.json({ success: true, text: result.text, provider: 'openai-whisper' });
} catch (err) {
res.status(500).json({ error: err.message });
}

107
src/utils/transcribeAWS.js Normal file
View file

@ -0,0 +1,107 @@
// ============================================================
// TRANSCRIBE-AWS.JS — Amazon Transcribe Streaming
// No S3 required. Audio streams directly to AWS.
//
// 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 CHUNK_SIZE = 32768; // 32 KB per audio chunk
async function* makeAudioStream(buffer) {
for (var i = 0; i < buffer.length; i += CHUNK_SIZE) {
yield { AudioEvent: { AudioChunk: buffer.slice(i, i + CHUNK_SIZE) } };
// Yield to event loop between chunks to avoid blocking
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 });
// Map browser MIME type to AWS MediaEncoding
// audio/webm;codecs=opus → ogg-opus (same Opus codec, different container)
// audio/ogg;codecs=opus → ogg-opus (exact match)
// audio/wav → pcm
var mediaEncoding = 'ogg-opus';
if (mimeType && mimeType.indexOf('wav') !== -1) mediaEncoding = 'pcm';
var useMedical = process.env.AWS_TRANSCRIBE_MEDICAL === 'true';
var specialty = process.env.AWS_TRANSCRIBE_SPECIALTY || 'PRIMARYCARE';
var transcript = '';
if (useMedical) {
var StartMedicalStreamTranscriptionCommand = TranscribeModule.StartMedicalStreamTranscriptionCommand;
var medCmd = new StartMedicalStreamTranscriptionCommand({
LanguageCode: 'en-US',
MediaSampleRateHertz: 48000,
MediaEncoding: mediaEncoding,
Specialty: specialty,
Type: 'DICTATION',
AudioStream: makeAudioStream(audioBuffer)
});
var medResp = await client.send(medCmd);
for await (var event of medResp.TranscriptResultStream) {
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]) {
transcript += r.Alternatives[0].Transcript + ' ';
}
}
}
}
} else {
var StartStreamTranscriptionCommand = TranscribeModule.StartStreamTranscriptionCommand;
var cmd = new StartStreamTranscriptionCommand({
LanguageCode: 'en-US',
MediaSampleRateHertz: 48000,
MediaEncoding: mediaEncoding,
AudioStream: makeAudioStream(audioBuffer)
});
var resp = await client.send(cmd);
for await (var ev of resp.TranscriptResultStream) {
if (ev.TranscriptEvent && ev.TranscriptEvent.Transcript) {
var res = ev.TranscriptEvent.Transcript.Results || [];
for (var j = 0; j < res.length; j++) {
var item = res[j];
if (!item.IsPartial && item.Alternatives && item.Alternatives[0]) {
transcript += item.Alternatives[0].Transcript + ' ';
}
}
}
}
}
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 };