From 8ddf3e20a9a2f37a346a6544c42a8a171375c8ec Mon Sep 17 00:00:00 2001 From: Chetan Date: Mon, 3 Aug 2026 13:55:48 +0530 Subject: [PATCH] fix: update whisper models and split-on-word flag --- README.md | 22 +++++++++++++- src/WhisperHelper.ts | 35 +++++++++------------ src/autoDownloadModel.ts | 10 +++--- src/constants.ts | 66 ++++++++++++++++++++++------------------ src/downloadModel.ts | 40 +++++++++++------------- test/core.integration.js | 3 ++ 6 files changed, 100 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 063b5aa..4e9858b 100644 --- a/README.md +++ b/README.md @@ -94,15 +94,35 @@ await nodewhisper(filePath, { const MODELS_LIST = [ 'tiny', 'tiny.en', + 'tiny-q5_1', + 'tiny.en-q5_1', + 'tiny-q8_0', 'base', 'base.en', + 'base-q5_1', + 'base.en-q5_1', + 'base-q8_0', 'small', 'small.en', + 'small.en-tdrz', + 'small-q5_1', + 'small.en-q5_1', + 'small-q8_0', 'medium', 'medium.en', + 'medium-q5_0', + 'medium.en-q5_0', + 'medium-q8_0', 'large-v1', - 'large', + 'large-v2', + 'large-v2-q5_0', + 'large-v2-q8_0', + 'large-v3', + 'large-v3-q5_0', 'large-v3-turbo', + 'large-v3-turbo-q5_0', + 'large-v3-turbo-q8_0', + 'large', // backward-compatible alias for large-v3 ] ``` diff --git a/src/WhisperHelper.ts b/src/WhisperHelper.ts index d2c0b53..799f4c0 100644 --- a/src/WhisperHelper.ts +++ b/src/WhisperHelper.ts @@ -1,6 +1,6 @@ import path from 'path' import fs from 'fs' -import { MODELS_LIST, MODEL_OBJECT, WHISPER_CPP_PATH } from './constants' +import { isModelName, LEGACY_MODEL_FILES, MODELS_LIST, MODEL_OBJECT, WHISPER_CPP_PATH } from './constants' import { IOptions } from '.' // Get the correct executable path based on platform and build system @@ -26,35 +26,30 @@ function getExecutablePath(): string { } export const constructCommand = (filePath: string, args: IOptions): string => { - let errors: string[] = [] - const modelName = MODEL_OBJECT[args.modelName as keyof typeof MODEL_OBJECT] - if (!args.modelName) { - errors.push('[Nodejs-whisper] Error: Provide model name') + throw new Error('[Nodejs-whisper] Error: Provide model name') } - if (!MODELS_LIST.includes(args.modelName)) { - errors.push(`[Nodejs-whisper] Error: Enter a valid model name. Available models are: ${MODELS_LIST.join(', ')}`) + if (!isModelName(args.modelName)) { + throw new Error( + `[Nodejs-whisper] Error: Enter a valid model name. Available models are: ${MODELS_LIST.join(', ')}` + ) } - if (errors.length > 0) { - throw new Error(errors.join('\n')) - } + const modelName = MODEL_OBJECT[args.modelName] - const modelPath = args.modelRootPath - ? path.resolve(args.modelRootPath, modelName) - : path.join(WHISPER_CPP_PATH, 'models', modelName) + const modelDirectory = args.modelRootPath ? path.resolve(args.modelRootPath) : path.join(WHISPER_CPP_PATH, 'models') + const modelFileNames = [modelName, ...(LEGACY_MODEL_FILES[args.modelName] || [])] + const modelFileName = + modelFileNames.find(fileName => fs.existsSync(path.join(modelDirectory, fileName))) || modelName + const modelPath = path.join(modelDirectory, modelFileName) if (!fs.existsSync(modelPath)) { - errors.push( + throw new Error( `[Nodejs-whisper] Error: Model file does not exist at ${modelPath}. Please ensure the model is downloaded and correctly placed.` ) } - if (errors.length > 0) { - throw new Error(errors.join('\n')) - } - // Get the actual executable path const executablePath = getExecutablePath() if (!executablePath) { @@ -69,7 +64,7 @@ export const constructCommand = (filePath: string, args: IOptions): string => { return `"${arg}"` } - const modelArg = args.modelRootPath ? modelPath : `./models/${modelName}` + const modelArg = args.modelRootPath ? modelPath : `./models/${modelFileName}` let command = `${escapeArg(executablePath)} ${constructOptionsFlags(args)} -l ${args.whisperOptions?.language || 'auto'} -m ${escapeArg(modelArg)} -f ${escapeArg(filePath)}` @@ -89,7 +84,7 @@ const constructOptionsFlags = (args: IOptions): string => { args.whisperOptions?.translateToEnglish ? '-tr ' : '', args.whisperOptions?.wordTimestamps ? '-ml 1 ' : '', args.whisperOptions?.timestamps_length ? `-ml ${args.whisperOptions.timestamps_length} ` : '', - args.whisperOptions?.splitOnWord ? '-sow true ' : '', + args.whisperOptions?.splitOnWord ? '-sow ' : '', args.whisperOptions?.noGpu ? '-ng ' : '', ].join('') diff --git a/src/autoDownloadModel.ts b/src/autoDownloadModel.ts index 441163c..2cde305 100644 --- a/src/autoDownloadModel.ts +++ b/src/autoDownloadModel.ts @@ -1,7 +1,7 @@ import path from 'path' import shell from 'shelljs' import fs from 'fs' -import { MODEL_OBJECT, MODELS_LIST, WHISPER_CPP_PATH } from './constants' +import { DOWNLOAD_MODEL_ALIASES, isModelName, LEGACY_MODEL_FILES, MODEL_OBJECT, WHISPER_CPP_PATH } from './constants' import { Logger } from './types' import { getCmakeConfigureCommand } from './buildConfig' @@ -17,7 +17,7 @@ export default async function autoDownloadModel( throw new Error('[Nodejs-whisper] Error: Model name must be provided.') } - if (!MODELS_LIST.includes(autoDownloadModelName)) { + if (!isModelName(autoDownloadModelName)) { throw new Error('[Nodejs-whisper] Error: Provide a valid model name') } @@ -27,7 +27,8 @@ export default async function autoDownloadModel( fs.mkdirSync(downloadDirectory, { recursive: true }) shell.cd(modelDirectory) - const modelAlreadyExist = fs.existsSync(path.join(downloadDirectory, MODEL_OBJECT[autoDownloadModelName])) + const modelFiles = [MODEL_OBJECT[autoDownloadModelName], ...(LEGACY_MODEL_FILES[autoDownloadModelName] || [])] + const modelAlreadyExist = modelFiles.some(modelFile => fs.existsSync(path.join(downloadDirectory, modelFile))) if (modelAlreadyExist) { logger.debug(`[Nodejs-whisper] ${autoDownloadModelName} already exist. Skipping download.`) @@ -35,6 +36,7 @@ export default async function autoDownloadModel( } logger.debug(`[Nodejs-whisper] Auto-download Model: ${autoDownloadModelName}`) + const downloadModelName = DOWNLOAD_MODEL_ALIASES[autoDownloadModelName] || autoDownloadModelName let scriptPath = './download-ggml-model.sh' if (process.platform === 'win32') { @@ -43,7 +45,7 @@ export default async function autoDownloadModel( shell.chmod('+x', scriptPath) const downloadPathArg = modelRootPath ? ` ${quoteShellArg(downloadDirectory)}` : '' - const result = shell.exec(`${scriptPath} ${autoDownloadModelName}${downloadPathArg}`) + const result = shell.exec(`${scriptPath} ${downloadModelName}${downloadPathArg}`) if (result.code !== 0) { throw new Error(`[Nodejs-whisper] Failed to download model: ${result.stderr}`) diff --git a/src/constants.ts b/src/constants.ts index b4dee17..3b6abcb 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,47 +1,55 @@ import path from 'path' -export const MODELS_LIST = [ - 'tiny', - 'tiny.en', - 'base', - 'base.en', - 'small', - 'small.en', - 'medium', - 'medium.en', - 'large-v1', - 'large', - 'large-v3-turbo', -] - -export const MODELS = [ - 'ggml-tiny.en.bin', - 'ggml-tiny.bin', - 'ggml-base.en.bin', - 'ggml-base.bin', - 'ggml-small.en.bin', - 'ggml-small.bin', - 'ggml-medium.en.bin', - 'ggml-medium.bin', - 'ggml-large-v1.bin', - 'ggml-large.bin', - 'ggml-large-v3-turbo.bin', -] - export const MODEL_OBJECT = { tiny: 'ggml-tiny.bin', 'tiny.en': 'ggml-tiny.en.bin', + 'tiny-q5_1': 'ggml-tiny-q5_1.bin', + 'tiny.en-q5_1': 'ggml-tiny.en-q5_1.bin', + 'tiny-q8_0': 'ggml-tiny-q8_0.bin', base: 'ggml-base.bin', 'base.en': 'ggml-base.en.bin', + 'base-q5_1': 'ggml-base-q5_1.bin', + 'base.en-q5_1': 'ggml-base.en-q5_1.bin', + 'base-q8_0': 'ggml-base-q8_0.bin', small: 'ggml-small.bin', 'small.en': 'ggml-small.en.bin', + 'small.en-tdrz': 'ggml-small.en-tdrz.bin', + 'small-q5_1': 'ggml-small-q5_1.bin', + 'small.en-q5_1': 'ggml-small.en-q5_1.bin', + 'small-q8_0': 'ggml-small-q8_0.bin', medium: 'ggml-medium.bin', 'medium.en': 'ggml-medium.en.bin', + 'medium-q5_0': 'ggml-medium-q5_0.bin', + 'medium.en-q5_0': 'ggml-medium.en-q5_0.bin', + 'medium-q8_0': 'ggml-medium-q8_0.bin', 'large-v1': 'ggml-large-v1.bin', - large: 'ggml-large.bin', + 'large-v2': 'ggml-large-v2.bin', + 'large-v2-q5_0': 'ggml-large-v2-q5_0.bin', + 'large-v2-q8_0': 'ggml-large-v2-q8_0.bin', + 'large-v3': 'ggml-large-v3.bin', + 'large-v3-q5_0': 'ggml-large-v3-q5_0.bin', 'large-v3-turbo': 'ggml-large-v3-turbo.bin', + 'large-v3-turbo-q5_0': 'ggml-large-v3-turbo-q5_0.bin', + 'large-v3-turbo-q8_0': 'ggml-large-v3-turbo-q8_0.bin', + large: 'ggml-large-v3.bin', +} + +export type ModelName = keyof typeof MODEL_OBJECT + +export const MODELS_LIST = Object.keys(MODEL_OBJECT) as ModelName[] +export const MODELS = Object.values(MODEL_OBJECT) + +export const DOWNLOAD_MODEL_ALIASES: Partial> = { + large: 'large-v3', +} + +export const LEGACY_MODEL_FILES: Partial> = { + large: ['ggml-large.bin'], } +export const isModelName = (modelName: unknown): modelName is ModelName => + typeof modelName === 'string' && Object.prototype.hasOwnProperty.call(MODEL_OBJECT, modelName) + export const DEFAULT_MODEL = 'tiny.en' export const WHISPER_CPP_PATH = path.join(__dirname, '..', 'cpp', 'whisper.cpp') diff --git a/src/downloadModel.ts b/src/downloadModel.ts index 5f54d45..4436b95 100644 --- a/src/downloadModel.ts +++ b/src/downloadModel.ts @@ -5,11 +5,20 @@ import path from 'path' import shell from 'shelljs' import readlineSync from 'readline-sync' -import { MODELS_LIST, DEFAULT_MODEL, MODELS, WHISPER_CPP_PATH, MODEL_OBJECT } from './constants' +import { + DEFAULT_MODEL, + DOWNLOAD_MODEL_ALIASES, + isModelName, + LEGACY_MODEL_FILES, + ModelName, + MODELS_LIST, + WHISPER_CPP_PATH, + MODEL_OBJECT, +} from './constants' import fs from 'fs' import { Logger } from './types' import { getCmakeConfigureCommand } from './buildConfig' -const askForModel = async (logger: Logger = console): Promise => { +const askForModel = async (logger: Logger = console): Promise => { const answer = await readlineSync.question( `\n[Nodejs-whisper] Enter model name (e.g. 'tiny.en') or 'cancel' to exit\n(ENTER for tiny.en): ` ) @@ -22,7 +31,7 @@ const askForModel = async (logger: Logger = console): Promise => { else if (answer === '') { logger.log('[Nodejs-whisper] Going with', DEFAULT_MODEL) return DEFAULT_MODEL - } else if (!MODELS_LIST.includes(answer)) { + } else if (!isModelName(answer)) { logger.log( '\n[Nodejs-whisper] FAIL: Name not found. Check your spelling OR quit wizard and use custom model.\n' ) @@ -53,11 +62,11 @@ async function downloadModel(logger: Logger = console) { try { shell.cd(path.join(WHISPER_CPP_PATH, 'models')) - let anyModelExist = [] + const anyModelExist: ModelName[] = [] MODELS_LIST.forEach(model => { - if (!fs.existsSync(path.join(WHISPER_CPP_PATH, 'models', MODEL_OBJECT[model]))) { - } else { + const modelFiles = [MODEL_OBJECT[model], ...(LEGACY_MODEL_FILES[model] || [])] + if (modelFiles.some(modelFile => fs.existsSync(path.join(WHISPER_CPP_PATH, 'models', modelFile)))) { anyModelExist.push(model) } }) @@ -68,21 +77,7 @@ async function downloadModel(logger: Logger = console) { logger.log('\n[Nodejs-whisper] You can install additional models from the list below.\n') } - logger.log(` -| Model | Disk | RAM | -|----------------|--------|---------| -| tiny | 75 MB | ~390 MB | -| tiny.en | 75 MB | ~390 MB | -| base | 142 MB | ~500 MB | -| base.en | 142 MB | ~500 MB | -| small | 466 MB | ~1.0 GB | -| small.en | 466 MB | ~1.0 GB | -| medium | 1.5 GB | ~2.6 GB | -| medium.en | 1.5 GB | ~2.6 GB | -| large-v1 | 2.9 GB | ~4.7 GB | -| large | 2.9 GB | ~4.7 GB | -| large-v3-turbo | 1.5 GB | ~2.6 GB | -`) + logger.log(`[Nodejs-whisper] Available models:\n${MODELS_LIST.join('\n')}`) const downloaderScript = process.platform === 'win32' ? 'download-ggml-model.cmd' : './download-ggml-model.sh' @@ -91,11 +86,12 @@ async function downloadModel(logger: Logger = console) { } const modelName = await askForModel() + const downloadModelName = DOWNLOAD_MODEL_ALIASES[modelName] || modelName const scriptPath = downloaderScript shell.chmod('+x', scriptPath) - shell.exec(`${scriptPath} ${modelName}`) + shell.exec(`${scriptPath} ${downloadModelName}`) logger.log('[Nodejs-whisper] Attempting to build whisper.cpp...\n') shell.cd('../') diff --git a/test/core.integration.js b/test/core.integration.js index 5bb0da8..741d188 100644 --- a/test/core.integration.js +++ b/test/core.integration.js @@ -48,6 +48,7 @@ test( whisperOptions: { noGpu: true, outputInVtt: true, + splitOnWord: true, timestamps_length: 14, }, }) @@ -71,6 +72,8 @@ test( assert.match(normalizedTranscript, /i want your.*forgiveness/) assert.match(loggedTranscript.toLowerCase(), /i do not want/) assert.match(debugOutput, /whisper_init_with_params_no_state:/) + assert.match(debugOutput, /-sow(?:\s|$)/) + assert.doesNotMatch(debugOutput, /-sow\s+true/) assert.deepEqual(leakedWhisperOutput, []) assert.equal(fs.existsSync(outputFile), true, 'VTT output should be created')