|
@@ -0,0 +1,208 @@
|
|
|
+import * as stream from 'stream';
|
|
|
+import * as path from 'path';
|
|
|
+import * as fs from 'fs';
|
|
|
+import * as binary from 'node-pre-gyp';
|
|
|
+
|
|
|
+const bindingPath: string = binary.find(path.resolve(path.join(__dirname, '../../package.json')));
|
|
|
+const SnowboyDetectNative: SnowboyDetectNativeInterface = require(bindingPath).SnowboyDetect;
|
|
|
+
|
|
|
+enum DetectionResult {
|
|
|
+ SILENCE = -2,
|
|
|
+ ERROR = -1,
|
|
|
+ SOUND = 0
|
|
|
+}
|
|
|
+
|
|
|
+enum ModelType {
|
|
|
+ PMDL,
|
|
|
+ UMDL
|
|
|
+}
|
|
|
+
|
|
|
+export interface HotwordModel {
|
|
|
+ file: string;
|
|
|
+ sensitivity?: string;
|
|
|
+ hotwords: string | Array<string>;
|
|
|
+}
|
|
|
+
|
|
|
+interface HotwordModelsInterface {
|
|
|
+ add(model: HotwordModel): void;
|
|
|
+ lookup(index: number): string;
|
|
|
+ numHotwords(): number;
|
|
|
+}
|
|
|
+
|
|
|
+export interface DetectorOptions {
|
|
|
+ resource: string;
|
|
|
+ models: HotwordModels;
|
|
|
+ audioGain?: number;
|
|
|
+ applyFrontend?: boolean;
|
|
|
+ highSensitivity?: string;
|
|
|
+}
|
|
|
+
|
|
|
+export interface SnowboyDetectInterface {
|
|
|
+ reset(): boolean;
|
|
|
+ runDetection(buffer: Buffer): number;
|
|
|
+ setSensitivity(sensitivity: string): void;
|
|
|
+ setHighSensitivity(highSensitivity: string): void;
|
|
|
+ getSensitivity(): string;
|
|
|
+ setAudioGain(gain: number): void;
|
|
|
+ updateModel(): void;
|
|
|
+ numHotwords(): number;
|
|
|
+ sampleRate(): number;
|
|
|
+ numChannels(): number;
|
|
|
+ bitsPerSample(): number;
|
|
|
+}
|
|
|
+
|
|
|
+export class HotwordModels implements HotwordModels {
|
|
|
+ private models: Array<HotwordModel> = [];
|
|
|
+ private lookupTable: Array<string>;
|
|
|
+
|
|
|
+ add(model: HotwordModel) {
|
|
|
+ model.hotwords = [].concat(model.hotwords);
|
|
|
+ model.sensitivity = model.sensitivity || "0.5";
|
|
|
+
|
|
|
+ if (fs.existsSync(model.file) === false) {
|
|
|
+ throw new Error(`Model ${model.file} does not exists.`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const type = path.extname(model.file).toUpperCase();
|
|
|
+
|
|
|
+ if (ModelType[type] === ModelType.PMDL && model.hotwords.length > 1) {
|
|
|
+ throw new Error('Personal models can define only one hotword.');
|
|
|
+ }
|
|
|
+
|
|
|
+ this.models.push(model);
|
|
|
+ this.lookupTable = this.generateHotwordsLookupTable();
|
|
|
+ }
|
|
|
+
|
|
|
+ get modelString(): string {
|
|
|
+ return this.models.map((model) => model.file).join();
|
|
|
+ }
|
|
|
+
|
|
|
+ get sensitivityString(): string {
|
|
|
+ return this.models.map((model) => model.sensitivity).join();
|
|
|
+ }
|
|
|
+
|
|
|
+ lookup(index: number): string {
|
|
|
+ const lookupIndex = index - 1;
|
|
|
+ if (lookupIndex < 0 || lookupIndex >= this.lookupTable.length) {
|
|
|
+ throw new Error('Index out of bounds.');
|
|
|
+ }
|
|
|
+ return this.lookupTable[lookupIndex];
|
|
|
+ }
|
|
|
+
|
|
|
+ numHotwords(): number {
|
|
|
+ return this.lookupTable.length;
|
|
|
+ }
|
|
|
+
|
|
|
+ private generateHotwordsLookupTable(): Array<string> {
|
|
|
+ return this.models.reduce((hotwords, model) => {
|
|
|
+ return hotwords.concat(model.hotwords);
|
|
|
+ }, new Array<string>());
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export class SnowboyDetect extends stream.Writable implements SnowboyDetectInterface {
|
|
|
+ nativeInstance: SnowboyDetectNativeInterface;
|
|
|
+ private models: HotwordModels;
|
|
|
+
|
|
|
+ constructor(options: DetectorOptions) {
|
|
|
+ super();
|
|
|
+
|
|
|
+ this.models = options.models;
|
|
|
+ this.nativeInstance = new SnowboyDetectNative(options.resource, options.models.modelString);
|
|
|
+
|
|
|
+ if (this.nativeInstance.NumHotwords() !== options.models.numHotwords()) {
|
|
|
+ throw new Error('Loaded hotwords count does not match number of hotwords defined.');
|
|
|
+ }
|
|
|
+
|
|
|
+ this.nativeInstance.SetSensitivity(options.models.sensitivityString);
|
|
|
+
|
|
|
+ if (options.audioGain) {
|
|
|
+ this.nativeInstance.SetAudioGain(options.audioGain);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (options.applyFrontend) {
|
|
|
+ this.nativeInstance.ApplyFrontend(options.applyFrontend);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (options.highSensitivity) {
|
|
|
+ this.nativeInstance.SetHighSensitivity(options.highSensitivity);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ reset(): boolean {
|
|
|
+ return this.nativeInstance.Reset();
|
|
|
+ }
|
|
|
+
|
|
|
+ runDetection(buffer: Buffer): number {
|
|
|
+ const index = this.nativeInstance.RunDetection(buffer);
|
|
|
+ this.processDetectionResult(index, buffer);
|
|
|
+ return index;
|
|
|
+ }
|
|
|
+
|
|
|
+ setSensitivity(sensitivity: string): void {
|
|
|
+ this.nativeInstance.SetSensitivity(sensitivity);
|
|
|
+ }
|
|
|
+
|
|
|
+ setHighSensitivity(highSensitivity: string): void {
|
|
|
+ this.nativeInstance.SetHighSensitivity(highSensitivity);
|
|
|
+ }
|
|
|
+
|
|
|
+ getSensitivity(): string {
|
|
|
+ return this.nativeInstance.GetSensitivity();
|
|
|
+ }
|
|
|
+
|
|
|
+ setAudioGain(gain: number): void {
|
|
|
+ this.nativeInstance.SetAudioGain(gain);
|
|
|
+ }
|
|
|
+
|
|
|
+ updateModel(): void {
|
|
|
+ this.nativeInstance.UpdateModel();
|
|
|
+ }
|
|
|
+
|
|
|
+ numHotwords(): number {
|
|
|
+ return this.nativeInstance.NumHotwords();
|
|
|
+ }
|
|
|
+
|
|
|
+ sampleRate(): number {
|
|
|
+ return this.nativeInstance.SampleRate();
|
|
|
+ }
|
|
|
+
|
|
|
+ numChannels(): number {
|
|
|
+ return this.nativeInstance.NumChannels();
|
|
|
+ }
|
|
|
+
|
|
|
+ bitsPerSample(): number {
|
|
|
+ return this.nativeInstance.BitsPerSample();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Stream implementation
|
|
|
+ _write(chunk: Buffer, encoding: string, callback: Function) {
|
|
|
+ const index = this.nativeInstance.RunDetection(chunk);
|
|
|
+ this.processDetectionResult(index, chunk);
|
|
|
+ return callback();
|
|
|
+ }
|
|
|
+
|
|
|
+ private processDetectionResult(index: number, buffer: Buffer): void {
|
|
|
+ switch (index) {
|
|
|
+ case DetectionResult.ERROR:
|
|
|
+ this.emit('error');
|
|
|
+ break;
|
|
|
+
|
|
|
+ case DetectionResult.SILENCE:
|
|
|
+ this.emit('silence');
|
|
|
+ break;
|
|
|
+
|
|
|
+ case DetectionResult.SOUND:
|
|
|
+ this.emit('sound', buffer);
|
|
|
+ break;
|
|
|
+
|
|
|
+ default:
|
|
|
+ const hotword = this.models.lookup(index);
|
|
|
+ this.emit('hotword', index, hotword, buffer);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export const Detector = SnowboyDetect;
|
|
|
+export const Models = HotwordModels;
|