whisper.rn 0.3.3 → 0.3.5

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/README.md CHANGED
@@ -21,13 +21,26 @@ React Native binding of [whisper.cpp](https://github.com/ggerganov/whisper.cpp).
21
21
  npm install whisper.rn
22
22
  ```
23
23
 
24
- For iOS, please re-run `npx pod-install` again.
24
+ #### iOS
25
+
26
+ Please re-run `npx pod-install` again.
27
+
28
+ #### Android
25
29
 
26
30
  If you want to use `medium` or `large` model, the [Extended Virtual Addressing](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_kernel_extended-virtual-addressing) capability is recommended to enable on iOS project.
27
31
 
28
32
  For Android, it's recommended to use `ndkVersion = "24.0.8215888"` (or above) in your root project build configuration for Apple Silicon Macs. Otherwise please follow this trobleshooting [issue](./TROUBLESHOOTING.md#android-got-build-error-unknown-host-cpu-architecture-arm64-on-apple-silicon-macs).
29
33
 
30
- For Expo, you will need to prebuild the project before using it. See [Expo guide](https://docs.expo.io/guides/using-libraries/#using-a-library-in-a-expo-project) for more details.
34
+ Don't forget to add proguard rule if it's enabled in project (android/app/proguard-rules.pro):
35
+
36
+ ```proguard
37
+ # whisper.rn
38
+ -keep class com.rnwhisper.** { *; }
39
+ ```
40
+
41
+ #### Expo
42
+
43
+ You will need to prebuild the project before using it. See [Expo guide](https://docs.expo.io/guides/using-libraries/#using-a-library-in-a-expo-project) for more details.
31
44
 
32
45
  ## Add Microphone Permissions (Optional)
33
46
 
@@ -30,6 +30,11 @@ def getExtOrIntegerDefault(name) {
30
30
  return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["RNWhisper_" + name]).toInteger()
31
31
  }
32
32
 
33
+ def reactNativeArchitectures() {
34
+ def value = project.getProperties().get("reactNativeArchitectures")
35
+ return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
36
+ }
37
+
33
38
  android {
34
39
  ndkVersion getExtOrDefault("ndkVersion")
35
40
  compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
@@ -38,10 +43,15 @@ android {
38
43
  minSdkVersion getExtOrIntegerDefault("minSdkVersion")
39
44
  targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
40
45
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
46
+ externalNativeBuild {
47
+ cmake {
48
+ abiFilters (*reactNativeArchitectures())
49
+ }
50
+ }
41
51
  }
42
52
  externalNativeBuild {
43
- ndkBuild {
44
- path 'src/main/jni/whisper/Android.mk'
53
+ cmake {
54
+ path = file('src/main/CMakeLists.txt')
45
55
  }
46
56
  }
47
57
  buildTypes {
@@ -0,0 +1,55 @@
1
+ cmake_minimum_required(VERSION 3.10)
2
+
3
+ project(whisper.rn)
4
+
5
+ set(CMAKE_CXX_STANDARD 11)
6
+ set(RNWHISPER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../../cpp)
7
+
8
+ set(
9
+ SOURCE_FILES
10
+ ${RNWHISPER_LIB_DIR}/ggml.c
11
+ ${RNWHISPER_LIB_DIR}/whisper.cpp
12
+ ${RNWHISPER_LIB_DIR}/rn-whisper.cpp
13
+ ${CMAKE_SOURCE_DIR}/jni.cpp
14
+ )
15
+
16
+ find_library(LOG_LIB log)
17
+
18
+ function(build_library target_name)
19
+ add_library(
20
+ ${target_name}
21
+ SHARED
22
+ ${SOURCE_FILES}
23
+ )
24
+
25
+ target_link_libraries(${target_name} ${LOG_LIB} android)
26
+
27
+ if (${target_name} STREQUAL "whisper_v8fp16_va")
28
+ target_compile_options(${target_name} PRIVATE -march=armv8.2-a+fp16)
29
+ elseif (${target_name} STREQUAL "whisper_vfpv4")
30
+ target_compile_options(${target_name} PRIVATE -mfpu=neon-vfpv4)
31
+ endif ()
32
+
33
+ # NOTE: If you want to debug the native code, you can uncomment if and endif
34
+ # if (NOT ${CMAKE_BUILD_TYPE} STREQUAL "Debug")
35
+
36
+ target_compile_options(${target_name} PRIVATE -O3 -DNDEBUG -pthread)
37
+ target_compile_options(${target_name} PRIVATE -fvisibility=hidden -fvisibility-inlines-hidden)
38
+ target_compile_options(${target_name} PRIVATE -ffunction-sections -fdata-sections)
39
+
40
+ target_link_options(${target_name} PRIVATE -Wl,--gc-sections)
41
+ target_link_options(${target_name} PRIVATE -Wl,--exclude-libs,ALL)
42
+ target_link_options(${target_name} PRIVATE -flto)
43
+
44
+ # endif ()
45
+ endfunction()
46
+
47
+ build_library("whisper") # Default target
48
+
49
+ if (${ANDROID_ABI} STREQUAL "arm64-v8a")
50
+ build_library("whisper_v8fp16_va")
51
+ elseif (${ANDROID_ABI} STREQUAL "armeabi-v7a")
52
+ build_library("whisper_vfpv4")
53
+ endif ()
54
+
55
+ include_directories(${RNWHISPER_LIB_DIR})
@@ -0,0 +1,268 @@
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
+
17
+ import java.util.HashMap;
18
+ import java.util.Random;
19
+ import java.io.File;
20
+ import java.io.FileInputStream;
21
+ import java.io.PushbackInputStream;
22
+
23
+ public class RNWhisper implements LifecycleEventListener {
24
+ private ReactApplicationContext reactContext;
25
+ private Downloader downloader;
26
+
27
+ public RNWhisper(ReactApplicationContext reactContext) {
28
+ reactContext.addLifecycleEventListener(this);
29
+ this.reactContext = reactContext;
30
+ this.downloader = new Downloader(reactContext);
31
+ }
32
+
33
+ public HashMap<String, Object> getTypedExportedConstants() {
34
+ HashMap<String, Object> constants = new HashMap<>();
35
+
36
+ // iOS only constants, put for passing type checks
37
+ constants.put("useCoreML", false);
38
+ constants.put("coreMLAllowFallback", false);
39
+
40
+ return constants;
41
+ }
42
+
43
+ private HashMap<Integer, WhisperContext> contexts = new HashMap<>();
44
+
45
+ private int getResourceIdentifier(String filePath) {
46
+ int identifier = reactContext.getResources().getIdentifier(
47
+ filePath,
48
+ "drawable",
49
+ reactContext.getPackageName()
50
+ );
51
+ if (identifier == 0) {
52
+ identifier = reactContext.getResources().getIdentifier(
53
+ filePath,
54
+ "raw",
55
+ reactContext.getPackageName()
56
+ );
57
+ }
58
+ return identifier;
59
+ }
60
+
61
+ public void initContext(final ReadableMap options, final Promise promise) {
62
+ new AsyncTask<Void, Void, Integer>() {
63
+ private Exception exception;
64
+
65
+ @Override
66
+ protected Integer doInBackground(Void... voids) {
67
+ try {
68
+ String modelPath = options.getString("filePath");
69
+ boolean isBundleAsset = options.getBoolean("isBundleAsset");
70
+
71
+ String modelFilePath = modelPath;
72
+ if (!isBundleAsset && (modelPath.startsWith("http://") || modelPath.startsWith("https://"))) {
73
+ modelFilePath = downloader.downloadFile(modelPath);
74
+ }
75
+
76
+ long context;
77
+ int resId = getResourceIdentifier(modelFilePath);
78
+ if (resId > 0) {
79
+ context = WhisperContext.initContextWithInputStream(
80
+ new PushbackInputStream(reactContext.getResources().openRawResource(resId))
81
+ );
82
+ } else if (isBundleAsset) {
83
+ context = WhisperContext.initContextWithAsset(reactContext.getAssets(), modelFilePath);
84
+ } else {
85
+ context = WhisperContext.initContext(modelFilePath);
86
+ }
87
+ if (context == 0) {
88
+ throw new Exception("Failed to initialize context");
89
+ }
90
+ int id = Math.abs(new Random().nextInt());
91
+ WhisperContext whisperContext = new WhisperContext(id, reactContext, context);
92
+ contexts.put(id, whisperContext);
93
+ return id;
94
+ } catch (Exception e) {
95
+ exception = e;
96
+ return null;
97
+ }
98
+ }
99
+
100
+ @Override
101
+ protected void onPostExecute(Integer id) {
102
+ if (exception != null) {
103
+ promise.reject(exception);
104
+ return;
105
+ }
106
+ promise.resolve(id);
107
+ }
108
+ }.execute();
109
+ }
110
+
111
+ public void transcribeFile(double id, double jobId, String filePath, ReadableMap options, Promise promise) {
112
+ final WhisperContext context = contexts.get((int) id);
113
+ if (context == null) {
114
+ promise.reject("Context not found");
115
+ return;
116
+ }
117
+ if (context.isCapturing()) {
118
+ promise.reject("The context is in realtime transcribe mode");
119
+ return;
120
+ }
121
+ if (context.isTranscribing()) {
122
+ promise.reject("Context is already transcribing");
123
+ return;
124
+ }
125
+ new AsyncTask<Void, Void, WritableMap>() {
126
+ private Exception exception;
127
+
128
+ @Override
129
+ protected WritableMap doInBackground(Void... voids) {
130
+ try {
131
+ String waveFilePath = filePath;
132
+
133
+ if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
134
+ waveFilePath = downloader.downloadFile(filePath);
135
+ }
136
+
137
+ int resId = getResourceIdentifier(waveFilePath);
138
+ if (resId > 0) {
139
+ return context.transcribeInputStream(
140
+ (int) jobId,
141
+ reactContext.getResources().openRawResource(resId),
142
+ options
143
+ );
144
+ }
145
+
146
+ return context.transcribeInputStream(
147
+ (int) jobId,
148
+ new FileInputStream(new File(waveFilePath)),
149
+ options
150
+ );
151
+ } catch (Exception e) {
152
+ exception = e;
153
+ return null;
154
+ }
155
+ }
156
+
157
+ @Override
158
+ protected void onPostExecute(WritableMap data) {
159
+ if (exception != null) {
160
+ promise.reject(exception);
161
+ return;
162
+ }
163
+ promise.resolve(data);
164
+ }
165
+ }.execute();
166
+ }
167
+
168
+ public void startRealtimeTranscribe(double id, double jobId, ReadableMap options, Promise promise) {
169
+ final WhisperContext context = contexts.get((int) id);
170
+ if (context == null) {
171
+ promise.reject("Context not found");
172
+ return;
173
+ }
174
+ if (context.isCapturing()) {
175
+ promise.reject("Context is already in capturing");
176
+ return;
177
+ }
178
+ int state = context.startRealtimeTranscribe((int) jobId, options);
179
+ if (state == AudioRecord.STATE_INITIALIZED) {
180
+ promise.resolve(null);
181
+ return;
182
+ }
183
+ promise.reject("Failed to start realtime transcribe. State: " + state);
184
+ }
185
+
186
+ public void abortTranscribe(double contextId, double jobId, Promise promise) {
187
+ WhisperContext context = contexts.get((int) contextId);
188
+ if (context == null) {
189
+ promise.reject("Context not found");
190
+ return;
191
+ }
192
+ context.stopTranscribe((int) jobId);
193
+ }
194
+
195
+ public void releaseContext(double id, Promise promise) {
196
+ final int contextId = (int) id;
197
+ new AsyncTask<Void, Void, Void>() {
198
+ private Exception exception;
199
+
200
+ @Override
201
+ protected Void doInBackground(Void... voids) {
202
+ try {
203
+ WhisperContext context = contexts.get(contextId);
204
+ if (context == null) {
205
+ throw new Exception("Context " + id + " not found");
206
+ }
207
+ context.release();
208
+ contexts.remove(contextId);
209
+ } catch (Exception e) {
210
+ exception = e;
211
+ }
212
+ return null;
213
+ }
214
+
215
+ @Override
216
+ protected void onPostExecute(Void result) {
217
+ if (exception != null) {
218
+ promise.reject(exception);
219
+ return;
220
+ }
221
+ promise.resolve(null);
222
+ }
223
+ }.execute();
224
+ }
225
+
226
+ public void releaseAllContexts(Promise promise) {
227
+ new AsyncTask<Void, Void, Void>() {
228
+ private Exception exception;
229
+
230
+ @Override
231
+ protected Void doInBackground(Void... voids) {
232
+ try {
233
+ onHostDestroy();
234
+ } catch (Exception e) {
235
+ exception = e;
236
+ }
237
+ return null;
238
+ }
239
+
240
+ @Override
241
+ protected void onPostExecute(Void result) {
242
+ if (exception != null) {
243
+ promise.reject(exception);
244
+ return;
245
+ }
246
+ promise.resolve(null);
247
+ }
248
+ }.execute();
249
+ }
250
+
251
+ @Override
252
+ public void onHostResume() {
253
+ }
254
+
255
+ @Override
256
+ public void onHostPause() {
257
+ }
258
+
259
+ @Override
260
+ public void onHostDestroy() {
261
+ WhisperContext.abortAllTranscribe();
262
+ for (WhisperContext context : contexts.values()) {
263
+ context.release();
264
+ }
265
+ contexts.clear();
266
+ downloader.clearCache();
267
+ }
268
+ }
@@ -1,18 +1,11 @@
1
1
  package com.rnwhisper;
2
2
 
3
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
4
 
10
5
  import com.facebook.react.bridge.Promise;
11
6
  import com.facebook.react.bridge.ReactApplicationContext;
12
7
  import com.facebook.react.bridge.ReactMethod;
13
- import com.facebook.react.bridge.LifecycleEventListener;
14
8
  import com.facebook.react.bridge.ReadableMap;
15
- import com.facebook.react.bridge.WritableMap;
16
9
  import com.facebook.react.module.annotations.ReactModule;
17
10
 
18
11
  import java.util.HashMap;
@@ -22,17 +15,14 @@ import java.io.FileInputStream;
22
15
  import java.io.PushbackInputStream;
23
16
 
24
17
  @ReactModule(name = RNWhisperModule.NAME)
25
- public class RNWhisperModule extends NativeRNWhisperSpec implements LifecycleEventListener {
18
+ public class RNWhisperModule extends NativeRNWhisperSpec {
26
19
  public static final String NAME = "RNWhisper";
27
20
 
28
- private ReactApplicationContext reactContext;
29
- private Downloader downloader;
21
+ private RNWhisper rnwhisper;
30
22
 
31
23
  public RNWhisperModule(ReactApplicationContext reactContext) {
32
24
  super(reactContext);
33
- reactContext.addLifecycleEventListener(this);
34
- this.reactContext = reactContext;
35
- this.downloader = new Downloader(reactContext);
25
+ rnwhisper = new RNWhisper(reactContext);
36
26
  }
37
27
 
38
28
  @Override
@@ -43,244 +33,36 @@ public class RNWhisperModule extends NativeRNWhisperSpec implements LifecycleEve
43
33
 
44
34
  @Override
45
35
  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;
36
+ return rnwhisper.getTypedExportedConstants();
71
37
  }
72
38
 
73
39
  @ReactMethod
74
40
  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();
41
+ rnwhisper.initContext(options, promise);
122
42
  }
123
43
 
124
44
  @ReactMethod
125
45
  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();
46
+ rnwhisper.transcribeFile(id, jobId, filePath, options, promise);
180
47
  }
181
48
 
182
49
  @ReactMethod
183
50
  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);
51
+ rnwhisper.startRealtimeTranscribe(id, jobId, options, promise);
199
52
  }
200
53
 
201
54
  @ReactMethod
202
55
  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);
56
+ rnwhisper.abortTranscribe(contextId, jobId, promise);
209
57
  }
210
58
 
211
59
  @ReactMethod
212
60
  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();
61
+ rnwhisper.releaseContext(id, promise);
241
62
  }
242
63
 
243
64
  @ReactMethod
244
65
  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();
66
+ rnwhisper.releaseAllContexts(promise);
285
67
  }
286
68
  }