171 lines
5.5 KiB
JavaScript
Executable File
171 lines
5.5 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// Drift check between the TypeScript source-of-truth registry
|
|
// (apps/web/src/media/models.ts) and the TS mirror used by the Node daemon
|
|
// (apps/daemon/src/media-models.ts). The two are kept in sync by hand because the
|
|
// daemon avoids a TS toolchain at runtime; this script lets CI fail the
|
|
// build the moment they diverge.
|
|
//
|
|
// Usage:
|
|
// node scripts/verify-media-models.mjs
|
|
//
|
|
// Exit codes:
|
|
// 0 — registries match
|
|
// 1 — drift detected (diff printed to stderr)
|
|
// 2 — could not parse one of the registry files
|
|
|
|
import { readFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
const TS_PATH = path.join(ROOT, 'apps', 'web', 'src', 'media', 'models.ts');
|
|
const JS_PATH = path.join(ROOT, 'apps', 'daemon', 'src', 'media-models.ts');
|
|
|
|
function fail(msg) {
|
|
process.stderr.write(`verify-media-models: ${msg}\n`);
|
|
process.exit(1);
|
|
}
|
|
|
|
function parseError(msg) {
|
|
process.stderr.write(`verify-media-models: ${msg}\n`);
|
|
process.exit(2);
|
|
}
|
|
|
|
// Pull a top-level array literal of `{ id: 'x', ... }` records out of the
|
|
// source. We deliberately avoid spinning up a TS compiler — we only need
|
|
// the IDs and the bucket shapes the two files agree on.
|
|
function extractIds(source, name) {
|
|
const re = new RegExp(`export const ${name}[^=]*=\\s*\\[([\\s\\S]*?)\\];`, 'm');
|
|
const m = source.match(re);
|
|
if (!m) return null;
|
|
const ids = [];
|
|
const idRe = /\bid:\s*['\"]([^'\"]+)['\"]/g;
|
|
let id;
|
|
while ((id = idRe.exec(m[1])) != null) ids.push(id[1]);
|
|
return ids;
|
|
}
|
|
|
|
function extractAudioIds(source) {
|
|
const re = /export const AUDIO_MODELS_BY_KIND[^=]*=\s*\{([\s\S]*?)\n\};/m;
|
|
const m = source.match(re);
|
|
if (!m) return null;
|
|
const body = m[1];
|
|
const out = {};
|
|
for (const kind of ['music', 'speech', 'sfx']) {
|
|
const kre = new RegExp(`${kind}\\s*:\\s*\\[([\\s\\S]*?)\\]`, 'm');
|
|
const km = body.match(kre);
|
|
if (!km) return null;
|
|
const ids = [];
|
|
const idRe = /\bid:\s*['\"]([^'\"]+)['\"]/g;
|
|
let id;
|
|
while ((id = idRe.exec(km[1])) != null) ids.push(id[1]);
|
|
out[kind] = ids;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function extractNumberArray(source, name) {
|
|
const re = new RegExp(`export const ${name}[^=]*=\\s*\\[([^\\]]*)\\]`, 'm');
|
|
const m = source.match(re);
|
|
if (!m) return null;
|
|
return m[1]
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)
|
|
.map(Number)
|
|
.filter((n) => Number.isFinite(n));
|
|
}
|
|
|
|
function dedupCheck(label, ids) {
|
|
const seen = new Set();
|
|
for (const id of ids) {
|
|
if (seen.has(id)) fail(`duplicate id "${id}" in ${label}`);
|
|
seen.add(id);
|
|
}
|
|
if (ids.length === 0) fail(`${label} is empty`);
|
|
}
|
|
|
|
let ts;
|
|
let js;
|
|
try {
|
|
ts = readFileSync(TS_PATH, 'utf8');
|
|
} catch (err) {
|
|
parseError(`could not read ${TS_PATH}: ${err.message}`);
|
|
}
|
|
try {
|
|
js = readFileSync(JS_PATH, 'utf8');
|
|
} catch (err) {
|
|
parseError(`could not read ${JS_PATH}: ${err.message}`);
|
|
}
|
|
|
|
const tsImage = extractIds(ts, 'IMAGE_MODELS');
|
|
const tsVideo = extractIds(ts, 'VIDEO_MODELS');
|
|
const tsAudio = extractAudioIds(ts);
|
|
const tsLengths = extractNumberArray(ts, 'VIDEO_LENGTHS_SEC');
|
|
const tsDurations = extractNumberArray(ts, 'AUDIO_DURATIONS_SEC');
|
|
|
|
const jsImage = extractIds(js, 'IMAGE_MODELS');
|
|
const jsVideo = extractIds(js, 'VIDEO_MODELS');
|
|
const jsAudio = extractAudioIds(js);
|
|
const jsLengths = extractNumberArray(js, 'VIDEO_LENGTHS_SEC');
|
|
const jsDurations = extractNumberArray(js, 'AUDIO_DURATIONS_SEC');
|
|
|
|
if (!tsImage || !tsVideo || !tsAudio) parseError('failed to parse TS registry');
|
|
if (!jsImage || !jsVideo || !jsAudio) parseError('failed to parse JS registry');
|
|
|
|
dedupCheck('IMAGE_MODELS (ts)', tsImage);
|
|
dedupCheck('VIDEO_MODELS (ts)', tsVideo);
|
|
dedupCheck('IMAGE_MODELS (js)', jsImage);
|
|
dedupCheck('VIDEO_MODELS (js)', jsVideo);
|
|
for (const kind of ['music', 'speech', 'sfx']) {
|
|
dedupCheck(`AUDIO_MODELS_BY_KIND.${kind} (ts)`, tsAudio[kind]);
|
|
dedupCheck(`AUDIO_MODELS_BY_KIND.${kind} (js)`, jsAudio[kind]);
|
|
}
|
|
|
|
function diffArrays(label, a, b) {
|
|
const aSet = new Set(a);
|
|
const bSet = new Set(b);
|
|
const onlyA = [...aSet].filter((x) => !bSet.has(x));
|
|
const onlyB = [...bSet].filter((x) => !aSet.has(x));
|
|
if (onlyA.length === 0 && onlyB.length === 0) return null;
|
|
return `${label}: ts only=[${onlyA.join(', ')}], js only=[${onlyB.join(', ')}]`;
|
|
}
|
|
|
|
const diffs = [];
|
|
const dImage = diffArrays('IMAGE_MODELS', tsImage, jsImage);
|
|
if (dImage) diffs.push(dImage);
|
|
const dVideo = diffArrays('VIDEO_MODELS', tsVideo, jsVideo);
|
|
if (dVideo) diffs.push(dVideo);
|
|
for (const kind of ['music', 'speech', 'sfx']) {
|
|
const d = diffArrays(`AUDIO_MODELS_BY_KIND.${kind}`, tsAudio[kind], jsAudio[kind]);
|
|
if (d) diffs.push(d);
|
|
}
|
|
if (tsLengths && jsLengths && tsLengths.join(',') !== jsLengths.join(',')) {
|
|
diffs.push(
|
|
`VIDEO_LENGTHS_SEC: ts=[${tsLengths.join(', ')}] js=[${jsLengths.join(', ')}]`,
|
|
);
|
|
}
|
|
if (
|
|
tsDurations &&
|
|
jsDurations &&
|
|
tsDurations.join(',') !== jsDurations.join(',')
|
|
) {
|
|
diffs.push(
|
|
`AUDIO_DURATIONS_SEC: ts=[${tsDurations.join(', ')}] js=[${jsDurations.join(', ')}]`,
|
|
);
|
|
}
|
|
|
|
if (diffs.length > 0) {
|
|
process.stderr.write(
|
|
'verify-media-models: drift detected between apps/web/src/media/models.ts and apps/daemon/src/media-models.ts\n',
|
|
);
|
|
for (const d of diffs) process.stderr.write(` - ${d}\n`);
|
|
process.stderr.write(
|
|
'\nFix: update both files in lockstep, then re-run this script.\n',
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
process.stdout.write('verify-media-models: OK (TS + JS registries match)\n');
|