Files
AI_spine/tools/model-check.mjs
T
2026-09-20 22:36:20 +08:00

153 lines
11 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
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import {loadConfig, resolveStage, publicStage, samInfo} from '../lib/config.mjs';
import {requestJson} from '../providers/http.mjs';
import {runUnderstanding, runCompletionProbe} from '../stages/model-stage.mjs';
import {readImage} from '../lib/images.mjs';
import {runSegmentation} from '../stages/segmentation-stage.mjs';
import {runCompletion} from '../stages/completion-stage.mjs';
const help = `模型接入与结构理解(新增模式,不调用旧拆件流程)
node pipeline.mjs --mode annotated-parts --reference image.png --out-dir output/run --stop-after understand
node tools/model-check.mjs --stage completion --image local.png --prompt-file task.txt --out-dir output/edit
node tools/model-check.mjs --stage segmentation --input-dir output/understand --out-dir output/segment --sam-dir F:/Study/SAM3
node tools/model-check.mjs --stage completion --input-dir output/segment --out-dir output/complete --part head --prepare-only
node tools/model-check.mjs --list-models
node tools/model-check.mjs --dry-run
公共选项:
--config FILE 默认 configs/astra-qwen.json(prompt 相对配置文件)
--env-file FILE 默认项目 .env,shell 环境变量优先
--understanding-model ID 理解模型覆盖(使用原样 ID,不自动映射)
--completion-model ID 图片编辑模型覆盖
--understanding-protocol NAME chat-completions / responses
--stream true|false 覆盖理解阶段的流式设置
--output-mode NAME text-json / json-object / json-schema
--resume 输入/配置/代码和产物哈希相同时复用
--parts-file FILE 离线读取已有 v1/v2 计划,做 Alpha 校验与种子生成,不请求模型
--python FILE ai-spine Conda 环境解释器(默认使用已激活环境)
--input-dir DIR SAM 阶段读理解目录;补全阶段读 SAM 目录
--part ID 补全部件 ID,可重复;默认处理所有需补全部件
--prepare-only 仅准备补全画布、掩码和提示词,不请求模型
--candidates-dir DIR 离线导入 ID.png,或旧补全目录的首张候选,不请求模型
--candidate-size-policy NAME strict(默认)/ uniform(同宽高比缩放后重新检查对齐)
--completion-size INT 补全方形画布边长,默认 1024,不缩放原像素
--extension-radius INT 补全区域距可见轮廓的最大像素距离,默认 32
--sam-dir DIR 本地 SAM 根目录,包含 sam3 与 sam3.1checkpoint
--sam-code-dir DIR SAM 源码仓库目录覆盖
--sam-checkpoint FILE 本地权重路径覆盖
--sam-confidence FLOAT 模型候选分数阈值,默认 0.2
--sam-precision NAME bf16(默认)或 fp32
--image FILE 图片编辑输入,可重复;多图需配置 capabilities
--mask FILE 透明处可编辑的 RGBA PNG;需配置 capabilities
--dry-run 检查配置与本地输入,不请求模型、不生成产物
--stop-after 支持 understand / segment。SAM 在临时 GPU 子进程运行,完成后退出。
部件补全使用 --stage completion --input-dir;可见像素恢复与几何检查已接入,Spine 导出尚未接入。`;
export function parseOptions(argv) {
const options = {stage: 'understanding', imagePaths: [], partIds: []};
const valueFlags = new Map([
['--config', 'configFile'], ['--env-file', 'envFile'], ['--reference', 'reference'], ['--out-dir', 'outDir'],
['--understanding-model', 'understandingModel'], ['--completion-model', 'completionModel'],
['--understanding-protocol', 'understandingProtocol'], ['--output-mode', 'outputMode'],
['--stage', 'stage'], ['--mask', 'maskPath'], ['--prompt-file', 'promptFile'], ['--stop-after', 'stopAfter'],
['--mode', 'mode'], ['--python', 'python'], ['--stream', 'streamRaw'], ['--parts-file', 'partsFile'],
['--input-dir', 'inputDir'], ['--sam-dir', 'samDir'], ['--sam-code-dir', 'samCodeDir'],
['--sam-checkpoint', 'samCheckpoint'], ['--sam-confidence', 'samConfidence'], ['--sam-precision', 'samPrecision'],
['--candidates-dir', 'candidatesDir'], ['--completion-size', 'completionSize'], ['--extension-radius', 'extensionRadius'],
['--candidate-size-policy', 'candidateSizePolicy'],
]);
for (let i = 0; i < argv.length; i += 1) {
const flag = argv[i];
if (['--help', '-h'].includes(flag)) options.help = true;
else if (flag === '--resume') options.resume = true;
else if (flag === '--dry-run') options.dryRun = true;
else if (flag === '--prepare-only') options.prepareOnly = true;
else if (flag === '--list-models') options.listModels = true;
else if (valueFlags.has(flag) || flag === '--image' || flag === '--part') {
const value = argv[++i];
if (!value || value.startsWith('--')) throw new Error(`${flag} 缺少参数值`);
if (flag === '--image') options.imagePaths.push(value);
else if (flag === '--part') options.partIds.push(value);
else options[valueFlags.get(flag)] = value;
} else throw new Error(`未知参数:${flag},请用 --help 查看`);
}
return options;
}
export async function main(argv = process.argv.slice(2)) {
const options = parseOptions(argv);
if (options.help) { console.log(help); return; }
if (!['understanding', 'completion', 'segmentation'].includes(options.stage)) throw new Error('--stage 仅支持 understanding / completion / segmentation');
if (options.partsFile && (options.stage !== 'understanding' || options.listModels)) throw new Error('--parts-file 仅用于离线结构理解');
if (options.streamRaw !== undefined && !['true', 'false'].includes(options.streamRaw)) throw new Error('--stream 仅支持 true 或 false');
options.stream = options.streamRaw === undefined ? undefined : options.streamRaw === 'true';
if (options.mode && options.mode !== 'annotated-parts') throw new Error('新入口仅支持 --mode annotated-parts');
if (options.stopAfter && (!['understand', 'segment'].includes(options.stopAfter) || options.stage === 'completion' || (options.stage === 'segmentation' && options.stopAfter !== 'segment'))) {
throw new Error('--stop-after 仅支持 understand / segment,且必须匹配阶段');
}
const loaded = loadConfig(options);
const partCompletion = options.stage === 'completion' && options.inputDir;
if (!partCompletion && (options.prepareOnly || options.candidatesDir || options.partIds.length || options.completionSize || options.extensionRadius || options.candidateSizePolicy)) {
throw new Error('部件补全参数需要 --stage completion --input-dir');
}
if (partCompletion) {
if (options.reference || options.imagePaths.length || options.maskPath || options.promptFile || options.listModels) throw new Error('部件补全从 --input-dir 生成画布与提示,不接受探针输入参数');
const stage = resolveStage(loaded, 'completion', {model: options.completionModel}, {requireKey: !options.dryRun && !options.prepareOnly && !options.candidatesDir});
const result = await runCompletion({...options, stage});
if (!result.dryRun) console.log(`部件补全${result.reused ? '复用完成' : options.prepareOnly ? '准备完成(未请求模型)' : '处理完成,需复核'}:${path.join(result.out, 'report.html')}`);
return result;
}
if (options.stage === 'segmentation' || (options.stopAfter === 'segment' && options.inputDir)) {
if (options.reference || options.partsFile || options.imagePaths.length || options.maskPath || options.promptFile || options.listModels) throw new Error('独立 SAM 阶段使用 --input-dir,不接受模型请求或编辑参数');
const result = await runSegmentation({...options, loaded});
if (!result.dryRun) console.log(`SAM 分割${result.reused ? '复用完成' : '完成'}:${path.join(result.out, 'report.html')}`);
return result;
}
if (options.inputDir) throw new Error('--input-dir 仅用于 SAM 分割或部件补全阶段');
const overrides = options.stage === 'understanding'
? {model: options.understandingModel, protocol: options.understandingProtocol, outputMode: options.outputMode, stream: options.stream}
: {model: options.completionModel};
const stage = resolveStage(loaded, options.stage, overrides, {requireKey: !options.dryRun && !options.partsFile});
if (options.listModels) {
if (options.dryRun) throw new Error('--list-models 会请求模型列表,不能与 --dry-run 同时使用');
const {payload} = await requestJson(stage, 'models', () => ({method: 'GET'}));
if (!Array.isArray(payload.data)) throw new Error('模型列表响应缺少 data 数组,请核对中转站协议');
console.log(payload.data.map((item) => item.id).filter((id) => typeof id === 'string').join('\n'));
return;
}
if (options.dryRun) {
const understanding = resolveStage(loaded, 'understanding', {model: options.understandingModel, protocol: options.understandingProtocol, outputMode: options.outputMode, stream: options.stream}, {requireKey: false});
const completion = resolveStage(loaded, 'completion', {model: options.completionModel}, {requireKey: false});
if (options.reference) readImage(options.reference, {pngOnly: true});
for (const file of options.imagePaths) readImage(file, {pngOnly: true});
console.log(JSON.stringify({understanding: publicStage(understanding), completion: publicStage(completion), sam: samInfo(loaded), credentials_present: Boolean(stage.apiKey && stage.baseUrl), network_requested: false}, null, 2));
return;
}
if (!options.outDir) throw new Error('需要 --out-dir,建议每个实验使用独立目录');
let result;
if (options.stage === 'understanding') {
if (!options.reference) throw new Error('结构理解需要 --reference');
if (options.imagePaths.length || options.maskPath || options.promptFile) throw new Error('结构理解不接受图片编辑参数');
console.log(options.partsFile ? '角色结构理解:离线计划几何校验' : `角色结构理解:${stage.model} (${stage.protocol})`);
result = await runUnderstanding({...options, loaded, stage});
console.log(`${result.reused ? '复用已校验产物' : '结构理解完成,语义待复核'}:${path.join(result.out, 'report.html')}`);
if (options.stopAfter === 'segment') {
result = await runSegmentation({...options, loaded, inputDir: result.out, outDir: path.join(result.out, 'segmentation')});
console.log(`SAM 分割完成:${path.join(result.out, 'report.html')}`);
}
} else {
if (options.reference) throw new Error('图片编辑使用 --image 指定局部画布;额外参考图也通过重复 --image 指定');
console.log(`图片编辑接入测试:${stage.model}`);
result = await runCompletionProbe({...options, stage});
console.log(`${result.reused ? '复用已校验产物' : '原始候选已保存,尚未生成最终部件'}:${result.out}`);
}
return result;
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main().catch((error) => { console.error(`模型阶段失败:${error.message}`); process.exitCode = 1; });
}