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();
|