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>
103 lines
3.5 KiB
TypeScript
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;
|
|
}
|