72 lines
2.4 KiB
JavaScript
72 lines
2.4 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { createReadStream, createWriteStream } from 'node:fs'
|
|
import { mkdir, rename, rm, stat } from 'node:fs/promises'
|
|
import { dirname, resolve } from 'node:path'
|
|
import { Readable } from 'node:stream'
|
|
import { pipeline } from 'node:stream/promises'
|
|
|
|
const MODEL_URL = 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin'
|
|
const MODEL_BYTES = 77_691_713
|
|
const MODEL_SHA256 = 'be07e048e1e599ad46341c8d2a135645097a538221678b7acdd1b1919c6e1b21'
|
|
const DEFAULT_OUTPUT = 'apps/mobile-rn/android/app/src/main/assets/models/ggml-tiny.bin'
|
|
|
|
async function digestFile(path) {
|
|
const hash = createHash('sha256')
|
|
await pipeline(createReadStream(path), hash)
|
|
return hash.digest('hex')
|
|
}
|
|
|
|
async function isVerifiedModel(path) {
|
|
try {
|
|
const metadata = await stat(path)
|
|
if (!metadata.isFile() || metadata.size !== MODEL_BYTES) return false
|
|
return (await digestFile(path)) === MODEL_SHA256
|
|
} catch (error) {
|
|
if (error?.code === 'ENOENT') return false
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async function downloadVerifiedModel(outputPath) {
|
|
const temporaryPath = `${outputPath}.part-${process.pid}`
|
|
await mkdir(dirname(outputPath), { recursive: true })
|
|
|
|
try {
|
|
const response = await fetch(MODEL_URL, {
|
|
redirect: 'follow',
|
|
signal: AbortSignal.timeout(120_000)
|
|
})
|
|
if (!response.ok || !response.body) {
|
|
throw new Error(`whisper_model_download_failed:${response.status}`)
|
|
}
|
|
|
|
const declaredLength = Number(response.headers.get('content-length'))
|
|
if (Number.isFinite(declaredLength) && declaredLength !== MODEL_BYTES) {
|
|
throw new Error(`whisper_model_length_mismatch:${declaredLength}`)
|
|
}
|
|
|
|
await pipeline(
|
|
Readable.fromWeb(response.body),
|
|
createWriteStream(temporaryPath, { flags: 'wx', mode: 0o600 })
|
|
)
|
|
|
|
if (!(await isVerifiedModel(temporaryPath))) {
|
|
throw new Error('whisper_model_checksum_mismatch')
|
|
}
|
|
|
|
await rm(outputPath, { force: true })
|
|
await rename(temporaryPath, outputPath)
|
|
} catch (error) {
|
|
await rm(temporaryPath, { force: true }).catch(() => undefined)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
const outputPath = resolve(process.argv[2] ?? DEFAULT_OUTPUT)
|
|
|
|
if (await isVerifiedModel(outputPath)) {
|
|
console.log(`Whisper model verified: ${outputPath}`)
|
|
} else {
|
|
await downloadVerifiedModel(outputPath)
|
|
console.log(`Whisper model downloaded and verified: ${outputPath}`)
|
|
}
|