Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,13 @@ jobs:
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4 # version comes from package.json#packageManager

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

# The enterprise Actions policy only permits GitHub-owned Marketplace
# actions. Corepack reads the pinned pnpm version from package.json.
- run: corepack enable

# --frozen-lockfile also fails on lockfile drift, which is a review gate
# in its own right.
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ jobs:
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4 # version comes from package.json#packageManager

- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
cache: pnpm

# Keep release compatible with the enterprise Actions allow-list while
# still using the pnpm version pinned in package.json.
- run: corepack enable

- run: pnpm install --frozen-lockfile
- run: pnpm run typecheck
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# meshy-cli

A small, well-structured command-line interface for the [Meshy AI API](https://docs.meshy.ai/) — text-to-3D, image-to-3D (standard and smart-topology), remesh, convert, resize, rigging, animation, retexture, 2D image generation, multi-color print output, and the `balance` endpoint. Built for humans and AI agents.
A small, well-structured command-line interface for the [Meshy AI API](https://docs.meshy.ai/) — text-to-3D, image-to-3D (standard and smart-topology), text-to-motion, remesh, convert, resize, rigging, animation, retexture, 2D image generation, multi-color print output, and the `balance` endpoint. Built for humans and AI agents.

Two layers. `meshy make` chains the documented flows so that one command
produces one model. Underneath, a per-endpoint command for every resource
Expand Down Expand Up @@ -126,6 +126,10 @@ meshy-cli text-to-3d create --mode refine --preview-task-id <id>
# image → 3D
meshy-cli image-to-3d create --image-url https://example.com/cat.png

# text → standalone motion clip (Prime/FBX by default; Swift produces BVH)
meshy-cli text-to-motion create --prompt "a character waving" --duration 3
meshy-cli text-to-motion create --prompt "a quick dodge to the left" --duration 2.5 --mode swift

# smart topology: component-aware low-poly with a native polycount
meshy-cli image-to-3d create --image-url cat.png --model-type smart-topology --target-polycount 10000

Expand Down Expand Up @@ -164,6 +168,7 @@ each one in full.
| `resize` | `/resize` | [docs](https://docs.meshy.ai/en/api/resize) |
| `rigging` | `/rigging` | [docs](https://docs.meshy.ai/en/api/rigging) |
| `animate` | `/animations` | [docs](https://docs.meshy.ai/en/api/animations) |
| `text-to-motion` | `/text-to-motion` | [docs](https://docs.meshy.ai/en/api/text-to-motion) |
| `retexture` | `/retexture` | [docs](https://docs.meshy.ai/en/api/retexture) |
| `text-to-image` | `/text-to-image` | [docs](https://docs.meshy.ai/en/api/text-to-image) |
| `image-to-image` | `/image-to-image` | [docs](https://docs.meshy.ai/en/api/image-to-image) |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "meshy-cli",
"version": "0.1.2",
"description": "Official command-line interface for the Meshy AI API — text-to-3D, image-to-3D, remesh, rigging, animate, retexture, 2D images, multi-color print, balance.",
"description": "Official command-line interface for the Meshy AI API — text-to-3D, image-to-3D, text-to-motion, remesh, rigging, animate, retexture, 2D images, multi-color print, balance.",
"license": "MIT",
"type": "module",
"packageManager": "pnpm@11.18.0",
Expand Down
7 changes: 5 additions & 2 deletions skills/meshy-cli/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: meshy-cli
description: "Generate 3D models and 2D images with the Meshy API through the meshy-cli command — text-to-3D, image-to-3D, remesh, rigging, animation, retexture, printability. Use for any Meshy asset request."
description: "Generate 3D models, motion clips, and 2D images with the Meshy API through the meshy-cli command — text-to-3D, image-to-3D, text-to-motion, remesh, rigging, animation, retexture, printability. Use for any Meshy asset request."
license: MIT
compatibility: Requires meshy-cli on PATH and a stored credential or MESHY_API_KEY; network access to api.meshy.ai
metadata:
Expand Down Expand Up @@ -32,7 +32,7 @@ than starting over, or the finished step is paid for twice.

## Everything else

`meshy resources` indexes the 16 endpoint commands; each carries the same verbs:
`meshy resources` indexes the 17 endpoint commands; each carries the same verbs:

```bash
meshy <resource> create [flags] [--data '<json>'] [--async] [--timeout <s>]
Expand Down Expand Up @@ -60,6 +60,9 @@ These are API rules, not preferences — ignoring them produces failed tasks:
its result before calling `animate`.
- **`animate` takes a rigging task id**, not a model task id, plus an integer
`--action-id`.
- **`text-to-motion` produces a standalone skeletal clip**, not an animated
character. Pass a 2–10 second duration in 0.5-second increments. Prime is the
default (FBX, 10 credits); Swift returns BVH (3 credits).
- **`repair-printability` drops textures and invalidates UVs.** Run it before
texturing, or re-`retexture` afterwards.
- **`multi-image-to-3d` is beta**; use `image-to-3d` unless multi-view input was
Expand Down
8 changes: 8 additions & 0 deletions src/client/endpoints/text-to-motion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { TaskEndpoint, type HttpFetch } from "./base.js";

/** POST/GET/LIST/DELETE /openapi/v1/text-to-motion. */
export class TextToMotionEndpoint extends TaskEndpoint {
constructor(http: HttpFetch) {
super(http, "/text-to-motion");
}
}
5 changes: 5 additions & 0 deletions src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { RiggingEndpoint } from "./endpoints/rigging.js";
import { TaskEndpoint, type HttpFetch } from "./endpoints/base.js";
import { TextTo3DEndpoint } from "./endpoints/text-to-3d.js";
import { TextToImageEndpoint } from "./endpoints/text-to-image.js";
import { TextToMotionEndpoint } from "./endpoints/text-to-motion.js";
import { mapHttpError, MeshyApiError } from "./errors.js";
import type { MeshyConfig } from "../internal/config.js";
import { logger } from "../internal/logger.js";
Expand Down Expand Up @@ -87,6 +88,7 @@ export class MeshyClient {
readonly animate: AnimateEndpoint;
readonly retexture: RetextureEndpoint;
readonly textToImage: TextToImageEndpoint;
readonly textToMotion: TextToMotionEndpoint;
readonly imageToImage: ImageToImageEndpoint;
readonly multiColorPrint: MultiColorPrintEndpoint;
readonly analyzePrintability: AnalyzePrintabilityEndpoint;
Expand All @@ -111,6 +113,7 @@ export class MeshyClient {
this.animate = new AnimateEndpoint(this.v1Fetch);
this.retexture = new RetextureEndpoint(this.v1Fetch);
this.textToImage = new TextToImageEndpoint(this.v1Fetch);
this.textToMotion = new TextToMotionEndpoint(this.v1Fetch);
this.imageToImage = new ImageToImageEndpoint(this.v1Fetch);
this.multiColorPrint = new MultiColorPrintEndpoint(this.v1Fetch);
this.analyzePrintability = new AnalyzePrintabilityEndpoint(this.v1Fetch);
Expand All @@ -129,6 +132,7 @@ export class MeshyClient {
case "animate": return this.animate;
case "retexture": return this.retexture;
case "text-to-image": return this.textToImage;
case "text-to-motion": return this.textToMotion;
case "image-to-image": return this.imageToImage;
case "multi-color-print": return this.multiColorPrint;
case "analyze-printability": return this.analyzePrintability;
Expand Down Expand Up @@ -159,6 +163,7 @@ export const RESOURCE_NAMES = [
"animate",
"retexture",
"text-to-image",
"text-to-motion",
"image-to-image",
"multi-color-print",
"analyze-printability",
Expand Down
1 change: 1 addition & 0 deletions src/cmd/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const RESOURCES: ResourceEntry[] = [
{ name: "animate", summary: "apply an animation clip to a rigged mesh" },
{ name: "retexture", summary: "regenerate textures" },
{ name: "text-to-image", summary: "2D image generation" },
{ name: "text-to-motion", summary: "generate a standalone skeletal motion clip from text" },
{ name: "image-to-image", summary: "2D image editing" },
{ name: "multi-color-print", summary: "color-separated 3D print output" },
{ name: "analyze-printability", summary: "inspect a model for 3D-printing issues (free)" },
Expand Down
61 changes: 61 additions & 0 deletions src/cmd/text-to-motion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* text-to-motion — generate a standalone skeletal motion clip from a prompt.
*/

import { Option } from "commander";
import { UsageError } from "../internal/errors.js";
import { parseNumber } from "../internal/flags.js";
import { buildResourceCommand, type ResourceCommandSpec } from "../internal/task-command.js";

const MIN_DURATION_SECONDS = 2;
const MAX_DURATION_SECONDS = 10;
const DURATION_STEP_SECONDS = 0.5;

const spec: ResourceCommandSpec = {
name: "text-to-motion",
description:
"Generate a standalone skeletal motion clip from a text prompt. Prime returns FBX; " +
"Swift returns BVH",
create: {
description: "Create a text-to-motion task",
configure(cmd) {
return cmd
.option("--prompt <text>", "required — describe the character motion; max 400 chars")
.addOption(
new Option("--mode <mode>", "generation model (default: prime)")
.choices(["prime", "swift"]),
)
.option(
"--duration <seconds>",
"required — clip length from 2 to 10 seconds in 0.5-second increments",
parseNumber,
);
},
toPayload(opts) {
if (!opts.prompt && !opts.data) {
throw new UsageError("--prompt is required");
}
if (opts.duration == null && !opts.data) {
throw new UsageError("--duration is required");
}
if (opts.duration != null) {
const duration = opts.duration as number;
const onStep = Number.isInteger(duration / DURATION_STEP_SECONDS);
if (duration < MIN_DURATION_SECONDS || duration > MAX_DURATION_SECONDS || !onStep) {
throw new UsageError("--duration must be from 2 to 10 seconds in 0.5-second increments");
}
}
return {
prompt: opts.prompt,
mode: opts.mode,
duration: opts.duration,
};
},
toDefaults() {
return { mode: "prime" };
},
},
endpointOf: (c) => c.textToMotion,
};

export const textToMotionCommand = buildResourceCommand(spec);
6 changes: 5 additions & 1 deletion src/internal/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,14 @@ export function enumerateArtifacts(task: Task): Artifact[] {

// Rigging / animate-style endpoints nest outputs under `result`.
if (task.result && typeof task.result === "object") {
const motionFormat = task.result["motion_format"];
for (const [key, value] of Object.entries(task.result)) {
if (typeof value !== "string" || !/^https?:/.test(value)) continue;
const extMatch = key.match(/_(fbx|glb|usdz|obj|png|jpg|jpeg|webp)_url$/i);
const preferredExt = extMatch ? extMatch[1]!.toLowerCase() : "";
let preferredExt = extMatch ? extMatch[1]!.toLowerCase() : "";
if (key === "motion_url" && (motionFormat === "fbx" || motionFormat === "bvh")) {
preferredExt = motionFormat;
}
out.push({ key, url: value, preferredExt });
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { retextureCommand } from "./cmd/retexture.js";
import { riggingCommand } from "./cmd/rigging.js";
import { textTo3dCommand } from "./cmd/text-to-3d.js";
import { textToImageCommand } from "./cmd/text-to-image.js";
import { textToMotionCommand } from "./cmd/text-to-motion.js";

const ROOT_LONG = `meshy-cli — command-line interface for the Meshy AI API.

Expand All @@ -52,7 +53,7 @@ EXAMPLES:
meshy make "a red sports car" --max-credits 25 # refuse if the estimate is over

# every endpoint, one at a time
meshy resources # index of the 16 commands
meshy resources # index of the 17 commands
meshy text-to-3d create --mode preview --prompt "a red sports car"
meshy image-to-3d get <task-id>
meshy delete <task-id> # unified: works for any task
Expand All @@ -75,7 +76,7 @@ AUTHENTICATION:
separate credentials.dev.json, so staging cannot clobber a production login.

RESOURCE COMMANDS:
All 16 endpoint commands stay available and stay supported — they are
All 17 endpoint commands stay available and stay supported — they are
indexed by \`meshy resources\` instead of listed here, so this help does not
grow with the API. \`meshy resources --help\` also carries the shared
create/get/list/wait/delete verb contract.
Expand Down Expand Up @@ -138,6 +139,7 @@ export function buildRootCommand(): Command {
animateCommand,
retextureCommand,
textToImageCommand,
textToMotionCommand,
imageToImageCommand,
multiColorPrintCommand,
analyzePrintabilityCommand,
Expand Down
21 changes: 21 additions & 0 deletions tests/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,27 @@ test("MeshyClient.imageTo3d.create — POSTs to v1 with JSON body", async () =>
}
});

test("MeshyClient.textToMotion — uses the v1 text-to-motion family", async () => {
const { calls, restore } = installFetch(() => jsonResponse({ result: "motion-task-1" }));
try {
const client = new MeshyClient(buildConfig());
const id = await client.textToMotion.create({
prompt: "a character waving",
mode: "prime",
duration: 3,
});
assert.equal(id, "motion-task-1");
assert.equal(calls[0]!.url, "https://api.example.com/v1/text-to-motion");
assert.deepEqual(JSON.parse(calls[0]!.body!), {
prompt: "a character waving",
mode: "prime",
duration: 3,
});
} finally {
restore();
}
});

test("MeshyClient — retrieve percent-encodes task ids with unusual characters", async () => {
const { calls, restore } = installFetch(() =>
jsonResponse({ id: "abc/slash", status: "SUCCEEDED", type: "image-to-3d", progress: 100 }),
Expand Down
18 changes: 18 additions & 0 deletions tests/download.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,24 @@ test("enumerateArtifacts — animate-style result.*_url", () => {
assert.deepEqual(keys, ["animation_fbx_url", "animation_glb_url"]);
});

test("enumerateArtifacts — text-to-motion uses motion_format for the extension", () => {
const arts = enumerateArtifacts(
task({
result: {
motion_url: "https://example.com/motion-clip",
motion_format: "bvh",
duration_ms: 3000,
mode: "swift",
},
}),
);
assert.deepEqual(arts, [{
key: "motion_url",
url: "https://example.com/motion-clip",
preferredExt: "bvh",
}]);
});

test("downloadArtifacts — single 2D file, content-type matches user ext, no conversion", async () => {
const url = "https://cdn.example.com/img_0";
const png = await tinyPngBytes();
Expand Down
15 changes: 15 additions & 0 deletions tests/surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,21 @@ test("rigging carries no texture flag and documents its bundled animations", ()
assert.match(sub(root, "rigging").description(), /walking and running/i);
});

test("text-to-motion exposes the v1 motion contract", () => {
const cmd = sub(root, "text-to-motion");
for (const verb of ["create", "get", "wait", "delete", "list"]) {
sub(cmd, verb);
}
const create = sub(cmd, "create");
const flags = createFlags("text-to-motion");
for (const flag of ["--prompt", "--mode", "--duration"]) {
assert.ok(flags.has(flag), `text-to-motion is missing ${flag}`);
}
const mode = create.options.find((o) => o.long === "--mode");
assert.deepEqual(mode?.argChoices, ["prime", "swift"]);
assert.match(mode?.description ?? "", /default: prime/);
});

test("descriptions and flag help stay lean: no credit prices in the command surface", () => {
// Pricing lives in the skills (linked to https://docs.meshy.ai/en/api/pricing),
// not in --help output that rides along in every agent context. "credit
Expand Down
Loading