whisper.rn 0.3.0-rc.4 → 0.3.0-rc.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.
@@ -0,0 +1,286 @@
1
+ package com.rnwhisper;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import android.util.Log;
5
+ import android.os.Build;
6
+ import android.os.Handler;
7
+ import android.os.AsyncTask;
8
+ import android.media.AudioRecord;
9
+
10
+ import com.facebook.react.bridge.Promise;
11
+ import com.facebook.react.bridge.ReactApplicationContext;
12
+ import com.facebook.react.bridge.ReactMethod;
13
+ import com.facebook.react.bridge.LifecycleEventListener;
14
+ import com.facebook.react.bridge.ReadableMap;
15
+ import com.facebook.react.bridge.WritableMap;
16
+ import com.facebook.react.module.annotations.ReactModule;
17
+
18
+ import java.util.HashMap;
19
+ import java.util.Random;
20
+ import java.io.File;
21
+ import java.io.FileInputStream;
22
+ import java.io.PushbackInputStream;
23
+
24
+ @ReactModule(name = RNWhisperModule.NAME)
25
+ public class RNWhisperModule extends NativeRNWhisperSpec implements LifecycleEventListener {
26
+ public static final String NAME = "RNWhisper";
27
+
28
+ private ReactApplicationContext reactContext;
29
+ private Downloader downloader;
30
+
31
+ public RNWhisperModule(ReactApplicationContext reactContext) {
32
+ super(reactContext);
33
+ reactContext.addLifecycleEventListener(this);
34
+ this.reactContext = reactContext;
35
+ this.downloader = new Downloader(reactContext);
36
+ }
37
+
38
+ @Override
39
+ @NonNull
40
+ public String getName() {
41
+ return NAME;
42
+ }
43
+
44
+ @Override
45
+ public HashMap<String, Object> getTypedExportedConstants() {
46
+ HashMap<String, Object> constants = new HashMap<>();
47
+
48
+ // iOS only constants, put for passing type checks
49
+ constants.put("useCoreML", false);
50
+ constants.put("coreMLAllowFallback", false);
51
+
52
+ return constants;
53
+ }
54
+
55
+ private HashMap<Integer, WhisperContext> contexts = new HashMap<>();
56
+
57
+ private int getResourceIdentifier(String filePath) {
58
+ int identifier = reactContext.getResources().getIdentifier(
59
+ filePath,
60
+ "drawable",
61
+ reactContext.getPackageName()
62
+ );
63
+ if (identifier == 0) {
64
+ identifier = reactContext.getResources().getIdentifier(
65
+ filePath,
66
+ "raw",
67
+ reactContext.getPackageName()
68
+ );
69
+ }
70
+ return identifier;
71
+ }
72
+
73
+ @ReactMethod
74
+ public void initContext(final ReadableMap options, final Promise promise) {
75
+ new AsyncTask<Void, Void, Integer>() {
76
+ private Exception exception;
77
+
78
+ @Override
79
+ protected Integer doInBackground(Void... voids) {
80
+ try {
81
+ String modelPath = options.getString("filePath");
82
+ boolean isBundleAsset = options.getBoolean("isBundleAsset");
83
+
84
+ String modelFilePath = modelPath;
85
+ if (!isBundleAsset && (modelPath.startsWith("http://") || modelPath.startsWith("https://"))) {
86
+ modelFilePath = downloader.downloadFile(modelPath);
87
+ }
88
+
89
+ long context;
90
+ int resId = getResourceIdentifier(modelFilePath);
91
+ if (resId > 0) {
92
+ context = WhisperContext.initContextWithInputStream(
93
+ new PushbackInputStream(reactContext.getResources().openRawResource(resId))
94
+ );
95
+ } else if (isBundleAsset) {
96
+ context = WhisperContext.initContextWithAsset(reactContext.getAssets(), modelFilePath);
97
+ } else {
98
+ context = WhisperContext.initContext(modelFilePath);
99
+ }
100
+ if (context == 0) {
101
+ throw new Exception("Failed to initialize context");
102
+ }
103
+ int id = Math.abs(new Random().nextInt());
104
+ WhisperContext whisperContext = new WhisperContext(id, reactContext, context);
105
+ contexts.put(id, whisperContext);
106
+ return id;
107
+ } catch (Exception e) {
108
+ exception = e;
109
+ return null;
110
+ }
111
+ }
112
+
113
+ @Override
114
+ protected void onPostExecute(Integer id) {
115
+ if (exception != null) {
116
+ promise.reject(exception);
117
+ return;
118
+ }
119
+ promise.resolve(id);
120
+ }
121
+ }.execute();
122
+ }
123
+
124
+ @ReactMethod
125
+ public void transcribeFile(double id, double jobId, String filePath, ReadableMap options, Promise promise) {
126
+ final WhisperContext context = contexts.get((int) id);
127
+ if (context == null) {
128
+ promise.reject("Context not found");
129
+ return;
130
+ }
131
+ if (context.isCapturing()) {
132
+ promise.reject("The context is in realtime transcribe mode");
133
+ return;
134
+ }
135
+ if (context.isTranscribing()) {
136
+ promise.reject("Context is already transcribing");
137
+ return;
138
+ }
139
+ new AsyncTask<Void, Void, WritableMap>() {
140
+ private Exception exception;
141
+
142
+ @Override
143
+ protected WritableMap doInBackground(Void... voids) {
144
+ try {
145
+ String waveFilePath = filePath;
146
+
147
+ if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
148
+ waveFilePath = downloader.downloadFile(filePath);
149
+ }
150
+
151
+ int resId = getResourceIdentifier(waveFilePath);
152
+ if (resId > 0) {
153
+ return context.transcribeInputStream(
154
+ (int) jobId,
155
+ reactContext.getResources().openRawResource(resId),
156
+ options
157
+ );
158
+ }
159
+
160
+ return context.transcribeInputStream(
161
+ (int) jobId,
162
+ new FileInputStream(new File(waveFilePath)),
163
+ options
164
+ );
165
+ } catch (Exception e) {
166
+ exception = e;
167
+ return null;
168
+ }
169
+ }
170
+
171
+ @Override
172
+ protected void onPostExecute(WritableMap data) {
173
+ if (exception != null) {
174
+ promise.reject(exception);
175
+ return;
176
+ }
177
+ promise.resolve(data);
178
+ }
179
+ }.execute();
180
+ }
181
+
182
+ @ReactMethod
183
+ public void startRealtimeTranscribe(double id, double jobId, ReadableMap options, Promise promise) {
184
+ final WhisperContext context = contexts.get((int) id);
185
+ if (context == null) {
186
+ promise.reject("Context not found");
187
+ return;
188
+ }
189
+ if (context.isCapturing()) {
190
+ promise.reject("Context is already in capturing");
191
+ return;
192
+ }
193
+ int state = context.startRealtimeTranscribe((int) jobId, options);
194
+ if (state == AudioRecord.STATE_INITIALIZED) {
195
+ promise.resolve(null);
196
+ return;
197
+ }
198
+ promise.reject("Failed to start realtime transcribe. State: " + state);
199
+ }
200
+
201
+ @ReactMethod
202
+ public void abortTranscribe(double contextId, double jobId, Promise promise) {
203
+ WhisperContext context = contexts.get((int) contextId);
204
+ if (context == null) {
205
+ promise.reject("Context not found");
206
+ return;
207
+ }
208
+ context.stopTranscribe((int) jobId);
209
+ }
210
+
211
+ @ReactMethod
212
+ public void releaseContext(double id, Promise promise) {
213
+ final int contextId = (int) id;
214
+ new AsyncTask<Void, Void, Void>() {
215
+ private Exception exception;
216
+
217
+ @Override
218
+ protected Void doInBackground(Void... voids) {
219
+ try {
220
+ WhisperContext context = contexts.get(contextId);
221
+ if (context == null) {
222
+ throw new Exception("Context " + id + " not found");
223
+ }
224
+ context.release();
225
+ contexts.remove(contextId);
226
+ } catch (Exception e) {
227
+ exception = e;
228
+ }
229
+ return null;
230
+ }
231
+
232
+ @Override
233
+ protected void onPostExecute(Void result) {
234
+ if (exception != null) {
235
+ promise.reject(exception);
236
+ return;
237
+ }
238
+ promise.resolve(null);
239
+ }
240
+ }.execute();
241
+ }
242
+
243
+ @ReactMethod
244
+ public void releaseAllContexts(Promise promise) {
245
+ new AsyncTask<Void, Void, Void>() {
246
+ private Exception exception;
247
+
248
+ @Override
249
+ protected Void doInBackground(Void... voids) {
250
+ try {
251
+ onHostDestroy();
252
+ } catch (Exception e) {
253
+ exception = e;
254
+ }
255
+ return null;
256
+ }
257
+
258
+ @Override
259
+ protected void onPostExecute(Void result) {
260
+ if (exception != null) {
261
+ promise.reject(exception);
262
+ return;
263
+ }
264
+ promise.resolve(null);
265
+ }
266
+ }.execute();
267
+ }
268
+
269
+ @Override
270
+ public void onHostResume() {
271
+ }
272
+
273
+ @Override
274
+ public void onHostPause() {
275
+ }
276
+
277
+ @Override
278
+ public void onHostDestroy() {
279
+ WhisperContext.abortAllTranscribe();
280
+ for (WhisperContext context : contexts.values()) {
281
+ context.release();
282
+ }
283
+ contexts.clear();
284
+ downloader.clearCache();
285
+ }
286
+ }
@@ -18,17 +18,22 @@ import com.facebook.react.module.annotations.ReactModule;
18
18
 
19
19
  import java.util.HashMap;
20
20
  import java.util.Random;
21
+ import java.io.File;
22
+ import java.io.FileInputStream;
23
+ import java.io.PushbackInputStream;
21
24
 
22
25
  @ReactModule(name = RNWhisperModule.NAME)
23
26
  public class RNWhisperModule extends ReactContextBaseJavaModule implements LifecycleEventListener {
24
27
  public static final String NAME = "RNWhisper";
25
28
 
26
29
  private ReactApplicationContext reactContext;
30
+ private Downloader downloader;
27
31
 
28
32
  public RNWhisperModule(ReactApplicationContext reactContext) {
29
33
  super(reactContext);
30
34
  reactContext.addLifecycleEventListener(this);
31
35
  this.reactContext = reactContext;
36
+ this.downloader = new Downloader(reactContext);
32
37
  }
33
38
 
34
39
  @Override
@@ -39,19 +44,48 @@ public class RNWhisperModule extends ReactContextBaseJavaModule implements Lifec
39
44
 
40
45
  private HashMap<Integer, WhisperContext> contexts = new HashMap<>();
41
46
 
47
+ private int getResourceIdentifier(String filePath) {
48
+ int identifier = reactContext.getResources().getIdentifier(
49
+ filePath,
50
+ "drawable",
51
+ reactContext.getPackageName()
52
+ );
53
+ if (identifier == 0) {
54
+ identifier = reactContext.getResources().getIdentifier(
55
+ filePath,
56
+ "raw",
57
+ reactContext.getPackageName()
58
+ );
59
+ }
60
+ return identifier;
61
+ }
62
+
42
63
  @ReactMethod
43
- public void initContext(final String modelPath, final boolean isBundleAsset, final Promise promise) {
64
+ public void initContext(final ReadableMap options, final Promise promise) {
44
65
  new AsyncTask<Void, Void, Integer>() {
45
66
  private Exception exception;
46
67
 
47
68
  @Override
48
69
  protected Integer doInBackground(Void... voids) {
49
70
  try {
71
+ String modelPath = options.getString("filePath");
72
+ boolean isBundleAsset = options.getBoolean("isBundleAsset");
73
+
74
+ String modelFilePath = modelPath;
75
+ if (!isBundleAsset && (modelPath.startsWith("http://") || modelPath.startsWith("https://"))) {
76
+ modelFilePath = downloader.downloadFile(modelPath);
77
+ }
78
+
50
79
  long context;
51
- if (isBundleAsset) {
52
- context = WhisperContext.initContextWithAsset(reactContext.getAssets(), modelPath);
80
+ int resId = getResourceIdentifier(modelFilePath);
81
+ if (resId > 0) {
82
+ context = WhisperContext.initContextWithInputStream(
83
+ new PushbackInputStream(reactContext.getResources().openRawResource(resId))
84
+ );
85
+ } else if (isBundleAsset) {
86
+ context = WhisperContext.initContextWithAsset(reactContext.getAssets(), modelFilePath);
53
87
  } else {
54
- context = WhisperContext.initContext(modelPath);
88
+ context = WhisperContext.initContext(modelFilePath);
55
89
  }
56
90
  if (context == 0) {
57
91
  throw new Exception("Failed to initialize context");
@@ -98,7 +132,26 @@ public class RNWhisperModule extends ReactContextBaseJavaModule implements Lifec
98
132
  @Override
99
133
  protected WritableMap doInBackground(Void... voids) {
100
134
  try {
101
- return context.transcribeFile(jobId, filePath, options);
135
+ String waveFilePath = filePath;
136
+
137
+ if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
138
+ waveFilePath = downloader.downloadFile(filePath);
139
+ }
140
+
141
+ int resId = getResourceIdentifier(waveFilePath);
142
+ if (resId > 0) {
143
+ return context.transcribeInputStream(
144
+ (int) jobId,
145
+ reactContext.getResources().openRawResource(resId),
146
+ options
147
+ );
148
+ }
149
+
150
+ return context.transcribeInputStream(
151
+ (int) jobId,
152
+ new FileInputStream(new File(waveFilePath)),
153
+ options
154
+ );
102
155
  } catch (Exception e) {
103
156
  exception = e;
104
157
  return null;
@@ -217,5 +270,6 @@ public class RNWhisperModule extends ReactContextBaseJavaModule implements Lifec
217
270
  context.release();
218
271
  }
219
272
  contexts.clear();
273
+ downloader.clearCache();
220
274
  }
221
275
  }
package/ios/RNWhisper.mm CHANGED
@@ -1,29 +1,42 @@
1
1
  #import "RNWhisper.h"
2
2
  #import "RNWhisperContext.h"
3
+ #import "RNWhisperDownloader.h"
3
4
  #include <stdlib.h>
4
5
  #include <string>
5
6
 
7
+ #ifdef RCT_NEW_ARCH_ENABLED
8
+ #import <RNWhisperSpec/RNWhisperSpec.h>
9
+ #endif
10
+
6
11
  @implementation RNWhisper
7
12
 
8
13
  NSMutableDictionary *contexts;
9
14
 
10
15
  RCT_EXPORT_MODULE()
11
16
 
17
+ + (BOOL)requiresMainQueueSetup
18
+ {
19
+ return NO;
20
+ }
21
+
12
22
  - (NSDictionary *)constantsToExport
13
23
  {
14
24
  return @{
15
25
  #if WHISPER_USE_COREML
16
- @"WHISPER_USE_COREML": @YES,
26
+ @"useCoreML": @YES,
27
+ #else
28
+ @"useCoreML": @NO,
17
29
  #endif
18
30
  #if WHISPER_COREML_ALLOW_FALLBACK
19
- @"WHISPER_COREML_ALLOW_FALLBACK": @YES,
31
+ @"coreMLAllowFallback": @YES,
32
+ #else
33
+ @"coreMLAllowFallback": @NO,
20
34
  #endif
21
35
  };
22
36
  }
23
37
 
24
38
  RCT_REMAP_METHOD(initContext,
25
- withPath:(NSString *)modelPath
26
- withBundleResource:(BOOL)isBundleAsset
39
+ withOptions:(NSDictionary *)modelOptions
27
40
  withResolver:(RCTPromiseResolveBlock)resolve
28
41
  withRejecter:(RCTPromiseRejectBlock)reject)
29
42
  {
@@ -31,7 +44,26 @@ RCT_REMAP_METHOD(initContext,
31
44
  contexts = [[NSMutableDictionary alloc] init];
32
45
  }
33
46
 
47
+ NSString *modelPath = [modelOptions objectForKey:@"filePath"];
48
+ BOOL isBundleAsset = [[modelOptions objectForKey:@"isBundleAsset"] boolValue];
49
+
50
+ // For support debug assets in development mode
51
+ BOOL downloadCoreMLAssets = [[modelOptions objectForKey:@"downloadCoreMLAssets"] boolValue];
52
+ if (downloadCoreMLAssets) {
53
+ NSArray *coreMLAssets = [modelOptions objectForKey:@"coreMLAssets"];
54
+ // Download coreMLAssets ([{ uri, filepath }])
55
+ for (NSDictionary *coreMLAsset in coreMLAssets) {
56
+ NSString *path = coreMLAsset[@"uri"];
57
+ if ([path hasPrefix:@"http://"] || [path hasPrefix:@"https://"]) {
58
+ [RNWhisperDownloader downloadFile:path toFile:coreMLAsset[@"filepath"]];
59
+ }
60
+ }
61
+ }
62
+
34
63
  NSString *path = modelPath;
64
+ if ([path hasPrefix:@"http://"] || [path hasPrefix:@"https://"]) {
65
+ path = [RNWhisperDownloader downloadFile:path toFile:nil];
66
+ }
35
67
  if (isBundleAsset) {
36
68
  path = [[NSBundle mainBundle] pathForResource:modelPath ofType:nil];
37
69
  }
@@ -71,10 +103,13 @@ RCT_REMAP_METHOD(transcribeFile,
71
103
  return;
72
104
  }
73
105
 
74
- NSURL *url = [NSURL fileURLWithPath:waveFilePath];
106
+ NSString *path = waveFilePath;
107
+ if ([path hasPrefix:@"http://"] || [path hasPrefix:@"https://"]) {
108
+ path = [RNWhisperDownloader downloadFile:path toFile:nil];
109
+ }
75
110
 
76
111
  int count = 0;
77
- float *waveFile = [self decodeWaveFile:url count:&count];
112
+ float *waveFile = [self decodeWaveFile:path count:&count];
78
113
  if (waveFile == nil) {
79
114
  reject(@"whisper_error", @"Invalid file", nil);
80
115
  return;
@@ -172,8 +207,9 @@ RCT_REMAP_METHOD(releaseAllContexts,
172
207
  resolve(nil);
173
208
  }
174
209
 
175
- - (float *)decodeWaveFile:(NSURL*)fileURL count:(int *)count {
176
- NSData *fileData = [NSData dataWithContentsOfURL:fileURL];
210
+ - (float *)decodeWaveFile:(NSString*)filePath count:(int *)count {
211
+ NSURL *url = [NSURL fileURLWithPath:filePath];
212
+ NSData *fileData = [NSData dataWithContentsOfURL:url];
177
213
  if (fileData == nil) {
178
214
  return nil;
179
215
  }
@@ -206,6 +242,16 @@ RCT_REMAP_METHOD(releaseAllContexts,
206
242
 
207
243
  [contexts removeAllObjects];
208
244
  contexts = nil;
245
+
246
+ [RNWhisperDownloader clearCache];
209
247
  }
210
248
 
249
+ #ifdef RCT_NEW_ARCH_ENABLED
250
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
251
+ (const facebook::react::ObjCTurboModule::InitParams &)params
252
+ {
253
+ return std::make_shared<facebook::react::NativeRNWhisperSpecJSI>(params);
254
+ }
255
+ #endif
256
+
211
257
  @end
@@ -0,0 +1,8 @@
1
+
2
+
3
+ @interface RNWhisperDownloader : NSObject
4
+
5
+ + (NSString *)downloadFile:(NSString *)urlString toFile:(NSString *)path;
6
+ + (void)clearCache;
7
+
8
+ @end
@@ -0,0 +1,39 @@
1
+ #import "RNWhisperDownloader.h"
2
+
3
+ /**
4
+ * NOTE: This is simple downloader,
5
+ * the main purpose is supported load assets on RN Debug mode,
6
+ * so it's a very crude implementation.
7
+ *
8
+ * If you want to use file download in production to load model / audio files,
9
+ * I would recommend using react-native-fs or expo-file-system to manage the files.
10
+ */
11
+ @implementation RNWhisperDownloader
12
+
13
+ + (NSString *)downloadFile:(NSString *)urlString toFile:(NSString *)path {
14
+ NSURL *url = [NSURL URLWithString:urlString];
15
+ NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"rnwhisper_debug_assets/"];
16
+ if (path) {
17
+ filePath = [filePath stringByAppendingPathComponent:path];
18
+ } else {
19
+ filePath = [filePath stringByAppendingPathComponent:[url lastPathComponent]];
20
+ }
21
+
22
+ NSString *folderPath = [filePath stringByDeletingLastPathComponent];
23
+ if (![[NSFileManager defaultManager] fileExistsAtPath:folderPath]) {
24
+ [[NSFileManager defaultManager] createDirectoryAtPath:folderPath withIntermediateDirectories:YES attributes:nil error:nil];
25
+ }
26
+ if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
27
+ return filePath;
28
+ }
29
+ NSData *urlData = [NSData dataWithContentsOfURL:url];
30
+ [urlData writeToFile:filePath atomically:YES];
31
+ return filePath;
32
+ }
33
+
34
+ + (void)clearCache {
35
+ NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"rnwhisper_debug_assets/"];
36
+ [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
37
+ }
38
+
39
+ @end
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _reactNative = require("react-native");
8
+ var _default = _reactNative.TurboModuleRegistry.get('RNWhisper');
9
+ exports.default = _default;
10
+ //# sourceMappingURL=NativeRNWhisper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_reactNative","require","_default","TurboModuleRegistry","get","exports","default"],"sourceRoot":"../../src","sources":["NativeRNWhisper.ts"],"mappings":";;;;;;AACA,IAAAA,YAAA,GAAAC,OAAA;AAAkD,IAAAC,QAAA,GA6EnCC,gCAAmB,CAACC,GAAG,CAAO,WAAW,CAAC;AAAAC,OAAA,CAAAC,OAAA,GAAAJ,QAAA"}