File size: 4,785 Bytes
c37ff1f 0dbae7f c37ff1f 0dbae7f c37ff1f 0dbae7f c37ff1f 0dbae7f c37ff1f 0dbae7f c37ff1f 0dbae7f c37ff1f 0dbae7f c37ff1f 0dbae7f c37ff1f 0dbae7f c37ff1f 0dbae7f c37ff1f 0dbae7f c37ff1f 0dbae7f c37ff1f 0dbae7f c37ff1f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | #!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
const ort = require('onnxruntime-web');
const root = path.resolve(__dirname, '..');
const modelId = process.argv[2];
if (!modelId) throw new Error('usage: node scripts/validate_web.cjs <model-id>');
ort.env.wasm.numThreads = 1;
ort.env.wasm.proxy = false;
async function main() {
const catalog = JSON.parse(fs.readFileSync(path.join(root, 'catalog.json'), 'utf8'));
const cases = JSON.parse(fs.readFileSync(path.join(root, 'validation/cases.json'), 'utf8'));
const model = catalog.models.find((item) => item.id === modelId);
if (!model) throw new Error(`unknown model: ${modelId}`);
const tokenizer = catalog.tokenizers.find((item) => item.id === model.tokenizerId);
const config = JSON.parse(fs.readFileSync(path.join(root, tokenizer.artifact.path), 'utf8'));
const vocab = config[catalog.runtime.tokenEncoding.vocabularyField];
let language;
let voice;
for (const candidateLanguage of catalog.languages) {
const candidateVoice = candidateLanguage.voices.find((item) => item.modelId === modelId);
if (candidateVoice) {
language = candidateLanguage;
voice = candidateVoice;
break;
}
}
if (!voice) throw new Error(`no voice references model: ${modelId}`);
const testCase = cases.find((item) => {
if (!item.webModelTest) return false;
const candidateLanguage = catalog.languages.find((entry) => entry.id === item.language);
const candidateVoice = candidateLanguage?.voices.find((entry) => entry.id === item.voice);
return candidateVoice?.modelId === modelId;
});
if (!testCase) throw new Error(`no webModelTest case for model: ${modelId}`);
language = catalog.languages.find((item) => item.id === testCase.language);
voice = language.voices.find((item) => item.id === testCase.voice);
function idsFor(phonemes) {
return [
catalog.runtime.tokenEncoding.bosTokenId,
...Array.from(phonemes).map((phoneme) => {
const id = vocab[phoneme];
if (id === undefined) throw new Error(`missing phoneme: ${phoneme}`);
return id;
}),
catalog.runtime.tokenEncoding.eosTokenId,
];
}
const voiceBytes = fs.readFileSync(path.join(root, voice.artifact.path));
function styleAt(row) {
const styleOffset = row * 256 * 4;
const style = new Float32Array(256);
for (let index = 0; index < style.length; index += 1) {
style[index] = voiceBytes.readFloatLE(styleOffset + index * 4);
}
return style;
}
const modelBytes = fs.readFileSync(path.join(root, model.artifact.path));
const session = await ort.InferenceSession.create(modelBytes, {
executionProviders: ['wasm'],
graphOptimizationLevel: 'all',
});
async function validate(caseId, ids, style) {
const output = await session.run({
input_ids: new ort.Tensor(
'int64',
BigInt64Array.from(ids.map(BigInt)),
[1, ids.length],
),
style: new ort.Tensor('float32', style, [1, 256]),
speed: new ort.Tensor('float32', Float32Array.of(1), [1]),
});
const waveform = output.waveform.data;
const durationSum = Array.from(output.duration.data).reduce(
(sum, item) => sum + Number(item),
0,
);
const expectedSamples = durationSum * catalog.runtime.durationToSamplesFactor;
if (waveform.length !== expectedSamples) {
throw new Error(`waveform/duration mismatch: ${waveform.length} vs ${expectedSamples}`);
}
let peak = 0;
for (const sample of waveform) {
if (!Number.isFinite(sample)) throw new Error('non-finite waveform');
peak = Math.max(peak, Math.abs(sample));
}
return {
id: caseId,
inputTokens: ids.length,
outputSamples: waveform.length,
peakAbsoluteAmplitude: peak,
};
}
const phonemeCount = Array.from(testCase.phonemes).length;
const testIds = idsFor(testCase.phonemes);
const maxPhonemes = catalog.runtime.maxPhonemeCodePoints;
const maxIds = idsFor('a'.repeat(maxPhonemes));
const result = {
modelId,
language: language.id,
voice: voice.id,
onnxRuntimeWebVersion: JSON.parse(
fs.readFileSync(path.join(root, 'node_modules/onnxruntime-web/package.json'), 'utf8'),
).version,
executionProvider: 'wasm',
cases: [
await validate(testCase.id, testIds, styleAt(phonemeCount - 1)),
await validate('max-phoneme-boundary', maxIds, styleAt(maxPhonemes - 1)),
],
};
const filename = `web-${modelId.replace(/[^a-z0-9_-]/g, '-')}.json`;
fs.writeFileSync(
path.join(root, 'validation', filename),
`${JSON.stringify(result, null, 2)}\n`,
);
console.log(JSON.stringify(result));
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
|