Skip to content
Open
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
25 changes: 25 additions & 0 deletions .claude/agents/image-blast-3d.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
name: image-blast-3d
description: Runs one Image Blast 3D object generation in the background. Use for non-blocking 3D generation when the prompt names exactly one world/object pair or one image plus object description.
argument-hint: [world-name] [object-id/name or image path + object description] [--image-edit-prompt prompt] [--provider meshy|hunyuan|tripo] [--regenerate] [--regenerate-reference] [--target-polycount N] [--face-count N] [--generate-type Normal|LowPoly|Geometry] [--polygon-type triangle|quadrilateral] [--enable-pbr true|false]
tools: Read, Write, Glob, Bash
model: inherit
background: true
Expand All @@ -12,6 +13,30 @@ Run exactly one 3D object generation.

Use the preloaded `image-blast-3d` skill as the task contract. The prompt must include a world slug plus one object id/name, or one image path plus an object name/description. Honor optional provider arguments when present.

Pass `--provider tripo` for the Tripo3D engine. Tripo defaults are:

```json
{
"texture": "standard",
"pbr": true,
"face_limit": 30000,
"quad": false,
"auto_size": true,
"texture_alignment": "original_image",
"orientation": "default"
}
```

For Tripo-specific requests, pass the matching options:

- `--texture no|standard|HD` (default `standard`)
- `--pbr true|false` (default `true`)
- `--face-limit <integer>` (default `30000`)
- `--quad true|false` (default `false`)
- `--auto-size true|false` (default `true`)
- `--texture-alignment original_image|geometry`
- `--orientation default|align_image`

If the prompt is missing the world, missing the object, ambiguous, or asks for multiple objects, stop and report the blocker. Do not batch objects in this agent.

