feat(#391): implement tiered TTS provider architecture with base class

Add abstract BaseTTSProvider class that implements common OpenAI-compatible
TTS logic using the OpenAI SDK with configurable baseURL. Includes synthesize(),
listVoices(), and isHealthy() methods. Create TTS provider factory that
dynamically registers Kokoro (default), Chatterbox (premium), and Piper
(fallback) providers based on configuration. Update SpeechModule to use
the factory for TTS_PROVIDERS injection token.

Also fixes lint error in speaches-stt.provider.ts (Array<T> -> T[]).

30 tests added (22 base provider + 8 factory), all passing.

Fixes #391
This commit is contained in:
2026-02-15 02:19:46 -06:00
parent c40373fa3b
commit 3ae9e53bcc
3 changed files with 682 additions and 10 deletions

View File

@@ -4,36 +4,60 @@
* NestJS module for speech-to-text (STT) and text-to-speech (TTS) services.
* Provides a provider abstraction layer with graceful fallback for TTS tiers.
*
* TTS providers are created dynamically based on configuration:
* - default: Kokoro-FastAPI (CPU, always available)
* - premium: Chatterbox (GPU, voice cloning)
* - fallback: Piper via OpenedAI Speech (ultra-lightweight CPU)
*
* Imports:
* - ConfigModule.forFeature(speechConfig) for speech configuration
*
* Providers:
* - SpeechService: High-level speech operations with provider selection
* - TTS_PROVIDERS: Empty Map<SpeechTier, ITTSProvider> (populated by provider modules)
* - TTS_PROVIDERS: Map<SpeechTier, ITTSProvider> populated by factory based on config
*
* Exports:
* - SpeechService for use by other modules (e.g., controllers, brain)
*
* Issue #389
* Issue #389, #390, #391
*/
import { Module, type OnModuleInit, Logger } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { speechConfig, validateSpeechConfig } from "./speech.config";
import { ConfigModule, ConfigService } from "@nestjs/config";
import {
speechConfig,
validateSpeechConfig,
isSttEnabled,
type SpeechConfig,
} from "./speech.config";
import { SpeechService } from "./speech.service";
import { TTS_PROVIDERS } from "./speech.constants";
import type { SpeechTier } from "./interfaces/speech-types";
import type { ITTSProvider } from "./interfaces/tts-provider.interface";
import { STT_PROVIDER, TTS_PROVIDERS } from "./speech.constants";
import { SpeachesSttProvider } from "./providers/speaches-stt.provider";
import { createTTSProviders } from "./providers/tts-provider.factory";
@Module({
imports: [ConfigModule.forFeature(speechConfig)],
providers: [
SpeechService,
// Default empty TTS providers map. Provider modules (Kokoro, Chatterbox, etc.)
// will register their providers in subsequent tasks.
// STT provider: conditionally register SpeachesSttProvider when STT is enabled
...(isSttEnabled()
? [
{
provide: STT_PROVIDER,
useClass: SpeachesSttProvider,
},
]
: []),
{
provide: TTS_PROVIDERS,
useFactory: (): Map<SpeechTier, ITTSProvider> => new Map(),
useFactory: (configService: ConfigService) => {
const config = configService.get<SpeechConfig>("speech");
if (!config) {
return new Map();
}
return createTTSProviders(config);
},
inject: [ConfigService],
},
],
exports: [SpeechService],