diff --git a/extension/android/CMakeLists.txt b/extension/android/CMakeLists.txt index 38b28a1407a..f6c1bc2a041 100644 --- a/extension/android/CMakeLists.txt +++ b/extension/android/CMakeLists.txt @@ -170,8 +170,11 @@ if(EXECUTORCH_BUILD_EXTENSION_TRAINING) endif() if(EXECUTORCH_BUILD_LLAMA_JNI) - target_sources(executorch_jni PRIVATE jni/jni_layer_llama.cpp jni/log.cpp) - list(APPEND link_libraries extension_llm_runner) + target_sources( + executorch_jni PRIVATE jni/jni_layer_llama.cpp jni/jni_layer_asr.cpp + jni/log.cpp + ) + list(APPEND link_libraries extension_llm_runner extension_asr_runner) target_compile_definitions(executorch_jni PUBLIC EXECUTORCH_BUILD_LLAMA_JNI=1) if(QNN_SDK_ROOT) diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrCallback.kt b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrCallback.kt new file mode 100644 index 00000000000..e2012d84c26 --- /dev/null +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrCallback.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +package org.pytorch.executorch.extension.asr + +import org.pytorch.executorch.annotations.Experimental + +/** + * Callback interface for ASR (Automatic Speech Recognition) module. Users can implement this + * interface to receive the transcribed tokens and completion notification. + * + * Warning: These APIs are experimental and subject to change without notice + */ +@Experimental +interface AsrCallback { + /** + * Called when a new token is available from JNI. Users will keep getting onToken() invocations + * until transcription finishes. + * + * @param token The decoded text token + */ + fun onToken(token: String) + + /** + * Called when transcription is complete. + * + * @param transcription The complete transcription (may be empty if tokens were streamed) + */ + fun onComplete(transcription: String) {} +} diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt new file mode 100644 index 00000000000..b875fc05353 --- /dev/null +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt @@ -0,0 +1,203 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +package org.pytorch.executorch.extension.asr + +import java.io.Closeable +import java.io.File +import java.util.concurrent.atomic.AtomicLong +import org.pytorch.executorch.annotations.Experimental + +/** + * AsrModule is a wrapper around the ExecuTorch ASR Runner. It provides a simple interface to + * transcribe audio from WAV files using speech recognition models like Whisper. + * + * The module loads a WAV file, optionally preprocesses it using a preprocessor module (e.g., for + * mel-spectrogram extraction), and then runs the ASR model to generate transcriptions. + * + * Warning: These APIs are experimental and subject to change without notice + * + * @param modelPath Path to the ExecuTorch model file (.pte). The model must expose exactly two + * callable methods named "encoder" and "text_decoder" (these names are required). + * @param tokenizerPath Path to the tokenizer directory containing tokenizer.json + * @param dataPath Optional path to additional data file (e.g., for delegate data) + * @param preprocessorPath Optional path to preprocessor .pte for converting raw audio to features. + * If not provided, raw audio samples will be passed directly to the model. + */ +@Experimental +class AsrModule( + modelPath: String, + tokenizerPath: String, + dataPath: String? = null, + preprocessorPath: String? = null, +) : Closeable { + + private val nativeHandle = AtomicLong(0L) + + init { + val modelFile = File(modelPath) + require(modelFile.canRead() && modelFile.isFile) { "Cannot load model path $modelPath" } + val tokenizerFile = File(tokenizerPath) + require(tokenizerFile.exists()) { "Cannot load tokenizer path $tokenizerPath" } + if (preprocessorPath != null) { + val preprocessorFile = File(preprocessorPath) + require(preprocessorFile.canRead() && preprocessorFile.isFile) { + "Cannot load preprocessor path $preprocessorPath" + } + } + + val handle = nativeCreate(modelPath, tokenizerPath, dataPath, preprocessorPath) + if (handle == 0L) { + throw RuntimeException("Failed to create native AsrModule") + } + nativeHandle.set(handle) + } + + companion object { + init { + System.loadLibrary("executorch") + } + + @JvmStatic + private external fun nativeCreate( + modelPath: String, + tokenizerPath: String, + dataPath: String?, + preprocessorPath: String?, + ): Long + + @JvmStatic private external fun nativeDestroy(nativeHandle: Long) + + @JvmStatic private external fun nativeLoad(nativeHandle: Long): Int + + @JvmStatic private external fun nativeIsLoaded(nativeHandle: Long): Boolean + + @JvmStatic + private external fun nativeTranscribe( + nativeHandle: Long, + wavPath: String, + maxNewTokens: Long, + temperature: Float, + decoderStartTokenId: Long, + callback: AsrCallback?, + ): Int + } + + /** Check if the native handle is valid. */ + val isValid: Boolean + get() = nativeHandle.get() != 0L + + /** Check if the module is loaded and ready for inference. */ + val isLoaded: Boolean + get() { + val handle = nativeHandle.get() + return handle != 0L && nativeIsLoaded(handle) + } + + /** Releases native resources. Call this when done with the module. */ + fun destroy() { + val handle = nativeHandle.getAndSet(0L) + if (handle != 0L) { + nativeDestroy(handle) + } + } + + /** Closeable implementation for use with use {} blocks. */ + override fun close() { + destroy() + } + + /** + * Force loading the module. Otherwise the model is loaded during first transcribe() call. + * + * @return 0 on success, error code otherwise + * @throws IllegalStateException if the module has been destroyed + */ + fun load(): Int { + val handle = nativeHandle.get() + check(handle != 0L) { "AsrModule has been destroyed" } + return nativeLoad(handle) + } + + /** + * Transcribe audio from a WAV file with default configuration. + * + * @param wavPath Path to the WAV audio file + * @param callback Callback to receive tokens, can be null + * @return 0 on success, error code otherwise + * @throws IllegalStateException if the module has been destroyed + */ + fun transcribe(wavPath: String, callback: AsrCallback? = null): Int = + transcribe(wavPath, AsrTranscribeConfig(), callback) + + /** + * Transcribe audio from a WAV file with custom configuration. + * + * @param wavPath Path to the WAV audio file + * @param config Configuration for transcription + * @param callback Callback to receive tokens, can be null + * @return 0 on success, error code otherwise + * @throws IllegalStateException if the module has been destroyed + */ + fun transcribe( + wavPath: String, + config: AsrTranscribeConfig, + callback: AsrCallback? = null, + ): Int { + val handle = nativeHandle.get() + check(handle != 0L) { "AsrModule has been destroyed" } + val wavFile = File(wavPath) + require(wavFile.canRead() && wavFile.isFile) { "Cannot read WAV file: $wavPath" } + return nativeTranscribe( + handle, + wavPath, + config.maxNewTokens, + config.temperature, + config.decoderStartTokenId, + callback, + ) + } + + /** + * Transcribe audio from a WAV file and return the full transcription. + * + * This is a blocking call that collects all tokens and returns the complete transcription. + * + * @param wavPath Path to the WAV audio file + * @param config Configuration for transcription + * @return The transcribed text + * @throws RuntimeException if transcription fails + */ + @JvmOverloads + fun transcribeBlocking( + wavPath: String, + config: AsrTranscribeConfig = AsrTranscribeConfig(), + ): String { + val result = StringBuilder() + val status = + transcribe( + wavPath, + config, + object : AsrCallback { + override fun onToken(token: String) { + result.append(token) + } + + override fun onComplete(transcription: String) { + // Tokens already collected + } + }, + ) + + if (status != 0) { + throw RuntimeException("Transcription failed with error code: $status") + } + + return result.toString() + } +} diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrTranscribeConfig.kt b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrTranscribeConfig.kt new file mode 100644 index 00000000000..2ae61ab8fa6 --- /dev/null +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrTranscribeConfig.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +package org.pytorch.executorch.extension.asr + +import org.pytorch.executorch.annotations.Experimental + +/** + * Configuration for ASR transcription. + * + * Warning: These APIs are experimental and subject to change without notice + * + * @property maxNewTokens Maximum number of new tokens to generate (must be positive) + * @property temperature Temperature for sampling. 0.0 means greedy decoding + * @property decoderStartTokenId The token ID to start decoding with (e.g., language token for + * Whisper) + */ +@Experimental +data class AsrTranscribeConfig( + val maxNewTokens: Long = 128, + val temperature: Float = 0.0f, + val decoderStartTokenId: Long = 0, +) { + init { + require(maxNewTokens > 0) { "maxNewTokens must be positive" } + require(temperature >= 0) { "temperature must be non-negative" } + } + + /** Builder class for AsrTranscribeConfig for Java interoperability. */ + class Builder { + private var maxNewTokens: Long = 128 + private var temperature: Float = 0.0f + private var decoderStartTokenId: Long = 0 + + fun setMaxNewTokens(maxNewTokens: Long) = apply { + require(maxNewTokens > 0) { "maxNewTokens must be positive" } + this.maxNewTokens = maxNewTokens + } + + fun setTemperature(temperature: Float) = apply { + require(temperature >= 0) { "temperature must be non-negative" } + this.temperature = temperature + } + + fun setDecoderStartTokenId(decoderStartTokenId: Long) = apply { + this.decoderStartTokenId = decoderStartTokenId + } + + fun build() = + AsrTranscribeConfig( + maxNewTokens = maxNewTokens, + temperature = temperature, + decoderStartTokenId = decoderStartTokenId, + ) + } +} diff --git a/extension/android/jni/jni_layer_asr.cpp b/extension/android/jni/jni_layer_asr.cpp new file mode 100644 index 00000000000..50a3656b437 --- /dev/null +++ b/extension/android/jni/jni_layer_asr.cpp @@ -0,0 +1,433 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace asr = ::executorch::extension::asr; +using ::executorch::extension::from_blob; +using ::executorch::extension::Module; +using ::executorch::extension::TensorPtr; +using ::executorch::runtime::Error; + +namespace { + +// Handle struct that holds both the ASR runner and optional preprocessor +struct AsrModuleHandle { + std::unique_ptr runner; + std::unique_ptr preprocessor; +}; + +// Helper to get a string from jstring +std::string jstringToString(JNIEnv* env, jstring jstr) { + if (jstr == nullptr) { + return ""; + } + const char* chars = env->GetStringUTFChars(jstr, nullptr); + std::string result(chars); + env->ReleaseStringUTFChars(jstr, chars); + return result; +} + +// Helper for UTF-8 validity checking (for streaming tokens) +bool utf8_check_validity(const char* str, size_t length) { + for (size_t i = 0; i < length; ++i) { + uint8_t byte = static_cast(str[i]); + if (byte >= 0x80) { + if (i + 1 >= length) { + return false; + } + uint8_t next_byte = static_cast(str[i + 1]); + if ((byte & 0xE0) == 0xC0 && (next_byte & 0xC0) == 0x80) { + i += 1; + } else if ( + (byte & 0xF0) == 0xE0 && (next_byte & 0xC0) == 0x80 && + (i + 2 < length) && + (static_cast(str[i + 2]) & 0xC0) == 0x80) { + i += 2; + } else if ( + (byte & 0xF8) == 0xF0 && (next_byte & 0xC0) == 0x80 && + (i + 2 < length) && + (static_cast(str[i + 2]) & 0xC0) == 0x80 && + (i + 3 < length) && + (static_cast(str[i + 3]) & 0xC0) == 0x80) { + i += 3; + } else { + return false; + } + } + } + return true; +} + +// Global cached JNI references for callback (shared across threads) +struct AsrCallbackCache { + jclass callbackClass = nullptr; + jmethodID onTokenMethod = nullptr; + jmethodID onCompleteMethod = nullptr; +}; + +AsrCallbackCache callbackCache; +std::once_flag callbackCacheInitFlag; + +void initCallbackCache(JNIEnv* env) { + std::call_once( + callbackCacheInitFlag, + [](JNIEnv* localEnv) { + jclass localClass = localEnv->FindClass( + "org/pytorch/executorch/extension/asr/AsrCallback"); + if (localClass != nullptr) { + callbackCache.callbackClass = + (jclass)localEnv->NewGlobalRef(localClass); + callbackCache.onTokenMethod = localEnv->GetMethodID( + callbackCache.callbackClass, "onToken", "(Ljava/lang/String;)V"); + callbackCache.onCompleteMethod = localEnv->GetMethodID( + callbackCache.callbackClass, + "onComplete", + "(Ljava/lang/String;)V"); + localEnv->DeleteLocalRef(localClass); + } + }, + env); +} + +// Helper to create a unique_ptr for JNI global references +auto make_scoped_global_ref(JNIEnv* env, jobject obj) { + auto deleter = [env](jobject ref) { + if (ref != nullptr) { + env->DeleteGlobalRef(ref); + } + }; + jobject globalRef = obj ? env->NewGlobalRef(obj) : nullptr; + return std::unique_ptr, decltype(deleter)>( + globalRef, deleter); +} + +} // namespace + +extern "C" { + +/* + * Class: org_pytorch_executorch_extension_asr_AsrModule + * Method: nativeCreate + * Signature: + * (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)J + */ +JNIEXPORT jlong JNICALL +Java_org_pytorch_executorch_extension_asr_AsrModule_nativeCreate( + JNIEnv* env, + jobject /* this */, + jstring modelPath, + jstring tokenizerPath, + jstring dataPath, + jstring preprocessorPath) { + std::string modelPathStr = jstringToString(env, modelPath); + std::string tokenizerPathStr = jstringToString(env, tokenizerPath); + std::string dataPathStr = jstringToString(env, dataPath); + std::string preprocessorPathStr = jstringToString(env, preprocessorPath); + + std::optional dataPathOpt; + if (!dataPathStr.empty()) { + dataPathOpt = dataPathStr; + } + + try { + auto handle = std::make_unique(); + + // Create the ASR runner + handle->runner = std::make_unique( + modelPathStr, dataPathOpt, tokenizerPathStr); + + // Create the preprocessor module if path is provided + if (!preprocessorPathStr.empty()) { + handle->preprocessor = + std::make_unique(preprocessorPathStr, Module::LoadMode::Mmap); + auto load_error = handle->preprocessor->load(); + if (load_error != Error::Ok) { + ET_LOG(Error, "Failed to load preprocessor module"); + env->ThrowNew( + env->FindClass("java/lang/RuntimeException"), + "Failed to load preprocessor module"); + return 0; + } + } + + return reinterpret_cast(handle.release()); + } catch (const std::exception& e) { + ET_LOG(Error, "Failed to create AsrModule: %s", e.what()); + env->ThrowNew( + env->FindClass("java/lang/RuntimeException"), + ("Failed to create AsrModule: " + std::string(e.what())).c_str()); + return 0; + } +} + +/* + * Class: org_pytorch_executorch_extension_asr_AsrModule + * Method: nativeDestroy + * Signature: (J)V + */ +JNIEXPORT void JNICALL +Java_org_pytorch_executorch_extension_asr_AsrModule_nativeDestroy( + JNIEnv* /* env */, + jobject /* this */, + jlong nativeHandle) { + if (nativeHandle != 0) { + auto* handle = reinterpret_cast(nativeHandle); + delete handle; + } +} + +/* + * Class: org_pytorch_executorch_extension_asr_AsrModule + * Method: nativeLoad + * Signature: (J)I + */ +JNIEXPORT jint JNICALL +Java_org_pytorch_executorch_extension_asr_AsrModule_nativeLoad( + JNIEnv* env, + jobject /* this */, + jlong nativeHandle) { + if (nativeHandle == 0) { + env->ThrowNew( + env->FindClass("java/lang/IllegalStateException"), + "Module has been destroyed"); + return -1; + } + + auto* handle = reinterpret_cast(nativeHandle); + Error error = handle->runner->load(); + return static_cast(error); +} + +/* + * Class: org_pytorch_executorch_extension_asr_AsrModule + * Method: nativeIsLoaded + * Signature: (J)Z + */ +JNIEXPORT jboolean JNICALL +Java_org_pytorch_executorch_extension_asr_AsrModule_nativeIsLoaded( + JNIEnv* env, + jobject /* this */, + jlong nativeHandle) { + if (nativeHandle == 0) { + return JNI_FALSE; + } + + auto* handle = reinterpret_cast(nativeHandle); + return handle->runner->is_loaded() ? JNI_TRUE : JNI_FALSE; +} + +/* + * Class: org_pytorch_executorch_extension_asr_AsrModule + * Method: nativeTranscribe + * Signature: + * (JLjava/lang/String;JFJLorg/pytorch/executorch/extension/asr/AsrCallback;)I + */ +JNIEXPORT jint JNICALL +Java_org_pytorch_executorch_extension_asr_AsrModule_nativeTranscribe( + JNIEnv* env, + jobject /* this */, + jlong nativeHandle, + jstring wavPath, + jlong maxNewTokens, + jfloat temperature, + jlong decoderStartTokenId, + jobject callback) { + if (nativeHandle == 0) { + env->ThrowNew( + env->FindClass("java/lang/IllegalStateException"), + "Module has been destroyed"); + return -1; + } + + if (wavPath == nullptr) { + env->ThrowNew( + env->FindClass("java/lang/IllegalArgumentException"), + "WAV path cannot be null"); + return -1; + } + + auto* handle = reinterpret_cast(nativeHandle); + std::string wavPathStr = jstringToString(env, wavPath); + + // Load audio data from WAV file + std::vector audioData; + try { + audioData = ::executorch::extension::llm::load_wav_audio_data(wavPathStr); + } catch (const std::exception& e) { + env->ThrowNew( + env->FindClass("java/lang/RuntimeException"), + ("Failed to load WAV file: " + std::string(e.what())).c_str()); + return -1; + } + + if (audioData.empty()) { + env->ThrowNew( + env->FindClass("java/lang/IllegalArgumentException"), + "WAV file contains no audio data"); + return -1; + } + + ET_LOG(Info, "Loaded %zu audio samples from WAV file", audioData.size()); + + // Create tensor from audio data + TensorPtr featuresTensor; + + if (handle->preprocessor) { + // Run preprocessor to convert raw audio to features + auto audioTensor = from_blob( + audioData.data(), + {static_cast<::executorch::aten::SizesType>(audioData.size())}, + ::executorch::aten::ScalarType::Float); + + auto processedResult = + handle->preprocessor->execute("forward", audioTensor); + if (processedResult.error() != Error::Ok) { + env->ThrowNew( + env->FindClass("java/lang/RuntimeException"), + "Audio preprocessing failed"); + return -1; + } + + auto outputs = std::move(processedResult.get()); + if (outputs.empty() || !outputs[0].isTensor()) { + env->ThrowNew( + env->FindClass("java/lang/RuntimeException"), + "Preprocessor returned unexpected output"); + return -1; + } + + auto tensor = outputs[0].toTensor(); + featuresTensor = + std::make_shared<::executorch::aten::Tensor>(std::move(tensor)); + + ET_LOG( + Info, + "Preprocessor output shape: %d dims", + static_cast(featuresTensor->dim())); + } else { + // No preprocessor - use raw audio as features (1D tensor) + // This is for models that expect raw waveform input + featuresTensor = from_blob( + audioData.data(), + {1, static_cast<::executorch::aten::SizesType>(audioData.size()), 1}, + ::executorch::aten::ScalarType::Float); + } + + // Build config + asr::AsrTranscribeConfig config; + config.max_new_tokens = static_cast(maxNewTokens); + config.temperature = temperature; + config.decoder_start_token_id = static_cast(decoderStartTokenId); + + // Set up callback + std::function tokenCallback = nullptr; + + // Use unique_ptr with custom deleter to ensure global ref is released + auto scopedCallback = make_scoped_global_ref(env, callback); + + // Local token buffer for UTF-8 accumulation. + // Note: unlike the LLM JNI layer (which uses a global token_buffer), + // this buffer is intentionally per-transcription-call and captured by + // reference in the callback lambda. This design keeps ASR callbacks + // thread-safe and avoids sharing state across concurrent calls. + std::string tokenBuffer; + + if (scopedCallback) { + initCallbackCache(env); + + jobject callbackRef = scopedCallback.get(); + + JavaVM* jvm = nullptr; + if (env->GetJavaVM(&jvm) != JNI_OK || jvm == nullptr) { + ET_LOG(Error, "Failed to get JavaVM for ASR token callback."); + } else { + tokenCallback = [jvm, callbackRef, &tokenBuffer]( + const std::string& token) { + JNIEnv* envLocal = nullptr; + jint getEnvResult = + jvm->GetEnv(reinterpret_cast(&envLocal), JNI_VERSION_1_6); + if (getEnvResult == JNI_EDETACHED) { +#if defined(__ANDROID__) || defined(ANDROID) + if (jvm->AttachCurrentThread(&envLocal, nullptr) != JNI_OK) +#else + if (jvm->AttachCurrentThread( + reinterpret_cast(&envLocal), nullptr) != JNI_OK) +#endif + { + ET_LOG( + Error, + "Failed to attach current thread to JVM for ASR token callback."); + return; + } + } else if (getEnvResult != JNI_OK || envLocal == nullptr) { + ET_LOG( + Error, + "Failed to get JNIEnv for ASR token callback (GetEnv error)."); + return; + } + + tokenBuffer += token; + if (!utf8_check_validity(tokenBuffer.c_str(), tokenBuffer.size())) { + ET_LOG( + Info, + "Current token buffer is not valid UTF-8. Waiting for more."); + return; + } + + std::string completeToken = tokenBuffer; + tokenBuffer.clear(); + + jstring jToken = envLocal->NewStringUTF(completeToken.c_str()); + envLocal->CallVoidMethod( + callbackRef, callbackCache.onTokenMethod, jToken); + if (envLocal->ExceptionCheck()) { + ET_LOG(Error, "Exception occurred in AsrCallback.onToken"); + envLocal->ExceptionClear(); + } + envLocal->DeleteLocalRef(jToken); + }; + } + } + + // Run transcription + auto result = + handle->runner->transcribe(featuresTensor, config, tokenCallback); + + // Call onComplete if callback provided + if (scopedCallback) { + jstring emptyStr = env->NewStringUTF(""); + env->CallVoidMethod( + scopedCallback.get(), callbackCache.onCompleteMethod, emptyStr); + if (env->ExceptionCheck()) { + ET_LOG(Error, "Exception occurred in AsrCallback.onComplete"); + env->ExceptionClear(); + } + env->DeleteLocalRef(emptyStr); + } + + if (!result.ok()) { + return static_cast(result.error()); + } + + return 0; +} + +} // extern "C" diff --git a/scripts/build_android_library.sh b/scripts/build_android_library.sh index 05f06da4fcd..0fb9e909884 100755 --- a/scripts/build_android_library.sh +++ b/scripts/build_android_library.sh @@ -42,6 +42,7 @@ build_android_native_library() { -DEXECUTORCH_ENABLE_EVENT_TRACER="${EXECUTORCH_ANDROID_PROFILING:-OFF}" \ -DEXECUTORCH_BUILD_EXTENSION_LLM="${EXECUTORCH_BUILD_EXTENSION_LLM:-ON}" \ -DEXECUTORCH_BUILD_EXTENSION_LLM_RUNNER="${EXECUTORCH_BUILD_EXTENSION_LLM:-ON}" \ + -DEXECUTORCH_BUILD_EXTENSION_ASR_RUNNER="${EXECUTORCH_BUILD_EXTENSION_LLM:-ON}" \ -DEXECUTORCH_BUILD_EXTENSION_TRAINING=ON \ -DEXECUTORCH_BUILD_LLAMA_JNI="${EXECUTORCH_BUILD_EXTENSION_LLM:-ON}" \ -DEXECUTORCH_BUILD_NEURON="${EXECUTORCH_BUILD_NEURON}" \