d3ro-voice/scripts/ci/prepare-whisper-model.mjs
2026-08-29 18:33:45 +09:00

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}`)
}