Files
gods-eye-view/build/onDeviceRuntime.js

171 lines
5.8 KiB
JavaScript

import { createRequire } from 'node:module';
import path from 'node:path';
// Two of the LiteRT-LM package's four builds: relaxed SIMD with JavaScript
// promise integration (current Chrome and Edge) and the most compatible one.
const LITERT_VARIANTS = {
internal: 'litertlm_wasm_internal',
compatAsyncify: 'litertlm_wasm_compat_asyncify_internal',
};
const ASSETS_MODULE = path.join(
'src',
'voice',
'local',
'workers',
'runtimeAssets.js',
);
// kokoro-js imports `phonemizer`, which embeds eSpeak NG. The build never
// includes it: the import resolves to this loader, which downloads the pinned
// file from the public npm CDN at runtime and checks its SHA-256.
const PHONEMIZER_MODULE = path.join(
'src',
'voice',
'local',
'workers',
'phonemizerRuntime.js',
);
/** Strings that only appear in the eSpeak NG phonemizer build. */
export const ESPEAK_MARKERS = Object.freeze([
'eSpeakNGWorker',
'espeak-ng-data',
'espeakEVENT',
]);
/** Absolute path of the runtime phonemizer loader for a checkout. */
export function phonemizerLoaderPath(root = process.cwd()) {
return path.join(root, PHONEMIZER_MODULE);
}
/** Names of emitted chunks or assets that contain eSpeak NG code. */
export function findEspeakChunks(bundle) {
const found = [];
for (const [name, output] of Object.entries(bundle || {})) {
const text =
typeof output.code === 'string'
? output.code
: typeof output.source === 'string'
? output.source
: output.source instanceof Uint8Array
? Buffer.from(output.source).toString('latin1')
: '';
if (ESPEAK_MARKERS.some((marker) => text.includes(marker)))
found.push(name);
}
return found;
}
const posix = (file) => file.split(path.sep).join('/');
/** Directory of the installed package that `from` resolves `name` to. */
function packageDir(from, name) {
const entry = createRequire(from).resolve(name);
const marker = `${path.sep}node_modules${path.sep}${name.split('/').join(path.sep)}${path.sep}`;
const at = entry.lastIndexOf(marker);
if (at < 0) throw new Error(`Cannot locate ${name} from ${from}`);
return entry.slice(0, at + marker.length - 1);
}
/**
* Serves the on-device voice runtimes from the application build instead of
* a CDN. It replaces src/voice/local/workers/runtimeAssets.js with imports
* of the installed LiteRT-LM Emscripten loaders (classic scripts, exported
* here as modules) and wasm files, and the ONNX Runtime Web files that the
* two transformers.js copies (speech recognition, Kokoro) use.
*/
export function onDeviceRuntimePlugin({ root = process.cwd() } = {}) {
let paths = null;
const resolvePaths = () => {
if (paths) return paths;
const rootFile = path.join(root, 'package.json');
const require = createRequire(rootFile);
const litert = path.join(packageDir(rootFile, '@litert-lm/core'), 'wasm');
const ort = path.join(
packageDir(
require.resolve('@huggingface/transformers'),
'onnxruntime-web',
),
'dist',
);
const kokoroTransformers = path.join(
packageDir(require.resolve('kokoro-js'), '@huggingface/transformers'),
'dist',
);
paths = {
assets: path.join(root, ASSETS_MODULE),
loaders: new Set(
Object.values(LITERT_VARIANTS).map((name) =>
path.join(litert, `${name}.js`),
),
),
source() {
const lines = [];
const runtimes = [];
for (const [key, name] of Object.entries(LITERT_VARIANTS)) {
const file = posix(path.join(litert, name));
lines.push(
`import ${key}Factory from ${JSON.stringify(`${file}.js`)};`,
);
lines.push(
`import ${key}Wasm from ${JSON.stringify(`${file}.wasm?url`)};`,
);
runtimes.push(
`${key}: { ModuleFactory: ${key}Factory, wasmUrl: ${key}Wasm }`,
);
}
const onnx = [];
for (const [key, dir, name] of [
['transformers', ort, 'ort-wasm-simd-threaded.asyncify'],
['kokoro', kokoroTransformers, 'ort-wasm-simd-threaded.jsep'],
]) {
const file = posix(path.join(dir, name));
lines.push(
`import ${key}Mjs from ${JSON.stringify(`${file}.mjs?url`)};`,
);
lines.push(
`import ${key}OrtWasm from ${JSON.stringify(`${file}.wasm?url`)};`,
);
onnx.push(`${key}: { mjs: ${key}Mjs, wasm: ${key}OrtWasm }`);
}
lines.push(`export const litertRuntimes = { ${runtimes.join(', ')} };`);
lines.push(`export const onnxWasmPaths = { ${onnx.join(', ')} };`);
lines.push('export function requireRuntime(value) { return value; }');
return lines.join('\n');
},
};
return paths;
};
const phonemizer = phonemizerLoaderPath(root);
return {
name: 'gev-on-device-runtime',
enforce: 'pre',
resolveId(source) {
return source === 'phonemizer' ? phonemizer : null;
},
generateBundle(_options, bundle) {
const found = findEspeakChunks(bundle);
if (found.length)
this.error(
`eSpeak NG phonemizer code must not be bundled (found in ${found.join(', ')})`,
);
},
load(id) {
const file = path.normalize(id.split('?')[0]);
return file === resolvePaths().assets ? resolvePaths().source() : null;
},
transform(code, id) {
const file = path.normalize(id.split('?')[0]);
if (!file.endsWith('.js') || !resolvePaths().loaders.has(file))
return null;
// Modules are strict: the loaders' debug hook declares a function
// inside a block, which only sloppy scripts hoist.
const prelude =
'var custom_dbg = function () { console.warn.apply(console, arguments); };\n';
return {
code: `${prelude}${code}\nexport default ModuleFactory;\n`,
map: null,
};
},
};
}