import fs from 'node:fs' import path from 'node:path' import { execFileSync } from 'node:child_process' const packageDirs = process.argv.slice(2).map((item) => path.resolve(item)) if (!packageDirs.length) { console.error('Usage: node tools/validate_battle_asset_packs.mjs [...]') process.exit(2) } const failures = [] const assert = (condition, message) => { if (!condition) failures.push(message) } function dimensions(file) { const output = execFileSync('sips', ['-g', 'pixelWidth', '-g', 'pixelHeight', file], { encoding: 'utf8' }) const width = Number(output.match(/pixelWidth: (\d+)/)?.[1]) const height = Number(output.match(/pixelHeight: (\d+)/)?.[1]) return { width, height } } for (const packageDir of packageDirs) { const label = path.basename(packageDir) const manifestPath = path.join(packageDir, 'manifest.json') const readmePath = path.join(packageDir, '素材说明.md') assert(fs.existsSync(manifestPath), `${label}: missing manifest.json`) assert(fs.existsSync(readmePath), `${label}: missing 素材说明.md`) if (!fs.existsSync(manifestPath)) continue let manifest try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) } catch (error) { failures.push(`${label}: invalid JSON (${error.message})`) continue } assert(manifest.event?.canonicalName === label, `${label}: canonicalName must match directory name`) assert(manifest.narration?.language === 'zh-CN', `${label}: narration language must be zh-CN`) assert(manifest.narration?.estimatedSeconds >= 60 && manifest.narration?.estimatedSeconds <= 90, `${label}: narration must be estimated at 60–90 seconds`) assert(typeof manifest.narration?.fullText === 'string' && manifest.narration.fullText.length >= 180, `${label}: narration is unexpectedly short`) assert(Array.isArray(manifest.scenes) && manifest.scenes.length >= 3 && manifest.scenes.length <= 5, `${label}: scene count must be 3–5`) assert(Array.isArray(manifest.sources) && manifest.sources.length > 0, `${label}: package sources are required`) assert(Array.isArray(manifest.blacklist) && manifest.blacklist.length > 0, `${label}: blacklist is required`) assert(manifest.bgm?.catalogId?.length > 0, `${label}: BGM catalogId is required`) const ids = new Set() for (const scene of manifest.scenes || []) { const prefix = `${label}/${scene.id || '?'}` assert(!ids.has(scene.id), `${prefix}: duplicate scene id`) ids.add(scene.id) assert(scene.title && scene.timePlace && scene.purpose && scene.narration, `${prefix}: scene metadata incomplete`) assert(scene.overlay?.heading && scene.overlay?.line, `${prefix}: overlay copy incomplete`) assert(Array.isArray(scene.people) && scene.people.length > 0, `${prefix}: people tags required`) assert(Array.isArray(scene.sources) && scene.sources.length > 0, `${prefix}: scene sources required`) assert(scene.image?.alt && scene.image?.prompt && scene.image?.visualInference, `${prefix}: image metadata incomplete`) for (const key of ['original', 'delivery']) { const relative = scene.image?.[key] assert(typeof relative === 'string' && relative.startsWith('images/'), `${prefix}: invalid image.${key}`) if (!relative) continue const file = path.join(packageDir, relative) assert(fs.existsSync(file), `${prefix}: missing ${relative}`) if (!fs.existsSync(file)) continue const { width, height } = dimensions(file) assert(width > 0 && height > 0, `${prefix}: could not read dimensions for ${relative}`) assert(Math.abs(width / height - 16 / 9) < 0.002, `${prefix}: ${relative} is ${width}x${height}, not 16:9`) } } console.log(`checked ${label}: ${manifest.scenes?.length || 0} scenes`) } if (failures.length) { console.error(`\n${failures.length} validation failure(s):`) for (const failure of failures) console.error(`- ${failure}`) process.exit(1) } console.log(`all ${packageDirs.length} package(s) passed validation`)