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