VOICEROID2で自動読み上げ&保存させてみた

## はじめに
VOICEROIDを使って何かコンテンツを作りたいと思ったのですが、**VOICEROIDのソフトウェアが入っていないパソコンでは音声を聞くことができない！！**

じゃあ、「API探して何とかしよう！」と思い、APIを探したのですがまったく見つからなかったので、入力されたテキストをVOICEROIDに通し、音声ファイルを保存、それを呼びだすものを作ってみました。

**参照記事**
[VOICEROID2(紲星あかり)をプログラムから動かしてみる](https://qiita.com/Teara/items/936733c9e7e47b5ebe79)
[Inspectツールを使ってUIを調査するには？［Windows 8／Windows 8.1ストアアプリ開発］](https://www.atmarkit.co.jp/ait/articles/1404/03/news004.html)

**使用したものなど**
python3.8最新バージョンでの動作確認済み（3.9で```pip install pywinauto```入れるとエラーが起きてます。）2020年10月時点
{tkinter, wave, pywinauto}

## 具体的な構造
python3のtkinterを使って入力ボックス、送信ボタン、キャラクターの画像を表示させる。
送信ボタンを押すとVOICEROIDソフトにテキストが送られてくるのでそれを音声として保存する。(pywinautoを使用)

## VOICEROID2の操作を見ていく
[参照記事](https://qiita.com/Teara/items/936733c9e7e47b5ebe79)に書いてあるものを参考にプログラムを見ていくと、ウィンドウのハンドルを取得することが今回のかなめであることが分かる。
どのように動いているかしるために、UIの動きを見ることのできるInspect.exeというツールを使います。

## 1、音声保存する流れを見る
「-」は下層を示してます

```
デスクトップ
- "VOICEROID2" ウィンドウ
-- "" カスタム
--- ""ボタン
---- "音声保存"テキスト
```

## 2、音声保存する流れを見る
「-」は下層を示してます

```
デスクトップ
- "VOICEROID2" ウィンドウ
-- "名前を付けて保存" ダイアログ
--- "保存(S)"ボタン
```

## 3、音声保存する流れを見る
「-」は下層を示してます

```
デスクトップ
- "VOICEROID2" ウィンドウ
-- "名前を付けて保存" ダイアログ
--- "名前を付けて保存" ダイアログ 
----| "はい(Y)"ボタン
----| "いいえ(N)"ボタン
```

## 4、音声保存する流れを見る
「-」は下層を示してます

```
デスクトップ
- "VOICEROID2" ウィンドウ
-- "音声保存" ウィンドウ
--- "情報" ダイアログ 
---- "OK"ボタン
```

##対応ものを取り出してコードに書いてみる
**ここ**が一番めんどくさいです。同じような名前が複数出てくるので、階層がおかしくなる。
頭が痛くなるかもしれないので曲をかけたり、コーヒー飲みながらやりましょう。

## プログラムコードの前に(補足)
from voice2 import talkVOICEROID2は[参照記事](https://qiita.com/Teara/items/936733c9e7e47b5ebe79)をそのまま使用しています。
予めVOICEROID2を起動してからvoiceroidtalk.pyを実行しましょう。
VOICEROID2の音声保存から「名前を付けて保存」に移動するときに確認ウィンドウがあるのですが、今回は「次回以降省略する」に設定しています

## メイン画面

```voiceroidtalk.py
import wave
import winsound as ws
import tkinter
import sys
import tkinter.messagebox as tkm
import time
from voice2 import talkVOICEROID2
from voiceroid2_1 import talkVOICEROID2_1
from voiceroid2_2 import talkVOICEROID2_2
from voiceroid2_3 import talkVOICEROID2_3
from voiceroid2_4 import talkVOICEROID2_4
import time

root=tkinter.Tk()
root.geometry("1920x1080")
root.title(u"aoi talk")

#なんでもいいので自分で指定した場所にpngの画像を用意してください。以下　例
kotonoha=tkinter.PhotoImage(file="C:/Users/***/Desktop/voice/01.png")
canvas=tkinter.Canvas(bg="white",width=475,height=750)
canvas.place(x=1300,y=250)
canvas.create_image(0,0, image=kotonoha, anchor=tkinter.NW)

def addlist(text):
    mysay="you: "+ text
    print(mysay)
    listbox.insert(tkinter.END,mysay)
    chat = "Aoi: " + talk(text)

    #if 0<=int(text):
    #    aoivoice=int(text)

    Entry1.delete(0, tkinter.END)
    chatCut(chat)
    #addRep(aoi)

def chatCut(chat):    
    aoi=chat
    addRep(aoi)

def addRep(aoi):
    listbox.insert(tkinter.END, aoi)
    #ボイス処理
    voiceroid=aoi[5:]
    voiceroid=voiceroid+"っ"
    word=len(voiceroid)
    destime=round(word/7+0.1,1)
    #destime=round(word/7+1.1,1)
    #デスクトップ上で喋らせない場合以下のコメントアウトを付ける
    #talkVOICEROID2(voiceroid)
    #time.sleep(destime)
    #-----------zzz
    #timeの遅延処理はスペックに合わせて変えること
    talkVOICEROID2_1(voiceroid)
    time.sleep(0.3)
    talkVOICEROID2_2(voiceroid)
    time.sleep(0.2)
    talkVOICEROID2_3(voiceroid)
    time.sleep(0.2)
    talkVOICEROID2_4(voiceroid)

def talk(say):
    if say == 'end':
        return ('ではまた')
    else:
        return (say)

static=tkinter.Label(text=u"葵ちゃんに話しかけてね！")
static.pack()

Entry1=tkinter.Entry(width=50)
Entry1.insert(tkinter.END,u"こんにちは")
Entry1.pack()

button=tkinter.Button(text=u"送信", width=50,command=lambda: addlist(Entry1.get()))
button.pack()

listbox=tkinter.Listbox(width=55,height=15)
listbox.pack()

root.mainloop()

```

## 処理部分
もっと簡単にまとめられる力があれば...

```voiceroid2_1.py
# -*- coding: utf-8 -*-
import pywinauto

def search_child_byclassname_1(class_name, uiaElementInfo, target_all = False):
    target = []
    # 全ての子要素検索
    for childElement in uiaElementInfo.children():

        # ClassNameの一致確認
        if childElement.class_name == class_name:
            if target_all == False:
                return childElement
            else:
                target.append(childElement)
    if target_all == False:
        # 無かったらFalse
        return False
    else:
        return target

def search_child_byname_1(name, uiaElementInfo):
    # 全ての子要素検索
    for childElement in uiaElementInfo.children():

        # Nameの一致確認
        if childElement.name == name:
            return childElement
    # 無かったらFalse
    return False

def talkVOICEROID2_1(speakPhrase):
    # デスクトップのエレメント
    parentUIAElement = pywinauto.uia_element_info.UIAElementInfo()

    # voiceroidを捜索する
    voiceroid2 = search_child_byname_1("VOICEROID2",parentUIAElement)
    # *がついている場合
    if voiceroid2 == False:
        voiceroid2 = search_child_byname_1("VOICEROID2*",parentUIAElement)

    #ここから変更
    # テキスト要素のElementInfoを取得
    TextEditViewEle = search_child_byclassname_1("TextEditView",voiceroid2)
    textBoxEle = search_child_byclassname_1("TextBox",TextEditViewEle)

    # コントロール取得
    textBoxEditControl = pywinauto.controls.uia_controls.EditWrapper(textBoxEle)

    # テキスト登録
    textBoxEditControl.set_edit_text(speakPhrase)

    # ボタン取得
    buttonsEle = search_child_byclassname_1("Button",TextEditViewEle,target_all = True)
    # 再生ボタンを探す
    playButtonEle = ""
    for buttonEle in buttonsEle:
        # テキストブロックを捜索
        textBlockEle = search_child_byclassname_1("TextBlock",buttonEle)
        if textBlockEle.name == "音声保存":
            playButtonEle = buttonEle
            break

    # ボタンコントロール取得
    playButtonControl = pywinauto.controls.uia_controls.ButtonWrapper(playButtonEle)

    # 再生ボタン押下
    playButtonControl.click()


```

```voiceroid2_2.py
#2回目
# -*- coding: utf-8 -*-
import pywinauto

def search_child_byclassname_2(class_name, uiaElementInfo, target_all = False):
    target = []
    # 全ての子要素検索
    for childElement in uiaElementInfo.children():
        # ClassNameの一致確認
        if childElement.class_name == class_name:
            if target_all == False:
                return childElement
            else:
                target.append(childElement)
    if target_all == False:
        # 無かったらFalse
        return False
    else:
        return target

def search_child_byname_2(name, uiaElementInfo):
    # 全ての子要素検索
    for childElement in uiaElementInfo.children():
        # Nameの一致確認
        if childElement.name == name:
            return childElement
    # 無かったらFalse
    return False

def talkVOICEROID2_2(speakPhrase):
    # デスクトップのエレメント
    parentUIAElement = pywinauto.uia_element_info.UIAElementInfo()
    # voiceroidを捜索する
    voiceroid2 = search_child_byname_2("VOICEROID2",parentUIAElement)
    # *がついている場合
    if voiceroid2 == False:
        voiceroid2 = search_child_byname_2("VOICEROID2*",parentUIAElement)

    #ここから変更
    # 名前を付けて保存　要素のElementInfoを取得
    saveEle = search_child_byclassname_2("#32770",voiceroid2)

    playsaveEle = search_child_byclassname_2("Button",saveEle,target_all = False)

    # ボタンコントロール取得
    playButtonControl = pywinauto.controls.uia_controls.ButtonWrapper(playsaveEle)

    # 再生ボタン押下
    playButtonControl.click()
```

```voiceroid2_3.py
#3回目
# -*- coding: utf-8 -*-
import pywinauto

def search_child_byclassname_3(class_name, uiaElementInfo, target_all = False):
    target = []
    # 全ての子要素検索
    for childElement in uiaElementInfo.children():
        # ClassNameの一致確認
        if childElement.class_name == class_name:
            if target_all == False:
                return childElement
            else:
                target.append(childElement)
    if target_all == False:
        # 無かったらFalse
        return False
    else:
        return target

def search_child_byname_3(name, uiaElementInfo):
    # 全ての子要素検索
    for childElement in uiaElementInfo.children():
        # Nameの一致確認
        if childElement.name == name:
            return childElement
    # 無かったらFalse
    return False

def talkVOICEROID2_3(speakPhrase):
    # デスクトップのエレメント
    parentUIAElement = pywinauto.uia_element_info.UIAElementInfo()
    # voiceroidを捜索する
    voiceroid2 = search_child_byname_3("VOICEROID2",parentUIAElement)
    # *がついている場合
    if voiceroid2 == False:
        voiceroid2 = search_child_byname_3("VOICEROID2*",parentUIAElement)

    #ここから変更
    # 1つ目の名前を付けて保存 要素のElementInfoを取得
    saveEle = search_child_byclassname_3("#32770",voiceroid2)
    
    #2つ目の名前を付けて保存 (はい or いいえ) 要素のElementInfoを取得
    resaveEle = search_child_byclassname_3("#32770",saveEle)
    
    # 2つ目の中のハンドル内を格納する (YES ボタン取得)
    yessEle = search_child_byclassname_3("Button",resaveEle,target_all = True)
    # はいボタンを探す
    playyesEle = ""
    for yesEle in yessEle:
        # 1つ目の引数に今いるハンドル、2つ目の引数に前の保存ハンドル
        # 今いるところの中にあるnameはいを探す
        a=search_child_byclassname_3("#32770",saveEle)
        if yesEle.name=="はい(Y)":
            playyesEle=yesEle
            break

    # ボタンコントロール取得
    playButtonControl = pywinauto.controls.uia_controls.ButtonWrapper(playyesEle)

    # 再生ボタン押下
    playButtonControl.click()
```

```voiceroid2_4.py
#4回目
# -*- coding: utf-8 -*-
import pywinauto

def search_child_byclassname_4(class_name, uiaElementInfo, target_all = False):
    target = []
    # 全ての子要素検索
    for childElement in uiaElementInfo.children():
        # ClassNameの一致確認
        if childElement.class_name == class_name:
            if target_all == False:
                return childElement
            else:
                target.append(childElement)
    if target_all == False:
        # 無かったらFalse
        return False
    else:
        return target

def search_child_byname_4(name, uiaElementInfo):
    # 全ての子要素検索
    for childElement in uiaElementInfo.children():
        # Nameの一致確認
        if childElement.name == name:
            return childElement
    # 無かったらFalse
    return False

def talkVOICEROID2_4(speakPhrase):
    # デスクトップのエレメント
    parentUIAElement = pywinauto.uia_element_info.UIAElementInfo()
    # voiceroidを捜索する
    voiceroid2 = search_child_byname_4("VOICEROID2",parentUIAElement)
    # *がついている場合
    if voiceroid2 == False:
        voiceroid2 = search_child_byname_4("VOICEROID2*",parentUIAElement)

    #ここから変更
    # 音声保存ウィンドウ (保存確認) 要素のElementInfoを取得
    wavEle = search_child_byclassname_4("Window",voiceroid2)
    # 情報ダイアログ 要素のElementInfoを取得
    infoEle = search_child_byclassname_4("#32770",wavEle)
    
    # YES　ボタン取得
    yessEle = search_child_byclassname_4("Button",infoEle,target_all = True)
    # はいボタンを探す
    playyesEle = ""
    for yesEle in yessEle:
        # テキストブロックを捜索

        w=search_child_byclassname_4("#32770",wavEle)
        if yesEle.name=="OK":
            playyesEle=yesEle
            break

    # ボタンコントロール取得
    playButtonControl = pywinauto.controls.uia_controls.ButtonWrapper(playyesEle)

    # 再生ボタン押下
    playButtonControl.click()
```
## 最後に
無事取り出せたよ！！アオイチャンカワイイヤッター(疲労)
まあ現段階だとVOICEROID2持っている人しか実行できないのですが、これをwebページとかで行えるようにしていきたいですね。