From 8df17787c19d961e852f1450e0832eaca25a708f Mon Sep 17 00:00:00 2001 From: D-K-P <8297864+D-K-P@users.noreply.github.com> Date: Thu, 3 Oct 2024 14:15:39 -0700 Subject: [PATCH 01/18] Added overview for guides and examples section and split them all out --- docs/guides/examples/intro.mdx | 17 +++-------------- docs/guides/frameworks/introduction.mdx | 14 ++------------ docs/guides/overview.mdx | 21 +++++++++++++++++++++ docs/mint.json | 7 ++++++- docs/snippets/intro-examples.mdx | 15 +++++++++++++++ docs/snippets/intro-frameworks.mdx | 13 +++++++++++++ docs/snippets/intro-guides.mdx | 6 ++++++ 7 files changed, 66 insertions(+), 27 deletions(-) create mode 100644 docs/guides/overview.mdx create mode 100644 docs/snippets/intro-examples.mdx create mode 100644 docs/snippets/intro-frameworks.mdx create mode 100644 docs/snippets/intro-guides.mdx diff --git a/docs/guides/examples/intro.mdx b/docs/guides/examples/intro.mdx index fe41b1f3bb..65a1f3d727 100644 --- a/docs/guides/examples/intro.mdx +++ b/docs/guides/examples/intro.mdx @@ -4,17 +4,6 @@ sidebarTitle: "Introduction" description: "Learn how to use Trigger.dev with these practical task examples." --- -| Example task | Description | -| :---------------------------------------------------------------------------- | :----------------------------------------------------------------------------- | -| [DALL·E 3 image generation](/guides/examples/dall-e3-generate-image) | Use OpenAI's GPT-4o and DALL·E 3 to generate an image and text. | -| [FFmpeg video processing](/guides/examples/ffmpeg-video-processing) | Use FFmpeg to process a video in various ways and save it to Cloudflare R2. | -| [OpenAI with retrying](/guides/examples/open-ai-with-retrying) | Create a reusable OpenAI task with custom retry options. | -| [PDF to image](/guides/examples/pdf-to-image) | Use `MuPDF` to turn a PDF into images and save them to Cloudflare R2. | -| [React to PDF](/guides/examples/react-pdf) | Use `react-pdf` to generate a PDF and save it to Cloudflare R2. | -| [Puppeteer](/guides/examples/puppeteer) | Use Puppeteer to generate a PDF or scrape a webpage. | -| [Resend email sequence](/guides/examples/resend-email-sequence) | Send a sequence of emails over several days using Resend with Trigger.dev. | -| [Sharp image processing](/guides/examples/sharp-image-processing) | Use Sharp to process an image and save it to Cloudflare R2. | -| [Stripe webhook](/guides/examples/stripe-webhook) | Trigger a task from Stripe webhook events. | -| [Supabase database operations](/guides/examples/supabase-database-operations) | Run basic CRUD operations on a table in a Supabase database using Trigger.dev. | -| [Supabase Storage upload](/guides/examples/supabase-storage-upload) | Download a video from a URL and upload it to Supabase Storage using S3. | -| [Vercel AI SDK](/guides/examples/vercel-ai-sdk) | Use Vercel AI SDK to generate text using OpenAI. | +import IntroExamples from "/snippets/intro-examples.mdx"; + + diff --git a/docs/guides/frameworks/introduction.mdx b/docs/guides/frameworks/introduction.mdx index eb74771e78..34421c85e2 100644 --- a/docs/guides/frameworks/introduction.mdx +++ b/docs/guides/frameworks/introduction.mdx @@ -5,16 +5,6 @@ description: "Get started with Trigger.dev in your favorite framework." icon: "grid-2" --- -import CardBun from "/snippets/card-bun.mdx"; -import CardNodejs from "/snippets/card-nodejs.mdx"; -import CardNextjs from "/snippets/card-nextjs.mdx"; -import CardRemix from "/snippets/card-remix.mdx"; -import CardSupabase from "/snippets/card-supabase.mdx"; +import IntroFrameworks from "/snippets/intro-frameworks.mdx"; - - - - - - - + diff --git a/docs/guides/overview.mdx b/docs/guides/overview.mdx new file mode 100644 index 0000000000..c8778eb457 --- /dev/null +++ b/docs/guides/overview.mdx @@ -0,0 +1,21 @@ +--- +title: "Frameworks, guides and examples overview" +sidebarTitle: "Introduction" +description: "An ever growing list of guides and examples to help you get setup with Trigger.dev." +--- + +import IntroFrameworks from "/snippets/intro-frameworks.mdx"; +import IntroGuides from "/snippets/intro-guides.mdx"; +import IntroExamples from "/snippets/intro-examples.mdx"; + +## Frameworks + + + +## Guides + + + +## Examples + + diff --git a/docs/mint.json b/docs/mint.json index b6312b9a32..a24d1aa968 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -250,6 +250,10 @@ "group": "Help", "pages": ["community", "help-slack", "help-email"] }, + { + "group": "Overview", + "pages": ["guides/overview"] + }, { "group": "Frameworks", "pages": [ @@ -265,7 +269,8 @@ "pages": [ "guides/frameworks/supabase-guides-overview", "guides/frameworks/supabase-edge-functions-basic", - "guides/frameworks/supabase-edge-functions-database-webhooks" + "guides/frameworks/supabase-edge-functions-database-webhooks", + "guides/frameworks/supabase-edge-functions-ffmpeg-deepgram" ] } ] diff --git a/docs/snippets/intro-examples.mdx b/docs/snippets/intro-examples.mdx new file mode 100644 index 0000000000..883ed517fe --- /dev/null +++ b/docs/snippets/intro-examples.mdx @@ -0,0 +1,15 @@ +| Example task | Description | +| :---------------------------------------------------------------------------- | :----------------------------------------------------------------------------- | +| [DALL·E 3 image generation](/guides/examples/dall-e3-generate-image) | Use OpenAI's GPT-4o and DALL·E 3 to generate an image and text. | +| [Deepgram audio transcription](/guides/examples/deepgram-transcribe-audio) | Transcribe audio using Deepgram's speech recognition API. | +| [FFmpeg video processing](/guides/examples/ffmpeg-video-processing) | Use FFmpeg to process a video in various ways and save it to Cloudflare R2. | +| [OpenAI with retrying](/guides/examples/open-ai-with-retrying) | Create a reusable OpenAI task with custom retry options. | +| [PDF to image](/guides/examples/pdf-to-image) | Use `MuPDF` to turn a PDF into images and save them to Cloudflare R2. | +| [React to PDF](/guides/examples/react-pdf) | Use `react-pdf` to generate a PDF and save it to Cloudflare R2. | +| [Puppeteer](/guides/examples/puppeteer) | Use Puppeteer to generate a PDF or scrape a webpage. | +| [Resend email sequence](/guides/examples/resend-email-sequence) | Send a sequence of emails over several days using Resend with Trigger.dev. | +| [Sharp image processing](/guides/examples/sharp-image-processing) | Use Sharp to process an image and save it to Cloudflare R2. | +| [Stripe webhook](/guides/examples/stripe-webhook) | Trigger a task from Stripe webhook events. | +| [Supabase database operations](/guides/examples/supabase-database-operations) | Run basic CRUD operations on a table in a Supabase database using Trigger.dev. | +| [Supabase Storage upload](/guides/examples/supabase-storage-upload) | Download a video from a URL and upload it to Supabase Storage using S3. | +| [Vercel AI SDK](/guides/examples/vercel-ai-sdk) | Use Vercel AI SDK to generate text using OpenAI. | diff --git a/docs/snippets/intro-frameworks.mdx b/docs/snippets/intro-frameworks.mdx new file mode 100644 index 0000000000..8241396355 --- /dev/null +++ b/docs/snippets/intro-frameworks.mdx @@ -0,0 +1,13 @@ +import CardBun from "/snippets/card-bun.mdx"; +import CardNodejs from "/snippets/card-nodejs.mdx"; +import CardNextjs from "/snippets/card-nextjs.mdx"; +import CardRemix from "/snippets/card-remix.mdx"; +import CardSupabase from "/snippets/card-supabase.mdx"; + + + + + + + + diff --git a/docs/snippets/intro-guides.mdx b/docs/snippets/intro-guides.mdx new file mode 100644 index 0000000000..fad8f16816 --- /dev/null +++ b/docs/snippets/intro-guides.mdx @@ -0,0 +1,6 @@ +Get set up fast using our detailed walk-through guides. + +| Guide | Description | +| :---------------------------------------------------- | :------------------------------------------------------------------------------- | +| [Prisma](/guides/frameworks/prisma) | This guide will show you how to setup Prisma with Trigger.dev | +| [Sequin database triggers](/guides/frameworks/sequin) | This guide will show you how to trigger tasks from database changes using Sequin | From 45c27c1589e87a2f49002f2a7cdf2b49612af543 Mon Sep 17 00:00:00 2001 From: D-K-P <8297864+D-K-P@users.noreply.github.com> Date: Thu, 3 Oct 2024 14:15:52 -0700 Subject: [PATCH 02/18] New supabase guide wip --- ...abase-edge-functions-database-webhooks.mdx | 2 +- ...upabase-edge-functions-ffmpeg-deepgram.mdx | 464 ++++++++++++++++++ 2 files changed, 465 insertions(+), 1 deletion(-) create mode 100644 docs/guides/frameworks/supabase-edge-functions-ffmpeg-deepgram.mdx diff --git a/docs/guides/frameworks/supabase-edge-functions-database-webhooks.mdx b/docs/guides/frameworks/supabase-edge-functions-database-webhooks.mdx index a9986a576c..0c8f1492c3 100644 --- a/docs/guides/frameworks/supabase-edge-functions-database-webhooks.mdx +++ b/docs/guides/frameworks/supabase-edge-functions-database-webhooks.mdx @@ -21,7 +21,7 @@ import SupabaseDocsCards from "/snippets/supabase-docs-cards.mdx"; Database webhooks allow you to send realtime data from your database to another system whenever an event occurs in your table e.g. when a row is inserted, updated, or deleted, or when a specific column is updated. -This guide shows you how to set up a Supabase database webhook and deploy a simple edge function that triggers a "Hello world" task every time a new row is inserted into your table. +This guide shows you how to set up a Supabase database webhook and deploy a simple edge function that triggers a "Hello world" task every time a new row is inserted into the original row in your table. ## Prerequisites diff --git a/docs/guides/frameworks/supabase-edge-functions-ffmpeg-deepgram.mdx b/docs/guides/frameworks/supabase-edge-functions-ffmpeg-deepgram.mdx new file mode 100644 index 0000000000..52289adc03 --- /dev/null +++ b/docs/guides/frameworks/supabase-edge-functions-ffmpeg-deepgram.mdx @@ -0,0 +1,464 @@ +--- +title: "Triggering tasks from Supabase Database Webhooks" +sidebarTitle: "Database webhooks" +description: "This guide shows you how to trigger a task when a row is added to a table, using a Supabase Database Webhook and Edge Function." +--- + +import Prerequisites from "/snippets/framework-prerequisites.mdx"; +import SupabasePrerequisites from "/snippets/supabase-prerequisites.mdx"; +import CliInitStep from "/snippets/step-cli-init.mdx"; +import CliDevStep from "/snippets/step-cli-dev.mdx"; +import CliRunTestStep from "/snippets/step-run-test.mdx"; +import CliViewRunStep from "/snippets/step-view-run.mdx"; +import UsefulNextSteps from "/snippets/useful-next-steps.mdx"; +import TriggerTaskNextjs from "/snippets/trigger-tasks-nextjs.mdx"; +import NextjsTroubleshootingMissingApiKey from "/snippets/nextjs-missing-api-key.mdx"; +import NextjsTroubleshootingButtonSyntax from "/snippets/nextjs-button-syntax.mdx"; +import WorkerFailedToStartWhenRunningDevCommand from "/snippets/worker-failed-to-start.mdx"; +import SupabaseDocsCards from "/snippets/supabase-docs-cards.mdx"; + +## Overview + +Generate a transcription from a video URL using [Supabase](https://supabase.com), [FFmpeg](https://www.ffmpeg.org/) and [Deepgram](https://deepgram.com) and Trigger.dev. + +In this walk-through guide you will learn how to set up and deploy a Supabase Edge Function that is triggered by an `insert` row action in a table via a Database Webhook. + +The Edge Function triggers a deployed Trigger.dev task which takes a payload from the new inserted data from the table. This is then processed using FFmpeg and Deepgram. The resulting string is then `updated` back into the original table row in Supabase. + +### Key features + +- Configuring a Supabase Database Webhook which triggers an Edge Function on an `insert` action on a table +- A Supabase Edge Function that triggers a Trigger.dev task, which uses the data from the `video_url` column as the payload. +- A multi-step Trigger.dev task example which: + - Uses FFmpeg to extract the audio track from a public video URL + - Uses Deepgram to transcribe the audio + - Adds the transcription back to your Supabase table in the correct row, using `update` + +## Prerequisites + +- Ensure you have the [Supabase CLI](https://supabase.com/docs/guides/cli/getting-started) installed +- Ensure TypeScript is installed +- [Create a Trigger.dev account](https://cloud.trigger.dev) +- [Create a new Trigger.dev project](/guides/dashboard/creating-a-project) +- [Create a new Deepgram account](https://deepgram.com/) + +## Initial setup + + + + + + + + + +## Create and deploy the video processing Trigger.dev task + +Before setting up your Edge Function and Database Webhook, you'll first need to create your Trigger.dev task. This can be tested independently from the rest of the setup. + +Create a new task file in your `/trigger` folder (the same place your example task is). Call it `videoProcessAndUpdate.ts`. + +This task with take a video url, extract the audio using FFmpeg and transcribe the audio using Deepgram. We will add the Supabase `update` step further on in the tutorial. + +```ts /trigger/videoProcessAndUpdate.ts +import { createClient as createDeepgramClient } from "@deepgram/sdk"; +import { logger, task } from "@trigger.dev/sdk/v3"; +import ffmpeg from "fluent-ffmpeg"; +import fs from "fs"; +import fetch from "node-fetch"; +import { Readable } from "node:stream"; +import os from "os"; +import path from "path"; +import { Database } from "../../database.types"; + +const deepgram = createDeepgramClient(process.env.DEEPGRAM_SECRET_KEY); + +export const videoProcessAndUpdate = task({ + id: "video-process-and-update", + run: async (payload: { videoUrl: string }) => { + const { videoUrl } = payload; + + logger.log(`Processing video at URL: ${videoUrl}`); + + // Generate temporary file video_urls + const tempDirectory = os.tmpdir(); + const outputPath = path.join(tempDirectory, `audio_${Date.now()}.wav`); + + // Fetch the video + const response = await fetch(videoUrl); + + // Extract the audio + await new Promise((resolve, reject) => { + if (!response.body) { + return reject(new Error("Failed to fetch video")); + } + + ffmpeg(Readable.from(response.body)) + .outputOptions([ + "-vn", // Disable video output + "-acodec pcm_s16le", // Use PCM 16-bit little-endian encoding + "-ar 44100", // Set audio sample rate to 44.1 kHz + "-ac 2", // Set audio channels to stereo + ]) + .output(outputPath) + .on("end", resolve) + .on("error", reject) + .run(); + }); + + logger.log(`Audio extracted from video`, { outputPath }); + + // Transcribe the audio using Deepgram + const { result, error } = await deepgram.listen.prerecorded.transcribeFile( + fs.readFileSync(outputPath), + { + model: "nova-2", + smart_format: true, + diarize: true, + } + ); + + if (error) { + throw error; + } + + console.dir(result, { depth: null }); + + // Convert the result object to a string + const transcription = result.results.channels[0].alternatives[0].paragraphs?.transcript; + + logger.log(`Transcription: ${transcription}`); + + // Delete the temporary audio file + fs.unlinkSync(outputPath); + logger.log(`Temporary audio file deleted`, { outputPath }); + + return { + message: `Summary of the audio: ${transcription}`, + result, + }; + }, +}); +``` + +### Adding the FFmpeg build extension + + + {" "} + This task can also be tested in `dev` without adding the build extension, but we recommend adding the** + ** extension during the setup process.{" "} + + +Before you can deploy and test the task, 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: "", + // Your other config settings... + build: { + extensions: [ffmpeg()], + }, +}); +``` + + + [Build extensions](/config/config-file#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. + + +You'll also need to add `@trigger.dev/build` to your `package.json` file under `devDependencies` if you don't already have it there. + +### Deploying your task + +You can now deploy your task and test it in the dashboard. + + + +```bash npm +npx trigger.dev@latest deploy +``` + +```bash pnpm +pnpm dlx trigger.dev@latest deploy +``` + +```bash yarn +yarn dlx trigger.dev@latest deploy +``` + + + +### Testing your task + +To test this task in the dashboard, you can use the following payload: + +```json +{ + "audioUrl": "https://dpgr.am/spacewalk.wav" +} +``` + +Congratulations, You should now see the video transcription in a successful run. + +## Create and deploy the Edge Function and configure the Database Webhook + +### Add your Trigger.dev prod secret key to the Supabase dashboard + +First, go to your Trigger.dev [project dashboard](https://cloud.trigger.dev) and copy the `prod` secret key from the API keys page. + +![How to find your prod secret key](/images/api-key-prod.png) + +Then, in [Supabase](https://supabase.com/dashboard/projects), select the project you want to use, navigate to 'Project settings' , click 'Edge Functions' in the configurations menu, and then click the 'Add new secret' button. + +Add `TRIGGER_SECRET_KEY` with the pasted value of your Trigger.dev `prod` secret key. + +![Add secret key in Supabase](/images/supabase-keys-1.png) + +### Create a new Edge Function using the Supabase CLI + +Now create a new Edge Function using the Supabase CLI. We will call it `video-processing-handler`. This will trigger our task using the data received from the Database Webhook. + +```bash +supabase functions new video-processing-handler +``` + +Replace the `video-processing-handler` placeholder code with the following code: + +```ts functions/video-processing-handler/index.ts +// Setup type definitions for built-in Supabase Runtime APIs +import "jsr:@supabase/functions-js/edge-runtime.d.ts"; +// Import the Trigger.dev SDK - replace "" with the version of the SDK you are using, e.g. "3.0.0". You can find this in your package.json file. +import { tasks } from "npm:@trigger.dev/sdk@3.0.0-beta.55/v3"; +import type { videoProcessAndUpdate } from "../../../src/trigger/video-process-and-update.ts"; +// 👆 **type-only** import + +// Sets up a Deno server that listens for incoming JSON requests +Deno.serve(async (req) => { + const payload = await req.json(); + + const videoUrl = payload.record.video_url; + + await tasks.trigger("video-process-and-update", { videoUrl }); + console.log(payload ?? "No video_url provided"); + + return new Response("ok"); +}); +``` + +You can only import the `type` from the task. + + Tasks in the `trigger` folder use Node, so they must stay in there or they will not run, + especially if you are using a different runtime like Deno. Also do not add "`npm:`" to imports + inside your task files, for the same reason. + + +### Deploy the Edge Function + +Now deploy your new Edge Function with the following command: + +```bash +supabase functions deploy video-processing-handler +``` + +Follow the CLI instructions, selecting the same project you added your `prod` secret key to, and once complete you should see your new Edge Function deployment in your Supabase Edge Functions dashboard. + +There will be a link to the dashboard in your terminal output, or you can find it at this URL: + +`https://supabase.com/dashboard/project//functions` + +Replace `` with your actual project ID. + +### Create a new table in your Supabase dashboard + +Next, in your Supabase project dashboard, click on 'Table Editor' in the left-hand menu and create a new table. + +![How to create a new table](/images/supabase-new-table-1.png) + +In this example we will call our table `video_transcriptions`. + +Add two new columns, one called `video_url` with the type `text`, and another called `transcription`, also with the type `text`. + + REPLACE IMAGE BELOW +![How to add a new column](/images/supabase-new-table-2.png) + +### Create a new Database Webhook + +First, go to your Supabase project dashboard, click 'Project settings' , then the 'API' tab , and copy the `anon` `public` API key from the table . + +![How to find your Supabase API keys](/images/supabase-api-key.png) + +Then, go to 'Database' click on 'Webhooks' , and then click 'Create a new hook' . + +![How to create a new webhook](/images/supabase-create-webhook-1.png) + + Call the hook `edge-function-hook`. + + Select the new table you have created: +`public` `video_transcriptions`. + + Choose the `insert` event. + +![How to create a new webhook 2](/images/supabase-create-webhook-2.png) + + Under 'Webhook configuration', select +'Supabase Edge Functions'{" "} + + Under 'Edge Function', choose `POST` +and select the Edge Function you have created: `video-processing-handler`.{" "} + + Under 'HTTP Headers', add a new header with the key `Authorization` and the value `Bearer ` (replace `` with the `anon` `public` API key you copied earlier). + + + Supabase Edge Functions require a JSON Web Token [JWT](https://supabase.com/docs/guides/auth/jwts) + in the authorization header. This is to ensure that only authorized users can access your edge + functions. + + + Click 'Create webhook'.{" "} + +![How to create a new webhook 3](/images/supabase-create-webhook-3.png) + +Your Database Webhook is now ready to use. + +### Triggering the task + +### Enabling updates to your supabase tables + +First, you must go back to your task code and add in the Supabase logic: + +- Creating a Supabase client (you must update your environment variables in order for this to work) +- Function to update the table row with the exact match of the `video_url` payload with the new transcription + +```ts /trigger/videoProcessAndUpdate.ts +import { createClient as createDeepgramClient } from "@deepgram/sdk"; +import { createClient as createSupabaseClient } from "@supabase/supabase-js"; +import { logger, task } from "@trigger.dev/sdk/v3"; +import ffmpeg from "fluent-ffmpeg"; +import fs from "fs"; +import fetch from "node-fetch"; +import { Readable } from "node:stream"; +import os from "os"; +import path from "path"; +import { Database } from "../../database.types"; + +// Create a single Supabase client for interacting with your database +// 'Database' supplies the type definitions to supabase-js +const supabase = createSupabaseClient( + // These details can be found in your Supabase project settings under `API` + process.env.SUPABASE_PROJECT_URL as string, // e.g. https://abc123.supabase.co - replace 'abc123' with your project ID + process.env.SUPABASE_SERVICE_ROLE_KEY as string // Your service role secret key +); + +const deepgram = createDeepgramClient(process.env.DEEPGRAM_SECRET_KEY); + +export const videoProcessAndUpdate = task({ + id: "video-process-and-update", + run: async (payload: { videoUrl: string }) => { + const { videoUrl } = payload; + + logger.log(`Processing video at URL: ${videoUrl}`); + + // Generate temporary file names + const tempDirectory = os.tmpdir(); + const outputPath = path.join(tempDirectory, `audio_${Date.now()}.wav`); + + // Fetch the video + const response = await fetch(videoUrl); + + // Extract the audio + await new Promise((resolve, reject) => { + if (!response.body) { + return reject(new Error("Failed to fetch video")); + } + + ffmpeg(Readable.from(response.body)) + .outputOptions([ + "-vn", // Disable video output + "-acodec pcm_s16le", // Use PCM 16-bit little-endian encoding + "-ar 44100", // Set audio sample rate to 44.1 kHz + "-ac 2", // Set audio channels to stereo + ]) + .output(outputPath) + .on("end", resolve) + .on("error", reject) + .run(); + }); + + logger.log(`Audio extracted from video`, { outputPath }); + + // Transcribe the audio using Deepgram + const { result, error } = await deepgram.listen.prerecorded.transcribeFile( + fs.readFileSync(outputPath), + { + model: "nova-2", + smart_format: true, + diarize: true, + } + ); + + if (error) { + throw error; + } + + console.dir(result, { depth: null }); + + // Convert the result object to a string + const transcription = result.results.channels[0].alternatives[0].paragraphs?.transcript; + + logger.log(`Transcription: ${transcription}`); + + // Delete the temporary audio file + fs.unlinkSync(outputPath); + logger.log(`Temporary audio file deleted`, { outputPath }); + + const { error: updateError } = await supabase + .from("video_transcriptions") + // Set the plan to the new plan and update the timestamp + .update({ transcription: transcription, video_url: videoUrl }) + .eq("video_url", videoUrl); + + // If there was an error updating the subscription, throw an error + if (updateError) { + throw new Error(`Failed to update user subscription: ${updateError.message}`); + } + + return { + message: `Summary of the audio: ${transcription}`, + result, + }; + }, +}); +``` + +### Testing the entire setup + +Your `video-processing-handler` Edge Function is now set up to trigger the `videoProcessAndUpdate` task every time a new row is inserted into your `video_transcriptions` table. + +To do this, go back to your Supabase project dashboard, click on 'Table Editor' in the left-hand menu, click on the `video_transcriptions` table , and then click 'Insert', 'Insert Row' . + +![How to insert a new row 1](/images/supabase-new-table-3.png) + +Add a new item under `video_url`, with a public video url. + + CHANGE IMAGE BELOW + +![How to insert a new row 2](/images/supabase-new-table-4.png) + + CHANGE IMAGE BELOW + +![How to view the logs](/images/supabase-logs.png) + +Then, check your [cloud.trigger.dev](http://cloud.trigger.dev) project 'Runs' list and you should see a processing `videoProcessAndUpdate` task which has been triggered when you added a new row with the video url to your `video_transcriptions` table. + +Once the run has completed successfully, go back to your Supabase `video_transcriptions` table, and you should see that in the row containing the original video URL, the transcription has now been added to the `transcription` column. + + REPLACE BELOW IMAGE +![How to insert a new row 2](/images/supabase-trigger-screenshot.png) + +**Congratulations! You have completed a full loop from Supabase to Trigger.dev and back again.** + + From 6014359e17348830fdf1713aee3542fc23b0a5e8 Mon Sep 17 00:00:00 2001 From: D-K-P <8297864+D-K-P@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:19:32 -0700 Subject: [PATCH 03/18] Updated images and improved docs --- ...upabase-edge-functions-ffmpeg-deepgram.mdx | 86 +++++++++--------- docs/images/supabase-create-webhook-2.png | Bin 59889 -> 76849 bytes docs/images/supabase-create-webhook-3.png | Bin 55890 -> 72637 bytes docs/images/supabase-new-table-2.png | Bin 52458 -> 140583 bytes docs/images/supabase-new-table-3.png | Bin 36996 -> 37818 bytes docs/images/supabase-new-table-4.png | Bin 40036 -> 400376 bytes docs/images/supabase-trigger-screenshot.png | Bin 38167 -> 54775 bytes 7 files changed, 41 insertions(+), 45 deletions(-) diff --git a/docs/guides/frameworks/supabase-edge-functions-ffmpeg-deepgram.mdx b/docs/guides/frameworks/supabase-edge-functions-ffmpeg-deepgram.mdx index 52289adc03..db027f569e 100644 --- a/docs/guides/frameworks/supabase-edge-functions-ffmpeg-deepgram.mdx +++ b/docs/guides/frameworks/supabase-edge-functions-ffmpeg-deepgram.mdx @@ -7,9 +7,6 @@ description: "This guide shows you how to trigger a task when a row is added to import Prerequisites from "/snippets/framework-prerequisites.mdx"; import SupabasePrerequisites from "/snippets/supabase-prerequisites.mdx"; import CliInitStep from "/snippets/step-cli-init.mdx"; -import CliDevStep from "/snippets/step-cli-dev.mdx"; -import CliRunTestStep from "/snippets/step-run-test.mdx"; -import CliViewRunStep from "/snippets/step-view-run.mdx"; import UsefulNextSteps from "/snippets/useful-next-steps.mdx"; import TriggerTaskNextjs from "/snippets/trigger-tasks-nextjs.mdx"; import NextjsTroubleshootingMissingApiKey from "/snippets/nextjs-missing-api-key.mdx"; @@ -17,7 +14,7 @@ import NextjsTroubleshootingButtonSyntax from "/snippets/nextjs-button-syntax.md import WorkerFailedToStartWhenRunningDevCommand from "/snippets/worker-failed-to-start.mdx"; import SupabaseDocsCards from "/snippets/supabase-docs-cards.mdx"; -## Overview +## Workflow overview Generate a transcription from a video URL using [Supabase](https://supabase.com), [FFmpeg](https://www.ffmpeg.org/) and [Deepgram](https://deepgram.com) and Trigger.dev. @@ -40,25 +37,24 @@ The Edge Function triggers a deployed Trigger.dev task which takes a payload fro - Ensure TypeScript is installed - [Create a Trigger.dev account](https://cloud.trigger.dev) - [Create a new Trigger.dev project](/guides/dashboard/creating-a-project) -- [Create a new Deepgram account](https://deepgram.com/) +- [Create a new Deepgram account](https://deepgram.com/) and get your API key from the dashboard ## Initial setup - - - ## Create and deploy the video processing Trigger.dev task -Before setting up your Edge Function and Database Webhook, you'll first need to create your Trigger.dev task. This can be tested independently from the rest of the setup. +Before setting up your Edge Function and Database Webhook, you'll need to create your Trigger.dev task. This can be tested independently from the rest of the workflow. -Create a new task file in your `/trigger` folder (the same place your example task is). Call it `videoProcessAndUpdate.ts`. +Create a new task file in your `/trigger` folder (the same place your 'Hello World' task is). Call it `videoProcessAndUpdate.ts`. -This task with take a video url, extract the audio using FFmpeg and transcribe the audio using Deepgram. We will add the Supabase `update` step further on in the tutorial. +This task with take a video from a public video url, extract the audio using FFmpeg and transcribe the audio using Deepgram. + +We will add the Supabase `update` step further on in the tutorial. ```ts /trigger/videoProcessAndUpdate.ts import { createClient as createDeepgramClient } from "@deepgram/sdk"; @@ -144,12 +140,11 @@ export const videoProcessAndUpdate = task({ ### Adding the FFmpeg build extension - {" "} - This task can also be tested in `dev` without adding the build extension, but we recommend adding the** - ** extension during the setup process.{" "} + This task can also be tested in `dev` without adding the build extension, but we recommend adding + the extension during the setup process. -Before you can deploy and test the task, you'll first need to add our FFmpeg extension to your project configuration like this: +Before you can deploy the task, you'll need to add our FFmpeg extension: ```ts trigger.config.ts import { ffmpeg } from "@trigger.dev/build/extensions/core"; @@ -172,9 +167,15 @@ export default defineConfig({ You'll also need to add `@trigger.dev/build` to your `package.json` file under `devDependencies` if you don't already have it there. +### Adding the Deepgram environment variable + +You will need to add your `DEEPGRAM_SECRET_KEY` as an environment variable in your Trigger.dev project. You can do this in the Trigger.dev dashboard under the 'environment variables' tab. + +![Adding environment variables](/images/environment-variables-page.jpg) + ### Deploying your task -You can now deploy your task and test it in the dashboard. +You can now deploy your task and test it in the Trigger.dev [dashboard](https://cloud.trigger.dev). @@ -194,7 +195,7 @@ yarn dlx trigger.dev@latest deploy ### Testing your task -To test this task in the dashboard, you can use the following payload: +To test this task in the dashboard, select the `prod` environment and use the following payload: ```json { @@ -202,9 +203,9 @@ To test this task in the dashboard, you can use the following payload: } ``` -Congratulations, You should now see the video transcription in a successful run. +Congratulations! You should now see the video transcription logged in a successful run. -## Create and deploy the Edge Function and configure the Database Webhook +## Create and deploy a Supabase Edge Function and configure the Database Webhook ### Add your Trigger.dev prod secret key to the Supabase dashboard @@ -220,7 +221,7 @@ Add `TRIGGER_SECRET_KEY` { }); ``` -You can only import the `type` from the task. Tasks in the `trigger` folder use Node, so they must stay in there or they will not run, especially if you are using a different runtime like Deno. Also do not add "`npm:`" to imports @@ -274,20 +274,17 @@ There will be a link to the dashboard in your terminal output, or you can find i ### Create a new table in your Supabase dashboard -Next, in your Supabase project dashboard, click on 'Table Editor' in the left-hand menu and create a new table. +Next, in your Supabase dashboard, click on 'Table Editor' in the left-hand menu and create a new table. ![How to create a new table](/images/supabase-new-table-1.png) -In this example we will call our table `video_transcriptions`. - -Add two new columns, one called `video_url` with the type `text`, and another called `transcription`, also with the type `text`. +Call your table `video_transcriptions`. - REPLACE IMAGE BELOW -![How to add a new column](/images/supabase-new-table-2.png) +Add two new columns, one called `video_url` with the type `text` , and another called `transcription`, also with the type `text` . ### Create a new Database Webhook -First, go to your Supabase project dashboard, click 'Project settings' , then the 'API' tab , and copy the `anon` `public` API key from the table . +In your Supabase project dashboard, click 'Project settings' , then the 'API' tab , and copy the `anon` `public` API key from the table . ![How to find your Supabase API keys](/images/supabase-api-key.png) @@ -324,14 +321,14 @@ and select the Edge Function you have created: `video-processing-handler`.{" "} Your Database Webhook is now ready to use. -### Triggering the task +## Triggering the task -### Enabling updates to your supabase tables +### Adding the logic to update the table row -First, you must go back to your task code and add in the Supabase logic: +First, you must go back to your `videoProcessAndUpdate` task code from earlier and add in the Supabase logic. This will: -- Creating a Supabase client (you must update your environment variables in order for this to work) -- Function to update the table row with the exact match of the `video_url` payload with the new transcription +- Create a Supabase client (you must update your environment variables in order for this to work) +- Create a function which updates the table row with the exact match of the `video_url` payload with the new generated transcription. ```ts /trigger/videoProcessAndUpdate.ts import { createClient as createDeepgramClient } from "@deepgram/sdk"; @@ -434,7 +431,13 @@ export const videoProcessAndUpdate = task({ }); ``` -### Testing the entire setup +### Adding your Supabase environment variables + +You will need to add your `SUPABASE_PROJECT_URL` and `SUPABASE_SERVICE_ROLE_KEY` as environment variables in your Trigger.dev project. + +![Adding environment variables](/images/environment-variables-page.jpg) + +### Testing the entire workflow Your `video-processing-handler` Edge Function is now set up to trigger the `videoProcessAndUpdate` task every time a new row is inserted into your `video_transcriptions` table. @@ -442,23 +445,16 @@ To do this, go back to your Supabase project dashboard, click on 'Table Editor' ![How to insert a new row 1](/images/supabase-new-table-3.png) -Add a new item under `video_url`, with a public video url. +Add a new item under `video_url`, with a public video url. . - CHANGE IMAGE BELOW +You can use the following public video URL for testing: `https://dpgr.am/spacewalk.wav`. ![How to insert a new row 2](/images/supabase-new-table-4.png) - CHANGE IMAGE BELOW - -![How to view the logs](/images/supabase-logs.png) - -Then, check your [cloud.trigger.dev](http://cloud.trigger.dev) project 'Runs' list and you should see a processing `videoProcessAndUpdate` task which has been triggered when you added a new row with the video url to your `video_transcriptions` table. +Once the new table row has been inserted, check your [cloud.trigger.dev](http://cloud.trigger.dev) project 'Runs' list and you should see a processing `videoProcessAndUpdate` task which has been triggered when you added a new row with the video url to your `video_transcriptions` table. Once the run has completed successfully, go back to your Supabase `video_transcriptions` table, and you should see that in the row containing the original video URL, the transcription has now been added to the `transcription` column. - REPLACE BELOW IMAGE -![How to insert a new row 2](/images/supabase-trigger-screenshot.png) - -**Congratulations! You have completed a full loop from Supabase to Trigger.dev and back again.** +**Congratulations! You have completed the full workflow from Supabase to Trigger.dev and back again.** diff --git a/docs/images/supabase-create-webhook-2.png b/docs/images/supabase-create-webhook-2.png index 18ab7195c65a2c21e3a006fe9375b5b961ebc6b1..0c21b485ec9465105fa021ab6a5a963a0d32cbb8 100644 GIT binary patch literal 76849 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