search
LoginSignup
1

私は英語が苦手です。とくに発音が苦手です。
なのでビデオ通話などで海外の方と英語で話すときは、とても緊張します。
ちゃんと聞き取ってもらえるのかと。

また海外の方が英語で話しているとき、聞き取れないこともよくあります。

そこで、ビデオ通話にリアルタイムで翻訳字幕をつけてみました。

完成形

今回つくったビデオ通話アプリのデモです。なお私の英語が下手すぎて、うまく自動翻訳できていない部分があります。

日本語ユーザーの話した言葉が、英語ユーザーには英語に翻訳されて表示されます。逆も同様です。
captions.png

コードはGitHubで公開しています。npmのworkspacesという機能を使って、フロント側とサーバー側のコードを分けています。
https://github.com/miya-start/trtc-captions

処理の流れ

以下は、日本語ユーザーの映像・音声・テキストデータが、英語ユーザーに届くまでの大まかな流れです。

flow.png

まず、映像と音声データについて。これはTencent CloudのSDKを使ってやりとりしています。これによりWebでビデオ通話ができます。

次に、字幕のためのテキストデータについて。以下の流れで処理をしています。

  1. まずChromeのWeb Speech APIを利用します。このAPIにより、ブラウザで認識した日本語の音声がGoogleのサーバーに送信され、日本語のテキスト(文字)に変換されます。

  2. 1で変換されたテキストデータを、自前のサーバー(Node.js)に送信します。通信にはWebSocketを利用しています。

  3. 2で受け取ったテキストデータを、Tencent Cloudの翻訳APIを使って英語に翻訳します。

  4. 3で翻訳された英語のテキストデータを、英語ユーザーのブラウザに送信します。

実際につくってみる

それでは実際につくっていきます。

Tencent CloudのSDKを使ったビデオ通話の実装については、以下の公式ドキュメントを参考にしました。

Tencent RTC SDK - Tutorial: Quick Start Call - Documentation

SDKの利用方法について、最初は専門用語が多くて戸惑いました。しかし上記ドキュメントに掲載されている図が分かりやすく、ビデオと音声通話の基本的な流れを理解できました。

次に、字幕の実装について。先ほど説明した大まかな流れにそって実装します。

1. Web Speech APIを利用する

Web Speech APIは、ブラウザで音声認識などを行うためのAPIです。音声認識の結果は、文字列として取得できます。

詳しくは、以下が参考になりました。

Web Speech APIを利用しブラウザで音声を認識する方法

なお今回の実装では、フロント側にReactを使っています。ReactでWeb Speech APIを利用するにあたって、以下を使いました。Reactのカスタムフックを通して、認識した音声をテキストに変換できます。

JamesBrill/react-speech-recognition: 💬Speech recognition for your React app

2. WebSocketを使って、自前のサーバーにテキストデータを送信する

音声からテキストに変換されたデータを、自前のサーバーに送信します。通信にはWebSocketを利用します。リアルタイムで字幕を表示するためには、すばやく双方向で通信しなければならないからです。

自前のサーバー側にはNode.jsを使っています。WebSocketの利用にあたっては、以下のライブラリを使いました。

Socket.IO

なおビデオ通話の音声・映像データも、WebSocketを使って通信しています。ただこちらはTencent CloudのSDKを使えば良いので、こちらで実装する必要はありません。

3. 翻訳APIを使って、日本語のテキストデータを英語に翻訳する

Tencent Cloudには、翻訳のためのSDKも用意されています。これを使って、日本語のテキストデータを英語に翻訳します。

SDKの使い方は、以下が参考になりました。閲覧するためには、Tencent Cloudのアカウントが必要です。

Tencent Cloud - Console

Node.js用のSDKは、以下のリポジトリにあります。

4. 翻訳したテキストデータを、英語ユーザーのブラウザに送信する

3で翻訳された英語のテキストデータを、英語ユーザーのブラウザに送信します。こちらもWebSocketを使って通信します。

今回書いたサーバー側のコードは以下です。

