@glissade/cli 0.61.0-pre.0 → 0.61.0-pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/semanticParity.js +121 -32
- package/package.json +10 -10
package/dist/semanticParity.js
CHANGED
|
@@ -186,6 +186,7 @@ function parseWarn(msg) {
|
|
|
186
186
|
const ORPHAN_RADIUS = 64;
|
|
187
187
|
const COVERAGE_MIN = .34;
|
|
188
188
|
const MIN_ORPHAN_TILES = 3;
|
|
189
|
+
const BENIGN_MEAN_FLOOR = .92;
|
|
189
190
|
/** How many 8×8 tiles a node's device bbox spans (≥1) — the coverage denominator. */
|
|
190
191
|
function bboxTileArea(box, win) {
|
|
191
192
|
return Math.max(1, (box.maxX - box.minX) / win) * Math.max(1, (box.maxY - box.minY) / win);
|
|
@@ -267,19 +268,63 @@ function edgeDistance(x, y, b) {
|
|
|
267
268
|
const dy = Math.max(b.minY - y, 0, y - b.maxY);
|
|
268
269
|
return Math.hypot(dx, dy);
|
|
269
270
|
}
|
|
270
|
-
/**
|
|
271
|
-
*
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
271
|
+
/** id-less render-only warns → the scene `describeType`(s) that produce them, so a
|
|
272
|
+
* warn with no quoted id still resolves to its render-only node(s) structurally. */
|
|
273
|
+
const FEATURE_TYPES = {
|
|
274
|
+
"motion-blur": ["MotionBlur"],
|
|
275
|
+
"echo-trails": ["Echo"],
|
|
276
|
+
"camera-shake": ["Camera"],
|
|
277
|
+
shake: ["Camera"],
|
|
278
|
+
followpath: ["FollowPath"],
|
|
279
|
+
orienttopath: ["OrientToPath"],
|
|
280
|
+
lookat: ["LookAt"],
|
|
281
|
+
"text-cursor": ["TextCursor"]
|
|
282
|
+
};
|
|
283
|
+
/** Collect a node's id-bearing subtree (itself + descendants). */
|
|
284
|
+
function collectSubtreeIds(node, into) {
|
|
285
|
+
if (node.id !== void 0) into.add(node.id);
|
|
286
|
+
const children = node.children;
|
|
287
|
+
if (Array.isArray(children)) for (const c of children) collectSubtreeIds(c, into);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Resolve each export warn to the set of scene node ids its drop STRUCTURALLY
|
|
291
|
+
* explains: a WRAPPER (motionBlur/echo/camera) explains its whole SUBTREE; a DRIVER
|
|
292
|
+
* (followPath/orientToPath/lookAt) explains its `.target`'s subtree (the target is a
|
|
293
|
+
* SIBLING it mutates, which an ancestry walk would miss); a leaf (Image/Video/Text)
|
|
294
|
+
* explains only ITSELF. This structural link — never bare geometric overlap — is what
|
|
295
|
+
* keeps an INDEPENDENT residual that merely sits inside a drop's bbox UNEXPLAINED.
|
|
296
|
+
*/
|
|
297
|
+
function buildDropExtents(scene, warns) {
|
|
298
|
+
const byType = /* @__PURE__ */ new Map();
|
|
299
|
+
const indexTree = (node) => {
|
|
300
|
+
const t = node.describeType;
|
|
301
|
+
(byType.get(t) ?? byType.set(t, []).get(t)).push(node);
|
|
302
|
+
const children = node.children;
|
|
303
|
+
if (Array.isArray(children)) for (const c of children) indexTree(c);
|
|
304
|
+
};
|
|
305
|
+
indexTree(scene.root);
|
|
306
|
+
const out = [];
|
|
307
|
+
warns.forEach((warn, i) => {
|
|
308
|
+
const resolved = [];
|
|
309
|
+
if (warn.node !== void 0) {
|
|
310
|
+
const n = scene.nodes.get(warn.node);
|
|
311
|
+
if (n) resolved.push(n);
|
|
312
|
+
} else for (const type of FEATURE_TYPES[warn.property] ?? []) resolved.push(...byType.get(type) ?? []);
|
|
313
|
+
if (resolved.length === 0) return;
|
|
314
|
+
const ids = /* @__PURE__ */ new Set();
|
|
315
|
+
for (const n of resolved) {
|
|
316
|
+
collectSubtreeIds(n, ids);
|
|
317
|
+
const target = n.target;
|
|
318
|
+
if (target && typeof target === "object") collectSubtreeIds(target, ids);
|
|
319
|
+
}
|
|
320
|
+
out.push({
|
|
321
|
+
key: warn.node ?? `${warn.property}@${i}`,
|
|
322
|
+
...warn.node !== void 0 ? { node: warn.node } : {},
|
|
323
|
+
warn,
|
|
324
|
+
ids
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
return out;
|
|
283
328
|
}
|
|
284
329
|
function regionRole(region, w, h) {
|
|
285
330
|
const cx = (region.minX + region.maxX) / 2;
|
|
@@ -378,6 +423,7 @@ async function semanticParityCommand(opts) {
|
|
|
378
423
|
if (!ex || res.worst < ex.worst) nodeAcc.set(id, {
|
|
379
424
|
region: res.region,
|
|
380
425
|
worst: res.worst,
|
|
426
|
+
mean: res.sum / res.count,
|
|
381
427
|
frame,
|
|
382
428
|
role: regionRole(res.region, w, h),
|
|
383
429
|
coverage
|
|
@@ -394,19 +440,29 @@ async function semanticParityCommand(opts) {
|
|
|
394
440
|
}
|
|
395
441
|
}
|
|
396
442
|
const findings = [];
|
|
397
|
-
const
|
|
443
|
+
const emittedCauses = /* @__PURE__ */ new Set();
|
|
398
444
|
const warnByNode = /* @__PURE__ */ new Map();
|
|
399
445
|
for (const pw of parsedWarns) if (pw.node !== void 0 && !warnByNode.has(pw.node)) warnByNode.set(pw.node, pw);
|
|
446
|
+
const dropExtents = buildDropExtents(refScene, parsedWarns);
|
|
447
|
+
const explainedBy = /* @__PURE__ */ new Map();
|
|
448
|
+
for (const ext of dropExtents) for (const id of ext.ids) {
|
|
449
|
+
const list = explainedBy.get(id);
|
|
450
|
+
if (list) list.push(ext);
|
|
451
|
+
else explainedBy.set(id, [ext]);
|
|
452
|
+
}
|
|
400
453
|
const attributed = /* @__PURE__ */ new Map();
|
|
401
|
-
const
|
|
454
|
+
const merge = (key, node, acc, warn, real, from) => {
|
|
402
455
|
const ex = attributed.get(key);
|
|
403
456
|
if (!ex) {
|
|
404
457
|
attributed.set(key, {
|
|
405
458
|
region: { ...acc.region },
|
|
406
459
|
worst: acc.worst,
|
|
460
|
+
mean: acc.mean,
|
|
407
461
|
frame: acc.frame,
|
|
408
462
|
role: acc.role,
|
|
409
463
|
...warn ? { warn } : {},
|
|
464
|
+
...node !== void 0 ? { node } : {},
|
|
465
|
+
coalesced: new Set(key === from ? [] : [from]),
|
|
410
466
|
real
|
|
411
467
|
});
|
|
412
468
|
return;
|
|
@@ -417,34 +473,44 @@ async function semanticParityCommand(opts) {
|
|
|
417
473
|
ex.region.maxY = Math.max(ex.region.maxY, acc.region.maxY);
|
|
418
474
|
if (acc.worst < ex.worst) {
|
|
419
475
|
ex.worst = acc.worst;
|
|
476
|
+
ex.mean = acc.mean;
|
|
420
477
|
ex.frame = acc.frame;
|
|
421
478
|
ex.role = acc.role;
|
|
422
479
|
}
|
|
480
|
+
if (key !== from) ex.coalesced.add(from);
|
|
423
481
|
ex.real = ex.real || real;
|
|
424
482
|
};
|
|
425
|
-
for (const [id, acc] of nodeAcc)
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
483
|
+
for (const [id, acc] of nodeAcc) {
|
|
484
|
+
if (warnByNode.has(id)) {
|
|
485
|
+
merge(id, id, acc, warnByNode.get(id), true, id);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const exts = explainedBy.get(id);
|
|
489
|
+
if (exts && exts.length > 0) {
|
|
490
|
+
const ext = exts.reduce((a, b) => a.key <= b.key ? a : b);
|
|
491
|
+
merge(ext.key, ext.node, acc, ext.warn, true, id);
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
merge(id, id, acc, void 0, acc.coverage >= COVERAGE_MIN, id);
|
|
430
495
|
}
|
|
431
|
-
for (const [
|
|
496
|
+
for (const [, at] of attributed) {
|
|
432
497
|
if (!at.real) continue;
|
|
433
498
|
anyResidual = true;
|
|
434
|
-
const
|
|
499
|
+
const coalesced = [...at.coalesced].sort();
|
|
435
500
|
if (at.warn) {
|
|
436
|
-
|
|
437
|
-
findings.push(dropFinding(at.warn,
|
|
501
|
+
emittedCauses.add(at.warn.cause);
|
|
502
|
+
findings.push(dropFinding(at.warn, at.node, at.region, at.frame, round(at.worst), at.role, coalesced));
|
|
438
503
|
} else {
|
|
439
|
-
const node = refScene.nodes.get(
|
|
440
|
-
if (node?.hasAnchor === true && (node.anchor[0] !== .5 || node.anchor[1] !== .5)) findings.push(anchorFinding(
|
|
441
|
-
else findings.push(
|
|
504
|
+
const node = at.node !== void 0 ? refScene.nodes.get(at.node) : void 0;
|
|
505
|
+
if (node?.hasAnchor === true && (node.anchor[0] !== .5 || node.anchor[1] !== .5)) findings.push(anchorFinding(at.node, node.anchor, at.region, at.frame, round(at.worst), at.role));
|
|
506
|
+
else if (at.mean >= BENIGN_MEAN_FLOOR) findings.push(compositingApprox(at.node, at.region, at.frame, round(at.mean), at.role));
|
|
507
|
+
else findings.push(unexplained(at.node, at.region, at.frame, round(at.worst), at.role));
|
|
442
508
|
}
|
|
443
509
|
}
|
|
444
510
|
for (const warn of parsedWarns) {
|
|
445
|
-
if (
|
|
446
|
-
findings.push(dropFinding(warn, warn.node, null, frames[0] ?? 0, null, null));
|
|
447
|
-
|
|
511
|
+
if (emittedCauses.has(warn.cause)) continue;
|
|
512
|
+
findings.push(dropFinding(warn, warn.node, null, frames[0] ?? 0, null, null, []));
|
|
513
|
+
emittedCauses.add(warn.cause);
|
|
448
514
|
}
|
|
449
515
|
if (orphanAcc) findings.push(unexplained(void 0, orphanAcc.region, orphanAcc.frame, round(orphanAcc.worst), regionRole(orphanAcc.region, w, h)));
|
|
450
516
|
const sorted = sortDiagnostics(findings);
|
|
@@ -480,7 +546,7 @@ async function semanticParityCommand(opts) {
|
|
|
480
546
|
report: formatReport(result, opts)
|
|
481
547
|
};
|
|
482
548
|
}
|
|
483
|
-
function dropFinding(warn, node, region, frame, ssim, role) {
|
|
549
|
+
function dropFinding(warn, node, region, frame, ssim, role, coalesced = []) {
|
|
484
550
|
return {
|
|
485
551
|
schemaVersion: DIAGNOSTIC_SCHEMA_VERSION,
|
|
486
552
|
code: warn.approximate ? "LOTTIE_APPROXIMATE" : "LOTTIE_DROP",
|
|
@@ -498,7 +564,30 @@ function dropFinding(warn, node, region, frame, ssim, role) {
|
|
|
498
564
|
...role ? {
|
|
499
565
|
role: role.role,
|
|
500
566
|
roleWeight: role.weight
|
|
501
|
-
} : {}
|
|
567
|
+
} : {},
|
|
568
|
+
...coalesced.length > 0 ? { coalesced } : {}
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
/** Class III: an UNWARNED but BENIGN sub-pixel compositing residual (Stack/sequence
|
|
573
|
+
* rounding) — tagged LOTTIE_APPROXIMATE (expected:true → masked from the default
|
|
574
|
+
* error view) so it doesn't alarm as an episode-breaking UNEXPLAINED. */
|
|
575
|
+
function compositingApprox(node, region, frame, meanSsim, role) {
|
|
576
|
+
return {
|
|
577
|
+
schemaVersion: DIAGNOSTIC_SCHEMA_VERSION,
|
|
578
|
+
code: "LOTTIE_APPROXIMATE",
|
|
579
|
+
severity: "warning",
|
|
580
|
+
source: "parity",
|
|
581
|
+
...node !== void 0 ? { node } : {},
|
|
582
|
+
message: `${node ? `node '${node}'` : "a region"} shows a BENIGN sub-pixel compositing difference on the Lottie round-trip (mean ssim ${meanSsim}, no feature dropped) — a rounding/layer-order approximation, not a loss.`,
|
|
583
|
+
detail: {
|
|
584
|
+
property: "compositing-approx",
|
|
585
|
+
frame,
|
|
586
|
+
region: roundBox(region),
|
|
587
|
+
ssim: meanSsim,
|
|
588
|
+
expected: true,
|
|
589
|
+
role: role.role,
|
|
590
|
+
roleWeight: role.weight
|
|
502
591
|
}
|
|
503
592
|
};
|
|
504
593
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/cli",
|
|
3
|
-
"version": "0.61.0-pre.
|
|
3
|
+
"version": "0.61.0-pre.1",
|
|
4
4
|
"description": "glissade CLI: headless rendering via backend-skia (+ FFmpeg mux).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"engines": {
|
|
@@ -28,15 +28,15 @@
|
|
|
28
28
|
"@napi-rs/canvas": "^0.1.65",
|
|
29
29
|
"esbuild": "0.28.0",
|
|
30
30
|
"jiti": "^2.4.2",
|
|
31
|
-
"@glissade/backend-skia": "0.61.0-pre.
|
|
32
|
-
"@glissade/core": "0.61.0-pre.
|
|
33
|
-
"@glissade/interact": "0.61.0-pre.
|
|
34
|
-
"@glissade/lottie": "0.61.0-pre.
|
|
35
|
-
"@glissade/narrate": "0.61.0-pre.
|
|
36
|
-
"@glissade/player": "0.61.0-pre.
|
|
37
|
-
"@glissade/scene": "0.61.0-pre.
|
|
38
|
-
"@glissade/sfx": "0.61.0-pre.
|
|
39
|
-
"@glissade/svg": "0.61.0-pre.
|
|
31
|
+
"@glissade/backend-skia": "0.61.0-pre.1",
|
|
32
|
+
"@glissade/core": "0.61.0-pre.1",
|
|
33
|
+
"@glissade/interact": "0.61.0-pre.1",
|
|
34
|
+
"@glissade/lottie": "0.61.0-pre.1",
|
|
35
|
+
"@glissade/narrate": "0.61.0-pre.1",
|
|
36
|
+
"@glissade/player": "0.61.0-pre.1",
|
|
37
|
+
"@glissade/scene": "0.61.0-pre.1",
|
|
38
|
+
"@glissade/sfx": "0.61.0-pre.1",
|
|
39
|
+
"@glissade/svg": "0.61.0-pre.1"
|
|
40
40
|
},
|
|
41
41
|
"repository": {
|
|
42
42
|
"type": "git",
|