Gemma 3n:移动设备全栈AI

在这篇博客文章中,我们将探讨如何在移动设备上完全运行 完整的 AI 栈,涵盖从语音到文本(STT)、函数调用、视觉语言模型(VLM)推理到文本到语音(TTS)的完整 Android 应用程序实现。

Gemma 3n:移动设备全栈AI

运行复杂的 AI 工作流,例如将语音转换为文本(STT)、调用函数、使用视觉语言模型(VLM)生成视觉洞察力以及合成语音(TTS),完全在移动设备上进行不再是未来的想法;它正在成为现实。这一转变不仅仅是一项技术成就,它代表了一个新的 AI 可访问性、隐私性和响应性的时代。随着用户期望的变化和数据隐私问题的加深,在设备上处理 AI 避免了对云的依赖,减少了延迟,并将敏感数据保留在其来源地。这是朝着民主化 AI 的重要一步,使其更快、更安全、更个性化。对于开发人员和决策者来说,这不仅仅是优化,而是重塑我们与智能系统日常互动的方式。

在这篇博客文章中,我们将探讨如何在移动设备上完全运行 完整的 AI 栈,涵盖从语音到文本(STT)、函数调用、视觉语言模型(VLM)推理到文本到语音(TTS)的完整 Android 应用程序实现。随着 Gemma 3n 的发布,该模型现在支持图像输入,以及 MediaPipe 库的最新更新,使得本地函数调用成为可能,完全在设备上的 AI 工作流不再只是理想,它们已经到来。我们将逐步介绍每个组件,展示所有这些任务如何在几秒钟内在移动硬件上执行,其性能令人惊讶地接近传统的基于云的管道。

1、语音到文本(STT)

今天与移动设备交互比以往任何时候都更加直观,通常从简单的语音命令开始。处理语音输入最有效的开源模型之一是 OpenAI 的 Whisper。存在两年多的 Whisper 被证明既可靠又多功能,能够处理从转录到翻译的各种任务。它有多种模型大小以适应不同的性能需求,对于这个项目,我们选择了轻量级的 Tiny English 版本,非常适合在设备上执行,而不会牺牲日常使用的准确性。

在移动应用程序中,您会找到 Whisper 的完整实现,从加载 .bin 词汇文件到初始化 TensorFlow Lite 解释器以进行设备上推理。该模型由 Mel 频谱图输入提供动力,后者使用 C++ 计算以实现更快和更高效的执行。这种设置确保了速度和准确性,使得实时转录在移动硬件上成为可能:

JNIEXPORT jfloatArray JNICALL  
Java_com_example_jetsonapp_whisperengine_WhisperEngine_transcribeFileWithMel(JNIEnv *env,  
                                                                             jobject thiz,  
                                                                             jlong nativePtr,  
                                                                             jstring waveFile,  
                                                                             jfloatArray filtersJava) {  
    talkandexecute *engine = reinterpret_cast<talkandexecute *>(nativePtr);  
    const char *cWaveFile = env->GetStringUTFChars(waveFile, NULL);  

    // 第一步:从 jfloatArray 获取原生数组  
    jfloat *nativeFiltersArray = env->GetFloatArrayElements(filtersJava, NULL);  
    jsize filtersSize = env->GetArrayLength(filtersJava);  

    // 第二步:将原生数组转换为 std::vector<float>  
    std::vector<float> filtersVector(nativeFiltersArray, nativeFiltersArray + filtersSize);  

    // 释放原生数组  
    env->ReleaseFloatArrayElements(filtersJava, nativeFiltersArray, JNI_ABORT);  

    // 调用引擎方法以转录音频文件并获取结果作为 float 向量  
    std::vector<float> result = engine->transcribeFileWithMel(cWaveFile, filtersVector);  

    env->ReleaseStringUTFChars(waveFile, cWaveFile);  

    // 将结果向量转换为 jfloatArray  
    jfloatArray resultArray = env->NewFloatArray(result.size());  
    env->SetFloatArrayRegion(resultArray, 0, result.size(), result.data());  

    return resultArray;  
}

您可以在这里查看初始化过程并了解 Whisper 如何在 Android 应用程序中运行 这里

2、函数调用(FC)

