whisper.rn 0.3.5 → 0.3.6

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.
@@ -21,6 +21,8 @@ import java.io.FileInputStream;
21
21
  import java.io.PushbackInputStream;
22
22
 
23
23
  public class RNWhisper implements LifecycleEventListener {
24
+ public static final String NAME = "RNWhisper";
25
+
24
26
  private ReactApplicationContext reactContext;
25
27
  private Downloader downloader;
26
28
 
@@ -40,6 +42,8 @@ public class RNWhisper implements LifecycleEventListener {
40
42
  return constants;
41
43
  }
42
44
 
45
+ private HashMap<AsyncTask, String> tasks = new HashMap<>();
46
+
43
47
  private HashMap<Integer, WhisperContext> contexts = new HashMap<>();
44
48
 
45
49
  private int getResourceIdentifier(String filePath) {
@@ -59,7 +63,7 @@ public class RNWhisper implements LifecycleEventListener {
59
63
  }
60
64
 
61
65
  public void initContext(final ReadableMap options, final Promise promise) {
62
- new AsyncTask<Void, Void, Integer>() {
66
+ AsyncTask task = new AsyncTask<Void, Void, Integer>() {
63
67
  private Exception exception;
64
68
 
65
69
  @Override
@@ -104,8 +108,10 @@ public class RNWhisper implements LifecycleEventListener {
104
108
  return;
105
109
  }
106
110
  promise.resolve(id);
111
+ tasks.remove(this);
107
112
  }
108
113
  }.execute();
114
+ tasks.put(task, "initContext");
109
115
  }
110
116
 
111
117
  public void transcribeFile(double id, double jobId, String filePath, ReadableMap options, Promise promise) {
@@ -122,7 +128,7 @@ public class RNWhisper implements LifecycleEventListener {
122
128
  promise.reject("Context is already transcribing");
123
129
  return;
124
130
  }
125
- new AsyncTask<Void, Void, WritableMap>() {
131
+ AsyncTask task = new AsyncTask<Void, Void, WritableMap>() {
126
132
  private Exception exception;
127
133
 
128
134
  @Override
@@ -161,8 +167,10 @@ public class RNWhisper implements LifecycleEventListener {
161
167
  return;
162
168
  }
163
169
  promise.resolve(data);
170
+ tasks.remove(this);
164
171
  }
165
172
  }.execute();
173
+ tasks.put(task, "transcribeFile-" + id);
166
174
  }
167
175
 
168
176
  public void startRealtimeTranscribe(double id, double jobId, ReadableMap options, Promise promise) {
@@ -183,18 +191,48 @@ public class RNWhisper implements LifecycleEventListener {
183
191
  promise.reject("Failed to start realtime transcribe. State: " + state);
184
192
  }
185
193
 
186
- public void abortTranscribe(double contextId, double jobId, Promise promise) {
187
- WhisperContext context = contexts.get((int) contextId);
194
+ public void abortTranscribe(double id, double jobId, Promise promise) {
195
+ WhisperContext context = contexts.get((int) id);
188
196
  if (context == null) {
189
197
  promise.reject("Context not found");
190
198
  return;
191
199
  }
192
- context.stopTranscribe((int) jobId);
200
+ AsyncTask task = new AsyncTask<Void, Void, Void>() {
201
+ private Exception exception;
202
+
203
+ @Override
204
+ protected Void doInBackground(Void... voids) {
205
+ try {
206
+ context.stopTranscribe((int) jobId);
207
+ AsyncTask completionTask = null;
208
+ for (AsyncTask task : tasks.keySet()) {
209
+ if (tasks.get(task).equals("transcribeFile-" + id)) {
210
+ task.get();
211
+ break;
212
+ }
213
+ }
214
+ } catch (Exception e) {
215
+ exception = e;
216
+ }
217
+ return null;
218
+ }
219
+
220
+ @Override
221
+ protected void onPostExecute(Void result) {
222
+ if (exception != null) {
223
+ promise.reject(exception);
224
+ return;
225
+ }
226
+ promise.resolve(null);
227
+ tasks.remove(this);
228
+ }
229
+ }.execute();
230
+ tasks.put(task, "abortTranscribe-" + id);
193
231
  }
194
232
 
195
233
  public void releaseContext(double id, Promise promise) {
196
234
  final int contextId = (int) id;
197
- new AsyncTask<Void, Void, Void>() {
235
+ AsyncTask task = new AsyncTask<Void, Void, Void>() {
198
236
  private Exception exception;
199
237
 
200
238
  @Override
@@ -204,6 +242,14 @@ public class RNWhisper implements LifecycleEventListener {
204
242
  if (context == null) {
205
243
  throw new Exception("Context " + id + " not found");
206
244
  }
245
+ context.stopCurrentTranscribe();
246
+ AsyncTask completionTask = null;
247
+ for (AsyncTask task : tasks.keySet()) {
248
+ if (tasks.get(task).equals("transcribeFile-" + contextId)) {
249
+ task.get();
250
+ break;
251
+ }
252
+ }
207
253
  context.release();
208
254
  contexts.remove(contextId);
209
255
  } catch (Exception e) {
@@ -219,12 +265,14 @@ public class RNWhisper implements LifecycleEventListener {
219
265
  return;
220
266
  }
221
267
  promise.resolve(null);
268
+ tasks.remove(this);
222
269
  }
223
270
  }.execute();
271
+ tasks.put(task, "releaseContext-" + id);
224
272
  }
225
273
 
226
274
  public void releaseAllContexts(Promise promise) {
227
- new AsyncTask<Void, Void, Void>() {
275
+ AsyncTask task = new AsyncTask<Void, Void, Void>() {
228
276
  private Exception exception;
229
277
 
230
278
  @Override
@@ -244,8 +292,10 @@ public class RNWhisper implements LifecycleEventListener {
244
292
  return;
245
293
  }
246
294
  promise.resolve(null);
295
+ tasks.remove(this);
247
296
  }
248
297
  }.execute();
298
+ tasks.put(task, "releaseAllContexts");
249
299
  }
250
300
 
251
301
  @Override
@@ -258,10 +308,20 @@ public class RNWhisper implements LifecycleEventListener {
258
308
 
259
309
  @Override
260
310
  public void onHostDestroy() {
261
- WhisperContext.abortAllTranscribe();
311
+ for (WhisperContext context : contexts.values()) {
312
+ context.stopCurrentTranscribe();
313
+ }
314
+ for (AsyncTask task : tasks.keySet()) {
315
+ try {
316
+ task.get();
317
+ } catch (Exception e) {
318
+ Log.e(NAME, "Failed to wait for task", e);
319
+ }
320
+ }
262
321
  for (WhisperContext context : contexts.values()) {
263
322
  context.release();
264
323
  }
324
+ WhisperContext.abortAllTranscribe(); // graceful abort
265
325
  contexts.clear();
266
326
  downloader.clearCache();
267
327
  }
@@ -61,6 +61,7 @@ public class WhisperContext {
61
61
  private boolean isCapturing = false;
62
62
  private boolean isStoppedByAction = false;
63
63
  private boolean isTranscribing = false;
64
+ private Thread rootFullHandler = null;
64
65
  private Thread fullHandler = null;
65
66
 
66
67
  public WhisperContext(int id, ReactApplicationContext reactContext, long context) {
@@ -81,6 +82,7 @@ public class WhisperContext {
81
82
  isCapturing = false;
82
83
  isStoppedByAction = false;
83
84
  isTranscribing = false;
85
+ rootFullHandler = null;
84
86
  fullHandler = null;
85
87
  }
86
88
 
@@ -117,7 +119,7 @@ public class WhisperContext {
117
119
  isCapturing = true;
118
120
  recorder.startRecording();
119
121
 
120
- new Thread(new Runnable() {
122
+ rootFullHandler = new Thread(new Runnable() {
121
123
  @Override
122
124
  public void run() {
123
125
  try {
@@ -195,7 +197,8 @@ public class WhisperContext {
195
197
  recorder = null;
196
198
  }
197
199
  }
198
- }).start();
200
+ });
201
+ rootFullHandler.start();
199
202
  return state;
200
203
  }
201
204
 
@@ -402,6 +405,14 @@ public class WhisperContext {
402
405
  abortTranscribe(jobId);
403
406
  isCapturing = false;
404
407
  isStoppedByAction = true;
408
+ if (rootFullHandler != null) {
409
+ try {
410
+ rootFullHandler.join();
411
+ } catch (Exception e) {
412
+ Log.e(NAME, "Error joining rootFullHandler: " + e.getMessage());
413
+ }
414
+ rootFullHandler = null;
415
+ }
405
416
  }
406
417
 
407
418
  public void stopCurrentTranscribe() {
package/ios/RNWhisper.mm CHANGED
@@ -68,13 +68,17 @@ RCT_REMAP_METHOD(initContext,
68
68
  path = [[NSBundle mainBundle] pathForResource:modelPath ofType:nil];
69
69
  }
70
70
 
71
- RNWhisperContext *context = [RNWhisperContext initWithModelPath:path];
71
+ int contextId = arc4random_uniform(1000000);
72
+
73
+ RNWhisperContext *context = [RNWhisperContext
74
+ initWithModelPath:path
75
+ contextId:contextId
76
+ ];
72
77
  if ([context getContext] == NULL) {
73
78
  reject(@"whisper_cpp_error", @"Failed to load the model", nil);
74
79
  return;
75
80
  }
76
81
 
77
- int contextId = arc4random_uniform(1000000);
78
82
  [contexts setObject:context forKey:[NSNumber numberWithInt:contextId]];
79
83
 
80
84
  resolve([NSNumber numberWithInt:contextId]);
@@ -122,36 +126,36 @@ RCT_REMAP_METHOD(transcribeFile,
122
126
  reject(@"whisper_error", @"Invalid file", nil);
123
127
  return;
124
128
  }
125
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
126
- int code = [context transcribeFile:jobId
127
- audioData:waveFile
128
- audioDataCount:count
129
- options:options
130
- onProgress: ^(int progress) {
131
- if (rn_whisper_transcribe_is_aborted(jobId)) {
132
- return;
133
- }
134
- dispatch_async(dispatch_get_main_queue(), ^{
135
- [self sendEventWithName:@"@RNWhisper_onTranscribeProgress"
136
- body:@{
137
- @"contextId": [NSNumber numberWithInt:contextId],
138
- @"jobId": [NSNumber numberWithInt:jobId],
139
- @"progress": [NSNumber numberWithInt:progress]
140
- }
141
- ];
142
- });
129
+ [context transcribeFile:jobId
130
+ audioData:waveFile
131
+ audioDataCount:count
132
+ options:options
133
+ onProgress: ^(int progress) {
134
+ if (rn_whisper_transcribe_is_aborted(jobId)) {
135
+ return;
136
+ }
137
+ dispatch_async(dispatch_get_main_queue(), ^{
138
+ [self sendEventWithName:@"@RNWhisper_onTranscribeProgress"
139
+ body:@{
140
+ @"contextId": [NSNumber numberWithInt:contextId],
141
+ @"jobId": [NSNumber numberWithInt:jobId],
142
+ @"progress": [NSNumber numberWithInt:progress]
143
+ }
144
+ ];
145
+ });
146
+ }
147
+ onEnd: ^(int code) {
148
+ if (code != 0) {
149
+ free(waveFile);
150
+ reject(@"whisper_cpp_error", [NSString stringWithFormat:@"Failed to transcribe the file. Code: %d", code], nil);
151
+ return;
143
152
  }
144
- ];
145
- if (code != 0) {
146
153
  free(waveFile);
147
- reject(@"whisper_cpp_error", [NSString stringWithFormat:@"Failed to transcribe the file. Code: %d", code], nil);
148
- return;
154
+ NSMutableDictionary *result = [context getTextSegments];
155
+ result[@"isAborted"] = @([context isStoppedByAction]);
156
+ resolve(result);
149
157
  }
150
- free(waveFile);
151
- NSMutableDictionary *result = [context getTextSegments];
152
- result[@"isAborted"] = @([context isStoppedByAction]);
153
- resolve(result);
154
- });
158
+ ];
155
159
  }
156
160
 
157
161
  RCT_REMAP_METHOD(startRealtimeTranscribe,
@@ -260,7 +264,7 @@ RCT_REMAP_METHOD(releaseAllContexts,
260
264
  }
261
265
 
262
266
  - (void)invalidate {
263
- rn_whisper_abort_all_transcribe();
267
+ [super invalidate];
264
268
 
265
269
  if (contexts == nil) {
266
270
  return;
@@ -271,6 +275,8 @@ RCT_REMAP_METHOD(releaseAllContexts,
271
275
  [context invalidate];
272
276
  }
273
277
 
278
+ rn_whisper_abort_all_transcribe(); // graceful abort
279
+
274
280
  [contexts removeAllObjects];
275
281
  contexts = nil;
276
282
 
@@ -36,21 +36,26 @@ typedef struct {
36
36
  } RNWhisperContextRecordState;
37
37
 
38
38
  @interface RNWhisperContext : NSObject {
39
+ int contextId;
40
+ dispatch_queue_t dQueue;
39
41
  struct whisper_context * ctx;
40
42
  RNWhisperContextRecordState recordState;
41
43
  }
42
44
 
43
- + (instancetype)initWithModelPath:(NSString *)modelPath;
45
+ + (instancetype)initWithModelPath:(NSString *)modelPath contextId:(int)contextId;
44
46
  - (struct whisper_context *)getContext;
47
+ - (dispatch_queue_t)getDispatchQueue;
45
48
  - (OSStatus)transcribeRealtime:(int)jobId
46
49
  options:(NSDictionary *)options
47
50
  onTranscribe:(void (^)(int, NSString *, NSDictionary *))onTranscribe;
48
- - (int)transcribeFile:(int)jobId
51
+ - (void)transcribeFile:(int)jobId
49
52
  audioData:(float *)audioData
50
53
  audioDataCount:(int)audioDataCount
51
54
  options:(NSDictionary *)options
52
- onProgress:(void (^)(int))onProgress;
55
+ onProgress:(void (^)(int))onProgress
56
+ onEnd:(void (^)(int))onEnd;
53
57
  - (void)stopTranscribe:(int)jobId;
58
+ - (void)stopCurrentTranscribe;
54
59
  - (bool)isCapturing;
55
60
  - (bool)isTranscribing;
56
61
  - (bool)isStoppedByAction;
@@ -4,9 +4,14 @@
4
4
 
5
5
  @implementation RNWhisperContext
6
6
 
7
- + (instancetype)initWithModelPath:(NSString *)modelPath {
7
+ + (instancetype)initWithModelPath:(NSString *)modelPath contextId:(int)contextId {
8
8
  RNWhisperContext *context = [[RNWhisperContext alloc] init];
9
+ context->contextId = contextId;
9
10
  context->ctx = whisper_init_from_file([modelPath UTF8String]);
11
+ context->dQueue = dispatch_queue_create(
12
+ [[NSString stringWithFormat:@"RNWhisperContext-%d", contextId] UTF8String],
13
+ DISPATCH_QUEUE_SERIAL
14
+ );
10
15
  return context;
11
16
  }
12
17
 
@@ -14,6 +19,10 @@
14
19
  return self->ctx;
15
20
  }
16
21
 
22
+ - (dispatch_queue_t)getDispatchQueue {
23
+ return self->dQueue;
24
+ }
25
+
17
26
  - (void)prepareRealtime:(NSDictionary *)options {
18
27
  self->recordState.options = options;
19
28
 
@@ -109,7 +118,7 @@ void AudioInputCallback(void * inUserData,
109
118
  nSamples != state->nSamplesTranscribing
110
119
  ) {
111
120
  state->isTranscribing = true;
112
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
121
+ dispatch_async([state->mSelf getDispatchQueue], ^{
113
122
  [state->mSelf fullTranscribeSamples:state];
114
123
  });
115
124
  }
@@ -140,7 +149,7 @@ void AudioInputCallback(void * inUserData,
140
149
 
141
150
  if (!state->isTranscribing) {
142
151
  state->isTranscribing = true;
143
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
152
+ dispatch_async([state->mSelf getDispatchQueue], ^{
144
153
  [state->mSelf fullTranscribeSamples:state];
145
154
  });
146
155
  }
@@ -263,19 +272,22 @@ void AudioInputCallback(void * inUserData,
263
272
  return status;
264
273
  }
265
274
 
266
- - (int)transcribeFile:(int)jobId
275
+ - (void)transcribeFile:(int)jobId
267
276
  audioData:(float *)audioData
268
277
  audioDataCount:(int)audioDataCount
269
278
  options:(NSDictionary *)options
270
279
  onProgress:(void (^)(int))onProgress
280
+ onEnd:(void (^)(int))onEnd
271
281
  {
272
- self->recordState.isStoppedByAction = false;
273
- self->recordState.isTranscribing = true;
274
- self->recordState.jobId = jobId;
275
- int code = [self fullTranscribeWithProgress:onProgress jobId:jobId audioData:audioData audioDataCount:audioDataCount options:options];
276
- self->recordState.jobId = -1;
277
- self->recordState.isTranscribing = false;
278
- return code;
282
+ dispatch_async(dQueue, ^{
283
+ self->recordState.isStoppedByAction = false;
284
+ self->recordState.isTranscribing = true;
285
+ self->recordState.jobId = jobId;
286
+ int code = [self fullTranscribeWithProgress:onProgress jobId:jobId audioData:audioData audioDataCount:audioDataCount options:options];
287
+ self->recordState.jobId = -1;
288
+ self->recordState.isTranscribing = false;
289
+ onEnd(code);
290
+ });
279
291
  }
280
292
 
281
293
  - (void)stopAudio {
@@ -293,6 +305,7 @@ void AudioInputCallback(void * inUserData,
293
305
  }
294
306
  self->recordState.isCapturing = false;
295
307
  self->recordState.isStoppedByAction = true;
308
+ dispatch_barrier_sync(dQueue, ^{});
296
309
  }
297
310
 
298
311
  - (void)stopCurrentTranscribe {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whisper.rn",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "React Native binding of whisper.cpp",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",