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.
Learn more about Text to motion.
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: Set up the client
Install your client library and authenticate using your API key. See the quickstart for setup instructions for each language.
Step 2: Generate motion from text
Create a text-to-motion animation by sending a mutation with your text prompt.
- Shell
- Python
- TypeScript
- React
- 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" }
}'
async def main():
# Basic usage — default model, default character (Tar)
result = await client.ttm.create("a person walking down the street")
print(result.character_id, result.motion_id)
# Specify a character
result2 = await client.ttm.create(
"a person dancing",
character_id=UthanaCharacters.ava,
)
asyncio.run(main())
// Basic usage — default model, default character (Tar)
const result = await client.ttm.create("a person walking down the street");
console.log(result.character_id, result.motion_id);
// Specify a character
const result2 = await client.ttm.create("a person dancing", {
character_id: UthanaCharacters.ava,
});
import { useUthanaTtm } from "@uthana/react";
function GenerateMotion() {
const ttm = useUthanaTtm();
return (
<button
onClick={() => ttm.mutate({ prompt: "a person walking down the street" })}
disabled={ttm.isPending}
>
{ttm.isPending ? "Generating..." : "Generate"}
</button>
);
}
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
- React
- C#
{
"data": {
"create_text_to_motion": {
"motion": {
"id": "m3G3XSJrjEJH",
"name": "a person walking down the street"
}
}
}
}
async def main():
result = await client.ttm.create("a person walking down the street")
print(f"Motion ID: {result.motion_id}")
print(f"Character ID: {result.character_id}")
asyncio.run(main())
// result.motion_id and result.character_id are available immediately
console.log(`Motion ID: ${result.motion_id}`);
console.log(`Character ID: ${result.character_id}`);
import { useUthanaTtm } from "@uthana/react";
function GenerateAndShow() {
const ttm = useUthanaTtm();
return (
<div>
<button onClick={() => ttm.mutate({ prompt: "a person waving" })}>Generate</button>
{ttm.data && (
<p>
Motion: {ttm.data.motion_id} · Character: {ttm.data.character_id}
</p>
)}
{ttm.isError && <p>Error: {String(ttm.error)}</p>}
</div>
);
}
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, download it in FBX or GLB format.
- Shell
- Python
- TypeScript
- React
- 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/fbx/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/glb/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/fbx/motion.fbx?fps=30" \
-u $API_KEY: \
-o motion-30fps.fbx
async def main():
result = await client.ttm.create("a person walking down the street")
# Download as GLB at 30 FPS
glb_data = await client.motions.download(
result.character_id,
result.motion_id,
output_format="glb",
fps=30,
)
with open("motion.glb", "wb") as f:
f.write(glb_data)
# Download as FBX
fbx_data = await client.motions.download(
result.character_id,
result.motion_id,
output_format="fbx",
)
with open("motion.fbx", "wb") as f:
f.write(fbx_data)
asyncio.run(main())
// Download as GLB at 30 FPS
const buffer = await client.motions.download(result.character_id, result.motion_id, {
output_format: "glb",
fps: 30,
});
// Node.js: write to disk
import { writeFileSync } from "fs";
writeFileSync("motion.glb", Buffer.from(buffer));
// Browser: create a download link
const url = URL.createObjectURL(new Blob([buffer], { type: "model/gltf-binary" }));
import { useUthanaClient } from "@uthana/react";
function DownloadButton({ characterId, motionId }: { characterId: string; motionId: string }) {
const client = useUthanaClient();
async function download(format: "glb" | "fbx") {
const buffer = await client.motions.download(characterId, motionId, {
output_format: format,
fps: 30,
});
const mime = format === "glb" ? "model/gltf-binary" : "application/octet-stream";
const url = URL.createObjectURL(new Blob([buffer], { type: mime }));
const a = document.createElement("a");
a.href = url;
a.download = `motion.${format}`;
a.click();
}
return (
<div>
<button onClick={() => download("glb")}>Download GLB</button>
<button onClick={() => download("fbx")}>Download FBX</button>
</div>
);
}
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}/{format}/{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 — replace with motion ID from Step 3
var motionId = "<motion-id-from-step-3>";
var fbxData = await DownloadMotionAsync(motionId, "fbx");
var glbData = await DownloadMotionAsync(motionId, "glb");
var motion30fps = await DownloadMotionAsync(motionId, "fbx", fps: 30);
Advanced options
Enable foot IK
For better foot placement on uneven surfaces, enable foot IK:
- Shell
- Python
- TypeScript
- React
- 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 } } }"
}'
async def main():
result = await client.ttm.create("walk casually", foot_ik=True)
asyncio.run(main())
const result = await client.ttm.create("walk casually", { foot_ik: true });
ttm.mutate({ prompt: "walk casually", foot_ik: true });
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 diffusion-v2 model (also available: vqvae-v1, flow-matching-v1):
- Shell
- Python
- TypeScript
- React
- 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 } } }"
}'
async def main():
result = await client.ttm.create("confident stride", model="diffusion-v2")
asyncio.run(main())
const result = await client.ttm.create("confident stride", { model: "diffusion-v2" });
ttm.mutate({ prompt: "confident stride", model: "diffusion-v2" });
var query = @"
mutation {
create_text_to_motion(
prompt: ""confident stride""
model: ""text-to-motion-bucmd""
) {
motion {
id
name
}
}
}";
BUCMD advanced options
The diffusion-v2 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.seed(Int): Random seed for reproducibility. Range1–99999, defaultNone(random).internal_ik(Boolean): Enable inverse kinematics for retargeting from the internal skeleton. Defaults totrue(recommended).
Example with advanced options:
- Shell
- Python
- TypeScript
- React
- 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
}
}'
async def main():
result = await client.ttm.create(
"confident stride",
model="diffusion-v2",
character_id=UthanaCharacters.manny,
length=5.0,
cfg_scale=2.5,
seed=12345,
)
asyncio.run(main())
const result = await client.ttm.create("confident stride", {
model: "diffusion-v2",
character_id: UthanaCharacters.manny,
length: 5.0,
cfg_scale: 2.5,
seed: 12345,
foot_ik: true,
steps: 50,
internal_ik: true,
});
ttm.mutate({
prompt: "confident stride",
model: "diffusion-v2",
length: 5.0,
cfg_scale: 2.5,
seed: 12345,
});
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
- Shell
- Python
- TypeScript
- React
- C#
# Check for errors in response
if echo "$RESPONSE" | jq -e '.errors' > /dev/null; then
echo "Error occurred:"
echo "$RESPONSE" | jq '.errors'
fi
from uthana import UthanaError
async def main():
try:
result = await client.ttm.create("a person walking")
except UthanaError as e:
print(e.status_code, e.api_message)
asyncio.run(main())
import { UthanaError } from "@uthana/client";
try {
const result = await client.ttm.create("a person walking");
} catch (err) {
if (err instanceof UthanaError) {
console.error(err.statusCode, err.apiMessage);
}
}
const ttm = useUthanaTtm();
// ttm.isError is true when the mutation fails
if (ttm.isError) {
console.error(ttm.error);
}
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 Locomotion for predictable, controllable, looptable travel in a given direction
- 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