← sakibchowdhury131.github.io

Can a World Model Generate Synthetic Training Data for a Real Pick-and-Place Task?

Sakib Chowdhury
Stevens Institute of Technology
Applied Project · Experiment Log

A Kinova Jaco2 arm with a spoon taped to its gripper scoops pasta from a container and transfers it to a plate — 37 real recorded episodes. Can NVIDIA's Cosmos3 world model generate more episodes like it, either to train a policy directly or as a way to stress-test whether video world models are a viable synthetic-data source for real robot manipulation at all?

Code (GitHub) Jump to Results
The strongest result: one real photo + one sentence describing the task, generated in a single ~30-second call — no trajectory data, no chaining.

The Question

Collecting real robot demonstrations is slow and expensive. World models like Cosmos3 can generate plausible video conditioned on an image and either an action sequence or a text prompt — which raises an obvious question for imitation learning: could a world model generate usable training episodes for a real manipulation task, cheaply, without a robot in the loop? This project is a hands-on log of testing that on one concrete task — pasta scooping and transfer — with two different generation strategies, and an attempt to close the loop by recovering action labels from the generated video.

What Was Tried

ApproachOutcome
Stock environment smoke test (no robot data)worked clean, artifact-free camera trajectory
Exact trajectory conditioning at native ~2.67Hz sample ratewrong timing outside the model's supported fps range
Same, resampled to 15fps (SLERP-interpolated)accurate matched real wrist rotation and object interaction
Chained 25× into a ~27s rollout, each chunk conditioned on the last framecollapsed artifact noise by ~10 seconds
Chained with 5-frame context instead of 1 frame per chunkcollapsed faster more synthetic context made drift worse
One real photo + one sentence describing the task, no trajectorybest result correct task execution, no drift
Re-tracking the generated video (Depth Anything + FoundationPose)worked accurate 6-DoF labels, no ground truth needed

The Setup, and a Labeling Bug Found Along the Way

The dataset: 37 real episodes of a Kinova Jaco2 arm re-executing a tracked scooping trajectory, recorded from two fixed RGB cameras at roughly 150 waypoints per episode. Before any generation could be evaluated, every episode needed accurate 6-DoF spoon-pose labels — the exact thing a model would be conditioned on or judged against.

The first labeling attempt used robot forward-kinematics plus a calibrated tool offset, but this was unreliable — 10–20cm off on some frames, from calibration error amplified by the wrist-to-spoon-tip lever arm. The fix was to track the spoon directly: Depth Anything V2 for metric depth (this recording had no depth sensor) feeding FoundationPose for 6-DoF pose from a CAD mesh, with periodic re-registration every 30 frames — a single registration drifts unrecoverably over a ~150-frame sequence.

Bug found: that periodic re-registration was itself silently producing physically-impossible jumps — one case measured a 58cm displacement between two frames 0.1s apart (5.8 m/s for a hand-held spoon), while the two source frames looked visually identical. This was systematic, appearing at the same re-registration boundaries in 35 of 37 episodes. The root cause: a fresh global pose search has no temporal prior, so if the spoon's silhouette has any near-symmetry from that viewpoint, registration can lock onto a wrong-but-plausible pose. The lesson for anyone reusing this pattern — "reprojects in-bounds" is not the same quality bar as "is physically continuous with the previous frame."

Approach 1: Exact Trajectory Conditioning

Cosmos3's forward_dynamics mode takes a starting image plus a sequence of relative 6-DoF pose deltas (translation, rotation, gripper state) and generates the corresponding video — the literal way to ask "show me this exact motion."

Environment smoke test

Stock example: single photo + a pre-made camera-motion trajectory, no robot data involved. A clean dolly-in with no artifacts, confirming the environment itself works before touching real data.

First attempt on real data: wrong timing

Our tracked trajectory fed in at its native ~2.67Hz sample rate. The model itself logs "FPS 3 is outside the recommended range [10, 30]." The general behavior is right (tool stays near the container, scene stays stable) but the timing is wrong — by the point this clip ends, the real arm had already lifted the spoon away.

Fixed: resample to 15fps first

Interpolating the tracked trajectory (linear translation + SLERP rotation) onto a continuous 15fps timeline — inside the model's supported range — fixed it. Tested on both a near-static window and a genuinely high-motion one (spoon dipping deeper into the container, wrist visibly rotating):

