207 lines
4.8 KiB
JavaScript
207 lines
4.8 KiB
JavaScript
/**
|
||
* 语音模型下载管理器
|
||
* 用于在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();
|