cozyclay 3.1.1 → 3.1.2
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/CHANGELOG.md +39 -0
- package/bin/agent/agent-routes.mjs +4 -4
- package/bin/agent/main-agent.mjs +21 -7
- package/bin/agent/main-prompt.mjs +7 -6
- package/bin/agent/script-tool.mjs +2 -1
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-rebase.mjs +5 -4
- package/bin/agent/studio-task-routes.mjs +1 -1
- package/bin/agent/studio-tasks.mjs +116 -25
- package/bin/agent/studio-tools.mjs +3 -2
- package/bin/cozyclay.mjs +4 -2
- package/bin/live-endpoint.mjs +2 -0
- package/dist/app/index.html +6 -6
- package/dist/assets/TaskBoard-GR8xZm5c.js +1 -0
- package/dist/assets/{app-BlwOpest.css → app-Bhe0TpGz.css} +1 -1
- package/dist/assets/app-C4QTcDcb.js +5144 -0
- package/dist/assets/compile-worker-oy_P9HHh.js +6 -0
- package/dist/assets/motion3-compile-dgGhrvPK.js +1 -0
- package/dist/assets/scene-asset-sync-BEjjhNEo.css +1 -0
- package/dist/assets/scene-asset-sync-qoxcdrhj.js +13 -0
- package/dist/assets/task-board-BqReHzOi.js +15 -0
- package/dist/assets/workflow-C87p6E1E.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +1 -1
- package/dist/sitemap.xml +1 -1
- package/dist/workflow/index.html +4 -4
- package/mcp/README.md +1 -1
- package/mcp/live-hub.mjs +5 -3
- package/mcp/tool-handlers.mjs +5 -5
- package/package.json +1 -1
- package/src/App.jsx +42 -64
- package/src/bus-task-scopes.js +59 -11
- package/src/bus-task-targets.js +35 -9
- package/src/bus-tasks.js +23 -10
- package/src/command-bus.js +7 -4
- package/src/commands/ai.js +1 -1
- package/src/commands/export.js +1 -1
- package/src/commands/motion3.js +27 -7
- package/src/export-workspace.js +244 -0
- package/src/keyframe-pack.js +5 -1
- package/src/live-control.js +14 -7
- package/src/motion3/compile-job.js +15 -3
- package/src/motion3/compiler/channels.js +74 -24
- package/src/motion3/compiler/compile.js +2 -1
- package/src/motion3/compiler/idle.js +2 -0
- package/src/motion3/compiler/overlap.js +180 -0
- package/src/motion3/compiler/pose.js +6 -4
- package/src/motion3/compiler/validate-common.js +1 -0
- package/src/motion3/compiler/validate-parts.js +5 -1
- package/src/motion3/compiler/validate.js +30 -10
- package/src/motion3/correct.js +236 -0
- package/src/motion3/docs.js +8 -6
- package/src/motion3/face-lean.js +31 -0
- package/src/motion3/fix/index.js +5 -1
- package/src/motion3/fix/reach-body.js +172 -0
- package/src/motion3/hint-check.js +44 -0
- package/src/motion3/json-patch.js +120 -12
- package/src/motion3/lenient.js +46 -4
- package/src/motion3/object-keys.js +17 -5
- package/src/motion3/pipeline.js +46 -9
- package/src/motion3/qa/arm-checks.js +96 -0
- package/src/motion3/qa/asked.js +113 -0
- package/src/motion3/qa/goals.js +37 -8
- package/src/motion3/qa/hints-goal.js +6 -5
- package/src/motion3/qa/index.js +5 -2
- package/src/motion3/qa/key-checks.js +4 -3
- package/src/motion3/qa/motion-life.js +243 -0
- package/src/motion3/qa/spec-checks.js +15 -9
- package/src/motion3/qa/verdict.js +50 -0
- package/src/motion3/refs.js +38 -14
- package/src/motion3/repair-watch.js +132 -0
- package/src/motion3/repair.js +52 -19
- package/src/motion3/report.js +50 -9
- package/src/motion3/schema/topics.js +6 -4
- package/src/motion3/schema/types.js +1 -1
- package/src/motion3/skills/basic.js +15 -8
- package/src/motion3/skills/index.js +52 -18
- package/src/motion3/skills/info.js +1 -1
- package/src/motion3/skills/stance.js +6 -0
- package/src/motion3/skills/walk-fit.js +170 -0
- package/src/panels/PoseImagePanel.jsx +3 -2
- package/src/panels/SculptPanel.jsx +2 -2
- package/src/result-modal.jsx +3 -3
- package/src/shell/ExportFolderPreference.jsx +103 -0
- package/src/shell/PreferencesDialog.jsx +3 -166
- package/src/shell/StatusBar.jsx +1 -1
- package/src/shell/preference-controls.jsx +114 -0
- package/src/shell/preference-pages.jsx +72 -0
- package/src/shell/preferences.css +46 -0
- package/src/shot-prompt.js +29 -9
- package/src/studio-actions.js +2 -2
- package/src/studio-agent-motion.js +19 -9
- package/src/studio-agent-protocol.js +1 -1
- package/src/studio-app-binding.js +3 -2
- package/src/workflow/AgentPanel.jsx +2 -2
- package/src/workflow/CozySceneNode.jsx +1 -1
- package/src/workflow/WorkflowBuilder.jsx +2 -2
- package/src/workflow/keyframe-pack-request.js +3 -10
- package/src/workflow/task-board.css +1 -1
- package/src/workflow/task-board.js +7 -5
- package/tools/loc-baseline.json +1 -2
- package/tools/run-tests.mjs +5 -5
- package/dist/assets/TaskBoard-ngdqiu7u.js +0 -1
- package/dist/assets/app-DmR7jBkz.js +0 -5144
- package/dist/assets/compile-worker-D0pKioa0.js +0 -5
- package/dist/assets/motion3-compile-BB3_1-EB.js +0 -1
- package/dist/assets/scene-asset-sync-CBx7XRti.css +0 -1
- package/dist/assets/scene-asset-sync-Cgdtr1Yw.js +0 -13
- package/dist/assets/task-board-ErQvfoaz.js +0 -14
- package/dist/assets/workflow-zQM-EKW1.js +0 -179
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Measured, not judged (output.observations.asked): what the spec asked against what the take did, the
|
|
2
|
+
// quantities agents probed or re-compiled for right after a compile (session study: the root where a walk
|
|
3
|
+
// ended, the mouth and the cup top on the drink frame, when the fingers met the cup), one line each:
|
|
4
|
+
// use[i] <skill>[ <side> <anchor>] f<a>-<b>: at [x, z] yaw <deg>[ (asked <deg>)][; to [x, z]: <m> m short|past[, <m> m aside][, ...]]
|
|
5
|
+
// at = the feet's midpoint at the skill's last frame (what walkTo.to and standIdle.at place), yaw = the
|
|
6
|
+
// body's heading there; a walkTo whose steps are given only aims at to (the steps set the distance)
|
|
7
|
+
// <contact name> f<a>-<b>: gap <cm> f<f> .. <cm> f<f> (tol <cm>) checks.contacts (reachGrip adds "fingers
|
|
8
|
+
// on <anchor>"): the frames its when holds, the closest and the farthest gap; "never on" when it never holds
|
|
9
|
+
// goal <name> f<f>: <ref> [x, y, z], <ref> [x, y, z][; best <m> f<f>] the goal's points on the frame its
|
|
10
|
+
// result names (near and above; moved and still when they fail); best: the closest frame of a near goal
|
|
11
|
+
// held on every frame
|
|
12
|
+
// An object ref is a point of the AABB of its boxes (refs.js), so a line names which: (box top) is the top-face
|
|
13
|
+
// centre, not a rim or an opening; (box base) the bottom-face centre; (box centre) the centre; a bare object in
|
|
14
|
+
// a near goal is its surface. Frames are absolute and follow a retime (out.timeMap); metres to 3 decimals.
|
|
15
|
+
import { refPoint, refDistance } from "../refs.js";
|
|
16
|
+
import { active, contactGap } from "./spec-checks.js";
|
|
17
|
+
import { goalKinds, goalName, movedRef } from "./goals.js";
|
|
18
|
+
import { m3 } from "./fmt.js";
|
|
19
|
+
|
|
20
|
+
const MAX_SKILLS = 8, MAX_CONTACTS = 6, MAX_GOALS = 8, OFF = 0.05, YAW_OFF = 5;
|
|
21
|
+
|
|
22
|
+
const v3 = p => `[${p.map(m3).join(", ")}]`;
|
|
23
|
+
const cm = v => `${Math.round(v * 1000) / 10}`;
|
|
24
|
+
const wrap = d => ((d % 360) + 540) % 360 - 180;
|
|
25
|
+
const meaning = { top: " (box top)", base: " (box base)", center: " (box centre)", rim: " (opening)" };
|
|
26
|
+
|
|
27
|
+
/** A ref with what an object point is: "object:cup.top (box top)". */
|
|
28
|
+
function named(ref, bare) {
|
|
29
|
+
const m = /^object:.+?(?:\.(center|top|base|rim))?$/s.exec(ref);
|
|
30
|
+
if (!m) return ref;
|
|
31
|
+
return `${ref}${m[1] ? meaning[m[1]] : bare ? " (surface)" : meaning.center}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The feet's midpoint [x, z] and the heading at absolute frame f, or null outside the compiled window. */
|
|
35
|
+
function standAt(out, f) {
|
|
36
|
+
const fr = out.frames[f - (out.f0 ?? 0)];
|
|
37
|
+
if (!fr) return null;
|
|
38
|
+
const { P } = fr.s, I = out.ctx.sk.index, l = P[I.LeftFoot], r = P[I.RightFoot];
|
|
39
|
+
return { at: [(l[0] + r[0]) / 2, (l[2] + r[2]) / 2], yaw: fr.c?.yaw ?? 0 };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function skillLine(out, u, i, use, map) {
|
|
43
|
+
const last = out.frameCount - 1, clamp = f => Math.max(0, Math.min(last, Math.round(map(f))));
|
|
44
|
+
const a = clamp(u.f0), b = clamp(u.end ?? u.f0), end = standAt(out, b), args = use?.args ?? {};
|
|
45
|
+
if (!end) return null;
|
|
46
|
+
const side = args.side ? ` ${args.side}` : "", anchor = args.anchor ? ` ${args.anchor}` : "";
|
|
47
|
+
let line = `use[${i}] ${u.skill}${side}${anchor} f${a}-${b}: at ${v3(end.at)} yaw ${Math.round(wrap(end.yaw))}`;
|
|
48
|
+
const heading = Array.isArray(args.heading) ? args.heading[args.heading.length - 1] : args.heading;
|
|
49
|
+
if (typeof heading === "number" && Math.abs(wrap(end.yaw - heading)) > YAW_OFF) line += ` (asked ${heading})`;
|
|
50
|
+
const goal = Array.isArray(args.to) ? [args.to[0], args.to[args.to.length - 1]] : Array.isArray(args.at) && !u.placed ? args.at : null;
|
|
51
|
+
if (goal) {
|
|
52
|
+
const d = [end.at[0] - goal[0], end.at[1] - goal[1]], off = Math.hypot(...d);
|
|
53
|
+
if (off > OFF) {
|
|
54
|
+
const start = args.to && standAt(out, a), dir = start && Math.hypot(goal[0] - start.at[0], goal[1] - start.at[1]) > 1e-6
|
|
55
|
+
? [goal[0] - start.at[0], goal[1] - start.at[1]].map((x, _, v) => x / Math.hypot(...v)) : null;
|
|
56
|
+
const along = dir ? d[0] * dir[0] + d[1] * dir[1] : null, aside = dir ? Math.abs(d[0] * dir[1] - d[1] * dir[0]) : null;
|
|
57
|
+
const how = dir ? `${m3(Math.abs(along))} m ${along < 0 ? "short" : "past"}${aside > OFF ? `, ${m3(aside)} m aside` : ""}` : `${m3(off)} m off`;
|
|
58
|
+
line += `; ${args.to ? "to" : "at"} ${v3(goal)}: ${how}${args.to && args.steps !== undefined ? ", args.steps set the distance (to only aims)" : ""}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return line;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function contactLine(out, k) {
|
|
65
|
+
const f0 = out.f0 ?? 0;
|
|
66
|
+
let lo = null, hi = null, first = null, lastOn = null;
|
|
67
|
+
for (let f = 0; f < out.frames.length; f++) {
|
|
68
|
+
if (!active(out, f, k.when)) continue;
|
|
69
|
+
first ??= f + f0; lastOn = f + f0;
|
|
70
|
+
const e = contactGap(out, f, k);
|
|
71
|
+
if (e === null) continue;
|
|
72
|
+
if (!lo || e < lo[0]) lo = [e, f + f0];
|
|
73
|
+
if (!hi || e > hi[0]) hi = [e, f + f0];
|
|
74
|
+
}
|
|
75
|
+
if (first === null) return `${k.name}: never on${typeof k.when === "string" ? ` (${k.when} never > 0.99)` : ""}`;
|
|
76
|
+
return `${k.name} f${first}-${lastOn}: gap ${lo ? `${cm(lo[0])} cm f${lo[1]} .. ${cm(hi[0])} cm f${hi[1]}` : "unknown"} (tol ${cm(k.tol ?? 0.01)})`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function goalLine(out, g, i, result) {
|
|
80
|
+
const kind = goalKinds(g)[0];
|
|
81
|
+
if (!result || result.at === null || !(kind === "near" || kind === "above" || (!result.ok && (kind === "moved" || kind === "still")))) return null;
|
|
82
|
+
const refs = kind === "near" || kind === "above" ? g[kind] : [movedRef(g[kind].includes(":") ? g[kind] : `object:${g[kind]}`)];
|
|
83
|
+
const pts = refs.map(r => [r, refPoint(out, result.at, r)]);
|
|
84
|
+
if (pts.some(([, p]) => !p)) return null;
|
|
85
|
+
let line = `goal ${goalName(g, i)} f${result.at}: ${pts.map(([r, p]) => `${named(r, kind === "near")} ${v3(p)}`).join(", ")}`;
|
|
86
|
+
if (kind === "near" && Array.isArray(g.at) && g.at[0] !== g.at[1] && (g.mode ?? "all") === "all") {
|
|
87
|
+
const f0 = out.f0 ?? 0;
|
|
88
|
+
let best = null;
|
|
89
|
+
for (let f = Math.max(g.at[0], f0); f <= Math.min(g.at[1], f0 + out.frames.length - 1); f++) {
|
|
90
|
+
const d = refDistance(out, f, g.near[0], g.near[1]);
|
|
91
|
+
if (d !== null && (!best || d < best[0])) best = [d, f];
|
|
92
|
+
}
|
|
93
|
+
if (best && best[1] !== result.at) line += `; best ${m3(best[0])} f${best[1]}`;
|
|
94
|
+
}
|
|
95
|
+
return line;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* observeAsked(out, { used, use, checks, goals, results }) -> string[] | null
|
|
100
|
+
* @param {object} out compileMotion result
|
|
101
|
+
* @param {{ used: object[], use?: object[], checks?: object, goals?: object[], results?: object[] }} ctx used = the skills
|
|
102
|
+
* expandUse ran, use = the spec's own use entries (their args are what was asked), checks = the measured checks,
|
|
103
|
+
* goals = the expanded spec's goals and results = qa.goals (same order, by name)
|
|
104
|
+
*/
|
|
105
|
+
export function observeAsked(out, { used = [], use = [], checks = {}, goals = [], results = [] }) {
|
|
106
|
+
const map = out.timeMap ?? (f => f), byName = new Map(results.map(r => [r.name, r]));
|
|
107
|
+
const lines = [
|
|
108
|
+
...used.slice(0, MAX_SKILLS).map((u, i) => skillLine(out, u, i, use[i], map)),
|
|
109
|
+
...(checks.contacts ?? []).slice(0, MAX_CONTACTS).map(k => contactLine(out, k)),
|
|
110
|
+
...goals.map((g, i) => goalLine(out, g, i, byName.get(goalName(g, i)))).filter(Boolean).slice(0, MAX_GOALS),
|
|
111
|
+
].filter(Boolean);
|
|
112
|
+
return lines.length ? lines : null;
|
|
113
|
+
}
|
package/src/motion3/qa/goals.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Goals (C8): what the motion must achieve, measured on the compiled frames. A goal names one
|
|
2
2
|
// predicate and the frames it must hold on (at: an absolute frame or [f0, f1]):
|
|
3
|
-
// near: [a, b] refDistance(a, b) <= tol (default 0.05 m)
|
|
3
|
+
// near: [a, b] refDistance(a, b) <= tol (default 0.05 m); a round body's .top and the mouth measure its rim
|
|
4
|
+
// (nearRefs), and a failing near goal on a .top says which point it read
|
|
4
5
|
// above: [a, b] a.y - b.y >= by (default 0 m)
|
|
5
6
|
// moved: ref an object's point (default its base) is at least by (default 0.05 m) from
|
|
6
7
|
// where the scene put it; any other point ref (prop:, joint:, landmark:, anchor:)
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
// count and first..last show how much of at fails and the search (search.js) sees progress; "any"
|
|
20
21
|
// one, at the best frame. Issue frames index out.frames (out.f0 + i); goals[].at and every frame in
|
|
21
22
|
// a message are absolute.
|
|
22
|
-
import { refPoint, refDistance } from "../refs.js";
|
|
23
|
+
import { refPoint, refDistance, openingOf } from "../refs.js";
|
|
23
24
|
import { partsOf } from "../object-pose.js";
|
|
24
25
|
import { add, apply, len, sub } from "../compiler/math.js";
|
|
25
26
|
import { m3 } from "./fmt.js";
|
|
@@ -42,13 +43,14 @@ export const goalNeed = (g, kind) => (AT_MOST.has(kind) ? g.tol : g.by) ?? DEFAU
|
|
|
42
43
|
const goalMode = (g, kind) => g.mode ?? (kind === "speed" ? "any" : "all");
|
|
43
44
|
|
|
44
45
|
/** moved and still measure the named point of an object ref (its base when none is named), any other ref as it is. */
|
|
45
|
-
export const movedRef = ref => (!ref.startsWith("object:") || /\.(center|top|base)$/.test(ref) ? ref : `${ref}.base`);
|
|
46
|
+
export const movedRef = ref => (!ref.startsWith("object:") || /\.(center|top|base|rim)$/.test(ref) ? ref : `${ref}.base`);
|
|
46
47
|
|
|
47
48
|
/** Where the scene put the point of a moved object ref: the same AABB point refs.js answers, from the scene boxes. */
|
|
48
49
|
function scenePoint(scene, ref) {
|
|
49
|
-
const [, id, which] = /^object:(.+)\.(center|top|base)$/s.exec(movedRef(ref)) ?? [];
|
|
50
|
+
const [, id, which] = /^object:(.+)\.(center|top|base|rim)$/s.exec(movedRef(ref)) ?? [];
|
|
50
51
|
const boxes = partsOf(scene, id).map(p => scene.objects[p]);
|
|
51
52
|
if (!boxes.length) return null;
|
|
53
|
+
if (which === "rim") return openingOf(boxes).o;
|
|
52
54
|
const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
|
|
53
55
|
for (const b of boxes) for (const x of [-1, 1]) for (const y of [0, 2]) for (const z of [-1, 1]) {
|
|
54
56
|
const p = add(b.pos, apply(b.R, [x * b.half[0], y * b.half[1], z * b.half[2]]));
|
|
@@ -74,9 +76,35 @@ function speedAt(out, ref, f) {
|
|
|
74
76
|
return q ? len(sub(p, q)) * fps : null;
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
const MOUTH = "landmark:mouth", TOP = /^object:(.+)\.top$/s;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A near goal of an object's .top and the mouth, on a body whose highest part is round (a cylinder: a cup, a can, a
|
|
83
|
+
* bottle), measures its opening's rim nearest the mouth (refs.js .rim): .top is the top-face centre of its world
|
|
84
|
+
* bounding box, which a tilted cup lifts off the rim it is drunk from (a session read 8 failures in a row off it).
|
|
85
|
+
* -> the refs measured, as given when nothing changes.
|
|
86
|
+
*/
|
|
87
|
+
export function nearRefs(scene, g) {
|
|
88
|
+
const [a, b] = g.near, top = [a, b].map(r => TOP.exec(r)?.[1]), i = top[0] && b === MOUTH ? 0 : top[1] && a === MOUTH ? 1 : -1;
|
|
89
|
+
if (i < 0) return g.near;
|
|
90
|
+
const parts = partsOf(scene, top[i]).map(p => scene.objects[p]).filter(p => p?.half && p.pos && p.R);
|
|
91
|
+
const high = parts.length ? parts.reduce((p, q) => (q.pos[1] + 2 * q.half[1] > p.pos[1] + 2 * p.half[1] ? q : p)) : null;
|
|
92
|
+
if (high?.renderer !== "cylinder") return g.near;
|
|
93
|
+
return g.near.map((r, j) => (j === i ? `object:${top[i]}.rim` : r));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** What a near goal's .top measured, said in its message (refs.js): the rim it was read at, or the bounding-box top it is. */
|
|
97
|
+
function topNote(scene, g) {
|
|
98
|
+
const ref = g.near.find(r => TOP.test(r));
|
|
99
|
+
if (!ref) return "";
|
|
100
|
+
const id = TOP.exec(ref)[1];
|
|
101
|
+
return nearRefs(scene, g) !== g.near ? ` (measured at ${id}'s rim nearest the mouth, object:${id}.rim; its .top is its bounding box's top)`
|
|
102
|
+
: ` (${ref} is the top-face centre of its world bounding box; object:${id}.rim is its opening, which tilts with it)`;
|
|
103
|
+
}
|
|
104
|
+
|
|
77
105
|
/** The measured value of goal g at absolute frame f (metres, m/s for speed), or null when a ref is unknown there. */
|
|
78
106
|
function measure(out, g, kind, f, home) {
|
|
79
|
-
if (kind === "near") return refDistance(out, f,
|
|
107
|
+
if (kind === "near") { const [a, b] = nearRefs(out.ctx.scene, g); return refDistance(out, f, a, b); }
|
|
80
108
|
if (kind === "above") {
|
|
81
109
|
const a = refPoint(out, f, g.above[0]), b = refPoint(out, f, g.above[1]);
|
|
82
110
|
return a && b ? a[1] - b[1] : null;
|
|
@@ -139,9 +167,9 @@ function windowOf(X, i) {
|
|
|
139
167
|
return { a, b };
|
|
140
168
|
}
|
|
141
169
|
|
|
142
|
-
function message(name, kind, row, need, any, from) {
|
|
170
|
+
function message(name, kind, row, need, any, from, note = "") {
|
|
143
171
|
const where = any ? `f${row.f}, the best of f${any[0]}-f${any[1]}` : `f${row.f}`;
|
|
144
|
-
if (kind === "near") return `${name}: ${m3(row.v)} m at ${where} (needs <= ${need})`;
|
|
172
|
+
if (kind === "near") return `${name}: ${m3(row.v)} m at ${where} (needs <= ${need})${note}`;
|
|
145
173
|
if (kind === "above") return `${name}: ${m3(row.v)} m above at ${where} (needs >= ${need})`;
|
|
146
174
|
if (kind === "speed") return `${name}: ${m3(row.v)} m/s at ${where} (needs >= ${need})`;
|
|
147
175
|
if (kind === "still") return `${name}: ${m3(row.v)} m from ${from === undefined ? "its scene pose" : `f${from}`} at ${where} (needs <= ${need})`;
|
|
@@ -183,8 +211,9 @@ export function goalChecks(out, goals) {
|
|
|
183
211
|
const ok = pick.short <= 0;
|
|
184
212
|
results.push({ name, kind, ok, value: m3(pick.v), at: pick.f, need });
|
|
185
213
|
if (ok) return;
|
|
214
|
+
const note = kind === "near" ? topNote(out.ctx.scene, g) : "";
|
|
186
215
|
for (const r of any ? [pick] : rows.filter(r => r.short > 0)) {
|
|
187
|
-
issues.push({ kind: "GOAL", msg: message(name, kind, r, need, any && cut ? [w.a, w.b] : null, from), frame: r.f - f0, value: round4(r.short), limit: 0 });
|
|
216
|
+
issues.push({ kind: "GOAL", msg: message(name, kind, r, need, any && cut ? [w.a, w.b] : null, from, kind === "near" ? note : ""), frame: r.f - f0, value: round4(r.short), limit: 0 });
|
|
188
217
|
}
|
|
189
218
|
});
|
|
190
219
|
return { issues, results };
|
|
@@ -14,7 +14,7 @@ import { inHandAt } from "../object-pose.js";
|
|
|
14
14
|
import { refPoint } from "../refs.js";
|
|
15
15
|
import { scale, norm, sub, len } from "../compiler/math.js";
|
|
16
16
|
import { moveHint } from "./hints-contact.js";
|
|
17
|
-
import { goalKinds, goalName, goalNeed, goalWindow, movedHome, movedRef } from "./goals.js";
|
|
17
|
+
import { goalKinds, goalName, goalNeed, goalWindow, movedHome, movedRef, nearRefs } from "./goals.js";
|
|
18
18
|
import { m3, vec } from "./fmt.js";
|
|
19
19
|
|
|
20
20
|
const MARGIN = 0.003, LEAK = 0.02, BODY_MAX = 0.2;
|
|
@@ -79,7 +79,7 @@ function partOf(X, ref, f) {
|
|
|
79
79
|
const [, kind, rest] = /^([a-z]+):(.+)$/s.exec(ref) ?? [];
|
|
80
80
|
if (kind === "prop") return X.spec.props?.[rest.split(".")[0]]?.hand ? `hand_${X.spec.props[rest.split(".")[0]].hand}` : null;
|
|
81
81
|
if (kind === "object") {
|
|
82
|
-
const o = carrierOf(X, rest.replace(/\.(center|top|base)$/, ""), f), k = o && X.spec.props?.[o.prop]?.hand;
|
|
82
|
+
const o = carrierOf(X, rest.replace(/\.(center|top|base|rim)$/, ""), f), k = o && X.spec.props?.[o.prop]?.hand;
|
|
83
83
|
return k ? `hand_${k}` : null;
|
|
84
84
|
}
|
|
85
85
|
if (kind === "landmark") return "root";
|
|
@@ -107,7 +107,8 @@ function plan(X, goal, kind, f, amount, from) {
|
|
|
107
107
|
const away = p && home && len(sub(p, home)) > 1e-3 ? norm(sub(p, home)) : [0, 1, 0];
|
|
108
108
|
return part ? [part, scale(away, kind === "still" ? -amount : amount)] : null;
|
|
109
109
|
}
|
|
110
|
-
|
|
110
|
+
// a near goal measures what goals.js nearRefs reads (a cup's rim at the mouth)
|
|
111
|
+
const [a, b] = kind === "near" ? nearRefs(X.out.ctx.scene, goal) : goal[kind], pa = partOf(X, a, f), pb = partOf(X, b, f);
|
|
111
112
|
const rank = p => (p?.startsWith("hand_") ? 2 : p ? 1 : 0), moveA = rank(pa) >= rank(pb);
|
|
112
113
|
const part = moveA ? pa : pb, sign = moveA ? 1 : -1;
|
|
113
114
|
if (!part) return null;
|
|
@@ -136,13 +137,13 @@ export const goalHints = {
|
|
|
136
137
|
const move = plan(X, goal, kind, f, amount, w.a);
|
|
137
138
|
if (!move) {
|
|
138
139
|
if (kind === "still" && goal.still.startsWith("object:")) {
|
|
139
|
-
const id = goal.still.slice(7).replace(/\.(center|top|base)$/, "");
|
|
140
|
+
const id = goal.still.slice(7).replace(/\.(center|top|base|rim)$/, "");
|
|
140
141
|
return { cause: `objects.${id}`, hint: `${id} moves at f${f}, where it should stay: hold its hinge channel or keys until then, or start them later${why}` };
|
|
141
142
|
}
|
|
142
143
|
if (kind === "still") return { cause: `goal ${name}`, hint: `${goal.still} moves at f${f} and nothing that follows the body moves it; name a joint, prop point or held object${why}` };
|
|
143
144
|
if (kind === "moved" && !goal.moved.startsWith("object:")) return { cause: `goal ${name}`, hint: `${goal.moved} does not move with the body at f${f}; name a joint, prop point or held object` };
|
|
144
145
|
if (kind === "moved") {
|
|
145
|
-
const id = goal.moved.slice(7).replace(/\.(center|top|base)$/, "");
|
|
146
|
+
const id = goal.moved.slice(7).replace(/\.(center|top|base|rim)$/, "");
|
|
146
147
|
return { cause: `objects.${id}`, hint: `${id} is not in a hand at f${f}, so nothing moves it: carry it (objects.${id} = { prop, grab }) or hinge it` };
|
|
147
148
|
}
|
|
148
149
|
return { cause: `goal ${name}`, hint: `neither ${goal[kind][0]} nor ${goal[kind][1]} moves with the body at f${f}; name a joint, prop point or held object` };
|
package/src/motion3/qa/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// moves (object-checks.js), the other people in the scene (people-checks.js, PERSON), its goals (goals.js) and the style checks it opted into by naming
|
|
3
3
|
// their limit (style.js; otherwise style is measured, not judged: observeMotion). Groups are
|
|
4
4
|
// ranked by severity.js. total counts frame-level issues (a goal held on "all" frames adds one per failing frame); problems
|
|
5
|
-
// counts the groups, which is what a person reads (src/qa-label.js).
|
|
5
|
+
// counts the groups, which is what a person reads (src/qa-label.js). qa.warnings (arm-checks.js) reads an awkward
|
|
6
|
+
// working arm and never fails a take.
|
|
6
7
|
import { generalChecks, QA_LIMITS } from "./checks.js";
|
|
7
8
|
import { styleChecks, floatChecks } from "./style.js";
|
|
8
9
|
import { specChecks } from "./spec-checks.js";
|
|
@@ -12,6 +13,7 @@ import { loadChecks } from "../load.js";
|
|
|
12
13
|
import { goalChecks } from "./goals.js";
|
|
13
14
|
import { wristIssues } from "./wrist-checks.js";
|
|
14
15
|
import { peopleChecks } from "./people-checks.js";
|
|
16
|
+
import { armWarnings } from "./arm-checks.js";
|
|
15
17
|
import { explainGroups } from "./explain.js";
|
|
16
18
|
import { LOW_IS_BAD, groupKey, rankGroups } from "./severity.js";
|
|
17
19
|
|
|
@@ -42,5 +44,6 @@ export function runMotionQa(sk, out, spec, opts = {}) {
|
|
|
42
44
|
if (out.f0) for (const i of issues) i.frame += out.f0;
|
|
43
45
|
const groups = summarizeIssues(issues);
|
|
44
46
|
if (opts.explain !== false) explainGroups(sk, out, spec, groups, { userSpec: opts.userSpec });
|
|
45
|
-
|
|
47
|
+
const warnings = armWarnings(sk, out);
|
|
48
|
+
return { ok: issues.length === 0, total: issues.length, problems: groups.length, ...(spec.goals ? { goals: goals.results } : {}), groups, ...(warnings.length ? { warnings } : {}) };
|
|
46
49
|
}
|
|
@@ -62,7 +62,7 @@ export function keyHint(X, g) {
|
|
|
62
62
|
|
|
63
63
|
const r1 = v => Math.round(v * 10) / 10, r2 = v => Math.round(v * 100) / 100;
|
|
64
64
|
/** observations.held units: what a row of a keyed object says. */
|
|
65
|
-
export const HELD_UNITS = "held: per key, deg = the drawn +Y off the asked one (aim, or rot's +Y), cm = the drawn base off pos, got = the drawn +Y (when 1 deg off or more), rot = what an aim key used";
|
|
65
|
+
export const HELD_UNITS = "held: per key, deg = the drawn +Y off the asked one (aim, or rot's +Y), cm = the drawn base off pos, got = the drawn +Y (when 1 deg off or more), rot = what an aim key used, lean = the lean a mouth key took by default (give lean 0 to hold aim exactly)";
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
68
|
* Every key of every keyed carry as drawn: { <id>: [{ f, deg, cm, got?, rot? }] } in key order
|
|
@@ -72,11 +72,12 @@ export function heldReadings(out) {
|
|
|
72
72
|
const held = keyedEntries(out.ctx.spec);
|
|
73
73
|
if (!held.length) return null;
|
|
74
74
|
return Object.fromEntries(held.map(([id, o]) => {
|
|
75
|
-
|
|
75
|
+
// the keys as the compile holds them: a mouth key the compile leaned says so (object-keys.js faceLeaned)
|
|
76
|
+
const curve = curveIn(out, id), keys = [...(out.ctx.hold?.entry?.(id) ?? o).keys].sort((a, b) => a.f - b.f);
|
|
76
77
|
return [id, keys.map((k, i) => {
|
|
77
78
|
const got = rootPoseAt(out, id, Math.round(curve.t[i])), y = [got.R[0][1], got.R[1][1], got.R[2][1]];
|
|
78
79
|
const want = k.aim ? norm(k.aim) : apply(matFromQuat(curve.q[i]), [0, 1, 0]), deg = r1(angle(want, y));
|
|
79
|
-
return { f: k.f, deg, cm: r1(dist(k.pos, got.pos) * 100), ...(deg >= 1 ? { got: y.map(r2) } : {}), ...(k.aim ? { rot: curve.rots[i] } : {}) };
|
|
80
|
+
return { f: k.f, deg, cm: r1(dist(k.pos, got.pos) * 100), ...(deg >= 1 ? { got: y.map(r2) } : {}), ...(k.aim ? { rot: curve.rots[i] } : {}), ...(k.leaned ? { lean: k.lean } : {}) };
|
|
80
81
|
})];
|
|
81
82
|
}));
|
|
82
83
|
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// Measured, not judged (output.observations.life): how mechanical a take reads. Five cheap readings
|
|
2
|
+
// over the baked frames and the keys the agent wrote, one linear pass each, warnings only (never a
|
|
3
|
+
// QA group, never qa.ok):
|
|
4
|
+
// sync outside walks, the frame each body part (CHAINS) starts and stops moving (its activity,
|
|
5
|
+
// below, past START of its usual level after a hold); the starts (stops) of several parts
|
|
6
|
+
// close together are one transition, synchronous where most of them go within +-1 frame.
|
|
7
|
+
// A body does not do that: the pelvis or a hand leads and the trunk, head and arms follow
|
|
8
|
+
// a few frames later. score = the share of transitions that are synchronous.
|
|
9
|
+
// keys the gaps between the agent's key frames (per channel and all channels as one): their
|
|
10
|
+
// coefficient of variation (cv, 0 = an even grid) and the share of keys on a half second.
|
|
11
|
+
// Keys on a beat move every pose at one tempo; keys on the extremes do not.
|
|
12
|
+
// jerk the third difference of each CHAINS joint's position (m/s^3): its 95th percentile on
|
|
13
|
+
// the frames the body moves, and spike frames, where a joint goes past SPIKE times its level
|
|
14
|
+
// (and JERK_FLOOR): the acceleration snaps there, a seam between two keys' curves (a landing
|
|
15
|
+
// too); warned past twice that.
|
|
16
|
+
// twin outside walks (the hips over WALK_SPEED) and jumps (no foot still), in WINDOW-frame windows: the correlation of
|
|
17
|
+
// the left and right limb's bend speeds (elbow and shoulder; knee and hip). Over TWIN both
|
|
18
|
+
// sides do the same thing on the same frames. score = the share of windows that twin.
|
|
19
|
+
// dead runs of a second or more with no joint moving over DEAD m a frame outside a still goal:
|
|
20
|
+
// a statue (idle false, or a pose held where the idle layer stays off).
|
|
21
|
+
// Activity is a part's own motion: the turn (deg/frame) of its local rotation, and for the hips
|
|
22
|
+
// their world turn or travel (cm/frame), so a hand carried by its arm does not count as moving.
|
|
23
|
+
// Why: compiled takes pass QA and still read mechanical (every part starts and stops on one frame,
|
|
24
|
+
// keys every 12 frames, both arms twinned), and nothing in the answer said so.
|
|
25
|
+
import { sub, len } from "../compiler/math.js";
|
|
26
|
+
|
|
27
|
+
const CHAINS = ["Hips", "Spine", "Spine2", "Head", "LeftArm", "RightArm", "LeftForeArm", "RightForeArm", "LeftHand", "RightHand",
|
|
28
|
+
"LeftUpLeg", "RightUpLeg", "LeftLeg", "RightLeg", "LeftFoot", "RightFoot"];
|
|
29
|
+
/** A part moves above MOVE deg (cm) a frame; holds are HOLD frames or more under it; START of a part's usual level starts it. */
|
|
30
|
+
const MOVE = 0.25, HOLD = 3, START = 0.2;
|
|
31
|
+
/** sync: events GAP frames or less apart are one transition; synchronous when SYNC_MIN parts and SYNC_SHARE of its parts go within +-1 frame. */
|
|
32
|
+
const SYNC_MIN = 4, SYNC_SHARE = 0.6, GAP = 3;
|
|
33
|
+
const GRID_CV = 0.15, GRID_KEYS = 4, HALF_SHARE = 0.7;
|
|
34
|
+
const SPIKE = 4, JERK_FLOOR = 20;
|
|
35
|
+
const WALK_SPEED = 0.25, FOOT_STILL = 0.005, WINDOW = 48, TWIN = 0.9, TWIN_SWEEP = 15;
|
|
36
|
+
const DEAD = 1e-4;
|
|
37
|
+
const r2 = x => Math.round(x * 100) / 100;
|
|
38
|
+
|
|
39
|
+
export const LIFE_UNITS = "life (warnings, not QA): [share or s, worst f0, f1]; keys [gap cv, even channels, half-second share]; jerk [p95 m/s^3, joint, spikes]";
|
|
40
|
+
|
|
41
|
+
const p95 = xs => { const s = [...xs].sort((a, b) => a - b); return s.length ? s[Math.min(s.length - 1, Math.floor(0.95 * s.length))] : 0; };
|
|
42
|
+
const cvOf = gaps => { const m = gaps.reduce((a, b) => a + b, 0) / gaps.length, v = gaps.reduce((a, g) => a + (g - m) ** 2, 0) / gaps.length; return m > 0 ? Math.sqrt(v) / m : 0; };
|
|
43
|
+
function pearson(a, b) {
|
|
44
|
+
const n = a.length, ma = a.reduce((s, x) => s + x, 0) / n, mb = b.reduce((s, x) => s + x, 0) / n;
|
|
45
|
+
let ab = 0, aa = 0, bb = 0;
|
|
46
|
+
for (let i = 0; i < n; i++) { ab += (a[i] - ma) * (b[i] - mb); aa += (a[i] - ma) ** 2; bb += (b[i] - mb) ** 2; }
|
|
47
|
+
return aa > 0 && bb > 0 ? ab / Math.sqrt(aa * bb) : 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Per CHAINS part, activity[f] (from f-1): its local turn in deg; the hips the larger of their world turn and travel in cm. */
|
|
51
|
+
function activities(sk, F) {
|
|
52
|
+
const I = sk.index, n = F.length, DEG = 180 / Math.PI;
|
|
53
|
+
return CHAINS.map(j => {
|
|
54
|
+
const b = I[j], p = j === "Hips" ? -1 : sk.parent[b], act = new Float64Array(n);
|
|
55
|
+
let L = new Float64Array(9), prev = new Float64Array(9);
|
|
56
|
+
for (let f = 0; f < n; f++) {
|
|
57
|
+
const { R, P } = F[f].s, B = R[b], A = p < 0 ? null : R[p];
|
|
58
|
+
// L = A^T B (the local rotation), written out: a pass over every frame of every part must not allocate
|
|
59
|
+
for (let r = 0; r < 3; r++) for (let c = 0; c < 3; c++) L[r * 3 + c] = A ? A[0][r] * B[0][c] + A[1][r] * B[1][c] + A[2][r] * B[2][c] : B[r][c];
|
|
60
|
+
if (f) {
|
|
61
|
+
let tr = 0;
|
|
62
|
+
for (let i = 0; i < 9; i++) tr += L[i] * prev[i];
|
|
63
|
+
const turn = Math.acos(Math.max(-1, Math.min(1, (tr - 1) / 2))) * DEG, Q = F[f - 1].s.P[b];
|
|
64
|
+
act[f] = p < 0 ? Math.max(turn, Math.hypot(P[b][0] - Q[0], P[b][1] - Q[1], P[b][2] - Q[2]) * 100) : turn;
|
|
65
|
+
}
|
|
66
|
+
[L, prev] = [prev, L];
|
|
67
|
+
}
|
|
68
|
+
return act;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Moves between holds: [a, b] runs of frames where some part moves, gaps under HOLD frames joined. */
|
|
73
|
+
function moves(acts, n) {
|
|
74
|
+
const runs = [];
|
|
75
|
+
let a = -1, quiet = 0;
|
|
76
|
+
for (let f = 1; f < n; f++) {
|
|
77
|
+
if (acts.some(act => act[f] > MOVE)) { if (a < 0) a = f; quiet = 0; }
|
|
78
|
+
else if (a >= 0 && ++quiet >= HOLD) { runs.push([a, f - quiet]); a = -1; quiet = 0; }
|
|
79
|
+
}
|
|
80
|
+
if (a >= 0) runs.push([a, n - 1 - quiet]);
|
|
81
|
+
return runs.filter(([x, y]) => y - x >= 3);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Starts and stops outside walks: a part starts where its activity rises past its threshold
|
|
86
|
+
* (MOVE, or START of its usual level: the 90th percentile of its moving frames) after HOLD frames
|
|
87
|
+
* under it, and stops where it falls under it for HOLD frames. Events of several parts that follow
|
|
88
|
+
* one another within GAP frames are one transition; it is synchronous when, of the m parts in it
|
|
89
|
+
* (SYNC_MIN or more), max(SYNC_MIN, SYNC_SHARE * m) go within one 3-frame window.
|
|
90
|
+
*/
|
|
91
|
+
function syncOf(acts, walking, n) {
|
|
92
|
+
const events = [[], []];
|
|
93
|
+
acts.forEach((act, j) => {
|
|
94
|
+
const level = [...act].filter(v => v > MOVE).sort((a, b) => a - b), thr = Math.max(MOVE, START * (level[Math.floor(0.9 * level.length)] ?? 0));
|
|
95
|
+
const on = f => act[f] > thr;
|
|
96
|
+
for (let f = 1; f < n; f++) {
|
|
97
|
+
if (walking[f]) continue;
|
|
98
|
+
if (on(f) && !on(f - 1)) { let q = 1; while (q <= HOLD && f - q >= 0 && !on(f - q)) q++; if (q > HOLD || f - q < 0) events[0].push([f, j]); }
|
|
99
|
+
if (!on(f) && on(f - 1)) { let q = 0; while (q < HOLD && f + q < n && !on(f + q)) q++; if (q >= HOLD && f + q < n) events[1].push([f - 1, j]); }
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
let total = 0, synced = 0, worst = null;
|
|
103
|
+
for (const list of events) {
|
|
104
|
+
list.sort((a, b) => a[0] - b[0]);
|
|
105
|
+
for (let i = 0; i < list.length;) {
|
|
106
|
+
let k = i + 1;
|
|
107
|
+
while (k < list.length && list[k][0] - list[k - 1][0] <= GAP) k++;
|
|
108
|
+
const group = list.slice(i, k), m = new Set(group.map(e => e[1])).size;
|
|
109
|
+
i = k;
|
|
110
|
+
if (m < SYNC_MIN) continue;
|
|
111
|
+
let best = 0, at = group[0][0];
|
|
112
|
+
for (let x = 0, y = 0; y < group.length; y++) {
|
|
113
|
+
while (group[y][0] - group[x][0] > 2) x++;
|
|
114
|
+
const c = new Set(group.slice(x, y + 1).map(e => e[1])).size;
|
|
115
|
+
if (c > best) { best = c; at = group[x][0]; }
|
|
116
|
+
}
|
|
117
|
+
total++;
|
|
118
|
+
if (best >= Math.max(SYNC_MIN, Math.ceil(SYNC_SHARE * m))) synced++;
|
|
119
|
+
if (!worst || best / m > worst.share || (best / m === worst.share && best > worst.k)) worst = { share: best / m, k: best, of: m, at: [at, at + 2] };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { score: total ? r2(synced / total) : 0, total, worst };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The agent's keys: cv of all key gaps, the channels keyed on an even grid, the share on half seconds. */
|
|
126
|
+
function keysOf(keys, fps) {
|
|
127
|
+
const per = new Map(), all = new Set();
|
|
128
|
+
for (const k of keys ?? []) {
|
|
129
|
+
if (!Number.isFinite(k?.f)) continue;
|
|
130
|
+
all.add(k.f);
|
|
131
|
+
for (const ch of Object.keys(k.set ?? {})) (per.get(ch) ?? per.set(ch, new Set()).get(ch)).add(k.f);
|
|
132
|
+
}
|
|
133
|
+
const gaps = fs => { const s = [...fs].sort((a, b) => a - b); return s.slice(1).map((f, i) => f - s[i]); };
|
|
134
|
+
const eligible = [...per].filter(([, fs]) => fs.size >= GRID_KEYS), grid = eligible.filter(([, fs]) => cvOf(gaps(fs)) < GRID_CV).map(([ch]) => ch);
|
|
135
|
+
const inner = [...all].filter(f => f > 0), half = Math.round(fps / 2);
|
|
136
|
+
return { cv: all.size >= GRID_KEYS ? r2(cvOf(gaps(all))) : null, grid, eligible: eligible.length,
|
|
137
|
+
halfSec: inner.length >= GRID_KEYS ? r2(inner.filter(f => f % half === 0).length / inner.length) : null };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function jerkOf(sk, F, moving, fps) {
|
|
141
|
+
const k3 = fps ** 3;
|
|
142
|
+
let top = [0, null], worst = null;
|
|
143
|
+
const spikes = new Set();
|
|
144
|
+
for (const j of CHAINS) {
|
|
145
|
+
const X = F.map(fr => fr.s.P[sk.index[j]]), J = new Float64Array(F.length), on = [];
|
|
146
|
+
for (let f = 3; f < F.length; f++) {
|
|
147
|
+
const d = i => X[f][i] - 3 * X[f - 1][i] + 3 * X[f - 2][i] - X[f - 3][i];
|
|
148
|
+
J[f] = Math.hypot(d(0), d(1), d(2)) * k3;
|
|
149
|
+
if (moving[f]) on.push(J[f]);
|
|
150
|
+
}
|
|
151
|
+
const level = p95(on);
|
|
152
|
+
if (level > top[0]) top = [r2(level), j];
|
|
153
|
+
for (let f = 3; f < F.length; f++) {
|
|
154
|
+
if (J[f] <= Math.max(JERK_FLOOR, SPIKE * level)) continue;
|
|
155
|
+
spikes.add(f);
|
|
156
|
+
const ratio = J[f] / Math.max(level, 1e-9);
|
|
157
|
+
if (!worst || ratio > worst.ratio) worst = { ratio, f, j };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { p95: top, spikes: spikes.size, worst };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Bend angles (deg) per frame of one side's limb: [elbow, shoulder] or [knee, hip]. */
|
|
164
|
+
function bends(sk, s, S, arm) {
|
|
165
|
+
const I = sk.index, P = s.P, up = sub(P[I.Spine2], P[I.Hips]);
|
|
166
|
+
const ang = (a, b) => Math.acos(Math.max(-1, Math.min(1, (a[0] * b[0] + a[1] * b[1] + a[2] * b[2]) / Math.max(1e-9, len(a) * len(b))))) * 180 / Math.PI;
|
|
167
|
+
const [r, m, e] = arm ? [`${S}Arm`, `${S}ForeArm`, `${S}Hand`] : [`${S}UpLeg`, `${S}Leg`, `${S}Foot`];
|
|
168
|
+
const u = sub(P[I[m]], P[I[r]]);
|
|
169
|
+
return [ang(u, sub(P[I[e]], P[I[m]])), ang(u, arm ? up : [-up[0], -up[1], -up[2]])];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function twinOf(sk, F, walking) {
|
|
173
|
+
const res = {};
|
|
174
|
+
for (const [name, arm] of [["arms", true], ["legs", false]]) {
|
|
175
|
+
const L = F.map(({ s }) => bends(sk, s, "Left", arm)), R = F.map(({ s }) => bends(sk, s, "Right", arm));
|
|
176
|
+
let windows = 0, twins = 0, worst = null;
|
|
177
|
+
for (let a = 1; a + WINDOW <= F.length; a += WINDOW / 4) {
|
|
178
|
+
if (walking.slice(a, a + WINDOW).some(Boolean)) continue;
|
|
179
|
+
const dl = [], dr = [];
|
|
180
|
+
for (let f = a; f < a + WINDOW; f++) for (let i = 0; i < 2; i++) { dl.push(L[f][i] - L[f - 1][i]); dr.push(R[f][i] - R[f - 1][i]); }
|
|
181
|
+
const sweep = d => d.reduce((x, v) => x + Math.abs(v), 0);
|
|
182
|
+
if (sweep(dl) < TWIN_SWEEP || sweep(dr) < TWIN_SWEEP) continue;
|
|
183
|
+
const c = pearson(dl, dr);
|
|
184
|
+
windows++;
|
|
185
|
+
if (c > TWIN) twins++;
|
|
186
|
+
if (!worst || c > worst.c) worst = { c, at: [a, a + WINDOW - 1] };
|
|
187
|
+
}
|
|
188
|
+
res[name] = { score: windows ? r2(twins / windows) : 0, windows, worst };
|
|
189
|
+
}
|
|
190
|
+
return res;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function deadOf(sk, F, spec, fps) {
|
|
194
|
+
const still = new Uint8Array(F.length);
|
|
195
|
+
for (const g of spec.goals ?? []) {
|
|
196
|
+
const at = Array.isArray(g?.at) ? g.at : null;
|
|
197
|
+
if (g?.still !== undefined && at?.length) for (let f = Math.max(0, Math.round(at[0])); f <= Math.min(F.length - 1, Math.round(at[at.length - 1])); f++) still[f] = 1;
|
|
198
|
+
}
|
|
199
|
+
let best = null, frames = 0;
|
|
200
|
+
for (let f = 1, a = -1; f <= F.length; f++) {
|
|
201
|
+
const dead = f < F.length && !still[f] && sk.names.every((_, j) => len(sub(F[f].s.P[j], F[f - 1].s.P[j])) < DEAD);
|
|
202
|
+
if (dead) { if (a < 0) a = f; continue; }
|
|
203
|
+
if (a >= 0 && f - a >= fps) { frames += f - a; if (!best || f - a > best[1] - best[0] + 1) best = [a, f - 1]; }
|
|
204
|
+
a = -1;
|
|
205
|
+
}
|
|
206
|
+
return { frames, best };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* observeLife(sk, out, keys) -> observations.life; keys are the spec keys as the agent wrote them
|
|
211
|
+
* (a skill's keys are the compiler's). Frames are absolute (out.f0 added). flags names the metrics
|
|
212
|
+
* past their mark, worst first; warn is what to change for the worst (one line: the receipt budget, report.js).
|
|
213
|
+
*/
|
|
214
|
+
export function observeLife(sk, out, keys) {
|
|
215
|
+
const F = out.frames, n = F.length, fps = out.fps ?? 24, f0 = out.f0 ?? 0, spec = out.ctx.spec, I = sk.index, warnings = [];
|
|
216
|
+
const acts = activities(sk, F), runs = moves(acts, n), moving = new Uint8Array(n);
|
|
217
|
+
for (const [a, b] of runs) moving.fill(1, a, b + 1);
|
|
218
|
+
// a walk: the hips travel over WALK_SPEED, widened by half a window so its first and last steps count
|
|
219
|
+
const walking = new Uint8Array(n), pad = WINDOW / 2;
|
|
220
|
+
for (let f = 1; f < n; f++) {
|
|
221
|
+
const P = F[f].s.P[I.Hips], Q = F[f - 1].s.P[I.Hips];
|
|
222
|
+
if (Math.hypot(P[0] - Q[0], P[2] - Q[2]) * fps > WALK_SPEED) walking.fill(1, Math.max(0, f - pad), Math.min(n, f + pad));
|
|
223
|
+
}
|
|
224
|
+
// twins leave out walks and jumps too: no foot still (a flip, a hop takes both legs together)
|
|
225
|
+
const free = walking.slice();
|
|
226
|
+
for (let f = 1; f < n; f++) if (["LeftFoot", "RightFoot"].every(j => len(sub(F[f].s.P[I[j]], F[f - 1].s.P[I[j]])) > FOOT_STILL)) free.fill(1, Math.max(0, f - pad), Math.min(n, f + pad));
|
|
227
|
+
const sync = syncOf(acts, walking, n), k = keysOf(keys, fps), jerk = jerkOf(sk, F, moving, fps), twin = twinOf(sk, F, free), dead = deadOf(sk, F, spec, fps);
|
|
228
|
+
const span = ([a, b]) => `f${a + f0}-${b + f0}`;
|
|
229
|
+
// [weight, metric, advice]: the answer keeps the worst advice and names every metric it flags
|
|
230
|
+
if (sync.total >= 2 && sync.score >= 0.5) warnings.push([sync.score, "sync", `${sync.worst.k} of ${sync.worst.of} parts start or stop together ${span(sync.worst.at)}: lead with pelvis or a hand, spine/head/arms 2-4 frames later`]);
|
|
231
|
+
const gridKeys = (k.cv !== null && k.cv < GRID_CV) || (k.eligible && k.grid.length / k.eligible >= 0.5) || (k.halfSec ?? 0) >= HALF_SHARE;
|
|
232
|
+
if (gridKeys) warnings.push([0.9, "keys", `keys on a grid: key the extremes (contact, peak, turn), vary the gaps`]);
|
|
233
|
+
if (jerk.worst && jerk.worst.ratio > 2 * SPIKE) warnings.push([0.6, "jerk", `${jerk.worst.j} jerks ${Math.round(jerk.worst.ratio)}x at f${jerk.worst.f + f0}: a key snaps; ease or move it`]);
|
|
234
|
+
for (const [name, t] of Object.entries(twin)) if (t.windows >= 2 && t.score >= 0.4) warnings.push([t.score, name, `${name} mirror ${span(t.worst.at)}: ${name === "legs" ? "weight onto one foot, not straight down" : "offset one side 2-4 frames or pose it apart"}`]);
|
|
235
|
+
if (dead.best) warnings.push([0.7, "dead", `dead still ${span(dead.best)}${spec.idle === false ? " (idle false)" : ""}: let idle run or key a settle`]);
|
|
236
|
+
warnings.sort((a, b) => b[0] - a[0]);
|
|
237
|
+
const at = (score, w) => (w ? [score, w[0] + f0, w[1] + f0] : [score]);
|
|
238
|
+
return {
|
|
239
|
+
sync: at(sync.score, sync.worst?.at), keys: [k.cv, k.grid.length, k.halfSec], jerk: [...jerk.p95, jerk.spikes],
|
|
240
|
+
arms: at(twin.arms.score, twin.arms.worst?.at), legs: at(twin.legs.score, twin.legs.worst?.at), dead: at(r2(dead.frames / fps), dead.best),
|
|
241
|
+
...(warnings.length ? { flags: warnings.map(w => w[1]), warn: warnings[0][2] } : {}),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
@@ -36,7 +36,7 @@ function graspPart(out, ref) {
|
|
|
36
36
|
return out.ctx.sockets?.get(name)?.owner ?? a.object;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
const active = (out, f, when) => (Array.isArray(when) ? f + (out.f0 ?? 0) >= when[0] && f + (out.f0 ?? 0) <= when[1] : when ? (out.frames[f].c[when] ?? 0) > 0.99 : true);
|
|
39
|
+
export const active = (out, f, when) => (Array.isArray(when) ? f + (out.f0 ?? 0) >= when[0] && f + (out.f0 ?? 0) <= when[1] : when ? (out.frames[f].c[when] ?? 0) > 0.99 : true);
|
|
40
40
|
|
|
41
41
|
function alongDir(out, f, along) {
|
|
42
42
|
if (along === "up") return [0, 1, 0];
|
|
@@ -44,19 +44,25 @@ function alongDir(out, f, along) {
|
|
|
44
44
|
return norm(along);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** How far a checks.contacts entry k's point is from its target at frame f (indexes out.frames), m; null when a point is unknown there. */
|
|
48
|
+
export function contactGap(out, f, k) {
|
|
49
|
+
const a = pointOf(out, f, k.point), b = pointOf(out, f, k.target);
|
|
50
|
+
if (!a || !b) return null;
|
|
51
|
+
// on a grasp's anchor the fingers close on the held part wherever the grasp turned them round it: they are on it
|
|
52
|
+
// when they are on its surface (as goals' near on an object), not on one point of it (session 7f15b4ae: 0.4 cm
|
|
53
|
+
// on the cup, read as 5.3 cm from the anchor)
|
|
54
|
+
// and a hand that holds a thing resting in its palm (seat.js) has its fingers on that thing's surface
|
|
55
|
+
const held = graspPart(out, k.target) ?? seatedIn(out, k.point, f + (out.f0 ?? 0));
|
|
56
|
+
return held ? refDistance(out, f, k.point, `object:${held}`) ?? len(sub(a, b)) : len(sub(a, b));
|
|
57
|
+
}
|
|
58
|
+
|
|
47
59
|
export function specChecks(out, checks = {}) {
|
|
48
60
|
const issues = [], n = out.frames.length, sk = out.ctx.sk, I = sk.index;
|
|
49
61
|
const push = (kind, msg, f, value, limit) => issues.push({ kind, msg, frame: f, value: +value.toFixed(4), limit });
|
|
50
62
|
for (let f = 0; f < n; f++) {
|
|
51
63
|
for (const k of checks.contacts ?? []) if (active(out, f, k.when)) {
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
// on a grasp's anchor the fingers close on the held part wherever the grasp turned them round it: they are on it
|
|
55
|
-
// when they are on its surface (as goals' near on an object), not on one point of it (session 7f15b4ae: 0.4 cm
|
|
56
|
-
// on the cup, read as 5.3 cm from the anchor)
|
|
57
|
-
// and a hand that holds a thing resting in its palm (seat.js) has its fingers on that thing's surface
|
|
58
|
-
const held = graspPart(out, k.target) ?? seatedIn(out, k.point, f + (out.f0 ?? 0)), e = held ? refDistance(out, f, k.point, `object:${held}`) ?? len(sub(a, b)) : len(sub(a, b));
|
|
59
|
-
if (e > (k.tol ?? 0.01)) push("CONTACT", `${k.name}: ${(e * 100).toFixed(1)} cm apart`, f, e, k.tol ?? 0.01);
|
|
64
|
+
const e = contactGap(out, f, k);
|
|
65
|
+
if (e !== null && e > (k.tol ?? 0.01)) push("CONTACT", `${k.name}: ${(e * 100).toFixed(1)} cm apart`, f, e, k.tol ?? 0.01);
|
|
60
66
|
}
|
|
61
67
|
for (const k of checks.orient ?? []) if (active(out, f, k.when)) {
|
|
62
68
|
const a = pointOf(out, f, k.from), b = pointOf(out, f, k.to);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// C6. Is one compile's QA better than another's: the one rule repair (repair.js) keeps a change by and the hint check
|
|
2
|
+
// (hint-check.js) offers a patch by. Better means all of
|
|
3
|
+
// less the summed severity (severity.js) drops
|
|
4
|
+
// goals no more goals fail, and every goal that held still holds
|
|
5
|
+
// problems no more issue groups than before
|
|
6
|
+
// trade what got worse (a new group, or a group whose severity rose) costs at most half of what got better
|
|
7
|
+
// Logs: a hint patch applied as sent made the take worse 49% of the time, and repair moved the root and broke the arms;
|
|
8
|
+
// a lower sum alone let a change through that traded one failure for several new ones.
|
|
9
|
+
import { groupKey } from "./severity.js";
|
|
10
|
+
|
|
11
|
+
const groupsOf = qa => qa?.groups ?? [];
|
|
12
|
+
const sevOf = qa => groupsOf(qa).reduce((sum, g) => sum + (g.severity ?? 0), 0);
|
|
13
|
+
const heldOf = qa => (qa?.goals ?? []).filter(g => g.ok).map(g => g.name);
|
|
14
|
+
const failedOf = qa => (qa?.goals ?? []).filter(g => !g.ok).length;
|
|
15
|
+
const TRADE = 0.5;
|
|
16
|
+
const r2 = x => Math.round(x * 100) / 100;
|
|
17
|
+
|
|
18
|
+
/** The group severities by key (severity.js groupKey). */
|
|
19
|
+
function byKey(qa) {
|
|
20
|
+
const m = new Map();
|
|
21
|
+
for (const g of groupsOf(qa)) m.set(groupKey(g), (m.get(groupKey(g)) ?? 0) + (g.severity ?? 0));
|
|
22
|
+
return m;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* compareQa(next, base) -> { better, why, sev: [base, next], problems: [base, next], worse: [{ key, severity: [a, b] }] }
|
|
27
|
+
* why: the first rule that failed ("" when better); worse: the groups that got worse, worst first (at most 3).
|
|
28
|
+
*/
|
|
29
|
+
export function compareQa(next, base) {
|
|
30
|
+
const a = byKey(base), b = byKey(next);
|
|
31
|
+
let rise = 0, drop = 0;
|
|
32
|
+
const worse = [];
|
|
33
|
+
for (const [k, v] of b) { const was = a.get(k) ?? 0; if (v > was + 1e-9) { rise += v - was; worse.push({ key: k, severity: [r2(was), r2(v)] }); } else drop += was - v; }
|
|
34
|
+
for (const [k, v] of a) if (!b.has(k)) drop += v;
|
|
35
|
+
worse.sort((x, y) => y.severity[1] - y.severity[0] - (x.severity[1] - x.severity[0]));
|
|
36
|
+
const sev = [r2(sevOf(base)), r2(sevOf(next))], problems = [groupsOf(base).length, groupsOf(next).length];
|
|
37
|
+
const lost = heldOf(base).filter(n => !heldOf(next).includes(n));
|
|
38
|
+
const why = !(sevOf(next) < sevOf(base) - 1e-9) ? `summed severity ${sev[0]} -> ${sev[1]}`
|
|
39
|
+
: failedOf(next) > failedOf(base) || lost.length ? `goal ${lost.length ? `"${lost[0]}" no longer holds` : "failures rose"}`
|
|
40
|
+
: problems[1] > problems[0] ? `problems ${problems[0]} -> ${problems[1]}`
|
|
41
|
+
: rise > TRADE * drop + 1e-9 ? `${worse[0]?.key} got worse (${worse[0]?.severity.join(" -> ")})` : "";
|
|
42
|
+
return { better: !why, why, sev, problems, worse: worse.slice(0, 3) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** next is no worse than base: no more problems (at as many, the summed severity does not rise), no more goals fail, none that held is lost. */
|
|
46
|
+
export const noWorse = (next, base) => (groupsOf(next).length < groupsOf(base).length || (groupsOf(next).length === groupsOf(base).length && sevOf(next) <= sevOf(base) + 1e-9))
|
|
47
|
+
&& failedOf(next) <= failedOf(base) && heldOf(base).every(n => heldOf(next).includes(n));
|
|
48
|
+
|
|
49
|
+
/** Severity of the group with key `key` in qa (0 when it is gone). */
|
|
50
|
+
export const severityIn = (qa, key) => groupsOf(qa).filter(g => groupKey(g) === key).reduce((s, g) => s + (g.severity ?? 0), 0);
|