@framers/agentos-ext-voice-synthesis 1.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,27 +1,41 @@
1
1
  /**
2
- * ElevenLabs TTS Tool — text-to-speech synthesis.
2
+ * Multi-provider TTS Tool — text-to-speech synthesis.
3
+ *
4
+ * Supports: OpenAI TTS, ElevenLabs, Ollama (local), any OpenAI-compatible TTS API.
5
+ * Auto-detects available provider from API keys in environment.
3
6
  */
4
7
 
5
- import type { ITool, ToolExecutionContext, ToolExecutionResult, JSONSchemaObject } from '../../../../../../agentos/src/core/tools/ITool.js';
8
+ import type { ITool, ToolExecutionContext, ToolExecutionResult, JSONSchemaObject } from '@framers/agentos';
9
+
10
+ export type TTSProvider = 'openai' | 'elevenlabs' | 'ollama' | 'auto';
6
11
 
7
12
  export interface TTSInput {
8
13
  text: string;
9
14
  voice?: string;
10
15
  model?: string;
16
+ provider?: TTSProvider;
17
+ /** ElevenLabs-specific */
11
18
  stability?: number;
19
+ /** ElevenLabs-specific */
12
20
  similarity_boost?: number;
21
+ /** OpenAI-specific: speed 0.25-4.0 */
22
+ speed?: number;
23
+ /** Output format: mp3, opus, aac, flac, wav */
24
+ format?: string;
13
25
  }
14
26
 
15
27
  export interface TTSOutput {
16
28
  text: string;
17
29
  voice: string;
18
30
  model: string;
31
+ provider: string;
19
32
  audioBase64: string;
20
33
  contentType: string;
21
34
  durationEstimateMs: number;
22
35
  }
23
36
 
