Text to motion
Generate high-quality 3D character animations from simple text prompts. Text-to-motion is perfect for quickly creating animations without needing video input or motion capture data.
Overview
Text-to-motion allows you to describe an animation in natural language, and Uthana will generate a motion sequence that matches your description. Results are returned immediately—no polling required.
Step-by-step tutorial
Step 1: Authenticate your request
First, ensure you have your API key ready. You'll use it to authenticate all requests to the Uthana API.
- Shell
- Python
- TypeScript
- C#
# Set your API key
API_KEY="{{apiKey}}"
import requests
API_URL = 'https://uthana.com/graphql'
API_KEY = '{{apiKey}}'
const API_URL = "https://uthana.com/graphql";
const API_KEY = "{{apiKey}}";
private const string ApiUrl = "https://uthana.com/graphql";
private readonly string _apiKey = "{{apiKey}}";
Step 2: Generate motion from text
Create a text-to-motion animation by sending a mutation with your text prompt.
- Shell
- Python
- TypeScript
- C#
curl -X POST https://uthana.com/graphql \
-u $API_KEY: \
-H "Content-Type: application/json" \
-d '{
"query": "mutation CreateTextToMotion($prompt: String!) { create_text_to_motion(prompt: $prompt) { motion { id name } } }",
"variables": { "prompt": "a person walking down the street" }
}'
import requests
query = '''
mutation CreateTextToMotion($prompt: String!) {
create_text_to_motion(prompt: $prompt) {
motion {
id
name
}
}
}
'''
variables = {"prompt": "a person walking down the street"}
response = requests.post(
API_URL,
auth=(API_KEY, ''),
json={
'query': query,
'variables': variables,
}
)
result = response.json()
motion = result["data"]["create_text_to_motion"]["motion"]
print(f"Created motion: {motion['id']} - {motion['name']}")
const authString = btoa(`${API_KEY}:`);
const response = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Basic ${authString}`,
},
body: JSON.stringify({
query: `
mutation CreateTextToMotion($prompt: String!) {
create_text_to_motion(prompt: $prompt) {
motion {
id
name
}
}
}
`,
variables: { prompt: "a person walking down the street" },
}),
});
const json = await response.json();
const motion = json.data.create_text_to_motion.motion;
console.log(`Created motion: ${motion.id} - ${motion.name}`);
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
public async Task<CreateTextToMotionResult> CreateTextToMotionAsync(string prompt)
{
var query = @"
mutation CreateTextToMotion($prompt: String!) {
create_text_to_motion(prompt: $prompt) {
motion {
id
name
}
}
}";
var request = new
{
query = query,
variables = new { prompt = prompt }
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
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<CreateTextToMotionData>>(responseJson);
return result.Data.CreateTextToMotion;
}
Step 3: Handle the response
Text-to-motion results are returned immediately. The response includes the motion ID and name.
- Shell
- Python
- TypeScript
- C#
{
"data": {
"create_text_to_motion": {
"motion": {
"id": "m3G3XSJrjEJH",
"name": "a person walking down the street"
}
}
}
}
result = response.json()
if "errors" in result:
print(f"Error: {result['errors']}")
else:
motion = result["data"]["create_text_to_motion"]["motion"]
print(f"Motion ID: {motion['id']}")
print(f"Motion Name: {motion['name']}")
const json = await response.json();
if (json.errors) {
console.error("GraphQL errors:", json.errors);
} else {
const motion = json.data.create_text_to_motion.motion;
console.log(`Motion ID: ${motion.id}`);
console.log(`Motion Name: ${motion.name}`);
}
var responseJson = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<GraphQLResponse<CreateTextToMotionData>>(responseJson);
if (result.Errors != null && result.Errors.Length > 0)
{
foreach (var error in result.Errors)
{
Console.WriteLine($"Error: {error.Message}");
}
}
else
{
var motion = result.Data.CreateTextToMotion.Motion;
Console.WriteLine($"Motion ID: {motion.Id}");
Console.WriteLine($"Motion Name: {motion.Name}");
}
Step 4: Download your motion
Once you have the motion ID, you can download it in FBX or GLB format. You'll need both the character ID and motion ID.
- Shell
- Python
- TypeScript
- C#
CHARACTER_ID="cXi2eAP19XwQ" # Default character, or use your own
MOTION_ID="m3G3XSJrjEJH"
# 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
# Download as GLB (filename is customizable)
curl -L "https://uthana.com/motion/file/motion_viewer/$CHARACTER_ID/$MOTION_ID/motion.glb" \
-u $API_KEY: \
-o motion.glb
# Download at specific FPS (24, 30, or 60)
curl -L "https://uthana.com/motion/file/motion_viewer/$CHARACTER_ID/$MOTION_ID/motion.fbx?fps=30" \
-u $API_KEY: \
-o motion-30fps.fbx
CHARACTER_ID = "cXi2eAP19XwQ" # Default character, or use your own
MOTION_ID = 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)
# Download as GLB (filename is customizable)
glb_url = f'https://uthana.com/motion/file/motion_viewer/{CHARACTER_ID}/{MOTION_ID}/motion.glb'
response = requests.get(glb_url, auth=(API_KEY, ''))
with open('motion.glb', 'wb') as f:
f.write(response.content)
const CHARACTER_ID = "cXi2eAP19XwQ"; // Default character, or use your own
const MOTION_ID = motion.id;
async function downloadMotion(
characterId: string,
motionId: string,
format: "fbx" | "glb",
fps?: 24 | 30 | 60,
noMesh?: boolean,
filename: string = "motion",
) {
let url = `https://uthana.com/motion/file/motion_viewer/${characterId}/${motionId}/${filename}.${format}`;
const params = new URLSearchParams();
if (fps) params.append("fps", fps.toString());
if (noMesh !== undefined) params.append("no_mesh", noMesh.toString());
if (params.toString()) url += `?${params.toString()}`;
const response = await fetch(url, {
headers: {
Authorization: `Basic ${btoa(`${API_KEY}:`)}`,
},
});
if (!response.ok) {
throw new Error(`Failed to download motion: ${response.statusText}`);
}
const blob = await response.blob();
return blob;
}
// Usage
const fbxBlob = await downloadMotion(CHARACTER_ID, MOTION_ID, "fbx");
const glbBlob = await downloadMotion(CHARACTER_ID, MOTION_ID, "glb");
const motion30fps = await downloadMotion(CHARACTER_ID, MOTION_ID, "fbx", 30);
private const string CHARACTER_ID = "cXi2eAP19XwQ"; // Default character, or use your own
public async Task<byte[]> DownloadMotionAsync(string motionId, string format = "fbx", int? fps = null, bool? noMesh = null, string filename = "motion")
{
var downloadUrl = $"https://uthana.com/motion/file/motion_viewer/{CHARACTER_ID}/{motionId}/{filename}.{format}";
var queryParams = new List<string>();
if (fps.HasValue) queryParams.Add($"fps={fps.Value}");
if (noMesh.HasValue) queryParams.Add($"no_mesh={noMesh.Value.ToString().ToLower()}");
if (queryParams.Any()) downloadUrl += "?" + string.Join("&", queryParams);
var response = await _httpClient.GetAsync(downloadUrl);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync();
}
// Usage
var fbxData = await DownloadMotionAsync(motion.Id, "fbx");
var glbData = await DownloadMotionAsync(motion.Id, "glb");
var motion30fps = await DownloadMotionAsync(motion.Id, "fbx", fps: 30);
Advanced options
Enable foot IK
For better foot placement on uneven surfaces, enable foot IK:
- Shell
- Python
- TypeScript
- C#
curl -X POST https://uthana.com/graphql \
-u $API_KEY: \
-H "Content-Type: application/json" \
-d '{
"query": "mutation { create_text_to_motion(prompt: \"walk casually\", foot_ik: true) { motion { id name } } }"
}'
query = '''
mutation {
create_text_to_motion(prompt: "walk casually", foot_ik: true) {
motion {
id
name
}
}
}
'''
response = requests.post(
API_URL,
auth=(API_KEY, ''),
json={'query': query}
)
const response = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Basic ${btoa(`${API_KEY}:`)}`,
},
body: JSON.stringify({
query: `
mutation {
create_text_to_motion(prompt: "walk casually", foot_ik: true) {
motion {
id
name
}
}
}
`,
}),
});
var query = @"
mutation {
create_text_to_motion(prompt: ""walk casually"", foot_ik: true) {
motion {
id
name
}
}
}";
Try Uthana's BUCMD model
For more control over generation parameters, use the text-to-motion-bucmd model:
- Shell
- Python
- TypeScript
- C#
curl -X POST https://uthana.com/graphql \
-u $API_KEY: \
-H "Content-Type: application/json" \
-d '{
"query": "mutation { create_text_to_motion(prompt: \"confident stride\", model: \"text-to-motion-bucmd\") { motion { id name } } }"
}'
query = '''
mutation {
create_text_to_motion(
prompt: "confident stride"
model: "text-to-motion-bucmd"
) {
motion {
id
name
}
}
}
'''
response = requests.post(
API_URL,
auth=(API_KEY, ''),
json={'query': query}
)
const response = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Basic ${btoa(`${API_KEY}:`)}`,
},
body: JSON.stringify({
query: `
mutation {
create_text_to_motion(
prompt: "confident stride"
model: "text-to-motion-bucmd"
) {
motion {
id
name
}
}
}
`,
}),
});
var query = @"
mutation {
create_text_to_motion(
prompt: ""confident stride""
model: ""text-to-motion-bucmd""
) {
motion {
id
name
}
}
}";
BUCMD advanced options
The text-to-motion-bucmd model supports additional parameters for fine-tuning generation:
foot_ik(Boolean): Enable foot IK for better foot placement on uneven surfaces. Defaults tofalseif not specified.steps(Int): Number of diffusion steps. Range1–150, default50.cfg_scale(Float): Classifier-free guidance scale. Range0–10, default2.length(Float): Desired motion length in seconds. Range0.25–10, default10.motion_length(Int): DEPRECATED. Uselengthinstead. Desired motion length in frames (20 FPS).seed(Int): Random seed for reproducibility. Range1–99999, defaultNone(random).retargeting_ik(Boolean): Enable inverse kinematics (IK) for retargeting from the internal skeleton. Defaults totrueif not specified (it is recommended to keep this enabled for best results).
Example with advanced options:
- Shell
- Python
- TypeScript
- C#
curl -X POST https://uthana.com/graphql \
-u $API_KEY: \
-H "Content-Type: application/json" \
-d '{
"query": "mutation CreateTextToMotion($prompt: String!, $model: String, $steps: Int, $cfg_scale: Float, $length: Float, $seed: Int, $retargeting_ik: Boolean) { create_text_to_motion(prompt: $prompt, model: $model, steps: $steps, cfg_scale: $cfg_scale, length: $length, seed: $seed, retargeting_ik: $retargeting_ik) { motion { id name } } }",
"variables": {
"prompt": "confident stride",
"model": "text-to-motion-bucmd",
"steps": 50,
"cfg_scale": 2.0,
"length": 10,
"seed": 12345,
"retargeting_ik": true
}
}'
query = '''
mutation CreateTextToMotion(
$prompt: String!
$model: String
$steps: Int
$cfg_scale: Float
$length: Float
$seed: Int
$retargeting_ik: Boolean
) {
create_text_to_motion(
prompt: $prompt
model: $model
steps: $steps
cfg_scale: $cfg_scale
length: $length
seed: $seed
retargeting_ik: $retargeting_ik
) {
motion {
id
name
}
}
}
'''
variables = {
"prompt": "confident stride",
"model": "text-to-motion-bucmd",
"steps": 50,
"cfg_scale": 2.0,
"length": 10,
"seed": 12345,
"retargeting_ik": True
}
response = requests.post(
API_URL,
auth=(API_KEY, ''),
json={'query': query, 'variables': variables}
)
const response = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Basic ${btoa(`${API_KEY}:`)}`,
},
body: JSON.stringify({
query: `
mutation CreateTextToMotion(
$prompt: String!
$model: String
$steps: Int
$cfg_scale: Float
$length: Float
$seed: Int
$retargeting_ik: Boolean
) {
create_text_to_motion(
prompt: $prompt
model: $model
steps: $steps
cfg_scale: $cfg_scale
length: $length
seed: $seed
retargeting_ik: $retargeting_ik
) {
motion {
id
name
}
}
}
`,
variables: {
prompt: "confident stride",
model: "text-to-motion-bucmd",
steps: 50,
cfg_scale: 2.0,
length: 10,
seed: 12345,
retargeting_ik: true,
},
}),
});
var query = @"
mutation CreateTextToMotion(
$prompt: String!
$model: String
$steps: Int
$cfg_scale: Float
$length: Float
$seed: Int
$retargeting_ik: Boolean
) {
create_text_to_motion(
prompt: $prompt
model: $model
steps: $steps
cfg_scale: $cfg_scale
length: $length
seed: $seed
retargeting_ik: $retargeting_ik
) {
motion {
id
name
}
}
}";
var request = new
{
query = query,
variables = new
{
prompt = "confident stride",
model = "text-to-motion-bucmd",
steps = 50,
cfg_scale = 2.0,
length = 10,
seed = 12345,
retargeting_ik = true
}
};
Error handling
If something goes wrong, check the errors array in the response:
- Shell
- Python
- TypeScript
- C#
# Check for errors in response
if echo "$RESPONSE" | jq -e '.errors' > /dev/null; then
echo "Error occurred:"
echo "$RESPONSE" | jq '.errors'
fi
result = response.json()
if "errors" in result:
for error in result["errors"]:
print(f"Error: {error['message']}")
if "extensions" in error:
print(f"Code: {error['extensions'].get('code', 'N/A')}")
const json = await response.json();
if (json.errors) {
json.errors.forEach((error: any) => {
console.error(`Error: ${error.message}`);
if (error.extensions) {
console.error(`Code: ${error.extensions.code || "N/A"}`);
}
});
}
if (result.Errors != null && result.Errors.Length > 0)
{
foreach (var error in result.Errors)
{
Console.WriteLine($"Error: {error.Message}");
if (error.Extensions != null && error.Extensions.ContainsKey("code"))
{
Console.WriteLine($"Code: {error.Extensions["code"]}");
}
}
}
Next steps
- Learn about Video to motion for converting video files
- Explore Retargeting to apply motions to custom characters
- Check the API reference for complete schema documentation