私は英語が苦手です。とくに発音が苦手です。
なのでビデオ通話などで海外の方と英語で話すときは、とても緊張します。
ちゃんと聞き取ってもらえるのかと。
また海外の方が英語で話しているとき、聞き取れないこともよくあります。
そこで、ビデオ通話にリアルタイムで翻訳字幕をつけてみました。
処理の流れ
以下は、日本語ユーザーの映像・音声・テキストデータが、英語ユーザーに届くまでの大まかな流れです。
まず、映像と音声データについて。これはTencent CloudのSDKを使ってやりとりしています。これによりWebでビデオ通話ができます。
次に、字幕のためのテキストデータについて。以下の流れで処理をしています。
-
まずChromeのWeb Speech APIを利用します。このAPIにより、ブラウザで認識した日本語の音声がGoogleのサーバーに送信され、日本語のテキスト(文字)に変換されます。
-
1で変換されたテキストデータを、自前のサーバー(Node.js)に送信します。通信にはWebSocketを利用しています。
-
2で受け取ったテキストデータを、Tencent Cloudの翻訳APIを使って英語に翻訳します。
-
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の利用にあたっては、以下のライブラリを使いました。
なおビデオ通話の音声・映像データも、WebSocketを使って通信しています。ただこちらはTencent CloudのSDKを使えば良いので、こちらで実装する必要はありません。
3. 翻訳APIを使って、日本語のテキストデータを英語に翻訳する
Tencent Cloudには、翻訳のためのSDKも用意されています。これを使って、日本語のテキストデータを英語に翻訳します。
SDKの使い方は、以下が参考になりました。閲覧するためには、Tencent Cloudのアカウントが必要です。
Node.js用のSDKは、以下のリポジトリにあります。
tencentcloud-sdk-nodejs
4. 翻訳したテキストデータを、英語ユーザーのブラウザに送信する
3で翻訳された英語のテキストデータを、英語ユーザーのブラウザに送信します。こちらもWebSocketを使って通信します。
今回書いたサーバー側のコードは以下です。
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年に施行された国家情報法などの問題です。日本国内向けのアプリ開発などの実務で使うときは、法的に問題ないかよく確認する必要がありそうです。なお先述のとおり、試しに使ってみる分には良かったです。