react-native-wakeword 1.1.77 → 1.1.79
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.
- package/ios/KeyWordRNBridge/KeyWordRNBridge.m +54 -0
- package/package.json +1 -1
- package/wakewords/KeyWordRNBridge.d.ts +2 -0
- package/wakewords/KeyWordRNBridge.js +8 -0
- package/android/src/main/java/com/davoice/speakeridrn/SpeakerIdRNBridge.java_not_used_yet +0 -588
- package/ios/KeyWordRNBridge/KeyWordRNBridge.mm +0 -416
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//ios/KeyWordRNBridge.m
|
|
2
2
|
|
|
3
3
|
#import "KeyWordRNBridge.h"
|
|
4
|
+
#import <AVFoundation/AVFoundation.h>
|
|
4
5
|
#import <React/RCTBridge.h>
|
|
5
6
|
#import <React/RCTLog.h>
|
|
6
7
|
#import <React/RCTEventEmitter.h>
|
|
@@ -383,6 +384,11 @@ static NSDictionary * _Nullable SVVerifyWav(id engine, NSString *wavPath, BOOL r
|
|
|
383
384
|
return msgSend(c, sel, engine, wavPath, reset, error);
|
|
384
385
|
}
|
|
385
386
|
|
|
387
|
+
static BOOL KWRNBHasMicPermission(void) {
|
|
388
|
+
AVAudioSessionRecordPermission permission = [[AVAudioSession sharedInstance] recordPermission];
|
|
389
|
+
return permission == AVAudioSessionRecordPermissionGranted;
|
|
390
|
+
}
|
|
391
|
+
|
|
386
392
|
@interface KeyWordRNBridge () <RCTBridgeModule>
|
|
387
393
|
|
|
388
394
|
@property (nonatomic, strong) NSMutableDictionary *instances;
|
|
@@ -913,6 +919,54 @@ RCT_EXPORT_METHOD(disableDuckingAndCleanup:(RCTPromiseResolveBlock)resolve rejec
|
|
|
913
919
|
resolve(@"disabled");
|
|
914
920
|
}
|
|
915
921
|
|
|
922
|
+
RCT_EXPORT_METHOD(hasMicPermissions:(RCTPromiseResolveBlock)resolve
|
|
923
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
924
|
+
{
|
|
925
|
+
resolve(@(KWRNBHasMicPermission()));
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
RCT_EXPORT_METHOD(requestMicPermissions:(nonnull NSNumber *)wait_timeout
|
|
929
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
930
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
931
|
+
{
|
|
932
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
933
|
+
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
|
|
934
|
+
AVAudioSessionRecordPermission permission = audioSession.recordPermission;
|
|
935
|
+
if (permission == AVAudioSessionRecordPermissionGranted) {
|
|
936
|
+
resolve(@YES);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (permission == AVAudioSessionRecordPermissionDenied) {
|
|
941
|
+
resolve(@NO);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
NSTimeInterval timeoutSeconds = MAX(wait_timeout.doubleValue, 0.0) / 1000.0;
|
|
946
|
+
__block BOOL didResolve = NO;
|
|
947
|
+
void (^finish)(BOOL) = ^(BOOL granted) {
|
|
948
|
+
if (didResolve) {
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
didResolve = YES;
|
|
952
|
+
resolve(@(granted));
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
if (timeoutSeconds > 0.0) {
|
|
956
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutSeconds * NSEC_PER_SEC)),
|
|
957
|
+
dispatch_get_main_queue(), ^{
|
|
958
|
+
finish(KWRNBHasMicPermission());
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
[audioSession requestRecordPermission:^(BOOL granted) {
|
|
963
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
964
|
+
finish(granted);
|
|
965
|
+
});
|
|
966
|
+
}];
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
|
|
916
970
|
RCT_EXPORT_METHOD(setKeywordDetectionLicense:(NSString *)instanceId licenseKey:(NSString *)licenseKey resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
917
971
|
{
|
|
918
972
|
KeyWordsDetectionWrapper *wrapper = self.instances[instanceId];
|
package/package.json
CHANGED
|
@@ -47,5 +47,7 @@ export function enableDucking(): Promise<void>;
|
|
|
47
47
|
export function disableDucking(): Promise<void>;
|
|
48
48
|
export function restartListeningAfterDucking(): Promise<void>;
|
|
49
49
|
export function initAudioSessAndDuckManage(): Promise<void>;
|
|
50
|
+
export function hasIOSMicPermissions(): Promise<any>;
|
|
51
|
+
export function requestIOSMicPermissions(wait_timeout: any): Promise<any>;
|
|
50
52
|
export function disableDuckingAndCleanup(): Promise<void>;
|
|
51
53
|
export function setWakewordAudioRoutingConfig(config: any): Promise<any>;
|
|
@@ -275,6 +275,14 @@ export const initAudioSessAndDuckManage = async () => {
|
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
export const hasIOSMicPermissions = async () => {
|
|
279
|
+
return await KeyWordRNBridge.hasMicPermissions();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export const requestIOSMicPermissions = async (wait_timeout) => {
|
|
283
|
+
return await KeyWordRNBridge.requestMicPermissions(wait_timeout);
|
|
284
|
+
}
|
|
285
|
+
|
|
278
286
|
export const disableDuckingAndCleanup = async () => {
|
|
279
287
|
if (Platform.OS === 'ios') {
|
|
280
288
|
await KeyWordRNBridge.disableDuckingAndCleanup();
|
|
@@ -1,588 +0,0 @@
|
|
|
1
|
-
package com.davoice.speakeridrn;
|
|
2
|
-
|
|
3
|
-
import com.facebook.react.bridge.*;
|
|
4
|
-
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
|
5
|
-
import androidx.annotation.Nullable;
|
|
6
|
-
|
|
7
|
-
import android.content.Context;
|
|
8
|
-
import android.net.Uri;
|
|
9
|
-
import android.os.Handler;
|
|
10
|
-
import android.os.Looper;
|
|
11
|
-
|
|
12
|
-
import ai.onnxruntime.OrtEnvironment;
|
|
13
|
-
import ai.onnxruntime.OrtException;
|
|
14
|
-
|
|
15
|
-
import com.davoice.speakerid.*;
|
|
16
|
-
|
|
17
|
-
import java.io.File;
|
|
18
|
-
import java.util.HashMap;
|
|
19
|
-
import java.util.Map;
|
|
20
|
-
import java.util.concurrent.*;
|
|
21
|
-
import android.content.res.AssetManager;
|
|
22
|
-
import java.io.InputStream;
|
|
23
|
-
import java.io.BufferedInputStream;
|
|
24
|
-
import java.io.ByteArrayOutputStream;
|
|
25
|
-
import java.io.IOException;
|
|
26
|
-
import java.util.Locale;
|
|
27
|
-
import android.util.Log;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* React Native bridge for the speaker-id Android library.
|
|
31
|
-
* It wraps SpeakerIdApi: onboarding (mic/stream/wav) + verification (mic/stream/wav).
|
|
32
|
-
*/
|
|
33
|
-
public class SpeakerIdRNBridge extends ReactContextBaseJavaModule {
|
|
34
|
-
|
|
35
|
-
private static final String REACT_CLASS = "SpeakerIdRNBridge";
|
|
36
|
-
private static ReactApplicationContext reactContext;
|
|
37
|
-
|
|
38
|
-
// Keep multiple instances if you want (keyed by id)
|
|
39
|
-
private final Map<String, SpeakerIdApi> instances = new HashMap<>();
|
|
40
|
-
|
|
41
|
-
// Streaming onboarding per instance
|
|
42
|
-
private final Map<String, SpeakerIdApi.OnboardingStream> onboardingStreams = new HashMap<>();
|
|
43
|
-
|
|
44
|
-
// Run heavy calls off the UI thread
|
|
45
|
-
private final ExecutorService exec = Executors.newCachedThreadPool();
|
|
46
|
-
private final Handler main = new Handler(Looper.getMainLooper());
|
|
47
|
-
|
|
48
|
-
public SpeakerIdRNBridge(ReactApplicationContext context) {
|
|
49
|
-
super(context);
|
|
50
|
-
reactContext = context;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
@Override public String getName() { return REACT_CLASS; }
|
|
54
|
-
|
|
55
|
-
// -------------------- Helpers --------------------
|
|
56
|
-
|
|
57
|
-
private static WritableMap toJs(OnboardingResult r) {
|
|
58
|
-
WritableMap m = Arguments.createMap();
|
|
59
|
-
m.putInt("clusterSize", r.clusterSize);
|
|
60
|
-
m.putInt("embeddingDim", r.embDim);
|
|
61
|
-
return m;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
private static WritableMap toJs(VerificationResult r) {
|
|
65
|
-
WritableMap m = Arguments.createMap();
|
|
66
|
-
m.putDouble("fullSec", r.fullSec);
|
|
67
|
-
m.putDouble("voicedSec", r.voicedSec);
|
|
68
|
-
m.putDouble("bestScore", r.bestScore);
|
|
69
|
-
m.putString("bestStrategy", r.bestStrategy);
|
|
70
|
-
m.putString("bestTargetLabel", r.bestTargetLabel);
|
|
71
|
-
|
|
72
|
-
// Flatten perTargetStrategy -> { strategyKey: { label: score } }
|
|
73
|
-
WritableMap strat = Arguments.createMap();
|
|
74
|
-
if (r.perTargetStrategy != null) {
|
|
75
|
-
for (Map.Entry<String, Map<String, Float>> e : r.perTargetStrategy.entrySet()) {
|
|
76
|
-
WritableMap col = Arguments.createMap();
|
|
77
|
-
for (Map.Entry<String, Float> colE : e.getValue().entrySet()) {
|
|
78
|
-
col.putDouble(colE.getKey(), colE.getValue());
|
|
79
|
-
}
|
|
80
|
-
strat.putMap(e.getKey(), col);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
m.putMap("perTargetStrategy", strat);
|
|
84
|
-
return m;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
private static File fileFromPath(String path) {
|
|
88
|
-
return new File(path);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// -------------------- Lifecycle / Creation --------------------
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Create a SpeakerIdApi instance.
|
|
95
|
-
* @param instanceId unique id you choose
|
|
96
|
-
*/
|
|
97
|
-
@ReactMethod
|
|
98
|
-
public void createInstance(String instanceId, Promise promise) {
|
|
99
|
-
if (instances.containsKey(instanceId)) {
|
|
100
|
-
promise.reject("InstanceExists", "Instance already exists: " + instanceId);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
exec.submit(() -> {
|
|
104
|
-
try {
|
|
105
|
-
SpeakerIdApi api = SpeakerIdApi.create(reactContext); // <-- resolves speaker_id.dm + layer1.dm internally
|
|
106
|
-
synchronized (instances) { instances.put(instanceId, api); }
|
|
107
|
-
main.post(() -> promise.resolve(true));
|
|
108
|
-
} catch (Exception e) {
|
|
109
|
-
main.post(() -> promise.reject("CreateError", e.getMessage(), e));
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
@ReactMethod
|
|
115
|
-
public void initVerificationUsingCurrentConfig(String instanceId, Promise promise) {
|
|
116
|
-
SpeakerIdApi api = instances.get(instanceId);
|
|
117
|
-
if (api == null) { promise.reject("InstanceNotFound", instanceId); return; }
|
|
118
|
-
try {
|
|
119
|
-
boolean ok = api.initVerificationUsingCurrentConfig();
|
|
120
|
-
promise.resolve(ok);
|
|
121
|
-
} catch (Throwable t) {
|
|
122
|
-
promise.reject("E_INIT_CHECK", t);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ======= External-audio cluster API (RN) =======
|
|
127
|
-
|
|
128
|
-
@ReactMethod
|
|
129
|
-
public void initCluster(String instanceId, int numOfEmb, Promise promise) {
|
|
130
|
-
SpeakerIdApi api = instances.get(instanceId);
|
|
131
|
-
if (api == null) { promise.reject("InstanceNotFound", instanceId); return; }
|
|
132
|
-
exec.submit(() -> {
|
|
133
|
-
try {
|
|
134
|
-
int cid = api.initCluster(numOfEmb);
|
|
135
|
-
main.post(() -> promise.resolve(cid));
|
|
136
|
-
} catch (Throwable t) {
|
|
137
|
-
main.post(() -> promise.reject("InitClusterError", t.getMessage(), t));
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
@ReactMethod
|
|
143
|
-
public void createAndPushEmbeddingsToCluster(String instanceId, int clusterId, ReadableArray pcmI16, int length, Promise promise) {
|
|
144
|
-
SpeakerIdApi api = instances.get(instanceId);
|
|
145
|
-
if (api == null) { promise.reject("InstanceNotFound", instanceId); return; }
|
|
146
|
-
// Convert RN int[] -> short[]
|
|
147
|
-
int n = Math.min(length, pcmI16.size());
|
|
148
|
-
final short[] block = new short[n];
|
|
149
|
-
for (int i = 0; i < n; i++) block[i] = (short) pcmI16.getInt(i);
|
|
150
|
-
|
|
151
|
-
exec.submit(() -> {
|
|
152
|
-
try {
|
|
153
|
-
api.createAndPushEmbeddingsToCluster(clusterId, block, n);
|
|
154
|
-
main.post(() -> promise.resolve(null));
|
|
155
|
-
} catch (Throwable t) {
|
|
156
|
-
main.post(() -> promise.reject("PushClusterError", t.getMessage(), t));
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
@ReactMethod
|
|
162
|
-
public void createAndVerifyEmbeddingsFromCluster(String instanceId, int clusterId, ReadableArray pcmI16, int length, Promise promise) {
|
|
163
|
-
SpeakerIdApi api = instances.get(instanceId);
|
|
164
|
-
if (api == null) { promise.reject("InstanceNotFound", instanceId); return; }
|
|
165
|
-
int n = Math.min(length, pcmI16.size());
|
|
166
|
-
final short[] block = new short[n];
|
|
167
|
-
for (int i = 0; i < n; i++) block[i] = (short) pcmI16.getInt(i);
|
|
168
|
-
|
|
169
|
-
exec.submit(() -> {
|
|
170
|
-
try {
|
|
171
|
-
float score = api.createAndVerifyEmbeddingsFromCluster(clusterId, block, n);
|
|
172
|
-
main.post(() -> promise.resolve(score));
|
|
173
|
-
} catch (Throwable t) {
|
|
174
|
-
main.post(() -> promise.reject("VerifyClusterError", t.getMessage(), t));
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
@ReactMethod
|
|
180
|
-
public void destroyInstance(String instanceId, Promise promise) {
|
|
181
|
-
SpeakerIdApi api;
|
|
182
|
-
synchronized (instances) { api = instances.remove(instanceId); }
|
|
183
|
-
if (api != null) {
|
|
184
|
-
try { api.close(); } catch (Exception ignore) {}
|
|
185
|
-
onboardingStreams.remove(instanceId);
|
|
186
|
-
promise.resolve(true);
|
|
187
|
-
} else {
|
|
188
|
-
promise.reject("InstanceNotFound", "No such instance: " + instanceId);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// -------------------- Verification init (targets) --------------------
|
|
193
|
-
|
|
194
|
-
@ReactMethod
|
|
195
|
-
public void initVerificationUsingDefaults(String instanceId, Promise promise) {
|
|
196
|
-
SpeakerIdApi api = instances.get(instanceId);
|
|
197
|
-
if (api == null) { promise.reject("InstanceNotFound", instanceId); return; }
|
|
198
|
-
promise.resolve(api.initVerificationUsingDefaults(reactContext));
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Use explicit files for mean & cluster (paths to .npy files).
|
|
203
|
-
*/
|
|
204
|
-
@ReactMethod
|
|
205
|
-
public void initVerificationWithFiles(String instanceId,
|
|
206
|
-
String meanEmbPath,
|
|
207
|
-
String clusterPath,
|
|
208
|
-
Promise promise) {
|
|
209
|
-
SpeakerIdApi api = instances.get(instanceId);
|
|
210
|
-
if (api == null) { promise.reject("InstanceNotFound", instanceId); return; }
|
|
211
|
-
boolean ok = api.initVerificationWithFiles(fileFromPath(meanEmbPath),
|
|
212
|
-
fileFromPath(clusterPath));
|
|
213
|
-
promise.resolve(ok);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// -------------------- Onboarding: Microphone --------------------
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Opens mic, captures first VAD-delimited utterance, enrolls.
|
|
220
|
-
* JS must have RECORD_AUDIO permission granted beforehand.
|
|
221
|
-
* @param maxMillis timeout in ms
|
|
222
|
-
*/
|
|
223
|
-
@ReactMethod
|
|
224
|
-
public void onboardFromMicrophone(String instanceId, double maxMillis, Promise promise) {
|
|
225
|
-
SpeakerIdApi api = instances.get(instanceId);
|
|
226
|
-
if (api == null) { promise.reject("InstanceNotFound", instanceId); return; }
|
|
227
|
-
|
|
228
|
-
exec.submit(() -> {
|
|
229
|
-
try {
|
|
230
|
-
OnboardingResult r = api.onboardFromMicrophone((long)maxMillis);
|
|
231
|
-
WritableMap out = toJs(r);
|
|
232
|
-
main.post(() -> promise.resolve(out));
|
|
233
|
-
} catch (SecurityException se) {
|
|
234
|
-
main.post(() -> promise.reject("MicPermission", "RECORD_AUDIO permission missing", se));
|
|
235
|
-
} catch (Exception e) {
|
|
236
|
-
main.post(() -> promise.reject("OnboardMicError", e.getMessage(), e));
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// -------------------- Onboarding: Stream --------------------
|
|
242
|
-
|
|
243
|
-
@ReactMethod
|
|
244
|
-
public void startOnboardingStream(String instanceId, Promise promise) {
|
|
245
|
-
SpeakerIdApi api = instances.get(instanceId);
|
|
246
|
-
if (api == null) { promise.reject("InstanceNotFound", instanceId); return; }
|
|
247
|
-
SpeakerIdApi.OnboardingStream stream = api.startOnboardingStream();
|
|
248
|
-
onboardingStreams.put(instanceId, stream);
|
|
249
|
-
promise.resolve(true);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Feed PCM16 block (short[]) as a base64 string? RN doesn’t pass short[] directly.
|
|
254
|
-
* For simplicity here, pass an int array of 16-bit values (range -32768..32767).
|
|
255
|
-
*/
|
|
256
|
-
@ReactMethod
|
|
257
|
-
public void feedOnboardingStream(String instanceId, ReadableArray pcmI16, Promise promise) {
|
|
258
|
-
SpeakerIdApi.OnboardingStream stream = onboardingStreams.get(instanceId);
|
|
259
|
-
if (stream == null) { promise.reject("StreamNotStarted", instanceId); return; }
|
|
260
|
-
|
|
261
|
-
short[] block = new short[pcmI16.size()];
|
|
262
|
-
for (int i = 0; i < pcmI16.size(); i++) block[i] = (short) pcmI16.getInt(i);
|
|
263
|
-
|
|
264
|
-
exec.submit(() -> {
|
|
265
|
-
try {
|
|
266
|
-
OnboardingResult r = stream.feed(block);
|
|
267
|
-
if (r != null) {
|
|
268
|
-
onboardingStreams.remove(instanceId);
|
|
269
|
-
WritableMap out = toJs(r);
|
|
270
|
-
main.post(() -> promise.resolve(out));
|
|
271
|
-
} else {
|
|
272
|
-
main.post(() -> promise.resolve(null)); // not done yet
|
|
273
|
-
}
|
|
274
|
-
} catch (Exception e) {
|
|
275
|
-
main.post(() -> promise.reject("OnboardFeedError", e.getMessage(), e));
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
@ReactMethod
|
|
281
|
-
public void finishOnboardingStream(String instanceId, Promise promise) {
|
|
282
|
-
SpeakerIdApi.OnboardingStream stream = onboardingStreams.remove(instanceId);
|
|
283
|
-
if (stream == null) { promise.reject("StreamNotStarted", instanceId); return; }
|
|
284
|
-
exec.submit(() -> {
|
|
285
|
-
try {
|
|
286
|
-
OnboardingResult r = stream.finish();
|
|
287
|
-
WritableMap out = (r == null) ? null : toJs(r);
|
|
288
|
-
main.post(() -> promise.resolve(out));
|
|
289
|
-
} catch (Exception e) {
|
|
290
|
-
main.post(() -> promise.reject("OnboardFinishError", e.getMessage(), e));
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// -------------------- Onboarding: WAV --------------------
|
|
296
|
-
|
|
297
|
-
@ReactMethod
|
|
298
|
-
public void onboardFromWav(String instanceId, String wavPath, Promise promise) {
|
|
299
|
-
SpeakerIdApi api = instances.get(instanceId);
|
|
300
|
-
if (api == null) { promise.reject("InstanceNotFound", instanceId); return; }
|
|
301
|
-
|
|
302
|
-
exec.submit(() -> {
|
|
303
|
-
try {
|
|
304
|
-
OnboardingResult r = api.onboardFromWav(fileFromPath(wavPath));
|
|
305
|
-
WritableMap out = toJs(r);
|
|
306
|
-
main.post(() -> promise.resolve(out));
|
|
307
|
-
} catch (Exception e) {
|
|
308
|
-
main.post(() -> promise.reject("OnboardWavError", e.getMessage(), e));
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// -------------------- Verification: Microphone --------------------
|
|
314
|
-
|
|
315
|
-
@ReactMethod
|
|
316
|
-
public void verifyFromMicrophone(String instanceId, double maxMillis, Promise promise) {
|
|
317
|
-
SpeakerIdApi api = instances.get(instanceId);
|
|
318
|
-
if (api == null) { promise.reject("InstanceNotFound", instanceId); return; }
|
|
319
|
-
exec.submit(() -> {
|
|
320
|
-
try {
|
|
321
|
-
VerificationResult r = api.verifyFromMicrophone((long)maxMillis);
|
|
322
|
-
WritableMap out = (r == null) ? null : toJs(r);
|
|
323
|
-
main.post(() -> promise.resolve(out));
|
|
324
|
-
} catch (SecurityException se) {
|
|
325
|
-
main.post(() -> promise.reject("MicPermission", "RECORD_AUDIO permission missing", se));
|
|
326
|
-
} catch (Exception e) {
|
|
327
|
-
main.post(() -> promise.reject("VerifyMicError", e.getMessage(), e));
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// -------------------- Verification: Stream --------------------
|
|
333
|
-
|
|
334
|
-
@ReactMethod
|
|
335
|
-
public void verifyStreamPush(String instanceId, ReadableArray pcmI16, Promise promise) {
|
|
336
|
-
SpeakerIdApi api = instances.get(instanceId);
|
|
337
|
-
if (api == null) { promise.reject("InstanceNotFound", instanceId); return; }
|
|
338
|
-
short[] block = new short[pcmI16.size()];
|
|
339
|
-
for (int i = 0; i < pcmI16.size(); i++) block[i] = (short) pcmI16.getInt(i);
|
|
340
|
-
exec.submit(() -> {
|
|
341
|
-
try {
|
|
342
|
-
VerificationResult r = api.verifyStreamPush(block);
|
|
343
|
-
WritableMap out = (r == null) ? null : toJs(r);
|
|
344
|
-
main.post(() -> promise.resolve(out));
|
|
345
|
-
} catch (Exception e) {
|
|
346
|
-
main.post(() -> promise.reject("VerifyStreamPushError", e.getMessage(), e));
|
|
347
|
-
}
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
@ReactMethod
|
|
352
|
-
public void verifyStreamFinish(String instanceId, Promise promise) {
|
|
353
|
-
SpeakerIdApi api = instances.get(instanceId);
|
|
354
|
-
if (api == null) { promise.reject("InstanceNotFound", instanceId); return; }
|
|
355
|
-
exec.submit(() -> {
|
|
356
|
-
try {
|
|
357
|
-
VerificationResult r = api.verifyStreamFinish();
|
|
358
|
-
WritableMap out = (r == null) ? null : toJs(r);
|
|
359
|
-
main.post(() -> promise.resolve(out));
|
|
360
|
-
} catch (Exception e) {
|
|
361
|
-
main.post(() -> promise.reject("VerifyStreamFinishError", e.getMessage(), e));
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// -------------------- Verification: WAV --------------------
|
|
367
|
-
|
|
368
|
-
@ReactMethod
|
|
369
|
-
public void verifyFromWav(String instanceId, String wavPath, Promise promise) {
|
|
370
|
-
SpeakerIdApi api = instances.get(instanceId);
|
|
371
|
-
if (api == null) { promise.reject("InstanceNotFound", instanceId); return; }
|
|
372
|
-
exec.submit(() -> {
|
|
373
|
-
try {
|
|
374
|
-
VerificationResult r = api.verifyFromWav(fileFromPath(wavPath));
|
|
375
|
-
WritableMap out = (r == null) ? null : toJs(r);
|
|
376
|
-
main.post(() -> promise.resolve(out));
|
|
377
|
-
} catch (Exception e) {
|
|
378
|
-
main.post(() -> promise.reject("VerifyWavError", e.getMessage(), e));
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// ======= WWD: create instance tuned for wake-word =======
|
|
384
|
-
|
|
385
|
-
private static final boolean DEBUG_SID_ASSETS = true; // set false for production
|
|
386
|
-
|
|
387
|
-
@ReactMethod
|
|
388
|
-
public void createInstanceWWD(String instanceId, Promise promise) {
|
|
389
|
-
if (instances.containsKey(instanceId)) {
|
|
390
|
-
promise.reject("InstanceExists", "Instance already exists: " + instanceId);
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
Log.d("WWD DEBUG", "createInstanceWWD");
|
|
394
|
-
|
|
395
|
-
exec.submit(() -> {
|
|
396
|
-
try {
|
|
397
|
-
Log.d("WWD DEBUG", "createInstanceWWD2");
|
|
398
|
-
SpeakerIdApi api = SpeakerIdApi.createWWD(reactContext);
|
|
399
|
-
Log.d("WWD DEBUG", "createInstanceWWD3");
|
|
400
|
-
synchronized (instances) { instances.put(instanceId, api); }
|
|
401
|
-
Log.d("WWD DEBUG", "createInstanceWWD4");
|
|
402
|
-
main.post(() -> promise.resolve(true));
|
|
403
|
-
// Then run debug async if enabled
|
|
404
|
-
if (DEBUG_SID_ASSETS) {
|
|
405
|
-
Log.d("WWD DEBUG", "Calling debugOnAssetsWWD");
|
|
406
|
-
exec.submit(() -> debugOnAssetsWWD(instanceId));
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
} catch (Exception e) {
|
|
410
|
-
main.post(() -> promise.reject("CreateWWDError", e.getMessage(), e));
|
|
411
|
-
}
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// ======= WWD: onboard from mic (embNum x 1s) =======
|
|
416
|
-
@ReactMethod
|
|
417
|
-
public void onboardFromMicrophoneWWD(String instanceId, int embNum, double maxMillis, Promise promise) {
|
|
418
|
-
SpeakerIdApi api = instances.get(instanceId);
|
|
419
|
-
if (api == null) { promise.reject("InstanceNotFound", instanceId); return; }
|
|
420
|
-
exec.submit(() -> {
|
|
421
|
-
try {
|
|
422
|
-
OnboardingResult r = api.onboardFromMicrophoneWWD(embNum, (long)maxMillis);
|
|
423
|
-
WritableMap out = toJs(r);
|
|
424
|
-
main.post(() -> promise.resolve(out));
|
|
425
|
-
} catch (SecurityException se) {
|
|
426
|
-
main.post(() -> promise.reject("MicPermission", "RECORD_AUDIO permission missing", se));
|
|
427
|
-
} catch (Exception e) {
|
|
428
|
-
main.post(() -> promise.reject("OnboardWWDError", e.getMessage(), e));
|
|
429
|
-
}
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// ======= WWD: verify from mic (exactly 1s) =======
|
|
434
|
-
@ReactMethod
|
|
435
|
-
public void verifyFromMicrophoneWWD(String instanceId, double maxMillis, Promise promise) {
|
|
436
|
-
SpeakerIdApi api = instances.get(instanceId);
|
|
437
|
-
if (api == null) { promise.reject("InstanceNotFound", instanceId); return; }
|
|
438
|
-
exec.submit(() -> {
|
|
439
|
-
try {
|
|
440
|
-
VerificationResult r = api.verifyFromMicrophoneWWD((long)maxMillis);
|
|
441
|
-
WritableMap out = (r == null) ? null : toJs(r);
|
|
442
|
-
main.post(() -> promise.resolve(out));
|
|
443
|
-
} catch (SecurityException se) {
|
|
444
|
-
main.post(() -> promise.reject("MicPermission", "RECORD_AUDIO permission missing", se));
|
|
445
|
-
} catch (Exception e) {
|
|
446
|
-
main.post(() -> promise.reject("VerifyWWDError", e.getMessage(), e));
|
|
447
|
-
}
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// -------------------- Export helpers --------------------
|
|
452
|
-
|
|
453
|
-
@ReactMethod
|
|
454
|
-
public void exportDefaultClusterToDownloads(String instanceId, Promise promise) {
|
|
455
|
-
SpeakerIdApi api = instances.get(instanceId);
|
|
456
|
-
if (api == null) { promise.reject("InstanceNotFound", instanceId); return; }
|
|
457
|
-
exec.submit(() -> {
|
|
458
|
-
try {
|
|
459
|
-
Uri uri = api.exportDefaultClusterToDownloads(reactContext);
|
|
460
|
-
main.post(() -> promise.resolve(uri.toString()));
|
|
461
|
-
} catch (Exception e) {
|
|
462
|
-
main.post(() -> promise.reject("ExportClusterError", e.getMessage(), e));
|
|
463
|
-
}
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
@ReactMethod
|
|
468
|
-
public void exportDefaultMeanToDownloads(String instanceId, Promise promise) {
|
|
469
|
-
SpeakerIdApi api = instances.get(instanceId);
|
|
470
|
-
if (api == null) { promise.reject("InstanceNotFound", instanceId); return; }
|
|
471
|
-
exec.submit(() -> {
|
|
472
|
-
try {
|
|
473
|
-
Uri uri = api.exportDefaultMeanToDownloads(reactContext);
|
|
474
|
-
main.post(() -> promise.resolve(uri.toString()));
|
|
475
|
-
} catch (Exception e) {
|
|
476
|
-
main.post(() -> promise.reject("ExportMeanError", e.getMessage(), e));
|
|
477
|
-
}
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
@ReactMethod
|
|
482
|
-
public void exportDefaultMeanCountToDownloads(String instanceId, Promise promise) {
|
|
483
|
-
SpeakerIdApi api = instances.get(instanceId);
|
|
484
|
-
if (api == null) { promise.reject("InstanceNotFound", instanceId); return; }
|
|
485
|
-
exec.submit(() -> {
|
|
486
|
-
try {
|
|
487
|
-
Uri uri = api.exportDefaultMeanCountToDownloads(reactContext);
|
|
488
|
-
main.post(() -> promise.resolve(uri.toString()));
|
|
489
|
-
} catch (Exception e) {
|
|
490
|
-
main.post(() -> promise.reject("ExportMeanCountError", e.getMessage(), e));
|
|
491
|
-
}
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// RN event helpers (if you later want to emit async events)
|
|
496
|
-
private void sendEvent(String eventName, @Nullable WritableMap params) {
|
|
497
|
-
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
|
498
|
-
.emit(eventName, params);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// No-op listener hooks
|
|
502
|
-
@ReactMethod public void addListener(String eventName) {}
|
|
503
|
-
@ReactMethod public void removeListeners(Integer count) {}
|
|
504
|
-
|
|
505
|
-
@Override
|
|
506
|
-
public void onCatalystInstanceDestroy() {
|
|
507
|
-
for (SpeakerIdApi api : instances.values()) try { api.close(); } catch (Exception ignore) {}
|
|
508
|
-
instances.clear();
|
|
509
|
-
onboardingStreams.clear();
|
|
510
|
-
exec.shutdownNow();
|
|
511
|
-
super.onCatalystInstanceDestroy();
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// ======= DEBUG harness (assets) =======
|
|
515
|
-
|
|
516
|
-
private void emitDebug(String msg) {
|
|
517
|
-
Log.d("WWD DEBUG", msg);
|
|
518
|
-
WritableMap m = Arguments.createMap();
|
|
519
|
-
m.putString("line", msg);
|
|
520
|
-
sendEvent("SpeakerIdDebug", m);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
private void debugOnAssetsWWD(String instanceId) {
|
|
524
|
-
try {
|
|
525
|
-
SpeakerIdApi api = instances.get(instanceId);
|
|
526
|
-
if (api == null) { emitDebug("debugOnAssetsWWD: instance gone: " + instanceId); return; }
|
|
527
|
-
|
|
528
|
-
// 1) Init a small FIFO cluster (K=3)
|
|
529
|
-
final int K = 3;
|
|
530
|
-
int clusterId = api.initCluster(K);
|
|
531
|
-
emitDebug("WWD DEBUG: initCluster -> " + clusterId + " (K=" + K + ")");
|
|
532
|
-
|
|
533
|
-
// 2) Onboarding from assets
|
|
534
|
-
String[] onboard = new String[] {
|
|
535
|
-
"onboard1.raw","onboard2.raw","onboard3.raw","onboard4.raw"
|
|
536
|
-
};
|
|
537
|
-
for (int i = 0; i < onboard.length; i++) {
|
|
538
|
-
short[] pcm = readPcm16LeRawAsset(onboard[i]);
|
|
539
|
-
api.createAndPushEmbeddingsToCluster(clusterId, pcm, pcm.length);
|
|
540
|
-
String line = String.format(Locale.US, "WWD DEBUG: push #%d '%s' samples=%d", i+1, onboard[i], pcm.length);
|
|
541
|
-
emitDebug(line);
|
|
542
|
-
}
|
|
543
|
-
emitDebug("WWD DEBUG: onboarding done; cluster+mean saved.");
|
|
544
|
-
|
|
545
|
-
// 3) Verification from assets
|
|
546
|
-
String[] tests = new String[] {
|
|
547
|
-
"test1.raw","test2.raw","test3.raw","test4.raw","test5.raw","test6.raw","test7.raw"
|
|
548
|
-
};
|
|
549
|
-
for (String tf : tests) {
|
|
550
|
-
short[] pcm = readPcm16LeRawAsset(tf);
|
|
551
|
-
float score = api.createAndVerifyEmbeddingsFromCluster(clusterId, pcm, pcm.length);
|
|
552
|
-
String line = String.format(Locale.US, "WWD DEBUG: verify '%s' samples=%d -> score=%.4f",
|
|
553
|
-
tf, pcm.length, score);
|
|
554
|
-
emitDebug(line);
|
|
555
|
-
}
|
|
556
|
-
emitDebug("WWD DEBUG: verification pass completed.");
|
|
557
|
-
} catch (Throwable t) {
|
|
558
|
-
emitDebug("WWD DEBUG ERROR: " + t.getMessage());
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/** Read RAW little-endian PCM16 asset → short[] (exactly your requested method) */
|
|
563
|
-
private short[] readPcm16LeRawAsset(String assetName) throws IOException {
|
|
564
|
-
AssetManager am = reactContext.getAssets();
|
|
565
|
-
try (InputStream in = am.open(assetName);
|
|
566
|
-
BufferedInputStream bis = new BufferedInputStream(in);
|
|
567
|
-
ByteArrayOutputStream baos = new ByteArrayOutputStream(32 * 1024)) {
|
|
568
|
-
|
|
569
|
-
byte[] buf = new byte[8192];
|
|
570
|
-
int n;
|
|
571
|
-
while ((n = bis.read(buf)) != -1) {
|
|
572
|
-
baos.write(buf, 0, n);
|
|
573
|
-
}
|
|
574
|
-
byte[] all = baos.toByteArray();
|
|
575
|
-
if ((all.length & 1) != 0) {
|
|
576
|
-
throw new IOException("Asset RAW has odd byte length: " + assetName + " (" + all.length + " bytes)");
|
|
577
|
-
}
|
|
578
|
-
int samples = all.length / 2;
|
|
579
|
-
short[] pcm = new short[samples];
|
|
580
|
-
for (int i = 0, s = 0; i < all.length; i += 2, s++) {
|
|
581
|
-
int lo = (all[i] & 0xFF);
|
|
582
|
-
int hi = (all[i + 1] << 8);
|
|
583
|
-
pcm[s] = (short) (hi | lo);
|
|
584
|
-
}
|
|
585
|
-
return pcm;
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
//ios/KeyWordRNBridge.m
|
|
2
|
-
|
|
3
|
-
#import "KeyWordRNBridge.h"
|
|
4
|
-
#import <React/RCTBridge.h>
|
|
5
|
-
#import <React/RCTLog.h>
|
|
6
|
-
#import <React/RCTEventEmitter.h>
|
|
7
|
-
//#import "KeyWordsDetection.h" // Import your KeyWordsDetection library header
|
|
8
|
-
|
|
9
|
-
// Ensure the protocol is correctly imported or declared
|
|
10
|
-
// Assuming the protocol is named 'KeywordDetectionRNDelegate'
|
|
11
|
-
@interface KeyWordsDetectionWrapper : NSObject <KeywordDetectionRNDelegate>
|
|
12
|
-
|
|
13
|
-
@property (nonatomic, strong) KeyWordsDetection *keyWordsDetection;
|
|
14
|
-
@property (nonatomic, strong) NSString *instanceId;
|
|
15
|
-
@property (nonatomic, weak) KeyWordRNBridge *bridge;
|
|
16
|
-
|
|
17
|
-
- (instancetype)initWithInstanceId:(NSString *)instanceId
|
|
18
|
-
modelName:(NSString *)modelName
|
|
19
|
-
threshold:(float)threshold
|
|
20
|
-
bufferCnt:(NSInteger)bufferCnt
|
|
21
|
-
bridge:(KeyWordRNBridge *)bridge
|
|
22
|
-
error:(NSError **)error;
|
|
23
|
-
|
|
24
|
-
- (instancetype)initWithInstanceId:(NSString *)instanceId
|
|
25
|
-
modelNames:(NSArray<NSString *> *)modelNames
|
|
26
|
-
thresholds:(NSArray<NSNumber *> *)thresholds
|
|
27
|
-
bufferCnts:(NSArray<NSNumber *> *)bufferCnts
|
|
28
|
-
msBetweenCallback:(NSArray<NSNumber *> *)msBetweenCallback
|
|
29
|
-
bridge:(KeyWordRNBridge *)bridge
|
|
30
|
-
error:(NSError **)error;
|
|
31
|
-
|
|
32
|
-
@end
|
|
33
|
-
|
|
34
|
-
@implementation KeyWordsDetectionWrapper
|
|
35
|
-
|
|
36
|
-
- (instancetype)initWithInstanceId:(NSString *)instanceId
|
|
37
|
-
modelName:(NSString *)modelName
|
|
38
|
-
threshold:(float)threshold
|
|
39
|
-
bufferCnt:(NSInteger)bufferCnt
|
|
40
|
-
bridge:(KeyWordRNBridge *)bridge
|
|
41
|
-
error:(NSError **)error
|
|
42
|
-
{
|
|
43
|
-
if (self = [super init]) {
|
|
44
|
-
_instanceId = instanceId;
|
|
45
|
-
_bridge = bridge;
|
|
46
|
-
_keyWordsDetection = [[KeyWordsDetection alloc] initWithModelPath:modelName threshold:threshold bufferCnt:bufferCnt error:error];
|
|
47
|
-
if (*error) {
|
|
48
|
-
return nil;
|
|
49
|
-
}
|
|
50
|
-
_keyWordsDetection.delegate = self;
|
|
51
|
-
}
|
|
52
|
-
return self;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
- (instancetype)initWithInstanceId:(NSString *)instanceId
|
|
56
|
-
modelNames:(NSArray<NSString *> *)modelNames
|
|
57
|
-
thresholds:(NSArray<NSNumber *> *)thresholds
|
|
58
|
-
bufferCnts:(NSArray<NSNumber *> *)bufferCnts
|
|
59
|
-
msBetweenCallback:(NSArray<NSNumber *> *)msBetweenCallback
|
|
60
|
-
bridge:(KeyWordRNBridge *)bridge
|
|
61
|
-
error:(NSError **)error {
|
|
62
|
-
if (self = [super init]) {
|
|
63
|
-
_instanceId = instanceId;
|
|
64
|
-
_bridge = bridge;
|
|
65
|
-
|
|
66
|
-
NSMutableArray<NSNumber *> *floatThresholds = [NSMutableArray array];
|
|
67
|
-
for (NSNumber *num in thresholds) {
|
|
68
|
-
[floatThresholds addObject:@(num.floatValue)];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
_keyWordsDetection = [[KeyWordsDetection alloc] initWithModelPaths:modelNames
|
|
72
|
-
thresholds:floatThresholds
|
|
73
|
-
bufferCnts:bufferCnts
|
|
74
|
-
msBetweenCallback:msBetweenCallback
|
|
75
|
-
error:error];
|
|
76
|
-
if (*error) return nil;
|
|
77
|
-
_keyWordsDetection.delegate = self;
|
|
78
|
-
}
|
|
79
|
-
return self;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Implement the delegate method
|
|
83
|
-
- (void)KeywordDetectionDidDetectEvent:(NSDictionary *)eventInfo {
|
|
84
|
-
NSMutableDictionary *mutableEventInfo = [eventInfo mutableCopy];
|
|
85
|
-
mutableEventInfo[@"instanceId"] = self.instanceId;
|
|
86
|
-
[_bridge sendEventWithName:@"onKeywordDetectionEvent" body:mutableEventInfo];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
@end
|
|
90
|
-
|
|
91
|
-
@interface KeyWordRNBridge () <RCTBridgeModule>
|
|
92
|
-
|
|
93
|
-
@property (nonatomic, strong) NSMutableDictionary *instances;
|
|
94
|
-
|
|
95
|
-
@end
|
|
96
|
-
|
|
97
|
-
@implementation KeyWordRNBridge
|
|
98
|
-
|
|
99
|
-
RCT_EXPORT_MODULE();
|
|
100
|
-
|
|
101
|
-
- (instancetype)init {
|
|
102
|
-
if (self = [super init]) {
|
|
103
|
-
_instances = [NSMutableDictionary new];
|
|
104
|
-
}
|
|
105
|
-
return self;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
+ (BOOL)requiresMainQueueSetup
|
|
109
|
-
{
|
|
110
|
-
return YES;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
- (NSArray<NSString *> *)supportedEvents {
|
|
114
|
-
return @[@"onKeywordDetectionEvent",
|
|
115
|
-
@"onVADDetectionEvent"]; // NEW
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
RCT_EXPORT_METHOD(createInstanceMulti:(NSString *)instanceId
|
|
119
|
-
modelPaths:(NSArray<NSString *> *)modelPaths
|
|
120
|
-
thresholds:(NSArray<NSNumber *> *)thresholds
|
|
121
|
-
bufferCnts:(NSArray<NSNumber *> *)bufferCnts
|
|
122
|
-
msBetweenCallback:(NSArray<NSNumber *> *)msBetweenCallback
|
|
123
|
-
resolver:(RCTPromiseResolveBlock)resolve
|
|
124
|
-
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
125
|
-
if (self.instances[instanceId]) {
|
|
126
|
-
reject(@"InstanceExists", [NSString stringWithFormat:@"Instance already exists with ID: %@", instanceId], nil);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
NSError *error = nil;
|
|
131
|
-
KeyWordsDetectionWrapper *wrapper = [[KeyWordsDetectionWrapper alloc]
|
|
132
|
-
initWithInstanceId:instanceId
|
|
133
|
-
modelNames:modelPaths
|
|
134
|
-
thresholds:thresholds
|
|
135
|
-
bufferCnts:bufferCnts
|
|
136
|
-
msBetweenCallback:msBetweenCallback
|
|
137
|
-
bridge:self
|
|
138
|
-
error:&error];
|
|
139
|
-
if (error) {
|
|
140
|
-
reject(@"CreateError", [NSString stringWithFormat:@"Failed to create multi-model instance: %@", error.localizedDescription], nil);
|
|
141
|
-
} else {
|
|
142
|
-
self.instances[instanceId] = wrapper;
|
|
143
|
-
resolve([NSString stringWithFormat:@"Multi-model instance created with ID: %@", instanceId]);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
RCT_EXPORT_METHOD(createInstance:(NSString *)instanceId modelName:(NSString *)modelName threshold:(float)threshold bufferCnt:(NSInteger)bufferCnt resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
148
|
-
{
|
|
149
|
-
if (self.instances[instanceId]) {
|
|
150
|
-
reject(@"InstanceExists", [NSString stringWithFormat:@"Instance already exists with ID: %@", instanceId], nil);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
NSError *error = nil;
|
|
155
|
-
KeyWordsDetectionWrapper *wrapper = [[KeyWordsDetectionWrapper alloc] initWithInstanceId:instanceId modelName:modelName threshold:threshold bufferCnt:bufferCnt bridge:self error:&error];
|
|
156
|
-
if (error) {
|
|
157
|
-
reject(@"CreateError", [NSString stringWithFormat:@"Failed to create instance: %@", error.localizedDescription], nil);
|
|
158
|
-
} else {
|
|
159
|
-
self.instances[instanceId] = wrapper;
|
|
160
|
-
resolve([NSString stringWithFormat:@"Instance created with ID: %@", instanceId]);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// NEW: receive global wakeword audio routing config from JS
|
|
165
|
-
RCT_EXPORT_METHOD(setAudioRoutingConfig:(NSString *)jsonConfig
|
|
166
|
-
resolver:(RCTPromiseResolveBlock)resolve
|
|
167
|
-
rejecter:(RCTPromiseRejectBlock)reject)
|
|
168
|
-
{
|
|
169
|
-
@try {
|
|
170
|
-
// Hand off to your audio/session manager (you implement this)
|
|
171
|
-
// e.g. in AudioSessionAndDuckingManager:
|
|
172
|
-
// - (void)setWakewordAudioRoutingConfigFromJSONString:(NSString *)jsonConfig;
|
|
173
|
-
[AudioSessionAndDuckingManager.shared setWakewordAudioRoutingConfigFromJSONString:jsonConfig];
|
|
174
|
-
|
|
175
|
-
NSLog(@"[KeyWordRNBridge] setAudioRoutingConfig JSON = %@", jsonConfig);
|
|
176
|
-
resolve(@"ok");
|
|
177
|
-
}
|
|
178
|
-
@catch (NSException *e) {
|
|
179
|
-
reject(@"AudioRoutingConfigError",
|
|
180
|
-
[NSString stringWithFormat:@"Failed to set audio routing config: %@", e.reason],
|
|
181
|
-
nil);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
RCT_EXPORT_METHOD(disableDucking:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
186
|
-
{
|
|
187
|
-
[AudioSessionAndDuckingManager.shared disableDucking];
|
|
188
|
-
resolve(@"enabled");
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
RCT_EXPORT_METHOD(initAudioSessAndDuckManage:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
192
|
-
{
|
|
193
|
-
[AudioSessionAndDuckingManager.shared initAudioSessAndDuckManage];
|
|
194
|
-
resolve(@"enabled");
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
RCT_EXPORT_METHOD(restartListeningAfterDucking:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
198
|
-
{
|
|
199
|
-
[AudioSessionAndDuckingManager.shared restartListeningAfterDucking];
|
|
200
|
-
resolve(@"disabled");
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
RCT_EXPORT_METHOD(enableAggressiveDucking:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
204
|
-
{
|
|
205
|
-
[AudioSessionAndDuckingManager.shared enableAggressiveDucking];
|
|
206
|
-
resolve(@"enabled");
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
RCT_EXPORT_METHOD(disableDuckingAndCleanup:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
210
|
-
{
|
|
211
|
-
[AudioSessionAndDuckingManager.shared disableDuckingAndCleanup];
|
|
212
|
-
resolve(@"disabled");
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
RCT_EXPORT_METHOD(setKeywordDetectionLicense:(NSString *)instanceId licenseKey:(NSString *)licenseKey resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
216
|
-
{
|
|
217
|
-
KeyWordsDetectionWrapper *wrapper = self.instances[instanceId];
|
|
218
|
-
KeyWordsDetection *instance = wrapper.keyWordsDetection;
|
|
219
|
-
BOOL isLicensed = NO;
|
|
220
|
-
if (instance) {
|
|
221
|
-
isLicensed = [instance setLicenseWithLicenseKey:licenseKey];
|
|
222
|
-
NSLog(@"License is valid?: %@", isLicensed ? @"YES" : @"NO");
|
|
223
|
-
resolve(@(isLicensed)); // Wrap BOOL in NSNumber
|
|
224
|
-
} else {
|
|
225
|
-
reject(@"InstanceNotFound", [NSString stringWithFormat:@"No instance found with ID: %@", instanceId], nil);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
RCT_EXPORT_METHOD(replaceKeywordDetectionModel:(NSString *)instanceId modelName:(NSString *)modelName threshold:(float)threshold bufferCnt:(NSInteger)bufferCnt resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
230
|
-
{
|
|
231
|
-
KeyWordsDetectionWrapper *wrapper = self.instances[instanceId];
|
|
232
|
-
KeyWordsDetection *instance = wrapper.keyWordsDetection;
|
|
233
|
-
if (instance) {
|
|
234
|
-
NSError *error = nil;
|
|
235
|
-
[instance replaceKeywordDetectionModelWithModelPath:modelName threshold:threshold bufferCnt:bufferCnt error:&error];
|
|
236
|
-
if (error) {
|
|
237
|
-
reject(@"ReplaceError", [NSString stringWithFormat:@"Failed to replace model: %@", error.localizedDescription], nil);
|
|
238
|
-
} else {
|
|
239
|
-
resolve([NSString stringWithFormat:@"Instance ID: %@ changed model to %@", instanceId, modelName]);
|
|
240
|
-
}
|
|
241
|
-
} else {
|
|
242
|
-
reject(@"InstanceNotFound", [NSString stringWithFormat:@"No instance found with ID: %@", instanceId], nil);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
RCT_EXPORT_METHOD(startKeywordDetection:(NSString *)instanceId
|
|
247
|
-
threshold:(float)threshold
|
|
248
|
-
noExternalActivation:(BOOL)noExternalActivation
|
|
249
|
-
duckOthers:(BOOL)duckOthers
|
|
250
|
-
mixWithOthers:(BOOL)mixWithOthers
|
|
251
|
-
defaultToSpeaker:(BOOL)defaultToSpeaker
|
|
252
|
-
resolver:(RCTPromiseResolveBlock)resolve
|
|
253
|
-
rejecter:(RCTPromiseRejectBlock)reject)
|
|
254
|
-
{
|
|
255
|
-
KeyWordsDetectionWrapper *wrapper = self.instances[instanceId];
|
|
256
|
-
KeyWordsDetection *instance = wrapper.keyWordsDetection;
|
|
257
|
-
if (instance) {
|
|
258
|
-
BOOL success = [instance startListeningWithNoExternalActivation:noExternalActivation
|
|
259
|
-
duckOthers:duckOthers
|
|
260
|
-
mixWithOthers:mixWithOthers
|
|
261
|
-
defaultToSpeaker:defaultToSpeaker];
|
|
262
|
-
if (success == false) {
|
|
263
|
-
reject(@"StartError", [NSString stringWithFormat:@"Failed to start detection"], nil);
|
|
264
|
-
} else {
|
|
265
|
-
resolve([NSString stringWithFormat:@"Started detection for instance: %@", instanceId]);
|
|
266
|
-
}
|
|
267
|
-
} else {
|
|
268
|
-
reject(@"InstanceNotFound", [NSString stringWithFormat:@"No instance found with ID: %@", instanceId], nil);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
RCT_EXPORT_METHOD(stopKeywordDetection:(NSString *)instanceId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
273
|
-
{
|
|
274
|
-
KeyWordsDetectionWrapper *wrapper = self.instances[instanceId];
|
|
275
|
-
KeyWordsDetection *instance = wrapper.keyWordsDetection;
|
|
276
|
-
if (instance) {
|
|
277
|
-
[instance stopListening];
|
|
278
|
-
resolve([NSString stringWithFormat:@"Stopped detection for instance: %@", instanceId]);
|
|
279
|
-
} else {
|
|
280
|
-
reject(@"InstanceNotFound", [NSString stringWithFormat:@"No instance found with ID: %@", instanceId], nil);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
RCT_EXPORT_METHOD(destroyInstance:(NSString *)instanceId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
285
|
-
{
|
|
286
|
-
KeyWordsDetectionWrapper *wrapper = self.instances[instanceId];
|
|
287
|
-
if (wrapper) {
|
|
288
|
-
[wrapper.keyWordsDetection stopListening];
|
|
289
|
-
[self.instances removeObjectForKey:instanceId];
|
|
290
|
-
resolve([NSString stringWithFormat:@"Destroyed instance: %@", instanceId]);
|
|
291
|
-
} else {
|
|
292
|
-
reject(@"InstanceNotFound", [NSString stringWithFormat:@"No instance found with ID: %@", instanceId], nil);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Keeping all APIs even if not called in JS yet
|
|
297
|
-
|
|
298
|
-
RCT_EXPORT_METHOD(getKeywordDetectionModel:(NSString *)instanceId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
299
|
-
{
|
|
300
|
-
KeyWordsDetectionWrapper *wrapper = self.instances[instanceId];
|
|
301
|
-
KeyWordsDetection *instance = wrapper.keyWordsDetection;
|
|
302
|
-
if (instance) {
|
|
303
|
-
NSString *modelName = [instance getKeywordDetectionModel];
|
|
304
|
-
resolve(modelName);
|
|
305
|
-
} else {
|
|
306
|
-
reject(@"InstanceNotFound", [NSString stringWithFormat:@"No instance found with ID: %@", instanceId], nil);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
RCT_EXPORT_METHOD(getRecordingWav:(NSString *)instanceId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
311
|
-
{
|
|
312
|
-
KeyWordsDetectionWrapper *wrapper = self.instances[instanceId];
|
|
313
|
-
KeyWordsDetection *instance = wrapper.keyWordsDetection;
|
|
314
|
-
if (instance) {
|
|
315
|
-
NSString *recWavPath = [instance getRecordingWav];
|
|
316
|
-
resolve(recWavPath);
|
|
317
|
-
} else {
|
|
318
|
-
reject(@"InstanceNotFound", [NSString stringWithFormat:@"No instance found with ID: %@", instanceId], nil);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
RCT_EXPORT_METHOD(getVoiceProps:(NSString *)instanceId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
323
|
-
{
|
|
324
|
-
KeyWordsDetectionWrapper *wrapper = self.instances[instanceId];
|
|
325
|
-
KeyWordsDetection *instance = wrapper.keyWordsDetection;
|
|
326
|
-
if (instance) {
|
|
327
|
-
@try {
|
|
328
|
-
NSDictionary *voiceProps = [instance getVoiceProps];
|
|
329
|
-
NSMutableDictionary *result = [NSMutableDictionary dictionary];
|
|
330
|
-
result[@"error"] = voiceProps[@"error"] ?: @"No Error";
|
|
331
|
-
result[@"voiceProbability"] = @([voiceProps[@"voiceProbability"] floatValue]);
|
|
332
|
-
result[@"lastTimeHumanVoiceHeard"] = @([voiceProps[@"lastTimeHumanVoiceHeard"] longLongValue]);
|
|
333
|
-
resolve(result);
|
|
334
|
-
} @catch (NSException *exception) {
|
|
335
|
-
reject(@"VoicePropsError", [NSString stringWithFormat:@"Failed to get voice properties: %@", exception.reason], nil);
|
|
336
|
-
}
|
|
337
|
-
} else {
|
|
338
|
-
reject(@"InstanceNotFound", [NSString stringWithFormat:@"No instance found with ID: %@", instanceId], nil);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Start/stop silent VAD (iOS only)
|
|
343
|
-
RCT_EXPORT_METHOD(startSilentVADDetection:(NSString *)instanceId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
344
|
-
{
|
|
345
|
-
KeyWordsDetectionWrapper *wrapper = self.instances[instanceId];
|
|
346
|
-
if (wrapper && wrapper.keyWordsDetection) {
|
|
347
|
-
BOOL success = [wrapper.keyWordsDetection startSilentListening];
|
|
348
|
-
success ? resolve(@"Started silent VAD detection") :
|
|
349
|
-
reject(@"StartError", @"Failed to start silent VAD detection", nil);
|
|
350
|
-
} else {
|
|
351
|
-
reject(@"InstanceNotFound", @"No instance found", nil);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
RCT_EXPORT_METHOD(stopSilentVADDetection:(NSString *)instanceId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
356
|
-
{
|
|
357
|
-
KeyWordsDetectionWrapper *wrapper = self.instances[instanceId];
|
|
358
|
-
if (wrapper && wrapper.keyWordsDetection) {
|
|
359
|
-
[wrapper.keyWordsDetection stopSilentListening];
|
|
360
|
-
resolve(@"Stopped silent VAD detection");
|
|
361
|
-
} else {
|
|
362
|
-
reject(@"InstanceNotFound", @"No instance found", nil);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Start/stop explicit VAD
|
|
367
|
-
RCT_EXPORT_METHOD(startVADDetection:(NSString *)instanceId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
368
|
-
{
|
|
369
|
-
KeyWordsDetectionWrapper *wrapper = self.instances[instanceId];
|
|
370
|
-
if (wrapper && wrapper.keyWordsDetection) {
|
|
371
|
-
BOOL success = [wrapper.keyWordsDetection startVADListening];
|
|
372
|
-
success ? resolve(@"Started VAD detection") :
|
|
373
|
-
reject(@"StartError", @"Failed to start VAD detection", nil);
|
|
374
|
-
} else {
|
|
375
|
-
reject(@"InstanceNotFound", @"No instance found", nil);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
RCT_EXPORT_METHOD(stopVADDetection:(NSString *)instanceId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
380
|
-
{
|
|
381
|
-
KeyWordsDetectionWrapper *wrapper = self.instances[instanceId];
|
|
382
|
-
if (wrapper && wrapper.keyWordsDetection) {
|
|
383
|
-
[wrapper.keyWordsDetection stopVADListening];
|
|
384
|
-
resolve(@"Stopped VAD detection");
|
|
385
|
-
} else {
|
|
386
|
-
reject(@"InstanceNotFound", @"No instance found", nil);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
RCT_EXPORT_METHOD(setVADParams:(NSString *)instanceId
|
|
391
|
-
threshold:(float)threshold
|
|
392
|
-
msWindow:(NSInteger)msWindow
|
|
393
|
-
resolver:(RCTPromiseResolveBlock)resolve
|
|
394
|
-
rejecter:(RCTPromiseRejectBlock)reject)
|
|
395
|
-
{
|
|
396
|
-
KeyWordsDetectionWrapper *wrapper = self.instances[instanceId];
|
|
397
|
-
if (wrapper && wrapper.keyWordsDetection) {
|
|
398
|
-
NSError *err = nil;
|
|
399
|
-
BOOL ok = [wrapper.keyWordsDetection setVADParamsWithThreshold:threshold
|
|
400
|
-
msWindow:msWindow
|
|
401
|
-
error:&err];
|
|
402
|
-
if (!ok || err) {
|
|
403
|
-
reject(@"VADParamsError",
|
|
404
|
-
err ? err.localizedDescription : @"Failed to set VAD params",
|
|
405
|
-
err);
|
|
406
|
-
} else {
|
|
407
|
-
resolve(@"VAD params updated");
|
|
408
|
-
}
|
|
409
|
-
} else {
|
|
410
|
-
reject(@"InstanceNotFound", @"No instance found", nil);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// You can add more methods here as needed, ensuring they use the instanceId
|
|
415
|
-
|
|
416
|
-
@end
|