@pose_flow/babylon 0.2.0 → 0.3.0

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.
@@ -1,5 +1,6 @@
1
1
  import { validateDatabase } from './database.js';
2
2
  import { compileSchema, LOCOMOTION_SCHEMA } from './schema.js';
3
+ import { matchesContext, validateContext, withinWindows } from './selection.js';
3
4
  export function defaultWeights(schema = LOCOMOTION_SCHEMA) {
4
5
  const layout = compileSchema(schema), weights = new Float32Array(layout.dimensions);
5
6
  for (const { channel, offset, dimensions } of layout.channels)
@@ -9,6 +10,20 @@ export function defaultWeights(schema = LOCOMOTION_SCHEMA) {
9
10
  return weights;
10
11
  }
11
12
  const legacySignature = compileSchema(LOCOMOTION_SCHEMA).signature;
13
+ export function readCandidateTrajectory(database, index) {
14
+ if (!Number.isInteger(index) || index < 0 || index >= database.manifest.frames.length)
15
+ throw new Error('Invalid candidate frame');
16
+ const { dimensions, mean, std } = database.manifest, layout = compileSchema(database.manifest.schema);
17
+ const points = new Map(layout.horizons.map(horizon => [horizon, { horizon }]));
18
+ for (const { channel, offset } of layout.channels)
19
+ if (channel.kind === 'trajectory-position' || channel.kind === 'trajectory-facing') {
20
+ channel.horizons.forEach((horizon, h) => {
21
+ const j = offset + h * 2, value = [database.features[index * dimensions + j] * std[j] + mean[j], database.features[index * dimensions + j + 1] * std[j + 1] + mean[j + 1]];
22
+ points.get(horizon)[channel.kind === 'trajectory-position' ? 'position' : 'facing'] = value;
23
+ });
24
+ }
25
+ return [...points.values()];
26
+ }
12
27
  export class PoseSearch {
13
28
  database;
14
29
  normalized;
@@ -27,6 +42,8 @@ export class PoseSearch {
27
42
  this.weights = weights.slice();
28
43
  }
29
44
  validateQuery(query) {
45
+ if (query.context !== undefined)
46
+ validateContext(query.context);
30
47
  if (query.schemaSignature !== this.database.manifest.schemaSignature && !(query.schemaSignature === undefined && this.database.manifest.schemaSignature === legacySignature))
31
48
  throw new Error('Incompatible query schema');
32
49
  if (query.features.length !== this.dimensions || !query.features.every(Number.isFinite))
@@ -36,6 +53,16 @@ export class PoseSearch {
36
53
  if (query.velocityTolerance !== undefined && (!Number.isFinite(query.velocityTolerance) || query.velocityTolerance < 0 || this.rootOffset === undefined))
37
54
  throw new Error('Velocity tolerance requires a root-velocity channel and a finite nonnegative value');
38
55
  }
56
+ /** Context/gait validity is separate from eligibility to enter at a sample. */
57
+ allowsClip(clip, query) {
58
+ const descriptor = this.database.manifest.clips[clip];
59
+ return !!descriptor && !query.excludedClips?.has(clip) && (!query.gaits || query.gaits.has(descriptor.gait)) && (!query.context || matchesContext(descriptor.context, query.context));
60
+ }
61
+ allowsFrame(index, query) {
62
+ const frame = this.database.manifest.frames[index], clip = this.database.manifest.clips[frame.clip];
63
+ return this.allowsClip(frame.clip, query) && withinWindows(frame.time, clip.entryWindows)
64
+ && (clip.excludedWindows === undefined || !withinWindows(frame.time, clip.excludedWindows));
65
+ }
39
66
  normalize(query) {
40
67
  this.validateQuery(query);
41
68
  const { mean, std } = this.database.manifest;
@@ -66,24 +93,28 @@ export class PoseSearch {
66
93
  search(query) {
67
94
  this.normalize(query);
68
95
  const { frames, clips } = this.database.manifest;
96
+ // Context is clip-level: resolve it once per clip, rather than once per
97
+ // sample in each of the velocity and ranking passes.
98
+ const eligibleClips = clips.map((_, clip) => this.allowsClip(clip, query));
99
+ const candidates = [];
100
+ for (let i = 0; i < frames.length; i++) {
101
+ const frame = frames[i], clip = clips[frame.clip];
102
+ if (eligibleClips[frame.clip] && withinWindows(frame.time, clip.entryWindows)
103
+ && (clip.excludedWindows === undefined || !withinWindows(frame.time, clip.excludedWindows)))
104
+ candidates.push(i);
105
+ }
69
106
  let index = -1, cost = Infinity;
70
107
  let velocityLimit = Infinity;
71
108
  if (query.velocityTolerance !== undefined) {
72
109
  let nearest = Infinity;
73
- for (let i = 0; i < frames.length; i++) {
74
- if (query.excludedClips?.has(frames[i].clip) || (query.gaits && !query.gaits.has(clips[frames[i].clip].gait)))
75
- continue;
110
+ for (const i of candidates) {
76
111
  nearest = Math.min(nearest, this.rootVelocityError(i, query));
77
112
  }
78
113
  // Sparse datasets retain a deterministic nearest-velocity fallback. Never
79
114
  // expand the pool to arbitrary stationary poses just because coverage is missing.
80
115
  velocityLimit = Math.max(query.velocityTolerance, nearest + 1e-6);
81
116
  }
82
- for (let i = 0; i < frames.length; i++) {
83
- if (query.excludedClips?.has(frames[i].clip))
84
- continue;
85
- if (query.gaits && !query.gaits.has(clips[frames[i].clip].gait))
86
- continue;
117
+ for (const i of candidates) {
87
118
  if (query.velocityTolerance !== undefined && this.rootVelocityError(i, query) > velocityLimit)
88
119
  continue;
89
120
  const score = this.cost(i);
@@ -94,4 +125,57 @@ export class PoseSearch {
94
125
  }
95
126
  return index < 0 ? null : { ...frames[index], index, cost };
96
127
  }
128
+ /** Opt-in diagnostic pass. Bounded output, exact ranking, no change to filters/weights. */
129
+ inspect(query, limit = 5) {
130
+ if (!Number.isInteger(limit) || limit < 1 || limit > 50)
131
+ throw new Error('Invalid diagnostic limit');
132
+ const best = this.search(query), { frames, clips } = this.database.manifest;
133
+ const rejected = { 'excluded-clip': 0, gait: 0, context: 0, 'entry-window': 0, 'excluded-window': 0, velocity: 0 };
134
+ const examples = [], candidates = [];
135
+ const layout = compileSchema(this.database.manifest.schema);
136
+ let nearest = Infinity, eligible = 0;
137
+ if (query.velocityTolerance !== undefined)
138
+ for (let i = 0; i < frames.length; i++)
139
+ if (this.allowsFrame(i, query))
140
+ nearest = Math.min(nearest, this.rootVelocityError(i, query));
141
+ const velocityLimit = query.velocityTolerance === undefined ? null : Math.max(query.velocityTolerance, nearest + 1e-6);
142
+ for (let index = 0; index < frames.length; index++) {
143
+ const frame = frames[index], clip = clips[frame.clip];
144
+ let reason;
145
+ if (query.excludedClips?.has(frame.clip))
146
+ reason = 'excluded-clip';
147
+ else if (query.gaits && !query.gaits.has(clip.gait))
148
+ reason = 'gait';
149
+ else if (!matchesContext(clip.context, query.context))
150
+ reason = 'context';
151
+ else if (!withinWindows(frame.time, clip.entryWindows))
152
+ reason = 'entry-window';
153
+ else if (clip.excludedWindows !== undefined && withinWindows(frame.time, clip.excludedWindows))
154
+ reason = 'excluded-window';
155
+ else if (velocityLimit !== null && this.rootVelocityError(index, query) > velocityLimit)
156
+ reason = 'velocity';
157
+ if (reason) {
158
+ rejected[reason]++;
159
+ if (examples.length < limit)
160
+ examples.push({ index, ...frame, reason });
161
+ continue;
162
+ }
163
+ eligible++;
164
+ const cost = this.cost(index);
165
+ if (candidates.length >= limit && cost >= candidates[candidates.length - 1].cost)
166
+ continue;
167
+ const channels = layout.channels.map(({ channel, offset, dimensions }) => {
168
+ let cost = 0;
169
+ for (let j = offset; j < offset + dimensions; j++)
170
+ cost += this.weights[j] * (this.normalized[j] - this.database.features[index * this.dimensions + j]) ** 2;
171
+ return { id: channel.id, cost };
172
+ });
173
+ const candidate = { index, ...frame, cost, channels, velocityError: this.rootOffset === undefined ? null : this.rootVelocityError(index, query) };
174
+ const at = candidates.findIndex(c => c.cost > cost);
175
+ candidates.splice(at < 0 ? candidates.length : at, 0, candidate);
176
+ if (candidates.length > limit)
177
+ candidates.pop();
178
+ }
179
+ return { best, candidates, rejected, examples, eligible, total: frames.length, velocityLimit: Number.isFinite(velocityLimit) ? velocityLimit : null };
180
+ }
97
181
  }
@@ -0,0 +1,41 @@
1
+ export function validateContext(value) {
2
+ if (!value || typeof value !== 'object' || Array.isArray(value)
3
+ || !Object.entries(value).every(([k, v]) => k.trim().length > 0 && typeof v === 'string' && v.trim().length > 0))
4
+ throw new Error('Invalid selection context');
5
+ }
6
+ export function matchesContext(metadata, requested = {}) {
7
+ return Object.entries(requested).every(([key, value]) => metadata !== undefined && Object.hasOwn(metadata, key) && metadata[key] === value);
8
+ }
9
+ export function withinWindows(time, windows) {
10
+ return windows === undefined || windows.some(([start, end]) => time >= start && time <= end);
11
+ }
12
+ export function validateSelection(value, duration = Infinity) {
13
+ if (value.context !== undefined)
14
+ validateContext(value.context);
15
+ for (const key of ['entryWindows', 'exitWindows', 'excludedWindows']) {
16
+ const windows = value[key];
17
+ if (windows !== undefined && (!Array.isArray(windows) || !windows.every(w => Array.isArray(w) && w.length === 2 && w.every(Number.isFinite)
18
+ && w[0] >= 0 && w[1] >= w[0] && w[1] <= duration)))
19
+ throw new Error('Invalid selection windows: ' + key);
20
+ }
21
+ }
22
+ /** Route before searching; costs from independent normalizations are never compared.
23
+ * Caller must use the selected bank's schema, compatible rig/player and matcher.
24
+ */
25
+ export function selectContextBank(banks, context) {
26
+ validateContext(context);
27
+ const ids = new Set();
28
+ let selected = null;
29
+ for (const bank of banks) {
30
+ if (!bank.id.trim() || ids.has(bank.id))
31
+ throw new Error('Invalid/duplicate context bank id');
32
+ ids.add(bank.id);
33
+ validateContext(bank.context);
34
+ if (!matchesContext(bank.context, context))
35
+ continue;
36
+ if (selected)
37
+ throw new Error('Ambiguous context bank selection');
38
+ selected = bank;
39
+ }
40
+ return selected;
41
+ }
@@ -6,3 +6,6 @@ export * from './core/search.js';
6
6
  export * from './core/controller.js';
7
7
  export * from './core/root-motion.js';
8
8
  export * from './core/profile.js';
9
+ export * from './core/selection.js';
10
+ export * from './core/inertialization.js';
11
+ export * from './core/scenario.js';
@@ -31,7 +31,9 @@ try {
31
31
  }
32
32
  if (motion && spec.range)
33
33
  motion = cropRootMotion(motion, start, end);
34
- const descriptor = { id, file, animation, duration, looping, gait, ...(spec.range ? { sourceStart: start } : {}), ...(spec.rootOrientation ? { rootOrientation: spec.rootOrientation } : {}),
34
+ const descriptor = { id, file, animation, duration, looping, gait,
35
+ ...Object.fromEntries(['context', 'entryWindows', 'exitWindows', 'excludedWindows'].filter(key => key in spec).map(key => [key, spec[key]])),
36
+ ...(spec.range ? { sourceStart: start } : {}), ...(spec.rootOrientation ? { rootOrientation: spec.rootOrientation } : {}),
35
37
  ...(motion ? { motionSource: motion.source } : { nominalVelocity, motionSource: 'nominal' }) };
36
38
  const player = new BabylonMotionPlayer(scene, [descriptor], new Map([[id, source]]), targets, 0, profile.stripRoot ?? '');
37
39
  const clipTime = (t) => looping ? ((t % duration) + duration) % duration : Math.max(0, Math.min(t, duration));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pose_flow/babylon",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "files": [
@@ -30,10 +30,10 @@
30
30
  "benchmark": "tsx tools/benchmark.ts"
31
31
  },
32
32
  "peerDependencies": {
33
- "@babylonjs/core": "^7.54.3"
33
+ "@babylonjs/core": "^9.32.1"
34
34
  },
35
35
  "devDependencies": {
36
- "@babylonjs/core": "7.54.3",
36
+ "@babylonjs/core": "9.32.1",
37
37
  "@types/node": "^22.0.0",
38
38
  "tsx": "^4.19.0",
39
39
  "typescript": "^5.7.0",
@@ -43,6 +43,6 @@
43
43
  "pose-flow": "./dist/offline/tools/cli.js"
44
44
  },
45
45
  "dependencies": {
46
- "@babylonjs/loaders": "7.54.3"
46
+ "@babylonjs/loaders": "9.32.1"
47
47
  }
48
48
  }