はじめに
iOS音声合成SDKは、すでに色々なアプリで活用されていると思います。
私は残念ながら今まで触ったことはありませんでした。今回、ある事情でゆめみのiOSエンジニア数名で、部活動的に3種類の音声合成SDKを試してみましたので、紹介します。
対象としたSDK
他にも候補はあったのですが、メンバーのスケジュールの都合上このあたりが精一杯でした🙇
気になるお値段
費用は気になるところだと思います。
下記のように、Apple純正は無料かつネット接続が必須ではないために、軽く使ってみる分にはお手頃です。
料金は各Webページを見ていただけると分かりますが、もう少し細かく料金設定されていますので、参考に留めて頂ければと思います。
もう一つの気になる合成の品質につきましては、後で述べたいと思います。
SDK(料金ページへのリンク) | 料金 | 1万文字あたりの料金 | インターネット接続 |
---|---|---|---|
Speech Synthesis | 無料 | 無料 | 不要 |
Amazon Polly | $4/100 万文字 (標準音声) | 4.56円 | 必要 |
GCP TextToSpeechAPI | $4/100 万文字 (標準音声) | 4.56円 | 必要 |
※ 1$ = 114円換算
実装について
実装は、各メンバーがそれぞれ担当しました。
SDK | Twitterアカウント |
---|---|
Speech Synthesis | @milanista_2nd |
Amazon Polly | @beowulf_tech (私) |
GCP TextToSpeechAPI | @potechi_if |
下記の参考ソースコードでは、play()関数まわりに絞って転記してみました。
Speech Synthesis
Appleのドキュメントは非常にそっけないです。
Convert text to spoken audio.
Overview
The Speech Synthesis framework manages voice and speech synthesis, and requires two primary tasks:
Create an AVSpeechUtterance instance that contains the text to speak. Optionally, configure speech parameters, such as voice and rate, for each utterance.
ソースコード
今回の実装の中では一番シンプルに記述できました。
再生用のコードを別に記述しなくても良いのが特徴でしょうか。
import Combine
import AVFoundation
final class AVSpeechSynthesizerUsecase: NSObject {
// 音声再生中か
let isSpeeching = PassthroughSubject<Bool, Never>()
private let synthesizer = AVSpeechSynthesizer()
override init() {
super.init()
self.synthesizer.delegate = self
}
// 日本語の声を抽出
static func voices() -> [AVSpeechSynthesisVoice] {
return AVSpeechSynthesisVoice.speechVoices().filter { $0.language == "ja-JP" }.sorted { voice1, voice2 in
voice1.name < voice2.name
}
}
func play(text: String, speechRate: Float, pitchMultiplier: Float, voiceType: Int, speechVolume: Float) {
isSpeeching.send(true)
let utterance = AVSpeechUtterance(string: text)
utterance.rate = speechRate
utterance.pitchMultiplier = pitchMultiplier
let voice = voice(for: voiceType)
if let voice = AVSpeechSynthesisVoice(identifier: voice.identifier) {
utterance.voice = voice
} else {
utterance.voice = AVSpeechSynthesisVoice(language: "ja-JP")
}
utterance.volume = speechVolume
synthesizer.speak(utterance)
}
private func voice(for voiceType: Int) -> AVSpeechSynthesisVoice {
let voices = AVSpeechSynthesizerUsecase.voices()
let index: Int = voiceType >= voices.count ? voices.count - 1 : voiceType
return voices[index]
}
}
extension AVSpeechSynthesizerUsecase: AVSpeechSynthesizerDelegate {
/// 音声再生が終了した際に呼ばれるdelegateメソッド
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
isSpeeching.send(false)
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) {
isSpeeching.send(false)
}
}
Amazon Polly
GCPも同様ですが、Amazon PollyはAWSとの接続が必要なためiOSアプリだけでは動作しません。
AWSの設定はそれだけで結構なボリュームになるので今回の記事では割愛させていただきます。
公式の解説はこちらです。
また、AWS公式のサンプルソースコードはこちらです。
以下、公式のページから引用します。
-
高品質— Amazon Polly は、新しいニューラル TTS とクラス最高の標準 TTS テクノロジーの両方を提供し、高い発音精度 (略語、頭字語の展開、日付/時刻の変換、同形異義語の読み分けを含む) で優れた自然音声を合成します。
-
低レイテンシー— Amazon Polly は応答が早いため、ダイアログシステムなどの低レイテンシーなユースケースにおいても選択肢になります。
-
言語と音声の大規模なポートフォリオをSupport— Amazon Polly は、多数のボイス言語をサポートしています。ほとんどの言語で男性と女性のボイスを選択できます。ニューラル TTS は現在、3 つの英国英語音声と 8 つの米国英語音声をサポートしています。さらに多くのニューラル音声が提供されるにつれて、この数は増え続ける予定です。米国英語の音声 Matthew と Joanna では、プロのニュースアンカーのようなニューラルニュースキャスターの話し方も使用できます。
-
コスト効率が良い— Amazon Polly は従量課金制であり、セットアップコストはかかりません。小規模で開始し、アプリケーションが大きくなるにつれてスケールアップできます。
-
クラウドベースソリューション— デバイス上の TTS ソリューションは、膨大なコンピューティングリソース、特に CPU パワー、RAM、ディスク容量を必要とします。そのため、開発コストが高くなり、またタブレットやスマートフォンなどのデバイスの電力消費も高くなります。これに対して、TTS 変換は AWS クラウド は、ローカルリソース要件を大幅に削減します。これにより、すべての利用可能な言語とボイスを可能な限りの最高品質でサポートできます。さらに、音声が改良されるとすぐにすべてのエンドユーザーが使用できるようになり、デバイスで追加更新する必要がありません。
ソースコード
AWSMobileClientとAWSPollyをCocoapodsで組み込みます。
platform :ios, '15.0'
use_frameworks!
target 'App' do
pod 'AWSMobileClient'
pod 'AWSPolly'
end
import AVFoundation
import AWSPolly
import Combine
final class AmazonPollyUsecase: NSObject, TextToSpeechProtocol {
static let defaultVoiceId = AWSPollyVoiceId.mizuki
private(set) var selectedVoiceId: AWSPollyVoiceId = AmazonPollyUsecase.defaultVoiceId
private var player: AVAudioPlayer?
private let queue = DispatchQueue(label: "AmazonPollyUsecase")
let isSpeeching = PassthroughSubject<Bool, Never>()
func play(text: String, speechRate: Float, pitchMultiplier: Float, voiceType: Int, speechVolume: Float) {
queue.async {
let voice = AmazonPollyUsecase.voices()
.enumerated()
.first(where: { $0.offset == voiceType })
.map { $0.element }
self.selectedVoiceId = voice?.voiceId ?? AmazonPollyUsecase.defaultVoiceId
self.generateSpeech(from: text) { url, _, error in
guard let url = url else {
// エラー
return
}
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(.playback, mode: .default, options: [])
} catch {
// エラー
}
// AVAudioPlayerで再生
self.player = try? AVAudioPlayer(contentsOf: url)
if let player = self.player {
player.prepareToPlay()
player.delegate = self
player.enableRate = true
player.rate = speechRate
player.volume = speechVolume
player.play()
self.isSpeeching.send(true)
} else {
// エラー
}
}
}
}
// Amazon Pollyに接続して、音声合成結果のファイルをダウンロードします。
private func generateSpeech(from text: String, completion: @escaping ((URL?, URLResponse?, Error?) -> Void)) {
let request = AWSPollySynthesizeSpeechURLBuilderRequest()
request.text = text
request.outputFormat = AWSPollyOutputFormat.mp3
request.voiceId = selectedVoiceId
let task = AWSPollySynthesizeSpeechURLBuilder.default().getPreSignedURL(request)
// Amazon Pollyによって音声合成を行う
task.continueOnSuccessWith { (awsTask: AWSTask<NSURL>) -> Any? in
guard let url = awsTask.result as URL? else { return nil }
// 音声合成されたファイルをダウンロードする
let config: URLSessionConfiguration = URLSessionConfiguration.default
let session: URLSession = URLSession(configuration: config)
let request: URLRequest = URLRequest(url: url)
let task: URLSessionDownloadTask = session.downloadTask(with: request) { url, response, error in
completion(url, response, error)
}
task.resume()
return nil
}
}
}
Pollyの日本語の声をリストアップします。
struct PollyVoice: Identifiable {
var id = UUID()
let voiceName: String
let voiceId: AWSPollyVoiceId
static var dummy: PollyVoice {
PollyVoice(voiceName: "mizuki", voiceId: AWSPollyVoiceId(rawValue: 0)!)
}
}
static func voices() -> [PollyVoice] {
var sortedVoices: [PollyVoice]?
let task = AWSPolly.default().describeVoices(AWSPollyDescribeVoicesInput())
task.continueOnSuccessWith { (awsTask: AWSTask<AWSPollyDescribeVoicesOutput>) -> Any? in
let data = (awsTask.result! as AWSPollyDescribeVoicesOutput).voices
sortedVoices = data!
.filter { $0.languageName == "Japanese" }
.map { PollyVoice(voiceName: $0.name ?? "", voiceId: $0.identifier) }
return nil
}
.waitUntilFinished()
guard let sortedVoices = sortedVoices else {
assertionFailure("音声が見つかりません")
return [.dummy]
}
return sortedVoices
}
音声再生の終了処理を行います。
extension AmazonPollyUsecase: AVAudioPlayerDelegate {
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
// 完了処理 ...
self.isSpeeching.send(false)
}
func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
// エラー処理...
self.isSpeeching.send(false)
}
}
GCP TextToSpeechAPI
標準音声
- 一般的な音声テクノロジーの 1 つであるパラメータ テキスト読み上げでは通常、ボコーダと呼ばれる信号処理アルゴリズムを介して出力を渡すことによって音声データを生成している。
- Text-to-Speech の標準音声も、このテクノロジーのバリエーションを使用している
WaveNet 音声
- Text-to-Speech による音声の作成方法は、合成音声技術が音声の機械モデルを作成する方法によって異なる
- WaveNetはGoogle アシスタント、Google 検索、Google 翻訳の音声生成に使用されているモデル
- WaveNet 音声を使うと他のテキスト読み上げシステムよりも自然な音声(より人間らしく、音節、音素、単語の強調や抑揚がある音声)が合成される。他の合成音声よりも暖かみがあり、人間のそれに似ていると感じられます。
final class GCPTextToSpeechUsecase: NSObject, TextToSpeechProtocol {
private struct Const {
static let apiURL: URL! = URL(string: "https://texttospeech.googleapis.com/v1/text:synthesize")
static let apiKey = "**************" // GCPで取得
}
private var player: AVAudioPlayer?
let isSpeeching = PassthroughSubject<Bool, Never>()
func play(text: String, speechRate: Float, pitchMultiplier: Float, voiceType: Int, speechVolume: Float) {
Task(priority: .background) { [weak self] in
guard let self = self, let requestParams = self.requestParams(text, voiceType, pitchMultiplier) else { return }
let headers = ["X-Goog-Api-Key": Const.apiKey, "Content-Type": "application/json; charset=utf-8"]
// リクエストを送信
let response = await self.postRequest(postData: requestParams, headers: headers)
// レスポンスからbase64エンコードされた音声合成データを取得
guard let audioContent = response["audioContent"] as? String else {
assertionFailure("無効なレスポンス")
return
}
// デコード(base64String → Data)
guard let audioData = Data(base64Encoded: audioContent) else {
return
}
self.player = try? AVAudioPlayer(data: audioData)
// 以下 Amazon Pollyの再生処理とほぼ同じ ...
}
}
private let requestParams: ((String, Int, Float) -> Data?) = { text, voiceType, pitch in
guard let voiceType = GCPVoiceType(rawValue: voiceType)?.identifier else { return nil }
let voiceParams: [String: Any] = [
"languageCode": "ja-JP",
"name": voiceType,
]
let requestParams: [String: Any] = [
"input": [
"text": text,
],
"voice": voiceParams,
"audioConfig": [
"pitch": pitch,
"audioEncoding": "LINEAR16",
],
]
return try? JSONSerialization.data(withJSONObject: requestParams)
}
// GCPのAPI音声合成データを取得すためのリクエストを送信します
private func postRequest(postData: Data, headers: [String: String]) async -> [String: AnyObject] {
var dict: [String: AnyObject] = [:]
var request = URLRequest(url: Const.apiURL)
request.httpMethod = "POST"
request.httpBody = postData
for header in headers {
request.addValue(header.value, forHTTPHeaderField: header.key)
}
let result = try? await URLSession.shared.data(for: request)
if let data = result?.0, let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: AnyObject] {
dict = json
}
return dict
}
}
聴き比べ
音声ファイルをQiitaにアップロードすることが難しそうだったので、品位の5段階評価を記載します。
5: 違和感がない読み上げ
4: 違和感があるが理解できる
3: 意味が分かりにくい
2: 意味が分からない
1: 読み上げの間違いが多く、意味が通じない
case 1
我々は宇宙人だ
SDK | 声 | 読み間違い | 品位(5段階) |
---|---|---|---|
Speech Synthesis | Kyoko | なし | 4 |
Amazon Polly | Takumi | なし | 5 |
GCP TextToSpeechAPI | waveNetFemale2 | なし | 5 |
Speech Synthesisは、シミュレータでは、「宇宙人」を「うちゅうひと」と読みます。
case 2
Macintoshは、Appleの創業者の一人、スティーブ・ジョブズの陣頭指揮のもとに開発された。
ジョブズの思想や夢、感性が設計思想に盛り込まれ、直感的で視覚的な操作インタフェース、画面に表示される文字フォントの細やかさや美しさ、画面と印刷物に表示される図像の精度(特にWYSIWYGの実現)、筺体の美しさなどが重視されている
SDK | 声 | 読み間違い | 品位(5段階) |
---|---|---|---|
Speech Synthesis | Kyoko | なし | 3 |
Amazon Polly | Takumi | なし | 5 |
GCP TextToSpeechAPI | waveNetFemale2 | なし | 5 |
Speech Synthesisは、実機でも「筺体」を「きょうからだ」と読んでいます。発話も辿々しいので、厳しく3!
おわり
音声合成の品位の評価はむずかしいので、余裕があったら定量的な評価に挑んでみたいと思います。
ソースコードは公開準備中です・・・