@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.
- package/dist/composer-view.d.ts +35 -6
- package/dist/composer-view.d.ts.map +1 -1
- package/dist/composer-view.js +50 -6
- package/dist/composer-view.js.map +1 -1
- package/dist/composer.d.ts +15 -6
- package/dist/composer.d.ts.map +1 -1
- package/dist/composer.js +24 -4
- package/dist/composer.js.map +1 -1
- package/dist/engine.d.ts +4 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js.map +1 -1
- package/dist/engines/fal.d.ts +25 -0
- package/dist/engines/fal.d.ts.map +1 -1
- package/dist/engines/fal.js +41 -0
- package/dist/engines/fal.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/model-routing.d.ts +2 -2
- package/dist/model-routing.d.ts.map +1 -1
- package/dist/run.d.ts +4 -4
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +2 -2
- package/dist/run.js.map +1 -1
- package/dist/suggested-models.d.ts.map +1 -1
- package/dist/suggested-models.js +2 -1
- package/dist/suggested-models.js.map +1 -1
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/voice-input.d.ts +79 -0
- package/dist/voice-input.d.ts.map +1 -0
- package/dist/voice-input.js +207 -0
- package/dist/voice-input.js.map +1 -0
- package/package.json +1 -1
- package/src/composer-view.ts +68 -9
- package/src/composer.ts +36 -9
- package/src/engine.ts +4 -0
- package/src/engines/fal.ts +55 -0
- package/src/index.ts +1 -0
- package/src/model-routing.ts +2 -2
- package/src/run.ts +4 -4
- package/src/suggested-models.ts +2 -1
- package/src/types.ts +13 -0
- package/src/voice-input.ts +257 -0
package/src/composer-view.ts
CHANGED
|
@@ -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 {
|
|
12
|
+
import type { ModelOption, OutputKind, ParameterOption } from "./types.js";
|
|
13
13
|
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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<
|
|
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
|
-
:
|
|
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
|
-
:
|
|
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
|
-
/**
|
|
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
|
|
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 {
|
|
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:
|
|
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<
|
|
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
|
-
/**
|
|
141
|
-
|
|
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
|
-
/**
|
|
466
|
-
|
|
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
|
package/src/engines/fal.ts
CHANGED
|
@@ -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
package/src/model-routing.ts
CHANGED
|
@@ -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:
|
|
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
|
|
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:
|
|
143
|
+
useCase: { kind: OutputKind } | null | undefined,
|
|
144
144
|
attachments: DroppedMedia[]
|
|
145
145
|
): GenerationUseCase | undefined {
|
|
146
146
|
if (!useCase) return undefined;
|
package/src/suggested-models.ts
CHANGED
|
@@ -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.
|
|
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`. */
|