PPTファイルに音声を自動で埋め込む その1 AHKによる実装

プログラミング

ここしばらく,FPGAを使ったデータ収集用システムの開発に時間を費やしておりました。

<詳しく読む>

この作業は研究室に残す技術資料の整理(と私の頭の中の整理)も兼ねています。のんびり進めるつもりだったのですが,元の職場の研究室の学生さんで,私の関係していた研究テーマをやりたいという人が出てきて事情が変わりました。卒研生・大学院生が残した引継ぎ資料だけでは開発用のツールを使いこなすことができないのです。就職した卒業生に連絡しても要領を得ません。卒業研究や修士論文研究の学生さんには論文の提出期限というものがありますので,急ぐ必要がありました。本当は,このような研究の後始末は退職前に済ませておくべきだったのです。でも,なかなか思うようには進みませんでした。

<閉じる>

3月に入り,卒業研究や修士論文研究も一段落つきました。技術資料の整理から少し離れて,このwebサイトの“本来の目的”である講義資料の整理を始めることにしました。

構想としては,在職中に作りためた講義資料を基に,部品として再利用できるような教材や,教える人や学ぶ人が活用できるシステムを作りたいのです。そのための第一歩として,Powerpoint(以下,PPT)のファイルにナレーションを自動で付けるプログラムを作り始めました。

この投稿は書きかけです。修正や追加をしていきます。

試作

現状は,試作の段階で,以下のようなAHKコードを作りました。

  • 音声データ生成スクリプト
    市販の音声合成ソフト,株式会社エーアイの「かんたん!AIトーク3」のGUIを制御するAHK(AutoHotKey)スクリプト。PPTのスライド送りやアニメーション用のクリックに連動するナレーション用のテキストデータから音声ファイルを生成する。
  • PPT制御スクリプト
    こちらもAHKスクリプト。PPTのGUIを制御して,合成された音声データを埋め込んでいく。現時点では速度に難があるので,将来的にはPythonで音声ファイルを直接埋め込むものを作りたい。

作成したスライドショーの例

講義資料のページの中のスライドショーの1つに音声データを埋め込んでみました。以下のリンクをクリックすると,別タブでスライドショーの画面が開きます。

音声を埋め込んだスライドショーの例

講義で使ったスライドをそのまま利用しています。音声は4枚目のスライドから挿入されています。スライドビューワの下部に操作用のボタンがありますが,スライドショーをスタートしてしまえば,マウスクリックだけで進行していくのはPPTの操作と同じです。

オーディオファイルの生成は,手動の作業よりはずっと速いようです。しかし,PPTにオーディオファイルを埋め込む作業は「すごく手の速い人が脇目もふらずに作業した場合」と,ほぼ同じくらいかもしれません。もちろん改善の余地はあります。

既存の製品

開発を始めた当初は,PPTに音声データを自動で埋め込むソフトは存在していました。しかし,有償(それも結構な高額)だったことがネックになりました。自由度の点でも合わなかったと思います。

現時点で調べてみると,以下のようなものが使えそうなことがわかりました。

  • リアルナレーターズ3
    PPTのノートに書かれたテキストデータから合成した音声を埋め込む有償ソフト。字幕を追加する機能もあり便利そう。でも,お高い(327,800円~)
  • Aspose.Slides
    Aspose Native APIs for cross platform development. Generate, manage and convert PowerPoint Presentations & Slides in .NET, Java, C++, Python, PHP, Android ということで,使いやすそうだが有償,Python Via .NETというソフトで999USD~。
  • Python言語のライブラリ,python-pptx
    PPTファイルをOfficeソフト無しに操作できる。オーディオファイルの埋め込みができるかどうかは調査中。
  • PyAutoGUI
    PythonでアプリケーションのGUIを操作するためのライブラリ。AHKの代わりに使えそう。

無償で使えそうなのは,Pythonのライブラリです。

方針

システムは,概略,以下のように動作させることにしました。

  • スライドショーのスクリプトに基づいて動作
    PPTスライドショーのスクリプト(「ト書き」)のテキストデータを手動で作る。スライド送りやアニメーションを進行させるためのクリックに番号を付けて順に並べる。音声ナレーションはクリックで起動されものとし,クリック番号の後ろに合成したいテキストを配置する。
  • スライドショースクリプトから音声ファイルを生成
    スライドショースクリプトから音声合成ソフトにより各クリック毎の音声データを生成する。
  • 音声データをPPTに埋め込む
    スライドショースクリプトに基づいて,音声データをPPTファイルに埋め込む。

スライドショースクリプトの仕様(暫定版)

スライドショースクリプトは以下のようなルールに基づいて手動で作成されるテキストデータです。将来的には,PPTファイルからクリック操作を抽出してスクリプトの基になる部分を自動生成するようにしたいです。

  • 行単位で動作を記述
    開始からリターンコードまでで1単位とする。
  • エスケープ行
    以下の文字列(全て半角)が先頭にある行は,スライド切り替えやアニメーションのためのマウスクリック(あるいは矢印キー操作)を表し,音声合成は実行しない。
    • #Snn:スライド番号を示す。nn:01~99(2桁の数字)
      例:#S01,#S20など。
    • #mm:1つのスライド内でのアニメーションのクリック番号。mm:00~99
      #00はスライド切り替え時のナレーションを表す。
      例:#00:スライド切り替え時のナレーション,#01:1番目のクリックのナレーション。
    • #nn_mm:nn番目クリックに続く自動アニメーションの番号を示す。mm:01~99
    • %:コメント行
  • 空行は無視する
  • 記述のルール(暫定版)
    全てのスライド番号とクリック番号を順番に並べる。スライド番号とクリック番号はエスケープ文字列を 行頭に持つ行で指定し,改行後の「コメント行でも空でない行」がナレーションとして処理される。

例:

#S01              ・・・スライド01番開始
#00                ・・・スライド切り替えナレーションの挿入を示す
×××・・・   ・・・スライド切り替えナレーション
#01
×××・・・   ・・・クリック01番のナレーション
#02
×××・・・   ・・・クリック02番のナレーション
#03                  ・・・ナレーションを伴わないクリッ
#04
×××・・・   ・・・クリック04番のナレーション
(クリック番号の次が空行または次のクリック番号の場合,ナレーションは挿入しない)
#05
×××・・・   ・・・クリック05番のナレーション
#05_02
×××・・・   ・・・クリック05番に続く自動アニメーションの2番目のナレーション
(自動アニメーションの場合,ナレーションを挿入するアニメーションの番号のみ記述すればよい)

スライドショーの例の作成に使ったスライドショースクリプトを以下に示します。

#S01
#S02
#00
#01
#02
#S03
#00
#01
#02
#S04
#00 
それでは,配布資料の1-1を見てください。現在,私たちが使っているパソコンやスマートフォンの中で使われている電子デバイスについて簡単に説明していきます。電子デバイスは,技術者の間では,俗に「たま」とか石などと呼ばれていました。また,アイシーという呼び名も聞いたことがあると思います。
#S05
#00
技術の歴史上,論理回路の部品として採用された一番古い電子デバイスは,真空管です。真空管は,このような構造になっていて,幾つかの金属製の部品が端子に接続されています。
#01
ヒーター。
#02
カソード,陰極と呼ばれることもあります。
#03
グリッド。金網です。
#04
プレート。その名前のとおり平たい板です。
#05
これらの金属製の部品をガラスの容器で覆って,中の空気を抜いて真空にします。
#06
ヒーターに電流を流すと熱が発生します。
#07
この熱でカソードが熱せられて熱電子が発生します。
#08
発生した電子はカソードからプレートに向かいます。グリッドに与えた電位で電子の流れを制御します。真空管は,電極の間の空間を電子が移動できるように電極全体をガラス製の容器に入れて,中を真空にします。この様なガラスの玉のような入れ物を使うので,日本では「たま」と呼ぶ人がいました。
#S06
#00
真空管は,現在でも使われていて,特にオーディオ用のものは見かけると思います。
#S07
#00
世界初の電子式コンピュータであるエニアックは,真空管を使って作られました。
#S08
#00
このようにエニアックは1つの部屋を占めるような大きなものでした。
#S09
#00
左上の写真は,コンピュータのプログラムを変更しているところです。
#01
現在のものとは異なり,配線を変更することでプログラムを変更しています。右側の写真は壊れた真空管を交換しているところです。毎月2000本の真空管を交換したそうです。
#02
当時,弾道計算に使われて計算速度が「弾より速い」と言われていましたが,プログラムは配線を食い直すことで変更されていたのです。
#S10
#00
真空管の次に現れたのが,半導体を使ったトランジスタです。
#S11
#00
#01
半導体の動作を説明します。
#02
半導体に電極を付けます。
#03
電極の間に電位差を与えます。
#04
すると,半導体の中を電子が移動していきます。
#05
この構造を,抽象化して,このような記号で表します。
#06
電極から取り出した端子には,,D,ドレーン,G,ゲート,S,ソースという名前を付けます。
#07
ゲートとソースの間の電位をVG,ドレーンとソースの間の電位をVDとします。
#08
電位に応じてドレーンとソースの間に電流が流れます。この電流をIDアンペアとします。
#09
電流と電位の関係をグラフにすると,例えばこんなふうになります。
#10
Vgが1ボルトのとき
#11
2ボルトのとき,
#12
3ボルト
#13
4ボルト となります。
#14
このような特性を持つことが分かってしまえば,後は,中身の構造のことは一旦忘れて考えることができます。

#S12
#00
#01
次に,I C,集積回路が登場します。半導体チップ上に複数の部品で構成された微細な電子回路を一体化して作ったものです。
#02
メリットは部品数を増やしても信頼性が高いことです。また,高速動作,低消費電力が可能になります。

#S13
#00
#01
マイクロプロセッサは,コンピュータの中心的な部品であるCPUを1チップのLSIとして実現したもののことです。LSIとは,ラージスケールインテグレーション,大規模集積化の略です。

#S14
#00
#01
マイクロプロセッサは,パソコンやスマホ以外の様々な機器の中でも使われています。
#02
炊飯器,洗濯機,自動車,・・・ などなどです。
#03
より大きなシステムの部品として 使われています。

#S15
#00
復習になります。現在のコンピュータはフォン・ノイマン型と呼ばれる構造をとっています。この特徴を3つあげると,次のようになります。
#01
プログラム内蔵
#02
逐次的処理
#03
バス接続  の3つです。皆さん,それぞれ,どんなことだったか説明できるでしょうか?
#04
プログラム内蔵というのは,コンピュータ内部の記憶装置に処理の手順を記録しておいて,それに従って動作するという仕組みです。この結果,コンピュータはそのハードウェアの構造を変えることなく様々な処理ができるようになります。
#05
逐次処理というのは,記憶している手順を1つ1つ順番に処理していく,ということです。
#06
バス接続というのは,バスという共通の信号線を使ってたくさんの部品を相互に接続する方法です。部品は割り当てた番地により指定するようになっていて,限られた数の配線でたくさんの部品を接続できます。このため高い拡張性を持たせることができます。

#S16
#00
プロセッサの基本動作を見てみましょう。
#01
命令読みだし
#02
命令解読 
#03
命令実行
#04
以上の3つを繰り返しています。

#S17
#00
コンピュータの復習はここまでとして,半導体を用いた論理回路の基本的な部品であるゲート回路について復習しましょう。
#01
ゲート回路は,スイッチを組合せて,基本的な論理演算を行います。
#02
2入力ゲートは,数個のトランジスタを使って構成されます。
#03 
電圧レベルの高い低いで論理の1と0を表します。
#04
高い,低い,と言いましたが,補足すると,この図のようになっています。 
#05
ある電圧レベルVHより電位が高ければハイとします。
#06
VHより低い電位VLよりさらに低ければローとします。

