curl --request POST \
--url https://apis.viggle.ai/v1/motions \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: multipart/form-data' \
--form motion_video='@example-file' \
--form 'motion_video_url=<string>' \
--form name= \
--form type=render \
--form enable_smoothing=false \
--form target_fps=30 \
--form 'task_id=<string>'import requests
url = "https://apis.viggle.ai/v1/motions"
files = { "motion_video": ("example-file", open("example-file", "rb")) }
payload = {
"motion_video_url": "<string>",
"name": "",
"type": "render",
"enable_smoothing": "false",
"target_fps": "30",
"task_id": "<string>"
}
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, data=payload, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('motion_video', '<string>');
form.append('motion_video_url', '<string>');
form.append('name', '');
form.append('type', 'render');
form.append('enable_smoothing', 'false');
form.append('target_fps', '30');
form.append('task_id', '<string>');
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
options.body = form;
fetch('https://apis.viggle.ai/v1/motions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://apis.viggle.ai/v1/motions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nrender\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"enable_smoothing\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"target_fps\"\r\n\r\n30\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"task_id\"\r\n\r\n<string>\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: multipart/form-data"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://apis.viggle.ai/v1/motions"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nrender\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"enable_smoothing\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"target_fps\"\r\n\r\n30\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"task_id\"\r\n\r\n<string>\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://apis.viggle.ai/v1/motions")
.header("Authorization", "Bearer <token>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nrender\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"enable_smoothing\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"target_fps\"\r\n\r\n30\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"task_id\"\r\n\r\n<string>\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://apis.viggle.ai/v1/motions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nrender\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"enable_smoothing\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"target_fps\"\r\n\r\n30\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"task_id\"\r\n\r\n<string>\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"status": "queued",
"name": "<string>",
"progress": 50,
"capabilities": [
"<string>"
],
"created_at": "<string>",
"completed_at": "<string>",
"error": {
"code": "<string>",
"message": "<string>",
"request_id": "<string>"
},
"type": "render",
"vsplat": {
"status": "queued",
"error": {
"code": "<string>",
"message": "<string>",
"request_id": "<string>"
}
},
"glb": {
"status": "queued",
"skeletons": [
"mixamo"
],
"error": {
"code": "<string>",
"message": "<string>",
"request_id": "<string>"
}
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}从视频创建动作
从驱动视频创建可复用动作。
curl --request POST \
--url https://apis.viggle.ai/v1/motions \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: multipart/form-data' \
--form motion_video='@example-file' \
--form 'motion_video_url=<string>' \
--form name= \
--form type=render \
--form enable_smoothing=false \
--form target_fps=30 \
--form 'task_id=<string>'import requests
url = "https://apis.viggle.ai/v1/motions"
files = { "motion_video": ("example-file", open("example-file", "rb")) }
payload = {
"motion_video_url": "<string>",
"name": "",
"type": "render",
"enable_smoothing": "false",
"target_fps": "30",
"task_id": "<string>"
}
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, data=payload, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('motion_video', '<string>');
form.append('motion_video_url', '<string>');
form.append('name', '');
form.append('type', 'render');
form.append('enable_smoothing', 'false');
form.append('target_fps', '30');
form.append('task_id', '<string>');
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
options.body = form;
fetch('https://apis.viggle.ai/v1/motions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://apis.viggle.ai/v1/motions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nrender\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"enable_smoothing\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"target_fps\"\r\n\r\n30\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"task_id\"\r\n\r\n<string>\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: multipart/form-data"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://apis.viggle.ai/v1/motions"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nrender\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"enable_smoothing\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"target_fps\"\r\n\r\n30\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"task_id\"\r\n\r\n<string>\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://apis.viggle.ai/v1/motions")
.header("Authorization", "Bearer <token>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nrender\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"enable_smoothing\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"target_fps\"\r\n\r\n30\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"task_id\"\r\n\r\n<string>\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://apis.viggle.ai/v1/motions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nrender\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"enable_smoothing\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"target_fps\"\r\n\r\n30\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"task_id\"\r\n\r\n<string>\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"status": "queued",
"name": "<string>",
"progress": 50,
"capabilities": [
"<string>"
],
"created_at": "<string>",
"completed_at": "<string>",
"error": {
"code": "<string>",
"message": "<string>",
"request_id": "<string>"
},
"type": "render",
"vsplat": {
"status": "queued",
"error": {
"code": "<string>",
"message": "<string>",
"request_id": "<string>"
}
},
"glb": {
"status": "queued",
"skeletons": [
"mixamo"
],
"error": {
"code": "<string>",
"message": "<string>",
"request_id": "<string>"
}
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}multipart/form-data 上传驱动视频或提供 URL。type 决定输出:render(默认)创建可用于渲染的 2D 动作,glb 提取 3D 骨骼动画,all 同时创建两者并分别计费。
本模式与从文本创建动作共用 POST /v1/motions,但请求内容类型和字段不同。
type=render 不单独收取预处理费。type=glb 按源视频时长向上取整后收取 5 积分/秒。type=all 合并两者费用。详见计费与保留期限。请求参数
使用multipart/form-data。
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
motion_video | file | 二选一 | — | 直接上传的驱动视频,提取其中可见的身体动作用于复用。与 motion_video_url 二选一。 |
motion_video_url | string | 二选一 | — | 可公开访问的驱动视频 HTTP(S) URL,替代文件上传,并在开始接收素材时保持可访问。 |
name | string | 否 | "" | render/all 的可选名称。仅 glb 的动作当前返回空名称。 |
type | string | 否 | render | render 创建 2D 动作(不收费);glb 提取 3D 动画(源视频时长向上取整后按 5 积分/秒);all 同时创建并合并费用。 |
motion_video 和 motion_video_url 必须二选一。服务会从源视频内部生成并保存可复用动作的缩略图。
以下参数仅适用于 type=glb 或 all。
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
enable_smoothing | boolean | 否 | false | 是否平滑提取的关节动作,减少帧间抖动,也可能略微弱化突然的动作。 |
target_fps | number | 否 | 30 | 提取和 GLB 时间轴的目标帧率,必须大于 0。省略时使用 30 FPS。 |
task_id | string | 否 | — | type=glb 的调用者自定义幂等 ID。同一任务重试使用稳定且唯一的值;type=all 使用新建动作 ID 作为配套提取任务的键。 |
响应参数
返回200 OK 和 Motion 对象。
| 字段 | 类型 | 始终返回 | 说明 |
|---|---|---|---|
id | string | 是 | 以 mot_ 开头的公开 ID,请原样保存。 |
status | string | 是 | 通常为 queued,终态为 ready/failed。 |
name | string | 是 | 传入的名称,或空字符串。 |
progress | integer 或 null | 是 | 可用时为 0–100 的进度。 |
capabilities | string[] | 是 | 就绪前为空;就绪且具备渲染能力时包含 video_render。 |
created_at / completed_at | string 或 null | 是 | ISO 8601 时间。 |
error | object 或 null | 是 | 失败时的结构化错误详情。 |
type | string | 是 | 请求中的 render、glb 或 all。 |
glb | object 或 null | 是 | 仅 render 时为 null;否则为 {status, skeletons, error}。就绪前 skeletons 为空,就绪后为 [mixamo, metahuman],两种版本总是一起生成。 |
vsplat | null | 是 | 动作始终为 null。 |
{
"id": "mot_550e8400-e29b-41d4-a716-446655440000",
"status": "queued",
"name": "Dance loop",
"progress": 0,
"capabilities": [],
"created_at": "2026-07-21T10:00:00+00:00",
"completed_at": null,
"error": null,
"type": "render",
"vsplat": null,
"glb": null
}
示例
file, _ := os.Open("dance.mp4"); defer file.Close()
body := &bytes.Buffer{}; writer := multipart.NewWriter(body)
part, _ := writer.CreateFormFile("motion_video", "dance.mp4"); io.Copy(part, file)
writer.WriteField("name", "Dance loop"); writer.Close()
req, _ := http.NewRequest("POST", "https://apis.viggle.ai/v1/motions", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("VIGGLE_API_KEY")); req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := http.DefaultClient.Do(req); if err != nil { panic(err) }; defer resp.Body.Close()
const form = new FormData(); form.append("motion_video", videoFile); form.append("name", "Dance loop");
const response = await fetch("https://apis.viggle.ai/v1/motions", {method:"POST", headers:{Authorization:`Bearer ${process.env.VIGGLE_API_KEY}`}, body:form});
const motion = await response.json();
import os, requests
with open("dance.mp4", "rb") as video:
response = requests.post("https://apis.viggle.ai/v1/motions", headers={"Authorization": f"Bearer {os.environ['VIGGLE_API_KEY']}"}, files={"motion_video": video}, data={"name": "Dance loop"})
response.raise_for_status(); motion = response.json()
curl -X POST "https://apis.viggle.ai/v1/motions" -H "Authorization: Bearer $VIGGLE_API_KEY" -F "[email protected]" -F "name=Dance loop"
type=glb;若同时保留渲染素材,使用 all:
curl -X POST "https://apis.viggle.ai/v1/motions" \
-H "Authorization: Bearer $VIGGLE_API_KEY" \
-F "[email protected]" \
-F "type=all" \
-F "enable_smoothing=true" \
-F "target_fps=30"
下一步
通过获取动作轮询,待glb.status=ready 后调用导出 3D 动作。
从文本创建动作
授权
服务端 SDK 使用项目 API 密钥,Remote MCP 使用 OAuth 访问令牌。不要在浏览器代码中暴露项目密钥。
请求头
可选的调用者自定义关联 ID,最长 128 字符。服务会在响应头返回实际使用的值,便于追踪和支持排查。
1 - 128可选来源标签,最长 128 字符,用于识别 SDK、集成、产品入口或内部工作流。
1 - 128请求体
motion_video 与 motion_video_url 必须二选一。type 决定输出,提取参数仅适用于 glb/all。暂存路径、输出位置、提取模板、角色 PKL、关节配置和追踪蒙版由服务管理。
直接上传的驱动视频,提取其中可见的身体动作用于复用。与 motion_video_url 二选一。
可公开访问的驱动视频 HTTP(S) URL,替代文件上传,并在开始接收素材时保持可访问。
render/all 的可选名称。仅 glb 的动作当前返回空名称。
render 创建 2D 动作(不收费);glb 提取 3D 动画(源视频时长向上取整后按 5 积分/秒);all 同时创建并合并费用。
render, glb, all 是否平滑提取的关节动作,减少帧间抖动,也可能略微弱化突然的动作。
提取和 GLB 时间轴的目标帧率,必须大于 0。省略时使用 30 FPS。
x > 0type=glb 的调用者自定义幂等 ID。同一任务重试使用稳定且唯一的值;type=all 使用新建动作 ID 作为配套提取任务的键。
1响应
请求成功。
角色和动作共用的资源结构。列表中的 progress 为 null,顶层 error 当前始终为 null,失败请检查 status。角色 type 为 render、vsplat、all,动作为 render、glb、all,旧资源默认报告 render。vsplat/glb 仅包含提取状态,下载链接通过角色或动作的 /export 获取。未请求提取时对应字段为 null。all 需等 2D 与 3D 均就绪才为 ready;任一失败则顶层 failed,子对象状态可用于定位。
素材公开 ID。角色通常以 char_ 开头,动作通常以 mot_ 开头。
1素材整体生命周期。所需能力就绪后才能用于渲染或导出。
queued, processing, ready, failed, cancelled 创建时提供或导入时生成的显示名称;不支持名称的流程可能返回空字符串。
详情中尽可能提供 0–100 的处理百分比,列表中或不可用时为 null。
0 <= x <= 100素材具备的能力。就绪且可渲染时包含 video_render。
带 UTC 偏移的 ISO 8601 时间,例如 2026-07-31T09:15:22+00:00。
所有请求处理进入终态时的 ISO 8601 时间,带 UTC 偏移;处理中为 null。
保留的顶层错误详情,目前始终为 null。通过 status 判断失败,并在适用时检查提取子对象。
Show child attributes
Show child attributes
创建时请求的输出。render 为可渲染的 2D 素材,vsplat/glb 为 3D 输出,all 同时请求两种支持的输出。
render, vsplat, glb, all 角色 vsplat 提取状态。动作及 type=render 的角色为 null。这里只返回状态,不回显 model_precision 等创建参数。
Show child attributes
Show child attributes
动作 3D 动画提取或生成状态。角色及 type=render 的动作为 null。
Show child attributes
Show child attributes

