接通第四步
This commit is contained in:
+23
-2
@@ -7,12 +7,14 @@ 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';
|
||||
import {runSpine} from '../stages/spine-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 --stage spine --input-dir output/segment --completion-dir output/complete --out-dir output/spine
|
||||
node tools/model-check.mjs --list-models
|
||||
node tools/model-check.mjs --dry-run
|
||||
|
||||
@@ -28,6 +30,11 @@ const help = `模型接入与结构理解(新增模式,不调用旧拆件流
|
||||
--parts-file FILE 离线读取已有 v1/v2 计划,做 Alpha 校验与种子生成,不请求模型
|
||||
--python FILE ai-spine Conda 环境解释器(默认使用已激活环境)
|
||||
--input-dir DIR SAM 阶段读理解目录;补全阶段读 SAM 目录
|
||||
--completion-dir DIR Spine 阶段可选补全目录;input-dir 指向同批 SAM 目录
|
||||
--fallback-policy sam|error Spine 未完成补全时回退 SAM(默认)或报错
|
||||
--draw-order-file FILE Spine 绘制顺序覆盖:{"draw_order":[全部可绘制ID,从后到前]}
|
||||
--check-angle FLOAT Spine 单骨骼检查动画角度,默认 10,范围 (0,45]
|
||||
--genielabs-dir DIR 外部 spine-animation-ai 路径,仅用于 Spine 图集和播放器
|
||||
--part ID 补全部件 ID,可重复;默认处理所有需补全部件
|
||||
--prepare-only 仅准备补全画布、掩码和提示词,不请求模型
|
||||
--candidates-dir DIR 离线导入 ID.png,或旧补全目录的首张候选,不请求模型
|
||||
@@ -44,7 +51,7 @@ const help = `模型接入与结构理解(新增模式,不调用旧拆件流
|
||||
--dry-run 检查配置与本地输入,不请求模型、不生成产物
|
||||
|
||||
--stop-after 支持 understand / segment。SAM 在临时 GPU 子进程运行,完成后退出。
|
||||
部件补全使用 --stage completion --input-dir;可见像素恢复与几何检查已接入,Spine 导出尚未接入。`;
|
||||
部件补全使用 --stage completion --input-dir;--stage spine 离线组装并导出 Spine 4.2,资产和动作质量仍需复核。`;
|
||||
|
||||
export function parseOptions(argv) {
|
||||
const options = {stage: 'understanding', imagePaths: [], partIds: []};
|
||||
@@ -58,6 +65,8 @@ export function parseOptions(argv) {
|
||||
['--sam-checkpoint', 'samCheckpoint'], ['--sam-confidence', 'samConfidence'], ['--sam-precision', 'samPrecision'],
|
||||
['--candidates-dir', 'candidatesDir'], ['--completion-size', 'completionSize'], ['--extension-radius', 'extensionRadius'],
|
||||
['--candidate-size-policy', 'candidateSizePolicy'],
|
||||
['--completion-dir', 'completionDir'], ['--fallback-policy', 'fallbackPolicy'],
|
||||
['--draw-order-file', 'drawOrderFile'], ['--check-angle', 'checkAngle'], ['--genielabs-dir', 'genielabsDir'],
|
||||
]);
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const flag = argv[i];
|
||||
@@ -80,7 +89,19 @@ export function parseOptions(argv) {
|
||||
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 (!['understanding', 'completion', 'segmentation', 'spine'].includes(options.stage)) throw new Error('--stage 仅支持 understanding / completion / segmentation / spine');
|
||||
if (options.stage === 'spine') {
|
||||
const allowed = new Set(['stage', 'inputDir', 'outDir', 'completionDir', 'fallbackPolicy', 'drawOrderFile',
|
||||
'checkAngle', 'genielabsDir', 'python', 'resume', 'dryRun', 'mode', 'imagePaths', 'partIds']);
|
||||
if (Object.keys(options).some((key) => !allowed.has(key)) || options.imagePaths.length || options.partIds.length ||
|
||||
(options.mode && options.mode !== 'annotated-parts')) throw new Error('Spine 为独立离线阶段,不接受模型配置、请求或补全参数');
|
||||
const result = await runSpine(options);
|
||||
if (!result.dryRun) console.log(`Spine ${result.reused ? '复用完成' : '导出完成,需复核'}:${path.join(result.out, 'report.html')}`);
|
||||
return result;
|
||||
}
|
||||
if (['completionDir', 'fallbackPolicy', 'drawOrderFile', 'checkAngle', 'genielabsDir'].some((key) => options[key] !== undefined)) {
|
||||
throw new Error('Spine 导出参数仅用于 --stage spine');
|
||||
}
|
||||
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';
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// Optional, offline compatibility check using a caller-provided official Spine 4.2 core IIFE.
|
||||
// node tools/verify-spine-runtime.mjs --runtime-file /path/spine-core.js --input-dir output/spine
|
||||
import assert from 'node:assert/strict';
|
||||
import {readFileSync} from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import vm from 'node:vm';
|
||||
import {readImage} from '../lib/images.mjs';
|
||||
|
||||
const options = {};
|
||||
for (let i = 2; i < process.argv.length; i += 2) {
|
||||
const flag = process.argv[i];
|
||||
if (!['--runtime-file', '--input-dir'].includes(flag) || !process.argv[i + 1]) {
|
||||
throw new Error('Usage: node tools/verify-spine-runtime.mjs --runtime-file spine-core.js --input-dir output/spine');
|
||||
}
|
||||
options[flag] = process.argv[i + 1];
|
||||
}
|
||||
if (!options['--runtime-file'] || !options['--input-dir']) throw new Error('Both --runtime-file and --input-dir are required');
|
||||
const context = vm.createContext({console});
|
||||
vm.runInContext(readFileSync(options['--runtime-file'], 'utf8'), context, {timeout: 10000});
|
||||
const spine = context.spine;
|
||||
const input = path.resolve(options['--input-dir']);
|
||||
const assembly = JSON.parse(readFileSync(path.join(input, 'assembly.json')));
|
||||
const json = JSON.parse(readFileSync(path.join(input, 'skeleton.json')));
|
||||
assert.match(json.skeleton.spine, /^4\.2\./u);
|
||||
const atlas = new spine.TextureAtlas(readFileSync(path.join(input, 'atlas/skeleton.atlas'), 'utf8'));
|
||||
for (const page of atlas.pages) {
|
||||
const png = readImage(path.join(input, 'atlas', page.name), {pngOnly: true});
|
||||
assert.equal(page.width, png.width);
|
||||
assert.equal(page.height, png.height);
|
||||
// CPU geometry needs region metadata, not a WebGL texture upload.
|
||||
page.setTexture({setFilters() {}, setWraps() {}, getImage() { return {width: png.width, height: png.height}; }});
|
||||
}
|
||||
const data = new spine.SkeletonJson(new spine.AtlasAttachmentLoader(atlas)).readSkeletonData(json);
|
||||
const skeleton = new spine.Skeleton(data);
|
||||
function vertices(slot) {
|
||||
const values = new Float32Array(8);
|
||||
slot.getAttachment().computeWorldVertices(slot, values, 0, 2);
|
||||
assert.ok([...values].every(Number.isFinite));
|
||||
return values;
|
||||
}
|
||||
function close(actual, expected, label) {
|
||||
assert.ok(Math.abs(actual - expected) < .002, `${label}: ${actual} != ${expected}`);
|
||||
}
|
||||
skeleton.setToSetupPose();
|
||||
skeleton.updateWorldTransform(spine.Physics.none);
|
||||
const originals = new Map();
|
||||
const assets = new Map(assembly.parts.map((p) => [p.id, p]));
|
||||
for (const slot of skeleton.slots) {
|
||||
const values = vertices(slot);
|
||||
originals.set(slot.data.name, values);
|
||||
const [x0, y0, x1, y1] = assets.get(slot.data.name).crop_xyxy;
|
||||
const xs = [values[0], values[2], values[4], values[6]];
|
||||
const ys = [values[1], values[3], values[5], values[7]];
|
||||
close(Math.min(...xs), x0 - assembly.width / 2, 'setup x0');
|
||||
close(Math.max(...xs), x1 - assembly.width / 2, 'setup x1');
|
||||
close(Math.min(...ys), assembly.height - y1, 'setup y0');
|
||||
close(Math.max(...ys), assembly.height - y0, 'setup y1');
|
||||
const region = atlas.findRegion(slot.data.name);
|
||||
assert.equal(region.originalWidth, x1 - x0);
|
||||
assert.equal(region.originalHeight, y1 - y0);
|
||||
}
|
||||
let samples = 0;
|
||||
for (const animation of data.animations) {
|
||||
if (animation.name === 'setup') continue;
|
||||
const boneName = animation.name.replace(/^check__/u, '');
|
||||
const frames = json.animations[animation.name].bones[boneName].rotate;
|
||||
for (const time of [.5, 1.5]) {
|
||||
skeleton.setToSetupPose();
|
||||
skeleton.updateWorldTransform(spine.Physics.none);
|
||||
const bone = skeleton.findBone(boneName);
|
||||
const px = bone.worldX, py = bone.worldY;
|
||||
const angle = frames.find((f) => f.time === time).value * Math.PI / 180;
|
||||
animation.apply(skeleton, 0, time, false, [], 1, spine.MixBlend.replace, spine.MixDirection.mixIn);
|
||||
skeleton.updateWorldTransform(spine.Physics.none);
|
||||
for (const slot of skeleton.slots) {
|
||||
let parent = slot.bone;
|
||||
while (parent && parent !== bone) parent = parent.parent;
|
||||
const before = originals.get(slot.data.name), after = vertices(slot);
|
||||
for (let i = 0; i < 8; i += 2) {
|
||||
const x = before[i] - px, y = before[i + 1] - py;
|
||||
close(after[i], parent ? px + x * Math.cos(angle) - y * Math.sin(angle) : before[i], 'rotated x');
|
||||
close(after[i + 1], parent ? py + x * Math.sin(angle) + y * Math.cos(angle) : before[i + 1], 'rotated y');
|
||||
}
|
||||
}
|
||||
samples++;
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify({input, spine_version: data.version, bones: data.bones.length,
|
||||
slots: data.slots.length, animations: data.animations.length, rotation_samples: samples,
|
||||
setup_geometry_verified: true, descendant_rotation_verified: true, browser_render_verified: false}, null, 2));
|
||||
Reference in New Issue
Block a user