mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-16 05:10:42 -07:00
Add Kokoro-82M as a new TTS engine — 82M params, CPU realtime, 8 languages, Apache 2.0. Unlike cloning engines, Kokoro uses pre-built voice styles, which required a new profile type system to support non-cloning engines cleanly. Kokoro engine: - New kokoro_backend.py implementing TTSBackend protocol - 50 built-in voices across en/es/fr/hi/it/pt/ja/zh - KPipeline API with language-aware G2P routing via misaki - PyInstaller bundling for misaki, language_tags, espeakng_loader, en_core_web_sm Voice profile type system: - New voice_type column: 'cloned' | 'preset' | 'designed' (future) - Preset profiles store engine + voice ID instead of audio samples - default_engine field on profiles — auto-selects engine on profile pick - Create Voice dialog: toggle between 'Clone from audio' and 'Built-in voice' - Edit dialog shows preset voice info instead of sample list for preset profiles - Engine selector locks to preset engine when preset profile is selected - Profile grid filters by engine — shows Kokoro voices when Kokoro selected - Custom empty state when no preset profiles exist for selected engine Bug fixes: - Fix relative audio paths in DB causing 404s in production builds - config.set_data_dir() now resolves to absolute paths - Startup migration converts existing relative paths to absolute Also updates PROJECT_STATUS.md and tts-engines.mdx developer guide.
81 lines
2.3 KiB
TypeScript
81 lines
2.3 KiB
TypeScript
import { create } from 'zustand';
|
|
|
|
// Draft state for the create voice profile form
|
|
export interface ProfileFormDraft {
|
|
name: string;
|
|
description: string;
|
|
language: string;
|
|
referenceText: string;
|
|
sampleMode: 'upload' | 'record' | 'system';
|
|
// Note: File objects can't be persisted, so we store metadata
|
|
sampleFileName?: string;
|
|
sampleFileType?: string;
|
|
sampleFileData?: string; // Base64 encoded
|
|
}
|
|
|
|
interface UIStore {
|
|
// Sidebar
|
|
sidebarOpen: boolean;
|
|
setSidebarOpen: (open: boolean) => void;
|
|
|
|
// Modals
|
|
profileDialogOpen: boolean;
|
|
setProfileDialogOpen: (open: boolean) => void;
|
|
editingProfileId: string | null;
|
|
setEditingProfileId: (id: string | null) => void;
|
|
|
|
generationDialogOpen: boolean;
|
|
setGenerationDialogOpen: (open: boolean) => void;
|
|
|
|
// Selected profile for generation
|
|
selectedProfileId: string | null;
|
|
setSelectedProfileId: (id: string | null) => void;
|
|
|
|
// Currently selected engine (synced from generation form)
|
|
selectedEngine: string;
|
|
setSelectedEngine: (engine: string) => void;
|
|
|
|
// Selected voice in Voices tab inspector
|
|
selectedVoiceId: string | null;
|
|
setSelectedVoiceId: (id: string | null) => void;
|
|
|
|
// Profile form draft (for persisting create voice modal state)
|
|
profileFormDraft: ProfileFormDraft | null;
|
|
setProfileFormDraft: (draft: ProfileFormDraft | null) => void;
|
|
|
|
// Theme
|
|
theme: 'light' | 'dark';
|
|
setTheme: (theme: 'light' | 'dark') => void;
|
|
}
|
|
|
|
export const useUIStore = create<UIStore>((set) => ({
|
|
sidebarOpen: true,
|
|
setSidebarOpen: (open) => set({ sidebarOpen: open }),
|
|
|
|
profileDialogOpen: false,
|
|
setProfileDialogOpen: (open) => set({ profileDialogOpen: open }),
|
|
editingProfileId: null,
|
|
setEditingProfileId: (id) => set({ editingProfileId: id }),
|
|
|
|
generationDialogOpen: false,
|
|
setGenerationDialogOpen: (open) => set({ generationDialogOpen: open }),
|
|
|
|
selectedProfileId: null,
|
|
setSelectedProfileId: (id) => set({ selectedProfileId: id }),
|
|
|
|
selectedEngine: 'qwen',
|
|
setSelectedEngine: (engine) => set({ selectedEngine: engine }),
|
|
|
|
selectedVoiceId: null,
|
|
setSelectedVoiceId: (id) => set({ selectedVoiceId: id }),
|
|
|
|
profileFormDraft: null,
|
|
setProfileFormDraft: (draft) => set({ profileFormDraft: draft }),
|
|
|
|
theme: 'light',
|
|
setTheme: (theme) => {
|
|
set({ theme });
|
|
document.documentElement.classList.toggle('dark', theme === 'dark');
|
|
},
|
|
}));
|