2026-06-22 16:40:23 +08:00
|
|
|
|
#!/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');
|
2026-06-22 20:34:23 +08:00
|
|
|
|
const DIRECTOR_ENCODING_V2 = path.join(OUT, 'ep01-director-encoding-3d-v2.json');
|
|
|
|
|
|
const PROMPTS_V2 = path.join(OUT, 'ep01-prompts-3d-v2.json');
|
2026-06-22 16:40:23 +08:00
|
|
|
|
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 });
|
|
|
|
|
|
}
|
2026-06-22 20:34:23 +08:00
|
|
|
|
if (!data.directorEncoding.exists && !data.directorEncodingV2.exists) {
|
|
|
|
|
|
issues.push({ level: 'red', title: '导演编码缺失', detail: `${DIRECTOR_ENCODING} / ${DIRECTOR_ENCODING_V2}` });
|
2026-06-22 16:40:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
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秒成品。',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-06-22 20:34:23 +08:00
|
|
|
|
if (!data.promptsV2.exists) {
|
|
|
|
|
|
issues.push({
|
|
|
|
|
|
level: 'red',
|
|
|
|
|
|
title: '3D主线提示词缺失',
|
|
|
|
|
|
detail: PROMPTS_V2,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data.promptsStillRealistic && !data.promptsV2.exists) {
|
2026-06-22 16:40:23 +08:00
|
|
|
|
issues.push({
|
|
|
|
|
|
level: 'yellow',
|
|
|
|
|
|
title: '旧提示词仍是真人写实',
|
2026-06-22 20:34:23 +08:00
|
|
|
|
detail: 'ep01-prompts.json属于旧线,不能作为3D主线继续生成;且尚未发现ep01-prompts-3d-v2.json。',
|
2026-06-22 16:40:23 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
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, {});
|
2026-06-22 20:34:23 +08:00
|
|
|
|
const encodingV2 = readJson(DIRECTOR_ENCODING_V2, {});
|
|
|
|
|
|
const promptsV2 = readJson(PROMPTS_V2, {});
|
2026-06-22 16:40:23 +08:00
|
|
|
|
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,
|
|
|
|
|
|
},
|
2026-06-22 20:34:23 +08:00
|
|
|
|
directorEncodingV2: {
|
|
|
|
|
|
exists: exists(DIRECTOR_ENCODING_V2),
|
|
|
|
|
|
file: DIRECTOR_ENCODING_V2,
|
|
|
|
|
|
shots: encodingV2.shots?.length || 0,
|
|
|
|
|
|
targetDuration: encodingV2.target_duration || null,
|
|
|
|
|
|
status: encodingV2._meta?.status || null,
|
|
|
|
|
|
},
|
|
|
|
|
|
promptsV2: {
|
|
|
|
|
|
exists: exists(PROMPTS_V2),
|
|
|
|
|
|
file: PROMPTS_V2,
|
|
|
|
|
|
shots: promptsV2.shots?.length || 0,
|
|
|
|
|
|
status: promptsV2._meta?.status || null,
|
|
|
|
|
|
},
|
2026-06-22 16:40:23 +08:00
|
|
|
|
localFinals: localVideos.filter(v => v.file.includes('/outputs/final/')),
|
|
|
|
|
|
localVideos,
|
|
|
|
|
|
jzaoVideos,
|
|
|
|
|
|
jzao3dVideos,
|
2026-06-22 16:48:06 +08:00
|
|
|
|
legacyOutputPolicy: '旧JZAO视频产物全部废弃,只作为踩坑样本和反例参考,不进入D140 V2生产线。',
|
2026-06-22 16:40:23 +08:00
|
|
|
|
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`);
|
2026-06-22 20:34:23 +08:00
|
|
|
|
lines.push(`导演编码: 旧${data.directorEncoding.shots}镜 / V2 ${data.directorEncodingV2.shots}镜`);
|
|
|
|
|
|
lines.push(`3D提示词V2: ${data.promptsV2.exists ? `${data.promptsV2.shots}镜` : '缺失'}`);
|
2026-06-22 16:40:23 +08:00
|
|
|
|
lines.push(`本地产物: ${data.localVideos.length}个mp4`);
|
|
|
|
|
|
lines.push(`JZAO ep01产物: ${data.jzaoVideos.length}个mp4`);
|
|
|
|
|
|
lines.push(`JZAO ep01-3D产物: ${data.jzao3dVideos.length}个mp4`);
|
2026-06-22 16:48:06 +08:00
|
|
|
|
lines.push(`旧产物裁决: ${data.legacyOutputPolicy}`);
|
2026-06-22 16:40:23 +08:00
|
|
|
|
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制作线。');
|
2026-06-22 20:34:23 +08:00
|
|
|
|
lines.push(data.promptsV2.exists
|
|
|
|
|
|
? '2. 旧真人提示词不再继续生成;使用 ep01-prompts-3d-v2.json 作为主线。'
|
|
|
|
|
|
: '2. 旧真人提示词不再继续生成,重新生成3D主线提示词。');
|
2026-06-22 16:48:06 +08:00
|
|
|
|
lines.push('3. 旧JZAO视频不进入生产线,只作为反例经验。');
|
2026-06-22 20:34:23 +08:00
|
|
|
|
lines.push(data.directorEncodingV2.exists && data.promptsV2.exists
|
|
|
|
|
|
? '4. 下一步生成/确认CHAR-003、ENV-002、PROP牌匾/广告牌视觉锚点。'
|
|
|
|
|
|
: '4. 按 PIPELINE-3D-MANJU.hdlp 重建 EP01-15S-V2。');
|
2026-06-22 16:48:06 +08:00
|
|
|
|
lines.push('5. 状态文件必须由体检报告回写,不再靠人工记忆。');
|
2026-06-22 20:34:23 +08:00
|
|
|
|
lines.push('6. 视觉锚点和生成前体检通过后才调用API。');
|
2026-06-22 16:40:23 +08:00
|
|
|
|
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();
|