UNPKG

speech-recognition-aws-polyfill

Version:

Polyfill for the SpeechRecognition browser API using AWS Transcribe

190 lines (166 loc) 6.3 kB
// forked from from https://github.com/department-stockholm/aws-signature-v4 with modifications import crypto from 'crypto-browserify' import querystring from 'query-string' import {CognitoIdentityCredentials, config as awsConfig, Credentials} from 'aws-sdk' import {when} from 'ramda' import {createPipe, pipe} from 'remeda' import {Config} from '../types/shared' import {downsampleBuffer, pcmEncode} from './audioUtils' import {toRaw} from 'microphone-stream' import {EventStreamMarshaller} from '@aws-sdk/eventstream-marshaller' import {fromUtf8, toUtf8} from '@aws-sdk/util-utf8-node' const eventStreamMarshaller = new EventStreamMarshaller(toUtf8, fromUtf8) interface Params { [key: string]: any } interface Headers { [key: string]: any } interface AWSOptions { bucket?: string method?: string protocol?: string headers?: Headers timestamp?: number region?: string expires?: number query: string key: string sessionToken?: string secret: string } export const getCredentials = async ({IdentityPoolId, region}: Pick<Config, "IdentityPoolId" | "region">) => { awsConfig.region = region awsConfig.credentials = new CognitoIdentityCredentials({IdentityPoolId}) await (awsConfig.credentials as Credentials).getPromise() return awsConfig.credentials } export function getAudioEventMessage(buffer: Buffer) { // wrap the audio data in a JSON envelope return { headers: { ':message-type': { type: 'string', value: 'event' }, ':event-type': { type: 'string', value: 'AudioEvent' } }, body: buffer } } export function convertAudioToBinaryMessage(audioChunk: Buffer, sampleRate: number): Uint8Array { return pipe( audioChunk, when<Uint8Array, Uint8Array>(Boolean, createPipe( toRaw, // downsample and convert the raw audio bytes to PCM (buffer: Float32Array) => downsampleBuffer({buffer, outputSampleRate: sampleRate}), pcmEncode, // @ts-ignore Buffer, // add the right JSON headers and structure to the message getAudioEventMessage, // convert the JSON object + headers into a binary event stream message eventStreamMarshaller.marshall.bind(eventStreamMarshaller) )) ) } export const createCanonicalQueryString = function (params: Params) { return Object.keys(params).sort().map(function (key) { return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]) }).join('&') } export const createCanonicalHeaders = function (headers: Headers) { return Object.keys(headers).sort().map(function (name) { return name.toLowerCase().trim() + ':' + headers[name].toString().trim() + '\n' }).join('') } export const createSignedHeaders = function (headers: Headers) { return Object.keys(headers).sort().map(function (name) { return name.toLowerCase().trim() }).join(';') } export const createCredentialScope = function (time: number, region: string, service: string) { return [toDate(time), region, service, 'aws4_request'].join('/') } export const createStringToSign = function (time: number, region: string, service: string, request: string) { return [ 'AWS4-HMAC-SHA256', toTime(time), createCredentialScope(time, region, service), hash(request, 'hex') ].join('\n') } export const createSignature = function (secret: string, time: number, region: string, service: string, stringToSign: string) { const h1 = hmac('AWS4' + secret, toDate(time)) // date-key const h2 = hmac(h1, region) // region-key const h3 = hmac(h2, service) // service-key const h4 = hmac(h3, 'aws4_request') // signing-key return hmac(h4, stringToSign, 'hex') } export const createPresignedS3URL = function (name: string, options: AWSOptions) { if (!options?.bucket) throw new Error('S3 Bucket not provided') options.method = options.method || 'GET' return createPresignedURL( options.method, options.bucket + '.s3.amazonaws.com', '/' + name, 's3', 'UNSIGNED-PAYLOAD', options ) } export const createCanonicalRequest = function (method: string, pathname: string, query: Params, headers: Headers, payload: unknown) { return [ method.toUpperCase(), pathname, createCanonicalQueryString(query), createCanonicalHeaders(headers), createSignedHeaders(headers), payload ].join('\n') } export const createPresignedURL = function (method: string, host: string, path: string, service: string, payload: unknown, options: AWSOptions) { options.protocol = options.protocol || 'https' options.headers = options.headers || {} options.timestamp = options.timestamp || Date.now() options.region = options.region || 'us-west-1' options.expires = options.expires || 86400 // 24 hours options.headers = options.headers || {} // host is required options.headers.Host = host const query = options.query ? querystring.parse(options.query) : {} query['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256' query['X-Amz-Credential'] = options.key + '/' + createCredentialScope(options.timestamp, options.region, service) query['X-Amz-Date'] = toTime(options.timestamp) query['X-Amz-Expires'] = String(options.expires) query['X-Amz-SignedHeaders'] = createSignedHeaders(options.headers) if (options.sessionToken) { query['X-Amz-Security-Token'] = options.sessionToken } const canonicalRequest = createCanonicalRequest(method, path, query, options.headers, payload) const stringToSign = createStringToSign(options.timestamp, options.region, service, canonicalRequest) const signature = createSignature(options.secret, options.timestamp, options.region, service, stringToSign) query['X-Amz-Signature'] = signature return options.protocol + '://' + host + path + '?' + querystring.stringify(query) } function toTime(time: number) { // eslint-disable-next-line no-useless-escape return new Date(time).toISOString().replace(/[:\-]|\.\d{3}/g, '') } function toDate(time: number) { return toTime(time).slice(0, 8) } function hmac(key: string, string: string, encoding?: string) { return crypto.createHmac('sha256', key) .update(string, 'utf8') .digest(encoding) } function hash(string: string, encoding?: string) { return crypto.createHash('sha256') .update(string, 'utf8') .digest(encoding) }