Added new example tasks (FFmpeg / Sharp / Vercel AI SDK) (#1312)

* Added FFmpeg / sharp / vercel and updated mint.json

* Amends including s3 -> r2
This commit is contained in:
Dan
2024-09-17 14:16:10 +01:00
committed by GitHub
parent 9a3eab5ac4
commit a84e601da8
5 changed files with 510 additions and 50 deletions
+329
View File
@@ -0,0 +1,329 @@
---
title: "Video processing with FFmpeg"
sidebarTitle: "FFmpeg video processing"
description: "These examples show you how to process videos in various ways using FFmpeg with Trigger.dev."
---
## Adding the FFmpeg build extension
To use these example tasks, you'll first need to add our FFmpeg extension to your project configuration like this:
```ts trigger.config.ts
import { ffmpeg } from "@trigger.dev/build/extensions/core";
import { defineConfig } from "@trigger.dev/sdk/v3";
export default defineConfig({
project: "<project ref>",
// Your other config settings...
build: {
extensions: [ffmpeg()],
},
});
```
<Note>
[Build extensions](../guides/build-extensions) allow you to hook into the build system and
customize the build process or the resulting bundle and container image (in the case of
deploying). You can use pre-built extensions or create your own.
</Note>
You'll also need to add `@trigger.dev/build` to your `package.json` file under `devDependencies` if you don't already have it there.
## Compress a video using FFmpeg
This task demonstrates how to use FFmpeg to compress a video, reducing its file size while maintaining reasonable quality, and upload the compressed video to R2 storage.
### Key Features:
- Fetches a video from a given URL
- Compresses the video using FFmpeg with various compression settings
- Uploads the compressed video to R2 storage
### Task code
```ts trigger/ffmpeg-compress-video.ts
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { logger, task } from "@trigger.dev/sdk/v3";
import ffmpeg from "fluent-ffmpeg";
import fs from "fs/promises";
import fetch from "node-fetch";
import { Readable } from "node:stream";
import os from "os";
import path from "path";
// Initialize S3 client
const s3Client = new S3Client({
// How to authenticate to R2: https://developers.cloudflare.com/r2/api/s3/tokens/
region: "auto",
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "",
},
});
export const ffmpegCompressVideo = task({
id: "ffmpeg-compress-video",
run: async (payload: { videoUrl: string }) => {
const { videoUrl } = payload;
// Generate temporary file names
const tempDirectory = os.tmpdir();
const outputPath = path.join(tempDirectory, `output_${Date.now()}.mp4`);
// Fetch the video
const response = await fetch(videoUrl);
// Compress the video
await new Promise((resolve, reject) => {
if (!response.body) {
return reject(new Error("Failed to fetch video"));
}
ffmpeg(Readable.from(response.body))
.outputOptions([
"-c:v libx264", // Use H.264 codec
"-crf 28", // Higher CRF for more compression (28 is near the upper limit for acceptable quality)
"-preset veryslow", // Slowest preset for best compression
"-vf scale=iw/2:ih/2", // Reduce resolution to 320p width (height auto-calculated)
"-c:a aac", // Use AAC for audio
"-b:a 64k", // Reduce audio bitrate to 64k
"-ac 1", // Convert to mono audio
])
.output(outputPath)
.on("end", resolve)
.on("error", reject)
.run();
});
// Read the compressed video
const compressedVideo = await fs.readFile(outputPath);
const compressedSize = compressedVideo.length;
// Log compression results
logger.log(`Compressed video size: ${compressedSize} bytes`);
logger.log(`Compressed video saved at: ${outputPath}`);
// Upload the compressed video to S3, replacing slashes with underscores
const r2Key = `processed-videos/${path.basename(outputPath)}`;
const uploadParams = {
Bucket: process.env.R2_BUCKET,
Key: r2Key,
Body: compressedVideo,
};
// Upload the video to R2 and get the URL
await s3Client.send(new PutObjectCommand(uploadParams));
const r2Url = `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET}/${r2Key}`;
logger.log("Compressed video uploaded to R2", { url: r2Url });
// Delete the temporary compressed video file
await fs.unlink(outputPath);
// Return the compressed video file path, compressed size, and S3 URL
return {
compressedVideoPath: outputPath,
compressedSize,
r2Url,
};
},
});
```
## Extract audio from a video using FFmpeg
This task demonstrates how to use FFmpeg to extract audio from a video, convert it to WAV format, and upload it to R2 storage.
### Key Features:
- Fetches a video from a given URL
- Extracts the audio from the video using FFmpeg
- Converts the extracted audio to WAV format
- Uploads the extracted audio to R2 storage
### Task code
<Warning>
When testing, make sure to provide a video URL that contains audio. If the video does not have
audio, the task will fail.
</Warning>
```ts trigger/ffmpeg-extract-audio.ts
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { logger, task } from "@trigger.dev/sdk/v3";
import ffmpeg from "fluent-ffmpeg";
import fs from "fs/promises";
import fetch from "node-fetch";
import { Readable } from "node:stream";
import os from "os";
import path from "path";
// Initialize S3 client
const s3Client = new S3Client({
// How to authenticate to R2: https://developers.cloudflare.com/r2/api/s3/tokens/
region: "auto",
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "",
},
});
export const ffmpegExtractAudio = task({
id: "ffmpeg-extract-audio",
run: async (payload: { videoUrl: string }) => {
const { videoUrl } = payload;
// Generate temporary and output file names
const tempDirectory = os.tmpdir();
const outputPath = path.join(tempDirectory, `output_${Date.now()}.wav`);
// Fetch the video
const response = await fetch(videoUrl);
// Convert the video to WAV
await new Promise((resolve, reject) => {
if (!response.body) {
return reject(new Error("Failed to fetch video"));
}
ffmpeg(Readable.from(response.body))
.toFormat("wav")
.save(outputPath)
.on("end", () => {
logger.log(`WAV file saved to ${outputPath}`);
resolve(outputPath);
})
.on("error", (err) => {
reject(err);
});
});
// Read the WAV file
const wavBuffer = await fs.readFile(outputPath);
// Log the output file path
logger.log(`Converted video saved at: ${outputPath}`);
// Upload the compressed video to S3, replacing slashes with underscores
const r2Key = `processed-audio/${path.basename(outputPath)}`;
const uploadParams = {
Bucket: process.env.R2_BUCKET,
Key: r2Key,
Body: wavBuffer,
};
// Upload the audio to R2 and get the URL
await s3Client.send(new PutObjectCommand(uploadParams));
const r2Url = `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET}/${r2Key}`;
logger.log("Extracted audio uploaded to R2", { url: r2Url });
// Delete the temporary file
await fs.unlink(outputPath);
// Return the WAV buffer and file path
return {
wavBuffer,
wavFilePath: outputPath,
r2Url,
};
},
});
```
## Generate a thumbnail from a video using FFmpeg
This task demonstrates how to use FFmpeg to generate a thumbnail from a video at a specific time and upload the generated thumbnail to R2 storage.
### Key Features:
- Fetches a video from a given URL
- Generates a thumbnail from the video at the 5-second mark
- Uploads the generated thumbnail to R2 storage
### Task code
```ts trigger/ffmpeg-generate-thumbnail.ts
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { logger, task } from "@trigger.dev/sdk/v3";
import ffmpeg from "fluent-ffmpeg";
import fs from "fs/promises";
import fetch from "node-fetch";
import { Readable } from "node:stream";
import os from "os";
import path from "path";
// Initialize S3 client
const s3Client = new S3Client({
// How to authenticate to R2: https://developers.cloudflare.com/r2/api/s3/tokens/
region: "auto",
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "",
},
});
export const ffmpegGenerateThumbnail = task({
id: "ffmpeg-generate-thumbnail",
run: async (payload: { videoUrl: string }) => {
const { videoUrl } = payload;
// Generate output file name
const tempDirectory = os.tmpdir();
const outputPath = path.join(tempDirectory, `thumbnail_${Date.now()}.jpg`);
// Fetch the video
const response = await fetch(videoUrl);
// Generate the thumbnail
await new Promise((resolve, reject) => {
if (!response.body) {
return reject(new Error("Failed to fetch video"));
}
ffmpeg(Readable.from(response.body))
.screenshots({
count: 1,
folder: "/tmp",
filename: path.basename(outputPath),
size: "320x240",
timemarks: ["5"], // 5 seconds
})
.on("end", resolve)
.on("error", reject);
});
// Read the generated thumbnail
const thumbnail = await fs.readFile(outputPath);
// Upload the compressed video to S3, replacing slashes with underscores
const r2Key = `thumbnails/${path.basename(outputPath)}`;
const uploadParams = {
Bucket: process.env.R2_BUCKET,
Key: r2Key,
Body: thumbnail,
};
// Upload the thumbnail to R2 and get the URL
await s3Client.send(new PutObjectCommand(uploadParams));
const r2Url = `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET}/${r2Key}`;
logger.log("Thumbnail uploaded to R2", { url: r2Url });
// Delete the temporary file
await fs.unlink(outputPath);
// Log thumbnail generation results
logger.log(`Thumbnail uploaded to S3: ${r2Url}`);
// Return the thumbnail buffer, file path, sizes, and S3 URL
return {
thumbnailBuffer: thumbnail,
thumbnailPath: outputPath,
r2Url,
};
},
});
```
+121
View File
@@ -0,0 +1,121 @@
---
title: "Process images using Sharp"
sidebarTitle: "Sharp image processing"
description: "This example demonstrates how to process images using the Sharp library with Trigger.dev."
---
## Overview
This task optimizes and watermarks an image using the Sharp library, and then uploads the processed image to R2 storage.
## Adding build configurations
To use this example, you'll first need to add these build settings to your `trigger.config.ts` file:
```ts trigger.config.ts
import { defineConfig } from "@trigger.dev/sdk/v3";
export default defineConfig({
project: "<project ref>",
// Your other config settings...
build: {
// This is required to use the Sharp library
external: ["sharp"],
},
});
```
<Note>
Any packages that install or build a native binary should be added to external, as native binaries
cannot be bundled.
</Note>
## Key features
- Resizes and rotates an image
- Adds a watermark to the image
- Uploads the processed image to R2 storage
## Task code
```ts trigger/sharp-image-processing.ts
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { logger, task } from "@trigger.dev/sdk/v3";
import fs from "fs/promises";
import fetch from "node-fetch";
import os from "os";
import path from "path";
import sharp from "sharp";
// Initialize R2 client
const r2Client = new S3Client({
region: "auto",
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "",
},
});
export const sharpProcessImage = task({
id: "sharp-process-image",
run: async (payload: { imageUrl: string; watermarkUrl: string }) => {
const { imageUrl, watermarkUrl } = payload;
// Generate temporary and output file names
const tempDirectory = os.tmpdir();
const outputPath = path.join(tempDirectory, `output_${Date.now()}.jpg`);
// Fetch the image and watermark
const [imageResponse, watermarkResponse] = await Promise.all([
fetch(imageUrl),
fetch(watermarkUrl),
]);
const imageBuffer = await imageResponse.arrayBuffer();
const watermarkBuffer = await watermarkResponse.arrayBuffer();
// Optimize the image using Sharp
await sharp(Buffer.from(imageBuffer))
.rotate(90) // Rotate the image by 90 degrees
.resize(800, 600) // Resize the image to 800x600
.composite([
{
input: Buffer.from(watermarkBuffer),
gravity: "southeast", // Position the watermark in the bottom-right corner
},
])
.toFormat("jpeg")
.toFile(outputPath);
// Log the output file path
logger.log(`Optimized image saved at: ${outputPath}`);
// Read the optimized image file
const optimizedImageBuffer = await fs.readFile(outputPath);
// Upload the optimized image to R2, replacing slashes with underscores
const r2Key = `processed-images/${path.basename(outputPath)}`;
const uploadParams = {
Bucket: process.env.R2_BUCKET,
Key: r2Key,
Body: optimizedImageBuffer,
};
// Upload the image to R2 and get the URL
await r2Client.send(new PutObjectCommand(uploadParams));
const r2Url = `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET}/${r2Key}`;
logger.log("Optimized image uploaded to R2", { url: r2Url });
// Delete the temporary file
await fs.unlink(outputPath);
// Return the optimized image buffer, file path, and R2 URL
return {
optimizedImageBuffer,
optimizedImagePath: outputPath,
r2Url,
};
},
});
```
+43
View File
@@ -0,0 +1,43 @@
---
title: "Using the Vercel AI SDK"
sidebarTitle: "Vercel AI SDK"
description: "This example demonstrates how to use the Vercel AI SDK with Trigger.dev."
---
## Overview
The [Vercel AI SDK](https://www.npmjs.com/package/ai) is a simple way to use AI models from many different providers, including OpenAI, Microsoft Azure, Google Generative AI, Anthropic, Amazon Bedrock, Groq, Perplexity and [more](https://sdk.vercel.ai/providers/ai-sdk-providers).
It provides a consistent interface to interact with the different AI models, so you can easily switch between them without needing to change your code.
## Generate text using OpenAI
This task shows how to use the Vercel AI SDK to generate text from a prompt with OpenAI.
### Task code
```ts trigger/vercel-ai-sdk-openai.ts
import { logger, task } from "@trigger.dev/sdk/v3";
import { generateText } from "ai";
// Install the package of the AI model you want to use, in this case OpenAI
import { openai } from "@ai-sdk/openai"; // Ensure OPENAI_API_KEY environment variable is set
export const openaiTask = task({
id: "openai-text-generate",
run: async (payload: { prompt: string }) => {
const chatCompletion = await generateText({
model: openai("gpt-4-turbo"),
// Add a system message which will be included with the prompt
system: "You are a friendly assistant!",
// The prompt passed in from the payload
prompt: payload.prompt,
});
// Log the generated text
logger.log("chatCompletion text:" + chatCompletion.text);
return chatCompletion;
},
});
```
+17 -50
View File
@@ -1,10 +1,7 @@
{
"$schema": "https://mintlify.com/schema.json",
"name": "Trigger.dev",
"openapi": [
"/openapi.yml",
"/v3-openapi.yaml"
],
"openapi": ["/openapi.yml", "/v3-openapi.yaml"],
"api": {
"playground": {
"mode": "simple"
@@ -103,23 +100,14 @@
"navigation": [
{
"group": "Getting Started",
"pages": [
"introduction",
"quick-start",
"how-it-works",
"upgrading-beta",
"limits"
]
"pages": ["introduction", "quick-start", "how-it-works", "upgrading-beta", "limits"]
},
{
"group": "Fundamentals",
"pages": [
{
"group": "Tasks",
"pages": [
"tasks/overview",
"tasks/scheduled"
]
"pages": ["tasks/overview", "tasks/scheduled"]
},
"triggering",
"apikeys",
@@ -128,10 +116,7 @@
},
{
"group": "Development",
"pages": [
"cli-dev",
"run-tests"
]
"pages": ["cli-dev", "run-tests"]
},
{
"group": "Deployment",
@@ -141,9 +126,7 @@
"github-actions",
{
"group": "Deployment integrations",
"pages": [
"vercel-integration"
]
"pages": ["vercel-integration"]
}
]
},
@@ -155,13 +138,7 @@
"errors-retrying",
{
"group": "Wait",
"pages": [
"wait",
"wait-for",
"wait-until",
"wait-for-event",
"wait-for-request"
]
"pages": ["wait", "wait-for", "wait-until", "wait-for-event", "wait-for-request"]
},
"queue-concurrency",
"versioning",
@@ -179,10 +156,7 @@
"management/overview",
{
"group": "Tasks API",
"pages": [
"management/tasks/trigger",
"management/tasks/batch-trigger"
]
"pages": ["management/tasks/trigger", "management/tasks/batch-trigger"]
},
{
"group": "Runs API",
@@ -220,9 +194,7 @@
},
{
"group": "Projects API",
"pages": [
"management/projects/runs"
]
"pages": ["management/projects/runs"]
}
]
},
@@ -268,11 +240,7 @@
},
{
"group": "Help",
"pages": [
"community",
"help-slack",
"help-email"
]
"pages": ["community", "help-slack", "help-email"]
},
{
"group": "Frameworks",
@@ -294,23 +262,22 @@
},
{
"group": "Dashboard",
"pages": [
"guides/dashboard/creating-a-project"
]
"pages": ["guides/dashboard/creating-a-project"]
},
{
"group": "Migrations",
"pages": [
"guides/use-cases/upgrading-from-v2"
]
"pages": ["guides/use-cases/upgrading-from-v2"]
},
{
"group": "Examples",
"pages": [
"examples/generate-image-with-dall-e3",
"examples/dall-e3-generate-image",
"examples/ffmpeg-video-processing",
"examples/open-ai-with-retrying",
"examples/sharp-image-processing",
"examples/react-pdf",
"examples/resend-email-sequence"
"examples/resend-email-sequence",
"examples/vercel-ai-sdk"
]
}
],
@@ -319,4 +286,4 @@
"github": "https://github.com/triggerdotdev",
"linkedin": "https://www.linkedin.com/company/triggerdotdev"
}
}
}