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.
package/README.md CHANGED
@@ -8,9 +8,12 @@ React Native binding of [whisper.cpp](https://github.com/ggerganov/whisper.cpp).
8
8
 
9
9
  [whisper.cpp](https://github.com/ggerganov/whisper.cpp): High-performance inference of [OpenAI's Whisper](https://github.com/openai/whisper) automatic speech recognition (ASR) model
10
10
 
11
- <img src="https://user-images.githubusercontent.com/3001525/225511664-8b2ba3ec-864d-4f55-bcb0-447aef168a32.jpeg" width="500" />
11
+ ## Screenshots
12
12
 
13
- > Run example with release mode on iPhone 13 Pro Max
13
+ | <img src="https://github.com/mybigday/whisper.rn/assets/3001525/2fea7b2d-c911-44fb-9afc-8efc7b594446" width="300" /> | <img src="https://github.com/mybigday/whisper.rn/assets/3001525/a5005a6c-44f7-4db9-95e8-0fd951a2e147" width="300" /> |
14
+ | :------------------------------------------: | :------------------------------------------: |
15
+ | iOS: Tested on iPhone 13 Pro Max | Android: Tested on Pixel 6 |
16
+ | (tiny.en, Core ML enabled) | (tiny.en, armv8.2-a+fp16) |
14
17
 
15
18
  ## Installation
16
19
 
@@ -47,8 +50,7 @@ Add the following line to ```android/app/src/main/AndroidManifest.xml```
47
50
  import { initWhisper } from 'whisper.rn'
48
51
 
49
52
  const whisperContext = await initWhisper({
50
- filePath: 'file://.../ggml-base.en.bin',
51
- isBundleAsset: false, // Set to true if you want to load the model from bundle resources, the filePath will be like `ggml-base.en.bin`
53
+ filePath: 'file://.../ggml-tiny.en.bin',
52
54
  })
53
55
 
54
56
  const sampleFilePath = 'file://.../sample.wav'
@@ -81,35 +83,92 @@ In Android, you may need to request the microphone permission by [`PermissionAnd
81
83
 
82
84
  Please visit the [Documentation](docs/) for more details.
83
85
 
86
+ ## Usage with assets
87
+
88
+ You can also use the model file / audio file from assets:
89
+
90
+ ```js
91
+ import { initWhisper } from 'whisper.rn'
92
+
93
+ const whisperContext = await initWhisper({
94
+ filePath: require('../assets/ggml-tiny.en.bin'),
95
+ })
96
+
97
+ const { stop, promise } =
98
+ whisperContext.transcribe(require('../assets/sample.wav'), options)
99
+
100
+ // ...
101
+ ```
102
+
103
+ This requires editing the `metro.config.js` to support assets:
104
+
105
+ ```js
106
+ // ...
107
+ const defaultAssetExts = require('metro-config/src/defaults/defaults').assetExts
108
+
109
+ module.exports = {
110
+ // ...
111
+ resolver: {
112
+ // ...
113
+ assetExts: [
114
+ ...defaultAssetExts,
115
+ 'bin', // whisper.rn: ggml model binary
116
+ 'mil', // whisper.rn: CoreML model asset
117
+ ]
118
+ },
119
+ }
120
+ ```
121
+
122
+ Please note that it will significantly increase the size of the app in release mode.
123
+
84
124
  ## Core ML support
85
125
 
86
126
  __*Platform: iOS 15.0+, tvOS 15.0+*__
87
127
 
88
128
  To use Core ML on iOS, you will need to have the Core ML model files.
89
129
 
90
- The `.mlmodelc` model files is load depend on the ggml model file path. For example, if your ggml model path is `ggml-base.en.bin`, the Core ML model path will be `ggml-base.en-encoder.mlmodelc`. Please note that the ggml model is still needed as decoder or encoder fallback.
130
+ The `.mlmodelc` model files is load depend on the ggml model file path. For example, if your ggml model path is `ggml-tiny.en.bin`, the Core ML model path will be `ggml-tiny.en-encoder.mlmodelc`. Please note that the ggml model is still needed as decoder or encoder fallback.
91
131
 
92
132
  Currently there is no official way to get the Core ML models by URL, you will need to convert Core ML models by yourself. Please see [Core ML Support](https://github.com/ggerganov/whisper.cpp#core-ml-support) of whisper.cpp for more details.
93
133
 
94
- During the `.mlmodelc` is a directory, you will need to download 5 files:
134
+ During the `.mlmodelc` is a directory, you will need to download 5 files (3 required):
95
135
 
96
136
  ```json5
97
137
  [
98
138
  'model.mil',
99
- 'metadata.json',
100
139
  'coremldata.bin',
101
140
  'weights/weight.bin',
102
- 'analytics/coremldata.bin',
141
+ // Not required:
142
+ // 'metadata.json', 'analytics/coremldata.bin',
103
143
  ]
104
144
  ```
105
145
 
106
- Or just add them to your app's bundle resources, like the example app does, but this would increase the app size significantly.
146
+ Or just use `require` to bundle that in your app, like the example app does, but this would increase the app size significantly.
147
+
148
+ ```js
149
+ const whisperContext = await initWhisper({
150
+ filePath: require('../assets/ggml-tiny.en.bin')
151
+ coreMLModelAsset:
152
+ Platform.OS === 'ios'
153
+ ? {
154
+ filename: 'ggml-tiny.en-encoder.mlmodelc',
155
+ assets: [
156
+ require('../assets/ggml-tiny.en-encoder.mlmodelc/weights/weight.bin'),
157
+ require('../assets/ggml-tiny.en-encoder.mlmodelc/model.mil'),
158
+ require('../assets/ggml-tiny.en-encoder.mlmodelc/coremldata.bin'),
159
+ ],
160
+ }
161
+ : undefined,
162
+ })
163
+ ```
164
+
165
+ In real world, we recommended to split the asset imports into another platform specific file (e.g. `context-opts.ios.js`) to avoid these unused files in the bundle for Android.
107
166
 
108
167
  ## Run with example
109
168
 
110
- The example app is using [react-native-fs](https://github.com/itinance/react-native-fs) to download the model file and audio file.
169
+ The example app provide a simple UI for testing the functions.
111
170
 
112
- Model: `base.en` in https://huggingface.co/datasets/ggerganov/whisper.cpp
171
+ Used Whisper model: `tiny.en` in https://huggingface.co/datasets/ggerganov/whisper.cpp
113
172
  Sample file: `jfk.wav` in https://github.com/ggerganov/whisper.cpp/tree/master/samples
114
173
 
115
174
  For test better performance on transcribe, you can run the app in Release mode.
@@ -130,6 +189,10 @@ jest.mock('whisper.rn', () => require('whisper.rn/jest/mock'))
130
189
 
131
190
  See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
132
191
 
192
+ ## Troubleshooting
193
+
194
+ See the [troubleshooting](TROUBLESHOOTING.md) if you encounter any problem while using `whisper.rn`.
195
+
133
196
  ## License
134
197
 
135
198
  MIT
@@ -59,6 +59,15 @@ android {
59
59
  targetCompatibility JavaVersion.VERSION_1_8
60
60
  }
61
61
 
62
+ sourceSets {
63
+ main {
64
+ if (isNewArchitectureEnabled()) {
65
+ java.srcDirs += ['src/newarch']
66
+ } else {
67
+ java.srcDirs += ['src/oldarch']
68
+ }
69
+ }
70
+ }
62
71
  }
63
72
 
64
73
  repositories {
@@ -0,0 +1,83 @@
1
+ package com.rnwhisper;
2
+
3
+ import android.content.Context;
4
+
5
+ import java.io.BufferedInputStream;
6
+ import java.io.FileOutputStream;
7
+ import java.io.File;
8
+ import java.io.InputStream;
9
+ import java.io.OutputStream;
10
+ import java.net.URL;
11
+ import java.net.URLConnection;
12
+
13
+ /**
14
+ * NOTE: This is simple downloader,
15
+ * the main purpose is supported load assets on RN Debug mode,
16
+ * so it's a very crude implementation.
17
+ *
18
+ * If you want to use file download in production to load model / audio files,
19
+ * I would recommend using react-native-fs or expo-file-system to manage the files.
20
+ */
21
+ public class Downloader {
22
+ private static Context context;
23
+
24
+ public Downloader(Context context) {
25
+ this.context = context;
26
+ }
27
+
28
+ private String getDir() {
29
+ String dir = context.getCacheDir().getAbsolutePath() + "/rnwhisper_debug_assets/";
30
+ File file = new File(dir);
31
+ if (!file.exists()) {
32
+ file.mkdirs();
33
+ }
34
+ return dir;
35
+ }
36
+
37
+ private boolean fileExists(String filename) {
38
+ File file = new File(getDir() + filename);
39
+ return file.exists();
40
+ }
41
+
42
+ public String downloadFile(String urlPath) throws Exception {
43
+ String filename = urlPath.substring(urlPath.lastIndexOf('/') + 1);
44
+ if (filename.contains("?")) {
45
+ filename = filename.substring(0, filename.indexOf("?"));
46
+ }
47
+ String filepath = getDir() + filename;
48
+ if (fileExists(filename)) {
49
+ return filepath;
50
+ }
51
+ try {
52
+ URL url = new URL(urlPath);
53
+ URLConnection connection = url.openConnection();
54
+ connection.connect();
55
+ InputStream input = new BufferedInputStream(url.openStream());
56
+ OutputStream output = new FileOutputStream(filepath);
57
+ byte data[] = new byte[1024];
58
+ int count;
59
+ while ((count = input.read(data)) != -1) {
60
+ output.write(data, 0, count);
61
+ }
62
+ output.flush();
63
+ output.close();
64
+ input.close();
65
+ } catch (Exception e) {
66
+ throw e;
67
+ }
68
+ return filepath;
69
+ }
70
+
71
+ private void deleteFile(File fileOrDir) {
72
+ if (fileOrDir.isDirectory()) {
73
+ for (File child : fileOrDir.listFiles()) {
74
+ deleteFile(child);
75
+ }
76
+ }
77
+ fileOrDir.delete();
78
+ }
79
+
80
+ public void clearCache() {
81
+ deleteFile(new File(getDir()));
82
+ }
83
+ }
@@ -1,28 +1,48 @@
1
1
  package com.rnwhisper;
2
2
 
3
3
  import androidx.annotation.NonNull;
4
+ import androidx.annotation.Nullable;
4
5
 
5
- import com.facebook.react.ReactPackage;
6
6
  import com.facebook.react.bridge.NativeModule;
7
7
  import com.facebook.react.bridge.ReactApplicationContext;
8
- import com.facebook.react.uimanager.ViewManager;
8
+ import com.facebook.react.module.model.ReactModuleInfo;
9
+ import com.facebook.react.module.model.ReactModuleInfoProvider;
10
+ import com.facebook.react.TurboReactPackage;
9
11
 
10
- import java.util.ArrayList;
11
- import java.util.Collections;
12
12
  import java.util.List;
13
+ import java.util.HashMap;
14
+ import java.util.Map;
13
15
 
14
- public class RNWhisperPackage implements ReactPackage {
15
- @NonNull
16
+ public class RNWhisperPackage extends TurboReactPackage {
17
+
18
+ @Nullable
16
19
  @Override
17
- public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
18
- List<NativeModule> modules = new ArrayList<>();
19
- modules.add(new RNWhisperModule(reactContext));
20
- return modules;
20
+ public NativeModule getModule(String name, ReactApplicationContext reactContext) {
21
+ if (name.equals(RNWhisperModule.NAME)) {
22
+ return new com.rnwhisper.RNWhisperModule(reactContext);
23
+ } else {
24
+ return null;
25
+ }
21
26
  }
22
27
 
23
- @NonNull
24
28
  @Override
25
- public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
26
- return Collections.emptyList();
29
+ public ReactModuleInfoProvider getReactModuleInfoProvider() {
30
+ return () -> {
31
+ final Map<String, ReactModuleInfo> moduleInfos = new HashMap<>();
32
+ boolean isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
33
+ moduleInfos.put(
34
+ RNWhisperModule.NAME,
35
+ new ReactModuleInfo(
36
+ RNWhisperModule.NAME,
37
+ RNWhisperModule.NAME,
38
+ false, // canOverrideExistingModule
39
+ false, // needsEagerInit
40
+ true, // hasConstants
41
+ false, // isCxxModule
42
+ isTurboModule // isTurboModule
43
+ )
44
+ );
45
+ return moduleInfos;
46
+ };
27
47
  }
28
48
  }
@@ -26,6 +26,7 @@ import java.io.File;
26
26
  import java.io.FileInputStream;
27
27
  import java.io.IOException;
28
28
  import java.io.InputStream;
29
+ import java.io.PushbackInputStream;
29
30
  import java.nio.ByteBuffer;
30
31
  import java.nio.ByteOrder;
31
32
  import java.nio.ShortBuffer;
@@ -281,10 +282,10 @@ public class WhisperContext {
281
282
  eventEmitter.emit(eventName, event);
282
283
  }
283
284
 
284
- public WritableMap transcribeFile(int jobId, String filePath, ReadableMap options) throws IOException, Exception {
285
+ public WritableMap transcribeInputStream(int jobId, InputStream inputStream, ReadableMap options) throws IOException, Exception {
285
286
  this.jobId = jobId;
286
287
  isTranscribing = true;
287
- float[] audioData = decodeWaveFile(new File(filePath));
288
+ float[] audioData = decodeWaveFile(inputStream);
288
289
  int code = full(jobId, options, audioData, audioData.length);
289
290
  isTranscribing = false;
290
291
  this.jobId = -1;
@@ -383,14 +384,12 @@ public class WhisperContext {
383
384
  freeContext(context);
384
385
  }
385
386
 
386
- public static float[] decodeWaveFile(File file) throws IOException {
387
+ public static float[] decodeWaveFile(InputStream inputStream) throws IOException {
387
388
  ByteArrayOutputStream baos = new ByteArrayOutputStream();
388
- try (InputStream inputStream = new FileInputStream(file)) {
389
- byte[] buffer = new byte[1024];
390
- int bytesRead;
391
- while ((bytesRead = inputStream.read(buffer)) != -1) {
392
- baos.write(buffer, 0, bytesRead);
393
- }
389
+ byte[] buffer = new byte[1024];
390
+ int bytesRead;
391
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
392
+ baos.write(buffer, 0, bytesRead);
394
393
  }
395
394
  ByteBuffer byteBuffer = ByteBuffer.wrap(baos.toByteArray());
396
395
  byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
@@ -472,6 +471,7 @@ public class WhisperContext {
472
471
 
473
472
  protected static native long initContext(String modelPath);
474
473
  protected static native long initContextWithAsset(AssetManager assetManager, String modelPath);
474
+ protected static native long initContextWithInputStream(PushbackInputStream inputStream);
475
475
  protected static native int fullTranscribe(
476
476
  int job_id,
477
477
  long context,
@@ -3,7 +3,7 @@ LOCAL_LDLIBS := -landroid -llog
3
3
 
4
4
  # Make the final output library smaller by only keeping the symbols referenced from the app.
5
5
  ifneq ($(APP_OPTIM),debug)
6
- LOCAL_CFLAGS += -O3
6
+ LOCAL_CFLAGS += -O3 -DNDEBUG
7
7
  LOCAL_CFLAGS += -fvisibility=hidden -fvisibility-inlines-hidden
8
8
  LOCAL_CFLAGS += -ffunction-sections -fdata-sections
9
9
  LOCAL_LDFLAGS += -Wl,--gc-sections
@@ -20,6 +20,97 @@ static inline int min(int a, int b) {
20
20
  return (a < b) ? a : b;
21
21
  }
22
22
 
23
+ // Load model from input stream (used for drawable / raw resources)
24
+ struct input_stream_context {
25
+ JNIEnv *env;
26
+ jobject input_stream;
27
+ };
28
+
29
+ static size_t input_stream_read(void *ctx, void *output, size_t read_size) {
30
+ input_stream_context *context = (input_stream_context *)ctx;
31
+ JNIEnv *env = context->env;
32
+ jobject input_stream = context->input_stream;
33
+ jclass input_stream_class = env->GetObjectClass(input_stream);
34
+
35
+ jbyteArray buffer = env->NewByteArray(read_size);
36
+ jint bytes_read = env->CallIntMethod(
37
+ input_stream,
38
+ env->GetMethodID(input_stream_class, "read", "([B)I"),
39
+ buffer
40
+ );
41
+
42
+ if (bytes_read > 0) {
43
+ env->GetByteArrayRegion(buffer, 0, bytes_read, (jbyte *) output);
44
+ }
45
+
46
+ env->DeleteLocalRef(buffer);
47
+
48
+ return bytes_read;
49
+ }
50
+
51
+ static bool input_stream_is_eof(void *ctx) {
52
+ input_stream_context *context = (input_stream_context *)ctx;
53
+ JNIEnv *env = context->env;
54
+ jobject input_stream = context->input_stream;
55
+
56
+ jclass input_stream_class = env->GetObjectClass(input_stream);
57
+
58
+ jbyteArray buffer = env->NewByteArray(1);
59
+ jint bytes_read = env->CallIntMethod(
60
+ input_stream,
61
+ env->GetMethodID(input_stream_class, "read", "([B)I"),
62
+ buffer
63
+ );
64
+
65
+ bool is_eof = (bytes_read == -1);
66
+ if (!is_eof) {
67
+ // If we successfully read a byte, "unread" it by pushing it back into the stream.
68
+ env->CallVoidMethod(
69
+ input_stream,
70
+ env->GetMethodID(input_stream_class, "unread", "([BII)V"),
71
+ buffer,
72
+ 0,
73
+ 1
74
+ );
75
+ }
76
+
77
+ env->DeleteLocalRef(buffer);
78
+
79
+ return is_eof;
80
+ }
81
+
82
+ static void input_stream_close(void *ctx) {
83
+ input_stream_context *context = (input_stream_context *)ctx;
84
+ JNIEnv *env = context->env;
85
+ jobject input_stream = context->input_stream;
86
+ jclass input_stream_class = env->GetObjectClass(input_stream);
87
+
88
+ env->CallVoidMethod(
89
+ input_stream,
90
+ env->GetMethodID(input_stream_class, "close", "()V")
91
+ );
92
+
93
+ env->DeleteGlobalRef(input_stream);
94
+ }
95
+
96
+ static struct whisper_context *whisper_init_from_input_stream(
97
+ JNIEnv *env,
98
+ jobject input_stream // PushbackInputStream
99
+ ) {
100
+ input_stream_context *context = new input_stream_context;
101
+ context->env = env;
102
+ context->input_stream = env->NewGlobalRef(input_stream);
103
+
104
+ whisper_model_loader loader = {
105
+ .context = context,
106
+ .read = &input_stream_read,
107
+ .eof = &input_stream_is_eof,
108
+ .close = &input_stream_close
109
+ };
110
+ return whisper_init(&loader);
111
+ }
112
+
113
+ // Load model from asset
23
114
  static size_t asset_read(void *ctx, void *output, size_t read_size) {
24
115
  return AAsset_read((AAsset *) ctx, output, read_size);
25
116
  }
@@ -81,6 +172,17 @@ Java_com_rnwhisper_WhisperContext_initContextWithAsset(
81
172
  return reinterpret_cast<jlong>(context);
82
173
  }
83
174
 
175
+ JNIEXPORT jlong JNICALL
176
+ Java_com_rnwhisper_WhisperContext_initContextWithInputStream(
177
+ JNIEnv *env,
178
+ jobject thiz,
179
+ jobject input_stream
180
+ ) {
181
+ UNUSED(thiz);
182
+ struct whisper_context *context = nullptr;
183
+ context = whisper_init_from_input_stream(env, input_stream);
184
+ return reinterpret_cast<jlong>(context);
185
+ }
84
186
 
85
187
  JNIEXPORT jint JNICALL
86
188
  Java_com_rnwhisper_WhisperContext_fullTranscribe(