diff --git a/.gitignore b/.gitignore index b432207..f2db7ca 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ __pycache__ /cache.json /frontend/stats.html /frontend/package.json.md5 +/backend-python/get-pip.py +/py310 +*.zip \ No newline at end of file diff --git a/backend-golang/app.go b/backend-golang/app.go index 4047019..69d2323 100644 --- a/backend-golang/app.go +++ b/backend-golang/app.go @@ -5,9 +5,10 @@ import ( "net/http" "os" "os/exec" + "runtime" "github.com/minio/selfupdate" - "github.com/wailsapp/wails/v2/pkg/runtime" + wruntime "github.com/wailsapp/wails/v2/pkg/runtime" ) // App struct @@ -46,6 +47,10 @@ func (a *App) UpdateApp(url string) (broken bool, err error) { return false, err } exec.Command(name, os.Args[1:]...).Start() - runtime.Quit(a.ctx) + wruntime.Quit(a.ctx) return false, nil } + +func (a *App) GetPlatform() string { + return runtime.GOOS +} diff --git a/backend-golang/rwkv.go b/backend-golang/rwkv.go index 5b19014..ddc020e 100644 --- a/backend-golang/rwkv.go +++ b/backend-golang/rwkv.go @@ -1,28 +1,52 @@ package backend_golang import ( + "errors" "os/exec" - "path/filepath" "strconv" ) -func cmd(args ...string) (string, error) { - cmdHelper, err := filepath.Abs("./cmd-helper") - if err != nil { - return "", err - } - cmd := exec.Command(cmdHelper, args...) - out, err := cmd.CombinedOutput() - if err != nil { - return "", err - } - return string(out), nil -} - func (a *App) StartServer(port int) (string, error) { - return cmd("python", "./backend-python/main.py", strconv.Itoa(port)) + python, err := GetPython() + if err != nil { + return "", err + } + return Cmd(python, "./backend-python/main.py", strconv.Itoa(port)) } func (a *App) ConvertModel(modelPath string, strategy string, outPath string) (string, error) { - return cmd("python", "./backend-python/convert_model.py", "--in", modelPath, "--out", outPath, "--strategy", strategy) + python, err := GetPython() + if err != nil { + return "", err + } + return Cmd(python, "./backend-python/convert_model.py", "--in", modelPath, "--out", outPath, "--strategy", strategy) +} + +func (a *App) DepCheck() error { + python, err := GetPython() + if err != nil { + return err + } + out, err := exec.Command(python, "./backend-python/dep_check.py").CombinedOutput() + if err != nil { + return errors.New("DepCheck Error: " + string(out)) + } + return nil +} + +func (a *App) InstallPyDep() (string, error) { + python, err := GetPython() + if err != nil { + return "", err + } + _, err = Cmd(python, "./backend-python/get-pip.py") + if err != nil { + return "", err + } + ChangeFileLine("./py310/python310._pth", 3, "Lib\\site-packages") + _, err = Cmd(python, "-m", "pip", "install", "torch", "torchvision", "torchaudio", "--index-url", "https://download.pytorch.org/whl/cu117") + if err != nil { + return "", err + } + return Cmd(python, "-m", "pip", "install", "-r", "./backend-python/requirements_versions.txt") } diff --git a/backend-golang/utils.go b/backend-golang/utils.go new file mode 100644 index 0000000..09419bd --- /dev/null +++ b/backend-golang/utils.go @@ -0,0 +1,130 @@ +package backend_golang + +import ( + "archive/zip" + "bufio" + "errors" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +func Cmd(args ...string) (string, error) { + cmdHelper, err := filepath.Abs("./cmd-helper") + if err != nil { + return "", err + } + cmd := exec.Command(cmdHelper, args...) + out, err := cmd.CombinedOutput() + if err != nil { + return "", err + } + return string(out), nil +} + +func GetPython() (string, error) { + switch platform := runtime.GOOS; platform { + case "windows": + _, err := os.Stat("py310/python.exe") + if err != nil { + _, err := os.Stat("python-3.10.11-embed-amd64.zip") + if err != nil { + return "", errors.New("python zip not found") + } else { + err := Unzip("python-3.10.11-embed-amd64.zip", "py310") + if err != nil { + return "", errors.New("failed to unzip python") + } else { + return "py310/python.exe", nil + } + } + } else { + return "py310/python.exe", nil + } + case "darwin": + return "python3", nil + case "linux": + return "python3", nil + } + return "", errors.New("unsupported OS") +} + +func ChangeFileLine(filePath string, lineNumber int, newText string) error { + file, err := os.OpenFile(filePath, os.O_RDWR, 0644) + if err != nil { + return err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + content := make([]string, 0) + for scanner.Scan() { + content = append(content, scanner.Text()) + } + + content[lineNumber-1] = newText + + newContent := strings.Join(content, "\n") + + err = file.Truncate(0) + if err != nil { + return err + } + _, err = file.Seek(0, io.SeekStart) + if err != nil { + return err + } + _, err = file.WriteString(newContent) + if err != nil { + return err + } + return nil +} + +// https://gist.github.com/paulerickson/6d8650947ee4e3f3dbcc28fde10eaae7 +func Unzip(source, destination string) error { + archive, err := zip.OpenReader(source) + if err != nil { + return err + } + defer archive.Close() + for _, file := range archive.Reader.File { + reader, err := file.Open() + if err != nil { + return err + } + defer reader.Close() + path := filepath.Join(destination, file.Name) + // Remove file if it already exists; no problem if it doesn't; other cases can error out below + _ = os.Remove(path) + // Create a directory at path, including parents + err = os.MkdirAll(path, os.ModePerm) + if err != nil { + return err + } + // If file is _supposed_ to be a directory, we're done + if file.FileInfo().IsDir() { + continue + } + // otherwise, remove that directory (_not_ including parents) + err = os.Remove(path) + if err != nil { + return err + } + // and create the actual file. This ensures that the parent directories exist! + // An archive may have a single file with a nested path, rather than a file for each parent dir + writer, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode()) + if err != nil { + return err + } + defer writer.Close() + _, err = io.Copy(writer, reader) + if err != nil { + return err + } + } + return nil +} diff --git a/backend-python/main.py b/backend-python/main.py index 1092a7c..a43cfd6 100644 --- a/backend-python/main.py +++ b/backend-python/main.py @@ -1,7 +1,9 @@ import os -import psutil import sys +sys.path.append(os.path.dirname(os.path.realpath(__file__))) + +import psutil from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware import uvicorn diff --git a/frontend/src/_locales/zh-hans/main.json b/frontend/src/_locales/zh-hans/main.json index f7bc167..82051df 100644 --- a/frontend/src/_locales/zh-hans/main.json +++ b/frontend/src/_locales/zh-hans/main.json @@ -90,5 +90,8 @@ "Continue": "继续", "Check": "查看", "Model file not found": "模型文件不存在", - "Can not find download url": "找不到下载地址" + "Can not find download url": "找不到下载地址", + "Python target not found, would you like to download it?": "没有找到目标Python, 是否下载?", + "Python dependencies are incomplete, would you like to install them?": "Python依赖缺失, 是否安装?", + "Install": "安装" } \ No newline at end of file diff --git a/frontend/src/components/RunButton.tsx b/frontend/src/components/RunButton.tsx index 539e3c1..d8e6a70 100644 --- a/frontend/src/components/RunButton.tsx +++ b/frontend/src/components/RunButton.tsx @@ -1,12 +1,12 @@ import React, {FC, MouseEventHandler, ReactElement} from 'react'; import commonStore, {ModelStatus} from '../stores/commonStore'; -import {AddToDownloadList, FileExists, StartServer} from '../../wailsjs/go/backend_golang/App'; +import {AddToDownloadList, DepCheck, FileExists, InstallPyDep, StartServer} from '../../wailsjs/go/backend_golang/App'; import {Button} from '@fluentui/react-components'; import {observer} from 'mobx-react-lite'; import {exit, readRoot, switchModel, updateConfig} from '../apis'; import {toast} from 'react-toastify'; import manifest from '../../../manifest.json'; -import {getStrategy, toastWithButton} from '../utils'; +import {getStrategy, saveCache, toastWithButton} from '../utils'; import {useTranslation} from 'react-i18next'; import {ToolTipButton} from './ToolTipButton'; import {Play16Regular, Stop16Regular} from '@fluentui/react-icons'; @@ -39,6 +39,32 @@ export const RunButton: FC<{ onClickRun?: MouseEventHandler, iconMode?: boolean const modelName = modelConfig.modelParameters.modelName; const modelPath = `./${manifest.localModelDir}/${modelName}`; + if (!commonStore.depComplete) { + let depErrorMsg = ''; + await DepCheck().catch((e) => { + depErrorMsg = e.message || e; + if (depErrorMsg === 'python zip not found') { + toastWithButton(t('Python target not found, would you like to download it?'), t('Download'), () => { + toastWithButton(`${t('Downloading')} Python`, t('Check'), () => { + navigate({pathname: '/downloads'}); + }, {autoClose: 3000}); + AddToDownloadList('python-3.10.11-embed-amd64.zip', 'https://www.python.org/ftp/python/3.10.11/python-3.10.11-embed-amd64.zip'); + }); + } else if (depErrorMsg.includes('DepCheck Error')) { + toastWithButton(t('Python dependencies are incomplete, would you like to install them?'), t('Install'), () => { + InstallPyDep(); + }); + } else { + toast(depErrorMsg, {type: 'error'}); + } + }); + if (depErrorMsg) { + return; + } + commonStore.setDepComplete(true); + saveCache(); + } + if (!await FileExists(modelPath)) { toastWithButton(t('Model file not found'), t('Download'), () => { const downloadUrl = commonStore.modelSourceList.find(item => item.name === modelName)?.downloadUrl; diff --git a/frontend/src/pages/Configs.tsx b/frontend/src/pages/Configs.tsx index 0cdbc90..7d2cd8d 100644 --- a/frontend/src/pages/Configs.tsx +++ b/frontend/src/pages/Configs.tsx @@ -256,7 +256,7 @@ export const Configs: FC = observer(() => { toast(`${t('Convert Success')} - ${newModelPath}`, {type: 'success'}); refreshLocalModels({models: commonStore.modelSourceList}, false); }).catch(e => { - toast(`${t('Convert Failed')} - ${e}`, {type: 'error'}); + toast(`${t('Convert Failed')} - ${e.message || e}`, {type: 'error'}); }); } else { toast(`${t('Model Not Found')} - ${modelPath}`, {type: 'error'}); diff --git a/frontend/src/pages/Downloads.tsx b/frontend/src/pages/Downloads.tsx index 563b1e0..942b1a4 100644 --- a/frontend/src/pages/Downloads.tsx +++ b/frontend/src/pages/Downloads.tsx @@ -4,7 +4,7 @@ import {Page} from '../components/Page'; import {observer} from 'mobx-react-lite'; import commonStore from '../stores/commonStore'; import {Divider, Field, ProgressBar} from '@fluentui/react-components'; -import {bytesToGb, bytesToMb, refreshLocalModels} from '../utils'; +import {bytesToGb, bytesToKb, bytesToMb, refreshLocalModels} from '../utils'; import {ToolTipButton} from '../components/ToolTipButton'; import {Folder20Regular, Pause20Regular, Play20Regular} from '@fluentui/react-icons'; import {ContinueDownload, OpenFileFolder, PauseDownload} from '../../wailsjs/go/backend_golang/App'; @@ -23,21 +23,31 @@ export type DownloadStatus = { export const Downloads: FC = observer(() => { const {t} = useTranslation(); - const finishedDownloads = commonStore.downloadList.filter((status) => status.done).length; + const finishedModelsLen = commonStore.downloadList.filter((status) => status.done && status.name.endsWith('.pth')).length; useEffect(() => { - if (finishedDownloads > 0) + if (finishedModelsLen > 0) refreshLocalModels({models: commonStore.modelSourceList}, false); - console.log('finishedDownloads:', finishedDownloads); - }, [finishedDownloads]); + console.log('finishedModelsLen:', finishedModelsLen); + }, [finishedModelsLen]); return ( - {commonStore.downloadList.map((status, index) => ( -
+ {commonStore.downloadList.slice().reverse().map((status, index) => { + const downloadProgress = `${status.progress.toFixed(2)}%`; + const downloadSpeed = `${status.downloading ? bytesToMb(status.speed) : '0'}MB/s`; + let downloadDetails: string; + if (status.size < 1024 * 1024) + downloadDetails = `${bytesToKb(status.transferred) + 'KB'}/${bytesToKb(status.size) + 'KB'}`; + else if (status.size < 1024 * 1024 * 1024) + downloadDetails = `${bytesToMb(status.transferred) + 'MB'}/${bytesToMb(status.size) + 'MB'}`; + else + downloadDetails = `${bytesToGb(status.transferred) + 'GB'}/${bytesToGb(status.size) + 'GB'}`; + + return
@@ -57,8 +67,8 @@ export const Downloads: FC = observer(() => {
-
- )) +
; + }) } }/> diff --git a/frontend/src/startup.ts b/frontend/src/startup.ts index e1bbacb..fed9d58 100644 --- a/frontend/src/startup.ts +++ b/frontend/src/startup.ts @@ -1,29 +1,28 @@ import commonStore from './stores/commonStore'; import {ReadJson} from '../wailsjs/go/backend_golang/App'; -import {Cache, checkUpdate, downloadProgramFiles, LocalConfig, refreshModels} from './utils'; +import {Cache, checkUpdate, downloadProgramFiles, LocalConfig, refreshModels, saveCache} from './utils'; import {getStatus} from './apis'; import {EventsOn} from '../wailsjs/runtime'; import {defaultModelConfigs} from './pages/Configs'; export async function startup() { downloadProgramFiles(); - - initRemoteText(); - initCache(); - await initConfig(); - - if (commonStore.settings.autoUpdatesCheck) - checkUpdate(); - - getStatus(500).then(status => { - if (status) - commonStore.setModelStatus(status); - }); - EventsOn('downloadList', (data) => { if (data) commonStore.setDownloadList(data); }); + + initCache().then(initRemoteText); + + await initConfig(); + + if (commonStore.settings.autoUpdatesCheck) // depends on config settings + checkUpdate(); + + getStatus(500).then(status => { // depends on config api port + if (status) + commonStore.setModelStatus(status); + }); } async function initRemoteText() { @@ -33,7 +32,7 @@ async function initRemoteText() { commonStore.setIntroduction(data.introduction); if (data.about) commonStore.setAbout(data.about); - }); + }).then(saveCache); } async function initConfig() { @@ -61,6 +60,8 @@ async function initCache() { commonStore.setIntroduction(cacheData.introduction); if (cacheData.about) commonStore.setAbout(cacheData.about); + if (cacheData.depComplete) + commonStore.setDepComplete(cacheData.depComplete); }).catch(() => { }); await refreshModels(false); diff --git a/frontend/src/stores/commonStore.ts b/frontend/src/stores/commonStore.ts index a8bd348..b97403a 100644 --- a/frontend/src/stores/commonStore.ts +++ b/frontend/src/stores/commonStore.ts @@ -24,6 +24,7 @@ class CommonStore { // global modelStatus: ModelStatus = ModelStatus.Offline; + depComplete: boolean = false; // home introduction: IntroductionContent = manifest.introduction; @@ -130,6 +131,10 @@ class CommonStore { this.about = value; }; + setDepComplete = (value: boolean) => { + this.depComplete = value; + }; + setDownloadList = (value: DownloadStatus[]) => { this.downloadList = value; }; diff --git a/frontend/src/utils/index.tsx b/frontend/src/utils/index.tsx index 1a54210..7e9b8f7 100644 --- a/frontend/src/utils/index.tsx +++ b/frontend/src/utils/index.tsx @@ -1,4 +1,5 @@ import { + AddToDownloadList, DeleteFile, DownloadFile, FileExists, @@ -23,6 +24,7 @@ export type Cache = { models: ModelSourceItem[] introduction: IntroductionContent, about: AboutContent + depComplete: boolean } export type LocalConfig = { @@ -153,7 +155,8 @@ export const saveCache = async () => { const data: Cache = { models: commonStore.modelSourceList, introduction: commonStore.introduction, - about: commonStore.about + about: commonStore.about, + depComplete: commonStore.depComplete }; return SaveJson('cache.json', data); }; @@ -175,7 +178,7 @@ export function downloadProgramFiles() { manifest.programFiles.forEach(({url, path}) => { FileExists(path).then(exists => { if (!exists) - DownloadFile(path, url); + AddToDownloadList(path, url); }); }); } @@ -188,7 +191,7 @@ export function forceDownloadProgramFiles() { export function deletePythonProgramFiles() { manifest.programFiles.forEach(({path}) => { - if (path.endsWith('.py')) + if (path.endsWith('.py') && !path.includes('get-pip.py')) DeleteFile(path); }); } @@ -201,6 +204,10 @@ export function bytesToMb(size: number) { return (size / 1024 / 1024).toFixed(2); } +export function bytesToKb(size: number) { + return (size / 1024).toFixed(2); +} + export async function checkUpdate() { let updateUrl = ''; await fetch('https://api.github.com/repos/josstorer/RWKV-Runner/releases/latest').then((r) => { @@ -214,7 +221,7 @@ export async function checkUpdate() { deletePythonProgramFiles(); setTimeout(() => { UpdateApp(updateUrl).catch((e) => { - toast(t('Update Error, Please restart this program') + ' - ' + e.message, { + toast(t('Update Error, Please restart this program') + ' - ' + e.message || e, { type: 'error', position: 'bottom-left', autoClose: false @@ -235,7 +242,7 @@ export async function checkUpdate() { } } ).catch((e) => { - toast(t('Updates Check Error') + ' - ' + e.message, {type: 'error', position: 'bottom-left'}); + toast(t('Updates Check Error') + ' - ' + e.message || e, {type: 'error', position: 'bottom-left'}); }); return updateUrl; } diff --git a/frontend/wailsjs/go/backend_golang/App.d.ts b/frontend/wailsjs/go/backend_golang/App.d.ts index c844e11..63a99ea 100644 --- a/frontend/wailsjs/go/backend_golang/App.d.ts +++ b/frontend/wailsjs/go/backend_golang/App.d.ts @@ -10,10 +10,16 @@ export function ConvertModel(arg1:string,arg2:string,arg3:string):Promise; +export function DepCheck():Promise; + export function DownloadFile(arg1:string,arg2:string):Promise; export function FileExists(arg1:string):Promise; +export function GetPlatform():Promise; + +export function InstallPyDep():Promise; + export function ListDirFiles(arg1:string):Promise>; export function OpenFileFolder(arg1:string):Promise; diff --git a/frontend/wailsjs/go/backend_golang/App.js b/frontend/wailsjs/go/backend_golang/App.js index 3fa22f1..93cac10 100644 --- a/frontend/wailsjs/go/backend_golang/App.js +++ b/frontend/wailsjs/go/backend_golang/App.js @@ -18,6 +18,10 @@ export function DeleteFile(arg1) { return window['go']['backend_golang']['App']['DeleteFile'](arg1); } +export function DepCheck() { + return window['go']['backend_golang']['App']['DepCheck'](); +} + export function DownloadFile(arg1, arg2) { return window['go']['backend_golang']['App']['DownloadFile'](arg1, arg2); } @@ -26,6 +30,14 @@ export function FileExists(arg1) { return window['go']['backend_golang']['App']['FileExists'](arg1); } +export function GetPlatform() { + return window['go']['backend_golang']['App']['GetPlatform'](); +} + +export function InstallPyDep() { + return window['go']['backend_golang']['App']['InstallPyDep'](); +} + export function ListDirFiles(arg1) { return window['go']['backend_golang']['App']['ListDirFiles'](arg1); } diff --git a/manifest.json b/manifest.json index f72d6ad..d591864 100644 --- a/manifest.json +++ b/manifest.json @@ -61,6 +61,10 @@ { "url": "https://cdn.jsdelivr.net/gh/josstorer/RWKV-Runner/backend-python/20B_tokenizer.json", "path": "backend-python/20B_tokenizer.json" + }, + { + "url": "https://raw.githubusercontent.com/pypa/get-pip/main/public/get-pip.py", + "path": "backend-python/get-pip.py" } ], "models": [