From 61aa51f345b780647e91e14b313875fb82cdcb46 Mon Sep 17 00:00:00 2001 From: linxiaodong Date: Tue, 18 Jun 2024 17:40:59 +0800 Subject: [PATCH] feat: show progress when downloading whisper and models --- main/background.ts | 4 - main/helpers/whisper.ts | 59 +++------------ renderer/components/DownModel.tsx | 44 ++++++----- renderer/components/DownModelButton.tsx | 29 ++++++++ renderer/components/DownModelDropdown.tsx | 69 +++++++++++++++++ renderer/components/DownModelLink.tsx | 26 +++++++ renderer/components/Guide.tsx | 90 +++++++++++------------ renderer/lib/utils.ts | 7 ++ renderer/pages/home.tsx | 36 +++------ renderer/pages/modelsControl.tsx | 10 ++- 10 files changed, 228 insertions(+), 146 deletions(-) create mode 100644 renderer/components/DownModelButton.tsx create mode 100644 renderer/components/DownModelDropdown.tsx create mode 100644 renderer/components/DownModelLink.tsx diff --git a/main/background.ts b/main/background.ts index 70ed49b..42ce31a 100644 --- a/main/background.ts +++ b/main/background.ts @@ -5,7 +5,6 @@ import serve from "electron-serve"; import { createWindow } from "./helpers"; import { install, - downModel, makeWhisper, checkWhisperInstalled, getModelsInstalled, @@ -183,9 +182,6 @@ ipcMain.on("makeWhisper", (event) => { makeWhisper(event); }); -ipcMain.on("downModel", (event, { model, source }) => { - downModel(event, model, source); -}); ipcMain.on("openUrl", (event, url) => { shell.openExternal(url); diff --git a/main/helpers/whisper.ts b/main/helpers/whisper.ts index b373a56..ec13145 100644 --- a/main/helpers/whisper.ts +++ b/main/helpers/whisper.ts @@ -61,6 +61,11 @@ export const install = (event, source) => { url: repoUrl, singleBranch: true, depth: 1, + onProgress: (res) => { + if (res.total) { + event.sender.send("installWhisperProgress", res.phase, res.loaded / res.total); + } + }, }) .then((res) => { if (checkWhisperInstalled()) { @@ -80,50 +85,6 @@ export const install = (event, source) => { }); }; -export const downModel = async ( - event, - whisperModel, - source = "hf-mirror.com", -) => { - const { modelsPath } = getPath(); - const modelName = whisperModel?.toLowerCase(); - const modelPath = path.join(modelsPath, `ggml-${modelName}.bin`); - if (fs.existsSync(modelPath)) return; - if (!checkWhisperInstalled()) { - event.sender.send("message", "whisper.cpp 未下载,请先下载 whisper.cpp"); - } - try { - let downShellPath; - let shell: string; - if (isDarwin()) { - downShellPath = path.join(modelsPath, "download-ggml-model.sh"); - shell = "bash"; - } else if (isWin32()) { - downShellPath = path.join(modelsPath, "download-ggml-model.cmd"); - shell = "cmd.exe /c"; - } else { - throw Error("platform does not support! "); - } - await replaceModelSource(`${downShellPath}`, source); - console.log("完成模型下载地址替换", modelName); - console.log("正在安装 whisper.cpp 模型"); - exec(`${shell} "${downShellPath}" ${modelName}`, (err, stdout) => { - if (err) { - event.sender.send("message", err); - } else { - event.sender.send("message", `模型 ${modelName} 下载完成`); - } - event.sender.send("downModelComplete", !err); - event.sender.send("getSystemInfoComplete", { - whisperInstalled: checkWhisperInstalled(), - modelsInstalled: getModelsInstalled(), - }); - }); - } catch (error) { - event.sender.send("message", error); - } -}; - export const makeWhisper = (event) => { const { whisperPath, mainPath } = getPath(); if (fs.existsSync(mainPath) || isWin32()) { @@ -133,6 +94,7 @@ export const makeWhisper = (event) => { if (!checkWhisperInstalled()) { event.sender.send("message", "whisper.cpp 未下载,请先下载 whisper.cpp"); } + event.sender.send("beginMakeWhisper", true); exec(`make -C "${whisperPath}"`, (err, stdout) => { if (err) { event.sender.send("message", err); @@ -168,12 +130,15 @@ export const downloadModelSync = async (model, source, onProcess) => { try { let downShellPath; let shell: string; + let args = []; if (isDarwin()) { downShellPath = path.join(modelsPath, "download-ggml-model.sh"); shell = "bash"; + args = [`${downShellPath}`, `${model}`]; } else if (isWin32()) { downShellPath = path.join(modelsPath, "download-ggml-model.cmd"); - shell = "cmd.exe /c"; + shell = "cmd"; + args = [`/c`, `${downShellPath}`, `${model}`]; } else { throw Error("platform does not support! "); } @@ -181,9 +146,7 @@ export const downloadModelSync = async (model, source, onProcess) => { console.log("完成模型下载地址替换", model); console.log("正在安装 whisper.cpp 模型"); try { - await runCommand(`${shell}`, [`${downShellPath}`, `${model}`], (data) => - onProcess(data), - ); + await runCommand(`${shell}`, args, (data) => onProcess(data)); } catch (error) { await deleteModel(model); throw error; diff --git a/renderer/components/DownModel.tsx b/renderer/components/DownModel.tsx index 22ae0f8..d41371b 100644 --- a/renderer/components/DownModel.tsx +++ b/renderer/components/DownModel.tsx @@ -1,8 +1,13 @@ -import React, { useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { Loader2 } from "lucide-react"; +import React, { useEffect, FC, PropsWithChildren } from "react"; -const DownModel = ({ modelName, callBack, downSource }) => { +interface IProps extends PropsWithChildren { + modelName: string; + callBack?: () => void; + downSource?: string; +} + +const DownModel: FC = (props) => { + const { modelName, callBack, downSource = "hf-mirror", children } = props; const [loading, setLoading] = React.useState(false); const [progress, setProgress] = React.useState(0); useEffect(() => { @@ -10,31 +15,34 @@ const DownModel = ({ modelName, callBack, downSource }) => { if (model === modelName) { setProgress(progress); setLoading(progress < 100); - if(progress >= 100) { - console.log(progress, 'progress') - callBack && callBack(); + if (progress >= 100) { + callBack && callBack(); } } }); }, []); - const handleDownModel = async () => { + const handleDownModel = async (source = downSource) => { setLoading(true); await window?.ipc?.invoke("downloadModel", { model: modelName, - source: downSource, + source, }); setLoading(false); }; return ( - + + {React.isValidElement<{ + loading?: boolean; + progress?: number; + handleDownModel?: (source?: string) => void; + }>(children) + ? React.cloneElement(children, { + loading, + progress, + handleDownModel, + }) + : children} + ); }; diff --git a/renderer/components/DownModelButton.tsx b/renderer/components/DownModelButton.tsx new file mode 100644 index 0000000..489ef0a --- /dev/null +++ b/renderer/components/DownModelButton.tsx @@ -0,0 +1,29 @@ +import React, { FC } from "react"; +import { Button } from "@/components/ui/button"; +import { Loader2 } from "lucide-react"; + +interface IProps { + loading?: boolean; + progress?: number; + handleDownModel?: () => void; +} + +const DownModelButton: FC = ({ + loading, + progress, + handleDownModel, +}) => { + return ( + + ); +}; + +export default DownModelButton; diff --git a/renderer/components/DownModelDropdown.tsx b/renderer/components/DownModelDropdown.tsx new file mode 100644 index 0000000..1f18b72 --- /dev/null +++ b/renderer/components/DownModelDropdown.tsx @@ -0,0 +1,69 @@ +import React, { FC } from "react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; +import { Loader2 } from "lucide-react"; + +interface IProps { + loading?: boolean; + progress?: number; + handleDownModel?: (source: string) => void; + setShowGuide?: (type: boolean) => void; + installComplete?: boolean; + whisperLoading?: boolean; +} + +const DownModelDropdown: FC = ({ + loading, + progress, + handleDownModel, + setShowGuide, + installComplete, + whisperLoading, +}) => { + return ( + + + + + + 请选择下载源 + + handleDownModel("hf-mirror")} + > + 国内镜像源(较快) + + handleDownModel("huggingface")} + > + huggingface官方源(较慢) + + + setShowGuide(false)} + > + 稍后下载 + + + + ); +}; + +export default DownModelDropdown; diff --git a/renderer/components/DownModelLink.tsx b/renderer/components/DownModelLink.tsx new file mode 100644 index 0000000..c053b23 --- /dev/null +++ b/renderer/components/DownModelLink.tsx @@ -0,0 +1,26 @@ +import React, { FC } from "react"; + +interface IProps { + loading?: boolean; + progress?: number; + handleDownModel?: () => void; +} +const DownModelLink: FC = ({ loading, progress, handleDownModel }) => { + return ( + + 该模型未下载, + {loading ? ( + `正在下载中 ${progress}%...` + ) : ( + handleDownModel()} + > + 立即下载 + + )} + + ); +}; + +export default DownModelLink; diff --git a/renderer/components/Guide.tsx b/renderer/components/Guide.tsx index bc48210..0aa1c38 100644 --- a/renderer/components/Guide.tsx +++ b/renderer/components/Guide.tsx @@ -12,15 +12,20 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { ISystemInfo } from "../types"; +import { gitCloneSteps } from "lib/utils"; +import DownModel from "@/components/DownModel"; +import DownModelDropdown from "@/components/DownModelDropdown"; interface IProps { systemInfo: ISystemInfo; + updateSystemInfo: () => Promise; } -const Guide: FC = ({ systemInfo }) => { +const Guide: FC = ({ systemInfo, updateSystemInfo }) => { const { whisperInstalled, modelsInstalled } = systemInfo; const [showGuide, setShowGuide] = useState(false); useEffect(() => { + console.log(whisperInstalled, showGuide, "whisperInstalled, showGuide"); if (!whisperInstalled && !showGuide) { setShowGuide(true); } @@ -28,7 +33,9 @@ const Guide: FC = ({ systemInfo }) => { const [loading, setLoading] = useState(false); const [installComplete, setInstallComplete] = useState(false); const [model, setModel] = useState("tiny"); - const [downModelLoading, setDownModelLoading] = useState(false); + const [progress, setProgress] = useState(0); + const [step, setStep] = useState(""); + const [beginMakeWhisper, setBeginMakeWhisper] = useState(false); useEffect(() => { window?.ipc?.on("installWhisperComplete", (res) => { if (res) { @@ -38,14 +45,23 @@ const Guide: FC = ({ systemInfo }) => { setLoading(false); } }); + window?.ipc?.on( + "installWhisperProgress", + (step: string, progressRes: number) => { + setStep(step); + setProgress(progressRes); + }, + ); window?.ipc?.on("makeWhisperComplete", (res) => { if (res) { setInstallComplete(true); + setBeginMakeWhisper(false); + updateSystemInfo(); } setLoading(false); }); - window?.ipc?.on("downModelComplete", () => { - setDownModelLoading(false); + window?.ipc?.on("beginMakeWhisper", () => { + setBeginMakeWhisper(true); }); }, []); const handleInstallWhisper = (source: "github" | "gitee") => { @@ -56,12 +72,20 @@ const Guide: FC = ({ systemInfo }) => { setLoading(false); } }; - const handleDownModel = (source: "huggingface" | "hf-mirror") => { - setDownModelLoading(true); - window?.ipc?.send("downModel", { model, source }); + const renderInstallText = () => { + if (installComplete) { + return "已经完成安装"; + } + if (beginMakeWhisper) { + return "正在编译 whisper"; + } + if (loading) { + return `${gitCloneSteps[step] || ""}${(progress * 100).toFixed(0)}%`; + } + return "安装 whisper"; }; return ( - +

准备, 开始!

@@ -74,8 +98,10 @@ const Guide: FC = ({ systemInfo }) => { className="" disabled={loading || installComplete} > - {loading && } - {installComplete ? "已经完成安装" : "安装 whisper"} + {(loading || beginMakeWhisper) && ( + + )} + {renderInstallText()} @@ -103,43 +129,13 @@ const Guide: FC = ({ systemInfo }) => { modelsInstalled={modelsInstalled} defaultValue={model} /> - - - - - - 请选择下载源 - - handleDownModel("hf-mirror")} - > - 国内镜像源(较快) - - handleDownModel("huggingface")} - > - huggingface官方源(较慢) - - - setShowGuide(false)} - > - 稍后下载 - - - + + +

完成以上两步

diff --git a/renderer/lib/utils.ts b/renderer/lib/utils.ts index 6c3f31d..09cd3bd 100644 --- a/renderer/lib/utils.ts +++ b/renderer/lib/utils.ts @@ -107,3 +107,10 @@ export const defaultUserConfig = { export const openUrl = (url) => { window?.ipc?.send('openUrl', url) } + +export const gitCloneSteps = { + 'Compressing objects': '打包文件', + 'Receiving objects': '下载文件', + 'Resolving deltas': '解压文件', + 'Updating workdir': '更新文件', +} diff --git a/renderer/pages/home.tsx b/renderer/pages/home.tsx index bbf7ac4..958002a 100644 --- a/renderer/pages/home.tsx +++ b/renderer/pages/home.tsx @@ -33,11 +33,13 @@ import { FormItem, FormLabel, } from "@/components/ui/form"; -import { supportedLanguage, defaultUserConfig, openUrl } from "lib/utils"; +import { supportedLanguage, defaultUserConfig } from "lib/utils"; import store from "lib/store"; import SavePathNotice from "@/components/SavePathNotice"; import { ISystemInfo, IFiles } from "../types"; import TaskStatus from "@/components/TaskStatus"; +import DownModel from "@/components/DownModel"; +import DownModelLink from "@/components/DownModelLink"; export default function Component() { const [files, setFiles] = React.useState([]); @@ -45,11 +47,9 @@ export default function Component() { whisperInstalled: true, modelsInstalled: [], }); - const [downModelLoading, setDownModelLoading] = React.useState(false); const beginWatch = useRef(false); const filesRef = useRef(files); const [taskLoading, setTaskLoading] = React.useState(false); - const [progress, setProgress] = React.useState(0); const form = useForm({ defaultValues: defaultUserConfig, }); @@ -100,9 +100,6 @@ export default function Component() { setFiles(finalFiles); }, ); - window?.ipc?.on('downloadProgress', (model, progress) => { - setProgress(+(progress || 0)); - }) const storeUserConfig: Object = store.getItem("userConfig") || defaultUserConfig; console.log(storeUserConfig, "storeUserConfig"); @@ -143,14 +140,6 @@ export default function Component() { form.setValue(`${value as "baidu" | "volc"}.apiKey`, apiKey); form.setValue(`${value as "baidu" | "volc"}.apiSecret`, apiSecret); }; - const handleInstallModel = async () => { - setDownModelLoading(true); - await window?.ipc?.invoke("downloadModel", { - model: formData.model, - }); - setDownModelLoading(false); - updateSystemInfo(); - }; const isInstalledModel = systemInfo?.modelsInstalled?.includes( formData.model?.toLowerCase(), ); @@ -183,17 +172,12 @@ export default function Component() { {!isInstalledModel && ( - 该模型未下载, - {downModelLoading ? ( - `正在下载中 ${progress}%...` - ) : ( - - 立即下载 - - )} + + + )} @@ -493,7 +477,7 @@ export default function Component() { 开始操作
- + ); } diff --git a/renderer/pages/modelsControl.tsx b/renderer/pages/modelsControl.tsx index f762dc7..5f9fe09 100644 --- a/renderer/pages/modelsControl.tsx +++ b/renderer/pages/modelsControl.tsx @@ -26,6 +26,7 @@ import { Button } from "@/components/ui/button"; import { ISystemInfo } from "../types"; import DeleteModel from "@/components/DeleteModel"; import DownModel from "@/components/DownModel"; +import DownModelButton from "@/components/DownModelButton"; const ModelsControl = () => { const [systemInfo, setSystemInfo] = React.useState({ @@ -53,8 +54,9 @@ const ModelsControl = () => { 您可以在这里管理你的模型,下载,删除
模型保存位置: {systemInfo?.modelsPath} - - @@ -96,7 +98,9 @@ const ModelsControl = () => { modelName={model.name} callBack={updateSystemInfo} downSource={downSource} - /> + > + + )}