@vgai/fal-client-compat 0.1.0 → 0.1.1
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.
- package/package.json +1 -1
- package/src/mock.ts +203 -4
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@vgai/fal-client-compat",
|
|
3
3
|
"author": "Volter AI, Inc.",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.1",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"description": "Native @fal-ai/client execution across direct, VGAI-managed, and deterministic mock transports.",
|
|
8
8
|
"homepage": "https://github.com/volter-ai/vgai-engine#readme",
|
package/src/mock.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type {
|
|
|
4
4
|
HunyuanWorldImageToWorldOutput,
|
|
5
5
|
HunyuanWorldInput,
|
|
6
6
|
HunyuanWorldOutput,
|
|
7
|
+
MeshyV5RetextureInput,
|
|
8
|
+
MeshyV5RetextureOutput,
|
|
7
9
|
MinimaxHailuo02StandardTextToVideoInput,
|
|
8
10
|
MinimaxHailuo02StandardTextToVideoOutput,
|
|
9
11
|
NanoBanana2EditInput,
|
|
@@ -30,6 +32,7 @@ const HAILUO_TEXT_TO_VIDEO_ENDPOINT = 'fal-ai/minimax/hailuo-02/standard/text-to
|
|
|
30
32
|
const SEEDANCE_2_REFERENCE_TO_VIDEO_ENDPOINT = 'bytedance/seedance-2.0/reference-to-video';
|
|
31
33
|
const HUNYUAN_IMAGE_TO_WORLD_ENDPOINT = 'fal-ai/hunyuan_world/image-to-world';
|
|
32
34
|
const HUNYUAN_PANORAMA_ENDPOINT = 'fal-ai/hunyuan_world';
|
|
35
|
+
const MESHY_V5_RETEXTURE_ENDPOINT = 'fal-ai/meshy/v5/retexture';
|
|
33
36
|
const SUPPORTED_ENDPOINTS = new Set([
|
|
34
37
|
NANO_BANANA_ENDPOINT,
|
|
35
38
|
NANO_BANANA_EDIT_ENDPOINT,
|
|
@@ -41,6 +44,7 @@ const SUPPORTED_ENDPOINTS = new Set([
|
|
|
41
44
|
SEEDANCE_2_REFERENCE_TO_VIDEO_ENDPOINT,
|
|
42
45
|
HUNYUAN_IMAGE_TO_WORLD_ENDPOINT,
|
|
43
46
|
HUNYUAN_PANORAMA_ENDPOINT,
|
|
47
|
+
MESHY_V5_RETEXTURE_ENDPOINT,
|
|
44
48
|
]);
|
|
45
49
|
const EXECUTION_RECORDER = Symbol.for('vgai.generative-execution-recorder.v1');
|
|
46
50
|
|
|
@@ -64,7 +68,8 @@ type MockTask = {
|
|
|
64
68
|
| MinimaxHailuo02StandardTextToVideoOutput
|
|
65
69
|
| Seedance2VideoOutput
|
|
66
70
|
| HunyuanWorldImageToWorldOutput
|
|
67
|
-
| HunyuanWorldOutput
|
|
71
|
+
| HunyuanWorldOutput
|
|
72
|
+
| MeshyV5RetextureOutput;
|
|
68
73
|
};
|
|
69
74
|
|
|
70
75
|
// Four tiny, real VP8/WebM motion designs. Request hash chooses the visible
|
|
@@ -907,6 +912,194 @@ function createHunyuanPanoramaOutput(input: HunyuanWorldInput): HunyuanWorldOutp
|
|
|
907
912
|
};
|
|
908
913
|
}
|
|
909
914
|
|
|
915
|
+
function pad(bytes: Uint8Array, multiple: number, fill = 0): Uint8Array {
|
|
916
|
+
const paddedLength = Math.ceil(bytes.byteLength / multiple) * multiple;
|
|
917
|
+
if (paddedLength === bytes.byteLength) return bytes;
|
|
918
|
+
const output = new Uint8Array(paddedLength);
|
|
919
|
+
output.fill(fill);
|
|
920
|
+
output.set(bytes);
|
|
921
|
+
return output;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function uint32LittleEndian(value: number): Uint8Array {
|
|
925
|
+
const bytes = new Uint8Array(4);
|
|
926
|
+
new DataView(bytes.buffer).setUint32(0, value, true);
|
|
927
|
+
return bytes;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function glbChunk(type: number, data: Uint8Array): Uint8Array {
|
|
931
|
+
return concatBytes([uint32LittleEndian(data.byteLength), uint32LittleEndian(type), data]);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function floatBytes(values: Float32Array): Uint8Array {
|
|
935
|
+
return new Uint8Array(values.buffer, values.byteOffset, values.byteLength);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function uint16Bytes(values: Uint16Array): Uint8Array {
|
|
939
|
+
return new Uint8Array(values.buffer, values.byteOffset, values.byteLength);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function mockTextureImage(canonicalInput: string, hash: Uint8Array): Uint8Array {
|
|
943
|
+
const size = 64;
|
|
944
|
+
const pixels = new Uint8Array(size * size * 4);
|
|
945
|
+
fillBackground(pixels, size, size, hash);
|
|
946
|
+
drawText(pixels, size, size, 'MESHY', 4, 6, 1, [255, 255, 255, 255]);
|
|
947
|
+
return encodePng(size, size, pixels, canonicalInput);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* A retexture result the accept path can actually check: a hash-derived box
|
|
952
|
+
* carrying a TEXCOORD_0 set, a real PNG image, and a baseColorTexture. Its
|
|
953
|
+
* geometry is deliberately the mock's OWN and not the submitted model's — a
|
|
954
|
+
* mock that echoed the input topology would answer, with fabricated data, the
|
|
955
|
+
* one question only a real Meshy run can answer.
|
|
956
|
+
*/
|
|
957
|
+
function createMockRetexturedGlb(canonicalInput: string): Uint8Array {
|
|
958
|
+
const hash = hashBytes(canonicalInput);
|
|
959
|
+
const halfX = 0.35 + ((hash[0] ?? 0) / 255) * 0.45;
|
|
960
|
+
const halfY = 0.35 + ((hash[1] ?? 0) / 255) * 0.65;
|
|
961
|
+
const halfZ = 0.35 + ((hash[2] ?? 0) / 255) * 0.45;
|
|
962
|
+
const corners: Array<[number, number, number]> = [
|
|
963
|
+
[-halfX, -halfY, -halfZ],
|
|
964
|
+
[halfX, -halfY, -halfZ],
|
|
965
|
+
[halfX, -halfY, halfZ],
|
|
966
|
+
[-halfX, -halfY, halfZ],
|
|
967
|
+
[-halfX, halfY, -halfZ],
|
|
968
|
+
[halfX, halfY, -halfZ],
|
|
969
|
+
[halfX, halfY, halfZ],
|
|
970
|
+
[-halfX, halfY, halfZ],
|
|
971
|
+
];
|
|
972
|
+
const positions = new Float32Array(corners.flat());
|
|
973
|
+
const uvs = new Float32Array(
|
|
974
|
+
corners.map(([x, , z]) => [(x + halfX) / (2 * halfX), (z + halfZ) / (2 * halfZ)]).flat(),
|
|
975
|
+
);
|
|
976
|
+
const indices = new Uint16Array([
|
|
977
|
+
0, 2, 1, 0, 3, 2, 4, 5, 6, 4, 6, 7, 0, 1, 5, 0, 5, 4, 1, 2, 6, 1, 6, 5, 2, 3, 7, 2, 7, 6, 3, 0,
|
|
978
|
+
4, 3, 4, 7,
|
|
979
|
+
]);
|
|
980
|
+
const bufferViews: Array<Record<string, number>> = [];
|
|
981
|
+
const parts: Uint8Array[] = [];
|
|
982
|
+
let byteOffset = 0;
|
|
983
|
+
const addView = (bytes: Uint8Array, target?: number): number => {
|
|
984
|
+
const index = bufferViews.length;
|
|
985
|
+
bufferViews.push({
|
|
986
|
+
buffer: 0,
|
|
987
|
+
byteOffset,
|
|
988
|
+
byteLength: bytes.byteLength,
|
|
989
|
+
...(target ? { target } : {}),
|
|
990
|
+
});
|
|
991
|
+
const padded = pad(bytes, 4);
|
|
992
|
+
parts.push(padded);
|
|
993
|
+
byteOffset += padded.byteLength;
|
|
994
|
+
return index;
|
|
995
|
+
};
|
|
996
|
+
const positionView = addView(floatBytes(positions), 34962);
|
|
997
|
+
const uvView = addView(floatBytes(uvs), 34962);
|
|
998
|
+
const indexView = addView(uint16Bytes(indices), 34963);
|
|
999
|
+
const imageView = addView(mockTextureImage(canonicalInput, hash));
|
|
1000
|
+
const binary = concatBytes(parts);
|
|
1001
|
+
const document = {
|
|
1002
|
+
asset: {
|
|
1003
|
+
version: '2.0',
|
|
1004
|
+
generator: '@vgai/fal-client-compat deterministic retexture mock',
|
|
1005
|
+
extras: {
|
|
1006
|
+
vgaiMock: true,
|
|
1007
|
+
provider: 'fal',
|
|
1008
|
+
canonicalInput,
|
|
1009
|
+
hash: hex(hash),
|
|
1010
|
+
geometryIsSubstitute: true,
|
|
1011
|
+
},
|
|
1012
|
+
},
|
|
1013
|
+
scene: 0,
|
|
1014
|
+
scenes: [{ name: 'VGAI Meshy Retexture Mock', nodes: [0] }],
|
|
1015
|
+
nodes: [{ mesh: 0, name: 'VGAI MOCK retextured', extras: { vgaiMock: true } }],
|
|
1016
|
+
meshes: [
|
|
1017
|
+
{
|
|
1018
|
+
name: 'Hash-derived textured mock geometry',
|
|
1019
|
+
primitives: [{ attributes: { POSITION: 0, TEXCOORD_0: 1 }, indices: 2, material: 0 }],
|
|
1020
|
+
},
|
|
1021
|
+
],
|
|
1022
|
+
materials: [
|
|
1023
|
+
{
|
|
1024
|
+
name: 'Hash-derived base color texture',
|
|
1025
|
+
pbrMetallicRoughness: {
|
|
1026
|
+
baseColorTexture: { index: 0, texCoord: 0 },
|
|
1027
|
+
metallicFactor: 0,
|
|
1028
|
+
roughnessFactor: 0.8,
|
|
1029
|
+
},
|
|
1030
|
+
},
|
|
1031
|
+
],
|
|
1032
|
+
textures: [{ source: 0, sampler: 0 }],
|
|
1033
|
+
samplers: [{ magFilter: 9729, minFilter: 9987, wrapS: 10497, wrapT: 10497 }],
|
|
1034
|
+
images: [{ bufferView: imageView, mimeType: 'image/png', name: 'vgai-mock-basecolor' }],
|
|
1035
|
+
accessors: [
|
|
1036
|
+
{
|
|
1037
|
+
bufferView: positionView,
|
|
1038
|
+
componentType: 5126,
|
|
1039
|
+
count: corners.length,
|
|
1040
|
+
type: 'VEC3',
|
|
1041
|
+
min: [-halfX, -halfY, -halfZ],
|
|
1042
|
+
max: [halfX, halfY, halfZ],
|
|
1043
|
+
},
|
|
1044
|
+
{ bufferView: uvView, componentType: 5126, count: corners.length, type: 'VEC2' },
|
|
1045
|
+
{ bufferView: indexView, componentType: 5123, count: indices.length, type: 'SCALAR' },
|
|
1046
|
+
],
|
|
1047
|
+
bufferViews,
|
|
1048
|
+
buffers: [{ byteLength: binary.byteLength }],
|
|
1049
|
+
};
|
|
1050
|
+
const json = pad(new TextEncoder().encode(JSON.stringify(document)), 4, 0x20);
|
|
1051
|
+
const jsonChunk = glbChunk(0x4e4f534a, json);
|
|
1052
|
+
const binaryChunk = glbChunk(0x004e4942, binary);
|
|
1053
|
+
return concatBytes([
|
|
1054
|
+
uint32LittleEndian(0x46546c67),
|
|
1055
|
+
uint32LittleEndian(2),
|
|
1056
|
+
uint32LittleEndian(12 + jsonChunk.byteLength + binaryChunk.byteLength),
|
|
1057
|
+
jsonChunk,
|
|
1058
|
+
binaryChunk,
|
|
1059
|
+
]);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function createMeshyRetextureOutput(input: MeshyV5RetextureInput): MeshyV5RetextureOutput {
|
|
1063
|
+
if (!input || typeof input.model_url !== 'string' || input.model_url.trim().length === 0) {
|
|
1064
|
+
throw new Error(
|
|
1065
|
+
'fal mock: fal-ai/meshy/v5/retexture requires input.model_url as a non-empty URL or data URI.',
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
const styled =
|
|
1069
|
+
(typeof input.text_style_prompt === 'string' && input.text_style_prompt.trim().length > 0) ||
|
|
1070
|
+
(typeof input.image_style_url === 'string' && input.image_style_url.trim().length > 0);
|
|
1071
|
+
if (!styled) {
|
|
1072
|
+
throw new Error(
|
|
1073
|
+
'fal mock: fal-ai/meshy/v5/retexture requires input.text_style_prompt or input.image_style_url.',
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
// The submitted model is usually a multi-megabyte data URI. Identify it by
|
|
1077
|
+
// digest so the deterministic identity stays cheap and the embedded record
|
|
1078
|
+
// stays small, while still differing for different models.
|
|
1079
|
+
const canonicalInput = stableJson({
|
|
1080
|
+
endpoint: MESHY_V5_RETEXTURE_ENDPOINT,
|
|
1081
|
+
input: { ...input, model_url: `sha:${hex(hashBytes(input.model_url))}` },
|
|
1082
|
+
});
|
|
1083
|
+
const glb = createMockRetexturedGlb(canonicalInput);
|
|
1084
|
+
const digest = hex(hashBytes(base64(glb))).slice(0, 16);
|
|
1085
|
+
const model = {
|
|
1086
|
+
url: `data:application/octet-stream;base64,${base64(glb)}`,
|
|
1087
|
+
content_type: 'model/gltf-binary',
|
|
1088
|
+
file_name: `vgai-mock-${digest}.glb`,
|
|
1089
|
+
file_size: glb.byteLength,
|
|
1090
|
+
};
|
|
1091
|
+
return {
|
|
1092
|
+
model_glb: model,
|
|
1093
|
+
model_urls: { glb: model },
|
|
1094
|
+
...(typeof input.text_style_prompt === 'string'
|
|
1095
|
+
? { text_style_prompt: input.text_style_prompt }
|
|
1096
|
+
: {}),
|
|
1097
|
+
...(typeof input.image_style_url === 'string'
|
|
1098
|
+
? { image_style_url: input.image_style_url }
|
|
1099
|
+
: {}),
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
|
|
910
1103
|
function createOutput(
|
|
911
1104
|
endpoint: string,
|
|
912
1105
|
input:
|
|
@@ -919,7 +1112,8 @@ function createOutput(
|
|
|
919
1112
|
| MinimaxHailuo02StandardTextToVideoInput
|
|
920
1113
|
| Seedance2R2VInput
|
|
921
1114
|
| HunyuanWorldImageToWorldInput
|
|
922
|
-
| HunyuanWorldInput
|
|
1115
|
+
| HunyuanWorldInput
|
|
1116
|
+
| MeshyV5RetextureInput,
|
|
923
1117
|
):
|
|
924
1118
|
| NanoBananaOutput
|
|
925
1119
|
| StableAudioOutput
|
|
@@ -927,7 +1121,8 @@ function createOutput(
|
|
|
927
1121
|
| MinimaxHailuo02StandardTextToVideoOutput
|
|
928
1122
|
| Seedance2VideoOutput
|
|
929
1123
|
| HunyuanWorldImageToWorldOutput
|
|
930
|
-
| HunyuanWorldOutput
|
|
1124
|
+
| HunyuanWorldOutput
|
|
1125
|
+
| MeshyV5RetextureOutput {
|
|
931
1126
|
if (
|
|
932
1127
|
endpoint === NANO_BANANA_ENDPOINT ||
|
|
933
1128
|
endpoint === NANO_BANANA_EDIT_ENDPOINT ||
|
|
@@ -954,6 +1149,9 @@ function createOutput(
|
|
|
954
1149
|
if (endpoint === HUNYUAN_PANORAMA_ENDPOINT) {
|
|
955
1150
|
return createHunyuanPanoramaOutput(input as HunyuanWorldInput);
|
|
956
1151
|
}
|
|
1152
|
+
if (endpoint === MESHY_V5_RETEXTURE_ENDPOINT) {
|
|
1153
|
+
return createMeshyRetextureOutput(input as MeshyV5RetextureInput);
|
|
1154
|
+
}
|
|
957
1155
|
throw new Error(`fal mock: endpoint ${JSON.stringify(endpoint)} is not implemented.`);
|
|
958
1156
|
}
|
|
959
1157
|
|
|
@@ -993,7 +1191,8 @@ function submitTask(
|
|
|
993
1191
|
| MinimaxHailuo02StandardTextToVideoInput
|
|
994
1192
|
| Seedance2R2VInput
|
|
995
1193
|
| HunyuanWorldImageToWorldInput
|
|
996
|
-
| HunyuanWorldInput
|
|
1194
|
+
| HunyuanWorldInput
|
|
1195
|
+
| MeshyV5RetextureInput,
|
|
997
1196
|
);
|
|
998
1197
|
const requestId = `mock_${hex(hashBytes(stableJson({ endpoint, body }))).slice(0, 24)}`;
|
|
999
1198
|
tasks.set(requestId, { endpoint, output });
|