+5

Speech to Text - Tạo subtitle tự động cho video

Chắc hẳn đã có đôi lần bạn tìm thấy một video trên mạng nói về đúng vấn đề mà mình đang quan tâm nhưng hơi buồn là khả năng nghe tiếng anh của bạn không được tốt nên nhiều khi làm cho việc tìm kiếm trước đó trở nên vô nghĩa. Giá như video đó có thêm phần subtile thì tốt biết mấy. Còn nếu như nó không có thì chúng ta sẽ tự tạo cho nó vậy.

Speach To Text là gì

Đây là dịch vụ chuyển đổi âm thanh giọng nói thành văn bản được hỗ trợ bởi Google, Azure (Microsoft) hay Apple. Tuy nhiên với một tài khoản free thì Azure của Microsoft hỗ trợ cho chúng ta tốt hơn cả. Vì thế trong bài viết này mình sẽ hướng dẫn các bạn sử dụng Nodejs để tạo ra một ứng dụng tự động tạo subtitle cho video nhé. Nào bắt đầu thôi !

Tạo tài khoản Azure

Các bạn có thể vào đây để tạo tài khoản Azure, nếu có mastercard thì sẽ được hỗ trợ tốt hơn. Đây là mà hình dashboard sau khi đã đăng kí thành công tài khoản Azure.

Các bạn vào phần Resource groups và tạo mới một subcription

Với mỗi một subcription đăng ký chúng ta sẽ được cung cấp subscription key, đây là thứ chung ta cần để có thể sử dụng được SDK mà Azure cung cấp.

Xây dựng ứng dụng

Chúng ta sẽ tạo ra 4 service tương ứng với 4 bước mà chúng ta cần phải làm để có thể tạo được subtitle cho video. Và cuối cùng, trong routes/transcriptions.js chúng ta sẽ gọi chúng ra như thế này:

const express = require('express')
const router = express.Router()
const Uploader = require('../services/uploader')
const Converter = require('../services/converter')
const Transcription = require('../services/transcription')
const Subtitle = require('../services/subtitle')

router.post('/transcriptions', async (req, res) => {
  Uploader.saveFile(req, res)
    .then(filename => Converter.videoToAudio(filename))
    .then(filename => Transcription.speechToText(filename))
    .then(result => Subtitle.write(result))
    .then(subtitle => res.send({subtitle: subtitle}))
})

module.exports = router

Toàn bộ source code mình có để trên github. Các bạn có thể vào đây để xem chi tiết hơn.

Bước 1: Thực hiện lưu lại video được gửi lên từ phía client

Tương tự như việc xử lý upload file trong Nodejs. Chúng ta sẽ xử dụng formidable để lưu video vào thư mục đã được định sẵn ở trong config.uploadDir.

const path = require('path')
const fs = require('fs')
const formidable = require('formidable')
const config = require('../config')

class Uploader {
  constructor() {}
}

Uploader.form = new formidable.IncomingForm()
Uploader.form.multiples = false
Uploader.form.keepExtensions = true
Uploader.form.uploadDir = config.uploadDir
Uploader.form.maxFileSize = 200 * 1024 * 1024;

