CoeFont CLOUDでオーディオブックを作成する

# はじめに
[CoeFont CLOUD](https://coefont.cloud/ "CoeFont LP")というAI音声合成サービス作っています．
今回はそのAPIを利用してオーディオブックを作成します．
[CoeFont APIドキュメント](https://coefont.cloud/webapi)

# CoeFont CLOUDとは
ざっくり言うと**色々な人の声で音声合成できるサービス**です．
あと自分の声でCoeFontも作れます．今回は使わないのでこちらについては説明しません．

>「CoeFont CLOUD」(https://coefont.cloud )はAI音声合成プラットフォームです。CoeFontとは、「声」を、手軽かつ表現力豊かな、「フォント」のようにする、というコンセプトの音声合成です。
従来では、50万円・10時間以上の収録を必要としていました。ですが、本サービスでは、500円・15分の収録で、それよりもかなり自然な発声のできる、CoeFont(AI音声合成)を作成できます。作成したCoeFontは、クラウド上で公開することができます。他のユーザーのCoeFontを利用して、音声合成も可能です。作成したCoeFontが利用されるたびに、CoeFontの作成者に収益として還元されます。またAPIを利用して、アプリやウェブサイトに組み込むこともできます。
[CoeFont CLOUDプレスリリースより](https://prtimes.jp/main/html/rd/p/000000006.000078329.html)

# オーディオブック作成
今回は青空文庫で『杜子春』を作ります。

## 単語登録
何もしないとCoeFontは杜子春を「もりこはる」と読みます．
これは固有名詞なので仕方がないでしょう．
まずCoeFontのLPからログインし，サイドバーの「CoeFontを使う」にあるいずれかの作品に行きます．
作品がなければ新規作成をしてください．
作品編集画面上部のユーザー辞書から単語と読みを登録します．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263501/a69e8ad1-97be-cdc8-428a-df44bd89b668.png)
登録するとAPIからでも読みが補正されます．

## 作成
APIから音声を作成します．
- 青空文庫のtextファイルをtextDirに置きます
- textファイルを読み込み，句点と改行で分割したListを作成
- for文で区切った文を順番に投げます
- 音声を結合します
- 完成！
for文で繰り返しリクエストする理由はリクエストする文字列に禁止ワードなど含むと400が返ってくるからです．
CoeFont CLOUDは悪用防止のためかなり厳しい禁止ワードが設定されている．切り取りでも禁止ワードは作成できないようになっている．

```python
import hmac
from typing import List
import os
import requests
import json
import hashlib
import re
import wave
import itertools
import datetime

from pydub import AudioSegment

import env

accesskey: str = env.accesskey
access_secret: str = env.access_secret

nobel_name: str = 'toshishun'
sentences_limit: int = 500


# テキストファイルから文章を取得
def get_text(filename):
    with open(filename, 'r', encoding='shift_jis') as f:
        text = f.read()
    return text


# テキストから一致する正規表現を削除
def del_ruby(t, *args):
    '''
    text : ルビを含む文章
    *args : ルビなどの正規表現のstrを持つlist
    '''
    for w in args:
        t = re.sub(w, '', t)
    return t


def req_api(text: str, font: str = 'Averuni', volume=3.0):
    '''
    text : 生成する文章
    font : 利用するCoeFont
    volume : 音声のvolume
    '''
    signature = hmac.new(bytes(access_secret, 'utf-8'), text.encode('utf-8'), hashlib.sha256).hexdigest()
    url = 'https://api.coefont.cloud/text2speech'

    response = requests.post(url, data=json.dumps({
        'coefont': font,
        'text': text,
        'accesskey': accesskey,
        'signature': signature,
        'volume': volume
    }), headers={'Content-Type': 'application/json'})
    return response.status_code, response.content


def join_waves(inputs, output):
    '''
    inputs : list of filenames
    output : output filename
    '''
    try:
        fps = [wave.open(f, 'r') for f in inputs]
        fpw = wave.open(output, 'w')

        fpw.setnchannels(fps[0].getnchannels())
        fpw.setsampwidth(fps[0].getsampwidth())
        fpw.setframerate(fps[0].getframerate())

        for fp in fps:
            fpw.writeframes(fp.readframes(fp.getnframes()))
            fp.close()
        fpw.close()

    except wave.Error as e:
        print(e)

    except Exception as  e:
        print('unexpected error -> ' + str(e))


os.makedirs('audiobook', exist_ok=True)
# 結合前のwavを置いとく
os.makedirs(os.path.join('audiobook', nobel_name), exist_ok=True)
# 作品の名前のwavs/nameに音声を置く
wavs_dir_path: str = os.path.join(os.path.join('audiobook', nobel_name, 'wavs'))
os.makedirs(wavs_dir_path, exist_ok=True)

text = get_text(f'text/{nobel_name}.txt')
ruby_list = ['《.+?》', '［＃１０字下げ］', '［＃.+?］', '〔.+?〕', '-{,10}']

text = del_ruby(text, *ruby_list)
print(f'文字数 {len(text)}')
sentence_list: List[str] = text.split('\n')
sentence_list = list(itertools.chain.from_iterable([s.split('。') for s in sentence_list])) + ['ボイスド　バイ コエフォントクラウド']

sentence_num: int = len(sentence_list)
fail_list: List[int] = []

with open(os.path.join(wavs_dir_path, 'sentences.json'), 'w') as f:
    json.dump(sentence_list, f, indent=2, ensure_ascii=False)

for i in range(sentence_num if sentence_num < 500 else sentences_limit):
    print(f'{datetime.datetime.now()} {str(i + 1).zfill(len(str(sentence_num)))}/{sentence_num} {sentence_list[i]}')
    file_name: str = f'{i}.wav'
    save_path: str = os.path.join(wavs_dir_path, file_name)
    if os.path.exists(save_path):
        continue
    if sentence_list[i] == '':
        AudioSegment.silent(duration=1000).export(save_path, 'wav')
        continue
    status, content = req_api(sentence_list[i])
    print(f'status {status}')
    if status == 200:
        with open(save_path, 'wb') as f:
            f.write(content)
    fail_list += [i]
    if status == 400:
        print('無効な文字列を含む可能性があります')

with open(os.path.join(wavs_dir_path, 'fail_sentences.json'), 'w') as f:
    json.dump(fail_list, f, indent=2, ensure_ascii=False)

out_path: str = os.path.join('audiobook', nobel_name, f'{nobel_name}.wav')
join_waves([os.path.join(wavs_dir_path, f'{i}.wav') for i in range(sentence_num if sentence_num < 500 else sentences_limit) if i not in fail_list], out_path)

```

:::note warn
作成したオーディオブックの公開をする際はプランに応じたクレジット表記を行いましょう
:::

# おわり
数学科の授業のPDFでオーディブック作ろうとしたけどexistsなどが読めなかったので諦めました．
集合位相のPDFではだいぶいいのができました．