PPTファイルに音声を自動で埋め込む その5 Pythonによる音声合成ソフトの制御

プログラミング

pptファイルにオーディオデータを埋め込むアプリケーションソフトに,音声合成ソフトのGUIを操作するコードを追加しました。pptxフォーマットのファイルにオーディオデータを埋め込むコードはPythonで作成し,GUI操作で実行するものに比べ圧倒的に高速であることがわかりました。次のステップとしては,pptxファイルからアニメーション起動の情報を読み取るコードを作成するつもりでした。しかし予定を変更し,音声合成ソフトのGUIを操作するコードを追加することにしました。音声合成ソフトは株式会社エーアイさんの「かんたん!AITalk®5」(以下,AITalkと記します)にアップデートしました。動作確認できたので,紹介します。

<詳しく読む>

PPTファイルに音声を自動で埋め込む その4の末尾に書いたように,音声合成ソフトを制御する機能の優先度は低いと考えていました。音声合成ソフトのGUIを操作してオーディオデータを生成して保存するので,Pythonでコードを作ったからといって速くなるわけではないからです。(GUIを使わないで操作できる製品群も提供されていますが,お値段の関係で手が出ません。)

それで,PythonでGUIを制御するコードへの移行は(興味があるのですが)後回しにしていました。しかし,ちょっと事情が変わってきました。音声合成ソフトを変更することになったからです。

実は,作業をしていたPCのDドライブが壊れてしまったのです。DドライブにはPythonの開発環境やソースコード,音声合成ソフトの「かんたん!AITalk®3」が入っていました。幸い,Pythonのソースコードはバックアップを取っていましたし「かんたん!AITalk®3」は再インストール可能です。しかし自分でイントネーションなどを調整して登録してきた辞書データは全部消えてしまいました。

ソフトの再イストールのため株式会社エーアイさんのサイトにアクセスしたところ後継ソフトの「かんたん!AITalk®5」にアップデートできるということがわかり,それをインストールすることにしました。

「かんたん!AITalk®5」と「かんたん!AITalk®3」ではユーザインタフェースが若干異なっているので,AHKスクリプトを直す必要があります。それならPythonで作り直してしまおうということになりました。それに,オーディオデータ作成の制御と埋め込みのコードは1つのアプリケーションソフトとしてまとまっていた方が便利だからです。

<閉じる>

アプリの概要

下がアプリの操作画面です。“Add new audio”というラベルのボタンが1つ増えています。

作成したアプリの操作画面

このボタンをクリックすると,音声合成の操作を開始します。”Script File:”のテキストボックスで設定さたテキスト形式のスクリプトファイルを読み込み,中に書かれているナレーションのテキストをAITalkのテキストボックスにコピーして音声合成します。得られたwav形式のオーディオファイルは,格納するためのフォルダ(Narration Folder:で指定する)に書き込みます。このフォルダの内容は,音声合成の前に全て削除されます。

全体の処理の流れ

GUIの外観はあまり変わっていないのですが,アプリケーションの内部の構造に手を加えました。初めにAHKで作成したコードは,スクリプトの指示に従って音声を合成してファイルを保存するもので,「スクリプトに従って働く仮想機械」をイメージして設計しています。スクリプトファイルのテキストを1行ずつ読み込んで保存するwavファイルの名前を決め,音声合成ソフトのGUIを制御しています。このコードのGUI制御部分をPowerpoint用に変更して,wav形式のデータをpptに埋め込むAHKコードを作りました。

Pythonで作成したオーディオデータ埋め込みソフトの設計は,この初めのイメージに引きずられています。しかし,このソフトに音声合成ソフトの制御コードをそのまま追加すると,コードの重複した部分が多くなり,拡張性やメンテナンスの点で問題があります。

そこで,機能追加をしやすいようにコードを少し整理しました。下に,全体の処理の流れの概念図を示します。(この図は,コード全体の機能を説明するためのものです。実際のコードの構造を示しているものではありません。例えば,read_script_file()関数は,make_audio_files()関数などの内部で呼び出されますが,独立したブロックとして描いています。)

アプリの処理の流れ