Uploader.saveFile = function(req, res) {

  fs.existsSync(config.uploadDir) || fs.mkdirSync(config.uploadDir)

  this.form.onPart = (part) => {
    if (!part.filename || part.mime.match(/audio\/|video\//)) {
      this.form.handlePart(part)
    } else {
      res.send({error: 'File type not allowed'})
    }
  }

  this.form.parse(req)

  this.form.on('fileBegin', (field, file) => {
    file.path = file.path.replace('upload_', '')
  })

  this.form.on('error', function(error) {
    res.send({error: error})
  })

  return new Promise((resolve, reject) => {
    this.form.on('file', (name, file) => {
      resolve(path.basename(file.path))
    })
  })
}

module.exports = Uploader

Để ý rằng config.uploadDir là thư mục chúng ta sử dụng để lưu video cũng như các file kết quả khác trong quá trình thực hiện. Sau khi thực hiện xong việc lưu video vào thư mục định trước, service Uploader sẽ trả về tên video để chuyển sang bước 2.

Bước 2: Convert video sang audio

Mặc định, SDK của Azure chỉ chấp nhận đầu vào là một mono audio có định dạng .wav. Chúng ta sử dụng ffmpeg để thực hiện việc chuyển đổi này:

const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path;
const ffmpeg = require('fluent-ffmpeg');
const crypto = require('crypto')
const fs = require('fs')
const config = require('../config')

ffmpeg.setFfmpegPath(ffmpegPath);

class Converter {
  constructor() {}
}

Converter.videoToAudio = function(videoName) {
  const videoPath = config.uploadDir + videoName
  const audioName = crypto.randomBytes(16).toString("hex") + ".wav"
  const audioPath = config.uploadDir + audioName
  
  return new Promise((resolve, reject) => {
    ffmpeg(videoPath).withNoVideo().outputFormat('wav').audioChannels(1)
      .audioBitrate(128).audioFrequency(16000).save(audioPath)
      .on('end', () => {
        resolve(audioName)
        fs.unlinkSync(videoPath)
      })
  })
}

module.exports = Converter

Service Converter sẽ nhận đầu vào là tên video và chuyển đổi nó sang một audio có các thông số theo yêu cầu của Azure. Sau quá trình xử lý, chúng ta sẽ trả về tên của audio để dùng nó cho các bước tiếp theo. Đồng thời sẽ xóa video này đi vì lúc này nó không còn cần thiết nữa.

Bước 3: Chuyển đổi audio thành văn bản

Để sử dụng SDK của Azure, chúng ta chạy lệnh sau để cài đặt:

npm install microsoft-cognitiveservices-speech-sdk

Nói qua một chút về quá trình chuyển đổi này, mỗi một audio sẽ được chia ra thành các khoảng thời gian từ 15 đến 30s. Kết quả trả về của một đoạn sẽ phụ thuộc vào đoạn đằng trước và đằng sau nó. Lý do cho điều này là để tránh mất mát dữ liệu cũng như việc tạo ra các văn bản phù hợp với ngữ cảnh hơn. Service Transcription sẽ nhận vào tên của audio và thực hiện việc chuyển đổi. YOUR_SERVICE_REGIONYOUR_SUBSCRIPTION_KEY các bạn có thể tìm nó ở mục Overview trong phần thông tin chi tiết của subscription mà bạn đã đăng ký.

const fs = require('fs')
const sdk = require('microsoft-cognitiveservices-speech-sdk')
const config = require('../config')

class Transcription {
  constructor() {}
}

Transcription.serviceRegion = 'YOUR_SERVICE_REGION'
Transcription.subscriptionKey = 'YOUR_SUBSCRIPTION_KEY'

Transcription.speechToText = function(filename, language = 'en-US') {
  const filePath = config.uploadDir + filename
  const pushStream = sdk.AudioInputStream.createPushStream()

  fs.createReadStream(filePath)
    .on('data', function(arrayBuffer) {
      pushStream.write(arrayBuffer.buffer)
    })
    .on('end', function() {
      pushStream.close()
    })

  const audioConfig = sdk.AudioConfig.fromStreamInput(pushStream)
  const speechConfig = sdk.SpeechConfig.fromSubscription(
    this.subscriptionKey,
    this.serviceRegion
  )

  speechConfig.speechRecognitionLanguage = language

  const recognizer = new sdk.SpeechRecognizer(speechConfig, audioConfig)
  
  recognizer.startContinuousRecognitionAsync()

  return new Promise((resolve, reject) => {
    let result = []
    
    recognizer.recognized = (r, event) => {
      result.push(JSON.parse(event.privResult.privJson))
    }
  
    recognizer.sessionStopped = () => {
      resolve(result)
      fs.unlinkSync(filePath)
    }
  })
}

module.exports = Transcription

Kết quả của mỗi đoạn sẽ được trả về trong event recognizer.recognized. Chúng ta sẽ lưu nó vào trong một biến result và trả về để đi đến bước cuối cùng.

Bước 4: Ghi subtitle cho video

Ở bước thứ 3 chúng ta nhận được một mảng data là dữ liệu của các đoạn tương ứng với các khoảng thời gian được tách ra từ audio. Việc của chúng ta sẽ là đọc và ghi ra một file có định dạng .vtt. Service Subtitle cuối cùng sẽ trả về tên file subttile tương ứng sau khi đã thực hiện xong công việc của mình.

const fs = require('fs')
const crypto = require('crypto')
const config = require('../config')

class Subtitle {
  constructor() {}
}

Subtitle.write = function(subtitle) {
  const fileName = crypto.randomBytes(16).toString("hex") + ".vtt"
  const filePath = config.uploadDir + fileName
  const stream = fs.createWriteStream(filePath)
  
  return new Promise((resolve, reject) => {
    stream.once('open', function() {
      let duration = 0
      stream.write('WEBVTT')
      subtitle.forEach(sub => {
        let startTime = duration
        duration = duration + sub.Duration
  
        if(sub.DisplayText) {
          stream.write('\n\n')
          stream.write(timeConvert(startTime) + " --> " + timeConvert(duration))
          stream.write('\n')
          stream.write(sub.DisplayText)
        }
      })
      stream.close()
      resolve(fileName)
    })
  })
}

function timeConvert(time) {
  time = time / 10000000
  var seconds = (time % 60).toFixed(3)
  var minutes = Math.floor(time / 60) % 60
  var hours = Math.floor(time / 3600)

  minutes = minutes < 10 ? `0${minutes}` : minutes
  seconds = seconds < 10 ? `0${seconds}` : seconds
  hours = hours < 10 ? `0${hours}` : hours

  return `${hours}:${minutes}:${seconds}`
}

module.exports = Subtitle

Kiểm tra trong thư mục config.uploadDir chúng ta sẽ thấy có một file định dạng .vtt, mở nó lên để xem chúng ta đã nhận được những gì nhé.

Conclusion

Vừa rồi mình đã giới thiệu về dịch vụ Speech To Text và ứng dụng nó vào việc tự động tạo subtitle cho video. SDK của Azure còn hỗ trợ việc chuyển đổi âm thanh giọng nói từ ngôn ngữ này sang văn bản của ngôn ngữ khác và ngược lại. Rất may mắn là nó có hỗ trợ cả tiếng việt, các bước thực hiện cũng tương tự như ở trên chỉ khác ở phần setting thông số đầu vào và đầu ra trong cho SDK. Không chỉ tạo subtitle cho video, các bạn có thể sử dụng nó vào việc tạo ra một ứng dụng có khả năng điều khiển bằng giọng nói hay nhiều thứ hay ho khác. Mình có demo ứng dụng của mình ở đây, quá trình lấy subtitle mất khá nhiều thời gian nên thường sẽ bị lỗi timeout. Vì vậy các bạn hoàn toàn có thể chạy dưới local của mình để có được kết quả tốt nhất nhé.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí