1
0
mirror of https://github.com/Picovoice/porcupine.git synced 2022-01-28 03:27:53 +03:00

add indexedDB to web binding (#612)

This commit is contained in:
Kwangsoo Yeo
2022-01-05 12:55:16 -08:00
committed by GitHub
parent 10dfa18c9f
commit 4c3ee8c06d
6 changed files with 115 additions and 253 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "$",
"version": "2.0.2",
"version": "2.0.3",
"description": "Porcupine library for web browsers (via WebAssembly)",
"author": "Picovoice Inc",
"license": "Apache-2.0",

View File

@@ -31,7 +31,7 @@ import {
arrayBufferToStringAtIndex,
base64ToUint8Array,
fetchWithTimeout,
getRuntimeEnvironment,
getPvStorage,
isAccessKeyValid,
stringHeaderToObject,
} from './utils';
@@ -39,8 +39,6 @@ import {
const DEFAULT_SENSITIVITY = 0.5;
const PV_STATUS_SUCCESS = 10000;
type EmptyPromise = (value: void) => void;
/**
* JavaScript/WebAssembly Binding for the Picovoice Porcupine wake word engine.
*
@@ -83,8 +81,6 @@ export class Porcupine implements PorcupineEngine {
private static _sampleRate: number;
private static _version: string;
private static _resolvePromise: EmptyPromise | null;
private static _rejectPromise: EmptyPromise | null;
private static _porcupineMutex = new Mutex;
private constructor(handleWasm: PorcupineWasmOutput, keywordLabels: ArrayLike<string>) {
@@ -313,25 +309,6 @@ export class Porcupine implements PorcupineEngine {
return returnPromise;
}
public static clearFilePromises(): void {
Porcupine._rejectPromise = null;
Porcupine._resolvePromise = null;
}
// eslint-disable-next-line
public static resolveFilePromise(args: any): void {
if (Porcupine._resolvePromise != null) {
Porcupine._resolvePromise(args);
}
}
// eslint-disable-next-line
public static rejectFilePromise(args: any): void {
if (Porcupine._rejectPromise != null) {
Porcupine._rejectPromise(args);
}
}
private static async initWasm(
accessKey: string,
keywordModels: ArrayLike<Uint8Array>,
@@ -343,73 +320,13 @@ export class Porcupine implements PorcupineEngine {
const memoryBufferInt32 = new Int32Array(memory.buffer);
const memoryBufferFloat32 = new Float32Array(memory.buffer);
const storage = getPvStorage();
const pvConsoleLogWasm = function (index: number): void {
// eslint-disable-next-line no-console
console.log(arrayBufferToStringAtIndex(memoryBufferUint8, index));
};
const pvFileOperationHelper = function (args: any): Promise<any> {
let promise: any;
const runtimeEnvironment = getRuntimeEnvironment();
if (runtimeEnvironment === 'worker') {
promise = new Promise((resolve, reject) => {
Porcupine._resolvePromise = resolve;
Porcupine._rejectPromise = reject;
});
self.postMessage(
{
command: args.command,
path: args.path,
content: args.content,
},
// @ts-ignore
undefined
);
} else if (runtimeEnvironment === 'browser') {
promise = new Promise<string>((resolve, reject) => {
try {
switch (args.command) {
case 'file-save':
localStorage.setItem(args.path, args.content);
resolve('saved');
break;
case 'file-exists':
{
const content = localStorage.getItem(args.path);
resolve(content as string);
}
break;
case 'file-load':
{
const content = localStorage.getItem(args.path);
if (content === null) {
reject(`${args.path} does not exist`);
} else {
resolve(content as string);
}
}
break;
case 'file-delete':
localStorage.removeItem(args.path);
resolve('deleted');
break;
default:
// eslint-disable-next-line no-console
console.warn(`Unexpected command: ${args.command}`);
reject();
}
} catch (error) {
reject();
}
});
} else {
// eslint-disable-next-line no-console
console.error(`Unexpected environment: ${runtimeEnvironment}`);
return Promise.reject();
}
return promise;
};
const pvAssertWasm = function (
expr: number,
line: number,
@@ -520,10 +437,7 @@ export class Porcupine implements PorcupineEngine {
): Promise<void> {
const path = arrayBufferToStringAtIndex(memoryBufferUint8, pathAddress);
try {
const contentBase64 = await pvFileOperationHelper({
command: 'file-load',
path: path,
});
const contentBase64 = await storage.getItem(path);
const contentBuffer = base64ToUint8Array(contentBase64);
// eslint-disable-next-line
const contentAddress = await aligned_alloc(
@@ -565,11 +479,7 @@ export class Porcupine implements PorcupineEngine {
contentAddress
);
try {
await pvFileOperationHelper({
command: 'file-save',
path: path,
content: content,
});
await storage.setItem(path, content);
memoryBufferInt32[
succeededAddress / Int32Array.BYTES_PER_ELEMENT
] = 1;
@@ -588,11 +498,8 @@ export class Porcupine implements PorcupineEngine {
const path = arrayBufferToStringAtIndex(memoryBufferUint8, pathAddress);
try {
const isExists = await pvFileOperationHelper({
command: 'file-exists',
path: path,
});
memoryBufferUint8[isExistsAddress] = isExists === null ? 0 : 1;
const isExists = await storage.getItem(path);
memoryBufferUint8[isExistsAddress] = (isExists === undefined || isExists === null) ? 0 : 1;
memoryBufferInt32[
succeededAddress / Int32Array.BYTES_PER_ELEMENT
] = 1;
@@ -609,10 +516,7 @@ export class Porcupine implements PorcupineEngine {
): Promise<void> {
const path = arrayBufferToStringAtIndex(memoryBufferUint8, pathAddress);
try {
await pvFileOperationHelper({
command: 'file-delete',
path: path,
});
await storage.removeItem(path);
memoryBufferInt32[
succeededAddress / Int32Array.BYTES_PER_ELEMENT
] = 1;
@@ -699,6 +603,7 @@ export class Porcupine implements PorcupineEngine {
wasmCodeArray,
importObject
);
const aligned_alloc = instance.exports.aligned_alloc as CallableFunction;
const pv_porcupine_version = instance.exports
.pv_porcupine_version as CallableFunction;
@@ -716,6 +621,7 @@ export class Porcupine implements PorcupineEngine {
Int32Array.BYTES_PER_ELEMENT,
Int32Array.BYTES_PER_ELEMENT
);
if (keywordIndexAddress === 0) {
throw new Error('malloc failed: Cannot allocate memory');
}

View File

@@ -43,25 +43,10 @@ export type PorcupineWorkerRequestInit = {
start?: boolean;
};
export type PorcupineWorkerRequestFileOperation = {
command:
| 'file-save-succeeded'
| 'file-save-failed'
| 'file-load-succeeded'
| 'file-load-failed'
| 'file-exists-succeeded'
| 'file-exists-failed'
| 'file-delete-succeeded'
| 'file-delete-failed';
message: string;
content?: string;
};
export type PorcupineWorkerRequest =
| PorcupineWorkerRequestInit
| PorcupineWorkerRequestProcess
| PorcupineWorkerRequestVoid
| PorcupineWorkerRequestFileOperation;
| PorcupineWorkerRequestVoid;
export type PorcupineWorkerResponseReady = {
command: 'ppn-ready';
@@ -82,18 +67,11 @@ export type PorcupineWorkerResponseError = {
message: string;
};
export type PorcupineWorkerResponseFileOperation = {
command: 'file-save' | 'file-load' | 'file-exists' | 'file-delete';
path: string;
content?: string;
};
export type PorcupineWorkerResponse =
| PorcupineWorkerResponseReady
| PorcupineWorkerResponseFailed
| PorcupineWorkerResponseKeyword
| PorcupineWorkerResponseError
| PorcupineWorkerResponseFileOperation;
| PorcupineWorkerResponseError;
export interface PorcupineEngine {
/** Release all resources acquired by Porcupine */

View File

@@ -78,55 +78,23 @@ onmessage = function (
event: MessageEvent<PorcupineWorkerRequest>
): void {
switch (event.data.command) {
case 'file-save-succeeded':
Porcupine.resolveFilePromise(event.data.message);
Porcupine.clearFilePromises();
case 'init':
init(event.data.accessKey, event.data.keywords, event.data.start);
break;
case 'file-save-failed':
Porcupine.rejectFilePromise(event.data.message);
Porcupine.clearFilePromises();
case 'process':
process(event.data.inputFrame);
break;
case 'file-load-succeeded':
Porcupine.resolveFilePromise(event.data.content);
Porcupine.clearFilePromises();
case 'pause':
paused = true;
break;
case 'file-load-failed':
Porcupine.rejectFilePromise(event.data.message);
Porcupine.clearFilePromises();
case 'resume':
paused = false;
break;
case 'file-exists-succeeded':
Porcupine.resolveFilePromise(event.data.content);
Porcupine.clearFilePromises();
case 'release':
release();
break;
case 'file-exists-failed':
Porcupine.rejectFilePromise(event.data.message);
Porcupine.clearFilePromises();
break;
case 'file-delete-succeeded':
Porcupine.resolveFilePromise(event.data.message);
Porcupine.clearFilePromises();
break;
case 'file-delete-failed':
Porcupine.rejectFilePromise(event.data.message);
Porcupine.clearFilePromises();
break;
case 'init':
init(event.data.accessKey, event.data.keywords,event.data.start);
break;
case 'process':
process(event.data.inputFrame);
break;
case 'pause':
paused = true;
break;
case 'resume':
paused = false;
break;
case 'release':
release();
break;
default:
// eslint-disable-next-line no-console
console.warn('Unhandled command in porcupine_worker: ' + event.data.command);
default:
// eslint-disable-next-line no-console
console.warn('Unhandled command in porcupine_worker: ' + event.data.command);
}
};

View File

@@ -76,65 +76,6 @@ export default class PorcupineWorkerFactory {
processErrorCallback(event.data.message);
}
break;
case 'file-save':
try {
localStorage.setItem(event.data.path, event.data.content || '');
porcupineWorker.postMessage({
command: 'file-save-succeeded',
message: `Saved ${event.data.path} successfully`,
});
} catch (error) {
porcupineWorker.postMessage({
command: 'file-save-failed',
message: `${error}`,
});
}
break;
case 'file-load':
try {
const content = localStorage.getItem(event.data.path);
if (content === null) {
throw new Error('file does not exist.');
}
porcupineWorker.postMessage({
command: 'file-load-succeeded',
content: content,
});
} catch (error) {
porcupineWorker.postMessage({
command: 'file-load-failed',
message: `${error}`,
});
}
break;
case 'file-exists':
try {
const content = localStorage.getItem(event.data.path);
porcupineWorker.postMessage({
command: 'file-exists-succeeded',
content: content,
});
} catch (error) {
porcupineWorker.postMessage({
command: 'file-exists-failed',
message: `${error}`,
});
}
break;
case 'file-delete':
try {
localStorage.removeItem(event.data.path);
porcupineWorker.postMessage({
command: 'file-delete-succeeded',
message: `Deleted ${event.data.path} successfully`,
});
} catch (error) {
porcupineWorker.postMessage({
command: 'file-delete-failed',
message: `${error}`,
});
}
break;
default:
// eslint-disable-next-line no-console
console.warn(`Unhandled message in main.js: ${event.data}`);

View File

@@ -1,14 +1,96 @@
/*
Copyright 2021 Picovoice Inc.
Copyright 2021 Picovoice Inc.
You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE"
file accompanying this source.
You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE"
file accompanying this source.
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
*/
/**
* Indexed DB configurations
*/
const DB_NAME = 'pv_db';
const STORE_NAME = 'pv_store';
const V = 1;
/**
* Storage Interface.
*/
interface PvStorage {
setItem: (key: string, value: string) => void | Promise<void>;
getItem: (key: string) => string | Promise<string>;
removeItem: (key: string) => void | Promise<void>;
}
/**
* Opens indexedDB connection, handles version changes and gets the db instance.
*
* @returns The instance of indexedDB connection.
*/
function getDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = self.indexedDB.open(DB_NAME, V);
request.onerror = () => {
reject(request.error);
};
request.onsuccess = () => {
resolve(request.result);
};
request.onupgradeneeded = () => {
request.result.createObjectStore(STORE_NAME);
};
});
}
/**
* Gets the storage to use. Either tries to use IndexedDB or localStorage.
*
* @returns PvStorage instance to use as storage.
*/
export function getPvStorage(): PvStorage {
if (self.indexedDB) {
const requestHelper = (request: IDBRequest): Promise<any> => {
return new Promise((resolve, reject) => {
request.onerror = () => {
reject(request.error);
};
request.onsuccess = () => {
resolve(request.result);
};
});
}
return {
setItem: async (key: string, value: string) => {
const db = await getDB();
const request = db.transaction(STORE_NAME, 'readwrite').objectStore(STORE_NAME).put(value, key);
await requestHelper(request);
db.close();
},
getItem: async (key: string) => {
const db = await getDB();
const request = db.transaction(STORE_NAME, 'readonly').objectStore(STORE_NAME).get(key);
const res = await requestHelper(request);
db.close();
return res;
},
removeItem: async (key: string) => {
const db = await getDB();
const request = db.transaction(STORE_NAME, 'readwrite').objectStore(STORE_NAME).delete(key);
await requestHelper(request);
db.close();
}
}
} else if (self.localStorage) {
return self.localStorage as PvStorage;
}
throw new Error("Cannot get a presistent storage object.");
}
/**
* Convert a null terminated phrase stored inside an array buffer to a string
*
@@ -103,19 +185,6 @@ export async function fetchWithTimeout(uri: string, options = {}, time = 5000):
return response;
}
/**
* Environment identifier
*
* @return a string containing the envirorment name
*/
export function getRuntimeEnvironment(): string {
if (typeof window === 'object' && typeof document === 'object') {
return 'browser';
}
return 'worker';
}
/**
* Checking whether the given AccessKey is valid
*
@@ -133,4 +202,4 @@ export function getRuntimeEnvironment(): string {
} catch (err) {
return false;
}
}
}