react-native-wakeword-sid 1.1.203 → 1.1.205

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- a1f8f0ccd4a36890e596d0b22f4ba7b7 keyworddetection-1.0.0.aar
1
+ 13fe069bbd5879f49a4ac72d819bb550 keyworddetection-1.0.0.aar
@@ -1 +1 @@
1
- ecce4612759e9765cb1eec2dcddf74d9a0a59869 keyworddetection-1.0.0.aar
1
+ 84d3a2d0f926a3369afa202996e8e36747664f26 keyworddetection-1.0.0.aar
@@ -1,6 +1,16 @@
1
1
  package com.davoice.keywordspotting;
2
2
 
3
3
  import com.davoice.keywordsdetection.keywordslibrary.KeyWordsDetection;
4
+ import com.davoice.keywordsdetection.keywordslibrary.SpeakerVerification;
5
+
6
+ import org.json.JSONObject;
7
+ import org.json.JSONException;
8
+
9
+ import java.io.*;
10
+ import java.nio.ByteBuffer;
11
+ import java.nio.ByteOrder;
12
+ import java.util.*;
13
+
4
14
  import com.facebook.react.bridge.*;
5
15
  import com.facebook.react.modules.core.DeviceEventManagerModule;
6
16
  import androidx.annotation.Nullable;
