diff --git a/apps/sim/blocks/blocks/tts.ts b/apps/sim/blocks/blocks/tts.ts index 3ff4a2948ab..448aab50548 100644 --- a/apps/sim/blocks/blocks/tts.ts +++ b/apps/sim/blocks/blocks/tts.ts @@ -11,7 +11,7 @@ export const TtsBlock: BlockConfig = { description: 'Convert text to speech using AI voices', authMode: AuthMode.ApiKey, longDescription: - '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.', + '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.', docsLink: 'https://docs.sim.ai/integrations/tts', category: 'blocks', integrationType: IntegrationType.AI, @@ -41,6 +41,7 @@ export const TtsBlock: BlockConfig = { { label: 'Google Cloud TTS', id: 'google' }, { label: 'Azure TTS', id: 'azure' }, { label: 'PlayHT', id: 'playht' }, + { label: 'Gandr', id: 'gandr' }, ], value: () => 'openai', required: true, @@ -458,6 +459,41 @@ export const TtsBlock: BlockConfig = { required: false, }, + // Gandr Voice Selection + { + id: 'voice', + title: 'Voice', + type: 'dropdown', + condition: { field: 'provider', value: 'gandr' }, + options: [ + { label: 'Mia', id: 'gandr-mia' }, + { label: 'Ava', id: 'gandr-ava' }, + { label: 'Jenny', id: 'gandr-jenny' }, + { label: 'Dane', id: 'gandr-dane' }, + { label: 'Leo', id: 'gandr-leo' }, + { label: 'Lewis', id: 'gandr-lewis' }, + ], + value: () => 'gandr-mia', + dependsOn: ['provider'], + required: false, + }, + + // Gandr Response Format + { + id: 'responseFormat', + title: 'Audio Format', + type: 'dropdown', + condition: { field: 'provider', value: 'gandr' }, + options: [ + { label: 'MP3', id: 'mp3' }, + { label: 'WAV', id: 'wav' }, + { label: 'PCM', id: 'pcm' }, + ], + value: () => 'mp3', + dependsOn: ['provider'], + required: false, + }, + // API Key (common to all providers) { id: 'apiKey', @@ -478,6 +514,7 @@ export const TtsBlock: BlockConfig = { 'tts_google', 'tts_azure', 'tts_playht', + 'tts_gandr', ], config: { tool: (params) => { @@ -497,6 +534,8 @@ export const TtsBlock: BlockConfig = { return 'tts_azure' case 'playht': return 'tts_playht' + case 'gandr': + return 'tts_gandr' default: return 'tts_openai' } @@ -576,6 +615,14 @@ export const TtsBlock: BlockConfig = { } } + if (params.provider === 'gandr') { + return { + ...baseParams, + voice: params.voice, + responseFormat: params.responseFormat, + } + } + return baseParams }, }, @@ -584,7 +631,7 @@ export const TtsBlock: BlockConfig = { inputs: { provider: { type: 'string', - description: 'TTS provider (openai, deepgram, elevenlabs, cartesia, google, azure, playht)', + description: 'TTS provider (openai, deepgram, elevenlabs, cartesia, google, azure, playht, gandr)', }, text: { type: 'string', description: 'Text to convert to speech' }, apiKey: { type: 'string', description: 'Provider API key' }, diff --git a/apps/sim/lib/internal/tool-operations/registry.server.ts b/apps/sim/lib/internal/tool-operations/registry.server.ts index c84c5735af6..9b38a44deb5 100644 --- a/apps/sim/lib/internal/tool-operations/registry.server.ts +++ b/apps/sim/lib/internal/tool-operations/registry.server.ts @@ -818,6 +818,7 @@ const TTS_TOOL_IDS = [ 'tts_cartesia', 'tts_deepgram', 'tts_elevenlabs', + 'tts_gandr', 'tts_google', 'tts_openai', 'tts_playht', diff --git a/apps/sim/lib/internal/tts/client.ts b/apps/sim/lib/internal/tts/client.ts index c88a9b4b23c..70b5e4b9ed1 100644 --- a/apps/sim/lib/internal/tts/client.ts +++ b/apps/sim/lib/internal/tts/client.ts @@ -16,6 +16,7 @@ import type { CartesiaTtsParams, DeepgramTtsParams, ElevenLabsTtsUnifiedParams, + GandrTtsParams, GoogleTtsParams, OpenAiTtsParams, PlayHtTtsParams, @@ -137,6 +138,44 @@ export async function synthesizeOpenAi( } } +export async function synthesizeGandr( + input: GandrTtsParams, + signal?: AbortSignal +): Promise { + assertTextWithinLimit(input.text) + if (input.text.length > 2000) { + throw new TtsOperationError('Gandr TTS accepts up to 2000 characters per request', 400) + } + const voice = input.voice || 'gandr-mia' + const format = input.responseFormat || 'mp3' + const response = await providerFetch( + 'https://tts.gandr.ai/v1/audio/speech', + { + method: 'POST', + headers: { + Authorization: `Bearer ${input.apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: 'tts-1', + input: input.text, + voice, + response_format: format, + }), + }, + signal + ) + if (!response.ok) { + const error = await readTtsErrorJson(response, 'Gandr TTS error response', signal) + throw new Error(`Gandr TTS API error: ${getTtsErrorMessage(error, response.statusText)}`) + } + return { + audioBuffer: await readAudio(response, 'Gandr TTS audio response', signal), + format, + mimeType: getTtsMimeType(format), + } +} + export async function synthesizeDeepgram( input: DeepgramTtsParams, signal?: AbortSignal diff --git a/apps/sim/lib/internal/tts/execute-tool.ts b/apps/sim/lib/internal/tts/execute-tool.ts index d7c3f8d40d9..dffc54eb328 100644 --- a/apps/sim/lib/internal/tts/execute-tool.ts +++ b/apps/sim/lib/internal/tts/execute-tool.ts @@ -12,6 +12,7 @@ import { executeCartesiaTts, executeDeepgramTts, executeElevenLabsTts, + executeGandrTts, executeGoogleTts, executeLegacyElevenLabsTts, executeOpenAiTts, @@ -114,6 +115,13 @@ const schemas = { textGuidance: z.coerce.number().optional(), sampleRate: z.coerce.number().optional(), }), + tts_gandr: z.object({ + ...auth, + voice: z + .enum(['gandr-mia', 'gandr-ava', 'gandr-jenny', 'gandr-dane', 'gandr-leo', 'gandr-lewis']) + .optional(), + responseFormat: z.enum(['mp3', 'wav', 'pcm']).optional(), + }), } as const type TtsToolId = keyof typeof schemas @@ -208,5 +216,7 @@ export const executeTtsTool: InternalToolOperationHandler = async (request) => { return executeOperation(schemas.tts_azure, request, executeAzureTts) case 'tts_playht': return executeOperation(schemas.tts_playht, request, executePlayHtTts) + case 'tts_gandr': + return executeOperation(schemas.tts_gandr, request, executeGandrTts) } } diff --git a/apps/sim/lib/internal/tts/operations.ts b/apps/sim/lib/internal/tts/operations.ts index d38db9670d4..64facdb3cb3 100644 --- a/apps/sim/lib/internal/tts/operations.ts +++ b/apps/sim/lib/internal/tts/operations.ts @@ -8,6 +8,7 @@ import { synthesizeCartesia, synthesizeDeepgram, synthesizeElevenLabs, + synthesizeGandr, synthesizeGoogle, synthesizeLegacyElevenLabs, synthesizeOpenAi, @@ -21,6 +22,7 @@ import type { ElevenLabsTtsParams } from '@/tools/elevenlabs/types' import type { DeepgramTtsParams, ElevenLabsTtsUnifiedParams, + GandrTtsParams, GoogleTtsParams, PlayHtTtsParams, TtsProvider, @@ -222,3 +224,15 @@ export async function executePlayHtTts( context ) } + +export async function executeGandrTts( + input: GandrTtsParams, + context: TtsOperationContext +): Promise { + return storeUnifiedAudio( + 'gandr', + input.text, + await synthesizeGandr(input, context.signal), + context + ) +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 0c3094ac46d..cce4436e224 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -5277,6 +5277,7 @@ import { cartesiaTtsTool, deepgramTtsTool, elevenLabsTtsUnifiedTool, + gandrTtsTool, googleTtsTool, openaiTtsTool, playhtTtsTool, @@ -9303,6 +9304,7 @@ export const tools: Record = { tts_google: googleTtsTool, tts_azure: azureTtsTool, tts_playht: playhtTtsTool, + tts_gandr: gandrTtsTool, video_runway: runwayVideoTool, video_veo: veoVideoTool, video_luma: lumaVideoTool, diff --git a/apps/sim/tools/tts/gandr.ts b/apps/sim/tools/tts/gandr.ts new file mode 100644 index 00000000000..84535dc1726 --- /dev/null +++ b/apps/sim/tools/tts/gandr.ts @@ -0,0 +1,86 @@ +import type { GandrTtsParams, TtsBlockResponse } from '@/tools/tts/types' +import type { InternalToolConfig } from '@/tools/types' + +export const gandrTtsTool: InternalToolConfig = { + id: 'tts_gandr', + name: 'Gandr TTS', + description: 'Convert text to speech using Gandr voices', + version: '1.0.0', + + params: { + text: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'The text content to convert to speech, up to 2000 characters per request (e.g., "Hello, welcome to our service!")', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Gandr API key', + }, + voice: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Gandr voice identifier (e.g., "gandr-mia", "gandr-ava", "gandr-jenny", "gandr-dane", "gandr-leo", "gandr-lewis")', + }, + responseFormat: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Audio format (mp3, wav, pcm)', + }, + }, + + operation: { + modelInput: { + mode: 'project', + select: (params) => ({ text: params.text }), + }, + input: (params) => ({ + text: params.text, + apiKey: params.apiKey, + voice: params.voice || 'gandr-mia', + responseFormat: params.responseFormat || 'mp3', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok || data.error) { + return { + success: false, + error: data.error || 'TTS generation failed', + output: { + audioUrl: '', + }, + } + } + + return { + success: true, + output: { + audioUrl: data.audioUrl, + audioFile: data.audioFile, + duration: data.duration, + characterCount: data.characterCount, + format: data.format, + provider: data.provider, + }, + } + }, + + outputs: { + audioUrl: { type: 'string', description: 'URL to the generated audio file' }, + audioFile: { type: 'file', description: 'Generated audio file object' }, + duration: { type: 'number', description: 'Audio duration in seconds' }, + characterCount: { type: 'number', description: 'Number of characters processed' }, + format: { type: 'string', description: 'Audio format' }, + provider: { type: 'string', description: 'TTS provider used' }, + }, +} diff --git a/apps/sim/tools/tts/index.ts b/apps/sim/tools/tts/index.ts index f095328750b..e8aa74da06d 100644 --- a/apps/sim/tools/tts/index.ts +++ b/apps/sim/tools/tts/index.ts @@ -2,6 +2,7 @@ export { azureTtsTool } from './azure' export { cartesiaTtsTool } from './cartesia' export { deepgramTtsTool } from './deepgram' export { elevenLabsTtsUnifiedTool } from './elevenlabs' +export { gandrTtsTool } from './gandr' export { googleTtsTool } from './google' export { openaiTtsTool } from './openai' export { playhtTtsTool } from './playht' diff --git a/apps/sim/tools/tts/operations.test.ts b/apps/sim/tools/tts/operations.test.ts index 062f5158565..0b1e648d3aa 100644 --- a/apps/sim/tools/tts/operations.test.ts +++ b/apps/sim/tools/tts/operations.test.ts @@ -7,6 +7,7 @@ import { azureTtsTool } from '@/tools/tts/azure' import { cartesiaTtsTool } from '@/tools/tts/cartesia' import { deepgramTtsTool } from '@/tools/tts/deepgram' import { elevenLabsTtsUnifiedTool } from '@/tools/tts/elevenlabs' +import { gandrTtsTool } from '@/tools/tts/gandr' import { googleTtsTool } from '@/tools/tts/google' import { openaiTtsTool } from '@/tools/tts/openai' import { playhtTtsTool } from '@/tools/tts/playht' @@ -20,6 +21,7 @@ const TOOLS = [ googleTtsTool, azureTtsTool, playhtTtsTool, + gandrTtsTool, ] as const describe('TTS operation declarations', () => { @@ -44,6 +46,12 @@ describe('TTS operation declarations', () => { responseFormat: 'mp3', speed: 1, }) + expect(gandrTtsTool.operation.input({ text: 'Hello', apiKey: 'key' })).toEqual({ + text: 'Hello', + apiKey: 'key', + voice: 'gandr-mia', + responseFormat: 'mp3', + }) expect( elevenLabsTtsTool.operation.input({ text: 'Hello', apiKey: 'key', voiceId: 'voice_1' }) ).toEqual({ diff --git a/apps/sim/tools/tts/types.ts b/apps/sim/tools/tts/types.ts index 8c19dee1513..add638575c3 100644 --- a/apps/sim/tools/tts/types.ts +++ b/apps/sim/tools/tts/types.ts @@ -9,6 +9,7 @@ export type TtsProvider = | 'google' | 'azure' | 'playht' + | 'gandr' // OpenAI TTS Types export interface OpenAiTtsParams { @@ -54,6 +55,14 @@ export interface ElevenLabsTtsUnifiedParams { apiKey: string } +// Gandr TTS Types +export interface GandrTtsParams { + text: string // up to 2000 characters per request + voice?: 'gandr-mia' | 'gandr-ava' | 'gandr-jenny' | 'gandr-dane' | 'gandr-leo' | 'gandr-lewis' + responseFormat?: 'mp3' | 'wav' | 'pcm' + apiKey: string +} + // Cartesia TTS Types export interface CartesiaTtsParams { text: string