Run the generation to completion and report the object id, output directory, generated model files, and any failed/resumable request metadata.
54 changes: 49 additions & 5 deletions .claude/scripts/asset-pipeline/generate-single-asset.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ import {
MESHY_3D_PROVIDER,
runMeshy3D
} from "./meshy-3d.mjs";
import {
DEFAULT_TRIPO_FACE_LIMIT,
DEFAULT_TRIPO_PBR,
DEFAULT_TRIPO_QUAD,
DEFAULT_TRIPO_TEXTURE,
TRIPO_3D_PROVIDER,
runTripo3D
} from "./tripo-3d.mjs";
import { runImageEdit } from "./image-edit.mjs";
import {
downloadRemoteFiles,
Expand Down Expand Up @@ -58,7 +66,12 @@ const MODEL_PROVIDER_ALIASES = new Map([
["hunyuan-3d", HUNYUAN_3D_PROVIDER],
["hunyuan3d-v3", HUNYUAN_3D_PROVIDER],
["fal-ai/hunyuan3d-v3/image-to-3d", HUNYUAN_3D_PROVIDER],
["fal-ai/hunyuan-3d/v3.1/pro/image-to-3d", HUNYUAN_3D_PROVIDER]
["fal-ai/hunyuan-3d/v3.1/pro/image-to-3d", HUNYUAN_3D_PROVIDER],
["tripo", TRIPO_3D_PROVIDER],
["tripo3d", TRIPO_3D_PROVIDER],
["tripo3d-v2.5", TRIPO_3D_PROVIDER],
["tripo3d/tripo/v2.5/image-to-3d", TRIPO_3D_PROVIDER],
["fal-ai/tripo3d/tripo/v2.5/image-to-3d", TRIPO_3D_PROVIDER]
]);

async function readJsonIfExists(filePath) {
Expand Down Expand Up @@ -119,7 +132,7 @@ function resolve3DProvider(value = DEFAULT_3D_PROVIDER) {
const normalized = String(value || DEFAULT_3D_PROVIDER).trim().toLowerCase();
const provider = MODEL_PROVIDER_ALIASES.get(normalized);
if (!provider) {
throw new Error(`Unsupported 3D provider "${value}". Use one of: meshy, hunyuan.`);
throw new Error(`Unsupported 3D provider "${value}". Use one of: meshy, hunyuan, tripo.`);
}
return provider;
}
Expand All @@ -128,6 +141,7 @@ function modelRequestPrefix(request, fallbackProvider) {
if (request?.data?.provider_slug) return request.data.provider_slug;
if (request?.data?.endpoint?.includes("hunyuan")) return HUNYUAN_3D_PROVIDER;
if (request?.data?.endpoint?.includes("meshy")) return MESHY_3D_PROVIDER;
if (request?.data?.endpoint?.includes("tripo")) return TRIPO_3D_PROVIDER;
return fallbackProvider || DEFAULT_3D_PROVIDER;
}

Expand All @@ -139,6 +153,9 @@ async function run3DProvider(options) {
if (provider === MESHY_3D_PROVIDER) {
return runMeshy3D(options);
}
if (provider === TRIPO_3D_PROVIDER) {
return runTripo3D(options);
}
throw new Error(`Unsupported 3D provider "${provider}".`);
}

Expand Down Expand Up @@ -373,6 +390,15 @@ export async function generateSingleObject(options) {
meshyEnableAnimation = DEFAULT_MESHY_ENABLE_ANIMATION,
meshyEnableRigging = DEFAULT_MESHY_ENABLE_RIGGING,
meshyEnablePbr = DEFAULT_MESHY_ENABLE_PBR,
tripoTexture = DEFAULT_TRIPO_TEXTURE,
tripoPbr = DEFAULT_TRIPO_PBR,
tripoFaceLimit = DEFAULT_TRIPO_FACE_LIMIT,
tripoQuad = DEFAULT_TRIPO_QUAD,
tripoAutoSize,
tripoTextureAlignment,
tripoOrientation,
tripoSeed,
tripoTextureSeed,
referenceOnly = false,
regenerateReference = false
} = options;
Expand Down Expand Up @@ -538,7 +564,16 @@ export async function generateSingleObject(options) {
animationActionId: meshyAnimationActionId,
enableSafetyChecker: meshyEnableSafetyChecker,
enableAnimation: meshyEnableAnimation,
enableRigging: meshyEnableRigging
enableRigging: meshyEnableRigging,
texture: tripoTexture,
pbr: tripoPbr,
faceLimit: tripoFaceLimit,
quad: tripoQuad,
autoSize: tripoAutoSize,
textureAlignment: tripoTextureAlignment,
orientation: tripoOrientation,
seed: tripoSeed,
textureSeed: tripoTextureSeed
});

const normalizedModelFiles = await normalizeModelFiles(
Expand Down Expand Up @@ -592,7 +627,7 @@ async function main() {

if (!world || (!objectId && !directImage)) {
throw new Error(
"Usage: node generate-single-asset.mjs --world <world-name> (--object-id <object-id> | --image <path>) --image-edit-prompt <prompt> [--object-name <name>] [--description <text>] [--provider hunyuan|meshy] [--regenerate] [--regenerate-reference] [--reference-only] [--face-count <40000-1500000>] [--generate-type Normal|LowPoly|Geometry] [--polygon-type triangle|quadrilateral] [--target-polycount 30000] [--enable-pbr true|false]"
"Usage: node generate-single-asset.mjs --world <world-name> (--object-id <object-id> | --image <path>) --image-edit-prompt <prompt> [--object-name <name>] [--description <text>] [--provider hunyuan|meshy|tripo] [--regenerate] [--regenerate-reference] [--reference-only] [--face-count <40000-1500000>] [--generate-type Normal|LowPoly|Geometry] [--polygon-type triangle|quadrilateral] [--target-polycount 30000] [--texture no|standard|HD] [--pbr true|false] [--face-limit 30000] [--enable-pbr true|false]"
);
}

Expand Down Expand Up @@ -622,7 +657,16 @@ async function main() {
meshyEnableSafetyChecker: one(flags, "enable-safety-checker", DEFAULT_MESHY_ENABLE_SAFETY_CHECKER),
meshyEnableAnimation: one(flags, "enable-animation", DEFAULT_MESHY_ENABLE_ANIMATION),
meshyEnableRigging: one(flags, "enable-rigging", DEFAULT_MESHY_ENABLE_RIGGING),
meshyEnablePbr: one(flags, "enable-pbr", DEFAULT_MESHY_ENABLE_PBR)
meshyEnablePbr: one(flags, "enable-pbr", DEFAULT_MESHY_ENABLE_PBR),
tripoTexture: one(flags, "texture", DEFAULT_TRIPO_TEXTURE),
tripoPbr: one(flags, "pbr", DEFAULT_TRIPO_PBR),
tripoFaceLimit: one(flags, "face-limit", DEFAULT_TRIPO_FACE_LIMIT),
tripoQuad: one(flags, "quad", DEFAULT_TRIPO_QUAD),
tripoAutoSize: one(flags, "auto-size"),
tripoTextureAlignment: one(flags, "texture-alignment"),
tripoOrientation: one(flags, "orientation"),
tripoSeed: one(flags, "seed"),
tripoTextureSeed: one(flags, "texture-seed")
});

console.log(JSON.stringify(result, null, 2));
Expand Down
129 changes: 129 additions & 0 deletions .claude/scripts/asset-pipeline/tripo-3d.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/env node
import { one, parseArgs } from "./fal-queue.mjs";
import { runFalImageTo3DProvider } from "./fal-3d-provider.mjs";

export const TRIPO_3D_ENDPOINT = "tripo3d/tripo/v2.5/image-to-3d";
export const TRIPO_3D_PROVIDER = "tripo";
export const DEFAULT_TRIPO_TEXTURE = "standard";
export const DEFAULT_TRIPO_PBR = true;
export const DEFAULT_TRIPO_FACE_LIMIT = 30000;
export const DEFAULT_TRIPO_QUAD = false;
export const DEFAULT_TRIPO_AUTO_SIZE = true;
export const DEFAULT_TRIPO_TEXTURE_ALIGNMENT = "original_image";
export const DEFAULT_TRIPO_ORIENTATION = "default";

function normalizeBoolean(value, fieldName) {
if (typeof value === "boolean") return value;
const normalized = String(value).trim().toLowerCase();
if (["true", "1", "yes", "on"].includes(normalized)) return true;
if (["false", "0", "no", "off"].includes(normalized)) return false;
throw new Error(`${fieldName} must be true or false.`);
}

function normalizeInteger(value, fieldName) {
const number = Number(value);
if (!Number.isInteger(number)) throw new Error(`${fieldName} must be an integer.`);
return number;
}

function normalizePositiveInteger(value, fieldName) {
const number = normalizeInteger(value, fieldName);
if (number <= 0) throw new Error(`${fieldName} must be greater than 0.`);
return number;
}

function normalizeNumber(value, fieldName) {
const number = Number(value);
if (!Number.isFinite(number)) throw new Error(`${fieldName} must be a number.`);
return number;
}

export function buildTripo3DInput(options = {}) {
const input = {
texture: options.texture || DEFAULT_TRIPO_TEXTURE,
pbr: normalizeBoolean(options.pbr ?? DEFAULT_TRIPO_PBR, "pbr"),
face_limit: normalizePositiveInteger(
options.faceLimit ?? DEFAULT_TRIPO_FACE_LIMIT,
"face-limit"
),
quad: normalizeBoolean(options.quad ?? DEFAULT_TRIPO_QUAD, "quad"),
auto_size: normalizeBoolean(options.autoSize ?? DEFAULT_TRIPO_AUTO_SIZE, "auto-size"),
texture_alignment: options.textureAlignment || DEFAULT_TRIPO_TEXTURE_ALIGNMENT,
orientation: options.orientation || DEFAULT_TRIPO_ORIENTATION
};

if (options.seed !== undefined) {
input.seed = normalizeInteger(options.seed, "seed");
}
if (options.textureSeed !== undefined) {
input.texture_seed = normalizeInteger(options.textureSeed, "texture-seed");
}

return input;
}

export async function runTripo3D(options) {
const {
image,
outputDir,
assetName,
metadataPath,
metadata = {},
onSubmit,
onStatus
} = options;

if (!image) throw new Error("Input image is required.");
if (!outputDir) throw new Error("outputDir is required.");

return runFalImageTo3DProvider({
endpoint: TRIPO_3D_ENDPOINT,
providerSlug: TRIPO_3D_PROVIDER,
imageInputKey: "image_url",
image,
outputDir,
assetName,
input: buildTripo3DInput(options),
metadataPath,
metadata,
pollIntervalMs: 10000,
onSubmit,
onStatus
});
}

async function main() {
const { flags } = parseArgs();
const image = one(flags, "image") || one(flags, "input-image");
const outputDir = one(flags, "output-dir");

if (!image || !outputDir) {
throw new Error(
"Usage: node tripo-3d.mjs --image <path-or-url> --output-dir <dir> [--asset-name <name>] [--texture no|standard|HD] [--pbr true|false] [--face-limit 30000]"
);
}

const summary = await runTripo3D({
image,
outputDir,
assetName: one(flags, "asset-name"),
texture: one(flags, "texture", DEFAULT_TRIPO_TEXTURE),
pbr: one(flags, "pbr", DEFAULT_TRIPO_PBR),
faceLimit: one(flags, "face-limit", DEFAULT_TRIPO_FACE_LIMIT),
quad: one(flags, "quad", DEFAULT_TRIPO_QUAD),
autoSize: one(flags, "auto-size", DEFAULT_TRIPO_AUTO_SIZE),
textureAlignment: one(flags, "texture-alignment", DEFAULT_TRIPO_TEXTURE_ALIGNMENT),
orientation: one(flags, "orientation", DEFAULT_TRIPO_ORIENTATION),
seed: one(flags, "seed"),
textureSeed: one(flags, "texture-seed")
});

console.log(JSON.stringify(summary, null, 2));
}

if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
console.error(error.message);
process.exit(1);
});
}
28 changes: 26 additions & 2 deletions .claude/skills/image-blast-3d/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: image-blast-3d
description: Generate one specified atomic 3D object. Use when the user names exactly one object instance to make, or provides one image plus the object name/description.
argument-hint: [world-name] [object-id/name or image path + object description] [--image-edit-prompt prompt] [--provider meshy|hunyuan] [--regenerate] [--regenerate-reference] [--target-polycount N] [--face-count N] [--generate-type Normal|LowPoly|Geometry] [--polygon-type triangle|quadrilateral] [--enable-pbr true|false]
argument-hint: [world-name] [object-id/name or image path + object description] [--image-edit-prompt prompt] [--provider meshy|hunyuan|tripo] [--regenerate] [--regenerate-reference] [--target-polycount N] [--face-count N] [--generate-type Normal|LowPoly|Geometry] [--polygon-type triangle|quadrilateral] [--enable-pbr true|false]
allowed-tools: Read Write Glob Bash(ls *) Bash(node .claude/scripts/project/project-state.mjs *) Bash(node .claude/scripts/project/ensure-local-assets.mjs *) Bash(node .claude/scripts/asset-pipeline/generate-single-asset.mjs *)
context: fork
agent: image-blast-3d
Expand Down Expand Up @@ -91,10 +91,34 @@ For Meshy-specific requests, pass the matching options:
- `--enable-animation true|false`
- `--enable-rigging true|false`