コードは以下のような機能の部分に分かれています。

  • main
     GUIのボタンやテキストボックスなどを生成し,ボタンをクリックすると予め定められた関数を呼び出す。
  • pptxファイルの処理とAITalkの制御をする関数群
    mainから呼び出され,zip解凍したpptxファイルにオーディオデータを埋め込んだり,AITalkのGUIを制御したりする複数の関数がある。
  • スクリプトの解析部分
    スクリプトの書かれたテキストファイルを読み込んで,pptxファイルの処理やAITalkの制御をするために必要なパラメータのリストを作成する。パラメータは,現時点では,アニメーションの番号(スライド番号,クリック番号,自動アニメーション番号),wavファイルのパス,ナレーションフォルダのパス。各関数は,このリストの順に処理やGUI制御を実行していく。

mainルーチンと関数の間のデータのやりとりは,基本的には関数の引数をつかっています。図の中で“p_info”と書いてあるのは,関連する複数のファイルやフォルダのパスの情報をまとめたクラスのインスタンスです。クラスといっても,C言語で複数の変数やパラメータをまとめて扱うのに使う「構造体」のような使い方しかしていません。また,このインスタンスp_infoはグローバル宣言して使っています。

関数間のデータの共有の方法については,今後,変更する可能性があります。

コードの説明

AITalkのGUIを操作する関数とその周辺を中心に説明していきます。コードの全部を掲載すると長くなるので,抜粋したもので説明します。

メイン関数

メイン部分のコードは以下のようになります。

# Main
root = tk.Tk()
root.title('AddAudio2pptx')
root.geometry("940x680")
frame = tk.Frame(root)
pyautogui.FAILSAFE = False

# Get Working Directory
wd = os.getcwd()
# Arrangeent of Widget
fonts = ("", 14)
fonts_b = ("", 11)
b_width = 60
pptx_lb = tk.Label(text='PPTX File Name:', font = fonts)
pptx_lb.grid(row = 1, column = 0, padx=10, pady=10,sticky=tk.W)
pptx_box = tk.Entry(width = b_width, font = fonts_b)
pptx_box.grid(row = 1, column = 1, padx=10, pady=10,sticky=tk.W)
pptx_btn = tk.Button(root,text ="Set",command =set_pptx_file,font =fonts)
pptx_btn.grid(row = 1, column = 2, padx=10, pady=10,sticky=tk.W)

chk_st = tk.BooleanVar()
chk_st.set(True)
chk = tk.Checkbutton(variable=chk_st, text='Auto Set')
chk.grid(row = 1, column = 3, padx=10, pady=10,sticky=tk.W)

dir_lb = tk.Label(text='Narration Folder:', font = fonts)
dir_lb.grid(row = 2, column = 0, padx=10, pady=10,sticky=tk.W)
dir_box = tk.Entry(width = b_width, font = fonts_b)
dir_box.grid(row = 2, column = 1, padx=10, pady=10,sticky=tk.W)
dir_btn = tk.Button(root,text ="Set",command =set_narration_folder,font =fonts)
dir_btn.grid(row = 2, column = 2, padx=10, pady=10, sticky=tk.W)

scpt_lb = tk.Label(text='Script File:', font = fonts)
scpt_lb.grid(row = 3, column = 0, padx=10, pady=10,sticky=tk.W)
scpt_box = tk.Entry(width=b_width, font = fonts_b)
scpt_box.grid(row = 3, column = 1, padx=10, pady=10,sticky=tk.W)
scpt_btn = tk.Button(root,text ="Set",command =set_script_file,font =fonts)
scpt_btn.grid(row = 3, column = 2,padx=10, pady=10, sticky=tk.W)

text_box = tk.Text(width=80, height = 20, font = fonts_b)
text_box.grid(row = 4, column = 0, columnspan = 4, padx=10, pady=10,sticky=tk.W)
xbar = tk.Scrollbar( orient = 'horizontal' )
ybar = tk.Scrollbar( orient = 'vertical' )
xbar.grid(row = 5, column = 0, columnspan =4, padx=10, pady=10,sticky=tk.W+tk.E)
ybar.grid(row = 4, column = 3, padx=10, pady=10,sticky=tk.W)
text_box[ 'xscrollcommand' ] = xbar.set
text_box[ 'yscrollcommand' ] = ybar.set
xbar[ 'command' ] = text_box.xview
ybar[ 'command' ] = text_box.yview

make_audio_btn = tk.Button(root,text ="Make new audio",command =run_make_audio, font =fonts)
make_audio_btn.grid(row = 6, column = 0,padx=10, pady=10, sticky=tk.W)

exe_btn = tk.Button(root,text ="Add audio",command =run_add_audio_files, font =fonts)
exe_btn.grid(pyautogui.FAILSAFE = False = 6, column = 1,padx=10, pady=10, sticky=tk.W)  
root.mainloop()

マウスポインタの位置が限界を超えたとき

pyautogui.FAILSAFE = False

GUIの制御中にマウスポインタの位置が限界を超えてエラーにることを防ぐため,このような設定にしてある。

ファイル/フォルダを設定するボタン

pptx_lb = tk.Label(text='PPTX File Name:', font = fonts)
pptx_lb.grid(row = 1, column = 0, padx=10, pady=10,sticky=tk.W)
pptx_box = tk.Entry(width = b_width, font = fonts_b)
pptx_box.grid(row = 1, column = 1, padx=10, pady=10,sticky=tk.W)
pptx_btn = tk.Button(root,text ="Set",command =set_pptx_file,font =fonts)
pptx_btn.grid(row = 1, column = 2, padx=10, pady=10,sticky=tk.W)

pptxファイルのパスを設定するためのボタンを生成しているコードです。

  • 表示のためのテキストボックスを生成
    pptx_box = tk.Entry(width = b_width, font = fonts_b)
    tkinterのEntryボックスを設定したpptxファイルのファイル名を表示するために使っている。
  • ボタンを生成し,クリック時に呼び出される関数を設定
    pptx_btn = tk.Button(root,text =”Set”,command =set_pptx_file,font =fonts)
    “Set”と表示されたボタンを生成する。このボタンをクリックすると,set_pptx_file()という関数が呼び出される。
chk_st = tk.BooleanVar()
chk_st.set(True)
chk = tk.Checkbutton(variable=chk_st, text='Auto Set')
chk.grid(row = 1, column = 3, padx=10, pady=10,sticky=tk.W)

チェックボックスと関連する変数を生成しています。

  • チェックボックスで設定するブール変数を定義
    chk_st = tk.BooleanVar()  # chk_stというブール型変数を定義
    chk_st.set(True) # その値をTrueに設定
    chk = tk.Checkbutton(variable=chk_st, text=’Auto Set’)
    # “Auto Set”というラベルをつけたチェックボックスを作り,その値と変数chk_stを関連づけ

このチェックボックスにチェックマークを入れておくと,標準的なファイル名とフォルダ名に従ったディレクトリ構成にしてある場合は,pptxファイルを設定するだけでナレーションのフォルダとスクリプトファイルは自動的に設定されるようにしています。

dir_lb = tk.Label(text='Narration Folder:', font = fonts)
dir_lb.grid(row = 2, column = 0, padx=10, pady=10,sticky=tk.W)
dir_box = tk.Entry(width = b_width, font = fonts_b)
dir_box.grid(row = 2, column = 1, padx=10, pady=10,sticky=tk.W)
dir_btn = tk.Button(root,text ="Set",command =set_narration_folder,font =fonts)
dir_btn.grid(row = 2, column = 2, padx=10, pady=10, sticky=tk.W)

ナレーションのフォルダを設定するコードです。”Set”ボタンのクリックでet_narration_folder()が呼び出されます。

scpt_lb = tk.Label(text='Script File:', font = fonts)
scpt_lb.grid(row = 3, column = 0, padx=10, pady=10,sticky=tk.W)
scpt_box = tk.Entry(width=b_width, font = fonts_b)
scpt_box.grid(row = 3, column = 1, padx=10, pady=10,sticky=tk.W)
scpt_btn = tk.Button(root,text ="Set",command =set_script_file,font =fonts)
scpt_btn.grid(row = 3, column = 2,padx=10, pady=10, sticky=tk.W)

スクリプトファイルのパスを設定するためのボタンです。set_script_file()という関数を呼び出すために使います。

スクリプトを表示するテキストボックス

text_box = tk.Text(width=80, height = 20, font = fonts_b)
text_box.grid(row = 4, column = 0, columnspan = 4, padx=10, pady=10,sticky=tk.W)
xbar = tk.Scrollbar( orient = 'horizontal' )
ybar = tk.Scrollbar( orient = 'vertical' )
xbar.grid(row = 5, column = 0, columnspan =4, padx=10, pady=10,sticky=tk.W+tk.E)
ybar.grid(row = 4, column = 3, padx=10, pady=10,sticky=tk.W)
text_box[ 'xscrollcommand' ] = xbar.set
text_box[ 'yscrollcommand' ] = ybar.set
xbar[ 'command' ] = text_box.xview
ybar[ 'command' ] = text_box.yview

スクリプトファイルをセットすると,その内容を読み込んで表示します。現在のところは表示だけで,編集などの機能はありません。またスクロールバーをつけるようにコーディングしたつもりですが,機能していないようです。

AITalkのGUIを制御する関数

“Make new audio”ボタンをクリックすると呼び出される関数make_audio_files(p_info)は,AITalkのGUIを制御してwav形式のオーディオファイルを作って指定されたフォルダに格納します。関数全体の記述を示します。

def make_audio_files(p_info): 
    if os.path.isdir(p_info.narr):
        shutil.rmtree(p_info.narr)
    os.mkdir(p_info.narr)
    ai_path = ('D:\Program Files\AI\KantanAITalk'
              '\KantanAITalkEditor\KantanAITalk.exe')
    ai_title = 'かんたん! AITalk 5' # AITalk 5 window tytle
    ai_title_a = 'かんたん! AITalk 5 *' # AITalk 5 window alt. tytle
    hwnd = win32gui.FindWindow(None, ai_title)
    if hwnd == 0:
        hwnd = win32gui.FindWindow(None, ai_title_a)
        if hwnd == 0:
            process = subprocess.Popen(ai_path)
            wait_until_state(ai_title, 20, True)
            hwnd = win32gui.FindWindow(None, ai_title)
    # Open and load the slideshow script file
    pptx_path = p_info.pptx

    anim_num = AnimNum()   # Numbers related to animations.
    anim_num.audio = 0 # Index of audio files to be written to ppt/media/.
    anim_num.sld = 1 # Initial value of slide number.  
 
    ani_list = []
    wav_list = []
    nar_list = []
    (ani_list, wav_list, nar_list) = read_script_file(p_info)
    list_len = len(ani_list)
    for i in range(list_len):
        anim_num = ani_list[i]
        wav_file_path = wav_list[i] 
        wav_dir = os.path.dirname(wav_file_path)
        wav_file = os.path.basename(wav_file_path)
        nar_text = nar_list[i] 

        pyperclip.copy(nar_text)  # Copy narration text to clipboard.
        forefront_window(hwnd)    # Set AITalk window to the forefront
        pyautogui.hotkey('ctrl', 'a') # Select all text.
        time.sleep(0.2)
        pyautogui.hotkey('ctrl', 'v') # Paste the clipboard contents into the text box.
        pyautogui.hotkey('ctrl', 'alt', 'S') # Synthesis and save command.
        time.sleep(1)        
        hwnd_save = win32gui.FindWindow(None, '名前を付けて保存') # Detect Dialog Box. 
        if i==0:                             # For the first time,
            narr_dir_set(hwnd_save, wav_dir) # set directory name.
        forefront_window(hwnd_save)
        pyautogui.press('tab')       # To ensure the next Alt+n works. 
        pyautogui.hotkey('alt', 'n') # Press Alt+n to set save file name.
        # pyautogui.hotkey('ctrl', 'a')
        # time.sleep(0.5)
        pyperclip.copy(wav_file)     # Copy the file name to the clipboard.
        pyautogui.hotkey('ctrl', 'v')# Paste the clipboard contents into the text box.
        # pyautogui.press('enter')
        time.sleep(0.3)
        pyautogui.hotkey('alt', 's') # To activate the Save Button.
        wait_until_state('情報', 20, True) # Wait until the End of Operation.
        pyautogui.press('enter') # Press Enter to close the window.
    return  

以下,コードの上から順に説明していきます。

関数の引数

def make_audio_files(p_info): 

引数はPathInfoクラスのインスタンス,p_infoです。呼び出し側で以下のようにして定義しています。

p_info = "global_var"
class PathInfo:
    def __init__(self):
        self.pptx = ''   # pptx file path.
        self.narr = ''   # Narration folder path.
        self.scpt = ''   # Script file path.
        self.idir = ''   # Initial directory.
p_info = PathInfo()   
  • グローバルとして使う
    p_info = “global_var”
  • ファイルパスをまとめて扱うクラスを定義
    self.pptx = ” # pptx file path. pptxファイルのフルパス
    self.narr = ” # Narration folder path.  wavファイルを格納するフォルダのフルパス
    self.scpt = ” # Script file path.     スクリプトファイルのフルパス
    self.idir = ” # Initial directory.     ディレクトリ指定の基点となるパス
  • インスタンスを生成
    p_info = PathInfo()

ナレーションフォルダをクリア

関数が呼び出されると,まずナレーションのwavファイルを格納するフォルダの中身を削除します。実際には,p_info.narrで指定されたフォルダが存在しているか確認し,存在すればフォルダごと削除し,その後で同じ名前のフォルダを作っています。

    if os.path.isdir(p_info.narr):
        shutil.rmtree(p_info.narr)
    os.mkdir(p_info.narr)
  • ディレクトリが存在する場合は削除
    if os.path.isdir(p_info.narr):
    shutil.rmtree(p_info.narr)
  • ディレクトリを新たに作成
    os.mkdir(p_info.narr)

AITalkの起動

次に,AITalkが立ち上がっているかどうか確認します。立ち上がっていない場合は起動します。

かんたん!AITalk 5の操作ウインドウ

    ai_path = ('D:\Program Files\AI\KantanAITalk'
              '\KantanAITalkEditor\KantanAITalk.exe')
    ai_title = 'かんたん! AITalk 5' # AITalk 5 window tytle
    ai_title_a = 'かんたん! AITalk 5 *' # AITalk 5 window alt. tytle
    hwnd = win32gui.FindWindow(None, ai_title)
    if hwnd == 0:
        hwnd = win32gui.FindWindow(None, ai_title_a)
        if hwnd == 0:
            process = subprocess.Popen(ai_path)
            wait_until_state(ai_title, 20, True)
            hwnd = win32gui.FindWindow(None, ai_title)
  • AITalkの実行形式のパスを設定
    ai_path = (‘D:\Program Files\AI\KantanAITalk’
    ‘\KantanAITalkEditor\KantanAITalk.exe’)
    ai_pathにパスの文字列を代入している。括弧でくくることで,文字列を複数行にわたって記述しても改行が挿入されないようにしている。
  • 起動したときのウインドウのタイトルを設定
    ai_title = ‘かんたん! AITalk 5’ # AITalk 5 window tytle
    ai_title_a = ‘かんたん! AITalk 5 *’ # AITalk 5 window alt. tytle
    AITalkの起動直後のタイトルは”かんたん! AITalk 5′”となっています。編集内容が保存されていない場合は,”かんたん! AITalk 5 *”となっています。この2つのタイトルのどちらかを持つウインドウが存在するかどうかを確認しています。(文字列の一部をタイトルに持つウインドウを探索する関数があれば,それを使うのですが,まだ勉強不足です。)
  • AITalkのウインドウを見つける
    hwnd = win32gui.FindWindow(None, ai_title)
    if hwnd == 0:
    hwnd = win32gui.FindWindow(None, ai_title_a)
    win32gui.FindWindow関数はウインドウが存在すれば,ウインドウのハンドル変数hwndを返します。見つからなかった場合はhwndとして0が返されます。ここでは,2つのタイトルについて試しています。
  • AITalkのウインドウが存在しない場合は,起動する
    if hwnd == 0:
    process = subprocess.Popen(ai_path)
    wait_until_state(ai_title, 20, True)
    hwnd = win32gui.FindWindow(None, ai_title)
    subprocess.Popen(ai_path)で,指定されたパスにある実行形式ファイルを起動します。次の,
    wait_until_state(ai_title, 20, True)はai_titleを持つウインドウが現れるまで最大20秒まで待つという自作の関数です。戻り値にウインドウのハンドル変数hwndを返すように作っておくべきでした。
    なお,コード内部でAITalkの実行ファイルのパスを直接指定しています。これでは,他のPCで動作させるときに問題になります。いずれ,環境に合わせて変更できるように,外部の設定ファイルから読み込むように修正する予定です。
    関数wait_until_state()のコードは,以下のようになります。
def wait_until_state(title, max_wait_time, active):
    dt = 0.5
    n = int(max_wait_time/dt)
    time.sleep(0.1)
    for i in range(n):
        a_win = gw.getActiveWindow()
        state = (a_win.title == title)
        if active == False:
            state = not(state)
        if state:
            break
        time.sleep(dt)
    return
  • 引数
    def wait_until_state(title, max_wait_time, active):
     title:ウインドウのタイトル
     max_wait_time:待ち時間の最大値(秒単位)
     active:TureまたはFalseで指定
      True:ウインドウが現れるまで待つ
      False:ウインドウが消えるまで待つ

