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.
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.
- 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/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}/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}/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