@tanstack/ai-worldlabs 0.0.0 → 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.
- package/LICENSE +21 -0
- package/README.md +42 -2
- package/dist/esm/adapters/world.d.ts +38 -0
- package/dist/esm/adapters/world.js +314 -0
- package/dist/esm/adapters/world.js.map +1 -0
- package/dist/esm/byok.d.ts +1 -0
- package/dist/esm/byok.js +11 -0
- package/dist/esm/byok.js.map +1 -0
- package/dist/esm/generated/client/client.gen.d.ts +2 -0
- package/dist/esm/generated/client/client.gen.js +174 -0
- package/dist/esm/generated/client/client.gen.js.map +1 -0
- package/dist/esm/generated/client/index.d.ts +10 -0
- package/dist/esm/generated/client/index.js +5 -0
- package/dist/esm/generated/client/types.gen.d.ts +120 -0
- package/dist/esm/generated/client/utils.gen.d.ts +37 -0
- package/dist/esm/generated/client/utils.gen.js +177 -0
- package/dist/esm/generated/client/utils.gen.js.map +1 -0
- package/dist/esm/generated/client.d.ts +1 -0
- package/dist/esm/generated/client.gen.d.ts +12 -0
- package/dist/esm/generated/client.gen.js +9 -0
- package/dist/esm/generated/client.gen.js.map +1 -0
- package/dist/esm/generated/client.js +5 -0
- package/dist/esm/generated/core/auth.gen.d.ts +25 -0
- package/dist/esm/generated/core/auth.gen.js +12 -0
- package/dist/esm/generated/core/auth.gen.js.map +1 -0
- package/dist/esm/generated/core/bodySerializer.gen.d.ts +25 -0
- package/dist/esm/generated/core/bodySerializer.gen.js +6 -0
- package/dist/esm/generated/core/bodySerializer.gen.js.map +1 -0
- package/dist/esm/generated/core/params.gen.d.ts +43 -0
- package/dist/esm/generated/core/params.gen.js +9 -0
- package/dist/esm/generated/core/params.gen.js.map +1 -0
- package/dist/esm/generated/core/pathSerializer.gen.d.ts +33 -0
- package/dist/esm/generated/core/pathSerializer.gen.js +82 -0
- package/dist/esm/generated/core/pathSerializer.gen.js.map +1 -0
- package/dist/esm/generated/core/queryKeySerializer.gen.d.ts +18 -0
- package/dist/esm/generated/core/serverSentEvents.gen.d.ts +71 -0
- package/dist/esm/generated/core/serverSentEvents.gen.js +96 -0
- package/dist/esm/generated/core/serverSentEvents.gen.js.map +1 -0
- package/dist/esm/generated/core/types.gen.d.ts +83 -0
- package/dist/esm/generated/core/utils.gen.d.ts +19 -0
- package/dist/esm/generated/core/utils.gen.js +78 -0
- package/dist/esm/generated/core/utils.gen.js.map +1 -0
- package/dist/esm/generated/index.d.ts +2 -0
- package/dist/esm/generated/sdk.gen.d.ts +275 -0
- package/dist/esm/generated/sdk.gen.js +79 -0
- package/dist/esm/generated/sdk.gen.js.map +1 -0
- package/dist/esm/generated/types.gen.d.ts +1610 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/model-meta.d.ts +63 -0
- package/dist/esm/model-meta.js +17 -0
- package/dist/esm/model-meta.js.map +1 -0
- package/dist/esm/utils/client.d.ts +16 -0
- package/dist/esm/utils/client.js +35 -0
- package/dist/esm/utils/client.js.map +1 -0
- package/package.json +55 -6
- package/src/adapters/world.ts +535 -0
- package/src/byok.ts +7 -0
- package/src/generated/client/client.gen.ts +301 -0
- package/src/generated/client/index.ts +27 -0
- package/src/generated/client/types.gen.ts +247 -0
- package/src/generated/client/utils.gen.ts +332 -0
- package/src/generated/client.gen.ts +26 -0
- package/src/generated/client.ts +3 -0
- package/src/generated/core/auth.gen.ts +48 -0
- package/src/generated/core/bodySerializer.gen.ts +96 -0
- package/src/generated/core/params.gen.ts +186 -0
- package/src/generated/core/pathSerializer.gen.ts +186 -0
- package/src/generated/core/queryKeySerializer.gen.ts +139 -0
- package/src/generated/core/serverSentEvents.gen.ts +265 -0
- package/src/generated/core/types.gen.ts +124 -0
- package/src/generated/core/utils.gen.ts +146 -0
- package/src/generated/index.ts +113 -0
- package/src/generated/sdk.gen.ts +531 -0
- package/src/generated/types.gen.ts +1767 -0
- package/src/index.ts +20 -0
- package/src/model-meta.ts +79 -0
- package/src/utils/client.ts +50 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import { BaseWorldAdapter } from '@tanstack/ai/adapters'
|
|
2
|
+
import {
|
|
3
|
+
generateWorldMarbleV1WorldsGeneratePost,
|
|
4
|
+
getOperationMarbleV1OperationsOperationIdGet,
|
|
5
|
+
} from '../generated/sdk.gen'
|
|
6
|
+
import {
|
|
7
|
+
createWorldLabsClient,
|
|
8
|
+
getWorldLabsApiKeyFromEnv,
|
|
9
|
+
} from '../utils/client'
|
|
10
|
+
import type {
|
|
11
|
+
WorldGenerationAssets,
|
|
12
|
+
WorldGenerationOptions,
|
|
13
|
+
WorldGenerationResult,
|
|
14
|
+
} from '@tanstack/ai'
|
|
15
|
+
import type { Client } from '../generated/client'
|
|
16
|
+
import type {
|
|
17
|
+
GenerateWorldResponse,
|
|
18
|
+
GetOperationResponseUnionWorldPanoDepthToRgbResultExportWorldResult,
|
|
19
|
+
World,
|
|
20
|
+
WorldsGenerateRequest,
|
|
21
|
+
} from '../generated/types.gen'
|
|
22
|
+
import type {
|
|
23
|
+
WorldLabsMediaRef,
|
|
24
|
+
WorldLabsWorldModel,
|
|
25
|
+
WorldLabsWorldProviderOptions,
|
|
26
|
+
} from '../model-meta'
|
|
27
|
+
import type { WorldLabsClientConfig } from '../utils/client'
|
|
28
|
+
|
|
29
|
+
export type { WorldLabsClientConfig }
|
|
30
|
+
|
|
31
|
+
type WorldOperation =
|
|
32
|
+
| GenerateWorldResponse
|
|
33
|
+
| GetOperationResponseUnionWorldPanoDepthToRgbResultExportWorldResult
|
|
34
|
+
|
|
35
|
+
const DEFAULT_POLL_INTERVAL_MS = 2_000
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* World Labs Marble adapter. Starts a world generation job and, by default,
|
|
39
|
+
* polls until the world is ready. `modelOptions.wait === false` returns
|
|
40
|
+
* `operationId` at once. A later `generateWorld` call cannot resume that id.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* import { generateWorld } from '@tanstack/ai'
|
|
45
|
+
* import { worldlabsWorld } from '@tanstack/ai-worldlabs'
|
|
46
|
+
*
|
|
47
|
+
* const world = await generateWorld({
|
|
48
|
+
* adapter: worldlabsWorld('marble-1.1'),
|
|
49
|
+
* prompt: 'A mystical forest with glowing mushrooms',
|
|
50
|
+
* })
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export class WorldLabsWorldAdapter<
|
|
54
|
+
TModel extends WorldLabsWorldModel,
|
|
55
|
+
> extends BaseWorldAdapter<TModel, WorldLabsWorldProviderOptions> {
|
|
56
|
+
readonly name = 'worldlabs' as const
|
|
57
|
+
private readonly client: Client
|
|
58
|
+
|
|
59
|
+
constructor(model: TModel, config: WorldLabsClientConfig = {}) {
|
|
60
|
+
super(model, {})
|
|
61
|
+
this.client = createWorldLabsClient(config).client
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async createWorld(
|
|
65
|
+
options: WorldGenerationOptions<WorldLabsWorldProviderOptions>,
|
|
66
|
+
): Promise<WorldGenerationResult> {
|
|
67
|
+
const { logger, prompt, abortSignal, modelOptions } = options
|
|
68
|
+
|
|
69
|
+
logger.request(
|
|
70
|
+
`activity=generateWorld provider=worldlabs model=${this.model}`,
|
|
71
|
+
{ provider: 'worldlabs', model: this.model },
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const started = await this.startGeneration({
|
|
76
|
+
prompt,
|
|
77
|
+
modelOptions,
|
|
78
|
+
abortSignal,
|
|
79
|
+
})
|
|
80
|
+
requireOperationId(started)
|
|
81
|
+
|
|
82
|
+
if (started.done || started.error) {
|
|
83
|
+
return logWorldResult(
|
|
84
|
+
logger,
|
|
85
|
+
this.model,
|
|
86
|
+
completedResult({
|
|
87
|
+
prompt,
|
|
88
|
+
model: this.model,
|
|
89
|
+
operation: started,
|
|
90
|
+
}),
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (modelOptions?.wait === false) {
|
|
95
|
+
return logWorldResult(
|
|
96
|
+
logger,
|
|
97
|
+
this.model,
|
|
98
|
+
pendingResult({
|
|
99
|
+
prompt,
|
|
100
|
+
model: this.model,
|
|
101
|
+
operation: started,
|
|
102
|
+
}),
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const finished = await this.pollUntilDone({
|
|
107
|
+
operationId: started.operation_id,
|
|
108
|
+
abortSignal,
|
|
109
|
+
pollIntervalMs: modelOptions?.pollIntervalMs,
|
|
110
|
+
})
|
|
111
|
+
return logWorldResult(
|
|
112
|
+
logger,
|
|
113
|
+
this.model,
|
|
114
|
+
completedResult({
|
|
115
|
+
prompt,
|
|
116
|
+
model: this.model,
|
|
117
|
+
operation: finished,
|
|
118
|
+
}),
|
|
119
|
+
)
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (!isAbortError(error)) {
|
|
122
|
+
logger.errors('worldlabs.createWorld failed', {
|
|
123
|
+
error,
|
|
124
|
+
source: 'worldlabs.createWorld',
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
throw error
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async startGeneration(args: {
|
|
132
|
+
prompt: string
|
|
133
|
+
modelOptions?: WorldLabsWorldProviderOptions
|
|
134
|
+
abortSignal?: AbortSignal
|
|
135
|
+
}): Promise<GenerateWorldResponse> {
|
|
136
|
+
const body: WorldsGenerateRequest = {
|
|
137
|
+
model: this.model,
|
|
138
|
+
world_prompt: buildWorldPrompt(args.prompt, args.modelOptions),
|
|
139
|
+
}
|
|
140
|
+
if (args.modelOptions?.displayName !== undefined) {
|
|
141
|
+
body.display_name = args.modelOptions.displayName
|
|
142
|
+
}
|
|
143
|
+
if (args.modelOptions?.seed !== undefined) {
|
|
144
|
+
body.seed = args.modelOptions.seed
|
|
145
|
+
}
|
|
146
|
+
if (args.modelOptions?.tags !== undefined) {
|
|
147
|
+
body.tags = args.modelOptions.tags
|
|
148
|
+
}
|
|
149
|
+
if (args.modelOptions?.permission !== undefined) {
|
|
150
|
+
body.permission = args.modelOptions.permission
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const result = await generateWorldMarbleV1WorldsGeneratePost({
|
|
154
|
+
client: this.client,
|
|
155
|
+
body,
|
|
156
|
+
...(args.abortSignal ? { signal: args.abortSignal } : {}),
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
if (result.error || !result.data) {
|
|
160
|
+
throw worldLabsRequestError(
|
|
161
|
+
'World Labs generate request failed',
|
|
162
|
+
result.response,
|
|
163
|
+
result.error,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return result.data
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private async pollUntilDone(args: {
|
|
171
|
+
operationId: string
|
|
172
|
+
abortSignal?: AbortSignal
|
|
173
|
+
pollIntervalMs?: number
|
|
174
|
+
}): Promise<WorldOperation> {
|
|
175
|
+
const interval =
|
|
176
|
+
args.pollIntervalMs && args.pollIntervalMs > 0
|
|
177
|
+
? args.pollIntervalMs
|
|
178
|
+
: DEFAULT_POLL_INTERVAL_MS
|
|
179
|
+
|
|
180
|
+
for (;;) {
|
|
181
|
+
await sleep(interval, args.abortSignal)
|
|
182
|
+
const result = await getOperationMarbleV1OperationsOperationIdGet({
|
|
183
|
+
client: this.client,
|
|
184
|
+
path: { operation_id: args.operationId },
|
|
185
|
+
...(args.abortSignal ? { signal: args.abortSignal } : {}),
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
if (result.error || !result.data) {
|
|
189
|
+
throw worldLabsRequestError(
|
|
190
|
+
'World Labs operation poll failed',
|
|
191
|
+
result.response,
|
|
192
|
+
result.error,
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (typeof result.data.done !== 'boolean') {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`World Labs operation ${args.operationId} returned an invalid poll payload.`,
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (result.data.done) {
|
|
203
|
+
return result.data
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Create a World Labs world adapter. Reads `WORLDLABS_API_KEY` when `apiKey`
|
|
211
|
+
* is omitted.
|
|
212
|
+
*/
|
|
213
|
+
export function worldlabsWorld<TModel extends WorldLabsWorldModel>(
|
|
214
|
+
model: TModel,
|
|
215
|
+
config?: WorldLabsClientConfig,
|
|
216
|
+
): WorldLabsWorldAdapter<TModel> {
|
|
217
|
+
const apiKey = config?.apiKey ?? getWorldLabsApiKeyFromEnv()
|
|
218
|
+
return createWorldLabsWorld(model, apiKey, config)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Create a World Labs world adapter with an explicit API key.
|
|
223
|
+
*/
|
|
224
|
+
export function createWorldLabsWorld<TModel extends WorldLabsWorldModel>(
|
|
225
|
+
model: TModel,
|
|
226
|
+
apiKey: string,
|
|
227
|
+
config?: Omit<WorldLabsClientConfig, 'apiKey'>,
|
|
228
|
+
): WorldLabsWorldAdapter<TModel> {
|
|
229
|
+
return new WorldLabsWorldAdapter(model, { ...config, apiKey })
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function buildWorldPrompt(
|
|
233
|
+
prompt: string,
|
|
234
|
+
options?: WorldLabsWorldProviderOptions,
|
|
235
|
+
): WorldsGenerateRequest['world_prompt'] {
|
|
236
|
+
if (
|
|
237
|
+
[options?.image, options?.images, options?.video].filter(Boolean).length > 1
|
|
238
|
+
) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
'World Labs: pass only one of modelOptions.image, modelOptions.images, or modelOptions.video.',
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const textPrompt = prompt.trim().length > 0 ? prompt : undefined
|
|
245
|
+
const shared = {
|
|
246
|
+
...(textPrompt !== undefined ? { text_prompt: textPrompt } : {}),
|
|
247
|
+
...(options?.disableRecaption !== undefined
|
|
248
|
+
? { disable_recaption: options.disableRecaption }
|
|
249
|
+
: {}),
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (options?.image) {
|
|
253
|
+
return {
|
|
254
|
+
type: 'image',
|
|
255
|
+
image_prompt: toContentRef(options.image),
|
|
256
|
+
...shared,
|
|
257
|
+
...(options.isPano !== undefined ? { is_pano: options.isPano } : {}),
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (options?.images) {
|
|
262
|
+
return {
|
|
263
|
+
type: 'multi-image',
|
|
264
|
+
multi_image_prompt: options.images.map((image) => ({
|
|
265
|
+
content: toContentRef(image),
|
|
266
|
+
...(image.azimuth !== undefined ? { azimuth: image.azimuth } : {}),
|
|
267
|
+
})),
|
|
268
|
+
...shared,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (options?.video) {
|
|
273
|
+
return {
|
|
274
|
+
type: 'video',
|
|
275
|
+
video_prompt: toContentRef(options.video),
|
|
276
|
+
...shared,
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
type: 'text',
|
|
282
|
+
text_prompt: prompt,
|
|
283
|
+
...(options?.disableRecaption !== undefined
|
|
284
|
+
? { disable_recaption: options.disableRecaption }
|
|
285
|
+
: {}),
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function toContentRef(
|
|
290
|
+
ref: WorldLabsMediaRef,
|
|
291
|
+
):
|
|
292
|
+
| { source: 'uri'; uri: string }
|
|
293
|
+
| { source: 'media_asset'; media_asset_id: string }
|
|
294
|
+
| { source: 'data_base64'; data_base64: string; extension?: string } {
|
|
295
|
+
const fields = [ref.uri, ref.mediaAssetId, ref.dataBase64].filter(
|
|
296
|
+
(value) => typeof value === 'string' && value.length > 0,
|
|
297
|
+
)
|
|
298
|
+
if (fields.length !== 1) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
'World Labs media ref needs exactly one of uri, mediaAssetId, or dataBase64.',
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
if (ref.uri) return { source: 'uri', uri: ref.uri }
|
|
304
|
+
if (ref.mediaAssetId) {
|
|
305
|
+
return { source: 'media_asset', media_asset_id: ref.mediaAssetId }
|
|
306
|
+
}
|
|
307
|
+
if (!ref.dataBase64) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
'World Labs media ref needs exactly one of uri, mediaAssetId, or dataBase64.',
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
source: 'data_base64',
|
|
314
|
+
data_base64: ref.dataBase64,
|
|
315
|
+
...(ref.extension !== undefined ? { extension: ref.extension } : {}),
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function pendingResult(args: {
|
|
320
|
+
prompt: string
|
|
321
|
+
model: string
|
|
322
|
+
operation: GenerateWorldResponse
|
|
323
|
+
}): WorldGenerationResult {
|
|
324
|
+
const expiresAt = expiresAtMs(args.operation.expires_at)
|
|
325
|
+
return {
|
|
326
|
+
id: args.operation.operation_id,
|
|
327
|
+
model: args.model,
|
|
328
|
+
prompt: args.prompt,
|
|
329
|
+
status: 'waiting',
|
|
330
|
+
operationId: args.operation.operation_id,
|
|
331
|
+
worldId: worldIdFromMetadata(args.operation.metadata),
|
|
332
|
+
...(expiresAt !== undefined ? { expiresAt } : {}),
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function completedResult(args: {
|
|
337
|
+
prompt: string
|
|
338
|
+
model: string
|
|
339
|
+
operation: WorldOperation
|
|
340
|
+
}): WorldGenerationResult {
|
|
341
|
+
if (args.operation.error) {
|
|
342
|
+
const raw = args.operation.error.message?.trim()
|
|
343
|
+
const code = args.operation.error.code
|
|
344
|
+
throw new Error(
|
|
345
|
+
raw ||
|
|
346
|
+
`World Labs operation failed${code != null ? ` (code ${code})` : ''}`,
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const world = worldFromResponse(args.operation.response)
|
|
351
|
+
if (!world) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
`World Labs operation ${args.operation.operation_id} completed without a world.`,
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const assets = mapAssets(world.assets)
|
|
358
|
+
const expiresAt = expiresAtMs(args.operation.expires_at)
|
|
359
|
+
return {
|
|
360
|
+
id: world.world_id,
|
|
361
|
+
model: world.model ?? args.model,
|
|
362
|
+
prompt: args.prompt,
|
|
363
|
+
status: 'ready',
|
|
364
|
+
url: world.world_marble_url,
|
|
365
|
+
worldId: world.world_id,
|
|
366
|
+
operationId: args.operation.operation_id,
|
|
367
|
+
...(assets ? { assets } : {}),
|
|
368
|
+
...(expiresAt !== undefined ? { expiresAt } : {}),
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function worldFromResponse(
|
|
373
|
+
response: WorldOperation['response'],
|
|
374
|
+
): World | undefined {
|
|
375
|
+
if (!response || typeof response !== 'object') return undefined
|
|
376
|
+
if (!('world_id' in response) || !('world_marble_url' in response)) {
|
|
377
|
+
return undefined
|
|
378
|
+
}
|
|
379
|
+
const worldId = response.world_id
|
|
380
|
+
const url = response.world_marble_url
|
|
381
|
+
if (typeof worldId !== 'string' || worldId.length === 0) return undefined
|
|
382
|
+
if (typeof url !== 'string' || url.length === 0) return undefined
|
|
383
|
+
return response as World
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function requireOperationId(data: GenerateWorldResponse): void {
|
|
387
|
+
if (typeof data.operation_id !== 'string' || data.operation_id.length === 0) {
|
|
388
|
+
throw new Error('World Labs generate request failed: missing operation_id')
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function logWorldResult(
|
|
393
|
+
logger: WorldGenerationOptions['logger'],
|
|
394
|
+
model: string,
|
|
395
|
+
result: WorldGenerationResult,
|
|
396
|
+
): WorldGenerationResult {
|
|
397
|
+
logger.output(`activity=generateWorld provider=worldlabs model=${model}`, {
|
|
398
|
+
model,
|
|
399
|
+
status: result.status,
|
|
400
|
+
})
|
|
401
|
+
return result
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function isAbortError(error: unknown): boolean {
|
|
405
|
+
return (
|
|
406
|
+
error instanceof Error &&
|
|
407
|
+
(error.name === 'AbortError' ||
|
|
408
|
+
error.message === 'The operation was aborted')
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function worldIdFromMetadata(
|
|
413
|
+
metadata: GenerateWorldResponse['metadata'],
|
|
414
|
+
): string | undefined {
|
|
415
|
+
if (!metadata || typeof metadata !== 'object') return undefined
|
|
416
|
+
const worldId = metadata.world_id
|
|
417
|
+
return typeof worldId === 'string' && worldId.length > 0 ? worldId : undefined
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function nonEmpty<T extends object>(value: T): T | undefined {
|
|
421
|
+
return Object.keys(value).length > 0 ? value : undefined
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function mapAssets(assets: World['assets']): WorldGenerationAssets | undefined {
|
|
425
|
+
if (!assets) return undefined
|
|
426
|
+
const splats = mapSplatAssets(assets.splats)
|
|
427
|
+
const mesh = mapMeshAssets(assets.mesh)
|
|
428
|
+
return nonEmpty({
|
|
429
|
+
...(assets.caption ? { caption: assets.caption } : {}),
|
|
430
|
+
...(assets.thumbnail_url ? { thumbnailUrl: assets.thumbnail_url } : {}),
|
|
431
|
+
...(splats ? { splats } : {}),
|
|
432
|
+
...(mesh ? { mesh } : {}),
|
|
433
|
+
...(assets.imagery?.pano_url
|
|
434
|
+
? { imagery: { panoUrl: assets.imagery.pano_url } }
|
|
435
|
+
: {}),
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function mapSplatAssets(
|
|
440
|
+
splats: NonNullable<World['assets']>['splats'],
|
|
441
|
+
): WorldGenerationAssets['splats'] | undefined {
|
|
442
|
+
if (!splats) return undefined
|
|
443
|
+
return nonEmpty({
|
|
444
|
+
...(splats.spz_urls && Object.keys(splats.spz_urls).length > 0
|
|
445
|
+
? { spzUrls: splats.spz_urls }
|
|
446
|
+
: {}),
|
|
447
|
+
...(splats.semantics_metadata?.metric_scale_factor != null
|
|
448
|
+
? { metricScaleFactor: splats.semantics_metadata.metric_scale_factor }
|
|
449
|
+
: {}),
|
|
450
|
+
...(splats.semantics_metadata?.ground_plane_offset != null
|
|
451
|
+
? { groundPlaneOffset: splats.semantics_metadata.ground_plane_offset }
|
|
452
|
+
: {}),
|
|
453
|
+
})
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function mapMeshAssets(
|
|
457
|
+
mesh: NonNullable<World['assets']>['mesh'],
|
|
458
|
+
): WorldGenerationAssets['mesh'] | undefined {
|
|
459
|
+
if (!mesh) return undefined
|
|
460
|
+
return nonEmpty({
|
|
461
|
+
...(mesh.collider_mesh_url
|
|
462
|
+
? { colliderMeshUrl: mesh.collider_mesh_url }
|
|
463
|
+
: {}),
|
|
464
|
+
...(mesh.hq_mesh_url ? { hqMeshUrl: mesh.hq_mesh_url } : {}),
|
|
465
|
+
...(mesh.full_res_mesh_url
|
|
466
|
+
? { fullResMeshUrl: mesh.full_res_mesh_url }
|
|
467
|
+
: {}),
|
|
468
|
+
})
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function expiresAtMs(value: string | null | undefined): number | undefined {
|
|
472
|
+
if (!value) return undefined
|
|
473
|
+
const parsed = Date.parse(value)
|
|
474
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function worldLabsRequestError(
|
|
478
|
+
prefix: string,
|
|
479
|
+
response: Response | undefined,
|
|
480
|
+
error: unknown,
|
|
481
|
+
): Error {
|
|
482
|
+
const status = response
|
|
483
|
+
? `${response.status}${response.statusText ? ` ${response.statusText}` : ''}`
|
|
484
|
+
: undefined
|
|
485
|
+
const detail = errorDetail(error)
|
|
486
|
+
return new Error(
|
|
487
|
+
`${prefix}${status ? ` (${status})` : ''}${detail ? `: ${detail}` : ''}`,
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function errorDetail(error: unknown): string | undefined {
|
|
492
|
+
if (typeof error === 'string' && error.length > 0) return error
|
|
493
|
+
if (typeof error === 'object' && error !== null) {
|
|
494
|
+
const record = error as { detail?: unknown; message?: unknown }
|
|
495
|
+
if (typeof record.detail === 'string' && record.detail.length > 0) {
|
|
496
|
+
return record.detail
|
|
497
|
+
}
|
|
498
|
+
if (typeof record.message === 'string' && record.message.length > 0) {
|
|
499
|
+
return record.message
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
return JSON.stringify(error)
|
|
503
|
+
} catch {
|
|
504
|
+
return undefined
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return undefined
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
511
|
+
if (!signal) {
|
|
512
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
513
|
+
}
|
|
514
|
+
if (signal.aborted) {
|
|
515
|
+
return Promise.reject(abortError(signal))
|
|
516
|
+
}
|
|
517
|
+
return new Promise((resolve, reject) => {
|
|
518
|
+
const timer = setTimeout(() => {
|
|
519
|
+
signal.removeEventListener('abort', onAbort)
|
|
520
|
+
resolve()
|
|
521
|
+
}, ms)
|
|
522
|
+
const onAbort = () => {
|
|
523
|
+
clearTimeout(timer)
|
|
524
|
+
reject(abortError(signal))
|
|
525
|
+
}
|
|
526
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
527
|
+
})
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function abortError(signal?: AbortSignal): Error {
|
|
531
|
+
if (signal?.reason instanceof Error) return signal.reason
|
|
532
|
+
const error = new Error('The operation was aborted')
|
|
533
|
+
error.name = 'AbortError'
|
|
534
|
+
return error
|
|
535
|
+
}
|