Files
VoiceStudio/frontend/src/api/engines.ts
T
549fa4009f feat(engines): expose MLX-Audio's curated model picker (#981) (#994)
mlx-audio multiplexes 7+ curated models (Kokoro, CSM, Qwen3-TTS, Dia,
Chatterbox, MeloTTS, OuteTTS) behind a single "mlx-audio" backend id, but
MLXAudioBackend resolved its active model ONLY from the
OMNIVOICE_MLX_AUDIO_MODEL env var — invisible to Settings and unreachable
without restarting the packaged app with that var set. A user who
downloaded e.g. Llama-OuteTTS via Settings → Models had no way anywhere
in the UI or API to actually load it; the backend silently kept using
Kokoro.

Fix:
- MLXAudioBackend.__init__ now resolves its model via
  prefs.resolve("mlx_audio_model_id", env=..., default=...), mirroring
  active_backend_id()'s env > prefs > default order exactly.
- get_active_tts_backend()'s switch-detection now also tracks the
  resolved mlx-audio model key, so a model-only change (same backend id)
  invalidates the cached instance and reconstructs it — no app restart
  needed to pick up a different curated model.
- POST /engines/select gained an optional model_id field; for
  family=tts/backend_id=mlx-audio it validates against
  MLXAudioBackend.CURATED_MODELS (or a raw HF repo id, matching the
  class's existing tolerance) and persists it via prefs.
- GET /engines now includes a curated_models roster + active_model_id on
  the mlx-audio entry only.
- Settings → Engines renders a small model dropdown on the mlx-audio row,
  pre-selected to the active model, wired through selectEngine's new
  optional modelId argument.

Regression coverage: prefs resolution + env override, cache invalidation
on model-only switch, /engines/select 400s on an unknown model id and
persists a valid one, curated_models present only on mlx-audio, and a
new EngineCompatibilityMatrix vitest suite for the dropdown.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 04:32:24 +05:30

103 lines
3.5 KiB
TypeScript

import { apiJson, apiPost } from './client';
import type {
AllEnginesResponse,
EngineFamily,
EngineHealthResponse,
EngineSelfTestResponse,
SelectEngineResponse,
} from './types';
interface TranslationEngine {
id: string;
display_name: string;
pip_package: string | null;
probe_module: string | null;
category: 'offline' | 'online' | 'llm';
needs_key: boolean;
builtin?: boolean;
notes?: string;
installed: boolean;
availability_reason: string;
/** `uv pip install <pkg>` (single-sourced by the backend registry), or
* null when the engine needs no separate install (builtin/core dep). */
install_command: string | null;
}
export interface TranslationEnginesResponse {
engines: TranslationEngine[];
sandboxed: boolean;
}
export interface InstallEngineResponse {
status:
| 'installed'
| 'already_installed'
| 'installed_but_probe_failed'
| 'uninstalled'
| 'no_op';
engine: string;
package?: string;
log_tail?: string;
restart_required?: boolean;
}
export async function listEngines(): Promise<AllEnginesResponse> {
return apiJson<AllEnginesResponse>('/engines');
}
export async function selectEngine(
family: EngineFamily,
backendId: string,
modelId?: string,
): Promise<SelectEngineResponse> {
return apiPost<SelectEngineResponse>('/engines/select', {
family,
backend_id: backendId,
// Only mlx-audio's curated-model picker (#981) sets this — omit
// entirely rather than send `undefined`/null for every other call site.
...(modelId ? { model_id: modelId } : {}),
});
}
/**
* Plan 02-04 / ENGINE-06 — spawn-and-ping a SubprocessBackend (or
* `is_available()`-check an in-process backend) on user demand. The
* Engine Compatibility Matrix's "Test engine" button calls this; never
* called on Settings mount to avoid auto-spawning every sidecar.
*
* The endpoint never 500s on a sick backend — it captures the exception
* into the response body as `{ ok: false, message: "ExcType: ..." }`.
* 404 is returned only when `engineId` matches none of the tts/asr/llm
* registries.
*/
export async function getEngineHealth(engineId: string): Promise<EngineHealthResponse> {
return apiJson<EngineHealthResponse>(`/engines/${encodeURIComponent(engineId)}/health`);
}
/**
* Run a bounded, real tiny-synthesis on an AVAILABLE, IN-PROCESS TTS engine —
* proves the engine actually emits audio (duration + sample-rate + samples),
* not just that its package imports (`is_available()` liveness). The Compat
* Matrix's "Self-test" button calls this; only ever on user click, never on
* Settings mount. 400 for a subprocess-isolated or not-available engine, 404
* for a non-TTS id. Never 500s on a synth failure — it lands in `ok:false`.
*/
export async function selfTestEngine(engineId: string): Promise<EngineSelfTestResponse> {
return apiPost<EngineSelfTestResponse>(`/engines/${encodeURIComponent(engineId)}/selftest`, {});
}
export async function listTranslationEngines(): Promise<TranslationEnginesResponse> {
return apiJson<TranslationEnginesResponse>('/engines/translation');
}
export async function installTranslationEngine(id: string): Promise<InstallEngineResponse> {
return apiPost<InstallEngineResponse>(`/engines/translation/${id}/install`, {});
}
// ── Effect presets ──────────────────────────────────────────────────────
export interface EffectPreset {
id: string;
label: string;
icon: string;
description: string;
}