HSI Generation Refactor Plan
Date: 2026-07-08
This document is the current design and cleanup plan for utils/hsi_generation.
It is not a historical migration log. Old run directories, old manifests, and
old v13 alignment notes are provenance only; they should not drive runtime
state or public interfaces.
Current Goal
HSI generation should be a task-first, general motion generation system:
The public interface stays small:
--task-plan--layout-json--out-dir
Runtime/debug strategy should not become public config shape, and task json should not become a hidden execution config carrying dated paths, old case state, or experiment knobs.
Core Problem
The current simplified path has the right stage outline but has not preserved all v13 motion-generation behavior. The biggest behavioral gap is not the overall flow; it is that some action operations are incomplete or not triggered from task semantics.
For pick/carry/place, the v13-quality chain must be present:
build scaffold inputs
-> build contact labels
-> pick/place/carry keyframe constraints
-> official or optimized pick grasp replacement
-> place grasp keyframe optimize
-> Kimodo generate
-> root cleanup
-> object motion with active-hand binding anchor
-> hand pose schedule bake
-> preserve official/optimized grasp relation or skip destructive refine
-> final place keyframe apply
-> metrics/render
The new implementation should keep this behavior, but not as a hard-coded v13 recipe. It should be triggered by generic action operations derived from the task plan.
Universality Requirement
HSI generation must not be a hand-crafted recipe for the N1 v13 pick-carry-place cup case. That case is a regression target, not the architecture.
The task plan describes semantics:
- ordered actions
- manipulated objects
- targets/supports
- active hands
- contact expectations
- interaction annotations
Kimodo text prompts are scaffold constraints derived from these semantics. They should not be public runtime config, and should not fall back to object-specific sentences such as "box" or "table" unless those labels came from the task/layout.
The pipeline derives an execution plan from those semantics. It should support the SAGE action set as a finite collection of composable operations:
open and close are not a current target because SAGE generation currently
disables them. They should fail clearly or remain unsupported until the
generator enables them.
Passing the v13 pick/carry/place comparison is necessary but not sufficient. It proves regression compatibility for one important case; it does not prove generality.
Unified Representation
HSI generation uses one simple chain across locomotion, HOI, and HSI:
The terms are strict:
Action Plan: ordered task actions parsed from task json.Anchors: semantic intent records. They are not motion arrays, not.npzfiles, and not solver-specific inputs.Constraints: concrete inputs derived from anchors for a backend or postprocess step. This is a broad HSI term, not only a Kimodo term.Scaffold Motion: the full-body human motion generated by one Kimodo call.Refined Motion: scaffold motion after object binding/release, hand bake/refine, final placement, and support/contact correction.
Examples of constraints:
- Kimodo root/path/facing/full-body keyframe constraints.
- HOI-FHLI grasp or release keyframe optimization inputs.
- Contact schedules for labels, audit, render overlay, and hand/object refine.
- Object relation schedules such as object-in-hand and hand-to-support transfer.
Do not call contact schedules "Kimodo contact constraints"; Kimodo does not currently consume contact constraints directly. Contact schedules are constraints only in the broader HSI sense.
Scene-agnostic priors belong under data/hsi_primitives/, ideally as atomic
keyframe candidates. Scene-conditioned outputs belong under output/.
Current full-motion backend priors are transitional Kimodo/keyframe-builder
debt; they must not become the conceptual HSI primitive library.
Target Flow
The target flow is:
task_plan + layout_json
-> build_clips(task_plan)
-> build_execution_plan(clips, geometry)
-> build_action_facing(task_plan, layout_json)
-> build_anchors(execution_plan)
-> build_constraints(anchors, task, geometry)
-> build_scaffold_spec(constraints)
-> kimodo.generate(scaffold_spec) once
-> kimodo.root_cleanup(scaffold_motion)
-> refine_interaction(scaffold_motion, constraints)
-> evaluation.metrics_render()
refine_interaction is a conceptual phase, not necessarily a new wrapper file.
It owns post-scaffold interaction correction:
apply object binding/release
-> bake scheduled hand pose
-> optional local hand/object refinement
-> final place/support correction
Object motion belongs inside this phase. It should not appear as a peer of Kimodo scaffold generation in the top-level pipeline.
Action-facing headings are task semantics. They are derived from task source
points, interaction targets, and layout targets, then stored only as
info["action_facing"]["by_step_id"]. Pick/place keyframe operations read the
heading through planning.heading_for_event(info, event). Do not recreate old
heading fields such as pick_facing, place_facing, or
recommended_first_heading_angle.
refine_interaction produces an interaction motion. This is one joint motion
state, not a human-only motion plus a loosely attached side product:
interaction_motion
human_motion: full-body/root/hand motion
object_motion: optional per-object motion
contacts/supports: optional interaction windows
object_motion is optional and action-dependent:
- Pure locomotion and turn actions have no object motion.
- Static support interactions such as sit, get_up, lie_down, and stand use scene support geometry/contact constraints, but normally do not produce a rigid object motion.
- Movable-object interactions such as pick, carry, place, push, pull, and kick should produce per-object rigid 6D object motion when the object is expected to move.
- Articulated interactions such as open/close, if enabled later, should use an articulated object motion rather than pretending the whole object is a rigid body.
Current native code only routes the pick/carry/place release chain into object motion refinement. Push/pull/kick anchors describe the target contract, but they must not reuse the pick/carry/place attach/release implementation until a contact-motion object solver exists.
Downstream tracking should consume this interaction motion contract:
human_motion
+ optional object_motion[object_id] = per-frame object pose/motion
+ optional contact/support windows
The pipeline should not make every stage rediscover object motion by probing stage files. If a tracking input file is needed, write it once from the interaction motion near the end of refinement.
This matches the current HOI-FHLI behavior: grasp keyframe optimization solves the hand against a fixed object pose, while sequence local refine keeps the Kimodo body/root scaffold mostly fixed but can refine the hand pose and bounded object motion together in the interaction window.
Keyframe Role
A keyframe is a constraint artifact, not a top-level pipeline phase. It can be produced by interaction logic and then used in different places:
- Pre-scaffold keyframes become Kimodo scaffold constraints, for example pick reach, pick grasp, place reach, or place grasp constraints.
- Post-scaffold keyframes can correct or lock the interaction motion, for example final place/release correction.
- The solver/helper code can be shared, but the logical role must stay clear: generating a scaffold constraint is not the same operation as refining an already generated interaction motion.
So grasp-related code belongs to the interaction domain, while the resulting keyframe may be consumed either before Kimodo generation or during post-scaffold refinement.
Refine Interaction Contract
Keep the refine interaction boundary simple:
interaction_motion always contains human motion. It contains object motion
only when the action semantics require a moving object.
interaction_motion:
human_motion: required
object_motion: optional
contacts: optional
support_windows: optional
Do not pass a long object-specific request dict through the pipeline. The inputs should be semantic and compact:
scaffold_motion: the Kimodo-generated human scaffold.constraints: action-derived constraints such as hand/object keyframes, object relation schedules, contact windows, support windows, and local-refine settings.task/layout geometry: object ids, support ids, meshes, scene coordinates, and active hands.segment timing: pick/carry/place/sit/etc. frame windows.
Output rules:
- No movable object: return human-only
interaction_motion; do not invent empty dynamic-object fields. - Static support interaction: return human motion plus support/contact windows; no rigid object motion unless the support itself is meant to move.
- Movable rigid object: return human motion plus per-object rigid 6D
object_motion. - Articulated object: later return articulated object motion, not fake rigid object motion.
The .npz representation may keep the current dynamic_object_* fields for
compatibility, but conceptually those fields are the serialized object_motion
inside interaction_motion.
Code Design
Use a small data-flow design, not a class hierarchy:
info = kimodo.prepare_task_scaffold(runtime)
constraints = constraint.prepare(runtime, info)
scaffold_motion = kimodo.generate_scaffold(runtime, constraints)
interaction_motion = refine_interaction(runtime, scaffold_motion, constraints, info)
metrics = evaluation.measure(runtime, interaction_motion, info)
refine_interaction can remain visible in pipeline.py while the code is being
simplified. It should be a grouped orchestration block, not a new wrapper layer:
The important boundary is the data contract:
constraint.prepare(...)consumes semantic anchors and returns a compact dict of action-derived artifacts: scaffold constraint paths, object-motion binding/release specs, hand-motion specs, post-refine specs, object relation schedules, contact windows, and support windows.kimodo.generate_scaffold(...)consumes only the scaffold-relevant part of those constraints.refine_interactionconsumes the interaction-relevant part and returns oneinteraction_motion.object_motionis optional insideinteraction_motion; functions must not assume every action hasdynamic_object_*fields.
Avoid copied request bags with many task ids, paths, and runtime knobs. Object motion refinement should receive the scaffold motion plus the compact interaction constraints it actually owns.
Prefer semantic handoff:
constraints["interaction"] = {
"op_events": [
{"op": "pick_grasp", "object_id": "...", "step_id": 1},
{"op": "bind_object", "object_id": "...", "step_id": 1},
{"op": "keep_object_bound", "object_id": "...", "step_id": 2},
{"op": "prepare_place_release", "object_id": "...", "step_id": 3},
{"op": "release_object", "object_id": "...", "step_id": 3},
],
"object_motion": {
"mode": "pick_carry_place_release",
"required": True,
"bound_intervals": [
{
"object_id": "...",
"bind_step_id": 1,
"bound_step_ids": [2],
"release_step_id": 3,
"binding": {...},
"release": {...},
},
],
},
"hand_motion": {
"pick_pose": {"artifact": "..."},
"place_pose": {"artifact": "..."},
"schedule": {"artifact": "..."},
},
"post_refine": {
"final_place_keyframe": {...},
"preserve_pick_grasp_source": "...",
},
"object_relations": [...],
"contact_windows": [...],
"support_windows": [...],
}
Then each domain function reads only the entries it owns and fails clearly if a required interaction artifact is missing.
The difference from v13 is dispatch, not motion behavior:
- v13: fixed script order plus many args.
- new path: ordered task actions produce ordered execution intents.
Pipeline files should remain orchestration only: prepare state, call domain APIs, write final provenance.
Execution Plan
Use small plain-function layers:
utils/hsi_generation/planning.py
utils/hsi_generation/anchors.py
utils/hsi_generation/constraint.py # anchors -> solver/postprocess inputs
utils/hsi_generation/object_motion.py # object relation apply inside refine
utils/hsi_generation/hands.py # hand/object refinement inside refine
Keep it plain:
- no class hierarchy
- no registry framework
- no validation state machine
- no hidden config bundle
- plain
dictandlist
Suggested API:
build_clips(task: dict) -> list[dict]
build_execution_plan(clips: list[dict], geometry: dict) -> list[dict]
build_anchors(plan: list[dict]) -> dict[str, list[dict]]
build_constraints(anchors: dict, task: dict, geometry: dict) -> dict
generate_scaffold(constraints: dict) -> MotionState
refine_interaction(scaffold_motion: MotionState, constraints: dict) -> InteractionMotion
InteractionMotion is a semantic contract, not a required class hierarchy. In
the current code it may still be represented by one .npz/MotionState, but
the meaning should be:
human motion: always present
object motion: optional, only for moving-object actions
contacts/supports: optional, for refinement and audit
Each intent is an ordered action record. The action is the ownership boundary, but the intent itself should stay semantic. It should not list executable operations; those are derived later by the constraint layer from anchors:
Do not put operations, needs, or requirements on intent records. The
clean layering is:
planning.py task -> intent
anchors.py intent -> semantic anchors
constraint.py anchors -> executable op events
Cross-action dependency must be explicit and small. For example, place may
consume the pick action's optimized grasp keyframe as its grasp/source relation,
but pick_grasp_optimization remains owned by pick; it must not become a
global fallback, public flag, or old-run lookup.
Action State Continuity
Every action must expose the state it changes, and the next action must start from any still-active state unless it explicitly releases, overrides, or ends that state. This is the core sequence contract:
This should stay simple. Do not add a large lifecycle framework. Pass a plain small state record between ordered operations when the state is still active.
Examples:
sit
end_state.posture = seated(support_id, root, heading, body_pose)
get_up
start_state.posture must be the preceding seated state on the same support
end_state.posture = standing(root)
pick
end_state.object[object_id] = bound_to_hand(hand, local_pose)
carry
start_state.object[object_id] must be bound_to_hand(...)
end_state.object[object_id] remains bound_to_hand(...)
place
start_state.object[object_id] must be bound_to_hand(...)
end_state.object[object_id] = supported_on(support_id)
walk / reach
end_state.root = path_end or root_target
the next action starts from that root unless it owns a more specific state
The sit -> get_up case is the posture version of this rule: the seated root
and seated full-body pose at the end of sit must also be the start state of
the following get_up. Otherwise the solver can jump from the seat back to a
standing point in one frame.
Anchors are plain semantic records. They are not config, not extra task schema, and not keyframes:
{
"action": "pick",
"step_id": 2,
"target": "wrist_object",
"purpose": "pick_reach",
"object_id": "...",
"target_id": "...",
"active_hand": "left",
}
Anchors must stay semantic and sparse; they should not become a trace of v13
stage names. For example, place anchors describe approach, object-on-support,
hand/object release, object/support contact, and hand-to-support transfer. The
constraint layer owns the detailed expansion into place reach, release
alignment, place grasp optimization, and final keyframe application.
Constraints are the first executable layer. They should be explicit by type:
{
"kimodo_constraints": [...],
"hoi_constraints": [...],
"contact_schedule": [...],
"object_relation_schedule": [...],
}
Keyframes are constraints, not a separate top-level stage. A grasp keyframe, posture keyframe, or final object placement keyframe belongs under the appropriate constraint bucket.
Action Operations
Initial operation mapping:
pick
pick_reach_keyframe
pick_grasp_optimization
pick_grasp_constraint_replacement
object_binding_anchor
hand_grasp_pose
carry
carry_scaffold
preserve_object_in_hand
carry_contact_labels
place
place_reach_keyframe
place_grasp_keyframe
release_alignment
release_binding_anchor
hand_release_schedule
final_place_keyframe
reach
scaffold_constraint
internal turn
scaffold_constraint
root_facing waypoint anchor
sit / get_up / lie_down
posture_scaffold_constraint
posture_state_continuity
support_contact_labels
push / pull / kick
contact_scaffold_constraint
object_contact_motion
measured_contact_labels
object_contact_motion is a planned movable-object solver operation, not the
same thing as pick/carry/place attach. Until that solver exists, native runtime
support should fail clearly instead of routing push/pull/kick through
pick/carry/place release logic.
For pick/carry/place, target behavior is v13-equivalent:
pickestablishes a high-quality grasp keyframe and hand pose target.- the pick grasp becomes the active-hand object binding anchor.
- carry preserves the hand-object local relation established by pick.
placebuilds a place keyframe and aligns release to that keyframe.placesupplies the release binding anchor.- hand pose baking uses a schedule, not only a generic prior.
- if a trustworthy grasp relation exists, local refine preserves it.
- final place keyframe application remains in the generation path.
This is a behavior requirement, not a path requirement. The new pipeline must run the required HOI-FHLI pick grasp optimization itself. Do not hard-code the N1 object, dated output directories, seed-specific files, or a v13 branch, and do not load old v13 artifacts as runtime input state.
If optimized grasp/keyframe assets are reused in future, selection must happen through a general domain mechanism keyed by action, object geometry/class, active hand, and target relation. Otherwise the operation should run the appropriate HOI-FHLI/Kimodo domain optimization or fail clearly instead of silently degrading to low-quality defaults.
Pick/Carry/Place Example
The current N1 case should map to the four-layer chain like this:
pick
anchor:
approach_object, pick_grasp, hand_object contact_start,
object_in_hand binding_anchor
constraint:
Kimodo wrist/full-body keyframe constraint
HOI-FHLI grasp keyframe optimization input/output
hand-object contact schedule start
object-in-hand relation schedule start
scaffold motion:
Kimodo-generated human motion with pick constraints
refined motion:
object motion follows the active hand
grasp hand pose is baked/refined while preserving relation
carry
anchor:
carry_path, hand_object contact_hold, object_in_hand preserve_relation
constraint:
Kimodo root/path constraint
hand-object contact hold schedule
object-in-hand relation schedule
scaffold motion:
Kimodo-generated human motion along the carry path
refined motion:
object follows active-hand local binding through the carry window
place
anchor:
approach_support, object_on_support place_target,
hand_object release, object_support support,
object_transfer hand_to_support
constraint:
Kimodo place reach/final object target constraints
optional HOI-FHLI release/grasp keyframe optimization input/output
release contact schedule
hand-to-support object relation schedule
scaffold motion:
Kimodo-generated human motion with place constraints
refined motion:
object releases from active hand to support/target
hand pose opens/releases
final placement correction is applied when needed
This is the general pattern. Other actions should add their own anchors and constraints, then reuse the same scaffold/refine phases instead of adding a new pipeline branch.
Target Structure
Keep the package small and layered:
utils/hsi_generation/
pipeline.py # orchestration only
files.py # paths/env/output/provenance, no hidden business policy
planning.py # task clips -> execution plan
anchors.py # execution plan -> semantic anchors
constraint.py # anchors -> solver/postprocess constraints
kimodo.py # scaffold spec/generation/root cleanup boundary
keyframe.py # temporary owner for grasp/object/hand keyframe logic
object_motion.py # object relation apply inside interaction refinement
hands.py # hand schedule, bake, and local hand/object refinement
contact.py # expected/measured contact schedules and timelines
evaluation.py # metrics/render/validation aggregation
geometry.py # reusable coordinate/rotation/scene geometry
constraint.py is a target consolidation point, not a wrapper layer. Keep the
direct object_motion and hands calls visible in pipeline.py until a real
shared interaction-refine domain API exists. Group them as one top-level
refine_interaction phase, because object binding, hand bake, and local refine
are all post-scaffold refinement.
Introduce subfolders only when a file becomes genuinely too large and the split removes complexity. Do not split one action per file as a default.
If actions/ is needed later, prefer coarse grouping:
utils/hsi_generation/actions/
object_ops.py # pick/carry/place/push/pull/kick
locomotion.py # reach/turn/walk-like scaffold operations
posture.py # sit/get_up/lie_down
Layer Boundaries
Owned by hsi_generation:
- task clips and execution operations
- stage orchestration
- object binding semantics
- contact schedule coordination
- canonical artifacts and validation aggregation
Owned by utils/kimodo:
- Kimodo prompt/constraint/meta/info schema
- scaffold generation
- Kimodo motion adapters
- root cleanup
Owned by utils/hoifhli:
- HOI-FHLI/DexGraspNet-style grasp optimization
- MANO/SMPL-X hand model glue
- grasp keyframe solving
Owned by utils/visualization:
- scene-human-object rendering
- camera policy
- preview/video export
Owned by utils/metrics:
- contact distance
- penetration
- temporal/motion quality metrics
- strict metric computation
Forbidden directions:
kimodo -> hsi_generation
hoifhli -> hsi_generation
visualization -> hsi_generation
metrics -> hsi_generation
hsi_generation -> tools/scripts
Non-Negotiable Constraints
pipeline_manifest.jsonis output provenance only. It must not be runtime state or input config.- The new pipeline must not infer config from old case directories or old manifests.
- Do not reintroduce legacy baseline-config or alignment execution modes.
- Do not put command-line or manual scripts under
utils. - Do not create
utils/scripts,utils/*/scripts, orutils/*/cli.py. - Do not add wrapper layers just to keep old call paths alive.
- Required task/config fields should be accessed explicitly; missing fields should fail immediately.
- Stage/runner files should not contain math, geometry, rotation logic, KNN, or long field mapping.
- Shared geometry/math should live in a utility module.
- Shared losses should live in solver/loss modules.
- Old tools may stay under
tools/orutils/*/legacy, but new pipeline code must not depend on them.
Refactor Order
- Keep the public interface fixed at
--task-plan --layout-json --out-dir. - Keep
planning.pyandanchors.pysmall; anchors remain semantic records. - Keep improving the constraint-building layer. It now converts anchors into
ordered
opevents, executes implemented operations in event order, validates bind/release ordering, and passesobject_motion.bound_intervalsinto interaction refinement. Operation state is keyed by object and step, so repeated pick/place intervals are represented as data instead of singleton stage state. - Make Kimodo generation consume a scaffold spec and run once per sequence.
- Make post-Kimodo interaction refinement consume explicit constraints: object binding/release, hand bake/refine, support transfer, and final placement are one refine phase rather than separate top-level pipeline stages.
- Restore v13-equivalent pick/carry/place behavior through the generic constraints/refine event flow: pick grasp, binding anchor, place grasp, release alignment, hand schedule, preserve relation, final place correction.
- Continue moving stage code from implicit runtime settings/artifact guessing to explicit action/anchor/constraint consumption.
- Add clear unsupported-action behavior for actions whose domain primitive is not implemented yet.
- Extend constraints/refine support to
sit/get_up/lie_down; each posture enter operation must expose a support state, and matching exit operations must inherit that state as their start state. - Extend constraints/refine support to
push/pull/kick. - Continue file simplification and merging after behavior is stable.
Current Code Checkpoint
Completed in the current code:
- Public HSI generation stays task-first:
--task-plan --layout-json --out-dir. planning -> anchors -> op_eventsexists for the supported action families.- Kimodo scaffold input now uses the ordered generic
plan_to_constraintsbuilder for task-plan steps, including pick/carry/place sequences. The new HSI pipeline no longer selects the old pick/carry/place task scaffold, so a task such asreach -> pick -> carry -> placekeeps the initial reach in the scaffold timeline. - Task step
prompt/textis preserved as the Kimodo scaffold text constraint when present; action templates are fallback only. - The constraint layer now dispatches implemented operations by ordered
op_eventsinstead of using global singleton_has_op(...)checks. - Object bind/release ordering is represented as
interaction.object_motion.bound_intervals, notcycleorcycles. - Binding and release contracts live on the interval that owns them. The refine
layer should not read global
interaction.object_motion.bindingorinteraction.object_motion.releasesingleton state. - Hand bake and final place refine are also list-based: pick/place hand poses, schedules, and final place keyframes are recorded per event before the hand stage merges schedules or applies the keyframes in order.
- The implemented object-motion backend supports one or more bound intervals per
dynamic object and can serialize multiple dynamic objects in one sequence.
For compatibility it still writes the first object to the legacy
dynamic_object_*fields, and also writes per-object fields such asdynamic_object_<token>_pos,dynamic_object_<token>_rot_mat, anddynamic_object_<token>_vertices. pipeline.pyhas one post-scaffold interaction-refine boundary: object motion, hand bake/refine, local interaction refine, and final place keyframe application are downstream of_refine_interaction(...).- Pick/place reach keyframe operations pass solver artifacts through explicit Python return values. Grasp optimization consumes those return values and no longer probes stage-root fixed filenames.
- HSI operation contracts no longer pass
operation_diras cross-stage state. When an external solver needs files, downstream code derives the operation directory from the explicitkeyframe_artifactit already received. - Scaffold constraints now move through explicit
constraints_jsonartifacts returned by each operation.stage_dir/constraints.jsonis only the initial Kimodo scaffold input written by the scaffold builder; later HSI operations no longer copy their result back over that stage-root file as a hidden state bus. - Pick/place binding, hand-pose, and release handoffs use explicit contracts
such as
keyframe,keyframe_artifact,hand_pose, andscheduleinstead of leaking*_npzfilenames as the interaction state shape. write_pick_carry_place_object_motion(...)consumes explicitobject_intervalscontracts rather than a spread of script-style binding/release arguments.pipeline_manifest.jsonremains provenance, not runtime state.- Full left/right task-plan generation is intentionally deferred until this refactor checkpoint passes focused tests and review. Do not launch long generation runs while the 1-7 cleanup is still in progress.
- Pick grasp uses the higher-quality native search policy used by the old official-grasp path: batch size 32 and 600 iterations.
- Pick grasp now restores the old official MANO probe/apply behavior as
utils.hoifhli.official_graspdomain APIs. The pick operation passes the pre-grasp motion and official-applied keyframe in memory, then writes only the final pick keyframe artifact plus concise provenance/object-model cache needed by the official ObjectModel. - HSI hand-stage provenance now uses
hand_pose_artifactandhand_pose_schedule_artifact; old*_npz/*_jsonnames remain only at lower-level solver call boundaries where those functions still accept file paths. - Human-only scaffold tasks can now use the generic Kimodo
plan_to_constraintsdomain builder from the task json interface. This coversreach,sit,get_up, andlie_downas scaffold actions without requiring a pick/place object pair; interaction refinement then records object motion as not applicable. - Generic scaffold no longer builds a temporary old
plan_dirormanifest.jsonas a runtime state bus; HSI calls the in-memorywrite_kimodo_inputs_from_steps(...)domain API and only persists Kimodo's requiredmeta.json,constraints.json, andinfo.json.
Still not complete:
- Pick/carry/place still needs final left/right full-run validation after the official-grasp domain API integration. The expected flow is now present, but motion quality and v13 metric parity must be checked by regenerating the two task-plan cases once the code review checkpoint is accepted.
- Multi-object object-motion serialization is implemented. Current render and
metrics consumers should still be treated as primary-object consumers until
they are extended to read
dynamic_object_field_tokens. push/pull/kickare not fully connected to the same constraint/refine backend path because the object contact-motion solver is still missing. They should keep failing clearly instead of pretending that scaffold-only motion moved the object.- File count and file boundaries can be reduced further after behavior is
stable; the current
utils/hsi_generationcode is already below the 8k-line target.
Immediate Code TODO
Next cleanup should stay bounded:
- Keep extending action coverage through the same anchor -> constraint -> scaffold motion -> refined motion flow.
- Extend render and metrics to consume the per-object dynamic-object fields when multi-object visual/metric validation becomes necessary.
- Do not reintroduce stage-root fixed solver filenames, manifest probing, or task-specific branches while adding action operations.
- Run the left/right task-plan cases after the current code checkpoint is accepted, then compare contact metrics and visual quality against v13.
- Merge files only after behavior is stable and tests cover the affected boundary.
Verification Plan
Focused tests after each meaningful change:
PYTHONPATH=/mnt/data/PhysHSI pytest -q \
tests/test_hsi_generation_pipeline.py \
tests/test_hsi_generation_binding.py \
tests/test_expected_contact_labels.py \
tests/test_measured_contact_labels.py \
tests/test_kimodo_contact_region_labels.py \
tests/test_physhsi_metrics.py
If touching Kimodo-facing modules:
PYTHONPATH=/mnt/data/PhysHSI pytest -q \
tests/test_kimodo_plan_to_constraints.py \
tests/test_object_keyframe_binding.py \
tests/test_lie_keyframe_binding.py \
tests/test_kimodo_render_contact_overlay.py
For motion-quality regression:
- run full left task plan;
- run full right task plan;
- render full opaque scene previews;
- compare against v13 reference behavior, not just pass/fail artifacts;
- inspect root motion, object motion, hand-object contact, place/release, and final rendered motion quality.
When CUDA-specific HOI-FHLI/Kimodo behavior is involved, use the project Isaac/HOI environment rather than silently falling back to CPU behavior.
Review Workflow
Code changes that implement this plan should use the project review-agent workflow:
- patch a complete bounded batch of related modules;
- run focused tests for that batch;
- spawn one reviewer subagent after the batch, not after every tiny patch;
- ask the reviewer to check behavior equivalence, simplicity, no hidden wrappers, no manifest-as-state, no v13 hard-code, and no unnecessary config knobs;
- integrate or reject reviewer findings before continuing.
Final Pass Criteria
The refactor is successful when:
- public entry remains
--task-plan --layout-json --out-dir; utils/hsi_generationhas a small, readable flow;- pick/carry/place generated from task operations is v13-equivalent in behavior;
- v13 is a regression case, not a code branch;
- action operations can represent the current SAGE action set;
- unsupported operations fail clearly;
- no new wrappers/legacy dependencies are introduced;
- no runtime state is read from old manifests or case directories;
- full left/right task runs produce motion quality no worse than the v13 reference by visual and metric sanity checks.