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
41 changes: 41 additions & 0 deletions docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -1491,3 +1491,44 @@ behind it, and what a reviewer should check. IDs are stable; append, do not renu
busy machine could fall back to the key file when sealing, or fail to open a
stored login. DPAPI never prompts; the Keychain and the Secret Service keep
5 s, since a locked keychain may.

## D-090 Retexture picks Meshy N1 with `--ai-model`; N1 flips the texture defaults (ENG-5144)

- Meshy N1 (`ai_model: "meshy-n1"`) is a retexture model. It makes a 2K base
color texture from reference images only: one `image_style_url`, read as the
front view, or 1–4 `multiview_image_urls`, the first the front and the rest in
any order. It costs 10 credits and takes models within the face limit in the
API docs (5,000,000 faces at launch, to be raised). The
API answers 400 for N1 with a text prompt and no image, `enable_pbr: true`,
`texture_resolution` 4k or 8k, `hd_texture: true` without a resolution, and
`remove_lighting` with any value.
- `--ai-model` is a choice list, like the Smart Topology flag (D-078). It offers
`latest`, `meshy-7`, `meshy-6` and `meshy-n1`: the models the CLI's defaults
work with. The retired meshy-5 is not offered; the API refuses it.
meshy-6-lite stays reachable through `--data` only, as before. It refuses the
4k default too, and giving it a 2k default is a separate change.
- Every other model keeps the game-ready defaults: PBR maps on, a 4k base color,
and the meshy-7 pin for multi-view. For N1 the defaults layer sends
`enable_pbr: false` and `texture_resolution: "2k"` and pins no model, so N1
with multi-view stays N1. The defaults flip per model instead of changing for
everyone. They exist so a game-engine import carries the full map set at 4k.
Dropping them globally would hand every Meshy 7 caller a 2k base color only,
to suit a model that nobody gets by accident: N1 runs only when it is named,
so its defaults can follow its name.
- The defaults read the model where the merged payload takes it from: the
flag, else `--data`. Before this change, `--data '{"ai_model":"meshy-n1"}'`
reached the API but carried the PBR and 4k defaults, a 400. Now it gets the
N1 defaults, so `--data` stays the escape hatch it is documented to be.
- An explicit ask N1 cannot honour is refused before the POST (exit 2), as in
D-076: a text prompt, `enable_pbr: true`, 4k or 8k, `hd_texture: true`,
`remove_lighting`. A prompt beside an image in `--data` is refused too: v1
would drop it without a word. A request with no reference image says N1 needs
one. The checks run on the merged payload, so `--data` meets the same rules
(D-078, D-083). Nothing is rewritten to what N1 can make: a quiet downgrade
would return something other than what was asked for.
- `--ai-model latest` or `meshy-6` with `--multiview-image-urls` is refused
locally: those values are the flag's own. An `ai_model` in `--data` beside
multi-view is left to the server's gate, like the meshy-7 pin it overrides.
- The face limit is not checked locally. The CLI cannot count a remote model,
and the API refuses a known count over the limit before the task starts.
`meshy inspect faces --model` measures a local GLB.
9 changes: 9 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,15 @@ meshy-cli multi-color-print create --input-task-id <id> --style cartoon --printe
# the web app's Ultra 2K geometry setting
meshy-cli multi-image-to-3d create --image-urls front.png,side.png,back.png --geometry-resolution 2k

# retexture an existing model from a prompt or a reference image (PBR maps, 4k base color).
# --ai-model meshy-n1 (Meshy N1, 10 credits) makes a 2k base color texture from reference
# images only — one image, read as the front view, or 1-4 views with the front first —
# with no text prompt and no PBR maps, on models within the face limit in the API docs; the CLI sends
# enable_pbr false and 2k for it, and refuses a prompt, PBR, 4k/8k or --remove-lighting.
meshy-cli retexture create --input-task-id <id> --image-style-url look.png
meshy-cli retexture create --input-task-id <id> --ai-model meshy-n1 --image-style-url front.png
meshy-cli retexture create --input-task-id <id> --ai-model meshy-n1 --multiview-image-urls front.png,back.png

