diff --git a/README.md b/README.md
index c15185d8..452f75e9 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,7 @@ Visit [http://localhost:3000](http://localhost:3000) to get started.
- **AI Gateway**: Generate Text, Generate Image
- **Blob**: Put Blob, List Blobs
+- **ElevenLabs**: Text to Speech, Get Voices, Speech to Text, Generate Sound Effect
- **Firecrawl**: Scrape URL, Search Web
- **GitHub**: Create Issue, List Issues, Get Issue, Update Issue
- **Linear**: Create Ticket, Find Issues
diff --git a/plugins/elevenlabs/credentials.ts b/plugins/elevenlabs/credentials.ts
new file mode 100644
index 00000000..a8add2f0
--- /dev/null
+++ b/plugins/elevenlabs/credentials.ts
@@ -0,0 +1,3 @@
+export type ElevenlabsCredentials = {
+ ELEVENLABS_API_KEY?: string;
+};
diff --git a/plugins/elevenlabs/icon.tsx b/plugins/elevenlabs/icon.tsx
new file mode 100644
index 00000000..960d9403
--- /dev/null
+++ b/plugins/elevenlabs/icon.tsx
@@ -0,0 +1,14 @@
+export function ElevenlabsIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/plugins/elevenlabs/index.ts b/plugins/elevenlabs/index.ts
new file mode 100644
index 00000000..5940d818
--- /dev/null
+++ b/plugins/elevenlabs/index.ts
@@ -0,0 +1,204 @@
+import type { IntegrationPlugin } from "../registry";
+import { registerIntegration } from "../registry";
+import { ElevenlabsIcon } from "./icon";
+
+const elevenlabsPlugin: IntegrationPlugin = {
+ type: "elevenlabs",
+ label: "ElevenLabs",
+ description: "AI voice generation and speech synthesis",
+
+ icon: ElevenlabsIcon,
+
+ formFields: [
+ {
+ id: "apiKey",
+ label: "API Key",
+ type: "password",
+ placeholder: "Your ElevenLabs API key",
+ configKey: "apiKey",
+ envVar: "ELEVENLABS_API_KEY",
+ helpText: "Get your API key from ",
+ helpLink: {
+ text: "elevenlabs.io/app/settings/api-keys",
+ url: "https://elevenlabs.io/app/settings/api-keys",
+ },
+ },
+ ],
+
+ testConfig: {
+ getTestFunction: async () => {
+ const { testElevenlabs } = await import("./test");
+ return testElevenlabs;
+ },
+ },
+
+ actions: [
+ {
+ slug: "text-to-speech",
+ label: "Text to Speech",
+ description: "Convert text to speech using ElevenLabs voices",
+ category: "ElevenLabs",
+ stepFunction: "textToSpeechStep",
+ stepImportPath: "text-to-speech",
+ outputFields: [
+ { field: "audioBase64", description: "Base64-encoded audio data" },
+ { field: "contentType", description: "Audio content type (e.g., audio/mpeg)" },
+ ],
+ configFields: [
+ {
+ key: "voiceId",
+ label: "Voice ID",
+ type: "template-input",
+ placeholder: "21m00Tcm4TlvDq8ikWAM (Rachel)",
+ example: "21m00Tcm4TlvDq8ikWAM",
+ required: true,
+ },
+ {
+ key: "text",
+ label: "Text",
+ type: "template-textarea",
+ placeholder: "Enter the text to convert to speech...",
+ rows: 4,
+ example: "Hello, welcome to my application!",
+ required: true,
+ },
+ {
+ key: "modelId",
+ label: "Model",
+ type: "select",
+ defaultValue: "eleven_multilingual_v2",
+ options: [
+ { value: "eleven_multilingual_v2", label: "Multilingual v2 (Best Quality)" },
+ { value: "eleven_turbo_v2_5", label: "Turbo v2.5 (Low Latency)" },
+ { value: "eleven_turbo_v2", label: "Turbo v2 (Legacy)" },
+ { value: "eleven_monolingual_v1", label: "Monolingual v1 (English)" },
+ { value: "eleven_flash_v2_5", label: "Flash v2.5 (Fastest)" },
+ { value: "eleven_flash_v2", label: "Flash v2 (Legacy Fast)" },
+ ],
+ },
+ {
+ type: "group",
+ label: "Voice Settings",
+ fields: [
+ {
+ key: "stability",
+ label: "Stability (0-1)",
+ type: "number",
+ placeholder: "0.5",
+ min: 0,
+ },
+ {
+ key: "similarityBoost",
+ label: "Similarity Boost (0-1)",
+ type: "number",
+ placeholder: "0.75",
+ min: 0,
+ },
+ {
+ key: "style",
+ label: "Style (0-1)",
+ type: "number",
+ placeholder: "0",
+ min: 0,
+ },
+ ],
+ },
+ {
+ type: "group",
+ label: "Output Settings",
+ fields: [
+ {
+ key: "outputFormat",
+ label: "Output Format",
+ type: "select",
+ defaultValue: "mp3_44100_128",
+ options: [
+ { value: "mp3_44100_128", label: "MP3 44.1kHz 128kbps" },
+ { value: "mp3_44100_192", label: "MP3 44.1kHz 192kbps" },
+ { value: "pcm_16000", label: "PCM 16kHz" },
+ { value: "pcm_22050", label: "PCM 22.05kHz" },
+ { value: "pcm_24000", label: "PCM 24kHz" },
+ { value: "pcm_44100", label: "PCM 44.1kHz" },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ slug: "get-voices",
+ label: "Get Voices",
+ description: "List all available voices in your ElevenLabs account",
+ category: "ElevenLabs",
+ stepFunction: "getVoicesStep",
+ stepImportPath: "get-voices",
+ outputFields: [
+ { field: "voices", description: "Array of available voices with their IDs and names" },
+ ],
+ configFields: [],
+ },
+ {
+ slug: "speech-to-text",
+ label: "Speech to Text",
+ description: "Transcribe audio to text using ElevenLabs",
+ category: "ElevenLabs",
+ stepFunction: "speechToTextStep",
+ stepImportPath: "speech-to-text",
+ outputFields: [
+ { field: "text", description: "Transcribed text from the audio" },
+ { field: "language", description: "Detected language code" },
+ ],
+ configFields: [
+ {
+ key: "audioUrl",
+ label: "Audio URL",
+ type: "template-input",
+ placeholder: "https://example.com/audio.mp3 or {{NodeName.url}}",
+ example: "https://example.com/recording.mp3",
+ required: true,
+ },
+ {
+ key: "languageCode",
+ label: "Language Code (Optional)",
+ type: "template-input",
+ placeholder: "en (auto-detect if empty)",
+ example: "en",
+ },
+ ],
+ },
+ {
+ slug: "sound-effects",
+ label: "Generate Sound Effect",
+ description: "Generate sound effects from text descriptions",
+ category: "ElevenLabs",
+ stepFunction: "soundEffectsStep",
+ stepImportPath: "sound-effects",
+ outputFields: [
+ { field: "audioBase64", description: "Base64-encoded audio data" },
+ { field: "contentType", description: "Audio content type" },
+ ],
+ configFields: [
+ {
+ key: "text",
+ label: "Description",
+ type: "template-textarea",
+ placeholder: "Describe the sound effect you want to generate...",
+ rows: 3,
+ example: "A gentle rain falling on a rooftop with occasional thunder in the distance",
+ required: true,
+ },
+ {
+ key: "durationSeconds",
+ label: "Duration (seconds, 0.5-22)",
+ type: "number",
+ placeholder: "Auto",
+ min: 0,
+ },
+ ],
+ },
+ ],
+};
+
+registerIntegration(elevenlabsPlugin);
+
+export default elevenlabsPlugin;
diff --git a/plugins/elevenlabs/steps/get-voices.ts b/plugins/elevenlabs/steps/get-voices.ts
new file mode 100644
index 00000000..3e4cdccf
--- /dev/null
+++ b/plugins/elevenlabs/steps/get-voices.ts
@@ -0,0 +1,118 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { ElevenlabsCredentials } from "../credentials";
+
+const ELEVENLABS_API_URL = "https://api.elevenlabs.io/v1";
+
+type Voice = {
+ voice_id: string;
+ name: string;
+ category: string;
+ description: string | null;
+ labels: Record;
+ preview_url: string | null;
+};
+
+type VoiceInfo = {
+ voiceId: string;
+ name: string;
+ category: string;
+ description: string | null;
+ labels: Record;
+ previewUrl: string | null;
+};
+
+type GetVoicesResult =
+ | { success: true; voices: VoiceInfo[] }
+ | { success: false; error: string };
+
+export type GetVoicesCoreInput = Record;
+
+export type GetVoicesInput = StepInput &
+ GetVoicesCoreInput & {
+ integrationId?: string;
+ };
+
+/**
+ * Core logic - portable between app and export
+ */
+async function stepHandler(
+ _input: GetVoicesCoreInput,
+ credentials: ElevenlabsCredentials
+): Promise {
+ const apiKey = credentials.ELEVENLABS_API_KEY;
+
+ if (!apiKey) {
+ return {
+ success: false,
+ error:
+ "ELEVENLABS_API_KEY is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ try {
+ const response = await fetch(`${ELEVENLABS_API_URL}/voices`, {
+ method: "GET",
+ headers: {
+ "xi-api-key": apiKey,
+ },
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ let errorMessage: string;
+ try {
+ const errorData = JSON.parse(errorText) as { detail?: { message?: string } };
+ errorMessage =
+ errorData.detail?.message || `HTTP ${response.status}: ${response.statusText}`;
+ } catch {
+ errorMessage = `HTTP ${response.status}: ${response.statusText}`;
+ }
+ return {
+ success: false,
+ error: errorMessage,
+ };
+ }
+
+ const data = (await response.json()) as { voices: Voice[] };
+
+ const voices: VoiceInfo[] = data.voices.map((voice) => ({
+ voiceId: voice.voice_id,
+ name: voice.name,
+ category: voice.category,
+ description: voice.description,
+ labels: voice.labels,
+ previewUrl: voice.preview_url,
+ }));
+
+ return {
+ success: true,
+ voices,
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return {
+ success: false,
+ error: `Failed to fetch voices: ${message}`,
+ };
+ }
+}
+
+/**
+ * App entry point - fetches credentials and wraps with logging
+ */
+export async function getVoicesStep(
+ input: GetVoicesInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler({}, credentials));
+}
+
+export const _integrationType = "elevenlabs";
diff --git a/plugins/elevenlabs/steps/sound-effects.ts b/plugins/elevenlabs/steps/sound-effects.ts
new file mode 100644
index 00000000..c1850e51
--- /dev/null
+++ b/plugins/elevenlabs/steps/sound-effects.ts
@@ -0,0 +1,113 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { ElevenlabsCredentials } from "../credentials";
+
+const ELEVENLABS_API_URL = "https://api.elevenlabs.io/v1";
+
+type SoundEffectsResult =
+ | { success: true; audioBase64: string; contentType: string }
+ | { success: false; error: string };
+
+export type SoundEffectsCoreInput = {
+ text: string;
+ durationSeconds?: number;
+};
+
+export type SoundEffectsInput = StepInput &
+ SoundEffectsCoreInput & {
+ integrationId?: string;
+ };
+
+/**
+ * Core logic - portable between app and export
+ */
+async function stepHandler(
+ input: SoundEffectsCoreInput,
+ credentials: ElevenlabsCredentials
+): Promise {
+ const apiKey = credentials.ELEVENLABS_API_KEY;
+
+ if (!apiKey) {
+ return {
+ success: false,
+ error:
+ "ELEVENLABS_API_KEY is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ if (!input.text || input.text.trim() === "") {
+ return {
+ success: false,
+ error: "Text description is required for sound effect generation",
+ };
+ }
+
+ try {
+ const requestBody: Record = {
+ text: input.text,
+ };
+
+ if (input.durationSeconds !== undefined) {
+ requestBody.duration_seconds = input.durationSeconds;
+ }
+
+ const response = await fetch(`${ELEVENLABS_API_URL}/sound-generation`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "xi-api-key": apiKey,
+ },
+ body: JSON.stringify(requestBody),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ let errorMessage: string;
+ try {
+ const errorData = JSON.parse(errorText) as { detail?: { message?: string } };
+ errorMessage =
+ errorData.detail?.message || `HTTP ${response.status}: ${response.statusText}`;
+ } catch {
+ errorMessage = `HTTP ${response.status}: ${response.statusText}`;
+ }
+ return {
+ success: false,
+ error: errorMessage,
+ };
+ }
+
+ const audioBuffer = await response.arrayBuffer();
+ const audioBase64 = Buffer.from(audioBuffer).toString("base64");
+
+ return {
+ success: true,
+ audioBase64,
+ contentType: "audio/mpeg",
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return {
+ success: false,
+ error: `Failed to generate sound effect: ${message}`,
+ };
+ }
+}
+
+/**
+ * App entry point - fetches credentials and wraps with logging
+ */
+export async function soundEffectsStep(
+ input: SoundEffectsInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+
+export const _integrationType = "elevenlabs";
diff --git a/plugins/elevenlabs/steps/speech-to-text.ts b/plugins/elevenlabs/steps/speech-to-text.ts
new file mode 100644
index 00000000..083286d0
--- /dev/null
+++ b/plugins/elevenlabs/steps/speech-to-text.ts
@@ -0,0 +1,140 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { ElevenlabsCredentials } from "../credentials";
+
+const ELEVENLABS_API_URL = "https://api.elevenlabs.io/v1";
+
+type SpeechToTextResult =
+ | { success: true; text: string; language: string }
+ | { success: false; error: string };
+
+export type SpeechToTextCoreInput = {
+ audioUrl: string;
+ languageCode?: string;
+};
+
+export type SpeechToTextInput = StepInput &
+ SpeechToTextCoreInput & {
+ integrationId?: string;
+ };
+
+/**
+ * Core logic - portable between app and export
+ */
+async function stepHandler(
+ input: SpeechToTextCoreInput,
+ credentials: ElevenlabsCredentials
+): Promise {
+ const apiKey = credentials.ELEVENLABS_API_KEY;
+
+ if (!apiKey) {
+ return {
+ success: false,
+ error:
+ "ELEVENLABS_API_KEY is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ if (!input.audioUrl || input.audioUrl.trim() === "") {
+ return {
+ success: false,
+ error: "Audio URL is required for speech-to-text conversion",
+ };
+ }
+
+ try {
+ // First, fetch the audio file from the URL
+ const audioResponse = await fetch(input.audioUrl);
+
+ if (!audioResponse.ok) {
+ return {
+ success: false,
+ error: `Failed to fetch audio from URL: HTTP ${audioResponse.status}`,
+ };
+ }
+
+ const audioBlob = await audioResponse.blob();
+ const contentType = audioResponse.headers.get("content-type") || "audio/mpeg";
+
+ // Determine file extension from content type
+ let extension = "mp3";
+ if (contentType.includes("wav")) {
+ extension = "wav";
+ } else if (contentType.includes("ogg")) {
+ extension = "ogg";
+ } else if (contentType.includes("flac")) {
+ extension = "flac";
+ } else if (contentType.includes("webm")) {
+ extension = "webm";
+ }
+
+ // Create FormData for the multipart request
+ const formData = new FormData();
+ formData.append("file", audioBlob, `audio.${extension}`);
+ formData.append("model_id", "scribe_v1");
+
+ if (input.languageCode) {
+ formData.append("language_code", input.languageCode);
+ }
+
+ const response = await fetch(`${ELEVENLABS_API_URL}/speech-to-text`, {
+ method: "POST",
+ headers: {
+ "xi-api-key": apiKey,
+ },
+ body: formData,
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ let errorMessage: string;
+ try {
+ const errorData = JSON.parse(errorText) as { detail?: { message?: string } };
+ errorMessage =
+ errorData.detail?.message || `HTTP ${response.status}: ${response.statusText}`;
+ } catch {
+ errorMessage = `HTTP ${response.status}: ${response.statusText}`;
+ }
+ return {
+ success: false,
+ error: errorMessage,
+ };
+ }
+
+ const data = (await response.json()) as {
+ text: string;
+ language_code: string;
+ };
+
+ return {
+ success: true,
+ text: data.text,
+ language: data.language_code,
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return {
+ success: false,
+ error: `Failed to transcribe audio: ${message}`,
+ };
+ }
+}
+
+/**
+ * App entry point - fetches credentials and wraps with logging
+ */
+export async function speechToTextStep(
+ input: SpeechToTextInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+
+export const _integrationType = "elevenlabs";
diff --git a/plugins/elevenlabs/steps/text-to-speech.ts b/plugins/elevenlabs/steps/text-to-speech.ts
new file mode 100644
index 00000000..01de3b90
--- /dev/null
+++ b/plugins/elevenlabs/steps/text-to-speech.ts
@@ -0,0 +1,145 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { ElevenlabsCredentials } from "../credentials";
+
+const ELEVENLABS_API_URL = "https://api.elevenlabs.io/v1";
+
+type TextToSpeechResult =
+ | { success: true; audioBase64: string; contentType: string }
+ | { success: false; error: string };
+
+export type TextToSpeechCoreInput = {
+ voiceId: string;
+ text: string;
+ modelId?: string;
+ stability?: number;
+ similarityBoost?: number;
+ style?: number;
+ outputFormat?: string;
+};
+
+export type TextToSpeechInput = StepInput &
+ TextToSpeechCoreInput & {
+ integrationId?: string;
+ };
+
+/**
+ * Core logic - portable between app and export
+ */
+async function stepHandler(
+ input: TextToSpeechCoreInput,
+ credentials: ElevenlabsCredentials
+): Promise {
+ const apiKey = credentials.ELEVENLABS_API_KEY;
+
+ if (!apiKey) {
+ return {
+ success: false,
+ error:
+ "ELEVENLABS_API_KEY is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ if (!input.voiceId) {
+ return {
+ success: false,
+ error: "Voice ID is required",
+ };
+ }
+
+ if (!input.text || input.text.trim() === "") {
+ return {
+ success: false,
+ error: "Text is required for text-to-speech conversion",
+ };
+ }
+
+ const outputFormat = input.outputFormat || "mp3_44100_128";
+
+ try {
+ const voiceSettings: Record = {};
+
+ if (input.stability !== undefined) {
+ voiceSettings.stability = input.stability;
+ }
+ if (input.similarityBoost !== undefined) {
+ voiceSettings.similarity_boost = input.similarityBoost;
+ }
+ if (input.style !== undefined) {
+ voiceSettings.style = input.style;
+ }
+
+ const requestBody: Record = {
+ text: input.text,
+ model_id: input.modelId || "eleven_multilingual_v2",
+ };
+
+ if (Object.keys(voiceSettings).length > 0) {
+ requestBody.voice_settings = voiceSettings;
+ }
+
+ const response = await fetch(
+ `${ELEVENLABS_API_URL}/text-to-speech/${input.voiceId}?output_format=${outputFormat}`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "xi-api-key": apiKey,
+ },
+ body: JSON.stringify(requestBody),
+ }
+ );
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ let errorMessage: string;
+ try {
+ const errorData = JSON.parse(errorText) as { detail?: { message?: string } };
+ errorMessage =
+ errorData.detail?.message || `HTTP ${response.status}: ${response.statusText}`;
+ } catch {
+ errorMessage = `HTTP ${response.status}: ${response.statusText}`;
+ }
+ return {
+ success: false,
+ error: errorMessage,
+ };
+ }
+
+ const audioBuffer = await response.arrayBuffer();
+ const audioBase64 = Buffer.from(audioBuffer).toString("base64");
+
+ const contentType = outputFormat.startsWith("mp3") ? "audio/mpeg" : "audio/wav";
+
+ return {
+ success: true,
+ audioBase64,
+ contentType,
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return {
+ success: false,
+ error: `Failed to generate speech: ${message}`,
+ };
+ }
+}
+
+/**
+ * App entry point - fetches credentials and wraps with logging
+ */
+export async function textToSpeechStep(
+ input: TextToSpeechInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+
+export const _integrationType = "elevenlabs";
diff --git a/plugins/elevenlabs/test.ts b/plugins/elevenlabs/test.ts
new file mode 100644
index 00000000..fe10e31e
--- /dev/null
+++ b/plugins/elevenlabs/test.ts
@@ -0,0 +1,42 @@
+const ELEVENLABS_API_URL = "https://api.elevenlabs.io/v1";
+
+export async function testElevenlabs(credentials: Record) {
+ try {
+ const apiKey = credentials.ELEVENLABS_API_KEY;
+
+ if (!apiKey) {
+ return {
+ success: false,
+ error: "ELEVENLABS_API_KEY is required",
+ };
+ }
+
+ // Validate API key by fetching user info (lightweight read-only endpoint)
+ const response = await fetch(`${ELEVENLABS_API_URL}/user`, {
+ method: "GET",
+ headers: {
+ "xi-api-key": apiKey,
+ },
+ });
+
+ if (!response.ok) {
+ if (response.status === 401) {
+ return {
+ success: false,
+ error: "Invalid API key. Please check your ElevenLabs API key.",
+ };
+ }
+ return {
+ success: false,
+ error: `API validation failed: HTTP ${response.status}`,
+ };
+ }
+
+ return { success: true };
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : String(error),
+ };
+ }
+}
diff --git a/plugins/index.ts b/plugins/index.ts
index af795cb5..dc7b4036 100644
--- a/plugins/index.ts
+++ b/plugins/index.ts
@@ -13,11 +13,12 @@
* 1. Delete the plugin directory
* 2. Run: pnpm discover-plugins (or it runs automatically on build)
*
- * Discovered plugins: ai-gateway, blob, firecrawl, github, linear, resend, slack, superagent, v0
+ * Discovered plugins: ai-gateway, blob, elevenlabs, firecrawl, github, linear, resend, slack, superagent, v0
*/
import "./ai-gateway";
import "./blob";
+import "./elevenlabs";
import "./firecrawl";
import "./github";
import "./linear";