diff --git a/.env.example b/.env.example index b024953..787e9c1 100644 --- a/.env.example +++ b/.env.example @@ -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= diff --git a/package-lock.json b/package-lock.json index e91a14a..aa35607 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 1b75350..1198ff6 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/routes/transcribe.js b/src/routes/transcribe.js index 53869ee..6eff469 100644 --- a/src/routes/transcribe.js +++ b/src/routes/transcribe.js @@ -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 }); } diff --git a/src/utils/transcribeAWS.js b/src/utils/transcribeAWS.js new file mode 100644 index 0000000..96d08a0 --- /dev/null +++ b/src/utils/transcribeAWS.js @@ -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 };