《我用Kotlin开发了一个免费的AI语音助手》

63 阅读6分钟

作者:诺言

项目名称:小端机器人

开源协议:MIT

一个完全免费的公益项目,让AI陪伴每一个家庭


一、项目背景:为什么要做这个项目?

去年春节回家,看到父母拿着手机,想查个天气、问个问题,却因为打字慢、不会用搜索而放弃。那一刻我突然意识到:AI技术发展这么快,但真正能让老人轻松使用的工具却很少。

市面上的语音助手要么需要联网、要么有隐私顾虑、要么功能单一。我想做一个:

  • 完全免费,无广告

  • 离线识别,保护隐私

  • 简单易用,老人小孩都能用

  • 功能丰富,能聊天、能放歌、有记忆

于是,小端机器人诞生了。


二、核心功能展示

1. 语音对话

只需说"小端"唤醒,就能像和朋友聊天一样:

  • "小端,武汉明天有没有雨?"

  • "小端,陪我聊聊天"

  • "小端,什么是人工智能?"

2. 本地音乐播放

支持播放手机里的音乐:

  • "小端,放首歌"

  • "小端,播放告白气球"

  • "小端,换一首"

3. 记忆系统

会记住你说过的话,越聊越懂你:

  • "小端,我昨天说了什么?"

  • "小端,我喜欢什么?"

4. 离线语音识别

使用 Sherpa-ONNX 实现完全离线的语音识别,不联网也能用,保护隐私


三、技术架构

技术栈选择

| 模块 | 技术方案 | 选择理由 |

|------|---------|---------|

| 开发语言 | Kotlin | 简洁、安全、协程支持好 |

| UI框架 | Jetpack Compose | 声明式UI,开发效率高 |

| 语音识别 | Sherpa-ONNX | 完全离线,支持中英文 |

| 语音合成 | Edge TTS | 免费、音质好、支持多音色 |

| AI对话 | 豆包API | 免费额度高,响应快 |

| 本地存储 | SQLite | 轻量、稳定 |

架构设计


┌─────────────────────────────────────┐

│         MainActivity                │

│  (Jetpack Compose UI + 生命周期管理) │

└──────────────┬──────────────────────┘

               │

       ┌───────┴───────┐

       │               │

┌──────▼──────┐ ┌─────▼──────┐

│ SherpaSpeech│ │  EdgeTTS   │

│   Manager   │ │  (TTS合成)  │

│ (离线ASR)    │ └────────────┘

└──────┬──────┘

       │

┌──────▼──────┐ ┌─────────────┐

│ AIManager   │ │ MusicPlayer │

│ (豆包对话)   │ │ (音乐播放)   │

└──────┬──────┘ └─────────────┘

       │

┌──────▼──────┐

│MemoryManager│

│ (记忆系统)   │

└─────────────┘


四、核心技术实现

1. 离线语音识别(Sherpa-ONNX)

为什么选择 Sherpa-ONNX?

  • ✅ 完全离线,不需要联网

  • ✅ 支持中英文混合识别

  • ✅ 识别准确率高

  • ✅ 资源占用低

集成步骤:


class SherpaSpeechManager(private val context: Context) {

    private var recognizer: OnlineRecognizer? = null

    private var stream: OnlineStream? = null

   

    fun initRecognizer() {

        val config = OnlineRecognizerConfig(

            featConfig = FeatureConfig(

                sampleRate = 16000,

                featureDim = 80

            ),

            modelConfig = OnlineModelConfig(

                transducer = OnlineTransducerModelConfig(

                    encoder = "asr-model/encoder-epoch-99-avg-1.onnx",

                    decoder = "asr-model/decoder-epoch-99-avg-1.onnx",

                    joiner = "asr-model/joiner-epoch-99-avg-1.onnx"

                ),

                tokens = "asr-model/tokens.txt",

                numThreads = 2,

                provider = "cpu"

            ),

            enableEndpoint = false,

            decodingMethod = "greedy_search"

        )

        recognizer = OnlineRecognizer(

            assetManager = context.assets,

            config = config

        )

    }

   

    fun startListening() {

        stream = recognizer?.createStream()

        // 开始录音并实时识别

        val audioRecord = AudioRecord(...)

        audioRecord.startRecording()

       

        // 实时处理音频流

        while (isListening) {

            val samples = readAudioSamples()

            stream?.acceptWaveform(samples, 16000)

           

            while (recognizer?.isReady(stream) == true) {

                recognizer?.decode(stream)

            }

           

            val result = recognizer?.getResult(stream)?.text

            onPartialResult?.invoke(result)

        }

    }

}

关键优化:

  1. 唤醒词检测:支持"小端"、"小段"等多种发音

  2. 音量阈值:播放音乐时提高阈值,避免误唤醒

  3. 资源管理:及时释放 AudioRecord,避免 -38 错误


2. 高质量语音合成(Edge TTS)

为什么选择 Edge TTS?

  • ✅ 完全免费

  • ✅ 音质接近真人

  • ✅ 支持多种音色

  • ✅ 支持语速调节

实现代码:


class EdgeTTS {

    fun generate(

        text: String,

        voice: String = "zh-CN-XiaoxiaoNeural",

        outputFile: File,

        rate: Float = 1.0f

    ): Boolean {

        val requestId = UUID.randomUUID().toString()

        val rateStr = if (rate >= 1.0f) "+${((rate - 1.0f) * 100).toInt()}%"

                      else "${((rate - 1.0f) * 100).toInt()}%"

       

        val ssml = """

            <speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='zh-CN'>

                <voice name='$voice'>

                    <prosody rate='$rateStr'>$text</prosody>

                </voice>

            </speak>

        """.trimIndent()

       

        // 通过 WebSocket 连接 Edge TTS 服务

        val url = "wss://api.msedgeservices.com/tts/cognitiveservices/websocket/v1?..."

       

        client.newWebSocket(request, object : WebSocketListener() {

            override fun onMessage(webSocket: WebSocket, bytes: ByteString) {

                // 接收音频数据并保存

                outputFile.outputStream().use { it.write(bytes.toByteArray()) }

            }

        })

       

        return true

    }

}

支持的音色:

  • 晓晓(温柔)、晓伊(活泼)、晓涵(温暖)

  • 晓梦(可爱)、晓墨(知性)、晓秋(温柔)

  • 云希(阳光男声)、云扬(新闻男声)


3. AI 对话(豆包 API)

集成豆包 API:


class AIManager(

    private val settings: SettingsManager,

    private val memory: MemoryManager

) {

    fun chat(userMessage: String): String {

        // 构建上下文(包含历史对话)

        val messages = buildList {

            add(Message("system", settings.systemPrompt))

            addAll(memory.getRecentContext(10))  // 最近10条对话

            add(Message("user", userMessage))

        }

       

        // 调用豆包 API

        val response = httpClient.post("https://ark.cn-beijing.volces.com/api/v3/chat/completions") {

            header("Authorization", "Bearer ${settings.doubaoApiKey}")

            setBody(ChatRequest(

                model = settings.doubaoModel,

                messages = messages,

                temperature = settings.temperature,

                max_tokens = settings.maxTokens

            ))

        }

       

        return response.body<ChatResponse>().choices[0].message.content

    }

}

记忆系统实现:


class MemoryManager(context: Context) {

    private val db = SQLiteDatabase.openOrCreateDatabase(...)

   

    fun learnFromConversation(userInput: String, aiResponse: String) {

        // 提取关键信息

        val keywords = extractKeywords(userInput, aiResponse)

       

        // 存储到数据库

        db.execSQL("""

            INSERT INTO conversations (user_input, ai_response, keywords, timestamp)

            VALUES (?, ?, ?, ?)

        """, arrayOf(userInput, aiResponse, keywords, System.currentTimeMillis()))

    }

   

    fun getRecentContext(count: Int): List<Message> {

        // 获取最近的对话记录

        val cursor = db.rawQuery("""

            SELECT user_input, ai_response

            FROM conversations

            ORDER BY timestamp DESC

            LIMIT ?

        """, arrayOf(count.toString()))

       

        return buildList {

            while (cursor.moveToNext()) {

                add(Message("user", cursor.getString(0)))

                add(Message("assistant", cursor.getString(1)))

            }

        }.reversed()

    }

}


4. 本地音乐播放

实现思路:

  1. 扫描手机音乐库(过滤短音效)

  2. 支持歌名模糊匹配

  3. 中文歌曲优先


class MusicPlayer(private val context: Context) {

    private val musicLibrary = mutableMapOf<String, String>()

   

    fun loadMusicLibrary() {

        // 只加载 150 秒以上的音乐

        val cursor = context.contentResolver.query(

            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,

            arrayOf(MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.DATA),

            "${MediaStore.Audio.Media.DURATION} >= ?",

            arrayOf("150000"),

            null

        )

       

        cursor?.use {

            while (it.moveToNext()) {

                val title = it.getString(0).lowercase()

                val path = it.getString(1)

                musicLibrary[title] = path

            }

        }

    }

   

    fun play(songName: String): String {

        // 随机播放时,中文歌曲优先

        val matchedSong = if (songName.isEmpty()) {

            val chineseSongs = musicLibrary.keys.filter { containsChinese(it) }

            if (chineseSongs.isNotEmpty()) chineseSongs.random()

            else musicLibrary.keys.random()

        } else {

            musicLibrary.keys.find { it.contains(songName) }

        }

       

        matchedSong?.let {

            mediaPlayer = MediaPlayer().apply {

                setDataSource(musicLibrary[it])

                prepare()

                start()

            }

            return "正在播放《$it》"

        }

       

        return "未找到歌曲"

    }

}


五、UI 设计(Jetpack Compose)

三色状态设计


@Composable

fun RobotScreen() {

    var isAwake by remember { mutableStateOf(false) }

    var isScreenBlack by remember { mutableStateOf(false) }

   

    Box(

        modifier = Modifier

            .fillMaxSize()

            .background(

                when {

                    isScreenBlack -> Color.Black      // 黑屏省电

                    isAwake -> Color(0xFFFF69B4)      // 粉色唤醒

                    else -> Color(0xFF2196F3)         // 蓝色待机

                }

            )

    ) {

        // 表情动画

        FaceAnimator(

            isListening = isAwake,

            isSpeaking = isSpeakingState

        )

       

        // 对话文本

        if (isAwake) {

            Column {

                Text(recognizedText, color = Color(0xFFFFEB3B))  // 黄色用户输入

                Text(responseText, color = Color.White)          // 白色AI回复

            }

        }

    }

}

自适应字体大小


val fontSize = when {

    text.length <= 10 -> (screenWidth * 0.04f).sp

    text.length <= 30 -> (screenWidth * 0.03f).sp

    else -> (screenWidth * 0.02f).sp

}


六、性能优化

1. 内存优化

  • TTS 音频文件播放完立即删除

  • 及时释放 MediaPlayer 资源

  • 使用异步 prepareAsync() 避免阻塞

2. 电量优化

  • 15 秒无声音自动黑屏

  • 播放音乐时降低识别灵敏度

  • 后台保持监听(避免重启导致的 -38 错误)

3. 稳定性优化

  • 全局异常捕获

  • 超时保护(TTS 10秒、AI 30秒)

  • 资源冲突检测


七、遇到的坑和解决方案

坑1:AudioRecord -38 错误

问题: 从后台回到前台时,AudioRecord 启动失败,报 -38 错误。

原因: 旧的 AudioRecord 没有完全释放,新的无法创建。

解决:


override fun onPause() {

    // 不停止监听,让它继续运行

    // speechManager.stop()  // ❌ 不要调用

}

override fun onResume() {

    // 监听一直在运行,无需重启

}

坑2:TTS 播放时被误唤醒

问题: 小端说话时,自己的声音被识别为唤醒词。

原因: 音量阈值太低。

解决:


// 播放音乐时提高音量阈值

if (isPlayingMusic && volume in 500..1000) {

    lastVolumeTime = System.currentTimeMillis()

} else if (!isPlayingMusic && volume > 1000) {

    lastVolumeTime = System.currentTimeMillis()

}

坑3:唤醒后 7 秒自动蓝屏

问题: 说"小端"后,回复"我在",然后立即蓝屏。

原因: 唤醒时没有更新 lastResponseTime

解决:


if (!isAwake && hasWakeWord) {

    isAwake = true

    speakText("我在")

    lastResponseTime = System.currentTimeMillis()  // ✅ 更新时间

}


八、项目亮点

1. 完全免费

  • 无广告、无内购

  • 代码开源(MIT 协议)

  • 所有 API 都有免费额度

2. 保护隐私

  • 语音识别完全离线

  • 对话记录存储在本地

  • 不上传任何用户数据

3. 适合老人

  • 大字体显示

  • 语音交互,不用打字

  • 操作简单,一句话搞定

4. 持续学习

  • 记忆系统,越聊越懂你

  • 支持上下文对话

  • 可自定义系统提示词


九、未来规划

短期计划(1-3个月)

  • 优化真机稳定性(收集用户日志)

  • 添加更多唤醒词

  • 支持方言识别

  • 优化电量消耗

长期计划(6-12个月)

  • 支持多轮对话

  • 添加日程提醒功能

  • 支持智能家居控制

  • 开发 iOS 版本


十、开源地址和下载

项目信息

  • 项目名称:小端机器人

  • 开源协议:MIT

  • 开发语言:Kotlin

  • 最低系统:Android 8.0+

下载体验

  • Gitee:[项目地址]

  • APK 下载:[蓝奏云链接]

  • 使用文档:[语雀文档]

技术交流

  • QQ 群:[群号]

  • 问题反馈:Gitee Issues

  • 邮箱:[联系邮箱]


十一、写在最后

这是一个公益项目,不求回报,只希望能帮助更多人。

如果你觉得这个项目有价值,欢迎:

  • ⭐ 给项目点个 Star

  • 🔄 分享给需要的人

  • 💬 提出你的建议

  • 🤝 参与代码贡献

让 AI 陪伴每一个家庭,让科技更有温度。


作者:诺言

一个热爱技术、关注公益的程序员


相关文章推荐


标签: #Android #Kotlin #AI #语音助手 #开源项目 #公益 #Sherpa-ONNX #Edge-TTS #豆包API


如果这篇文章对你有帮助,欢迎点赞、收藏、关注! 抖音搜小端机器人,有基本教程

有任何问题欢迎在评论区或者Q群362422425留言交流!

使用指南:xiaoduan: 傻傻的机器人 gitee.com/yiliu66/xia…

wechat_2025-11-11_111359_501.png