Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/desktop/public/icons/models/nvidia.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 81 additions & 0 deletions apps/desktop/src/constants/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export interface AvailableWhisperModel {
description: string;
downloadUrl: string;
filename: string; // Expected filename after download
artifacts?: {
filename: string;
downloadUrl: string;
checksum?: string;
size?: number;
}[];
checksum?: string; // Optional checksum for validation
features: {
icon: string;
Expand All @@ -15,9 +21,11 @@ export interface AvailableWhisperModel {
speed: number;
accuracy: number;
setup: "offline" | "cloud";
runtime: "whisper-local" | "parakeet-onnx" | "cloud";
provider: string;
providerIcon: string;
modelSize: string;
sourceUrl?: string;
}

// DownloadedModel type is now imported from the database schema
Expand Down Expand Up @@ -145,9 +153,76 @@ export const AVAILABLE_MODELS: AvailableWhisperModel[] = [
speed: 4.5,
accuracy: 4.5,
setup: "cloud",
runtime: "cloud",
provider: "Amical Cloud",
providerIcon: "/assets/icon_logo.svg",
},
{
id: "parakeet-tdt-0.6b-v3-int8",
name: "NVIDIA Parakeet TDT 0.6B v3",
type: "whisper",
description:
"Transducer speech model with improved multilingual quality and robustness using ONNX Runtime.",
checksum: "",
filename: "encoder-model.int8.onnx",
artifacts: [
{
filename: "encoder-model.int8.onnx",
downloadUrl:
"https://huggingface.co/istupakov/parakeet-tdt-0.6b-v3-onnx/resolve/main/encoder-model.int8.onnx",
size: 652183999,
},
{
filename: "decoder_joint-model.int8.onnx",
downloadUrl:
"https://huggingface.co/istupakov/parakeet-tdt-0.6b-v3-onnx/resolve/main/decoder_joint-model.int8.onnx",
size: 18202004,
},
{
filename: "nemo128.onnx",
downloadUrl:
"https://huggingface.co/istupakov/parakeet-tdt-0.6b-v3-onnx/resolve/main/nemo128.onnx",
size: 139764,
},
{
filename: "vocab.txt",
downloadUrl:
"https://huggingface.co/istupakov/parakeet-tdt-0.6b-v3-onnx/resolve/main/vocab.txt",
size: 93939,
},
{
filename: "config.json",
downloadUrl:
"https://huggingface.co/istupakov/parakeet-tdt-0.6b-v3-onnx/resolve/main/config.json",
},
],
downloadUrl:
"https://huggingface.co/istupakov/parakeet-tdt-0.6b-v3-onnx/resolve/main/encoder-model.int8.onnx",
size: 670619706,
sizeFormatted: "~640 MB",
modelSize: "~640 MB",
features: [
{
icon: "award",
tooltip: "Higher-quality transducer decoding",
},
{
icon: "gauge",
tooltip: "DirectML/CPU ONNX runtime",
},
{
icon: "languages",
tooltip: "Strong multilingual support",
},
],
speed: 4.3,
accuracy: 4.6,
setup: "offline",
runtime: "parakeet-onnx",
provider: "NVIDIA",
providerIcon: "/icons/models/nvidia.svg",
sourceUrl: "https://huggingface.co/istupakov/parakeet-tdt-0.6b-v3-onnx",
},
{
id: "whisper-tiny",
name: "Whisper Tiny",
Expand Down Expand Up @@ -177,6 +252,7 @@ export const AVAILABLE_MODELS: AvailableWhisperModel[] = [
speed: 5.0,
accuracy: 2.5,
setup: "offline",
runtime: "whisper-local",
provider: "Local",
providerIcon: "/icons/models/local.svg",
},
Expand Down Expand Up @@ -209,6 +285,7 @@ export const AVAILABLE_MODELS: AvailableWhisperModel[] = [
speed: 4.0,
accuracy: 3.0,
setup: "offline",
runtime: "whisper-local",
provider: "Local",
providerIcon: "/icons/models/local.svg",
},
Expand Down Expand Up @@ -242,6 +319,7 @@ export const AVAILABLE_MODELS: AvailableWhisperModel[] = [
speed: 3.0,
accuracy: 3.8,
setup: "offline",
runtime: "whisper-local",
provider: "Local",
providerIcon: "/icons/models/local.svg",
},
Expand Down Expand Up @@ -274,6 +352,7 @@ export const AVAILABLE_MODELS: AvailableWhisperModel[] = [
speed: 2.0,
accuracy: 4.3,
setup: "offline",
runtime: "whisper-local",
provider: "Local",
providerIcon: "/icons/models/local.svg",
},
Expand Down Expand Up @@ -306,6 +385,7 @@ export const AVAILABLE_MODELS: AvailableWhisperModel[] = [
speed: 1.5,
accuracy: 4.7,
setup: "offline",
runtime: "whisper-local",
provider: "Local",
providerIcon: "/icons/models/local.svg",
},
Expand Down Expand Up @@ -338,6 +418,7 @@ export const AVAILABLE_MODELS: AvailableWhisperModel[] = [
speed: 3.5,
accuracy: 4.2,
setup: "offline",
runtime: "whisper-local",
provider: "Local",
providerIcon: "/icons/models/local.svg",
},
Expand Down
103 changes: 82 additions & 21 deletions apps/desktop/src/db/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,8 @@ export async function getModelsByIds(
}

/**
* Sync Local Whisper models with filesystem
* Scans the models directory and syncs database records with actual files
* Sync local speech models with filesystem
* Scans expected model paths and syncs database records with actual files
*/
export async function syncLocalWhisperModels(
modelsDirectory: string,
Expand All @@ -238,6 +238,9 @@ export async function syncLocalWhisperModels(
speed: number;
accuracy: number;
filename: string;
artifacts?: Array<{
filename: string;
}>;
}>,
): Promise<{ added: number; updated: number; removed: number }> {
const fs = await import("fs");
Expand All @@ -251,41 +254,99 @@ export async function syncLocalWhisperModels(
const existingModels = await getModelsByProvider("local-whisper");
const existingModelMap = new Map(existingModels.map((m) => [m.id, m]));

// Scan the models directory for .bin files
const modelFiles = new Set<string>();
if (fs.existsSync(modelsDirectory)) {
const files = fs.readdirSync(modelsDirectory);
for (const file of files) {
if (file.endsWith(".bin")) {
modelFiles.add(file);
}
}
}

// Map available models by ID for easy lookup
// (we already have them indexed by ID, so we don't need this map)

const resolveRequiredLocalFiles = (
model: (typeof availableModels)[number],
) => {
const requiredFilenames =
model.artifacts && model.artifacts.length > 0
? model.artifacts.map((artifact) => artifact.filename)
: [model.filename];

const resolvedFiles = requiredFilenames
.map((filename) => {
const candidatePaths = [
path.join(modelsDirectory, filename),
path.join(modelsDirectory, model.id, filename),
];

return candidatePaths.find((candidatePath) =>
fs.existsSync(candidatePath),
);
})
.filter((filePath): filePath is string => !!filePath);

return resolvedFiles.length === requiredFilenames.length
? resolvedFiles
: null;
};

// Process each available model
for (const model of availableModels) {
const filePath = path.join(modelsDirectory, model.filename);
const fileExists = modelFiles.has(model.filename);
const resolvedFiles = resolveRequiredLocalFiles(model);
const filePath =
resolvedFiles?.find(
(resolvedFilePath) =>
path.basename(resolvedFilePath) === model.filename,
) || path.join(modelsDirectory, model.id, model.filename);
const fileExists = !!resolvedFiles;
const existingRecord = existingModelMap.get(model.id);

if (fileExists) {
// File exists on disk
const stats = fs.statSync(filePath);
const sizeBytes = resolvedFiles.reduce(
(sum, resolvedFilePath) => sum + fs.statSync(resolvedFilePath).size,
0,
);
const existingLocalFiles =
existingRecord?.originalModel &&
typeof existingRecord.originalModel === "object" &&
!Array.isArray(existingRecord.originalModel) &&
Array.isArray(
(
existingRecord.originalModel as {
localFiles?: unknown;
}
).localFiles,
)
? (
existingRecord.originalModel as {
localFiles: unknown[];
}
).localFiles.filter(
(value): value is string => typeof value === "string",
)
: [];
const localFilesChanged =
existingLocalFiles.length !== resolvedFiles.length ||
existingLocalFiles.some(
(existingLocalFile, index) =>
existingLocalFile !== resolvedFiles[index],
);
const originalModel =
existingRecord?.originalModel &&
typeof existingRecord.originalModel === "object" &&
!Array.isArray(existingRecord.originalModel)
? {
...existingRecord.originalModel,
localFiles: resolvedFiles,
}
: { localFiles: resolvedFiles };

if (existingRecord) {
// Update existing record if needed
if (
existingRecord.localPath !== filePath ||
existingRecord.sizeBytes !== stats.size
existingRecord.sizeBytes !== sizeBytes ||
localFilesChanged
) {
await upsertModel({
...existingRecord,
localPath: filePath,
sizeBytes: stats.size,
sizeBytes,
downloadedAt: existingRecord.downloadedAt || new Date(),
originalModel,
});
updated++;
}
Expand All @@ -304,10 +365,10 @@ export async function syncLocalWhisperModels(
speed: model.speed,
accuracy: model.accuracy,
localPath: filePath,
sizeBytes: stats.size,
sizeBytes,
downloadedAt: new Date(),
context: null,
originalModel: null,
originalModel,
});
added++;
}
Expand Down
Loading