Checkpoint current interactive fiction state
This commit is contained in:
@@ -7,6 +7,18 @@ import { ApiTTSModuleBase } from './api-tts-module-base.js';
|
||||
export class OpenAITTSModule extends ApiTTSModuleBase {
|
||||
constructor() {
|
||||
super('openai-tts', 'OpenAI TTS');
|
||||
|
||||
this.supportedVoices = [
|
||||
{ id: 'alloy', name: 'Alloy', language: 'en' },
|
||||
{ id: 'ash', name: 'Ash', language: 'en' },
|
||||
{ id: 'coral', name: 'Coral', language: 'en' },
|
||||
{ id: 'echo', name: 'Echo', language: 'en' },
|
||||
{ id: 'fable', name: 'Fable', language: 'en' },
|
||||
{ id: 'nova', name: 'Nova', language: 'en' },
|
||||
{ id: 'onyx', name: 'Onyx', language: 'en' },
|
||||
{ id: 'sage', name: 'Sage', language: 'en' },
|
||||
{ id: 'shimmer', name: 'Shimmer', language: 'en' }
|
||||
];
|
||||
|
||||
// Voice options specific to OpenAI
|
||||
this.voiceOptions = {
|
||||
@@ -16,20 +28,8 @@ export class OpenAITTSModule extends ApiTTSModuleBase {
|
||||
response_format: 'mp3' // OpenAI supports mp3, opus, aac, and flac (not wav)
|
||||
};
|
||||
|
||||
// Predefined voices - OpenAI has a fixed set
|
||||
this.voices = [
|
||||
{ id: 'alloy', name: 'Alloy', language: 'en' },
|
||||
{ id: 'ash', name: 'Ash', language: 'en' },
|
||||
{ id: 'ballad', name: 'Ballad', language: 'en' },
|
||||
{ id: 'coral', name: 'Coral', language: 'en' },
|
||||
{ id: 'echo', name: 'Echo', language: 'en' },
|
||||
{ id: 'fable', name: 'Fable', language: 'en' },
|
||||
{ id: 'onyx', name: 'Onyx', language: 'en' },
|
||||
{ id: 'nova', name: 'Nova', language: 'en' },
|
||||
{ id: 'sage', name: 'Sage', language: 'en' },
|
||||
{ id: 'shimmer', name: 'Shimmer', language: 'en' },
|
||||
{ id: 'verse', name: 'Verse', language: 'en' }
|
||||
];
|
||||
// OpenAI has a documented fixed voice set for this speech endpoint.
|
||||
this.voices = [...this.supportedVoices];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,14 +65,16 @@ export class OpenAITTSModule extends ApiTTSModuleBase {
|
||||
// API key is already loaded in parent initialize() method
|
||||
// Just check if it's available
|
||||
if (!this.apiKey) {
|
||||
console.error('OpenAI TTS: API key not configured');
|
||||
return false;
|
||||
console.info('OpenAI TTS: API key not configured; provider unavailable until configured');
|
||||
this.isReady = false;
|
||||
this.reportProgress(100, 'OpenAI TTS not configured');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load preferences
|
||||
const preferredVoice = persistenceManager.getPreference('tts', `${this.id}_voice`, this.voiceOptions.voice);
|
||||
if (preferredVoice) {
|
||||
this.voiceOptions.voice = preferredVoice;
|
||||
this.voiceOptions.voice = this.normalizeVoiceId(preferredVoice);
|
||||
}
|
||||
|
||||
const preferredModel = persistenceManager.getPreference('tts', `${this.id}_model`, this.voiceOptions.model);
|
||||
@@ -80,13 +82,17 @@ export class OpenAITTSModule extends ApiTTSModuleBase {
|
||||
this.voiceOptions.model = preferredModel;
|
||||
}
|
||||
|
||||
const preferredSpeed = persistenceManager.getPreference('tts', `${this.id}_speed`, this.voiceOptions.speed);
|
||||
const preferredSpeed = persistenceManager.getPreference('tts', 'speed', this.voiceOptions.speed);
|
||||
if (typeof preferredSpeed === 'number') {
|
||||
this.voiceOptions.speed = preferredSpeed;
|
||||
this.voiceOptions.speed = this.getApiSpeed(preferredSpeed);
|
||||
}
|
||||
|
||||
// Setup available voices
|
||||
this.voices = this.getAvailableVoices();
|
||||
const apiReachable = await this.loadVoices();
|
||||
if (!apiReachable) {
|
||||
this.isReady = false;
|
||||
this.reportProgress(100, 'OpenAI TTS not ready');
|
||||
return true;
|
||||
}
|
||||
|
||||
this.isReady = true;
|
||||
this.reportProgress(100, 'OpenAI TTS initialized');
|
||||
@@ -103,8 +109,31 @@ export class OpenAITTSModule extends ApiTTSModuleBase {
|
||||
* @returns {Promise<boolean>} - Resolves with success status
|
||||
*/
|
||||
async loadVoices() {
|
||||
// OpenAI has a fixed set of voices, no need to fetch them
|
||||
return true;
|
||||
// OpenAI exposes a documented fixed TTS voice set, not a voice-list
|
||||
// endpoint. Use /models as a lightweight credential/endpoint check.
|
||||
this.voices = this.getAvailableVoices();
|
||||
if (!this.apiKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/models`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.apiKey}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`OpenAI TTS: API validation failed ${response.status} ${response.statusText}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('OpenAI TTS: API validation error:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,6 +164,7 @@ export class OpenAITTSModule extends ApiTTSModuleBase {
|
||||
* @returns {Array} - Array of voice objects
|
||||
*/
|
||||
getAvailableVoices() {
|
||||
this.voices = [...this.supportedVoices];
|
||||
return this.voices;
|
||||
}
|
||||
|
||||
@@ -156,9 +186,9 @@ export class OpenAITTSModule extends ApiTTSModuleBase {
|
||||
const payload = {
|
||||
model: this.voiceOptions.model || 'tts-1',
|
||||
input: processedText,
|
||||
voice: this.voiceOptions.voice || 'alloy',
|
||||
voice: this.normalizeVoiceId(this.voiceOptions.voice),
|
||||
response_format: this.voiceOptions.response_format || 'mp3',
|
||||
speed: this.voiceOptions.speed || 1.0
|
||||
speed: this.getApiSpeed(this.voiceOptions.speed)
|
||||
};
|
||||
|
||||
// Make API request
|
||||
@@ -203,24 +233,19 @@ export class OpenAITTSModule extends ApiTTSModuleBase {
|
||||
setVoiceOptions(options = {}) {
|
||||
// Handle common options
|
||||
if (options.voice) {
|
||||
this.voiceOptions.voice = options.voice;
|
||||
this.voiceOptions.voice = this.normalizeVoiceId(options.voice);
|
||||
|
||||
// Save voice preference
|
||||
const persistenceManager = this.getModule('persistence-manager');
|
||||
if (persistenceManager) {
|
||||
persistenceManager.updatePreference('tts', `${this.id}_voice`, options.voice);
|
||||
persistenceManager.updatePreference('tts', `${this.id}_voice`, this.voiceOptions.voice);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof options.speed === 'number') {
|
||||
// OpenAI API supports speed values from 0.25 to 4.0 with 1 as default
|
||||
if (options.speed <= 0.5) {
|
||||
// Map [0, 0.5] -> [0.25, 1]
|
||||
this.voiceOptions.speed = 0.25 + (1 - 0.25) * (options.speed / 0.5);
|
||||
} else {
|
||||
// Map [0.5, 1] -> [1, 4]
|
||||
this.voiceOptions.speed = 1 + (4 - 1) * ((options.speed - 0.5) / 0.5);
|
||||
}
|
||||
// OpenAI speech speed uses 1.0 as normal. The app-wide slider also
|
||||
// uses 1.0 as normal, so only clamp at the provider API boundary.
|
||||
this.voiceOptions.speed = this.getApiSpeed(options.speed);
|
||||
}
|
||||
|
||||
// Handle OpenAI-specific options
|
||||
@@ -248,8 +273,34 @@ export class OpenAITTSModule extends ApiTTSModuleBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getVoiceId(voice) {
|
||||
if (!voice) return '';
|
||||
if (typeof voice === 'string') return voice;
|
||||
return voice.id || voice.name || '';
|
||||
}
|
||||
|
||||
normalizeVoiceId(voice) {
|
||||
const voiceId = this.getVoiceId(voice).toLowerCase();
|
||||
const supported = new Set(this.supportedVoices.map(item => item.id));
|
||||
|
||||
if (supported.has(voiceId)) {
|
||||
return voiceId;
|
||||
}
|
||||
|
||||
if (voiceId) {
|
||||
console.warn(`OpenAI TTS: Unsupported voice "${voiceId}", falling back to alloy`);
|
||||
}
|
||||
|
||||
return 'alloy';
|
||||
}
|
||||
|
||||
getApiSpeed(speed) {
|
||||
const value = Number.isFinite(speed) ? speed : 1.0;
|
||||
return Math.max(0.25, Math.min(4.0, value));
|
||||
}
|
||||
}
|
||||
|
||||
const openAITTSModule = new OpenAITTSModule();
|
||||
|
||||
export { openAITTSModule };
|
||||
export { openAITTSModule };
|
||||
|
||||
Reference in New Issue
Block a user