Add Activepieces integration for workflow automation
- Add Activepieces fork with SmoothSchedule custom piece - Create integrations app with Activepieces service layer - Add embed token endpoint for iframe integration - Create Automations page with embedded workflow builder - Add sidebar visibility fix for embed mode - Add list inactive customers endpoint to Public API - Include SmoothSchedule triggers: event created/updated/cancelled - Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { createAction, Property } from "@activepieces/pieces-framework";
|
||||
import { HttpMethod } from "@activepieces/pieces-common";
|
||||
import { makeRequest } from "../common/client";
|
||||
import { murfAuth } from "../common/auth";
|
||||
import { murfCommon } from "../common/dropdown";
|
||||
|
||||
export const listVoices = createAction({
|
||||
auth: murfAuth,
|
||||
name: "list-voices",
|
||||
displayName: "List Voices",
|
||||
description: "Get the list of available voices for text-to-speech",
|
||||
props: {
|
||||
locale: murfCommon.language,
|
||||
style: Property.Dropdown({
|
||||
auth: murfAuth,
|
||||
displayName: "Style",
|
||||
description: "Filter by style (optional)",
|
||||
required: false,
|
||||
refreshers: ["locale"],
|
||||
options: async ({ auth, locale }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder: "Please connect your Murf account first",
|
||||
};
|
||||
}
|
||||
|
||||
const response = await makeRequest(auth.secret_text , HttpMethod.GET, "/speech/voices");
|
||||
const voices = Array.isArray(response) ? response : [];
|
||||
|
||||
|
||||
const filtered = locale
|
||||
? voices.filter((v: any) => v.locale === locale || (Array.isArray(v.supportedLocales) && v.supportedLocales.includes(locale)))
|
||||
: voices;
|
||||
|
||||
const allStyles = new Set<string>();
|
||||
filtered.forEach((voice: any) => {
|
||||
if (Array.isArray(voice.availableStyles)) {
|
||||
voice.availableStyles.forEach((s: string) => allStyles.add(s));
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
options: [
|
||||
{ label: "All Styles", value: "" },
|
||||
...Array.from(allStyles).map((s) => ({
|
||||
label: s.charAt(0).toUpperCase() + s.slice(1),
|
||||
value: s,
|
||||
})),
|
||||
],
|
||||
};
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
async run(context) {
|
||||
const { locale, style } = context.propsValue;
|
||||
|
||||
const response = await makeRequest(context.auth.secret_text, HttpMethod.GET, "/speech/voices");
|
||||
const voices = Array.isArray(response) ? response : [];
|
||||
|
||||
let filtered = voices;
|
||||
if (locale) {
|
||||
filtered = filtered.filter(
|
||||
(v: any) => v.locale === locale || (Array.isArray(v.supportedLocales) && v.supportedLocales.includes(locale))
|
||||
);
|
||||
}
|
||||
if (style) {
|
||||
filtered = filtered.filter((v: any) =>
|
||||
v.availableStyles?.includes(style)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
voices: filtered.map((voice: any) => ({
|
||||
voiceId: voice.voiceId,
|
||||
displayName: voice.displayName,
|
||||
gender: voice.gender,
|
||||
locale: voice.locale,
|
||||
supportedLocales: voice.supportedLocales,
|
||||
availableStyles: voice.availableStyles,
|
||||
})),
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,145 @@
|
||||
import { createAction, Property } from "@activepieces/pieces-framework";
|
||||
import { murfAuth } from "../common/auth";
|
||||
import { makeRequest } from "../common/client";
|
||||
import { HttpMethod } from "@activepieces/pieces-common";
|
||||
import { murfCommon } from "../common/dropdown";
|
||||
|
||||
export const textToSpeech = createAction({
|
||||
auth: murfAuth,
|
||||
name: "text_to_speech",
|
||||
displayName: "Text to Speech",
|
||||
description: "Converts input text into speech using Murf AI.",
|
||||
props: {
|
||||
language: murfCommon.language,
|
||||
voiceId: murfCommon.voiceId,
|
||||
text: Property.LongText({
|
||||
displayName: "Text",
|
||||
description: "The text to be synthesized.",
|
||||
required: true,
|
||||
}),
|
||||
audioDuration: Property.Number({
|
||||
displayName: "Audio Duration (seconds)",
|
||||
description:
|
||||
"Specify audio length. If 0, Murf ignores this. Only for Gen2 model.",
|
||||
required: false,
|
||||
}),
|
||||
channelType: Property.StaticDropdown({
|
||||
displayName: "Channel Type",
|
||||
description: "Mono or Stereo output.",
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: "Mono", value: "MONO" },
|
||||
{ label: "Stereo", value: "STEREO" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
encodeAsBase64: Property.Checkbox({
|
||||
displayName: "Encode as Base64",
|
||||
description:
|
||||
"Return Base64 encoded audio instead of URL (zero retention).",
|
||||
required: false,
|
||||
}),
|
||||
format: Property.StaticDropdown({
|
||||
displayName: "Audio Format",
|
||||
description: "Select audio format.",
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: "MP3", value: "MP3" },
|
||||
{ label: "WAV", value: "WAV" },
|
||||
{ label: "FLAC", value: "FLAC" },
|
||||
{ label: "ALAW", value: "ALAW" },
|
||||
{ label: "ULAW", value: "ULAW" },
|
||||
{ label: "PCM", value: "PCM" },
|
||||
{ label: "OGG", value: "OGG" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
modelVersion: Property.StaticDropdown({
|
||||
displayName: "Model Version",
|
||||
description: "Choose Gen1 or Gen2.",
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: "GEN1", value: "GEN1" },
|
||||
{ label: "GEN2", value: "GEN2" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
multiNativeLocale: Property.ShortText({
|
||||
displayName: "Multi Native Locale",
|
||||
description:
|
||||
"Set multi-language voice (e.g., en-US, es-ES). Only for Gen2.",
|
||||
required: false,
|
||||
}),
|
||||
pitch: Property.Number({
|
||||
displayName: "Pitch",
|
||||
description: "Pitch adjustment (-50 to 50).",
|
||||
required: false,
|
||||
}),
|
||||
rate: Property.Number({
|
||||
displayName: "Rate",
|
||||
description: "Speed adjustment (-50 to 50).",
|
||||
required: false,
|
||||
}),
|
||||
style: Property.ShortText({
|
||||
displayName: "Style",
|
||||
description:
|
||||
"Voice style (e.g., 'default', 'calm', 'energetic'). Check voice details.",
|
||||
required: false,
|
||||
}),
|
||||
sampleRate: Property.Number({
|
||||
displayName: "Sample Rate",
|
||||
description: "Defaults to 44100. Allowed: 8000, 24000, 44100, 48000.",
|
||||
required: false,
|
||||
}),
|
||||
variation: Property.Number({
|
||||
displayName: "Variation",
|
||||
description:
|
||||
"Variation level (0–5). Defaults to 1. Higher = more natural pauses/pitch/speed.",
|
||||
required: false,
|
||||
}),
|
||||
wordDurationsAsOriginalText: Property.Checkbox({
|
||||
displayName: "Word Durations as Original Text",
|
||||
description:
|
||||
"If true, response word timings map to original text. (English only).",
|
||||
required: false,
|
||||
}),
|
||||
pronunciationDictionary: Property.Json({
|
||||
displayName: "Pronunciation Dictionary",
|
||||
description:
|
||||
"Custom word pronunciations.",
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const body = {
|
||||
text: context.propsValue.text,
|
||||
voiceId: context.propsValue.voiceId,
|
||||
audioDuration: context.propsValue.audioDuration,
|
||||
channelType: context.propsValue.channelType,
|
||||
encodeAsBase64: context.propsValue.encodeAsBase64,
|
||||
format: context.propsValue.format,
|
||||
modelVersion: context.propsValue.modelVersion,
|
||||
multiNativeLocale: context.propsValue.multiNativeLocale,
|
||||
pitch: context.propsValue.pitch,
|
||||
rate: context.propsValue.rate,
|
||||
sampleRate: context.propsValue.sampleRate,
|
||||
style: context.propsValue.style,
|
||||
variation: context.propsValue.variation,
|
||||
wordDurationsAsOriginalText:
|
||||
context.propsValue.wordDurationsAsOriginalText,
|
||||
pronunciationDictionary: context.propsValue.pronunciationDictionary,
|
||||
};
|
||||
|
||||
const response = await makeRequest(
|
||||
context.auth.secret_text ,
|
||||
HttpMethod.POST,
|
||||
"/speech/generate",
|
||||
body
|
||||
);
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createAction, Property } from "@activepieces/pieces-framework";
|
||||
import { murfAuth } from "../common/auth";
|
||||
import { murfCommon } from "../common/dropdown";
|
||||
import { makeRequest } from "../common/client";
|
||||
import { HttpMethod } from "@activepieces/pieces-common";
|
||||
|
||||
export const translateText = createAction({
|
||||
auth: murfAuth,
|
||||
name: "translateText",
|
||||
displayName: "Translate Text",
|
||||
description: "Translate one or more texts to the target language.",
|
||||
props: {
|
||||
targetLanguage: murfCommon.language,
|
||||
texts: Property.Array({
|
||||
displayName: "Texts",
|
||||
description: "List of texts to translate",
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
async run({ auth, propsValue }) {
|
||||
const response = await makeRequest(
|
||||
auth.secret_text ,
|
||||
HttpMethod.POST,
|
||||
"/text/translate",
|
||||
{
|
||||
target_language: propsValue.targetLanguage,
|
||||
texts: propsValue.texts,
|
||||
}
|
||||
);
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import { ApFile, createAction, Property } from "@activepieces/pieces-framework";
|
||||
import { HttpMethod } from "@activepieces/pieces-common";
|
||||
import { murfAuth } from "../common/auth";
|
||||
import { murfCommon } from "../common/dropdown";
|
||||
import { makeRequest } from "../common/client";
|
||||
import FormData from "form-data";
|
||||
|
||||
export const voiceChange = createAction({
|
||||
auth: murfAuth,
|
||||
name: "voice-changer-convert",
|
||||
displayName: "Voice Changer",
|
||||
description: "Convert an input audio file to a different voice using Murf Voice Changer.",
|
||||
props: {
|
||||
language: murfCommon.language,
|
||||
voiceId: murfCommon.voiceId,
|
||||
fileUrl: Property.ShortText({
|
||||
displayName: "File URL",
|
||||
description: "Publicly accessible URL of the input audio file. Either provide this or upload a file.",
|
||||
required: false,
|
||||
}),
|
||||
file: Property.File({
|
||||
displayName: "Upload File",
|
||||
description: "Upload an audio file for voice conversion",
|
||||
required: false,
|
||||
}),
|
||||
format: Property.StaticDropdown({
|
||||
displayName: "Output Format",
|
||||
description: "Format of the generated audio file",
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: "MP3", value: "MP3" },
|
||||
{ label: "WAV", value: "WAV" },
|
||||
{ label: "FLAC", value: "FLAC" },
|
||||
{ label: "OGG", value: "OGG" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
channelType: Property.StaticDropdown({
|
||||
displayName: "Channel Type",
|
||||
description: "Choose MONO or STEREO output",
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: "Mono", value: "MONO" },
|
||||
{ label: "Stereo", value: "STEREO" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
pitch: Property.Number({
|
||||
displayName: "Pitch",
|
||||
description: "Pitch adjustment (-50 to 50)",
|
||||
required: false,
|
||||
}),
|
||||
rate: Property.Number({
|
||||
displayName: "Rate",
|
||||
description: "Speed adjustment (-50 to 50)",
|
||||
required: false,
|
||||
}),
|
||||
encodeOutputAsBase64: Property.Checkbox({
|
||||
displayName: "Encode Output as Base64",
|
||||
description: "Receive audio directly as Base64 instead of a file URL",
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
|
||||
async run({ auth, propsValue }) {
|
||||
try {
|
||||
const { voiceId, file, fileUrl, format, pitch, rate, encodeOutputAsBase64 } =
|
||||
propsValue as {
|
||||
voiceId: string;
|
||||
file?: ApFile;
|
||||
fileUrl?: string;
|
||||
format?: string;
|
||||
pitch?: number;
|
||||
rate?: number;
|
||||
encodeOutputAsBase64?: boolean;
|
||||
};
|
||||
|
||||
// Validation
|
||||
if (!file && !fileUrl) {
|
||||
throw new Error(" Either 'Source Audio File' or 'Source File URL' must be provided.");
|
||||
}
|
||||
if (file && fileUrl) {
|
||||
throw new Error(" Provide only one: 'Source Audio File' OR 'Source File URL', not both.");
|
||||
}
|
||||
|
||||
// Build FormData
|
||||
const formData = new FormData();
|
||||
formData.append("voice_id", voiceId);
|
||||
|
||||
if (fileUrl) {
|
||||
formData.append("file_url", fileUrl);
|
||||
}
|
||||
if (file) {
|
||||
try {
|
||||
const fileBuffer = Buffer.from(file.base64, "base64");
|
||||
const blob = new Blob([fileBuffer]);
|
||||
formData.append("file", blob, file.filename);
|
||||
} catch (e) {
|
||||
throw new Error("Failed to process uploaded file. Ensure it's a valid audio file.");
|
||||
}
|
||||
}
|
||||
|
||||
if (format) formData.append("format", format);
|
||||
if (pitch !== undefined) formData.append("pitch", pitch.toString());
|
||||
if (rate !== undefined) formData.append("rate", rate.toString());
|
||||
if (encodeOutputAsBase64) formData.append("encode_output_as_base64", "true");
|
||||
|
||||
// API request
|
||||
const response = await makeRequest(
|
||||
auth.secret_text,
|
||||
HttpMethod.POST,
|
||||
"/voice-changer/convert",
|
||||
formData,
|
||||
true
|
||||
);
|
||||
|
||||
// Handle Murf error response
|
||||
if (response?.errorMessage) {
|
||||
throw new Error(
|
||||
` Murf API error (${response.errorCode || "unknown"}): ${response.errorMessage}`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: " Voice conversion successful",
|
||||
data: response,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || "Unexpected error during voice conversion",
|
||||
details: error.response?.body || error,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { PieceAuth } from "@activepieces/pieces-framework";
|
||||
import { makeRequest } from "./client";
|
||||
import { HttpMethod } from "@activepieces/pieces-common";
|
||||
|
||||
export const murfAuth = PieceAuth.SecretText({
|
||||
displayName: "API Key",
|
||||
description: "Enter your API key from https://murf.ai",
|
||||
required: true,
|
||||
validate: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "API Key is required",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await makeRequest(auth as string, HttpMethod.GET, "/auth/token");
|
||||
|
||||
if (response && response.token) {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
error: "Invalid API key or token could not be generated",
|
||||
};
|
||||
} catch (e: any) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Auth validation failed: ${e.message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { HttpMethod, httpClient } from "@activepieces/pieces-common";
|
||||
|
||||
export const BASE_URL = "https://api.murf.ai/v1";
|
||||
|
||||
export async function makeRequest(
|
||||
apiKey: string,
|
||||
method: HttpMethod,
|
||||
path: string,
|
||||
body?: any,
|
||||
isFormData = false
|
||||
) {
|
||||
try {
|
||||
let headers: Record<string, string> = {
|
||||
"api-key": apiKey,
|
||||
};
|
||||
|
||||
const requestBody = body;
|
||||
|
||||
if (!isFormData) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
} else if (body && typeof (body as any).getHeaders === "function") {
|
||||
headers = { ...headers, ...(body as any).getHeaders() };
|
||||
}
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method,
|
||||
url: `${BASE_URL}${path}`,
|
||||
headers,
|
||||
body: requestBody,
|
||||
});
|
||||
|
||||
return response.body;
|
||||
} catch (error: any) {
|
||||
throw new Error(
|
||||
`Unexpected error: ${JSON.stringify(error.response || error.message || error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Property } from "@activepieces/pieces-framework";
|
||||
import { HttpMethod } from "@activepieces/pieces-common";
|
||||
import { makeRequest } from "./client";
|
||||
import { murfAuth } from "./auth";
|
||||
|
||||
// Helper to fetch voices
|
||||
const getVoices = async (apiKey: string) => {
|
||||
return await makeRequest(apiKey, HttpMethod.GET, "/speech/voices");
|
||||
};
|
||||
|
||||
// Helper to build unique language list
|
||||
const getLanguages = async (apiKey: string) => {
|
||||
const voices = await getVoices(apiKey);
|
||||
const languageMap = new Map<string, string>();
|
||||
|
||||
voices.forEach((voice: any) => {
|
||||
if (voice.supportedLocales) {
|
||||
Object.keys(voice.supportedLocales).forEach((localeCode) => {
|
||||
if (!languageMap.has(localeCode)) {
|
||||
languageMap.set(
|
||||
localeCode,
|
||||
voice.supportedLocales[localeCode].detail || localeCode
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(languageMap, ([value, label]) => ({ label, value }));
|
||||
};
|
||||
|
||||
export const murfCommon = {
|
||||
language: Property.Dropdown({
|
||||
auth: murfAuth,
|
||||
displayName: "Language",
|
||||
description: "Select your preferred language for the translated output.",
|
||||
required: true,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: "Please connect your Murf account first.",
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
const langs = await getLanguages(auth.secret_text);
|
||||
return {
|
||||
disabled: false,
|
||||
options: langs,
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
voiceId: Property.Dropdown({
|
||||
auth: murfAuth,
|
||||
displayName: "Voice",
|
||||
description: "Choose a voice for converting text into speech",
|
||||
required: true,
|
||||
refreshers: ["language"],
|
||||
options: async ({ auth, language }) => {
|
||||
if (!auth|| !language) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: "Please select a language and connect your Murf account first.",
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
const voices = await getVoices(auth.secret_text);
|
||||
const filtered = voices.filter((v: any) =>
|
||||
Object.keys(v.supportedLocales || {}).includes(language as string)
|
||||
);
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
options: filtered.map((v: any) => ({
|
||||
label: `${v.displayName} (${v.gender}, ${v.locale})`,
|
||||
value: v.voiceId,
|
||||
})),
|
||||
};
|
||||
},
|
||||
}),
|
||||
sourceLocale: Property.Dropdown({
|
||||
auth: murfAuth,
|
||||
displayName: "Source Locale",
|
||||
description: "Select the source locale for input text.",
|
||||
required: false,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: "Please connect your Murf account first.",
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
const voices = await getVoices(auth.secret_text);
|
||||
|
||||
const localeMap = new Map<string, string>();
|
||||
voices.forEach((voice: any) => {
|
||||
if (voice.supportedLocales) {
|
||||
Object.entries(voice.supportedLocales).forEach(([localeCode, localeData]: any) => {
|
||||
if (!localeMap.has(localeCode)) {
|
||||
localeMap.set(localeCode, localeData.detail);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
options: Array.from(localeMap, ([value, label]) => ({ value, label })),
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user