On this page

Convert 2D video files into high-quality 3D character animations. Video-to-motion is perfect for extracting motion from existing video content and applying it to your 3D characters.

Learn more about [Video to motion](/content/product/video-to-motion/index.html).

## Overview

Video-to-motion processes video files asynchronously. Unlike text-to-motion, video-to-motion returns a job that you'll need to poll to check when processing is complete.

**Duplicate uploads:** If you upload the same video file while a job for that file is already running, or if it was successfully processed in the last 24 hours or failed in the last hour, the API returns the existing job instead of throwing an error. Use the returned job ID to poll for status as usual.

**Available models:** Pass an optional `model` parameter to select the model. The default is `video-to-motion-2.0`.

- `video-to-motion-2.0` – Uthana's flagship video-to-motion model. Returns a single base motion.
- `video-to-motion-2.1` – Includes post-processing refinements. Returns refined motion IDs rather than the base motion.
- `video-to-motion-v2` – Alias for `video-to-motion-2.0`, accepted for backward compatibility.

Model removed

`video-to-motion-v1` is no longer available. The deprecation deadline of 30 Apr 2026 has passed. Use `video-to-motion-2.0` instead.

## Step-by-step tutorial

### Step 1: Set up the client

Install your client library and authenticate using your API key. See the [quickstart](/content/docs/api/#step-1-install-the-client/index.html) for setup instructions for each language.

### Step 2: Prepare your video file

Choose a video of you or someone else performing the motion you want to create. The video should be a single, continuous shot, with movement clearly visible and centered in the frame.

**Video requirements:**

- **Supported formats**: `.mp4`, `.mov`, `.avi`
- **Video parameters**: 2-60 seconds, 24-120 fps, 300px to 4096px resolution
- **Camera**: Use a stable, flat surface or tripod
- **Framing**: Keep your whole body in view
- **Start position**: Begin standing with both feet on the ground
- **Subject**: Ensure only one person is in the video
- **Clothing**: Wear well-fitting, distinct colors
- **Background**: Plain, contrasting, and well-lit
- **Lighting**: Avoid direct sunlight or harsh shadows

### Step 3: Upload video and create motion

- Shell

```bash
curl -X POST https://uthana.com/graphql \
  -u $API_KEY: \
  -F 'operations={
    "query": "mutation ($file: Upload!, $motion_name: String!) { create_video_to_motion(file: $file, motion_name: $motion_name) { job { id status } } }",
    "variables": { "file": null, "motion_name": "My Video Motion" }
  }' \
  -F 'map={ "0": ["variables.file"] }' \
  -F '0=@"$VIDEO_FILE"
```

- Python

```python
async def main():
    job = await client.vtm.create("/path/to/your/video.mp4", motion_name="My Video Motion")
    print(f"Job ID: {job['id']}, Status: {job['status']}" )

asyncio.run(main())
```

- TypeScript

```typescript
// Browser: pass a File object from a file picker
const videoInput = document.querySelector<HTMLInputElement>("#videoFile");
const videoFile = videoInput?.files?.[0];
if (!videoFile) throw new Error("Select a video file before uploading.");

const job = await client.vtm.create(videoFile, { motion_name: "My Video Motion" });
console.log(`Job ID: ${job.id}, Status: ${job.status}`);
```

- React

```tsx
import { useUthanaVtm } from "@uthana/react";

function UploadVideo() {
  const vtm = useUthanaVtm();

return (
    <input
      type="file"
      accept="video/*"
      onChange={(e) => {
        const file = e.target.files?.[0];
        if (file) vtm.mutate({ file, motion_name: "My Video Motion" });
      }}
    />
  );
}
```

- C#

```csharp
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;

public async Task<Job> CreateVideoToMotionAsync(string videoFilePath, string motionName) {
    var operations = new
    {
        query = @"
            mutation ($file: Upload!, $motion_name: String!) {
                create_video_to_motion(file: $file, motion_name: $motion_name) {
                    job {
                        id
                        status
                    }
                }
            }",
        variables = new
        {
            file = (object)null,
            motion_name = motionName
        }
    };

var map = new { ["0"] = new[] { "variables.file" } };

using var content = new MultipartFormDataContent();
    content.Add(new StringContent(JsonSerializer.Serialize(operations), Encoding.UTF8, "application/json"), "operations");
    content.Add(new StringContent(JsonSerializer.Serialize(map), Encoding.UTF8, "application/json"), "map");

var fileBytes = await File.ReadAllBytesAsync(videoFilePath);
    content.Add(new ByteArrayContent(fileBytes), "0", Path.GetFileName(videoFilePath));

var authValue = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_apiKey}:")); 
    _httpClient.DefaultRequestHeaders.Authorization =
        new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", authValue);

var response = await _httpClient.PostAsync(ApiUrl, content);
    response.EnsureSuccessStatusCode();

var responseJson = await response.Content.ReadAsStringAsync();
    var result = JsonSerializer.Deserialize<GraphQLResponse<CreateVideoToMotionData>>(responseJson);

return result.Data.CreateVideoToMotion.Job;
}
```

### Step 4: Poll for job completion

Video-to-motion processing happens asynchronously. Poll the job status until it's complete.

- Shell

```bash
JOB_ID="your-job-id"

# Poll until job is complete
while true; do
  RESPONSE=$(curl -s https://uthana.com/graphql \
    -u $API_KEY: \
    -H "Content-Type: application/json" \
    -d "{\"query\": \"query { job(job_id: \\\"$JOB_ID\\\") { id status result } }\"}")

STATUS=$(echo "$RESPONSE" | jq -r '.data.job.status')
  echo "Job status: $STATUS"

if [ "$STATUS" = "FINISHED" ] || [ "$STATUS" = "FAILED" ]; then
    break
  fi

sleep 5  # Wait 5 seconds before checking again
done

# Get the final result
echo "$RESPONSE" | jq '.data.job'
```

- Python

```python
import asyncio

async def main():
    job = await client.vtm.create("/path/to/your/video.mp4", motion_name="My Video Motion")

while job["status"] not in ("FINISHED", "FAILED"):
        await asyncio.sleep(5)
        job = await client.jobs.get(job["id"])
        print(f"Job status: {job['status']}")

if job["status"] == "FINISHED":
        motion_id = job["result"]["result"]["id"]
        print(f"Motion created: {motion_id}")

asyncio.run(main())
```

- TypeScript

```typescript
// The JS client has a built-in wait helper that polls automatically
const finished = await client.jobs.wait(job.id!, {
  intervalMs: 5000,
  timeoutMs: 120_000,
});

if (finished.status === "FINISHED") {
  const motionId = finished.result?.result?.id;
  console.log(`Motion created: ${motionId}`);
}
```

- React

```tsx
import { useUthanaVtm, useUthanaJob } from "@uthana/react";

function VideoUploadWithStatus() {
  const vtm = useUthanaVtm();
  const jobId = vtm.data?.id;

// useUthanaJob polls automatically when a job ID is provided
  const { job } = useUthanaJob(jobId);

return (
    <div>
      <input
        type="file"
        accept="video/*"
        onChange={(e) => {
          const file = e.target.files?.[0];
          if (file) vtm.mutate({ file, motion_name: "My Video Motion" });
        }}
      />
      {job && <p>Status: {job.status}</p>}
      {job?.status === "FINISHED" && <p>Motion ID: {job.result?.result?.id}</p>}
    </div>
  );
}
```

- C#

```csharp
public async Task<Job> PollJobStatusAsync(string jobId) {
    var query = @"
        query GetJob($job_id: String!) {
            job(job_id: $job_id) {
                id
                status
                result
            }
        }";

while (true)
    {
        var request = new
        {
            query = query,
            variables = new { job_id = jobId }
        };

var json = JsonSerializer.Serialize(request);
        var content = new StringContent(json, Encoding.UTF8, "application/json");

var response = await _httpClient.PostAsync(ApiUrl, content);
        response.EnsureSuccessStatusCode();

var responseJson = await response.Content.ReadAsStringAsync();
        var result = JsonSerializer.Deserialize<GraphQLResponse<JobData>>(responseJson);

var job = result.Data.Job;
        Console.WriteLine($"Job status: {job.Status}");

if (job.Status == "FINISHED" || job.Status == "FAILED")
        {
            return job;
        }

await Task.Delay(5000); // Wait 5 seconds
    }
}

// Usage — replace with job ID from Step 3
var finalJob = await PollJobStatusAsync("<job-id-from-step-3>");
if (finalJob.Status == "FINISHED")
{
    var motionId = finalJob.Result["result"]["id"].ToString();
    Console.WriteLine($"Motion created: {motionId}");
}
```

### Step 5: Download your motion

Once the job is finished, extract the motion ID from the result and download it.

- Shell

```bash
CHARACTER_ID="cXi2eAP19XwQ"  # Default character, or use your own
MOTION_ID=$(echo "$RESPONSE" | jq -r '.data.job.result.result.id')

# Download as FBX (filename is customizable)
curl -L "https://uthana.com/motion/file/motion_viewer/$CHARACTER_ID/$MOTION_ID/fbx/motion.fbx" \
  -u $API_KEY: \
  -o motion.fbx
```

- Python

```python
from uthana import UthanaCharacters

async def main():
    job = await client.vtm.create("/path/to/your/video.mp4", motion_name="My Video Motion")
    while job["status"] not in ("FINISHED", "FAILED"):
        await asyncio.sleep(5)
        job = await client.jobs.get(job["id"])

motion_id = job["result"]["result"]["id"]
    data = await client.motions.download(
        UthanaCharacters.tar,  # or your own character ID
        motion_id,
        output_format="glb",
        fps=30,
    )
    with open("dance.glb", "wb") as f:
        f.write(data)

asyncio.run(main())
```

- TypeScript

```typescript
import { UthanaCharacters } from "@uthana/client";

const motionId = finished.result?.result?.id;

const buffer = await client.motions.download(
  UthanaCharacters.tar, // or your own character ID
  motionId,
  { output_format: "glb", fps: 30 },
);

// Node.js: write to disk
import { writeFileSync } from "fs";
writeFileSync("dance.glb", Buffer.from(buffer));
```

- React

```tsx
import { useUthanaClient } from "@uthana/react";

function DownloadMotion({ characterId, motionId }: { characterId: string; motionId: string }) {
  const client = useUthanaClient();

async function download() {
    const buffer = await client.motions.download(characterId, motionId, {
      output_format: "glb",
      fps: 30,
    });
    const url = URL.createObjectURL(new Blob([buffer], { type: "model/gltf-binary" }));
    const a = document.createElement("a");
    a.href = url;
    a.download = "motion.glb";
    a.click();
  }

return <button onClick={download}>Download GLB</button>;
}
```

- C#

```csharp
private const string CHARACTER_ID = "cXi2eAP19XwQ"; // Default character, or use your own
var motionId = "<motion-id-from-step-4>";

var downloadUrl = $"https://uthana.com/motion/file/motion_viewer/{CHARACTER_ID}/{motionId}/fbx/motion.fbx";

var downloadResponse = await _httpClient.GetAsync(downloadUrl);
downloadResponse.EnsureSuccessStatusCode();

var motionData = await downloadResponse.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync("motion.fbx", motionData);
```

## Job statuses

- `RESERVED`: Job has been created and is waiting to start
- `READY`: Job is ready to be processed
- `FINISHED`: Job completed successfully
- `FAILED`: Job failed (check the `result` field for error details)

## Error handling

- Shell

```bash
if [ "$STATUS" = "FAILED" ]; then
    echo "Job failed:"
    echo "$RESPONSE" | jq '.data.job.result'
fi
```

- Python

```python
async def main():
    # ... (create job and poll as shown above)
    if job["status"] == "FAILED":
        print(f"Job failed: {job.get('result', {})}")

asyncio.run(main())
```

- TypeScript

```typescript
if (finished.status === "FAILED") {
  console.error("Job failed:", finished.result);
}
```

- React

```tsx
{job?.status === "FAILED" && <p>Job failed: {JSON.stringify(job.result)}</p>}
```

- C#

```csharp
// After polling (see Step 4), finalJob.Status may be "FAILED":
if (finalJob.Status == "FAILED")
{
    Console.WriteLine($"Job failed: {JsonSerializer.Serialize(finalJob.Result)}");
}
```

## Next steps

- Learn about [Text to motion](/content/docs/api/capabilities/text-to-motion/index.html) for generating animations from text
- Learn about [Locomotion](/content/docs/api/capabilities/locomotion/index.html) for predictable, looptable movement with explicit direction, speed, and style
- Explore [Retargeting](/content/docs/api/capabilities/retargeting/index.html) to apply motions to custom characters
- Check the [API reference](/content/docs/api/graphql/index.html) for complete schema documentation.