#S18
#00
2入力のゲート回路の中身を見てみましょう。
#01
性質の異なるトランジスタを,幾つか組み合わせた回路です。
#02
入力が2つ,出力が1つあります。それぞれA,B,Cという名前を付けましょう。
#03
この回路を,中身の構造はとりあえず忘れて1つにまとめて考えます。
#04
これを,右のような記号で表します。
#S19
#00
複数のゲートを使って様々な論理回路を作ります。まず,「組み合わせ論理回路」について説明します。
#01
これは,入力の組合せで出力が定まる,というものです。
#02
ですから,真理値表と呼ばれる表で,入出力関係が決定されます。

#S20
#00
組み合わせ論理回路の例として,「アンド・オア・セレクタ」をとりあげましょう。ANDゲート2つとORゲート1つを使っています。入力信号はA,B,Cの3つ。出力は一つで,Dとします。
#01
真理値表は,このようになります。入力は,A,B,Cの3つで,それぞれが0か1という2つの値のどちらかになります。ですから,入力の組み合わせの数は2の3乗,つまり八つの組み合わせがあります。少し詳しく見ていきましょう。
#02
制御入力のCが1のときを考えます。丸印は信号の論理否定を意味していることに注意します。
#03
すると,上のANDゲートには1が入力されることになります。
#04
一方,下のANDゲートには丸印を通っているので0が入力されます。
#05
0が入力されている下のANDゲートは,もう1つの入力であるBの値が0であっても1であっても,出力は0になります。一方,上のANDゲートの出力は信号Aと1のANDなので,Aに一致します。
#06
出力回路はORゲートで,下側の入力が0になっているので,出力は信号Aと一致します。つまり,Cが1のときは,出力Dは2つの入力のうち,Aと一致することになります。

#S21
#00
#01
次に,信号Cが0のときを考えます。
#02
今度は,上のANDゲートへの入力の片方が0となります。
#03
下のANDゲートへの入力の片方は1となります。
#04
上のANDゲートの出力は入力Aの値によらず0となります。
#05
ORゲートへの入力は0とBになります。
#06
したがって,ORゲートの出力は入力信号のBと一致することになります。以上のように,入力Cが0のときは信号Aが出力され,Cが1のときはBが出力されることがわかります。
#S22
#00
組み合わせ論理回路は,入力の組み合わせで出力が定まります。これに対して,順序回路とか,ステートマシンと呼ばれる論理回路があります。
#01
これらは,「記憶」あるいは「状態」を持つ回路です。そして,これらの回路の出力は入力だけでなく「記憶」や「状態」に依存します。細かいことをいうと,出力が記憶や状態のみで定まるものと,記憶や状態に加え入力にも依存するものに分けることができます。
#02
順序回路やステートマシンの入出力関係は,状態遷移表や状態遷移図で定義されます。

AHKによる実装

まず,AHKにより,音声データ生成スクリプトとPPT制御スクリプトを実装しました。AHKを採用したのは,小さいファイル操作用コードを作った経験があったためです。

音声データ生成スクリプト

音声データを生成するためのAHKスクリプトについて説明します。音声合成に使用するのは「かんたん!AITalk 3」というソフトです。現時点で音声合成エンジンに直接アクセスする方法は公開されていないので,AHKでGUIを操作するしかありません。

動作環境など
  • 「かんたん!AITalk 3」をインストール済みであること
    KantanAITalk.exeが配置されているディレクトリをコードの中で指定している。したがって,ユーザの使用環境に応じで,AHKコードを変更する必要がある。
  • コードとライブラリは同じディレクトリに配置する
    AITalkScript.ahkとMyLib.ahkを同じディレクトリに配置する。
  • 入力ファイル
    スライドショースクリプトをテキストファイルとして用意する。このテキストファイルのファイル名が生成されるファイルの名前のベースとなる。
  • 出力ファイル
    指定した出力フォルダに,生成された音声ファイル(wave形式)と,それに対応するテキストファイルが格納される。出力フォルダ内に,以前に生成した音声ファイルなどが残っていて,新しく生成されたファイルと名称が同じ場合は,上書きの許可のプロンプトが出される。
    スライドショースクリプトファイルの名称がXXXX.txtの場合,出力ファイルの名称はXXXX_01_00.wav,XXXX_01_00.txtなどのようになる。
AHKスクリプトの処理手順

音声データ生成のAHKスクリプトは,以下の操作を実行している。

ⅰ)「かんたん!AITalk 3」を起動
 ①音量・話速などは事前に手動で設定しておく
 AHKスクリプトでは音量などのパラメータは操作していない。
 一度設定しておけば,変更するまでその設定が使用される。例に挙げたスライドショーでは話速を1.5としている。

ⅱ)テキストをコピー&ペーストし,音声合成
 ②テキストウインドウに音声合成したいテキストをペースト
 ③音声保存ボタンをクリック
  これにより音声合成が実行され,ファイル保存の操作に移行する

「かんたん!AITalk 3」の操作インタフェース画面

ⅲ)ファイル名を指定して保存
 ④ファイル名入力テキストボックスにファイル名をペースト
 ⑤保存ボタンをクリック

ファイル保存のダイアログボックス
AHKスクリプトのコード

AHKスクリプトは以下のようになっています。

#NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.

; 変数名を解釈するとき、環境変数を無視する。

; #Warn  ; Enable warnings to assist with detecting common errors.

#Include MyLib.ahk ; 自作のライブラリ

SendMode Input  ; Recommended for new scripts due to its superior speed and reliability.

                ; 入力の再生速度を重視した設定

SetWorkingDir %A_ScriptDir%   ; Ensures a consistent starting directory.

                              ; このスクリプトを配置したディレクトリパス

;msgbox % “Your AHK version is ” A_AhkVersion ; バージョン確認用

FileEncoding , UTF-8 ; 日本語対応

IfWinNotExist, かんたん! AITalk 3 ; AITalkが起動していない場合は起動する

{

  ; AITalkを起動

  ; 注意:AITalkの実行ファイルのフルパスを指定 以下のパスはユーザの環境に合わせて変更する

  AITalkExe = D:\Program Files (x86)\AI\AITalk3\KantanAITalk\KantanAITalk.exe

  Run, %AITalkExe%

  ; 起動を確認

  Process, Wait, %AITalkExe%, 10

  PID = %ErrorLevel%

  If(PID == NotFound)

  {

    MsgBox, Process”%AITalkExe%” not found.

    ExitApp

  }

}

