536 lines
10 KiB
Markdown
536 lines
10 KiB
Markdown
|
|
# 运行时下载模型 - 完整实现示例
|
|||
|
|
|
|||
|
|
## ✅ **确认:完全可以实现!**
|
|||
|
|
|
|||
|
|
uni-app 的 `uni.downloadFile` API 完全支持运行时下载大文件。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📁 **文件清单**
|
|||
|
|
|
|||
|
|
已创建:
|
|||
|
|
- ✅ `utils/modelDownloader.js` - 模型下载管理器(已创建)
|
|||
|
|
|
|||
|
|
需要修改:
|
|||
|
|
- ⚠️ `pages/speech/speech.vue` - 语音识别页面
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎯 **在语音页面中使用**
|
|||
|
|
|
|||
|
|
### **完整代码示例**
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<view class="container">
|
|||
|
|
<!-- 模型下载提示 -->
|
|||
|
|
<view v-if="!modelReady" class="download-container">
|
|||
|
|
<view class="download-header">
|
|||
|
|
<text class="title">语音识别模型初始化</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 未开始下载 -->
|
|||
|
|
<view v-if="downloadStatus === 'idle'" class="idle-state">
|
|||
|
|
<text class="tip">首次使用需要下载语音识别模型</text>
|
|||
|
|
<text class="size">文件大小:41.87 MB</text>
|
|||
|
|
<text class="wifi-tip">建议在WiFi环境下下载</text>
|
|||
|
|
|
|||
|
|
<button
|
|||
|
|
type="primary"
|
|||
|
|
@click="startDownload"
|
|||
|
|
class="download-btn">
|
|||
|
|
开始下载
|
|||
|
|
</button>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 下载中 -->
|
|||
|
|
<view v-if="downloadStatus === 'downloading'" class="downloading-state">
|
|||
|
|
<text class="progress-text">下载进度:{{ downloadProgress }}%</text>
|
|||
|
|
<progress
|
|||
|
|
:percent="downloadProgress"
|
|||
|
|
:show-info="true"
|
|||
|
|
:stroke-width="10"
|
|||
|
|
activeColor="#07c160" />
|
|||
|
|
<text class="speed-text">速度:{{ downloadSpeed }}</text>
|
|||
|
|
<text class="size-text">{{ downloadedSize }} / {{ totalSize }}</text>
|
|||
|
|
|
|||
|
|
<button
|
|||
|
|
type="warn"
|
|||
|
|
@click="cancelDownload"
|
|||
|
|
class="cancel-btn"
|
|||
|
|
size="mini">
|
|||
|
|
取消下载
|
|||
|
|
</button>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 下载失败 -->
|
|||
|
|
<view v-if="downloadStatus === 'error'" class="error-state">
|
|||
|
|
<text class="error-text">下载失败:{{ errorMessage }}</text>
|
|||
|
|
<button
|
|||
|
|
type="primary"
|
|||
|
|
@click="retryDownload"
|
|||
|
|
class="retry-btn">
|
|||
|
|
重试
|
|||
|
|
</button>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 语音识别界面 -->
|
|||
|
|
<view v-else class="speech-container">
|
|||
|
|
<text class="ready-text">✅ 模型已就绪</text>
|
|||
|
|
|
|||
|
|
<!-- 原来的语音识别界面 -->
|
|||
|
|
<button @click="startSpeech" type="primary">开始识别</button>
|
|||
|
|
|
|||
|
|
<!-- 设置:重新下载模型 -->
|
|||
|
|
<view class="settings">
|
|||
|
|
<button @click="redownloadModel" size="mini">重新下载模型</button>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import modelDownloader from '@/utils/modelDownloader.js';
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
modelReady: false,
|
|||
|
|
downloadStatus: 'idle', // idle, downloading, success, error
|
|||
|
|
downloadProgress: 0,
|
|||
|
|
downloadSpeed: '',
|
|||
|
|
downloadedSize: '',
|
|||
|
|
totalSize: '',
|
|||
|
|
errorMessage: '',
|
|||
|
|
downloadTask: null,
|
|||
|
|
modelPath: ''
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onLoad() {
|
|||
|
|
// 页面加载时检查模型
|
|||
|
|
this.checkModel();
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
methods: {
|
|||
|
|
/**
|
|||
|
|
* 检查模型是否存在
|
|||
|
|
*/
|
|||
|
|
async checkModel() {
|
|||
|
|
try {
|
|||
|
|
uni.showLoading({
|
|||
|
|
title: '检查模型...',
|
|||
|
|
mask: true
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const result = await modelDownloader.checkModelExists();
|
|||
|
|
|
|||
|
|
uni.hideLoading();
|
|||
|
|
|
|||
|
|
if (result.exists) {
|
|||
|
|
console.log('模型已存在,路径:', result.path);
|
|||
|
|
this.modelPath = result.path;
|
|||
|
|
this.modelReady = true;
|
|||
|
|
this.downloadStatus = 'success';
|
|||
|
|
} else {
|
|||
|
|
console.log('模型不存在,需要下载');
|
|||
|
|
this.modelReady = false;
|
|||
|
|
this.downloadStatus = 'idle';
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('检查模型失败:', e);
|
|||
|
|
uni.hideLoading();
|
|||
|
|
this.downloadStatus = 'idle';
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 开始下载
|
|||
|
|
*/
|
|||
|
|
startDownload() {
|
|||
|
|
// 检查网络类型
|
|||
|
|
uni.getNetworkType({
|
|||
|
|
success: (res) => {
|
|||
|
|
const networkType = res.networkType;
|
|||
|
|
|
|||
|
|
if (networkType === '2g' || networkType === '3g') {
|
|||
|
|
// 移动网络提示
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '提示',
|
|||
|
|
content: '当前使用移动网络,下载将消耗约42MB流量,是否继续?',
|
|||
|
|
success: (modalRes) => {
|
|||
|
|
if (modalRes.confirm) {
|
|||
|
|
this.doDownload();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
// WiFi或其他网络直接下载
|
|||
|
|
this.doDownload();
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
fail: () => {
|
|||
|
|
// 无法获取网络类型,直接下载
|
|||
|
|
this.doDownload();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 执行下载
|
|||
|
|
*/
|
|||
|
|
doDownload() {
|
|||
|
|
this.downloadStatus = 'downloading';
|
|||
|
|
this.downloadProgress = 0;
|
|||
|
|
|
|||
|
|
this.downloadTask = modelDownloader.downloadModel(
|
|||
|
|
// 进度回调
|
|||
|
|
(progressData) => {
|
|||
|
|
this.downloadProgress = Math.floor(progressData.progress);
|
|||
|
|
this.downloadSpeed = modelDownloader.formatSpeed(progressData.speed);
|
|||
|
|
this.downloadedSize = modelDownloader.formatSize(progressData.loaded);
|
|||
|
|
this.totalSize = modelDownloader.formatSize(progressData.total);
|
|||
|
|
},
|
|||
|
|
// 成功回调
|
|||
|
|
(filePath) => {
|
|||
|
|
console.log('下载成功:', filePath);
|
|||
|
|
this.modelPath = filePath;
|
|||
|
|
this.modelReady = true;
|
|||
|
|
this.downloadStatus = 'success';
|
|||
|
|
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '模型下载完成',
|
|||
|
|
icon: 'success',
|
|||
|
|
duration: 2000
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
// 失败回调
|
|||
|
|
(error) => {
|
|||
|
|
console.error('下载失败:', error);
|
|||
|
|
this.downloadStatus = 'error';
|
|||
|
|
this.errorMessage = error.message || '未知错误';
|
|||
|
|
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '下载失败',
|
|||
|
|
icon: 'none',
|
|||
|
|
duration: 2000
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 取消下载
|
|||
|
|
*/
|
|||
|
|
cancelDownload() {
|
|||
|
|
if (this.downloadTask) {
|
|||
|
|
this.downloadTask.abort();
|
|||
|
|
this.downloadTask = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.downloadStatus = 'idle';
|
|||
|
|
this.downloadProgress = 0;
|
|||
|
|
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '已取消下载',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 重试下载
|
|||
|
|
*/
|
|||
|
|
retryDownload() {
|
|||
|
|
this.doDownload();
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 重新下载模型
|
|||
|
|
*/
|
|||
|
|
redownloadModel() {
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '确认',
|
|||
|
|
content: '确定要重新下载模型吗?',
|
|||
|
|
success: async (res) => {
|
|||
|
|
if (res.confirm) {
|
|||
|
|
try {
|
|||
|
|
// 删除旧模型
|
|||
|
|
await modelDownloader.deleteModel();
|
|||
|
|
|
|||
|
|
// 重置状态
|
|||
|
|
this.modelReady = false;
|
|||
|
|
this.downloadStatus = 'idle';
|
|||
|
|
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '请重新下载',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('删除模型失败:', e);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 开始语音识别
|
|||
|
|
*/
|
|||
|
|
startSpeech() {
|
|||
|
|
if (!this.modelReady) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '模型未就绪',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 调用语音识别插件
|
|||
|
|
// 传入模型路径:this.modelPath
|
|||
|
|
console.log('使用模型路径:', this.modelPath);
|
|||
|
|
|
|||
|
|
// 原来的语音识别代码
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.container {
|
|||
|
|
padding: 30rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.download-container {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 40rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.download-header {
|
|||
|
|
margin-bottom: 30rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.title {
|
|||
|
|
font-size: 36rpx;
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.idle-state {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 20rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tip {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
color: #666;
|
|||
|
|
margin-top: 20rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.size {
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
color: #07c160;
|
|||
|
|
font-weight: bold;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.wifi-tip {
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
color: #999;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.download-btn {
|
|||
|
|
margin-top: 40rpx;
|
|||
|
|
width: 300rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.downloading-state {
|
|||
|
|
width: 100%;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 20rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.progress-text {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
color: #333;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.speed-text,
|
|||
|
|
.size-text {
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
color: #666;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.cancel-btn {
|
|||
|
|
margin-top: 20rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.error-state {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 20rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.error-text {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
color: #ff0000;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.retry-btn {
|
|||
|
|
width: 300rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.speech-container {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 30rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.ready-text {
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
color: #07c160;
|
|||
|
|
font-weight: bold;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.settings {
|
|||
|
|
margin-top: 50rpx;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ✅ **优势说明**
|
|||
|
|
|
|||
|
|
### **1. 用户体验好**
|
|||
|
|
- ✅ 首次下载有进度显示
|
|||
|
|
- ✅ 显示下载速度和剩余大小
|
|||
|
|
- ✅ 移动网络下有流量提醒
|
|||
|
|
- ✅ 可以取消和重试
|
|||
|
|
|
|||
|
|
### **2. 技术可靠**
|
|||
|
|
- ✅ uni-app 官方 API 支持
|
|||
|
|
- ✅ 支持大文件下载(GB级别)
|
|||
|
|
- ✅ 自动处理文件保存
|
|||
|
|
- ✅ 支持断点续传(需服务器配合)
|
|||
|
|
|
|||
|
|
### **3. 易于维护**
|
|||
|
|
- ✅ 模型可以随时更新
|
|||
|
|
- ✅ 不需要重新打包APP
|
|||
|
|
- ✅ 可以支持多个模型版本
|
|||
|
|
- ✅ 用户可以手动重新下载
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📊 **效果对比**
|
|||
|
|
|
|||
|
|
| 方案 | APK大小 | 首次启动 | 更新模型 | 推荐度 |
|
|||
|
|
|------|---------|----------|----------|--------|
|
|||
|
|
| 打包方式 | ~100MB | 即用 | 需重新打包 | ⭐⭐ |
|
|||
|
|
| 运行时下载 | ~58MB | 需下载1分钟 | 直接更新 | ⭐⭐⭐⭐⭐ |
|
|||
|
|
| 自定义基座 | ~100MB | 即用 | 需重新打包 | ⭐⭐⭐ |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🚀 **实施步骤**
|
|||
|
|
|
|||
|
|
### **1. 模型文件已移除** ✅
|
|||
|
|
```
|
|||
|
|
vosk-model-small-cn-0.22.zip 已移动到桌面
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### **2. 下载管理器已创建** ✅
|
|||
|
|
```
|
|||
|
|
utils/modelDownloader.js
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### **3. 上传模型到服务器**
|
|||
|
|
```
|
|||
|
|
将 vosk-model-small-cn-0.22.zip 上传到:
|
|||
|
|
https://app.liuyingyong.cn/static/vosk-model-small-cn-0.22.zip
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### **4. 修改语音页面**
|
|||
|
|
```
|
|||
|
|
参考上面的示例代码
|
|||
|
|
修改 pages/speech/speech.vue
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### **5. 测试**
|
|||
|
|
```
|
|||
|
|
1. 打包APK(现在可以打包了,大小约58MB)
|
|||
|
|
2. 安装到手机
|
|||
|
|
3. 打开语音页面
|
|||
|
|
4. 点击"开始下载"
|
|||
|
|
5. 等待下载完成
|
|||
|
|
6. 测试语音识别功能
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ⚠️ **注意事项**
|
|||
|
|
|
|||
|
|
### **服务器要求**
|
|||
|
|
|
|||
|
|
1. **支持大文件下载**
|
|||
|
|
- 确保服务器允许下载42MB文件
|
|||
|
|
- 设置合适的超时时间
|
|||
|
|
|
|||
|
|
2. **HTTPS支持**
|
|||
|
|
- iOS必须使用HTTPS
|
|||
|
|
- Android建议使用HTTPS
|
|||
|
|
|
|||
|
|
3. **CDN加速(可选)**
|
|||
|
|
- 使用CDN可以提升下载速度
|
|||
|
|
- 减轻服务器压力
|
|||
|
|
|
|||
|
|
### **APP权限**
|
|||
|
|
|
|||
|
|
确保manifest.json中有存储权限:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"permissions": {
|
|||
|
|
"WRITE_EXTERNAL_STORAGE": {},
|
|||
|
|
"READ_EXTERNAL_STORAGE": {}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎯 **现在可以做什么?**
|
|||
|
|
|
|||
|
|
### **方案A:先打包测试(推荐)** ⭐⭐⭐⭐⭐
|
|||
|
|
|
|||
|
|
1. ✅ 模型已移除,现在可以打包
|
|||
|
|
2. ✅ 打包后测试除语音外的功能
|
|||
|
|
3. ⏰ 后续实施运行时下载
|
|||
|
|
|
|||
|
|
### **方案B:立即实施运行时下载**
|
|||
|
|
|
|||
|
|
1. 上传模型到服务器
|
|||
|
|
2. 按照示例修改speech.vue
|
|||
|
|
3. 打包测试完整功能
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📞 **需要帮助?**
|
|||
|
|
|
|||
|
|
如果您选择方案B,我可以:
|
|||
|
|
1. 帮您修改 `pages/speech/speech.vue`
|
|||
|
|
2. 检查服务器配置
|
|||
|
|
3. 提供测试建议
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**总结:完全可以运行时下载!建议先打包测试,后续再实施下载功能。** ✅
|