@chai-ui/core 0.7.1 → 0.9.0

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 (45) hide show
  1. package/dist/composer-view.d.ts +35 -6
  2. package/dist/composer-view.d.ts.map +1 -1
  3. package/dist/composer-view.js +50 -6
  4. package/dist/composer-view.js.map +1 -1
  5. package/dist/composer.d.ts +15 -6
  6. package/dist/composer.d.ts.map +1 -1
  7. package/dist/composer.js +24 -4
  8. package/dist/composer.js.map +1 -1
  9. package/dist/engine.d.ts +4 -0
  10. package/dist/engine.d.ts.map +1 -1
  11. package/dist/engine.js.map +1 -1
  12. package/dist/engines/fal.d.ts +25 -0
  13. package/dist/engines/fal.d.ts.map +1 -1
  14. package/dist/engines/fal.js +41 -0
  15. package/dist/engines/fal.js.map +1 -1
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/model-routing.d.ts +2 -2
  21. package/dist/model-routing.d.ts.map +1 -1
  22. package/dist/run.d.ts +4 -4
  23. package/dist/run.d.ts.map +1 -1
  24. package/dist/run.js +2 -2
  25. package/dist/run.js.map +1 -1
  26. package/dist/suggested-models.d.ts.map +1 -1
  27. package/dist/suggested-models.js +2 -1
  28. package/dist/suggested-models.js.map +1 -1
  29. package/dist/types.d.ts +12 -0
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/voice-input.d.ts +79 -0
  32. package/dist/voice-input.d.ts.map +1 -0
  33. package/dist/voice-input.js +207 -0
  34. package/dist/voice-input.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/composer-view.ts +68 -9
  37. package/src/composer.ts +36 -9
  38. package/src/engine.ts +4 -0
  39. package/src/engines/fal.ts +55 -0
  40. package/src/index.ts +1 -0
  41. package/src/model-routing.ts +2 -2
  42. package/src/run.ts +4 -4
  43. package/src/suggested-models.ts +2 -1
  44. package/src/types.ts +13 -0
  45. package/src/voice-input.ts +257 -0
@@ -9,15 +9,29 @@ import { readMediaFiles } from "./media-analyzer.js";
9
9
  import type { EditRegion } from "./region-edit.js";
10
10
  import { deriveGenerationUseCase } from "./run.js";
11
11
  import { suggestedModels } from "./suggested-models.js";
12
- import type { MediaKind, ModelOption, ParameterOption } from "./types.js";
12
+ import type { ModelOption, OutputKind, ParameterOption } from "./types.js";
13
13
 
14
- export type ComposerAttachMenuAction = "add-media" | "create-image" | "create-video" | "edit-media";
14
+ /** Which prompt field an Enhance click is for: the bar's prompt, or a world's environment or character. */
15
+ export type ComposerPromptField = "prompt" | "environment" | "character";
15
16
 
