Files
open-design/apps/web/src/media/models.ts
T
bcc58af931 refactor(web): rename Execution mode and tighten settings dialog UI (#1568)
* refactor(web): rename Execution mode and tighten settings dialog UI

- Rename "Settings → Execution & model" to "Settings → Execution mode"
  across the web UI, i18n keys, docs, and e2e selectors.
- Redesign SettingsDialog: kicker + title row in the modal head, a
  flatMap-driven agent grid that renders the inline test-result row
  beside the selected card, compact unavailable cards with right-aligned
  install/docs links, and an install guide that only shows when the
  user has no working agent picked.
- Trim verbose subtitle / hint copy across chat model, CLI proxy,
  media providers, custom instructions, and memory sections.
- Add an `info` Icon variant for the redesigned settings hints.
- Update e2e selectors and docs that referenced the old menu label.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(web): polish Settings dialog — media providers, skills, MCP

Media providers
- Hide internal Stub fixture provider (settingsVisible: false)
- Split provider list into Available (integrated, editable) and Coming
  Soon (collapsed <details> drawer with name/hint/Docs link only)
- Drop right-side Integrated/Configured badges from every row; all rows
  in the main list are integrated by definition; inline grey "Saved"
  chip next to the provider name is the only status indicator now
- "Saved" badge moves inline to the right of the provider name and uses
  a neutral grey treatment (was a standalone green pill below the name)
- "Reload from daemon" button shows a 2s green "✓ Reloaded" flash on
  success instead of leaving a permanent paragraph under the header;
  errors remain sticky

Skills
- Replace three pill-row filter banks (Source, Type, Category) with a
  compact single-row toolbar: search + three inline <select> dropdowns
  side by side; active filter highlighted with a stronger border

MCP server
- Shorten section hint to one line
- Move WHAT YOUR AGENT CAN DO capabilities above the client dropdown
  (motivate before asking to act)
- Move "Build the daemon first" warning below the code block where it
  contextually explains why the command might fail, not as a top-level
  error before the user has done anything
- Downgrade "Restart your client" left-border from accent orange to
  border-strong grey — it is a next step, not a warning

External MCP
- Shorten section hint to one line

Misc CSS
- Add .sr-only utility for accessible off-screen live regions
- Add button.ghost.is-success-flash for transient success feedback
- Add .library-filter-selects / .library-filter-select for dropdown
  filter rows
- Add .media-provider-coming-soon-* for the roadmap drawer

Co-authored-by: Cursor <cursoragent@cursor.com>

* [codex] Add Cursor Agent auth diagnostics (#1538)

* Add Cursor Agent auth diagnostics

* Handle Cursor not logged in auth status

* Address Cursor auth review feedback

* Classify Cursor stdout auth failures

* test: expand Memory and Routines coverage (#1521)

* test: expand settings and packaged coverage

* test: extend memory settings coverage

* test: cover routine settings failure states

* test: cover routine operation failures

* test: fix daemon test typing on CI

* test: decouple packaged smoke from orbit bug

* test: avoid live memory LLM calls in route tests

* test: fix daemon fetch typing in CI

* fix: restore preview comment and inspect toggles

* test: align manual edit flow with current inspector UX

* test: align comment attachment flow with current preview comments UI

* fix: probe resolved Codex launch path during detection

* fix: remove duplicate board activation helper after rebase

* test: update ghost cli detection mock

* test: align FileViewer toolbar expectation

* ci: move full app tests to extended lane

* ci: run app tests by changed scope

* ci: cover shared app inputs in test scopes

* ci: avoid setup-node cache in windows packaged smoke

* test: align extended settings and manual edit flows

* refactor(web): rename Execution mode and tighten settings dialog UI

- Rename "Settings → Execution & model" to "Settings → Execution mode"
  across the web UI, i18n keys, docs, and e2e selectors.
- Redesign SettingsDialog: kicker + title row in the modal head, a
  flatMap-driven agent grid that renders the inline test-result row
  beside the selected card, compact unavailable cards with right-aligned
  install/docs links, and an install guide that only shows when the
  user has no working agent picked.
- Trim verbose subtitle / hint copy across chat model, CLI proxy,
  media providers, custom instructions, and memory sections.
- Add an `info` Icon variant for the redesigned settings hints.
- Update e2e selectors and docs that referenced the old menu label.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(web): settings dialog UX polish — layout, dedup, and interactions

- Remove duplicate section headers from all settings sections
  (Notifications, Appearance, Privacy, About, Design Systems, Skills,
  MCP server, Connectors, Media providers, Routines)
- Restructure Notifications cards: title + toggle on same row, hint below
- Restructure Skills toolbar: search + New skill button in row 1,
  filter dropdowns in row 2 with left-aligned labels
- Restructure Pet section: tabs and Wake button on same row
- MCP server: group capabilities and setup into separate cards,
  remove nested double border on client picker
- Connectors: show connect errors as toast instead of inline card text,
  position toast inside panel, hide single-provider tab
- Media providers: move Reload button to left-aligned small ghost button
- Memory: info icon shows path on hover, Path copied badge inline;
  Extraction history and MEMORY.md as standalone collapsible cards;
  group header hidden when only one type visible
- Pet grid cards: Adopt button hidden until hover, icon-only when adopted,
  description truncated to 2 lines, text fills full width via abs positioning
- Agent cards: selected state uses accent border only, no background change
- Add sun/moon icons to Appearance theme buttons (Light/Dark)
- Shorten several hint strings for clarity

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): resolve i18n review comments from PR #1568

- Update settings.title and settings.envConfigure to localized
  "Execution mode" in all 17 non-English locale files
- Add settings.memoryFlashPathCopied to all locales and use t()
  in MemorySection instead of hardcoded English "Path copied"
- Add settings.agentModelHead to all locales and use t() in
  SettingsDialog for "Model for:" agent model row header

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): update tests to match settings dialog redesign

- Add role prop to Toast (alert/status) so error toasts from
  ConnectorsBrowser are announced immediately by screen readers
- Clear connectErrorToast on successful connector retry
- Update SettingsDialog.execution tests:
  - Remove heading assertions for About and MCP server (headers
    were intentionally removed as duplicate nav labels)
  - Rewrite CLI env test to use codex-only fields (per-agent
    filtering means only selected agent's fields are shown)
  - Update Composio key hint text assertion to match shortened copy
  - Replace filter button click with select change for Type filter
  - Replace Configured/Unsupported/Integrated badge checks with
    updated assertions matching the new media provider UI
  - Replace disabled BFL row test with coming-soon section check
- Update SettingsDialog.media test: remove Fal.ai input assertions
  (non-integrated providers no longer have editable fields)

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): unblock CI for #1568

Three small fixes to get Playwright back to green on the settings
dialog redesign:

1. `en.ts`: revert `settings.envConfigure` to "Configure execution mode".
   This PR collapsed both `settings.title` (header gear) and
   `settings.envConfigure` (entry-side foot pill) to the same string
   "Execution mode", so `getByRole('button', { name: 'Execution mode' })`
   resolved to two elements and tripped Playwright strict mode in the
   three Composio-flow tests (entry-configuration-flows.test.ts:174,
   228, 285). Restoring the distinct label also gives screen readers
   a clearer hint for the pill, which doubles as a status display.
   Non-English locales still alias the two keys; happy to follow up
   on those, but they don't gate the (English-only) Playwright suite.

2. entry-configuration-flows.test.ts:167 — `Connectors` heading is now
   rendered at `<h2>` in the modal-head (SettingsDialog.tsx:1545), with
   the inner `<h3>` removed by design (see comment around line 1448).
   Updated the assertion from `level: 3` to `level: 2`.

3. project-management-flows.test.ts:360 — same change for the `Pets`
   heading.

Verified locally with `pnpm --filter @open-design/web typecheck` and
`pnpm --filter @open-design/e2e typecheck`. The actual Playwright
specs need the dev server up; I didn't rerun them here, but the
locator changes are mechanical and match the new DOM.

* fix(web): use exact match for Execution mode button locator

Playwright's `getByRole({ name })` defaults to substring matching, so
`{ name: 'Execution mode' }` still resolved to both the header gear
(aria-label "Execution mode") and the entry-side foot pill (aria-label
"Configure execution mode" — substring contains "Execution mode").
Strict mode tripped in the three composio-flow tests at lines 202,
257, and 319.

Adding `exact: true` makes each call resolve to just the header gear,
which opens the same dialog the foot pill does — the test outcomes
are unchanged.

---------

Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Caprika <56862773+alchemistklk@users.noreply.github.com>
Co-authored-by: shangxinyu1 <shangxinyu@refly.ai>
Co-authored-by: lefarcen <935902669@qq.com>
2026-05-15 14:35:06 +08:00

526 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Single source of truth for the media-generation model registry.
*
* Both the frontend (NewProjectPanel model pickers, Settings dialog
* provider list) and the daemon (od media generate dispatcher) consume
* this registry. When you add a model entry here, the picker shows it,
* the daemon can dispatch to it, and the Settings dialog knows which
* API keys are needed.
*
* The model catalogue mirrors the breadth of lobehub's model-bank:
* every image / video model that lobehub natively supports is listed
* here so the user can pick from the same surface area without us
* re-implementing every provider's transport. For provider integrations
* we only ship the two flagship paths today — OpenAI (gpt-image-*) and
* Volcengine Ark (Seedance 2.0) — the rest fall back to a placeholder
* with a clear "no provider integration yet" note. The contract the
* code agent follows is identical regardless.
*
* The daemon imports the JS mirror of this file at
* daemon/media-models.js (kept in sync by review).
*/
import type { AudioKind, MediaAspect } from '../types';
/**
* Provider identifier — used both as a grouping key in the picker and as
* the lookup key for API-credentials in `AppConfig.mediaProviders`. New
* providers must be added to {@link MEDIA_PROVIDERS} below.
*/
export type MediaProviderId =
| 'openai'
| 'volcengine'
| 'grok'
| 'hyperframes'
| 'nanobanana'
| 'bfl'
| 'fal'
| 'replicate'
| 'google'
| 'midjourney'
| 'kling'
| 'minimax'
| 'suno'
| 'udio'
| 'elevenlabs'
| 'fishaudio'
| 'senseaudio'
| 'tavily'
| 'stub';
export interface MediaProvider {
id: MediaProviderId;
/** Display name shown in Settings + ModelPicker headers. */
label: string;
/** Short marketing-style sub-label. */
hint: string;
/** Whether the daemon ships a real integration for this provider. */
integrated: boolean;
/** Whether the provider needs user-supplied credentials. */
credentialsRequired?: boolean;
/** Whether the provider should appear in Settings -> Media. */
settingsVisible?: boolean;
/** Default base URL the daemon hits when no override is configured. */
defaultBaseUrl?: string;
/** Documentation URL for getting an API key. */
docsUrl?: string;
/** Whether Settings should expose a custom model override field. */
supportsCustomModel?: boolean;
}
/**
* Catalogue of providers. The Settings dialog renders one section per
* entry; the new-project model picker uses {@link integrated} to flag
* cards that will silently fall back to a stub if the user hasn't
* configured a key.
*/
export const MEDIA_PROVIDERS: MediaProvider[] = [
{
id: 'openai',
label: 'OpenAI',
hint: 'gpt-image-2 / dall-e-3',
integrated: true,
defaultBaseUrl: 'https://api.openai.com/v1',
docsUrl: 'https://platform.openai.com/api-keys',
},
{
id: 'volcengine',
label: 'Volcengine Ark (Doubao)',
hint: 'Seedance 2.0 / Seedream',
integrated: true,
defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
docsUrl: 'https://console.volcengine.com/ark',
},
{
id: 'grok',
label: 'xAI Grok Imagine',
hint: 'grok-imagine — image + video with native audio',
integrated: true,
defaultBaseUrl: 'https://api.x.ai/v1',
docsUrl: 'https://docs.x.ai/developers/model-capabilities/video/generation',
},
{
id: 'hyperframes',
label: 'HyperFrames',
hint: 'Local HTML -> MP4 renderer',
integrated: true,
credentialsRequired: false,
settingsVisible: false,
docsUrl: 'https://hyperframes.heygen.com',
},
{
id: 'nanobanana',
label: 'Nano Banana',
hint: 'Google official by default; custom gateway configurable',
integrated: true,
defaultBaseUrl: 'https://generativelanguage.googleapis.com',
docsUrl: 'https://ai.google.dev/gemini-api/docs/api-key',
supportsCustomModel: true,
},
{
id: 'bfl',
label: 'Black Forest Labs',
hint: 'FLUX 1.1 Pro / FLUX Pro / Dev',
integrated: false,
defaultBaseUrl: 'https://api.bfl.ai',
docsUrl: 'https://docs.bfl.ai/quick_start/create_account',
},
{
id: 'fal',
label: 'Fal.ai',
hint: 'Sora / Seedance / Veo / FLUX',
integrated: false,
defaultBaseUrl: 'https://fal.run',
docsUrl: 'https://fal.ai/dashboard/keys',
},
{
id: 'replicate',
label: 'Replicate',
hint: 'FLUX / SDXL / Ideogram',
integrated: false,
defaultBaseUrl: 'https://api.replicate.com/v1',
docsUrl: 'https://replicate.com/account/api-tokens',
},
{
id: 'google',
label: 'Google AI / Vertex',
hint: 'Imagen 4 / Veo 3 / Lyria',
integrated: false,
docsUrl: 'https://ai.google.dev/gemini-api/docs/api-key',
},
{
id: 'kling',
label: 'Kuaishou Kling',
hint: 'Kling 1.6 / 2.0 video',
integrated: false,
docsUrl: 'https://klingai.com/dev-center',
},
{
id: 'midjourney',
label: 'Midjourney (proxy)',
hint: 'midjourney-v7',
integrated: false,
},
{
id: 'minimax',
label: 'MiniMax',
hint: 'TTS / video-01',
integrated: true,
defaultBaseUrl: 'https://api.minimaxi.chat/v1',
docsUrl: 'https://platform.minimaxi.com',
},
{
id: 'suno',
label: 'Suno',
hint: 'Music generation',
integrated: false,
},
{
id: 'udio',
label: 'Udio',
hint: 'Music generation',
integrated: false,
},
{
id: 'elevenlabs',
label: 'ElevenLabs',
hint: 'Voice / SFX',
integrated: true,
defaultBaseUrl: 'https://api.elevenlabs.io',
docsUrl: 'https://elevenlabs.io/app/settings/api-keys',
},
{
id: 'fishaudio',
label: 'FishAudio',
hint: 'Speech / voice clone',
integrated: true,
defaultBaseUrl: 'https://api.fish.audio',
docsUrl: 'https://fish.audio',
},
{
id: 'senseaudio',
label: 'SenseAudio',
hint: 'TTS · 70+ system voices · clone',
integrated: true,
defaultBaseUrl: 'https://api.senseaudio.cn',
docsUrl: 'https://docs.senseaudio.cn',
},
{
id: 'tavily',
label: 'Tavily Search',
hint: 'Agent-callable web research',
integrated: true,
defaultBaseUrl: 'https://api.tavily.com',
docsUrl: 'https://app.tavily.com/home',
},
{
id: 'stub',
label: 'Stub (placeholder)',
hint: 'Deterministic local placeholder bytes',
integrated: true,
// Internal fixture provider used by the daemon for deterministic
// tests / offline demos. Hidden from Settings the same way
// HyperFrames is — end users have nothing to configure here, and
// exposing it pollutes the provider list.
settingsVisible: false,
},
];
export interface MediaModel {
/** Stable ID used in metadata.imageModel / videoModel / audioModel. */
id: string;
/** Short label shown in pickers. */
label: string;
/** Vendor / context hint shown under the label. */
hint: string;
/** Provider this model is dispatched through. */
provider: MediaProviderId;
/**
* Capabilities the agent may rely on when planning. Used downstream by
* the dispatcher to decide which provider call to make.
*/
caps?: string[];
/** Marks the default-checked card per surface in the picker. */
default?: boolean;
}
/**
* Image generation models. Mirrors the breadth of
* `packages/model-bank/src/aiModels/openai.ts` and friends in lobehub.
*/
export const IMAGE_MODELS: MediaModel[] = [
// OpenAI — fully integrated path.
{
id: 'gpt-image-2',
label: 'gpt-image-2',
hint: 'OpenAI · 4K, native multimodal',
provider: 'openai',
caps: ['t2i', 'i2i', 'inpaint'],
default: true,
},
{
id: 'gpt-image-1.5',
label: 'gpt-image-1.5',
hint: 'OpenAI · 4× faster than gpt-image-1',
provider: 'openai',
caps: ['t2i', 'i2i', 'inpaint'],
},
{
id: 'gpt-image-1',
label: 'gpt-image-1',
hint: 'OpenAI · ChatGPT native',
provider: 'openai',
caps: ['t2i', 'i2i', 'inpaint'],
},
{
id: 'gpt-image-1-mini',
label: 'gpt-image-1-mini',
hint: 'OpenAI · low-cost variant',
provider: 'openai',
caps: ['t2i', 'i2i'],
},
{
id: 'dall-e-3',
label: 'dall-e-3',
hint: 'OpenAI · classic',
provider: 'openai',
caps: ['t2i'],
},
{
id: 'dall-e-2',
label: 'dall-e-2',
hint: 'OpenAI · legacy',
provider: 'openai',
caps: ['t2i'],
},
// Volcengine — Doubao Seedream image generation.
{
id: 'doubao-seedream-3-0-t2i-250415',
label: 'seedream-3.0',
hint: 'ByteDance · Doubao image',
provider: 'volcengine',
caps: ['t2i'],
},
{
id: 'doubao-seededit-3-0-i2i-250628',
label: 'seededit-3.0',
hint: 'ByteDance · image edit',
provider: 'volcengine',
caps: ['i2i'],
},
// xAI Grok Imagine — text-to-image (1k/2k, 11+ aspect ratios).
{
id: 'grok-imagine-image',
label: 'grok-imagine-image',
hint: 'xAI · 2K text-to-image',
provider: 'grok',
caps: ['t2i'],
},
// Nano Banana — Google-compatible generateContent image path.
{
id: 'gemini-3.1-flash-image-preview',
label: 'nano-banana-2',
hint: 'Nano Banana · text-to-image',
provider: 'nanobanana',
caps: ['t2i'],
},
// Black Forest Labs FLUX family.
{ id: 'flux-1.1-pro', label: 'flux-1.1-pro', hint: 'BFL · flagship', provider: 'bfl', caps: ['t2i', 'i2i'] },
{ id: 'flux-pro', label: 'flux-pro', hint: 'BFL', provider: 'bfl', caps: ['t2i'] },
{ id: 'flux-dev', label: 'flux-dev', hint: 'BFL · open weights', provider: 'bfl', caps: ['t2i'] },
{ id: 'flux-schnell', label: 'flux-schnell', hint: 'BFL · fast', provider: 'bfl', caps: ['t2i'] },
{ id: 'flux-kontext-pro', label: 'flux-kontext-pro', hint: 'BFL · in-context edits', provider: 'bfl', caps: ['t2i', 'i2i'] },
// Google.
{ id: 'imagen-4', label: 'imagen-4', hint: 'Google · latest', provider: 'google', caps: ['t2i'] },
{ id: 'imagen-3', label: 'imagen-3', hint: 'Google', provider: 'google', caps: ['t2i'] },
{ id: 'gemini-3-pro-image-preview', label: 'gemini-3-pro-image', hint: 'Google · Nano Banana Pro', provider: 'google', caps: ['t2i', 'i2i'] },
// Replicate / Fal hosted image models.
{ id: 'ideogram-v2', label: 'ideogram-v2', hint: 'Replicate · typography', provider: 'replicate', caps: ['t2i'] },
{ id: 'sdxl', label: 'stable-diffusion-xl', hint: 'Replicate · SDXL', provider: 'replicate', caps: ['t2i'] },
{ id: 'sd-3.5', label: 'stable-diffusion-3.5', hint: 'Fal · SD 3.5', provider: 'fal', caps: ['t2i'] },
// Midjourney via community proxies.
{ id: 'midjourney-v7', label: 'midjourney-v7', hint: 'Midjourney · via proxy', provider: 'midjourney', caps: ['t2i'] },
];
/**
* Video generation models. Mirrors lobehub's volcengine.ts (Seedance,
* Seedance Lite), kling.ts and friends.
*/
export const VIDEO_MODELS: MediaModel[] = [
// Volcengine — Seedance 2.0 (integrated).
{
id: 'doubao-seedance-2-0-260128',
label: 'seedance-2.0',
hint: 'ByteDance · t2v + i2v + audio',
provider: 'volcengine',
caps: ['t2v', 'i2v', 'audio'],
default: true,
},
{
id: 'doubao-seedance-2-0-fast-260128',
label: 'seedance-2.0-fast',
hint: 'ByteDance · faster, cheaper',
provider: 'volcengine',
caps: ['t2v', 'i2v', 'audio'],
},
{
id: 'doubao-seedance-1-0-pro-250528',
label: 'seedance-1.0-pro',
hint: 'ByteDance · 1.0',
provider: 'volcengine',
caps: ['t2v', 'i2v'],
},
{
id: 'doubao-seedance-1-0-lite-i2v-250428',
label: 'seedance-1.0-lite-i2v',
hint: 'ByteDance · image-to-video',
provider: 'volcengine',
caps: ['i2v'],
},
{
id: 'doubao-seedance-1-0-lite-t2v-250428',
label: 'seedance-1.0-lite-t2v',
hint: 'ByteDance · text-to-video',
provider: 'volcengine',
caps: ['t2v'],
},
// xAI Grok Imagine — 720p t2v + i2v with natively generated audio.
{
id: 'grok-imagine-video',
label: 'grok-imagine-video',
hint: 'xAI · 720p t2v + i2v + native audio',
provider: 'grok',
caps: ['t2v', 'i2v', 'audio'],
},
// Kuaishou Kling.
{ id: 'kling-2.0', label: 'kling-2.0', hint: 'Kuaishou · latest', provider: 'kling', caps: ['t2v', 'i2v'] },
{ id: 'kling-1.6', label: 'kling-1.6', hint: 'Kuaishou', provider: 'kling', caps: ['t2v', 'i2v'] },
{ id: 'kling-1.5', label: 'kling-1.5', hint: 'Kuaishou', provider: 'kling', caps: ['t2v', 'i2v'] },
// Google Veo.
{ id: 'veo-3', label: 'veo-3', hint: 'Google · sound-on', provider: 'google', caps: ['t2v', 'audio'] },
{ id: 'veo-2', label: 'veo-2', hint: 'Google', provider: 'google', caps: ['t2v'] },
// OpenAI Sora (via Fal hosting today).
{ id: 'sora-2', label: 'sora-2', hint: 'OpenAI · via Fal', provider: 'fal', caps: ['t2v'] },
{ id: 'sora-2-pro', label: 'sora-2-pro', hint: 'OpenAI · via Fal', provider: 'fal', caps: ['t2v'] },
// MiniMax video.
{ id: 'minimax-video-01', label: 'video-01', hint: 'MiniMax · Hailuo', provider: 'minimax', caps: ['t2v', 'i2v'] },
{ id: 'hyperframes-html', label: 'hyperframes-html', hint: 'HyperFrames · local HTML renderer', provider: 'hyperframes', caps: ['t2v'] },
];
export const AUDIO_MODELS_BY_KIND: Record<AudioKind, MediaModel[]> = {
music: [
{ id: 'suno-v5', label: 'suno-v5', hint: 'Suno · default', provider: 'suno', caps: ['music'], default: true },
{ id: 'suno-v4-5', label: 'suno-v4.5', hint: 'Suno', provider: 'suno', caps: ['music'] },
{ id: 'udio-v2', label: 'udio-v2', hint: 'Udio', provider: 'udio', caps: ['music'] },
{ id: 'lyria-2', label: 'lyria-2', hint: 'Google', provider: 'google', caps: ['music'] },
],
speech: [
{ id: 'minimax-tts', label: 'minimax-tts', hint: 'MiniMax', provider: 'minimax', caps: ['tts'], default: true },
{ id: 'fish-speech-2', label: 'fish-speech-2', hint: 'FishAudio', provider: 'fishaudio', caps: ['tts', 'voice-clone'] },
{ id: 'elevenlabs-v3', label: 'elevenlabs-v3', hint: 'ElevenLabs', provider: 'elevenlabs', caps: ['tts', 'voice-clone'] },
{ id: 'senseaudio-tts', label: 'senseaudio-tts', hint: 'SenseAudio', provider: 'senseaudio', caps: ['tts', 'voice-clone'] },
{ id: 'doubao-tts', label: 'doubao-tts', hint: 'Volcengine', provider: 'volcengine', caps: ['tts'] },
{ id: 'gpt-4o-mini-tts', label: 'gpt-4o-mini-tts', hint: 'OpenAI', provider: 'openai', caps: ['tts'] },
],
sfx: [
{ id: 'elevenlabs-sfx', label: 'elevenlabs-sfx', hint: 'ElevenLabs SFX', provider: 'elevenlabs', caps: ['sfx'], default: true },
{ id: 'audiocraft', label: 'audiocraft', hint: 'Meta · open', provider: 'replicate', caps: ['sfx', 'music'] },
],
};
export const MEDIA_ASPECTS: MediaAspect[] = ['1:1', '16:9', '9:16', '4:3', '3:4'];
export const VIDEO_LENGTHS_SEC: number[] = [3, 5, 8, 10, 15, 30];
export const AUDIO_DURATIONS_SEC: number[] = [5, 10, 15, 30, 60, 120];
export const DEFAULT_IMAGE_MODEL =
IMAGE_MODELS.find((m) => m.default)?.id ?? IMAGE_MODELS[0]!.id;
export const DEFAULT_VIDEO_MODEL =
VIDEO_MODELS.find((m) => m.default)?.id ?? VIDEO_MODELS[0]!.id;
export const DEFAULT_AUDIO_MODEL: Record<AudioKind, string> = {
music:
AUDIO_MODELS_BY_KIND.music.find((m) => m.default)?.id
?? AUDIO_MODELS_BY_KIND.music[0]!.id,
speech:
AUDIO_MODELS_BY_KIND.speech.find((m) => m.default)?.id
?? AUDIO_MODELS_BY_KIND.speech[0]!.id,
sfx:
AUDIO_MODELS_BY_KIND.sfx.find((m) => m.default)?.id
?? AUDIO_MODELS_BY_KIND.sfx[0]!.id,
};
/**
* Look up a model record across all surfaces by ID. Returns null if the
* agent passes an unknown model — the dispatcher rejects with a clear
* error so the agent re-plans instead of silently falling back.
*/
export function findMediaModel(id: string): MediaModel | null {
const all: MediaModel[] = [
...IMAGE_MODELS,
...VIDEO_MODELS,
...AUDIO_MODELS_BY_KIND.music,
...AUDIO_MODELS_BY_KIND.speech,
...AUDIO_MODELS_BY_KIND.sfx,
];
return all.find((m) => m.id === id) ?? null;
}
export function findProvider(id: MediaProviderId): MediaProvider | null {
return MEDIA_PROVIDERS.find((p) => p.id === id) ?? null;
}
/** All model IDs grouped by surface, used for prompt-side disclosure. */
export function modelIdsBySurface(): {
image: string[];
video: string[];
audio: { music: string[]; speech: string[]; sfx: string[] };
} {
return {
image: IMAGE_MODELS.map((m) => m.id),
video: VIDEO_MODELS.map((m) => m.id),
audio: {
music: AUDIO_MODELS_BY_KIND.music.map((m) => m.id),
speech: AUDIO_MODELS_BY_KIND.speech.map((m) => m.id),
sfx: AUDIO_MODELS_BY_KIND.sfx.map((m) => m.id),
},
};
}
/**
* Group a flat list of {@link MediaModel} by provider while preserving
* the catalogue order. Used by the picker to render section headers.
*/
export function groupByProvider(models: MediaModel[]): Array<{
provider: MediaProvider;
models: MediaModel[];
}> {
const order: MediaProviderId[] = [];
const map = new Map<MediaProviderId, MediaModel[]>();
for (const m of models) {
if (!map.has(m.provider)) {
order.push(m.provider);
map.set(m.provider, []);
}
map.get(m.provider)!.push(m);
}
return order
.map((id) => {
const provider = findProvider(id);
const list = map.get(id) ?? [];
return provider ? { provider, models: list } : null;
})
.filter((entry): entry is { provider: MediaProvider; models: MediaModel[] } => entry != null);
}