关于模型设置-模型下载进度条的若干修复 (#293)

* fix: Various fixes for the model settings - model download progress bar

* fix(local-asr): log partial-size errors and seed redownload progress
This commit is contained in:
Hongbro886 authored and GitHub committed 2026-05-07 20:21:51 +08:00
1 parent 813d9ec2e9
commit 32133b3438
3 files changed
+118 -10

No files matched your search

@@ -470,6 +470,59 @@ const PARALLEL: usize = 8;
const PER_CHUNK_ATTEMPTS: u32 = 4;
const PARALLEL_FILES: usize = 3;
/// `.partial` 文件的真实已下字节(不是 sparse 逻辑大小)。
/// 有 `.partial.idx` → chunked 模式,按 idx 里 chunk 数还原;
/// 没有 → append/single-stream 模式,partial 是 dense,meta.len() 即真实字节。
pub fn partial_actual_size(partial: &Path) -> u64 {
let total_size = match std::fs::metadata(partial) {
Ok(m) => m.len(),
Err(e) => {
eprintln!(
"[local-asr] partial_actual_size: stat partial failed ({}): {}",
partial.display(),
e
);
return 0;
}
};
if total_size == 0 {
return 0;
}
let idx_path = partial.with_extension("partial.idx");
if !idx_path.exists() {
return total_size;
}
let content = match std::fs::read_to_string(&idx_path) {
Ok(s) => s,
Err(e) => {
// idx 不可读 → 不知道哪些 chunk 已落盘,sparse 全长不可信,只能回 0。
// 但日志要留,否则进度条无故归零没法排查。
eprintln!(
"[local-asr] partial_actual_size: read idx failed ({}): {}",
idx_path.display(),
e
);
return 0;
}
};
let mut seen: HashSet<usize> = HashSet::new();
let mut total: u64 = 0;
for line in content.lines() {
let Ok(idx) = line.trim().parse::<usize>() else { continue };
if !seen.insert(idx) {
continue;
}
let start = (idx as u64).saturating_mul(CHUNK_SIZE);
if start >= total_size {
continue;
}
// 最后一块可能不到 CHUNK_SIZE
let end = (start + CHUNK_SIZE).min(total_size);
total += end - start;
}
total
}
async fn download_one(
client: &reqwest::Client,
url: &str,
@@ -524,6 +577,12 @@ async fn download_one(
f.set_len(total_size)
.with_context(|| format!("set_len partial failed: {}", partial.display()))?;
}
// 模式标记:sparse partial 必须配对 .partial.idx(哪怕空),
// 否则 walk_files 看到 partial 有但 idx 无,会把 sparse 全长当成已下完。
if !idx_path.exists() {
std::fs::write(&idx_path, b"")
.with_context(|| format!("touch partial.idx failed: {}", idx_path.display()))?;
}
// 4. 总计已下字节(用于初始化进度)
let initial_done: u64 = chunks
@@ -82,15 +82,22 @@ fn walk_files<F: FnMut(u64)>(dir: &std::path::Path, on_size: &mut F) {
};
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name();
// 哨兵文件本身体积忽略不计,但路径过滤更直白:保留所有非空文件。
let name_os = entry.file_name();
let name = name_os.to_string_lossy();
if name == READY_SENTINEL {
continue;
}
// .partial.idx 是 chunk 完成索引,不算下载字节
if name.ends_with(".partial.idx") {
continue;
}
match entry.file_type() {
Ok(ft) if ft.is_dir() => walk_files(&path, on_size),
Ok(ft) if ft.is_file() => {
if let Ok(meta) = entry.metadata() {
// .partial 在 chunked 模式下是 sparse 全长,meta.len() 不是真实字节
if name.ends_with(".partial") {
on_size(super::download::partial_actual_size(&path));
} else if let Ok(meta) = entry.metadata() {
on_size(meta.len());
}
}
+49 -7
View File
@@ -205,7 +205,16 @@ export function LocalAsr() {
const { listen } = await import('@tauri-apps/api/event');
const off = await listen<LocalAsrDownloadProgress>('local-asr-download-progress', e => {
const payload = e.payload;
setProgress(prev => ({ ...prev, [payload.modelId]: payload }));
if (payload.phase === 'cancelled') {
// 取消时清条目,bar 是否还显示交给 hasPartial 判断
setProgress(prev => {
const next = { ...prev };
delete next[payload.modelId];
return next;
});
} else {
setProgress(prev => ({ ...prev, [payload.modelId]: payload }));
}
if (
payload.phase === 'finished' ||
payload.phase === 'cancelled' ||
@@ -368,18 +377,56 @@ export function LocalAsr() {
const handleDownload = async (modelId: string) => {
setBusyModelId(modelId);
// 重下载时,第一个后端事件到达前先用本地已知值占位,避免进度条从 0% 跳到真实位置。
// 优先级:上一次 progress(取消后已删,通常没有)→ models 里的 downloadedBytes(cancel 时乐观写入)
const model = models.find(m => m.id === modelId);
const initialDownloaded =
progress[modelId]?.bytesDownloaded ?? model?.downloadedBytes ?? 0;
setProgress(prev => ({
...prev,
[modelId]: {
modelId,
file: '',
fileIndex: 0,
fileCount: remoteSizes[modelId]?.fileCount ?? 0,
bytesDownloaded: initialDownloaded,
bytesTotal: remoteSizes[modelId]?.totalBytes ?? 0,
phase: 'started',
error: null,
},
}));
try {
await downloadLocalAsrModel(modelId, settings?.mirror);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
setProgress(prev => {
const cur = prev[modelId];
if (cur?.phase === 'started') {
return { ...prev, [modelId]: { ...cur, phase: 'failed', error: e instanceof Error ? e.message : String(e) } };
}
return prev;
});
} finally {
setBusyModelId(null);
}
};
const handleCancel = async (modelId: string) => {
// Progress 事件里的 bytesDownloaded 是后端 in_flight + already_done,是真实字节
const lastBytes = progress[modelId]?.bytesDownloaded ?? 0;
try {
await cancelLocalAsrDownload(modelId);
setProgress(prev => {
const next = { ...prev };
delete next[modelId];
return next;
});
// 乐观更新:让 hasPartial 立刻翻 true,不等 listener 200ms 后的 refresh
if (lastBytes > 0) {
setModels(prev =>
prev.map(m => (m.id === modelId ? { ...m, downloadedBytes: lastBytes } : m)),
);
}
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
}
@@ -907,8 +954,7 @@ function ModelRow({
// 进度条要保留:有 partial 残留(downloadedBytes>0 但未完整)就一直显示,
// 让用户看到上次下到哪里了,再点下载会从那里续。
const hasPartial = !model.isDownloaded && model.downloadedBytes > 0;
const showProgress =
isDownloading || progress?.phase === 'failed' || progress?.phase === 'cancelled' || hasPartial;
const showProgress = isDownloading || progress?.phase === 'failed' || hasPartial;
const sizeLabel = remoteSize?.loading
? t('localAsr.sizeLoading')
@@ -946,8 +992,6 @@ function ModelRow({
background:
progress?.phase === 'failed'
? '#d04545'
: progress?.phase === 'cancelled'
? 'var(--ol-ink-4)'
: 'var(--ol-accent-blue, #2c5cff)',
transition: 'width 120ms linear',
}}
@@ -956,8 +1000,6 @@ function ModelRow({
<div style={{ fontSize: 11, color: 'var(--ol-ink-4)', marginTop: 6 }}>
{progress?.phase === 'failed'
? `${t('localAsr.failed')}: ${progress.error ?? ''}`
: progress?.phase === 'cancelled'
? t('localAsr.cancelled')
: `${formatBytes(downloadedBytes)} / ${formatBytes(totalBytes)}` +
(progress?.file ? ` · ${progress.file}` : '')}
</div>