MediaPipe 最近发布了 指南,介绍了如何直接在移动设备上实现函数调用!除了文档外,他们还提供了 GitHub 示例,任何人都可以构建并在 Android 手机上部署。此快速入门演示利用了 LLM 推理 APIHammer 2.1 (1.5B),提供了一种简便的方法来开始。以下是任务的基本初始化和使用示例:

private fun createGenerativeModel(): GenerativeModel {  
        val getCameraImage = FunctionDeclaration.newBuilder()  
            .setName("getCameraImage")  
            .setDescription("打开相机的功能")  
            .build()  
        val openPhoneGallery = FunctionDeclaration.newBuilder()  
            .setName("openPhoneGallery")  
            .setDescription("打开相册的功能")  
            .build()  
        val tool = Tool.newBuilder()  
            .addFunctionDeclarations(getCameraImage)  
            .addFunctionDeclarations(openPhoneGallery)  
            .build()  

        val formatter =  
            HammerFormatter(ModelFormatterOptions.builder().setAddPromptTemplate(true).build())  

        val llmInferenceOptions = LlmInferenceOptions.builder()  
            // hammer2.1_1.5b_q8_ekv4096.task  
            // gemma-3n-E2B-it-int4.task  
            .setModelPath("/data/local/tmp/Hammer2.1-1.5b_seq128_q8_ekv1280.task")  
            .setMaxTokens(512)  
            .apply { setPreferredBackend(Backend.GPU) }  
            .build()  

        val llmInference =  
            LlmInference.createFromOptions(context, llmInferenceOptions)  
        val llmInferenceBackend =  
            LlmInferenceBackend(llmInference, formatter)  

        val systemInstruction = Content.newBuilder()  
            .setRole("system")  
            .addParts(  
                Part.newBuilder()  
                    .setText("你是一个有用的助手,可以打开相机或手机相册。")  
            )  
            .build()  

        val model = GenerativeModel(  
            llmInferenceBackend,  
            systemInstruction,  
            listOf(tool).toMutableList()  
        )  
        return model  
    }  

