diff --git a/components/alttextify/actions/delete-image/delete-image.mjs b/components/alttextify/actions/delete-image/delete-image.mjs new file mode 100644 index 0000000000000..28e143141e830 --- /dev/null +++ b/components/alttextify/actions/delete-image/delete-image.mjs @@ -0,0 +1,28 @@ +import alttextify from "../../alttextify.app.mjs"; + +export default { + key: "alttextify-delete-image", + name: "Delete Image Alt Text", + description: "Delete the generated alt text for a specific image using the asset ID. [See the documentation](https://apidoc.alttextify.net/#api-Image-DeleteImage)", + version: "0.0.1", + type: "action", + props: { + alttextify, + assetId: { + propDefinition: [ + alttextify, + "assetId", + ], + description: "The ID of the asset for retrieving or deleting alt text.", + }, + }, + async run({ $ }) { + const response = await this.alttextify.deleteAltTextByAssetId({ + $, + assetId: this.assetId, + }); + + $.export("$summary", `Successfully deleted alt text for asset ID: ${this.assetId}`); + return response; + }, +}; diff --git a/components/alttextify/actions/get-alttext-by-asset-id/get-alttext-by-asset-id.mjs b/components/alttextify/actions/get-alttext-by-asset-id/get-alttext-by-asset-id.mjs new file mode 100644 index 0000000000000..1beba5946463a --- /dev/null +++ b/components/alttextify/actions/get-alttext-by-asset-id/get-alttext-by-asset-id.mjs @@ -0,0 +1,27 @@ +import alttextify from "../../alttextify.app.mjs"; + +export default { + key: "alttextify-get-alttext-by-asset-id", + name: "Retrieve Alt Text by Asset ID", + description: "Retrieve alt text for a previously submitted image using the asset ID. [See the documentation](https://apidoc.alttextify.net/#api-Image-GetImageByAssetID)", + version: "0.0.1", + type: "action", + props: { + alttextify, + assetId: { + propDefinition: [ + alttextify, + "assetId", + ], + }, + }, + async run({ $ }) { + const response = await this.alttextify.retrieveAltTextByAssetId({ + $, + assetId: this.assetId, + }); + $.export("$summary", `Successfully retrieved alt text by Asset ID: ${this.assetId}`); + + return response; + }, +}; diff --git a/components/alttextify/actions/submit-image/submit-image.mjs b/components/alttextify/actions/submit-image/submit-image.mjs new file mode 100644 index 0000000000000..689146c3a7bfa --- /dev/null +++ b/components/alttextify/actions/submit-image/submit-image.mjs @@ -0,0 +1,132 @@ +import { getFileStreamAndMetadata } from "@pipedream/platform"; +import alttextify from "../../alttextify.app.mjs"; + +export default { + key: "alttextify-submit-image", + name: "Submit Image to Alttextify", + description: "Upload or submit an image to Alttextify for alt text generation. [See the documentation](https://apidoc.alttextify.net/#api-Image-UploadRawImage)", + version: "0.0.1", + type: "action", + props: { + alttextify, + alert: { + type: "alert", + alertType: "info", + content: "Supported formats: JPEG, PNG, GIF, WEBP, BMP\nMaximum file size: 16 MB\nMinimum dimensions: 50 x 50 (smaller images may not be able to generate alt text).", + }, + async: { + type: "boolean", + label: "Async", + description: "Whether to add the image in the background or immediately (synchronously). If async is set to true, the API response will always be successful with an empty response body.", + default: false, + }, + image: { + type: "string", + label: "Image", + description: "The URL of the file or path to the file saved to the `/tmp` directory. [See the documentation on working with files](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + }, + lang: { + type: "string", + label: "Language", + description: "The language for the alt text. Supported language codes are accepted. If not provided, the account's default language is used.", + default: "en", + }, + maxChars: { + type: "integer", + label: "Max Characters", + description: "Maximum length of the generated alt text.", + }, + assetId: { + type: "string", + label: "Asset ID", + description: "The unique identifier for the asset.", + optional: true, + }, + keywords: { + type: "string[]", + label: "Keywords", + description: "List of keywords/phrases for SEO-optimized alt text. Only one or two will be used per alt text, but all are considered. Keywords must be in English, even for alt text in other languages.", + optional: true, + }, + ecommerceRunOCR: { + type: "boolean", + label: "Ecommerce Run OCR", + description: "Flag to indicate if OCR should be run on the product.", + }, + ecommerceProductName: { + type: "string", + label: "Ecommerce Product Name", + description: "The name of the product in the image.", + optional: true, + }, + ecommerceProductBrand: { + type: "string", + label: "Ecommerce Product Brand", + description: "The brand of the product in the image.", + optional: true, + }, + ecommerceProductColor: { + type: "string", + label: "Ecommerce Product Color", + description: "The color of the product in the image.", + optional: true, + }, + ecommerceProductSize: { + type: "string", + label: "Ecommerce Product Size", + description: "The size of the product in the image.", + optional: true, + }, + }, + methods: { + async streamToBase64(stream) { + return new Promise((resolve, reject) => { + const chunks = []; + + stream.on("data", (chunk) => { + chunks.push(chunk); + }); + + stream.on("end", () => { + const buffer = Buffer.concat(chunks); + resolve(buffer.toString("base64")); + }); + + stream.on("error", (err) => { + reject(err); + }); + }); + }, + }, + async run({ $ }) { + const { + stream, metadata, + } = await getFileStreamAndMetadata(this.image); + const base64String = await this.streamToBase64(stream); + + const response = await this.alttextify.uploadImage({ + $, + data: { + async: this.async, + image: `data:${metadata.contentType};base64,${base64String}`, + lang: this.lang, + maxChars: this.maxChars, + assetId: this.assetId, + keywords: this.keywords, + ecommerce: { + run_ocr: this.ecommerceRunOCR, + product: { + name: this.ecommerceProductName, + brand: this.ecommerceProductBrand, + color: this.ecommerceProductColor, + size: this.ecommerceProductSize, + }, + }, + }, + + }); + + $.export("$summary", `Successfully submitted image to Alttextify for alt text generation with Asset ID: ${response.asset_id}`); + return response; + }, +}; diff --git a/components/alttextify/alttextify.app.mjs b/components/alttextify/alttextify.app.mjs index 5e78812a0d315..34118b4ffb335 100644 --- a/components/alttextify/alttextify.app.mjs +++ b/components/alttextify/alttextify.app.mjs @@ -1,11 +1,100 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "alttextify", - propDefinitions: {}, + propDefinitions: { + assetId: { + type: "string", + label: "Asset ID", + description: "The ID of the asset for retrieving alt text.", + async options({ page }) { + const data = await this.listAltTexts({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + asset_id: value, alt_text: label, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.alttextify.net/api/v1"; + }, + _headers() { + return { + "x-api-key": `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + uploadImage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/image/raw", + ...opts, + }); + }, + deleteAltTextByAssetId({ + assetId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + path: `/image/${assetId}`, + ...opts, + }); + }, + retrieveAltTextByAssetId({ + assetId, ...opts + }) { + return this._makeRequest({ + path: `/image/${assetId}`, + ...opts, + }); + }, + listAltTexts({ ...opts }) { + return this._makeRequest({ + path: "/image", + ...opts, + }); + }, + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const data = await fn({ + params, + ...opts, + }); + for (const d of data) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = data.length; + } while (hasMore); }, }, }; diff --git a/components/alttextify/common/utils.mjs b/components/alttextify/common/utils.mjs new file mode 100644 index 0000000000000..1a5e36f32a603 --- /dev/null +++ b/components/alttextify/common/utils.mjs @@ -0,0 +1,6 @@ +export const checkTmp = (filename) => { + if (!filename.startsWith("/tmp")) { + return `/tmp/${filename}`; + } + return filename; +}; diff --git a/components/alttextify/package.json b/components/alttextify/package.json index a250623577574..b96abc34e37c8 100644 --- a/components/alttextify/package.json +++ b/components/alttextify/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/alttextify", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream AltTextify Components", "main": "alttextify.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } -} \ No newline at end of file +} diff --git a/components/alttextify/sources/new-alttext-generated/new-alttext-generated.mjs b/components/alttextify/sources/new-alttext-generated/new-alttext-generated.mjs new file mode 100644 index 0000000000000..9b4ae6bc6b93f --- /dev/null +++ b/components/alttextify/sources/new-alttext-generated/new-alttext-generated.mjs @@ -0,0 +1,67 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import alttextify from "../../alttextify.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "alttextify-new-alttext-generated", + name: "New Alt Text Generated", + description: "Emit new event when new alt text is generated for an image. [See the documentation](https://apidoc.alttextify.net/#api-Image-GetImages)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + alttextify, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + + const response = this.alttextify.paginate({ + fn: this.alttextify.listAltTexts, + }); + + let responseArray = []; + for await (const item of response) { + if (Date.parse(item.created_at) <= lastDate) break; + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastDate(Date.parse(responseArray[0].created_at)); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.asset_id, + summary: `New alt text generated for asset ${item.asset_id}`, + ts: Date.parse(item.created_at), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, + sampleEmit, +}; diff --git a/components/alttextify/sources/new-alttext-generated/test-event.mjs b/components/alttextify/sources/new-alttext-generated/test-event.mjs new file mode 100644 index 0000000000000..82922f56f8c3c --- /dev/null +++ b/components/alttextify/sources/new-alttext-generated/test-event.mjs @@ -0,0 +1,7 @@ +export default { + "asset_id": "pQ0-oLcLT", + "image_source": "06e1acf1-2312-450c-87f1-d4a1ee0547be.webp", + "alt_text": ["A sprawling metropolis transforms into a kaleidoscope of vibrant lights at dusk, as skyscrapers stand sentinel amidst a symphony of urban activity."], + "tag": null, + "created_at": "2025-03-21T08:39:29.815Z" +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b411287c392a5..e0b85eb1fdfc9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -721,7 +721,11 @@ importers: specifier: ^1.5.1 version: 1.6.6 - components/alttextify: {} + components/alttextify: + dependencies: + '@pipedream/platform': + specifier: ^3.0.3 + version: 3.1.0 components/amara: dependencies: