SHAREVOX core の API を探る(2)ReadLine

Explore the SHAREVOX core API (2) ReadLine


2022/12/20
藤田昭人


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 を見てもらうとして…

ja.wikipedia.org

要はシェルのライン・エディティング機能を 提供してくれるライブラリーでして、 今回のように UTF-8 のコード入力が必須の場合 これをかました方が安全なんです。 ヒストリー機能も使えるしね。

Mac の場合、 インストールはホームブリューを使うのが 一番お手軽です。次のページに簡単な解説が 書いてあるので見てもうとして…

qiita.com

このページに掲載されている サンプルコードと 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 ファイルの再生機能を紹介します。

それではお約束の…

#つくよみちゃんを利用してフォロワー増やしたい

以上

*1:クラッキング防止のためなんだそうですが…

*2:Apple vs Google の構図なんでしょうかね?

*3:正直に答えましょう。 僕が見たのはこちら…

www.youtube.com

そもそもニコ動に縁のなかった僕には この手の動画は当然ノーマークで… だって、こんなところから テクニカルな情報を拾えるなんて思ってなかったもん。

でも…

結局これが 「唯一ヘルニアになって良かったこと」 になりました😀