@speechly/browser-client
Advanced tools
Comparing version 1.5.0 to 2.0.0-beta.1
@@ -1,2 +0,2 @@ | ||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).Speechly={})}(this,(function(t){"use strict";var e;function n(t){var e;return null!==(e=s.get(t))&&void 0!==e?e:i}t.ClientState=void 0,(e=t.ClientState||(t.ClientState={}))[e.Failed=0]="Failed",e[e.NoBrowserSupport=1]="NoBrowserSupport",e[e.NoAudioConsent=2]="NoAudioConsent",e[e.__UnrecoverableErrors=3]="__UnrecoverableErrors",e[e.Disconnected=4]="Disconnected",e[e.Disconnecting=5]="Disconnecting",e[e.Connecting=6]="Connecting",e[e.Preinitialized=7]="Preinitialized",e[e.Initializing=8]="Initializing",e[e.Connected=9]="Connected",e[e.Stopping=10]="Stopping",e[e.Starting=11]="Starting",e[e.Recording=12]="Recording";const i="Unknown",s=new Map([[t.ClientState.Failed,"Failed"],[t.ClientState.NoBrowserSupport,"NoBrowserSupport"],[t.ClientState.NoAudioConsent,"NoAudioConsent"],[t.ClientState.Disconnecting,"Disconnecting"],[t.ClientState.Disconnected,"Disconnected"],[t.ClientState.Connecting,"Connecting"],[t.ClientState.Preinitialized,"Preinitialized"],[t.ClientState.Initializing,"Initializing"],[t.ClientState.Connected,"Connected"],[t.ClientState.Stopping,"Stopping"],[t.ClientState.Starting,"Starting"],[t.ClientState.Recording,"Recording"]]); | ||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).Speechly={})}(this,(function(t){"use strict";const e=new Error("Current device does not support microphone API"),n=new Error("AppId changed without project login"),i=16e3;class s{constructor(t,e){this.isFinalized=!1,this.words=[],this.entities=new Map,this.intent={intent:"",isFinal:!1},this.contextId=t,this.id=e}toSegment(){let t=0;const e=new Array(this.entities.size);return this.entities.forEach((n=>{e[t]=n,t++})),{id:this.id,contextId:this.contextId,isFinal:this.isFinalized,words:this.words,entities:e,intent:this.intent}}toString(){const t=this.toSegment(),e=t.words.filter((t=>t.value)).map((t=>({value:t.value,index:t.index}))),n=Object.assign(Object.assign({},t),{words:e});return JSON.stringify(n,null,2)}updateTranscript(t){return t.forEach((t=>{this.isFinalized&&!t.isFinal||(this.words[t.index]=t)})),this}updateEntities(t){return t.forEach((t=>{this.isFinalized&&!t.isFinal||this.entities.set(function(t){return`${t.startPosition.toString()}:${t.endPosition.toString()}`} | ||
/*! ***************************************************************************** | ||
@@ -15,6 +15,5 @@ Copyright (c) Microsoft Corporation. | ||
PERFORMANCE OF THIS SOFTWARE. | ||
***************************************************************************** */ | ||
function o(t,e,n,i){return new(n||(n=Promise))((function(s,o){function a(t){try{l(i.next(t))}catch(t){o(t)}}function r(t){try{l(i.throw(t))}catch(t){o(t)}}function l(t){var e;t.done?s(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(a,r)}l((i=i.apply(t,e||[])).next())}))}var a,r=new Uint8Array(16);function l(){if(!a&&!(a="undefined"!=typeof crypto&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto)||"undefined"!=typeof msCrypto&&"function"==typeof msCrypto.getRandomValues&&msCrypto.getRandomValues.bind(msCrypto)))throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");return a(r)}var c=/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;function h(t){return"string"==typeof t&&c.test(t)}for(var d=[],u=0;u<256;++u)d.push((u+256).toString(16).substr(1));function p(t,e,n){var i=(t=t||{}).random||(t.rng||l)();if(i[6]=15&i[6]|64,i[8]=63&i[8]|128,e){n=n||0;for(var s=0;s<16;++s)e[n+s]=i[s];return e}return function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=(d[t[e+0]]+d[t[e+1]]+d[t[e+2]]+d[t[e+3]]+"-"+d[t[e+4]]+d[t[e+5]]+"-"+d[t[e+6]]+d[t[e+7]]+"-"+d[t[e+8]]+d[t[e+9]]+"-"+d[t[e+10]]+d[t[e+11]]+d[t[e+12]]+d[t[e+13]]+d[t[e+14]]+d[t[e+15]]).toLowerCase();if(!h(n))throw TypeError("Stringified UUID is invalid");return n}(i)}var f="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},S={exports:{}}; | ||
***************************************************************************** */(t),t)})),this}updateIntent(t){return this.isFinalized&&!t.isFinal||(this.intent=t),this}finalize(){return this.entities.forEach(((t,e)=>{t.isFinal||this.entities.delete(e)})),this.words=this.words.filter((t=>t.isFinal)),this.intent.isFinal||(this.intent.intent="",this.intent.isFinal=!0),this.isFinalized=!0,this}}function o(t,e,n,i){return new(n||(n=Promise))((function(s,o){function r(t){try{c(i.next(t))}catch(t){o(t)}}function a(t){try{c(i.throw(t))}catch(t){o(t)}}function c(t){var e;t.done?s(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(r,a)}c((i=i.apply(t,e||[])).next())}))}const r=new Error("Microphone is not initialized"),a=new Error("Microphone is already initialized"),c=new Error("Microphone consent is not given");var d;t.WebsocketResponseType=void 0,(d=t.WebsocketResponseType||(t.WebsocketResponseType={})).Opened="WEBSOCKET_OPEN",d.Closed="WEBSOCKET_CLOSED",d.SourceSampleRateSetSuccess="SOURCE_SAMPLE_RATE_SET_SUCCESS",d.Started="started",d.Stopped="stopped",d.SegmentEnd="segment_end",d.Transcript="transcript",d.Entity="entity",d.Intent="intent",d.TentativeTranscript="tentative_transcript",d.TentativeEntities="tentative_entities",d.TentativeIntent="tentative_intent";const h=new Error("Current device does not support storage API"),l=new Error("Requested key was not present in storage");var u;t.DecoderState=void 0,(u=t.DecoderState||(t.DecoderState={}))[u.Failed=0]="Failed",u[u.Disconnected=1]="Disconnected",u[u.Connected=2]="Connected",u[u.Active=3]="Active";class p{constructor(){this.stateChangeCbs=[],this.transcriptCbs=[],this.entityCbs=[],this.intentCbs=[],this.segmentChangeCbs=[],this.tentativeTranscriptCbs=[],this.tentativeEntityCbs=[],this.tentativeIntentCbs=[],this.contextStartedCbs=[],this.contextStoppedCbs=[]}}function f(t){var e;return null!==(e=v.get(t))&&void 0!==e?e:"unknown"}const v=new Map([[t.DecoderState.Failed,"Failed"],[t.DecoderState.Disconnected,"Disconnected"],[t.DecoderState.Connected,"Connected"],[t.DecoderState.Active,"Active"]]);var S,g=new Uint8Array(16);function C(){if(!S&&!(S="undefined"!=typeof crypto&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto)||"undefined"!=typeof msCrypto&&"function"==typeof msCrypto.getRandomValues&&msCrypto.getRandomValues.bind(msCrypto)))throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");return S(g)}var b=/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;function m(t){return"string"==typeof t&&b.test(t)}for(var w=[],y=0;y<256;++y)w.push((y+256).toString(16).substr(1));function E(t,e,n){var i=(t=t||{}).random||(t.rng||C)();if(i[6]=15&i[6]|64,i[8]=63&i[8]|128,e){n=n||0;for(var s=0;s<16;++s)e[n+s]=i[s];return e}return function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=(w[t[e+0]]+w[t[e+1]]+w[t[e+2]]+w[t[e+3]]+"-"+w[t[e+4]]+w[t[e+5]]+"-"+w[t[e+6]]+w[t[e+7]]+"-"+w[t[e+8]]+w[t[e+9]]+"-"+w[t[e+10]]+w[t[e+11]]+w[t[e+12]]+w[t[e+13]]+w[t[e+14]]+w[t[e+15]]).toLowerCase();if(!m(n))throw TypeError("Stringified UUID is invalid");return n}(i)}var A="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},k={exports:{}}; | ||
/*! http://mths.be/base64 v0.1.0 by @mathias | MIT license */ | ||
!function(t,e){!function(n){var i=e,s=t&&t.exports==i&&t,o="object"==typeof f&&f;o.global!==o&&o.window!==o||(n=o);var a=function(t){this.message=t};(a.prototype=new Error).name="InvalidCharacterError";var r=function(t){throw new a(t)},l="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",c=/[\t\n\f\r ]/g,h={encode:function(t){t=String(t),/[^\0-\xFF]/.test(t)&&r("The string to be encoded contains characters outside of the Latin1 range.");for(var e,n,i,s,o=t.length%3,a="",c=-1,h=t.length-o;++c<h;)e=t.charCodeAt(c)<<16,n=t.charCodeAt(++c)<<8,i=t.charCodeAt(++c),a+=l.charAt((s=e+n+i)>>18&63)+l.charAt(s>>12&63)+l.charAt(s>>6&63)+l.charAt(63&s);return 2==o?(e=t.charCodeAt(c)<<8,n=t.charCodeAt(++c),a+=l.charAt((s=e+n)>>10)+l.charAt(s>>4&63)+l.charAt(s<<2&63)+"="):1==o&&(s=t.charCodeAt(c),a+=l.charAt(s>>2)+l.charAt(s<<4&63)+"=="),a},decode:function(t){var e=(t=String(t).replace(c,"")).length;e%4==0&&(e=(t=t.replace(/==?$/,"")).length),(e%4==1||/[^+a-zA-Z0-9/]/.test(t))&&r("Invalid character: the string to be decoded is not correctly encoded.");for(var n,i,s=0,o="",a=-1;++a<e;)i=l.indexOf(t.charAt(a)),n=s%4?64*n+i:i,s++%4&&(o+=String.fromCharCode(255&n>>(-2*s&6)));return o},version:"0.1.0"};if(i&&!i.nodeType)if(s)s.exports=h;else for(var d in h)h.hasOwnProperty(d)&&(i[d]=h[d]);else n.base64=h}(f)}(S,S.exports);function C(t,e,n,i,s=Date.now){const o=function(t){const e=t.split(".")[1];let n;try{n=JSON.parse(S.exports.decode(e))}catch(t){throw new Error("Error decoding Speechly token!")}return{appId:n.appId,projectId:n.projectId,deviceId:n.deviceId,configId:n.configId,scopes:n.scope.split(" "),issuer:n.iss,audience:n.aud,expiresAtMs:1e3*n.exp}}(t);return!(o.expiresAtMs-s()<36e5)&&(o.appId===n&&o.projectId===e&&o.deviceId===i)}const g=16e3,b=new Error("Microphone is not initialized"),v=new Error("Microphone is already initialized"),m=new Error("Current device does not support microphone API"),y=new Error("Microphone consent is no given"),w=new Error("AppId changed without project login");class k{constructor(t,e,n,i=!1){this.initialized=!1,this.muted=!1,this.stats={maxSignalEnergy:0},this.handleAudio=t=>{this.muted||t.length>0&&this.apiClient.sendAudio(t)},this.isWebkit=t,this.apiClient=n,this.sampleRate=e,this.debug=i}initialize(t,e){var n;return o(this,void 0,void 0,(function*(){if(void 0===(null===(n=window.navigator)||void 0===n?void 0:n.mediaDevices))throw m;this.audioContext=t,this.resampleRatio=this.audioContext.sampleRate/this.sampleRate;try{this.mediaStream=yield window.navigator.mediaDevices.getUserMedia(e)}catch(t){throw y}if(this.audioTrack=this.mediaStream.getAudioTracks()[0],this.isWebkit||(yield this.audioContext.resume()),void 0!==window.AudioWorkletNode){const t=new Blob(["\n// Indices for the Control SAB.\nconst CONTROL = {\n 'WRITE_INDEX': 0,\n 'FRAMES_AVAILABLE': 1,\n 'LOCK': 2,\n};\n\nclass SpeechlyProcessor extends AudioWorkletProcessor {\n constructor() {\n super();\n\n this._initialized = false;\n this.debug = false;\n this.port.onmessage = this._initialize.bind(this);\n }\n\n _initialize(event) {\n this.controlSAB = new Int32Array(event.data.controlSAB);\n this.dataSAB = new Float32Array(event.data.dataSAB);\n this.debug = event.data.debug;\n this.sharedBufferSize = this.dataSAB.length;\n this.buffer = new Float32Array(0);\n this._initialized = true;\n }\n\n _transferDataToSharedBuffer(data) {\n this.controlSAB[CONTROL.LOCK] = 1\n let inputWriteIndex = this.controlSAB[CONTROL.WRITE_INDEX]\n if (this.controlSAB[CONTROL.FRAMES_AVAILABLE] > 0) {\n if (inputWriteIndex + data.length > this.sharedBufferSize) {\n // console.log('buffer overflow')\n inputWriteIndex = 0\n }\n }\n this.dataSAB.set(data, inputWriteIndex)\n this.controlSAB[CONTROL.WRITE_INDEX] = inputWriteIndex + data.length\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = inputWriteIndex + data.length\n this.controlSAB[CONTROL.LOCK] = 0\n }\n\n _pushData(data) {\n if (this.debug) {\n const signalEnergy = getStandardDeviation(data)\n this.port.postMessage({\n type: 'STATS',\n signalEnergy: signalEnergy\n });\n }\n\n if (this.buffer.length > this.sharedBufferSize) {\n const dataToTransfer = this.buffer.subarray(0, this.sharedBufferSize)\n this._transferDataToSharedBuffer(dataToTransfer)\n this.buffer = this.buffer.subarray(this.sharedBufferSize)\n }\n let concat = new Float32Array(this.buffer.length + data.length)\n concat.set(this.buffer)\n concat.set(data, this.buffer.length)\n this.buffer = concat\n }\n\n process(inputs, outputs, parameters) {\n const inputChannelData = inputs[0][0];\n if (inputChannelData !== undefined) {\n if (this.controlSAB && this.dataSAB) {\n this._pushData(inputChannelData);\n } else {\n this.port.postMessage({\n type: 'DATA',\n frames: inputChannelData\n });\n }\n }\n \n return true;\n }\n}\n\nfunction getStandardDeviation(array) {\n const n = array.length\n const mean = array.reduce((a, b) => a + b) / n\n return Math.sqrt(array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n)\n}\n\nregisterProcessor('speechly-worklet', SpeechlyProcessor);\n"],{type:"text/javascript"}),e=window.URL.createObjectURL(t);yield this.audioContext.audioWorklet.addModule(e);const n=new AudioWorkletNode(this.audioContext,"speechly-worklet");if(this.audioContext.createMediaStreamSource(this.mediaStream).connect(n),n.connect(this.audioContext.destination),void 0!==window.SharedArrayBuffer){const t=new window.SharedArrayBuffer(4*Int32Array.BYTES_PER_ELEMENT),e=new window.SharedArrayBuffer(1024*Float32Array.BYTES_PER_ELEMENT);this.apiClient.postMessage({type:"SET_SHARED_ARRAY_BUFFERS",controlSAB:t,dataSAB:e}),n.port.postMessage({type:"SET_SHARED_ARRAY_BUFFERS",controlSAB:t,dataSAB:e,debug:this.debug})}else this.debug&&console.log("[SpeechlyClient]","can not use SharedArrayBuffer");n.port.onmessage=t=>{switch(t.data.type){case"STATS":t.data.signalEnergy>this.stats.maxSignalEnergy&&(this.stats.maxSignalEnergy=t.data.signalEnergy);break;case"DATA":this.handleAudio(t.data.frames)}}}else{if(this.debug&&console.log("[SpeechlyClient]","can not use AudioWorkletNode"),this.isWebkit){const t=4096*Math.pow(2,Math.ceil(Math.log(this.resampleRatio)/Math.log(2)));this.audioProcessor=this.audioContext.createScriptProcessor(t,1,1)}else this.audioProcessor=this.audioContext.createScriptProcessor(void 0,1,1);this.audioContext.createMediaStreamSource(this.mediaStream).connect(this.audioProcessor),this.audioProcessor.connect(this.audioContext.destination),this.audioProcessor.addEventListener("audioprocess",(t=>{this.handleAudio(t.inputBuffer.getChannelData(0))}))}this.initialized=!0,this.mute()}))}close(){return o(this,void 0,void 0,(function*(){if(this.mute(),!this.initialized)throw b;this.audioTrack.enabled=!1;if(this.mediaStream.getTracks().forEach((t=>t.stop())),null!=this.audioProcessor){this.audioProcessor.disconnect()}this.mediaStream=void 0,this.audioTrack=void 0,this.audioProcessor=void 0,this.initialized=!1}))}mute(){this.muted=!0}unmute(){this.muted=!1}printStats(){if(null!=this.audioTrack){const t=this.audioTrack.getSettings();console.log(this.audioTrack.label,this.audioTrack.readyState),console.log("channelCount",t.channelCount),console.log("latency",t.latency),console.log("autoGainControl",t.autoGainControl)}console.log("maxSignalEnergy",this.stats.maxSignalEnergy)}}var E;t.WebsocketResponseType=void 0,(E=t.WebsocketResponseType||(t.WebsocketResponseType={})).Opened="WEBSOCKET_OPEN",E.Closed="WEBSOCKET_CLOSED",E.SourceSampleRateSetSuccess="SOURSE_SAMPLE_RATE_SET_SUCCESS",E.Started="started",E.Stopped="stopped",E.SegmentEnd="segment_end",E.Transcript="transcript",E.Entity="entity",E.Intent="intent",E.TentativeTranscript="tentative_transcript",E.TentativeEntities="tentative_entities",E.TentativeIntent="tentative_intent";class A{constructor(){this.startCbs=[],this.stopCbs=[],this.onResponseCb=()=>{},this.onCloseCb=()=>{},this.onWebsocketMessage=e=>{const n=e.data;switch(n.type){case t.WebsocketResponseType.Opened:null!=this.resolveInitialization&&this.resolveInitialization();break;case t.WebsocketResponseType.Closed:this.onCloseCb({code:e.data.code,reason:e.data.reason,wasClean:e.data.wasClean});break;case t.WebsocketResponseType.SourceSampleRateSetSuccess:null!=this.resolveSourceSampleRateSet&&this.resolveSourceSampleRateSet();break;case t.WebsocketResponseType.Started:this.startCbs.forEach((t=>{try{t(void 0,n.audio_context)}catch(t){console.error('[SpeechlyClient] Error while invoking "onStart" callback:',t)}})),this.startCbs.length=0;break;case t.WebsocketResponseType.Stopped:this.stopCbs.forEach((t=>{try{t(void 0,n.audio_context)}catch(t){console.error('[SpeechlyClient] Error while invoking "onStop" callback:',t)}})),this.stopCbs.length=0;break;default:this.onResponseCb(n)}};const e=new Blob(["/**\n * Known WebSocket response types.\n * @public\n */\nvar WebsocketResponseType;\n(function (WebsocketResponseType) {\n WebsocketResponseType[\"Opened\"] = \"WEBSOCKET_OPEN\";\n WebsocketResponseType[\"SourceSampleRateSetSuccess\"] = \"SOURSE_SAMPLE_RATE_SET_SUCCESS\";\n WebsocketResponseType[\"Started\"] = \"started\";\n WebsocketResponseType[\"Stopped\"] = \"stopped\";\n})(WebsocketResponseType || (WebsocketResponseType = {}));\nvar CONTROL = {\n WRITE_INDEX: 0,\n FRAMES_AVAILABLE: 1,\n LOCK: 2\n};\nvar WebsocketClient = /** @class */ (function () {\n function WebsocketClient(ctx) {\n var _this = this;\n this.isContextStarted = false;\n this.isStartContextConfirmed = false;\n this.shouldResendLastFramesSent = false;\n this.buffer = new Float32Array(0);\n this.lastFramesSent = new Int16Array(0); // to re-send after switch context\n this.debug = false;\n this.initialized = false;\n // WebSocket's close handler, called e.g. when\n // - normal close (code 1000)\n // - network unreachable or unable to (re)connect (code 1006)\n // List of CloseEvent.code values: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code\n this.onWebsocketClose = function (event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'onWebsocketClose');\n }\n _this.websocket.removeEventListener('open', _this.onWebsocketOpen);\n _this.websocket.removeEventListener('message', _this.onWebsocketMessage);\n _this.websocket.removeEventListener('error', _this.onWebsocketError);\n _this.websocket.removeEventListener('close', _this.onWebsocketClose);\n _this.websocket = undefined;\n _this.workerCtx.postMessage({ type: 'WEBSOCKET_CLOSED', code: event.code, reason: event.reason, wasClean: event.wasClean });\n };\n this.onWebsocketOpen = function (_event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'websocket opened');\n }\n if (_this.isContextStarted && !_this.isStartContextConfirmed) {\n _this.send(_this.outbox);\n }\n _this.workerCtx.postMessage({ type: 'WEBSOCKET_OPEN' });\n };\n this.onWebsocketError = function (_event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'websocket error');\n }\n };\n this.onWebsocketMessage = function (event) {\n var response;\n try {\n response = JSON.parse(event.data);\n }\n catch (e) {\n console.error('[SpeechlyClient] Error parsing response from the server:', e);\n return;\n }\n if (response.type === WebsocketResponseType.Started) {\n _this.isStartContextConfirmed = true;\n if (_this.shouldResendLastFramesSent) {\n _this.resendLastFrames();\n _this.shouldResendLastFramesSent = false;\n }\n }\n _this.workerCtx.postMessage(response);\n };\n this.workerCtx = ctx;\n }\n WebsocketClient.prototype.init = function (apiUrl, authToken, targetSampleRate, debug) {\n this.debug = debug;\n if (this.debug) {\n console.log('[SpeechlyClient]', 'initialize worker');\n }\n this.apiUrl = apiUrl;\n this.authToken = authToken;\n this.targetSampleRate = targetSampleRate;\n this.initialized = true;\n this.isContextStarted = false;\n this.connect(0);\n };\n WebsocketClient.prototype.setSourceSampleRate = function (sourceSampleRate) {\n this.sourceSampleRate = sourceSampleRate;\n this.resampleRatio = this.sourceSampleRate / this.targetSampleRate;\n if (this.debug) {\n console.log('[SpeechlyClient]', 'resampleRatio', this.resampleRatio);\n }\n if (this.resampleRatio > 1) {\n this.filter = generateFilter(this.sourceSampleRate, this.targetSampleRate, 127);\n }\n this.workerCtx.postMessage({ type: 'SOURSE_SAMPLE_RATE_SET_SUCCESS' });\n if (isNaN(this.resampleRatio)) {\n throw Error(\"resampleRatio is NaN source rate is \".concat(this.sourceSampleRate, \" and target rate is \").concat(this.targetSampleRate));\n }\n };\n WebsocketClient.prototype.setSharedArrayBuffers = function (controlSAB, dataSAB) {\n this.controlSAB = new Int32Array(controlSAB);\n this.dataSAB = new Float32Array(dataSAB);\n var audioHandleInterval = this.dataSAB.length / 32; // ms\n if (this.debug) {\n console.log('[SpeechlyClient]', 'Audio handle interval', audioHandleInterval, 'ms');\n }\n setInterval(this.sendAudioFromSAB.bind(this), audioHandleInterval);\n };\n WebsocketClient.prototype.connect = function (timeout) {\n if (timeout === void 0) { timeout = 1000; }\n if (this.debug) {\n console.log('[SpeechlyClient]', 'connect in ', timeout / 1000, 'sec');\n }\n setTimeout(this.initializeWebsocket.bind(this), timeout);\n };\n WebsocketClient.prototype.initializeWebsocket = function () {\n if (this.debug) {\n console.log('[SpeechlyClient]', 'connecting to ', this.apiUrl);\n }\n this.websocket = new WebSocket(this.apiUrl, this.authToken);\n this.websocket.addEventListener('open', this.onWebsocketOpen);\n this.websocket.addEventListener('message', this.onWebsocketMessage);\n this.websocket.addEventListener('error', this.onWebsocketError);\n this.websocket.addEventListener('close', this.onWebsocketClose);\n };\n WebsocketClient.prototype.isOpen = function () {\n return this.websocket !== undefined && this.websocket.readyState === this.websocket.OPEN;\n };\n WebsocketClient.prototype.resendLastFrames = function () {\n if (this.lastFramesSent.length > 0) {\n this.send(this.lastFramesSent);\n this.lastFramesSent = new Int16Array(0);\n }\n };\n WebsocketClient.prototype.sendAudio = function (audioChunk) {\n if (!this.isContextStarted) {\n return;\n }\n if (audioChunk.length > 0) {\n if (this.resampleRatio > 1) {\n // Downsampling\n this.send(this.downsample(audioChunk));\n }\n else {\n this.send(float32ToInt16(audioChunk));\n }\n }\n };\n WebsocketClient.prototype.sendAudioFromSAB = function () {\n if (!this.isContextStarted) {\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = 0;\n this.controlSAB[CONTROL.WRITE_INDEX] = 0;\n return;\n }\n if (this.controlSAB == undefined) {\n return;\n }\n var framesAvailable = this.controlSAB[CONTROL.FRAMES_AVAILABLE];\n var lock = this.controlSAB[CONTROL.LOCK];\n if (lock == 0 && framesAvailable > 0) {\n var data = this.dataSAB.subarray(0, framesAvailable);\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = 0;\n this.controlSAB[CONTROL.WRITE_INDEX] = 0;\n if (data.length > 0) {\n var frames_1;\n if (this.resampleRatio > 1) {\n frames_1 = this.downsample(data);\n }\n else {\n frames_1 = float32ToInt16(data);\n }\n this.send(frames_1);\n // 16000 per second, 1000 in 100 ms\n // save last 250 ms\n if (this.lastFramesSent.length > 1024 * 4) {\n this.lastFramesSent = frames_1;\n }\n else {\n var concat = new Int16Array(this.lastFramesSent.length + frames_1.length);\n concat.set(this.lastFramesSent);\n concat.set(frames_1, this.lastFramesSent.length);\n this.lastFramesSent = concat;\n }\n }\n }\n };\n WebsocketClient.prototype.startContext = function (appId) {\n if (this.isContextStarted) {\n console.log('Cant start context: it has been already started');\n return;\n }\n this.isContextStarted = true;\n this.isStartContextConfirmed = false;\n if (appId !== undefined) {\n this.outbox = JSON.stringify({ event: 'start', appId: appId });\n }\n else {\n this.outbox = JSON.stringify({ event: 'start' });\n }\n this.send(this.outbox);\n };\n WebsocketClient.prototype.stopContext = function () {\n if (!this.websocket) {\n throw Error('Cant start context: websocket is undefined');\n }\n if (!this.isContextStarted) {\n console.log('Cant stop context: it is not started');\n return;\n }\n this.isContextStarted = false;\n this.isStartContextConfirmed = false;\n var StopEventJSON = JSON.stringify({ event: 'stop' });\n this.send(StopEventJSON);\n };\n WebsocketClient.prototype.switchContext = function (newAppId) {\n if (!this.websocket) {\n throw Error('Cant switch context: websocket is undefined');\n }\n if (!this.isContextStarted) {\n console.log('Cant switch context: it is not started');\n return;\n }\n if (newAppId == undefined) {\n console.log('Cant switch context: new app id is undefined');\n return;\n }\n this.isStartContextConfirmed = false;\n var StopEventJSON = JSON.stringify({ event: 'stop' });\n this.send(StopEventJSON);\n this.shouldResendLastFramesSent = true;\n this.send(JSON.stringify({ event: 'start', appId: newAppId }));\n };\n WebsocketClient.prototype.closeWebsocket = function (websocketCode, reason) {\n if (websocketCode === void 0) { websocketCode = 1005; }\n if (reason === void 0) { reason = \"No Status Received\"; }\n if (this.debug) {\n console.log('[SpeechlyClient]', 'Websocket closing');\n }\n if (!this.websocket) {\n throw Error('Websocket is not open');\n }\n this.websocket.close(websocketCode, reason);\n };\n WebsocketClient.prototype.downsample = function (input) {\n var inputBuffer = new Float32Array(this.buffer.length + input.length);\n inputBuffer.set(this.buffer, 0);\n inputBuffer.set(input, this.buffer.length);\n var outputLength = Math.ceil((inputBuffer.length - this.filter.length) / this.resampleRatio);\n var outputBuffer = new Int16Array(outputLength);\n for (var i = 0; i < outputLength; i++) {\n var offset = Math.round(this.resampleRatio * i);\n var val = 0.0;\n for (var j = 0; j < this.filter.length; j++) {\n val += inputBuffer[offset + j] * this.filter[j];\n }\n outputBuffer[i] = val * (val < 0 ? 0x8000 : 0x7fff);\n }\n var remainingOffset = Math.round(this.resampleRatio * outputLength);\n if (remainingOffset < inputBuffer.length) {\n this.buffer = inputBuffer.subarray(remainingOffset);\n }\n else {\n this.buffer = new Float32Array(0);\n }\n return outputBuffer;\n };\n WebsocketClient.prototype.send = function (data) {\n if (this.isOpen()) {\n try {\n this.websocket.send(data);\n }\n catch (error) {\n console.log('[SpeechlyClient]', 'Server connection error', error);\n }\n }\n };\n return WebsocketClient;\n}());\nvar ctx = self;\nvar websocketClient = new WebsocketClient(ctx);\nctx.onmessage = function (e) {\n switch (e.data.type) {\n case 'INIT':\n websocketClient.init(e.data.apiUrl, e.data.authToken, e.data.targetSampleRate, e.data.debug);\n break;\n case 'SET_SOURSE_SAMPLE_RATE':\n websocketClient.setSourceSampleRate(e.data.sourceSampleRate);\n break;\n case 'SET_SHARED_ARRAY_BUFFERS':\n websocketClient.setSharedArrayBuffers(e.data.controlSAB, e.data.dataSAB);\n break;\n case 'CLOSE':\n websocketClient.closeWebsocket(1000, \"Close requested by client\");\n break;\n case 'START_CONTEXT':\n websocketClient.startContext(e.data.appId);\n break;\n case 'SWITCH_CONTEXT':\n websocketClient.switchContext(e.data.appId);\n break;\n case 'STOP_CONTEXT':\n websocketClient.stopContext();\n break;\n case 'AUDIO':\n websocketClient.sendAudio(e.data.payload);\n break;\n default:\n console.log('WORKER', e);\n }\n};\nfunction float32ToInt16(buffer) {\n var buf = new Int16Array(buffer.length);\n for (var l = 0; l < buffer.length; l++) {\n buf[l] = buffer[l] * (buffer[l] < 0 ? 0x8000 : 0x7fff);\n }\n return buf;\n}\nfunction generateFilter(sourceSampleRate, targetSampleRate, length) {\n if (length % 2 === 0) {\n throw Error('Filter length must be odd');\n }\n var cutoff = targetSampleRate / 2;\n var filter = new Float32Array(length);\n var sum = 0;\n for (var i = 0; i < length; i++) {\n var x = sinc(((2 * cutoff) / sourceSampleRate) * (i - (length - 1) / 2));\n sum += x;\n filter[i] = x;\n }\n for (var i = 0; i < length; i++) {\n filter[i] = filter[i] / sum;\n }\n return filter;\n}\nfunction sinc(x) {\n if (x === 0.0) {\n return 1.0;\n }\n var piX = Math.PI * x;\n return Math.sin(piX) / piX;\n}\n"],{type:"text/javascript"}),n=window.URL.createObjectURL(e);this.worker=new Worker(n),this.worker.addEventListener("message",this.onWebsocketMessage)}onResponse(t){this.onResponseCb=t}onClose(t){this.onCloseCb=t}initialize(t,e,n,i){return o(this,void 0,void 0,(function*(){return this.worker.postMessage({type:"INIT",apiUrl:t,authToken:e,targetSampleRate:n,debug:i}),this.startCbs=[],this.stopCbs=[],new Promise((t=>{this.resolveInitialization=t}))}))}setSourceSampleRate(t){return o(this,void 0,void 0,(function*(){return this.worker.postMessage({type:"SET_SOURSE_SAMPLE_RATE",sourceSampleRate:t}),new Promise((t=>{this.resolveSourceSampleRateSet=t}))}))}close(){return o(this,void 0,void 0,(function*(){return new Promise(((t,e)=>{this.worker.postMessage({type:"CLOSE",code:1e3,message:"Client has ended the session"}),t()}))}))}startContext(t){return o(this,void 0,void 0,(function*(){return new Promise(((e,n)=>{this.startCbs.push(((t,i)=>{void 0!==t?n(t):e(i)})),null!=t?this.worker.postMessage({type:"START_CONTEXT",appId:t}):this.worker.postMessage({type:"START_CONTEXT"})}))}))}stopContext(){return o(this,void 0,void 0,(function*(){return new Promise(((t,e)=>{this.stopCbs.push(((n,i)=>{void 0!==n?e(n):t(i)})),this.worker.postMessage({type:"STOP_CONTEXT"})}))}))}switchContext(t){return o(this,void 0,void 0,(function*(){return new Promise(((e,n)=>{this.startCbs.push(((t,i)=>{void 0!==t?n(t):e(i)})),this.worker.postMessage({type:"SWITCH_CONTEXT",appId:t})}))}))}postMessage(t){this.worker.postMessage(t)}sendAudio(t){this.worker.postMessage({type:"AUDIO",payload:t})}}class R{constructor(){this.storage=window.localStorage}get(t){return this.storage.getItem(t)}set(t,e){this.storage.setItem(t,e)}getOrSet(t,e){let n=this.storage.getItem(t);return null===n&&(n=e(),this.storage.setItem(t,n)),n}}const T=new Error("Current device does not support storage API"),x=new Error("Requested key was not present in storage");class I{constructor(t,e){this.isFinalized=!1,this.words=[],this.entities=new Map,this.intent={intent:"",isFinal:!1},this.contextId=t,this.id=e}toSegment(){let t=0;const e=new Array(this.entities.size);return this.entities.forEach((n=>{e[t]=n,t++})),{id:this.id,contextId:this.contextId,isFinal:this.isFinalized,words:this.words,entities:e,intent:this.intent}}toString(){const t=this.toSegment(),e=t.words.filter((t=>t.value)).map((t=>({value:t.value,index:t.index}))),n=Object.assign(Object.assign({},t),{words:e});return JSON.stringify(n,null,2)}updateTranscript(t){return t.forEach((t=>{this.isFinalized&&!t.isFinal||(this.words[t.index]=t)})),this}updateEntities(t){return t.forEach((t=>{this.isFinalized&&!t.isFinal||this.entities.set(function(t){return`${t.startPosition.toString()}:${t.endPosition.toString()}`}(t),t)})),this}updateIntent(t){return this.isFinalized&&!t.isFinal||(this.intent=t),this}finalize(){return this.entities.forEach(((t,e)=>{t.isFinal||this.entities.delete(e)})),this.words=this.words.filter((t=>t.isFinal)),this.intent.isFinal||(this.intent.intent="",this.intent.isFinal=!0),this.isFinalized=!0,this}}function _(t,e){return{intent:t.intent,isFinal:e}}const W="speechly-auth-token";t.Client=class{constructor(e){var n,i,s,o,a,r,l,c,h,d,u;this.listening=!1,this.activeContexts=new Map,this.maxReconnectAttemptCount=10,this.contextStopDelay=250,this.connectAttempt=0,this.connectPromise=null,this.initializePromise=null,this.listeningPromise=null,this.state=t.ClientState.Disconnected,this.stateChangeCb=()=>{},this.segmentChangeCb=()=>{},this.tentativeTranscriptCb=()=>{},this.tentativeEntitiesCb=()=>{},this.tentativeIntentCb=()=>{},this.transcriptCb=()=>{},this.entityCb=()=>{},this.intentCb=()=>{},this.handleWebsocketResponse=e=>{var n;this.debug&&console.log("[SpeechlyClient]","Received response",e);const{audio_context:i,segment_id:s,type:o}=e;let{data:a}=e;const r=this.activeContexts.get(i);if(void 0===r)return void console.warn("[SpeechlyClient]","Received response for non-existent context",i);let l=null!==(n=r.get(s))&&void 0!==n?n:new I(i,s);switch(o){case t.WebsocketResponseType.TentativeTranscript:a=a;const e=function(t){return t.words.map((({word:t,index:e,start_timestamp:n,end_timestamp:i})=>({value:t,index:e,startTimestamp:n,endTimestamp:i,isFinal:!1})))}(a);this.tentativeTranscriptCb(i,s,e,a.transcript),l=l.updateTranscript(e);break;case t.WebsocketResponseType.Transcript:a=a;const n=function(t){return{value:t.word,index:t.index,startTimestamp:t.start_timestamp,endTimestamp:t.end_timestamp,isFinal:!0}}(a);this.transcriptCb(i,s,n),l=l.updateTranscript([n]);break;case t.WebsocketResponseType.TentativeEntities:a=a;const o=function(t){return t.entities.map((({entity:t,value:e,start_position:n,end_position:i})=>({type:t,value:e,startPosition:n,endPosition:i,isFinal:!1})))}(a);this.tentativeEntitiesCb(i,s,o),l=l.updateEntities(o);break;case t.WebsocketResponseType.Entity:a=a;const r=function(t){return{type:t.entity,value:t.value,startPosition:t.start_position,endPosition:t.end_position,isFinal:!0}}(a);this.entityCb(i,s,r),l=l.updateEntities([r]);break;case t.WebsocketResponseType.TentativeIntent:a=a;const c=_(a,!1);this.tentativeIntentCb(i,s,c),l=l.updateIntent(c);break;case t.WebsocketResponseType.Intent:a=a;const h=_(a,!0);this.intentCb(i,s,h),l=l.updateIntent(h);break;case t.WebsocketResponseType.SegmentEnd:l=l.finalize()}r.set(s,l),this.activeContexts.set(i,r),this.logSegments&&console.info(l.toString()),this.segmentChangeCb(l.toSegment())},this.handleWebsocketClosure=e=>{if(1e3===e.code)this.debug&&console.log("[SpeechlyClient]","Websocket closed",e);else{if(console.error("[SpeechlyClient]","Websocket closed due to error",e),void 0===this.deviceId)return void this.setState(t.ClientState.Failed);this.listening=!1,this.listeningPromise=null,this.microphone.mute(),this.setState(t.ClientState.Disconnected),this.reconnect()}},this.sampleRate=null!==(n=e.sampleRate)&&void 0!==n?n:g;try{const t=window.navigator.mediaDevices.getSupportedConstraints();this.nativeResamplingSupported=!0===t.sampleRate,null!=e.autoGainControl&&e.autoGainControl?this.autoGainControl=!0===t.autoGainControl:this.autoGainControl=!1}catch(t){this.nativeResamplingSupported=!1,this.autoGainControl=!1}if(this.debug=null!==(i=e.debug)&&void 0!==i&&i,this.logSegments=null!==(s=e.logSegments)&&void 0!==s&&s,this.loginUrl=null!==(o=e.loginUrl)&&void 0!==o?o:"https://api.speechly.com/login",this.appId=null!==(a=e.appId)&&void 0!==a?a:void 0,this.projectId=null!==(r=e.projectId)&&void 0!==r?r:void 0,this.apiClient=null!==(l=e.apiClient)&&void 0!==l?l:new A,this.apiUrl=function(t,e){const n=new URLSearchParams;return n.append("sampleRate",e.toString()),`${t}?${n.toString()}`}(null!==(c=e.apiUrl)&&void 0!==c?c:"wss://api.speechly.com/ws/v1",null!==(h=e.sampleRate)&&void 0!==h?h:g),void 0!==this.appId&&void 0!==this.projectId)throw Error("[SpeechlyClient] You cannot use both appId and projectId at the same time");if(this.storage=null!==(d=e.storage)&&void 0!==d?d:new R,this.deviceId=this.storage.getOrSet("speechly-device-id",p),void 0!==window.AudioContext)this.isWebkit=!1;else{if(void 0===window.webkitAudioContext)throw m;this.isWebkit=!0}this.microphone=null!==(u=e.microphone)&&void 0!==u?u:new k(this.isWebkit,this.sampleRate,this.apiClient,this.debug),this.apiClient.onResponse(this.handleWebsocketResponse),this.apiClient.onClose(this.handleWebsocketClosure),window.SpeechlyClient=this,!1!==e.connect&&this.connect()}getReconnectDelayMs(t){return 100*Math.pow(2,t)}sleep(t){return o(this,void 0,void 0,(function*(){return new Promise((e=>setTimeout(e,t)))}))}isListening(){return this.listening}connect(){return o(this,void 0,void 0,(function*(){null===this.connectPromise&&(this.connectPromise=(()=>o(this,void 0,void 0,(function*(){this.advanceState(t.ClientState.Connecting);const e=this.storage.get(W);if(null!=e&&C(e,this.projectId,this.appId,this.deviceId))this.authToken=e;else try{this.authToken=yield function(t,e,n,i,s=fetch,a=Date.now){var r;return o(this,void 0,void 0,(function*(){let o;o=void 0!==e?{projectId:e,deviceId:i}:{appId:n,deviceId:i};const l=yield s(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}),c=yield l.json();if(200!==l.status)throw Error(null!==(r=c.error)&&void 0!==r?r:`Speechly API login request failed with ${l.status}`);if(void 0===c.access_token)throw Error("Invalid login response from Speechly API");if(!C(c.access_token,e,n,i,a))throw Error("Invalid token received from Speechly API");return c.access_token}))}(this.loginUrl,this.projectId,this.appId,this.deviceId),this.storage.set(W,this.authToken)}catch(e){throw this.setState(t.ClientState.Failed),e}try{yield this.apiClient.initialize(this.apiUrl,this.authToken,this.sampleRate,this.debug)}catch(e){throw this.setState(t.ClientState.Failed),e}})))()),yield this.connectPromise,this.advanceState(t.ClientState.Preinitialized)}))}initialize(){return o(this,void 0,void 0,(function*(){null===this.initializePromise&&(this.initializePromise=(()=>o(this,void 0,void 0,(function*(){yield this.connect(),this.advanceState(t.ClientState.Initializing);try{if(this.isWebkit)void 0!==window.webkitAudioContext&&(this.audioContext=new window.webkitAudioContext);else{const t={};this.nativeResamplingSupported&&(t.sampleRate=this.sampleRate),this.audioContext=new window.AudioContext(t)}const e={video:!1};if(this.nativeResamplingSupported||this.autoGainControl?e.audio={sampleRate:this.sampleRate,autoGainControl:this.autoGainControl}:e.audio=!0,null==this.audioContext)throw m;this.isWebkit&&(yield this.audioContext.resume()),yield this.apiClient.setSourceSampleRate(this.audioContext.sampleRate),yield this.microphone.initialize(this.audioContext,e),this.advanceState(t.ClientState.Connected)}catch(e){switch(e){case m:this.setState(t.ClientState.NoBrowserSupport);break;case y:this.setState(t.ClientState.NoAudioConsent);break;default:this.setState(t.ClientState.Failed)}throw e}})))()),yield this.initializePromise,this.advanceState(t.ClientState.Connected)}))}close(){return o(this,void 0,void 0,(function*(){const e=[];try{yield this.microphone.close()}catch(t){e.push(t.message)}try{yield this.apiClient.close()}catch(t){e.push(t.message)}if(this.activeContexts.clear(),this.connectPromise=null,this.initializePromise=null,this.setState(t.ClientState.Disconnected),e.length>0)throw Error(e.join(","))}))}hasUnrecoverableError(){return this.state<t.ClientState.__UnrecoverableErrors}queueTask(t){return o(this,void 0,void 0,(function*(){const e=this.listeningPromise;return this.listeningPromise=(()=>o(this,void 0,void 0,(function*(){return yield e,t()})))(),this.listeningPromise}))}startContext(e){return o(this,void 0,void 0,(function*(){if(!this.hasUnrecoverableError()){if(this.listening)throw Error("Already listening");this.listening=!0;return yield this.queueTask((()=>o(this,void 0,void 0,(function*(){if(this.state<t.ClientState.Connected&&(yield this.initialize()),this.state!==t.ClientState.Connected)throw Error("[SpeechlyClient] Unable to complete startContext: Expected Connected state, but was in "+n(this.state)+". Did you call startContext multiple times without stopContext?");let i;if(this.setState(t.ClientState.Starting),this.microphone.unmute(),null!=this.projectId)i=yield this.apiClient.startContext(e);else{if(null!=e&&this.appId!==e)throw this.setState(t.ClientState.Failed),w;i=yield this.apiClient.startContext()}if(this.state!==t.ClientState.Starting)throw Error("[SpeechlyClient] Unable to complete startContext: Problem acquiring contextId");return this.activeContexts.set(i,new Map),this.setState(t.ClientState.Recording),i}))))}throw Error("[SpeechlyClient] startContext cannot be run in unrecovable error state.")}))}stopContext(){return o(this,void 0,void 0,(function*(){if(!this.hasUnrecoverableError()){if(!this.listening)throw Error("Already stopped listening");this.listening=!1;return yield this.queueTask((()=>o(this,void 0,void 0,(function*(){if(this.state!==t.ClientState.Recording)throw Error("[SpeechlyClient] Unable to complete stopContext: Expected Recording state, but was in "+n(this.state)+".");this.setState(t.ClientState.Stopping),yield this.sleep(this.contextStopDelay),this.microphone.mute();try{const e=yield this.apiClient.stopContext();return this.activeContexts.delete(e),this.setState(t.ClientState.Connected),e}catch(e){throw this.setState(t.ClientState.Failed),e}}))))}throw Error("[SpeechlyClient] stopContext cannot be run in unrecovable error state.")}))}switchContext(e){return o(this,void 0,void 0,(function*(){yield this.queueTask((()=>o(this,void 0,void 0,(function*(){if(this.state!==t.ClientState.Recording)throw Error("[SpeechlyClient] Unable to complete switchContext: Expected Recording state, but was in "+n(this.state)+".");const i=yield this.apiClient.switchContext(e);this.activeContexts.set(i,new Map)}))))}))}onStateChange(t){this.stateChangeCb=t}onSegmentChange(t){this.segmentChangeCb=t}onTentativeTranscript(t){this.tentativeTranscriptCb=t}onTranscript(t){this.transcriptCb=t}onTentativeEntities(t){this.tentativeEntitiesCb=t}onEntity(t){this.entityCb=t}onTentativeIntent(t){this.tentativeIntentCb=t}onIntent(t){this.intentCb=t}reconnect(){return o(this,void 0,void 0,(function*(){this.debug&&console.log("[SpeechlyClient]","Reconnecting...",this.connectAttempt),this.connectPromise=null,!this.hasUnrecoverableError()&&this.connectAttempt<this.maxReconnectAttemptCount?(yield this.sleep(this.getReconnectDelayMs(this.connectAttempt++)),yield this.connect()):console.error("[SpeechlyClient] Maximum reconnect count reached, giving up automatic reconnect.")}))}advanceState(t){this.state>=t||this.setState(t)}setState(t){this.state!==t&&(this.debug&&console.log("[SpeechlyClient]",n(this.state),"->",n(t)),this.state=t,this.stateChangeCb(t))}printStats(){this.microphone.printStats()}sendAudioData(e){return o(this,void 0,void 0,(function*(){const n=yield this.audioContext.decodeAudioData(e),i=n.getChannelData(0);if(n.numberOfChannels>1){const t=n.getChannelData(1);for(let e=0;e<i.length;e++)i[e]=(i[e]+t[e])/2}this.listening=!0,this.setState(t.ClientState.Starting);const s=yield this.apiClient.startContext();let o;this.activeContexts.set(s,new Map),this.setState(t.ClientState.Recording);for(let t=0;t<i.length;t+=16e3){const e=t+16e3;o=e>i.length?i.slice(t):i.slice(t,e),this.apiClient.sendAudio(o)}this.listening=!1,this.setState(t.ClientState.Stopping),yield this.apiClient.stopContext(),this.activeContexts.delete(s),this.setState(t.ClientState.Connected)}))}},t.DefaultSampleRate=g,t.ErrAlreadyInitialized=v,t.ErrAppIdChangeWithoutProjectLogin=w,t.ErrDeviceNotSupported=m,t.ErrKeyNotFound=x,t.ErrNoAudioConsent=y,t.ErrNoStorageSupport=T,t.ErrNotInitialized=b,t.stateToString=n,Object.defineProperty(t,"__esModule",{value:!0})})); | ||
!function(t,e){!function(n){var i=e,s=t&&t.exports==i&&t,o="object"==typeof A&&A;o.global!==o&&o.window!==o||(n=o);var r=function(t){this.message=t};(r.prototype=new Error).name="InvalidCharacterError";var a=function(t){throw new r(t)},c="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",d=/[\t\n\f\r ]/g,h={encode:function(t){t=String(t),/[^\0-\xFF]/.test(t)&&a("The string to be encoded contains characters outside of the Latin1 range.");for(var e,n,i,s,o=t.length%3,r="",d=-1,h=t.length-o;++d<h;)e=t.charCodeAt(d)<<16,n=t.charCodeAt(++d)<<8,i=t.charCodeAt(++d),r+=c.charAt((s=e+n+i)>>18&63)+c.charAt(s>>12&63)+c.charAt(s>>6&63)+c.charAt(63&s);return 2==o?(e=t.charCodeAt(d)<<8,n=t.charCodeAt(++d),r+=c.charAt((s=e+n)>>10)+c.charAt(s>>4&63)+c.charAt(s<<2&63)+"="):1==o&&(s=t.charCodeAt(d),r+=c.charAt(s>>2)+c.charAt(s<<4&63)+"=="),r},decode:function(t){var e=(t=String(t).replace(d,"")).length;e%4==0&&(e=(t=t.replace(/==?$/,"")).length),(e%4==1||/[^+a-zA-Z0-9/]/.test(t))&&a("Invalid character: the string to be decoded is not correctly encoded.");for(var n,i,s=0,o="",r=-1;++r<e;)i=c.indexOf(t.charAt(r)),n=s%4?64*n+i:i,s++%4&&(o+=String.fromCharCode(255&n>>(-2*s&6)));return o},version:"0.1.0"};if(i&&!i.nodeType)if(s)s.exports=h;else for(var l in h)h.hasOwnProperty(l)&&(i[l]=h[l]);else n.base64=h}(A)}(k,k.exports);function R(t,e,n,i,s=Date.now){const o=function(t){const e=t.split(".")[1];let n;try{n=JSON.parse(k.exports.decode(e))}catch(t){throw new Error("Error decoding Speechly token!")}return{appId:n.appId,projectId:n.projectId,deviceId:n.deviceId,configId:n.configId,scopes:n.scope.split(" "),issuer:n.iss,audience:n.aud,expiresAtMs:1e3*n.exp}}(t);return!(o.expiresAtMs-s()<36e5)&&(o.appId===n&&o.projectId===e&&o.deviceId===i)}class x{constructor(){this.startCbs=[],this.stopCbs=[],this.onResponseCb=()=>{},this.onCloseCb=()=>{},this.onWebsocketMessage=e=>{const n=e.data;switch(n.type){case t.WebsocketResponseType.Opened:null!=this.resolveInitialization&&this.resolveInitialization();break;case t.WebsocketResponseType.Closed:this.onCloseCb({code:e.data.code,reason:e.data.reason,wasClean:e.data.wasClean});break;case t.WebsocketResponseType.SourceSampleRateSetSuccess:null!=this.resolveSourceSampleRateSet&&this.resolveSourceSampleRateSet();break;case t.WebsocketResponseType.Started:this.startCbs.forEach((t=>{try{t(void 0,n.audio_context)}catch(t){console.error('[SpeechlyClient] Error while invoking "onStart" callback:',t)}})),this.startCbs.length=0;break;case t.WebsocketResponseType.Stopped:this.stopCbs.forEach((t=>{try{t(void 0,n.audio_context)}catch(t){console.error('[SpeechlyClient] Error while invoking "onStop" callback:',t)}})),this.stopCbs.length=0;break;default:this.onResponseCb(n)}};const e=new Blob(["/**\n * Known WebSocket response types.\n * @public\n */\nvar WebsocketResponseType;\n(function (WebsocketResponseType) {\n WebsocketResponseType[\"Opened\"] = \"WEBSOCKET_OPEN\";\n WebsocketResponseType[\"SourceSampleRateSetSuccess\"] = \"SOURCE_SAMPLE_RATE_SET_SUCCESS\";\n WebsocketResponseType[\"Started\"] = \"started\";\n WebsocketResponseType[\"Stopped\"] = \"stopped\";\n})(WebsocketResponseType || (WebsocketResponseType = {}));\nvar CONTROL = {\n WRITE_INDEX: 0,\n FRAMES_AVAILABLE: 1,\n LOCK: 2\n};\nvar WebsocketClient = /** @class */ (function () {\n function WebsocketClient(ctx) {\n var _this = this;\n this.isContextStarted = false;\n this.isStartContextConfirmed = false;\n this.shouldResendLastFramesSent = false;\n this.buffer = new Float32Array(0);\n this.lastFramesSent = new Int16Array(0); // to re-send after switch context\n this.debug = false;\n this.initialized = false;\n // WebSocket's close handler, called e.g. when\n // - normal close (code 1000)\n // - network unreachable or unable to (re)connect (code 1006)\n // List of CloseEvent.code values: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code\n this.onWebsocketClose = function (event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'onWebsocketClose');\n }\n _this.websocket.removeEventListener('open', _this.onWebsocketOpen);\n _this.websocket.removeEventListener('message', _this.onWebsocketMessage);\n _this.websocket.removeEventListener('error', _this.onWebsocketError);\n _this.websocket.removeEventListener('close', _this.onWebsocketClose);\n _this.websocket = undefined;\n _this.workerCtx.postMessage({\n type: 'WEBSOCKET_CLOSED',\n code: event.code,\n reason: event.reason,\n wasClean: event.wasClean\n });\n };\n this.onWebsocketOpen = function (_event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'websocket opened');\n }\n if (_this.isContextStarted && !_this.isStartContextConfirmed) {\n _this.send(_this.outbox);\n }\n _this.workerCtx.postMessage({ type: 'WEBSOCKET_OPEN' });\n };\n this.onWebsocketError = function (_event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'websocket error');\n }\n };\n this.onWebsocketMessage = function (event) {\n var response;\n try {\n response = JSON.parse(event.data);\n }\n catch (e) {\n console.error('[SpeechlyClient] Error parsing response from the server:', e);\n return;\n }\n if (response.type === WebsocketResponseType.Started) {\n _this.isStartContextConfirmed = true;\n if (_this.shouldResendLastFramesSent) {\n _this.resendLastFrames();\n _this.shouldResendLastFramesSent = false;\n }\n }\n _this.workerCtx.postMessage(response);\n };\n this.workerCtx = ctx;\n }\n WebsocketClient.prototype.init = function (apiUrl, authToken, targetSampleRate, debug) {\n this.debug = debug;\n if (this.debug) {\n console.log('[SpeechlyClient]', 'initialize worker');\n }\n this.apiUrl = apiUrl;\n this.authToken = authToken;\n this.targetSampleRate = targetSampleRate;\n this.initialized = true;\n this.isContextStarted = false;\n this.connect(0);\n };\n WebsocketClient.prototype.setSourceSampleRate = function (sourceSampleRate) {\n this.sourceSampleRate = sourceSampleRate;\n this.resampleRatio = this.sourceSampleRate / this.targetSampleRate;\n if (this.debug) {\n console.log('[SpeechlyClient]', 'resampleRatio', this.resampleRatio);\n }\n if (this.resampleRatio > 1) {\n this.filter = generateFilter(this.sourceSampleRate, this.targetSampleRate, 127);\n }\n this.workerCtx.postMessage({ type: 'SOURCE_SAMPLE_RATE_SET_SUCCESS' });\n if (isNaN(this.resampleRatio)) {\n throw Error(\"resampleRatio is NaN source rate is \".concat(this.sourceSampleRate, \" and target rate is \").concat(this.targetSampleRate));\n }\n };\n WebsocketClient.prototype.setSharedArrayBuffers = function (controlSAB, dataSAB) {\n this.controlSAB = new Int32Array(controlSAB);\n this.dataSAB = new Float32Array(dataSAB);\n var audioHandleInterval = this.dataSAB.length / 32; // ms\n if (this.debug) {\n console.log('[SpeechlyClient]', 'Audio handle interval', audioHandleInterval, 'ms');\n }\n setInterval(this.sendAudioFromSAB.bind(this), audioHandleInterval);\n };\n WebsocketClient.prototype.connect = function (timeout) {\n if (timeout === void 0) { timeout = 1000; }\n if (this.debug) {\n console.log('[SpeechlyClient]', 'connect in ', timeout / 1000, 'sec');\n }\n setTimeout(this.initializeWebsocket.bind(this), timeout);\n };\n WebsocketClient.prototype.initializeWebsocket = function () {\n if (this.debug) {\n console.log('[SpeechlyClient]', 'connecting to ', this.apiUrl);\n }\n this.websocket = new WebSocket(this.apiUrl, this.authToken);\n this.websocket.addEventListener('open', this.onWebsocketOpen);\n this.websocket.addEventListener('message', this.onWebsocketMessage);\n this.websocket.addEventListener('error', this.onWebsocketError);\n this.websocket.addEventListener('close', this.onWebsocketClose);\n };\n WebsocketClient.prototype.isOpen = function () {\n return this.websocket !== undefined && this.websocket.readyState === this.websocket.OPEN;\n };\n WebsocketClient.prototype.resendLastFrames = function () {\n if (this.lastFramesSent.length > 0) {\n this.send(this.lastFramesSent);\n this.lastFramesSent = new Int16Array(0);\n }\n };\n WebsocketClient.prototype.sendAudio = function (audioChunk) {\n if (!this.isContextStarted) {\n return;\n }\n if (audioChunk.length > 0) {\n if (this.resampleRatio > 1) {\n // Downsampling\n this.send(this.downsample(audioChunk));\n }\n else {\n this.send(float32ToInt16(audioChunk));\n }\n }\n };\n WebsocketClient.prototype.sendAudioFromSAB = function () {\n if (!this.isContextStarted) {\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = 0;\n this.controlSAB[CONTROL.WRITE_INDEX] = 0;\n return;\n }\n if (this.controlSAB == undefined) {\n return;\n }\n var framesAvailable = this.controlSAB[CONTROL.FRAMES_AVAILABLE];\n var lock = this.controlSAB[CONTROL.LOCK];\n if (lock == 0 && framesAvailable > 0) {\n var data = this.dataSAB.subarray(0, framesAvailable);\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = 0;\n this.controlSAB[CONTROL.WRITE_INDEX] = 0;\n if (data.length > 0) {\n var frames_1;\n if (this.resampleRatio > 1) {\n frames_1 = this.downsample(data);\n }\n else {\n frames_1 = float32ToInt16(data);\n }\n this.send(frames_1);\n // 16000 per second, 1000 in 100 ms\n // save last 250 ms\n if (this.lastFramesSent.length > 1024 * 4) {\n this.lastFramesSent = frames_1;\n }\n else {\n var concat = new Int16Array(this.lastFramesSent.length + frames_1.length);\n concat.set(this.lastFramesSent);\n concat.set(frames_1, this.lastFramesSent.length);\n this.lastFramesSent = concat;\n }\n }\n }\n };\n WebsocketClient.prototype.startContext = function (appId) {\n if (this.isContextStarted) {\n console.log('Cant start context: it has been already started');\n return;\n }\n this.isContextStarted = true;\n this.isStartContextConfirmed = false;\n if (appId !== undefined) {\n this.outbox = JSON.stringify({ event: 'start', appId: appId });\n }\n else {\n this.outbox = JSON.stringify({ event: 'start' });\n }\n this.send(this.outbox);\n };\n WebsocketClient.prototype.stopContext = function () {\n if (!this.websocket) {\n throw Error('Cant start context: websocket is undefined');\n }\n if (!this.isContextStarted) {\n console.log('Cant stop context: it is not started');\n return;\n }\n this.isContextStarted = false;\n this.isStartContextConfirmed = false;\n var StopEventJSON = JSON.stringify({ event: 'stop' });\n this.send(StopEventJSON);\n };\n WebsocketClient.prototype.switchContext = function (newAppId) {\n if (!this.websocket) {\n throw Error('Cant switch context: websocket is undefined');\n }\n if (!this.isContextStarted) {\n console.log('Cant switch context: it is not started');\n return;\n }\n if (newAppId == undefined) {\n console.log('Cant switch context: new app id is undefined');\n return;\n }\n this.isStartContextConfirmed = false;\n var StopEventJSON = JSON.stringify({ event: 'stop' });\n this.send(StopEventJSON);\n this.shouldResendLastFramesSent = true;\n this.send(JSON.stringify({ event: 'start', appId: newAppId }));\n };\n WebsocketClient.prototype.closeWebsocket = function (websocketCode, reason) {\n if (websocketCode === void 0) { websocketCode = 1005; }\n if (reason === void 0) { reason = 'No Status Received'; }\n if (this.debug) {\n console.log('[SpeechlyClient]', 'Websocket closing');\n }\n if (!this.websocket) {\n throw Error('Websocket is not open');\n }\n this.websocket.close(websocketCode, reason);\n };\n WebsocketClient.prototype.downsample = function (input) {\n var inputBuffer = new Float32Array(this.buffer.length + input.length);\n inputBuffer.set(this.buffer, 0);\n inputBuffer.set(input, this.buffer.length);\n var outputLength = Math.ceil((inputBuffer.length - this.filter.length) / this.resampleRatio);\n var outputBuffer = new Int16Array(outputLength);\n for (var i = 0; i < outputLength; i++) {\n var offset = Math.round(this.resampleRatio * i);\n var val = 0.0;\n for (var j = 0; j < this.filter.length; j++) {\n val += inputBuffer[offset + j] * this.filter[j];\n }\n outputBuffer[i] = val * (val < 0 ? 0x8000 : 0x7fff);\n }\n var remainingOffset = Math.round(this.resampleRatio * outputLength);\n if (remainingOffset < inputBuffer.length) {\n this.buffer = inputBuffer.subarray(remainingOffset);\n }\n else {\n this.buffer = new Float32Array(0);\n }\n return outputBuffer;\n };\n WebsocketClient.prototype.send = function (data) {\n if (this.isOpen()) {\n try {\n this.websocket.send(data);\n }\n catch (error) {\n console.log('[SpeechlyClient]', 'Server connection error', error);\n }\n }\n };\n return WebsocketClient;\n}());\nvar ctx = self;\nvar websocketClient = new WebsocketClient(ctx);\nctx.onmessage = function (e) {\n switch (e.data.type) {\n case 'INIT':\n websocketClient.init(e.data.apiUrl, e.data.authToken, e.data.targetSampleRate, e.data.debug);\n break;\n case 'SET_SOURCE_SAMPLE_RATE':\n websocketClient.setSourceSampleRate(e.data.sourceSampleRate);\n break;\n case 'SET_SHARED_ARRAY_BUFFERS':\n websocketClient.setSharedArrayBuffers(e.data.controlSAB, e.data.dataSAB);\n break;\n case 'CLOSE':\n websocketClient.closeWebsocket(1000, 'Close requested by client');\n break;\n case 'START_CONTEXT':\n websocketClient.startContext(e.data.appId);\n break;\n case 'SWITCH_CONTEXT':\n websocketClient.switchContext(e.data.appId);\n break;\n case 'STOP_CONTEXT':\n websocketClient.stopContext();\n break;\n case 'AUDIO':\n websocketClient.sendAudio(e.data.payload);\n break;\n default:\n console.log('WORKER', e);\n }\n};\nfunction float32ToInt16(buffer) {\n var buf = new Int16Array(buffer.length);\n for (var l = 0; l < buffer.length; l++) {\n buf[l] = buffer[l] * (buffer[l] < 0 ? 0x8000 : 0x7fff);\n }\n return buf;\n}\nfunction generateFilter(sourceSampleRate, targetSampleRate, length) {\n if (length % 2 === 0) {\n throw Error('Filter length must be odd');\n }\n var cutoff = targetSampleRate / 2;\n var filter = new Float32Array(length);\n var sum = 0;\n for (var i = 0; i < length; i++) {\n var x = sinc(((2 * cutoff) / sourceSampleRate) * (i - (length - 1) / 2));\n sum += x;\n filter[i] = x;\n }\n for (var i = 0; i < length; i++) {\n filter[i] = filter[i] / sum;\n }\n return filter;\n}\nfunction sinc(x) {\n if (x === 0.0) {\n return 1.0;\n }\n var piX = Math.PI * x;\n return Math.sin(piX) / piX;\n}\n"],{type:"text/javascript"}),n=window.URL.createObjectURL(e);this.worker=new Worker(n),this.worker.addEventListener("message",this.onWebsocketMessage)}onResponse(t){this.onResponseCb=t}onClose(t){this.onCloseCb=t}initialize(t,e,n,i){return o(this,void 0,void 0,(function*(){return this.worker.postMessage({type:"INIT",apiUrl:t,authToken:e,targetSampleRate:n,debug:i}),this.startCbs=[],this.stopCbs=[],new Promise((t=>{this.resolveInitialization=t}))}))}setSourceSampleRate(t){return o(this,void 0,void 0,(function*(){return this.worker.postMessage({type:"SET_SOURCE_SAMPLE_RATE",sourceSampleRate:t}),new Promise((t=>{this.resolveSourceSampleRateSet=t}))}))}close(){return o(this,void 0,void 0,(function*(){return new Promise(((t,e)=>{this.worker.postMessage({type:"CLOSE",code:1e3,message:"Client has ended the session"}),t()}))}))}startContext(t){return o(this,void 0,void 0,(function*(){return new Promise(((e,n)=>{this.startCbs.push(((t,i)=>{void 0!==t?n(t):e(i)})),null!=t?this.worker.postMessage({type:"START_CONTEXT",appId:t}):this.worker.postMessage({type:"START_CONTEXT"})}))}))}stopContext(){return o(this,void 0,void 0,(function*(){return new Promise(((t,e)=>{this.stopCbs.push(((n,i)=>{void 0!==n?e(n):t(i)})),this.worker.postMessage({type:"STOP_CONTEXT"})}))}))}switchContext(t){return o(this,void 0,void 0,(function*(){return new Promise(((e,n)=>{this.startCbs.push(((t,i)=>{void 0!==t?n(t):e(i)})),this.worker.postMessage({type:"SWITCH_CONTEXT",appId:t})}))}))}postMessage(t){this.worker.postMessage(t)}sendAudio(t){this.worker.postMessage({type:"AUDIO",payload:t})}}class T{constructor(){this.storage=window.localStorage}get(t){return this.storage.getItem(t)}set(t,e){this.storage.setItem(t,e)}getOrSet(t,e){let n=this.storage.getItem(t);return null===n&&(n=e(),this.storage.setItem(t,n)),n}}function I(t,e){return{intent:t.intent,isFinal:e}}const _="speechly-auth-token";class W{constructor(e){var n,o,r,a,c,d,h,l,u;if(this.activeContexts=new Map,this.maxReconnectAttemptCount=10,this.contextStopDelay=250,this.connectAttempt=0,this.connectPromise=null,this.listeningPromise=null,this.cbs=[],this.state=t.DecoderState.Disconnected,this.handleWebsocketResponse=e=>{var n;this.debug&&console.log("[Decoder]","Received response",e);const{audio_context:i,segment_id:o,type:r}=e;let{data:a}=e;const c=this.activeContexts.get(i);if(void 0===c)return void console.warn("[Decoder]","Received response for non-existent context",i);let d=null!==(n=c.get(o))&&void 0!==n?n:new s(i,o);switch(r){case t.WebsocketResponseType.TentativeTranscript:a=a;const e=function(t){return t.words.map((({word:t,index:e,start_timestamp:n,end_timestamp:i})=>({value:t,index:e,startTimestamp:n,endTimestamp:i,isFinal:!1})))}(a),n=a.transcript;this.cbs.forEach((t=>t.tentativeTranscriptCbs.forEach((t=>t(i,o,e,n))))),d=d.updateTranscript(e);break;case t.WebsocketResponseType.Transcript:a=a;const s=function(t){return{value:t.word,index:t.index,startTimestamp:t.start_timestamp,endTimestamp:t.end_timestamp,isFinal:!0}}(a);this.cbs.forEach((t=>t.transcriptCbs.forEach((t=>t(i,o,s))))),d=d.updateTranscript([s]);break;case t.WebsocketResponseType.TentativeEntities:a=a;const r=function(t){return t.entities.map((({entity:t,value:e,start_position:n,end_position:i})=>({type:t,value:e,startPosition:n,endPosition:i,isFinal:!1})))}(a);this.cbs.forEach((t=>t.tentativeEntityCbs.forEach((t=>t(i,o,r))))),d=d.updateEntities(r);break;case t.WebsocketResponseType.Entity:a=a;const c=function(t){return{type:t.entity,value:t.value,startPosition:t.start_position,endPosition:t.end_position,isFinal:!0}}(a);this.cbs.forEach((t=>t.entityCbs.forEach((t=>t(i,o,c))))),d=d.updateEntities([c]);break;case t.WebsocketResponseType.TentativeIntent:a=a;const h=I(a,!1);this.cbs.forEach((t=>t.tentativeIntentCbs.forEach((t=>t(i,o,h))))),d=d.updateIntent(h);break;case t.WebsocketResponseType.Intent:a=a;const l=I(a,!0);this.cbs.forEach((t=>t.intentCbs.forEach((t=>t(i,o,l))))),d=d.updateIntent(l);break;case t.WebsocketResponseType.SegmentEnd:d=d.finalize()}c.set(o,d),this.activeContexts.set(i,c),this.logSegments&&console.info(d.toString()),this.cbs.forEach((t=>t.segmentChangeCbs.forEach((t=>t(d.toSegment())))))},this.handleWebsocketClosure=e=>{if(1e3===e.code)this.debug&&console.log("[Decoder]","Websocket closed",e);else{if(console.error("[Decoder]","Websocket closed due to error",e),void 0===this.deviceId)return void this.setState(t.DecoderState.Failed);this.listeningPromise=null,this.setState(t.DecoderState.Disconnected),this.reconnect()}},this.logSegments=null!==(n=e.logSegments)&&void 0!==n&&n,this.loginUrl=null!==(o=e.loginUrl)&&void 0!==o?o:"https://api.speechly.com/login",this.appId=null!==(r=e.appId)&&void 0!==r?r:void 0,this.projectId=null!==(a=e.projectId)&&void 0!==a?a:void 0,this.sampleRate=null!==(c=e.sampleRate)&&void 0!==c?c:i,this.debug=null!==(d=e.debug)&&void 0!==d&&d,void 0!==this.appId&&void 0!==this.projectId)throw Error("[Decoder] You cannot use both appId and projectId at the same time");if(void 0===this.appId&&void 0===this.projectId)throw Error("[Decoder] Either an appId or a projectId is required");this.apiUrl=function(t,e){const n=new URLSearchParams;return n.append("sampleRate",e.toString()),`${t}?${n.toString()}`}(null!==(h=e.apiUrl)&&void 0!==h?h:"wss://api.speechly.com/ws/v1",this.sampleRate),this.storage=null!==(l=e.storage)&&void 0!==l?l:new T,this.deviceId=this.storage.getOrSet("speechly-device-id",E),this.apiClient=new x,this.apiClient.onResponse(this.handleWebsocketResponse),this.apiClient.onClose(this.handleWebsocketClosure),(null===(u=e.connect)||void 0===u||u)&&this.connect()}getReconnectDelayMs(t){return 100*Math.pow(2,t)}sleep(t){return o(this,void 0,void 0,(function*(){return new Promise((e=>setTimeout(e,t)))}))}connect(){return o(this,void 0,void 0,(function*(){null===this.connectPromise&&(this.connectPromise=(()=>o(this,void 0,void 0,(function*(){const e=this.storage.get(_);if(null!=e&&R(e,this.projectId,this.appId,this.deviceId))this.authToken=e;else try{this.authToken=yield function(t,e,n,i,s=fetch,r=Date.now){var a;return o(this,void 0,void 0,(function*(){let o;o=void 0!==e?{projectId:e,deviceId:i}:{appId:n,deviceId:i};const c=yield s(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}),d=yield c.json();if(200!==c.status)throw Error(null!==(a=d.error)&&void 0!==a?a:`Speechly API login request failed with ${c.status}`);if(void 0===d.access_token)throw Error("Invalid login response from Speechly API");if(!R(d.access_token,e,n,i,r))throw Error("Invalid token received from Speechly API");return d.access_token}))}(this.loginUrl,this.projectId,this.appId,this.deviceId),this.storage.set(_,this.authToken)}catch(e){throw this.setState(t.DecoderState.Failed),e}yield this.apiClient.initialize(this.apiUrl,this.authToken,this.sampleRate,this.debug),this.advanceState(t.DecoderState.Connected)})))()),yield this.connectPromise}))}close(){return o(this,void 0,void 0,(function*(){let e;try{yield this.apiClient.close()}catch(t){e=t.message}if(this.activeContexts.clear(),this.connectPromise=null,this.setState(t.DecoderState.Disconnected),void 0!==e)throw Error(e)}))}queueTask(t){return o(this,void 0,void 0,(function*(){const e=this.listeningPromise;return this.listeningPromise=(()=>o(this,void 0,void 0,(function*(){return yield e,t()})))(),this.listeningPromise}))}startContext(e){return o(this,void 0,void 0,(function*(){if(this.state===t.DecoderState.Failed)throw Error("[Decoder] startContext cannot be run in Failed state.");if(this.state<t.DecoderState.Connected)yield this.connect();else if(this.state>t.DecoderState.Connected)throw Error("[Decoder] Unable to complete startContext: Expected Connected state, but was in "+f(this.state)+".");return this.queueTask((()=>o(this,void 0,void 0,(function*(){let i;if(this.setState(t.DecoderState.Active),null!=this.projectId)i=yield this.apiClient.startContext(null==e?void 0:e.appId);else{if(null!=(null==e?void 0:e.appId)&&this.appId!==(null==e?void 0:e.appId))throw this.setState(t.DecoderState.Failed),n;i=yield this.apiClient.startContext()}if(this.state<t.DecoderState.Active)throw Error("[Decoder] Unable to complete startContext: Problem acquiring contextId");return this.activeContexts.set(i,new Map),this.cbs.forEach((t=>t.contextStartedCbs.forEach((t=>t(i))))),i}))))}))}sendAudio(e){if(this.state!==t.DecoderState.Active)throw Error("[Decoder] Unable to complete startContext: Expected Active state, but was in "+f(this.state)+".");this.apiClient.sendAudio(e)}stopContext(){return o(this,void 0,void 0,(function*(){if(this.state===t.DecoderState.Failed)throw Error("[Decoder] stopContext cannot be run in unrecovable error state.");if(this.state!==t.DecoderState.Active)throw Error("[Decoder] Unable to complete stopContext: Expected Active state, but was in "+f(this.state)+".");this.setState(t.DecoderState.Connected);const e=yield this.queueTask((()=>o(this,void 0,void 0,(function*(){yield this.sleep(this.contextStopDelay);try{const t=yield this.apiClient.stopContext();return this.activeContexts.delete(t),t}catch(e){throw this.setState(t.DecoderState.Failed),e}}))));return this.cbs.forEach((t=>t.contextStoppedCbs.forEach((t=>t(e))))),e}))}switchContext(e){return o(this,void 0,void 0,(function*(){if(this.state!==t.DecoderState.Active)throw Error("[Decoder] Unable to complete switchContext: Expected Active state, but was in "+f(this.state)+".");yield this.queueTask((()=>o(this,void 0,void 0,(function*(){const t=yield this.apiClient.switchContext(e);this.activeContexts.set(t,new Map)}))))}))}registerListener(t){this.cbs.push(t)}setSampleRate(t){return o(this,void 0,void 0,(function*(){this.sampleRate=t,yield this.apiClient.setSourceSampleRate(t)}))}useSharedArrayBuffers(t,e){this.apiClient.postMessage({type:"SET_SHARED_ARRAY_BUFFERS",controlSAB:t,dataSAB:e})}reconnect(){return o(this,void 0,void 0,(function*(){this.debug&&console.log("[Decoder]","Reconnecting...",this.connectAttempt),this.connectPromise=null,this.connectAttempt<this.maxReconnectAttemptCount?(yield this.sleep(this.getReconnectDelayMs(this.connectAttempt++)),yield this.connect()):console.error("[Decoder] Maximum reconnect count reached, giving up automatic reconnect.")}))}advanceState(t){this.state>=t||this.setState(t)}setState(t){this.state!==t&&(this.debug&&console.log("[Decoder]",f(this.state),"->",f(t)),this.state=t,this.cbs.forEach((e=>{var n;return null===(n=e.stateChangeCbs)||void 0===n?void 0:n.forEach((e=>e(t)))})))}}t.BrowserClient=class{constructor(t){var e,n;this.debug=!1,this.initialized=!1,this.active=!1,this.stats={maxSignalEnergy:0,sentSamples:0};const i=window.navigator.mediaDevices.getSupportedConstraints();this.nativeResamplingSupported=!0===i.sampleRate,this.isMobileSafari=["iPad Simulator","iPhone Simulator","iPod Simulator","iPad","iPhone","iPod"].indexOf(navigator.platform)>=0||navigator.userAgent.includes("Mac")&&"ontouchend"in document,this.isSafari=this.isMobileSafari||void 0!==window.safari,this.useSAB=!this.isSafari,this.debug=null===(e=t.debug)||void 0===e||e,this.callbacks=new p,this.decoder=null!==(n=t.decoder)&&void 0!==n?n:new W(t),this.decoder.registerListener(this.callbacks)}initialize(t){var n,s;return o(this,void 0,void 0,(function*(){if(!this.initialized){this.initialized=!0,this.debug&&console.log("[BrowserClient]","initializing"),yield this.decoder.connect();try{const t={};if(this.nativeResamplingSupported&&(t.sampleRate=i),void 0!==window.webkitAudioContext)try{this.audioContext=new window.webkitAudioContext(t)}catch(t){this.debug&&console.log("[BrowserClient]","creating audioContext without samplerate conversion",t),this.audioContext=new window.webkitAudioContext}else this.audioContext=new window.AudioContext(t),void 0!==window.webkitAudioContext&&(yield this.audioContext.resume())}catch(t){throw e}if(this.isSafari||void 0===window.AudioWorkletNode){if(this.debug&&console.log("[BrowserClient]","using ScriptProcessorNode"),void 0!==window.webkitAudioContext){const t=this.audioContext.sampleRate/i,e=4096*Math.pow(2,Math.ceil(Math.log(t)/Math.log(2)));this.audioProcessor=this.audioContext.createScriptProcessor(e,1,1)}else this.audioProcessor=this.audioContext.createScriptProcessor(void 0,1,1);this.audioProcessor.connect(this.audioContext.destination),this.audioProcessor.addEventListener("audioprocess",(t=>{this.handleAudio(t.inputBuffer.getChannelData(0))}))}else{this.debug&&console.log("[BrowserClient]","using AudioWorkletNode");const t=new Blob(["\n// Indices for the Control SAB.\nconst CONTROL = {\n 'WRITE_INDEX': 0,\n 'FRAMES_AVAILABLE': 1,\n 'LOCK': 2,\n};\n\nclass SpeechlyProcessor extends AudioWorkletProcessor {\n constructor() {\n super();\n\n this._initialized = false;\n this.debug = false;\n this.port.onmessage = this._initialize.bind(this);\n }\n\n _initialize(event) {\n this.controlSAB = new Int32Array(event.data.controlSAB);\n this.dataSAB = new Float32Array(event.data.dataSAB);\n this.debug = event.data.debug;\n if (this.debug) {\n console.log('[BrowserClient AudioWorkletNode]', 'initializing audioworklet');\n }\n this.sharedBufferSize = this.dataSAB.length;\n this.buffer = new Float32Array(0);\n this._initialized = true;\n }\n\n _transferDataToSharedBuffer(data) {\n this.controlSAB[CONTROL.LOCK] = 1;\n let inputWriteIndex = this.controlSAB[CONTROL.WRITE_INDEX];\n if (this.controlSAB[CONTROL.FRAMES_AVAILABLE] > 0) {\n if (inputWriteIndex + data.length > this.sharedBufferSize) {\n // console.log('buffer overflow')\n inputWriteIndex = 0;\n }\n }\n this.dataSAB.set(data, inputWriteIndex);\n this.controlSAB[CONTROL.WRITE_INDEX] = inputWriteIndex + data.length;\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = inputWriteIndex + data.length;\n this.controlSAB[CONTROL.LOCK] = 0;\n }\n\n _pushData(data) {\n if (this.debug) {\n const signalEnergy = getStandardDeviation(data)\n this.port.postMessage({\n type: 'STATS',\n signalEnergy: signalEnergy,\n samples: data.length,\n });\n }\n\n if (this.buffer.length > this.sharedBufferSize) {\n const dataToTransfer = this.buffer.subarray(0, this.sharedBufferSize);\n this._transferDataToSharedBuffer(dataToTransfer);\n this.buffer = this.buffer.subarray(this.sharedBufferSize);\n }\n let concat = new Float32Array(this.buffer.length + data.length);\n concat.set(this.buffer);\n concat.set(data, this.buffer.length);\n this.buffer = concat;\n }\n\n process(inputs, outputs, parameters) {\n const inputChannelData = inputs[0][0];\n if (inputChannelData !== undefined) {\n if (this.controlSAB && this.dataSAB) {\n this._pushData(inputChannelData);\n } else {\n this.port.postMessage({\n type: 'DATA',\n frames: inputChannelData\n });\n }\n }\n\n return true;\n }\n}\n\nfunction getStandardDeviation(array) {\n const n = array.length\n const mean = array.reduce((a, b) => a + b) / n\n return Math.sqrt(array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n)\n}\n\nregisterProcessor('speechly-worklet', SpeechlyProcessor);\n"],{type:"text/javascript"}),e=window.URL.createObjectURL(t);if(yield this.audioContext.audioWorklet.addModule(e),this.speechlyNode=new AudioWorkletNode(this.audioContext,"speechly-worklet"),this.speechlyNode.connect(this.audioContext.destination),this.useSAB&&void 0!==window.SharedArrayBuffer){this.debug&&console.log("[BrowserClient]","using SharedArrayBuffer");const t=new window.SharedArrayBuffer(4*Int32Array.BYTES_PER_ELEMENT),e=new window.SharedArrayBuffer(1024*Float32Array.BYTES_PER_ELEMENT);this.decoder.useSharedArrayBuffers(t,e),this.speechlyNode.port.postMessage({type:"SET_SHARED_ARRAY_BUFFERS",controlSAB:t,dataSAB:e,debug:this.debug})}else this.debug&&console.log("[BrowserClient]","can not use SharedArrayBuffer");this.speechlyNode.port.onmessage=t=>{switch(t.data.type){case"STATS":t.data.signalEnergy>this.stats.maxSignalEnergy&&(this.stats.maxSignalEnergy=t.data.signalEnergy),this.stats.sentSamples+=parseInt(t.data.samples);break;case"DATA":this.handleAudio(t.data.frames)}}}this.debug&&console.log("[BrowserClient]","audioContext sampleRate is",null===(n=this.audioContext)||void 0===n?void 0:n.sampleRate),yield this.decoder.setSampleRate(null===(s=this.audioContext)||void 0===s?void 0:s.sampleRate),(null==t?void 0:t.mediaStream)&&(yield this.attach(null==t?void 0:t.mediaStream))}}))}close(){var t,e,n;return o(this,void 0,void 0,(function*(){yield this.detach(),null!==this.speechlyNode&&(null===(t=this.speechlyNode)||void 0===t||t.port.close(),null===(e=this.speechlyNode)||void 0===e||e.disconnect()),void 0!==this.audioProcessor&&(null===(n=this.audioProcessor)||void 0===n||n.disconnect()),yield this.decoder.close()}))}attach(t){var e,n,i,s,r,a;return o(this,void 0,void 0,(function*(){if(yield this.initialize(),yield this.detach(),this.stream=null===(e=this.audioContext)||void 0===e?void 0:e.createMediaStreamSource(t),"running"!==(null===(n=this.audioContext)||void 0===n?void 0:n.state)&&(this.debug&&console.log("[BrowserClient]","audioContext resume required, state is",null===(i=this.audioContext)||void 0===i?void 0:i.state),yield null===(s=this.audioContext)||void 0===s?void 0:s.resume()),this.speechlyNode)null===(r=this.stream)||void 0===r||r.connect(this.speechlyNode);else{if(!this.audioProcessor)throw Error("[BrowserClient] cannot attach to mediaStream, not initialized");null===(a=this.stream)||void 0===a||a.connect(this.audioProcessor)}}))}detach(){return o(this,void 0,void 0,(function*(){this.active&&(yield this.stop()),this.stream&&(this.stream.disconnect(),this.stream=void 0)}))}uploadAudioData(t,e){var n;return o(this,void 0,void 0,(function*(){yield this.initialize();const i=yield null===(n=this.audioContext)||void 0===n?void 0:n.decodeAudioData(t);if(void 0===i)throw Error("Could not decode audioData");const s=i.getChannelData(0);if(i.numberOfChannels>1){const t=i.getChannelData(1);for(let e=0;e<s.length;e++)s[e]=(s[e]+t[e])/2}const o=yield this.start(e);let r;for(let t=0;t<s.length;t+=16e3){const e=t+16e3;r=e>s.length?s.slice(t):s.slice(t,e),this.handleAudio(r)}return yield this.stop(),o}))}start(t){return o(this,void 0,void 0,(function*(){yield this.initialize();const e=this.decoder.startContext(t);return this.active=!0,e}))}stop(){return o(this,void 0,void 0,(function*(){const t=this.decoder.stopContext();return this.active=!1,0===this.stats.sentSamples&&console.warn("[BrowserClient]","audioContext contained no audio data"),this.stats.sentSamples=0,t}))}handleAudio(t){this.active&&t.length>0&&(this.stats.sentSamples+=t.length,this.decoder.sendAudio(t))}onSegmentChange(t){this.callbacks.segmentChangeCbs.push(t)}onTranscript(t){this.callbacks.transcriptCbs.push(t)}onEntity(t){this.callbacks.entityCbs.push(t)}onIntent(t){this.callbacks.intentCbs.push(t)}onTentativeTranscript(t){this.callbacks.tentativeTranscriptCbs.push(t)}onTentativeEntities(t){this.callbacks.tentativeEntityCbs.push(t)}onTentativeIntent(t){this.callbacks.tentativeIntentCbs.push(t)}onStateChange(t){this.callbacks.stateChangeCbs.push(t)}},t.BrowserMicrophone=class{constructor(){this.muted=!1,this.initialized=!1;try{const t=window.navigator.mediaDevices.getSupportedConstraints();this.nativeResamplingSupported=!0===t.sampleRate,this.autoGainControl=!0===t.autoGainControl}catch(t){this.nativeResamplingSupported=!1,this.autoGainControl=!1}}initialize(){var t;return o(this,void 0,void 0,(function*(){if(this.initialized)return;if(void 0===(null===(t=window.navigator)||void 0===t?void 0:t.mediaDevices))throw e;const n={video:!1};this.nativeResamplingSupported||this.autoGainControl?n.audio={sampleRate:i,autoGainControl:this.autoGainControl}:n.audio=!0;try{this.mediaStream=yield window.navigator.mediaDevices.getUserMedia(n)}catch(t){throw console.error(t),c}this.initialized=!0,this.muted=!0}))}close(){return o(this,void 0,void 0,(function*(){if(!this.initialized)throw r;this.muted=!0;this.mediaStream.getTracks().forEach((t=>t.stop())),this.mediaStream=void 0,this.initialized=!1}))}isRecording(){return!this.muted}},t.CloudDecoder=W,t.DefaultSampleRate=i,t.ErrAlreadyInitialized=a,t.ErrAppIdChangeWithoutProjectLogin=n,t.ErrDeviceNotSupported=e,t.ErrKeyNotFound=l,t.ErrNoAudioConsent=c,t.ErrNoStorageSupport=h,t.ErrNotInitialized=r,t.EventCallbacks=p,t.SegmentState=s,t.stateToString=f,Object.defineProperty(t,"__esModule",{value:!0})})); | ||
//# sourceMappingURL=speechly.umd.min.js.map |
@@ -1,4 +0,687 @@ | ||
export * from './speechly'; | ||
export * from './microphone/types'; | ||
export * from './websocket/types'; | ||
export * from './storage/types'; | ||
/** | ||
* The interface for a client for Speechly SLU WebSocket API. | ||
* @public | ||
*/ | ||
export declare interface APIClient { | ||
/** | ||
* Registers a callback that is invoked whenever a response is received from the API. | ||
* | ||
* @param cb - this callback to invoke. | ||
*/ | ||
onResponse(cb: ResponseCallback): void; | ||
/** | ||
* Registers a callback that is invoked whenever WebSocket connection is closed (either normally or due to an error). | ||
* | ||
* @param cb - the callback to invoke. | ||
*/ | ||
onClose(cb: CloseCallback): void; | ||
/** | ||
* Initialises the client. | ||
* | ||
* This method will be called by the Client as part of the initialisation process. | ||
* | ||
* @param apiUrl - url. | ||
* @param authToken - authentication token. | ||
* @param targetSampleRate - target sample rate of audio. | ||
* @param debug - debug flag. | ||
*/ | ||
initialize(apiUrl: string, authToken: string, targetSampleRate: number, debug: boolean): Promise<void>; | ||
/** | ||
* Initialises the client. | ||
* | ||
* This should prepare websocket to be used (set source sample rate). | ||
* This method will be called by the Client as part of the initialisation process. | ||
* | ||
* @param sourceSampleRate - sample rate of audio source. | ||
*/ | ||
setSourceSampleRate(sourceSampleRate: number): Promise<void>; | ||
/** | ||
* Closes the client. | ||
* | ||
* This should close the connection and tear down all infrastructure related to it. | ||
* Calling `initialize` again after calling `close` should be possible. | ||
*/ | ||
close(): Promise<void>; | ||
/** | ||
* Starts a new audio context by sending the start event to the API. | ||
* The promise returned should resolve or reject after the API has responded with confirmation or an error has occured. | ||
*/ | ||
startContext(appId?: string): Promise<string>; | ||
/** | ||
* Stops an audio context by sending the stop event to the API. | ||
* The promise returned should resolve or reject after the API has responded with confirmation or an error has occured. | ||
*/ | ||
stopContext(): Promise<string>; | ||
/** | ||
* Stops current context and immediately starts a new SLU context | ||
* by sending a start context event to the API and unmuting the microphone. | ||
*/ | ||
switchContext(appId: string): Promise<string>; | ||
/** | ||
* Sends audio to the API. | ||
* If there is no active context (no successful previous calls to `startContext`), this must fail. | ||
* | ||
* @param audioChunk - audio chunk to send. | ||
*/ | ||
sendAudio(audioChunk: Float32Array): void; | ||
/** | ||
* Sends message to the Worker. | ||
* | ||
* @param message - message to send. | ||
*/ | ||
postMessage(message: Object): void; | ||
} | ||
/** | ||
* BrowserClient connects a browser based mediaStream to the Speechly API, including any | ||
* needed downsampling. | ||
* @public | ||
*/ | ||
export declare class BrowserClient { | ||
private audioContext?; | ||
private readonly nativeResamplingSupported; | ||
private readonly debug; | ||
private readonly useSAB; | ||
private readonly isSafari; | ||
private readonly isMobileSafari; | ||
private readonly decoder; | ||
private readonly callbacks; | ||
private initialized; | ||
private active; | ||
private speechlyNode?; | ||
private audioProcessor?; | ||
private stream?; | ||
private stats; | ||
constructor(options: DecoderOptions); | ||
/** | ||
* Create an AudioContext for resampling audio. | ||
*/ | ||
initialize(options?: { | ||
mediaStream?: MediaStream; | ||
}): Promise<void>; | ||
close(): Promise<void>; | ||
attach(mediaStream: MediaStream): Promise<void>; | ||
detach(): Promise<void>; | ||
uploadAudioData(audioData: ArrayBuffer, options?: ContextOptions): Promise<string>; | ||
start(options?: ContextOptions): Promise<string>; | ||
stop(): Promise<string>; | ||
private handleAudio; | ||
/** | ||
* Adds a listener for current segment change events. | ||
* @param cb - the callback to invoke on segment change events. | ||
*/ | ||
onSegmentChange(cb: (segment: Segment) => void): void; | ||
/** | ||
* Adds a listener for transcript responses from the API. | ||
* @param cb - the callback to invoke on a transcript response. | ||
*/ | ||
onTranscript(cb: (contextId: string, segmentId: number, word: Word) => void): void; | ||
/** | ||
* Adds a listener for entity responses from the API. | ||
* @param cb - the callback to invoke on an entity response. | ||
*/ | ||
onEntity(cb: (contextId: string, segmentId: number, entity: Entity) => void): void; | ||
/** | ||
* Adds a listener for intent responses from the API. | ||
* @param cb - the callback to invoke on an intent response. | ||
*/ | ||
onIntent(cb: (contextId: string, segmentId: number, intent: Intent) => void): void; | ||
/** | ||
* Adds a listener for tentative transcript responses from the API. | ||
* @param cb - the callback to invoke on a tentative transcript response. | ||
*/ | ||
onTentativeTranscript(cb: (contextId: string, segmentId: number, words: Word[], text: string) => void): void; | ||
/** | ||
* Adds a listener for tentative entities responses from the API. | ||
* @param cb - the callback to invoke on a tentative entities response. | ||
*/ | ||
onTentativeEntities(cb: (contextId: string, segmentId: number, entities: Entity[]) => void): void; | ||
/** | ||
* Adds a listener for tentative intent responses from the API. | ||
* @param cb - the callback to invoke on a tentative intent response. | ||
*/ | ||
onTentativeIntent(cb: (contextId: string, segmentId: number, intent: Intent) => void): void; | ||
onStateChange(cb: (state: DecoderState) => void): void; | ||
} | ||
/** | ||
* Gets browser based microphone using the window.navigator.mediaDevices interface. | ||
* The exposed `mediaStream` can be attached to a `BrowserClient` instance. | ||
* @public | ||
*/ | ||
export declare class BrowserMicrophone { | ||
private muted; | ||
private initialized; | ||
private readonly nativeResamplingSupported; | ||
private readonly autoGainControl; | ||
mediaStream?: MediaStream; | ||
constructor(); | ||
/** | ||
* Initializes the microphone. Needs to happen after a user interaction in the view. | ||
* The reason for that is that it's required for user to first interact with the page, | ||
* before it can capture or play audio and video, for privacy and user experience reasons. | ||
*/ | ||
initialize(): Promise<void>; | ||
/** | ||
* Closes the microphone, releases all resources and stops the Speechly client. | ||
*/ | ||
close(): Promise<void>; | ||
/** | ||
* @returns true if microphone is open | ||
*/ | ||
isRecording(): boolean; | ||
} | ||
/** | ||
* A callback that is invoked whenever WebSocket connection is closed. | ||
* @public | ||
*/ | ||
export declare type CloseCallback = (err: { | ||
code: number; | ||
reason: string; | ||
wasClean: boolean; | ||
}) => void; | ||
/** | ||
* A client for Speechly Spoken Language Understanding (SLU) API. The client handles initializing the websocket | ||
* connection to Speechly API, sending control events and audio stream. It reads and dispatches the responses | ||
* through a high-level API for interacting with so-called speech segments. | ||
* @public | ||
*/ | ||
export declare class CloudDecoder { | ||
private readonly debug; | ||
private readonly logSegments; | ||
private readonly projectId?; | ||
private readonly appId?; | ||
private readonly storage; | ||
private readonly apiClient; | ||
private readonly loginUrl; | ||
private readonly deviceId; | ||
private readonly apiUrl; | ||
private readonly activeContexts; | ||
private readonly maxReconnectAttemptCount; | ||
private readonly contextStopDelay; | ||
private connectAttempt; | ||
private connectPromise; | ||
private listeningPromise; | ||
private authToken?; | ||
private readonly cbs; | ||
private sampleRate; | ||
state: DecoderState; | ||
constructor(options: DecoderOptions); | ||
private getReconnectDelayMs; | ||
private sleep; | ||
/** | ||
* Connect to Speechly backend. | ||
* This function will be called by initialize if not manually called earlier. | ||
* Calling connect() immediately after constructor and setting callbacks allows | ||
* prewarming the connection, resulting in less noticeable waits for the user. | ||
*/ | ||
connect(): Promise<void>; | ||
/** | ||
* Closes the client by closing the API connection and disabling the microphone. | ||
*/ | ||
close(): Promise<void>; | ||
private queueTask; | ||
/** | ||
* Starts a new SLU context by sending a start context event to the API. | ||
*/ | ||
startContext(options?: ContextOptions): Promise<string>; | ||
/** | ||
* Send audio array. | ||
*/ | ||
sendAudio(audio: Float32Array): void; | ||
/** | ||
* Stops current SLU context by sending a stop context event to the API and muting the microphone | ||
* delayed by contextStopDelay = 250 ms | ||
*/ | ||
stopContext(): Promise<string>; | ||
/** | ||
* Stops current context and immediately starts a new SLU context | ||
* by sending a start context event to the API and unmuting the microphone. | ||
* @param appId - unique identifier of an app in the dashboard. | ||
*/ | ||
switchContext(appId: string): Promise<void>; | ||
registerListener(listener: EventCallbacks): void; | ||
setSampleRate(sr: number): Promise<void>; | ||
useSharedArrayBuffers(controlSAB: any, dataSAB: any): void; | ||
private readonly handleWebsocketResponse; | ||
private readonly handleWebsocketClosure; | ||
private reconnect; | ||
private advanceState; | ||
private setState; | ||
} | ||
/** | ||
* Valid options for a new audioContext. All options are optional. | ||
* @public | ||
*/ | ||
export declare interface ContextOptions { | ||
appId?: string; | ||
} | ||
/** | ||
* The options which can be used to configure the client. | ||
* @public | ||
*/ | ||
export declare interface DecoderOptions { | ||
/** | ||
* Connect to Speechly upon creating the client instance. Defaults to true. | ||
*/ | ||
connect?: boolean; | ||
/** | ||
* The unique identifier of an app in the dashboard. | ||
*/ | ||
appId?: string; | ||
/** | ||
* The unique identifier of a project in the dashboard. | ||
*/ | ||
projectId?: string; | ||
/** | ||
* The URL of Speechly login endpoint. | ||
*/ | ||
loginUrl?: string; | ||
/** | ||
* The URL of Speechly SLU API endpoint. | ||
*/ | ||
apiUrl?: string; | ||
/** | ||
* The sample rate of the audio to use. | ||
*/ | ||
sampleRate?: number; | ||
/** | ||
* Whether to output debug statements to the console. | ||
*/ | ||
debug?: boolean; | ||
/** | ||
* Whether to output updated segments to the console. | ||
*/ | ||
logSegments?: boolean; | ||
/** | ||
* Listener for client state changes. | ||
*/ | ||
callbacks?: EventCallbacks; | ||
/** | ||
* Custom API client implementation. | ||
* If not provided, an implementation based on Speechly SLU WebSocket API is used. | ||
*/ | ||
decoder?: CloudDecoder; | ||
/** | ||
* Custom storage implementation. | ||
* If not provided, browser's LocalStorage API is used. | ||
*/ | ||
storage?: Storage_2; | ||
} | ||
/** | ||
* All possible states of a Speechly API client. Failed state is non-recoverable. | ||
* It is also possible to use arithmetics for state comparison, e.g. `if (state < speechly.ClientState.Disconnected)`, | ||
* to react to non-recoverable states. | ||
* @public | ||
*/ | ||
export declare enum DecoderState { | ||
Failed = 0, | ||
Disconnected = 1, | ||
Connected = 2, | ||
Active = 3 | ||
} | ||
/** | ||
* Default sample rate for microphone streams. | ||
* @public | ||
*/ | ||
export declare const DefaultSampleRate = 16000; | ||
/** | ||
* A single entity detected by the SLU API. | ||
* @public | ||
*/ | ||
export declare interface Entity { | ||
/** | ||
* The type specified by the developer in the NLU rules in the dashboard (e.g. restaurant_type). | ||
*/ | ||
type: string; | ||
/** | ||
* The value of the entity (e.g. Papa Joe's). | ||
*/ | ||
value: string; | ||
/** | ||
* The index of the first word that contains this entity. | ||
*/ | ||
startPosition: number; | ||
/** | ||
* The index of the last word that contains this entity. | ||
*/ | ||
endPosition: number; | ||
/** | ||
* Whether the entity was detected as final. | ||
*/ | ||
isFinal: boolean; | ||
} | ||
/** | ||
* Entity response payload. | ||
* @public | ||
*/ | ||
export declare interface EntityResponse { | ||
/** | ||
* Entity type (e.g. restaurant, direction, room, device). | ||
*/ | ||
entity: string; | ||
/** | ||
* Entity value (e.g. "sushi bar", "northwest", "living room", "kitchen lights"). | ||
*/ | ||
value: string; | ||
/** | ||
* Start position of the entity in the segment. Correlates with TranscriptResponse indices. | ||
* Inclusive. | ||
*/ | ||
start_position: number; | ||
/** | ||
* End position of the entity in the segment. Correlates with TranscriptResponse indices. | ||
* Exclusive. | ||
*/ | ||
end_position: number; | ||
} | ||
/** | ||
* Error to be thrown when the initialize method of a Microphone instance is called more than once. | ||
* @public | ||
*/ | ||
export declare const ErrAlreadyInitialized: Error; | ||
/** | ||
* Error to be thrown when user tries to change appId without project login. | ||
* @public | ||
*/ | ||
export declare const ErrAppIdChangeWithoutProjectLogin: Error; | ||
/** | ||
* Error to be thrown when the device does not support audioContext. | ||
* @public | ||
*/ | ||
export declare const ErrDeviceNotSupported: Error; | ||
/** | ||
* Error to be thrown if requested key was not found in the storage. | ||
* @public | ||
*/ | ||
export declare const ErrKeyNotFound: Error; | ||
/** | ||
* Error to be thrown when user did not give consent to the application to record audio. | ||
* @public | ||
*/ | ||
export declare const ErrNoAudioConsent: Error; | ||
/** | ||
* Error to be thrown if storage API is not supported by the device. | ||
* @public | ||
*/ | ||
export declare const ErrNoStorageSupport: Error; | ||
/** | ||
* Error to be thrown when the microphone was accessed before it was initialized. | ||
* @public | ||
*/ | ||
export declare const ErrNotInitialized: Error; | ||
/** | ||
* All possible callbacks for the decoder. | ||
* @public | ||
*/ | ||
export declare class EventCallbacks { | ||
stateChangeCbs: Array<(state: DecoderState) => void>; | ||
transcriptCbs: Array<(contextId: string, segmentId: number, word: Word) => void>; | ||
entityCbs: Array<(contextId: string, segmentId: number, entity: Entity) => void>; | ||
intentCbs: Array<(contextId: string, segmentId: number, intent: Intent) => void>; | ||
segmentChangeCbs: Array<(segment: Segment) => void>; | ||
tentativeTranscriptCbs: Array<(contextId: string, segmentId: number, words: Word[], text: string) => void>; | ||
tentativeEntityCbs: Array<(contextId: string, segmentId: number, entities: Entity[]) => void>; | ||
tentativeIntentCbs: Array<(contextId: string, segmentId: number, intent: Intent) => void>; | ||
contextStartedCbs: Array<(contextId: string) => void>; | ||
contextStoppedCbs: Array<(contextId: string) => void>; | ||
} | ||
/** | ||
* The intent detected by the SLU API. | ||
* @public | ||
*/ | ||
export declare interface Intent { | ||
/** | ||
* The value of the intent. | ||
*/ | ||
intent: string; | ||
/** | ||
* Whether the intent was detected as final. | ||
*/ | ||
isFinal: boolean; | ||
} | ||
/** | ||
* Intent response payload. | ||
* @public | ||
*/ | ||
export declare interface IntentResponse { | ||
/** | ||
* Intent type (e.g. "book", "find", "turn_on"). | ||
*/ | ||
intent: string; | ||
} | ||
/** | ||
* A callback that is invoked whenever a response is received from Speechly SLU WebSocket API. | ||
* @public | ||
*/ | ||
export declare type ResponseCallback = (response: WebsocketResponse) => void; | ||
/** | ||
* The smallest component of SLU API, defined by an intent. | ||
* @public | ||
*/ | ||
export declare interface Segment { | ||
/** | ||
* The identifier of parent SLU context. | ||
*/ | ||
contextId: string; | ||
/** | ||
* The identifier of the segment within the parent context. | ||
*/ | ||
id: number; | ||
/** | ||
* Whether the segment is final. A final segment is guaranteed to only contain final parts. | ||
*/ | ||
isFinal: boolean; | ||
/** | ||
* The intent of the segment. | ||
*/ | ||
intent: Intent; | ||
/** | ||
* All words which belong to the segment, ordered by their indices. | ||
*/ | ||
words: Word[]; | ||
/** | ||
* All entities which belong to the segment, not ordered. | ||
*/ | ||
entities: Entity[]; | ||
} | ||
/** | ||
* Contains the internal state of a `Segment`, with methods to update it based on new events. | ||
* @public | ||
*/ | ||
export declare class SegmentState { | ||
id: number; | ||
contextId: string; | ||
isFinalized: boolean; | ||
words: Word[]; | ||
entities: Map<string, Entity>; | ||
intent: Intent; | ||
constructor(ctxId: string, sId: number); | ||
toSegment(): Segment; | ||
toString(): string; | ||
updateTranscript(words: Word[]): SegmentState; | ||
updateEntities(entities: Entity[]): SegmentState; | ||
updateIntent(intent: Intent): SegmentState; | ||
finalize(): SegmentState; | ||
} | ||
/** | ||
* Converts client state value to a string, which could be useful for debugging or metrics. | ||
* @param state - the state of the client | ||
* @public | ||
*/ | ||
export declare function stateToString(state: DecoderState): string; | ||
/** | ||
* The interface for local key-value storage. | ||
* @public | ||
*/ | ||
declare interface Storage_2 { | ||
/** | ||
* Retrieves a key from the storage. | ||
* | ||
* @param key - the key to retrieve | ||
*/ | ||
get(key: string): string | null; | ||
/** | ||
* Adds a key to the storage, possibly overwriting existing value. | ||
* | ||
* @param key - the key to write | ||
* @param val - the value to write | ||
*/ | ||
set(key: string, val: string): void; | ||
/** | ||
* Adds a key to the storage, possibly overwriting existing value. | ||
* | ||
* @param key - the key to write | ||
* @param genFn - generator function that will be invoked if the key cannot be found in the storage. | ||
* The return value of the function will be used as the value that will be stored under the given key. | ||
*/ | ||
getOrSet(key: string, genFn: () => string): string; | ||
} | ||
export { Storage_2 as Storage } | ||
/** | ||
* Tenative entities response payload. | ||
* @public | ||
*/ | ||
export declare interface TentativeEntitiesResponse { | ||
/** | ||
* Individual entities. | ||
*/ | ||
entities: EntityResponse[]; | ||
} | ||
/** | ||
* Tentative transcript response payload. | ||
* @public | ||
*/ | ||
export declare interface TentativeTranscriptResponse { | ||
/** | ||
* Transcript text, i.e. the full transcript of the audio to-date. | ||
*/ | ||
transcript: string; | ||
/** | ||
* Individual transcript words. | ||
*/ | ||
words: TranscriptResponse[]; | ||
} | ||
/** | ||
* Transcript response payload. | ||
* @public | ||
*/ | ||
export declare interface TranscriptResponse { | ||
/** | ||
* Transcripted word. | ||
*/ | ||
word: string; | ||
/** | ||
* The index of the transcripted word in the segment. | ||
*/ | ||
index: number; | ||
/** | ||
* Start timestamp of the transcript in the audio stream in milliseconds. | ||
*/ | ||
start_timestamp: number; | ||
/** | ||
* End timestamp of the transcript in the audio stream in milliseconds. | ||
*/ | ||
end_timestamp: number; | ||
} | ||
/** | ||
* The interface for response returned by WebSocket client. | ||
* @public | ||
*/ | ||
export declare interface WebsocketResponse { | ||
/** | ||
* Response type. | ||
*/ | ||
type: WebsocketResponseType; | ||
/** | ||
* Audio context ID. | ||
*/ | ||
audio_context: string; | ||
/** | ||
* Segment ID. | ||
*/ | ||
segment_id: number; | ||
/** | ||
* Response payload. | ||
* | ||
* The payload value should match the response type (i.e. TranscriptResponse should have Transcript type). | ||
* Not all response types have payloads - Started, Stopped and SegmentEnd don't have payloads. | ||
* TentativeIntent and Intent share the same payload interface (IntentResponse). | ||
*/ | ||
data: TranscriptResponse | EntityResponse | IntentResponse | TentativeTranscriptResponse | TentativeEntitiesResponse; | ||
} | ||
/** | ||
* Known WebSocket response types. | ||
* @public | ||
*/ | ||
export declare enum WebsocketResponseType { | ||
Opened = "WEBSOCKET_OPEN", | ||
Closed = "WEBSOCKET_CLOSED", | ||
SourceSampleRateSetSuccess = "SOURCE_SAMPLE_RATE_SET_SUCCESS", | ||
Started = "started", | ||
Stopped = "stopped", | ||
SegmentEnd = "segment_end", | ||
Transcript = "transcript", | ||
Entity = "entity", | ||
Intent = "intent", | ||
TentativeTranscript = "tentative_transcript", | ||
TentativeEntities = "tentative_entities", | ||
TentativeIntent = "tentative_intent" | ||
} | ||
/** | ||
* A single word detected by the SLU API. | ||
* @public | ||
*/ | ||
export declare interface Word { | ||
/** | ||
* The value of the word. | ||
*/ | ||
value: string; | ||
/** | ||
* The index of the word within a segment. | ||
*/ | ||
index: number; | ||
/** | ||
* Start timestamp of the word within the audio of the context. | ||
*/ | ||
startTimestamp: number; | ||
/** | ||
* End timestamp of the word within the audio of the context. | ||
*/ | ||
endTimestamp: number; | ||
/** | ||
* Whether the word was detected as final. | ||
*/ | ||
isFinal: boolean; | ||
} | ||
export { } |
@@ -1,2 +0,2 @@ | ||
declare const _default: "\n// Indices for the Control SAB.\nconst CONTROL = {\n 'WRITE_INDEX': 0,\n 'FRAMES_AVAILABLE': 1,\n 'LOCK': 2,\n};\n\nclass SpeechlyProcessor extends AudioWorkletProcessor {\n constructor() {\n super();\n\n this._initialized = false;\n this.debug = false;\n this.port.onmessage = this._initialize.bind(this);\n }\n\n _initialize(event) {\n this.controlSAB = new Int32Array(event.data.controlSAB);\n this.dataSAB = new Float32Array(event.data.dataSAB);\n this.debug = event.data.debug;\n this.sharedBufferSize = this.dataSAB.length;\n this.buffer = new Float32Array(0);\n this._initialized = true;\n }\n\n _transferDataToSharedBuffer(data) {\n this.controlSAB[CONTROL.LOCK] = 1\n let inputWriteIndex = this.controlSAB[CONTROL.WRITE_INDEX]\n if (this.controlSAB[CONTROL.FRAMES_AVAILABLE] > 0) {\n if (inputWriteIndex + data.length > this.sharedBufferSize) {\n // console.log('buffer overflow')\n inputWriteIndex = 0\n }\n }\n this.dataSAB.set(data, inputWriteIndex)\n this.controlSAB[CONTROL.WRITE_INDEX] = inputWriteIndex + data.length\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = inputWriteIndex + data.length\n this.controlSAB[CONTROL.LOCK] = 0\n }\n\n _pushData(data) {\n if (this.debug) {\n const signalEnergy = getStandardDeviation(data)\n this.port.postMessage({\n type: 'STATS',\n signalEnergy: signalEnergy\n });\n }\n\n if (this.buffer.length > this.sharedBufferSize) {\n const dataToTransfer = this.buffer.subarray(0, this.sharedBufferSize)\n this._transferDataToSharedBuffer(dataToTransfer)\n this.buffer = this.buffer.subarray(this.sharedBufferSize)\n }\n let concat = new Float32Array(this.buffer.length + data.length)\n concat.set(this.buffer)\n concat.set(data, this.buffer.length)\n this.buffer = concat\n }\n\n process(inputs, outputs, parameters) {\n const inputChannelData = inputs[0][0];\n if (inputChannelData !== undefined) {\n if (this.controlSAB && this.dataSAB) {\n this._pushData(inputChannelData);\n } else {\n this.port.postMessage({\n type: 'DATA',\n frames: inputChannelData\n });\n }\n }\n \n return true;\n }\n}\n\nfunction getStandardDeviation(array) {\n const n = array.length\n const mean = array.reduce((a, b) => a + b) / n\n return Math.sqrt(array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n)\n}\n\nregisterProcessor('speechly-worklet', SpeechlyProcessor);\n"; | ||
declare const _default: "\n// Indices for the Control SAB.\nconst CONTROL = {\n 'WRITE_INDEX': 0,\n 'FRAMES_AVAILABLE': 1,\n 'LOCK': 2,\n};\n\nclass SpeechlyProcessor extends AudioWorkletProcessor {\n constructor() {\n super();\n\n this._initialized = false;\n this.debug = false;\n this.port.onmessage = this._initialize.bind(this);\n }\n\n _initialize(event) {\n this.controlSAB = new Int32Array(event.data.controlSAB);\n this.dataSAB = new Float32Array(event.data.dataSAB);\n this.debug = event.data.debug;\n if (this.debug) {\n console.log('[BrowserClient AudioWorkletNode]', 'initializing audioworklet');\n }\n this.sharedBufferSize = this.dataSAB.length;\n this.buffer = new Float32Array(0);\n this._initialized = true;\n }\n\n _transferDataToSharedBuffer(data) {\n this.controlSAB[CONTROL.LOCK] = 1;\n let inputWriteIndex = this.controlSAB[CONTROL.WRITE_INDEX];\n if (this.controlSAB[CONTROL.FRAMES_AVAILABLE] > 0) {\n if (inputWriteIndex + data.length > this.sharedBufferSize) {\n // console.log('buffer overflow')\n inputWriteIndex = 0;\n }\n }\n this.dataSAB.set(data, inputWriteIndex);\n this.controlSAB[CONTROL.WRITE_INDEX] = inputWriteIndex + data.length;\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = inputWriteIndex + data.length;\n this.controlSAB[CONTROL.LOCK] = 0;\n }\n\n _pushData(data) {\n if (this.debug) {\n const signalEnergy = getStandardDeviation(data)\n this.port.postMessage({\n type: 'STATS',\n signalEnergy: signalEnergy,\n samples: data.length,\n });\n }\n\n if (this.buffer.length > this.sharedBufferSize) {\n const dataToTransfer = this.buffer.subarray(0, this.sharedBufferSize);\n this._transferDataToSharedBuffer(dataToTransfer);\n this.buffer = this.buffer.subarray(this.sharedBufferSize);\n }\n let concat = new Float32Array(this.buffer.length + data.length);\n concat.set(this.buffer);\n concat.set(data, this.buffer.length);\n this.buffer = concat;\n }\n\n process(inputs, outputs, parameters) {\n const inputChannelData = inputs[0][0];\n if (inputChannelData !== undefined) {\n if (this.controlSAB && this.dataSAB) {\n this._pushData(inputChannelData);\n } else {\n this.port.postMessage({\n type: 'DATA',\n frames: inputChannelData\n });\n }\n }\n\n return true;\n }\n}\n\nfunction getStandardDeviation(array) {\n const n = array.length\n const mean = array.reduce((a, b) => a + b) / n\n return Math.sqrt(array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n)\n}\n\nregisterProcessor('speechly-worklet', SpeechlyProcessor);\n"; | ||
export default _default; |
@@ -1,26 +0,27 @@ | ||
import { Microphone } from './types'; | ||
import { APIClient } from '../websocket'; | ||
export declare class BrowserMicrophone implements Microphone { | ||
private readonly debug; | ||
private readonly isWebkit; | ||
private readonly apiClient; | ||
private readonly sampleRate; | ||
/** | ||
* Gets browser based microphone using the window.navigator.mediaDevices interface. | ||
* The exposed `mediaStream` can be attached to a `BrowserClient` instance. | ||
* @public | ||
*/ | ||
export declare class BrowserMicrophone { | ||
private muted; | ||
private initialized; | ||
private muted; | ||
private audioContext?; | ||
private resampleRatio?; | ||
private audioTrack?; | ||
private mediaStream?; | ||
private audioProcessor?; | ||
private stats; | ||
constructor(isWebkit: boolean, sampleRate: number, apiClient: APIClient, debug?: boolean); | ||
initialize(audioContext: AudioContext, mediaStreamConstraints: MediaStreamConstraints): Promise<void>; | ||
private readonly nativeResamplingSupported; | ||
private readonly autoGainControl; | ||
mediaStream?: MediaStream; | ||
constructor(); | ||
/** | ||
* Initializes the microphone. Needs to happen after a user interaction in the view. | ||
* The reason for that is that it's required for user to first interact with the page, | ||
* before it can capture or play audio and video, for privacy and user experience reasons. | ||
*/ | ||
initialize(): Promise<void>; | ||
/** | ||
* Closes the microphone, releases all resources and stops the Speechly client. | ||
*/ | ||
close(): Promise<void>; | ||
mute(): void; | ||
unmute(): void; | ||
private readonly handleAudio; | ||
/** | ||
* print statistics to console | ||
* @returns true if microphone is open | ||
*/ | ||
printStats(): void; | ||
isRecording(): boolean; | ||
} |
/** | ||
* Default sample rate for microphone streams. | ||
* @public | ||
*/ | ||
export declare const DefaultSampleRate = 16000; | ||
/** | ||
* Error to be thrown when the microphone was accessed before it was initialized. | ||
@@ -17,7 +12,2 @@ * @public | ||
/** | ||
* Error to be thrown when the device does not support the Microphone instance's target audio APIs. | ||
* @public | ||
*/ | ||
export declare const ErrDeviceNotSupported: Error; | ||
/** | ||
* Error to be thrown when user did not give consent to the application to record audio. | ||
@@ -27,45 +17,1 @@ * @public | ||
export declare const ErrNoAudioConsent: Error; | ||
/** | ||
* Error to be thrown when user tries to change appId without project login. | ||
* @public | ||
*/ | ||
export declare const ErrAppIdChangeWithoutProjectLogin: Error; | ||
/** | ||
* A callback that receives an ArrayBuffer representing a frame of audio. | ||
* @public | ||
*/ | ||
export declare type AudioCallback = (audioBuffer: Int16Array) => void; | ||
/** | ||
* The interface for a microphone. | ||
* @public | ||
*/ | ||
export interface Microphone { | ||
/** | ||
* Initialises the microphone. | ||
* | ||
* This should prepare the microphone infrastructure for receiving audio chunks, | ||
* but the microphone should remain muted after the call. | ||
* This method will be called by the Client as part of client initialisation process. | ||
*/ | ||
initialize(audioContext: AudioContext, mediaStreamConstraints: MediaStreamConstraints): Promise<void>; | ||
/** | ||
* Closes the microphone, tearing down all the infrastructure. | ||
* | ||
* The microphone should stop emitting audio after this is called. | ||
* Calling `initialize` again after calling `close` should succeed and make microphone ready to use again. | ||
* This method will be called by the Client as part of client closure process. | ||
*/ | ||
close(): Promise<void>; | ||
/** | ||
* Mutes the microphone. If the microphone is muted, the `onAudio` callbacks should not be called. | ||
*/ | ||
mute(): void; | ||
/** | ||
* Unmutes the microphone. | ||
*/ | ||
unmute(): void; | ||
/** | ||
* Print usage stats to console in debug mode. | ||
*/ | ||
printStats(): void; | ||
} |
export * from './types'; | ||
export { stateToString } from './state'; | ||
export { Client } from './client'; | ||
export * from './segment'; |
import { Word, Entity, Intent, Segment } from './types'; | ||
/** | ||
* Contains the internal state of a `Segment`, with methods to update it based on new events. | ||
* @public | ||
*/ | ||
export declare class SegmentState { | ||
@@ -3,0 +7,0 @@ id: number; |
@@ -1,126 +0,17 @@ | ||
import { Microphone } from '../microphone'; | ||
import { Storage } from '../storage'; | ||
import { APIClient } from '../websocket'; | ||
/** | ||
* The options which can be used to configure the client. | ||
* Error to be thrown when the device does not support audioContext. | ||
* @public | ||
*/ | ||
export interface ClientOptions { | ||
/** | ||
* The unique identifier of an app in the dashboard. | ||
*/ | ||
appId?: string; | ||
/** | ||
* Connect to Speechly upon creating the client instance. Defaults to true. | ||
*/ | ||
connect?: boolean; | ||
/** | ||
* The unique identifier of a project in the dashboard. | ||
*/ | ||
projectId?: string; | ||
/** | ||
* @deprecated | ||
* The language which is used by the app. | ||
*/ | ||
language?: string; | ||
/** | ||
* The URL of Speechly login endpoint. | ||
*/ | ||
loginUrl?: string; | ||
/** | ||
* The URL of Speechly SLU API endpoint. | ||
*/ | ||
apiUrl?: string; | ||
/** | ||
* The sample rate of the audio to use. | ||
*/ | ||
sampleRate?: number; | ||
/** | ||
* Whether to output debug statements to the console. | ||
*/ | ||
debug?: boolean; | ||
/** | ||
* Whether to use auto gain control. | ||
* True by default. | ||
*/ | ||
autoGainControl?: boolean; | ||
/** | ||
* Whether to output updated segments to the console. | ||
*/ | ||
logSegments?: boolean; | ||
/** | ||
* Custom microphone implementation. | ||
* If not provided, an implementation based on getUserMedia and Web Audio API is used. | ||
*/ | ||
microphone?: Microphone; | ||
/** | ||
* Custom API client implementation. | ||
* If not provided, an implementation based on Speechly SLU WebSocket API is used. | ||
*/ | ||
apiClient?: APIClient; | ||
/** | ||
* Custom storage implementation. | ||
* If not provided, browser's LocalStorage API is used. | ||
*/ | ||
storage?: Storage; | ||
} | ||
export declare const ErrDeviceNotSupported: Error; | ||
/** | ||
* A callback that is invoked whenever the {@link ClientState | client state} changes. | ||
* Error to be thrown when user tries to change appId without project login. | ||
* @public | ||
*/ | ||
export declare type StateChangeCallback = (state: ClientState) => void; | ||
export declare const ErrAppIdChangeWithoutProjectLogin: Error; | ||
/** | ||
* A callback that is invoked whenever current {@link Segment | segment} changes. | ||
* Default sample rate for microphone streams. | ||
* @public | ||
*/ | ||
export declare type SegmentChangeCallback = (segment: Segment) => void; | ||
export declare const DefaultSampleRate = 16000; | ||
/** | ||
* A callback that is invoked whenever a new tentative transcript is received from the API. | ||
* @public | ||
*/ | ||
export declare type TentativeTranscriptCallback = (contextId: string, segmentId: number, words: Word[], text: string) => void; | ||
/** | ||
* A callback that is invoked whenever a new transcript is received from the API. | ||
* @public | ||
*/ | ||
export declare type TranscriptCallback = (contextId: string, segmentId: number, word: Word) => void; | ||
/** | ||
* A callback that is invoked whenever new tentative entities are received from the API. | ||
* @public | ||
*/ | ||
export declare type TentativeEntitiesCallback = (contextId: string, segmentId: number, entities: Entity[]) => void; | ||
/** | ||
* A callback that is invoked whenever new entity is received from the API. | ||
* @public | ||
*/ | ||
export declare type EntityCallback = (contextId: string, segmentId: number, entity: Entity) => void; | ||
/** | ||
* A callback that is invoked whenever new intent (tentative or not) is received from the API. | ||
* @public | ||
*/ | ||
export declare type IntentCallback = (contextId: string, segmentId: number, intent: Intent) => void; | ||
/** | ||
* All possible states of a Speechly API client. Failed, NoBrowserSupport and NoAudioConsent states are non-recoverable | ||
* erroneous states, which should be handled by the end user, according to the semantics of an application. | ||
* Other states can also be utilized for e.g. enabling and disabling recording buttons or showing the status in the app. | ||
* It is also possible to use arithmetics for state comparison, e.g. `if (state < speechly.ClientState.Disconnected)`, | ||
* to react to non-recoverable states. | ||
* @public | ||
*/ | ||
export declare enum ClientState { | ||
Failed = 0, | ||
NoBrowserSupport = 1, | ||
NoAudioConsent = 2, | ||
__UnrecoverableErrors = 3, | ||
Disconnected = 4, | ||
Disconnecting = 5, | ||
Connecting = 6, | ||
Preinitialized = 7, | ||
Initializing = 8, | ||
Connected = 9, | ||
Stopping = 10, | ||
Starting = 11, | ||
Recording = 12 | ||
} | ||
/** | ||
* The smallest component of SLU API, defined by an intent. | ||
@@ -127,0 +18,0 @@ * @public |
@@ -34,3 +34,3 @@ /** | ||
Closed = "WEBSOCKET_CLOSED", | ||
SourceSampleRateSetSuccess = "SOURSE_SAMPLE_RATE_SET_SUCCESS", | ||
SourceSampleRateSetSuccess = "SOURCE_SAMPLE_RATE_SET_SUCCESS", | ||
Started = "started", | ||
@@ -37,0 +37,0 @@ Stopped = "stopped", |
@@ -1,2 +0,2 @@ | ||
declare const _default: "/**\n * Known WebSocket response types.\n * @public\n */\nvar WebsocketResponseType;\n(function (WebsocketResponseType) {\n WebsocketResponseType[\"Opened\"] = \"WEBSOCKET_OPEN\";\n WebsocketResponseType[\"SourceSampleRateSetSuccess\"] = \"SOURSE_SAMPLE_RATE_SET_SUCCESS\";\n WebsocketResponseType[\"Started\"] = \"started\";\n WebsocketResponseType[\"Stopped\"] = \"stopped\";\n})(WebsocketResponseType || (WebsocketResponseType = {}));\nvar CONTROL = {\n WRITE_INDEX: 0,\n FRAMES_AVAILABLE: 1,\n LOCK: 2\n};\nvar WebsocketClient = /** @class */ (function () {\n function WebsocketClient(ctx) {\n var _this = this;\n this.isContextStarted = false;\n this.isStartContextConfirmed = false;\n this.shouldResendLastFramesSent = false;\n this.buffer = new Float32Array(0);\n this.lastFramesSent = new Int16Array(0); // to re-send after switch context\n this.debug = false;\n this.initialized = false;\n // WebSocket's close handler, called e.g. when\n // - normal close (code 1000)\n // - network unreachable or unable to (re)connect (code 1006)\n // List of CloseEvent.code values: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code\n this.onWebsocketClose = function (event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'onWebsocketClose');\n }\n _this.websocket.removeEventListener('open', _this.onWebsocketOpen);\n _this.websocket.removeEventListener('message', _this.onWebsocketMessage);\n _this.websocket.removeEventListener('error', _this.onWebsocketError);\n _this.websocket.removeEventListener('close', _this.onWebsocketClose);\n _this.websocket = undefined;\n _this.workerCtx.postMessage({ type: 'WEBSOCKET_CLOSED', code: event.code, reason: event.reason, wasClean: event.wasClean });\n };\n this.onWebsocketOpen = function (_event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'websocket opened');\n }\n if (_this.isContextStarted && !_this.isStartContextConfirmed) {\n _this.send(_this.outbox);\n }\n _this.workerCtx.postMessage({ type: 'WEBSOCKET_OPEN' });\n };\n this.onWebsocketError = function (_event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'websocket error');\n }\n };\n this.onWebsocketMessage = function (event) {\n var response;\n try {\n response = JSON.parse(event.data);\n }\n catch (e) {\n console.error('[SpeechlyClient] Error parsing response from the server:', e);\n return;\n }\n if (response.type === WebsocketResponseType.Started) {\n _this.isStartContextConfirmed = true;\n if (_this.shouldResendLastFramesSent) {\n _this.resendLastFrames();\n _this.shouldResendLastFramesSent = false;\n }\n }\n _this.workerCtx.postMessage(response);\n };\n this.workerCtx = ctx;\n }\n WebsocketClient.prototype.init = function (apiUrl, authToken, targetSampleRate, debug) {\n this.debug = debug;\n if (this.debug) {\n console.log('[SpeechlyClient]', 'initialize worker');\n }\n this.apiUrl = apiUrl;\n this.authToken = authToken;\n this.targetSampleRate = targetSampleRate;\n this.initialized = true;\n this.isContextStarted = false;\n this.connect(0);\n };\n WebsocketClient.prototype.setSourceSampleRate = function (sourceSampleRate) {\n this.sourceSampleRate = sourceSampleRate;\n this.resampleRatio = this.sourceSampleRate / this.targetSampleRate;\n if (this.debug) {\n console.log('[SpeechlyClient]', 'resampleRatio', this.resampleRatio);\n }\n if (this.resampleRatio > 1) {\n this.filter = generateFilter(this.sourceSampleRate, this.targetSampleRate, 127);\n }\n this.workerCtx.postMessage({ type: 'SOURSE_SAMPLE_RATE_SET_SUCCESS' });\n if (isNaN(this.resampleRatio)) {\n throw Error(\"resampleRatio is NaN source rate is \".concat(this.sourceSampleRate, \" and target rate is \").concat(this.targetSampleRate));\n }\n };\n WebsocketClient.prototype.setSharedArrayBuffers = function (controlSAB, dataSAB) {\n this.controlSAB = new Int32Array(controlSAB);\n this.dataSAB = new Float32Array(dataSAB);\n var audioHandleInterval = this.dataSAB.length / 32; // ms\n if (this.debug) {\n console.log('[SpeechlyClient]', 'Audio handle interval', audioHandleInterval, 'ms');\n }\n setInterval(this.sendAudioFromSAB.bind(this), audioHandleInterval);\n };\n WebsocketClient.prototype.connect = function (timeout) {\n if (timeout === void 0) { timeout = 1000; }\n if (this.debug) {\n console.log('[SpeechlyClient]', 'connect in ', timeout / 1000, 'sec');\n }\n setTimeout(this.initializeWebsocket.bind(this), timeout);\n };\n WebsocketClient.prototype.initializeWebsocket = function () {\n if (this.debug) {\n console.log('[SpeechlyClient]', 'connecting to ', this.apiUrl);\n }\n this.websocket = new WebSocket(this.apiUrl, this.authToken);\n this.websocket.addEventListener('open', this.onWebsocketOpen);\n this.websocket.addEventListener('message', this.onWebsocketMessage);\n this.websocket.addEventListener('error', this.onWebsocketError);\n this.websocket.addEventListener('close', this.onWebsocketClose);\n };\n WebsocketClient.prototype.isOpen = function () {\n return this.websocket !== undefined && this.websocket.readyState === this.websocket.OPEN;\n };\n WebsocketClient.prototype.resendLastFrames = function () {\n if (this.lastFramesSent.length > 0) {\n this.send(this.lastFramesSent);\n this.lastFramesSent = new Int16Array(0);\n }\n };\n WebsocketClient.prototype.sendAudio = function (audioChunk) {\n if (!this.isContextStarted) {\n return;\n }\n if (audioChunk.length > 0) {\n if (this.resampleRatio > 1) {\n // Downsampling\n this.send(this.downsample(audioChunk));\n }\n else {\n this.send(float32ToInt16(audioChunk));\n }\n }\n };\n WebsocketClient.prototype.sendAudioFromSAB = function () {\n if (!this.isContextStarted) {\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = 0;\n this.controlSAB[CONTROL.WRITE_INDEX] = 0;\n return;\n }\n if (this.controlSAB == undefined) {\n return;\n }\n var framesAvailable = this.controlSAB[CONTROL.FRAMES_AVAILABLE];\n var lock = this.controlSAB[CONTROL.LOCK];\n if (lock == 0 && framesAvailable > 0) {\n var data = this.dataSAB.subarray(0, framesAvailable);\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = 0;\n this.controlSAB[CONTROL.WRITE_INDEX] = 0;\n if (data.length > 0) {\n var frames_1;\n if (this.resampleRatio > 1) {\n frames_1 = this.downsample(data);\n }\n else {\n frames_1 = float32ToInt16(data);\n }\n this.send(frames_1);\n // 16000 per second, 1000 in 100 ms\n // save last 250 ms\n if (this.lastFramesSent.length > 1024 * 4) {\n this.lastFramesSent = frames_1;\n }\n else {\n var concat = new Int16Array(this.lastFramesSent.length + frames_1.length);\n concat.set(this.lastFramesSent);\n concat.set(frames_1, this.lastFramesSent.length);\n this.lastFramesSent = concat;\n }\n }\n }\n };\n WebsocketClient.prototype.startContext = function (appId) {\n if (this.isContextStarted) {\n console.log('Cant start context: it has been already started');\n return;\n }\n this.isContextStarted = true;\n this.isStartContextConfirmed = false;\n if (appId !== undefined) {\n this.outbox = JSON.stringify({ event: 'start', appId: appId });\n }\n else {\n this.outbox = JSON.stringify({ event: 'start' });\n }\n this.send(this.outbox);\n };\n WebsocketClient.prototype.stopContext = function () {\n if (!this.websocket) {\n throw Error('Cant start context: websocket is undefined');\n }\n if (!this.isContextStarted) {\n console.log('Cant stop context: it is not started');\n return;\n }\n this.isContextStarted = false;\n this.isStartContextConfirmed = false;\n var StopEventJSON = JSON.stringify({ event: 'stop' });\n this.send(StopEventJSON);\n };\n WebsocketClient.prototype.switchContext = function (newAppId) {\n if (!this.websocket) {\n throw Error('Cant switch context: websocket is undefined');\n }\n if (!this.isContextStarted) {\n console.log('Cant switch context: it is not started');\n return;\n }\n if (newAppId == undefined) {\n console.log('Cant switch context: new app id is undefined');\n return;\n }\n this.isStartContextConfirmed = false;\n var StopEventJSON = JSON.stringify({ event: 'stop' });\n this.send(StopEventJSON);\n this.shouldResendLastFramesSent = true;\n this.send(JSON.stringify({ event: 'start', appId: newAppId }));\n };\n WebsocketClient.prototype.closeWebsocket = function (websocketCode, reason) {\n if (websocketCode === void 0) { websocketCode = 1005; }\n if (reason === void 0) { reason = \"No Status Received\"; }\n if (this.debug) {\n console.log('[SpeechlyClient]', 'Websocket closing');\n }\n if (!this.websocket) {\n throw Error('Websocket is not open');\n }\n this.websocket.close(websocketCode, reason);\n };\n WebsocketClient.prototype.downsample = function (input) {\n var inputBuffer = new Float32Array(this.buffer.length + input.length);\n inputBuffer.set(this.buffer, 0);\n inputBuffer.set(input, this.buffer.length);\n var outputLength = Math.ceil((inputBuffer.length - this.filter.length) / this.resampleRatio);\n var outputBuffer = new Int16Array(outputLength);\n for (var i = 0; i < outputLength; i++) {\n var offset = Math.round(this.resampleRatio * i);\n var val = 0.0;\n for (var j = 0; j < this.filter.length; j++) {\n val += inputBuffer[offset + j] * this.filter[j];\n }\n outputBuffer[i] = val * (val < 0 ? 0x8000 : 0x7fff);\n }\n var remainingOffset = Math.round(this.resampleRatio * outputLength);\n if (remainingOffset < inputBuffer.length) {\n this.buffer = inputBuffer.subarray(remainingOffset);\n }\n else {\n this.buffer = new Float32Array(0);\n }\n return outputBuffer;\n };\n WebsocketClient.prototype.send = function (data) {\n if (this.isOpen()) {\n try {\n this.websocket.send(data);\n }\n catch (error) {\n console.log('[SpeechlyClient]', 'Server connection error', error);\n }\n }\n };\n return WebsocketClient;\n}());\nvar ctx = self;\nvar websocketClient = new WebsocketClient(ctx);\nctx.onmessage = function (e) {\n switch (e.data.type) {\n case 'INIT':\n websocketClient.init(e.data.apiUrl, e.data.authToken, e.data.targetSampleRate, e.data.debug);\n break;\n case 'SET_SOURSE_SAMPLE_RATE':\n websocketClient.setSourceSampleRate(e.data.sourceSampleRate);\n break;\n case 'SET_SHARED_ARRAY_BUFFERS':\n websocketClient.setSharedArrayBuffers(e.data.controlSAB, e.data.dataSAB);\n break;\n case 'CLOSE':\n websocketClient.closeWebsocket(1000, \"Close requested by client\");\n break;\n case 'START_CONTEXT':\n websocketClient.startContext(e.data.appId);\n break;\n case 'SWITCH_CONTEXT':\n websocketClient.switchContext(e.data.appId);\n break;\n case 'STOP_CONTEXT':\n websocketClient.stopContext();\n break;\n case 'AUDIO':\n websocketClient.sendAudio(e.data.payload);\n break;\n default:\n console.log('WORKER', e);\n }\n};\nfunction float32ToInt16(buffer) {\n var buf = new Int16Array(buffer.length);\n for (var l = 0; l < buffer.length; l++) {\n buf[l] = buffer[l] * (buffer[l] < 0 ? 0x8000 : 0x7fff);\n }\n return buf;\n}\nfunction generateFilter(sourceSampleRate, targetSampleRate, length) {\n if (length % 2 === 0) {\n throw Error('Filter length must be odd');\n }\n var cutoff = targetSampleRate / 2;\n var filter = new Float32Array(length);\n var sum = 0;\n for (var i = 0; i < length; i++) {\n var x = sinc(((2 * cutoff) / sourceSampleRate) * (i - (length - 1) / 2));\n sum += x;\n filter[i] = x;\n }\n for (var i = 0; i < length; i++) {\n filter[i] = filter[i] / sum;\n }\n return filter;\n}\nfunction sinc(x) {\n if (x === 0.0) {\n return 1.0;\n }\n var piX = Math.PI * x;\n return Math.sin(piX) / piX;\n}\n"; | ||
declare const _default: "/**\n * Known WebSocket response types.\n * @public\n */\nvar WebsocketResponseType;\n(function (WebsocketResponseType) {\n WebsocketResponseType[\"Opened\"] = \"WEBSOCKET_OPEN\";\n WebsocketResponseType[\"SourceSampleRateSetSuccess\"] = \"SOURCE_SAMPLE_RATE_SET_SUCCESS\";\n WebsocketResponseType[\"Started\"] = \"started\";\n WebsocketResponseType[\"Stopped\"] = \"stopped\";\n})(WebsocketResponseType || (WebsocketResponseType = {}));\nvar CONTROL = {\n WRITE_INDEX: 0,\n FRAMES_AVAILABLE: 1,\n LOCK: 2\n};\nvar WebsocketClient = /** @class */ (function () {\n function WebsocketClient(ctx) {\n var _this = this;\n this.isContextStarted = false;\n this.isStartContextConfirmed = false;\n this.shouldResendLastFramesSent = false;\n this.buffer = new Float32Array(0);\n this.lastFramesSent = new Int16Array(0); // to re-send after switch context\n this.debug = false;\n this.initialized = false;\n // WebSocket's close handler, called e.g. when\n // - normal close (code 1000)\n // - network unreachable or unable to (re)connect (code 1006)\n // List of CloseEvent.code values: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code\n this.onWebsocketClose = function (event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'onWebsocketClose');\n }\n _this.websocket.removeEventListener('open', _this.onWebsocketOpen);\n _this.websocket.removeEventListener('message', _this.onWebsocketMessage);\n _this.websocket.removeEventListener('error', _this.onWebsocketError);\n _this.websocket.removeEventListener('close', _this.onWebsocketClose);\n _this.websocket = undefined;\n _this.workerCtx.postMessage({\n type: 'WEBSOCKET_CLOSED',\n code: event.code,\n reason: event.reason,\n wasClean: event.wasClean\n });\n };\n this.onWebsocketOpen = function (_event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'websocket opened');\n }\n if (_this.isContextStarted && !_this.isStartContextConfirmed) {\n _this.send(_this.outbox);\n }\n _this.workerCtx.postMessage({ type: 'WEBSOCKET_OPEN' });\n };\n this.onWebsocketError = function (_event) {\n if (_this.debug) {\n console.log('[SpeechlyClient]', 'websocket error');\n }\n };\n this.onWebsocketMessage = function (event) {\n var response;\n try {\n response = JSON.parse(event.data);\n }\n catch (e) {\n console.error('[SpeechlyClient] Error parsing response from the server:', e);\n return;\n }\n if (response.type === WebsocketResponseType.Started) {\n _this.isStartContextConfirmed = true;\n if (_this.shouldResendLastFramesSent) {\n _this.resendLastFrames();\n _this.shouldResendLastFramesSent = false;\n }\n }\n _this.workerCtx.postMessage(response);\n };\n this.workerCtx = ctx;\n }\n WebsocketClient.prototype.init = function (apiUrl, authToken, targetSampleRate, debug) {\n this.debug = debug;\n if (this.debug) {\n console.log('[SpeechlyClient]', 'initialize worker');\n }\n this.apiUrl = apiUrl;\n this.authToken = authToken;\n this.targetSampleRate = targetSampleRate;\n this.initialized = true;\n this.isContextStarted = false;\n this.connect(0);\n };\n WebsocketClient.prototype.setSourceSampleRate = function (sourceSampleRate) {\n this.sourceSampleRate = sourceSampleRate;\n this.resampleRatio = this.sourceSampleRate / this.targetSampleRate;\n if (this.debug) {\n console.log('[SpeechlyClient]', 'resampleRatio', this.resampleRatio);\n }\n if (this.resampleRatio > 1) {\n this.filter = generateFilter(this.sourceSampleRate, this.targetSampleRate, 127);\n }\n this.workerCtx.postMessage({ type: 'SOURCE_SAMPLE_RATE_SET_SUCCESS' });\n if (isNaN(this.resampleRatio)) {\n throw Error(\"resampleRatio is NaN source rate is \".concat(this.sourceSampleRate, \" and target rate is \").concat(this.targetSampleRate));\n }\n };\n WebsocketClient.prototype.setSharedArrayBuffers = function (controlSAB, dataSAB) {\n this.controlSAB = new Int32Array(controlSAB);\n this.dataSAB = new Float32Array(dataSAB);\n var audioHandleInterval = this.dataSAB.length / 32; // ms\n if (this.debug) {\n console.log('[SpeechlyClient]', 'Audio handle interval', audioHandleInterval, 'ms');\n }\n setInterval(this.sendAudioFromSAB.bind(this), audioHandleInterval);\n };\n WebsocketClient.prototype.connect = function (timeout) {\n if (timeout === void 0) { timeout = 1000; }\n if (this.debug) {\n console.log('[SpeechlyClient]', 'connect in ', timeout / 1000, 'sec');\n }\n setTimeout(this.initializeWebsocket.bind(this), timeout);\n };\n WebsocketClient.prototype.initializeWebsocket = function () {\n if (this.debug) {\n console.log('[SpeechlyClient]', 'connecting to ', this.apiUrl);\n }\n this.websocket = new WebSocket(this.apiUrl, this.authToken);\n this.websocket.addEventListener('open', this.onWebsocketOpen);\n this.websocket.addEventListener('message', this.onWebsocketMessage);\n this.websocket.addEventListener('error', this.onWebsocketError);\n this.websocket.addEventListener('close', this.onWebsocketClose);\n };\n WebsocketClient.prototype.isOpen = function () {\n return this.websocket !== undefined && this.websocket.readyState === this.websocket.OPEN;\n };\n WebsocketClient.prototype.resendLastFrames = function () {\n if (this.lastFramesSent.length > 0) {\n this.send(this.lastFramesSent);\n this.lastFramesSent = new Int16Array(0);\n }\n };\n WebsocketClient.prototype.sendAudio = function (audioChunk) {\n if (!this.isContextStarted) {\n return;\n }\n if (audioChunk.length > 0) {\n if (this.resampleRatio > 1) {\n // Downsampling\n this.send(this.downsample(audioChunk));\n }\n else {\n this.send(float32ToInt16(audioChunk));\n }\n }\n };\n WebsocketClient.prototype.sendAudioFromSAB = function () {\n if (!this.isContextStarted) {\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = 0;\n this.controlSAB[CONTROL.WRITE_INDEX] = 0;\n return;\n }\n if (this.controlSAB == undefined) {\n return;\n }\n var framesAvailable = this.controlSAB[CONTROL.FRAMES_AVAILABLE];\n var lock = this.controlSAB[CONTROL.LOCK];\n if (lock == 0 && framesAvailable > 0) {\n var data = this.dataSAB.subarray(0, framesAvailable);\n this.controlSAB[CONTROL.FRAMES_AVAILABLE] = 0;\n this.controlSAB[CONTROL.WRITE_INDEX] = 0;\n if (data.length > 0) {\n var frames_1;\n if (this.resampleRatio > 1) {\n frames_1 = this.downsample(data);\n }\n else {\n frames_1 = float32ToInt16(data);\n }\n this.send(frames_1);\n // 16000 per second, 1000 in 100 ms\n // save last 250 ms\n if (this.lastFramesSent.length > 1024 * 4) {\n this.lastFramesSent = frames_1;\n }\n else {\n var concat = new Int16Array(this.lastFramesSent.length + frames_1.length);\n concat.set(this.lastFramesSent);\n concat.set(frames_1, this.lastFramesSent.length);\n this.lastFramesSent = concat;\n }\n }\n }\n };\n WebsocketClient.prototype.startContext = function (appId) {\n if (this.isContextStarted) {\n console.log('Cant start context: it has been already started');\n return;\n }\n this.isContextStarted = true;\n this.isStartContextConfirmed = false;\n if (appId !== undefined) {\n this.outbox = JSON.stringify({ event: 'start', appId: appId });\n }\n else {\n this.outbox = JSON.stringify({ event: 'start' });\n }\n this.send(this.outbox);\n };\n WebsocketClient.prototype.stopContext = function () {\n if (!this.websocket) {\n throw Error('Cant start context: websocket is undefined');\n }\n if (!this.isContextStarted) {\n console.log('Cant stop context: it is not started');\n return;\n }\n this.isContextStarted = false;\n this.isStartContextConfirmed = false;\n var StopEventJSON = JSON.stringify({ event: 'stop' });\n this.send(StopEventJSON);\n };\n WebsocketClient.prototype.switchContext = function (newAppId) {\n if (!this.websocket) {\n throw Error('Cant switch context: websocket is undefined');\n }\n if (!this.isContextStarted) {\n console.log('Cant switch context: it is not started');\n return;\n }\n if (newAppId == undefined) {\n console.log('Cant switch context: new app id is undefined');\n return;\n }\n this.isStartContextConfirmed = false;\n var StopEventJSON = JSON.stringify({ event: 'stop' });\n this.send(StopEventJSON);\n this.shouldResendLastFramesSent = true;\n this.send(JSON.stringify({ event: 'start', appId: newAppId }));\n };\n WebsocketClient.prototype.closeWebsocket = function (websocketCode, reason) {\n if (websocketCode === void 0) { websocketCode = 1005; }\n if (reason === void 0) { reason = 'No Status Received'; }\n if (this.debug) {\n console.log('[SpeechlyClient]', 'Websocket closing');\n }\n if (!this.websocket) {\n throw Error('Websocket is not open');\n }\n this.websocket.close(websocketCode, reason);\n };\n WebsocketClient.prototype.downsample = function (input) {\n var inputBuffer = new Float32Array(this.buffer.length + input.length);\n inputBuffer.set(this.buffer, 0);\n inputBuffer.set(input, this.buffer.length);\n var outputLength = Math.ceil((inputBuffer.length - this.filter.length) / this.resampleRatio);\n var outputBuffer = new Int16Array(outputLength);\n for (var i = 0; i < outputLength; i++) {\n var offset = Math.round(this.resampleRatio * i);\n var val = 0.0;\n for (var j = 0; j < this.filter.length; j++) {\n val += inputBuffer[offset + j] * this.filter[j];\n }\n outputBuffer[i] = val * (val < 0 ? 0x8000 : 0x7fff);\n }\n var remainingOffset = Math.round(this.resampleRatio * outputLength);\n if (remainingOffset < inputBuffer.length) {\n this.buffer = inputBuffer.subarray(remainingOffset);\n }\n else {\n this.buffer = new Float32Array(0);\n }\n return outputBuffer;\n };\n WebsocketClient.prototype.send = function (data) {\n if (this.isOpen()) {\n try {\n this.websocket.send(data);\n }\n catch (error) {\n console.log('[SpeechlyClient]', 'Server connection error', error);\n }\n }\n };\n return WebsocketClient;\n}());\nvar ctx = self;\nvar websocketClient = new WebsocketClient(ctx);\nctx.onmessage = function (e) {\n switch (e.data.type) {\n case 'INIT':\n websocketClient.init(e.data.apiUrl, e.data.authToken, e.data.targetSampleRate, e.data.debug);\n break;\n case 'SET_SOURCE_SAMPLE_RATE':\n websocketClient.setSourceSampleRate(e.data.sourceSampleRate);\n break;\n case 'SET_SHARED_ARRAY_BUFFERS':\n websocketClient.setSharedArrayBuffers(e.data.controlSAB, e.data.dataSAB);\n break;\n case 'CLOSE':\n websocketClient.closeWebsocket(1000, 'Close requested by client');\n break;\n case 'START_CONTEXT':\n websocketClient.startContext(e.data.appId);\n break;\n case 'SWITCH_CONTEXT':\n websocketClient.switchContext(e.data.appId);\n break;\n case 'STOP_CONTEXT':\n websocketClient.stopContext();\n break;\n case 'AUDIO':\n websocketClient.sendAudio(e.data.payload);\n break;\n default:\n console.log('WORKER', e);\n }\n};\nfunction float32ToInt16(buffer) {\n var buf = new Int16Array(buffer.length);\n for (var l = 0; l < buffer.length; l++) {\n buf[l] = buffer[l] * (buffer[l] < 0 ? 0x8000 : 0x7fff);\n }\n return buf;\n}\nfunction generateFilter(sourceSampleRate, targetSampleRate, length) {\n if (length % 2 === 0) {\n throw Error('Filter length must be odd');\n }\n var cutoff = targetSampleRate / 2;\n var filter = new Float32Array(length);\n var sum = 0;\n for (var i = 0; i < length; i++) {\n var x = sinc(((2 * cutoff) / sourceSampleRate) * (i - (length - 1) / 2));\n sum += x;\n filter[i] = x;\n }\n for (var i = 0; i < length; i++) {\n filter[i] = filter[i] / sum;\n }\n return filter;\n}\nfunction sinc(x) {\n if (x === 0.0) {\n return 1.0;\n }\n var piX = Math.PI * x;\n return Math.sin(piX) / piX;\n}\n"; | ||
export default _default; |
{ | ||
"name": "@speechly/browser-client", | ||
"version": "1.5.0", | ||
"version": "2.0.0-beta.1", | ||
"description": "Browser client for Speechly API", | ||
@@ -24,13 +24,2 @@ "keywords": [ | ||
], | ||
"scripts": { | ||
"build": "pnpm run lint && pnpm run test 2>&1 && rm -rf ./dist/ && pnpm run buildworker && pnpx rollup -c --silent", | ||
"build:watch": "rm -rf ./dist/ && pnpm run buildworker && pnpx rollup -c --silent", | ||
"buildworker": "pnpx tsc ./worker/worker.ts && cat ./worker/templateOpen > ./src/websocket/worker.ts && cat ./worker/worker.js >> ./src/websocket/worker.ts && cat ./worker/templateEnd >> ./src/websocket/worker.ts", | ||
"check": "pnpm run build && pnpx api-extractor run --verbose", | ||
"docs": "rimraf docs && pnpx typedoc --readme none --excludeExternals --excludePrivate --excludeProtected --out ./docs/ --entryPointStrategy expand --sort required-first --disableSources ./src/", | ||
"getdeps": "pnpm install --force --frozen-lockfile", | ||
"lint": "pnpx eslint --cache --max-warnings 0 'src/**/*.{ts,tsx}'", | ||
"precommit": "pnpx prettier --write src/**/*.ts && pnpm run build && pnpx api-extractor run --local && pnpm run docs", | ||
"test": "pnpx jest --config ./config/jest.config.js" | ||
}, | ||
"repository": { | ||
@@ -44,3 +33,6 @@ "type": "git", | ||
}, | ||
"files": ["core/**/*", "src/**/*"], | ||
"files": [ | ||
"core/**/*", | ||
"src/**/*" | ||
], | ||
"main": "./core/speechly.umd.min.js", | ||
@@ -84,3 +76,4 @@ "module": "./core/speechly.es.js", | ||
"typedoc": "^0.22.6", | ||
"typedoc-plugin-markdown": "^3.11.3" | ||
"typedoc-plugin-markdown": "^3.11.3", | ||
"@rollup/plugin-typescript": "~8.3.2" | ||
}, | ||
@@ -92,3 +85,15 @@ "publishConfig": { | ||
"**/optimist/minimist": "0.2.1" | ||
} | ||
} | ||
}, | ||
"scripts": { | ||
"build": "pnpm run lint && pnpm run test 2>&1 && rm -rf ./dist/ && pnpm run buildworker && pnpx rollup -c --silent", | ||
"build:watch": "rm -rf ./dist/ && pnpm run buildworker && pnpx rollup -c --silent", | ||
"buildworker": "pnpx tsc ./worker/worker.ts && cat ./worker/templateOpen > ./src/websocket/worker.ts && cat ./worker/worker.js >> ./src/websocket/worker.ts && cat ./worker/templateEnd >> ./src/websocket/worker.ts", | ||
"check": "pnpm run build && pnpx api-extractor run --verbose", | ||
"docs": "rimraf docs && pnpx typedoc --readme none --excludeExternals --excludePrivate --excludeProtected --out ./docs/ --entryPointStrategy expand --sort required-first --disableSources ./src/", | ||
"getdeps": "pnpm install --force --frozen-lockfile", | ||
"lint": "pnpx eslint --cache --max-warnings 0 'src/**/*.{ts,tsx}'", | ||
"precommit": "pnpx prettier --write src/**/*.ts && pnpm run build && pnpx api-extractor run --local && pnpm run docs", | ||
"test": "pnpx jest --config ./config/jest.config.js" | ||
}, | ||
"readme": "<div align=\"center\" markdown=\"1\">\n<a href=\"https://www.speechly.com\">\n <img src=\"https://d33wubrfki0l68.cloudfront.net/f15fc952956e1952d6bd23661b7a7ee6b775faaa/c1b30/img/speechly-logo-duo-black.svg\" height=\"48\" />\n</a>\n\n### Real-time automatic speech recognition and natural language understanding tools in one flexible API\n\n[Website](https://www.speechly.com/)\n | \n[Docs](https://docs.speechly.com/)\n | \n[Discussions](https://github.com/speechly/speechly/discussions)\n | \n[Blog](https://www.speechly.com/blog/)\n | \n[Podcast](https://anchor.fm/the-speechly-podcast)\n\n---\n\n</div>\n\n# Speechly browser client\n\n![Release build](https://github.com/speechly/browser-client/workflows/Release%20build/badge.svg?branch=master&event=release)\n[![npm version](https://badge.fury.io/js/%40speechly%2Fbrowser-client.svg)](https://badge.fury.io/js/%40speechly%2Fbrowser-client)\n[![License](http://img.shields.io/:license-mit-blue.svg)](LICENSE)\n\nWith the browser-client you can add voice features to any website. It handles authentication, audio capture, network streaming and connection management with the Speechly Voice API.\n\nCheck out the [browser-client-example](https://github.com/speechly/speechly/tree/main/examples/browser-client-example) repository for a demo app built using this client.\n\nNOTE: If you are using React, you can use our [React client](https://github.com/speechly/speechly/tree/main/libraries/react-client) instead. It provides the same functionalities, but provides a programming model that is idiomatic to React.\n\n## Usage with Node\n\nInstall the package:\n\n```shell\n# Using Yarn\nyarn add @speechly/browser-client\n\n# Using NPM\nnpm install --save @speechly/browser-client\n```\n\nStart using the client:\n\n```typescript\nimport { BrowserClient, BrowserMicrophone, Segment } from '@speechly/browser-client'\n\n// Create a new client.\n// NOTE: Configure and get your appId from https://api.speechly.com/dashboard\nconst client = new BrowserClient({ appId: 'your-app-id' })\n\n// Create a microphone\nconst microphone = new BrowserMicrophone()\n// Initialize the microphone - this will ask the user for microphone permissions\n// and establish the connection to Speechly API.\n// Make sure you call `initialize` from a user action handler\n// (e.g. from a button press handler).\nawait microphone.initialize()\n\n// bind the microphone to the client\nawait client.attach(microphone.mediaStream)\n\n// React to the updates from the API.\nclient.onSegmentChange((segment: Segment) => {\n console.log('Received new segment from the API:',\n segment.intent,\n segment.entities,\n segment.words,\n segment.isFinal\n )\n})\n\n// Start recording.\n// This can be bound to e.g. a button press.\nawait client.startContext()\n\n// Stop recording after a timeout.\n// This can be bound to e.g. a button press.\nsetTimeout(async function () {\n await client.stopContext()\n}, 3000)\n```\n\n## Usage with browsers\n\nThis sample HTML loads Speechly's `browser-client` ES modules via a CDN that mirrors npm packages. The page displays a text field that you dictate text into. See browser's console log for raw segment feed from Speechly.\n\nPlease use a HTML server to view the example. Running it as a file will not work due to browser's security restrictions. For example run `serve .` on command line and open `localhost:3000` in your browser.\n\n```HTML\n<html>\n <body>\n\n <input id=\"textBox\" type=\"text\" placeholder=\"Hold to talk...\" autofocus />\n\n <script type=\"module\">\n // Load Speechly ES module from a CDN. Note script type=\"module\"\n import { BrowserClient, BrowserMicrophone } from \"../core/speechly.es.js\"\n\n const widget = document.getElementById(\"textBox\")\n\n // Create a Speechly client instance.\n // NOTE: Configure and get your appId from https://api.speechly.com/dashboard\n const speechly = new BrowserClient({\n appId: \"your-app-id\",\n debug: true,\n logSegments: true,\n })\n const microphone = new BrowserMicrophone()\n\n speechly.onSegmentChange(segment => {\n // Clean up and concatenate words\n let transcript = segment.words\n .map(w => w.value.toLowerCase())\n .join(\" \");\n // Add trailing period upon segment end.\n if (segment.isFinal) transcript += \".\";\n widget.value = transcript;\n });\n\n const startListening = async () => {\n if (microphone.mediaStream === undefined) {\n await microphone.initialize()\n speechly.attach(microphone.mediaStream)\n }\n return speechly.startContext();\n }\n\n const stopListening = async () => {\n if (speechly.isListening()) {\n return speechly.stopContext();\n }\n }\n\n // Bind start listening to a widget hold, release anywhere to stop\n widget.addEventListener(\"mousedown\", startListening)\n document.addEventListener(\"mouseup\", stopListening)\n </script>\n </body>\n\n</html>\n```\n\n## Documentation\n\nYou can find the detailed [browser-client API documentation](docs/classes/_index_d_.client.md) in the GitHub repository.\n\nYou can also refer to [Speechly Docs](https://docs.speechly.com/?utm_source=github&utm_medium=browser-client&utm_campaign=text) for more information.\n\n## Contributing\n\nSee contribution guide in [CONTRIBUTING.md](https://github.com/speechly/speechly/blob/main/CONTRIBUTING.md).\n" | ||
} |
@@ -19,2 +19,3 @@ <div align="center" markdown="1"> | ||
--- | ||
</div> | ||
@@ -49,14 +50,27 @@ | ||
```typescript | ||
import { Client, Segment } from '@speechly/browser-client' | ||
import { BrowserClient, BrowserMicrophone, Segment } from '@speechly/browser-client' | ||
// Create a new Client. NOTE: Configure and get your appId from https://api.speechly.com/dashboard | ||
const client = new Client({appId: 'your-app-id'}) | ||
// Create a new client. | ||
// NOTE: Configure and get your appId from https://api.speechly.com/dashboard | ||
const client = new BrowserClient({ appId: 'your-app-id' }) | ||
// Initialize the client - this will ask the user for microphone permissions and establish the connection to Speechly API. | ||
// Make sure you call `initlialize` from a user action handler (e.g. from a button press handler). | ||
await client.initialize() | ||
// Create a microphone | ||
const microphone = new BrowserMicrophone() | ||
// Initialize the microphone - this will ask the user for microphone permissions | ||
// and establish the connection to Speechly API. | ||
// Make sure you call `initialize` from a user action handler | ||
// (e.g. from a button press handler). | ||
await microphone.initialize() | ||
// bind the microphone to the client | ||
await client.attach(microphone.mediaStream) | ||
// React to the updates from the API. | ||
client.onSegmentChange((segment: Segment) => { | ||
console.log('Received new segment from the API:', segment.intent, segment.entities, segment.words, segment.isFinal) | ||
console.log('Received new segment from the API:', | ||
segment.intent, | ||
segment.entities, | ||
segment.words, | ||
segment.isFinal | ||
) | ||
}) | ||
@@ -70,3 +84,3 @@ | ||
// This can be bound to e.g. a button press. | ||
setTimeout(async function() { | ||
setTimeout(async function () { | ||
await client.stopContext() | ||
@@ -90,8 +104,9 @@ }, 3000) | ||
// Load Speechly ES module from a CDN. Note script type="module" | ||
import { Client } from "../core/speechly.es.js" | ||
import { BrowserClient, BrowserMicrophone } from "../core/speechly.es.js" | ||
const widget = document.getElementById("textBox") | ||
// Create a Speechly client instance. NOTE: Configure and get your appId from https://api.speechly.com/dashboard | ||
const speechly = new Client({ | ||
// Create a Speechly client instance. | ||
// NOTE: Configure and get your appId from https://api.speechly.com/dashboard | ||
const speechly = new BrowserClient({ | ||
appId: "your-app-id", | ||
@@ -101,6 +116,9 @@ debug: true, | ||
}) | ||
const microphone = new BrowserMicrophone() | ||
speechly.onSegmentChange(segment => { | ||
// Clean up and concatenate words | ||
let transcript = segment.words.map(w => w.value.toLowerCase()).filter(w => w !== "").join(" "); | ||
let transcript = segment.words | ||
.map(w => w.value.toLowerCase()) | ||
.join(" "); | ||
// Add trailing period upon segment end. | ||
@@ -111,10 +129,13 @@ if (segment.isFinal) transcript += "."; | ||
const startListening = async () => { | ||
speechly.startContext(); | ||
if (microphone.mediaStream === undefined) { | ||
await microphone.initialize() | ||
speechly.attach(microphone.mediaStream) | ||
} | ||
return speechly.startContext(); | ||
} | ||
const stopListening = () => { | ||
const stopListening = async () => { | ||
if (speechly.isListening()) { | ||
speechly.stopContext(); | ||
return speechly.stopContext(); | ||
} | ||
@@ -128,3 +149,3 @@ } | ||
</body> | ||
</html> | ||
@@ -142,2 +163,1 @@ ``` | ||
See contribution guide in [CONTRIBUTING.md](https://github.com/speechly/speechly/blob/main/CONTRIBUTING.md). | ||
export * from './speechly' | ||
export * from './microphone/types' | ||
export * from './microphone' | ||
export * from './websocket/types' | ||
export * from './storage/types' | ||
export * from './client' |
@@ -22,2 +22,5 @@ export default ` | ||
this.debug = event.data.debug; | ||
if (this.debug) { | ||
console.log('[BrowserClient AudioWorkletNode]', 'initializing audioworklet'); | ||
} | ||
this.sharedBufferSize = this.dataSAB.length; | ||
@@ -29,14 +32,14 @@ this.buffer = new Float32Array(0); | ||
_transferDataToSharedBuffer(data) { | ||
this.controlSAB[CONTROL.LOCK] = 1 | ||
let inputWriteIndex = this.controlSAB[CONTROL.WRITE_INDEX] | ||
this.controlSAB[CONTROL.LOCK] = 1; | ||
let inputWriteIndex = this.controlSAB[CONTROL.WRITE_INDEX]; | ||
if (this.controlSAB[CONTROL.FRAMES_AVAILABLE] > 0) { | ||
if (inputWriteIndex + data.length > this.sharedBufferSize) { | ||
// console.log('buffer overflow') | ||
inputWriteIndex = 0 | ||
inputWriteIndex = 0; | ||
} | ||
} | ||
this.dataSAB.set(data, inputWriteIndex) | ||
this.controlSAB[CONTROL.WRITE_INDEX] = inputWriteIndex + data.length | ||
this.controlSAB[CONTROL.FRAMES_AVAILABLE] = inputWriteIndex + data.length | ||
this.controlSAB[CONTROL.LOCK] = 0 | ||
this.dataSAB.set(data, inputWriteIndex); | ||
this.controlSAB[CONTROL.WRITE_INDEX] = inputWriteIndex + data.length; | ||
this.controlSAB[CONTROL.FRAMES_AVAILABLE] = inputWriteIndex + data.length; | ||
this.controlSAB[CONTROL.LOCK] = 0; | ||
} | ||
@@ -49,3 +52,4 @@ | ||
type: 'STATS', | ||
signalEnergy: signalEnergy | ||
signalEnergy: signalEnergy, | ||
samples: data.length, | ||
}); | ||
@@ -55,10 +59,10 @@ } | ||
if (this.buffer.length > this.sharedBufferSize) { | ||
const dataToTransfer = this.buffer.subarray(0, this.sharedBufferSize) | ||
this._transferDataToSharedBuffer(dataToTransfer) | ||
this.buffer = this.buffer.subarray(this.sharedBufferSize) | ||
const dataToTransfer = this.buffer.subarray(0, this.sharedBufferSize); | ||
this._transferDataToSharedBuffer(dataToTransfer); | ||
this.buffer = this.buffer.subarray(this.sharedBufferSize); | ||
} | ||
let concat = new Float32Array(this.buffer.length + data.length) | ||
concat.set(this.buffer) | ||
concat.set(data, this.buffer.length) | ||
this.buffer = concat | ||
let concat = new Float32Array(this.buffer.length + data.length); | ||
concat.set(this.buffer); | ||
concat.set(data, this.buffer.length); | ||
this.buffer = concat; | ||
} | ||
@@ -68,14 +72,14 @@ | ||
const inputChannelData = inputs[0][0]; | ||
if (inputChannelData !== undefined) { | ||
if (this.controlSAB && this.dataSAB) { | ||
this._pushData(inputChannelData); | ||
} else { | ||
this.port.postMessage({ | ||
type: 'DATA', | ||
frames: inputChannelData | ||
}); | ||
} | ||
if (inputChannelData !== undefined) { | ||
if (this.controlSAB && this.dataSAB) { | ||
this._pushData(inputChannelData); | ||
} else { | ||
this.port.postMessage({ | ||
type: 'DATA', | ||
frames: inputChannelData | ||
}); | ||
} | ||
return true; | ||
} | ||
return true; | ||
} | ||
@@ -82,0 +86,0 @@ } |
@@ -1,41 +0,41 @@ | ||
import { ErrDeviceNotSupported, ErrNoAudioConsent, ErrNotInitialized, Microphone } from './types' | ||
import { APIClient } from '../websocket' | ||
import audioworklet from './audioworklet' | ||
import { ErrDeviceNotSupported, DefaultSampleRate } from '../speechly' | ||
import { ErrNoAudioConsent, ErrNotInitialized } from './types' | ||
const audioProcessEvent = 'audioprocess' | ||
const baseBufferSize = 4096 | ||
export class BrowserMicrophone implements Microphone { | ||
private readonly debug: boolean | ||
private readonly isWebkit: boolean | ||
private readonly apiClient: APIClient | ||
private readonly sampleRate: number | ||
/** | ||
* Gets browser based microphone using the window.navigator.mediaDevices interface. | ||
* The exposed `mediaStream` can be attached to a `BrowserClient` instance. | ||
* @public | ||
*/ | ||
export class BrowserMicrophone { | ||
private muted: boolean = false | ||
private initialized: boolean = false | ||
private muted: boolean = false | ||
private audioContext?: AudioContext | ||
private resampleRatio?: number | ||
private readonly nativeResamplingSupported: boolean | ||
private readonly autoGainControl: boolean | ||
// The media stream and audio track are initialized during `initialize()` call. | ||
private audioTrack?: MediaStreamTrack | ||
private mediaStream?: MediaStream | ||
mediaStream?: MediaStream | ||
// Audio processing functionality is initialized lazily during first `unmute()` call. | ||
// The reason for that is that it's required for user to first interact with the page, | ||
// before it can capture or play audio and video, for privacy and user experience reasons. | ||
private audioProcessor?: ScriptProcessorNode | ||
private stats = { | ||
maxSignalEnergy: 0.0, | ||
constructor() { | ||
try { | ||
const constraints = window.navigator.mediaDevices.getSupportedConstraints() | ||
this.nativeResamplingSupported = constraints.sampleRate === true | ||
this.autoGainControl = constraints.autoGainControl === true | ||
} catch { | ||
this.nativeResamplingSupported = false | ||
this.autoGainControl = false | ||
} | ||
} | ||
constructor(isWebkit: boolean, sampleRate: number, apiClient: APIClient, debug: boolean = false) { | ||
this.isWebkit = isWebkit | ||
this.apiClient = apiClient | ||
this.sampleRate = sampleRate | ||
this.debug = debug | ||
} | ||
/** | ||
* Initializes the microphone. Needs to happen after a user interaction in the view. | ||
* The reason for that is that it's required for user to first interact with the page, | ||
* before it can capture or play audio and video, for privacy and user experience reasons. | ||
*/ | ||
async initialize(): Promise<void> { | ||
if (this.initialized) { | ||
return | ||
} | ||
async initialize(audioContext: AudioContext, mediaStreamConstraints: MediaStreamConstraints): Promise<void> { | ||
// ensure mediaDevices are available | ||
if (window.navigator?.mediaDevices === undefined) { | ||
@@ -45,95 +45,30 @@ throw ErrDeviceNotSupported | ||
this.audioContext = audioContext | ||
this.resampleRatio = this.audioContext.sampleRate / this.sampleRate | ||
try { | ||
this.mediaStream = await window.navigator.mediaDevices.getUserMedia(mediaStreamConstraints) | ||
} catch { | ||
throw ErrNoAudioConsent | ||
const mediaStreamConstraints: MediaStreamConstraints = { | ||
video: false, | ||
} | ||
this.audioTrack = this.mediaStream.getAudioTracks()[0] | ||
// Start audio context if we are dealing with a non-WebKit browser. | ||
// | ||
// Non-webkit browsers (currently only Chrome on Android) | ||
// require that user media is obtained before resuming the audio context. | ||
// | ||
// If audio context is attempted to be resumed before `mediaDevices.getUserMedia`, | ||
// `audioContext.resume()` will hang indefinitely, without being resolved or rejected. | ||
if (!this.isWebkit) { | ||
await this.audioContext.resume() | ||
} | ||
if (window.AudioWorkletNode !== undefined) { | ||
const blob = new Blob([audioworklet], { type: 'text/javascript' }) | ||
const blobURL = window.URL.createObjectURL(blob) | ||
await this.audioContext.audioWorklet.addModule(blobURL) | ||
const speechlyNode = new AudioWorkletNode(this.audioContext, 'speechly-worklet') | ||
this.audioContext.createMediaStreamSource(this.mediaStream).connect(speechlyNode) | ||
speechlyNode.connect(this.audioContext.destination) | ||
// @ts-ignore | ||
if (window.SharedArrayBuffer !== undefined) { | ||
// Chrome, Edge, Firefox, Firefox Android | ||
if (this.nativeResamplingSupported || this.autoGainControl) { | ||
mediaStreamConstraints.audio = { | ||
sampleRate: DefaultSampleRate, | ||
// @ts-ignore | ||
const controlSAB = new window.SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT) | ||
// @ts-ignore | ||
const dataSAB = new window.SharedArrayBuffer(1024 * Float32Array.BYTES_PER_ELEMENT) | ||
this.apiClient.postMessage({ | ||
type: 'SET_SHARED_ARRAY_BUFFERS', | ||
controlSAB, | ||
dataSAB, | ||
}) | ||
speechlyNode.port.postMessage({ | ||
type: 'SET_SHARED_ARRAY_BUFFERS', | ||
controlSAB, | ||
dataSAB, | ||
debug: this.debug, | ||
}) | ||
} else { | ||
// Opera, Chrome Android, Webview Anroid | ||
if (this.debug) { | ||
console.log('[SpeechlyClient]', 'can not use SharedArrayBuffer') | ||
} | ||
autoGainControl: this.autoGainControl, | ||
} | ||
speechlyNode.port.onmessage = (event: MessageEvent) => { | ||
switch (event.data.type) { | ||
case 'STATS': | ||
if (event.data.signalEnergy > this.stats.maxSignalEnergy) { | ||
this.stats.maxSignalEnergy = event.data.signalEnergy | ||
} | ||
break | ||
case 'DATA': | ||
this.handleAudio(event.data.frames) | ||
break | ||
default: | ||
} | ||
} | ||
} else { | ||
if (this.debug) { | ||
console.log('[SpeechlyClient]', 'can not use AudioWorkletNode') | ||
} | ||
// Safari, iOS Safari and Internet Explorer | ||
if (this.isWebkit) { | ||
// Multiply base buffer size of 4 kB by the resample ratio rounded up to the next power of 2. | ||
// i.e. for 48 kHz to 16 kHz downsampling, this will be 4096 (base) * 4 = 16384. | ||
const bufSize = baseBufferSize * Math.pow(2, Math.ceil(Math.log(this.resampleRatio) / Math.log(2))) | ||
this.audioProcessor = this.audioContext.createScriptProcessor(bufSize, 1, 1) | ||
} else { | ||
this.audioProcessor = this.audioContext.createScriptProcessor(undefined, 1, 1) | ||
} | ||
this.audioContext.createMediaStreamSource(this.mediaStream).connect(this.audioProcessor) | ||
this.audioProcessor.connect(this.audioContext.destination) | ||
this.audioProcessor.addEventListener(audioProcessEvent, (event: AudioProcessingEvent) => { | ||
this.handleAudio(event.inputBuffer.getChannelData(0)) | ||
}) | ||
mediaStreamConstraints.audio = true | ||
} | ||
try { | ||
this.mediaStream = await window.navigator.mediaDevices.getUserMedia(mediaStreamConstraints) | ||
} catch (err) { | ||
console.error(err) | ||
throw ErrNoAudioConsent | ||
} | ||
this.initialized = true | ||
this.mute() | ||
this.muted = true | ||
} | ||
/** | ||
* Closes the microphone, releases all resources and stops the Speechly client. | ||
*/ | ||
async close(): Promise<void> { | ||
this.mute() | ||
if (!this.initialized) { | ||
@@ -143,4 +78,3 @@ throw ErrNotInitialized | ||
const t = this.audioTrack as MediaStreamTrack | ||
t.enabled = false | ||
this.muted = true | ||
@@ -151,48 +85,13 @@ // Stop all media tracks | ||
// Disconnect and stop ScriptProcessorNode | ||
if (this.audioProcessor != null) { | ||
const proc = this.audioProcessor | ||
proc.disconnect() | ||
} | ||
// Unset all audio infrastructure | ||
this.mediaStream = undefined | ||
this.audioTrack = undefined | ||
this.audioProcessor = undefined | ||
this.initialized = false | ||
} | ||
mute(): void { | ||
this.muted = true | ||
} | ||
unmute(): void { | ||
this.muted = false | ||
} | ||
private readonly handleAudio = (array: Float32Array): void => { | ||
if (this.muted) { | ||
return | ||
} | ||
if (array.length > 0) { | ||
this.apiClient.sendAudio(array) | ||
} | ||
} | ||
/** | ||
* print statistics to console | ||
* @returns true if microphone is open | ||
*/ | ||
public printStats(): void { | ||
if (this.audioTrack != null) { | ||
const settings: MediaTrackSettings = this.audioTrack.getSettings() | ||
console.log(this.audioTrack.label, this.audioTrack.readyState) | ||
// @ts-ignore | ||
console.log('channelCount', settings.channelCount) | ||
// @ts-ignore | ||
console.log('latency', settings.latency) | ||
// @ts-ignore | ||
console.log('autoGainControl', settings.autoGainControl) | ||
} | ||
console.log('maxSignalEnergy', this.stats.maxSignalEnergy) | ||
public isRecording(): boolean { | ||
return !this.muted | ||
} | ||
} |
/** | ||
* Default sample rate for microphone streams. | ||
* @public | ||
*/ | ||
export const DefaultSampleRate = 16000 | ||
/** | ||
* Error to be thrown when the microphone was accessed before it was initialized. | ||
@@ -20,62 +14,5 @@ * @public | ||
/** | ||
* Error to be thrown when the device does not support the Microphone instance's target audio APIs. | ||
* @public | ||
*/ | ||
export const ErrDeviceNotSupported = new Error('Current device does not support microphone API') | ||
/** | ||
* Error to be thrown when user did not give consent to the application to record audio. | ||
* @public | ||
*/ | ||
export const ErrNoAudioConsent = new Error('Microphone consent is no given') | ||
/** | ||
* Error to be thrown when user tries to change appId without project login. | ||
* @public | ||
*/ | ||
export const ErrAppIdChangeWithoutProjectLogin = new Error('AppId changed without project login') | ||
/** | ||
* A callback that receives an ArrayBuffer representing a frame of audio. | ||
* @public | ||
*/ | ||
export type AudioCallback = (audioBuffer: Int16Array) => void | ||
/** | ||
* The interface for a microphone. | ||
* @public | ||
*/ | ||
export interface Microphone { | ||
/** | ||
* Initialises the microphone. | ||
* | ||
* This should prepare the microphone infrastructure for receiving audio chunks, | ||
* but the microphone should remain muted after the call. | ||
* This method will be called by the Client as part of client initialisation process. | ||
*/ | ||
initialize(audioContext: AudioContext, mediaStreamConstraints: MediaStreamConstraints): Promise<void> | ||
/** | ||
* Closes the microphone, tearing down all the infrastructure. | ||
* | ||
* The microphone should stop emitting audio after this is called. | ||
* Calling `initialize` again after calling `close` should succeed and make microphone ready to use again. | ||
* This method will be called by the Client as part of client closure process. | ||
*/ | ||
close(): Promise<void> | ||
/** | ||
* Mutes the microphone. If the microphone is muted, the `onAudio` callbacks should not be called. | ||
*/ | ||
mute(): void | ||
/** | ||
* Unmutes the microphone. | ||
*/ | ||
unmute(): void | ||
/** | ||
* Print usage stats to console in debug mode. | ||
*/ | ||
printStats(): void | ||
} | ||
export const ErrNoAudioConsent = new Error('Microphone consent is not given') |
export * from './types' | ||
export { stateToString } from './state' | ||
export { Client } from './client' | ||
export * from './segment' |
import { Word, Entity, Intent, Segment } from './types' | ||
/** | ||
* Contains the internal state of a `Segment`, with methods to update it based on new events. | ||
* @public | ||
*/ | ||
export class SegmentState { | ||
@@ -4,0 +8,0 @@ id: number |
@@ -1,147 +0,20 @@ | ||
import { Microphone } from '../microphone' | ||
import { Storage } from '../storage' | ||
import { APIClient } from '../websocket' | ||
/** | ||
* The options which can be used to configure the client. | ||
* Error to be thrown when the device does not support audioContext. | ||
* @public | ||
*/ | ||
export interface ClientOptions { | ||
/** | ||
* The unique identifier of an app in the dashboard. | ||
*/ | ||
appId?: string | ||
export const ErrDeviceNotSupported = new Error('Current device does not support microphone API') | ||
/** | ||
* Connect to Speechly upon creating the client instance. Defaults to true. | ||
*/ | ||
connect?: boolean | ||
/** | ||
* The unique identifier of a project in the dashboard. | ||
*/ | ||
projectId?: string | ||
/** | ||
* @deprecated | ||
* The language which is used by the app. | ||
*/ | ||
language?: string | ||
/** | ||
* The URL of Speechly login endpoint. | ||
*/ | ||
loginUrl?: string | ||
/** | ||
* The URL of Speechly SLU API endpoint. | ||
*/ | ||
apiUrl?: string | ||
/** | ||
* The sample rate of the audio to use. | ||
*/ | ||
sampleRate?: number | ||
/** | ||
* Whether to output debug statements to the console. | ||
*/ | ||
debug?: boolean | ||
/** | ||
* Whether to use auto gain control. | ||
* True by default. | ||
*/ | ||
autoGainControl?: boolean | ||
/** | ||
* Whether to output updated segments to the console. | ||
*/ | ||
logSegments?: boolean | ||
/** | ||
* Custom microphone implementation. | ||
* If not provided, an implementation based on getUserMedia and Web Audio API is used. | ||
*/ | ||
microphone?: Microphone | ||
/** | ||
* Custom API client implementation. | ||
* If not provided, an implementation based on Speechly SLU WebSocket API is used. | ||
*/ | ||
apiClient?: APIClient | ||
/** | ||
* Custom storage implementation. | ||
* If not provided, browser's LocalStorage API is used. | ||
*/ | ||
storage?: Storage | ||
} | ||
/** | ||
* A callback that is invoked whenever the {@link ClientState | client state} changes. | ||
* Error to be thrown when user tries to change appId without project login. | ||
* @public | ||
*/ | ||
export type StateChangeCallback = (state: ClientState) => void | ||
export const ErrAppIdChangeWithoutProjectLogin = new Error('AppId changed without project login') | ||
/** | ||
* A callback that is invoked whenever current {@link Segment | segment} changes. | ||
* Default sample rate for microphone streams. | ||
* @public | ||
*/ | ||
export type SegmentChangeCallback = (segment: Segment) => void | ||
export const DefaultSampleRate = 16000 | ||
/** | ||
* A callback that is invoked whenever a new tentative transcript is received from the API. | ||
* @public | ||
*/ | ||
export type TentativeTranscriptCallback = (contextId: string, segmentId: number, words: Word[], text: string) => void | ||
/** | ||
* A callback that is invoked whenever a new transcript is received from the API. | ||
* @public | ||
*/ | ||
export type TranscriptCallback = (contextId: string, segmentId: number, word: Word) => void | ||
/** | ||
* A callback that is invoked whenever new tentative entities are received from the API. | ||
* @public | ||
*/ | ||
export type TentativeEntitiesCallback = (contextId: string, segmentId: number, entities: Entity[]) => void | ||
/** | ||
* A callback that is invoked whenever new entity is received from the API. | ||
* @public | ||
*/ | ||
export type EntityCallback = (contextId: string, segmentId: number, entity: Entity) => void | ||
/** | ||
* A callback that is invoked whenever new intent (tentative or not) is received from the API. | ||
* @public | ||
*/ | ||
export type IntentCallback = (contextId: string, segmentId: number, intent: Intent) => void | ||
/** | ||
* All possible states of a Speechly API client. Failed, NoBrowserSupport and NoAudioConsent states are non-recoverable | ||
* erroneous states, which should be handled by the end user, according to the semantics of an application. | ||
* Other states can also be utilized for e.g. enabling and disabling recording buttons or showing the status in the app. | ||
* It is also possible to use arithmetics for state comparison, e.g. `if (state < speechly.ClientState.Disconnected)`, | ||
* to react to non-recoverable states. | ||
* @public | ||
*/ | ||
export enum ClientState { | ||
Failed = 0, | ||
NoBrowserSupport, | ||
NoAudioConsent, | ||
__UnrecoverableErrors, | ||
Disconnected, | ||
Disconnecting, | ||
Connecting, | ||
Preinitialized, | ||
Initializing, | ||
Connected, | ||
Stopping, | ||
Starting, | ||
Recording, | ||
} | ||
/** | ||
* The smallest component of SLU API, defined by an intent. | ||
@@ -148,0 +21,0 @@ * @public |
@@ -38,3 +38,3 @@ /** | ||
Closed = 'WEBSOCKET_CLOSED', | ||
SourceSampleRateSetSuccess = 'SOURSE_SAMPLE_RATE_SET_SUCCESS', | ||
SourceSampleRateSetSuccess = 'SOURCE_SAMPLE_RATE_SET_SUCCESS', | ||
Started = 'started', | ||
@@ -153,3 +153,4 @@ Stopped = 'stopped', | ||
*/ | ||
export type CloseCallback = (err: {code: number, reason: string, wasClean: boolean}) => void | ||
// eslint-disable-next-line @typescript-eslint/member-delimiter-style | ||
export type CloseCallback = (err: { code: number; reason: string; wasClean: boolean }) => void | ||
@@ -156,0 +157,0 @@ /** |
@@ -51,3 +51,3 @@ import { APIClient, ResponseCallback, CloseCallback, WebsocketResponse, WebsocketResponseType } from './types' | ||
this.worker.postMessage({ | ||
type: 'SET_SOURSE_SAMPLE_RATE', | ||
type: 'SET_SOURCE_SAMPLE_RATE', | ||
sourceSampleRate, | ||
@@ -54,0 +54,0 @@ }) |
@@ -8,3 +8,3 @@ export default `/** | ||
WebsocketResponseType["Opened"] = "WEBSOCKET_OPEN"; | ||
WebsocketResponseType["SourceSampleRateSetSuccess"] = "SOURSE_SAMPLE_RATE_SET_SUCCESS"; | ||
WebsocketResponseType["SourceSampleRateSetSuccess"] = "SOURCE_SAMPLE_RATE_SET_SUCCESS"; | ||
WebsocketResponseType["Started"] = "started"; | ||
@@ -41,3 +41,8 @@ WebsocketResponseType["Stopped"] = "stopped"; | ||
_this.websocket = undefined; | ||
_this.workerCtx.postMessage({ type: 'WEBSOCKET_CLOSED', code: event.code, reason: event.reason, wasClean: event.wasClean }); | ||
_this.workerCtx.postMessage({ | ||
type: 'WEBSOCKET_CLOSED', | ||
code: event.code, | ||
reason: event.reason, | ||
wasClean: event.wasClean | ||
}); | ||
}; | ||
@@ -99,3 +104,3 @@ this.onWebsocketOpen = function (_event) { | ||
} | ||
this.workerCtx.postMessage({ type: 'SOURSE_SAMPLE_RATE_SET_SUCCESS' }); | ||
this.workerCtx.postMessage({ type: 'SOURCE_SAMPLE_RATE_SET_SUCCESS' }); | ||
if (isNaN(this.resampleRatio)) { | ||
@@ -240,3 +245,3 @@ throw Error("resampleRatio is NaN source rate is ".concat(this.sourceSampleRate, " and target rate is ").concat(this.targetSampleRate)); | ||
if (websocketCode === void 0) { websocketCode = 1005; } | ||
if (reason === void 0) { reason = "No Status Received"; } | ||
if (reason === void 0) { reason = 'No Status Received'; } | ||
if (this.debug) { | ||
@@ -292,3 +297,3 @@ console.log('[SpeechlyClient]', 'Websocket closing'); | ||
break; | ||
case 'SET_SOURSE_SAMPLE_RATE': | ||
case 'SET_SOURCE_SAMPLE_RATE': | ||
websocketClient.setSourceSampleRate(e.data.sourceSampleRate); | ||
@@ -300,3 +305,3 @@ break; | ||
case 'CLOSE': | ||
websocketClient.closeWebsocket(1000, "Close requested by client"); | ||
websocketClient.closeWebsocket(1000, 'Close requested by client'); | ||
break; | ||
@@ -303,0 +308,0 @@ case 'START_CONTEXT': |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
385433
63
5846
156
30
2