Update TTS providers and story markup
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* LocalOpenAITTSModule
|
||||
* Provides TTS via local or self-hosted OpenAI-compatible /audio/speech APIs.
|
||||
*/
|
||||
import { ApiTTSModuleBase } from './api-tts-module-base.js';
|
||||
|
||||
export class LocalOpenAITTSModule extends ApiTTSModuleBase {
|
||||
constructor() {
|
||||
super('local-openai-tts', 'Local OpenAI TTS');
|
||||
|
||||
this.voiceOptions = {
|
||||
voice: 'alloy',
|
||||
model: 'tts-1',
|
||||
speed: 1.0,
|
||||
response_format: 'mp3'
|
||||
};
|
||||
this.voices = [];
|
||||
}
|
||||
|
||||
getDefaultApiBaseUrl() {
|
||||
return 'http://localhost:8000/v1';
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
try {
|
||||
this.reportProgress(10, 'Initializing Local OpenAI TTS');
|
||||
|
||||
const parentInit = await super.initialize();
|
||||
if (!parentInit) {
|
||||
console.error('Local OpenAI TTS: Parent initialization failed');
|
||||
return false;
|
||||
}
|
||||
|
||||
const persistenceManager = this.getModule('persistence-manager');
|
||||
if (!persistenceManager) {
|
||||
console.error('Local OpenAI TTS: Required dependency persistence-manager not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
const preferredVoice = persistenceManager.getPreference('tts', `${this.id}_voice`, this.voiceOptions.voice);
|
||||
if (preferredVoice) {
|
||||
this.voiceOptions.voice = this.normalizeTextOption(preferredVoice, this.voiceOptions.voice);
|
||||
}
|
||||
|
||||
const preferredModel = persistenceManager.getPreference('tts', `${this.id}_model`, this.voiceOptions.model);
|
||||
if (preferredModel) {
|
||||
this.voiceOptions.model = this.normalizeTextOption(preferredModel, this.voiceOptions.model);
|
||||
}
|
||||
|
||||
const preferredFormat = persistenceManager.getPreference('tts', `${this.id}_format`, this.voiceOptions.response_format);
|
||||
if (preferredFormat) {
|
||||
this.voiceOptions.response_format = this.normalizeResponseFormat(preferredFormat);
|
||||
}
|
||||
|
||||
const preferredSpeed = persistenceManager.getPreference('tts', 'speed', this.voiceOptions.speed);
|
||||
if (typeof preferredSpeed === 'number') {
|
||||
this.voiceOptions.speed = this.normalizeAppSpeed(preferredSpeed);
|
||||
}
|
||||
|
||||
this.isReady = Boolean(this.apiBaseUrl && this.voiceOptions.voice && this.voiceOptions.model);
|
||||
this.reportProgress(100, this.isReady ? 'Local OpenAI TTS initialized' : 'Local OpenAI TTS not configured');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Local OpenAI TTS: Initialization error:', error);
|
||||
this.isReady = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async loadVoices() {
|
||||
this.voices = [];
|
||||
return true;
|
||||
}
|
||||
|
||||
selectVoiceForLocale() {
|
||||
return this.selectDefaultVoice();
|
||||
}
|
||||
|
||||
selectDefaultVoice() {
|
||||
this.voiceOptions.voice = this.normalizeTextOption(this.voiceOptions.voice, 'alloy');
|
||||
return true;
|
||||
}
|
||||
|
||||
getAvailableVoices() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getVoices() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async generateSpeechAudio(text, options = {}) {
|
||||
if (!this.isReady || !this.apiBaseUrl) {
|
||||
return { success: false, reason: 'not_ready' };
|
||||
}
|
||||
|
||||
try {
|
||||
const processedText = this.preprocessText(text);
|
||||
if (!processedText) {
|
||||
return { success: false, reason: 'empty_text' };
|
||||
}
|
||||
|
||||
const payload = {
|
||||
model: this.normalizeTextOption(this.voiceOptions.model, 'tts-1'),
|
||||
input: processedText,
|
||||
voice: this.normalizeTextOption(this.voiceOptions.voice, 'alloy'),
|
||||
response_format: this.normalizeResponseFormat(this.voiceOptions.response_format),
|
||||
speed: this.getApiSpeed(this.voiceOptions.speed)
|
||||
};
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
if (this.apiKey) {
|
||||
headers.Authorization = `Bearer ${this.apiKey}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiBaseUrl.replace(/\/+$/, '')}/audio/speech`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(payload),
|
||||
signal: options.signal
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`API error: ${response.status} ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
|
||||
const audioBlob = await response.blob();
|
||||
const arrayBuffer = await audioBlob.arrayBuffer();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
audioData: arrayBuffer
|
||||
};
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
console.error('Local OpenAI TTS: Speech request was aborted:', error);
|
||||
return {
|
||||
success: false,
|
||||
reason: 'aborted',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
console.error('Local OpenAI TTS: Error generating speech:', error);
|
||||
return {
|
||||
success: false,
|
||||
reason: 'api_error',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
setVoiceOptions(options = {}) {
|
||||
const persistenceManager = this.getModule('persistence-manager');
|
||||
|
||||
if (typeof options.voice === 'string') {
|
||||
this.voiceOptions.voice = this.normalizeTextOption(options.voice, this.voiceOptions.voice);
|
||||
if (persistenceManager) {
|
||||
persistenceManager.updatePreference('tts', `${this.id}_voice`, this.voiceOptions.voice);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof options.speed === 'number') {
|
||||
this.voiceOptions.speed = this.normalizeAppSpeed(options.speed);
|
||||
}
|
||||
|
||||
if (typeof options.model === 'string') {
|
||||
this.voiceOptions.model = this.normalizeTextOption(options.model, this.voiceOptions.model);
|
||||
if (persistenceManager) {
|
||||
persistenceManager.updatePreference('tts', `${this.id}_model`, this.voiceOptions.model);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof options.response_format === 'string') {
|
||||
this.voiceOptions.response_format = this.normalizeResponseFormat(options.response_format);
|
||||
if (persistenceManager) {
|
||||
persistenceManager.updatePreference('tts', `${this.id}_format`, this.voiceOptions.response_format);
|
||||
}
|
||||
}
|
||||
|
||||
this.isReady = Boolean(this.apiBaseUrl && this.voiceOptions.voice && this.voiceOptions.model);
|
||||
this.notifyReadyState();
|
||||
}
|
||||
|
||||
handleApiKeyChanged(event) {
|
||||
if (!event?.detail || event.detail.provider !== this.id) return;
|
||||
const newKey = event.detail.key || '';
|
||||
if (newKey && /^https?:\/\//i.test(newKey)) {
|
||||
console.error('Local OpenAI TTS: Received URL instead of API key, ignoring it');
|
||||
return;
|
||||
}
|
||||
|
||||
const oldKey = this.apiKey;
|
||||
this.apiKey = newKey;
|
||||
|
||||
const persistenceManager = this.getModule('persistence-manager');
|
||||
if (persistenceManager && oldKey !== newKey) {
|
||||
persistenceManager.updatePreference('tts', `${this.id}_api_key`, newKey);
|
||||
}
|
||||
|
||||
const wasReady = this.isReady;
|
||||
this.isReady = Boolean(this.apiBaseUrl && this.voiceOptions.voice && this.voiceOptions.model);
|
||||
if (wasReady !== this.isReady) {
|
||||
this.notifyReadyState();
|
||||
}
|
||||
}
|
||||
|
||||
handleApiUrlChanged(event) {
|
||||
if (!event?.detail || event.detail.provider !== this.id) return;
|
||||
const oldUrl = this.apiBaseUrl;
|
||||
const newUrl = String(event.detail.url || this.getDefaultApiBaseUrl()).trim().replace(/\/+$/, '');
|
||||
this.apiBaseUrl = newUrl;
|
||||
|
||||
const persistenceManager = this.getModule('persistence-manager');
|
||||
if (persistenceManager && oldUrl !== newUrl) {
|
||||
persistenceManager.updatePreference('tts', `${this.id}_api_url`, newUrl);
|
||||
}
|
||||
|
||||
const wasReady = this.isReady;
|
||||
this.isReady = Boolean(this.apiBaseUrl && this.voiceOptions.voice && this.voiceOptions.model);
|
||||
if (wasReady !== this.isReady || oldUrl !== newUrl) {
|
||||
this.notifyReadyState();
|
||||
}
|
||||
}
|
||||
|
||||
normalizeTextOption(value, fallback) {
|
||||
const text = String(value || '').trim();
|
||||
return text || fallback;
|
||||
}
|
||||
|
||||
normalizeResponseFormat(value) {
|
||||
const format = String(value || '').trim().toLowerCase();
|
||||
const validFormats = ['mp3', 'opus', 'aac', 'flac', 'wav', 'pcm'];
|
||||
return validFormats.includes(format) ? format : 'mp3';
|
||||
}
|
||||
|
||||
getApiSpeed(speed) {
|
||||
const value = Number.isFinite(Number(speed)) ? Number(speed) : this.normalizeAppSpeed(speed);
|
||||
return Math.max(0.25, Math.min(4.0, value));
|
||||
}
|
||||
|
||||
normalizeAppSpeed(speed) {
|
||||
const value = Number.isFinite(Number(speed)) ? Number(speed) : 1.0;
|
||||
return Math.max(0.5, Math.min(2.0, value));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const localOpenAITTSModule = new LocalOpenAITTSModule();
|
||||
|
||||
export { localOpenAITTSModule };
|
||||
|
||||
if (window.moduleRegistry) {
|
||||
window.moduleRegistry.register(localOpenAITTSModule);
|
||||
}
|
||||
|
||||
window.LocalOpenAITTSModule = localOpenAITTSModule;
|
||||
Reference in New Issue
Block a user