Skip to content

Commit 4e6e904

Browse files
committed
Add Gandr as a provider to the Text-to-Speech block
1 parent c20cc1d commit 4e6e904

10 files changed

Lines changed: 219 additions & 2 deletions

File tree

apps/sim/blocks/blocks/tts.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const TtsBlock: BlockConfig<TtsBlockResponse> = {
1111
description: 'Convert text to speech using AI voices',
1212
authMode: AuthMode.ApiKey,
1313
longDescription:
14-
'Generate natural-sounding speech from text using state-of-the-art AI voices from OpenAI, Deepgram, ElevenLabs, Cartesia, Google Cloud, Azure, and PlayHT. Supports multiple voices, languages, and audio formats.',
14+
'Generate natural-sounding speech from text using state-of-the-art AI voices from OpenAI, Deepgram, ElevenLabs, Cartesia, Google Cloud, Azure, PlayHT, and Gandr. Supports multiple voices, languages, and audio formats.',
1515
docsLink: 'https://docs.sim.ai/integrations/tts',
1616
category: 'blocks',
1717
integrationType: IntegrationType.AI,
@@ -41,6 +41,7 @@ export const TtsBlock: BlockConfig<TtsBlockResponse> = {
4141
{ label: 'Google Cloud TTS', id: 'google' },
4242
{ label: 'Azure TTS', id: 'azure' },
4343
{ label: 'PlayHT', id: 'playht' },
44+
{ label: 'Gandr', id: 'gandr' },
4445
],
4546
value: () => 'openai',
4647
required: true,
@@ -458,6 +459,41 @@ export const TtsBlock: BlockConfig<TtsBlockResponse> = {
458459
required: false,
459460
},
460461

462+
// Gandr Voice Selection
463+
{
464+
id: 'voice',
465+
title: 'Voice',
466+
type: 'dropdown',
467+
condition: { field: 'provider', value: 'gandr' },
468+
options: [
469+
{ label: 'Mia', id: 'gandr-mia' },
470+
{ label: 'Ava', id: 'gandr-ava' },
471+
{ label: 'Jenny', id: 'gandr-jenny' },
472+
{ label: 'Dane', id: 'gandr-dane' },
473+
{ label: 'Leo', id: 'gandr-leo' },
474+
{ label: 'Lewis', id: 'gandr-lewis' },
475+
],
476+
value: () => 'gandr-mia',
477+
dependsOn: ['provider'],
478+
required: false,
479+
},
480+
481+
// Gandr Response Format
482+
{
483+
id: 'responseFormat',
484+
title: 'Audio Format',
485+
type: 'dropdown',
486+
condition: { field: 'provider', value: 'gandr' },
487+
options: [
488+
{ label: 'MP3', id: 'mp3' },
489+
{ label: 'WAV', id: 'wav' },
490+
{ label: 'PCM', id: 'pcm' },
491+
],
492+
value: () => 'mp3',
493+
dependsOn: ['provider'],
494+
required: false,
495+
},
496+
461497
// API Key (common to all providers)
462498
{
463499
id: 'apiKey',
@@ -478,6 +514,7 @@ export const TtsBlock: BlockConfig<TtsBlockResponse> = {
478514
'tts_google',
479515
'tts_azure',
480516
'tts_playht',
517+
'tts_gandr',
481518
],
482519
config: {
483520
tool: (params) => {
@@ -497,6 +534,8 @@ export const TtsBlock: BlockConfig<TtsBlockResponse> = {
497534
return 'tts_azure'
498535
case 'playht':
499536
return 'tts_playht'
537+
case 'gandr':
538+
return 'tts_gandr'
500539
default:
501540
return 'tts_openai'
502541
}
@@ -576,6 +615,14 @@ export const TtsBlock: BlockConfig<TtsBlockResponse> = {
576615
}
577616
}
578617

618+
if (params.provider === 'gandr') {
619+
return {
620+
...baseParams,
621+
voice: params.voice,
622+
responseFormat: params.responseFormat,
623+
}
624+
}
625+
579626
return baseParams
580627
},
581628
},
@@ -584,7 +631,7 @@ export const TtsBlock: BlockConfig<TtsBlockResponse> = {
584631
inputs: {
585632
provider: {
586633
type: 'string',
587-
description: 'TTS provider (openai, deepgram, elevenlabs, cartesia, google, azure, playht)',
634+
description: 'TTS provider (openai, deepgram, elevenlabs, cartesia, google, azure, playht, gandr)',
588635
},
589636
text: { type: 'string', description: 'Text to convert to speech' },
590637
apiKey: { type: 'string', description: 'Provider API key' },

apps/sim/lib/internal/tool-operations/registry.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,7 @@ const TTS_TOOL_IDS = [
818818
'tts_cartesia',
819819
'tts_deepgram',
820820
'tts_elevenlabs',
821+
'tts_gandr',
821822
'tts_google',
822823
'tts_openai',
823824
'tts_playht',

apps/sim/lib/internal/tts/client.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
CartesiaTtsParams,
1717
DeepgramTtsParams,
1818
ElevenLabsTtsUnifiedParams,
19+
GandrTtsParams,
1920
GoogleTtsParams,
2021
OpenAiTtsParams,
2122
PlayHtTtsParams,
@@ -137,6 +138,44 @@ export async function synthesizeOpenAi(
137138
}
138139
}
139140

141+
export async function synthesizeGandr(
142+
input: GandrTtsParams,
143+
signal?: AbortSignal
144+
): Promise<TtsAudioResult> {
145+
assertTextWithinLimit(input.text)
146+
if (input.text.length > 2000) {
147+
throw new TtsOperationError('Gandr TTS accepts up to 2000 characters per request', 400)
148+
}
149+
const voice = input.voice || 'gandr-mia'
150+
const format = input.responseFormat || 'mp3'
151+
const response = await providerFetch(
152+
'https://tts.gandr.ai/v1/audio/speech',
153+
{
154+
method: 'POST',
155+
headers: {
156+
Authorization: `Bearer ${input.apiKey}`,
157+
'Content-Type': 'application/json',
158+
},
159+
body: JSON.stringify({
160+
model: 'tts-1',
161+
input: input.text,
162+
voice,
163+
response_format: format,
164+
}),
165+
},
166+
signal
167+
)
168+
if (!response.ok) {
169+
const error = await readTtsErrorJson(response, 'Gandr TTS error response', signal)
170+
throw new Error(`Gandr TTS API error: ${getTtsErrorMessage(error, response.statusText)}`)
171+
}
172+
return {
173+
audioBuffer: await readAudio(response, 'Gandr TTS audio response', signal),
174+
format,
175+
mimeType: getTtsMimeType(format),
176+
}
177+
}
178+
140179
export async function synthesizeDeepgram(
141180
input: DeepgramTtsParams,
142181
signal?: AbortSignal

apps/sim/lib/internal/tts/execute-tool.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
executeCartesiaTts,
1313
executeDeepgramTts,
1414
executeElevenLabsTts,
15+
executeGandrTts,
1516
executeGoogleTts,
1617
executeLegacyElevenLabsTts,
1718
executeOpenAiTts,
@@ -114,6 +115,13 @@ const schemas = {
114115
textGuidance: z.coerce.number().optional(),
115116
sampleRate: z.coerce.number().optional(),
116117
}),
118+
tts_gandr: z.object({
119+
...auth,
120+
voice: z
121+
.enum(['gandr-mia', 'gandr-ava', 'gandr-jenny', 'gandr-dane', 'gandr-leo', 'gandr-lewis'])
122+
.optional(),
123+
responseFormat: z.enum(['mp3', 'wav', 'pcm']).optional(),
124+
}),
117125
} as const
118126

119127
type TtsToolId = keyof typeof schemas
@@ -208,5 +216,7 @@ export const executeTtsTool: InternalToolOperationHandler = async (request) => {
208216
return executeOperation(schemas.tts_azure, request, executeAzureTts)
209217
case 'tts_playht':
210218
return executeOperation(schemas.tts_playht, request, executePlayHtTts)
219+
case 'tts_gandr':
220+
return executeOperation(schemas.tts_gandr, request, executeGandrTts)
211221
}
212222
}

apps/sim/lib/internal/tts/operations.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
synthesizeCartesia,
99
synthesizeDeepgram,
1010
synthesizeElevenLabs,
11+
synthesizeGandr,
1112
synthesizeGoogle,
1213
synthesizeLegacyElevenLabs,
1314
synthesizeOpenAi,
@@ -21,6 +22,7 @@ import type { ElevenLabsTtsParams } from '@/tools/elevenlabs/types'
2122
import type {
2223
DeepgramTtsParams,
2324
ElevenLabsTtsUnifiedParams,
25+
GandrTtsParams,
2426
GoogleTtsParams,
2527
PlayHtTtsParams,
2628
TtsProvider,
@@ -222,3 +224,15 @@ export async function executePlayHtTts(
222224
context
223225
)
224226
}
227+
228+
export async function executeGandrTts(
229+
input: GandrTtsParams,
230+
context: TtsOperationContext
231+
): Promise<TtsResponse> {
232+
return storeUnifiedAudio(
233+
'gandr',
234+
input.text,
235+
await synthesizeGandr(input, context.signal),
236+
context
237+
)
238+
}

apps/sim/tools/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5277,6 +5277,7 @@ import {
52775277
cartesiaTtsTool,
52785278
deepgramTtsTool,
52795279
elevenLabsTtsUnifiedTool,
5280+
gandrTtsTool,
52805281
googleTtsTool,
52815282
openaiTtsTool,
52825283
playhtTtsTool,
@@ -9303,6 +9304,7 @@ export const tools: Record<string, ExecutableToolConfig> = {
93039304
tts_google: googleTtsTool,
93049305
tts_azure: azureTtsTool,
93059306
tts_playht: playhtTtsTool,
9307+
tts_gandr: gandrTtsTool,
93069308
video_runway: runwayVideoTool,
93079309
video_veo: veoVideoTool,
93089310
video_luma: lumaVideoTool,

apps/sim/tools/tts/gandr.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import type { GandrTtsParams, TtsBlockResponse } from '@/tools/tts/types'
2+
import type { InternalToolConfig } from '@/tools/types'
3+
4+
export const gandrTtsTool: InternalToolConfig<GandrTtsParams, TtsBlockResponse> = {
5+
id: 'tts_gandr',
6+
name: 'Gandr TTS',
7+
description: 'Convert text to speech using Gandr voices',
8+
version: '1.0.0',
9+
10+
params: {
11+
text: {
12+
type: 'string',
13+
required: true,
14+
visibility: 'user-or-llm',
15+
description:
16+
'The text content to convert to speech, up to 2000 characters per request (e.g., "Hello, welcome to our service!")',
17+
},
18+
apiKey: {
19+
type: 'string',
20+
required: true,
21+
visibility: 'user-only',
22+
description: 'Gandr API key',
23+
},
24+
voice: {
25+
type: 'string',
26+
required: false,
27+
visibility: 'user-or-llm',
28+
description:
29+
'Gandr voice identifier (e.g., "gandr-mia", "gandr-ava", "gandr-jenny", "gandr-dane", "gandr-leo", "gandr-lewis")',
30+
},
31+
responseFormat: {
32+
type: 'string',
33+
required: false,
34+
visibility: 'user-only',
35+
description: 'Audio format (mp3, wav, pcm)',
36+
},
37+
},
38+
39+
operation: {
40+
modelInput: {
41+
mode: 'project',
42+
select: (params) => ({ text: params.text }),
43+
},
44+
input: (params) => ({
45+
text: params.text,
46+
apiKey: params.apiKey,
47+
voice: params.voice || 'gandr-mia',
48+
responseFormat: params.responseFormat || 'mp3',
49+
}),
50+
},
51+
52+
transformResponse: async (response: Response) => {
53+
const data = await response.json()
54+
55+
if (!response.ok || data.error) {
56+
return {
57+
success: false,
58+
error: data.error || 'TTS generation failed',
59+
output: {
60+
audioUrl: '',
61+
},
62+
}
63+
}
64+
65+
return {
66+
success: true,
67+
output: {
68+
audioUrl: data.audioUrl,
69+
audioFile: data.audioFile,
70+
duration: data.duration,
71+
characterCount: data.characterCount,
72+
format: data.format,
73+
provider: data.provider,
74+
},
75+
}
76+
},
77+
78+
outputs: {
79+
audioUrl: { type: 'string', description: 'URL to the generated audio file' },
80+
audioFile: { type: 'file', description: 'Generated audio file object' },
81+
duration: { type: 'number', description: 'Audio duration in seconds' },
82+
characterCount: { type: 'number', description: 'Number of characters processed' },
83+
format: { type: 'string', description: 'Audio format' },
84+
provider: { type: 'string', description: 'TTS provider used' },
85+
},
86+
}

apps/sim/tools/tts/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export { azureTtsTool } from './azure'
22
export { cartesiaTtsTool } from './cartesia'
33
export { deepgramTtsTool } from './deepgram'
44
export { elevenLabsTtsUnifiedTool } from './elevenlabs'
5+
export { gandrTtsTool } from './gandr'
56
export { googleTtsTool } from './google'
67
export { openaiTtsTool } from './openai'
78
export { playhtTtsTool } from './playht'

apps/sim/tools/tts/operations.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { azureTtsTool } from '@/tools/tts/azure'
77
import { cartesiaTtsTool } from '@/tools/tts/cartesia'
88
import { deepgramTtsTool } from '@/tools/tts/deepgram'
99
import { elevenLabsTtsUnifiedTool } from '@/tools/tts/elevenlabs'
10+
import { gandrTtsTool } from '@/tools/tts/gandr'
1011
import { googleTtsTool } from '@/tools/tts/google'
1112
import { openaiTtsTool } from '@/tools/tts/openai'
1213
import { playhtTtsTool } from '@/tools/tts/playht'
@@ -20,6 +21,7 @@ const TOOLS = [
2021
googleTtsTool,
2122
azureTtsTool,
2223
playhtTtsTool,
24+
gandrTtsTool,
2325
] as const
2426

2527
describe('TTS operation declarations', () => {
@@ -44,6 +46,12 @@ describe('TTS operation declarations', () => {
4446
responseFormat: 'mp3',
4547
speed: 1,
4648
})
49+
expect(gandrTtsTool.operation.input({ text: 'Hello', apiKey: 'key' })).toEqual({
50+
text: 'Hello',
51+
apiKey: 'key',
52+
voice: 'gandr-mia',
53+
responseFormat: 'mp3',
54+
})
4755
expect(
4856
elevenLabsTtsTool.operation.input({ text: 'Hello', apiKey: 'key', voiceId: 'voice_1' })
4957
).toEqual({

apps/sim/tools/tts/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type TtsProvider =
99
| 'google'
1010
| 'azure'
1111
| 'playht'
12+
| 'gandr'
1213

1314
// OpenAI TTS Types
1415
export interface OpenAiTtsParams {
@@ -54,6 +55,14 @@ export interface ElevenLabsTtsUnifiedParams {
5455
apiKey: string
5556
}
5657

58+
// Gandr TTS Types
59+
export interface GandrTtsParams {
60+
text: string // up to 2000 characters per request
61+
voice?: 'gandr-mia' | 'gandr-ava' | 'gandr-jenny' | 'gandr-dane' | 'gandr-leo' | 'gandr-lewis'
62+
responseFormat?: 'mp3' | 'wav' | 'pcm'
63+
apiKey: string
64+
}
65+
5766
// Cartesia TTS Types
5867
export interface CartesiaTtsParams {
5968
text: string

0 commit comments

Comments
 (0)