@chai-ui/core 0.10.1 → 0.11.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 +30 -6
- package/dist/composer-view.d.ts.map +1 -1
- package/dist/composer-view.js +79 -18
- package/dist/composer-view.js.map +1 -1
- package/dist/composer.d.ts +13 -1
- package/dist/composer.d.ts.map +1 -1
- package/dist/composer.js +23 -6
- package/dist/composer.js.map +1 -1
- package/dist/engine.d.ts +2 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js.map +1 -1
- package/dist/engines/fal.d.ts +10 -1
- package/dist/engines/fal.d.ts.map +1 -1
- package/dist/engines/fal.js +10 -7
- 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/menu-placement.d.ts +16 -0
- package/dist/menu-placement.d.ts.map +1 -0
- package/dist/menu-placement.js +20 -0
- package/dist/menu-placement.js.map +1 -0
- package/dist/run.d.ts +6 -4
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +7 -3
- package/dist/run.js.map +1 -1
- package/dist/types.d.ts +29 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/composer-view.ts +102 -19
- package/src/composer.ts +28 -3
- package/src/engine.ts +2 -0
- package/src/engines/fal.ts +19 -8
- package/src/index.ts +1 -0
- package/src/menu-placement.ts +29 -0
- package/src/run.ts +9 -5
- package/src/types.ts +33 -3
package/src/composer.ts
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
type Run,
|
|
30
30
|
} from "./run.js";
|
|
31
31
|
import type { ComposerPromptField } from "./composer-view.js";
|
|
32
|
-
import type { DroppedMedia, GenerationUseCase, MediaKind, ModelOption, OutputKind } from "./types.js";
|
|
32
|
+
import type { AudioUseCase, DroppedMedia, GenerationUseCase, MediaKind, ModelOption, OutputKind } from "./types.js";
|
|
33
33
|
|
|
34
34
|
export interface ComposerAttachment extends DroppedMedia {
|
|
35
35
|
id: string;
|
|
@@ -52,6 +52,8 @@ export interface ComposerUseCase {
|
|
|
52
52
|
* either a prompt or a region. Images only for now.
|
|
53
53
|
*/
|
|
54
54
|
edit?: boolean;
|
|
55
|
+
/** For an `"audio"` use case, which audio it creates: spoken words or music. */
|
|
56
|
+
audio?: AudioUseCase;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
/** A plain text request — what a submit means when no use case is picked, unless the builder sets `defaultUseCase`. */
|
|
@@ -60,6 +62,12 @@ export const TEXT_USE_CASE: ComposerUseCase = { kind: "text", label: "Text" };
|
|
|
60
62
|
/** Editing the attached image, the attach menu's "Edit image". Pair it with `useComposer`'s `editImage` and an `EditCard`. */
|
|
61
63
|
export const EDIT_IMAGE_USE_CASE: ComposerUseCase = { kind: "image", label: "Edit image", edit: true };
|
|
62
64
|
|
|
65
|
+
/** Spoken audio from text, the attach menu's "Create speech". */
|
|
66
|
+
export const CREATE_SPEECH_USE_CASE: ComposerUseCase = { kind: "audio", label: "Speech", audio: "speech" };
|
|
67
|
+
|
|
68
|
+
/** Music from a description, the attach menu's "Create music". */
|
|
69
|
+
export const CREATE_MUSIC_USE_CASE: ComposerUseCase = { kind: "audio", label: "Music", audio: "music" };
|
|
70
|
+
|
|
63
71
|
/** One use case to run and the models picked for it. Empty `modelIds` means the engine's default model. */
|
|
64
72
|
export interface ComposerSelection {
|
|
65
73
|
useCase: ComposerUseCase;
|
|
@@ -92,6 +100,15 @@ export interface ComposerSubmitPayload {
|
|
|
92
100
|
character?: string;
|
|
93
101
|
/** The picked frame rate for a world, or null for the model's default. Undefined for other use cases. */
|
|
94
102
|
frameRate?: number | null;
|
|
103
|
+
/** The picked voice for Create speech, or null for the model's default. Undefined for other use cases. */
|
|
104
|
+
speechVoice?: string | null;
|
|
105
|
+
/** The picked genres for Create music, written into the prompt at submit. Undefined for other use cases. */
|
|
106
|
+
genres?: string[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A music prompt with the picked genres written in: music models take genre as part of the description. */
|
|
110
|
+
export function promptWithGenres(prompt: string, genres: string[]): string {
|
|
111
|
+
return genres.length > 0 ? `${prompt.trim()}\n\nGenre: ${genres.join(", ")}` : prompt;
|
|
95
112
|
}
|
|
96
113
|
|
|
97
114
|
/**
|
|
@@ -212,7 +229,13 @@ function defaultEnhancePrompt(value: string, mode?: GenerationUseCase, field: Co
|
|
|
212
229
|
if (field === "character") {
|
|
213
230
|
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
231
|
}
|
|
215
|
-
|
|
232
|
+
// The prompt is the script itself: keep the words, only make them read aloud well.
|
|
233
|
+
if (mode === "text-to-speech") {
|
|
234
|
+
return `Edit the following text so it reads naturally when spoken aloud: fix punctuation, and spell out numbers, symbols and abbreviations. Keep the same words and meaning. Reply with only the edited text, nothing else.\n\n${value}`;
|
|
235
|
+
}
|
|
236
|
+
const subject = mode === "text-to-music"
|
|
237
|
+
? "a music-generation"
|
|
238
|
+
: mode?.includes("video")
|
|
216
239
|
? "a video-generation"
|
|
217
240
|
: mode === "text-to-text" || mode === "vision"
|
|
218
241
|
? "an AI assistant"
|
|
@@ -314,9 +337,10 @@ export function createComposerStore(getOptions: () => ComposerOptions, getProvid
|
|
|
314
337
|
if (payload.character?.trim()) params.character = payload.character;
|
|
315
338
|
if (payload.frameRate != null) params.frameRate = payload.frameRate;
|
|
316
339
|
}
|
|
340
|
+
if (selection.useCase.audio === "speech" && payload.speechVoice) params.voice = payload.speechVoice;
|
|
317
341
|
const request: Request = {
|
|
318
342
|
mode: deriveGenerationUseCase(selection.useCase, attachments),
|
|
319
|
-
prompt: payload.value,
|
|
343
|
+
prompt: selection.useCase.audio === "music" ? promptWithGenres(payload.value, payload.genres ?? []) : payload.value,
|
|
320
344
|
attachments,
|
|
321
345
|
modelIds: selection.modelIds.length > 0 ? selection.modelIds : [fallback ?? DEFAULT_MODEL_ID],
|
|
322
346
|
params,
|
|
@@ -382,6 +406,7 @@ export function createComposerStore(getOptions: () => ComposerOptions, getProvid
|
|
|
382
406
|
aspectRatio: newRun.request.params.aspectRatio as string | undefined,
|
|
383
407
|
character: newRun.request.params.character as string | undefined,
|
|
384
408
|
frameRate: newRun.request.params.frameRate as number | undefined,
|
|
409
|
+
voice: newRun.request.params.voice as string | undefined,
|
|
385
410
|
// Text engines that stream fill `output` in while still "running".
|
|
386
411
|
onText: (text) => update(newRun.id, (r) => streamResult(r, result.id, text)),
|
|
387
412
|
signal,
|
package/src/engine.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface GenerationEngine {
|
|
|
27
27
|
character?: string;
|
|
28
28
|
/** A world's frame rate, e.g. `16`. Only set for a model that lists it in `ModelOption.frameRates`. */
|
|
29
29
|
frameRate?: number;
|
|
30
|
+
/** A text-to-speech voice, the model's own id for it. Only set for a model that lists it in `ModelOption.voices`. */
|
|
31
|
+
voice?: string;
|
|
30
32
|
/**
|
|
31
33
|
* Optional streaming hook for text output. An engine that can stream
|
|
32
34
|
* calls this with the full text received so far (not a delta) each time
|
package/src/engines/fal.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { abortError, abortableSleep, isAbortError, readEvaluations, type GenerationEngine } from "../engine.js";
|
|
25
|
-
import type { DroppedMedia, MediaKind } from "../types.js";
|
|
25
|
+
import type { DroppedMedia, FalInputFormat, MediaKind, ModelOption } from "../types.js";
|
|
26
26
|
import { SUGGESTED_FAL_MODELS } from "../suggested-models.js";
|
|
27
27
|
import type { Transcribe } from "../voice-input.js";
|
|
28
28
|
|
|
@@ -38,6 +38,12 @@ export interface FalEngineOptions {
|
|
|
38
38
|
modelByKind?: Partial<Record<FalOutputKind, string>>;
|
|
39
39
|
/** Which kind of media this engine instance produces. Defaults to "image". */
|
|
40
40
|
outputKind?: FalOutputKind;
|
|
41
|
+
/**
|
|
42
|
+
* Your own models, so the engine sends each one's inputs in the fields its
|
|
43
|
+
* Fal schema names (`ModelOption.falInput`). Chai's suggested models are
|
|
44
|
+
* already known.
|
|
45
|
+
*/
|
|
46
|
+
models?: ModelOption[];
|
|
41
47
|
/** Poll interval while a queued job is in progress. Defaults to 1500ms. */
|
|
42
48
|
pollIntervalMs?: number;
|
|
43
49
|
/** Give up waiting after this long. Defaults to 120000ms (2 min) — video/audio jobs run longer than image ones. */
|
|
@@ -48,9 +54,9 @@ export interface FalEngineOptions {
|
|
|
48
54
|
|
|
49
55
|
/**
|
|
50
56
|
* Fal model ids verified live against fal.ai's own docs/catalog
|
|
51
|
-
* 2026-09-24, not invented. No default for "audio":
|
|
52
|
-
*
|
|
53
|
-
*
|
|
57
|
+
* 2026-09-24, not invented. No default for "audio": Create speech and
|
|
58
|
+
* Create music are opt-in, so the builder names the model when they turn
|
|
59
|
+
* one on.
|
|
54
60
|
*/
|
|
55
61
|
const DEFAULT_MODEL_BY_KIND: Partial<Record<FalOutputKind, string>> = {
|
|
56
62
|
image: "fal-ai/flux/schnell",
|
|
@@ -132,9 +138,13 @@ export function buildFalRequestBody(args: {
|
|
|
132
138
|
attachments?: DroppedMedia[];
|
|
133
139
|
model?: string;
|
|
134
140
|
aspectRatio?: string;
|
|
141
|
+
voice?: string;
|
|
142
|
+
/** The model's input format when the caller knows it (see `FalEngineOptions.models`); else a suggested model's, else guessed. */
|
|
143
|
+
input?: FalInputFormat;
|
|
135
144
|
}): Record<string, unknown> {
|
|
136
|
-
const
|
|
137
|
-
const
|
|
145
|
+
const known = args.input ?? (args.model ? SUGGESTED_FAL_MODELS.get(args.model)?.falInput : undefined);
|
|
146
|
+
const body: Record<string, unknown> = { [known?.prompt ?? "prompt"]: args.prompt };
|
|
147
|
+
if (args.voice) body.voice = args.voice;
|
|
138
148
|
if (args.aspectRatio) {
|
|
139
149
|
const sizes = args.model?.startsWith("fal-ai/gpt-image")
|
|
140
150
|
? GPT_IMAGE_1_SIZES
|
|
@@ -241,7 +251,7 @@ export function createFalEngine(options: FalEngineOptions = {}): GenerationEngin
|
|
|
241
251
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
242
252
|
|
|
243
253
|
return {
|
|
244
|
-
async generate({ modelId, prompt, attachments, aspectRatio, signal }) {
|
|
254
|
+
async generate({ modelId, prompt, attachments, aspectRatio, voice, signal }) {
|
|
245
255
|
const model =
|
|
246
256
|
modelId ?? options.modelByKind?.[outputKind] ?? options.model ?? DEFAULT_MODEL_BY_KIND[outputKind];
|
|
247
257
|
if (!model) {
|
|
@@ -250,7 +260,8 @@ export function createFalEngine(options: FalEngineOptions = {}): GenerationEngin
|
|
|
250
260
|
);
|
|
251
261
|
}
|
|
252
262
|
|
|
253
|
-
const
|
|
263
|
+
const input = options.models?.find((m) => m.id === model)?.falInput;
|
|
264
|
+
const body = buildFalRequestBody({ prompt, attachments, model, aspectRatio, voice, input });
|
|
254
265
|
if (signal?.aborted) throw abortError();
|
|
255
266
|
|
|
256
267
|
try {
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which way a component's menus open from their trigger. `"above"` suits a
|
|
3
|
+
* bar docked to the bottom of the screen and `"below"` one docked to the
|
|
4
|
+
* top; `"auto"` measures at open time, for a bar that sits in the page.
|
|
5
|
+
*/
|
|
6
|
+
export type MenuPlacement = "auto" | "above" | "below";
|
|
7
|
+
|
|
8
|
+
/** The gap between a trigger and its open menu, and the least room kept to the screen's edge. */
|
|
9
|
+
const GAP = 8;
|
|
10
|
+
const EDGE = 16;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Whether a menu opens above its trigger. `"auto"` opens below when the menu
|
|
14
|
+
* fits there, above when only that side fits, and otherwise toward the side
|
|
15
|
+
* with more room, so it's never cut off when it could have fit.
|
|
16
|
+
*/
|
|
17
|
+
export function menuOpensAbove(
|
|
18
|
+
placement: MenuPlacement,
|
|
19
|
+
trigger: { top: number; bottom: number },
|
|
20
|
+
menuHeight: number,
|
|
21
|
+
viewportHeight: number
|
|
22
|
+
): boolean {
|
|
23
|
+
if (placement !== "auto") return placement === "above";
|
|
24
|
+
const roomBelow = viewportHeight - EDGE - (trigger.bottom + GAP);
|
|
25
|
+
const roomAbove = trigger.top - GAP - EDGE;
|
|
26
|
+
if (menuHeight <= roomBelow) return false;
|
|
27
|
+
if (menuHeight <= roomAbove) return true;
|
|
28
|
+
return roomAbove > roomBelow;
|
|
29
|
+
}
|
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, OutputKind, Usage } from "./types.js";
|
|
7
|
+
import type { AudioUseCase, 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. */
|
|
@@ -134,16 +134,20 @@ export function cancelResult(run: Run, resultId: string): Run {
|
|
|
134
134
|
* Derives a `GenerationUseCase` from Composer's own `{ kind, label }` use
|
|
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
|
-
* nothing attached but `"image-to-video"` once an image is.
|
|
138
|
-
*
|
|
139
|
-
*
|
|
137
|
+
* nothing attached but `"image-to-video"` once an image is. An `"audio"`
|
|
138
|
+
* use case is `"text-to-speech"` or `"text-to-music"` by its `audio` field,
|
|
139
|
+
* and `undefined` without one. Returns `undefined` for "world" (world
|
|
140
|
+
* models await provider specs). `"text"` is a text request: a reply to
|
|
140
141
|
* the prompt, or a description of the attached media ("vision").
|
|
141
142
|
*/
|
|
142
143
|
export function deriveGenerationUseCase(
|
|
143
|
-
useCase: { kind: OutputKind } | null | undefined,
|
|
144
|
+
useCase: { kind: OutputKind; audio?: AudioUseCase } | null | undefined,
|
|
144
145
|
attachments: DroppedMedia[]
|
|
145
146
|
): GenerationUseCase | undefined {
|
|
146
147
|
if (!useCase) return undefined;
|
|
148
|
+
if (useCase.kind === "audio") {
|
|
149
|
+
return useCase.audio === "speech" ? "text-to-speech" : useCase.audio === "music" ? "text-to-music" : undefined;
|
|
150
|
+
}
|
|
147
151
|
const hasMedia = attachments.length > 0;
|
|
148
152
|
if (useCase.kind === "video") return hasMedia ? "image-to-video" : "text-to-video";
|
|
149
153
|
if (useCase.kind === "image") return hasMedia ? "image-edit" : "text-to-image";
|
package/src/types.ts
CHANGED
|
@@ -12,6 +12,13 @@ export type MediaKind = "image" | "video" | "audio" | "text";
|
|
|
12
12
|
*/
|
|
13
13
|
export type OutputKind = MediaKind | "world";
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Which audio an `"audio"` use case creates: spoken words (Create speech) or
|
|
17
|
+
* music (Create music). Each needs its own models and result surface, so a
|
|
18
|
+
* new kind of audio (e.g. sound effects) gets its own value, not a reuse.
|
|
19
|
+
*/
|
|
20
|
+
export type AudioUseCase = "speech" | "music";
|
|
21
|
+
|
|
15
22
|
/**
|
|
16
23
|
* A piece of media a person attached (or an engine received) — just enough
|
|
17
24
|
* to know its kind and where it lives.
|
|
@@ -119,12 +126,33 @@ export interface ModelOption {
|
|
|
119
126
|
* a model with one fixed rate it doesn't take as an input: no menu shows.
|
|
120
127
|
*/
|
|
121
128
|
frameRates?: number[];
|
|
129
|
+
/**
|
|
130
|
+
* The voices a text-to-speech model speaks in, from its schema. Composer's
|
|
131
|
+
* Voice menu offers them once this is the one model picked, and the
|
|
132
|
+
* engine sends the pick as the model's `voice` input. Leave it out for a
|
|
133
|
+
* model with no voice to choose.
|
|
134
|
+
*/
|
|
135
|
+
voices?: VoiceOption[];
|
|
136
|
+
/**
|
|
137
|
+
* The genres offered with a music model. Composer's Genre menu offers the
|
|
138
|
+
* ones every picked model lists, several at once. Music models take genre
|
|
139
|
+
* as part of the description, so the picks are written into the prompt
|
|
140
|
+
* (see `promptWithGenres`).
|
|
141
|
+
*/
|
|
142
|
+
genres?: ParameterOption<string>[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** One voice in `ModelOption.voices`: the model's own id for it, its name, and an optional menu section (e.g. "Female"). */
|
|
146
|
+
export interface VoiceOption extends ParameterOption<string> {
|
|
147
|
+
group?: string;
|
|
122
148
|
}
|
|
123
149
|
|
|
124
150
|
/** See `ModelOption.falInput`. */
|
|
125
151
|
export interface FalInputFormat {
|
|
126
152
|
image?: "image_url" | "image_urls" | "start_image_url";
|
|
127
153
|
aspectRatio?: "aspect_ratio" | "image_size";
|
|
154
|
+
/** Which field the prompt goes in. Most models take `prompt`; some text-to-speech models take the words as `text`. */
|
|
155
|
+
prompt?: "prompt" | "text";
|
|
128
156
|
}
|
|
129
157
|
|
|
130
158
|
/** How an edit's regions reach the model — see `ModelOption.regionFormat`. */
|
|
@@ -135,8 +163,8 @@ export type RegionFormat = "flux-3-boxes" | "text";
|
|
|
135
163
|
* leaderboard categories at github.com/oolong-tea-2026/arena-ai-leaderboards,
|
|
136
164
|
* so a future model-ranking lookup can join on it directly, plus
|
|
137
165
|
* "text-to-text" for a plain text request (a Composer with no use case
|
|
138
|
-
* picked). That repo has no audio-generation category
|
|
139
|
-
*
|
|
166
|
+
* picked). That repo has no audio-generation category, so "text-to-speech"
|
|
167
|
+
* and "text-to-music" have nothing to join on yet.
|
|
140
168
|
*/
|
|
141
169
|
export type GenerationUseCase =
|
|
142
170
|
| "text-to-image"
|
|
@@ -145,4 +173,6 @@ export type GenerationUseCase =
|
|
|
145
173
|
| "image-to-video"
|
|
146
174
|
| "video-edit"
|
|
147
175
|
| "vision"
|
|
148
|
-
| "text-to-text"
|
|
176
|
+
| "text-to-text"
|
|
177
|
+
| "text-to-speech"
|
|
178
|
+
| "text-to-music";
|