; エスケープ用の先頭文字を決める

; 以下の文字列が先頭にある行は,ナレーションとしては処理しない

ComStr  := “%”    ; コメント指定用文字

NumStr  := “#”    ; クリック/アニメ番号指定用文字

SldStr  := “#S”   ; スライド指定用文字

; FileSelectFileコマンドによりスライドショースクリプトファイルのパスを取得

FileSelectFile, textFilePath

; SplitPathコマンドでファイル名を抽出し出力ファイル名のベースにする

SplitPath, textFilePath, name, dir, ext, name_no_ext, drive

fileName = %name_no_ext% ; 拡張子を除いたファイル名

; FileSelectFolderコマンドにより出力ファイルのフォルダを指定

; 既存のフォルダを使う場合,フォルダ内のファイルは全て削除しておくこと

  FileSelectFolder, outputDir , %A_ScriptDir%, 1

; かんたん! AITalk 3 のウインドウをアクティブに

WinActivate, かんたん! AITalk 3

; 1行ずつ読み込んでAITalk 3で音声を生成し,テキストファイルとwavファイルを生成

iLine := 0  ; 行番号

iSld := 0   ; スライド番号

iNar := -1  ; スライド中のナレーション番号

iClk := 0   ; スライド中のクリック番号

iAut := 0   ; 自動スタートアニメのスタートからの番号

firstTimeFlag := 1  ; 初回のファイル書き込みであることのフラグ

Loop

{

  iLine += 1

  FileReadLine, line, %textFilePath%, %iLine% ;変数lineに1行読み込む

  If ErrorLevel <> 0 ; テキストファイルから読めなくなったらループから抜ける

    Break

    ; 1行ずつ確認する場合の例

      ;MsgBox, 4, , Line #%iLine% is “%line%”.  Continue?

      ;IfMsgBox, No

      ;Return

  ; 行(パラグラフ)の文字数を取得

  StrLength := StrLen(line)

  ; 空白の段落でない場合は,処理する

  If( StrLength != 0 )

  {

    ; 行の属性linAtbと関連する番号num1,num2を取得

    lineAtb := CheckLine(line, ComStr,NumStr,SldStr,num1, num2)

    ; コメント,スライド,番号指定以外の属性ならナレーションを音声合成

    ; コメント行ならループの先頭に移行

    If(lineAtb = “Comment”)

     {

       Continue ; 以下の処理をスキップしてループの先頭に

     }

    ; 番号指定行ならナレーション番号を1増加してループ先頭へ

    If(lineAtb = “Number”)

      {

        iNar += 1    ; ナレーション番号

        iClk := num1 ; スライド内のクリック番号

        iAut := num2 ; 自動開始イフェクトの番号

        Continue

       }

    ; スライド指定行ならスライド番号にnum1を代入

    ; ナレーションの番号などをクリアしてループ先頭へ

    If(lineAtb = “Slide”)

       {

         iSld := num1

         iNar = -1

         iClk := 0

         iAut := 0

         Continue

       }

     ; コメント行でも番号指定行でもなけばナレーション合成処理を実行

      clipboard = %line%  ; クリップボードにコピー

      ClipWait

      ; AITalkの編集ウインドウに文字列を入力し音声に変換

      WinActivate, かんたん! AITalk 3

      Send, ^a    ; Ctrl+a ウインドウ内の文字列を全部選択

      Send, ^v    ; AITalk編集ウインドウに上書きペースト

      sleep, 500

      ; ファイル > 音声ファイル保存 > wave形式で保存

      Send, {LAlt Down}   ; Altキーを押した状態にする

        Send,  f    ; AITalkメインメニューの“ファイル”を選択

        sleep, 333

        Send,  v    ; ファイルメニューの“音声ファイルを保存”を選択

        sleep, 333

        Send, w     ; “wave形式で保存”

        sleep, 333           ; スリープ必要?

      Send,{LAlt Up}      ; Altキーを上げた状態にする

      ; 保存ファイル指定のダイアログボックスの処理

      WinActivate, 音声ファイルの保存ウインドウ

      sleep, 500

      ; スライドとナレーションの番号を2桁表示用に変換

      ; 先頭に”00″を追加してから下2桁を取り出す

        cSld := SubStr(“00” . iSld,  -1)      

        cNar := SubStr(“00” . iNar, -1)

        cClk := SubStr(“00” . iClk, -1)

      fName_no_ext = %fileName%_%cSld%_%cClk%  

      If(firstTimeFlag =1){ ; 一番初めの書込みのみフルパスを指定

        fName_no_ext = %outputDir%\%fName_no_ext%

        sleepTime := 2000

        firstTimeFlag = 0 ; フラグのクリア

      }

      Else{

        sleepTime :=1000

      }

      ; 自動アニメーション(iAut≠0)のときはファイル名に枝番をつける

      If(iAut <>0){

        cAut := SubStr(“00” . iAut, -1) ; iAutを2桁の数字に変換

        fName_no_ext = %fName_no_ext%_%cAut%

      }

      fName = %fName_no_ext%.wav  ; 拡張子.wavを追加してファイル名を決定

      Send, %fName% ; 保存ウィンドウのファイル名ボックスに書込む

      sleep, sleepTime

      ControlSend, Edit1, {Enter}, 音声ファイルの保存

      Sleep, 1200  

      WinWaitClose, 音声ファイルの保存 ; ウィンドウが閉じるまで待つ

    ;} ; ここまでナレーションの処理

  } ; ここまで文字列の長さが0でない場合の処理

} ; end of loop

MsgBox, The end of the file has been reached or there was a problem.

exitApp       ; AutoHotKeyを終了

Esc::ExitApp  ; Exit script with Escape key

自作のライブラリです。PPT制御用に使う関数も含まれています。

#NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.

; #Warn  ; Enable warnings to assist with detecting common errors.

SendMode Input  ; Recommended for new scripts due to its superior speed and reliability.

