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",
|
"name": "gerbil",
|
||||||
"productName": "Gerbil",
|
"productName": "Gerbil",
|
||||||
"version": "2.0.0",
|
"version": "1.10.0",
|
||||||
"description": "Run Large Language Models locally",
|
"description": "Run Large Language Models locally",
|
||||||
"main": "out/main/index.js",
|
"main": "out/main/index.js",
|
||||||
"homepage": "./",
|
"homepage": "./",
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ export const ImageGenerationTab = () => {
|
||||||
tooltip="T5-XXL text encoder model for advanced text understanding."
|
tooltip="T5-XXL text encoder model for advanced text understanding."
|
||||||
onChange={handleSdt5xxlChange}
|
onChange={handleSdt5xxlChange}
|
||||||
onSelectFile={handleSelectSdt5xxlFile}
|
onSelectFile={handleSelectSdt5xxlFile}
|
||||||
|
searchUrl="https://huggingface.co/models?search=t5-xxl&library=gguf&sort=trending"
|
||||||
paramType="sdt5xxl"
|
paramType="sdt5xxl"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -97,6 +98,7 @@ export const ImageGenerationTab = () => {
|
||||||
tooltip="CLIP-L text encoder model for text-image understanding."
|
tooltip="CLIP-L text encoder model for text-image understanding."
|
||||||
onChange={handleSdcliplChange}
|
onChange={handleSdcliplChange}
|
||||||
onSelectFile={handleSelectSdcliplFile}
|
onSelectFile={handleSelectSdcliplFile}
|
||||||
|
searchUrl="https://huggingface.co/models?search=clip-l&library=gguf&sort=trending"
|
||||||
paramType="sdclipl"
|
paramType="sdclipl"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -107,6 +109,7 @@ export const ImageGenerationTab = () => {
|
||||||
tooltip="CLIP-G text encoder model for enhanced text-image understanding."
|
tooltip="CLIP-G text encoder model for enhanced text-image understanding."
|
||||||
onChange={handleSdclipgChange}
|
onChange={handleSdclipgChange}
|
||||||
onSelectFile={handleSelectSdclipgFile}
|
onSelectFile={handleSelectSdclipgFile}
|
||||||
|
searchUrl="https://huggingface.co/models?search=clip-g&library=gguf&sort=trending"
|
||||||
paramType="sdclipg"
|
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)."
|
tooltip="PhotoMaker is a model that allows face cloning. Select a .safetensors PhotoMaker file to be loaded (SDXL only)."
|
||||||
onChange={handleSdphotomakerChange}
|
onChange={handleSdphotomakerChange}
|
||||||
onSelectFile={handleSelectSdphotomakerFile}
|
onSelectFile={handleSelectSdphotomakerFile}
|
||||||
|
searchUrl="https://huggingface.co/models?search=photomaker&library=safetensors&sort=trending"
|
||||||
paramType="sdphotomaker"
|
paramType="sdphotomaker"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -127,6 +131,7 @@ export const ImageGenerationTab = () => {
|
||||||
tooltip="Variational Autoencoder model for improved image quality."
|
tooltip="Variational Autoencoder model for improved image quality."
|
||||||
onChange={handleSdvaeChange}
|
onChange={handleSdvaeChange}
|
||||||
onSelectFile={handleSelectSdvaeFile}
|
onSelectFile={handleSelectSdvaeFile}
|
||||||
|
searchUrl="https://huggingface.co/models?search=vae&library=safetensors&sort=trending"
|
||||||
paramType="sdvae"
|
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."
|
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}
|
onChange={handleSdloraChange}
|
||||||
onSelectFile={handleSelectSdloraFile}
|
onSelectFile={handleSelectSdloraFile}
|
||||||
|
searchUrl="https://huggingface.co/models?search=lora&library=safetensors&sort=trending"
|
||||||
paramType="sdlora"
|
paramType="sdlora"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import { startFrontend as startSillyTavernFrontend } from '@/main/modules/sillyt
|
||||||
import { startFrontend as startOpenWebUIFrontend } from '@/main/modules/openwebui';
|
import { startFrontend as startOpenWebUIFrontend } from '@/main/modules/openwebui';
|
||||||
import { patchKliteEmbd, patchKcppSduiEmbd, filterSpam } from './patches';
|
import { patchKliteEmbd, patchKcppSduiEmbd, filterSpam } from './patches';
|
||||||
import { startProxy, stopProxy } from '../proxy';
|
import { startProxy, stopProxy } from '../proxy';
|
||||||
import { resolveModelPath } from '../model-download';
|
import { resolveModelPath, abortActiveDownloads } from '../model-download';
|
||||||
import type {
|
import type {
|
||||||
FrontendPreference,
|
FrontendPreference,
|
||||||
ImageGenerationFrontendPreference,
|
ImageGenerationFrontendPreference,
|
||||||
|
|
@ -55,6 +55,11 @@ async function resolveModelPaths(args: string[]) {
|
||||||
resolvedArgs.push(resolvedPath);
|
resolvedArgs.push(resolvedPath);
|
||||||
i++;
|
i++;
|
||||||
} catch (error) {
|
} 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);
|
logError(`Failed to resolve model path for ${arg}:`, error as Error);
|
||||||
resolvedArgs.push(urlOrPath);
|
resolvedArgs.push(urlOrPath);
|
||||||
i++;
|
i++;
|
||||||
|
|
@ -207,6 +212,7 @@ export async function launchKoboldCpp(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function stopKoboldCpp() {
|
export async function stopKoboldCpp() {
|
||||||
|
abortActiveDownloads();
|
||||||
await stopProxy();
|
await stopProxy();
|
||||||
return terminateProcess(koboldProcess);
|
return terminateProcess(koboldProcess);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { join, basename, dirname } from 'path';
|
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 { createWriteStream } from 'fs';
|
||||||
import { get as httpGet } from 'http';
|
import { get as httpGet } from 'http';
|
||||||
import { get as httpsGet } from 'https';
|
import { get as httpsGet } from 'https';
|
||||||
|
|
@ -8,6 +8,12 @@ import { pathExists } from '@/utils/node/fs';
|
||||||
import { logError } from '@/utils/node/logging';
|
import { logError } from '@/utils/node/logging';
|
||||||
import { sendToRenderer } from '@/main/modules/window';
|
import { sendToRenderer } from '@/main/modules/window';
|
||||||
import type { ModelParamType, CachedModel } from '@/types';
|
import type { ModelParamType, CachedModel } from '@/types';
|
||||||
|
import type { IncomingMessage } from 'http';
|
||||||
|
|
||||||
|
const activeDownloads = new Set<{
|
||||||
|
abort: () => void;
|
||||||
|
tempPath: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
interface DownloadProgress {
|
interface DownloadProgress {
|
||||||
type: 'progress' | 'complete' | 'error';
|
type: 'progress' | 'complete' | 'error';
|
||||||
|
|
@ -61,19 +67,45 @@ async function downloadFile(
|
||||||
) {
|
) {
|
||||||
const normalizedUrl = normalizeUrl(url);
|
const normalizedUrl = normalizeUrl(url);
|
||||||
const outputDir = dirname(outputPath);
|
const outputDir = dirname(outputPath);
|
||||||
|
const tempPath = `${outputPath}.tmp`;
|
||||||
|
|
||||||
await mkdir(outputDir, { recursive: true });
|
await mkdir(outputDir, { recursive: true });
|
||||||
|
|
||||||
return new Promise<boolean>((resolve, reject) => {
|
return new Promise<boolean>((resolve, reject) => {
|
||||||
const httpModule = normalizedUrl.startsWith('https') ? httpsGet : httpGet;
|
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 => {
|
const handleRedirect = (requestUrl: string, redirectCount = 0): void => {
|
||||||
|
if (isAborted) return;
|
||||||
|
|
||||||
if (redirectCount > 10) {
|
if (redirectCount > 10) {
|
||||||
|
activeDownloads.delete(abortController);
|
||||||
reject(new Error('Too many redirects'));
|
reject(new Error('Too many redirects'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
httpModule(requestUrl, (response) => {
|
httpModule(requestUrl, (response) => {
|
||||||
|
if (isAborted) return;
|
||||||
|
currentRequest = response;
|
||||||
if (
|
if (
|
||||||
response.statusCode === 301 ||
|
response.statusCode === 301 ||
|
||||||
response.statusCode === 302 ||
|
response.statusCode === 302 ||
|
||||||
|
|
@ -90,6 +122,7 @@ async function downloadFile(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.statusCode !== 200) {
|
if (response.statusCode !== 200) {
|
||||||
|
activeDownloads.delete(abortController);
|
||||||
reject(
|
reject(
|
||||||
new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`)
|
new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`)
|
||||||
);
|
);
|
||||||
|
|
@ -101,7 +134,7 @@ async function downloadFile(
|
||||||
let lastReportTime = Date.now();
|
let lastReportTime = Date.now();
|
||||||
let lastReportedBytes = 0;
|
let lastReportedBytes = 0;
|
||||||
|
|
||||||
const fileStream = createWriteStream(outputPath);
|
fileStream = createWriteStream(tempPath);
|
||||||
|
|
||||||
response.on('data', (chunk: Buffer) => {
|
response.on('data', (chunk: Buffer) => {
|
||||||
downloadedBytes += chunk.length;
|
downloadedBytes += chunk.length;
|
||||||
|
|
@ -152,22 +185,37 @@ async function downloadFile(
|
||||||
});
|
});
|
||||||
|
|
||||||
response.on('end', () => {
|
response.on('end', () => {
|
||||||
|
if (isAborted) return;
|
||||||
fileStream.end();
|
fileStream.end();
|
||||||
fileStream.on('finish', () => {
|
fileStream.on('finish', async () => {
|
||||||
sendToRenderer('kobold-output', '\n');
|
try {
|
||||||
resolve(true);
|
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) => {
|
response.on('error', async (err) => {
|
||||||
fileStream.close();
|
if (isAborted) return;
|
||||||
|
activeDownloads.delete(abortController);
|
||||||
|
await cleanup();
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
fileStream.on('error', (err) => {
|
fileStream.on('error', async (err) => {
|
||||||
|
if (isAborted) return;
|
||||||
|
activeDownloads.delete(abortController);
|
||||||
|
await cleanup();
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
}).on('error', (err) => {
|
}).on('error', (err) => {
|
||||||
|
if (isAborted) return;
|
||||||
|
activeDownloads.delete(abortController);
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -289,3 +337,9 @@ export async function getLocalModelsForType(paramType: ModelParamType) {
|
||||||
|
|
||||||
return models;
|
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