mujoco-react 9.3.0 → 9.5.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.
@@ -56,6 +56,27 @@ export interface ResolvedMountedCameraFrameSource {
56
56
  source: MountedCameraFrameCaptureSource;
57
57
  }
58
58
 
59
+ export const MountedCameraFrameSourceSuggestionMatch = {
60
+ Direct: 'direct',
61
+ Alias: 'alias',
62
+ Normalized: 'normalized',
63
+ Prefix: 'prefix',
64
+ Suffix: 'suffix',
65
+ Contains: 'contains',
66
+ } as const;
67
+
68
+ export type MountedCameraFrameSourceSuggestionMatch =
69
+ (typeof MountedCameraFrameSourceSuggestionMatch)[keyof typeof MountedCameraFrameSourceSuggestionMatch];
70
+
71
+ export interface MountedCameraFrameSourceSuggestion {
72
+ key: string;
73
+ selector: CameraFrameMountSelector;
74
+ source: MountedCameraFrameCaptureSource;
75
+ resourceName: string;
76
+ resourceKind: MountedCameraFrameCaptureSource['kind'];
77
+ match: MountedCameraFrameSourceSuggestionMatch;
78
+ }
79
+
59
80
  export type MountedCameraFrameSequenceDefaults = Omit<
60
81
  CameraFrameSequenceCamera,
61
82
  'key' | 'cameraName' | 'siteName' | 'bodyName' | 'source'
@@ -123,6 +144,57 @@ export interface MountedCameraFrameSequenceRecordResult
123
144
  readiness: MountedCameraFrameSequenceReadiness;
124
145
  }
125
146
 
147
+ export const MountedCameraFrameSequenceManifestStatus = {
148
+ Complete: 'complete',
149
+ Partial: 'partial',
150
+ Missing: 'missing',
151
+ } as const;
152
+
153
+ export type MountedCameraFrameSequenceManifestStatus =
154
+ (typeof MountedCameraFrameSequenceManifestStatus)[keyof typeof MountedCameraFrameSequenceManifestStatus];
155
+
156
+ export interface MountedCameraFrameSequenceStreamSummary {
157
+ key: string;
158
+ ready: boolean;
159
+ complete: boolean;
160
+ status: MountedCameraFrameSequenceManifestStatus;
161
+ source?: CameraFrameCaptureSource;
162
+ selector?: CameraFrameMountSelector;
163
+ target?: string;
164
+ width?: number;
165
+ height?: number;
166
+ expectedFrameCount: number;
167
+ recordedFrameCount: number;
168
+ missingFrameCount: number;
169
+ firstFrameIndex: number | null;
170
+ lastFrameIndex: number | null;
171
+ firstTimestamp: number | null;
172
+ lastTimestamp: number | null;
173
+ message: string;
174
+ }
175
+
176
+ export interface CreateMountedCameraFrameSequenceManifestOptions {
177
+ expectedFrameCount?: number;
178
+ cameraKeys?: readonly string[];
179
+ }
180
+
181
+ export interface MountedCameraFrameSequenceManifest {
182
+ schema: 'mujoco-react/mounted-camera-frame-sequence-manifest@1';
183
+ ready: boolean;
184
+ complete: boolean;
185
+ status: MountedCameraFrameSequenceManifestStatus;
186
+ cameraKeys: string[];
187
+ resolvedKeys: string[];
188
+ missingKeys: string[];
189
+ expectedFrameCount: number;
190
+ recordedFrameCount: number;
191
+ missingFrameCount: number;
192
+ streamSummaries: Record<string, MountedCameraFrameSequenceStreamSummary>;
193
+ streams: MountedCameraFrameSequenceStreamSummary[];
194
+ readiness: MountedCameraFrameSequenceReadiness;
195
+ message: string;
196
+ }
197
+
126
198
  export type MountedCameraFrameSequenceRecorderTarget = Pick<