src/index.ts
import { createServer } from 'http'
import path from 'path'
import dotenv from 'dotenv'
import express from 'express'
import { Server, Socket } from 'socket.io'
import tencentcloud from 'tencentcloud-sdk-nodejs-tmt'
import { TextTranslateResponse } from 'tencentcloud-sdk-nodejs-tmt/tencentcloud/services/tmt/v20180321/tmt_models'

type Languages = 'en' | 'ja'

type MessageReceived = {
  isTranscriptEnded: boolean
  language: Languages
  transcript: string
  time: number
  userId: string
}

type MessageToEmit = MessageReceived & {
  translates: readonly (readonly [Languages, string])[]
}

if (process.env.NODE_ENV !== 'production') {
  dotenv.config({ path: path.resolve(process.cwd(), '.env.local') })
}

const app = express()
const httpServer = createServer(app)
const io = new Server(httpServer)

const TmtClient = tencentcloud.tmt.v20180321.Client
const client = new TmtClient({
  credential: {
    secretId: process.env.TENCENT_SECRET_ID,
    secretKey: process.env.TENCENT_SECRET_KEY,
  },
  region: 'ap-singapore',
  profile: {
    httpProfile: {
      endpoint: 'tmt.tencentcloudapi.com',
    },
  },
})

function textTranslate(
  text: string,
  target: Languages
): Promise<TextTranslateResponse> {
  const params = {
    SourceText: text,
    Source: 'auto',
    Target: target,
    ProjectId: 0,
  }

  return client.TextTranslate(params)
}

async function emitMessage({
  languageSet,
  message,
  roomId,
  socket,
}: {
  languageSet: Set<Languages>
  message: MessageReceived
  roomId: string
  socket: Socket
}): Promise<void> {
  const responsePromises = [...languageSet]
    .filter((language) => language !== message.language)
    .map((language) =>
      textTranslate(message.transcript, language).catch((err) =>
        console.error('error', err)
      )
    )
  const responses = await Promise.all(responsePromises)

  const messageToEmit: MessageToEmit = {
    ...message,
    translates: responses
      .flatMap((response) => (response == null ? [] : [response]))
      .filter(
        (
          response
        ): response is TextTranslateResponse & { Target: Languages } => {
          if (languageSet.has(response.Target as Languages)) return true
          throw Error(`Invalid language: ${response.Target}`)
        }
      )
      .map(({ Target, TargetText }) => [Target, TargetText]),
  }
  socket.to(roomId).emit('receive-message', messageToEmit)
}

const languageSet: Set<Languages> = new Set()
io.on('connection', (socket) => {
  let roomId = '0'
  console.log(`connect ${socket.id}`)

  socket.on('join-room', (iroomId: string) => {
    socket.join(iroomId)
    roomId = iroomId
  })

  socket.on('send-language', (language: Languages) => {
    languageSet.add(language)
    console.log(`send-language ${socket.id} ${language}`)
  })

  socket.on('send-message', async (message: MessageReceived) => {
    console.log(`send-message ${socket.id} ${message.transcript}`)
    console.log('languages', languageSet)
    emitMessage({
      languageSet,
      message,
      roomId,
      socket,
    })
  })

  socket.on('disconnect', () => {
    console.log(`disconnect ${socket.id}`)
  })
})

httpServer.listen(3004, () => {
  console.log('Listening on port 3004!')
})

Tencont Cloudのメリット・デメリット

最後に、Tencent Cloudを使ってみた感想です。

メリットとしては、まず無料枠が多いです。たとえばビデオ・音声通話の場合、毎月10,000分まで無料で利用できます。料金の詳しい計算式は、以下のPDFなどを参照してください。

Tencent Real-Time Communication Purchase Guide

またTencent Cloudの主要なSDKには、TypeScriptの型定義が付いています。なのでTypeScriptを使って開発するときはありがたいです。

デメリットとしては、日本語のドキュメントが少ないです。英語のドキュメントはだいたいあります。ただコードのコメントは、中国語のみの場合があります。

またこれはTencent Cloudに限ったことではありませんが、中国のサービスということで法的リスクを感じます。具体的には、2017年に施行された国家情報法などの問題です。日本国内向けのアプリ開発などの実務で使うときは、法的に問題ないかよく確認する必要がありそうです。なお先述のとおり、試しに使ってみる分には良かったです。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1