repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
quickshell/nuguland/noko.mjs
Raw#!/usr/bin/env node
import { createRequire } from 'node:module';
import { appendFileSync, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
import { access, constants } from 'node:fs/promises';
import { spawnSync } from 'node:child_process';
import { Readable } from 'node:stream';
import { discoverParakeetDir, loadSentencePiece, ParakeetOnnx } from './noko-parakeet.mjs';
import { loadOnnxRuntime, OnnxVad, vadProbabilities, vadRuntimeReport } from './noko-vad.mjs';
const require = createRequire(import.meta.url);
const Noko = require('./noko.js');
function usage() {
return `usage:
node noko.mjs doctor
node noko.mjs system-deps-json
node noko.mjs assets-json
node noko.mjs check-assets-json
node noko.mjs install-assets
node noko.mjs update-assets
node noko.mjs install-assets-progress
node noko.mjs update-assets-progress
node noko.mjs capture-json seconds output.json [device]
node noko.mjs vad-json input.json output.json
node noko.mjs vad-gate-json input.json output.json
node noko.mjs transcribe-real-json input.json history.jsonl
node noko.mjs transcribe-raw-json input.f32 history.jsonl
node noko.mjs transcribe-capture-json seconds history.jsonl [device]
node noko.mjs transcribe-json input.json history.jsonl
env:
NOKO_DATA_DIR Noko-owned asset root, defaults to XDG data dir
NOKO_VAD_ONNX override path to Silero/equivalent VAD ONNX
NOKO_PARAKEET_DIR override Parakeet model directory
NOKO_NO_SYSTEM_INSTALL=1 skips package-manager install during setup
`;
}
function commandExists(name) {
const result = spawnSync('sh', ['-c', `command -v ${JSON.stringify(name)} >/dev/null 2>&1`]);
return result.status === 0;
}
const SYSTEM_DEPENDENCIES = [
{
command: 'pw-record',
label: 'mic capture',
required: true,
packages: {
arch: 'pipewire',
fedora: 'pipewire-utils',
debian: 'pipewire-bin'
}
},
{
command: 'wtype',
label: 'text insertion',
required: true,
packages: {
arch: 'wtype',
fedora: 'wtype',
debian: 'wtype'
}
}
];
function systemDependencyReport() {
return SYSTEM_DEPENDENCIES.map(dep => Object.assign({}, dep, {
exists: commandExists(dep.command)
}));
}
function missingSystemDependencies(report = systemDependencyReport()) {
return report.filter(dep => dep.required && !dep.exists).map(dep => dep.command);
}
function osIdLike() {
try {
const text = readFileSync('/etc/os-release', 'utf8');
const ids = [];
for (const key of ['ID', 'ID_LIKE']) {
const match = text.match(new RegExp(`^${key}=(.*)$`, 'm'));
if (match) ids.push(...match[1].replace(/^"|"$/g, '').split(/\s+/));
}
return ids;
} catch (_error) {
return [];
}
}
function packageFamily() {
const ids = osIdLike();
if (ids.includes('arch') || commandExists('pacman')) return 'arch';
if (ids.includes('fedora') || ids.includes('rhel') || commandExists('dnf')) return 'fedora';
if (ids.includes('debian') || ids.includes('ubuntu') || commandExists('apt-get')) return 'debian';
return '';
}
function unique(values) {
return Array.from(new Set(values.filter(Boolean)));
}
function systemInstallCommand(report = systemDependencyReport()) {
const missing = report.filter(dep => dep.required && !dep.exists);
if (missing.length === 0) return [];
const family = packageFamily();
const packages = unique(missing.map(dep => dep.packages && dep.packages[family]));
if (packages.length === 0) return [];
if (family === 'arch') return ['pkexec', 'pacman', '-S', '--needed', '--noconfirm', ...packages];
if (family === 'fedora') return ['pkexec', 'dnf', 'install', '-y', ...packages];
if (family === 'debian') return ['pkexec', 'apt-get', 'install', '-y', ...packages];
return [];
}
function systemDependencyStatus() {
const dependencies = systemDependencyReport();
return {
dependencies,
missing: missingSystemDependencies(dependencies),
installCommand: systemInstallCommand(dependencies),
packageFamily: packageFamily(),
canInstall: systemInstallCommand(dependencies).length > 0 && commandExists('pkexec')
};
}
function installSystemDependencies(options = {}) {
const status = systemDependencyStatus();
emitProgress(options.emit, { event: 'system-deps-start', label: 'system tools', progress: 0.01, missing: status.missing });
if (status.missing.length === 0 || process.env.NOKO_NO_SYSTEM_INSTALL === '1') {
emitProgress(options.emit, { event: 'system-deps-done', label: 'system tools', progress: 0.08, skipped: true, missing: status.missing });
return Object.assign({ skipped: true }, status);
}
if (!status.canInstall) throw new Error(`missing system tools: ${status.missing.join(', ')}; no supported pkexec install command`);
const result = spawnSync(status.installCommand[0], status.installCommand.slice(1), {
encoding: 'utf8',
maxBuffer: 1024 * 1024 * 4
});
if (result.status !== 0) throw new Error(`system tool install failed: ${(result.stderr || result.stdout || '').trim()}`);
const after = systemDependencyStatus();
if (after.missing.length > 0) throw new Error(`system tools still missing: ${after.missing.join(', ')}`);
emitProgress(options.emit, { event: 'system-deps-done', label: 'system tools', progress: 0.08, skipped: false });
return Object.assign({ skipped: false }, after);
}
function dataDir() {
if (process.env.NOKO_DATA_DIR) return process.env.NOKO_DATA_DIR;
const root = process.env.XDG_DATA_HOME || `${process.env.HOME || '.'}/.local/share`;
return `${root}/nuguland/noko`;
}
const PARAKEET_REPO = process.env.NOKO_PARAKEET_REPO || 'https://huggingface.co/smcleod/parakeet-tdt-0.6b-v3-int8/resolve/main';
const ASSET_FILES = {
vad: {
file: 'silero_vad.onnx',
url: process.env.NOKO_VAD_URL || 'https://github.com/snakers4/silero-vad/raw/master/src/silero_vad/data/silero_vad.onnx'
},
parakeet: {
dir: 'parakeet-tdt-0.6b-v3-int8',
files: {
config: { file: 'config.json', url: `${PARAKEET_REPO}/config.json` },
preprocessor: { file: 'nemo128.onnx', url: `${PARAKEET_REPO}/nemo128.onnx` },
encoder: { file: 'encoder-model.int8.onnx', url: `${PARAKEET_REPO}/encoder-model.int8.onnx` },
decoderJoint: { file: 'decoder_joint-model.int8.onnx', url: `${PARAKEET_REPO}/decoder_joint-model.int8.onnx` },
vocab: { file: 'vocab.txt', url: `${PARAKEET_REPO}/vocab.txt` }
}
}
};
function defaultVadPath() {
return `${dataDir()}/vad/${ASSET_FILES.vad.file}`;
}
function defaultParakeetDir() {
return `${dataDir()}/${ASSET_FILES.parakeet.dir}`;
}
function runtimeDir() {
return `${dataDir()}/runtime`;
}
function runtimeModulePath() {
return `${runtimeDir()}/node_modules/onnxruntime-node`;
}
function resolvedVadPath() {
return process.env.NOKO_VAD_ONNX || defaultVadPath();
}
function resolvedParakeetDir() {
return process.env.NOKO_PARAKEET_DIR || defaultParakeetDir();
}
function fileStatus(path) {
try {
const stat = statSync(path);
return { path, exists: true, size: stat.size };
} catch (_error) {
return { path, exists: false, size: 0 };
}
}
function assetFiles(report = null) {
const current = report || assetReport();
return [{ key: 'vad', url: current.vad.url, path: current.vad.path }]
.concat(Object.entries(current.parakeet.files).map(([key, value]) => Object.assign({ key }, value)));
}
function assetReport() {
const root = dataDir();
const parakeetPath = resolvedParakeetDir();
const parakeetFiles = Object.fromEntries(Object.entries(ASSET_FILES.parakeet.files).map(([key, value]) => [key, Object.assign({
url: value.url
}, fileStatus(`${parakeetPath}/${value.file}`))]));
return {
dataDir: root,
vad: Object.assign({ url: ASSET_FILES.vad.url }, fileStatus(resolvedVadPath())),
parakeet: {
path: parakeetPath,
files: parakeetFiles,
complete: Object.values(parakeetFiles).every(file => file.exists)
},
runtime: Object.assign({
package: 'onnxruntime-node@1.27.0'
}, fileStatus(runtimeModulePath()))
};
}
async function remoteStatus(file) {
try {
const response = await fetch(file.url, { method: 'HEAD' });
if (!response.ok) return { key: file.key, url: file.url, path: file.path, ok: false, error: `http ${response.status}`, updateAvailable: false };
const remoteSize = Number(response.headers.get('content-length') || 0);
const local = fileStatus(file.path);
return {
key: file.key,
url: file.url,
path: file.path,
exists: local.exists,
localSize: local.size,
remoteSize,
etag: response.headers.get('etag') || '',
lastModified: response.headers.get('last-modified') || '',
ok: true,
updateAvailable: !local.exists || (remoteSize > 0 && local.size !== remoteSize)
};
} catch (error) {
return { key: file.key, url: file.url, path: file.path, ok: false, error: String(error && error.message ? error.message : error), updateAvailable: false };
}
}
async function checkAssets() {
const assets = assetReport();
const files = Object.fromEntries(await Promise.all(assetFiles(assets).map(async file => [file.key, await remoteStatus(file)])));
return {
checkedAt: new Date().toISOString(),
assets,
files,
updateAvailable: Object.values(files).some(file => file.updateAvailable),
unknown: Object.values(files).filter(file => !file.ok).map(file => file.key)
};
}
function emitProgress(emit, event) {
if (emit) emit(event);
}
async function writeStream(stream, path, emit, base) {
const out = createWriteStream(path);
let loaded = 0;
for await (const chunk of Readable.fromWeb(stream)) {
loaded += chunk.length;
if (!out.write(chunk)) await new Promise(resolve => out.once('drain', resolve));
emitProgress(emit, Object.assign({}, base, {
event: 'download',
loaded,
progress: base.totalBytes > 0 ? loaded / base.totalBytes : base.progress
}));
}
await new Promise((resolve, reject) => out.end(error => error ? reject(error) : resolve()));
}
async function downloadFile(url, path, options = {}) {
if (options.force && existsSync(path)) unlinkSync(path);
if (existsSync(path)) {
emitProgress(options.emit, { event: 'skip', label: options.label || path, progress: options.progress || 1, path });
return { path, skipped: true };
}
mkdirSync(path.slice(0, path.lastIndexOf('/')), { recursive: true });
const tmp = `${path}.tmp-${process.pid}`;
const response = await fetch(url);
if (!response.ok || !response.body) throw new Error(`download failed ${response.status} ${url}`);
const totalBytes = Number(response.headers.get('content-length') || 0);
emitProgress(options.emit, { event: 'download-start', label: options.label || path, progress: options.progress || 0, totalBytes, path });
await writeStream(response.body, tmp, options.emit, {
label: options.label || path,
progress: options.progress || 0,
totalBytes,
path
});
renameSync(tmp, path);
emitProgress(options.emit, { event: 'download-done', label: options.label || path, progress: options.doneProgress || 1, path });
return { path, skipped: false };
}
function installRuntime(options = {}) {
emitProgress(options.emit, { event: 'runtime-start', label: 'runtime', progress: 0.08, path: runtimeModulePath() });
if (existsSync(runtimeModulePath())) {
emitProgress(options.emit, { event: 'runtime-done', label: 'runtime', progress: 0.18, path: runtimeModulePath(), skipped: true });
return { path: runtimeModulePath(), skipped: true };
}
mkdirSync(runtimeDir(), { recursive: true });
const result = spawnSync('npm', ['install', '--prefix', runtimeDir(), 'onnxruntime-node@1.27.0'], {
encoding: 'utf8',
maxBuffer: 1024 * 1024 * 8
});
if (result.status !== 0) throw new Error(`runtime install failed: ${(result.stderr || result.stdout || '').trim()}`);
emitProgress(options.emit, { event: 'runtime-done', label: 'runtime', progress: 0.18, path: runtimeModulePath(), skipped: false });
return { path: runtimeModulePath(), skipped: false };
}
async function installAssets(options = {}) {
const report = assetReport();
const results = [];
const emit = options.emit;
const files = assetFiles(report);
emitProgress(emit, { event: 'start', label: options.force ? 'update' : 'install', progress: 0 });
try {
if (!options.force) results.push(installSystemDependencies({ emit }));
results.push(installRuntime({ emit }));
for (let i = 0; i < files.length; i++) {
const start = 0.18 + i / files.length * 0.82;
const end = 0.18 + (i + 1) / files.length * 0.82;
results.push(await downloadFile(files[i].url, files[i].path, {
label: files[i].key,
progress: start,
doneProgress: end,
emit,
force: options.force
}));
}
} catch (error) {
for (const file of [assetReport().vad].concat(Object.values(assetReport().parakeet.files))) {
const tmp = `${file.path}.tmp-${process.pid}`;
if (existsSync(tmp)) unlinkSync(tmp);
}
throw error;
}
const done = { event: 'done', progress: 1, installed: results, assets: assetReport() };
emitProgress(emit, done);
return { installed: results, assets: done.assets };
}
function jsonLine(event) {
process.stdout.write(JSON.stringify(event) + '\n');
}
function recursiveNames(dir, prefix = '') {
if (!dir || !existsSync(dir)) return [];
const out = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const name = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isDirectory()) out.push(...recursiveNames(`${dir}/${entry.name}`, name));
else out.push(name);
}
return out;
}
async function readable(path) {
if (!path) return false;
try {
await access(path, constants.R_OK);
return true;
} catch (_error) {
return false;
}
}
async function doctor() {
const vadPath = resolvedVadPath();
const modelDir = resolvedParakeetDir();
const modelFiles = recursiveNames(modelDir);
const model = discoverParakeetDir(modelDir);
const conceptualModel = Noko.validateParakeetModelFiles(modelFiles);
const onnxRuntime = await loadOnnxRuntime().then(() => true, () => false);
const sentencePiece = (() => {
try {
loadSentencePiece();
return true;
} catch (_error) {
return false;
}
})();
const vadDetails = vadPath && onnxRuntime ? await vadRuntimeReport(vadPath) : null;
const systemDependencies = systemDependencyReport();
const missingSystem = missingSystemDependencies(systemDependencies);
const report = {
systemDependencies,
missingSystemDependencies: missingSystem,
capture: {
pwRecord: commandExists('pw-record'),
parec: commandExists('parec')
},
vad: Object.assign({
path: vadPath,
readable: await readable(vadPath)
}, vadDetails ? {
loadable: vadDetails.loadable,
inputNames: vadDetails.inputNames,
outputNames: vadDetails.outputNames,
stateful: vadDetails.stateful,
srInput: vadDetails.srInput,
frameSamples: vadDetails.frameSamples,
smokeProbability: vadDetails.smokeProbability,
error: vadDetails.error
} : {}),
parakeet: {
path: modelDir,
readable: await readable(modelDir),
ok: model.ok,
missing: model.missing,
files: model.files,
conceptualMissing: conceptualModel.missing
},
runtime: {
onnxruntimeNode: onnxRuntime,
sentencepieceJs: sentencePiece
}
};
const needsSentencePiece = Boolean(model.files.tokenizer && model.files.tokenizer.endsWith('.model'));
const parakeetRuntimeOk = report.runtime.onnxruntimeNode && (!needsSentencePiece || report.runtime.sentencepieceJs);
report.parakeet.needsSentencePiece = needsSentencePiece;
report.ok = missingSystem.length === 0 && report.vad.readable && report.parakeet.ok && parakeetRuntimeOk;
return report;
}
function captureSamples(secondsArg, device) {
const seconds = Math.max(0.03, Math.min(60, Number(secondsArg)));
if (!isFinite(seconds)) throw new Error('seconds must be a number');
const sampleCount = Math.round(seconds * Noko.TARGET_RATE);
const argv = ['--raw', '--rate', String(Noko.TARGET_RATE), '--channels', '1', '--format', 'f32', '--sample-count', String(sampleCount)];
if (device) argv.push('--target', String(device));
argv.push('-');
const result = spawnSync('pw-record', argv, {
encoding: 'buffer',
maxBuffer: Math.max(1024 * 1024, sampleCount * 8)
});
if (result.status !== 0 && result.stdout.length === 0) {
const err = result.stderr ? result.stderr.toString('utf8').trim() : '';
throw new Error(`pw-record failed${err ? `: ${err}` : ''}`);
}
return Noko.float32leToFloat32(result.stdout.subarray(0, sampleCount * 4), 1);
}
function capturePayload(secondsArg, device) {
const samples = captureSamples(secondsArg, device);
return {
sampleRate: Noko.TARGET_RATE,
channels: 1,
format: 'float32-mono-json',
samples: Array.from(samples)
};
}
function captureJson(secondsArg, outputPath, device) {
if (!secondsArg || !outputPath) throw new Error(usage());
const payload = capturePayload(secondsArg, device);
writeFileSync(outputPath, JSON.stringify(payload));
return {
outputPath,
sampleRate: Noko.TARGET_RATE,
channels: 1,
sampleCount: payload.samples.length,
duration: Math.round(payload.samples.length / Noko.TARGET_RATE * 1000) / 1000
};
}
function normalizedSamplesFromJson(inputPath) {
const input = JSON.parse(readFileSync(inputPath, 'utf8'));
const mono = Noko.interleavedToMonoFloat32(new Float32Array(input.samples || []), input.channels || 1);
return Noko.resampleLinear(mono, input.sampleRate || Noko.TARGET_RATE, Noko.TARGET_RATE);
}
function samplesFromRawFloat32(path) {
return Noko.float32leToFloat32(readFileSync(path), 1);
}
async function vadJson(inputPath, outputPath) {
if (!inputPath || !outputPath) throw new Error(usage());
const samples = normalizedSamplesFromJson(inputPath);
const vad = await OnnxVad.create(resolvedVadPath());
const probabilities = await vadProbabilities(samples, { vad });
writeFileSync(outputPath, JSON.stringify({
sampleRate: Noko.TARGET_RATE,
frameSamples: Noko.FRAME_SAMPLES,
frameCount: probabilities.length,
probabilities
}));
return {
outputPath,
frameSamples: Noko.FRAME_SAMPLES,
frameCount: probabilities.length
};
}
async function vadGateJson(inputPath, outputPath) {
if (!inputPath || !outputPath) throw new Error(usage());
const samples = normalizedSamplesFromJson(inputPath);
const frames = Noko.splitFrames(samples, Noko.FRAME_SAMPLES);
const vad = await OnnxVad.create(resolvedVadPath());
const probabilities = await vadProbabilities(samples, { vad });
const gated = Noko.vadGate(frames, probabilities, {
threshold: 0.3,
preRoll: 15,
onset: 2,
hangover: 15
});
writeFileSync(outputPath, JSON.stringify({
sampleRate: Noko.TARGET_RATE,
frameSamples: Noko.FRAME_SAMPLES,
inputFrameCount: frames.length,
speechFrameCount: gated.speechFrameCount,
frameIndexes: gated.frameIndexes,
probabilities,
samples: Array.from(gated.samples)
}));
return {
outputPath,
inputFrameCount: frames.length,
speechFrameCount: gated.speechFrameCount,
sampleCount: gated.samples.length
};
}
async function transcribeSamples(samples, historyPath) {
if (!historyPath) throw new Error(usage());
samples = samples instanceof Float32Array ? samples : new Float32Array(samples);
const frames = Noko.splitFrames(samples, Noko.FRAME_SAMPLES);
const vad = await OnnxVad.create(resolvedVadPath());
const probabilities = await vadProbabilities(samples, { vad });
const gated = Noko.vadGate(frames, probabilities, {
threshold: 0.3,
preRoll: 15,
onset: 2,
hangover: 15
});
if (gated.samples.length === 0) throw new Error('empty speech buffer after vad');
const raw = await (await ParakeetOnnx.create(resolvedParakeetDir())).transcribe(gated.samples);
const text = Noko.postprocess(raw && raw.text !== undefined ? raw.text : raw);
if (!text) throw new Error('empty transcript');
const record = Noko.historyRecord(text, gated.samples, Object.assign({
runtimeProvider: 'onnxruntime-node'
}, raw && raw.meta ? raw.meta : {}));
appendFileSync(historyPath, JSON.stringify(record) + '\n');
return {
text,
frameIndexes: gated.frameIndexes,
history: record
};
}
async function transcribeRealJson(inputPath, historyPath) {
if (!inputPath || !historyPath) throw new Error(usage());
return transcribeSamples(normalizedSamplesFromJson(inputPath), historyPath);
}
async function transcribeRawJson(inputPath, historyPath) {
if (!inputPath || !historyPath) throw new Error(usage());
return transcribeSamples(samplesFromRawFloat32(inputPath), historyPath);
}
async function transcribeCaptureJson(secondsArg, historyPath, device) {
if (!secondsArg || !historyPath) throw new Error(usage());
return transcribeSamples(capturePayload(secondsArg, device).samples, historyPath);
}
function transcribeJson(inputPath, historyPath) {
if (!inputPath || !historyPath) throw new Error(usage());
const input = JSON.parse(readFileSync(inputPath, 'utf8'));
const probabilities = input.probabilities || [];
const result = Noko.transcribePipeline({
samples: new Float32Array(input.samples || []),
sampleRate: input.sampleRate || Noko.TARGET_RATE,
channels: input.channels || 1
}, {
probability: (_frame, index) => Number(probabilities[index] || 0)
}, {
transcribe: () => ({
text: String(input.transcript || ''),
meta: {
model: input.model || 'json-fixture',
quantization: input.quantization || 'fixture',
runtimeProvider: 'fixture'
}
})
}, {
write: record => appendFileSync(historyPath, JSON.stringify(record) + '\n')
}, input.options || {});
return result;
}
async function main(argv) {
const command = argv[2] || 'help';
if (command === 'doctor') return doctor();
if (command === 'system-deps-json') return systemDependencyStatus();
if (command === 'assets-json') return assetReport();
if (command === 'check-assets-json') return checkAssets();
if (command === 'install-assets') return installAssets();
if (command === 'update-assets') return installAssets({ force: true });
if (command === 'install-assets-progress') {
await installAssets({ emit: jsonLine });
return null;
}
if (command === 'update-assets-progress') {
await installAssets({ force: true, emit: jsonLine });
return null;
}
if (command === 'capture-json') return captureJson(argv[3], argv[4], argv[5]);
if (command === 'vad-json') return vadJson(argv[3], argv[4]);
if (command === 'vad-gate-json') return vadGateJson(argv[3], argv[4]);
if (command === 'transcribe-real-json') return transcribeRealJson(argv[3], argv[4]);
if (command === 'transcribe-raw-json') return transcribeRawJson(argv[3], argv[4]);
if (command === 'transcribe-capture-json') return transcribeCaptureJson(argv[3], argv[4], argv[5]);
if (command === 'transcribe-json') return transcribeJson(argv[3], argv[4]);
if (command === 'help' || command === '--help' || command === '-h') return usage();
throw new Error(`unknown command: ${command}\n${usage()}`);
}
main(process.argv).then(result => {
if (result === null || result === undefined) return;
if (typeof result === 'string') {
process.stdout.write(result);
return;
}
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
}).catch(error => {
process.stderr.write(String(error && error.message ? error.message : error) + '\n');
process.exit(1);
});