@forgeax-extension/character-3d 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/SOURCE_PROVENANCE.md +8 -0
  2. package/dist/assets/index-CIx3aXo1.js +4336 -0
  3. package/dist/assets/index-CIx3aXo1.js.map +1 -0
  4. package/dist/assets/index-CmhOHLEB.css +1 -0
  5. package/dist/hdr/README.md +28 -0
  6. package/dist/hdr/presets.json +8 -0
  7. package/dist/index.html +13 -0
  8. package/forgeax-extension.json +428 -0
  9. package/marketplace-card.json +56 -0
  10. package/package.json +59 -0
  11. package/packages/external-asset-meta/package.json +24 -0
  12. package/packages/external-asset-meta/src/cook.test.ts +211 -0
  13. package/packages/external-asset-meta/src/cook.ts +188 -0
  14. package/packages/external-asset-meta/src/draco3dgltf.d.ts +11 -0
  15. package/packages/external-asset-meta/src/index.ts +18 -0
  16. package/packages/external-asset-meta/src/normalize.test.ts +36 -0
  17. package/packages/external-asset-meta/src/normalize.ts +101 -0
  18. package/packages/external-asset-meta/src/types.ts +82 -0
  19. package/schemas/adopt-playable-character.args.json +26 -0
  20. package/schemas/adopt-playable-character.returns.json +31 -0
  21. package/schemas/apply-motion.args.json +47 -0
  22. package/schemas/apply-motion.returns.json +23 -0
  23. package/schemas/auto-rig.args.json +35 -0
  24. package/schemas/auto-rig.returns.json +23 -0
  25. package/schemas/delete-asset.args.json +18 -0
  26. package/schemas/delete-asset.returns.json +18 -0
  27. package/schemas/engine-import-status.args.json +18 -0
  28. package/schemas/engine-import-status.returns.json +50 -0
  29. package/schemas/export-playable-character.args.json +21 -0
  30. package/schemas/export-playable-character.returns.json +47 -0
  31. package/schemas/gen3d-asset-manifest.json +281 -0
  32. package/schemas/generate-meshy-text-mock.args.json +42 -0
  33. package/schemas/generate-meshy-text-mock.returns.json +301 -0
  34. package/schemas/get-credentials.args.json +7 -0
  35. package/schemas/get-credentials.returns.json +45 -0
  36. package/schemas/get-playable-profile.args.json +11 -0
  37. package/schemas/get-playable-profile.returns.json +188 -0
  38. package/schemas/image-to-3d.args.json +47 -0
  39. package/schemas/image-to-3d.returns.json +306 -0
  40. package/schemas/import-to-engine.args.json +18 -0
  41. package/schemas/import-to-engine.returns.json +35 -0
  42. package/schemas/list-assets.args.json +22 -0
  43. package/schemas/list-assets.returns.json +296 -0
  44. package/schemas/list-motions.args.json +29 -0
  45. package/schemas/list-motions.returns.json +41 -0
  46. package/schemas/pose-standardization.args.json +28 -0
  47. package/schemas/pose-standardization.returns.json +29 -0
  48. package/schemas/provider-status.args.json +7 -0
  49. package/schemas/provider-status.returns.json +70 -0
  50. package/schemas/refine-mesh.args.json +33 -0
  51. package/schemas/refine-mesh.returns.json +306 -0
  52. package/schemas/rename-asset.args.json +22 -0
  53. package/schemas/rename-asset.returns.json +14 -0
  54. package/schemas/retopo-lowpoly.args.json +34 -0
  55. package/schemas/retopo-lowpoly.returns.json +24 -0
  56. package/schemas/score-quality.args.json +36 -0
  57. package/schemas/score-quality.returns.json +12 -0
  58. package/schemas/set-credentials.args.json +32 -0
  59. package/schemas/set-credentials.returns.json +45 -0
  60. package/schemas/set-playable-motion-mapping.args.json +25 -0
  61. package/schemas/set-playable-motion-mapping.returns.json +33 -0
  62. package/schemas/set-playable-profile.args.json +32 -0
  63. package/schemas/set-playable-profile.returns.json +67 -0
  64. package/schemas/text-to-3d.args.json +51 -0
  65. package/schemas/text-to-3d.returns.json +306 -0
  66. package/schemas/upload-image.args.json +19 -0
  67. package/schemas/upload-image.returns.json +21 -0
  68. package/schemas/upload-video.args.json +14 -0
  69. package/schemas/upload-video.returns.json +14 -0
  70. package/schemas/views-to-3d.args.json +58 -0
  71. package/schemas/views-to-3d.returns.json +306 -0
  72. package/server/adopt-playable-character.ts +291 -0
  73. package/server/asset-storage.ts +136 -0
  74. package/server/asset-upload.ts +123 -0
  75. package/server/audit.ts +58 -0
  76. package/server/cache.ts +75 -0
  77. package/server/cos-uploader.ts +141 -0
  78. package/server/credentials-store.ts +186 -0
  79. package/server/engine-import.ts +238 -0
  80. package/server/env.ts +287 -0
  81. package/server/export-playable-character.ts +364 -0
  82. package/server/generate.ts +89 -0
  83. package/server/merge-playable-character.ts +259 -0
  84. package/server/motion-catalog.ts +137 -0
  85. package/server/per-game-store.ts +1016 -0
  86. package/server/providers/gateway-client.ts +171 -0
  87. package/server/providers/gateway-data.ts +182 -0
  88. package/server/providers/gateway-models.ts +49 -0
  89. package/server/providers/hunyuan-rest.ts +317 -0
  90. package/server/providers/hunyuan-workflow.ts +225 -0
  91. package/server/providers/meshy-direct-client.ts +223 -0
  92. package/server/providers/meshy.ts +548 -0
  93. package/server/providers/rodin-gateway.ts +193 -0
  94. package/server/providers/rodin.ts +253 -0
  95. package/server/providers/visvise.ts +329 -0
  96. package/server/rate-guard.ts +29 -0
  97. package/server/tool-handlers.ts +2193 -0
  98. package/shared/catalog.ts +307 -0
  99. package/shared/manifest.ts +378 -0
  100. package/shared/meshy-actions.ts +690 -0
  101. package/shared/playable-preview-url.test.ts +14 -0
  102. package/shared/playable-preview-url.ts +13 -0
  103. package/shared/playable-profile.test.ts +127 -0
  104. package/shared/playable-profile.ts +171 -0
  105. package/shared/provider-params.test.ts +172 -0
  106. package/shared/provider-params.ts +389 -0
  107. package/shared/quality/heuristics.test.ts +84 -0
  108. package/shared/quality/heuristics.ts +100 -0
  109. package/shared/rodin-image.test.ts +51 -0
  110. package/shared/rodin-image.ts +125 -0
