0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

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

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

処理の流れ

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

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は、以下のリポジトリにあります。
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

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

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

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

0
1
0

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
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?