127
199
  MujocoSimAPI,
128
200
  'getCameras' | 'getSites' | 'getBodies' | 'recordCameraSequence'
@@ -147,6 +219,16 @@ function createNameSet(
147
219
  );
148
220
  }
149
221
 
222
+ function createResourceNames(
223
+ resources:
224
+ | readonly (string | NamedCameraFrameResource | null | undefined)[]
225
+ | undefined
226
+ ) {
227
+ return (resources ?? [])
228
+ .map((resource) => getResourceName(resource))
229
+ .filter((name): name is string => Boolean(name));
230
+ }
231
+
150
232
  function normalizeAliasCandidates(
151
233
  value: CameraFrameMountSelector | readonly CameraFrameMountSelector[] | undefined
152
234
  ) {
@@ -160,6 +242,35 @@ function countMountedSelectors(selector: CameraFrameMountSelector) {
160
242
  Number(Boolean(selector.bodyName));
161
243
  }
162
244
 
245
+ function normalizeCameraSourceName(value: string) {
246
+ return value
247
+ .trim()
248
+ .toLowerCase()
249
+ .replace(/[^a-z0-9]+/g, '_')
250
+ .replace(/^_+|_+$/g, '');
251
+ }
252
+
253
+ function createCameraSourceKeyVariants(key: string) {
254
+ const candidates = [
255
+ key,
256
+ key.startsWith('observation.images.')
257
+ ? key.slice('observation.images.'.length)
258
+ : '',
259
+ key.includes('.') ? key.split('.').at(-1) ?? '' : '',
260
+ key.includes('/') ? key.split('/').at(-1) ?? '' : '',
261
+ ];
262
+ return candidates
263
+ .map((candidate) => candidate.trim())
264
+ .filter((candidate, index, items) => candidate && items.indexOf(candidate) === index);
265
+ }
266
+
267
+ function getSelectorKey(selector: CameraFrameMountSelector) {
268
+ if (selector.cameraName) return `camera:${selector.cameraName}`;
269
+ if (selector.siteName) return `site:${selector.siteName}`;
270
+ if (selector.bodyName) return `body:${selector.bodyName}`;
271
+ return null;
272
+ }
273
+
163
274
  export function getMountedCameraFrameCaptureSource(
164
275
  selector: CameraFrameMountSelector
165
276
  ): MountedCameraFrameCaptureSource | null {
@@ -197,6 +308,66 @@ export function getCameraFrameCaptureSourceTarget(
197
308
  return 'fallback camera';
198
309
  }
199
310
 
311
+ function createMountedCameraFrameSourceSuggestion(
312
+ key: string,
313
+ selector: CameraFrameMountSelector,
314
+ resourceName: string,
315
+ match: MountedCameraFrameSourceSuggestionMatch
316
+ ): MountedCameraFrameSourceSuggestion | null {
317
+ const source = getMountedCameraFrameCaptureSource(selector);
318
+ if (!source) return null;
319
+ return {
320
+ key,
321
+ selector,
322
+ source,
323
+ resourceName,
324
+ resourceKind: source.kind,
325
+ match,
326
+ };
327
+ }
328
+
329
+ function addMountedCameraFrameSourceSuggestion(
330
+ suggestions: MountedCameraFrameSourceSuggestion[],
331
+ seen: Set<string>,
332
+ suggestion: MountedCameraFrameSourceSuggestion | null
333
+ ) {
334
+ if (!suggestion) return;
335
+ const selectorKey = getSelectorKey(suggestion.selector);
336
+ if (!selectorKey || seen.has(selectorKey)) return;
337
+ seen.add(selectorKey);
338
+ suggestions.push(suggestion);
339
+ }
340
+
341
+ function getCameraFrameResourceMatch(
342
+ key: string,
343
+ resourceName: string
344
+ ): MountedCameraFrameSourceSuggestionMatch | null {
345
+ if (resourceName === key) return MountedCameraFrameSourceSuggestionMatch.Direct;
346
+
347
+ const normalizedResource = normalizeCameraSourceName(resourceName);
348
+ if (!normalizedResource) return null;
349
+
350
+ for (const variant of createCameraSourceKeyVariants(key)) {
351
+ if (resourceName === variant) return MountedCameraFrameSourceSuggestionMatch.Direct;
352
+
353
+ const normalizedKey = normalizeCameraSourceName(variant);
354
+ if (!normalizedKey) continue;
355
+ if (normalizedResource === normalizedKey) {
356
+ return MountedCameraFrameSourceSuggestionMatch.Normalized;
357
+ }
358
+ if (normalizedResource.startsWith(`${normalizedKey}_`)) {
359
+ return MountedCameraFrameSourceSuggestionMatch.Prefix;
360
+ }
361
+ if (normalizedResource.endsWith(`_${normalizedKey}`)) {
362
+ return MountedCameraFrameSourceSuggestionMatch.Suffix;
363
+ }
364
+ if (normalizedResource.includes(`_${normalizedKey}_`)) {
365
+ return MountedCameraFrameSourceSuggestionMatch.Contains;
366
+ }
367
+ }
368
+ return null;
369
+ }
370
+
200
371
  function isSelectorMounted(
201
372
  selector: CameraFrameMountSelector,
202
373
  cameraNames: Set<string>,
@@ -211,6 +382,82 @@ function isSelectorMounted(
211
382
  );
212
383
  }
213
384
 
385
+ export function createMountedCameraFrameSourceSuggestions(
386
+ key: string,
387
+ options: ResolveMountedCameraFrameSourceOptions
388
+ ): MountedCameraFrameSourceSuggestion[] {
389
+ const cameraNames = createNameSet(options.cameras);
390
+ const siteNames = createNameSet(options.sites);
391
+ const bodyNames = createNameSet(options.bodies);
392
+ const suggestions: MountedCameraFrameSourceSuggestion[] = [];
393
+ const seen = new Set<string>();
394
+
395
+ for (const selector of normalizeAliasCandidates(options.aliases?.[key])) {
396
+ if (!isSelectorMounted(selector, cameraNames, siteNames, bodyNames)) {
397
+ continue;
398
+ }
399
+ const source = getMountedCameraFrameCaptureSource(selector);
400
+ if (!source) continue;
401
+ addMountedCameraFrameSourceSuggestion(
402
+ suggestions,
403
+ seen,
404
+ createMountedCameraFrameSourceSuggestion(
405
+ key,
406
+ selector,
407
+ getCameraFrameCaptureSourceTarget(source),
408
+ MountedCameraFrameSourceSuggestionMatch.Alias
409
+ )
410
+ );
411
+ }
412
+
413
+ for (const cameraName of createResourceNames(options.cameras)) {
414
+ const match = getCameraFrameResourceMatch(key, cameraName);
415
+ if (!match) continue;
416
+ addMountedCameraFrameSourceSuggestion(
417
+ suggestions,
418
+ seen,
419
+ createMountedCameraFrameSourceSuggestion(
420
+ key,
421
+ { cameraName },
422
+ cameraName,
423
+ match
424
+ )
425
+ );
426
+ }
427
+
428
+ for (const siteName of createResourceNames(options.sites)) {
429
+ const match = getCameraFrameResourceMatch(key, siteName);
430
+ if (!match) continue;
431
+ addMountedCameraFrameSourceSuggestion(
432
+ suggestions,
433
+ seen,
434
+ createMountedCameraFrameSourceSuggestion(
435
+ key,
436
+ { siteName },
437
+ siteName,
438
+ match
439
+ )
440
+ );
441
+ }
442
+
443
+ for (const bodyName of createResourceNames(options.bodies)) {
444
+ const match = getCameraFrameResourceMatch(key, bodyName);
445
+ if (!match) continue;
446
+ addMountedCameraFrameSourceSuggestion(
447
+ suggestions,
448
+ seen,
449
+ createMountedCameraFrameSourceSuggestion(
450
+ key,
451
+ { bodyName },
452
+ bodyName,
453
+ match
454
+ )
455
+ );
456
+ }
457
+
458
+ return suggestions;
459
+ }
460
+
214
461
  export function resolveMountedCameraFrameSource(
215
462
  key: string,
216
463
  options: ResolveMountedCameraFrameSourceOptions
@@ -224,9 +471,8 @@ export function resolveMountedCameraFrameSource(
224
471
  { bodyName: key },
225
472
  ];
226
473
  const aliasCandidates = normalizeAliasCandidates(options.aliases?.[key]);
227
- const candidates = [...directCandidates, ...aliasCandidates];
228
474
 
229
- for (const selector of candidates) {
475
+ for (const selector of aliasCandidates) {
230
476
  if (!isSelectorMounted(selector, cameraNames, siteNames, bodyNames)) {
231
477
  continue;
232
478
  }
@@ -235,6 +481,15 @@ export function resolveMountedCameraFrameSource(
235
481
  return { key, selector, source };
236
482
  }
237
483
 
484
+ const [suggestion] = createMountedCameraFrameSourceSuggestions(key, options);
485
+ if (suggestion) {
486
+ return {
487
+ key,
488
+ selector: suggestion.selector,
489
+ source: suggestion.source,
490
+ };
491
+ }
492
+
238
493
  if (options.allowAliasFallback) {
239
494
  for (const selector of aliasCandidates) {
240
495
  const source = getMountedCameraFrameCaptureSource(selector);
@@ -243,6 +498,15 @@ export function resolveMountedCameraFrameSource(
243
498
  }
244
499
  }
245
500
 
501
+ for (const selector of directCandidates) {
502
+ if (!isSelectorMounted(selector, cameraNames, siteNames, bodyNames)) {
503
+ continue;
504
+ }
505
+ const source = getMountedCameraFrameCaptureSource(selector);
506
+ if (!source) continue;
507
+ return { key, selector, source };
508
+ }
509
+
246
510
  return null;
247
511
  }
248
512
 
@@ -335,6 +599,122 @@ export function createMountedCameraFrameSequenceReadiness(
335
599
  };
336
600
  }
337
601
 
602
+ function normalizeFrameCount(frameCount: number | undefined) {
603
+ return Number.isFinite(frameCount) && frameCount !== undefined
604
+ ? Math.max(0, Math.floor(frameCount))
605
+ : 0;
606
+ }
607
+
608
+ export function createMountedCameraFrameSequenceManifest(
609
+ result: MountedCameraFrameSequenceRecordResult,
610
+ options: CreateMountedCameraFrameSequenceManifestOptions = {}
611
+ ): MountedCameraFrameSequenceManifest {
612
+ const cameraKeys = [
613
+ ...(options.cameraKeys ??
614
+ result.readiness.cameraKeys ??
615
+ result.plan.cameraKeys ??
616
+ result.cameraKeys),
617
+ ];
618
+ const expectedFrameCount = normalizeFrameCount(
619
+ options.expectedFrameCount ?? result.frameCount
620
+ );
621
+ const recordedFrameCount = normalizeFrameCount(result.frameCount);
622
+ const streamSummaries: Record<string, MountedCameraFrameSequenceStreamSummary> =
623
+ {};
624
+ const streams: MountedCameraFrameSequenceStreamSummary[] = [];
625
+ let missingFrameCount = 0;
626
+ let completeStreamCount = 0;
627
+ let resolvedOrRecordedStreamCount = 0;
628
+
629
+ for (const key of cameraKeys) {
630
+ const summary = result.cameraSummaries[key];
631
+ const readiness = result.readiness.cameras[key];
632
+ const source = summary?.source ?? readiness?.source;
633
+ const ready = readiness?.ready ?? Boolean(summary);
634
+ const recorded = normalizeFrameCount(summary?.frameCount);
635
+ const missing = Math.max(expectedFrameCount - recorded, 0);
636
+ const complete = ready && missing === 0;
637
+ const status = complete
638
+ ? MountedCameraFrameSequenceManifestStatus.Complete
639
+ : ready || recorded > 0
640
+ ? MountedCameraFrameSequenceManifestStatus.Partial
641
+ : MountedCameraFrameSequenceManifestStatus.Missing;
642
+ const target = source
643
+ ? getCameraFrameCaptureSourceTarget(source)
644
+ : readiness?.message
645
+ ? undefined
646
+ : 'missing MuJoCo camera';
647
+ const message = complete
648
+ ? `Camera stream "${key}" recorded ${recorded} of ${expectedFrameCount} frame${
649
+ expectedFrameCount === 1 ? '' : 's'
650
+ }.`
651
+ : ready || recorded > 0
652
+ ? `Camera stream "${key}" recorded ${recorded} of ${expectedFrameCount} frame${
653
+ expectedFrameCount === 1 ? '' : 's'
654
+ }.`
655
+ : readiness?.message ??
656
+ `Camera stream "${key}" did not record any frames.`;
657
+ const stream = {
658
+ key,
659
+ ready,
660
+ complete,
661
+ status,
662
+ source,
663
+ selector: readiness?.selector,
664
+ target,
665
+ width: summary?.width,
666
+ height: summary?.height,
667
+ expectedFrameCount,
668
+ recordedFrameCount: recorded,
669
+ missingFrameCount: missing,
670
+ firstFrameIndex: summary?.firstFrameIndex ?? null,
671
+ lastFrameIndex: summary?.lastFrameIndex ?? null,
672
+ firstTimestamp: summary?.firstTimestamp ?? null,
673
+ lastTimestamp: summary?.lastTimestamp ?? null,
674
+ message,
675
+ };
676
+
677
+ streamSummaries[key] = stream;
678
+ streams.push(stream);
679
+ missingFrameCount += missing;
680
+ if (complete) completeStreamCount += 1;
681
+ if (ready || recorded > 0) resolvedOrRecordedStreamCount += 1;
682
+ }
683
+
684
+ const complete =
685
+ result.readiness.ready &&
686
+ streams.length === completeStreamCount &&
687
+ missingFrameCount === 0;
688
+ const status = complete
689
+ ? MountedCameraFrameSequenceManifestStatus.Complete
690
+ : resolvedOrRecordedStreamCount > 0
691
+ ? MountedCameraFrameSequenceManifestStatus.Partial
692
+ : MountedCameraFrameSequenceManifestStatus.Missing;
693
+
694
+ return {
695
+ schema: 'mujoco-react/mounted-camera-frame-sequence-manifest@1',
696
+ ready: result.readiness.ready,
697
+ complete,
698
+ status,
699
+ cameraKeys,
700
+ resolvedKeys: [...result.readiness.resolvedKeys],
701
+ missingKeys: [...result.readiness.missingKeys],
702
+ expectedFrameCount,
703
+ recordedFrameCount,
704
+ missingFrameCount,
705
+ streamSummaries,
706
+ streams,
707
+ readiness: result.readiness,
708
+ message: complete
709
+ ? `All ${cameraKeys.length} camera stream${
710
+ cameraKeys.length === 1 ? '' : 's'
711
+ } recorded ${expectedFrameCount} frame${
712
+ expectedFrameCount === 1 ? '' : 's'
713
+ }.`
714
+ : `Mounted camera sequence coverage is ${status}.`,
715
+ };
716
+ }
717
+
338
718
  export function createMountedCameraFrameSequencePlanFromApi(
339
719
  api: MountedCameraFrameSequenceRecorderTarget,
340
720
  cameraKeys: readonly string[],