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:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -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,
})),
};
},
});

View File

@@ -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 (05). 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;
},
});

View File

@@ -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;
},
});

View File

@@ -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,
};
}
},
});

View File

@@ -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}`,
};
}
},
});

View File

@@ -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)}`
);
}
}

View File

@@ -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 })),
};
},
}),
};