Skip to content

Commit e67f763

Browse files
committed
feat(models): 模型管理面板——原结构分析模型改名并覆盖全部模型+说话人下载入口
1 parent ffffe2a commit e67f763

3 files changed

Lines changed: 221 additions & 5 deletions

File tree

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/**
2+
* ModelManagementPanel — 模型管理面板(2026-08-20 用户需求:原「结构分析模型」
3+
* 面板改名为模型管理并覆盖**全部模型**——转写/说话人/标点/OCR/结构,各自
4+
* 状态可见、可下载的提供应用内下载入口,未下载的说明降级路径)。
5+
*
6+
* @ai-context: 各区段数据源:health_status(missing_models 按前缀分类:
7+
* streaming/sensevoice/speaker/punctuation)+ asr_streaming_model_status
8+
* + model_download_status(流式进度)+ speaker_model_download_status
9+
* (说话人进度)+ structure_model_status(内嵌 StructureModelSetting)。
10+
* @ai-context: 下载命令幂等:download_streaming_model / download_speaker_model /
11+
* structure_model_download(结构面板内部);进度事件
12+
* model:download-* 与 speaker-model:download-*。
13+
*/
14+
import { useCallback, useEffect, useState } from "react";
15+
import { invoke } from "@tauri-apps/api/core";
16+
import { listen } from "@tauri-apps/api/event";
17+
import StructureModelSetting from "./StructureModelSetting";
18+
19+
/** 健康快照(health_status 载荷)。 */
20+
interface HealthSnapshot {
21+
disk_free_gb: number | null;
22+
disk_warn: boolean;
23+
missing_models: string[];
24+
asr_alive: boolean;
25+
ocr_alive: boolean;
26+
}
27+
28+
/** 流式模型就绪状态。 */
29+
interface StreamingModelStatus {
30+
ready: boolean;
31+
missing: string[];
32+
}
33+
34+
/** 下载进度(model_downloader::DownloadProgress 契约)。 */
35+
interface DownloadProgress {
36+
file: string;
37+
downloadedBytes: number;
38+
totalBytes: number;
39+
}
40+
41+
/** 说话人下载状态。 */
42+
interface SpeakerDownloadStatus {
43+
state: string; // idle | downloading | done | failed
44+
downloadedBytes: number;
45+
totalBytes: number;
46+
error: string | null;
47+
}
48+
49+
const btn: React.CSSProperties = { padding: "2px 10px", fontSize: 11, borderRadius: 6, border: "1px solid #0d9488", background: "#f0fdfa", color: "#0f766e", cursor: "pointer" };
50+
const badge: React.CSSProperties = { fontSize: 11, padding: "1px 8px", borderRadius: 10, background: "#f3f4f6", color: "#6b7280" };
51+
52+
export default function ModelManagementPanel() {
53+
const [health, setHealth] = useState<HealthSnapshot | null>(null);
54+
const [streaming, setStreaming] = useState<StreamingModelStatus | null>(null);
55+
const [streamProgress, setStreamProgress] = useState<DownloadProgress | null>(null);
56+
const [streamBusy, setStreamBusy] = useState(false);
57+
const [speaker, setSpeaker] = useState<SpeakerDownloadStatus | null>(null);
58+
const [speakerBusy, setSpeakerBusy] = useState(false);
59+
const [error, setError] = useState("");
60+
61+
const refresh = useCallback(async () => {
62+
setError("");
63+
try {
64+
const [h, s] = await Promise.all([
65+
invoke<HealthSnapshot>("health_status"),
66+
invoke<StreamingModelStatus>("asr_streaming_model_status"),
67+
]);
68+
setHealth(h);
69+
setStreaming(s);
70+
setSpeaker(await invoke<SpeakerDownloadStatus>("speaker_model_download_status"));
71+
} catch (e) {
72+
setError(`模型状态查询失败: ${e}`);
73+
}
74+
}, []);
75+
76+
useEffect(() => {
77+
void refresh();
78+
}, [refresh]);
79+
80+
// 流式 + 说话人下载进度事件
81+
useEffect(() => {
82+
const unlisten = Promise.all([
83+
listen<DownloadProgress>("model:download-progress", (e) => setStreamProgress(e.payload)),
84+
listen("model:download-done", () => {
85+
setStreamProgress(null);
86+
setStreamBusy(false);
87+
void refresh();
88+
}),
89+
listen<string>("model:download-failed", (e) => {
90+
setStreamBusy(false);
91+
setError(`流式模型下载失败: ${e.payload}`);
92+
}),
93+
listen<DownloadProgress>("speaker-model:download-progress", (e) => setSpeaker({ state: "downloading", downloadedBytes: e.payload.downloadedBytes, totalBytes: e.payload.totalBytes, error: null })),
94+
listen("speaker-model:download-done", () => void refresh()),
95+
listen<string>("speaker-model:download-failed", (e) => {
96+
setSpeakerBusy(false);
97+
setError(`说话人模型下载失败: ${e.payload}`);
98+
}),
99+
]);
100+
return () => {
101+
void unlisten.then((fns) => fns.forEach((fn) => fn()));
102+
};
103+
}, [refresh]);
104+
105+
const downloadStreaming = async () => {
106+
setStreamBusy(true);
107+
setError("");
108+
try {
109+
await invoke("download_streaming_model");
110+
} catch (e) {
111+
setStreamBusy(false);
112+
setError(`流式模型下载启动失败: ${e}`);
113+
}
114+
};
115+
116+
const downloadSpeaker = async () => {
117+
setSpeakerBusy(true);
118+
setError("");
119+
try {
120+
await invoke("download_speaker_model");
121+
} catch (e) {
122+
setSpeakerBusy(false);
123+
setError(`说话人模型下载启动失败: ${e}`);
124+
}
125+
};
126+
127+
const missing = (prefix: string) => (health?.missing_models ?? []).filter((m) => m.startsWith(prefix));
128+
const pct = (d: number, t: number) => (t > 0 ? `${Math.round((d / t) * 100)}%` : "连接中…");
129+
const speakerState = speaker?.state ?? "idle";
130+
131+
return (
132+
<div style={{ fontSize: 12, color: "#374151" }}>
133+
<div style={{ fontSize: 12, color: "#6b7280", marginBottom: 8 }}>
134+
🧩 模型管理(全部模型按需下载 · 未下载自动降级)
135+
</div>
136+
{error && <div style={{ fontSize: 11, color: "#dc2626", marginBottom: 6 }}>{error}</div>}
137+
138+
{/* 转写模型:流式主链路 + 离线重打分 */}
139+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 7, flexWrap: "wrap" }}>
140+
<span style={{ fontSize: 11, width: 200 }}>流式转写(Zipformer)</span>
141+
{streaming?.ready ? (
142+
<span style={{ ...badge, background: "#f0fdfa", color: "#0d9488" }}>✓ 已就绪</span>
143+
) : (
144+
<span style={{ ...badge, background: "#fef2f2", color: "#dc2626" }}>未下载</span>
145+
)}
146+
{!streaming?.ready && !streamBusy && !streamProgress && (
147+
<button style={btn} onClick={() => void downloadStreaming()}>下载(~300MB)</button>
148+
)}
149+
{(streamBusy || streamProgress) && (
150+
<span style={{ fontSize: 11, color: "#0f766e" }}>
151+
⬇ {streamProgress ? `${streamProgress.file} ${pct(streamProgress.downloadedBytes, streamProgress.totalBytes)}` : "连接中…"}
152+
</span>
153+
)}
154+
</div>
155+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 7, flexWrap: "wrap" }}>
156+
<span style={{ fontSize: 11, width: 200 }}>离线转写(SenseVoice)</span>
157+
{missing("sensevoice").length === 0 ? (
158+
<span style={{ ...badge, background: "#f0fdfa", color: "#0d9488" }}>✓ 已就绪</span>
159+
) : (
160+
<span style={{ ...badge, background: "#fef2f2", color: "#dc2626" }}>缺失(重打分降级)</span>
161+
)}
162+
<span style={{ fontSize: 10.5, color: "#9ca3af" }}>随安装包捆绑;脚本 scripts/download-streaming-asr.mjs 可重装</span>
163+
</div>
164+
165+
{/* 说话人模型(讲者切换;应用内下载入口——用户需求) */}
166+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 7, flexWrap: "wrap" }}>
167+
<span style={{ fontSize: 11, width: 200 }}>说话人模型(wespeaker)</span>
168+
{speakerState === "done" ? (
169+
<span style={{ ...badge, background: "#f0fdfa", color: "#0d9488" }}>✓ 已就绪</span>
170+
) : speakerState === "downloading" ? (
171+
<span style={{ fontSize: 11, color: "#0f766e" }}>
172+
⬇ {pct(speaker?.downloadedBytes ?? 0, speaker?.totalBytes ?? 0)}
173+
</span>
174+
) : (
175+
<span style={{ ...badge, background: "#fef2f2", color: "#dc2626" }}>
176+
{speakerState === "failed" ? "下载失败" : "未下载"}
177+
</span>
178+
)}
179+
{speakerState !== "done" && speakerState !== "downloading" && (
180+
<button style={btn} onClick={() => void downloadSpeaker()} disabled={speakerBusy}>
181+
{speakerBusy ? "启动中…" : "下载(20-70MB)"}
182+
</button>
183+
)}
184+
{speakerState === "failed" && speaker?.error && (
185+
<span style={{ fontSize: 10.5, color: "#dc2626" }}>{speaker.error}</span>
186+
)}
187+
</div>
188+
189+
{/* 标点恢复模型 */}
190+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 7, flexWrap: "wrap" }}>
191+
<span style={{ fontSize: 11, width: 200 }}>标点恢复(ct-transformer)</span>
192+
{missing("punctuation").length === 0 ? (
193+
<span style={{ ...badge, background: "#f0fdfa", color: "#0d9488" }}>✓ 已就绪</span>
194+
) : (
195+
<span style={{ ...badge, background: "#fffbeb", color: "#b45309" }}>未下载(无标点降级)</span>
196+
)}
197+
<span style={{ fontSize: 10.5, color: "#9ca3af" }}>脚本 scripts/download-punctuation.mjs 可安装</span>
198+
</div>
199+
200+
{/* OCR 模型(ModelScope 自动缓存) */}
201+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 9, flexWrap: "wrap" }}>
202+
<span style={{ fontSize: 11, width: 200 }}>OCR(PP-OCRv6)</span>
203+
{health?.ocr_alive ? (
204+
<span style={{ ...badge, background: "#f0fdfa", color: "#0d9488" }}>✓ 引擎运行中</span>
205+
) : (
206+
<span style={{ ...badge, background: "#fffbeb", color: "#b45309" }}>引擎未就绪</span>
207+
)}
208+
<span style={{ fontSize: 10.5, color: "#9ca3af" }}>首次使用经 ModelScope 自动缓存</span>
209+
</div>
210+
211+
{/* 结构模型(版面/表格/公式——原结构分析模型区段) */}
212+
<StructureModelSetting />
213+
</div>
214+
);
215+
}

‎app/src/components/StructureModelSetting.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export default function StructureModelSetting() {
113113
return (
114114
<div style={{ fontSize: 12, color: "#374151" }}>
115115
<div style={{ fontSize: 12, color: "#6b7280", marginBottom: 6 }}>
116-
结构分析模型(按需下载,未下载自动用规则版)
116+
结构模型(版面/表格/公式;按需下载,未下载自动用规则版)
117117
</div>
118118
{(["layout", "table", "formula"] as ModelKind[]).map((kind) => {
119119
const st = statusOf(kind);

‎app/src/pages/ClassroomPage.tsx‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ import AudioLevelMeter from "../components/AudioLevelMeter";
2929
import ReadyCheckCard from "../components/ReadyCheckCard";
3030
import { VocabManager } from "../components/VocabManager";
3131
import { SystemStatusBadge } from "../components/SystemStatusBadge";
32-
// v0.5.0 模型版:结构模型设置(版面/表格/公式按需下载)
33-
import StructureModelSetting from "../components/StructureModelSetting";
32+
// 2026-08-20 用户需求:模型管理面板(原结构分析模型改名,覆盖全部模型)
33+
import ModelManagementPanel from "../components/ModelManagementPanel";
3434
// 2026-08 审查硬拆:右栏内容区 / 文件素材输入与提取
3535
import ClassroomRightPane from "../components/ClassroomRightPane";
3636
import MaterialInputPanel from "../components/MaterialInputPanel";
@@ -546,9 +546,10 @@ export default function ClassroomPage({ onOpenSessions }: { onOpenSessions?: (se
546546
<VocabManager />
547547
</div>
548548

549-
{/* 结构分析模型(v0.5.0 模型版:版面/表格/公式按需下载,未下载自动规则版) */}
549+
{/* 模型管理(2026-08-20 用户需求:原结构分析模型面板改名并覆盖全部模型——
550+
转写/说话人/标点/OCR/结构,按需下载、未下载自动降级) */}
550551
<div style={panel}>
551-
<StructureModelSetting />
552+
<ModelManagementPanel />
552553
{/* TD-2026-08-19-E 清偿:模型磁盘占用面板(REQ-131 命令前端接入) */}
553554
<ModelDiskPanel />
554555
</div>

0 commit comments

Comments
 (0)