diff --git a/.claude/agents/image-blast-3d.md b/.claude/agents/image-blast-3d.md index daa58a2..28c01f5 100644 --- a/.claude/agents/image-blast-3d.md +++ b/.claude/agents/image-blast-3d.md @@ -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 @@ -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 ` (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. diff --git a/.claude/scripts/asset-pipeline/generate-single-asset.mjs b/.claude/scripts/asset-pipeline/generate-single-asset.mjs index 73d2208..6b97ed9 100644 --- a/.claude/scripts/asset-pipeline/generate-single-asset.mjs +++ b/.claude/scripts/asset-pipeline/generate-single-asset.mjs @@ -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, @@ -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) { @@ -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; } @@ -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; } @@ -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}".`); } @@ -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; @@ -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( @@ -592,7 +627,7 @@ async function main() { if (!world || (!objectId && !directImage)) { throw new Error( - "Usage: node generate-single-asset.mjs --world (--object-id | --image ) --image-edit-prompt [--object-name ] [--description ] [--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 (--object-id | --image ) --image-edit-prompt [--object-name ] [--description ] [--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]" ); } @@ -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)); diff --git a/.claude/scripts/asset-pipeline/tripo-3d.mjs b/.claude/scripts/asset-pipeline/tripo-3d.mjs new file mode 100644 index 0000000..70e8cd9 --- /dev/null +++ b/.claude/scripts/asset-pipeline/tripo-3d.mjs @@ -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 --output-dir [--asset-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); + }); +} diff --git a/.claude/skills/image-blast-3d/SKILL.md b/.claude/skills/image-blast-3d/SKILL.md index d196c23..1ee1880 100644 --- a/.claude/skills/image-blast-3d/SKILL.md +++ b/.claude/skills/image-blast-3d/SKILL.md @@ -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 @@ -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 ` (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 "" --object-name "" --description "" --image-edit-prompt "" ``` -Final response: report the object id, output directory, generated model files, and any failed/resumable request metadata. \ No newline at end of file +Final response: report the object id, output directory, generated model files, and any failed/resumable request metadata. diff --git a/README.md b/README.md index 633aa3b..11ad97c 100644 --- a/README.md +++ b/README.md @@ -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: