/** * 语音模型下载管理器 * 用于在APP运行时下载和管理语音识别模型 */ class ModelDownloader { constructor() { this.modelUrl = 'https://app.liuyingyong.cn/static/vosk-model-small-cn-0.22.zip'; this.modelFileName = 'vosk-model-small-cn-0.22.zip'; this.modelSize = 41.87 * 1024 * 1024; // 41.87 MB } /** * 获取模型本地路径 */ getModelPath() { // #ifdef APP-PLUS return plus.io.convertLocalFileSystemURL('_doc/' + this.modelFileName); // #endif // #ifndef APP-PLUS return ''; // #endif } /** * 检查模型是否已下载 */ checkModelExists() { return new Promise((resolve, reject) => { // #ifdef APP-PLUS const localPath = this.getModelPath(); plus.io.resolveLocalFileSystemURL(localPath, (entry) => { // 文件存在,检查大小 entry.file((file) => { if (file.size > 0) { console.log('模型文件已存在,大小:', file.size); resolve({ exists: true, path: localPath, size: file.size }); } else { console.log('模型文件存在但大小为0,需要重新下载'); resolve({ exists: false }); } }, () => { resolve({ exists: false }); }); }, () => { // 文件不存在 console.log('模型文件不存在'); resolve({ exists: false }); }); // #endif // #ifndef APP-PLUS resolve({ exists: false }); // #endif }); } /** * 下载模型 * @param {Function} onProgress 进度回调 (progress, speed) * @param {Function} onSuccess 成功回调 (filePath) * @param {Function} onError 失败回调 (error) */ downloadModel(onProgress, onSuccess, onError) { console.log('开始下载模型:', this.modelUrl); const localPath = this.getModelPath(); let startTime = Date.now(); let lastLoaded = 0; const downloadTask = uni.downloadFile({ url: this.modelUrl, filePath: localPath, timeout: 300000, // 5分钟超时 success: (res) => { if (res.statusCode === 200) { console.log('模型下载成功:', res.tempFilePath); // 验证文件大小 // #ifdef APP-PLUS plus.io.resolveLocalFileSystemURL(localPath, (entry) => { entry.file((file) => { if (file.size > 10 * 1024 * 1024) { // 至少10MB console.log('模型文件验证通过,大小:', file.size); onSuccess && onSuccess(localPath); } else { console.error('模型文件大小异常:', file.size); onError && onError(new Error('文件大小异常')); } }, (err) => { onError && onError(err); }); }, (err) => { onError && onError(err); }); // #endif // #ifndef APP-PLUS onSuccess && onSuccess(localPath); // #endif } else { console.error('下载失败,状态码:', res.statusCode); onError && onError(new Error('下载失败:' + res.statusCode)); } }, fail: (err) => { console.error('下载失败:', err); onError && onError(err); } }); // 监听下载进度 downloadTask.onProgressUpdate((res) => { const progress = res.progress; const totalBytesWritten = res.totalBytesWritten; const totalBytesExpectedToWrite = res.totalBytesExpectedToWrite; // 计算下载速度 const now = Date.now(); const elapsed = (now - startTime) / 1000; // 秒 const downloaded = totalBytesWritten - lastLoaded; const speed = downloaded / elapsed; // 字节/秒 lastLoaded = totalBytesWritten; startTime = now; console.log('下载进度:', progress + '%', '速度:', (speed / 1024).toFixed(2) + ' KB/s'); onProgress && onProgress({ progress: progress, loaded: totalBytesWritten, total: totalBytesExpectedToWrite, speed: speed }); }); return downloadTask; } /** * 删除模型文件 */ deleteModel() { return new Promise((resolve, reject) => { // #ifdef APP-PLUS const localPath = this.getModelPath(); plus.io.resolveLocalFileSystemURL(localPath, (entry) => { entry.remove(() => { console.log('模型文件已删除'); resolve(); }, (err) => { console.error('删除失败:', err); reject(err); }); }, () => { // 文件不存在 resolve(); }); // #endif // #ifndef APP-PLUS resolve(); // #endif }); } /** * 格式化文件大小 */ formatSize(bytes) { if (bytes < 1024) { return bytes + ' B'; } else if (bytes < 1024 * 1024) { return (bytes / 1024).toFixed(2) + ' KB'; } else if (bytes < 1024 * 1024 * 1024) { return (bytes / (1024 * 1024)).toFixed(2) + ' MB'; } else { return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; } } /** * 格式化下载速度 */ formatSpeed(bytesPerSecond) { return this.formatSize(bytesPerSecond) + '/s'; } } // 导出单例 export default new ModelDownloader();