68 lines
3.1 KiB
JavaScript
68 lines
3.1 KiB
JavaScript
import {createHash} from 'node:crypto';
|
|
import {createWriteStream} from 'node:fs';
|
|
import {cp, mkdir, readdir, rm, stat, rename, unlink} from 'node:fs/promises';
|
|
import {join} from 'node:path';
|
|
import {Readable, Transform} from 'node:stream';
|
|
import {pipeline} from 'node:stream/promises';
|
|
import {assetPrefix, requiredOnnxFiles, voiceFiles, voiceRepository, voiceRevision} from './assets.mjs';
|
|
|
|
const root = process.cwd();
|
|
const output = join(root, 'public', 'static', assetPrefix);
|
|
const onnxSource = join(root, 'node_modules', 'onnxruntime-web', 'dist');
|
|
const piperSource = join(root, 'node_modules', '@diffusionstudio', 'piper-wasm', 'build');
|
|
|
|
async function downloadVoiceFile(file) {
|
|
const destination = join(output, 'voices', file.path);
|
|
const temporary = `${destination}.part`;
|
|
await mkdir(join(destination, '..'), {recursive: true});
|
|
const response = await fetch(`${voiceRepository}/${voiceRevision}/${file.path}`);
|
|
if (!response.ok || !response.body) throw new Error(`Unable to download ${file.path}: HTTP ${response.status}`);
|
|
const sha256 = createHash('sha256');
|
|
const sha1 = createHash('sha1').update(Buffer.from(`blob ${file.size}\0`));
|
|
let size = 0;
|
|
const verify = new Transform({
|
|
transform(chunk, _encoding, callback) {
|
|
size += chunk.length;
|
|
sha256.update(chunk);
|
|
sha1.update(chunk);
|
|
callback(null, chunk);
|
|
},
|
|
});
|
|
try {
|
|
await pipeline(Readable.fromWeb(response.body), verify, createWriteStream(temporary));
|
|
if (size !== file.size || (file.sha256 && sha256.digest('hex') !== file.sha256) ||
|
|
(file.gitBlobSha1 && sha1.digest('hex') !== file.gitBlobSha1)) {
|
|
throw new Error(`Integrity check failed for ${file.path} (${size} bytes)`);
|
|
}
|
|
await rename(temporary, destination);
|
|
} catch (error) {
|
|
await unlink(temporary).catch(() => {});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
await rm(join(root, 'public', 'static'), {recursive: true, force: true});
|
|
await mkdir(join(output, 'onnx'), {recursive: true});
|
|
await mkdir(join(output, 'piper'), {recursive: true});
|
|
for (const file of voiceFiles) await downloadVoiceFile(file);
|
|
await cp(join(root, 'PIPER_VOICE_NOTICES.txt'), join(output, 'voices', 'ATTRIBUTION.txt'));
|
|
|
|
const onnxFiles = (await readdir(onnxSource)).filter(name => name.endsWith('.wasm') || name.endsWith('.mjs'));
|
|
for (const file of requiredOnnxFiles) {
|
|
if (!onnxFiles.includes(file)) throw new Error(`Missing ONNX Runtime asset: ${file}`);
|
|
}
|
|
for (const file of onnxFiles) await cp(join(onnxSource, file), join(output, 'onnx', file));
|
|
|
|
for (const file of ['piper_phonemize.wasm', 'piper_phonemize.data']) {
|
|
const source = join(piperSource, file);
|
|
const fileStat = await stat(source).catch(() => null);
|
|
if (!fileStat?.isFile() || fileStat.size === 0) throw new Error(`Missing Piper runtime file: ${file}`);
|
|
await cp(source, join(output, 'piper', file));
|
|
}
|
|
|
|
for (const file of voiceFiles) {
|
|
const downloaded = join(output, 'voices', file.path);
|
|
if ((await stat(downloaded)).size !== file.size) throw new Error(`Missing verified voice file: ${file.path}`);
|
|
}
|
|
console.log(`Static Piper assets prepared at ${output}`);
|