Video to motion
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.
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.
Multiple models: The API supports two video-to-motion models. Pass an optional model parameter:
video-to-motion-v1— video-to-motion-v1video-to-motion-v2— video-to-motion-v2 fast (preview)
Different models may offer different quality/speed tradeoffs. If omitted, the default model is used.
Step-by-step tutorial
Step 1: 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
You can upload a file directly or provide a URL.
- Shell
- Python
- TypeScript
- C#
# Set your API key and video file path
API_KEY="{{apiKey}}"
VIDEO_FILE="/path/to/your/video.mp4"
import requests
API_URL = 'https://uthana.com/graphql'
API_KEY = '{{apiKey}}'
VIDEO_FILE = '/path/to/your/video.mp4'
const API_URL = "https://uthana.com/graphql";
const API_KEY = "{{apiKey}}";
// Example markup: <input type="file" id="videoFile" accept="video/*" />
const videoInput = document.querySelector<HTMLInputElement>("#videoFile");
const VIDEO_FILE = videoInput?.files?.[0];
if (!VIDEO_FILE) {
throw new Error("Select a video file before uploading.");
}
private const string ApiUrl = "https://uthana.com/graphql";
private readonly string _apiKey = "{{apiKey}}";
private readonly string _videoFile = "/path/to/your/video.mp4";
Step 2: Upload video and create motion
Upload your video file using the GraphQL multipart request spec. This creates a job that will process your video. To use a specific model, add model: "video-to-motion-v1" or model: "video-to-motion-v2" to the variables.
- Shell
- Python
- TypeScript
- C#
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"
import requests
operations = {
"query": """
mutation ($file: Upload!, $motion_name: String!) {
create_video_to_motion(file: $file, motion_name: $motion_name) {
job {
id
status
}
}
}
""",
"variables": {
"file": None,
"motion_name": "My Video Motion"
}
}
map_data = {"0": ["variables.file"]}
files = {
"operations": (None, json.dumps(operations)),
"map": (None, json.dumps(map_data)),
"0": (open(VIDEO_FILE, "rb"), "video/mp4")
}
response = requests.post(
API_URL,
auth=(API_KEY, ''),
files=files
)
result = response.json()
job = result["data"]["create_video_to_motion"]["job"]
print(f"Job ID: {job['id']}, Status: {job['status']}")
Browser (file picker):
// Example markup: <input type="file" id="videoFile" accept="video/*" />
const videoInput = document.querySelector<HTMLInputElement>("#videoFile");
const videoFile = videoInput?.files?.[0];
if (!videoFile) {
throw new Error("Select a video file before uploading.");
}
const formData = new FormData();
formData.append(
"operations",
JSON.stringify({
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",
},
}),
);
formData.append("map", JSON.stringify({ "0": ["variables.file"] }));
formData.append("0", videoFile, videoFile.name);
const authString = btoa(`${API_KEY}:`);
const response = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Basic ${authString}`,
},
body: formData,
});
const json = await response.json();
const job = json.data.create_video_to_motion.job;
console.log(`Job ID: ${job.id}, Status: ${job.status}`);
Node.js (file path):
import FormData from "form-data";
import fs from "fs";
const VIDEO_FILE_PATH = "/path/to/your/video.mp4";
const formData = new FormData();
formData.append(
"operations",
JSON.stringify({
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",
},
}),
);
formData.append("map", JSON.stringify({ "0": ["variables.file"] }));
formData.append("0", fs.createReadStream(VIDEO_FILE_PATH));
const authString = btoa(`${API_KEY}:`);
const response = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Basic ${authString}`,
...formData.getHeaders(),
},
body: formData,
});
const json = await response.json();
const job = json.data.create_video_to_motion.job;
console.log(`Job ID: ${job.id}, Status: ${job.status}`);
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 3: Poll for job completion
Video-to-motion processing happens asynchronously. Poll the job status until it's complete.
- Shell
- Python
- TypeScript
- C#
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'
import time
JOB_ID = job["id"]
while True:
query = '''
query GetJob($job_id: String!) {
job(job_id: $job_id) {
id
status
result
}
}
'''
response = requests.post(
API_URL,
auth=(API_KEY, ''),
json={
'query': query,
'variables': {'job_id': JOB_ID}
}
)
result = response.json()
job_status = result["data"]["job"]
print(f"Job status: {job_status['status']}")
if job_status['status'] in ['FINISHED', 'FAILED']:
break
time.sleep(5) # Wait 5 seconds before checking again
# Get motion ID from result
if job_status['status'] == 'FINISHED':
motion_id = job_status['result']['motion']['id']
print(f"Motion created: {motion_id}")
async function pollJobStatus(jobId: string): Promise<any> {
const authString = btoa(`${API_KEY}:`);
while (true) {
const response = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Basic ${authString}`,
},
body: JSON.stringify({
query: `
query GetJob($job_id: String!) {
job(job_id: $job_id) {
id
status
result
}
}
`,
variables: { job_id: jobId },
}),
});
const json = await response.json();
const job = json.data.job;
console.log(`Job status: ${job.status}`);
if (job.status === "FINISHED" || job.status === "FAILED") {
return job;
}
await new Promise((resolve) => setTimeout(resolve, 5000)); // Wait 5 seconds
}
}
// Usage
const finalJob = await pollJobStatus(job.id);
if (finalJob.status === "FINISHED") {
const motionId = finalJob.result.motion.id;
console.log(`Motion created: ${motionId}`);
}
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
var finalJob = await PollJobStatusAsync(job.Id);
if (finalJob.Status == "FINISHED")
{
var motionId = finalJob.Result["motion"]["id"].ToString();
Console.WriteLine($"Motion created: {motionId}");
}
Step 4: Download your motion
Once the job is finished, extract the motion ID from the result and download it.
- Shell
- Python
- TypeScript
- C#
CHARACTER_ID="cXi2eAP19XwQ" # Default character, or use your own
MOTION_ID=$(echo "$RESPONSE" | jq -r '.data.job.result.motion.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
CHARACTER_ID = "cXi2eAP19XwQ" # Default character, or use your own
MOTION_ID = job_status['result']['motion']['id']
# Download as FBX (filename is customizable)
fbx_url = f'https://uthana.com/motion/file/motion_viewer/{CHARACTER_ID}/{MOTION_ID}/fbx/motion.fbx'
response = requests.get(fbx_url, auth=(API_KEY, ''))
with open('motion.fbx', 'wb') as f:
f.write(response.content)
const CHARACTER_ID = "cXi2eAP19XwQ"; // Default character, or use your own
const MOTION_ID = finalJob.result.motion.id;
const fbxUrl = `https://uthana.com/motion/file/motion_viewer/${CHARACTER_ID}/${MOTION_ID}/fbx/${CHARACTER_ID}-${MOTION_ID}.fbx`;
const downloadResponse = await fetch(fbxUrl, {
headers: {
Authorization: `Basic ${btoa(`${API_KEY}:`)}`,
},
});
const blob = await downloadResponse.blob();
// Save blob to file or use as needed
private const string CHARACTER_ID = "cXi2eAP19XwQ"; // Default character, or use your own
var motionId = finalJob.Result["motion"]["id"].ToString();
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 startREADY: Job is ready to be processedFINISHED: Job completed successfullyFAILED: Job failed (check theresultfield for error details)
Error handling
Check for errors in the job result:
- Shell
- Python
- TypeScript
- C#
if [ "$STATUS" = "FAILED" ]; then
echo "Job failed:"
echo "$RESPONSE" | jq '.data.job.result'
fi
if job_status['status'] == 'FAILED':
print(f"Job failed: {job_status.get('result', {})}")
if (finalJob.status === "FAILED") {
console.error("Job failed:", finalJob.result);
}
if (finalJob.Status == "FAILED")
{
Console.WriteLine($"Job failed: {JsonSerializer.Serialize(finalJob.Result)}");
}
Next steps
- Learn about Text to motion for generating animations from text
- Explore Retargeting to apply motions to custom characters
- Check the API reference for complete schema documentation