guanghulab/video-ai-system/tools/audit-system.js
冰朔 a1a20aea2a
Some checks failed
自动更新代码和重启 / update-and-restart (push) Has been cancelled
CI检查 + 自动部署 / check (push) Has been cancelled
CI检查 + 自动部署 / deploy (push) Has been cancelled
D140: switch video AI to 3D audit path
2026-06-22 16:40:23 +08:00

253 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* 光湖视频AI系统 · 无成本体检脚本
* D140 · Codex收口层
*
* 只扫描本地仓库和JZAO外置盘不调用任何视频/图片生成API。
* 目标: 把剧本、分镜、导演编码、产物、注册表、制作线状态对齐成一张报告。
*/
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const ROOT = path.resolve(__dirname, '../..');
const SYS = path.join(ROOT, 'video-ai-system');
const OUT = path.join(SYS, 'outputs');
const SCRIPT_MD = path.join(ROOT, '动态漫:《付费才能修仙?我的宗门全免费》.md');
const PRODUCT_LINES = path.join(SYS, 'config/product-lines.json');
const STORYBOARD = path.join(SYS, 'data/ep01-storyboard.json');
const DIRECTOR_ENCODING = path.join(OUT, '付费修仙-ep01-director-encoding.json');
const VIDEO_REGISTRY = path.join(OUT, 'video-registry.json');
const STATUS = path.join(SYS, 'memory/zai-fu-fei-xiu-xian/STATUS.hdlp');
const JZAO_EP01 = '/Volumes/JZAO/铸渊-ICE-GL-ZY001/OUT-输出/视频/zai-fu-fei-xiu-xian/ep01';
const JZAO_EP01_3D = '/Volumes/JZAO/铸渊-ICE-GL-ZY001/OUT-输出/视频/zai-fu-fei-xiu-xian/ep01-3D';
function readJson(file, fallback = null) {
try {
return JSON.parse(fs.readFileSync(file, 'utf8'));
} catch (e) {
return fallback;
}
}
function exists(file) {
return fs.existsSync(file);
}
function listFiles(dir, exts) {
if (!exists(dir)) return [];
const out = [];
const stack = [dir];
while (stack.length) {
const current = stack.pop();
for (const name of fs.readdirSync(current)) {
if (name.startsWith('._')) continue;
const p = path.join(current, name);
const stat = fs.statSync(p);
if (stat.isDirectory()) {
stack.push(p);
} else if (exts.includes(path.extname(name).toLowerCase())) {
out.push(p);
}
}
}
return out.sort();
}
function ffprobe(file) {
try {
const raw = execFileSync('ffprobe', [
'-v', 'quiet',
'-print_format', 'json',
'-show_format',
'-show_streams',
file,
], { encoding: 'utf8', timeout: 8000 });
const meta = JSON.parse(raw);
const stream = (meta.streams || []).find(s => s.codec_type === 'video') || {};
return {
duration: Number.parseFloat(meta.format?.duration || stream.duration || 0),
size: Number.parseInt(meta.format?.size || '0', 10),
width: stream.width || null,
height: stream.height || null,
codec: stream.codec_name || null,
};
} catch (e) {
return { error: e.message };
}
}
function summarizeVideos(files) {
return files.map(file => ({
file,
name: path.basename(file),
...ffprobe(file),
}));
}
function activeLines(config) {
const lines = config?.lines || {};
return Object.entries(lines)
.filter(([, v]) => v.active)
.map(([k, v]) => ({ key: k, name: v.name, status: v.status || 'active' }));
}
function detectIssues(data) {
const issues = [];
if (data.activeLines.length !== 1) {
issues.push({
level: 'red',
title: '制作线必须只有一条active',
detail: `当前active数量=${data.activeLines.length}`,
});
}
if (data.activeLines[0]?.key !== '3d') {
issues.push({
level: 'red',
title: '当前主线不是3D漫剧',
detail: '冰朔已决策注销真人线主线必须是3d。',
});
}
if (!data.script.exists) {
issues.push({ level: 'red', title: '剧本MD缺失', detail: SCRIPT_MD });
}
if (!data.storyboard.exists) {
issues.push({ level: 'red', title: '分镜JSON缺失', detail: STORYBOARD });
}
if (!data.directorEncoding.exists) {
issues.push({ level: 'red', title: '导演编码缺失', detail: DIRECTOR_ENCODING });
}
if (data.localFinals.length === 0 && data.jzaoVideos.length === 0 && data.jzao3dVideos.length === 0) {
issues.push({ level: 'red', title: '没有发现任何视频产物', detail: 'outputs和JZAO均为空。' });
}
if (data.statusClaimsInProgress && data.jzaoFound15s) {
issues.push({
level: 'yellow',
title: '状态文件滞后',
detail: 'STATUS仍写生成中但JZAO已发现D136 15秒成品。',
});
}
if (data.promptsStillRealistic) {
issues.push({
level: 'yellow',
title: '旧提示词仍是真人写实',
detail: 'ep01-prompts.json属于旧线不能作为3D主线继续生成。',
});
}
if (data.registryMissingExistingFiles.length > 0) {
issues.push({
level: 'yellow',
title: '注册表存在失效文件路径',
detail: `${data.registryMissingExistingFiles.length}条registry路径在当前机器上不存在。`,
});
}
return issues;
}
function main() {
const product = readJson(PRODUCT_LINES, {});
const storyboard = readJson(STORYBOARD, {});
const encoding = readJson(DIRECTOR_ENCODING, {});
const registry = readJson(VIDEO_REGISTRY, { shots: {} });
const promptsText = exists(path.join(SYS, 'data/ep01-prompts.json'))
? fs.readFileSync(path.join(SYS, 'data/ep01-prompts.json'), 'utf8')
: '';
const statusText = exists(STATUS) ? fs.readFileSync(STATUS, 'utf8') : '';
const statusClaimsInProgress = statusText
.split(/\r?\n/)
.some(line => /(\|.*(🔄|生成中|重新生成中))|(\[[ x]\].*(生成中|重新生成中))/.test(line));
const localVideos = summarizeVideos([
...listFiles(path.join(OUT, 'final'), ['.mp4']),
...listFiles(path.join(OUT, 'shots'), ['.mp4']),
...listFiles(OUT, ['.mp4']),
]);
const jzaoVideos = summarizeVideos(listFiles(JZAO_EP01, ['.mp4']));
const jzao3dVideos = summarizeVideos(listFiles(JZAO_EP01_3D, ['.mp4']));
const registryEntries = Object.values(registry.shots || {});
const registryMissingExistingFiles = registryEntries.filter(x => x.filePath && !exists(x.filePath));
const data = {
generatedAt: new Date().toISOString(),
activeLines: activeLines(product),
script: {
exists: exists(SCRIPT_MD),
file: SCRIPT_MD,
size: exists(SCRIPT_MD) ? fs.statSync(SCRIPT_MD).size : 0,
},
storyboard: {
exists: exists(STORYBOARD),
shots: storyboard.shots?.length || 0,
totalDuration: (storyboard.shots || []).reduce((s, x) => s + (Number(x.duration) || 0), 0),
},
directorEncoding: {
exists: exists(DIRECTOR_ENCODING),
shots: encoding.shots?.length || 0,
project: encoding.project,
episode: encoding.episode,
},
localFinals: localVideos.filter(v => v.file.includes('/outputs/final/')),
localVideos,
jzaoVideos,
jzao3dVideos,
jzaoFound15s: jzaoVideos.some(v => v.name.includes('15s') || v.name.includes('opening')),
registry: {
total: registryEntries.length,
missingFiles: registryMissingExistingFiles.length,
},
registryMissingExistingFiles,
statusClaimsInProgress,
promptsStillRealistic: /真人写实/.test(promptsText),
};
data.issues = detectIssues(data);
const jsonPath = path.join(OUT, 'system-audit-latest.json');
fs.writeFileSync(jsonPath, JSON.stringify(data, null, 2), 'utf8');
const lines = [];
lines.push('# 视频AI系统 · 无成本体检报告');
lines.push('');
lines.push(`生成时间: ${data.generatedAt}`);
lines.push('');
lines.push(`当前active制作线: ${data.activeLines.map(x => `${x.key}(${x.name})`).join(', ') || '无'}`);
lines.push(`剧本MD: ${data.script.exists ? '存在' : '缺失'}`);
lines.push(`分镜: ${data.storyboard.shots}镜 / ${data.storyboard.totalDuration}s`);
lines.push(`导演编码: ${data.directorEncoding.shots}`);
lines.push(`本地产物: ${data.localVideos.length}个mp4`);
lines.push(`JZAO ep01产物: ${data.jzaoVideos.length}个mp4`);
lines.push(`JZAO ep01-3D产物: ${data.jzao3dVideos.length}个mp4`);
lines.push(`注册表: ${data.registry.total}条 / 失效路径${data.registry.missingFiles}`);
lines.push('');
lines.push('## 问题');
if (data.issues.length === 0) {
lines.push('- 无红黄问题。');
} else {
for (const issue of data.issues) {
lines.push(`- ${issue.level.toUpperCase()} · ${issue.title}: ${issue.detail}`);
}
}
lines.push('');
lines.push('## 推荐下一步');
lines.push('1. 只保留3D漫剧为active制作线。');
lines.push('2. 旧真人提示词不再继续生成重新生成3D主线提示词。');
lines.push('3. 以JZAO中已存在的D136/3D产物为基准做铸渊之眼质检。');
lines.push('4. 状态文件必须由体检报告回写,不再靠人工记忆。');
lines.push('5. 生成API只在体检通过后调用。');
lines.push('');
const mdPath = path.join(OUT, 'system-audit-latest.md');
fs.writeFileSync(mdPath, lines.join('\n'), 'utf8');
console.log(`体检完成: ${jsonPath}`);
console.log(`报告: ${mdPath}`);
if (data.issues.length) {
console.log('发现问题:');
data.issues.forEach(i => console.log(`- ${i.level}: ${i.title}`));
} else {
console.log('未发现红黄问题。');
}
}
main();