藤田昭人
5日間連続投稿の2回目、 今回はテクニカル(?)なトピックから始めます。
実はこの春先まで僕は、 音声合成を含む機械学習のアルゴリズムの実行には GPU を使った実行環境が必須だと思い込んでいました。 なので昨年、シンデレラの童話読み聞かせを作ったときも Chrome で標準的に使える Web Speech API を迷わず使っていました。 噂によればこの API は 裏で Google の音声認識と音声合成が 動いているのだとか… 「まぁ、そう言うもんなんだろう」 という認識だったのです。 っていうかぁ… 無料の割には音声認識の精度が高かったので 結構満足していた訳で、 難点はブラウザ内蔵のAPIの面倒くささ*1と、 あと iPhone では音声認識が動かない*2ぐらいかな? と思っていたのです。
ところが、 夏の椎間板ヘルニアで 寝たきりになっている時期、 本当にやれることが無くて 暇つぶしに スマホ相手にググりまくっている時期に たまたま見つけたのが VOICEVOX だった訳です*3。
…って語ってると 明日のマクラがなくなるので、 本日はこの辺で止めて 本題に入ります。
【注】昨日の diff に誤りがありました
昨日の記事 の simple_tts.cpp の修正箇所に漏れがありました。 正しくはこちら。
$ diff -u ../example/cpp/unix/simple_tts.cpp simple_tts.cpp --- ../example/cpp/unix/simple_tts.cpp 2022-10-28 17:21:34.000000000 +0900 +++ simple_tts.cpp 2022-12-20 13:35:13.000000000 +0900 @@ -2,7 +2,7 @@ #include <iostream> #include <string> -#include "../../../core/src/core.h" +#include "../core/src/core.h" #define OUTPUT_WAV_NAME "audio.wav" @@ -17,7 +17,7 @@ std::cout << "coreの初期化中..." << std::endl; - if (!initialize("../../../model", false)) { + if (!initialize("../model", false)) { std::cout << "coreの初期化に失敗しました" << std::endl; return 1; } #
修正前のコードだと model ディレクトリのパスが違うので初期化でコケます。
それから OpenJTalk の辞書も simple_tts ディレクトリに
コピーしておく必要があります。
関連ファイルが正しく配置できると…
$ ls CMakeLists.txt open_jtalk_dic_utf_8-1.11 build simple_tts.cpp maverick:simple_tts fujita$ ./build/simple_tts 'こんにちは' coreの初期化中... openjtalk辞書の読み込み中... 音声生成中... 音声ファイル保存中... 音声ファイル保存完了 (audio.wav) maverick:simple_tts fujita$ ls CMakeLists.txt open_jtalk_dic_utf_8-1.11 audio.wav simple_tts.cpp build $
見てのとおり audio.wav が生成されます。
Simple_TTS は遅い
さて、 この simple_tts は WAV ファイルを生成するだけ (再生はしてくれない) のも困りものですが、 とにかく実行が遅い。
実行時間を測ってみると…
$ time -p ./build/simple_tts 'こんにちは' coreの初期化中... openjtalk辞書の読み込み中... 音声生成中... 音声ファイル保存中... 音声ファイル保存完了 (audio.wav) real 18.64 user 18.95 sys 0.40 $
なんと 18 秒もかかっています。 「ひょっとして初期化がノロいのでは?」 と考えたので次のようなプログラムを 書いてみました。
$ cat c.cpp #include "../core/src/core.h" int main() { initialize("../model", false); finalize(); } $ g++ -o ccc c.cpp -Wl,-rpath,../core/lib ../core/lib/libcore.dylib ld: warning: dylib (../core/lib/libcore.dylib) was built for newer macOS version (11.7) than being linked (11.0) $ time -p ./ccc real 18.88 user 17.97 sys 0.38 $
このプログラムは初期化処理(initialize)と 終了処理(finalize)だけを実行する2行のプログラムです。 ld コマンドが出力する waring は忘れてもらうとして…
どうやら simple_tts がノロいのは model ファイルを取り込む 初期化処理に非常に時間がかかっているようです。 言い換えれば WAVファイルを生成する sharevox_tts の実行はそれほど時間を要しない。
であれば…
起動時に初期化処理を行い、 その後は対話形式で text-to-speach を実行する 対話型のプログラムを作れば良いことになります。
コマンド t2s
…という事で、 simple_tts にちょこっとコードを追加して 対話型のコマンドにすることにしました。 名付けて t2s 。もちろん爺さん趣味です😁
GNU ReadLine
対話型インターフェースといえば GNU ReadLine です。詳細は Wikipedia を見てもらうとして…
要はシェルのライン・エディティング機能を 提供してくれるライブラリーでして、 今回のように UTF-8 のコード入力が必須の場合 これをかました方が安全なんです。 ヒストリー機能も使えるしね。
Mac の場合、 インストールはホームブリューを使うのが 一番お手軽です。次のページに簡単な解説が 書いてあるので見てもうとして…
このページに掲載されている サンプルコードと simple_tts.cpp を ドッキングすることにしました。 (詳細は後述の「ソースコード」の項を参照)
CMakeLists.txt
コマンド t2s のディレクトリは 昨日の記事 の simple_tts ディレクトリの隣におくことにしました。 なので CMakeLists.txt は昨日作成したものをちょいと修正するだけです。
$ diff -u CMakeLists.txt.orig CMakeLists.txt --- CMakeLists.txt.orig 2022-12-19 10:33:15.000000000 +0900 +++ CMakeLists.txt 2022-12-20 15:01:02.000000000 +0900 @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(SimpleTTS) +project(T2S) set(CMAKE_CXX_STANDARD 11) find_path(PARENT NAMES "sharevox_core-0.1.2" PATHS "../..") @@ -9,5 +9,5 @@ find_library(CORE_LIB NAMES core PATHS "${TOP_DIR}/core" PATH_SUFFIXES lib) message(STATUS "CORE_LIB: ${CORE_LIB}") -add_executable(simple_tts simple_tts.cpp) -target_link_libraries(simple_tts ${CORE_LIB}) +add_executable(t2s t2s.cpp) +target_link_libraries(t2s ${CORE_LIB} readline) $
名前を simple_tts から t2s に変更し、 target_link_libraries に readline を追加しました。
ソースコード
ソースコード t2s.cpp はこちら。
#include <stdio.h> #include <string.h> #include <stdlib.h> #include "../core/src/core.h" #define MODEL "../../model" #define OPENJTALK_DIC "../open_jtalk_dic_utf_8-1.11" #define OUTPUT_WAV_NAME "audio.wav" void init() { SharevoxResultCode result; if (!initialize(MODEL, false)) { printf("coreの初期化に失敗しました\n"); exit(1); } else { printf("coreの初期化に成功しました\n"); } printf("openjtalk辞書の読み込み中..."); result = sharevox_load_openjtalk_dict(OPENJTALK_DIC); if (result != SHAREVOX_RESULT_SUCCEED) { printf("\n%s\n", sharevox_error_result_to_message(result)); exit(1); } else { printf("終了\n"); } } int generate(const char *text, int64_t speaker, int *output_size, uint8_t **output) { SharevoxResultCode result; printf("音声生成中..."); result = sharevox_tts(text, speaker, output_size, output); if (result != SHAREVOX_RESULT_SUCCEED) { printf("\n%s\n", sharevox_error_result_to_message(result)); return(-1); } printf("終了: %d bytes\n", *output_size); return(0); } int save(const char *name, uint8_t *output, int size) { FILE* fp; printf("音声ファイル保存中..."); fp = fopen(name, "wb"); if (fp == NULL){ printf("失敗\n"); return(-1); } if (fwrite(output, size, 1, fp) < 1 ){ printf("ファイルへの書き込みに失敗しました。\n"); return(-1); } fclose(fp); printf("音声ファイル保存完了 (%s)\n", name); return(0); } void term() { finalize(); } // readline #include <readline/readline.h> #include <readline/history.h> int numMB(char *str) { if ((*str & 0x80) == 0x00) return(1); if ((*str & 0xE0) == 0xC0) return(2); if ((*str & 0xF0) == 0xE0) return(3); if ((*str & 0xF8) == 0xF0) return(4); if ((*str & 0xFC) == 0xF8) return(5); if ((*str & 0xFE) == 0xFC) return(6); return(-1); } int main() { char *line = NULL; int n; SharevoxResultCode result; int64_t speaker_id = 0; int output_binary_size = 0; uint8_t *output_wav = nullptr; init(); while (1) { line = readline("> "); if (line == NULL || strlen(line) == 0) { free(line); continue; } n = numMB(line); if (n == 1) { if (strcmp(line, "q") == 0 || strcmp(line, "quit") == 0 || strcmp(line, "exit") == 0) { free(line); break; } } if (n == 3) { printf("[ja] %s\n", line); if (generate(line, speaker_id, &output_binary_size, &output_wav) < 0) exit(1); if (save(OUTPUT_WAV_NAME, output_wav, output_binary_size) < 0) exit(1); sharevox_wav_free(output_wav); } else { printf("[%d] %s\n", n, line); } add_history(line); free(line); } printf("exit\n"); term(); exit(0); }
前述のとおり、 サンプルコードと simple_tts.cpp を シンプルにドッキングした内容です。 ミソは int numMB(char *str) なる関数。 この関数はマルチバイト文字のバイト数を判定します。 返り値が1なら1バイト文字(=英字)、 返り値が3なら3バイト文字(≒漢字)で、 t2s の対話インターフェースでは 3バイト文字から始まる文字列はWAVファイルを生成するテキスト、 1バイト文字から始まる文字列は t2s の内部コマンドと判定しています。 もっとも、このコードでは内部コマンドは exit/quit/q の1つだけです。
ビルドと実行
トップディレクトリの直下に t2s ディレクトリを作成し、 上記の open_jtalk_dic_utf_8-1.11 と CMakeLists.txt と t2s.cpp を作成したら、 昨日の記事 とほぼ同様の手順でビルドができます。
$ rm -r build $ mkdir build $ cd build/ $ cmake .. -- The C compiler identification is AppleClang 13.0.0.13000029 -- The CXX compiler identification is AppleClang 13.0.0.13000029 -- Detecting C compiler ABI info -- Detecting C compiler ABI info - done -- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped -- Detecting C compile features -- Detecting C compile features - done -- Detecting CXX compiler ABI info -- Detecting CXX compiler ABI info - done -- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped -- Detecting CXX compile features -- Detecting CXX compile features - done -- PARENT: /Users/fujita/xtr/BookBot/BookBot3/04_T2S_ReadLine -- TOP_DIR: /Users/fujita/xtr/BookBot/BookBot3/04_T2S_ReadLine/sharevox_core-0.1.2 -- CORE_LIB: /Users/fujita/xtr/BookBot/BookBot3/04_T2S_ReadLine/sharevox_core-0.1.2/core/lib/libcore.dylib -- Configuring done -- Generating done -- Build files have been written to: /Users/fujita/xtr/BookBot/BookBot3/04_T2S_ReadLine/sharevox_core-0.1.2/t2s/build $ cmake --build . [ 50%] Building CXX object CMakeFiles/t2s.dir/t2s.cpp.o [100%] Linking CXX executable t2s [100%] Built target t2s $ $ ./t2s coreの初期化に成功しました openjtalk辞書の読み込み中...終了 > こんにちは [ja] こんにちは 音声生成中...終了: 69504 bytes 音声ファイル保存中...音声ファイル保存完了 (audio.wav) > q exit $
予想したとおり、一度 initialize を実行して model をロードすると、 sharevox_tts の実行は短時間です。 が、やはり WAV ファイルの再生機能がないと t2s コマンドは役に立ちそうにありません。
明日は WAV ファイルの再生機能を紹介します。
それではお約束の…
#つくよみちゃんを利用してフォロワー増やしたい
以上