SetWorkingDir %A_ScriptDir%  ; Ensures a consistent starting directory.

FileEncoding , UTF-8 ; 日本語対応

; AITalkScript.ahk用のライブラリ

CheckLine(line, ComStr, NumStr,SldStr, ByRef num1, ByRef num2)

; 動作指示ファイルから読み込んだ行の役割をチェックし,

; スライド番号,アニメーション番号を返す

; 入力

;   line    :入力文字列

;   ComStr    :コメント行

;   NumStr    :アニメーション番号を表す行

;   SldStr    :スライド番号を表す

; 出力

;   num1, num2  :抽出した番号を返す

; 戻り値は,行の属性を表す以下の文字列

;   “Comment” :コメント,

;   “Number”  :アニメーション番号指定,

;   “Slide”   :スライド番号指定,

;   “Narration” :ナレーション

{

; 各文字列の長さを取得

  ComStrLen := StrLen(ComStr)

  NumStrLen := StrLen(NumStr)

  SldStrLen := StrLen(SldStr)

; 先頭文字列を抽出し処理

  StringLeft, FirstStrSld, line, SldStrLen

  StringLeft, FirstStrNum, line, NumStrLen

  StringLeft, FirstStrCom, line, ComStrLen

; スライド指定行であるかチェックし,

; その場合は,スライド番号をnumに代入

; 例:#S02

  If(FirstStrSld = SldStr)

   { ; #Sの次の長さ2の文字列を10進数の番号に変換

     StringMid, nStrC10, line, SldStrLen+1,1 ; 10の桁

     StringMid, nStrC01, line, SldStrLen+2,1 ;  1の桁

     num1 := 10*nStrC10 + nStrC01 ; 10進数に変換

     num2 := 0

     Atb = Slide

    }

    Else If(FirstStrNum = NumStr)

    {

     ; クリックまたはアニメーションの番号指定行であるかチェック

     ; 指定行の場合は,番号をnum1に代入

     ; 例:#01 → num1 = 1,num2 =0, #02_03 → num1 = 2,num2 = 3

     StringMid, nStrC10, line, NumStrLen+1,1 ; 10の桁

     StringMid, nStrC01, line, NumStrLen+2,1 ;  1の桁

     num1 := 10*nStrC10 + nStrC01      

     underBar := SubStr(line,NumStrLen+3, 1)

     If(underBar= “_”){

       nStrC10 := SubStr(line,NumStrLen+4, 1)

       nStrC01 := SubStr(line,NumStrLen+5, 1)

       num2 := 10*nStrC10 + nStrC01

     }

     Else {

      num2 := 0

     }

     Atb = Number ;

    }

    Else If(FirstStrCom = ComStr)

    {

      ; コメント行であるかチェック, numは0とする

      num1 := 0

      num2 := 0

      Atb = Comment

    }

    Else

     {

      ; 上記以外なら,

      ;num1 := 0

      ;num2 := 0

      Atb = Narration

     }

    Return Atb

  }

 ActivateAnimeWindow()

 {

    ; アニメーションウインドウをアクティブにする

    ; PPTウインドウ最上段のリボンタブが有効であることが条件

    WinActivate,  ahk_class PPTFrameClass

    sleep,500

;   リボンからアニメーション ウィンドウを開く(Alt+a → Alt+c)

;   ただし,すでに開かれているときは,意図していない動作になるので注意

    Send, {LAlt Down}

      Send, a     ; リボンのアニメーションタブを選択

      sleep, 333  

      Send, c     ; アニメーションウィンドウを選択

      sleep, 333

    Send {LAlt Up}

    ControlFocus, UserControl1, A  

    Return ErrorLevel

  }

SelectAnimationTop(UChwnd)

  ; アニメーションウィンドウ内のアニメの最上段を選択

  ; コントロールにフォーカスする

  {

   ;ControlFocus, UserControl1, A   ; 以前はこの方法を使っていたが

   ControlFocus, , ahk_id %UChwnd%  ; この方法に変えた

   ;Click, 1500, 900                ; PPTスライドの中央をクリックしたこともある

   ControlSend, UserControl1, {Down} ; 下向き矢印キーでフォーカスし,

   ControlSend, UserControl1, {PgUp} ; PgUpキーで最上段を選択

   Send, {Blind} {Down}

   Send, {Blind} {Up}

   sleep, 300

   Return ErrorLevel

  }

MoveNextClick(UChwnd)

  ; アニメーションUChwndウィンドウ内で1つ下のクリック番号を選択

  ; スタート属性が“クリック時”でないアニメーションはスキップする

  {

   ;ControlFocus, UserControl1, A

   Loop

    {

     ControlFocus, , ahk_id %UChwnd%

     ;Click, 1500,900

     ;ControlSend, UserControl1, {Down} ; 下向き矢印キー

     Send, {Blind} {Down}

     sleep, 500

     hctrl := GetComboBox(UChwnd)

     ;MsgBox, hctrl = %hctrl%

     If(hctrl = “クリック時”)

     {

       Break

     }

    }

   Return ErrorLevel

  }

GetComboBox(UChwnd)

 ; シェイプのアニメタイミングタブを開き,クリック起動かどうか確認する

 {

   ControlFocus, , ahk_id %UChwnd%

   Send, {Appskey}  ; メニューを展開し

   Send, t    ; タイミング(T)… を選択

   ;Send, {Enter} ; Enterで確定

   sleep, 300

   ; タイミングタブ最上部の“開始(S)”を選択

   Send {LAlt Down}

     Send, s

   Send, {LAlt Up}

   sleep, 500

   WinGetTitle, Title, A ; ウインドウのタイトルを取得

    ;MsgBox, WinGetTitle = %Title%, %ErrorLevel%

    ;MsgBox, The active window is “%Title%”.

    ; “アピール”など,効果名がついている

    ;WinMenuSelectItem, %Title%,  , 1&

   sleep, 500

   ; コンボボックスの設定値を読みとる

   ControlGet, hctrl, Choice, , REComboBox20W1, %Title%

   sleep 500

     ;MsgBox, hctrl= %hctrl%, %ErrorLevel%

   Send, {Enter}

   Return, hctrl

 }