24
- const VOICES: Record<string, string> = {
37
+ // ── ElevenLabs voice name → ID mapping ──
38
+ const ELEVENLABS_VOICES: Record<string, string> = {
25
39
  rachel: '21m00Tcm4TlvDq8ikWAM',
26
40
  domi: 'AZnzlk1XvdvUeBnXmlld',
27
41
  bella: 'EXAVITQu4vr4xnSDxMaL',
@@ -32,58 +46,224 @@ const VOICES: Record<string, string> = {
32
46
  sam: 'yoZ06aMxZJJ28mfd3POQ',
33
47
  };
34
48
 
49
+ // ── OpenAI voice options ──
50
+ const OPENAI_VOICES = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];
51
+
52
+ export interface TTSConfig {
53
+ openaiApiKey?: string;
54
+ openaiBaseUrl?: string;
55
+ elevenLabsApiKey?: string;
56
+ ollamaBaseUrl?: string;
57
+ defaultProvider?: TTSProvider;
58
+ }
59
+
35
60
  export class TextToSpeechTool implements ITool<TTSInput, TTSOutput> {
36
- readonly id = 'elevenlabs-tts-v1';
61
+ readonly id = 'tts-multi-provider-v1';
37
62
  readonly name = 'text_to_speech';
38
63
  readonly displayName = 'Text to Speech';
39
64
  readonly description =
40
- 'Convert text to speech using ElevenLabs. Returns base64-encoded MP3 audio. ' +
41
- 'Voices: rachel, domi, bella, antoni, josh, arnold, adam, sam.';
65
+ 'Convert text to speech audio. Supports multiple providers: OpenAI TTS (alloy/echo/fable/onyx/nova/shimmer), ' +
66
+ 'ElevenLabs (rachel/domi/bella/antoni/josh/arnold/adam/sam), or local Ollama TTS. ' +
67
+ 'Auto-detects available provider from API keys. Returns base64-encoded audio.';
42
68
  readonly category = 'media';
43
- readonly version = '1.0.0';
69
+ readonly version = '2.0.0';
44
70
  readonly hasSideEffects = false;
45
71
 
46
72
  readonly inputSchema: JSONSchemaObject = {
47
73
  type: 'object',
48
74
  properties: {
49
- text: { type: 'string', description: 'Text to convert. Max 5000 chars.' },
50
- voice: { type: 'string', default: 'rachel', description: 'Voice name or ID.' },
51
- model: { type: 'string', default: 'eleven_monolingual_v1' },
52
- stability: { type: 'number', minimum: 0, maximum: 1, default: 0.5 },
53
- similarity_boost: { type: 'number', minimum: 0, maximum: 1, default: 0.75 },
75
+ text: { type: 'string', description: 'Text to convert to speech. Max 5000 chars.' },
76
+ voice: {
77
+ type: 'string',
78
+ description:
79
+ 'Voice name. OpenAI: alloy, echo, fable, onyx, nova (default), shimmer. ' +
80
+ 'ElevenLabs: rachel (default), domi, bella, antoni, josh, arnold, adam, sam. ' +
81
+ 'Or a custom voice ID.',
82
+ },
83
+ model: {
84
+ type: 'string',
85
+ description: 'TTS model. OpenAI: tts-1 (default), tts-1-hd. ElevenLabs: eleven_monolingual_v1 (default), eleven_multilingual_v2.',
86
+ },
87
+ provider: {
88
+ type: 'string',
89
+ enum: ['openai', 'elevenlabs', 'ollama', 'auto'],
90
+ description: 'TTS provider. Default: auto (detects from available API keys).',
91
+ },
92
+ speed: { type: 'number', minimum: 0.25, maximum: 4.0, description: 'OpenAI speed multiplier (0.25-4.0).' },
93
+ stability: { type: 'number', minimum: 0, maximum: 1, description: 'ElevenLabs voice stability (0-1).' },
94
+ similarity_boost: { type: 'number', minimum: 0, maximum: 1, description: 'ElevenLabs similarity boost (0-1).' },
95
+ format: { type: 'string', enum: ['mp3', 'opus', 'aac', 'flac', 'wav'], description: 'Output audio format.' },
54
96
  },
55
97
  required: ['text'],
56
98
  };
57
99
 
58
100
  readonly requiredCapabilities = ['capability:tts'];
59
101
 
60
- private apiKey: string;
102
+ private config: TTSConfig;
61
103
 
62
- constructor(apiKey?: string) {
63
- this.apiKey = apiKey || process.env.ELEVENLABS_API_KEY || '';
104
+ constructor(config?: TTSConfig) {
105
+ this.config = {
106
+ openaiApiKey: config?.openaiApiKey || process.env.OPENAI_API_KEY || '',
107
+ openaiBaseUrl: config?.openaiBaseUrl || process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
108
+ elevenLabsApiKey: config?.elevenLabsApiKey || process.env.ELEVENLABS_API_KEY || '',
109
+ ollamaBaseUrl: config?.ollamaBaseUrl || process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
110
+ defaultProvider: config?.defaultProvider || (process.env.TTS_PROVIDER as TTSProvider) || 'auto',
111
+ };
64
112
  }
65
113
 
66
- async execute(args: TTSInput, _context: ToolExecutionContext): Promise<ToolExecutionResult<TTSOutput>> {
67
- if (!this.apiKey) return { success: false, error: 'ELEVENLABS_API_KEY not configured.' };
114
+ private resolveProvider(requested?: TTSProvider): TTSProvider | null {
115
+ const pref = requested || this.config.defaultProvider || 'auto';
116
+ if (pref !== 'auto') {
117
+ // Verify the requested provider has credentials
118
+ if (pref === 'openai' && this.config.openaiApiKey) return 'openai';
119
+ if (pref === 'elevenlabs' && this.config.elevenLabsApiKey) return 'elevenlabs';
120
+ if (pref === 'ollama') return 'ollama';
121
+ // Fall through to auto if requested provider isn't configured
122
+ }
68
123
 
124
+ // Auto-detect: prefer OpenAI (cheaper, faster), then ElevenLabs, then Ollama
125
+ if (this.config.openaiApiKey) return 'openai';
126
+ if (this.config.elevenLabsApiKey) return 'elevenlabs';
127
+ return 'ollama'; // Local fallback — may or may not have TTS model
128
+ }
129
+
130
+ async execute(args: TTSInput, _context: ToolExecutionContext): Promise<ToolExecutionResult<TTSOutput>> {
69
131
  const text = args.text.slice(0, 5000);
70
- const voiceId = VOICES[(args.voice || 'rachel').toLowerCase()] || args.voice || VOICES.rachel;
132
+ const provider = this.resolveProvider(args.provider);
133
+
134
+ if (!provider) {
135
+ return {
136
+ success: false,
137
+ error:
138
+ 'No TTS provider available. Set one of: OPENAI_API_KEY, ELEVENLABS_API_KEY, or configure Ollama with a TTS model. ' +
139
+ 'Get an OpenAI key at https://platform.openai.com/api-keys or ElevenLabs at https://elevenlabs.io',
140
+ };
141
+ }
142
+
143
+ try {
144
+ switch (provider) {
145
+ case 'openai':
146
+ return await this.synthesizeOpenAI(text, args);
147
+ case 'elevenlabs':
148
+ return await this.synthesizeElevenLabs(text, args);
149
+ case 'ollama':
150
+ return await this.synthesizeOllama(text, args);
151
+ default:
152
+ return { success: false, error: `Unknown TTS provider: ${provider}` };
153
+ }
154
+ } catch (err: any) {
155
+ return { success: false, error: `TTS failed (${provider}): ${err.message}` };
156
+ }
157
+ }
158
+
159
+ // ── OpenAI TTS ──
160
+
161
+ private async synthesizeOpenAI(text: string, args: TTSInput): Promise<ToolExecutionResult<TTSOutput>> {
162
+ const voice = args.voice && OPENAI_VOICES.includes(args.voice) ? args.voice : 'nova';
163
+ const model = args.model || 'tts-1';
164
+ const format = args.format || 'mp3';
165
+
166
+ const response = await fetch(`${this.config.openaiBaseUrl}/audio/speech`, {
167
+ method: 'POST',
168
+ headers: {
169
+ Authorization: `Bearer ${this.config.openaiApiKey}`,
170
+ 'Content-Type': 'application/json',
171
+ },
172
+ body: JSON.stringify({
173
+ model,
174
+ voice,
175
+ input: text,
176
+ response_format: format,
177
+ speed: args.speed,
178
+ }),
179
+ });
180
+
181
+ if (!response.ok) {
182
+ const err = await response.text();
183
+ return { success: false, error: `OpenAI TTS error (${response.status}): ${err.slice(0, 300)}` };
184
+ }
185
+
186
+ const buf = await response.arrayBuffer();
187
+ const audioBase64 = Buffer.from(buf).toString('base64');
188
+ const contentType = format === 'opus' ? 'audio/opus' : format === 'wav' ? 'audio/wav' : 'audio/mpeg';
189
+ const durationEstimateMs = Math.round((text.split(/\s+/).length / 150) * 60 * 1000);
190
+
191
+ return {
192
+ success: true,
193
+ output: { text, voice, model, provider: 'openai', audioBase64, contentType, durationEstimateMs },
194
+ contentType,
195
+ };
196
+ }
197
+
198
+ // ── ElevenLabs TTS ──
199
+
200
+ private async synthesizeElevenLabs(text: string, args: TTSInput): Promise<ToolExecutionResult<TTSOutput>> {
201
+ const voiceId = ELEVENLABS_VOICES[(args.voice || 'rachel').toLowerCase()] || args.voice || ELEVENLABS_VOICES.rachel;
71
202
  const model = args.model || 'eleven_monolingual_v1';
72
203
 
204
+ const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {
205
+ method: 'POST',
206
+ headers: {
207
+ 'xi-api-key': this.config.elevenLabsApiKey!,
208
+ 'Content-Type': 'application/json',
209
+ Accept: 'audio/mpeg',
210
+ },
211
+ body: JSON.stringify({
212
+ text,
213
+ model_id: model,
214
+ voice_settings: {
215
+ stability: args.stability ?? 0.5,
216
+ similarity_boost: args.similarity_boost ?? 0.75,
217
+ },
218
+ }),
219
+ });
220
+
221
+ if (!response.ok) {
222
+ const err = await response.text();
223
+ return { success: false, error: `ElevenLabs error (${response.status}): ${err.slice(0, 300)}` };
224
+ }
225
+
226
+ const buf = await response.arrayBuffer();
227
+ const audioBase64 = Buffer.from(buf).toString('base64');
228
+ const durationEstimateMs = Math.round((text.split(/\s+/).length / 150) * 60 * 1000);
229
+
230
+ return {
231
+ success: true,
232
+ output: {
233
+ text,
234
+ voice: args.voice || 'rachel',
235
+ model,
236
+ provider: 'elevenlabs',
237
+ audioBase64,
238
+ contentType: 'audio/mpeg',
239
+ durationEstimateMs,
240
+ },
241
+ contentType: 'audio/mpeg',
242
+ };
243
+ }
244
+
245
+ // ── Ollama TTS (local, experimental) ──
246
+
247
+ private async synthesizeOllama(text: string, args: TTSInput): Promise<ToolExecutionResult<TTSOutput>> {
248
+ // Ollama doesn't natively support TTS yet, but some models (e.g., bark, piper)
249
+ // can be served via OpenAI-compatible endpoints. Try the OpenAI-compat path.
250
+ const voice = args.voice || 'default';
251
+ const model = args.model || 'tts'; // User must have a TTS model loaded
252
+
73
253
  try {
74
- const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {
254
+ const response = await fetch(`${this.config.ollamaBaseUrl}/v1/audio/speech`, {
75
255
  method: 'POST',
76
- headers: { 'xi-api-key': this.apiKey, 'Content-Type': 'application/json', Accept: 'audio/mpeg' },
77
- body: JSON.stringify({
78
- text,
79
- model_id: model,
80
- voice_settings: { stability: args.stability ?? 0.5, similarity_boost: args.similarity_boost ?? 0.75 },
81
- }),
256
+ headers: { 'Content-Type': 'application/json' },
257
+ body: JSON.stringify({ model, voice, input: text }),
82
258
  });
83
259
 
84
260
  if (!response.ok) {
85
- const err = await response.text();
86
- return { success: false, error: `ElevenLabs error (${response.status}): ${err}` };
261
+ return {
262
+ success: false,
263
+ error:
264
+ `Ollama TTS not available (${response.status}). Ollama doesn't natively support TTS yet. ` +
265
+ 'Set OPENAI_API_KEY or ELEVENLABS_API_KEY for cloud TTS, or use a dedicated local TTS server.',
266
+ };
87
267
  }
88
268
 
89
269
  const buf = await response.arrayBuffer();
@@ -92,11 +272,16 @@ export class TextToSpeechTool implements ITool<TTSInput, TTSOutput> {
92
272
 
93
273
  return {
94
274
  success: true,
95
- output: { text, voice: args.voice || 'rachel', model, audioBase64, contentType: 'audio/mpeg', durationEstimateMs },
275
+ output: { text, voice, model, provider: 'ollama', audioBase64, contentType: 'audio/mpeg', durationEstimateMs },
96
276
  contentType: 'audio/mpeg',
97
277
  };
98
- } catch (err: any) {
99
- return { success: false, error: `TTS failed: ${err.message}` };
278
+ } catch {
279
+ return {
280
+ success: false,
281
+ error:
282
+ 'Ollama TTS endpoint not reachable. Ollama doesn\'t natively support TTS yet. ' +
283
+ 'Set OPENAI_API_KEY or ELEVENLABS_API_KEY for cloud TTS.',
284
+ };
100
285
  }
101
286
  }
102
287
  }