From 45afdc7e4ec22b2b943e91dda7597d75000c12ae Mon Sep 17 00:00:00 2001 From: Jhen-Jie Hong Date: Fri, 8 Nov 2024 12:58:32 +0800 Subject: [PATCH] feat: add bench method & update example (#269) * feat(example): bump to rn 0.74 & add react-navigation * feat(example): wip Bench container * feat(example): download models * feat(ios, cpp): add bench method * feat(ios): log as markdown * fix: timings * feat(example): copy to clipboard for logs * fix(ts): lint errors * feat(example): update default select * feat(cpp, android): add system config & update android module * fix(cpp): system_info parse * fix: patch * feat(cpp): refactor whisper_timings * fix(example): use react-native-gesture-handler * feat(example): split copy logs button * fix(example): log * feat(example): move useFlashAttn usage to context-opts * feat(example): minor refactor * chore: update deps & docgen * fix(example): remove default props * feat(ts): update mock * fix: deps * feat(example): update model button style * fix(example): realtime button title --- .../main/java/com/rnwhisper/RNWhisper.java | 9 + .../java/com/rnwhisper/WhisperContext.java | 5 + android/src/main/jni.cpp | 13 + .../java/com/rnwhisper/RNWhisperModule.java | 5 + .../java/com/rnwhisper/RNWhisperModule.java | 5 + cpp/rn-whisper.cpp | 91 ++ cpp/rn-whisper.h | 2 + cpp/whisper.cpp | 43 +- cpp/whisper.h | 18 + docs/API/README.md | 60 +- docs/API/classes/WhisperContext.md | 35 +- docs/API/enums/AudioSessionCategoryIos.md | 12 +- .../enums/AudioSessionCategoryOptionIos.md | 14 +- docs/API/enums/AudioSessionModeIos.md | 16 +- example/android/app/build.gradle | 1 - .../java/com/rnwhisperexample/MainActivity.kt | 5 + .../com/rnwhisperexample/MainApplication.kt | 4 +- .../res/drawable/rn_edit_text_material.xml | 5 +- example/android/build.gradle | 6 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- example/android/gradlew | 14 +- example/android/gradlew.bat | 22 +- example/ios/Podfile | 11 +- example/ios/Podfile.lock | 1012 ++++++++------ .../project.pbxproj | 32 +- example/ios/RNWhisperExample/AppDelegate.mm | 4 +- example/ios/RNWhisperExample/Info.plist | 2 +- .../RNWhisperExample/PrivacyInfo.xcprivacy | 37 + example/package.json | 10 +- example/src/App.tsx | 440 +------ example/src/Bench.tsx | 360 +++++ example/src/Button.tsx | 36 + example/src/Transcribe.tsx | 386 ++++++ example/src/context-opts.ios.ts | 1 + example/src/util.ts | 14 + example/yarn.lock | 710 ++++++---- ios/RNWhisper.mm | 19 + ios/RNWhisperContext.h | 1 + ios/RNWhisperContext.mm | 10 + jest/mock.js | 10 +- package.json | 6 +- scripts/whisper.cpp.patch | 66 +- scripts/whisper.h.patch | 29 +- src/NativeRNWhisper.ts | 2 + src/index.ts | 15 + yarn.lock | 1163 ++++++++++------- 46 files changed, 3140 insertions(+), 1623 deletions(-) create mode 100644 example/ios/RNWhisperExample/PrivacyInfo.xcprivacy create mode 100644 example/src/Bench.tsx create mode 100644 example/src/Button.tsx create mode 100644 example/src/Transcribe.tsx create mode 100644 example/src/util.ts diff --git a/android/src/main/java/com/rnwhisper/RNWhisper.java b/android/src/main/java/com/rnwhisper/RNWhisper.java index bcab6f2..e04a5d9 100644 --- a/android/src/main/java/com/rnwhisper/RNWhisper.java +++ b/android/src/main/java/com/rnwhisper/RNWhisper.java @@ -235,6 +235,15 @@ protected void onPostExecute(Void result) { tasks.put(task, "abortTranscribe-" + id); } + public void bench(double id, double nThreads, Promise promise) { + final WhisperContext context = contexts.get((int) id); + if (context == null) { + promise.reject("Context not found"); + return; + } + promise.resolve(context.bench((int) nThreads)); + } + public void releaseContext(double id, Promise promise) { final int contextId = (int) id; AsyncTask task = new AsyncTask() { diff --git a/android/src/main/java/com/rnwhisper/WhisperContext.java b/android/src/main/java/com/rnwhisper/WhisperContext.java index 0229474..0b5b2be 100644 --- a/android/src/main/java/com/rnwhisper/WhisperContext.java +++ b/android/src/main/java/com/rnwhisper/WhisperContext.java @@ -423,6 +423,10 @@ public void stopCurrentTranscribe() { stopTranscribe(this.jobId); } + public String bench(int n_threads) { + return bench(context, n_threads); + } + public void release() { stopCurrentTranscribe(); freeContext(context); @@ -527,4 +531,5 @@ protected static native int fullWithJob( int slice_index, int n_samples ); + protected static native String bench(long context, int n_threads); } diff --git a/android/src/main/jni.cpp b/android/src/main/jni.cpp index 8e2f013..206fc23 100644 --- a/android/src/main/jni.cpp +++ b/android/src/main/jni.cpp @@ -508,4 +508,17 @@ Java_com_rnwhisper_WhisperContext_getTextSegmentSpeakerTurnNext( return whisper_full_get_segment_speaker_turn_next(context, index); } +JNIEXPORT jstring JNICALL +Java_com_rnwhisper_WhisperContext_bench( + JNIEnv *env, + jobject thiz, + jlong context_ptr, + jint n_threads +) { + UNUSED(thiz); + struct whisper_context *context = reinterpret_cast(context_ptr); + std::string result = rnwhisper::bench(context, n_threads); + return env->NewStringUTF(result.c_str()); +} + } // extern "C" diff --git a/android/src/newarch/java/com/rnwhisper/RNWhisperModule.java b/android/src/newarch/java/com/rnwhisper/RNWhisperModule.java index bb87857..bdf6972 100644 --- a/android/src/newarch/java/com/rnwhisper/RNWhisperModule.java +++ b/android/src/newarch/java/com/rnwhisper/RNWhisperModule.java @@ -57,6 +57,11 @@ public void abortTranscribe(double contextId, double jobId, Promise promise) { rnwhisper.abortTranscribe(contextId, jobId, promise); } + @ReactMethod + public void bench(double id, double nThreads, Promise promise) { + rnwhisper.bench(id, nThreads, promise); + } + @ReactMethod public void releaseContext(double id, Promise promise) { rnwhisper.releaseContext(id, promise); diff --git a/android/src/oldarch/java/com/rnwhisper/RNWhisperModule.java b/android/src/oldarch/java/com/rnwhisper/RNWhisperModule.java index da4491d..e0f37c7 100644 --- a/android/src/oldarch/java/com/rnwhisper/RNWhisperModule.java +++ b/android/src/oldarch/java/com/rnwhisper/RNWhisperModule.java @@ -57,6 +57,11 @@ public void abortTranscribe(double contextId, double jobId, Promise promise) { rnwhisper.abortTranscribe(contextId, jobId, promise); } + @ReactMethod + public void bench(double id, double nThreads, Promise promise) { + rnwhisper.bench(id, nThreads, promise); + } + @ReactMethod public void releaseContext(double id, Promise promise) { rnwhisper.releaseContext(id, promise); diff --git a/cpp/rn-whisper.cpp b/cpp/rn-whisper.cpp index ab98be6..2565690 100644 --- a/cpp/rn-whisper.cpp +++ b/cpp/rn-whisper.cpp @@ -8,6 +8,97 @@ namespace rnwhisper { +const char * system_info(void) { + static std::string s; + s = ""; + if (wsp_ggml_cpu_has_avx() == 1) s += "AVX "; + if (wsp_ggml_cpu_has_avx2() == 1) s += "AVX2 "; + if (wsp_ggml_cpu_has_avx512() == 1) s += "AVX512 "; + if (wsp_ggml_cpu_has_fma() == 1) s += "FMA "; + if (wsp_ggml_cpu_has_neon() == 1) s += "NEON "; + if (wsp_ggml_cpu_has_arm_fma() == 1) s += "ARM_FMA "; + if (wsp_ggml_cpu_has_metal() == 1) s += "METAL "; + if (wsp_ggml_cpu_has_f16c() == 1) s += "F16C "; + if (wsp_ggml_cpu_has_fp16_va() == 1) s += "FP16_VA "; + if (wsp_ggml_cpu_has_blas() == 1) s += "BLAS "; + if (wsp_ggml_cpu_has_sse3() == 1) s += "SSE3 "; + if (wsp_ggml_cpu_has_ssse3() == 1) s += "SSSE3 "; + if (wsp_ggml_cpu_has_vsx() == 1) s += "VSX "; +#ifdef WHISPER_USE_COREML + s += "COREML "; +#endif + s.erase(s.find_last_not_of(" ") + 1); + return s.c_str(); +} + +std::string bench(struct whisper_context * ctx, int n_threads) { + const int n_mels = whisper_model_n_mels(ctx); + + if (int ret = whisper_set_mel(ctx, nullptr, 0, n_mels)) { + return "error: failed to set mel: " + std::to_string(ret); + } + // heat encoder + if (int ret = whisper_encode(ctx, 0, n_threads) != 0) { + return "error: failed to encode: " + std::to_string(ret); + } + + whisper_token tokens[512]; + memset(tokens, 0, sizeof(tokens)); + + // prompt heat + if (int ret = whisper_decode(ctx, tokens, 256, 0, n_threads) != 0) { + return "error: failed to decode: " + std::to_string(ret); + } + + // text-generation heat + if (int ret = whisper_decode(ctx, tokens, 1, 256, n_threads) != 0) { + return "error: failed to decode: " + std::to_string(ret); + } + + whisper_reset_timings(ctx); + + // actual run + if (int ret = whisper_encode(ctx, 0, n_threads) != 0) { + return "error: failed to encode: " + std::to_string(ret); + } + + // text-generation + for (int i = 0; i < 256; i++) { + if (int ret = whisper_decode(ctx, tokens, 1, i, n_threads) != 0) { + return "error: failed to decode: " + std::to_string(ret); + } + } + + // batched decoding + for (int i = 0; i < 64; i++) { + if (int ret = whisper_decode(ctx, tokens, 5, 0, n_threads) != 0) { + return "error: failed to decode: " + std::to_string(ret); + } + } + + // prompt processing + for (int i = 0; i < 16; i++) { + if (int ret = whisper_decode(ctx, tokens, 256, 0, n_threads) != 0) { + return "error: failed to decode: " + std::to_string(ret); + } + } + + const struct whisper_timings * timings = whisper_get_timings(ctx); + + const int32_t n_encode = std::max(1, timings->n_encode); + const int32_t n_decode = std::max(1, timings->n_decode); + const int32_t n_batchd = std::max(1, timings->n_batchd); + const int32_t n_prompt = std::max(1, timings->n_prompt); + + return std::string("[") + + "\"" + system_info() + "\"," + + std::to_string(n_threads) + "," + + std::to_string(1e-3f * timings->t_encode_us / n_encode) + "," + + std::to_string(1e-3f * timings->t_decode_us / n_decode) + "," + + std::to_string(1e-3f * timings->t_batchd_us / n_batchd) + "," + + std::to_string(1e-3f * timings->t_prompt_us / n_prompt) + "]"; +} + void high_pass_filter(std::vector & data, float cutoff, float sample_rate) { const float rc = 1.0f / (2.0f * M_PI * cutoff); const float dt = 1.0f / sample_rate; diff --git a/cpp/rn-whisper.h b/cpp/rn-whisper.h index 46adbb9..b25c881 100644 --- a/cpp/rn-whisper.h +++ b/cpp/rn-whisper.h @@ -9,6 +9,8 @@ namespace rnwhisper { +std::string bench(whisper_context * ctx, int n_threads); + struct vad_params { bool use_vad = false; float vad_thold = 0.6f; diff --git a/cpp/whisper.cpp b/cpp/whisper.cpp index 24bfd05..9c88649 100644 --- a/cpp/whisper.cpp +++ b/cpp/whisper.cpp @@ -4190,28 +4190,51 @@ whisper_token whisper_token_transcribe(struct whisper_context * ctx) { return ctx->vocab.token_transcribe; } +struct whisper_timings * whisper_get_timings(struct whisper_context * ctx) { + if (ctx->state == nullptr) { + return nullptr; + } + return new whisper_timings { + .load_us = ctx->t_load_us, + .t_start_us = ctx->t_start_us, + .fail_p = ctx->state->n_fail_p, + .fail_h = ctx->state->n_fail_h, + .t_mel_us = ctx->state->t_mel_us, + .n_sample = ctx->state->n_sample, + .n_encode = ctx->state->n_encode, + .n_decode = ctx->state->n_decode, + .n_batchd = ctx->state->n_batchd, + .n_prompt = ctx->state->n_prompt, + .t_sample_us = ctx->state->t_sample_us, + .t_encode_us = ctx->state->t_encode_us, + .t_decode_us = ctx->state->t_decode_us, + .t_batchd_us = ctx->state->t_batchd_us, + .t_prompt_us = ctx->state->t_prompt_us, + }; +} + void whisper_print_timings(struct whisper_context * ctx) { const int64_t t_end_us = wsp_ggml_time_us(); + const struct whisper_timings * timings = whisper_get_timings(ctx); WHISPER_LOG_INFO("\n"); - WHISPER_LOG_INFO("%s: load time = %8.2f ms\n", __func__, ctx->t_load_us / 1000.0f); + WHISPER_LOG_INFO("%s: load time = %8.2f ms\n", __func__, timings->load_us / 1000.0f); if (ctx->state != nullptr) { - const int32_t n_sample = std::max(1, ctx->state->n_sample); const int32_t n_encode = std::max(1, ctx->state->n_encode); const int32_t n_decode = std::max(1, ctx->state->n_decode); const int32_t n_batchd = std::max(1, ctx->state->n_batchd); const int32_t n_prompt = std::max(1, ctx->state->n_prompt); - WHISPER_LOG_INFO("%s: fallbacks = %3d p / %3d h\n", __func__, ctx->state->n_fail_p, ctx->state->n_fail_h); - WHISPER_LOG_INFO("%s: mel time = %8.2f ms\n", __func__, ctx->state->t_mel_us / 1000.0f); - WHISPER_LOG_INFO("%s: sample time = %8.2f ms / %5d runs (%8.2f ms per run)\n", __func__, 1e-3f * ctx->state->t_sample_us, n_sample, 1e-3f * ctx->state->t_sample_us / n_sample); - WHISPER_LOG_INFO("%s: encode time = %8.2f ms / %5d runs (%8.2f ms per run)\n", __func__, 1e-3f * ctx->state->t_encode_us, n_encode, 1e-3f * ctx->state->t_encode_us / n_encode); - WHISPER_LOG_INFO("%s: decode time = %8.2f ms / %5d runs (%8.2f ms per run)\n", __func__, 1e-3f * ctx->state->t_decode_us, n_decode, 1e-3f * ctx->state->t_decode_us / n_decode); - WHISPER_LOG_INFO("%s: batchd time = %8.2f ms / %5d runs (%8.2f ms per run)\n", __func__, 1e-3f * ctx->state->t_batchd_us, n_batchd, 1e-3f * ctx->state->t_batchd_us / n_batchd); - WHISPER_LOG_INFO("%s: prompt time = %8.2f ms / %5d runs (%8.2f ms per run)\n", __func__, 1e-3f * ctx->state->t_prompt_us, n_prompt, 1e-3f * ctx->state->t_prompt_us / n_prompt); + WHISPER_LOG_INFO("%s: fallbacks = %3d p / %3d h\n", __func__, timings->fail_p, timings->fail_h); + WHISPER_LOG_INFO("%s: mel time = %8.2f ms\n", __func__, timings->t_mel_us/1000.0f); + WHISPER_LOG_INFO("%s: sample time = %8.2f ms / %5d runs (%8.2f ms per run)\n", __func__, 1e-3f * timings->t_sample_us, n_sample, 1e-3f * timings->t_sample_us / n_sample); + WHISPER_LOG_INFO("%s: encode time = %8.2f ms / %5d runs (%8.2f ms per run)\n", __func__, 1e-3f * timings->t_encode_us, n_encode, 1e-3f * timings->t_encode_us / n_encode); + WHISPER_LOG_INFO("%s: decode time = %8.2f ms / %5d runs (%8.2f ms per run)\n", __func__, 1e-3f * timings->t_decode_us, n_decode, 1e-3f * timings->t_decode_us / n_decode); + WHISPER_LOG_INFO("%s: batchd time = %8.2f ms / %5d runs (%8.2f ms per run)\n", __func__, 1e-3f * timings->t_batchd_us, n_batchd, 1e-3f * timings->t_batchd_us / n_batchd); + WHISPER_LOG_INFO("%s: prompt time = %8.2f ms / %5d runs (%8.2f ms per run)\n", __func__, 1e-3f * timings->t_prompt_us, n_prompt, 1e-3f * timings->t_prompt_us / n_prompt); } - WHISPER_LOG_INFO("%s: total time = %8.2f ms\n", __func__, (t_end_us - ctx->t_start_us)/1000.0f); + WHISPER_LOG_INFO("%s: total time = %8.2f ms\n", __func__, (t_end_us - timings->t_start_us)/1000.0f); } void whisper_reset_timings(struct whisper_context * ctx) { diff --git a/cpp/whisper.h b/cpp/whisper.h index 5f84c22..382fb95 100644 --- a/cpp/whisper.h +++ b/cpp/whisper.h @@ -424,6 +424,24 @@ extern "C" { WHISPER_API whisper_token whisper_token_transcribe(struct whisper_context * ctx); // Performance information from the default state. + struct whisper_timings { + int64_t load_us; + int64_t t_start_us; + int32_t fail_p; + int32_t fail_h; + int64_t t_mel_us; + int32_t n_sample; + int32_t n_encode; + int32_t n_decode; + int32_t n_batchd; + int32_t n_prompt; + int64_t t_sample_us; + int64_t t_encode_us; + int64_t t_decode_us; + int64_t t_batchd_us; + int64_t t_prompt_us; + }; + WHISPER_API struct whisper_timings * whisper_get_timings(struct whisper_context * ctx); WHISPER_API void whisper_print_timings(struct whisper_context * ctx); WHISPER_API void whisper_reset_timings(struct whisper_context * ctx); diff --git a/docs/API/README.md b/docs/API/README.md index c5b4fb5..55c9d4d 100644 --- a/docs/API/README.md +++ b/docs/API/README.md @@ -17,6 +17,7 @@ whisper.rn ### Type Aliases - [AudioSessionSettingIos](README.md#audiosessionsettingios) +- [BenchResult](README.md#benchresult) - [ContextOptions](README.md#contextoptions) - [TranscribeFileOptions](README.md#transcribefileoptions) - [TranscribeNewSegmentsNativeEvent](README.md#transcribenewsegmentsnativeevent) @@ -58,7 +59,28 @@ whisper.rn #### Defined in -[index.ts:76](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L76) +[index.ts:76](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L76) + +___ + +### BenchResult + +Ƭ **BenchResult**: `Object` + +#### Type declaration + +| Name | Type | +| :------ | :------ | +| `batchMs` | `number` | +| `config` | `string` | +| `decodeMs` | `number` | +| `encodeMs` | `number` | +| `nThreads` | `number` | +| `promptMs` | `number` | + +#### Defined in + +[index.ts:177](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L177) ___ @@ -76,11 +98,12 @@ ___ | `filePath` | `string` \| `number` | - | | `isBundleAsset?` | `boolean` | Is the file path a bundle asset for pure string filePath | | `useCoreMLIos?` | `boolean` | Prefer to use Core ML model if exists. If set to false, even if the Core ML model exists, it will not be used. | +| `useFlashAttn?` | `boolean` | Use Flash Attention, only recommended if GPU available | | `useGpu?` | `boolean` | Use GPU if available. Currently iOS only, if it's enabled, Core ML option will be ignored. | #### Defined in -[index.ts:441](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L441) +[index.ts:456](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L456) ___ @@ -90,7 +113,7 @@ ___ #### Defined in -[index.ts:59](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L59) +[index.ts:59](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L59) ___ @@ -108,7 +131,7 @@ ___ #### Defined in -[index.ts:52](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L52) +[index.ts:52](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L52) ___ @@ -127,7 +150,7 @@ ___ #### Defined in -[index.ts:45](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L45) +[index.ts:45](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L45) ___ @@ -148,7 +171,6 @@ ___ | `maxThreads?` | `number` | Number of threads to use during computation (Default: 2 for 4-core devices, 4 for more cores) | | `offset?` | `number` | Time offset in milliseconds | | `prompt?` | `string` | Initial Prompt | -| `speedUp?` | `boolean` | Speed up audio by x2 (reduced accuracy) | | `tdrzEnable?` | `boolean` | Enable tinydiarize (requires a tdrz model) | | `temperature?` | `number` | Tnitial decoding temperature | | `temperatureInc?` | `number` | - | @@ -158,7 +180,7 @@ ___ #### Defined in -[NativeRNWhisper.ts:5](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/NativeRNWhisper.ts#L5) +[NativeRNWhisper.ts:5](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/NativeRNWhisper.ts#L5) ___ @@ -176,7 +198,7 @@ ___ #### Defined in -[index.ts:70](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L70) +[index.ts:70](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L70) ___ @@ -201,7 +223,7 @@ ___ #### Defined in -[index.ts:138](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L138) +[index.ts:138](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L138) ___ @@ -219,7 +241,7 @@ ___ #### Defined in -[index.ts:171](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L171) +[index.ts:171](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L171) ___ @@ -243,7 +265,7 @@ ___ #### Defined in -[index.ts:158](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L158) +[index.ts:158](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L158) ___ @@ -253,7 +275,7 @@ ___ #### Defined in -[index.ts:84](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L84) +[index.ts:84](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L84) ___ @@ -271,7 +293,7 @@ ___ #### Defined in -[NativeRNWhisper.ts:39](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/NativeRNWhisper.ts#L39) +[NativeRNWhisper.ts:37](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/NativeRNWhisper.ts#L37) ## Variables @@ -296,7 +318,7 @@ AudioSession Utility, iOS only. #### Defined in -[AudioSessionIos.ts:50](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L50) +[AudioSessionIos.ts:50](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L50) ___ @@ -308,7 +330,7 @@ Is allow fallback to CPU if load CoreML model failed #### Defined in -[index.ts:543](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L543) +[index.ts:562](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L562) ___ @@ -320,7 +342,7 @@ Is use CoreML models on iOS #### Defined in -[index.ts:540](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L540) +[index.ts:559](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L559) ___ @@ -332,7 +354,7 @@ Current version of whisper.cpp #### Defined in -[index.ts:535](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L535) +[index.ts:554](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L554) ## Functions @@ -352,7 +374,7 @@ Current version of whisper.cpp #### Defined in -[index.ts:467](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L467) +[index.ts:484](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L484) ___ @@ -366,4 +388,4 @@ ___ #### Defined in -[index.ts:530](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L530) +[index.ts:549](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L549) diff --git a/docs/API/classes/WhisperContext.md b/docs/API/classes/WhisperContext.md index bff1326..8ab4309 100644 --- a/docs/API/classes/WhisperContext.md +++ b/docs/API/classes/WhisperContext.md @@ -16,6 +16,7 @@ ### Methods +- [bench](WhisperContext.md#bench) - [release](WhisperContext.md#release) - [transcribe](WhisperContext.md#transcribe) - [transcribeRealtime](WhisperContext.md#transcriberealtime) @@ -34,7 +35,7 @@ #### Defined in -[index.ts:195](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L195) +[index.ts:204](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L204) ## Properties @@ -44,7 +45,7 @@ #### Defined in -[index.ts:191](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L191) +[index.ts:200](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L200) ___ @@ -54,7 +55,7 @@ ___ #### Defined in -[index.ts:189](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L189) +[index.ts:198](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L198) ___ @@ -64,10 +65,30 @@ ___ #### Defined in -[index.ts:193](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L193) +[index.ts:202](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L202) ## Methods +### bench + +▸ **bench**(`maxThreads`): `Promise`<[`BenchResult`](../README.md#benchresult)\> + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `maxThreads` | `number` | + +#### Returns + +`Promise`<[`BenchResult`](../README.md#benchresult)\> + +#### Defined in + +[index.ts:445](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L445) + +___ + ### release ▸ **release**(): `Promise`<`void`\> @@ -78,7 +99,7 @@ ___ #### Defined in -[index.ts:436](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L436) +[index.ts:451](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L451) ___ @@ -106,7 +127,7 @@ Transcribe audio file #### Defined in -[index.ts:206](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L206) +[index.ts:215](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L215) ___ @@ -128,4 +149,4 @@ Transcribe the microphone audio stream, the microphone user permission is requir #### Defined in -[index.ts:302](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/index.ts#L302) +[index.ts:311](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/index.ts#L311) diff --git a/docs/API/enums/AudioSessionCategoryIos.md b/docs/API/enums/AudioSessionCategoryIos.md index 49cf6c8..efb394d 100644 --- a/docs/API/enums/AudioSessionCategoryIos.md +++ b/docs/API/enums/AudioSessionCategoryIos.md @@ -25,7 +25,7 @@ https://developer.apple.com/documentation/avfaudio/avaudiosessioncategory?langua #### Defined in -[AudioSessionIos.ts:8](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L8) +[AudioSessionIos.ts:8](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L8) ___ @@ -35,7 +35,7 @@ ___ #### Defined in -[AudioSessionIos.ts:13](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L13) +[AudioSessionIos.ts:13](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L13) ___ @@ -45,7 +45,7 @@ ___ #### Defined in -[AudioSessionIos.ts:12](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L12) +[AudioSessionIos.ts:12](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L12) ___ @@ -55,7 +55,7 @@ ___ #### Defined in -[AudioSessionIos.ts:10](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L10) +[AudioSessionIos.ts:10](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L10) ___ @@ -65,7 +65,7 @@ ___ #### Defined in -[AudioSessionIos.ts:11](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L11) +[AudioSessionIos.ts:11](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L11) ___ @@ -75,4 +75,4 @@ ___ #### Defined in -[AudioSessionIos.ts:9](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L9) +[AudioSessionIos.ts:9](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L9) diff --git a/docs/API/enums/AudioSessionCategoryOptionIos.md b/docs/API/enums/AudioSessionCategoryOptionIos.md index df25ad8..e213cd8 100644 --- a/docs/API/enums/AudioSessionCategoryOptionIos.md +++ b/docs/API/enums/AudioSessionCategoryOptionIos.md @@ -26,7 +26,7 @@ https://developer.apple.com/documentation/avfaudio/avaudiosessioncategoryoptions #### Defined in -[AudioSessionIos.ts:25](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L25) +[AudioSessionIos.ts:25](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L25) ___ @@ -36,7 +36,7 @@ ___ #### Defined in -[AudioSessionIos.ts:23](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L23) +[AudioSessionIos.ts:23](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L23) ___ @@ -46,7 +46,7 @@ ___ #### Defined in -[AudioSessionIos.ts:24](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L24) +[AudioSessionIos.ts:24](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L24) ___ @@ -56,7 +56,7 @@ ___ #### Defined in -[AudioSessionIos.ts:26](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L26) +[AudioSessionIos.ts:26](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L26) ___ @@ -66,7 +66,7 @@ ___ #### Defined in -[AudioSessionIos.ts:21](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L21) +[AudioSessionIos.ts:21](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L21) ___ @@ -76,7 +76,7 @@ ___ #### Defined in -[AudioSessionIos.ts:22](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L22) +[AudioSessionIos.ts:22](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L22) ___ @@ -86,4 +86,4 @@ ___ #### Defined in -[AudioSessionIos.ts:20](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L20) +[AudioSessionIos.ts:20](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L20) diff --git a/docs/API/enums/AudioSessionModeIos.md b/docs/API/enums/AudioSessionModeIos.md index 00ddf9b..0b6c516 100644 --- a/docs/API/enums/AudioSessionModeIos.md +++ b/docs/API/enums/AudioSessionModeIos.md @@ -27,7 +27,7 @@ https://developer.apple.com/documentation/avfaudio/avaudiosessionmode?language=o #### Defined in -[AudioSessionIos.ts:33](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L33) +[AudioSessionIos.ts:33](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L33) ___ @@ -37,7 +37,7 @@ ___ #### Defined in -[AudioSessionIos.ts:36](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L36) +[AudioSessionIos.ts:36](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L36) ___ @@ -47,7 +47,7 @@ ___ #### Defined in -[AudioSessionIos.ts:38](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L38) +[AudioSessionIos.ts:38](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L38) ___ @@ -57,7 +57,7 @@ ___ #### Defined in -[AudioSessionIos.ts:39](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L39) +[AudioSessionIos.ts:39](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L39) ___ @@ -67,7 +67,7 @@ ___ #### Defined in -[AudioSessionIos.ts:40](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L40) +[AudioSessionIos.ts:40](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L40) ___ @@ -77,7 +77,7 @@ ___ #### Defined in -[AudioSessionIos.ts:35](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L35) +[AudioSessionIos.ts:35](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L35) ___ @@ -87,7 +87,7 @@ ___ #### Defined in -[AudioSessionIos.ts:37](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L37) +[AudioSessionIos.ts:37](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L37) ___ @@ -97,4 +97,4 @@ ___ #### Defined in -[AudioSessionIos.ts:34](https://github.com/mybigday/whisper.rn/blob/8f61e46/src/AudioSessionIos.ts#L34) +[AudioSessionIos.ts:34](https://github.com/mybigday/whisper.rn/blob/5effdc8/src/AudioSessionIos.ts#L34) diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index cae45ab..cffce73 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -107,7 +107,6 @@ android { dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") - implementation("com.facebook.react:flipper-integration") if (hermesEnabled.toBoolean()) { implementation("com.facebook.react:hermes-android") diff --git a/example/android/app/src/main/java/com/rnwhisperexample/MainActivity.kt b/example/android/app/src/main/java/com/rnwhisperexample/MainActivity.kt index 2063180..49a9944 100644 --- a/example/android/app/src/main/java/com/rnwhisperexample/MainActivity.kt +++ b/example/android/app/src/main/java/com/rnwhisperexample/MainActivity.kt @@ -4,6 +4,7 @@ import com.facebook.react.ReactActivity import com.facebook.react.ReactActivityDelegate import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled import com.facebook.react.defaults.DefaultReactActivityDelegate +import android.os.Bundle class MainActivity : ReactActivity() { @@ -19,4 +20,8 @@ class MainActivity : ReactActivity() { */ override fun createReactActivityDelegate(): ReactActivityDelegate = DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } } diff --git a/example/android/app/src/main/java/com/rnwhisperexample/MainApplication.kt b/example/android/app/src/main/java/com/rnwhisperexample/MainApplication.kt index 9248b49..a8bb017 100644 --- a/example/android/app/src/main/java/com/rnwhisperexample/MainApplication.kt +++ b/example/android/app/src/main/java/com/rnwhisperexample/MainApplication.kt @@ -9,7 +9,6 @@ import com.facebook.react.ReactPackage import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.defaults.DefaultReactNativeHost -import com.facebook.react.flipper.ReactNativeFlipper import com.facebook.soloader.SoLoader class MainApplication : Application(), ReactApplication { @@ -31,7 +30,7 @@ class MainApplication : Application(), ReactApplication { } override val reactHost: ReactHost - get() = getDefaultReactHost(this.applicationContext, reactNativeHost) + get() = getDefaultReactHost(applicationContext, reactNativeHost) override fun onCreate() { super.onCreate() @@ -40,6 +39,5 @@ class MainApplication : Application(), ReactApplication { // If you opted-in for the New Architecture, we load the native entry point for this app. load() } - ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager) } } diff --git a/example/android/app/src/main/res/drawable/rn_edit_text_material.xml b/example/android/app/src/main/res/drawable/rn_edit_text_material.xml index f35d996..2214ed4 100644 --- a/example/android/app/src/main/res/drawable/rn_edit_text_material.xml +++ b/example/android/app/src/main/res/drawable/rn_edit_text_material.xml @@ -16,11 +16,10 @@ + android:insetTop="@dimen/abc_edit_text_inset_top_material"> - ${toTimestamp( - segment.t1, - )}] ${segment.text}`, - ) - .join('\n')}`, - ) - log('Finished transcribing') - }} - > - Transcribe File - - { - if (!whisperContext) return log('No context') - if (stopTranscribe?.stop) { - const t0 = Date.now() - await stopTranscribe?.stop() - const t1 = Date.now() - log('Stopped transcribing in', t1 - t0, 'ms') - setStopTranscribe(null) - return - } - log('Start realtime transcribing...') - try { - await createDir(log) - const { stop, subscribe } = - await whisperContext.transcribeRealtime({ - maxLen: 1, - language: 'en', - // Enable beam search (may be slower than greedy but more accurate) - // beamSize: 2, - // Record duration in seconds - realtimeAudioSec: 60, - // Slice audio into 25 (or < 30) sec chunks for better performance - realtimeAudioSliceSec: 25, - // Save audio on stop - audioOutputPath: recordFile, - // iOS Audio Session - audioSessionOnStartIos: { - category: AudioSessionIos.Category.PlayAndRecord, - options: [ - AudioSessionIos.CategoryOption.MixWithOthers, - AudioSessionIos.CategoryOption.AllowBluetooth, - ], - mode: AudioSessionIos.Mode.Default, - }, - audioSessionOnStopIos: 'restore', // Or an AudioSessionSettingIos - // Voice Activity Detection - Start transcribing when speech is detected - // useVad: true, - }) - setStopTranscribe({ stop }) - subscribe((evt) => { - const { isCapturing, data, processTime, recordingTime } = evt - setTranscibeResult( - `Realtime transcribing: ${isCapturing ? 'ON' : 'OFF'}\n` + - `Result: ${data?.result}\n\n` + - `Process time: ${processTime}ms\n` + - `Recording time: ${recordingTime}ms` + - `\n` + - `Segments:` + - `\n${data?.segments - .map( - (segment) => - `[${toTimestamp(segment.t0)} --> ${toTimestamp( - segment.t1, - )}] ${segment.text}`, - ) - .join('\n')}`, - ) - if (!isCapturing) { - setStopTranscribe(null) - log('Finished realtime transcribing') - } - }) - } catch (e) { - log('Error:', e) - } - }} - > - - {stopTranscribe?.stop ? 'Stop' : 'Realtime'} - - - - - {logs.map((msg, index) => ( - - {msg} - - ))} - - {transcibeResult && ( - - {transcibeResult} - - )} - - { - if (!whisperContext) return - await whisperContext.release() - setWhisperContext(null) - log('Released context') - }} - > - Release Context - - { - setLogs([]) - setTranscibeResult('') - }} - > - Clear Logs - - { - await RNFS.unlink(fileDir).catch(() => {}) - log('Deleted files') - }} - > - Clear Download files - - { - if (!(await RNFS.exists(recordFile))) { - log('Recorded file does not exist') - return - } - const player = new Sound(recordFile, '', (e) => { - if (e) { - log('error', e) - return - } - player.play((success) => { - if (success) { - log('successfully finished playing') - } else { - log('playback failed due to audio decoding errors') - } - player.release() - }) - }) - }} - > - Play Recorded file - - - + + + + + + + + + ) } + +export default App diff --git a/example/src/Bench.tsx b/example/src/Bench.tsx new file mode 100644 index 0000000..0a913ae --- /dev/null +++ b/example/src/Bench.tsx @@ -0,0 +1,360 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { StyleSheet, ScrollView, View, Text, Platform } from 'react-native' +import { TouchableOpacity } from 'react-native-gesture-handler' +import RNFS from 'react-native-fs' +import Clipboard from '@react-native-clipboard/clipboard' +import { initWhisper } from '../../src' // whisper.rn +import { createDir, fileDir, modelHost } from './util' +import { Button } from './Button' + +const modelList = [ + // TODO: Add coreml model download + { name: 'tiny', default: true }, + { name: 'tiny-q5_1', default: true }, + { name: 'tiny-q8_0', default: true }, + { name: 'base', default: true }, + { name: 'base-q5_1', default: true }, + { name: 'base-q8_0', default: true }, + { name: 'small', default: true }, + { name: 'small-q5_1', default: true }, + { name: 'small-q8_0', default: true }, + { name: 'medium', default: true }, + { name: 'medium-q5_0', default: true }, + { name: 'medium-q8_0', default: true }, + { name: 'large-v1', default: false }, + { name: 'large-v1-q5_0', default: false }, + { name: 'large-v1-q8_0', default: false }, + { name: 'large-v2', default: false }, + { name: 'large-v2-q5_0', default: false }, + { name: 'large-v2-q8_0', default: false }, + { name: 'large-v3', default: false }, + { name: 'large-v3-q5_0', default: false }, + { name: 'large-v3-q8_0', default: false }, + { name: 'large-v3-turbo', default: false }, + { name: 'large-v3-turbo-q5_0', default: false }, + { name: 'large-v3-turbo-q8_0', default: false }, +] as const + +const modelNameMap = modelList.reduce((acc, model) => { + acc[model.name as keyof typeof acc] = model.default + return acc +}, {} as Record) + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 10, + }, + contentContainer: { + alignItems: 'center', + }, + title: { + fontSize: 20, + fontWeight: 'bold', + }, + modelList: { + padding: 10, + flexWrap: 'wrap', + flexDirection: 'row', + }, + modelItem: { + backgroundColor: '#333', + borderRadius: 5, + margin: 4, + flexDirection: 'row', + alignItems: 'center', + }, + modelItemUnselected: { + backgroundColor: '#33333388', + }, + modelItemText: { + margin: 6, + color: '#ccc', + fontSize: 12, + fontWeight: 'bold', + }, + progressBar: { + backgroundColor: '#3388ff', + position: 'absolute', + left: 0, + top: 0, + bottom: 0, + opacity: 0.5, + width: '0%', + borderRadius: 5, + }, + logContainer: { + backgroundColor: 'lightgray', + padding: 8, + width: '100%', + borderRadius: 8, + marginVertical: 8, + }, + logText: { fontSize: 12, color: '#333' }, + buttonContainer: { + flexDirection: 'row', + justifyContent: 'center', + }, +}) + +const Model = (props: { + model: (typeof modelList)[number] + state: 'select' | 'download' + downloadMap: Record + setDownloadMap: (downloadMap: Record) => void + onDownloadStarted: (modelName: string) => void + onDownloaded: (modelName: string) => void +}) => { + const { + model, + state, + downloadMap, + setDownloadMap, + onDownloadStarted, + onDownloaded, + } = props + + const downloadRef = useRef(null) + const [progress, setProgress] = useState(0) + + const downloadNeeded = downloadMap[model.name] + + const cancelDownload = async () => { + if (downloadRef.current) { + RNFS.stopDownload(downloadRef.current) + downloadRef.current = null + setProgress(0) + } + } + + useEffect(() => { + if (state !== 'select') return + RNFS.exists(`${fileDir}/ggml-${model.name}.bin`).then((exists) => { + if (exists) setProgress(1) + else setProgress(0) + }) + }, [model.name, state]) + + useEffect(() => { + if (state === 'download') { + const download = async () => { + if (!downloadNeeded) return cancelDownload() + if (await RNFS.exists(`${fileDir}/ggml-${model.name}.bin`)) { + setProgress(1) + onDownloaded(model.name) + return + } + await createDir(null) + const { jobId, promise } = RNFS.downloadFile({ + fromUrl: `${modelHost}/ggml-${model.name}.bin?download=true`, + toFile: `${fileDir}/ggml-${model.name}.bin`, + begin: () => { + setProgress(0) + onDownloadStarted(model.name) + }, + progress: (res) => { + setProgress(res.bytesWritten / res.contentLength) + }, + }) + downloadRef.current = jobId + promise.then(() => { + setProgress(1) + onDownloaded(model.name) + }) + } + download() + } else { + cancelDownload() + } + }, [state, downloadNeeded, model.name, onDownloadStarted, onDownloaded]) + + return ( + { + if (downloadRef.current) { + cancelDownload() + return + } + if (state === 'download') return + setDownloadMap({ + ...downloadMap, + [model.name]: !downloadMap[model.name], + }) + }} + > + {model.name} + + {downloadNeeded && ( + + )} + + ) +} + +export default function Bench() { + const [logs, setLogs] = useState([]) + const [downloadMap, setDownloadMap] = + useState>(modelNameMap) + const [modelState, setModelState] = useState<'select' | 'download'>('select') + + const downloadedModelsRef = useRef([]) + + const log = useCallback((...messages: any[]) => { + setLogs((prev) => [...prev, messages.join(' ')]) + }, []) + + useEffect(() => { + const count = downloadedModelsRef.current.length + if ( + count > 0 && + count === Object.values(downloadMap).filter(Boolean).length + ) { + downloadedModelsRef.current = [] + setModelState('select') + log('All models downloaded') + } + }, [log, logs, downloadMap]) + + const handleDownloadStarted = useCallback( + (modelName: string) => { + log(`Downloading ${modelName}`) + }, + [log], + ) + + const handleDownloaded = useCallback( + (modelName: string) => { + downloadedModelsRef.current = [...downloadedModelsRef.current, modelName] + log(`Downloaded ${modelName}`) + }, + [log], + ) + + const downloadCount = Object.keys(downloadMap).filter( + (key) => downloadMap[key], + ).length + return ( + + Model List + + {modelList.map((model) => ( + + ))} + +