mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
improve handling of incomplete model downloads on app quit or eject, more helpful hf searching for image gen models
This commit is contained in:
parent
763c4ab6c8
commit
4eec7c7670
4 changed files with 76 additions and 10 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "gerbil",
|
||||
"productName": "Gerbil",
|
||||
"version": "2.0.0",
|
||||
"version": "1.10.0",
|
||||
"description": "Run Large Language Models locally",
|
||||
"main": "out/main/index.js",
|
||||
"homepage": "./",
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ export const ImageGenerationTab = () => {
|
|||
tooltip="T5-XXL text encoder model for advanced text understanding."
|
||||
onChange={handleSdt5xxlChange}
|
||||
onSelectFile={handleSelectSdt5xxlFile}
|
||||
searchUrl="https://huggingface.co/models?search=t5-xxl&library=gguf&sort=trending"
|
||||
paramType="sdt5xxl"
|
||||
/>
|
||||
|
||||
|
|
@ -97,6 +98,7 @@ export const ImageGenerationTab = () => {
|
|||
tooltip="CLIP-L text encoder model for text-image understanding."
|
||||
onChange={handleSdcliplChange}
|
||||
onSelectFile={handleSelectSdcliplFile}
|
||||
searchUrl="https://huggingface.co/models?search=clip-l&library=gguf&sort=trending"
|
||||
paramType="sdclipl"
|
||||
/>
|
||||
|
||||
|
|
@ -107,6 +109,7 @@ export const ImageGenerationTab = () => {
|
|||
tooltip="CLIP-G text encoder model for enhanced text-image understanding."
|
||||
onChange={handleSdclipgChange}
|
||||
onSelectFile={handleSelectSdclipgFile}
|
||||
searchUrl="https://huggingface.co/models?search=clip-g&library=gguf&sort=trending"
|
||||
paramType="sdclipg"
|
||||
/>
|
||||
|
||||
|
|
@ -117,6 +120,7 @@ export const ImageGenerationTab = () => {
|
|||
tooltip="PhotoMaker is a model that allows face cloning. Select a .safetensors PhotoMaker file to be loaded (SDXL only)."
|
||||
onChange={handleSdphotomakerChange}
|
||||
onSelectFile={handleSelectSdphotomakerFile}
|
||||
searchUrl="https://huggingface.co/models?search=photomaker&library=safetensors&sort=trending"
|
||||
paramType="sdphotomaker"
|
||||
/>
|
||||
|
||||
|
|
@ -127,6 +131,7 @@ export const ImageGenerationTab = () => {
|
|||
tooltip="Variational Autoencoder model for improved image quality."
|
||||
onChange={handleSdvaeChange}
|
||||
onSelectFile={handleSelectSdvaeFile}
|
||||
searchUrl="https://huggingface.co/models?search=vae&library=safetensors&sort=trending"
|
||||
paramType="sdvae"
|
||||
/>
|
||||
|
||||
|
|
@ -137,6 +142,7 @@ export const ImageGenerationTab = () => {
|
|||
tooltip="LoRa (Low-Rank Adaptation) file for customizing image generation. Select a .safetensors or .gguf LoRa file to be loaded. Should be unquantized."
|
||||
onChange={handleSdloraChange}
|
||||
onSelectFile={handleSelectSdloraFile}
|
||||
searchUrl="https://huggingface.co/models?search=lora&library=safetensors&sort=trending"
|
||||
paramType="sdlora"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { startFrontend as startSillyTavernFrontend } from '@/main/modules/sillyt
|
|||
import { startFrontend as startOpenWebUIFrontend } from '@/main/modules/openwebui';
|
||||
import { patchKliteEmbd, patchKcppSduiEmbd, filterSpam } from './patches';
|
||||
import { startProxy, stopProxy } from '../proxy';
|
||||
import { resolveModelPath } from '../model-download';
|
||||
import { resolveModelPath, abortActiveDownloads } from '../model-download';
|
||||
import type {
|
||||
FrontendPreference,
|
||||
ImageGenerationFrontendPreference,
|
||||
|
|
@ -55,6 +55,11 @@ async function resolveModelPaths(args: string[]) {
|
|||
resolvedArgs.push(resolvedPath);
|
||||
i++;
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
if (errorMessage.includes('aborted')) {
|
||||
throw error;
|
||||
}
|
||||
logError(`Failed to resolve model path for ${arg}:`, error as Error);
|
||||
resolvedArgs.push(urlOrPath);
|
||||
i++;
|
||||
|
|
@ -207,6 +212,7 @@ export async function launchKoboldCpp(
|
|||
}
|
||||
|
||||
export async function stopKoboldCpp() {
|
||||
abortActiveDownloads();
|
||||
await stopProxy();
|
||||
return terminateProcess(koboldProcess);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { join, basename, dirname } from 'path';
|
||||
import { mkdir, readdir, stat } from 'fs/promises';
|
||||
import { mkdir, readdir, stat, rename, unlink } from 'fs/promises';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { get as httpGet } from 'http';
|
||||
import { get as httpsGet } from 'https';
|
||||
|
|
@ -8,6 +8,12 @@ import { pathExists } from '@/utils/node/fs';
|
|||
import { logError } from '@/utils/node/logging';
|
||||
import { sendToRenderer } from '@/main/modules/window';
|
||||
import type { ModelParamType, CachedModel } from '@/types';
|
||||
import type { IncomingMessage } from 'http';
|
||||
|
||||
const activeDownloads = new Set<{
|
||||
abort: () => void;
|
||||
tempPath: string;
|
||||
}>();
|
||||
|
||||
interface DownloadProgress {
|
||||
type: 'progress' | 'complete' | 'error';
|
||||
|
|
@ -61,19 +67,45 @@ async function downloadFile(
|
|||
) {
|
||||
const normalizedUrl = normalizeUrl(url);
|
||||
const outputDir = dirname(outputPath);
|
||||
const tempPath = `${outputPath}.tmp`;
|
||||
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
const httpModule = normalizedUrl.startsWith('https') ? httpsGet : httpGet;
|
||||
let currentRequest: IncomingMessage | null = null;
|
||||
let fileStream: ReturnType<typeof createWriteStream>;
|
||||
let isAborted = false;
|
||||
|
||||
const cleanup = async () => {
|
||||
fileStream?.close();
|
||||
await unlink(tempPath).catch(() => void 0);
|
||||
};
|
||||
|
||||
const abortController = {
|
||||
abort: () => {
|
||||
isAborted = true;
|
||||
currentRequest?.destroy();
|
||||
cleanup();
|
||||
reject(new Error('Download aborted by user'));
|
||||
},
|
||||
tempPath,
|
||||
};
|
||||
|
||||
activeDownloads.add(abortController);
|
||||
|
||||
const handleRedirect = (requestUrl: string, redirectCount = 0): void => {
|
||||
if (isAborted) return;
|
||||
|
||||
if (redirectCount > 10) {
|
||||
activeDownloads.delete(abortController);
|
||||
reject(new Error('Too many redirects'));
|
||||
return;
|
||||
}
|
||||
|
||||
httpModule(requestUrl, (response) => {
|
||||
if (isAborted) return;
|
||||
currentRequest = response;
|
||||
if (
|
||||
response.statusCode === 301 ||
|
||||
response.statusCode === 302 ||
|
||||
|
|
@ -90,6 +122,7 @@ async function downloadFile(
|
|||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
activeDownloads.delete(abortController);
|
||||
reject(
|
||||
new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`)
|
||||
);
|
||||
|
|
@ -101,7 +134,7 @@ async function downloadFile(
|
|||
let lastReportTime = Date.now();
|
||||
let lastReportedBytes = 0;
|
||||
|
||||
const fileStream = createWriteStream(outputPath);
|
||||
fileStream = createWriteStream(tempPath);
|
||||
|
||||
response.on('data', (chunk: Buffer) => {
|
||||
downloadedBytes += chunk.length;
|
||||
|
|
@ -152,22 +185,37 @@ async function downloadFile(
|
|||
});
|
||||
|
||||
response.on('end', () => {
|
||||
if (isAborted) return;
|
||||
fileStream.end();
|
||||
fileStream.on('finish', () => {
|
||||
fileStream.on('finish', async () => {
|
||||
try {
|
||||
await rename(tempPath, outputPath);
|
||||
sendToRenderer('kobold-output', '\n');
|
||||
activeDownloads.delete(abortController);
|
||||
resolve(true);
|
||||
} catch (err) {
|
||||
activeDownloads.delete(abortController);
|
||||
reject(err instanceof Error ? err : new Error(String(err)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
response.on('error', (err) => {
|
||||
fileStream.close();
|
||||
response.on('error', async (err) => {
|
||||
if (isAborted) return;
|
||||
activeDownloads.delete(abortController);
|
||||
await cleanup();
|
||||
reject(err);
|
||||
});
|
||||
|
||||
fileStream.on('error', (err) => {
|
||||
fileStream.on('error', async (err) => {
|
||||
if (isAborted) return;
|
||||
activeDownloads.delete(abortController);
|
||||
await cleanup();
|
||||
reject(err);
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
if (isAborted) return;
|
||||
activeDownloads.delete(abortController);
|
||||
reject(err);
|
||||
});
|
||||
};
|
||||
|
|
@ -289,3 +337,9 @@ export async function getLocalModelsForType(paramType: ModelParamType) {
|
|||
|
||||
return models;
|
||||
}
|
||||
|
||||
export function abortActiveDownloads() {
|
||||
const downloads = Array.from(activeDownloads);
|
||||
downloads.forEach((controller) => controller.abort());
|
||||
activeDownloads.clear();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue