diff --git a/components/leonardo_ai/README.md b/components/leonardo_ai/README.md new file mode 100644 index 0000000000000..bab2cb162520d --- /dev/null +++ b/components/leonardo_ai/README.md @@ -0,0 +1,14 @@ +# Leonardo AI + +[Leonardo AI](https://leonardo.ai) is an AI-powered image generation platform that allows you to create stunning images, videos, and 3D models using advanced machine learning models. + +## API Reference + +For detailed information about Leonardo AI's API, visit the [official documentation](https://docs.leonardo.ai/reference). + +## Support + +For support with this component or Leonardo AI's API, please refer to: +- [Leonardo AI Documentation](https://docs.leonardo.ai) +- [Leonardo AI Community](https://community.leonardo.ai) +- [Pipedream Support](https://pipedream.com/support) diff --git a/components/leonardo_ai/actions/generate-image/generate-image.mjs b/components/leonardo_ai/actions/generate-image/generate-image.mjs new file mode 100644 index 0000000000000..30f48d7ffd0b5 --- /dev/null +++ b/components/leonardo_ai/actions/generate-image/generate-image.mjs @@ -0,0 +1,111 @@ +import app from "../../leonardo_ai.app.mjs"; + +export default { + key: "leonardo_ai-generate-image", + name: "Generate Image", + description: "Generates new images using Leonardo AI's image generation API. [See the documentation](https://docs.leonardo.ai/reference/creategeneration)", + version: "0.0.1", + type: "action", + props: { + app, + prompt: { + type: "string", + label: "Prompt", + description: "The text prompt describing the image you want to generate.", + }, + modelId: { + type: "string", + label: "Model", + description: "The model to use for generation. Leave empty to use the default model.", + async options() { + const models = await this.app.getPlatformModels(); + return models.map((model) => ({ + label: model.name || model.id, + value: model.id, + })); + }, + optional: true, + }, + width: { + type: "integer", + label: "Width", + description: "Width of the generated image in pixels.", + default: 1024, + min: 32, + max: 1536, + }, + height: { + type: "integer", + label: "Height", + description: "Height of the generated image in pixels.", + default: 768, + min: 32, + max: 1536, + }, + numImages: { + type: "integer", + label: "Number of Images", + description: "Number of images to generate (1-8). If either width or height is over 768, must be between 1 and 4.", + default: 1, + min: 1, + max: 8, + }, + guidanceScale: { + type: "integer", + label: "Guidance Scale", + description: "How closely the model should follow the prompt. Must be between 1 and 20. Higher values = more adherence to prompt.", + default: 7, + min: 1, + max: 20, + optional: true, + }, + numInferenceSteps: { + type: "integer", + label: "Inference Steps", + description: "Number of denoising steps. More steps = higher quality but slower generation.", + default: 15, + min: 10, + max: 60, + optional: true, + }, + seed: { + type: "integer", + label: "Seed", + description: "Random seed for reproducible generation. Leave empty for random generation.", + optional: true, + }, + }, + async run({ $ }) { + const { + prompt, + modelId, + width, + height, + numImages, + guidanceScale, + numInferenceSteps, + seed, + } = this; + + const data = { + prompt, + width, + height, + num_images: numImages, + modelId, + guidance_scale: guidanceScale, + num_inference_steps: numInferenceSteps, + seed, + }; + + const response = await this.app._makeRequest({ + $, + method: "POST", + path: "/generations", + data, + }); + + $.export("$summary", `Successfully generated ${numImages} image(s)`); + return response; + }, +}; diff --git a/components/leonardo_ai/actions/generate-motion/generate-motion.mjs b/components/leonardo_ai/actions/generate-motion/generate-motion.mjs new file mode 100644 index 0000000000000..8d4de7206a721 --- /dev/null +++ b/components/leonardo_ai/actions/generate-motion/generate-motion.mjs @@ -0,0 +1,117 @@ +import app from "../../leonardo_ai.app.mjs"; + +export default { + key: "leonardo_ai-generate-motion", + name: "Generate Motion", + description: "Generates a motion (video) from the provided image using Leonardo AI's SVD Motion Generation API. [See the documentation](https://docs.leonardo.ai/reference/createsvdmotiongeneration)", + version: "0.0.1", + type: "action", + props: { + app, + imageId: { + type: "string", + label: "Image ID", + description: "The ID of the image to generate motion from. You can either select from previously generated images or manually enter the ID of an uploaded image.", + async options({ prevContext }) { + // Get user info to retrieve userId + const userInfo = await this.app.getUserInfo({ + $: this, + }); + // Extract userId from the response structure + const userId = userInfo.user_details?.[0]?.user?.id || userInfo.id; + + // Get generations with pagination + const offset = prevContext?.offset || 0; + const limit = 20; + + const generations = await this.app.getGenerationsByUserId({ + $: this, + userId, + offset, + limit, + }); + + // Extract image IDs from generated_images array + const options = []; + if (generations.generations) { + for (const generation of generations.generations) { + if (generation.generated_images) { + for (const image of generation.generated_images) { + options.push({ + label: `Image ${image.id} (Generation ${generation.id})`, + value: image.id, + }); + } + } + } + } + + // Check if there are more pages + const hasMore = generations.generations && generations.generations.length === limit; + const nextOffset = hasMore + ? offset + limit + : null; + + return { + options, + context: nextOffset + ? { + offset: nextOffset, + } + : {}, + }; + }, + }, + motionStrength: { + type: "integer", + label: "Motion Strength", + description: "The motion strength for the video generation.", + optional: true, + }, + isPublic: { + type: "boolean", + label: "Is Public", + description: "Whether the generation is public or not.", + optional: true, + }, + isInitImage: { + type: "boolean", + label: "Is Init Image", + description: "Whether the image being used is an init image uploaded by the user.", + optional: true, + }, + isVariation: { + type: "boolean", + label: "Is Variation", + description: "Whether the image being used is a variation image.", + optional: true, + }, + }, + async run({ $ }) { + const { + imageId, + motionStrength, + isPublic, + isInitImage, + isVariation, + } = this; + + const data = { + imageId, + motionStrength, + isPublic, + isInitImage, + isVariation, + }; + + const response = await this.app._makeRequest({ + $, + method: "POST", + path: "/generations-motion-svd", + data, + }); + + $.export("$summary", `Successfully generated motion from image ID: ${imageId}`); + return response; + }, +}; diff --git a/components/leonardo_ai/actions/unzoom-image/unzoom-image.mjs b/components/leonardo_ai/actions/unzoom-image/unzoom-image.mjs new file mode 100644 index 0000000000000..f79db0dc52fb8 --- /dev/null +++ b/components/leonardo_ai/actions/unzoom-image/unzoom-image.mjs @@ -0,0 +1,44 @@ +import app from "../../leonardo_ai.app.mjs"; + +export default { + key: "leonardo_ai-unzoom-image", + name: "Unzoom Image", + description: "Creates an unzoom variation for a generated or variation image using Leonardo AI's unzoom API. [See the documentation](https://docs.leonardo.ai/reference/createvariationunzoom)", + version: "0.0.1", + type: "action", + props: { + app, + imageId: { + type: "string", + label: "Image ID", + description: "The ID of the image to create an unzoom variation for. This should be a previously generated or variation image ID.", + }, + isVariation: { + type: "boolean", + label: "Is Variation", + description: "Whether the image is a variation image.", + default: false, + }, + }, + async run({ $ }) { + const { + imageId, + isVariation, + } = this; + + const data = { + id: imageId, + isVariation, + }; + + const response = await this.app._makeRequest({ + $, + method: "POST", + path: "/variations/unzoom", + data, + }); + + $.export("$summary", `Successfully created unzoom variation for image ID: ${imageId}`); + return response; + }, +}; diff --git a/components/leonardo_ai/actions/upload-image/upload-image.mjs b/components/leonardo_ai/actions/upload-image/upload-image.mjs new file mode 100644 index 0000000000000..3776134de71ee --- /dev/null +++ b/components/leonardo_ai/actions/upload-image/upload-image.mjs @@ -0,0 +1,96 @@ +import app from "../../leonardo_ai.app.mjs"; +import FormData from "form-data"; +import { getFileStreamAndMetadata } from "@pipedream/platform"; + +export default { + key: "leonardo_ai-upload-image", + name: "Upload Image", + description: "Uploads a new image to Leonardo AI for use in generations and variations. [See the documentation](https://docs.leonardo.ai/docs/how-to-upload-an-image-using-a-presigned-url)", + version: "0.0.1", + type: "action", + props: { + app, + extension: { + type: "string", + label: "File Extension", + description: "The file extension of the image to upload.", + options: [ + { + label: "PNG", + value: "png", + }, + { + label: "JPG", + value: "jpg", + }, + { + label: "JPEG", + value: "jpeg", + }, + { + label: "WebP", + value: "webp", + }, + ], + }, + filePath: { + type: "string", + label: "File Path or URL", + description: "The file to upload. Provide either a file URL or a path to a file in the `/tmp` directory (for example, `/tmp/myImage.png`)", + }, + syncDir: { + type: "dir", + accessMode: "read", + sync: true, + optional: true, + }, + }, + async run({ $ }) { + const { + extension, + filePath, + } = this; + // Get file stream from URL or /tmp based path + const { + stream, + metadata, + } = await getFileStreamAndMetadata(filePath); + + // Step 1: Get the presigned URL, upload fields appended to formData + const uploadResponse = await this.app.getUploadInitImage({ + $, + extension, + }); + + const { uploadInitImage } = uploadResponse; + const fields = JSON.parse(uploadInitImage.fields); + const formData = new FormData(); + + //Important: Order of fields is sanctioned by Leonardo AI API. Fields go first, then the file + for (const [ + label, + value, + ] of Object.entries(fields)) { + formData.append(label, value.toString()); + } + formData.append("file", stream, { + contentType: metadata.contentType, + knownLength: metadata.size, + filename: metadata.name, + }); + const uploadUrl = uploadInitImage.url; + + // Step 2: Upload the file to the presigned URL + const uploadResult = await this.app.uploadFileToPresignedUrl({ + $, + url: uploadUrl, + formData, + }); + + $.export("$summary", `Successfully uploaded image: ${metadata.name || filePath}`); + return { + uploadInitImage, + uploadResult, + }; + }, +}; diff --git a/components/leonardo_ai/actions/upscale-image/upscale-image.mjs b/components/leonardo_ai/actions/upscale-image/upscale-image.mjs new file mode 100644 index 0000000000000..99485b213e335 --- /dev/null +++ b/components/leonardo_ai/actions/upscale-image/upscale-image.mjs @@ -0,0 +1,34 @@ +import app from "../../leonardo_ai.app.mjs"; + +export default { + key: "leonardo_ai-upscale-image", + name: "Upscale Image", + description: "Creates a high-resolution upscale of the provided image using Leonardo AI's upscale API. [See the documentation](https://docs.leonardo.ai/reference/createvariationupscale)", + version: "0.0.1", + type: "action", + props: { + app, + imageId: { + type: "string", + label: "Image ID", + description: "The ID of the image to upscale. This should be a previously generated or uploaded image ID.", + }, + }, + async run({ $ }) { + const { imageId } = this; + + const data = { + id: imageId, + }; + + const response = await this.app._makeRequest({ + $, + method: "POST", + path: "/variations/upscale", + data, + }); + + $.export("$summary", `Successfully upscaled image ID: ${imageId}`); + return response; + }, +}; diff --git a/components/leonardo_ai/leonardo_ai.app.mjs b/components/leonardo_ai/leonardo_ai.app.mjs index 526c1b8b0d642..fd342f19c8e58 100644 --- a/components/leonardo_ai/leonardo_ai.app.mjs +++ b/components/leonardo_ai/leonardo_ai.app.mjs @@ -1,11 +1,109 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "leonardo_ai", propDefinitions: {}, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://cloud.leonardo.ai/api/rest/v1"; + }, + _getHeaders() { + return { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.$auth.api_key}`, + "Accept": "application/json", + }; + }, + async _makeRequest({ + $ = this, + method = "GET", + path, + url, // Allow external URLs (e.g., presigned URLs) + data, + ...opts + }) { + const { + headers: userHeaders, + ...rest + } = opts; + + // Use provided URL or construct from base URL + path + const requestUrl = url || `${this._baseUrl()}${path}`; + + // For external URLs (like presigned URLs), don't add default headers + // For internal API calls, add default headers + const defaultHeaders = url + ? {} + : this._getHeaders(); + + const config = { + method, + ...rest, + url: requestUrl, + headers: { + ...defaultHeaders, + ...(userHeaders || {}), + }, + data, + }; + return await axios($, config); + }, + async getPlatformModels() { + const data = await this._makeRequest({ + method: "GET", + path: "/platformModels", + }); + return data.custom_models || []; + }, + async getUploadInitImage({ + $, extension, + }) { + const data = await this._makeRequest({ + $, + method: "POST", + path: "/init-image", + data: { + extension, + }, + }); + return data; + }, + async uploadFileToPresignedUrl({ + $, url, formData, + }) { + const response = await this._makeRequest({ + $, + url, // Use the presigned URL directly + method: "POST", + data: formData, + headers: { + ...formData.getHeaders(), + }, + }); + return response; + }, + async getUserInfo({ $ }) { + const data = await this._makeRequest({ + $, + method: "GET", + path: "/me", + }); + return data; + }, + async getGenerationsByUserId({ + $, userId, offset = 0, limit = 20, + }) { + const data = await this._makeRequest({ + $, + method: "GET", + path: `/generations/user/${userId}`, + params: { + offset, + limit, + }, + }); + return data; }, }, }; diff --git a/components/leonardo_ai/package.json b/components/leonardo_ai/package.json index 01a75f55d3cda..1a19e553d99a1 100644 --- a/components/leonardo_ai/package.json +++ b/components/leonardo_ai/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/leonardo_ai", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Leonardo AI Components", "main": "leonardo_ai.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.1.0", + "form-data": "^4.0.4" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cfc3a254fa106..84fdad8e11c70 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7085,8 +7085,7 @@ importers: specifier: ^1.5.1 version: 1.6.6 - components/instamojo: - specifiers: {} + components/instamojo: {} components/instant: {} @@ -7873,7 +7872,14 @@ importers: specifier: ^0.3.2 version: 0.3.2 - components/leonardo_ai: {} + components/leonardo_ai: + dependencies: + '@pipedream/platform': + specifier: ^3.1.0 + version: 3.1.0 + form-data: + specifier: ^4.0.4 + version: 4.0.4 components/lessaccounting: {} @@ -11743,8 +11749,7 @@ importers: components/redmine: {} - components/reduct_video: - specifiers: {} + components/reduct_video: {} components/referral_rocket: {} @@ -13048,8 +13053,7 @@ importers: specifier: 1.6.0 version: 1.6.0 - components/shopware: - specifiers: {} + components/shopware: {} components/short: dependencies: