react-native-wakeword 1.1.56 → 1.1.58

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.
Files changed (39) hide show
  1. package/KeyWordRNBridge.podspec +1 -1
  2. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  3. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  4. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  5. package/android/.gradle/buildOutputCleanup/cache.properties +1 -1
  6. package/android/libs/com/davoice/keyworddetection/1.0.0/keyworddetection-1.0.0.aar +0 -0
  7. package/android/libs/com/davoice/keyworddetection/1.0.0/keyworddetection-1.0.0.aar.md5 +1 -1
  8. package/android/libs/com/davoice/keyworddetection/1.0.0/keyworddetection-1.0.0.aar.sha1 +1 -1
  9. package/android/src/main/java/com/davoice/keywordspotting/KeyWordRNBridge.java +764 -85
  10. package/android/src/main/java/com/davoice/speakeridrn/SpeakerIdRNBridge.java_not_used_yet +588 -0
  11. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64/KeyWordDetection.framework/Headers/KeyWordDetection-Swift.h +56 -0
  12. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64/KeyWordDetection.framework/Info.plist +0 -0
  13. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64/KeyWordDetection.framework/KeyWordDetection +0 -0
  14. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64/KeyWordDetection.framework/Modules/KeyWordDetection.swiftmodule/arm64-apple-ios.abi.json +8970 -2462
  15. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64/KeyWordDetection.framework/Modules/KeyWordDetection.swiftmodule/arm64-apple-ios.private.swiftinterface +133 -0
  16. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64/KeyWordDetection.framework/Modules/KeyWordDetection.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  17. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64/KeyWordDetection.framework/Modules/KeyWordDetection.swiftmodule/arm64-apple-ios.swiftinterface +133 -0
  18. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64_x86_64-simulator/KeyWordDetection.framework/Headers/KeyWordDetection-Swift.h +112 -0
  19. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64_x86_64-simulator/KeyWordDetection.framework/Info.plist +0 -0
  20. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64_x86_64-simulator/KeyWordDetection.framework/KeyWordDetection +0 -0
  21. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64_x86_64-simulator/KeyWordDetection.framework/Modules/KeyWordDetection.swiftmodule/arm64-apple-ios-simulator.abi.json +8970 -2462
  22. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64_x86_64-simulator/KeyWordDetection.framework/Modules/KeyWordDetection.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +133 -0
  23. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64_x86_64-simulator/KeyWordDetection.framework/Modules/KeyWordDetection.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  24. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64_x86_64-simulator/KeyWordDetection.framework/Modules/KeyWordDetection.swiftmodule/arm64-apple-ios-simulator.swiftinterface +133 -0
  25. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64_x86_64-simulator/KeyWordDetection.framework/Modules/KeyWordDetection.swiftmodule/x86_64-apple-ios-simulator.abi.json +8970 -2462
  26. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64_x86_64-simulator/KeyWordDetection.framework/Modules/KeyWordDetection.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +133 -0
  27. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64_x86_64-simulator/KeyWordDetection.framework/Modules/KeyWordDetection.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  28. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64_x86_64-simulator/KeyWordDetection.framework/Modules/KeyWordDetection.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +133 -0
  29. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64_x86_64-simulator/KeyWordDetection.framework/_CodeSignature/CodeDirectory +0 -0
  30. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64_x86_64-simulator/KeyWordDetection.framework/_CodeSignature/CodeRequirements-1 +0 -0
  31. package/ios/KeyWordRNBridge/KeyWordDetection.xcframework/ios-arm64_x86_64-simulator/KeyWordDetection.framework/_CodeSignature/CodeResources +34 -34
  32. package/ios/KeyWordRNBridge/KeyWordRNBridge.m +704 -3
  33. package/package.json +1 -1
  34. package/wakewords/SpeakerVerificationRNBridge.d.ts +49 -0
  35. package/wakewords/SpeakerVerificationRNBridge.js +294 -0
  36. package/wakewords/index.d.ts +18 -10
  37. package/wakewords/index.js +28 -0
  38. package/android/src/main/assets/coca_cola_model_28_05052025.dm +0 -0
  39. package/wakewords/index.d.ts.chat_idiot +0 -2
@@ -1,18 +1,55 @@
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;
7
17
  import android.util.Log;
8
18
  import java.util.HashMap;
9
19
  import java.util.Map;
20
+ import java.util.concurrent.ExecutorService;
21
+ import java.util.concurrent.Executors;
22
+ import java.util.concurrent.ConcurrentHashMap;
23
+ import java.util.concurrent.atomic.AtomicInteger;
24
+
10
25
 