....  
val chat = generativeModel?.startChat()  
        val response = chat?.sendMessage(userPrompt.value)  
        Log.v("function", "Model response: $response")  

        if (response != null && response.candidatesCount > 0 && response.getCandidates(0).content.partsList.size > 0) {  
            val message = response.getCandidates(0).content.getParts(0)  

            // 如果消息包含函数调用,则执行相应的函数。  
            if (message.hasFunctionCall()) {  
                val functionCall = message.functionCall  
                    // 调用适当的函数。  
                    when (functionCall.name) {  
                        "getCameraImage" -> {  
                            Log.v("function", "getCameraImage")  
                            _cameraFunctionTriggered.value = true  
                            updateJetsonIsWorking(false)  
                        }  

                        "openPhoneGallery" -> {  
                            Log.v("function", "openPhoneGallery")  
                            _phoneGalleryTriggered.value = true  
                            updateJetsonIsWorking(false)  
                        }  

                        else -> {  
                            Log.e("function", "没有要调用的函数")  
                            withContext(Dispatchers.Main) {  
                                Toast.makeText(  
                                    context,  
                                    "没有要调用的函数,请说类似“打开相机”的话",  
                                    Toast.LENGTH_LONG  
                                ).show()  
                            }  
                            updateJetsonIsWorking(false)  
                        }  
                    }  
....

一旦任务成功识别出一个函数,就会触发相应的操作,例如打开相机。更多内容请参阅应用程序中的 JetsonViewModel.kt 文件。用户然后可以通过相机 API 与之交互以捕获图像,该图像无缝传递到视觉语言模型(VLM)中以进一步处理和生成见解。

3、视觉语言模型(VLM)

备受期待的 Gemma 3n 模型终于发布了,提供两种变体,2B4B,两者均具备视觉支持。虽然多家公司和开发者之前已经推出了自己的视觉语言模型,但 这篇文章 探讨了为什么 Gemma 作为一个生产级选择脱颖而出,重点在于性能、效率和安全性。

MediaPipe 是第一个从第一天起就支持执行 Gemma 3n 模型的库。凭借内置的选项可以在 CPU 或 GPU 上运行模型,以及其标志性的高级 API,将 Gemma 3n 集成到 Android 项目中非常流畅且简单,提供了可靠的性能,没有任何意外的障碍。

private fun createSession(context: Context): LlmInferenceSession {  
        // 配置推理选项并创建推理实例  
        val options = LlmInferenceOptions.builder()  
            .setModelPath("/data/local/tmp/gemma-3n-E2B-it-int4.task")  
            .setMaxTokens(1024)  
            .setPreferredBackend(Backend.GPU)  
            .setMaxNumImages(1)  
            .build()  
        val llmInference = LlmInference.createFromOptions(context, options)  

        // 配置会话选项并创建会话  
        val sessionOptions = LlmInferenceSession.LlmInferenceSessionOptions.builder()  
            .setTopK(40) // 默认值  
            .setTopP(0.9f)  
            .setTemperature(1.0f)  
            .setGraphOptions(GraphOptions.builder().setEnableVisionModality(true).build())  
            .build()  
        return LlmInferenceSession.createFromOptions(llmInference, sessionOptions)  
    }  

....  

val mpImage = BitmapImageBuilder(bitmap).build()  
        session?.addQueryChunk(userPrompt.value + " in 20 words") // 如果不想输出太多,可以限制。  
        session?.addImage(mpImage)  

        var stringBuilder = ""  
        session?.generateResponseAsync { chunk, done ->  
            updateJetsonIsWorking(false)  
            stringBuilder += chunk  
            // Log.v("image_partial", "$stringBuilder $done")  
            updateVlmResult(transcribedText.trim() + "\n\n" + stringBuilder)  

....

您可以在 此文件 中查看代码。

4、文本到语音(TTS)

虽然视觉语言模型(VLM)的输出是文本,但该项目更进一步,添加了 离线文本到语音(TTS) 功能,使整个语音响应无需互联网连接即可完成。一个关键的中间步骤涉及 检测 VLM 输出的语言。这确保系统可以动态加载适当的发音,特别是当输出的语言不是英语时。由于 Gemma 3 和 3n 提供了超过 140 种语言 的内置支持,此功能为未来的多语言语音交互奠定了基础。语言检测使用的是 ML Kit 的语言识别 API

private val languageIdentifier = LanguageIdentification.getClient()  
private fun speakOut(text: String) {  
    val defaultLocale = Locale("en")  
    languageIdentifier.identifyLanguage(text)  
        .addOnSuccessListener { languageCode ->  
            val locale = if (languageCode == "und") defaultLocale else Locale(languageCode)  
            textToSpeech.setLanguage(locale)  
            // Log.v("available_languages", textToSpeech.availableLanguages.toString())  
            textToSpeech.speak(text, TextToSpeech.QUEUE_ADD, null, "speech_utterance_id")  
        }  
        .addOnFailureListener {  
            textToSpeech.setLanguage(defaultLocale)  
            textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, null, "speech_utterance_id")  
        }  
}

您可以在这里了解更多关于 Android API 的 TextToSpeech

观看一段在功能齐全的 三星 S24 上录制的短片,该设备拥有 12GB 内存,允许函数调用和 VLM 推理模型在 GPU 上平稳运行。

本项目中使用的模型总结:

  1. Whisper Tiny English 模型用于语音到文本(下载 vocab.binwhisper.tflite 并将其放入 assets 文件夹)
  2. Hammer 2.1 1.5B 用于函数调用 (下载)
  3. Gemma 3n 2B 用于 VLM (下载)
  4. ML KIT 语言识别 API

您可以直接从这个分支构建 项目

5、结束语

这个项目展示了设备上 AI 走得多远,将语音到文本、函数调用、视觉语言建模和文本到语音整合到一个移动应用程序中。借助 Whisper、Hammer 和 Gemma 3n 等开源模型,以及 MediaPipe 和 ML Kit 等框架,现在可以完全离线交付智能、多模式的体验。除了展示技术可行性外,这个端到端的移动 AI 栈还强调了实际影响:减少延迟、增强隐私,并通过不依赖云的方式实现更丰富、以语音驱动的交互。随着 AI 更加个人化并嵌入我们的日常设备,构建本地执行的解决方案已不再是例外情况。


原文链接:Gemma 3n models made possible the full AI stack entirely on mobile!

汇智网翻译整理,转载请标明出处