Skip to content
Merged
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
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
]
```

Expand Down
35 changes: 15 additions & 20 deletions src/WhisperHelper.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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) {
Expand All @@ -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)}`

Expand All @@ -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('')

Expand Down
10 changes: 6 additions & 4 deletions src/autoDownloadModel.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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')
}

Expand All @@ -27,14 +27,16 @@ 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.`)
return 'Models already exist. Skipping download.'
}

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') {
Expand All @@ -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}`)
Expand Down
66 changes: 37 additions & 29 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -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<Record<ModelName, ModelName>> = {
large: 'large-v3',
}

export const LEGACY_MODEL_FILES: Partial<Record<ModelName, string[]>> = {
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')
Expand Down
40 changes: 18 additions & 22 deletions src/downloadModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> => {
const askForModel = async (logger: Logger = console): Promise<ModelName> => {
const answer = await readlineSync.question(
`\n[Nodejs-whisper] Enter model name (e.g. 'tiny.en') or 'cancel' to exit\n(ENTER for tiny.en): `
)
Expand All @@ -22,7 +31,7 @@ const askForModel = async (logger: Logger = console): Promise<string> => {
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'
)
Expand Down Expand Up @@ -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)
}
})
Expand All @@ -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'

Expand All @@ -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('../')
Expand Down
3 changes: 3 additions & 0 deletions test/core.integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ test(
whisperOptions: {
noGpu: true,
outputInVtt: true,
splitOnWord: true,
timestamps_length: 14,
},
})
Expand All @@ -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')
Expand Down
Loading