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,68 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { propsValidation } from '@activepieces/pieces-common';
|
||||
import { runwayAuth } from '../common';
|
||||
import RunwayML from '@runwayml/sdk';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const cancelOrDeleteTask = createAction({
|
||||
auth: runwayAuth,
|
||||
name: 'cancel_or_delete_task',
|
||||
displayName: 'Cancel or Delete Task',
|
||||
description: 'Cancel or delete a task. Running/pending tasks are cancelled, completed tasks are deleted.',
|
||||
props: {
|
||||
taskId: Property.ShortText({
|
||||
displayName: 'Task ID',
|
||||
description: 'The ID of the task to cancel or delete (UUID format)',
|
||||
required: true
|
||||
}),
|
||||
},
|
||||
async run({ auth, propsValue }) {
|
||||
await propsValidation.validateZod(propsValue, {
|
||||
taskId: z.string().uuid('Task ID must be a valid UUID format'),
|
||||
});
|
||||
|
||||
const apiKey = auth.secret_text;
|
||||
const client = new RunwayML({ apiKey });
|
||||
|
||||
// First get task details to determine what action will be taken
|
||||
let task;
|
||||
try {
|
||||
task = await client.tasks.retrieve(propsValue.taskId);
|
||||
} catch (error: any) {
|
||||
if (error.status === 404) {
|
||||
throw new Error(`Task not found: ${propsValue.taskId}. The task may have already been deleted.`);
|
||||
}
|
||||
throw new Error(`Failed to retrieve task: ${error.message || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
const wasRunning = ['RUNNING', 'PENDING', 'THROTTLED'].includes(task.status);
|
||||
const action = wasRunning ? 'cancelled' : 'deleted';
|
||||
|
||||
try {
|
||||
await client.tasks.delete(propsValue.taskId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
taskId: task.id,
|
||||
action: action,
|
||||
message: `Task ${action} successfully`,
|
||||
previousStatus: task.status,
|
||||
wasRunning: wasRunning
|
||||
};
|
||||
} catch (error: any) {
|
||||
if (error.status === 404) {
|
||||
return {
|
||||
success: true,
|
||||
taskId: propsValue.taskId,
|
||||
action: 'already_deleted',
|
||||
message: 'Task was already deleted',
|
||||
previousStatus: 'UNKNOWN',
|
||||
wasRunning: false
|
||||
};
|
||||
}
|
||||
throw new Error(`Failed to ${action.slice(0, -1)} task: ${error.message || 'Unknown error'}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { propsValidation } from '@activepieces/pieces-common';
|
||||
import { runwayAuth } from '../common';
|
||||
import RunwayML from '@runwayml/sdk';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const generateImageFromText = createAction({
|
||||
auth: runwayAuth,
|
||||
name: 'generate_image_from_text',
|
||||
displayName: 'Generate Image From Text',
|
||||
description: 'Generates an image using a text prompt via Runway\'s AI models',
|
||||
props: {
|
||||
model: Property.StaticDropdown({
|
||||
displayName: 'Model',
|
||||
description: 'AI model to use for image generation',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Gen4 Image (High Quality)', value: 'gen4_image' },
|
||||
{ label: 'Gen4 Image Turbo (Fast)', value: 'gen4_image_turbo' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
promptText: Property.LongText({
|
||||
displayName: 'Prompt',
|
||||
description: 'Describe what you want to see in the image (max 1000 characters)',
|
||||
required: true,
|
||||
}),
|
||||
ratio: Property.StaticDropdown({
|
||||
displayName: 'Image Ratio',
|
||||
description: 'Resolution of the output image',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'HD Landscape (1920x1080)', value: '1920:1080' },
|
||||
{ label: 'HD Portrait (1080x1920)', value: '1080:1920' },
|
||||
{ label: 'Square (1024x1024)', value: '1024:1024' },
|
||||
{ label: 'Wide (1360x768)', value: '1360:768' },
|
||||
{ label: 'Square HD (1080x1080)', value: '1080:1080' },
|
||||
{ label: 'Standard Wide (1168x880)', value: '1168:880' },
|
||||
{ label: 'Standard Landscape (1440x1080)', value: '1440:1080' },
|
||||
{ label: 'Standard Portrait (1080x1440)', value: '1080:1440' },
|
||||
{ label: 'Ultra Wide (1808x768)', value: '1808:768' },
|
||||
{ label: 'Cinema Wide (2112x912)', value: '2112:912' },
|
||||
{ label: 'HD 720p (1280x720)', value: '1280:720' },
|
||||
{ label: 'HD 720p Portrait (720x1280)', value: '720:1280' },
|
||||
{ label: 'Square 720p (720x720)', value: '720:720' },
|
||||
{ label: 'Standard (960x720)', value: '960:720' },
|
||||
{ label: 'Standard Portrait (720x960)', value: '720:960' },
|
||||
{ label: 'Wide 720p (1680x720)', value: '1680:720' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
seed: Property.Number({
|
||||
displayName: 'Seed (Optional)',
|
||||
description: 'Random seed for reproducible results (0-4294967295)',
|
||||
required: false,
|
||||
}),
|
||||
referenceImages: Property.Array({
|
||||
displayName: 'Reference Images (Optional)',
|
||||
description: 'Up to 3 reference images (required for gen4_image_turbo)',
|
||||
required: false,
|
||||
properties: {
|
||||
uri: Property.ShortText({
|
||||
displayName: 'Image URL',
|
||||
description: 'HTTPS URL or data URI of the reference image',
|
||||
required: true
|
||||
}),
|
||||
tag: Property.ShortText({
|
||||
displayName: 'Tag',
|
||||
description: 'Name to reference this image in your prompt using @tag (3-16 characters, alphanumeric + underscore)',
|
||||
required: false
|
||||
}),
|
||||
},
|
||||
}),
|
||||
publicFigureThreshold: Property.StaticDropdown({
|
||||
displayName: 'Public Figure Detection',
|
||||
description: 'How strict content moderation should be for recognizable public figures',
|
||||
required: false,
|
||||
defaultValue: 'auto',
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Auto (Recommended)', value: 'auto' },
|
||||
{ label: 'Low (Less Strict)', value: 'low' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
async run({ auth, propsValue }) {
|
||||
// Zod validation
|
||||
await propsValidation.validateZod(propsValue, {
|
||||
promptText: z.string().min(1, 'Prompt text cannot be empty').max(1000, 'Prompt text must be 1000 characters or fewer'),
|
||||
seed: z.number().min(0, 'Seed must be at least 0').max(4294967295, 'Seed must be at most 4294967295').optional(),
|
||||
referenceImages: z.array(z.object({
|
||||
uri: z.string().url('Reference image URI must be a valid URL'),
|
||||
tag: z.string().min(3, 'Tag must be at least 3 characters').max(16, 'Tag must be at most 16 characters').regex(/^[a-zA-Z][a-zA-Z0-9_]*$/, 'Tag must start with a letter and contain only letters, numbers, and underscores').optional()
|
||||
})).max(3, 'Maximum of 3 reference images allowed').optional(),
|
||||
});
|
||||
|
||||
// Special validation for gen4_image_turbo
|
||||
if (propsValue.model === 'gen4_image_turbo' && (!propsValue.referenceImages || propsValue.referenceImages.length === 0)) {
|
||||
throw new Error('gen4_image_turbo requires at least one reference image');
|
||||
}
|
||||
|
||||
const apiKey = auth.secret_text;
|
||||
const client = new RunwayML({ apiKey });
|
||||
|
||||
const requestBody: any = {
|
||||
model: propsValue.model,
|
||||
promptText: propsValue.promptText,
|
||||
ratio: propsValue.ratio,
|
||||
};
|
||||
|
||||
if (propsValue.seed !== undefined) {
|
||||
requestBody.seed = propsValue.seed;
|
||||
}
|
||||
|
||||
if (propsValue.referenceImages && propsValue.referenceImages.length > 0) {
|
||||
requestBody.referenceImages = propsValue.referenceImages;
|
||||
}
|
||||
|
||||
if (propsValue.publicFigureThreshold) {
|
||||
requestBody.contentModeration = {
|
||||
publicFigureThreshold: propsValue.publicFigureThreshold
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const task = await client.textToImage.create(requestBody);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
taskId: task.id,
|
||||
};
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to generate image: ${error.message || 'Unknown error'}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
import { createAction, Property, DynamicPropsValue } from '@activepieces/pieces-framework';
|
||||
import { propsValidation } from '@activepieces/pieces-common';
|
||||
import { runwayAuth } from '../common';
|
||||
import RunwayML from '@runwayml/sdk';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const generateVideoFromImage = createAction({
|
||||
auth: runwayAuth,
|
||||
name: 'generate_video_from_image',
|
||||
displayName: 'Generate Video From Image',
|
||||
description: 'Generates a video based on image(s) and text prompt using Runway\'s AI models',
|
||||
props: {
|
||||
model: Property.StaticDropdown({
|
||||
displayName: 'Model',
|
||||
description: 'AI model to use for video generation',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Gen4 Turbo (Fast, High Quality)', value: 'gen4_turbo' },
|
||||
{ label: 'Gen3a Turbo (Balanced)', value: 'gen3a_turbo' },
|
||||
{ label: 'Veo3 (Google\'s Latest)', value: 'veo3' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
promptImageFile: Property.File({
|
||||
displayName: 'Prompt Image File',
|
||||
description: 'Upload an image file to use as the video\'s starting frame',
|
||||
required: false
|
||||
}),
|
||||
promptImageUrl: Property.ShortText({
|
||||
displayName: 'Prompt Image URL',
|
||||
description: 'HTTPS URL of an image to use as the video\'s starting frame',
|
||||
required: false
|
||||
}),
|
||||
imagePosition: Property.StaticDropdown({
|
||||
displayName: 'Image Position',
|
||||
description: 'Position of the image in the video (last frame only supported by gen3a_turbo)',
|
||||
required: false,
|
||||
defaultValue: 'first',
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'First Frame', value: 'first' },
|
||||
{ label: 'Last Frame (gen3a_turbo only)', value: 'last' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
promptText: Property.LongText({
|
||||
displayName: 'Prompt Text (Optional)',
|
||||
description: 'Describe what should happen in the video (max 1000 characters)',
|
||||
required: false
|
||||
}),
|
||||
ratio: Property.DynamicProperties({
|
||||
auth: runwayAuth,
|
||||
displayName: 'Video Resolution',
|
||||
description: 'Available resolutions depend on the selected model',
|
||||
required: true,
|
||||
refreshers: ['model'],
|
||||
props: async ({ model }) => {
|
||||
const ratioOptions: DynamicPropsValue = {};
|
||||
|
||||
if ((model as unknown as string) === 'gen4_turbo') {
|
||||
ratioOptions['ratio'] = Property.StaticDropdown({
|
||||
displayName: 'Resolution',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'HD Landscape (1280x720)', value: '1280:720' },
|
||||
{ label: 'HD Portrait (720x1280)', value: '720:1280' },
|
||||
{ label: 'Wide (1104x832)', value: '1104:832' },
|
||||
{ label: 'Tall (832x1104)', value: '832:1104' },
|
||||
{ label: 'Square (960x960)', value: '960:960' },
|
||||
{ label: 'Ultra Wide (1584x672)', value: '1584:672' },
|
||||
],
|
||||
},
|
||||
});
|
||||
} else if ((model as unknown as string) === 'gen3a_turbo') {
|
||||
ratioOptions['ratio'] = Property.StaticDropdown({
|
||||
displayName: 'Resolution',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Landscape (1280x768)', value: '1280:768' },
|
||||
{ label: 'Portrait (768x1280)', value: '768:1280' },
|
||||
],
|
||||
},
|
||||
});
|
||||
} else if ((model as unknown as string) === 'veo3') {
|
||||
ratioOptions['ratio'] = Property.StaticDropdown({
|
||||
displayName: 'Resolution',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'HD Landscape (1280x720)', value: '1280:720' },
|
||||
{ label: 'HD Portrait (720x1280)', value: '720:1280' },
|
||||
],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
ratioOptions['ratio'] = Property.StaticDropdown({
|
||||
displayName: 'Resolution',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Select a model first', value: '' },
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return ratioOptions;
|
||||
},
|
||||
}),
|
||||
duration: Property.DynamicProperties({
|
||||
auth: runwayAuth,
|
||||
displayName: 'Video Duration',
|
||||
description: 'Available durations depend on the selected model',
|
||||
required: true,
|
||||
refreshers: ['model'],
|
||||
props: async ({ model }) => {
|
||||
const durationOptions: DynamicPropsValue = {};
|
||||
|
||||
if ((model as unknown as string) === 'veo3') {
|
||||
durationOptions['duration'] = Property.StaticDropdown({
|
||||
displayName: 'Duration (seconds)',
|
||||
required: true,
|
||||
defaultValue: 8,
|
||||
options: {
|
||||
options: [
|
||||
{ label: '8 seconds (required for veo3)', value: 8 },
|
||||
],
|
||||
},
|
||||
});
|
||||
} else if ((model as unknown as string) === 'gen4_turbo' || (model as unknown as string) === 'gen3a_turbo') {
|
||||
durationOptions['duration'] = Property.StaticDropdown({
|
||||
displayName: 'Duration (seconds)',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: '5 seconds', value: 5 },
|
||||
{ label: '10 seconds', value: 10 },
|
||||
],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
durationOptions['duration'] = Property.StaticDropdown({
|
||||
displayName: 'Duration (seconds)',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Select a model first', value: '' },
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return durationOptions;
|
||||
},
|
||||
}),
|
||||
seed: Property.Number({
|
||||
displayName: 'Seed (Optional)',
|
||||
description: 'Random seed for reproducible results (0-4294967295)',
|
||||
required: false
|
||||
}),
|
||||
contentModeration: Property.DynamicProperties({
|
||||
auth: runwayAuth,
|
||||
displayName: 'Content Moderation',
|
||||
description: 'Content moderation settings (not available for veo3)',
|
||||
required: false,
|
||||
refreshers: ['model'],
|
||||
props: async ({ model }) => {
|
||||
const moderationOptions: DynamicPropsValue = {};
|
||||
|
||||
if ((model as unknown as string) === 'veo3') {
|
||||
moderationOptions['info'] = Property.MarkDown({
|
||||
value: '**Note:** Content moderation is not supported by the veo3 model.',
|
||||
});
|
||||
} else {
|
||||
moderationOptions['publicFigureThreshold'] = Property.StaticDropdown({
|
||||
displayName: 'Public Figure Detection',
|
||||
description: 'How strict content moderation should be for recognizable public figures',
|
||||
required: false,
|
||||
defaultValue: 'auto',
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Auto (Recommended)', value: 'auto' },
|
||||
{ label: 'Low (Less Strict)', value: 'low' },
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return moderationOptions;
|
||||
},
|
||||
}),
|
||||
},
|
||||
async run({ auth, propsValue, files }) {
|
||||
// Zod validation
|
||||
await propsValidation.validateZod(propsValue, {
|
||||
promptText: z.string().max(1000, 'Prompt text must be 1000 characters or fewer').optional(),
|
||||
seed: z.number().min(0, 'Seed must be at least 0').max(4294967295, 'Seed must be at most 4294967295').optional(),
|
||||
promptImageUrl: z.string().url('Prompt image URL must be a valid HTTPS URL').optional(),
|
||||
});
|
||||
|
||||
// Input validation
|
||||
const hasFile = !!propsValue.promptImageFile;
|
||||
const hasUrl = !!propsValue.promptImageUrl;
|
||||
|
||||
if ((hasFile && hasUrl) || (!hasFile && !hasUrl)) {
|
||||
throw new Error('You must provide either a Prompt Image File or a Prompt Image URL, but not both.');
|
||||
}
|
||||
|
||||
// Model-specific validations
|
||||
const model = propsValue.model;
|
||||
const duration = (propsValue.duration as any)?.['duration'] || propsValue.duration;
|
||||
const ratio = (propsValue.ratio as any)?.['ratio'] || propsValue.ratio;
|
||||
const imagePosition = propsValue.imagePosition || 'first';
|
||||
|
||||
// Validate duration per model
|
||||
if (model === 'veo3' && duration !== 8) {
|
||||
throw new Error('veo3 model requires a duration of exactly 8 seconds');
|
||||
}
|
||||
if ((model === 'gen4_turbo' || model === 'gen3a_turbo') && duration !== 5 && duration !== 10) {
|
||||
throw new Error(`${model} model requires a duration of either 5 or 10 seconds`);
|
||||
}
|
||||
|
||||
// Validate ratio per model
|
||||
const validRatios = {
|
||||
gen4_turbo: ['1280:720', '720:1280', '1104:832', '832:1104', '960:960', '1584:672'],
|
||||
gen3a_turbo: ['1280:768', '768:1280'],
|
||||
veo3: ['1280:720', '720:1280']
|
||||
};
|
||||
|
||||
if (!validRatios[model as keyof typeof validRatios]?.includes(ratio)) {
|
||||
throw new Error(`Invalid resolution ${ratio} for model ${model}`);
|
||||
}
|
||||
|
||||
// Validate image position per model
|
||||
if (imagePosition === 'last' && model !== 'gen3a_turbo') {
|
||||
throw new Error('Last frame positioning is only supported by gen3a_turbo model');
|
||||
}
|
||||
|
||||
// Prepare image URL
|
||||
let imageUrl: string;
|
||||
if (hasFile) {
|
||||
const f = propsValue.promptImageFile as any;
|
||||
const ext = f?.extension || 'jpeg';
|
||||
imageUrl = `data:image/${ext};base64,${f?.base64}`;
|
||||
} else {
|
||||
imageUrl = propsValue.promptImageUrl as string;
|
||||
}
|
||||
|
||||
const apiKey = auth.secret_text;
|
||||
const client = new RunwayML({ apiKey });
|
||||
|
||||
// Build request body according to SDK specification
|
||||
const requestBody: any = {
|
||||
model: model,
|
||||
ratio: ratio,
|
||||
};
|
||||
|
||||
// Handle different image input formats based on position
|
||||
if (imagePosition === 'first' || model !== 'gen3a_turbo') {
|
||||
// Simple string format for first frame or non-gen3a_turbo models
|
||||
requestBody.promptImage = imageUrl;
|
||||
} else {
|
||||
// Array format for last frame positioning (gen3a_turbo only)
|
||||
requestBody.promptImage = [{
|
||||
uri: imageUrl,
|
||||
position: imagePosition
|
||||
}];
|
||||
}
|
||||
|
||||
// Add optional parameters
|
||||
if (duration) {
|
||||
requestBody.duration = duration;
|
||||
}
|
||||
|
||||
if (propsValue.promptText) {
|
||||
requestBody.promptText = propsValue.promptText;
|
||||
}
|
||||
|
||||
if (propsValue.seed !== undefined) {
|
||||
requestBody.seed = propsValue.seed;
|
||||
}
|
||||
|
||||
// Add content moderation only for supported models
|
||||
if (model !== 'veo3' && (propsValue.contentModeration as any)?.['publicFigureThreshold']) {
|
||||
requestBody.contentModeration = {
|
||||
publicFigureThreshold: (propsValue.contentModeration as any)['publicFigureThreshold']
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const task = await client.imageToVideo.create(requestBody);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
taskId: task.id,
|
||||
};
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to generate video: ${error.message || 'Unknown error'}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { AuthenticationType, httpClient, HttpMethod, propsValidation } from '@activepieces/pieces-common';
|
||||
import { runwayAuth } from '../common';
|
||||
import RunwayML from '@runwayml/sdk';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Helper function to get file extension from URL or Content-Type
|
||||
const getFileExtensionFromUrl = (url: string, contentType?: string): string => {
|
||||
// Try to get extension from URL first
|
||||
const urlExt = url.split('.').pop()?.split('?')[0]?.toLowerCase();
|
||||
if (urlExt && ['mp4', 'mov', 'avi', 'gif', 'webm', 'jpg', 'jpeg', 'png', 'webp'].includes(urlExt)) {
|
||||
return urlExt;
|
||||
}
|
||||
|
||||
// Fallback to content type
|
||||
if (contentType) {
|
||||
if (contentType.includes('video/mp4')) return 'mp4';
|
||||
if (contentType.includes('video/quicktime')) return 'mov';
|
||||
if (contentType.includes('video/webm')) return 'webm';
|
||||
if (contentType.includes('image/gif')) return 'gif';
|
||||
if (contentType.includes('image/jpeg')) return 'jpg';
|
||||
if (contentType.includes('image/png')) return 'png';
|
||||
if (contentType.includes('image/webp')) return 'webp';
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return 'mp4';
|
||||
};
|
||||
|
||||
const getStatusDescription = (status: string): string => {
|
||||
switch (status) {
|
||||
case 'PENDING': return 'Task is queued and waiting to start';
|
||||
case 'THROTTLED': return 'Task is waiting for other jobs to complete';
|
||||
case 'RUNNING': return 'Task is currently being processed';
|
||||
case 'SUCCEEDED': return 'Task completed successfully';
|
||||
case 'FAILED': return 'Task failed to complete';
|
||||
case 'CANCELLED': return 'Task was cancelled or aborted';
|
||||
default: return 'Unknown status';
|
||||
}
|
||||
};
|
||||
|
||||
export const getTaskDetails = createAction({
|
||||
auth: runwayAuth,
|
||||
name: 'get_task_details',
|
||||
displayName: 'Get Task Details',
|
||||
description: 'Retrieve details of an existing Runway task by its ID',
|
||||
props: {
|
||||
taskId: Property.ShortText({
|
||||
displayName: 'Task ID',
|
||||
description: 'The unique ID of the task to retrieve (UUID format)',
|
||||
required: true
|
||||
}),
|
||||
downloadOutput: Property.Checkbox({
|
||||
displayName: 'Download Output Files',
|
||||
description: 'Download and return the generated files as attachments (if task succeeded)',
|
||||
required: false,
|
||||
defaultValue: false
|
||||
}),
|
||||
},
|
||||
async run({ auth, propsValue, files }) {
|
||||
// Zod validation
|
||||
await propsValidation.validateZod(propsValue, {
|
||||
taskId: z.string().uuid('Task ID must be a valid UUID format'),
|
||||
});
|
||||
|
||||
const apiKey = auth.secret_text;
|
||||
const client = new RunwayML({ apiKey });
|
||||
|
||||
let task;
|
||||
try {
|
||||
task = await client.tasks.retrieve(propsValue.taskId);
|
||||
} catch (error: any) {
|
||||
if (error.status === 404) {
|
||||
throw new Error(`Task not found: ${propsValue.taskId}. Please verify the task ID is correct.`);
|
||||
}
|
||||
throw new Error(`Failed to retrieve task: ${error.message || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
const outputs = task.output || [];
|
||||
const artifacts: Array<any> = [];
|
||||
|
||||
// Download output files if requested and available
|
||||
if (propsValue.downloadOutput && outputs.length > 0) {
|
||||
if (task.status !== 'SUCCEEDED') {
|
||||
console.warn(`Task status is ${task.status}, but download was requested. Only succeeded tasks have downloadable outputs.`);
|
||||
} else {
|
||||
for (const [index, url] of outputs.entries()) {
|
||||
try {
|
||||
// First, make a HEAD request to get content type
|
||||
const headResponse = await httpClient.sendRequest({
|
||||
method: HttpMethod.HEAD,
|
||||
url,
|
||||
authentication: { type: AuthenticationType.BEARER_TOKEN, token: apiKey },
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
const contentType = headResponse.headers?.['content-type'] as string;
|
||||
const extension = getFileExtensionFromUrl(url, contentType);
|
||||
|
||||
// Now download the actual file
|
||||
const response = await httpClient.sendRequest<ArrayBuffer>({
|
||||
method: HttpMethod.GET,
|
||||
url,
|
||||
responseType: 'arraybuffer',
|
||||
authentication: { type: AuthenticationType.BEARER_TOKEN, token: apiKey },
|
||||
timeout: 300000, // 5 minutes for large video files
|
||||
});
|
||||
|
||||
const file = await files.write({
|
||||
fileName: `runway-output-${task.id}-${index + 1}.${extension}`,
|
||||
data: Buffer.from(response.body as any),
|
||||
});
|
||||
|
||||
artifacts.push({
|
||||
fileName: `runway-output-${task.id}-${index + 1}.${extension}`,
|
||||
fileUrl: file,
|
||||
contentType: contentType,
|
||||
originalUrl: url,
|
||||
index: index + 1
|
||||
});
|
||||
} catch (downloadError: any) {
|
||||
console.error(`Failed to download output ${index + 1}:`, downloadError.message);
|
||||
// Continue with other files even if one fails
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate completion percentage for display
|
||||
let completionPercentage = 0;
|
||||
if (task.status === 'SUCCEEDED') {
|
||||
completionPercentage = 100;
|
||||
} else if (task.status === 'RUNNING' && typeof task.progress === 'number') {
|
||||
completionPercentage = Math.round(task.progress * 100);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
taskId: task.id,
|
||||
status: task.status,
|
||||
statusDescription: getStatusDescription(task.status),
|
||||
createdAt: task.createdAt,
|
||||
completionPercentage,
|
||||
progress: task.progress,
|
||||
outputUrls: outputs,
|
||||
downloadedFiles: artifacts,
|
||||
hasOutputs: outputs.length > 0,
|
||||
isComplete: task.status === 'SUCCEEDED',
|
||||
isFailed: task.status === 'FAILED',
|
||||
isRunning: task.status === 'RUNNING',
|
||||
failureCode: task.failureCode,
|
||||
failureMessage: task.failure,
|
||||
// Summary for easy consumption
|
||||
summary: {
|
||||
id: task.id,
|
||||
status: task.status,
|
||||
created: task.createdAt,
|
||||
progress: completionPercentage,
|
||||
outputCount: outputs.length,
|
||||
downloadedCount: artifacts.length
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { PieceAuth } from '@activepieces/pieces-framework';
|
||||
|
||||
export const runwayAuth = PieceAuth.SecretText({
|
||||
displayName: 'API Key',
|
||||
description: 'Your Runway API key. Get it from your Runway account settings.',
|
||||
required: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export * from './auth';
|
||||
import { Property } from '@activepieces/pieces-framework';
|
||||
|
||||
export const runwayModelProperty = Property.ShortText({ displayName: 'Model', required: true });
|
||||
|
||||
export const runwayTaskIdProperty = Property.ShortText({
|
||||
displayName: 'Task ID',
|
||||
description: 'The UUID of the task to retrieve (copy from the task creation response)',
|
||||
required: true
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user