6715.jp

2017-05-25

クラウド東北きりたん その1 ~Win32APIでVOICEROIDを操作~

sekaisekai
PythonVOICEROIDWin32API東北きりたん

Win32APIでVOICEROIDをいじってみます

東北きりたん

VOICEROID+ 東北きりたん EXを買いました。

う~~~~んかわいい!!! かわいいです。

声もしっとりしていて完全にボク好みです。最高。

クラウドきりたん

いろいろ使いみちが思いつくんですが、Windowsでしか動かないのがネックです……

HTTPでテキストをぶん投げたら音声が飛んでくる感じになったら色々幸せじゃないですか。 ということで作っていきたいと思います。

Linuxで動かないかな?

Linuxで動かすとすれば、Wineですね。

Linux の Docker の中で voiceroid+ ゆかりさんを動かすという記事を見つけました。 どうやらWineで動くみたい?しかもDockerの中で。すごい!

試してみたんですが、うまくいきませんでした>< VOICEROID+ EXになってからいろいろ変わったんでしょうか。

自分でもWine環境を作って試してみたんですが、 .NET Framework 3.5のインストールがうまく行かず失敗。

ということでWineは諦めます。

WindowsServerで動かないかな?

動作環境には当然乗っていませんが、Windows Server 2016で適当に試してみたら普通に動きました。

ですが、VOICEROIDにはGUIしかありません。 CUIから操作できれば全て解決なんですが、用意されてません。かなしい。

ということで、Win32APIを叩いて自作プログラムからVOICEROIDの機能を使えるようにしてみましょう。 とはいっても、GUIを無理やり操作して動かすだけです。 筋肉ソリューション感が否めませんが、仕方がないです。

Win32APIを叩いてVOICEROIDを操作

このテの話は、「ウィンドウ 操作 Win32API」とかでググると無限に見つかるかと思うので、ザックリとだけ説明します。

SendMessage関数を使うとユーザのマウス操作やキーボード操作がエミュレートできるので、 うまい感じにテキストを入力させて保存ボタンを押させてあげれば、読み上げたwavファイルを得ることができそうです。

やりました

方針が定まったら書くだけ…… Pythonで書いてみました。

ffmpegを使っているので、別途用意が必要です。 必要なPythonのライブラリはpypiwin32です

pip install pypiwin32

コード

# coding: UTF-8

import os
import sys
import time
import hashlib
import threading
import subprocess

from win32con import *
from win32gui import *
from win32process import *

# 共通設定
waitSec = 0.5
windowName = "VOICEROID+ 東北きりたん EX"

def talk(inputText):
	# 出力先ディレクトリ作成
	outdir = "./output/"
	try:
		os.mkdir(outdir)
	except:
		pass

	# ファイルが存在してたらやめる
	outfile = outdir + hashlib.md5(inputText.encode("utf-8")).hexdigest() + ".mp3"
	if os.path.exists(outfile):
		return outfile

	# 一時ファイルが存在している間は待つ
	tmpfile = "tmp.wav"
	while True:
		if os.path.exists(outfile):
			time.sleep(waitSec)
		else:
			break

	while True:
		# VOICEROIDプロセスを探す
		window = FindWindow(None, windowName) or FindWindow(None, windowName + "*")

		# 見つからなかったらVOICEROIDを起動
		if window == 0:
			subprocess.Popen(["C:\Program Files (x86)\AHS\VOICEROID+\KiritanEX\VOICEROID.exe"])
			time.sleep(3 * waitSec)
		else:
			break

	while True:
		# ダイアログが出ていたら閉じる
		errorDialog = FindWindow(None, "エラー") or FindWindow(None, "注意") or FindWindow(None, "音声ファイルの保存")
		if errorDialog:
			SendMessage(errorDialog, WM_CLOSE, 0, 0)
			time.sleep(waitSec)
		else:
			break

	# 最前列に持ってくる
	SetWindowPos(window, HWND_TOPMOST, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOMOVE | SWP_NOSIZE)

	# 保存ダイアログの操作
	def enumDialogCallback(hwnd, param):
		className = GetClassName(hwnd)
		winText = GetWindowText(hwnd)

		# ファイル名を設定
		if className.count("Edit"):
			SendMessage(hwnd, WM_SETTEXT, 0, tmpfile)

		# 保存する
		if winText.count("保存"):
			SendMessage(hwnd, WM_LBUTTONDOWN, MK_LBUTTON, 0)
			SendMessage(hwnd, WM_LBUTTONUP, 0, 0)

	# 音声の保存
	def save():
		time.sleep(waitSec)

		# ダイアログがあれば操作する
		dialog = FindWindow(None, "音声ファイルの保存")
		if dialog:
			EnumChildWindows(dialog, enumDialogCallback, None)
			return

		# 再試行
		save()

	# VOICEROIDを操作
	def enumCallback(hwnd, param):
		className = GetClassName(hwnd)
		winText = GetWindowText(hwnd)

		# テキストを入力する
		if className.count("RichEdit20W"):
			SendMessage(hwnd, WM_SETTEXT, 0, inputText)

		if winText.count("音声保存"):
			# 最小化解除
			ShowWindow(window, SW_SHOWNORMAL)

			# 保存ダイアログ操作用スレッド起動
			threading.Thread(target=save).start()

			# 保存ボタンを押す
			SendMessage(hwnd, WM_LBUTTONDOWN, MK_LBUTTON, 0)
			SendMessage(hwnd, WM_LBUTTONUP, 0, 0)

	# VOICEROIDにテキストを読ませる
	EnumChildWindows(window, enumCallback, None)

	# プログレスダイアログが表示されている間は待つ
	while True:
		if FindWindow(None, "音声保存"):
			time.sleep(waitSec)
		else:
			break

	# MP3に変換
	subprocess.run(["ffmpeg", "-i", tmpfile, "-acodec", "libmp3lame", "-ab", "128k", "-ac", "2", "-ar", "44100", outfile])

	# 一時ファイルが存在していたら消す
	try:
		os.remove(tmpfile)
		os.remove(tmpfile.replace("wav", "txt"))
	except:
		pass

	return outfile

print(talk(sys.argv[1]))

注意

一度適当なテキストを読み上げさせ、スクリプトを実行するディレクトリに保存させておく必要があります。 保存先ダイアログを操作するときに、保存先ディレクトリを変更せずに保存させるため、 スクリプトの実行ディレクトリと同じところがデフォルトになっていないと以後の処理が失敗します。

手抜きです……

ハマりそうなポイント

  • ところどころにsleepを入れないと操作が失敗することがある
  • フォーカスが当たってないとか最小化されてるとかでボタン操作に失敗することがある
  • 出力が終わってない状況で新しい読み上げをさせようとすると死ぬ
    • 今回は前のが終わるまでブロックするようにした
  • Windowsのバージョンが違うと保存ウィンドウが違う気がするので上手く行かないかも
    • 今回はWindowsServer2016(Windows 10)です
  • 同じテキストの繰り返しを投げるとVOICEROIDがエラーを吐く
    • よくわからん

次回予告

ということで、Pythonから好きなテキストをVOICEROIDに送って読み上げたWAVを得ることができるようになりました。 コレだけでもうだいぶ夢が広がるカンジですね!!

次回は、コイツをクラウドで動かしていつでもどこでもきりたんボイスが作れる環境を作ります。