diff --git a/CHANGELOG.md b/CHANGELOG.md index e8514ae..f412705 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,36 @@ Current schema contract version: **1**. ### Added +- **The reference pipeline: three source categories, one downstream half** + — `workspace_reference_pipeline` + ([`tests/motion/test_reference_pipeline.py`](tests/motion/test_reference_pipeline.py)). + This is the motion migration's cross-repository test (MIG-5) and boundary + consolidation's BND-1. It bakes three clips onto `Seed-san.vrm`: + - a `.vrma`, through this repository's importer; + - a BVH export, as `usd-motion-plugins`' `motion_convert` wrote it; + - a recorded live session, as its `motion_record` wrote it. + + All three go through one `motion_retarget` argument list that differs only + in `--animation`. The test asserts that the downstream half is the same code, + in three ways: + - `--load-report`'s plugins and modules differ across sources by the first + arrow's plugin alone (`usdVrmaFileFormat` for a `.vrma`); + - the authored layers have one shape and one joint order; + - every bake passes the same checks with one tolerance. The bound bones + reproduce the clip's rotation away from rest and the hips its travel, + unbindable bones are named, nothing outside the humanoid moves, and scales + are authored. + + Four mutations each fail at the check meant to catch them: a per-source + flag on the root motion, a swapped arm mapping, a first arrow that is not + allowed, and a renamed animation prim. + + `workspace_reference_pipeline_local` runs the same checks over data that + cannot be committed. It reads `USDVRM_LOCAL_AVATAR` and + `USDVRM_LOCAL_VRMA_DIR`, can keep each bake in `USDVRM_LOCAL_OUTPUT_DIR`, + and reports Skipped without them. Measured on 2026-09-24 over + `AliciaSolid.vrm` (VRM 0.x) and the seven VRM Animation MotionPack clips, + with the export and the session beside them: all nine pass. - **A plain-CMake lane with no `ost` in it** — [`.github/workflows/plain-cmake.yml`](.github/workflows/plain-cmake.yml). On Linux it builds and tests the workspace from an OpenUSD 26.08 install diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d97316..a894f69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -248,6 +248,46 @@ if(USDVRM_BUILD_TESTS AND USDVRM_TEST_PYTHON endif() endif() +# The reference pipeline: a `.vrma`, a converted BVH export and a recorded +# session through one downstream invocation onto one VRM, asserted to be the +# same code rather than three plausible outputs (MIG-5's cross-repository test, +# boundary consolidation BND-1). Two of the three first arrows are +# usd-motion-plugins' tools, read as their committed output; the third is +# usdVrmaFileFormat, so this needs both importers and the tool in one build. +# +# `_local` is the same test over an avatar and `.vrma` files that cannot be +# committed, named by USDVRM_LOCAL_AVATAR and USDVRM_LOCAL_VRMA_DIR in the +# environment of the run; without them it reports Skipped. +if(USDVRM_BUILD_TESTS AND USDVRM_TEST_PYTHON + AND TARGET motion_retarget + AND TARGET UsdVrmFileFormat AND TARGET UsdVrmaFileFormat + AND EXISTS "${_real_avatar}") + set(_reference_plugins "${CMAKE_CURRENT_SOURCE_DIR}/plugins") + set(_reference_resources "${CMAKE_CURRENT_BINARY_DIR}/plugins") + set(_reference_common + "${CMAKE_CURRENT_SOURCE_DIR}/tests/motion/test_reference_pipeline.py" + --retarget "$" + --bvh-clip "${CMAKE_CURRENT_SOURCE_DIR}/tests/motion/fixtures/mocopi-mobile-arm-raise-turn.usda" + --trace-clip "${CMAKE_CURRENT_SOURCE_DIR}/tools/motionRetarget/tests/fixtures/recorded_session_clip.usda") + add_test(NAME workspace_reference_pipeline + COMMAND "${USDVRM_TEST_PYTHON}" ${_reference_common} + --avatar "${_real_avatar}" + --vrma "${_reference_plugins}/usdVrmaFileFormat/tests/fixtures/canonical_walk.vrma") + add_test(NAME workspace_reference_pipeline_local + COMMAND "${USDVRM_TEST_PYTHON}" ${_reference_common} --local) + set_tests_properties(workspace_reference_pipeline workspace_reference_pipeline_local + PROPERTIES + ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:${_e2e_usd_root}/bin;PATH=path_list_prepend:${_e2e_usd_root}/lib;PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:${_reference_plugins}/vrmSchema/lib;PATH=path_list_prepend:$;PYTHONPATH=path_list_prepend:${_e2e_usd_root}/lib/python;PYTHONPATH=path_list_prepend:${_e2e_usd_root}/lib/site-packages;PXR_PLUGINPATH_NAME=path_list_prepend:${_reference_resources}/usdVrmFileFormat/tests/resources/usdVrmFileFormat;PXR_PLUGINPATH_NAME=path_list_prepend:${_reference_resources}/usdVrmaFileFormat/tests/resources/usdVrmaFileFormat;PXR_PLUGINPATH_NAME=path_list_prepend:${_reference_plugins}/vrmSchema/plugin/resources/vrmSchema" + LABELS "motion.integration;motion.real-corpus") + set_tests_properties(workspace_reference_pipeline_local PROPERTIES + SKIP_RETURN_CODE 77) +else() + message(STATUS + "usd-vrm-plugins: a tool or a file-format bundle is not in this build " + "tree; skipping workspace_reference_pipeline") +endif() + # The OpenExec plan's P0-6: a `motion_retarget` bake against `execMotion` + # `execVrm` evaluating the same avatar and clip. Here for the same reason as the # two tests above -- it is a composition of both exec bundles and the retarget diff --git a/docs/roadmap/boundary-consolidation.md b/docs/roadmap/boundary-consolidation.md index f4c859b..2169f51 100644 --- a/docs/roadmap/boundary-consolidation.md +++ b/docs/roadmap/boundary-consolidation.md @@ -108,7 +108,7 @@ Generator: request/context -> MotionClip or a pose stream Done when a fifth producer can be added by naming which crossing it takes. -## 3. BND-1 — one reference pipeline, proved once for every category ⬜ +## 3. BND-1 — one reference pipeline, proved once for every category ✅ Every category reaches `UsdSkelAnimation` today, and each is proved by its own tests along its own path. There is no single test that says *the same thing* @@ -118,14 +118,27 @@ happens to all of them. source → canonical MotionClip → vrmRetarget → UsdSkelAnimation → validation ``` -- ⬜ One integration test, three sources: a `.vrma` clip, a BVH export, and a +- ✅ One integration test, three sources: a `.vrma` clip, a BVH export, and a recorded live trace — through the identical downstream call sequence, with the source-specific part confined to the first arrow. -- ⬜ The assertion is that the downstream half is **the same code**, not that +- ✅ The assertion is that the downstream half is **the same code**, not that three outputs are individually plausible. A source that needs a downstream branch has found a boundary defect, which is the point of running them together. +**Done on 2026-09-24, as `workspace_reference_pipeline`** +(`tests/motion/test_reference_pipeline.py`), which is also the motion +migration's MIG-5 cross-repository test. `vrmRetarget` in the diagram above is +now the consumed `motionRetarget` driven by `motion_retarget`. The BVH export +and the recorded trace arrive as the clips `usd-motion-plugins`' converter and +recorder wrote, because those first arrows are that repository's now. "The same +code" is measured three ways: + +- the plugins and modules each run loads differ by the first arrow's importer + alone; +- the authored layers share one shape and joint order; +- every bake passes one set of motion checks. + This is the test a fourth source is added *to*. NPZ/AMASS ships when it can join it without changing it. diff --git a/docs/roadmap/current.md b/docs/roadmap/current.md index 90aaa57..4ab18d6 100644 --- a/docs/roadmap/current.md +++ b/docs/roadmap/current.md @@ -209,8 +209,9 @@ Each step's record is in [the track](motion-foundation-split.md). - ⬜ The aggregate product installs and opens a `.vrm` and a `.vrma` from release artifacts, with the shared core resolved as a dependency. That is the next release's dry run. -- ⬜ The cross-repository test — VRMA → `MotionClip` → a target VRM — runs by - default somewhere, and here until an integration repository exists. +- ✅ The cross-repository test — VRMA → `MotionClip` → a target VRM — runs by + default: `workspace_reference_pipeline`, here until an integration + repository exists (2026-09-24). A new generic motion feature is proposed in `usd-motion-plugins`, and a new device or protocol input in `motion-connectors` diff --git a/docs/roadmap/motion-foundation-split.md b/docs/roadmap/motion-foundation-split.md index 6aed5fa..d314529 100644 --- a/docs/roadmap/motion-foundation-split.md +++ b/docs/roadmap/motion-foundation-split.md @@ -1,6 +1,6 @@ # Motion migration — generic motion to `usd-motion-plugins`, input to `motion-connectors` -**Status:** ✅ MIG-0..MIG-4 on this side — every identity [WORKSPACE.md §9.1](../architecture/WORKSPACE.md#91-destination-of-every-identity) sends elsewhere arrived there with its history (2026-09-19..21) and is consumed or deleted here (2026-09-21..24), `execMotion` last; what MIG-4 still owes is `motion-connectors`' (recorded evidence, ARDY); 🚧 MIG-5 — the mechanical check and the reduced contract are done, the release-artifact proof and the cross-repository test are not · **Target:** after the OpenExec foundation · +**Status:** ✅ MIG-0..MIG-4 on this side — every identity [WORKSPACE.md §9.1](../architecture/WORKSPACE.md#91-destination-of-every-identity) sends elsewhere arrived there with its history (2026-09-19..21) and is consumed or deleted here (2026-09-21..24), `execMotion` last; what MIG-4 still owes is `motion-connectors`' (recorded evidence, ARDY); 🚧 MIG-5 — the mechanical check, the reduced contract and the cross-repository test are done; the release-artifact proof is the next release's dry run · **Target:** after the OpenExec foundation · **Structure:** [architecture/WORKSPACE.md §9](../architecture/WORKSPACE.md#9-destinations-under-the-motion-architecture) · **Policy:** the `usd-motion-plugins` design policy §37, and [design/INTEGRATION_SCOPE_POLICY.md](../design/INTEGRATION_SCOPE_POLICY.md) §13 · @@ -572,10 +572,22 @@ repository's, and needs nothing from this one. rule kept. - ⬜ The aggregate product still installs and opens a `.vrm` and a `.vrma`, with the shared core resolved as a dependency, from release artifacts. -- ⬜ The cross-repository test — VRMA → `MotionClip` → a target VRM — runs +- ✅ The cross-repository test — VRMA → `MotionClip` → a target VRM — runs somewhere by default. The motion-plugins policy puts such tests in a runtime or integration repository (its §30.6); until one exists it stays here, because this repository is its natural integrator. + - It is `workspace_reference_pipeline` (2026-09-24), which is also boundary + consolidation's BND-1. It bakes three categories onto `Seed-san.vrm` + through one `motion_retarget` argument list: a `.vrma` through this + repository's importer, and a BVH export and a recorded session as + `motion_convert` and `motion_record` wrote them. + - It asserts that the half after the clip is one code path. The loaded + plugins and modules differ by the first arrow's importer alone, the + output layers share one shape, and each bake passes the same motion + checks. + - `workspace_reference_pipeline_local` repeats it over data that cannot be + committed, and is skipped without it. It passed over `AliciaSolid.vrm` + (VRM 0.x) and the seven VRM Animation MotionPack clips on 2026-09-24. ## 8. Open questions diff --git a/tests/motion/test_reference_pipeline.py b/tests/motion/test_reference_pipeline.py new file mode 100644 index 0000000..916e8b5 --- /dev/null +++ b/tests/motion/test_reference_pipeline.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""One downstream half for every source category: the reference pipeline. + + .vrma clip -> usdVrmaFileFormat -+ + BVH export -> motion_convert -+-> MotionClip -> motion_retarget -> VRM + recorded session -> motion_record -+ + +Only the first arrow knows where a clip came from. Two of the three first +arrows are `usd-motion-plugins`' tools, whose committed output this reads +(`tests/motion/fixtures/`, `tools/motionRetarget/tests/fixtures/`); the third is +this repository's importer. Everything after the clip is the shared packages +and `motion_retarget`, and the claim here is that it is **the same code** for +all three, not that three outputs are each plausible (boundary consolidation +BND-1, which the motion migration made its MIG-5 cross-repository test). + +"The same code" is asserted three ways, none of which a per-source test can +make: + + * **One invocation.** Every source is baked by one argument list that differs + in `--animation` alone. A source that needed a flag has found a boundary + defect. + * **One set of loaded code.** `--load-report` names every plugin and module + the run loaded. Across sources they may differ by the first arrow's plugin + and nothing else -- `usdVrmaFileFormat` for a `.vrma`, nothing for a clip + already on a stage. A downstream branch that pulled in code of its own + would show up here as a module one source loads and another does not. + * **One output shape.** The authored layer -- every spec's path, kind and + type, the animation's joint order -- is identical across sources. The + values differ, because the motion does. + +Then each bake is held to the same checks, by the same function, with the same +tolerance: the bound bones reproduce the clip's motion away from rest, the ones +the avatar cannot represent are named, nothing outside the humanoid moves, and +the skeleton resolves the animation rather than its rest pose (#64). + +Two runs are registered. `workspace_reference_pipeline` runs by default, over +the committed inputs: `Seed-san.vrm`, a generated `.vrma`, the converted mocopi +export and the recorded session. `workspace_reference_pipeline_local` runs the +same checks over data that cannot be committed, named by two environment +variables, and is skipped without them: + + USDVRM_LOCAL_AVATAR a .vrm to bake onto + USDVRM_LOCAL_VRMA_DIR a directory whose *.vrma are the .vrma sources + USDVRM_LOCAL_OUTPUT_DIR optional: where to keep each bake and its load + report, to open in usdview afterwards + +The converted export and the recorded session join those, so the local run +still compares three categories. +""" + +from __future__ import annotations + +import argparse +import json +import os +import pathlib +import sys +import tempfile + +from pxr import Sdf, Usd + +from rigcheck import (DISTANCE_TOLERANCE, ROTATION_TOLERANCE, Failures, Rig, + as_quatf, quat_distance, run_tool) +from test_real_avatar import bones_bound_by, check_binding_resolves + +# The plugin each first arrow may add to a run. Everything else a run loads has +# to be loaded by every other run too. +FIRST_ARROW = { + "vrma": frozenset({"UsdVrmaFileFormat"}), + "bvh": frozenset(), + "trace": frozenset(), +} + +# ctest's SKIP_RETURN_CODE for the local run without its data. +SKIPPED = 77 + + +class Source: + def __init__(self, kind: str, path: pathlib.Path) -> None: + self.kind = kind + self.path = path + self.name = f"{kind}:{path.name}" + + +def downstream(tool: str, avatar: pathlib.Path, source: Source, + output: pathlib.Path, report: pathlib.Path): + """The one invocation. Nothing in it may depend on `source.kind`.""" + return run_tool(tool, "--avatar", str(avatar), "--animation", str(source.path), + "--output", str(output), "--load-report", str(report)) + + +def layer_shape(layer: Sdf.Layer) -> set[tuple[str, str, str]]: + """Every spec's path, kind and type -- what was authored, not its value.""" + shape: set[tuple[str, str, str]] = set() + + def visit(path: Sdf.Path) -> None: + spec = layer.GetObjectAtPath(path) + if spec is None: + return + kind = type(spec).__name__ + if isinstance(spec, Sdf.PrimSpec): + detail = spec.typeName + elif isinstance(spec, Sdf.AttributeSpec): + detail = str(spec.typeName) + else: + detail = "" + shape.add((str(path), kind, detail)) + + layer.Traverse(Sdf.Path.absoluteRootPath, visit) + return shape + + +def normalized(path: str) -> str: + return os.path.normcase(os.path.normpath(path)) + + +def check_bake(failures: Failures, source: Source, output: pathlib.Path, + bound: dict[str, str], stderr: str) -> Rig | None: + """The same checks for every source category, and the same tolerance.""" + stage = Usd.Stage.Open(str(output)) + if not failures.check(stage is not None, f"{source.name}: the bake does not open"): + return None + target = Rig(stage) + clip = Rig(Usd.Stage.Open(str(source.path))) + if not check_binding_resolves(failures, target): + return None + + failures.check( + target.animation.GetScalesAttr().HasAuthoredValue(), + f"{source.name}: the bake authors no scales, so UsdSkel resolves every " + f"joint to its rest pose (#64)") + + silenced = frozenset({clip.leaf(token) for token in clip.joints} - set(bound)) + for bone in sorted(silenced): + failures.check( + bone in stderr, + f"{source.name}: the clip drives {bone}, the avatar binds no joint " + f"for it, and motion_retarget did not say so") + + times = target.times + sampled = sorted({times[0], times[len(times) // 3], + times[2 * len(times) // 3], times[-1]}) + compared = 0 + carried = 0 + for bone, token in sorted(bound.items()): + clip_token = clip.find_leaf(bone) + if clip_token is None: + continue + carried += 1 + if not failures.check( + token in target.slot, + f"{source.name}: the avatar binds {bone} to '{token}', which " + f"its own skeleton does not list"): + continue + compared += 1 + for time in sampled: + expected = clip.rest_relative(clip_token, time, silenced) + actual = target.rest_relative(token, time) + failures.check( + quat_distance(as_quatf(actual), as_quatf(expected)) + <= ROTATION_TOLERANCE, + f"{source.name}, time {time}: {bone} turns {actual} away from " + f"its rest on the avatar and {expected} in the clip") + failures.check( + compared and compared == carried, + f"{source.name}: {compared} of the {carried} bones the clip carries and " + f"the avatar binds were compared") + + # Root motion is a delta from each rig's own start (MOTION_CONTRACT.md, root + # and hips), so the hips travel what the clip's travel, whatever the two + # rigs' heights. + clip_hips = clip.find_leaf("hips") + if failures.check(clip_hips is not None and "hips" in bound, + f"{source.name}: no hips on one side of the bake"): + clip_start = clip.translation(clip_hips, clip.times[0]) + target_start = target.translation(bound["hips"], times[0]) + for time in sampled: + clip_delta = clip.translation(clip_hips, time) - clip_start + target_delta = target.translation(bound["hips"], time) - target_start + failures.check( + all(abs(a - b) <= DISTANCE_TOLERANCE + for a, b in zip(clip_delta, target_delta)), + f"{source.name}, time {time}: the avatar's hips moved " + f"{tuple(target_delta)} and the clip's {tuple(clip_delta)}") + + clip_moving = {bone for bone in bound + if (token := clip.find_leaf(bone)) and clip.moved(token)} + target_moving = {bone for bone, token in bound.items() if target.moved(token)} + failures.check( + bool(clip_moving), + f"{source.name}: no bound bone moves in the clip, so the bake is " + f"compared against a still pose") + failures.check( + clip_moving == target_moving, + f"{source.name}: the clip moves {sorted(clip_moving)} and the bake " + f"moves {sorted(target_moving)}") + + unbound = [token for token in target.joints if token not in set(bound.values())] + moved = sorted(token for token in unbound if target.moved(token)) + failures.check( + not moved, + f"{source.name}: the bake moves {len(moved)} joint(s) no human bone is " + f"bound to: {moved[:8]}") + return target + + +def check_same_code(failures: Failures, sources: list[Source], + reports: dict[str, dict]) -> None: + plugins = {s.name: frozenset(reports[s.name]["loaded_plugins"]) for s in sources} + modules = {s.name: frozenset(normalized(m) + for m in reports[s.name]["loaded_modules"]) + for s in sources} + common_plugins = frozenset.intersection(*plugins.values()) + common_modules = frozenset.intersection(*modules.values()) + for source in sources: + extra = plugins[source.name] - common_plugins + failures.check( + extra <= FIRST_ARROW[source.kind], + f"{source.name} loads {sorted(extra)} that another source does " + f"not, beyond its own first arrow {sorted(FIRST_ARROW[source.kind])}") + arrow_libraries = {normalized(reports[source.name]["loaded_plugins"][name]) + for name in extra} + stray = modules[source.name] - common_modules - arrow_libraries + failures.check( + not stray, + f"{source.name} loads module(s) no other source loads and no " + f"first-arrow plugin accounts for: {sorted(stray)}") + kinds = {source.kind for source in sources} + failures.check( + kinds == set(FIRST_ARROW), + f"only {sorted(kinds)} were compared; the claim is about " + f"{sorted(FIRST_ARROW)}") + + +def check_same_shape(failures: Failures, sources: list[Source], + outputs: dict[str, pathlib.Path], + targets: dict[str, Rig]) -> None: + reference = sources[0] + shape = layer_shape(Sdf.Layer.FindOrOpen(str(outputs[reference.name]))) + for source in sources[1:]: + other = layer_shape(Sdf.Layer.FindOrOpen(str(outputs[source.name]))) + failures.check( + other == shape, + f"{source.name} and {reference.name} bake layers of different " + f"shape: {sorted(other ^ shape)[:6]}") + if reference.name in targets and source.name in targets: + failures.check( + targets[source.name].anim_joints + == targets[reference.name].anim_joints, + f"{source.name} and {reference.name} animate different joints " + f"or the same joints in a different order") + + +def local_inputs() -> tuple[pathlib.Path, list[pathlib.Path]] | None: + avatar = os.environ.get("USDVRM_LOCAL_AVATAR") + vrma_dir = os.environ.get("USDVRM_LOCAL_VRMA_DIR") + if not avatar or not vrma_dir: + return None + return pathlib.Path(avatar), sorted(pathlib.Path(vrma_dir).glob("*.vrma")) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + parser.add_argument("--retarget", required=True, help="motion_retarget") + parser.add_argument("--avatar", type=pathlib.Path, + help="the target .vrm (not with --local)") + parser.add_argument("--vrma", type=pathlib.Path, action="append", default=[], + help="a .vrma source (not with --local)") + parser.add_argument("--bvh-clip", type=pathlib.Path, required=True, + help="motion_convert's output for a BVH export") + parser.add_argument("--trace-clip", type=pathlib.Path, required=True, + help="motion_record's output for a recorded session") + parser.add_argument("--local", action="store_true", + help="take the avatar and the .vrma sources from " + "USDVRM_LOCAL_AVATAR and USDVRM_LOCAL_VRMA_DIR") + arguments = parser.parse_args() + + if arguments.local: + local = local_inputs() + if local is None: + print("USDVRM_LOCAL_AVATAR / USDVRM_LOCAL_VRMA_DIR are not set; " + "skipping the local reference pipeline") + return SKIPPED + avatar, vrmas = local + else: + avatar, vrmas = arguments.avatar, arguments.vrma + + sources = ([Source("vrma", path) for path in vrmas] + + [Source("bvh", arguments.bvh_clip), + Source("trace", arguments.trace_clip)]) + missing = [p for p in [avatar, *(s.path for s in sources)] + if p is None or not p.exists()] + if missing or not vrmas: + print(f"missing input: {missing or 'no .vrma source'}", file=sys.stderr) + return 1 + + avatar_stage = Usd.Stage.Open(str(avatar)) + if avatar_stage is None: + print(f"{avatar} did not open. A .vrm needs usdVrmFileFormat on " + f"PXR_PLUGINPATH_NAME.", file=sys.stderr) + return 1 + failures = Failures() + bound = bones_bound_by(avatar_stage) + if not failures.check( + len(bound) >= 20, + f"{avatar.name} carries {len(bound)} vrm:humanBones bindings"): + return failures.report() + + keep = None + if arguments.local and os.environ.get("USDVRM_LOCAL_OUTPUT_DIR"): + keep = pathlib.Path(os.environ["USDVRM_LOCAL_OUTPUT_DIR"]) + keep.mkdir(parents=True, exist_ok=True) + + with tempfile.TemporaryDirectory(prefix="reference-pipeline-") as directory: + work = pathlib.Path(directory) + outputs: dict[str, pathlib.Path] = {} + reports: dict[str, dict] = {} + targets: dict[str, Rig] = {} + for index, source in enumerate(sources): + output = work / f"{index}-bake.usda" + report = work / f"{index}-load.json" + if keep is not None: + # Baked in place rather than copied there: the bake references + # its avatar by a path relative to itself, which a copy breaks. + stem = f"{avatar.stem}__{source.kind}__{source.path.stem}" + output = keep / f"{stem}.usda" + report = keep / f"{stem}.load.json" + result = downstream(arguments.retarget, avatar, source, output, report) + if not failures.check( + result.returncode == 0, + f"{source.name}: motion_retarget failed with the argument " + f"list every other source takes: {result.stderr[-2000:]}"): + continue + outputs[source.name] = output + reports[source.name] = json.loads(report.read_text(encoding="utf-8")) + target = check_bake(failures, source, output, bound, result.stderr) + if target is not None: + targets[source.name] = target + print(f"{source.name}: baked {len(target.times) if target else 0} " + f"samples onto {avatar.name}") + + baked = [source for source in sources if source.name in outputs] + if len(baked) == len(sources): + check_same_code(failures, baked, reports) + check_same_shape(failures, baked, outputs, targets) + + return failures.report() + + +if __name__ == "__main__": + raise SystemExit(main())