11
26
  public class KeyWordRNBridge extends ReactContextBaseJavaModule {
12
27
 
13
28
  private final String TAG = "KeyWordsDetection";
14
29
  private static final String REACT_CLASS = "KeyWordRNBridge";
15
30
  private static ReactApplicationContext reactContext;
31
+ // ===============================
32
+ // Speaker Verification holders
33
+ // ===============================
34
+ private static final String SV_TAG = "SV.RNBridge";
35
+
36
+ private static final class SVEngineHolder {
37
+ String engineId;
38
+ SpeakerVerification.SpeakerVerificationConfig cfg;
39
+ SpeakerVerification.SpeakerVerificationEngine engine;
40
+ SpeakerVerification.SpeakerEnrollment enrollment;
41
+ }
42
+
43
+ private final Map<String, SVEngineHolder> svEngines = new ConcurrentHashMap<>();
44
+
45
+ private final Map<String, SpeakerVerification.SpeakerVerificationMicController> svMicControllers = new ConcurrentHashMap<>();
46
+ private final Map<String, Boolean> svAutoOnboarding = new ConcurrentHashMap<>();
47
+ private final Map<String, Integer> svAutoTarget = new ConcurrentHashMap<>();
48
+ private final Map<String, Integer> svAutoCollected = new ConcurrentHashMap<>();
49
+ private final Map<String, String> svAutoEnrollmentId = new ConcurrentHashMap<>();
50
+ // IMPORTANT: serialize ALL SV ops to avoid races with mic frames / onboarding state.
51
+ private final ExecutorService svExec = Executors.newSingleThreadExecutor();
52
+ private final AtomicInteger svJobN = new AtomicInteger(0);
16
53
 
17
54
  // VAD API:
18
55
  private final Map<String, Float> vadThresholdByInstance = new HashMap<>();
@@ -223,113 +260,755 @@ public class KeyWordRNBridge extends ReactContextBaseJavaModule {
223
260
  .emit(eventName, params);
224
261
  }
225
262
 
226
- // VAD API:
263
+ // VAD API:
227
264
 
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();
265
+ // ===== Add: VAD parity methods (anywhere among other @ReactMethod methods) =====
266
+ @ReactMethod
267
+ public void getVoiceProps(String instanceId, Promise promise) {
268
+ KeyWordsDetection instance = instances.get(instanceId);
269
+ if (instance == null) {
270
+ promise.reject("InstanceNotFound", "No instance found with ID: " + instanceId);
271
+ return;
272
+ }
273
+ try {
274
+ @SuppressWarnings("unchecked")
275
+ Map<String, Object> props = instance.getVoiceProps();
276
+ WritableMap out = Arguments.createMap();
240
277
 
241
- Object err = props.get("error");
242
- Object prob = props.get("voiceProbability");
243
- Object last = props.get("lastTimeHumanVoiceHeard");
278
+ Object err = props.get("error");
279
+ Object prob = props.get("voiceProbability");
280
+ Object last = props.get("lastTimeHumanVoiceHeard");
244
281
 
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);
282
+ out.putString("error", err == null ? "" : String.valueOf(err));
283
+ out.putDouble("voiceProbability", prob instanceof Number ? ((Number) prob).doubleValue() : 0.0);
284
+ out.putDouble("lastTimeHumanVoiceHeard", last instanceof Number ? ((Number) last).doubleValue() : 0.0);
248
285
 
249
- promise.resolve(out);
250
- } catch (Exception e) {
251
- promise.reject("GetVoicePropsError", e.getMessage());
286
+ promise.resolve(out);
287
+ } catch (Exception e) {
288
+ promise.reject("GetVoicePropsError", e.getMessage());
289
+ }
290
+ }
291
+
292
+ @ReactMethod
293
+ public void startVADDetection(String instanceId, Promise promise) {
294
+ KeyWordsDetection instance = instances.get(instanceId);
295
+ if (instance == null) {
296
+ promise.reject("InstanceNotFound", "No instance found with ID: " + instanceId);
297
+ return;
298
+ }
299
+ try {
300
+ // after: API-21 safe
301
+ Float _thr = vadThresholdByInstance.get(instanceId);
302
+ float thr = (_thr != null) ? _thr : DEFAULT_VAD_THRESHOLD;
303
+ Integer _win = vadMsWindowByInstance.get(instanceId);
304
+ int win = (_win != null) ? _win : DEFAULT_VAD_MSWINDOW;
305
+ instance.setVADParams(thr, win);
306
+ boolean ok = instance.startVADListening();
307
+ promise.resolve(ok);
308
+ } catch (Exception e) {
309
+ promise.reject("StartVADError", e.getMessage());
310
+ }
311
+ }
312
+
313
+ @ReactMethod
314
+ public void stopVADDetection(String instanceId, Promise promise) {
315
+ KeyWordsDetection instance = instances.get(instanceId);
316
+ if (instance == null) {
317
+ promise.reject("InstanceNotFound", "No instance found with ID: " + instanceId);
318
+ return;
319
+ }
320
+ try {
321
+ instance.stopVADListening();
322
+ promise.resolve("Stopped VAD for instance: " + instanceId);
323
+ } catch (Exception e) {
324
+ promise.reject("StopVADError", e.getMessage());
325
+ }
252
326
  }
253
- }
254
327
 
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;
328
+ @ReactMethod
329
+ public void setVADParams(String instanceId, double threshold, int msWindow, Promise promise) {
330
+ KeyWordsDetection instance = instances.get(instanceId);
331
+ if (instance == null) {
332
+ promise.reject("InstanceNotFound", "No instance found with ID: " + instanceId);
333
+ return;
334
+ }
335
+ try {
336
+ float thr = (float) threshold;
337
+ vadThresholdByInstance.put(instanceId, thr);
338
+ vadMsWindowByInstance.put(instanceId, msWindow);
339
+ instance.setVADParams(thr, msWindow);
340
+ promise.resolve(null);
341
+ } catch (Exception e) {
342
+ promise.reject("SetVADParamsError", e.getMessage());
343
+ }
261
344
  }