Pass `--provider tripo` for the Tripo3D engine. Tripo defaults are:

```json
{
"texture": "standard",
"pbr": true,
"face_limit": 30000,
"quad": false,
"auto_size": true,
"texture_alignment": "original_image",
"orientation": "default"
}
```

For Tripo-specific requests, pass the matching options:

- `--texture no|standard|HD` (default `standard`)
- `--pbr true|false` (default `true`)
- `--face-limit <integer>` (default `30000`)
- `--quad true|false` (default `false`)
- `--auto-size true|false` (default `true`)
- `--texture-alignment original_image|geometry`
- `--orientation default|align_image`

For explicit model regeneration from the existing reference, append `--regenerate`. For a new source extraction and model, append `--regenerate-reference`. For direct single-image generation, use:

```bash
node .claude/scripts/asset-pipeline/generate-single-asset.mjs --world "$0" --image "<image-path>" --object-name "<object-name>" --description "<description>" --image-edit-prompt "<object-specific extraction prompt>"
```

Final response: report the object id, output directory, generated model files, and any failed/resumable request metadata.
Final response: report the object id, output directory, generated model files, and any failed/resumable request metadata.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ IMAGE-BLASTER uses a few generation models:
- `nano-banana` - default image edit preference for source cleanup, clean plates, and object reference images.
- `gpt-image-2` - alternate image edit provider when the edit skill is asked to prefer it.
- `hunyuan-3d` - Hunyuan 3D model creates 3D object models through FAL.
- `tripo3d-v2.5` - Tripo3D image-to-3D model creates an alternate 3D object provider through FAL.
- `elevenlabs-sfx` - ElevenLabs sound effects model creates ambient and object-specific sounds.

3D model creation supports these Hunyuan parameters:
Expand Down