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

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

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

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

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

![flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140258/bc3249ca-af77-0cc8-55e8-37616c189eac.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](https://web.sdk.qcloud.com/trtc/webrtc/doc/en/tutorial-11-basic-video-call.html)

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

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

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

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

[Web Speech APIを利用しブラウザで音声を認識する方法](https://www.twilio.com/blog/speech-recognition-browser-web-speech-api-jp)

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

[JamesBrill/react-speech-recognition: 💬Speech recognition for your React app](https://github.com/JamesBrill/react-speech-recognition)

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

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

[Socket.IO](https://socket.io/)

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

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

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

[Tencent Cloud - Console](https://console.tencentcloud.com/api/explorer?Product=tmt&Version=2018-03-21&Action=TextTranslate)

Node.js用のSDKは、以下のリポジトリにあります。
[tencentcloud-sdk-nodejs](https://github.com/tencentcloud/tencentcloud-sdk-nodejs)


## 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](https://main.qcloudimg.com/raw/document/intl/product/pdf/tencent-cloud_647_17155_en.pdf)

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

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

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