《我用Kotlin开发了一个免费的AI语音助手》
> 作者:诺言
> 项目名称:小端机器人
> 开源协议: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?**
- ✅ 完全离线,不需要联网
- ✅ 支持中英文混合识别
- ✅ 识别准确率高
- ✅ 资源占用低
**集成步骤:**
```kotlin
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?**
- ✅ 完全免费
- ✅ 音质接近真人
- ✅ 支持多种音色
- ✅ 支持语速调节
**实现代码:**
```kotlin
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:**
```kotlin
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
}
}
```
**记忆系统实现:**
```kotlin
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. 中文歌曲优先
```kotlin
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)
### 三色状态设计
```kotlin
@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回复
}
}
}
}
```
### 自适应字体大小
```kotlin
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 没有完全释放,新的无法创建。
**解决:**
```kotlin
override fun onPause() {
// 不停止监听,让它继续运行
// speechManager.stop() // ❌ 不要调用
}
override fun onResume() {
// 监听一直在运行,无需重启
}
```
### 坑2:TTS 播放时被误唤醒
**问题:** 小端说话时,自己的声音被识别为唤醒词。
**原因:** 音量阈值太低。
**解决:**
```kotlin
// 播放音乐时提高音量阈值
if (isPlayingMusic && volume in 500..1000) {
lastVolumeTime = System.currentTimeMillis()
} else if (!isPlayingMusic && volume > 1000) {
lastVolumeTime = System.currentTimeMillis()
}
```
### 坑3:唤醒后 7 秒自动蓝屏
**问题:** 说"小端"后,回复"我在",然后立即蓝屏。
**原因:** 唤醒时没有更新 `lastResponseTime`。
**解决:**
```kotlin
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 陪伴每一个家庭,让科技更有温度。**
---
**作者:诺言**
**一个热爱技术、关注公益的程序员**
---
## 相关文章推荐
- [Sherpa-ONNX 离线语音识别实战]()
- [Android 集成 Edge TTS 实现高质量语音合成]()
- [Jetpack Compose 实现动态表情动画]()
---
**标签:** #Android #Kotlin #AI #语音助手 #开源项目 #公益 #Sherpa-ONNX #Edge-TTS #豆包API
---
> 如果这篇文章对你有帮助,欢迎点赞、收藏、关注! 抖音搜小端机器人,有基本教程
> 有任何问题欢迎在评论区或者Q群362422425留言交流!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)