guoyu/log/运行时下载模型完整示例.md

10 KiB
Raw Blame History

运行时下载模型 - 完整实现示例

确认:完全可以实现!

uni-app 的 uni.downloadFile API 完全支持运行时下载大文件。


📁 文件清单

已创建:

  • utils/modelDownloader.js - 模型下载管理器(已创建)

需要修改:

  • ⚠️ pages/speech/speech.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中有存储权限

{
  "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. 提供测试建议

总结:完全可以运行时下载!建议先打包测试,后续再实施下载功能。