# fire-and-forget (--async): exactly one POST, returns the task_id, query later
TASK=$(meshy-cli text-to-image create --prompt "mountain landscape" --async | jq -r .result.submission.task_id)
meshy-cli text-to-image get "$TASK"
Expand Down
12 changes: 10 additions & 2 deletions skills/meshy-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,16 @@ These are API rules, not preferences — ignoring them produces failed tasks:
`--model-type smart-topology` (text-to-3d and image-to-3d) has a native
`--target-polycount`: meshy-t2.5 is the CLI default, 100-30000 (default
15000); `--ai-model meshy-t2` selects T2, 100-15000 (default 10000). `retexture`'s
`--multiview-image-urls` needs Meshy 7 — it takes 1-4 views of **the same
object**, not style references.
`--multiview-image-urls` runs on Meshy 7 (pinned by default) or Meshy N1 — it
takes 1-4 views of **the same object**, front first, not style references.
- **`retexture --ai-model meshy-n1`** (Meshy N1, 10 credits) makes a 2K base color
texture from a reference image only: `--image-style-url` (read as the front view)
or 1-4 `--multiview-image-urls` (front first, the rest in any order). No text
prompt, no PBR maps, no 4K/8K: the CLI sends `enable_pbr: false` and `2k` for it
and refuses `--text-style-prompt`, `--enable-pbr true`, `--texture-resolution
4k|8k` and `--remove-lighting`. Models over the Meshy N1 face limit in the
API docs are refused; check a local GLB with `meshy inspect faces --model <file>
--max-faces <limit>`.
- Printing: `analyze-`/`repair-printability` take `--printer-type
fdm|sla|full_color` and one `--intended-print-height-mm` /
`--intended-print-longest-side-mm`; `multi-color-print` takes `--style
Expand Down
111 changes: 92 additions & 19 deletions src/cmd/retexture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,70 @@
* --multiview-image-urls (1-4 views OF THE SAME OBJECT, element 0 being the
* front reference).
*
* Multi-view is the one place this CLI names a model. The endpoint's gate is
* literal: `multiview_image_urls requires ai_model meshy-7` is returned even
* for ai_model "latest" and for an omitted ai_model, so leaving the model to
* the server — what every other 3D command here does — is a guaranteed 400.
* (image-to-3d's comparable geometry_resolution gate accepts both, which is
* why only this command pins a version.) It is pinned in the defaults layer, so
* `--data '{"ai_model":"..."}'` still wins if the gate ever loosens.
* Multi-view pins a model. The endpoint's gate is literal: multiview_image_urls
* takes an explicit meshy-7 or meshy-n1 and is refused even for ai_model
* "latest" and for an omitted ai_model, so leaving the model to the server —
* what every other 3D command here does — is a guaranteed 400. (image-to-3d's
* comparable geometry_resolution gate accepts both, which is why only this
* command pins a version.) The meshy-7 pin rides the defaults layer, so
* --ai-model and `--data '{"ai_model":"..."}'` still win.
*
* --ai-model meshy-n1 (Meshy N1) makes a 2k base color texture from reference
* images only: one style image (the front view) or 1-4 views. The API refuses
* it a prompt alone, PBR maps, 4k/8k and remove_lighting, so the game-ready
* defaults (PBR on, 4k) flip for it, and an explicit ask for any of those is
* refused before the POST (decisions.md D-090).
*/

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

/**
* The models --ai-model offers: the ones the CLI's defaults work with. The
* retired meshy-5 is gone; meshy-6-lite (2k only) stays reachable through --data.
*/
export const RETEXTURE_MODELS = ["latest", "meshy-7", "meshy-6", "meshy-n1"] as const;
const MESHY_N1 = "meshy-n1";
/** The only models the API lets take multiview_image_urls. */
const MULTIVIEW_MODELS: readonly string[] = ["meshy-7", MESHY_N1];

const N1_NEEDS_IMAGE =
"meshy-n1 needs a reference image: pass --image-style-url (read as the front view) or --multiview-image-urls (1-4 views, the front first) — it does not texture from a text prompt";

/** The model the request runs on: the flag beats --data, as in the merged payload. */
function requestedModel(opts: Record<string, unknown>, data: Record<string, unknown>): unknown {
return opts.aiModel ?? data["ai_model"];
}

/**
* Refuse what meshy-n1 cannot make, on the merged payload, so a field sent
* through --data meets the same rule as its flag. Defaults never trip this:
* for N1 they are enable_pbr false and 2k.
*/
function refuseMeshyN1Conflicts(payload: Record<string, unknown>): void {
if (payload["ai_model"] !== MESHY_N1) return;
const views = payload["multiview_image_urls"];
if (!payload["image_style_url"] && !(Array.isArray(views) && views.length > 0)) {
throw new UsageError(N1_NEEDS_IMAGE);
}
const conflicts: string[] = [];
// v1 drops a prompt sent beside the image; refuse rather than let it vanish.
if (payload["text_style_prompt"]) conflicts.push("--text-style-prompt (it textures from the reference images only)");
if (payload["enable_pbr"] === true) conflicts.push("--enable-pbr true (it makes a base color texture only)");
const resolution = payload["texture_resolution"];
if (resolution === "4k" || resolution === "8k") conflicts.push(`--texture-resolution ${resolution} (it makes 2k only)`);
// The API takes an explicit texture_resolution over hd_texture, so
// hd_texture true beside "2k" would run at 2k there; refusing it is the
// CLI's own rule, since the caller asked for an HD texture N1 cannot make.
if (payload["hd_texture"] === true) conflicts.push("hd_texture true (that is 4k; it makes 2k only)");
if (payload["remove_lighting"] !== undefined) conflicts.push("--remove-lighting (the API refuses it with meshy-n1)");
if (conflicts.length > 0) {
throw new UsageError(`meshy-n1 cannot be used with ${conflicts.join(", ")}; drop them or pick another --ai-model`);
}
}

const spec: ResourceCommandSpec = {
name: "retexture",
description:
Expand All @@ -30,32 +80,38 @@ const spec: ResourceCommandSpec = {
return cmd
.option("--input-task-id <id>", "source Meshy task id (takes priority over --model-url)")
.option("--model-url <src>", "3D model: an http(s) URL (.glb/.gltf/.obj/.fbx/.stl) or a local .glb/.gltf file — the API takes inline uploads as glTF only, so host .stl/.obj/.fbx at a URL")
.option("--text-style-prompt <text>", "text style description; mutually exclusive with --image-style-url")
.option("--text-style-prompt <text>", "text style description; mutually exclusive with --image-style-url; not with meshy-n1")
.option(
"--image-style-url <src>",
"style reference image (URL or local path); mutually exclusive with --text-style-prompt",
"style reference image (URL or local path); mutually exclusive with --text-style-prompt. meshy-n1 reads it as the front view",
)
.option(
"--multiview-image-urls <csv>",
"1-4 views of the SAME object as CSV, first entry is the front reference; each is a URL or local file path. Mutually exclusive with the two style flags",
"1-4 views of the SAME object as CSV, first entry is the front reference; each is a URL or local file path. Mutually exclusive with the two style flags. Runs on meshy-7 (pinned by default) or --ai-model meshy-n1",
parseCsv,
)
.addOption(
new Option(
"--ai-model <model>",
"latest (default; Meshy 7) | meshy-7 | meshy-6 | meshy-n1 — Meshy N1: a 2k base color texture from a reference image only (--image-style-url or --multiview-image-urls), no text prompt, no PBR maps, models within the face limit in the API docs",
).choices([...RETEXTURE_MODELS]),
)
.option("--enable-original-uv <bool>", "keep the model's existing UV layout — true suits Meshy outputs and models with good UVs (default: false, fresh UVs)", parseBool)
.option(
"--enable-pbr <bool>",
"generate normal/metallic/roughness maps, plus an emission map on meshy-6 below 8k (default: true, so game-engine imports carry the full map set)",
"generate normal/metallic/roughness maps, plus an emission map on meshy-6 below 8k (default: true, so game-engine imports carry the full map set; false on meshy-n1, which makes a base color texture only)",
parseBool,
)
.addOption(
new Option("--texture-resolution <res>", "base color texture size (default: 4k)").choices([
new Option("--texture-resolution <res>", "base color texture size (default: 4k; 2k on meshy-n1, its only size)").choices([
"2k",
"4k",
"8k",
]),
)
.option(
"--remove-lighting <bool>",
"strip baked lighting from the base color (default: true)",
"strip baked lighting from the base color (default: true); not with meshy-n1, which the API refuses it for",
parseBool,
)
.option("--target-formats <csv>", "glb,obj,fbx,stl,usdz,3mf (default: glb)", parseCsv)
Expand All @@ -68,11 +124,13 @@ const spec: ResourceCommandSpec = {
const multiview = opts.multiviewImageUrls as string[] | undefined;
if (!opts.textStylePrompt && !opts.imageStyleUrl && !multiview?.length && !opts.data) {
throw new UsageError(
"provide --text-style-prompt, --image-style-url or --multiview-image-urls",
opts.aiModel === MESHY_N1 ? N1_NEEDS_IMAGE : "provide --text-style-prompt, --image-style-url or --multiview-image-urls",
);
}
// The API takes exactly one style input; picking for the caller would
// silently drop the other one, so refuse instead.
// Exactly one style flag. The API is looser: views beside a prompt or an
// image are a 400 there, but v1 clears a prompt beside one image and
// textures from the image alone. Picking for the caller would drop one
// flag without a word, so the CLI refuses every combination.
const styleFlags = [
["--text-style-prompt", Boolean(opts.textStylePrompt)],
["--image-style-url", Boolean(opts.imageStyleUrl)],
Expand All @@ -85,12 +143,19 @@ const spec: ResourceCommandSpec = {
if (multiview && (multiview.length < 1 || multiview.length > 4)) {
throw new UsageError("--multiview-image-urls takes 1-4 images of the same object");
}
// The flag's own values are the CLI's to check; an ai_model in --data is
// left to the server's gate, like the pin it overrides.
const model = opts.aiModel as string | undefined;
if (multiview?.length && model !== undefined && !MULTIVIEW_MODELS.includes(model)) {
throw new UsageError(`--multiview-image-urls runs on meshy-7 (the default with it) or meshy-n1; the API refuses it with --ai-model ${model}`);
}
return {
input_task_id: opts.inputTaskId,
model_url: opts.modelUrl,
text_style_prompt: opts.textStylePrompt,
image_style_url: opts.imageStyleUrl,
multiview_image_urls: multiview,
ai_model: model,
enable_original_uv: opts.enableOriginalUv,
enable_pbr: opts.enablePbr,
texture_resolution: opts.textureResolution,
Expand All @@ -99,10 +164,17 @@ const spec: ResourceCommandSpec = {
alpha_thumbnail: opts.alphaThumbnail,
};
},
toDefaults(opts) {
toDefaults(opts, data) {
// meshy-n1 makes a 2k base color only: the game-ready defaults below
// would be a 400 on every N1 request, so they flip for it. The model is
// read where the merged payload takes it from (flag, then --data), so
// N1 picked either way gets these defaults.
if (requestedModel(opts, data) === MESHY_N1) {
return { enable_pbr: false, texture_resolution: "2k", target_formats: ["glb"] };
}
// Game-ready defaults (agent-ts parity); see image-to-3d. The one model
// pin in the 3D surface rides here — see the header note on the literal
// meshy-7 gate.
// pin in the 3D surface rides here — see the header note on the
// multi-view gate.
const multiview = (opts.multiviewImageUrls as string[] | undefined)?.length;
return {
ai_model: multiview ? "meshy-7" : undefined,
Expand All @@ -111,6 +183,7 @@ const spec: ResourceCommandSpec = {
target_formats: ["glb"],
};
},
validatePayload: refuseMeshyN1Conflicts,
},
endpointOf: (c) => c.retexture,
};
Expand Down
Loading
Loading