Letting Supabase Users Transcribe YouTube Videos
Tags: supabase, ai, javascript
Published: 2023-10-10 19:38:46

Have you ever wanted to let your Supabase users transcribe YouTube videos using an open-source model deployed to a serverless GPU platform? Probably way too specific for a random scenario (fair enough) but it's a fun project anyway.

So first things, first: How do we transcribe a video? 

While we could get into the intricacies of machine learning models all day but that might be a little out of scope for this article so we are going to use [OpenAI's Whispher](https://github.com/openai/whisper) model. It's open source and pretty good.

We'll need a GPU to run the model (technically not 100% necessary but we want our inference speed to be really fast). The problem is that GPUs are expensive - as you'll know if you've seen NVIDIA's stock price lately...

![Stock graph](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j2ybqwwfvqf15dfa9zgv.jpg)

Thankfully, serverless GPUs have really caught on lately and for this video we'll be using a platform called [Beam](https://www.beam.cloud/) to deploy our Whispher. They have a [great article](https://docs.beam.cloud/examples/whisper) on this very topic. Our focus is what to do when the model is ready for inference by our Supabase users.

So first of all, how do we call it in Supabase? Let's use an edge function.

- Start a Supabase project
- Install the [CLI and create a new function](https://supabase.com/docs/guides/functions/quickstart) by calling `supabase functions new transcribe-yt` (transcribe-yt is our function name)

Our index.ts should look something like this:

```javascript
// Follow this setup guide to integrate the Deno language server with your editor:
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.

import { serve } from "https://deno.land/std@0.168.0/http/server.ts"

console.log("Hello from Functions!")

serve(async (req) => {
  const { name } = await req.json()
  const data = {
    message: `Hello ${name}!`,
  }

  return new Response(
    JSON.stringify(data),
    { headers: { "Content-Type": "application/json" } },
  )
})

// To invoke:
// curl -i --location --request POST 'http://localhost:54321/functions/v1/' \
//   --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
//   --header 'Content-Type: application/json' \
//   --data '{"name":"Functions"}'

```

Let's change the 'name' parameter to 'video_url' first:

```javascript
  const { video_url } = await req.json()
  const data = {
    message: `Hello ${video_url}!`,
  }
```

Now let's actually call the model on Beam:

```javascript
fetch("https://apps.beam.cloud/<ID_FROM_BEAM>", {
  method: "POST",
  headers: {
    "Authorization": "[YOUR_AUTH_TOKEN]",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    video_url: video_url
  })
})
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));
```

We will use `.then` today, and `data['pred']` will return our transcription and we can now return our transcription to the user like this:

```javascript
  .then(data => {
    const transcription = data['pred']

    return new Response(
      JSON.stringify({ transcription }),
      { headers: { "Content-Type": "application/json" } },
    )
  })
```

Congratulations, you've done it! 

BUT...

One more thing, anyone who logins in can blow up your bill and bankrupt your cool new startup. We need to do something else and that's figuring out who is asking for a transcription. By 'logging in as the user' (in a sense), first we'll import our favourite SDK

```javascript
import { createClient } from '@supabase/supabase-js'
```

Then we can get our user plan:

```javascript
    const { user } = await supabaseClient.auth.getUser()
    const user_id = user.id

    // Now we can run any query to check user usage, billing plan, etc as them
    const { data, error } = await supabaseClient
      .from('user_plans')
      .select('plan')
      .eq('user_id', user_id)
      .single()
```

Hopefully, you're already using excellent [Row Level Security](https://supabase.com/docs/guides/auth/row-level-security) policies and then if a user isn't on a supported plan you would need to cancel the request and send an error.

Here's how all the code looks together:

```javascript
// Follow this setup guide to integrate the Deno language server with your editor:
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.

import { serve } from "https://deno.land/std@0.168.0/http/server.ts"

console.log("Hello from Functions!")

serve(async (req) => {
  const { video_url } = await req.json()
  
    // We use our client here
    const supabaseClient = createClient(
      // Use the URL and anon key (these will be in your Supabase project)
      Deno.env.get('SUPABASE_URL') ?? '',
      Deno.env.get('SUPABASE_ANON_KEY') ?? '',
      { global: { headers: { Authorization: req.headers.get('Authorization')! } } }
    )

    const { user } = await supabaseClient.auth.getUser()
    const user_id = user.id

    // Now we can run any query to check user usage, billing plan, etc as them
    const { data, error } = await supabaseClient
      .from('user_plans')
      .select('plan')
      .eq('user_id', user_id)
      .single()

  fetch("https://apps.beam.cloud/<ID_FROM_BEAM>", {
    method: "POST",
    headers: {
      "Authorization": "[YOUR_AUTH_TOKEN]",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      video_url: video_url
    })
  })
  .then(response => response.json())
  .then(data => {
    const transcription = data['pred']

    return new Response(
      JSON.stringify({ transcription }),
      { headers: { "Content-Type": "application/json" } },
    )
  })
  .catch(error => console.error(error));

  return new Response(
    JSON.stringify(data),
    { headers: { "Content-Type": "application/json" } },
  )
})

// To invoke:
// curl -i --location --request POST 'http://localhost:54321/functions/v1/' \
//   --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
//   --header 'Content-Type: application/json' \
//   --data '{"name":"Functions"}'

```

A super helpful hint to save you some research: If your users are invoking the functions from a browser use [CORS](https://supabase.com/docs/guides/functions/cors).

There are some more things that can be done to optimise this function (like handling those tricky `catch` statements but for now this is enough to get you started on your AI adventure.

If you have any questions leave a comment and I (not AI) will answer.