@@ -13,6 +23,21 @@ public class KeyWordRNBridge extends ReactContextBaseJavaModule {
13
23
  private final String TAG = "KeyWordsDetection";
14
24
  private static final String REACT_CLASS = "KeyWordRNBridge";
15
25
  private static ReactApplicationContext reactContext;
26
+ // ===============================
27
+ // Speaker Verification holders
28
+ // ===============================
29
+ private static final String SV_TAG = "SV.RNBridge";
30
+
31
+ private static final class SVEngineHolder {
32
+ String engineId;
33
+ SpeakerVerification.SpeakerVerificationConfig cfg;
34
+ SpeakerVerification.SpeakerVerificationEngine engine;
35
+ SpeakerVerification.SpeakerEnrollment enrollment;
36
+ }
37
+
38
+ private final Map<String, SVEngineHolder> svEngines = new HashMap<>();
39
+
40
+ private final Map<String, SpeakerVerification.SpeakerVerificationMicController> svMicControllers = new HashMap<>();
16
41
 
17
42
  // VAD API:
18
43
  private final Map<String, Float> vadThresholdByInstance = new HashMap<>();
@@ -223,113 +248,706 @@ public class KeyWordRNBridge extends ReactContextBaseJavaModule {
223
248
  .emit(eventName, params);
224
249
  }
225
250
 
226
- // VAD API:
251
+ // VAD API:
227
252
 
228
- // ===== Add: VAD parity methods (anywhere among other @ReactMethod methods) =====
229
- @ReactMethod
230
- public void getVoiceProps(String instanceId, Promise promise) {
231
- KeyWordsDetection instance = instances.get(instanceId);
232
- if (instance == null) {
233
- promise.reject("InstanceNotFound", "No instance found with ID: " + instanceId);
234
- return;
235
- }
236
- try {
237
- @SuppressWarnings("unchecked")
238
- Map<String, Object> props = instance.getVoiceProps();
239
- WritableMap out = Arguments.createMap();
253
+ // ===== Add: VAD parity methods (anywhere among other @ReactMethod methods) =====
254
+ @ReactMethod
255
+ public void getVoiceProps(String instanceId, Promise promise) {
256
+ KeyWordsDetection instance = instances.get(instanceId);
257
+ if (instance == null) {
258
+ promise.reject("InstanceNotFound", "No instance found with ID: " + instanceId);
259
+ return;
260
+ }
261
+ try {
262
+ @SuppressWarnings("unchecked")
263
+ Map<String, Object> props = instance.getVoiceProps();
264
+ WritableMap out = Arguments.createMap();
240
265
 
241
- Object err = props.get("error");
242
- Object prob = props.get("voiceProbability");
243
- Object last = props.get("lastTimeHumanVoiceHeard");
266
+ Object err = props.get("error");
267
+ Object prob = props.get("voiceProbability");
268
+ Object last = props.get("lastTimeHumanVoiceHeard");
244
269
 
245
- out.putString("error", err == null ? "" : String.valueOf(err));
246
- out.putDouble("voiceProbability", prob instanceof Number ? ((Number) prob).doubleValue() : 0.0);
247
- out.putDouble("lastTimeHumanVoiceHeard", last instanceof Number ? ((Number) last).doubleValue() : 0.0);
270
+ out.putString("error", err == null ? "" : String.valueOf(err));
271
+ out.putDouble("voiceProbability", prob instanceof Number ? ((Number) prob).doubleValue() : 0.0);
272
+ out.putDouble("lastTimeHumanVoiceHeard", last instanceof Number ? ((Number) last).doubleValue() : 0.0);
248
273
 
249
- promise.resolve(out);
250
- } catch (Exception e) {
251
- promise.reject("GetVoicePropsError", e.getMessage());
274
+ promise.resolve(out);
275
+ } catch (Exception e) {
276
+ promise.reject("GetVoicePropsError", e.getMessage());
277
+ }
278
+ }
279
+
280
+ @ReactMethod
281
+ public void startVADDetection(String instanceId, Promise promise) {
282
+ KeyWordsDetection instance = instances.get(instanceId);
283
+ if (instance == null) {
284
+ promise.reject("InstanceNotFound", "No instance found with ID: " + instanceId);
285
+ return;
286
+ }
287
+ try {
288
+ // after: API-21 safe
289
+ Float _thr = vadThresholdByInstance.get(instanceId);
290
+ float thr = (_thr != null) ? _thr : DEFAULT_VAD_THRESHOLD;
291
+ Integer _win = vadMsWindowByInstance.get(instanceId);
292
+ int win = (_win != null) ? _win : DEFAULT_VAD_MSWINDOW;
293
+ instance.setVADParams(thr, win);
294
+ boolean ok = instance.startVADListening();
295
+ promise.resolve(ok);
296
+ } catch (Exception e) {
297
+ promise.reject("StartVADError", e.getMessage());
298
+ }
252
299
  }
253
- }
254
300
 
255
- @ReactMethod
256
- public void startVADDetection(String instanceId, Promise promise) {
257
- KeyWordsDetection instance = instances.get(instanceId);
258
- if (instance == null) {
259
- promise.reject("InstanceNotFound", "No instance found with ID: " + instanceId);
260
- return;
301
+ @ReactMethod
302
+ public void stopVADDetection(String instanceId, Promise promise) {
303
+ KeyWordsDetection instance = instances.get(instanceId);
304
+ if (instance == null) {
305
+ promise.reject("InstanceNotFound", "No instance found with ID: " + instanceId);
306
+ return;
307
+ }
308
+ try {
309
+ instance.stopVADListening();
310
+ promise.resolve("Stopped VAD for instance: " + instanceId);
311
+ } catch (Exception e) {
312
+ promise.reject("StopVADError", e.getMessage());
313
+ }
261
314
  }
262
- try {
263
- // after: API-21 safe
315
+
316
+ @ReactMethod
317
+ public void setVADParams(String instanceId, double threshold, int msWindow, Promise promise) {
318
+ KeyWordsDetection instance = instances.get(instanceId);
319
+ if (instance == null) {
320
+ promise.reject("InstanceNotFound", "No instance found with ID: " + instanceId);
321
+ return;
322
+ }
323
+ try {
324
+ float thr = (float) threshold;
325
+ vadThresholdByInstance.put(instanceId, thr);
326
+ vadMsWindowByInstance.put(instanceId, msWindow);
327
+ instance.setVADParams(thr, msWindow);
328
+ promise.resolve(null);
329
+ } catch (Exception e) {
330
+ promise.reject("SetVADParamsError", e.getMessage());
331
+ }
332
+ }
333
+
334
+ @ReactMethod
335
+ public void getVADParams(String instanceId, Promise promise) {
336
+ if (!instances.containsKey(instanceId)) {
337
+ promise.reject("InstanceNotFound", "No instance found with ID: " + instanceId);
338
+ return;
339
+ }
340
+ WritableMap out = Arguments.createMap();
264
341
  Float _thr = vadThresholdByInstance.get(instanceId);
265
342
  float thr = (_thr != null) ? _thr : DEFAULT_VAD_THRESHOLD;
266
343
  Integer _win = vadMsWindowByInstance.get(instanceId);
267
344
  int win = (_win != null) ? _win : DEFAULT_VAD_MSWINDOW;
268
- instance.setVADParams(thr, win);
269
- boolean ok = instance.startVADListening();
270
- promise.resolve(ok);
271
- } catch (Exception e) {
272
- promise.reject("StartVADError", e.getMessage());
345
+ out.putDouble("threshold", (double) thr);
346
+ out.putInt("msWindow", win);
347
+ promise.resolve(out);
348
+ }
349
+
350
+ @ReactMethod
351
+ public void addListener(String eventName) {
352
+ // Set up any upstream listeners or background tasks as necessary
273
353
  }
274
- }
275
354
 
276
- @ReactMethod
277
- public void stopVADDetection(String instanceId, Promise promise) {
278
- KeyWordsDetection instance = instances.get(instanceId);
279
- if (instance == null) {
280
- promise.reject("InstanceNotFound", "No instance found with ID: " + instanceId);
281
- return;
355
+ @ReactMethod
356
+ public void removeListeners(Integer count) {
357
+ // Remove upstream listeners, stop unnecessary background tasks
282
358
  }
283
- try {
284
- instance.stopVADListening();
285
- promise.resolve("Stopped VAD for instance: " + instanceId);
286
- } catch (Exception e) {
287
- promise.reject("StopVADError", e.getMessage());
359
+
360
+ // Implement other methods as needed, ensuring to use instanceId
361
+ private String stripFileSchemeAndroid(String s) {
362
+ if (s == null) return null;
363
+ if (s.startsWith("file://")) return s.replace("file://", "");
364
+ return s;
288
365
  }
289
- }
290
366
 
291
- @ReactMethod
292
- public void setVADParams(String instanceId, double threshold, int msWindow, Promise promise) {
293
- KeyWordsDetection instance = instances.get(instanceId);
294
- if (instance == null) {
295
- promise.reject("InstanceNotFound", "No instance found with ID: " + instanceId);
296
- return;
297
- }
298
- try {
299
- float thr = (float) threshold;
300
- vadThresholdByInstance.put(instanceId, thr);
301
- vadMsWindowByInstance.put(instanceId, msWindow);
302
- instance.setVADParams(thr, msWindow);
303
- promise.resolve(null);
304
- } catch (Exception e) {
305
- promise.reject("SetVADParamsError", e.getMessage());
367
+ // ********** SPEAKER IDENTIFICATION APIs **********
368
+ // Resolves:
369
+ // - absolute filesystem path -> returns as-is
370
+ // - "asset:/name.onnx" or "assets:/name.onnx" -> copies from APK assets to cache, returns real path
371
+ // - "name.onnx" (bundle asset name) -> copies from assets to cache
372
+ private String resolveToRealFilePath(String input) throws IOException {
373
+ input = stripFileSchemeAndroid(input);
374
+ if (input == null || input.isEmpty()) return input;
375
+
376
+ // Absolute existing path
377
+ if (input.startsWith("/") && new File(input).exists()) {
378
+ return input;
379
+ }
380
+
381
+ // RN Android require(...) often yields "asset:/..."
382
+ String assetName = input;
383
+ if (assetName.startsWith("asset:/")) assetName = assetName.substring("asset:/".length());
384
+ if (assetName.startsWith("assets:/")) assetName = assetName.substring("assets:/".length());
385
+
386
+ // If still contains directories like "assets/.../name.onnx", keep last component
387
+ // (your iOS resolver scans bundle; Android assets are flat-ish but can be nested)
388
+ // We'll try direct open first; if fails, try lastPathComponent.
389
+ String try1 = assetName;
390
+ String try2 = new File(assetName).getName();
391
+
392
+ File out1 = copyAssetToCacheIfExists(try1);
393
+ if (out1 != null) return out1.getAbsolutePath();
394
+
395
+ File out2 = copyAssetToCacheIfExists(try2);
396
+ if (out2 != null) return out2.getAbsolutePath();
397
+
398
+ // Not found
399
+ throw new FileNotFoundException("Cannot resolve asset/path: " + input + " (tried " + try1 + " and " + try2 + ")");
306
400
  }
307
- }
308
401
 
309
- @ReactMethod
310
- public void getVADParams(String instanceId, Promise promise) {
311
- if (!instances.containsKey(instanceId)) {
312
- promise.reject("InstanceNotFound", "No instance found with ID: " + instanceId);
313
- return;
314
- }
315
- WritableMap out = Arguments.createMap();
316
- Float _thr = vadThresholdByInstance.get(instanceId);
317
- float thr = (_thr != null) ? _thr : DEFAULT_VAD_THRESHOLD;
318
- Integer _win = vadMsWindowByInstance.get(instanceId);
319
- int win = (_win != null) ? _win : DEFAULT_VAD_MSWINDOW;
320
- out.putDouble("threshold", (double) thr);
321
- out.putInt("msWindow", win);
322
- promise.resolve(out);
323
- }
402
+ private File copyAssetToCacheIfExists(String assetName) {
403
+ if (assetName == null || assetName.isEmpty()) return null;
404
+ try (InputStream is = reactContext.getAssets().open(assetName)) {
405
+ File out = new File(reactContext.getCacheDir(), assetName);
406
+ // ensure parent
407
+ File parent = out.getParentFile();
408
+ if (parent != null) parent.mkdirs();
409
+
410
+ try (OutputStream os = new FileOutputStream(out)) {
411
+ byte[] buf = new byte[64 * 1024];
412
+ int n;
413
+ while ((n = is.read(buf)) > 0) os.write(buf, 0, n);
414
+ }
415
+ return out;
416
+ } catch (Throwable ignore) {
417
+ return null;
418
+ }
419
+ }
420
+
421
+ private SpeakerVerification.SpeakerVerificationConfig parseSVMicConfigJson(String configJson) throws JSONException, IOException {
422
+ JSONObject root = new JSONObject(configJson == null ? "{}" : configJson);
423
+
424
+ SpeakerVerification.SpeakerVerificationConfig cfg = new SpeakerVerification.SpeakerVerificationConfig();
425
+
426
+ // Top-level fields (if you use them)
427
+ String modelPath = root.optString("modelPath", "");
428
+ int sampleRate = root.optInt("sampleRate", cfg.sampleRate);
429
+ int frameSize = root.optInt("frameSize", cfg.frameSize);
430
+
431
+ // iOS-style puts most stuff in "options"
432
+ JSONObject opts = root.optJSONObject("options");
433
+ if (opts != null) {
434
+ cfg.decisionThreshold = (float) opts.optDouble("decisionThreshold", cfg.decisionThreshold);
435
+ cfg.tailSeconds = (float) opts.optDouble("tailSeconds", cfg.tailSeconds);
436
+ cfg.maxTailSeconds = (float) opts.optDouble("maxTailSeconds", cfg.maxTailSeconds);
437
+ cfg.cmn = opts.optBoolean("cmn", cfg.cmn);
438
+ cfg.expectedLayoutBDT = opts.optBoolean("expectedLayoutBDT", cfg.expectedLayoutBDT);
439
+
440
+ // Prefer opts.frameSize if present
441
+ frameSize = opts.has("frameSize") ? opts.optInt("frameSize", frameSize) : frameSize;
442
+ sampleRate = opts.has("sampleRate") ? opts.optInt("sampleRate", sampleRate) : sampleRate;
443
+ }
444
+
445
+ cfg.sampleRate = sampleRate > 0 ? sampleRate : 16000;
446
+ cfg.frameSize = frameSize > 0 ? frameSize : 1280;
447
+
448
+ // Resolve modelPath (MUST be real path for ORT)
449
+ if (modelPath != null && !modelPath.isEmpty()) {
450
+ cfg.modelPath = resolveToRealFilePath(modelPath);
451
+ } else {
452
+ // You can choose to throw here, but keeping consistent with iOS:
453
+ // iOS requires it; so Android should too.
454
+ throw new JSONException("Missing modelPath in configJson");
455
+ }
456
+ return cfg;
457
+ }
324
458
 
325
459
  @ReactMethod
326
- public void addListener(String eventName) {
327
- // Set up any upstream listeners or background tasks as necessary
460
+ public void createSpeakerVerifier(String engineId,
461
+ String modelPathOrName,
462
+ String enrollmentJsonPathOrName,
463
+ ReadableMap options,
464
+ Promise promise) {
465
+ if (svEngines.containsKey(engineId)) {
466
+ promise.reject("SVEngineExists", "Speaker verifier already exists with ID: " + engineId);
467
+ return;
468
+ }
469
+
470
+ new Thread(() -> {
471
+ try {
472
+ String modelPath = resolveToRealFilePath(modelPathOrName);
473
+ String jsonPath = resolveToRealFilePath(enrollmentJsonPathOrName);
474
+
475
+ // Build cfg from options (mirror iOS options)
476
+ SpeakerVerification.SpeakerVerificationConfig cfg = new SpeakerVerification.SpeakerVerificationConfig();
477
+ cfg.modelPath = modelPath;
478
+
479
+ if (options != null) {
480
+ if (options.hasKey("decisionThreshold")) cfg.decisionThreshold = (float) options.getDouble("decisionThreshold");
481
+ if (options.hasKey("frameSize")) cfg.frameSize = options.getInt("frameSize");
482
+ if (options.hasKey("tailSeconds")) cfg.tailSeconds = (float) options.getDouble("tailSeconds");
483
+ if (options.hasKey("maxTailSeconds")) cfg.maxTailSeconds = (float) options.getDouble("maxTailSeconds");
484
+ if (options.hasKey("cmn")) cfg.cmn = options.getBoolean("cmn");
485
+ if (options.hasKey("expectedLayoutBDT")) cfg.expectedLayoutBDT = options.getBoolean("expectedLayoutBDT");
486
+ }
487
+
488
+ // Load enrollment json file -> string -> enrollment object
489
+ String enrollmentJson = readAllText(jsonPath);
490
+ SpeakerVerification.SpeakerEnrollment enrollment = SpeakerVerification.SpeakerEnrollment.fromJson(enrollmentJson);
491
+
492
+ SpeakerVerification.SpeakerVerificationEngine engine = new SpeakerVerification.SpeakerVerificationEngine(cfg);
493
+ engine.setEnrollment(enrollment);
494
+
495
+ SVEngineHolder h = new SVEngineHolder();
496
+ h.engineId = engineId;
497
+ h.cfg = cfg;
498
+ h.enrollment = enrollment;
499
+ h.engine = engine;
500
+ svEngines.put(engineId, h);
501
+
502
+ WritableMap out = Arguments.createMap();
503
+ out.putBoolean("ok", true);
504
+ out.putString("engineId", engineId);
505
+ out.putString("modelPath", modelPath);
506
+ out.putString("enrollmentJsonPath", jsonPath);
507
+
508
+ promise.resolve(out);
509
+
510
+ } catch (Throwable t) {
511
+ promise.reject("SVCreateError", String.valueOf(t.getMessage()), t);
512
+ }
513
+ }).start();
514
+ }
515
+
516
+ private String readAllText(String path) throws IOException {
517
+ try (InputStream is = new FileInputStream(path)) {
518
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
519
+ byte[] buf = new byte[64 * 1024];
520
+ int n;
521
+ while ((n = is.read(buf)) > 0) bos.write(buf, 0, n);
522
+ return bos.toString("UTF-8");
523
+ }
524
+ }
525
+
526
+ @ReactMethod
527
+ public void verifySpeakerWavStreaming(String engineId,
528
+ String wavPathOrName,
529
+ boolean resetState,
530
+ Promise promise) {
531
+ SVEngineHolder h = svEngines.get(engineId);
532
+ if (h == null || h.engine == null) {
533
+ promise.reject("SVEngineNotFound", "No speaker verifier with ID: " + engineId);
534
+ return;
535
+ }
536
+
537
+ new Thread(() -> {
538
+ try {
539
+ String wavPath = resolveToRealFilePath(wavPathOrName);
540
+
541
+ if (resetState) {
542
+ h.engine.resetStreamingState();
543
+ }
544
+
545
+ SpeakerVerification.SpeakerVerificationResult res =
546
+ runWavThroughEngineStreaming(h.engine, wavPath, h.cfg.frameSize);
547
+
548
+ WritableMap out = Arguments.createMap();
549
+ out.putBoolean("ok", true);
550
+ out.putString("engineId", engineId);
551
+
552
+ // Mirror iOS-ish keys (your JS can read bestScore/score etc)
553
+ out.putDouble("scoreBest", res.scoreBest);
554
+ out.putDouble("scoreMean", res.scoreMean);
555
+ out.putDouble("scoreWorst", res.scoreWorst);
556
+ out.putBoolean("isMatch", res.isMatch);
557
+ out.putInt("embeddingDim", res.embeddingDim);
558
+ out.putDouble("usedSeconds", res.usedSeconds);
559
+
560
+ promise.resolve(out);
561
+
562
+ } catch (Throwable t) {
563
+ promise.reject("SVVerifyError", String.valueOf(t.getMessage()), t);
564
+ }
565
+ }).start();
566
+ }
567
+
568
+ private SpeakerVerification.SpeakerVerificationResult runWavThroughEngineStreaming(
569
+ SpeakerVerification.SpeakerVerificationEngine engine,
570
+ String wavPath,
571
+ int frameSizeSamples
572
+ ) throws Exception {
573
+
574
+ WavPcm16 wav = readWavPcm16Mono(wavPath);
575
+ if (wav.sampleRate != 16000) {
576
+ throw new IllegalArgumentException("WAV must be 16kHz. Got " + wav.sampleRate);
577
+ }
578
+
579
+ final short[] pcm = wav.pcm16;
580
+ int i = 0;
581
+
582
+ while (i + frameSizeSamples <= pcm.length) {
583
+ float[] f = new float[frameSizeSamples];
584
+ for (int k = 0; k < frameSizeSamples; k++) {
585
+ f[k] = pcm[i + k] / 32768.0f;
586
+ }
587
+ i += frameSizeSamples;
588
+
589
+ SpeakerVerification.SpeakerVerificationOutput out = engine.processFrame(f);
590
+ if (out != null && out.type == SpeakerVerification.SpeakerVerificationOutput.Type.RESULT) {
591
+ return out.result;
592
+ }
593
+ }
594
+
595
+ throw new IllegalStateException("NO_RESULT: WAV ended before engine produced RESULT");
596
+ }
597
+
598
+ // Minimal WAV reader: PCM16 LE, mono.
599
+ private static final class WavPcm16 {
600
+ int sampleRate;
601
+ short[] pcm16;
602
+ }
603
+
604
+ private WavPcm16 readWavPcm16Mono(String path) throws IOException {
605
+ try (InputStream is = new BufferedInputStream(new FileInputStream(path))) {
606
+ byte[] header = new byte[12];
607
+ readFully(is, header);
608
+
609
+ // "RIFF" .... "WAVE"
610
+ if (!(header[0]=='R' && header[1]=='I' && header[2]=='F' && header[3]=='F')) {
611
+ throw new IOException("Not RIFF WAV");
612
+ }
613
+ if (!(header[8]=='W' && header[9]=='A' && header[10]=='V' && header[11]=='E')) {
614
+ throw new IOException("Not WAVE");
615
+ }
616
+
617
+ int channels = -1, sampleRate = -1, bitsPerSample = -1;
618
+ ByteArrayOutputStream data = new ByteArrayOutputStream();
619
+
620
+ while (true) {
621
+ byte[] chunkHdr = new byte[8];
622
+ int n = is.read(chunkHdr);
623
+ if (n < 0) break;
624
+ if (n != 8) throw new IOException("Bad WAV chunk header");
625
+
626
+ String id = new String(chunkHdr, 0, 4);
627
+ int size = ByteBuffer.wrap(chunkHdr, 4, 4).order(ByteOrder.LITTLE_ENDIAN).getInt();
628
+
629
+ if ("fmt ".equals(id)) {
630
+ byte[] fmt = new byte[size];
631
+ readFully(is, fmt);
632
+
633
+ int audioFormat = ByteBuffer.wrap(fmt, 0, 2).order(ByteOrder.LITTLE_ENDIAN).getShort() & 0xFFFF;
634
+ channels = ByteBuffer.wrap(fmt, 2, 2).order(ByteOrder.LITTLE_ENDIAN).getShort() & 0xFFFF;
635
+ sampleRate = ByteBuffer.wrap(fmt, 4, 4).order(ByteOrder.LITTLE_ENDIAN).getInt();
636
+ bitsPerSample = ByteBuffer.wrap(fmt, 14, 2).order(ByteOrder.LITTLE_ENDIAN).getShort() & 0xFFFF;
637
+
638
+ if (audioFormat != 1) throw new IOException("WAV must be PCM (audioFormat=1). Got " + audioFormat);
639
+ } else if ("data".equals(id)) {
640
+ byte[] buf = new byte[64 * 1024];
641
+ int remaining = size;
642
+ while (remaining > 0) {
643
+ int r = is.read(buf, 0, Math.min(buf.length, remaining));
644
+ if (r < 0) throw new EOFException("WAV data truncated");
645
+ data.write(buf, 0, r);
646
+ remaining -= r;
647
+ }
648
+ } else {
649
+ // skip other chunks
650
+ long skipped = is.skip(size);
651
+ while (skipped < size) {
652
+ long s = is.skip(size - skipped);
653
+ if (s <= 0) break;
654
+ skipped += s;
655
+ }
656
+ }
657
+
658
+ // chunks are word-aligned
659
+ if ((size & 1) == 1) is.skip(1);
660
+ }
661
+
662
+ if (channels != 1) throw new IOException("WAV must be mono. Got channels=" + channels);
663
+ if (bitsPerSample != 16) throw new IOException("WAV must be 16-bit. Got bits=" + bitsPerSample);
664
+ if (sampleRate <= 0) throw new IOException("Missing/invalid sampleRate");
665
+
666
+ byte[] raw = data.toByteArray();
667
+ short[] pcm16 = new short[raw.length / 2];
668
+ ByteBuffer bb = ByteBuffer.wrap(raw).order(ByteOrder.LITTLE_ENDIAN);
669
+ for (int i = 0; i < pcm16.length; i++) pcm16[i] = bb.getShort();
670
+
671
+ WavPcm16 out = new WavPcm16();
672
+ out.sampleRate = sampleRate;
673
+ out.pcm16 = pcm16;
674
+ return out;
675
+ }
676
+ }
677
+
678
+ private void readFully(InputStream is, byte[] buf) throws IOException {
679
+ int off = 0;
680
+ while (off < buf.length) {
681
+ int n = is.read(buf, off, buf.length - off);
682
+ if (n < 0) throw new EOFException("Unexpected EOF");
683
+ off += n;
684
+ }
328
685
  }
329
686
 
330
687
  @ReactMethod
331
- public void removeListeners(Integer count) {
332
- // Remove upstream listeners, stop unnecessary background tasks
688
+ public void destroySpeakerVerifier(String engineId, Promise promise) {
689
+ SVEngineHolder h = svEngines.remove(engineId);
690
+ if (h == null) {
691
+ promise.reject("SVEngineNotFound", "No speaker verifier with ID: " + engineId);
692
+ return;
693
+ }
694
+ WritableMap out = Arguments.createMap();
695
+ out.putBoolean("ok", true);
696
+ out.putString("engineId", engineId);
697
+ promise.resolve(out);
333
698
  }
334
- // Implement other methods as needed, ensuring to use instanceId
699
+
700
+ private WritableMap toWritableMap(Map<String, Object> m) {
701
+ WritableMap out = Arguments.createMap();
702
+ if (m == null) return out;
703
+
704
+ for (Map.Entry<String, Object> e : m.entrySet()) {
705
+ String k = e.getKey();
706
+ Object v = e.getValue();
707
+ if (v == null) continue;
708
+
709
+ if (v instanceof String) out.putString(k, (String) v);
710
+ else if (v instanceof Boolean) out.putBoolean(k, (Boolean) v);
711
+ else if (v instanceof Integer) out.putInt(k, (Integer) v);
712
+ else if (v instanceof Long) out.putDouble(k, ((Long) v).doubleValue());
713
+ else if (v instanceof Float) out.putDouble(k, ((Float) v).doubleValue());
714
+ else if (v instanceof Double) out.putDouble(k, (Double) v);
715
+ else out.putString(k, String.valueOf(v));
716
+ }
717
+ return out;
718
+ }
719
+
720
+ private void sendEventUi(String eventName, WritableMap params) {
721
+ reactContext.runOnUiQueueThread(() -> sendEvent(eventName, params));
722
+ }
723
+ @ReactMethod
724
+ public void createSpeakerVerificationMicController(String controllerId,
725
+ String configJson,
726
+ Promise promise) {
727
+ if (svMicControllers.containsKey(controllerId)) {
728
+ promise.reject("SVMicExists", "Speaker mic controller already exists with ID: " + controllerId);
729
+ return;
730
+ }
731
+
732
+ new Thread(() -> {
733
+ try {
734
+ SpeakerVerification.SpeakerVerificationConfig cfg = parseSVMicConfigJson(configJson);
735
+
736
+ SpeakerVerification.SpeakerVerificationMicController ctrl =
737
+ new SpeakerVerification.SpeakerVerificationMicController(reactContext, cfg);
738
+
739
+ // Delegate proxy -> RN events
740
+ ctrl.delegate = new SpeakerVerification.SpeakerVerificationNativeDelegate() {
741
+ @Override
742
+ public void svOnboardingProgress(Map<String, Object> info) {
743
+ Map<String, Object> m = info == null ? new HashMap<>() : new HashMap<>(info);
744
+ m.put("controllerId", controllerId);
745
+ sendEventUi("onSpeakerVerificationOnboardingProgress", toWritableMap(m));
746
+ }
747
+
748
+ @Override
749
+ public void svOnboardingDone(Map<String, Object> info) {
750
+ Map<String, Object> m = info == null ? new HashMap<>() : new HashMap<>(info);
751
+ m.put("controllerId", controllerId);
752
+ sendEventUi("onSpeakerVerificationOnboardingDone", toWritableMap(m));
753
+ }
754
+
755
+ @Override
756
+ public void svVerifyResult(Map<String, Object> info) {
757
+ Map<String, Object> m = info == null ? new HashMap<>() : new HashMap<>(info);
758
+ m.put("controllerId", controllerId);
759
+ sendEventUi("onSpeakerVerificationVerifyResult", toWritableMap(m));
760
+ }
761
+
762
+ @Override
763
+ public void svError(Map<String, Object> info) {
764
+ Map<String, Object> m = info == null ? new HashMap<>() : new HashMap<>(info);
765
+ m.put("controllerId", controllerId);
766
+ sendEventUi("onSpeakerVerificationError", toWritableMap(m));
767
+ }
768
+ };
769
+
770
+ svMicControllers.put(controllerId, ctrl);
771
+
772
+ WritableMap out = Arguments.createMap();
773
+ out.putBoolean("ok", true);
774
+ out.putString("controllerId", controllerId);
775
+ promise.resolve(out);
776
+
777
+ } catch (Throwable t) {
778
+ promise.reject("SVMicCreateError", String.valueOf(t.getMessage()), t);
779
+ }
780
+ }).start();
781
+ }
782
+ @ReactMethod
783
+ public void destroySpeakerVerificationMicController(String controllerId, Promise promise) {
784
+ SpeakerVerification.SpeakerVerificationMicController ctrl = svMicControllers.remove(controllerId);
785
+ if (ctrl == null) {
786
+ promise.reject("SVMicNotFound", "No speaker mic controller with ID: " + controllerId);
787
+ return;
788
+ }
789
+
790
+ try { ctrl.stop(); } catch (Throwable ignore) {}
791
+
792
+ WritableMap out = Arguments.createMap();
793
+ out.putBoolean("ok", true);
794
+ out.putString("controllerId", controllerId);
795
+ promise.resolve(out);
796
+ }
797
+ @ReactMethod
798
+ public void svBeginOnboarding(String controllerId,
799
+ String enrollmentId,
800
+ int targetEmbeddingCount,
801
+ boolean reset,
802
+ Promise promise) {
803
+ SpeakerVerification.SpeakerVerificationMicController ctrl = svMicControllers.get(controllerId);
804
+ if (ctrl == null) {
805
+ promise.reject("SVMicNotFound", "No speaker mic controller with ID: " + controllerId);
806
+ return;
807
+ }
808
+
809
+ new Thread(() -> {
810
+ try {
811
+ ctrl.beginOnboarding(enrollmentId, targetEmbeddingCount, reset);
812
+
813
+ WritableMap out = Arguments.createMap();
814
+ out.putBoolean("ok", true);
815
+ out.putString("controllerId", controllerId);
816
+ out.putString("enrollmentId", enrollmentId);
817
+ out.putInt("target", targetEmbeddingCount);
818
+ promise.resolve(out);
819
+ } catch (Throwable t) {
820
+ promise.reject("SVMicBeginError", String.valueOf(t.getMessage()), t);
821
+ }
822
+ }).start();
823
+ }
824
+
825
+ @ReactMethod
826
+ public void svGetNextEmbeddingFromMic(String controllerId, Promise promise) {
827
+ SpeakerVerification.SpeakerVerificationMicController ctrl = svMicControllers.get(controllerId);
828
+ if (ctrl == null) {
829
+ promise.reject("SVMicNotFound", "No speaker mic controller with ID: " + controllerId);
830
+ return;
831
+ }
832
+
833
+ new Thread(() -> {
834
+ try {
835
+ ctrl.getNextEmbeddingFromMic();
836
+ WritableMap out = Arguments.createMap();
837
+ out.putBoolean("ok", true);
838
+ out.putString("controllerId", controllerId);
839
+ promise.resolve(out);
840
+ } catch (Throwable t) {
841
+ promise.reject("SVMicGetNextError", String.valueOf(t.getMessage()), t);
842
+ }
843
+ }).start();
844
+ }
845
+
846
+ @ReactMethod
847
+ public void svFinalizeOnboardingNow(String controllerId, Promise promise) {
848
+ SpeakerVerification.SpeakerVerificationMicController ctrl = svMicControllers.get(controllerId);
849
+ if (ctrl == null) {
850
+ promise.reject("SVMicNotFound", "No speaker mic controller with ID: " + controllerId);
851
+ return;
852
+ }
853
+
854
+ new Thread(() -> {
855
+ try {
856
+ ctrl.finalizeOnboardingNow();
857
+ WritableMap out = Arguments.createMap();
858
+ out.putBoolean("ok", true);
859
+ out.putString("controllerId", controllerId);
860
+ promise.resolve(out);
861
+ } catch (Throwable t) {
862
+ promise.reject("SVMicFinalizeError", String.valueOf(t.getMessage()), t);
863
+ }
864
+ }).start();
865
+ }
866
+
867
+ @ReactMethod
868
+ public void svSetEnrollmentJson(String controllerId, String enrollmentJson, Promise promise) {
869
+ SpeakerVerification.SpeakerVerificationMicController ctrl = svMicControllers.get(controllerId);
870
+ if (ctrl == null) {
871
+ promise.reject("SVMicNotFound", "No speaker mic controller with ID: " + controllerId);
872
+ return;
873
+ }
874
+
875
+ new Thread(() -> {
876
+ try {
877
+ ctrl.setEnrollmentJson(enrollmentJson);
878
+ WritableMap out = Arguments.createMap();
879
+ out.putBoolean("ok", true);
880
+ out.putString("controllerId", controllerId);
881
+ promise.resolve(out);
882
+ } catch (Throwable t) {
883
+ promise.reject("SVMicSetEnrollError", String.valueOf(t.getMessage()), t);
884
+ }
885
+ }).start();
886
+ }
887
+
888
+ @ReactMethod
889
+ public void svStartVerifyFromMic(String controllerId, boolean resetState, Promise promise) {
890
+ SpeakerVerification.SpeakerVerificationMicController ctrl = svMicControllers.get(controllerId);
891
+ if (ctrl == null) {
892
+ promise.reject("SVMicNotFound", "No speaker mic controller with ID: " + controllerId);
893
+ return;
894
+ }
895
+
896
+ new Thread(() -> {
897
+ try {
898
+ ctrl.startVerifyFromMic(resetState);
899
+ WritableMap out = Arguments.createMap();
900
+ out.putBoolean("ok", true);
901
+ out.putString("controllerId", controllerId);
902
+ out.putBoolean("resetState", resetState);
903
+ promise.resolve(out);
904
+ } catch (Throwable t) {
905
+ promise.reject("SVMicStartVerifyError", String.valueOf(t.getMessage()), t);
906
+ }
907
+ }).start();
908
+ }
909
+
910
+ @ReactMethod
911
+ public void svStartEndlessVerifyFromMic(String controllerId, double hopSeconds, boolean stopOnMatch, boolean resetState, Promise promise) {
912
+ SpeakerVerification.SpeakerVerificationMicController ctrl = svMicControllers.get(controllerId);
913
+ if (ctrl == null) {
914
+ promise.reject("SVMicNotFound", "No speaker mic controller with ID: " + controllerId);
915
+ return;
916
+ }
917
+
918
+ new Thread(() -> {
919
+ try {
920
+ ctrl.startEndlessVerifyFromMic((float) hopSeconds, stopOnMatch, resetState);
921
+ WritableMap out = Arguments.createMap();
922
+ out.putBoolean("ok", true);
923
+ out.putString("controllerId", controllerId);
924
+ out.putDouble("hopSeconds", hopSeconds);
925
+ out.putBoolean("stopOnMatch", stopOnMatch);
926
+ out.putBoolean("resetState", resetState);
927
+ promise.resolve(out);
928
+ } catch (Throwable t) {
929
+ promise.reject("SVMicStartEndlessVerifyError", String.valueOf(t.getMessage()), t);
930
+ }
931
+ }).start();
932
+ }
933
+
934
+ @ReactMethod
935
+ public void svStopMic(String controllerId, Promise promise) {
936
+ SpeakerVerification.SpeakerVerificationMicController ctrl = svMicControllers.get(controllerId);
937
+ if (ctrl == null) {
938
+ promise.reject("SVMicNotFound", "No speaker mic controller with ID: " + controllerId);
939
+ return;
940
+ }
941
+
942
+ try {
943
+ ctrl.stop();
944
+ WritableMap out = Arguments.createMap();
945
+ out.putBoolean("ok", true);
946
+ out.putString("controllerId", controllerId);
947
+ promise.resolve(out);
948
+ } catch (Throwable t) {
949
+ promise.reject("SVMicStopError", String.valueOf(t.getMessage()), t);
950
+ }
951
+ }
952
+
335
953
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-wakeword-sid",
3
- "version": "1.1.203",
3
+ "version": "1.1.205",
4
4
  "description": "Voice/Wake-word detection library for React Native",
5
5
  "main": "wakewords/index.js",
6
6
  "types": "wakewords/index.d.ts",
@@ -41,12 +41,6 @@ function normalizePathOrName(input) {
41
41
  return String(input);
42
42
  }
43
43
 
44
- function assertIOS() {
45
- if (Platform.OS !== 'ios') {
46
- throw new Error('Speaker verification bridge is currently implemented for iOS only.');
47
- }
48
- }
49
-
50
44
  function assertMethod(name) {
51
45
  if (!KeyWordRNBridge?.[name]) {
52
46
  throw new Error(`KeyWordRNBridge.${name} is not available (native not linked / iOS only?)`);
@@ -75,7 +69,7 @@ export class SpeakerVerificationRNBridgeInstance {
75
69
  * { decisionThreshold, frameSize, tailSeconds, maxTailSeconds, cmn, expectedLayoutBDT, logLevel }
76
70
  */
77
71
  async create(modelPathOrName, enrollmentJsonPathOrName, options = {}) {
78
- assertIOS();
72
+
79
73
  assertMethod('createSpeakerVerifier');
80
74
 
81
75
  const modelArg = normalizePathOrName(modelPathOrName);
@@ -103,7 +97,7 @@ export class SpeakerVerificationRNBridgeInstance {
103
97
  * - if true, clears internal streaming state before verification
104
98
  */
105
99
  async verifyWavStreaming(wavPathOrName, resetState = true) {
106
- assertIOS();
100
+
107
101
  assertMethod('verifySpeakerWavStreaming');
108
102
 
109
103
  const wavArg = normalizePathOrName(wavPathOrName);
@@ -117,11 +111,6 @@ export class SpeakerVerificationRNBridgeInstance {
117
111
  }
118
112
 
119
113
  async destroy() {
120
-
121
- if (Platform.OS !== 'ios') {
122
- // no-op for now
123
- return { ok: true, engineId: this.engineId, noop: true };
124
- }
125
114
  assertMethod('destroySpeakerVerifier');
126
115
  return await KeyWordRNBridge.destroySpeakerVerifier(this.engineId);
127
116
  }
@@ -143,7 +132,7 @@ export class SpeakerVerificationMicController {
143
132
  }
144
133
 
145
134
  async create(configJson) {
146
- assertIOS();
135
+
147
136
  assertMethod('createSpeakerVerificationMicController');
148
137
  const cfg =
149
138
  typeof configJson === 'string'
@@ -159,7 +148,7 @@ export class SpeakerVerificationMicController {
159
148
  }
160
149
 
161
150
  async beginOnboarding(enrollmentId, targetEmbeddingCount, reset = true) {
162
- assertIOS();
151
+
163
152
  assertMethod('svBeginOnboarding');
164
153
  dbg('svBeginOnboarding args:', { controllerId: this.controllerId, enrollmentId, targetEmbeddingCount, reset: !!reset });
165
154
 
@@ -172,7 +161,7 @@ export class SpeakerVerificationMicController {
172
161
  }
173
162
 
174
163
  async getNextEmbeddingFromMic() {
175
- assertIOS();
164
+
176
165
  assertMethod('svGetNextEmbeddingFromMic');
177
166
  dbg('svGetNextEmbeddingFromMic args:', { controllerId: this.controllerId });
178
167
 
@@ -180,7 +169,7 @@ export class SpeakerVerificationMicController {
180
169
  }
181
170
 
182
171
  async finalizeOnboardingNow() {
183
- assertIOS();
172
+
184
173
  assertMethod('svFinalizeOnboardingNow');
185
174
  dbg('svFinalizeOnboardingNow args:', { controllerId: this.controllerId });
186
175
 
@@ -188,7 +177,7 @@ export class SpeakerVerificationMicController {
188
177
  }
189
178
 
190
179
  async setEnrollmentJson(enrollmentJson) {
191
- assertIOS();
180
+
192
181
  assertMethod('svSetEnrollmentJson');
193
182
  dbg('svSetEnrollmentJson args:', { controllerId: this.controllerId, len: String(enrollmentJson ?? '').length });
194
183
 
@@ -205,7 +194,7 @@ export class SpeakerVerificationMicController {
205
194
  }
206
195
 
207
196
  async startVerifyFromMic(resetState = true) {
208
- assertIOS();
197
+
209
198
  assertMethod('svStartVerifyFromMic');
210
199
  dbg('svStartVerifyFromMic args:', { controllerId: this.controllerId, resetState: !!resetState });
211
200
 
@@ -216,17 +205,11 @@ export class SpeakerVerificationMicController {
216
205
  }
217
206
 
218
207
  async stop() {
219
- if (Platform.OS !== 'ios') {
220
- return { ok: true, controllerId: this.controllerId, noop: true };
221
- }
222
208
  assertMethod('svStopMic');
223
209
  return await KeyWordRNBridge.svStopMic(this.controllerId);
224
210
  }
225
211
 
226
212
  async destroy() {
227
- if (Platform.OS !== 'ios') {
228
- return { ok: true, controllerId: this.controllerId, noop: true };
229
- }
230
213
  assertMethod('destroySpeakerVerificationMicController');
231
214
  return await KeyWordRNBridge.destroySpeakerVerificationMicController(this.controllerId);
232
215
  }
@@ -237,7 +220,6 @@ export const createSpeakerVerificationMicController = async (controllerId) => {
237
220
  };
238
221
 
239
222
  async function verifyFromMicWithEnrollment(enrollmentJson, setUiMessage) {
240
- if (Platform.OS !== 'ios') return;
241
223
 
242
224
  const micConfig = {
243
225
  modelPath: 'speaker_model.onnx',