262
- try {
263
- // after: API-21 safe
345
+
346
+ @ReactMethod
347
+ public void getVADParams(String instanceId, Promise promise) {
348
+ if (!instances.containsKey(instanceId)) {
349
+ promise.reject("InstanceNotFound", "No instance found with ID: " + instanceId);
350
+ return;
351
+ }
352
+ WritableMap out = Arguments.createMap();
264
353
  Float _thr = vadThresholdByInstance.get(instanceId);
265
354
  float thr = (_thr != null) ? _thr : DEFAULT_VAD_THRESHOLD;
266
355
  Integer _win = vadMsWindowByInstance.get(instanceId);
267
356
  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());
357
+ out.putDouble("threshold", (double) thr);
358
+ out.putInt("msWindow", win);
359
+ promise.resolve(out);
360
+ }
361
+
362
+ @ReactMethod
363
+ public void addListener(String eventName) {
364
+ // Set up any upstream listeners or background tasks as necessary
273
365
  }
274
- }
275
366
 
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;
367
+ @ReactMethod
368
+ public void removeListeners(Integer count) {
369
+ // Remove upstream listeners, stop unnecessary background tasks
282
370
  }
283
- try {
284
- instance.stopVADListening();
285
- promise.resolve("Stopped VAD for instance: " + instanceId);
286
- } catch (Exception e) {
287
- promise.reject("StopVADError", e.getMessage());
371
+
372
+ // Implement other methods as needed, ensuring to use instanceId
373
+ private String stripFileSchemeAndroid(String s) {
374
+ if (s == null) return null;
375
+ if (s.startsWith("file://")) return s.replace("file://", "");
376
+ return s;
288
377
  }
289
- }
290
378
 
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());
379
+ // ********** SPEAKER IDENTIFICATION APIs **********
380
+ // Resolves:
381
+ // - absolute filesystem path -> returns as-is
382
+ // - "asset:/name.onnx" or "assets:/name.onnx" -> copies from APK assets to cache, returns real path
383
+ // - "name.onnx" (bundle asset name) -> copies from assets to cache
384
+ private String resolveToRealFilePath(String input) throws IOException {
385
+ input = stripFileSchemeAndroid(input);
386
+ if (input == null || input.isEmpty()) return input;
387
+
388
+ // Absolute existing path
389
+ if (input.startsWith("/") && new File(input).exists()) {
390
+ return input;
391
+ }
392
+
393
+ // RN Android require(...) often yields "asset:/..."
394
+ String assetName = input;
395
+ if (assetName.startsWith("asset:/")) assetName = assetName.substring("asset:/".length());
396
+ if (assetName.startsWith("assets:/")) assetName = assetName.substring("assets:/".length());
397
+
398
+ // If still contains directories like "assets/.../name.onnx", keep last component
399
+ // (your iOS resolver scans bundle; Android assets are flat-ish but can be nested)
400
+ // We'll try direct open first; if fails, try lastPathComponent.
401
+ String try1 = assetName;
402
+ String try2 = new File(assetName).getName();
403
+
404
+ File out1 = copyAssetToCacheIfExists(try1);
405
+ if (out1 != null) return out1.getAbsolutePath();
406
+
407
+ File out2 = copyAssetToCacheIfExists(try2);
408
+ if (out2 != null) return out2.getAbsolutePath();
409
+
410
+ // Not found
411
+ throw new FileNotFoundException("Cannot resolve asset/path: " + input + " (tried " + try1 + " and " + try2 + ")");
306
412
  }
307
- }
308
413
 
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
- }
414
+ private File copyAssetToCacheIfExists(String assetName) {
415
+ if (assetName == null || assetName.isEmpty()) return null;
416
+ try (InputStream is = reactContext.getAssets().open(assetName)) {
417
+ File out = new File(reactContext.getCacheDir(), assetName);
418
+ // ensure parent
419
+ File parent = out.getParentFile();
420
+ if (parent != null) parent.mkdirs();
421
+
422
+ try (OutputStream os = new FileOutputStream(out)) {
423
+ byte[] buf = new byte[64 * 1024];
424
+ int n;
425
+ while ((n = is.read(buf)) > 0) os.write(buf, 0, n);
426
+ }
427
+ return out;
428
+ } catch (Throwable ignore) {
429
+ return null;
430
+ }
431
+ }
432
+
433
+ private SpeakerVerification.SpeakerVerificationConfig parseSVMicConfigJson(String configJson) throws JSONException, IOException {
434
+ JSONObject root = new JSONObject(configJson == null ? "{}" : configJson);
435
+
436
+ SpeakerVerification.SpeakerVerificationConfig cfg = new SpeakerVerification.SpeakerVerificationConfig();
437
+
438
+ // Top-level fields (if you use them)
439
+ String modelPath = root.optString("modelPath", "");
440
+ int sampleRate = root.optInt("sampleRate", cfg.sampleRate);
441
+ int frameSize = root.optInt("frameSize", cfg.frameSize);
442
+
443
+ // iOS-style puts most stuff in "options"
444
+ JSONObject opts = root.optJSONObject("options");
445
+ if (opts != null) {
446
+ cfg.decisionThreshold = (float) opts.optDouble("decisionThreshold", cfg.decisionThreshold);
447
+ cfg.tailSeconds = (float) opts.optDouble("tailSeconds", cfg.tailSeconds);
448
+ cfg.maxTailSeconds = (float) opts.optDouble("maxTailSeconds", cfg.maxTailSeconds);
449
+ cfg.cmn = opts.optBoolean("cmn", cfg.cmn);
450
+ cfg.expectedLayoutBDT = opts.optBoolean("expectedLayoutBDT", cfg.expectedLayoutBDT);
451
+
452
+ // Prefer opts.frameSize if present
453
+ frameSize = opts.has("frameSize") ? opts.optInt("frameSize", frameSize) : frameSize;
454
+ sampleRate = opts.has("sampleRate") ? opts.optInt("sampleRate", sampleRate) : sampleRate;
455
+ }
456
+
457
+ cfg.sampleRate = sampleRate > 0 ? sampleRate : 16000;
458
+ cfg.frameSize = frameSize > 0 ? frameSize : 1280;
459
+
460
+ // Resolve modelPath (MUST be real path for ORT)
461
+ if (modelPath != null && !modelPath.isEmpty()) {
462
+ cfg.modelPath = resolveToRealFilePath(modelPath);
463
+ } else {
464
+ // You can choose to throw here, but keeping consistent with iOS:
465
+ // iOS requires it; so Android should too.
466
+ throw new JSONException("Missing modelPath in configJson");
467
+ }
468
+ return cfg;
469
+ }
324
470
 