Low-motion window — near-exact match to real
High-motion window — wrist rotation direction and pasta pickup both tracked correctly
The fps mismatch, not the domain gap, was the real blocker. Once resampled into the model's supported range, exact trajectory conditioning followed real coarse motion accurately — including a case with genuine wrist rotation and object interaction, on an embodiment (this specific Jaco2 with a taped-on spoon) the model had never seen before.

Chaining Chunks Into Longer Rollouts

Each forward_dynamics call only produces a fixed ~1-second chunk (16 actions → 17 frames). A longer synthetic episode means chaining: feed the last generated frame back in as the next chunk's starting image, and repeat.

25 chunks chained (~27 seconds), each conditioned on the previous chunk's single last generated frame. Sharp and correct for the first ~5 seconds; scribble-artifact texture noise appears by ~10 seconds; near-total structural collapse by the end.

The natural fix to try: condition each new chunk on the last several generated frames (as a short video) instead of just one, hoping more temporal context would stabilize things.

9 chunks, each conditioned on the previous chunk's last 5 generated frames. Degraded to scribble-artifact noise by ~5 seconds — faster than the single-frame version above, not slower.
More self-generated context made it worse, not better. The likely mechanism: every context frame past chunk 0 is itself synthetic. Feeding 5 synthetic frames back in as "here's what just happened" gives the model a stronger, more insistent signal to continue from — but it's a stronger signal reinforcing whatever drift had already crept in, rather than damping it. A single imperfect frame is a weaker anchor, but also a weaker amplifier.

This suggests the fix for long rollouts isn't more synthetic self-conditioning — it's periodically re-anchoring against real frames instead, which wasn't tested here but is the natural next experiment.

Approach 2: Just Describe the Task

Cosmos3 also has an image2video mode: one real photo, one text prompt, no trajectory data of any kind. Up to 300 frames in a single call at this resolution — no chaining, no chunk-boundary seams, and no compounding drift by construction.

"A robotic arm with a spoon attached to its gripper scoops pasta from a
plastic container on a wooden table and transfers it into a white plate.
The camera is static, looking down at the table. The arm dips the spoon
into the container, lifts a scoop of pasta, moves over to the plate, and
drops the pasta onto the plate."
49 frames (3.27s), generated in a single ~30-second call from the real starting frame + the prompt above. No action data. It performed the described task: dipped into the container, filled the spoon, carried it to the plate, dropped it in — pasta quantity visibly changes correctly in both locations.
No trajectory No chaining No drift Correct task semantics

The obvious tradeoff: you give up exact trajectory control. You can't specify "the spoon must be at this precise pose at t=1.2s" — only describe the task and let the model improvise plausible motion. For generating training data specifically, rather than a precise digital twin, that's arguably a reasonable trade — real demonstrations have natural variation too.

Closing the Loop: Re-Tracking the Generated Video

A generated clip is only useful as training data if action labels can be recovered from it. The same labeling pipeline from the setup — Depth Anything for per-frame metric depth, then FoundationPose with the same spoon mesh for 6-DoF tracking — was run directly on the image2video output above.

Same 49 frames, tracked pose rendered back on (green box + RGB axes = FoundationPose's own visualization of what it estimated). Includes one mid-clip re-registration on a fully generated frame (frame 30), not just the real starting frame.
Tracking held up despite the spoon visibly softening in later frames. The frame-30 re-registration ran a fresh segmentation — not incremental tracking — on a generated frame, and still found a recognizable spoon. Reprojecting the tracked pose back onto frames 0, 72, 90, and 144 (the last frame, spoon over the plate) landed on the actual wrist joint every time, with no drift.

This is the first fully closed loop in this project: real photo + one sentence → Cosmos3 generation → Depth Anything → FoundationPose → validated 6-DoF action labels, with no ground-truth trajectory anywhere in the pipeline.

What's Still Open

Code & Environment

Every script referenced above — Cosmos3 inference wrappers, the FoundationPose/Depth Anything labeling pipeline, and the overlay-rendering QC tool — is on GitHub, with notes on what each one does and why.

generation
cosmos3_container_run.sh — starts the Cosmos3 container, with host-specific GPU/CUDA fixes baked in
cosmos3_infer.sh — wrapper around cosmos_framework.scripts.inference
labeling
phase0_foundationpose_track.py — real-episode 6-DoF tracking
phase4_synthetic_track.py — same, adapted for generated video
phase4_render_overlay.py — renders tracked pose back onto video for QC

Both pipelines ran in separate Docker containers on a single machine with 2× RTX A6000. Models used: Cosmos3 / cosmos-framework (Cosmos3-Edge, 4B), FoundationPose, and Depth Anything V2 (metric, indoor).

Full Code on GitHub