@@ -0,0 +1,171 @@
1
+ import type { LitellmEnv } from '../env';
2
+
3
+ export type FetchLike = (url: string, init: RequestInit) => Promise<Response>;
4
+
5
+ export type GatewayStatus = 'succeeded' | 'failed' | 'pending';
6
+
7
+ export interface GatewayClientDeps {
8
+ env: LitellmEnv;
9
+ fetchImpl?: FetchLike;
10
+ sleep?: (ms: number) => Promise<void>;
11
+ }
12
+
13
+ export interface PollOptions {
14
+ timeoutMs: number;
15
+ intervalMs: number;
16
+ label?: string;
17
+ }
18
+
19
+ const FAILURE_STATUSES = new Set(['failed', 'error', 'fail', 'canceled', 'cancelled']);
20
+
21
+ export class GatewayClient {
22
+ private readonly env: LitellmEnv;
23
+ private readonly fetchImpl: FetchLike;
24
+ private readonly sleep: (ms: number) => Promise<void>;
25
+
26
+ constructor(deps: GatewayClientDeps) {
27
+ this.env = deps.env;
28
+ this.fetchImpl = deps.fetchImpl ?? ((url, init) => fetch(url, init));
29
+ this.sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
30
+ }
31
+
32
+ async post(path: string, body: unknown): Promise<Record<string, unknown>> {
33
+ const resp = await this.fetchImpl(`${this.env.baseUrl}${path}`, {
34
+ method: 'POST',
35
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.env.apiKey}` },
36
+ body: JSON.stringify(body),
37
+ });
38
+ return readJsonOrThrow(resp);
39
+ }
40
+
41
+ async get(path: string): Promise<Record<string, unknown>> {
42
+ const resp = await this.fetchImpl(`${this.env.baseUrl}${path}`, {
43
+ method: 'GET',
44
+ headers: { Authorization: `Bearer ${this.env.apiKey}` },
45
+ });
46
+ return readJsonOrThrow(resp);
47
+ }
48
+
49
+ async pollTask(path: string, options: PollOptions): Promise<Record<string, unknown>> {
50
+ const deadline = Date.now() + options.timeoutMs;
51
+ let lastError: unknown = null;
52
+
53
+ while (Date.now() < deadline) {
54
+ try {
55
+ const resp = await this.get(path);
56
+ const status = normalizeGatewayStatus(resp.status);
57
+ if (status === 'succeeded') return resp;
58
+ if (status === 'failed') {
59
+ throw Object.assign(
60
+ new Error(`${options.label ?? 'gateway task'} failed: ${gatewayErrorMessage(resp) ?? String(resp.status ?? 'failed')}`),
61
+ { code: 'provider_failed' },
62
+ );
63
+ }
64
+ } catch (error) {
65
+ lastError = error;
66
+ if (isTerminalGatewayError(error)) throw error;
67
+ }
68
+ await this.sleep(options.intervalMs);
69
+ }
70
+
71
+ const suffix = lastError instanceof Error ? `; last error: ${lastError.message}` : '';
72
+ throw Object.assign(new Error(`${options.label ?? 'gateway task'} poll timed out${suffix}`), {
73
+ code: 'provider_timeout',
74
+ lastError,
75
+ });
76
+ }
77
+
78
+ async download(url: string, options: { format?: string; image?: boolean } = {}): Promise<Uint8Array> {
79
+ const resp = await this.fetchImpl(url, { method: 'GET' });
80
+ if (!resp.ok) throw httpError(resp.status);
81
+ const data = new Uint8Array(await resp.arrayBuffer());
82
+ if (data.byteLength === 0) {
83
+ throw Object.assign(new Error(`download returned empty bytes: ${safeUrlForError(url)}`), {
84
+ code: 'provider_empty_download',
85
+ });
86
+ }
87
+ if (options.format === 'glb' && !hasGlbMagic(data)) {
88
+ throw Object.assign(new Error(`download is not a GLB: ${safeUrlForError(url)}`), {
89
+ code: 'provider_invalid_download',
90
+ });
91
+ }
92
+ if (options.image && !hasImageExtension(url)) {
93
+ throw Object.assign(new Error(`download URL is not an image: ${safeUrlForError(url)}`), {
94
+ code: 'provider_invalid_download',
95
+ });
96
+ }
97
+ return data;
98
+ }
99
+ }
100
+
101
+ export function normalizeGatewayStatus(status: unknown): GatewayStatus {
102
+ const value = String(status ?? '').toLowerCase();
103
+ if (value === 'succeeded') return 'succeeded';
104
+ if (FAILURE_STATUSES.has(value)) return 'failed';
105
+ return 'pending';
106
+ }
107
+
108
+ export function gatewayErrorMessage(resp: Record<string, unknown>): string | undefined {
109
+ const err = resp.error as Record<string, unknown> | undefined;
110
+ return err && typeof err.message === 'string' && err.message ? err.message : undefined;
111
+ }
112
+
113
+ async function readJsonOrThrow(resp: Response): Promise<Record<string, unknown>> {
114
+ if (!resp.ok) {
115
+ const body = await resp.text().catch(() => '');
116
+ throw httpError(resp.status, body);
117
+ }
118
+ return (await resp.json()) as Record<string, unknown>;
119
+ }
120
+
121
+ export function httpError(
122
+ status: number,
123
+ body = '',
124
+ ): Error & { code: string; status: number; httpStatus: number; body?: string } {
125
+ const code =
126
+ status === 400 ? 'provider_bad_request'
127
+ : status === 401 ? 'provider_unauthorized'
128
+ : status === 402 ? 'provider_insufficient_credits'
129
+ : status === 404 ? 'provider_not_enabled'
130
+ : status === 429 ? 'provider_rate_limited'
131
+ : 'provider_http_error';
132
+ const snippet = body.replace(/\s+/g, ' ').trim().slice(0, 240);
133
+ const suffix = snippet ? `: ${snippet}` : '';
134
+ return Object.assign(new Error(`gateway http ${status}${suffix}`), {
135
+ code,
136
+ status,
137
+ httpStatus: status,
138
+ ...(snippet ? { body: snippet } : {}),
139
+ });
140
+ }
141
+
142
+ function isTerminalGatewayError(error: unknown): boolean {
143
+ const code = (error as { code?: unknown } | null)?.code;
144
+ return (
145
+ code === 'provider_bad_request' ||
146
+ code === 'provider_unauthorized' ||
147
+ code === 'provider_insufficient_credits' ||
148
+ code === 'provider_not_enabled' ||
149
+ code === 'provider_rate_limited' ||
150
+ code === 'provider_failed'
151
+ );
152
+ }
153
+
154
+ function hasGlbMagic(data: Uint8Array): boolean {
155
+ return data.length >= 4 && data[0] === 0x67 && data[1] === 0x6c && data[2] === 0x54 && data[3] === 0x46;
156
+ }
157
+
158
+ function hasImageExtension(url: string): boolean {
159
+ const path = url.split('?', 1)[0] ?? url;
160
+ return /\.(png|jpe?g|webp|gif)$/i.test(path);
161
+ }
162
+
163
+ function safeUrlForError(url: string): string {
164
+ try {
165
+ const parsed = new URL(url);
166
+ parsed.search = '';
167
+ return parsed.toString();
168
+ } catch {
169
+ return url.split('?', 1)[0] ?? url;
170
+ }
171
+ }
@@ -0,0 +1,182 @@
1
+ // gateway-data.ts — SSOT for parsing the LiteLLM 3D gateway `data[]` output.
2
+ //
3
+ // Every provider (meshy, hunyuan-workflow, hunyuan-rest) polls the same gateway
4
+ // task endpoint, which returns outputs as a `data[]` array of
5
+ // { url, type, format } entries. Two shared quirks live here so the three
6
+ // providers can't drift:
7
+ //
8
+ // 1. Model binaries are usually type:'mesh' (format=glb/fbx/…). Hunyuan
9
+ // auto-rig via LiteLLM also emits type:'rigged_model' for FBX — still keyed
10
+ // by format. We key model urls by format.
11
+ // 2. Every *other* PNG — the rendered thumbnail AND all PBR texture maps —
12
+ // is tagged type:'preview'. Keying the thumbnail off `type` alone let the
13
+ // LAST type:'preview' entry (texture_0_emission.png, a near-black map)
14
+ // overwrite the real render (preview.png), so saved previews came out
15
+ // black (the marketplace #16 gateway regression). Real Meshy filenames:
16
+ // output/preview.png → the render (thumbnail)
17
+ // output/texture_0.png → base_color
18
+ // output/texture_0_{metallic,roughness,normal,emission}.png → PBR maps
19
+ // So non-mesh images are classified by filename, not the gateway `type`.
20
+
21
+ const IMAGE_EXT = /\.(png|jpe?g|webp)$/i;
22
+ const MESH_EXTS = new Set(['glb', 'fbx', 'obj', 'usdz', 'stl']);
23
+
24
+ function formatFromUrl(url: string): string {
25
+ const ext = gatewayFileName(url).split('.').pop()?.toLowerCase() ?? '';
26
+ return MESH_EXTS.has(ext) ? ext : '';
27
+ }
28
+
29
+ // Flatten a gateway task response's `data[]` into a flat url map:
30
+ // out[<format>] mesh urls keyed by format (glb/fbx/obj/usdz/stl)
31
+ // out.__thumbnail the rendered preview image
32
+ // out.__texture_<kind> texture maps (base_color/metallic/roughness/…)
33
+ export function extractGatewayUrls(resp: Record<string, unknown>): Record<string, string> {
34
+ const out: Record<string, string> = {};
35
+ const data = resp.data;
36
+ if (Array.isArray(data)) {
37
+ for (const raw of data) {
38
+ if (typeof raw !== 'object' || !raw) continue;
39
+ const item = raw as Record<string, unknown>;
40
+ const directUrl = item.url;
41
+ if (typeof directUrl === 'string' && directUrl) {
42
+ assignGatewayUrl(out, directUrl, String(item.type ?? ''), String(item.format ?? ''), item);
43
+ }
44
+ for (const [key, value] of Object.entries(item)) {
45
+ if (!key.endsWith('_url') || typeof value !== 'string' || !value) continue;
46
+ assignHunyuanStyleUrl(out, key, value);
47
+ }
48
+ }
49
+ }
50
+ for (const [key, value] of Object.entries(resp)) {
51
+ if (!key.endsWith('_url') || typeof value !== 'string' || !value) continue;
52
+ assignHunyuanStyleUrl(out, key, value);
53
+ }
54
+ return out;
55
+ }
56
+
57
+ function assignGatewayUrl(
58
+ out: Record<string, string>,
59
+ url: string,
60
+ type: string,
61
+ format: string,
62
+ item: Record<string, unknown>,
63
+ ): void {
64
+ const typ = type.toLowerCase();
65
+ const fmt = format.toLowerCase();
66
+ // Model binaries: gateway usually tags type:'mesh'. LiteLLM Hunyuan auto-rig
67
+ // tags the rigged FBX as type:'rigged_model' (format:'fbx') — still a model
68
+ // URL we must keep (else apply-motion loses FBX transport). Probed 2026-07-17.
69
+ if (typ === 'mesh' || typ === 'rigged_model' || typ === 'animated_model') {
70
+ // Rodin/Volcengine hyper3d-gen2 (live 2026-08-14): type=mesh, format=null,
71
+ // URL still ends in .glb. Empty format used to drop the only mesh.
72
+ const key = MESH_EXTS.has(fmt) ? fmt : formatFromUrl(url);
73
+ if (key) out[key] = url;
74
+ return;
75
+ }
76
+ // Defensive: some gateways omit type but set format to a mesh extension.
77
+ if (!typ && (fmt === 'fbx' || fmt === 'glb' || fmt === 'obj' || fmt === 'usdz' || fmt === 'stl')) {
78
+ out[fmt] = url;
79
+ return;
80
+ }
81
+
82
+ // Non-mesh entry: classify by output filename, because the gateway tags the
83
+ // render and every texture map alike as type:'preview'.
84
+ const name = gatewayFileName(url);
85
+ if (/^preview\./i.test(name)) {
86
+ // The real render. Wins over any earlier fallback thumbnail.
87
+ out.__thumbnail = url;
88
+ } else if (/^texture_/i.test(name)) {
89
+ // A PBR texture map — never the thumbnail. base_color feeds the optional
90
+ // texture sidefile; the rest are kept but currently unused downstream.
91
+ out[`__texture_${textureKindFromName(name)}`] = url;
92
+ } else if (type === 'texture') {
93
+ // Defensive: a future gateway that tags textures correctly.
94
+ const kind = String(item.texture_kind ?? format) || 'base_color';
95
+ out[`__texture_${kind}`] = url;
96
+ } else if (!out.__thumbnail && IMAGE_EXT.test(name)) {
97
+ // Fallback for gateways that name the render differently: take the first
98
+ // rendered image. Non-image outputs (e.g. .mtl) are ignored.
99
+ out.__thumbnail = url;
100
+ }
101
+ }
102
+
103
+ function assignHunyuanStyleUrl(out: Record<string, string>, key: string, url: string): void {
104
+ if (key === 'glb_url' || key.endsWith('_glb_url')) out.glb = url;
105
+ else if (key === 'fbx_url' || key.endsWith('_fbx_url')) out.fbx = url;
106
+ else if (key === 'obj_url' || key.endsWith('_obj_url')) out.obj = url;
107
+ else if (key === 'usdz_url' || key.endsWith('_usdz_url')) out.usdz = url;
108
+ else if (key === 'stl_url' || key.endsWith('_stl_url')) out.stl = url;
109
+ else if (key === 'thumbnail_url' || key === 'preview_url' || key === 'image_url') out.__thumbnail = url;
110
+ }
111
+
112
+ // Rig-specific URL extraction. The Meshy auto-rig gateway response carries
113
+ // FIVE mesh entries per format, and `extractGatewayUrls`' `out[format] = url`
114
+ // (last-wins) lets the final `Animation_*_withSkin_armature.glb` (a
115
+ // skeleton-only 288-face proxy, no mesh) overwrite `Character_output.glb` (the
116
+ // full rigged mesh). That made gen3d store a skeleton-only rigged_model.glb
117
+ // → viewport showed 288 faces, mesh invisible. Classify by Meshy output
118
+ // filename instead:
119
+ // Character_output.{glb,fbx} → the rigged model (mesh+skeleton)
120
+ // Animation_Walking_withSkin.{glb,fbx} → free walk clip (full mesh+anim)
121
+ // Animation_Running_withSkin.{glb,fbx} → free run clip (full mesh+anim)
122
+ // *_armature.{glb,fbx} → skeleton-only proxies (skipped)
123
+ // `extractGatewayUrls` is left untouched for generate/animate, whose responses
124
+ // carry one mesh per format and don't need this disambiguation.
125
+ export interface RigUrls {
126
+ glb: string | undefined;
127
+ fbx: string | undefined;
128
+ walkGlb: string | undefined;
129
+ walkFbx: string | undefined;
130
+ runGlb: string | undefined;
131
+ runFbx: string | undefined;
132
+ }
133
+
134
+ export function extractRigUrls(resp: Record<string, unknown>): RigUrls {
135
+ const out: RigUrls = {
136
+ glb: undefined,
137
+ fbx: undefined,
138
+ walkGlb: undefined,
139
+ walkFbx: undefined,
140
+ runGlb: undefined,
141
+ runFbx: undefined,
142
+ };
143
+ const data = resp.data;
144
+ if (!Array.isArray(data)) return out;
145
+ for (const raw of data) {
146
+ if (typeof raw !== 'object' || !raw) continue;
147
+ const item = raw as Record<string, unknown>;
148
+ const url = item.url;
149
+ if (typeof url !== 'string' || !url) continue;
150
+ if (String(item.type ?? '') !== 'mesh') continue;
151
+ const format = String(item.format ?? '');
152
+ if (format !== 'glb' && format !== 'fbx') continue;
153
+ const name =
154
+ typeof item.name === 'string' && item.name.trim() ? item.name.trim() : gatewayFileName(url);
155
+ const isGlb = format === 'glb';
156
+ if (/^Character_output\./i.test(name)) {
157
+ if (isGlb) out.glb = url;
158
+ else out.fbx = url;
159
+ } else if (/^Animation_Walking_withSkin\./i.test(name)) {
160
+ if (isGlb) out.walkGlb = url;
161
+ else out.walkFbx = url;
162
+ } else if (/^Animation_Running_withSkin\./i.test(name)) {
163
+ if (isGlb) out.runGlb = url;
164
+ else out.runFbx = url;
165
+ }
166
+ // _armature variants and anything else: ignored (skeleton-only, no mesh).
167
+ }
168
+ return out;
169
+ }
170
+
171
+ function gatewayFileName(url: string): string {
172
+ const path = url.split('?', 1)[0] ?? url;
173
+ return path.split('/').pop() ?? '';
174
+ }
175
+
176
+ // texture_0.png → base_color ; texture_0_metallic.png → metallic
177
+ function textureKindFromName(name: string): string {
178
+ const stem = name.replace(IMAGE_EXT, '');
179
+ const m = /^texture_\d+(?:_(.+))?$/i.exec(stem);
180
+ if (!m) return 'base_color';
181
+ return m[1] ? m[1].toLowerCase() : 'base_color';
182
+ }
@@ -0,0 +1,49 @@
1
+ // LiteLLM 3D gateway model ids — SSOT for Meshy / Hunyuan / Rodin / VISVISE.
2
+ // Hunyuan/Meshy verified 2026-07-16. Rodin/VISVISE verified 2026-08-14
3
+ // (docs/evidence/gateway-rodin-visvise-probe-2026-08-14.json).
4
+ // Older lab ids (hunyuan-3d-v3.1-*-wf, *-gamestudio, motion-retarget, …) are
5
+ // superseded by the shorter gateway aliases below.
6
+
7
+ export const MESHY_GATEWAY_MODELS = {
8
+ text: 'meshy-3d-text',
9
+ image: 'meshy-3d-image',
10
+ views: 'meshy-3d-multi-image',
11
+ refine: 'meshy-3d-text',
12
+ autoRig: 'meshy-3d-auto-rigging',
13
+ animation: 'meshy-3d-animation',
14
+ } as const;
15
+
16
+ export const HUNYUAN_GATEWAY_MODELS = {
17
+ text: 'hunyuan-3d-text',
18
+ image: 'hunyuan-3d-image',
19
+ views: 'hunyuan-3d-views',
20
+ autoRig: 'hunyuan-3d-rigging',
21
+ motion: 'hunyuan-3d-motion',
22
+ // Not listed on the current gateway catalog — keep ids for when ops adds them.
23
+ pose: 'hunyuan-3d-images-pose-standardization',
24
+ lowPoly: 'hunyuan-3d-low-poly-v1.5',
25
+ } as const;
26
+
27
+ export const RODIN_GATEWAY_MODELS = {
28
+ generate: 'rodin-3d',
29
+ } as const;
30
+
31
+ export const VISVISE_GATEWAY_MODELS = {
32
+ generate: 'visvise-3d',
33
+ mid: 'visvise-3d-mid',
34
+ low: 'visvise-3d-low',
35
+ pose: 'visvise-image-pose',
36
+ autoRig: 'visvise-rigging',
37
+ skinning: 'visvise-skinning',
38
+ textMotion: 'visvise-text-motion',
39
+ videoMotion: 'visvise-video-motion',
40
+ } as const;
41
+
42
+ export type VisviseQuality = 'standard' | 'mid' | 'low';
43
+
44
+ /** C1: visvise-3d-mid is image-only (`image_url` required). Text never uses mid. */
45
+ export function visviseGenerateModel(mode: 'text' | 'image', quality?: VisviseQuality): string {
46
+ if (mode === 'image' && quality === 'mid') return VISVISE_GATEWAY_MODELS.mid;
47
+ if (quality === 'low') return VISVISE_GATEWAY_MODELS.low;
48
+ return VISVISE_GATEWAY_MODELS.generate;
49
+ }