SelectNextAnimation(UChwnd)

  ; アニメーションウィンドウ内の1つ下のアニメを選択

  {

   ;ControlFocus, UserControl1, A

   ControlFocus, , ahk_id %UChwnd%

   ;Click, 1500,900

   ;ControlSend, UserControl1, {Down} ; 下向き矢印キー

   Send, {Blind} {Down}

   sleep, 500

   Return ErrorLevel

  }

MoveSelectedAnimation(UChwnd,iCurrent,iDest)

  ; 指定された自動アニメーションに移動

  ; iCurrent : 現在の自動アニメーションの番号

  ; iDest : 目的とする自動アニメーションの番号

  {

   ;ControlFocus, UserControl1, A

   If(iDest<iCurrent)

   {

     MsgBox, iDest < iCurrent

     ErrorLevel:= 1

     Return ErrorLevel

    }

    Else

    {

      count := iDest-iCurrent

      Loop, %count%

       {

         ControlFocus, , ahk_id %UChwnd%

         ;Click, 1500,900

         ;ControlSend, UserControl1, {Down} ; 下向き矢印キー

         Send, {Blind} {Down}

         sleep, 500

       }

    }

   Return ErrorLevel

  }  

MoveToNextSlide(MDhwnd)

{

    ; 次のスライドの処理に移行

    ; コントロールはスライドウインドウにフォーカスしたままになる

    WinActivate,  ahk_class PPTFrameClass  

    ;ControlFocus, mdiClass1, A

    ;Send, Ctrl+{Tab}

    ;Sleep, 333

    ;Send, {LAlt Down}

    ;Sleep, 500

    ;Send, {Enter}

    ControlFocus, , ahk_id %MDhwnd%

    ;ControlGetFocus, ClassNN, A

    MsgBox, 4, , %ClassNN%, 0.01 ; これを入れないと動かない!

    ;Click 1383, 900

    ControlSend,,{Blind} {Down},ahk_id %MDhwnd%

    ;Send, {Blind} {Down}   ; 下向き矢印キー

    ;MsgBox, Move to Next Slide

    Sleep, 2000

    Return ErrorLevel

}  

MoveToSelectedSlide(CurSld, DestSld)

{

  ; 指定したスライドに移動

  ; CurSld : 現在のスライド番号,DestSld : 移動したいスライド番号

  ; DestSld > CurSldであることが必要

  Click, 1383,900; スライドウィンドウにフォーカス

  n = DestSld – CurSld

  if(n <= 0){

    MsgBox, DestSld < CurSld)

    Return

  }

  loop, n

  {

    Send, {Blind} {Down}

    Sleep, 100

  }

  Return ErrorLevel

 }

MoveToTopSlide(MDhwnd)

{

    ; 次のスライドの処理に移行

    ; コントロールはスライドウインドウにフォーカスしたままになる

    WinActivate,  ahk_class PPTFrameClass  

    ControlFocus, , ahk_id %MDhwnd%

    MsgBox, 4, , %ClassNN%, 0.01 ; これを入れないと動かない!

    ControlSend,,{Blind} {Home},ahk_id %MDhwnd%

    Sleep, 500

    Return ErrorLevel

}  

AddSoundsSldTrans(fileName) ; 画面切り替えのサウンド付加

{

  WinActivate,  ahk_class PPTFrameClass

  ControlFocus,mdiClass1, A ; スライド表示画面にフォーカス

  Click 1383, 900

  ; リボンタブの“画面切り替え”> “サウンド”を選択

  Sleep, 333

  Send, {LAlt Down}

    Send, k     ; リボンの“画面切り替え”タブを選択

    sleep, 333  

    Send, u     ; “サウンド”タブを選択

    sleep, 333  

  Send, {LAlt Up}

  Sleep,500

  Send, {PgDn}      ; 最下部の“その他のサウンド”を選択

  Send, {PgDn}

  Send, {PgDn}

  Send, {PgDn}

  Send, {PgDn}

  Send, {Enter}

  AddSounds(fileName)

  Sleep, 1000

  Return ErrorLevel

 }

 AddSoundsAnimation(fileName,UChwnd) ; アニメーションにサウンド付加

 ; 注意:アニメーションウインドウが開いていることが条件

 ; サウンド付加の後で,次のアニメーションにコントロールを移動

{

    ;ControlFocus, UserControl1, A

    ControlFocus, , ahk_id %UChwnd%

    ;Click, 1500, 900

    Send, {Appskey} ; メニューを展開し

    Send, e   ; 効果のオプション(E)… を選択

    ;Send, {Enter}  ; Enterで確定

    sleep, 333

    WinGetTitle, Title, A ; ウインドウのタイトルを取得

    ; “アピール”などの効果名がついている

    sleep, 333

    ; メニューの一番下の項目(“その他のサウンド…”)を選択

    Send, !s    ; Alt+sで“サウンド(S)”を選択

    Send, ^{End}  ; Ctrl+Endキーで最下部の項目を選択

    Send, {Enter}   ; Enterで決定  “オーディオの追加”ボックスが開く

    Sleep, 333

    AddSounds(fileName)

    Return ErrorLevel

 }

AddSounds(fileName)

{

  ; オーディオの追加

  ; Window Title: オーディオの追加,フォルダ指定コンボボックス : Edit2

  ; ファイル名(N)指定テキストボックス : Edit1

  ; MsgBox,File Length=%fLength%

   WinActivate, オーディオの追加

   WinWait, オーディオの追加

   ; 保存ファイル名を指定 クリップボード経由

   ControlFocus, Edit1, A

   clipboard = %fileName%

   ClipWait

   outc = %fileName%

   Send, ^a

   Sleep, 333

   ;Send, ^v

   StringReplace,outc,outc,`r`n,`n,All

   outc := RegExReplace(outc,”[!#+^{}]”,”{$0}”)

   Send,%outc%  

   Sleep, 333

   Send, {Enter}

   sleep, 500

   ;OKボタンをクリック

    sleep 333

    ;ControlSend, Button4, {Enter},  %Title% ahk_class #32770

    ControlSend, Button4, {Enter}, A

    WinWaitClose, 音声ファイルの保存 ; 閉じるまで待つ

    FileGetSize, fLength , %fileName%, K

    ;Sleep, 1000

    sleepTime := (fLength ) * 25 + 500

    Sleep, %sleepTime%

    Return ErrorLevel

}

Num2Str(num, numDigits)

{

  ; 整数をnumDigits桁の文字列に変換 ただしnumDigitsは最大5とする

  ; 先頭にnumDigits桁の”00・・0″を追加してから下numDigits桁を取り出す

  StringLeft, zeros,00000,numDigits

  cNum := SubStr(zeros . num,  1-numDigits)  

  Return cNum

 }

CreateFileName(fileNameBase,iSld, iClk, iAut)

 {

  ; ファイル名(拡張子.wav)を生成

  ; iSld : スライド番号1,iClk : クリック番号

  ; iAut : 自動イフェクトの番号

  ; スライドとナレーションの番号を2桁表示用に変換

    ; 先頭に”00″を追加してから下2桁を取り出す

      cSld := SubStr(“00” . iSld, -1)      

      cClk := SubStr(“00” . iClk, -1)      

  fName_no_ext = %fileNameBase%_%cSld%_%cClk%

  If(iAut <>0){

     cAut := SubStr(“00” . iAut, -1)

     fName_no_ext = %fName_no_ext%_%cAut%

   }

  fName = %fName_no_ext%.wav

  Return fName

 }

問題点

AHKで作ったスクリプトは,それほど長いものではありません。それでも,動かすまでは結構苦労しました。

コーディングの初期段階では,変数の扱い方などに癖があるAHKを,C言語の感覚でコーディングしてしまいました。そのためパース段階でのエラーや予期しない動作の原因になかなか気づきませんでした。

スクリプトが動くようになった後も,動作が安定しないという問題が出てきました。音声データの一部が欠けたスライドが1枚程度あります。しかもそのスライドが動かす度に変わります。

挿入すべき箇所に手動でマウスやキーボードによりGUIに対して行っている操作を,AHKスクリプトで実行しています。調べると,ある操作(例えばCtr+cでクリップ動作にコピー)に続いて別の操作(Ctrl+vでテキストボックスに貼り付ける)をするときに,Ctrl+cが完了しないうちに次のCtrl+vを実行してしまうようなことが起きているようです。

そこで,操作の間にsleepを挿入して動作するようにしています。ところが,このスクリプトをインタプリタではなく実行形式に変換して動かすと,また,音声が欠けるスライドが出てきます。

つまり,上に載せたAHKスクリプトは,別のパソコンでの動作は保証されない可能性が高いと考えられます。注意してください。

PPT制御スクリプト

次に,生成された音声データをPPTファイルに埋め込むためのAHKスクリプトについて説明します。

動作環境など
  • コードとライブラリは同じディレクトリに配置する
    AITalkScript.ahkとMyLib.ahkを同じディレクトリに配置する。
  • 入力ファイル
    音声データを生成する際に使ったスライドショースクリプトと同じファイルを使う。また,生成された音声ファイルが格納されたフォルダを用意する。
  • PPTの設定
    PPTは音声ファイルを埋め込んでいないものを使う。PPTを起動したら,スライドの1枚目を表示し,リボンタブは“ホーム”を選択する。アニメーションウインドウなどは閉じておく。
起動の方法

ⅰ)AHKスクリプト本体のファイルアイコンのダブルクリックで起動
ⅱ)スライドショースクリプトを記述したテキストファイルを指定
ⅲ)音声データが格納されたフォルダを指定
ⅳ)作業が終わるまで待つ
 作業の間はマウスやキーボード操作はしない。
ⅴ)The end of the file has been reached.が表示されたらリターンキーを押す

AHKスクリプト

AHKスクリプトを以下に示します。後述するように,いろいろ問題アリです。

#NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.

; 変数名を解釈するとき、環境変数を無視する。

; #Warn  ; Enable warnings to assist with detecting common errors.

#Include MyLib.ahk

SendMode, Input  ; Recommended for new scripts due to its superior speed and reliability.

; 入力の再生速度を重視した設定

SetWorkingDir, %A_ScriptDir%  ; Ensures a consistent starting directory.

; スクリプトのあるディレクトリパス

;msgbox % “Your AHK version is ” A_AhkVersion ; バージョン確認用

FileEncoding , UTF-8 ; 日本語対応

SetKeyDelay, 20

; PPTファイルに音声を付加する

; 音声合成に用いたものと同一のテキストファイルに従って動作する

; エスケープ用の先頭文字を決める

; 以下の文字列が先頭にある行は,ナレーションとしては処理しない

  ComStr  := “%”    ; コメント指定用文字

  NumStr  := “#”    ; アニメ番号指定用文字

  SldStr  := “#S”   ; スライド指定用文字

; FileSelectFileコマンドでPPTスクリプトファイルのパスを取得

  FileSelectFile, textFilePath

; ファイル名を抽出し,出力ファイル名のベースにする

  SplitPath, textFilePath, name, dir, ext, name_no_ext, drive

  ; %name_no_ext% ; 拡張子を除いたファイル名

; 出力ファイルのフォルダを指定

; 既存のフォルダを使う場合,ファイルは全て削除しておかないと

; ファイル作成の際に,上書き許可のプロンプトが表示される

  FileSelectFolder, outputDir , %A_ScriptDir%, 1

  fileNameBase = %outputDir%\%name_no_ext% ; ファイル名のベース

; リボンタブからアニメウインドウを開く

; 確認動作が未実装なので注意

   ActivateAnimeWindow()

   ; アニメーションウィンドウのコントロールのハンドル取得

   ControlGet, UChwnd, Hwnd , , UserControl1, A

    If(ErrorLevel = 1){

     MsgBox, UChwnd = %UChwnd%, %ErrorLevel%

    }  

   ; スライド表示ウィンドウのコントロールのハンドル取得

   ControlGet, MDhwnd, Hwnd , , mdiClass1, A

    If(ErrorLevel = 1){

     MsgBox, MDhwnd = %MDhwnd%, %ErrorLevel%

    }

   ; 先頭のスライドに移動

   MoveToTopSlide(MDhwnd)

   ; アニメーションウィンドウのトップを選択

   SelectAnimationTop(UChwnd)

   Sleep, 1000

; PPTスクリプトファイルのテキストを1行ずつ読み込み,指示に従って動作

iLine = 0   ; 行番号の初期値

iSld  = 0   ; スライド番号の初期値

iNar := -1  ; スライド中のナレーション番号

iClk := 0   ; スライド中のクリック番号

iAut := 0   ; 自動スタートアニメのスタートからの番号

iAni := -1  ; アニメーション番号の初期値

Loop

{

  iLine += 1

  FileReadLine, line, %textFilePath%, %iLine% ;変数lineに1行読み込む

    If ErrorLevel <> 0 ; ファイルから読めなくなったらループを抜ける

     {

       Break

      }

     ; 1行ずつ確認する場合の記述例:

     ;MsgBox, 4, , Line #%iLine% is “%line%”.  Continue?

     ;IfMsgBox, No

     ;  Return

  StrLength := StrLen(line)   ; 1行の文字数を取得

  If( StrLength != 0 )      ; 空白行でない場合は処理する

  {

  ; 冒頭の文字から行の属性とオプションの数字num1,num2を取得

    ; 属性:Comment,Slide,Number, Narration

    ; num1,num2:スライドまたはアニメーションの番号

      lineAtb := CheckLine(line, ComStr,NumStr,SldStr,num1,num2)

    ; コメント,スライド,番号指定以外の属性ならナレーションを音声合成    

    If(lineAtb = “Comment”) ; コメント行ならループの先頭に移行

     {

       Continue ; 以下の処理をスキップしてループの先頭に移行

     } ; End of If(lineAT = “Comment”)

    If(lineAtb = “Number”)  ; 番号指定行ならアニメ番号を1増加してループ先頭へ

      {

        iAni += 1

        iNar += 1

        iClk := num1 ; スライド内のクリック番号

        iAut := num2 ; 自動開始イフェクトの番号

        ; アニメ番号0は画面切り替えのナレーション,

        ; アニメ番号1は1番初めのアニメなので,フォーカスはトップのまま

        ; アニメ番号2以上では,アニメ番号を1だけ増加してループトップへ

        If(iAni >=2) {

          If(iAut = 0){     ; 自動起動アニメではない場合

            iCurrent := 0   ; 現在の自動アニメ番号を0にクリア

            MoveNextClick(UChwnd)

          }

          Else {            ; 自動アニメ番号iAutに移動

            MoveSelectedAnimation(UChwnd,iCurrent,iAut)

          }

        }

        Continue

       } ; End of If(lineAtb = “Number”)

    If(lineAtb = “Slide”)   ; スライド指定行ならスライド番号を1増加し,

       {                    ; その他の番号をクリアしてループ先頭へ

         iSld += 1

         iNar := -1

         iClk := 0

         iAut := 0

         iAni = -1

         If(iSld <> 1) {    ; スライドが1番目でない場合にPPTスライドを1つ進める

          MoveToNextSlide(MDhwnd)

         }

         SelectAnimationTop(UChwnd)

         Continue

       } ; End of If(lineAtb = “Slide”)

    ; コメント行でも番号指定行でもなけばナレーション付加の処理を実行

     ; ファイル名を定め,存在するかどうか確認

     fileName := CreateFileName(fileNameBase,iSld, iClk, iAut)

     FileGetSize, fLength , %fileName%, K

     IfNotExist, %fileName% ; ファイルが存在しない場合はループから抜ける

       {

         MsgBox, file %fileName% does not exist.

         break

       } ; IfNotExist, %fileName%

     ; ファイルが存在する場合は,以下の処理を実行

     If(iAni = 0){  ; iAni = 0の場合,画面の切り替えのサウンドを付加する

      AddSoundsSldTrans(fileName)

      Sleep, 1000

      ;MsgBox, 切替えのサウンドを挿入しました

      SelectAnimationTop(UChwnd)

      }

      Else ; iAni >=1の場合は,アニメーションのサウンドを付加する

      {

        Sleep, 500

        AddSoundsAnimation(fileName,UChwnd)

        ;Sleep, 500

      }    

  } ; ここまで文字列の長さが0でない場合の処理

} ; end of loop

MsgBox, The end of the file has been reached.

MoveToTopSlide(MDhwnd)

SelectAnimationTop(UChwnd)

exitApp ; AutoHotKeyを終了

Esc::ExitApp  ; Exit script with Escape key

問題点

こちらも,音声データ生成用のスクリプトと同じで,安定性に問題があります。

しかし最大の問題は,処理速度が遅いことです。PPTの編集モードでは,音声データをファイルから指定して埋め込む際にその音声を実際に再生しています。この再生が終わる前に次の操作をすると誤動作するようです。

そこで,音声データのサイズに応じた長さのスリープを挿入するようにしました。これで動作はするようになったのですが,「大急ぎでプレゼンするのと同程度の時間」がかかってしまいます。しかも,処理中はAHKがPPTのGUIを操作しているわけですから,パソコンを使うことはできないのです。これでは,自動化のメリットはほとんどありません。(複数のPPTファイルを処理するようにして,パソコンを使っていない時間帯に動かすことはできるでしょう。)

今後の対応

PPTファイルへのl音声データの埋め込みは,AHKを使った方法では使い勝手が悪そうです。PPTファイルに直接音声データを埋め込む方法を考えることにします。

PPTのファイルフォーマットであるpptxは,xml形式のファイルと画像やオーディオデータのファイルをまとめてzip圧縮されたものになっています。ファイル拡張子をzipに変更してスライドデータのファイルを調べると,Pythonコードで編集できそうなことがわかりました。

Pythonの環境をそろえたりするので時間がかかりそうですが,目途がついたら報告します。

コメント