16
- /** The `+` menu, in order. "Add media" is the only item with a divider after it. */
17
- export const COMPOSER_ATTACH_MENU_ITEMS: { action: ComposerAttachMenuAction; label: string; divider?: boolean }[] = [
17
+ export type ComposerAttachMenuAction = "add-media" | "create-image" | "create-video" | "create-world" | "edit-media";
18
+
19
+ /**
20
+ * The `+` menu, in order. "Add media" is the only item with a divider after
21
+ * it. An `optIn` item shows only when `attachMenuActions` lists it.
22
+ */
23
+ export const COMPOSER_ATTACH_MENU_ITEMS: {
24
+ action: ComposerAttachMenuAction;
25
+ label: string;
26
+ divider?: boolean;
27
+ optIn?: boolean;
28
+ }[] = [
18
29
  { action: "add-media", label: "Add media", divider: true },
19
30
  { action: "create-image", label: "Create image" },
20
31
  { action: "create-video", label: "Create video" },
32
+ // No suggested world models or shipped world engine yet: a builder adds
33
+ // this with their own models and `engineByKind.world`.
34
+ { action: "create-world", label: "Create world", optIn: true },
21
35
  { action: "edit-media", label: "Edit image" },
22
36
  ];
23
37
 
@@ -41,7 +55,7 @@ export interface ComposerViewInput {
41
55
  defaultUseCase?: ComposerUseCase;
42
56
  multiSelectUseCases?: boolean;
43
57
  useCases?: ComposerUseCase[];
44
- modelsByKind?: Partial<Record<MediaKind, ModelOption[]>>;
58
+ modelsByKind?: Partial<Record<OutputKind, ModelOption[]>>;
45
59
  multiSelectModels?: boolean;
46
60
  modelIds?: string[];
47
61
  models?: ModelOption[];
@@ -51,6 +65,9 @@ export interface ComposerViewInput {
51
65
  autoSelectModel?: boolean;
52
66
  aspectRatios?: ParameterOption<string>[];
53
67
  aspectRatio?: string | null;
68
+ /** A world's character field. */
69
+ character?: string;
70
+ frameRate?: number | null;
54
71
  submitError?: string | null;
55
72
  submitDisabled?: boolean;
56
73
  submitting?: boolean;
@@ -75,6 +92,8 @@ export function composerView(props: ComposerViewInput) {
75
92
  autoSelectModel = false,
76
93
  aspectRatios,
77
94
  aspectRatio = null,
95
+ character = "",
96
+ frameRate = null,
78
97
  submitError = null,
79
98
  submitting = false,
80
99
  regions = [],
@@ -88,17 +107,27 @@ export function composerView(props: ComposerViewInput) {
88
107
  // An edit needs an image, then a change: a prompt for the whole image, a
89
108
  // region with its own instruction, or both.
90
109
  const editing = activeUseCases.some((u) => u.edit);
110
+ // A world takes two prompts: the environment (`value`, required) and the
111
+ // character the person plays (optional). An image can start the world,
112
+ // but the environment is still needed.
113
+ const world = activeUseCases.some((u) => u.kind === "world");
91
114
  const hasImage = attachments.some((a) => a.kind === "image");
92
115
  const hasContent = editing
93
116
  ? hasImage && (value.trim().length > 0 || regions.length > 0)
94
- : value.trim().length > 0 || attachments.length > 0;
117
+ : world
118
+ ? value.trim().length > 0
119
+ : value.trim().length > 0 || attachments.length > 0;
95
120
  const placeholder =
96
121
  props.placeholder ??
97
122
  (editing
98
123
  ? hasImage
99
124
  ? "Describe a change across the whole image, or leave this empty"
100
125
  : "Attach an image to edit"
101
- : "Describe media to create");
126
+ : world
127
+ ? attachments.length > 0
128
+ ? "Describe the world, and how to use the attached media"
129
+ : "Describe the world and how you move through it"
130
+ : "Describe media to create");
102
131
  // The stop icon only appears with somewhere for its click to go.
103
132
  const showStop = submitting && Boolean(props.onAbort);
104
133
 
@@ -153,6 +182,13 @@ export function composerView(props: ComposerViewInput) {
153
182
  const offeredAspect = showAspectRatio && aspectOptions.some((o) => o.value === aspectRatio) ? aspectRatio : null;
154
183
  const selectedAspect = aspectOptions.find((o) => o.value === offeredAspect) ?? null;
155
184
 
185
+ // A world's frame rate works like aspect ratio: only rates every model
186
+ // that might run takes, and a pick outside them is never sent.
187
+ const frameRateOptions = world
188
+ ? (ratioModels.reduce<number[] | null>((shared, m) => (shared ?? m.frameRates ?? []).filter((r) => m.frameRates?.includes(r)), null) ?? [])
189
+ : [];
190
+ const offeredFrameRate = frameRate != null && frameRateOptions.includes(frameRate) ? frameRate : null;
191
+
156
192
  const selections = (): ComposerSelection[] =>
157
193
  activeUseCases.map((u) => ({
158
194
  useCase: u,
@@ -183,6 +219,24 @@ export function composerView(props: ComposerViewInput) {
183
219
  aspectOptions,
184
220
  offeredAspect,
185
221
  selectedAspect,
222
+ world,
223
+ /**
224
+ * What an Enhance click rewrites for: the active use case, derived like
225
+ * a submit's `mode` ("text-to-text" with no use case picked, "vision"
226
+ * with media attached, "image-edit" while editing). Undefined for a world.
227
+ */
228
+ enhanceMode: editing ? ("image-edit" as const) : deriveGenerationUseCase(activeUseCases[0], attachments),
229
+ /**
230
+ * Why submit can't go yet, shown under the environment: there's
231
+ * something to send (a character or an attachment) but no environment.
232
+ */
233
+ environmentHint:
234
+ world && !value.trim() && (character.trim().length > 0 || attachments.length > 0)
235
+ ? "Add an environment to create a world"
236
+ : null,
237
+ characterPlaceholder: "Describe who you play as, or leave this empty",
238
+ frameRateOptions,
239
+ offeredFrameRate,
186
240
  /** The model ids after a row click: the one picked, or with multi-select that one toggled. */
187
241
  toggledModelIds: (id: string) => (modelIds.includes(id) ? modelIds.filter((m) => m !== id) : [...modelIds, id]),
188
242
  /** Everything a submit sends. */
@@ -195,20 +249,25 @@ export function composerView(props: ComposerViewInput) {
195
249
  selections: selections(),
196
250
  autoSelectModel,
197
251
  regions: editing ? regions : [],
252
+ ...(world && { character, frameRate: offeredFrameRate }),
198
253
  }),
199
254
  };
200
255
  }
201
256
 
202
257
  export type ComposerView = ReturnType<typeof composerView>;
203
258
 
204
- /** The `+` menu items that can work: "Add media" needs somewhere to put files, the use-case items a handler. */
259
+ /**
260
+ * The `+` menu items that can work: "Add media" needs somewhere to put
261
+ * files, the use-case items a handler. With no `actions`, every item that
262
+ * isn't opt-in.
263
+ */
205
264
  export function composerAttachMenuItems(
206
265
  actions: ComposerAttachMenuAction[] | undefined,
207
266
  { canAddMedia, canSelect }: { canAddMedia: boolean; canSelect: boolean }
208
267
  ) {
209
268
  return COMPOSER_ATTACH_MENU_ITEMS.filter(
210
269
  (item) =>
211
- (actions ?? COMPOSER_ATTACH_MENU_ITEMS.map((i) => i.action)).includes(item.action) &&
270
+ (actions ? actions.includes(item.action) : !item.optIn) &&
212
271
  (item.action === "add-media" ? canAddMedia : canSelect)
213
272
  );
214
273
  }
package/src/composer.ts CHANGED
@@ -28,7 +28,8 @@ import {
28
28
  type ResultStatus,
29
29
  type Run,
30
30
  } from "./run.js";
31
- import type { DroppedMedia, GenerationUseCase, MediaKind, ModelOption } from "./types.js";
31
+ import type { ComposerPromptField } from "./composer-view.js";
32
+ import type { DroppedMedia, GenerationUseCase, MediaKind, ModelOption, OutputKind } from "./types.js";
32
33
 
33
34
  export interface ComposerAttachment extends DroppedMedia {
34
35
  id: string;
@@ -43,7 +44,7 @@ export interface ComposerAttachment extends DroppedMedia {
43
44
  * the design). With none picked, `defaultUseCase` applies.
44
45
  */
45
46
  export interface ComposerUseCase {
46
- kind: MediaKind;
47
+ kind: OutputKind;
47
48
  label: string;
48
49
  /**
49
50
  * An edit of the attached image rather than a new one: no aspect ratio,
@@ -87,6 +88,10 @@ export interface ComposerSubmitPayload {
87
88
  autoSelectModel: boolean;
88
89
  /** An edit's marked regions, each with its own instruction. Empty unless the use case is an edit. */
89
90
  regions?: EditRegion[];
91
+ /** A world's character, the person's own figure in it. With a world use case, `value` is the environment. Undefined for other use cases. */
92
+ character?: string;
93
+ /** The picked frame rate for a world, or null for the model's default. Undefined for other use cases. */
94
+ frameRate?: number | null;
90
95
  }
91
96
 
92
97
  /**
@@ -116,7 +121,7 @@ export interface ComposerOptions {
116
121
  * for multi-use-case select, since one Fal engine outputs one kind, e.g.
117
122
  * `{ image: createFalEngine({ outputKind: "image" }), video: createFalEngine({ outputKind: "video" }) }`.
118
123
  */
119
- engineByKind?: Partial<Record<MediaKind, GenerationEngine>>;
124
+ engineByKind?: Partial<Record<OutputKind, GenerationEngine>>;
120
125
  /**
121
126
  * The engine for text requests (a Composer with no use case picked, or a
122
127
  * text use case). Defaults to `reasoningEngine`, and to a mock text engine
@@ -137,8 +142,12 @@ export interface ComposerOptions {
137
142
  reasoningEngine?: GenerationEngine;
138
143
  /** Set with `reasoningEngine` to override the provider's pair; used for every kind (§5.5). */
139
144
  reasoningModel?: string;
140
- /** Overrides the instruction wrapping a raw prompt for Enhance. Defaults to a generic still/video-aware rewrite instruction. */
141
- enhancePrompt?: (value: string, mode?: GenerationUseCase) => string;
145
+ /**
146
+ * Overrides the instruction wrapping a raw prompt for Enhance. Defaults to
147
+ * a rewrite instruction for the kind of prompt: still, video, text, or a
148
+ * world's environment or character (`field`).
149
+ */
150
+ enhancePrompt?: (value: string, mode?: GenerationUseCase, field?: ComposerPromptField) => string;
142
151
  /**
143
152
  * Auto-select: with `autoSelectModel` on in the payload, the reasoning
144
153
  * model picks one model per use case at submit, then these fire with the
@@ -195,7 +204,14 @@ export interface ComposerEdit {
195
204
 
196
205
  const mockTextEngine = createMockEngine({ outputKind: "text" });
197
206
 
198
- function defaultEnhancePrompt(value: string, mode?: GenerationUseCase): string {
207
+ function defaultEnhancePrompt(value: string, mode?: GenerationUseCase, field: ComposerPromptField = "prompt"): string {
208
+ const reply = "Reply with only the rewritten description, nothing else.";
209
+ if (field === "environment") {
210
+ return `Rewrite the following description of an interactive world for a world model to be more detailed and specific, keeping the same intent: the setting, its look and lighting, and how the person moves through it. Don't add a character. ${reply}\n\n${value}`;
211
+ }
212
+ if (field === "character") {
213
+ return `Rewrite the following description of the character a person plays in an interactive world to be more detailed and specific, keeping the same intent: who or what they are, how they look, and how they move. Don't describe the world around them. ${reply}\n\n${value}`;
214
+ }
199
215
  const subject = mode?.includes("video")
200
216
  ? "a video-generation"
201
217
  : mode === "text-to-text" || mode === "vision"
@@ -293,6 +309,11 @@ export function createComposerStore(getOptions: () => ComposerOptions, getProvid
293
309
  const fallback = isText ? (opts.textModel ?? (r && reasoningModelFor(r, attachments))) : undefined;
294
310
  const params: Record<string, unknown> = payload.aspectRatio && !isEdit ? { aspectRatio: payload.aspectRatio } : {};
295
311
  if (isEdit) params.regions = payload.regions ?? [];
312
+ // A world's extra inputs, for a world engine to map to its model.
313
+ if (selection.useCase.kind === "world") {
314
+ if (payload.character?.trim()) params.character = payload.character;
315
+ if (payload.frameRate != null) params.frameRate = payload.frameRate;
316
+ }
296
317
  const request: Request = {
297
318
  mode: deriveGenerationUseCase(selection.useCase, attachments),
298
319
  prompt: payload.value,
@@ -359,6 +380,8 @@ export function createComposerStore(getOptions: () => ComposerOptions, getProvid
359
380
  prompt,
360
381
  attachments: newRun.request.attachments,
361
382
  aspectRatio: newRun.request.params.aspectRatio as string | undefined,
383
+ character: newRun.request.params.character as string | undefined,
384
+ frameRate: newRun.request.params.frameRate as number | undefined,
362
385
  // Text engines that stream fill `output` in while still "running".
363
386
  onText: (text) => update(newRun.id, (r) => streamResult(r, result.id, text)),
364
387
  signal,
@@ -462,13 +485,17 @@ export function createComposerStore(getOptions: () => ComposerOptions, getProvid
462
485
  set((s) => ({ routing: false, runs: cancelAll(s.runs), editRuns: cancelAll(s.editRuns) }));
463
486
  }
464
487
 
465
- /** Rewrites a prompt with the reasoning model. Only offer it when `reasoning()` is set. */
466
- async function enhance(value: string): Promise<string> {
488
+ /**
489
+ * Rewrites a prompt with the reasoning model, worded for its `field` and
490
+ * `mode` (the Composer passes both to `onEnhance`, so `onEnhance={enhance}`
491
+ * rewrites for the use case on screen). Only offer it when `reasoning()` is set.
492
+ */
493
+ async function enhance(value: string, field?: ComposerPromptField, mode?: GenerationUseCase): Promise<string> {
467
494
  const r = reasoning();
468
495
  if (!r) throw new Error("[chai-ui] Enhance needs a reasoning engine and model.");
469
496
  set(() => ({ enhancing: true }));
470
497
  try {
471
- const prompt = (getOptions().enhancePrompt ?? defaultEnhancePrompt)(value, state.runs[0]?.request.mode);
498
+ const prompt = (getOptions().enhancePrompt ?? defaultEnhancePrompt)(value, mode ?? state.runs[0]?.request.mode, field);
472
499
  const { src } = await r.engine.generate({ modelId: r.model, prompt, attachments: [] });
473
500
  return src;
474
501
  } finally {
package/src/engine.ts CHANGED
@@ -23,6 +23,10 @@ export interface GenerationEngine {
23
23
  attachments: DroppedMedia[];
24
24
  /** The output aspect ratio the person picked, e.g. `"16:9"`. Only set for a model that lists it in `ModelOption.aspectRatios`. */
25
25
  aspectRatio?: string;
26
+ /** A world's character (Create world), when the person gave one. `prompt` is the environment. */
27
+ character?: string;
28
+ /** A world's frame rate, e.g. `16`. Only set for a model that lists it in `ModelOption.frameRates`. */
29
+ frameRate?: number;
26
30
  /**
27
31
  * Optional streaming hook for text output. An engine that can stream
28
32
  * calls this with the full text received so far (not a delta) each time
@@ -24,6 +24,7 @@
24
24
  import { abortError, abortableSleep, isAbortError, readEvaluations, type GenerationEngine } from "../engine.js";
25
25
  import type { DroppedMedia, MediaKind } from "../types.js";
26
26
  import { SUGGESTED_FAL_MODELS } from "../suggested-models.js";
27
+ import type { Transcribe } from "../voice-input.js";
27
28
 
28
29
  /** Output kinds this engine can produce — text goes through a reasoning engine instead (see file doc comment). */
29
30
  type FalOutputKind = Exclude<MediaKind, "text">;
@@ -77,6 +78,9 @@ interface FalResultResponse {
77
78
  video?: { url: string };
78
79
  audio?: { url: string };
79
80
  audio_url?: string;
81
+ /** Speech-to-text models answer with the transcript in `text` or `output`. */
82
+ text?: string;
83
+ output?: string;
80
84
  error?: unknown;
81
85
  detail?: unknown;
82
86
  }
@@ -205,6 +209,12 @@ async function awaitFalJob(args: {
205
209
  return json;
206
210
  }
207
211
 
212
+ /** The transcript in a Fal speech-to-text result: `text` (Wizper, Scribe, Cohere) or `output` (Fal's own, Nemotron). */
213
+ export function extractFalTranscript(json: { text?: unknown; output?: unknown }): string | undefined {
214
+ const text = typeof json.text === "string" ? json.text : typeof json.output === "string" ? json.output : undefined;
215
+ return text?.trim();
216
+ }
217
+
208
218
  /**
209
219
  * Asks Fal to drop a job (`PUT …/requests/<id>/cancel`). A queued job is
210
220
  * removed and never runs; a running one gets a cancel signal and may still
@@ -318,3 +328,48 @@ export async function pingFal(
318
328
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
319
329
  }
320
330
  }
331
+
332
+ export interface FalTranscriberOptions {
333
+ /** A Fal speech-to-text model id, e.g. `"fal-ai/wizper"`. Required: Chai doesn't pick one for you. */
334
+ model: string;
335
+ /** Same proxy route `createFalEngine` uses. Defaults to `/api/chai/fal`. */
336
+ endpoint?: string;
337
+ fetchImpl?: typeof fetch;
338
+ pollIntervalMs?: number;
339
+ timeoutMs?: number;
340
+ }
341
+
342
+ /**
343
+ * A `Transcribe` function on a Fal speech-to-text model, through the same
344
+ * server route as `createFalEngine`, so the key stays on the server. The
345
+ * recording goes to the model as `audio_url`, a data URL, like images do.
346
+ * With `allowedModels` on the handler, list the transcription model too.
347
+ */
348
+ export function createFalTranscriber(options: FalTranscriberOptions): Transcribe {
349
+ const endpoint = options.endpoint ?? "/api/chai/fal";
350
+ // Called through a wrapper: browsers throw "Illegal invocation" when fetch runs with an object as `this`.
351
+ const fetchImpl: typeof fetch = (input, init) => (options.fetchImpl ?? fetch)(input, init);
352
+ const { model } = options;
353
+ return async (audio, { signal } = {}) => {
354
+ if (signal?.aborted) throw abortError();
355
+ const { requestId, status } = await submitFalJob({ endpoint, model, body: { audio_url: audio.src }, fetchImpl });
356
+ if (signal?.aborted) {
357
+ await cancelFalJob({ endpoint, model, requestId, fetchImpl });
358
+ throw abortError();
359
+ }
360
+ const result = await awaitFalJob({
361
+ endpoint,
362
+ model,
363
+ requestId,
364
+ status,
365
+ fetchImpl,
366
+ pollIntervalMs: options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
367
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
368
+ signal,
369
+ }).catch(async (err) => {
370
+ if (isAbortError(err)) await cancelFalJob({ endpoint, model, requestId, fetchImpl });
371
+ throw err;
372
+ });
373
+ return extractFalTranscript(result) ?? "";
374
+ };
375
+ }
package/src/index.ts CHANGED
@@ -15,3 +15,4 @@ export * from "./media-actions.js";
15
15
  export * from "./result-format.js";
16
16
  export * from "./composer-view.js";
17
17
  export * from "./edit-card.js";
18
+ export * from "./voice-input.js";
@@ -6,13 +6,13 @@
6
6
  */
7
7
 
8
8
  import { normalizeGenerationError, type GenerationEngine } from "./engine.js";
9
- import type { DroppedMedia, MediaKind, ModelOption } from "./types.js";
9
+ import type { DroppedMedia, MediaKind, ModelOption, OutputKind } from "./types.js";
10
10
 
11
11
  export interface ModelRoutingInput {
12
12
  /** What the person typed. Empty for media-only requests (e.g. Media Analyzer). */
13
13
  prompt: string;
14
14
  /** What the request produces, e.g. "image" for Create image, "text" for analysis. */
15
- outputKind: MediaKind;
15
+ outputKind: OutputKind;
16
16
  /** Kinds of media attached, if any. */
17
17
  attachmentKinds: MediaKind[];
18
18
  /** The candidates: only models the builder offers for this request. */
package/src/run.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * (`@chai-ui/react`) is the thin React binding on top.
5
5
  */
6
6
 
7
- import type { DroppedMedia, Evaluation, GenerationUseCase, MediaKind, Usage } from "./types.js";
7
+ import type { DroppedMedia, Evaluation, GenerationUseCase, MediaKind, OutputKind, Usage } from "./types.js";
8
8
 
9
9
  export interface Request {
10
10
  /** The specific generation use case, when one could be derived — see `deriveGenerationUseCase`. Undefined when the use case (or its kind) has no `GenerationUseCase` match yet, e.g. audio. */
@@ -135,12 +135,12 @@ export function cancelResult(run: Run, resultId: string): Run {
135
135
  * case plus whether anything is attached — this is real derivation logic,
136
136
  * not just plumbing: the same `kind: "video"` means `"text-to-video"` with
137
137
  * nothing attached but `"image-to-video"` once an image is. Returns
138
- * `undefined` for "audio" (no `GenerationUseCase` member exists yet;
139
- * audio generation is planned). `"text"` is a text request: a reply to
138
+ * `undefined` for "audio" and "world" (no `GenerationUseCase` member exists
139
+ * yet; audio generation is planned, world models await provider specs). `"text"` is a text request: a reply to
140
140
  * the prompt, or a description of the attached media ("vision").
141
141
  */
142
142
  export function deriveGenerationUseCase(
143
- useCase: { kind: MediaKind } | null | undefined,
143
+ useCase: { kind: OutputKind } | null | undefined,
144
144
  attachments: DroppedMedia[]
145
145
  ): GenerationUseCase | undefined {
146
146
  if (!useCase) return undefined;
@@ -4,7 +4,8 @@ import { DEFAULT_MAX_REGIONS } from "./region-edit.js";
4
4
  /**
5
5
  * Suggested models, so builders don't have to write model lists. Composer
6
6
  * and MediaAnalyzer use them when no `models` is passed; passing `models`
7
- * still replaces them. The first model in each list is the default pick.
7
+ * still replaces them. Nothing is picked for the person: with a Model menu
8
+ * they pick or turn on auto-select; with no menu, the list's first runs.
8
9
  *
9
10
  * Generation runs on Fal (the default engine), analysis on OpenRouter (the
10
11
  * reasoning engine). Every entry was checked against the provider's own
package/src/types.ts CHANGED
@@ -5,6 +5,13 @@
5
5
  /** The kinds of media a component can take in or produce. */
6
6
  export type MediaKind = "image" | "video" | "audio" | "text";
7
7
 
8
+ /**
9
+ * What a generation produces: any `MediaKind`, or an interactive world from
10
+ * a world model (Create world). A world is never an attachment, so it lives
11
+ * here rather than in `MediaKind`.
12
+ */
13
+ export type OutputKind = MediaKind | "world";
14
+
8
15
  /**
9
16
  * A piece of media a person attached (or an engine received) — just enough
10
17
  * to know its kind and where it lives.
@@ -106,6 +113,12 @@ export interface ModelOption {
106
113
  * the input image): no menu shows.
107
114
  */
108
115
  aspectRatios?: string[];
116
+ /**
117
+ * The frame rates a world model runs at, e.g. `[16]`. Composer's frame
118
+ * rate menu offers only rates the selected models share. Leave it out for
119
+ * a model with one fixed rate it doesn't take as an input: no menu shows.
120
+ */
121
+ frameRates?: number[];
109
122
  }
110
123
 
111
124
  /** See `ModelOption.falInput`. */