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. 提供测试建议
|
||
|
||
---
|
||
|
||
**总结:完全可以运行时下载!建议先打包测试,后续再实施下载功能。** ✅
|