325
471
  @ReactMethod
326
- public void addListener(String eventName) {
327
- // Set up any upstream listeners or background tasks as necessary
472
+ public void createSpeakerVerifier(String engineId,
473
+ String modelPathOrName,
474
+ String enrollmentJsonPathOrName,
475
+ ReadableMap options,
476
+ Promise promise) {
477
+ if (svEngines.containsKey(engineId)) {
478
+ promise.reject("SVEngineExists", "Speaker verifier already exists with ID: " + engineId);
479
+ return;
480
+ }
481
+
482
+ new Thread(() -> {
483
+ try {
484
+ String modelPath = resolveToRealFilePath(modelPathOrName);
485
+ String jsonPath = resolveToRealFilePath(enrollmentJsonPathOrName);
486
+
487
+ // Build cfg from options (mirror iOS options)
488
+ SpeakerVerification.SpeakerVerificationConfig cfg = new SpeakerVerification.SpeakerVerificationConfig();
489
+ cfg.modelPath = modelPath;
490
+
491
+ if (options != null) {
492
+ if (options.hasKey("decisionThreshold")) cfg.decisionThreshold = (float) options.getDouble("decisionThreshold");
493
+ if (options.hasKey("frameSize")) cfg.frameSize = options.getInt("frameSize");
494
+ if (options.hasKey("tailSeconds")) cfg.tailSeconds = (float) options.getDouble("tailSeconds");
495
+ if (options.hasKey("maxTailSeconds")) cfg.maxTailSeconds = (float) options.getDouble("maxTailSeconds");
496
+ if (options.hasKey("cmn")) cfg.cmn = options.getBoolean("cmn");
497
+ if (options.hasKey("expectedLayoutBDT")) cfg.expectedLayoutBDT = options.getBoolean("expectedLayoutBDT");
498
+ }
499
+
500
+ // Load enrollment json file -> string -> enrollment object
501
+ String enrollmentJson = readAllText(jsonPath);
502
+ SpeakerVerification.SpeakerEnrollment enrollment = SpeakerVerification.SpeakerEnrollment.fromJson(enrollmentJson);
503
+
504
+ SpeakerVerification.SpeakerVerificationEngine engine = new SpeakerVerification.SpeakerVerificationEngine(cfg);
505
+ engine.setEnrollment(enrollment);
506
+
507
+ SVEngineHolder h = new SVEngineHolder();
508
+ h.engineId = engineId;
509
+ h.cfg = cfg;
510
+ h.enrollment = enrollment;
511
+ h.engine = engine;
512
+ svEngines.put(engineId, h);
513
+
514
+ WritableMap out = Arguments.createMap();
515
+ out.putBoolean("ok", true);
516
+ out.putString("engineId", engineId);
517
+ out.putString("modelPath", modelPath);
518
+ out.putString("enrollmentJsonPath", jsonPath);
519
+
520
+ promise.resolve(out);
521
+
522
+ } catch (Throwable t) {
523
+ promise.reject("SVCreateError", String.valueOf(t.getMessage()), t);
524
+ }
525
+ }).start();
526
+ }
527
+
528
+ private String readAllText(String path) throws IOException {
529
+ try (InputStream is = new FileInputStream(path)) {
530
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
531
+ byte[] buf = new byte[64 * 1024];
532
+ int n;
533
+ while ((n = is.read(buf)) > 0) bos.write(buf, 0, n);
534
+ return bos.toString("UTF-8");
535
+ }
536
+ }
537
+
538
+ @ReactMethod
539
+ public void verifySpeakerWavStreaming(String engineId,
540
+ String wavPathOrName,
541
+ boolean resetState,
542
+ Promise promise) {
543
+ SVEngineHolder h = svEngines.get(engineId);
544
+ if (h == null || h.engine == null) {
545
+ promise.reject("SVEngineNotFound", "No speaker verifier with ID: " + engineId);
546
+ return;
547
+ }
548
+
549
+ new Thread(() -> {
550
+ try {
551
+ String wavPath = resolveToRealFilePath(wavPathOrName);
552
+
553
+ if (resetState) {
554
+ h.engine.resetStreamingState();
555
+ }
556
+
557
+ SpeakerVerification.SpeakerVerificationResult res =
558
+ runWavThroughEngineStreaming(h.engine, wavPath, h.cfg.frameSize);
559
+
560
+ WritableMap out = Arguments.createMap();
561
+ out.putBoolean("ok", true);
562
+ out.putString("engineId", engineId);
563
+
564
+ // Mirror iOS-ish keys (your JS can read bestScore/score etc)
565
+ out.putDouble("scoreBest", res.scoreBest);
566
+ out.putDouble("scoreMean", res.scoreMean);
567
+ out.putDouble("scoreWorst", res.scoreWorst);
568
+ out.putBoolean("isMatch", res.isMatch);
569
+ out.putInt("embeddingDim", res.embeddingDim);
570
+ out.putDouble("usedSeconds", res.usedSeconds);
571
+
572
+ promise.resolve(out);
573
+
574
+ } catch (Throwable t) {
575
+ promise.reject("SVVerifyError", String.valueOf(t.getMessage()), t);
576
+ }
577
+ }).start();
578
+ }
579
+
580
+ private SpeakerVerification.SpeakerVerificationResult runWavThroughEngineStreaming(
581
+ SpeakerVerification.SpeakerVerificationEngine engine,
582
+ String wavPath,
583
+ int frameSizeSamples
584
+ ) throws Exception {
585
+
586
+ WavPcm16 wav = readWavPcm16Mono(wavPath);
587
+ if (wav.sampleRate != 16000) {
588
+ throw new IllegalArgumentException("WAV must be 16kHz. Got " + wav.sampleRate);
589
+ }
590
+
591
+ final short[] pcm = wav.pcm16;
592
+ int i = 0;
593
+
594
+ while (i + frameSizeSamples <= pcm.length) {
595
+ float[] f = new float[frameSizeSamples];
596
+ for (int k = 0; k < frameSizeSamples; k++) {
597
+ f[k] = pcm[i + k] / 32768.0f;
598
+ }
599
+ i += frameSizeSamples;
600
+
601
+ SpeakerVerification.SpeakerVerificationOutput out = engine.processFrame(f);
602
+ if (out != null && out.type == SpeakerVerification.SpeakerVerificationOutput.Type.RESULT) {
603
+ return out.result;
604
+ }
605
+ }
606
+
607
+ throw new IllegalStateException("NO_RESULT: WAV ended before engine produced RESULT");
608
+ }
609
+
610
+ // Minimal WAV reader: PCM16 LE, mono.
611
+ private static final class WavPcm16 {
612
+ int sampleRate;
613
+ short[] pcm16;
614
+ }
615
+
616
+ private WavPcm16 readWavPcm16Mono(String path) throws IOException {
617
+ try (InputStream is = new BufferedInputStream(new FileInputStream(path))) {
618
+ byte[] header = new byte[12];
619
+ readFully(is, header);
620
+
621
+ // "RIFF" .... "WAVE"
622
+ if (!(header[0]=='R' && header[1]=='I' && header[2]=='F' && header[3]=='F')) {
623
+ throw new IOException("Not RIFF WAV");
624
+ }
625
+ if (!(header[8]=='W' && header[9]=='A' && header[10]=='V' && header[11]=='E')) {
626
+ throw new IOException("Not WAVE");
627
+ }
628
+
629
+ int channels = -1, sampleRate = -1, bitsPerSample = -1;
630
+ ByteArrayOutputStream data = new ByteArrayOutputStream();
631
+
632
+ while (true) {
633
+ byte[] chunkHdr = new byte[8];
634
+ int n = is.read(chunkHdr);
635
+ if (n < 0) break;
636
+ if (n != 8) throw new IOException("Bad WAV chunk header");
637
+
638
+ String id = new String(chunkHdr, 0, 4);
639
+ int size = ByteBuffer.wrap(chunkHdr, 4, 4).order(ByteOrder.LITTLE_ENDIAN).getInt();
640
+
641
+ if ("fmt ".equals(id)) {
642
+ byte[] fmt = new byte[size];
643
+ readFully(is, fmt);
644
+
645
+ int audioFormat = ByteBuffer.wrap(fmt, 0, 2).order(ByteOrder.LITTLE_ENDIAN).getShort() & 0xFFFF;
646
+ channels = ByteBuffer.wrap(fmt, 2, 2).order(ByteOrder.LITTLE_ENDIAN).getShort() & 0xFFFF;
647
+ sampleRate = ByteBuffer.wrap(fmt, 4, 4).order(ByteOrder.LITTLE_ENDIAN).getInt();
648
+ bitsPerSample = ByteBuffer.wrap(fmt, 14, 2).order(ByteOrder.LITTLE_ENDIAN).getShort() & 0xFFFF;
649
+
650
+ if (audioFormat != 1) throw new IOException("WAV must be PCM (audioFormat=1). Got " + audioFormat);
651
+ } else if ("data".equals(id)) {
652
+ byte[] buf = new byte[64 * 1024];
653
+ int remaining = size;
654
+ while (remaining > 0) {
655
+ int r = is.read(buf, 0, Math.min(buf.length, remaining));
656
+ if (r < 0) throw new EOFException("WAV data truncated");
657
+ data.write(buf, 0, r);
658
+ remaining -= r;
659
+ }
660
+ } else {
661
+ // skip other chunks
662
+ long skipped = is.skip(size);
663
+ while (skipped < size) {
664
+ long s = is.skip(size - skipped);
665
+ if (s <= 0) break;
666
+ skipped += s;
667
+ }
668
+ }
669
+
670
+ // chunks are word-aligned
671
+ if ((size & 1) == 1) is.skip(1);
672
+ }
673
+
674
+ if (channels != 1) throw new IOException("WAV must be mono. Got channels=" + channels);
675
+ if (bitsPerSample != 16) throw new IOException("WAV must be 16-bit. Got bits=" + bitsPerSample);
676
+ if (sampleRate <= 0) throw new IOException("Missing/invalid sampleRate");
677
+
678
+ byte[] raw = data.toByteArray();
679
+ short[] pcm16 = new short[raw.length / 2];
680
+ ByteBuffer bb = ByteBuffer.wrap(raw).order(ByteOrder.LITTLE_ENDIAN);
681
+ for (int i = 0; i < pcm16.length; i++) pcm16[i] = bb.getShort();
682
+
683
+ WavPcm16 out = new WavPcm16();
684
+ out.sampleRate = sampleRate;
685
+ out.pcm16 = pcm16;
686
+ return out;
687
+ }
688
+ }
689
+
690
+ private void readFully(InputStream is, byte[] buf) throws IOException {
691
+ int off = 0;
692
+ while (off < buf.length) {
693
+ int n = is.read(buf, off, buf.length - off);
694
+ if (n < 0) throw new EOFException("Unexpected EOF");
695
+ off += n;
696
+ }
328
697
  }
329
698
 
330
699
  @ReactMethod
331
- public void removeListeners(Integer count) {
332
- // Remove upstream listeners, stop unnecessary background tasks
700
+ public void destroySpeakerVerifier(String engineId, Promise promise) {
701
+ SVEngineHolder h = svEngines.remove(engineId);
702
+ if (h == null) {
703
+ promise.reject("SVEngineNotFound", "No speaker verifier with ID: " + engineId);
704
+ return;
705
+ }
706
+ WritableMap out = Arguments.createMap();
707
+ out.putBoolean("ok", true);
708
+ out.putString("engineId", engineId);
709
+ promise.resolve(out);
710
+ }
711
+
712
+ private WritableMap toWritableMap(Map<String, Object> m) {
713
+ WritableMap out = Arguments.createMap();
714
+ if (m == null) return out;
715
+
716
+ for (Map.Entry<String, Object> e : m.entrySet()) {
717
+ String k = e.getKey();
718
+ Object v = e.getValue();
719
+ if (v == null) continue;
720
+
721
+ if (v instanceof String) out.putString(k, (String) v);
722
+ else if (v instanceof Boolean) out.putBoolean(k, (Boolean) v);
723
+ else if (v instanceof Integer) out.putInt(k, (Integer) v);
724
+ else if (v instanceof Long) out.putDouble(k, ((Long) v).doubleValue());
725
+ else if (v instanceof Float) out.putDouble(k, ((Float) v).doubleValue());
726
+ else if (v instanceof Double) out.putDouble(k, (Double) v);
727
+ else out.putString(k, String.valueOf(v));
728
+ }
729
+ return out;
730
+ }
731
+
732
+ private void sendEventUi(String eventName, WritableMap params) {
733
+ final ReactApplicationContext rc = reactContext;
734
+ if (rc == null) return;
735
+ if (!rc.hasActiveCatalystInstance()) return;
736
+ rc.runOnUiQueueThread(() -> {
737
+ try {
738
+ if (!rc.hasActiveCatalystInstance()) return;
739
+ rc.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
740
+ .emit(eventName, params);
741
+ } catch (Throwable t) {
742
+ // don't crash native if RN is tearing down
743
+ Log.w(SV_TAG, "sendEventUi failed event=" + eventName + " err=" + t);
744
+ }
745
+ });
746
+ }
747
+ @ReactMethod
748
+ public void createSpeakerVerificationMicController(String controllerId,
749
+ String configJson,
750
+ Promise promise) {
751
+ if (svMicControllers.containsKey(controllerId)) {
752
+ promise.reject("SVMicExists", "Speaker mic controller already exists with ID: " + controllerId);
753
+ return;
754
+ }
755
+
756
+ new Thread(() -> {
757
+ try {
758
+ SpeakerVerification.SpeakerVerificationConfig cfg = parseSVMicConfigJson(configJson);
759
+
760
+ SpeakerVerification.SpeakerVerificationMicController ctrl =
761
+ new SpeakerVerification.SpeakerVerificationMicController(reactContext, cfg);
762
+
763
+ // Delegate proxy -> RN events
764
+ ctrl.delegate = new SpeakerVerification.SpeakerVerificationNativeDelegate() {
765
+ @Override
766
+ public void svOnboardingProgress(Map<String, Object> info) {
767
+ Map<String, Object> m = info == null ? new HashMap<>() : new HashMap<>(info);
768
+ m.put("controllerId", controllerId);
769
+ sendEventUi("onSpeakerVerificationOnboardingProgress", toWritableMap(m));
770
+ }
771
+
772
+ @Override
773
+ public void svOnboardingDone(Map<String, Object> info) {
774
+ Map<String, Object> m = info == null ? new HashMap<>() : new HashMap<>(info);
775
+ m.put("controllerId", controllerId);
776
+ sendEventUi("onSpeakerVerificationOnboardingDone", toWritableMap(m));
777
+ }
778
+
779
+ @Override
780
+ public void svVerifyResult(Map<String, Object> info) {
781
+ Map<String, Object> m = info == null ? new HashMap<>() : new HashMap<>(info);
782
+ m.put("controllerId", controllerId);
783
+ sendEventUi("onSpeakerVerificationVerifyResult", toWritableMap(m));
784
+ }
785
+
786
+ @Override
787
+ public void svError(Map<String, Object> info) {
788
+ Map<String, Object> m = info == null ? new HashMap<>() : new HashMap<>(info);
789
+ m.put("controllerId", controllerId);
790
+ sendEventUi("onSpeakerVerificationError", toWritableMap(m));
791
+ }
792
+ };
793
+
794
+ svMicControllers.put(controllerId, ctrl);
795
+
796
+ WritableMap out = Arguments.createMap();
797
+ out.putBoolean("ok", true);
798
+ out.putString("controllerId", controllerId);
799
+ promise.resolve(out);
800
+
801
+ } catch (Throwable t) {
802
+ promise.reject("SVMicCreateError", String.valueOf(t.getMessage()), t);
803
+ }
804
+ }).start();
805
+ }
806
+ @ReactMethod
807
+ public void destroySpeakerVerificationMicController(String controllerId, Promise promise) {
808
+ SpeakerVerification.SpeakerVerificationMicController ctrl = svMicControllers.get(controllerId);
809
+ if (ctrl == null) {
810
+ promise.reject("SVMicNotFound", "No speaker mic controller with ID: " + controllerId);
811
+ return;
812
+ }
813
+
814
+ final int job = svJobN.incrementAndGet();
815
+ svExec.execute(() -> {
816
+ try {
817
+ try { ctrl.stop(); } catch (Throwable ignore) {}
818
+ svMicControllers.remove(controllerId); // remove only after stop
819
+ WritableMap out = Arguments.createMap();
820
+ out.putBoolean("ok", true);
821
+ out.putString("controllerId", controllerId);
822
+ out.putInt("job", job);
823
+ promise.resolve(out);
824
+ } catch (Throwable t) {
825
+ promise.reject("SVMicDestroyError", String.valueOf(t.getMessage()), t);
826
+ }
827
+ });
828
+ }
829
+
830
+ @ReactMethod
831
+ public void svBeginOnboarding(String controllerId,
832
+ String enrollmentId,
833
+ int targetEmbeddingCount,
834
+ boolean reset,
835
+ Promise promise) {
836
+ SpeakerVerification.SpeakerVerificationMicController ctrl = svMicControllers.get(controllerId);
837
+ if (ctrl == null) {
838
+ promise.reject("SVMicNotFound", "No speaker mic controller with ID: " + controllerId);
839
+ return;
840
+ }
841
+
842
+ final int job = svJobN.incrementAndGet();
843
+ svExec.execute(() -> {
844
+ try {
845
+ ctrl.beginOnboarding(enrollmentId, targetEmbeddingCount, reset);
846
+ WritableMap out = Arguments.createMap();
847
+ out.putBoolean("ok", true);
848
+ out.putString("controllerId", controllerId);
849
+ out.putString("enrollmentId", enrollmentId);
850
+ out.putInt("target", targetEmbeddingCount);
851
+ out.putInt("job", job);
852
+ promise.resolve(out);
853
+ } catch (Throwable t) {
854
+ promise.reject("SVMicBeginError", String.valueOf(t.getMessage()), t);
855
+ }
856
+ });
857
+ }
858
+
859
+ @ReactMethod
860
+ public void svGetNextEmbeddingFromMic(String controllerId, Promise promise) {
861
+ SpeakerVerification.SpeakerVerificationMicController ctrl = svMicControllers.get(controllerId);
862
+ if (ctrl == null) {
863
+ promise.reject("SVMicNotFound", "No speaker mic controller with ID: " + controllerId);
864
+ return;
865
+ }
866
+
867
+ final int job = svJobN.incrementAndGet();
868
+ svExec.execute(() -> {
869
+ try {
870
+ ctrl.getNextEmbeddingFromMic();
871
+ WritableMap out = Arguments.createMap();
872
+ out.putBoolean("ok", true);
873
+ out.putString("controllerId", controllerId);
874
+ out.putInt("job", job);
875
+ promise.resolve(out);
876
+ } catch (Throwable t) {
877
+ promise.reject("SVMicGetNextError", String.valueOf(t.getMessage()), t);
878
+ }
879
+ });
880
+
881
+ }
882
+
883
+ @ReactMethod
884
+ public void svFinalizeOnboardingNow(String controllerId, Promise promise) {
885
+ SpeakerVerification.SpeakerVerificationMicController ctrl = svMicControllers.get(controllerId);
886
+ if (ctrl == null) {
887
+ promise.reject("SVMicNotFound", "No speaker mic controller with ID: " + controllerId);
888
+ return;
889
+ }
890
+
891
+ final int job = svJobN.incrementAndGet();
892
+ svExec.execute(() -> {
893
+ try {
894
+ ctrl.finalizeOnboardingNow();
895
+ WritableMap out = Arguments.createMap();
896
+ out.putBoolean("ok", true);
897
+ out.putString("controllerId", controllerId);
898
+ out.putInt("job", job);
899
+ promise.resolve(out);
900
+ } catch (Throwable t) {
901
+ promise.reject("SVMicFinalizeError", String.valueOf(t.getMessage()), t);
902
+ }
903
+ });
904
+ }
905
+
906
+ @ReactMethod
907
+ public void svSetEnrollmentJson(String controllerId, String enrollmentJson, Promise promise) {
908
+ SpeakerVerification.SpeakerVerificationMicController ctrl = svMicControllers.get(controllerId);
909
+ if (ctrl == null) {
910
+ promise.reject("SVMicNotFound", "No speaker mic controller with ID: " + controllerId);
911
+ return;
912
+ }
913
+ final int job = svJobN.incrementAndGet();
914
+ svExec.execute(() -> {
915
+ try {
916
+ ctrl.setEnrollmentJson(enrollmentJson);
917
+ WritableMap out = Arguments.createMap();
918
+ out.putBoolean("ok", true);
919
+ out.putString("controllerId", controllerId);
920
+ out.putInt("job", job);
921
+ promise.resolve(out);
922
+ } catch (Throwable t) {
923
+ promise.reject("SVMicSetEnrollError", String.valueOf(t.getMessage()), t);
924
+ }
925
+ });
926
+
927
+ }
928
+
929
+ @ReactMethod
930
+ public void svStartVerifyFromMic(String controllerId, boolean resetState, Promise promise) {
931
+ SpeakerVerification.SpeakerVerificationMicController ctrl = svMicControllers.get(controllerId);
932
+ if (ctrl == null) {
933
+ promise.reject("SVMicNotFound", "No speaker mic controller with ID: " + controllerId);
934
+ return;
935
+ }
936
+
937
+ final int job = svJobN.incrementAndGet();
938
+ svExec.execute(() -> {
939
+ try {
940
+ ctrl.startVerifyFromMic(resetState);
941
+ WritableMap out = Arguments.createMap();
942
+ out.putBoolean("ok", true);
943
+ out.putString("controllerId", controllerId);
944
+ out.putBoolean("resetState", resetState);
945
+ out.putInt("job", job);
946
+ promise.resolve(out);
947
+ } catch (Throwable t) {
948
+ promise.reject("SVMicStartVerifyError", String.valueOf(t.getMessage()), t);
949
+ }
950
+ });
951
+
952
+ }
953
+
954
+ @ReactMethod
955
+ public void svStartEndlessVerifyFromMic(String controllerId, double hopSeconds, boolean stopOnMatch, boolean resetState, Promise promise) {
956
+ SpeakerVerification.SpeakerVerificationMicController ctrl = svMicControllers.get(controllerId);
957
+ if (ctrl == null) {
958
+ promise.reject("SVMicNotFound", "No speaker mic controller with ID: " + controllerId);
959
+ return;
960
+ }
961
+
962
+ final int job = svJobN.incrementAndGet();
963
+ svExec.execute(() -> {
964
+ try {
965
+ ctrl.startEndlessVerifyFromMic((float) hopSeconds, stopOnMatch, resetState);
966
+ WritableMap out = Arguments.createMap();
967
+ out.putBoolean("ok", true);
968
+ out.putString("controllerId", controllerId);
969
+ out.putDouble("hopSeconds", hopSeconds);
970
+ out.putBoolean("stopOnMatch", stopOnMatch);
971
+ out.putBoolean("resetState", resetState);
972
+ out.putInt("job", job);
973
+ promise.resolve(out);
974
+ } catch (Throwable t) {
975
+ promise.reject("SVMicStartEndlessVerifyError", String.valueOf(t.getMessage()), t);
976
+ }
977
+ });
978
+ }
979
+
980
+ @ReactMethod
981
+ public void svStopMic(String controllerId, Promise promise) {
982
+ SpeakerVerification.SpeakerVerificationMicController ctrl = svMicControllers.get(controllerId);
983
+ if (ctrl == null) {
984
+ promise.reject("SVMicNotFound", "No speaker mic controller with ID: " + controllerId);
985
+ return;
986
+ }
987
+
988
+ final int job = svJobN.incrementAndGet();
989
+ svExec.execute(() -> {
990
+ try {
991
+ ctrl.stop();
992
+ WritableMap out = Arguments.createMap();
993
+ out.putBoolean("ok", true);
994
+ out.putString("controllerId", controllerId);
995
+ out.putInt("job", job);
996
+ promise.resolve(out);
997
+ } catch (Throwable t) {
998
+ promise.reject("SVMicStopError", String.valueOf(t.getMessage()), t);
999
+ }
1000
+ });
1001
+
1002
+ }
1003
+ @ReactMethod
1004
+ public void startVerifyContinuousFromMic(String controllerId, boolean resetState, double hopSeconds, Promise promise) {
1005
+ // parity: continuous verify == endless verify
1006
+ // choose stopOnMatch=false by default (continuous)
1007
+ svStartEndlessVerifyFromMic(controllerId, hopSeconds, false, resetState, promise);
1008
+ }
1009
+
1010
+ @ReactMethod
1011
+ public void stopVerifyContinuousFromMic(String controllerId, Promise promise) {
1012
+ svStopMic(controllerId, promise);
333
1013
  }
334
- // Implement other methods as needed, ensuring to use instanceId
335
1014
  }