Close Menu
    DevStackTipsDevStackTips
    • Home
    • News & Updates
      1. Tech & Work
      2. View All

      Sunshine And March Vibes (2025 Wallpapers Edition)

      May 16, 2025

      The Case For Minimal WordPress Setups: A Contrarian View On Theme Frameworks

      May 16, 2025

      How To Fix Largest Contentful Paint Issues With Subpart Analysis

      May 16, 2025

      How To Prevent WordPress SQL Injection Attacks

      May 16, 2025

      Microsoft has closed its “Experience Center” store in Sydney, Australia — as it ramps up a continued digital growth campaign

      May 16, 2025

      Bing Search APIs to be “decommissioned completely” as Microsoft urges developers to use its Azure agentic AI alternative

      May 16, 2025

      Microsoft might kill the Surface Laptop Studio as production is quietly halted

      May 16, 2025

      Minecraft licensing robbed us of this controversial NFL schedule release video

      May 16, 2025
    • Development
      1. Algorithms & Data Structures
      2. Artificial Intelligence
      3. Back-End Development
      4. Databases
      5. Front-End Development
      6. Libraries & Frameworks
      7. Machine Learning
      8. Security
      9. Software Engineering
      10. Tools & IDEs
      11. Web Design
      12. Web Development
      13. Web Security
      14. Programming Languages
        • PHP
        • JavaScript
      Featured

      The power of generators

      May 16, 2025
      Recent

      The power of generators

      May 16, 2025

      Simplify Factory Associations with Laravel’s UseFactory Attribute

      May 16, 2025

      This Week in Laravel: React Native, PhpStorm Junie, and more

      May 16, 2025
    • Operating Systems
      1. Windows
      2. Linux
      3. macOS
      Featured

      Microsoft has closed its “Experience Center” store in Sydney, Australia — as it ramps up a continued digital growth campaign

      May 16, 2025
      Recent

      Microsoft has closed its “Experience Center” store in Sydney, Australia — as it ramps up a continued digital growth campaign

      May 16, 2025

      Bing Search APIs to be “decommissioned completely” as Microsoft urges developers to use its Azure agentic AI alternative

      May 16, 2025

      Microsoft might kill the Surface Laptop Studio as production is quietly halted

      May 16, 2025
    • Learning Resources
      • Books
      • Cheatsheets
      • Tutorials & Guides
    Home»Development»Artificial Intelligence»Create Multi-Lingual Subtitles with AssemblyAI and DeepL

    Create Multi-Lingual Subtitles with AssemblyAI and DeepL

    July 8, 2024

    Subtitles are vital for enhancing the accessibility of an application. Users who have difficulty understanding the language of a video rely on subtitles to help them understand and grasp the contents of the video. Typically, creating subtitles for videos requires tedious manual work to carefully transcribe each and every word while also keeping track of the timeline. But with AssemblyAI’s audio transcription feature, you can generate subtitles for a video in minutes.

    With AssemblyAI, you can asynchronously transcribe a video and download the transcription as subtitles in either SRT or VTT format.

    In this tutorial, you’ll learn how to build a web app in Go that’ll use AssemblyAI to transcribe an uploaded video file and generate subtitles. You’ll then use DeepL to translate the subtitles into different languages.

    Before you start

    To complete this tutorial, you’ll need:

    An upgraded AssemblyAI accountA DeepL API account. This is different from a regular DeepL account. Make sure you register a DeepL API account.Go installed on your computer

    Set up the application

    You’ll start by creating a web app where users can upload a video file.

    First, create a directory for the project and navigate to the directory:

    mkdir subtitle-video
    cd subtitle-video

    Initialize a Go module by running the following command, replacing <your-username> with your GitHub username:

    go mod init <your-username>/aai-demo-go

    Create a file named main.go where you’ll write the code for the server. Start by importing the necessary modules:

    package main

    import (
    “fmt”
    “log”
    “net/http”

    “github.com/gin-gonic/gin”
    “github.com/google/uuid”
    )

    Here, you’re using two external modules: Gin to simplify writing the server and uuid for generating unique identifiers.

    Since you’re using AssemblyAI to create transcriptions asynchronously, you’ll need a way to keep track of the jobs submitted to AssemblyAI and the video file that was uploaded. For that, create the following type:

    // The Job definition
    type Job struct {
    ID string `json:”id”`
    FilePath string `json:”file_path”`
    }

    // Empty jobs array
    var jobs []Job
    Note: For simplicity, this app uses an in-memory array for storing the jobs. This means that you’ll lose the jobs when you restart the server. This is fine for learning, but in a production app, you should use a database.

    Write the main function, where you’ll set up the gin.Router:

    func main() {
    r := gin.Default()
    r.Use(ErrorHandler)

    r.LoadHTMLGlob(“views/*.html”)
    r.Static(“/uploads”, “uploads”)

    r.Run(“:3000”)
    }

    Here, you’re using LoadHTMLGlob to load all HTML files from the views directory. This allows you to render the HTML files without having to write the full path every time. You’re also using Gin to serve the uploads directory. This directory will store the uploaded videos, and serving the files from here lets you load the video in the browser.

    Finally, the server is started on port 3000.

    Now it’s time to define the routes. The first route is the home page. Paste the following code after the r.Static line:

    r.GET(“/”, func(c *gin.Context) {
    c.HTML(http.StatusOK, “index.html”, nil)
    })

    This route will simply render the views/index.html file.

    The next route is the /upload route, which will handle the video upload:

    r.POST(“/upload”, func(c *gin.Context) {
    file, _ := c.FormFile(“myFile”)
    log.Println(file.Filename)

    c.SaveUploadedFile(file, “uploads/” + file.Filename)

    ID := uuid.NewString()
    job := Job{ID: ID, FilePath: “uploads/” + file.Filename}
    jobs = append(jobs, job)

    fmt.Printf(“File %s uploaded successfully with ID %sn”, file.Filename, ID)
    c.Redirect(http.StatusMovedPermanently, “/jobs/” + ID)
    })

    This route uses the SaveUploadedFile method to save the uploaded file in the uploads directory. It then creates a new Job with a randomly generated UUID. In the next section, you’ll upload this file to AssemblyAI and replace this UUID with the ID generated by AssemblyAI.

    After the upload completes, the user is redirected to the /jobs/:id route, which you’ll write next:

    r.GET(“/jobs/:id”, func(c *gin.Context) {
    ID := c.Param(“id”)
    var job Job
    for _, j := range jobs {
    if j.ID == ID {
    job = j
    }
    }
    c.HTML(http.StatusOK, “job.html”, gin.H{
    “job”: job,
    })
    })

    This route loads the Job using the id parameter. It then renders views/job.html and passes the Job to it.

    Lastly, define the ErrorHandler function that’ll handle any error that might occur in the routes:

    func ErrorHandler(c *gin.Context) {
    c.Next()

    if(len(c.Errors) == 0) {
    return
    }

    for _, err := range c.Errors {
    log.Printf(“Error: %sn”, err.Error())
    }

    c.JSON(http.StatusInternalServerError, “”)
    }

    The entire main.go file should look like this:

    package main

    import (
    “fmt”
    “log”
    “net/http”

    “github.com/gin-gonic/gin”
    “github.com/google/uuid”
    )

    // The Job definition
    type Job struct {
    ID string `json:”id”`
    FilePath string `json:”file_path”`
    }

    // Empty jobs array
    var jobs []Job

    func main() {
    r := gin.Default()
    r.Use(ErrorHandler)

    r.LoadHTMLGlob(“views/*.html”)
    r.Static(“/uploads”, “uploads”)

    r.GET(“/”, func(c *gin.Context) {
    c.HTML(http.StatusOK, “index.html”, nil)
    })

    r.POST(“/upload”, func(c *gin.Context) {
    file, _ := c.FormFile(“myFile”)
    log.Println(file.Filename)

    c.SaveUploadedFile(file, “uploads/” + file.Filename)

    ID := uuid.NewString()
    job := Job{ID: ID, FilePath: “uploads/” + file.Filename}
    jobs = append(jobs, job)

    fmt.Printf(“File %s uploaded successfully with ID %sn”, file.Filename, ID)
    c.Redirect(http.StatusMovedPermanently, “/jobs/” + ID)
    })

    r.GET(“/jobs/:id”, func(c *gin.Context) {
    ID := c.Param(“id”)
    var job Job
    for _, j := range jobs {
    if j.ID == ID {
    job = j
    }
    }
    c.HTML(http.StatusOK, “job.html”, gin.H{
    “job”: job,
    })
    })

    r.Run(“:3000”)
    }

    func ErrorHandler(c *gin.Context) {
    c.Next()

    if(len(c.Errors) == 0) {
    return
    }

    for _, err := range c.Errors {
    log.Printf(“Error: %sn”, err.Error())
    }

    c.JSON(http.StatusInternalServerError, “”)
    }

    Create the upload form

    Now, create a new file at views/index.html with the following code:

    <!DOCTYPE html>
    <html lang=”en”>
    <head>
    <meta charset=”UTF-8″ />
    <meta name=”viewport” content=”width=device-width, initial-scale=1.0″ />
    <meta http-equiv=”X-UA-Compatible” content=”ie=edge” />
    <link
    href=”https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,900&display=swap”
    rel=”stylesheet”
    />
    <link
    rel=”stylesheet”
    href=”https://cdn.jsdelivr.net/npm/tw-elements/dist/css/tw-elements.min.css”
    />
    <script src=”https://cdn.tailwindcss.com”></script>
    <script>
    tailwind.config = {
    darkMode: “class”,
    theme: {
    fontFamily: {
    sans: [“Roboto”, “sans-serif”],
    body: [“Roboto”, “sans-serif”],
    mono: [“ui-monospace”, “monospace”],
    },
    },
    corePlugins: {
    preflight: false,
    },
    };
    </script>
    <title>AssemblyAI Demo</title>
    </head>
    <body class=”flex items-center justify-center h-screen”>
    <form
    enctype=”multipart/form-data”
    action=”/upload”
    method=”post”
    onsubmit=”upload.disabled = true; upload.innerText = ‘Uploading’; upload.classList.replace(‘bg-primary’, ‘bg-gray-300’); return true;”
    >
    <div class=”mb-3″>
    <label
    for=”myFile”
    class=”mb-2 inline-block text-neutral-500 dark:text-neutral-400″
    >Upload a video file</label
    >
    <input
    class=”relative m-0 block w-full min-w-0 flex-auto cursor-pointer rounded border border-solid border-secondary-500 bg-transparent bg-clip-padding px-3 py-[0.32rem] text-base font-normal text-surface transition duration-300 ease-in-out file:-mx-3 file:-my-[0.32rem] file:me-3 file:cursor-pointer file:overflow-hidden file:rounded-none file:border-0 file:border-e file:border-solid file:border-inherit file:bg-transparent file:px-3 file:py-[0.32rem] file:text-surface focus:border-primary focus:text-gray-700 focus:shadow-inset focus:outline-none dark:border-white/70 dark:text-white file:dark:text-white”
    type=”file”
    id=”myFile”
    name=”myFile”
    accept=”video/*”
    />
    <button
    type=”submit”
    name=””upload””
    class=”inline-block rounded bg-primary px-6 pb-2 pt-2.5 m-5 text-xs font-medium uppercase leading-normal text-white shadow-primary-3 transition duration-150 ease-in-out hover:bg-primary-accent-300 hover:shadow-primary-2 focus:bg-primary-accent-300 focus:shadow-primary-2 focus:outline-none focus:ring-0 active:bg-primary-600 active:shadow-primary-2 motion-reduce:transition-none dark:shadow-black/30 dark:hover:shadow-dark-strong dark:focus:shadow-dark-strong dark:active:shadow-dark-strong”
    >
    Upload File
    </button>
    </div>
    </form>
    </body>
    </html>

    This file simply displays a file chooser and lets the user upload a video file. Tailwind CSS is used to style the page to make it look more presentable.

    Create views/job.html with the following code:

    <!DOCTYPE html>
    <html lang=”en”>
    <head>
    <meta charset=”UTF-8″ />
    <meta name=”viewport” content=”width=device-width, initial-scale=1.0″ />
    <meta http-equiv=”X-UA-Compatible” content=”ie=edge” />
    <link
    href=”https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,900&display=swap”
    rel=”stylesheet”
    />
    <link
    rel=”stylesheet”
    href=”https://cdn.jsdelivr.net/npm/tw-elements/dist/css/tw-elements.min.css”
    />
    <script src=”https://cdn.tailwindcss.com”></script>
    <script>
    tailwind.config = {
    darkMode: “class”,
    theme: {
    fontFamily: {
    sans: [“Roboto”, “sans-serif”],
    body: [“Roboto”, “sans-serif”],
    mono: [“ui-monospace”, “monospace”],
    },
    },
    corePlugins: {
    preflight: false,
    },
    };
    </script>
    <title>Video Playback</title>
    </head>
    <body class=”flex flex-col items-center justify-center h-screen”>
    <span id=”loading”>Processing {{ .job.FilePath }} (Job {{ .job.ID }})</span>
    </body>
    </html>

    For now, it only shows text indicating that it’s processing. The job variable that was passed during rendering is accessed using Gin’s {{…}} syntax.

    To run the server, first download the dependencies:

    go get .

    Then, use go run to start the server:

    go run .

    Open http://localhost:3000 in your browser to see the app’s home page:

    Upload a video and click Upload file and start processing. Once the upload is finished, you’ll see a screen indicating that your upload is being processed.

    The video will be saved in the uploads directory.

    Transcribe the video using AssemblyAI

    Log in to the AssemblyAI dashboard and copy the API key. Create a .env file at the root of your project and add the AssemblyAI API key:

    ASSEMBLYAI_API_KEY=<YOUR_API_KEY>

    In main.go, add the new imports:

    import (
    …
    “context”
    “os”

    “github.com/AssemblyAI/assemblyai-go-sdk”
    “github.com/joho/godotenv”
    )

    You’ll be using GoDotEnv to load the environment variables from the .env file and the AssemblyAI Go SDK to communicate with the AssemblyAI server.

    In the main function, add the following code before the existing code:

    err := godotenv.Load()
    if err != nil {
    log.Fatal(“Error loading .env file”)
    }

    aaiApiKey := os.Getenv(“ASSEMBLYAI_API_KEY”)

    client := assemblyai.NewClient(aaiApiKey)

    This code loads the environment variable and constructs an AssemblyAI client.

    Replace the /upload route with the following code:

    r.POST(“/upload”, func(c *gin.Context) {

    file, _ := c.FormFile(“myFile”)
    log.Println(file.Filename)

    c.SaveUploadedFile(file, “uploads/” + file.Filename)

    ctx := context.Background()
    f, err := os.Open(“uploads/” + file.Filename)
    if err != nil {
    c.AbortWithError(http.StatusInternalServerError, err)
    return
    }
    defer f.Close()

    transcript, err := client.Transcripts.SubmitFromReader(ctx, f, nil)
    if err != nil {
    c.AbortWithError(http.StatusInternalServerError, err)
    return
    }

    ID := *transcript.ID
    job := Job{ID: ID, FilePath: “uploads/” + file.Filename}
    jobs = append(jobs, job)

    fmt.Printf(“File %s uploaded successfully with ID %sn”, file.Filename, ID)
    c.Redirect(http.StatusMovedPermanently, “/jobs/” + ID)
    })

    This code is similar to the previous version, except this time, you’re uploading the file to AssemblyAI for transcription. The client.Transcripts.SubmitFromReader function lets you submit a file to AssemblyAI without waiting for transcription to finish. It returns metadata about the submitted transcription, from which the ID is used to set the ID of the Job. You can now remove the import for the uuid module, as you don’t need it anymore.

    Create a new route, /jobs/:id/status:

    r.GET(“/jobs/:id/status”, func(c *gin.Context) {
    ID := c.Param(“id”)
    var job Job
    for _, j := range jobs {
    if j.ID == ID {
    job = j
    }
    }

    ctx := context.Background()
    transcript, err := client.Transcripts.Get(ctx, id)
    if err != nil {
    c.AbortWithError(http.StatusInternalServerError, err)
    return
    }

    if transcript.Status == “processing” || transcript.Status == “queued” {
    c.JSON(http.StatusOK, gin.H{
    “status”: “loading”,
    “job”: job,
    })
    } else if transcript.Status == “completed” {
    subtitles, err := client.Transcripts.GetSubtitles(ctx, ID, “vtt”, nil)
    if err != nil {
    c.AbortWithError(http.StatusInternalServerError, err)
    return
    }
    c.JSON(http.StatusOK, gin.H{
    “status”: “completed”,
    “job”: job,
    “subtitles”: string(subtitles),
    })
    }
    })

    This route, when invoked, will check the status of the transcription using its ID with the client.Transcripts.Get function. If the status is processing or queued, a response will be sent to the frontend with status set to loading. If the transcription status is completed, the client.Transcripts.GetSubtitles function is used to download the transcription as subtitles in the WebVTT format. This subtitle is then sent to the frontend.

    The frontend will periodically poll this route to determine when the transcription is complete.

    Open views/job.html and add the following code before the closing body tag:

    <div id=”video”></div>
    <script>
    c = setInterval(() => {
    fetch(“/jobs/{{ .job.ID }}/status”)
    .then((res) => res.json())
    .then((data) => {
    if (data.status === “completed”) {
    const { subtitles } = data;
    const { file_path } = data.job;

    const video = document.createElement(“video”);
    video.src = “/” + file_path;
    video.controls = true;
    video.autoplay = false;
    video.loop = true;
    video.width = 500;
    video.style = “border-radius: 10px;”;

    let loading = document.getElementById(“loading”);
    document.body.removeChild(loading);
    document.getElementById(“video”).appendChild(video);

    createSubtitles(subtitles, “en”);
    clearInterval(c);
    }
    });
    }, 3000);

    function createSubtitles(subtitles, srclang) {
    const blob = new Blob([subtitles], { type: “text/vtt” });
    const url = URL.createObjectURL(blob);

    const video = document.querySelector(“video”);
    for (let i = 0; i < video.textTracks.length; i++) {
    video.textTracks[i].mode = “hidden”;
    }

    const track = document.createElement(“track”);
    track.src = url;
    track.kind = “subtitles”;
    track.label = srclang;
    track.srclang = srclang;
    track.mode = “showing”;
    video.appendChild(track);
    }
    </script>

    This code adds two JavaScript functions. The first is a setInterval call that polls the /jobs/:id/status route every three seconds. If the returned status is completed, it creates a new video element and adds it to the body. The src attribute of the video is set to the FilePath attribute of the Job, which lets the browser load the video file from the uploads directory.

    The createSubtitles function takes the subtitles and the language. It creates a Blob from the subtitles and creates a new track element to display the subtitles. This track element is added to the video element, and any other existing tracks are set to be hidden.

    Stop the server if it’s running and restart it with go get . && go run . . Go to http://localhost:3000 again and upload the video file. This time, after a little while, the video will appear. You can click the CC button to turn on the English captions.

    Note: To keep things simple, this tutorial assumes that the uploaded video is in English. However, AssemblyAI supports over sixteen languages for transcription.

    Translate subtitles using DeepL

    Log in to the DeepL API dashboard and copy the API key.

    Add the DeepL API key in the .env file:

    DEEPL_API_KEY=<YOUR_API_KEY>

    To convert the responses from DeepL into JSON, you’ll need to define a couple of types. Paste the following code after the definition of the Job type:

    type Translation struct{
    Text string `json:”text”`
    DetectedSourceLanguage string `json:”detected_source_language”`
    }

    type TranslationResponse struct{
    Translations []Translation `json:”translations”`
    }

    Add the new imports:

    import (
    …
    “encoding/json”
    “io”
    “bytes”
    )

    Add a new /translate route:

    r.POST(“/translate”, func(c *gin.Context) {
    ID := c.PostForm(“job_id”)
    language := c.PostForm(“language”)

    ctx := context.Background()
    subtitles, err := client.Transcripts.GetSubtitles(ctx, ID, “vtt”, nil)
    if err != nil {
    c.AbortWithError(http.StatusInternalServerError, err)
    return
    }

    deepLApiKey := os.Getenv(“DEEPL_API_KEY”)
    body, _ := json.Marshal(map[string]any{
    “text”: []string{string(subtitles)},
    “target_lang”: language,
    })
    req, err := http.NewRequest(“POST”, “https://api-free.deepl.com/v2/translate”, bytes.NewBuffer(body))
    if err != nil {
    c.AbortWithError(http.StatusInternalServerError, err)
    return
    }
    req.Header.Set(“Content-Type”, “application/json”)
    req.Header.Set(“Authorization”, “DeepL-Auth-Key ” + deepLApiKey)

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
    c.AbortWithError(http.StatusInternalServerError, err)
    return
    }
    defer resp.Body.Close()

    respBody, err := io.ReadAll(resp.Body)
    if err != nil {
    c.AbortWithError(http.StatusInternalServerError, err)
    return
    }

    var translationResponse TranslationResponse
    err = json.Unmarshal(respBody, &translationResponse)
    if err != nil {
    c.AbortWithError(http.StatusInternalServerError, err)
    return
    }
    c.JSON(http.StatusOK, translationResponse)
    })

    This route will be invoked with the transcript ID and the target language. First, the subtitles are downloaded from AssemblyAI using a similar code from the previous section. Then, the DeepL API is called by making a request to https://api-free.deepl.com/v2/translate, and the returned translations are sent to the frontend.

    Open the views/job.html file and add a select element to show the list of languages:

    <div id=”language” class=”m-5″>
    <select
    id=”language-select”
    class=”bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500″
    >
    <option value=”NONE” selected>Choose a language</option>
    <option value=”ES”>Spanish</option>
    <option value=”FR”>French</option>
    <option value=”DE”>German</option>
    <!– Add more options as needed –>
    </select>
    </div>

    You can find a list of languages supported by DeepL here.

    Inside the script tag, add the following code before all other existing code:

    document.getElementById(“language”).style.display = “none”;

    This code will hide the language selector when the page loads.

    Inside the setInterval call, add the following code after the createSubtitles call:

    document.getElementById(“language”).style.display = “block”;

    This brings back the language selector when the video is done loading.

    Add an event listener to the language selector, which will call the /translate route:

    document
    .getElementById(“language-select”)
    .addEventListener(“change”, function () {
    const selectedLanguage = this.value;
    if (selectedLanguage === “NONE”) {
    return;
    }
    document.querySelector(“video”).pause();

    const notice = document.createElement(“span”);
    notice.innerHTML = “Translating…”;
    document.getElementById(“video”).appendChild(notice);

    const formData = new FormData();
    formData.append(“language”, selectedLanguage);
    formData.append(“job_id”, “{{ .job.ID }}”);
    fetch(“/translate”, {
    method: “POST”,
    body: formData,
    })
    .then((response) => response.json())
    .then((data) =>
    createSubtitles(
    data.translations[0].text,
    selectedLanguage.toLowerCase()
    )
    )
    .catch((error) => {
    console.error(“Error:”, error);
    });
    });

    Apart from calling the /translate route, this code also displays a message saying “Translating…” so that the user knows it’s working. Once the translated subtitles are available, the createSubtitles function is called to add the translated subtitles.

    Modify the createSubtitles function to remove the “Translating…” notice if the target language is anything except English. Add this to the bottom of the function:

    if (srclang === “en”) {
    return;
    }
    document.querySelector(“#video span”).remove();
    alert(“Translations added”);

    This is how the file should look now:

    <!DOCTYPE html>
    <html lang=”en”>
    <head>
    <meta charset=”UTF-8″ />
    <meta name=”viewport” content=”width=device-width, initial-scale=1.0″ />
    <meta http-equiv=”X-UA-Compatible” content=”ie=edge” />
    <link
    href=”https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,900&display=swap”
    rel=”stylesheet”
    />
    <link
    rel=”stylesheet”
    href=”https://cdn.jsdelivr.net/npm/tw-elements/dist/css/tw-elements.min.css”
    />
    <script src=”https://cdn.tailwindcss.com”></script>
    <script>
    tailwind.config = {
    darkMode: “class”,
    theme: {
    fontFamily: {
    sans: [“Roboto”, “sans-serif”],
    body: [“Roboto”, “sans-serif”],
    mono: [“ui-monospace”, “monospace”],
    },
    },
    corePlugins: {
    preflight: false,
    },
    };
    </script>
    <title>Video Playback</title>
    </head>
    <body class=”flex flex-col items-center justify-center h-screen”>
    <span id=”loading”>Processing {{ .job.FilePath }} (Job {{ .job.ID }})</span>
    <div id=”video”></div>
    <div id=”language” class=”m-5″>
    <select
    id=”language-select”
    class=”bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500″
    >
    <option value=”NONE” selected>Choose a language</option>
    <option value=”ES”>Spanish</option>
    <option value=”FR”>French</option>
    <option value=”DE”>German</option>
    <!– Add more options as needed –>
    </select>
    </div>
    <script>
    document.getElementById(“language”).style.display = “none”;
    c = setInterval(() => {
    fetch(“/jobs/{{ .job.ID }}/status”)
    .then((res) => res.json())
    .then((data) => {
    if (data.status === “completed”) {
    const { subtitles } = data;
    const { file_path } = data.job;

    const video = document.createElement(“video”);
    video.src = “/” + file_path;
    video.controls = true;
    video.autoplay = false;
    video.loop = true;
    video.width = 500;
    video.style = “border-radius: 10px;”;

    let loading = document.getElementById(“loading”);
    document.body.removeChild(loading);
    document.getElementById(“video”).appendChild(video);

    createSubtitles(subtitles, “en”);
    document.getElementById(“language”).style.display = “block”;
    clearInterval(c);
    }
    });
    }, 3000);

    document
    .getElementById(“language-select”)
    .addEventListener(“change”, function () {
    const selectedLanguage = this.value;
    if (selectedLanguage === “NONE”) {
    return;
    }
    document.querySelector(“video”).pause();

    const notice = document.createElement(“span”);
    notice.innerHTML = “Translating…”;
    document.getElementById(“video”).appendChild(notice);

    const formData = new FormData();
    formData.append(“language”, selectedLanguage);
    formData.append(“job_id”, “{{ .job.ID }}”);
    fetch(“/translate”, {
    method: “POST”,
    body: formData,
    })
    .then((response) => response.json())
    .then((data) =>
    createSubtitles(
    data.translations[0].text,
    selectedLanguage.toLowerCase()
    )
    )
    .catch((error) => {
    console.error(“Error:”, error);
    });
    });

    function createSubtitles(subtitles, srclang) {
    const blob = new Blob([subtitles], { type: “text/vtt” });
    const url = URL.createObjectURL(blob);

    const video = document.querySelector(“video”);
    for (let i = 0; i < video.textTracks.length; i++) {
    video.textTracks[i].mode = “hidden”;
    }

    const track = document.createElement(“track”);
    track.src = url;
    track.kind = “subtitles”;
    track.label = srclang;
    track.srclang = srclang;
    track.mode = “showing”;
    video.appendChild(track);

    if (srclang === “en”) {
    return;
    }
    document.querySelector(“#video span”).remove();
    alert(“Translations added”);
    }
    </script>
    </body>
    </html>

    Restart the server and go through the entire process of uploading the video again. When the video loads, you’ll see the language selector:

    Selecting a language will trigger the translation, and you’ll get an alert when the translation is done:

    The new language will be added to the closed captions menu:

    You can now select the newly added subtitles and enjoy the translation.

    Note: In DeepL’s free plan, the text must be 128 KB or less in size, or it will need to be broken up into multiple texts. So, you’ll need to upload videos that aren’t too long.

    You can find the full code on GitHub.

    Conclusion

    In this article, you learned how AssemblyAI can be used to transcribe a video and generate subtitles. You then used DeepL to translate the subtitles to different languages on demand.

    AssemblyAI can do much more than generate subtitles. Features like speaker diarization, profanity filtering, custom spelling, and custom vocabulary make AssemblyAI a versatile tool for all your transcription needs.

    Source: Read More 

    Hostinger
    Facebook Twitter Reddit Email Copy Link
    Previous ArticleLast Week in AI #278 – Apple eyes OpenAI board seat, OpenAI caught storing chats in plain text, Kyutai’s Moshi, and more!
    Next Article The best early Prime Day deals on the weirdest tech we could find

    Related Posts

    Security

    Nmap 7.96 Launches with Lightning-Fast DNS and 612 Scripts

    May 16, 2025
    Common Vulnerabilities and Exposures (CVEs)

    CVE-2025-47916 – Invision Community Themeeditor Remote Code Execution

    May 16, 2025
    Leave A Reply Cancel Reply

    Hostinger

    Continue Reading

    Minecraft testing new flat world presets and a better way to locate your friends in-game

    News & Updates

    Spoofing Shein for Credential Harvesting

    Development

    Who is Vijay Gupta

    Development

    Linux Voice Assistants: Revolutionizing Human-Computer Interaction with Natural Language Processing

    Development
    Hostinger

    Highlights

    CVE-2023-53140 – “Linux Kernel SCSI Core /proc/scsi Directory Removal Vulnerability”

    May 2, 2025

    CVE ID : CVE-2023-53140

    Published : May 2, 2025, 4:15 p.m. | 34 minutes ago

    Description : In the Linux kernel, the following vulnerability has been resolved:

    scsi: core: Remove the /proc/scsi/${proc_name} directory earlier

    Remove the /proc/scsi/${proc_name} directory earlier to fix a race
    condition between unloading and reloading kernel modules. This fixes a bug
    introduced in 2009 by commit 77c019768f06 (“[SCSI] fix /proc memory leak in
    the SCSI core”).

    Fix the following kernel warning:

    proc_dir_entry ‘scsi/scsi_debug’ already registered
    WARNING: CPU: 19 PID: 27986 at fs/proc/generic.c:376 proc_register+0x27d/0x2e0
    Call Trace:
    proc_mkdir+0xb5/0xe0
    scsi_proc_hostdir_add+0xb5/0x170
    scsi_host_alloc+0x683/0x6c0
    sdebug_driver_probe+0x6b/0x2d0 [scsi_debug]
    really_probe+0x159/0x540
    __driver_probe_device+0xdc/0x230
    driver_probe_device+0x4f/0x120
    __device_attach_driver+0xef/0x180
    bus_for_each_drv+0xe5/0x130
    __device_attach+0x127/0x290
    device_initial_probe+0x17/0x20
    bus_probe_device+0x110/0x130
    device_add+0x673/0xc80
    device_register+0x1e/0x30
    sdebug_add_host_helper+0x1a7/0x3b0 [scsi_debug]
    scsi_debug_init+0x64f/0x1000 [scsi_debug]
    do_one_initcall+0xd7/0x470
    do_init_module+0xe7/0x330
    load_module+0x122a/0x12c0
    __do_sys_finit_module+0x124/0x1a0
    __x64_sys_finit_module+0x46/0x50
    do_syscall_64+0x38/0x80
    entry_SYSCALL_64_after_hwframe+0x46/0xb0

    Severity: 0.0 | NA

    Visit the link for more details, such as CVSS details, affected products, timeline, and more…

    How to Organize Your Figma Design Files Like a Pro

    June 12, 2024

    CSS – Advanced Animations and Transitions

    December 8, 2024

    SeaGOAT – semantic-code search engine

    July 3, 2024
    © DevStackTips 2025. All rights reserved.
    • Contact
    • Privacy Policy

    Type above and press Enter to search. Press Esc to cancel.