スクリプトファイルからパラメータリストを読み込む

    # Open and load the slideshow script file
    pptx_path = p_info.pptx

    anim_num = AnimNum()   # Numbers related to animations.
    anim_num.audio = 0 # Index of audio files to be written to ppt/media/.
    anim_num.sld = 1 # Initial value of slide number.  
 
    ani_list = []
    wav_list = []
    nar_list = []
    (ani_list, wav_list, nar_list) = read_script_file(p_info)
  • パスを設定
    pptx_path = p_info.pptx
    pptxファイルのパスは,関数の引数のインスタンスp_infoのメンバー変数pptxで与えられている。
  • アニメーション番号のインスタンスを生成
    anim_num = AnimNum() # Numbers related to animations.
    anim_num.audio = 0 # Index of audio files to be written to ppt/media/.
    anim_num.sld = 1 # Initial value of slide number.
    AnimNumクラスのインスタンス,anim_numを生成し,初期化する。
     anim_num.audio:オーディオファイルaudio1.wav,audio2.wav,・・・の番号
     anim_num.sld::スライドの番号
  • パラメータリストの読み込み
    (ani_list, wav_list, nar_list) = read_script_file(p_info)
     ani_list:anim_numのリスト
     wav_list:合成された音声を書き込むwavファイルのパスのリスト
     nar_list:合成するテキストのリスト
    プレゼンテーションで使うオーディオデータの全てについて必要な情報を処理の順番に従って並べたリストとして取得する。

read_script_file()関数のコードは次のようになっています。

def read_script_file(p_info):
    class LineAtb:
        def __init__(self):
            self.cmd = ''    # Script command of the line.
            self.num1 = '00' # First parameter of the command.
            self.num2 = '00' # Second parameter of the command.

    # Determine the first character for escaping.
    # Lines prefixed with the following strings are not processed 
    # as narration.
    sld_str = '#S'  # for specifying slides
    num_str = '#'   # for specifying click/animation number
    com_str = '%'   # for specifying comment

    script_file_name = p_info.scpt
    pptx_path = p_info.pptx
    with open(script_file_name, encoding='utf-8') as slide_script:
        script_lines = slide_script.readlines()

    name_no_ext = os.path.splitext(os.path.basename(pptx_path))[0]
    # name_no_ext: File name without extension

    # Specify the directory for voice data files
    wav_data_dir = p_info.narr

    anim_num = AnimNum()   # Numbers related to animations.
    anim_num.audio = 0 # Index of audio files to be written to ppt/media/.
    anim_num.sld = 1 # Initial value of slide number. 

    # Read and execute scripts line by line.
    line_atb = LineAtb() # Line attribute.
    ani_list = []
    wav_list = []
    nar_list = []
    for line in script_lines:
        str_len = len(line) 
        if str_len == 0 : # Return to the top of the loop if blank line.
            continue   
        line_atb = check_line(line, com_str, num_str, sld_str, line_atb)    
        if line_atb.cmd == "Comment": # If the command is "Comment",
            continue                  #  return to the top of the loop.
        if line_atb.cmd == "Slide": 
            anim_num.sld = int(line_atb.num1)
            anim_num.clk = int(line_atb.num2)
            anim_num.aut = 0
            continue
        if line_atb.cmd == "Number" :  # The line indicates click number.
            anim_num.clk = int(line_atb.num1) # Click number in the slide.
            anim_num.aut =int (line_atb.num2) # Number of auto animation.
            continue 
        if line_atb.cmd == "Narration" :
            # The line is processed as narration.
            # Determine the file name.
            anim_num.audio += 1
            wav_file_name = create_audio_file_name(name_no_ext, anim_num)
            wav_file_path = wav_data_dir + '/' + wav_file_name
            anim_num_c = copy.copy(anim_num)
            ani_list.append(anim_num_c)
            wav_list.append(wav_file_path)
            nar_list.append(line)
    return ani_list, wav_list, nar_list

AHKで作成した「スクリプトを読んで制御信号を生成する」部分を基にしています。オーディオデータの生成や埋め込みで必要なのは,オーディオファイル,ナレーションのテキスト,そしてどのアニメーションにオーディオを付加するか,という情報なので,これらを出現順に並べたリストを出力します。

<詳しく読む>

実は,今回のAITalk制御用コードの作成で,最も「はまった」のは,このread_script_file()関数の挙動が原因になっています。(頭を使うのに時間がかかった,ということです。もともと暇なときにコーディングをするのですが,このときは,夜寝る前にああでもない,こうでもないと1時間ほど連続して試行錯誤を繰り返しました。

まず第一のつまづきは,クラスのインスタンスのリストを作るところに原因がありました。コードを動かしてみると,できあがったpptxファイルにはプレゼンの一番最後のオーディオファイルしか埋め込まれていないのです。調べると,read_script_file()関数から戻ってきたリストのうち,anim_numに関するリストの内容が,全て一番最後にリストに追加した内容と同じになっていました。この問題は,

            anim_num_c = copy.copy(anim_num)
            ani_list.append(anim_num_c)

のように,copy関数でコピーした値をリストに追加することで解決しました。これについては,時間があれば別稿で説明したいと考えています。

もう1つのつまづきは,スクリプトを解釈する部分が原因でした。スクリプトの解釈では「空白の行があったら,何も処理しないで次の行の処理に移る」というルールに基づいて動作させているつもりでした。そして空白の行であることを「読み込んだラインの文字列数が0なら空白である」ことでlen()関数を使って判断していました。しかし,空白の行といっても改行を含んでいるので長さは0になりません。これでは空白行もナレーションのテキストとして処理され,同じテキストに対する音声合成と保存の操作が複数回繰り返されてしまいました。この場合,処理側で想定していない「同じファイルがあるけど上書きしますか?」みたいなプロンプトが出されてしまいます。(ユーザがEnterキーを押せば次のステップに進み,ファイルもちゃんと作られますが。)

この第2の問題も,新しいread_script()関数では直した(つもり)です。

<閉じる>

パラメータのリストの順に処理していく

ここからが,AITalkのGUIを操作するコードになります。

    list_len = len(ani_list)
    for i in range(list_len):
        anim_num = ani_list[i]
        wav_file_path = wav_list[i] 
        wav_dir = os.path.dirname(wav_file_path)
        wav_file = os.path.basename(wav_file_path)
        nar_text = nar_list[i] 

        pyperclip.copy(nar_text)  # Copy narration text to clipboard.
        forefront_window(hwnd)    # Set AITalk window to the forefront
        pyautogui.hotkey('ctrl', 'a') # Select all text.
        time.sleep(0.2)
        pyautogui.hotkey('ctrl', 'v') # Paste the clipboard contents into the text box.
        pyautogui.hotkey('ctrl', 'alt', 'S') # Synthesis and save command.
        time.sleep(1)        
        hwnd_save = win32gui.FindWindow(None, '名前を付けて保存') # Detect Dialog Box. 
        if i==0:                             # For the first time,
            narr_dir_set(hwnd_save, wav_dir) # set directory name.
        forefront_window(hwnd_save)
        pyautogui.press('tab')       # To ensure the next Alt+n works. 
        pyautogui.hotkey('alt', 'n') # Press Alt+n to set save file name.
        # pyautogui.hotkey('ctrl', 'a')
        # time.sleep(0.5)
        pyperclip.copy(wav_file)     # Copy the file name to the clipboard.
        pyautogui.hotkey('ctrl', 'v')# Paste the clipboard contents into the text box.
        # pyautogui.press('enter')
        time.sleep(0.3)
        pyautogui.hotkey('alt', 's') # To activate the Save Button.
        wait_until_state('情報', 20, True) # Wait until the End of Operation.
        pyautogui.press('enter') # Press Enter to close the window.
    return  
  • リストの長さを取得してループ処理を開始
    list_len = len(ani_list)
    for i in range(list_len):
     リストの長さはani_list,wac_list,scpt_listの3つで同じ。ここではani_listの長さを調べて繰り返し回数list_lenを取得し,カウンタ変数iを使ったループ処理を開始する。
  • パラメータをリストから読み込む
    anim_num = ani_list[i] # アニメーション番号
    wav_file_path = wav_list[i] # 書込むwavファイルのパス
    wav_dir = os.path.dirname(wav_file_path) # パスからディレクトリ名を取得
    wav_file = os.path.basename(wav_file_path) # パスからファイル名を取得
    nar_text = nar_list[i] # 音声合成するナレーション
  • ナレーションのテキストをクリップボードにコピーし,AITalkの編集ウインドウにペースト
    pyperclip.copy(nar_text) # ウインドウズのクリップボードにナレーションをコピー
    forefront_window(hwnd) # 自作の関数でウインドウを前面に移動
    pyautogui.hotkey(‘ctrl’, ‘a’) # Ctrl+aキーでテキスト編集ウインドウ内の全てを選択
    time.sleep(0.2) # 0.2秒待つ(この時間はシステム毎に調整すべきかも)
    pyautogui.hotkey(‘ctrl’, ‘v’) # Ctrl+vキーでテキストウインドウ内にクリップボードをペースト
    time.sleep(1) # 1秒スリープ(この時間もシステム毎に調整すべき)
  • テキストから音声ファイルを保存
    pyautogui.hotkey(‘ctrl’, ‘alt’, ‘S’)
    AITalkのメニュー > ファイル > テキストから音声ファイルを保存…
    で起動される。コードではCtrl+Alt+Sにより起動している。
  • 「名前を付けて保存」ダイアログボックスを検出
    hwnd_save = win32gui.FindWindow(None, ‘名前を付けて保存’)
    ダイアログボックスを検出したら,そのハンドル変数をhwnd_saveに代入している。

「名前を付けて保存」ボックスが検出されたら,ディレクトリ名とファイル名を設定し,音声合成と保存のプロセスを起動する。GUIを使う場合は,次の①~③の操作をする。

「名前を付けて保存」ダイアログボックス

  • 保存ファイルのディレクトリパスを設定(①)
    if i==0:
    narr_dir_set(hwnd_save, wav_dir)
    初回(カウンタ変数iが0)には,ディレクトリをセットする。2回目以降はファイル名のみ設定でよい。
  • ファイル名を設定(②)
    forefront_window(hwnd_save) #「名前を付けて保存」ボックスを前面に(自作関数を使用)
    pyautogui.press(‘tab’) # タブキーを1回押す
    pyautogui.hotkey(‘alt’, ‘n’) # Alt+nキーを押して保存ファイル名のテキストボックスを選択
    pyperclip.copy(wav_file) # 保存ファイル名をクリップボードにコピー
    pyautogui.hotkey(‘ctrl’, ‘v’) # 保存ファイル名テキストボックスにペースト
    time.sleep(0.3)  # 0.3秒スリープ

tabキーを押すのは,経験上,こうすると次のAlt+nが機能するから・・・なので,もう少し考える必要がありそうです。forefront_windowはウインドウを最前面に持ってくるための自作の関数です。

def forefront_window(hwnd):
    # Set AITalk window to the forefront
    ctypes.windll.user32.SetForegroundWindow(hwnd)
    left, top, right, bottom = win32gui.GetWindowRect(hwnd)
    pyautogui.moveTo(left+60, top + 10)
    pyautogui.click()
    return

ウインドウの四隅の座標を取得し,その中にマウスポインタを移動してクリックする,というアナログっぽい操作をしています。ハンドル変数hwndがわかっているので,もっと直接的な方法があれば変更したいです。

  • 保存ボタンを起動し(③),保存が終了まで待ち,OKボタンを押す(④)
    pyautogui.hotkey(‘alt’, ‘s’) # Alt+sを入力
    wait_until_state(‘情報’, 20, True) # “情報”というタイトルのウインドウが現れるまで待つ
    pyautogui.press(‘enter’) # Enterキーを送って,ダイアログボックスを閉じる。

処理時間など

80個のwav形式のファイルを作成するのに数分かかっています。挿入したsleepの時間を調整しても,あまり短くできないと考えています。GUIを介しているので,いたしかたないでしょう。

そろそろ,実行形式のファイルを公開して,使ってみた人からのフィードバックしてもらってコードを手直しすべきなのかもしれません。(需要があるのかどうかは,わかりませんが。)でも,もう少し試してからでないと危ない気がします。

例えば,今回,作ったGUI制御のコードをデバッグしているときのことです。ブレークポイントで停止させVSCodeの編集ウインドウ上で変数の値を確認し,メニューから「継続」を選びます。すると,AITalkのウインドウではなくVSCodeの編集ウインドウをアクティブにしたまま動作が進行します。GUI制御の信号がCtrl+a,Ctrl+vだとすると,VSCodeの編集ウインドウが全部選択され,そこにクリップボードの内容が上書きされてしまいます。

慌てて停止させ,Ctrl+zで編集操作を取り消すことで事なきを得ました。AHKでコードを作成しているときも,デバッグ中に編集中のコードが書き換わってしまい,さっきまで動いていたコードが動かなくなってしばらく原因がわからない,という経験がありました。AHKの場合は,Escキーなどを押すと実行が止まるような設定のコードを追加してからは楽になりました。

また,大事なフォルダを削除してしまう恐れもあります。Pythonコードの中で削除したファイルやフォルダはOSが提供する「ゴミ箱」に移動されるのではなく本当に削除される(と私は理解しています)ので,大変危ないです。

コメント