プログラミング | TamLab 授業のおもちゃ箱 https://tamlab.fc2.page 情報系科目の講義資料とツールを公開 Sat, 30 Nov 2024 11:02:47 +0000 ja hourly 1 https://wordpress.org/?v=6.7.2 PythonアプリでSpeech SDKを使う https://tamlab.fc2.page/category-system/category-programing/2551/ https://tamlab.fc2.page/category-system/category-programing/2551/#respond Sat, 30 Nov 2024 11:02:46 +0000 https://tamlab.fc2.page/?p=2551 Pythonで作るアプリケーションソフトにMicrosoft AzureのSpeech SDKを組み込もうとしています。主な目的は,PPTXファイルにナレーションを組み込むアプリ(PowerpointNarrator)のテキスト入力の補助です。インストールの手順などの備忘録として残しておきます。

Azureについて

Azure(アジュール,Microsoft Azure)は,Microsoftのクラウドコンピューティングサービスです。様々なサービスの中にSpeech Serviceがあり,マイクロフォンから入力した音声データをテキスト化するSpeech to Textなどに使うことができます。

以下,参考にしたURLはその都度紹介します。

SDKのインストールの手順

Quickstart: Recognize and convert speech to text

クイック スタート: Speech SDK のインストール

を参考にしました。

準備

以下のような手順で進めました。

1)システム要件を確認

  • Windowsの場合,64bitターゲットアーキテクチャが必要(Windows10以降である必要)
  • プラットフォームに対応した Visual C++ 再頒布可能パッケージがインストールされていること
  • Python のバージョンは 3.8 以降

2)Azureアカウントを作成し音声サービスを使えるようにする

私はAzureのアカウントを持っていないので,次のような作業になりました。

  • Azureアカウントを作成する
  • サービス用のAzure サブスクリプションとSpeech resourceを作成
  • Speech resource key と regionコードを取得する

少し詳しく説明します。

・Azureアカウントを作成する
 https://azure.microsoft.com/ja-jp/free/ にアクセスしてアカウントを作成しました。
 Microsoft Accountを持っていることが必要です。Azureの無料アカウントは、新規Azureユーザーのみ1つだけ利用可能となっています。
 必要な情報を入力し,最後にクレジットカードによる認証を求められました。あくまでも認証のためで,入金が必要になった場合は別途要求する・・・とのことです。しかし,もしこのサイトがMicrosoftを騙るサイトだったらえらいことになっちゃいます。いったん作業をやめて考えてから,もう一度やり直しました。

支払いが発生しないアカウントはカード無しで作成するようにしてもよいのにと思います。ユーザ側もきちんとサービスが働いていることを確認できれば,偽サイトではないとわかるのですから。でも,始めのうちはサービスを提供するようにした巧妙な偽サイトを作られたら,どうしようもありませんが。

・Azure サブスクリプションを作成
 Azure のPortalサイトのホームで“サブスクリプション”と表示された鍵の形のアイコンをクリックし新規の無料サブスクリプションを作成しました。サブスクリプションの名前はサイト側で用意した“Azure subscription 1”をそのまま使いました。

・音声サービスのリソースを作成
 ホームから“音声サービス”のページに移動し,メニューにある“作成”をクリックするとSpeech Services の作成というタイトルのページに移動します。

次の図1のように,幾つか設定する項目があります。

図1 Speech Services の作成ページの表示

  • サブスクリプション
    使用するサブスクリプションを選びます。“Azure Subscription 1”しかないので,そのままにします。
  • リソースグループ
    既存のグループはないので,新規作成します。リソース名はSpeech2Textとしました。
  • リージョン
    アカウントを持つユーザの所属する地域です。Japan Eastを選択しました。(図1ではEast USとなっています。)
  • 名前
    適当に入力しました。ちゃんと考えて決めればよかった・・・。
  • 価格レベル
    Free F0を選択しました。

この後の操作で,「タグ名と値」の設定を要求されました。上の図には表示されていません(再現できなかった)。タグ名:Tag0,値:0として設定して,特に今のところ問題はないようです。

・Speech resource key と regionコードを取得
 音声サービスをPythonコードから利用する際にSpeech resource key とregionコードが必要になります。これらを取得するには,音声サービスリソースのページに移動します(ホームのリソース欄でリソース名をクリック)。“キーとエンドポイント”という項目を見ます。

図2 キーとエンドポイントなどの設定

Speech resource key:“キー1”の右端をクリップしてをコピーして取得しました。長い文字列です。このキーは共有しないようにすることが必要なので,作成したアプリを他のユーザに使ってもらう場合には注意が必要です。

regionコードは,“場所/地域”の欄に書かれている“japaneast”という文字列です。

Speech SDK for Pythonのインストール

クイック スタート: Speech SDK のインストール というサイトの記述に従って作業をしました。

pip install azure-cognitiveservices-speech

でインストールしました。

動作確認

サンプルコードをダウンロード

動作確認するため,サンプルコードを探しました。以下の2つのファイルをダウンロードしました。

quickstart.py 参考にしたサイトのURL

speech_recognition.py 参考にしたサイトのURL

リスト1 quickstart.py

# Copyright (c) Microsoft. All rights reserved.
# Licensed under the MIT license. See LICENSE.md file in the project root for full license information.

# <code>
import azure.cognitiveservices.speech as speechsdk

# Creates an instance of a speech config with specified subscription key and service region.
# Replace with your own subscription key and service region (e.g., "westus").
speech_key, service_region = "YourSubscriptionKey", "YourServiceRegion"
speech_config = speechsdk.SpeechConfig(subscription=speech_key, region=service_region)

# Creates a recognizer with the given settings
speech_recognizer = speechsdk.SpeechRecognizer(speech_config=speech_config)

print("Say something...")


# Starts speech recognition, and returns after a single utterance is recognized. The end of a
# single utterance is determined by listening for silence at the end or until a maximum of about 30
# seconds of audio is processed.  The task returns the recognition text as result. 
# Note: Since recognize_once() returns only a single utterance, it is suitable only for single
# shot recognition like command or query. 
# For long-running multi-utterance recognition, use start_continuous_recognition() instead.
result = speech_recognizer.recognize_once()

# Checks result.
if result.reason == speechsdk.ResultReason.RecognizedSpeech:
    print("Recognized: {}".format(result.text))
elif result.reason == speechsdk.ResultReason.NoMatch:
    print("No speech could be recognized: {}".format(result.no_match_details))
elif result.reason == speechsdk.ResultReason.Canceled:
    cancellation_details = result.cancellation_details
    print("Speech Recognition canceled: {}".format(cancellation_details.reason))
    if cancellation_details.reason == speechsdk.CancellationReason.Error:
        print("Error details: {}".format(cancellation_details.error_details))
# </code>

5行目に
import azure.cognitiveservices.speech as speechsdk
とあります。音声/テキスト変換のサービスをするために,このimport文が必要になります。

また,9行目の,
speech_key, service_region = “YourSubscriptionKey”, “YourServiceRegion”
とあります。式文の右辺にある文字列”YourSubscriptionKey”を取得したSpeech resource keyの長い文字列に,”YourServiceRegion”を”japaneast”に,それぞれ置き換えます。

リスト2 speech_recognition.py

import os
import azure.cognitiveservices.speech as speechsdk

def recognize_from_microphone():
    # This example requires environment variables named "SPEECH_KEY" and "SPEECH_REGION"
    speech_config = speechsdk.SpeechConfig(subscription=os.environ.get('SPEECH_KEY'), region=os.environ.get('SPEECH_REGION'))
    speech_config.speech_recognition_language="en-US"

    audio_config = speechsdk.audio.AudioConfig(use_default_microphone=True)
    speech_recognizer = speechsdk.SpeechRecognizer(speech_config=speech_config, audio_config=audio_config)

    print("Speak into your microphone.")
    speech_recognition_result = speech_recognizer.recognize_once_async().get()

    if speech_recognition_result.reason == speechsdk.ResultReason.RecognizedSpeech:
        print("Recognized: {}".format(speech_recognition_result.text))
    elif speech_recognition_result.reason == speechsdk.ResultReason.NoMatch:
        print("No speech could be recognized: {}".format(speech_recognition_result.no_match_details))
    elif speech_recognition_result.reason == speechsdk.ResultReason.Canceled:
        cancellation_details = speech_recognition_result.cancellation_details
        print("Speech Recognition canceled: {}".format(cancellation_details.reason))
        if cancellation_details.reason == speechsdk.CancellationReason.Error:
            print("Error details: {}".format(cancellation_details.error_details))
            print("Did you set the speech resource key and region values?")

recognize_from_microphone()

動かしてみた

quickstart.pyを使って動作確認します。

マイクロフォンを接続して実行します。コンソールに何か喋るようにプロンプトが表示されるので,日本語でしゃべると,図3のようにテキストが表示されます。でも,英語アルファベットを使ったよくわからない文字列が表示されました。

図3 何の言語だろう?

変換の対象となる言語の設定が日本語設定になっていないためでしょう(デフォルトの言語が何なのかは,わかりません)。

もう1つのサンプル speech_recognition.pyを見ると,7行目に
speech_config.speech_recognition_language=”en-US”
とあります。ここで,言語をアメリカ英語に設定しているようです。Implement language identificationなどのサイトを見ると,この文字列を”ja-JP”に変換すれば日本語になりそうです。そこで,quickstart.pyの10行目の後ろに,

speech_config.speech_recognition_language=”ja-JP”

という行を追加して動かすと,マイクロフォンから入力した日本語の音声をテキストに変換できました。かな漢字変換もできています。

図4 ちゃんと日本語に変換された

他の言語の指定については,Speech SDKのドキュメントで確認するとよいでしょう。

以上で大体のことがわかりました。SDKのドキュメントを探して,それを参考にしながらアプリに組み込んでいく予定です。

]]>
https://tamlab.fc2.page/category-system/category-programing/2551/feed/ 0
PPTXファイルに字幕を付ける 使い方の説明 https://tamlab.fc2.page/category-materials/2218/ https://tamlab.fc2.page/category-materials/2218/#respond Tue, 09 Apr 2024 10:18:59 +0000 https://tamlab.fc2.page/?p=2218 PPTXファイルに音声や文字のナレーションを埋め込むWindowsPC用ソフトを作っています。オーディオのナレーションを付けることを目的として作り始めたソフトでした。しかし字幕を付けられるようにしてみると、これだけでも使い道が多そうな気がしてきました。

そこで、今回の投稿ではスライドショーに字幕を付けるやり方の例を紹介します。せっかくなので開発中のソフトで使った字幕付きスライドショーを使うことにします。

なお、このソフトを「このアプリ」とか「開発中のソフト」などと呼んできました。Pythonファイルの名前は“AddAudio2pptx.py”などとしていましたが、何かもう少しそれっぽい名前で呼ぶことにしましょう。適切かどうかわかりませんが、“Powerpoint Narratorパワーポイントナレーター)”と呼ぶことにします。注)

<注を読む>

ChatGPTで、以下のように質問しました。

「Powerpoint Narratorという名前のソフトウェアがありますか? もしあるとすれば、どのようなことをするソフトウェアだと考えられるでしょうか?」

回答は以下のようでした。

「2022年1月時点では、Powerpoint Narratorという名前のソフトウェアは存在しませんでした。ただし、Microsoft PowerPointには、スライドのナレーションを録音する機能があります。これにより、プレゼンテーションを行う際にスライドごとに音声を付けることができます。この機能を利用することで、スライドショーを再生するときに自動的にナレーションが再生されるようになります。また、Microsoft PowerPointには、アクセシビリティを向上させるための機能もありますので、それらの機能の組み合わせによって、スライドのナレーションを作成するソフトウェアとしての機能を果たすことができるかもしれません。」

<注を閉じる>

では、以下のように3つに分けて説明をしていきます。

PPTXスライドを準備
スクリプトを作る
ナレーションを付ける

PPTXスライドを準備

Powerpoint Narratorは、現時点ではPPTXのスライドの自動生成や生成をサポートする機能は持っていません。まずはじめにナレーションを付ける元のPPTXファイルを準備する必要があります。

ここで想定しているのは、1枚のスライドの中に複数ステップのアニメ効果を持つようなPPTスライドショーです。数行の文字列が一度に表示されるだけのスライドが延々と続くようなスライドショーでは字幕を付けるだけのためならPowerpoint Narratorは不要です。PPTアドインのiSpring Suiteを使えばPPTのノートの記述を字幕として表示するHTMLスライドショーが作れてしまうからです。

スライドの準備についての説明は次のスライドショーでご覧ください。

1)PPTXスライドを準備

スクリプトを作る

PPTXファイルの準備ができたら、スライドショーの「スクリプト」を作ります。スクリプトは、スライド切替えやマウスクリックを示す文字列と、これらに同期して発声や表示をするナレーションのテキストを順番に並べたものです。スクリプトはテキストファイルの形で作成し保存されます。

スクリプトは複数の作り方が可能です。今回紹介するのは、現時点で一番楽で失敗が少ないと思われる方法です。

次のスライドショーをご覧ください。

2)スクリプトを作る

スクリプトは、以下の規則に従って作ります。

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

上記のルールは,AutoHotKeyを使ったアプリ開発(PTファイルに音声を自動で埋め込む その1 AHKによる実装)の際に作ったものです。現在のアプリはPythonでコーディングされていて,上のルールから外れた記述でも動作することがわかっています。以下にまとめます。

  • スライド切替え時のナレーション
    #00 という文字は略してもよい。例えば,2枚目のスライド切替え時のナレーションは,“#S02”の次の行に記述すればよい。
  • ナレーションを付加しないスライドやアニメーションの番号は略してもよい
    例えば,3枚目のスライドの2番目のクリックのみにナレーションを付加する場合は,
    #S03
    #02
    ×××・・・・・(ナレーションのテキスト)
    というスクリプトで動作する。
  • コメント行
    #Snnや#mmなどのエスケープ文字列を先頭に持つ行は,これらの文字列の後ろに1つ以上の空白文字があれば,その後ろに挿入した文字列は改行コードまでが無視される。これをコメント文字列として使うことができる。

ナレーションを付ける

スクリプトに従ってPPTXスライドショーにナレーションを付けた新しいPPTXファイルを作ります。

手順は次のスライドショーをご覧ください。

3)ナレーションを付ける

Powerpoint Narratorの操作ウインドウにある“Audio”と“Caption”というラベルのついた2つのチェックボックスで、付けるナレーションの種類を選択します。Audio(音声ナレーション)とCaption(字幕)の両方を同時に付けることも可能です。ただし、音声ナレーションを埋め込むためには、オーディオファイルをあらかじめ作成しナレーションフォルダ(上の例では“PPT_Test”という名前)に格納しておく必要があります。また、既に字幕やオーディオのナレーションを付けてあるPPTXファイルを対象とした場合、どうなるかはこれから確認する予定です。

感想

  • ちょっと疲れるかも
    今回の投稿では、Powerpoint Narratorで字幕をつけたPPTXファイルを説明に使いました。スライドの枚数が少し多かったので閲覧する人は疲れるかもしれません。
  • 字幕の問題
    スライドの画像と字幕が、両方とも視覚を使うことからくる「わかりにくさ」の問題は、少なくとも私は感じました。オーディオのナレーションの方が速く頭に入りそうです。また、字幕の背景の色やスライド本体との区切りの方法など、改善すべき点があります。
  • スライドの枚数は適切か?
    講義の場合には、手元に配布資料もあり、それに目を通しながらスライドショーを閲覧できるので、スライド枚数が多くても分かりにくくなることはなさそうです。しかし、webページの場合は、スライドを見ているときにページの他の場所を見るのは難しくなります。スライドが切り替わってしまうので、何枚か前の画像を記憶していることも難しくなるでしょう。スライドの枚数や画像の提示の仕方については、もう少し研究が必要になりそうです。
  • 作る方も疲れる
    スライドショーが長いと、見るほうも疲れるかもしれませんが、作る方も疲れます。一番大変なのは、PPTスライドショーを作る過程です。この過程に費やす時間を短くして、しかも内容が伝わり易いスライドを作ることをサポートするソフトが必要だと感じました。例えば、よく使うアニメのシーケンスを自動生成する機能とか、です。
  • こんな機能が欲しくなる
    次に時間がかかるのが、スクリプトの作成です。これは、スライドショーをクリックにより進行させながらナレーションを喋って録音するような機能を、Powerpoint Narratorに持たせることで改善できそうです。録音した音声をテキストに変換すれば、スクリプトの編集の時間を短くできるかもしれません。(勉強しないといけないので、だいぶ先になりそうです。)
    また、録音したナレーションだけで良い場合もあると思います。それに対応するようにコードを変更するのは、そんなに難しくはなさそうです。
]]>
https://tamlab.fc2.page/category-materials/2218/feed/ 0
PPTXファイルを直接書き換えて音声や字幕を付加 https://tamlab.fc2.page/category-materials/2159/ https://tamlab.fc2.page/category-materials/2159/#respond Thu, 04 Apr 2024 08:33:18 +0000 https://tamlab.fc2.page/?p=2159 PPTXファイルに音声や文字のナレーションを埋め込むWindowsPC用ソフトを作っています。コーディングに時間が取れなかったこともあって、前回の投稿からだいぶ間が空いてしまいました。でも開発は「遅々として進む」という状況で、幾つかの機能を付け加えてきました。この数週間での大きな成果(と自分で思っていること)は「クリックと連動する字幕を表示できるようになった」ことです。

作ったスライドショーの例

このソフトで字幕を付けたPowerpointスライドショーの例を以下に示します。自分で動かしてみてください。表示画面下の矢印ボタンのクリック、スライド画面内にポインタを置いた状態でのマウスクリック,あるいは矢印キーなどでスライドショーが進行します。

字幕をつけたスライドショーの例

実際の講義でも,この例と同様に数式の変形過程などを細かなステップに分け,何回もクリックしてアニメで表示し,口頭で説明を加えています。このため,このPPTスライドショーを音声や字幕が付いていないままweb用に変換しても,どういう意図で細かくステップ分けしているかを分かっていただけない可能性があります

<注を読む>

なお,上のPPTスライドショーは,iSpring社のPPTアドインであるiSpring Suite 10を使ってHTML5ファイルに変換しています。このソフトは有料ですが,同様のことをするフリーのアドインや,PPTファイルを変換してくれるフリーのwebサイトもあります。

<注を閉じる>

このソフトウェアでできること

  • 音声ナレーションと字幕をスライドショーに付加
    PPTXのスライドショーに音声や字幕によるナレーションを自動で(つまりPPTのGUI操作をしないで)付加できる。また「かんたん!Aiトーク5」を制御して、テキストから自動的にオーディオデータを作ることができる。
  • スクリプトにより自動実行
    付加される音声ナレーションや字幕は、スライドの進行やアニメを制御するマウスクリックなどと同期している。この動作は、スライド切替え、マウスクリック、ナレーションの内容をテキストで表して順番に並べた「スクリプト(台本)」に従って作られる。 
  • サポート機能
    スクリプトの作成や、オーディオデータの確認と再編集をサポートする機能などが実装されている。

字幕を付けることの意義

もともとは,私自身の講義の記録(アーカイブ)と,学生さんの自習に使うことを考えてソフトを作り始めました。そのときは,字幕は「おまけ」の機能と考えていたのです。講義でスライドショーを見せる場合は,教員による口頭での説明があるので字幕は不要です。自習の場合も字幕は邪魔かなと考えていました。スライドは視覚により情報を伝えています。字幕という視覚による情報伝達がさらに加わると,「見て読んで理解する」という視覚による認知の負担が増えてしまいます。それで,あまり教育の現場での需要は少ないかも,と考えました。

<注を読む>

字幕のような視覚情報が増えても学習の効率は低下しない,と主張する人もいます(星 友啓:脳を活かすスマホ術 スタンフォード哲学博士が教える知的活用法、朝日新聞出版(2023))。でも、多様な学生を対象とした調査結果を見てからでないと、判断は難しいと思います。

<注を閉じる>

しかし,字幕だけで済むならファイルの容量を減らせます。また,聴覚が弱かったり、口頭での説明の理解に時間がかかったりする学生さんには助けになるでしょう。web上で閲覧できるスライドショーなら学習する人が自分のペースに合わせた進行や繰り返しができます。ですから視覚的負担の増加はそれほど問題にならないと考えられます。

スライドショーに字幕を付けることの効用や弊害,どのような使い方が効果的か,などは今後の研究課題だと思います(既に研究されているかもしれませんが)。簡単に字幕を付けられるソフトがあれば,そのような研究の助けになると思います。

技術的な内容の説明に移るの前に,背景となっている事情について説明します。過去の投稿とダブる部分もあります。興味のある方は目をとおしてください。

<詳しく読む>

技術的には・・・PPTXファイルを直接書き換えている

GUIでもできることをしている

まず,はじめに申し上げておきます。開発しているソフトは,これまで技術的に不可能だったことを可能にするようなものではありません。

アニメを起動するマウスクリックにオーディオの効果を付けたり,テキストボックスで作った字幕を出現させたり消したりすることは,全てPowerpointの標準的なGUIを使ってできてしまいます。しかし実際問題として,少しナレーションの数が増えるとGUIを使った作業は非常に時間がかかるものになります。字幕の作成も,正確な位置にテキストボックスを作成し,文字を入れ,出現と消滅のアニメの効果を付ける,ということを繰り返すので,字幕の数が少なくても大変です。

自動化ソフトでも問題あり

それでは,ソフトウェアを自動で操作するRPAツールやPythonなどで作ったGUI信号生成ソフトを使う方法はどうでしょうか? 開発しているアプリの音声ナレーションを合成したり追加する部分は、AHK(AutoHotKey)スクリプトやPythonコードによるGUI制御により作っていました。しかし動作が遅いのです。オーディオデータが揃った状態で「用意ドン!」すれば,物凄く手の速い人には負けるんじゃないかな,という感じでした。

また,動かすたびにタイミングが微妙にずれるため動作が不安定で,そのための誤動作もありました。Pythonコードにしたことで改善されましたが、それでも時々誤動作します(“時々”というのが厄介です)。一番問題なのは,動かしている間はPCを使った他の作業ができないということです(これはちゃんとしたRPAツールなら解決されている問題かもしれません)。

PPTファイルの直接操作

そこで,PPTのファイルを直接書き直す方法を採用することにしました。Pythonのライブラリにpython-pptxというものがあり,これを使うとPPTXファイルの操作ができそうです。それで大いに期待したのですが,アニメの操作が可能かどうかがよくわかりません(たぶん私の調べ方が下手なせいだと思います)。

PPTXファイルの中身は複数のxml(Extensible Markup Language)ファイルやデータファイルをまとめてzip圧縮した形になっています。したがって,解凍すればpython-pptxを使わなくてもxmlファイルの書き直しやデータの追加が可能になります。

開発中のソフトは,基本的にはzip展開されたPPTXファイルの中身を直接書き換えたりデータファイルを追加することで音声や字幕の追加を実行しています。ほとんどがテキスト操作になります。

もちろん,ファイルの書き換えなので,圧縮してPPTXファイルにしてみたらPPTアプリで開けなくなっているという危険性があります。書き換えた結果がxmlの規則に反している可能性もあります。実際,アプリの動作確認をしている段階では,出来上がったPPTXを開こうとしても「壊れているので修復しますか?」といったメッセージが出され,修復して開いても,意図した動作はしないというケースが多くありました。(ごく最近も思わぬ原因で誤動作することが分かりました。)

そこで,このアプリでは,元のPPTXファイルはそのままにして手を付けないようにしています。zip展開したフォルダ内のファイルだけを操作し,最後に圧縮して別の名前のPPTXファイルを作るようにしています。

<閉じる>

字幕付加の処理の全体の流れ

字幕を付加するソフトは,PPTXファイルをzip展開した中にあるslide1.xml,slide2.xml・・・などのxmlファイルを直接書き換えています。処理の全体を理解するため,まず,次のスライドショーをごらんになってください。

アプリは,大きく分けると以下のことをしています。

  • テキストボックス用のshapeの生成
    字幕はテキストボックスを使って表示します。テキストボックスはPPTスライド上の図形であるshapeの1つです。スライド毎にslide1.xml,slide2.xml,・・・という名前で作られたxmlファイルの中のShape Treeと呼ばれる構造の中に以下のようなxmlコードのブロックを挿入していきます。
  • アニメの記述を<p:timing>要素に追加
    スライド画面上にshapeを出現(英語版のPPTでは“enter”のようです)させたり消したり(“exit”)させるアニメのタイミングや表現方法について<p:timing>~</p:timing>の間で記述します。
  • 必要ならばスライドの寸法を変更する
    字幕用のテキストボックスはスライド画面の一番下に配置しています。元々ある図形や文字が字幕に隠されないように,スライド画面の寸法を下の方向に増やしてそこに字幕を配置するモードも用意しました。この操作はslide.xmlではなく別のxmlファイル(presentation.xml)を書き換えることになります。

字幕用shapeの生成と挿入

図形(shape)の形や位置の情報は,<p:spTree>要素の中にある<p:sp>~</p:sp>の間で記述される<p:sp>要素で表されます。実際に挿入される字幕用テキストボックスの<p:sp>要素の例は,以下のようになります。

      <p:sp>
        <p:nvSpPr>
          <p:cNvPr id="9" name="Caption 1">
            <a:extLst>
              <a:ext uri="{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}">
                <a16:creationId xmlns:a16="http://schemas.microsoft.com/office/drawing/2014/main" id="{DF488853-4B8B-58A9-2718-B1BC0D4C3A87}" />
              </a:ext>
            </a:extLst>
          </p:cNvPr>
          <p:cNvSpPr txBox="1" />
          <p:nvPr />
        </p:nvSpPr>
        <p:spPr>
          <a:xfrm>
            <a:off x="0" y="6172200" />
            <a:ext cx="9144000" cy="685800" />
          </a:xfrm>
          <a:prstGeom prst="rect">
            <a:avLst />
          </a:prstGeom>
          <a:solidFill>
            <a:srgbClr val="FFFFCC" />
          </a:solidFill>
          <a:ln>
            <a:solidFill>
              <a:schemeClr val="tx1">
                <a:lumMod val="50000" />
                <a:lumOff val="50000" />
              </a:schemeClr>
            </a:solidFill>
          </a:ln>
        </p:spPr>
        <p:txBody>
          <a:bodyPr wrap="square" rtlCol="0">
            <a:spAutoFit />
          </a:bodyPr>
          <a:lstStyle />
          <a:p>
            <a:r>
              <a:rPr lang="ja-JP" altLang="en-US" sz="1600" b="1" dirty="0" />
              <a:t>クリック1です。</a:t>
            </a:r>
          </a:p>
        </p:txBody>
      </p:sp>

Shape Treeに挿入されたコードの例

字幕用shapeの生成

朱書きされた部分をアプリが書き換えてshapeを生成します。以下,上から順に説明していきます。

  • i<p:cNvPr id=”9
    <p:cNvPr・・・からnon-visual properties要素の記述になります。“id =”の次の数字がshapeの識別番号になります。異なるshapeには異なる数字が割り当てられます。この数字はPPTの編集でshapeを作るたびに1ずつ増加して割り当てているようです。
  • name=”Caption 1
    shapeの名前です。日本語版のPPTではテキストボックスには“テキストボックス1”のような全角文字列で名前が割り当てられています。開発中のアプリでは“Caption”+“数字”という半角文字列で名前を割り当てています。数字のない“Caption”だけにしても問題なく動いていますが,後で手動で調整する際,字幕ごとに名前が異なっていた方が良いかもしれないので,異なる番号を割り当てるようにしています。
  • <a:off x=”0” y=”6172200” />
    字幕の位置です。なお,位置や寸法の単位はEMU(English Metric Unit)と呼ばれるものになっています。1インチ=914400EMU,1cm=360000EMU,文字の大きさの単位の1pt=12700EMUと,全て整数のEMU値になります。
  • <a:ext cx=”9144000” cy=”685800” />
    shapeのx方向,y方向の拡がり(extension)つまり寸法です。この例では,横幅がPPTの4×3スクリーンでの10インチ幅(9144000EMU)と同じになっています。
  • <a:srgbClr val=”FFFFCC” />
    テキストボックスの色です。24bitのRGBで指定します。この例では薄い黄色になります。その他,テキストボックスの枠線の色や幅も変更可能ですが,アプリのコードでは固定しています。
  • <a:t>クリック1です。</a:t>
    朱書きの文字が字幕として表示する文字列になります。その前の行の,
    <a:rPr lang=”ja-JP” altLang=”en-US” sz=”1600″ b=”1″ dirty=”0″ />
    で全角/半角の切り替えをしています。

Shape Treeへの挿入

必要な書き換えを行った<p:sp>要素をShape Treeに追加していきます。方法は簡単です。Shape Treeの末尾である</p:spTree>を見つけ,その直前に<p:sp>要素を挿入します。shapeの識別番号,Shape IDは,Shape Tree内を走査して見つかったshape ID値の最大値を求め,それに1を加えた値を新しいshapeに割り当てています。

<p:timing>要素の書き換えと挿入

作った字幕は,そのままではスライド下部の字幕領域に重なった状態で表示されています。これにクリックに同期して出現したり消えたりするアニメ効果を与える必要があります。このための<p:timing>要素の書き換えは,shapeの追加よりはちょっと面倒臭い操作になります。

<p:timing>要素の書き換え

以下は,字幕を表示させるために<p:timing>要素に追加されたコードブロックの例です。

                              <p:par>
                                <p:cTn id="5" presetID="1" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="withEffect">
                                  <p:stCondLst>
                                    <p:cond delay="0" />
                                  </p:stCondLst>
                                  <p:childTnLst>
                                    <p:set>
                                      <p:cBhvr>
                                        <p:cTn id="6" dur="1" fill="hold">
                                          <p:stCondLst>
                                            <p:cond delay="0" />
                                          </p:stCondLst>
                                        </p:cTn>
                                        <p:tgtEl>
                                          <p:spTgt spid="8" />
                                        </p:tgtEl>
                                        <p:attrNameLst>
                                          <p:attrName>style.visibility</p:attrName>
                                        </p:attrNameLst>
                                      </p:cBhvr>
                                      <p:to>
                                        <p:strVal val="visible" />
                                      </p:to>
                                    </p:set>
                                  </p:childTnLst>
                                </p:cTn>
                              </p:par>
                            </p:childTnLst>
                          </p:cTn>
                        </p:par>

<p:timing>要素に挿入されたコードの例

朱文字はアプリが書き換える文字列,緑文字は注目してほしい箇所です。<p:par>~</p:par>で1つの単位となります。

  • <p:cTn id=”5
    挿入されるTime nodeの内容をここから記述していきます。項目ごとに識別番号idが割り当てられます。この番号は,<p:timing>要素の一番上から1で始まり,順番に1つずつ増えていきます。
  • presetClass=”entr
    図形を出現(entry,だと思います)させる効果を指定しています。PPTアプリ(日本語版)では“開始”というアニメーションのクラスに属していることを示します。“開始”クラスのアニメの効果にはフェードインなど様々なパターンがありますが,図形全体が一度に出現するシンプルな“表示”にしています。字幕を消すときは,この文字列”entr“を”exit“にします。
  • nodeType=”withEffect
    直前のクリックと同時に字幕が表示されるようにしています。
  • <p:cTn id=”6
    挿入するコードブロックの中には2つの<p:cTn>要素があるため,idを指定する箇所も2つあります。
  • <p:spTgt spid=”8” />
    コードブロックで記述している効果を与えるshapeを,その識別番号(Shape ID,spid)で指定しています。この例ではspidが”8″であるshapeを対象としています。

コードブロックの<p:timing>要素への挿入

パラメータを書き換えたコードブロックを<p:timing>要素に挿入します。手順は以下のようにしています。

ⅰ)<p:timing>~</p:timing>内でアニメを起動するクリックを見つける
 文字列“clickEffect”を見つけ,スクリプトに載っている番号のものを選ぶ。
ⅱ)“clickEffect”の後で最初に見つけた</p:par>の直後に字幕表示開始のブロックを挿入
 </p:par>がこのクリックで起動されるアニメ効果の記述の終わりになるので,その直後に字幕開始のコードブロックを挿入する。
ⅲ)次のクリックを見つけ,その後ろに字幕終了のブロックを挿入
 これで,次のクリックにより字幕の表示が終わります。このクリックが新しいアニメを起動するものになっていれば,同時に次の字幕が現れます。
ⅳ)最後に<p:timing>要素内のcTn idを割り当て直す
 Shape Treeとは異なり,<p:timing>要素内のcTn idは昇順にする必要があるので,スライド内の全ての字幕のコードの挿入が終わったところで,cTn idの再割り当てをする。

なお,ⅲ)の字幕終了ブロックの挿入については,次のクリックが見つからない場合はスライドの最後に達したことになります。ここでPPTアプリの操作者がマウスをクリックすると,次のスライドに移ります。同時に字幕も消えますので,スライドの一番最後の字幕に関しては消すためのコードブロックは入れないようにしています。クリックで最後の字幕を消し,次のクリックでスライドが切り替わるようにしても良いのですが,ただでさえ多いクリック回数を増やさないようにしました。

とりあえずここまでにしておきます。ソースコードについても忘れないうちに投稿したいです(そもそもこのwebサイト自体,私の備忘録として作ったものです)。

でも,コードがどんどんスパゲッティ化していて,とてもじゃないけど人様の目に耐えられないものになってきています。メンテナンス性も悪く,久しぶりにコーディングに取り掛かると,頭が復帰するのにすごく時間がかかってしまいます。小さなコードから紹介をしていくことになると思います。

]]>
https://tamlab.fc2.page/category-materials/2159/feed/ 0
ドラッグ&ドロップで実行形式ファイルも添付可能なGmail用zipファイルを処理 https://tamlab.fc2.page/category-materials/category-materials-tools/1913/ https://tamlab.fc2.page/category-materials/category-materials-tools/1913/#respond Mon, 26 Jun 2023 07:56:11 +0000 https://tamlab.fc2.page/?p=1913

.exeや.vhdなどの拡張子を持つファイルをgmailに添付できるようにするWindowsアプリをPythonで作成してみました。gmailサーバが実行形式などのファイルの添付を拒否する問題はone driveを使うなどの方法で回避できます。ですから,このアプリにはそれほど需要があるとは思えません。しかし,ドラッグ&ドロップでの起動など,いろいろ勉強になったので,記録に残しておきます。

きっかけは,ある時期からVHDLのソースコード(拡張子は.vhd)をgmailの添付で送信できなくなったからです。中身はテキストデータなのに拡張子が.vhdになっているだけでgmailのシステムが添付を拒否してしまいます。(.vhdはVertual Hard Driveファイルの拡張子でもあるので,安全性を考えれば仕方がないことですが。)

VHDLソースコードをgmailで送受したい場合は,拡張子.vhdを.txtに書き直して送信し,受信したら拡張子を.vhdに戻せばよいのです。でもファイルの数が多くなると面倒なので,Pythonで拡張子変更とzip圧縮/解凍をするPythonコードを作ってみました。

そこで止めておけばいいものを,実行形式ファイルも送受できるコードにしてみました。Pythonのzip圧縮/解凍のコードの挙動やドラッグ&ドロップでの起動に興味があったからです。

注意:

<注意を読む>

この投稿は,十分にテストをしていないプログラムの開発過程の備忘録として書かれたものです。紹介されているコードにはバグや冗長な部分が含まれています。また,十分な例外処理を組み込んでいないので,重要なファイルの入っているフォルダを削除したり上書きしたりする可能性もあります。コードを使ったり参考にしたりする場合は注意をお願いします。

また,将来Pythonライブラリやgmailの仕様が変更されると機能しなくなる可能性もあります。

<閉じる>

使い方

実行ファイルは以下のボタンをクリックしてダウンロードしてください。

添付用フォルダを作る

空のフォルダを準備し,添付したいファイルを格納します。サブフォルダは作らないでください。

フォルダのアイコンを,gmailZippy.exeのアイコンにドラッグ&ドロップします。

ドラッグ&ドロップ

zipファイルができる

ドラッグしたフォルダと同じディレクトリに圧縮されたzipファイルが現れます。これをgmailに添付して送ります。

受信したファイルを解凍する

gmailで受信したzipファイルをgmailZippy.exeにドラッグ&ドロップします。解凍されたフォルダの中には,送信したファイルが格納されています。拡張子の書き替え/復元をしたファイルは,exe,vhdなどの名前のフォルダごとに整理されています。

zipファイルをD&D       →         復元された添付フォルダ

処理の流れ

送信側では空のフォルダを用意します。仮に名前をXXXとします。その中に送信したいファイルを格納します。

作成したコードは圧縮用と解凍用の2つの関数を持ち,圧縮/解凍と拡張子の書き換えを自動で行います。コードの処理の手順は以下のようになっています。

圧縮:

  1. フォルダXXXの中にある.exe,.vhdなどの拡張子を持つファイルの拡張子を.txtに変換しサブフォルダを作って整理
  2. フォルダ“XXX”をzip圧縮 → XXX.zipが生成される
  3. XXX.zipの拡張子を.zippyに変更してXXX.zippyにする
  4. XXX.zippyをフォルダに格納してzip圧縮 → XXX.zipが生成される

このXXX.zipをgmailに添付して送信します。

なお,上の1~4の操作をWindows OSを使った手動操作で行う場合は,XXX.zippyをフォルダに入れないでそのまま“右クリック>ZIPファイルに圧縮する”によりXXX.zipというファイルができ,これをzip展開すると,XXXフォルダの中にXXX.zippyが格納されたものが得られます。しかし,Pythonコードで実行する場合は,1度目の圧縮で生成されたXXX.zippyをフォルダに格納してから2度目の圧縮をする必要があります。

解凍:

  1. XXX.zipをzip解凍 → XXX.zipyが格納されたXXXフォルダが得られる
  2. XXX.zippyをXXX.zipにリネーム
  3. XXX.zipをzip解凍 → 解凍されたファイルを格納したフォルダXXXが得られる
  4. フォルダ内のファイルの拡張子を元に戻す

以上の圧縮と解凍の手順は手動で実行してもよいのです。なお,中間の圧縮ファイルの拡張子として.zippyを使っています。使われていない拡張子なら何でもよいと思いますが,確認はしていません。また,XXX.zippyをXXXにリネームしても動作するように思えますが,その場合の処理手順についても試しておりません。

Pythonコードの説明

ソースコードは1つのファイル“gmailZippy.py”にまとめられています。リストを上から順に説明していきます。

ライブラリ

ライブラリ宣言部分です。

# gmailZippy.py gmail添付用の拡張子変更・圧縮・解凍
# Change extension and compress/decompress to attach with gmail
# Copyright © 2023 Tamlab All rights reserved. ver. 1.0
from tkinter import filedialog
from tkinter import messagebox # for debugging
import os
import sys
import shutil
import glob
  • from tkinter import messagebox
    公開しているコードの中では使ってないが,デバッグの際にmessagebox関数を使用した。生成したファイルの上書や処理できないファイルの入力などの際の表示に使う予定。

拡張子書き換え

ファイルの拡張子を.txtに書き換える関数です。書き換えたファイルは書き換え前の拡張子の名前をつけたサブフォルダを作って,その中に格納します。

def ext_change(ext,temp_dir):
    # ext: file extention tring, temp_dir: working directory
    # 文字列extで与えられる拡張子を.txtに変更,Change the file extention to '.txt'
    path_name = temp_dir + '/*.' + ext
    files = glob.glob(path_name)
    if len(files) != 0: # If the corresponding file exists, do the following
        target_dir = temp_dir + '/' + ext
        os.makedirs(target_dir)  
        for file in files:  
            file_no_ext = os.path.splitext(os.path.basename(file))[0]
            name_dst = temp_dir + '/' + file_no_ext + '.txt'
            os.rename(file, name_dst) 
            shutil.copy(name_dst, target_dir) # ファイルを格納用フォルダに移動
            os.remove(name_dst)
  • def ext_change(ext,temp_dir):
     ext:拡張子の“.”を除いた文字列
     temp_dir:拡張子を書き換えたファイルを格納する一時フォルダの名前

拡張子の復元

拡張子を元に戻す関数です。解凍したフォルダに格納されているファイルの拡張子.txtを元のファイルの拡張子に戻します。

def ext_recovery(ext, folder_path): # 拡張子を復元, file extention recovery
    target_dir = folder_path + '/' + ext 
    path_name = target_dir + '/*'  # 対象とするフォルダ内の全てのファイル
    files = glob.glob(path_name)
    for file in files:    
        file_no_ext = os.path.splitext(os.path.basename(file))[0]
        name_dst = target_dir + '/' + file_no_ext + '.' + ext
        os.rename(file, name_dst) 
  • def ext_recovery(ext, folder_path):
     ext:戻したい拡張子の“.”を除いた文字列
     folder_path:拡張子を書き換えるファイルを格納しているフォルダの名前
  • path_name = target_dir + ‘/*’
    files = glob.glob(path_name)
     target_dirはフォルダパスの文字列。この文字列を”XXX””とすると,path_nameは”XXX/*”となる。次のglob関数でフォルダXXXの下にある全てのファイルのリストが得られる。

圧縮

フォルダを圧縮する関数です。まずext_change関数でフォルダ内のファイルの拡張子を書き換え,次に圧縮します。圧縮では,まずzip圧縮し,得られた圧縮ファイルの拡張子を.zipから書き換え,再度zip圧縮しています。

zip圧縮を2回実行するのですが,2回目の圧縮で,ちょっとひっかりました。

Windows OSの操作ではXXX.zippyをzip圧縮すると,XXX.zipというファイルができます。これを解凍するとXXX.zippyを格納したXXXフォルダが得られます。

ところが,Pythonコードでshutil.make_archiveを使うと,2回目の圧縮ではXXX.zippy.zipというファイルができますが,中身の情報が失われています。そこで,XXX.zippyをXXXフォルダに格納したものを作り,これをshutil.make_archiveで圧縮しています。

def compress(path_name):
    # 非圧縮状態のフォルダを指定,Specify a object folder in uncompressed state.
    file_dir = path_name
    file_name = os.path.splitext(os.path.basename(path_name))[0]
    os.chdir(file_dir) # Sets the current directory to the specified folder.
    os.chdir('../')
    target_dir = os.getcwd()

    temp_dir = file_dir + '/temp' # Create temporary directory.
    if os.path.isdir(temp_dir):
       shutil.rmtree(temp_dir)
    shutil.copytree(file_dir , temp_dir) # Copy all files to temp_dir. 

    ext_change('vhd', temp_dir) # Change file extentions to '.txt'.
    ext_change('exe', temp_dir)
    ext_change('py', temp_dir)

    # フォルダをzipにアーカイブ, Archive folder to zip file.
    # First compression
    shutil.make_archive(temp_dir, format='zip', root_dir=temp_dir)
    # Rename .zip -> .zippy
    temp_name = temp_dir + '.zip'
    temp_name_zippy = temp_name + 'py'
    os.rename(temp_name, temp_name_zippy)

    # Renew temp directory
    shutil.rmtree(temp_dir)
    os.mkdir(temp_dir)

    # Copy the .zippy file to temp directory
    temp_dir_file = temp_dir + '/temp.zippy'
    shutil.copyfile(temp_name_zippy, temp_dir_file)   

    # Second compression
    shutil.make_archive(temp_dir, format='zip',root_dir=temp_dir)

    # Copy the compressed folder to destination directory.
    dst_name = file_name + '.zip'
    shutil.copy(temp_name, dst_name)
    
    # 一時フォルダの削除
    os.remove(temp_name)
    shutil.rmtree(temp_dir)
    os.remove(temp_name_zippy)
  • ext_change(‘vhd’, temp_dir)
    ext_change(‘exe’, temp_dir)
    ext_change(‘py’, temp_dir)
     書き直す拡張子は,現在,”.vhd”,”.exe”,”.py”の3種類。.pyは書き直さなくても大丈夫だと思うが,サブフォルダに整理する機能を利用するため使っている。将来,対応したい拡張子が増えた場合は,この関数を追加していけばよい。
  • shutil.make_archive(temp_dir, format=’zip’, root_dir=temp_dir)
     1回目のzip圧縮。
  • shutil.rmtree(temp_dir)
    os.mkdir(temp_dir)
     作業ディレクトリを一旦,中のファイルと共に削除し,その後で同じ名前のディレクトリを作っている。
  • temp_dir_file = temp_dir + ‘/temp.zippy’
    shutil.copyfile(temp_name_zippy, temp_dir_file)
     空になっている作業用ディレクトリに.zippyファイルを格納。
  • shutil.make_archive(temp_dir, format=’zip’,root_dir=temp_dir)
     2回目のzip圧縮。

解凍

解凍するための関数です。zip展開し,得られたファイルの拡張子を.zippyから.zipに戻し2度目のzip展開をして,最後にファイルの拡張子を元に戻します。

def decompress(path):
    # Decompress the zippy archive.
    fileName = path
    # Get the string of the file name without the file extension.
    folder_path = fileName.replace('.zip', '')
    if os.path.exists(folder_path):
        folder_path = filedialog.askdirectory(
            title = "Specify a folder.",
            initialdir = dir) 
    # zipファイルを展開,Unzip the file.
    zippy_path = folder_path + '/temp.zippy'
    zip_path = folder_path + '/temp.zip'
    shutil.unpack_archive(fileName, folder_path) # 1回目の展開, first extraction
    os.rename(zippy_path, zip_path)
    shutil.unpack_archive(zip_path,folder_path)  # 2回目の展開, second extraction
    os.remove(zip_path)  # 一時フォルダの削除, Remove temp folder. 
    ext_recovery('vhd', folder_path) # File extention recovery.
    ext_recovery('exe', folder_path)
    ext_recovery('py', folder_path)
  • folder_path = fileName.replace(‘.zip’, ”)
    if os.path.exists(folder_path):
    folder_path = filedialog.askdirectory(
    title = “Specify a folder.”,
    initialdir = dir)
     zip展開された後に得られる“XXX”と同じ名前のフォルダが存在する場合は,フォルダ名を改めて指定するようにしている。
  • shutil.unpack_archive(fileName, folder_path)
    os.rename(zippy_path, zip_path)
    shutil.unpack_archive(zip_path,folder_path)
     zipファイルを展開し,拡張子を.zippyから.zipに戻してから2度目のzip展開をしている。
  • ext_recovery(‘vhd’, folder_path)
    ext_recovery(‘exe’, folder_path)
    ext_recovery(‘py’, folder_path)
     拡張子を復元する関数。復元したい拡張子の数だけ関数を呼び出す。

main関数

main関数です。引数path_nameでフォルダまたはzipファイルのパスを指定します。パスがフォルダのものであればcompress関数を,zipファイルのものであればdecompress関数を呼び出します。

def main(path_name):
    if os.path.isdir(path_name):
        compress(path_name)
    else :
        path_ext = os.path.splitext(path_name)[1]
        if path_ext == '.zip':
            decompress(path_name) 

ドラッグ&ドロップで起動する

main関数を呼び出す部分です。これにより,フォルダやファイルのアイコンをドラッグ&ドロップすることでアプリケーションを起動するようになっています。

if __name__ == '__main__': # If the script is activated as main,
    # sys.argv[1] is the dragged and dropped path name.
    main(sys.argv[1])
  • if __name__ == ‘__main__’:
     変数__name__ は,このPythonコードがメインとして起動されたとき’__main__’という文字列になる。つまり,Pythonコードがメインとして起動されたときに,ifブロック内の式文が実行される。
  • main( sys.argv[1])
     フォルダやファイルをアプリのアイコンにドラッグ&ドロップして起動すると,sys.argv[1]に そのファイルやフォルダのパスが格納される。このパスを引数としてmain関数を呼び出す。

<VSCodeでのデバッグのときは・・・>

コードの開発はVisual Studio Codeを使っています。デバッグモードで実行させることで,設定したブレークポイントの位置で変数の値などをチェックできます。しかし,ドラッグ&ドロップで実行させると,VSCodeを介さず実行させるらしく,ブレークポイントが機能しません。それでデバッグ中は,main関数の部分を,以下のように変更しています。

  • if __name__ == __main__’:
    arg_d = ‘aaa・・・a’
    main(arg_d)
     文字列aaa・・・aには動作テストに使うフォルダやzipファイルのパス文字列を記しておきます。

私が知らないだけで,VSCodeを使ってドロップ&ドロップで起動するコードをデバッグする方法はあるのかもしれません。

<閉じる>

コードの全体

改めてコード全体のリストを示します。

# gmailZippy.py gmail添付用の拡張子変更・圧縮・解凍
# Change extension and compress/decompress to attach with gmail
# Copyright © 2023 Tamlab All rights reserved. ver. 1.0
from tkinter import filedialog
from tkinter import messagebox # for debugging
import os
import sys
import shutil
import glob

def ext_change(ext,temp_dir):
    # ext: file extention tring, temp_dir: working directory
    # 文字列extで与えられる拡張子を.txtに変更,Change the file extention to '.txt'
    path_name = temp_dir + '/*.' + ext
    files = glob.glob(path_name)
    if len(files) != 0: # If the corresponding file exists, do the following
        target_dir = temp_dir + '/' + ext
        os.makedirs(target_dir)  
        for file in files:  
            file_no_ext = os.path.splitext(os.path.basename(file))[0]
            name_dst = temp_dir + '/' + file_no_ext + '.txt'
            os.rename(file, name_dst) 
            shutil.copy(name_dst, target_dir) # ファイルを格納用フォルダに移動
            os.remove(name_dst)

def ext_recovery(ext, folder_path): # 拡張子を復元, file extention recovery
    target_dir = folder_path + '/' + ext 
    path_name = target_dir + '/*'  # 対象とするフォルダ内の全てのファイル
    files = glob.glob(path_name)
    for file in files:    
        file_no_ext = os.path.splitext(os.path.basename(file))[0]
        name_dst = target_dir + '/' + file_no_ext + '.' + ext
        os.rename(file, name_dst) 

def compress(path_name):
    # 非圧縮状態のフォルダを指定,Specify a object folder in uncompressed state.
    file_dir = path_name
    file_name = os.path.splitext(os.path.basename(path_name))[0]
    os.chdir(file_dir) # Sets the current directory to the specified folder.
    os.chdir('../')
    target_dir = os.getcwd()

    temp_dir = file_dir + '/temp' # Create temporary directory.
    if os.path.isdir(temp_dir):
       shutil.rmtree(temp_dir)
    shutil.copytree(file_dir , temp_dir) # Copy all files to temp_dir. 

    ext_change('vhd', temp_dir) # Change file extentions to '.txt'.
    ext_change('exe', temp_dir)
    ext_change('py', temp_dir)

    # フォルダをzipにアーカイブ, Archive folder to zip file.
    # First compression
    shutil.make_archive(temp_dir, format='zip', root_dir=temp_dir)
    # Rename .zip -> .zippy
    temp_name = temp_dir + '.zip'
    temp_name_zippy = temp_name + 'py'
    os.rename(temp_name, temp_name_zippy)

    # Renew temp directory
    shutil.rmtree(temp_dir)
    os.mkdir(temp_dir)

    # Copy the .zippy file to temp directory
    temp_dir_file = temp_dir + '/temp.zippy'
    shutil.copyfile(temp_name_zippy, temp_dir_file)   

    # Second compression
    shutil.make_archive(temp_dir, format='zip',root_dir=temp_dir)

    # Copy the compressed folder to destination directory.
    dst_name = file_name + '.zip'
    shutil.copy(temp_name, dst_name)
    
    # 一時フォルダの削除
    os.remove(temp_name)
    shutil.rmtree(temp_dir)
    os.remove(temp_name_zippy)

def decompress(path):
    # Decompress the zippy archive.
    fileName = path
    # Get the string of the file name without the file extension.
    folder_path = fileName.replace('.zip', '')
    if os.path.exists(folder_path):
        folder_path = filedialog.askdirectory(
            title = "Specify a folder.",
            initialdir = dir) 
    # zipファイルを展開,Unzip the file.
    zippy_path = folder_path + '/temp.zippy'
    zip_path = folder_path + '/temp.zip'
    shutil.unpack_archive(fileName, folder_path) # 1回目の展開, first extraction
    os.rename(zippy_path, zip_path)
    shutil.unpack_archive(zip_path,folder_path)  # 2回目の展開, second extraction
    os.remove(zip_path)  # 一時フォルダの削除, Remove temp folder. 
    ext_recovery('vhd', folder_path) # File extention recovery.
    ext_recovery('exe', folder_path)
    ext_recovery('py', folder_path)

def main(path_name):
    if os.path.isdir(path_name):
        compress(path_name)
    else :
        path_ext = os.path.splitext(path_name)[1]
        if path_ext == '.zip':
            decompress(path_name) 
            
if __name__ == '__main__': # If the script is activated as main,
    # sys.argv[1] is the dragged and dropped path name.
    main(sys.argv[1])

使ってみた感想

圧縮のときに,動作が遅いように感じます。2回の圧縮の間に実行しているファイルの削除やコピーなどの手間が解凍処理のときよりも多いためかもしれません。コードも圧縮処理の方がゴチャゴチャしています。時間があれば見直したいです。

圧縮したファイルをgmailに添付すると,アップロードの際,プログレッシブバーの表示があるところで停止して,しばし“考え込んで”いるような挙動を示します。gmailが添付可能なファイルを判断するアルゴリズムが不明なのですが,何やら怪しんでいるような雰囲気です。

実際にファイルをgmailで他のPCとやりとりしてみると,実行形式ファイルも送受できています。しかし,他のPCでは解凍の際と解凍した実行ファイルを実行させる際にWindowsのセキュリティシステムが警告を表示してきます。開発に使ったPCでは,そのような警告は出てきません。

さて,このコードで気になるのは安全性の問題です。zip圧縮は自己解凍の機能が無いはずだし,受け手側で幾つかのステップの処理をするので,比較的安全だと考えています。しかし,自己解凍や自動起動の機能を持つ悪意のあるプログラムの送受に利用できてしまうことは,問題かもしれません。

実際にgmailでのファイル添付に使う場合は,用途を教育用のコードにやり取りに限って利用していただくのが適切だろうと考えています。

]]>
https://tamlab.fc2.page/category-materials/category-materials-tools/1913/feed/ 0
PPTファイルに音声を自動で埋め込む その6 配布用実行ファイル https://tamlab.fc2.page/category-materials/1730/ https://tamlab.fc2.page/category-materials/1730/#respond Sun, 07 May 2023 01:31:27 +0000 https://tamlab.fc2.page/?p=1730 pptファイルにオーディオデータを埋め込むアプリケーションソフトに機能を追加し,実行ファイルを作成しました。追加した機能は,その4で「今後の予定」として挙げていた4つの機能のうちの,自動アニメーションへのオーディオデータの付加,スクリプト作成補助の機能pptのノート機能の利用,の3つです。

作成したアプリの動作中のGUI画面

これらの機能を追加するのは後回しでもよいと思っていたのですが,結局全部を付け加えてしまいました。考えてみたら,使う上では必要だったからです。

とりあえず一応の機能がそろったので,本来やりたかった「講義資料の整理」の作業に使うための実行ファイルも作成しました。こうしておくと,Pythonをインストールしていない環境でも使うことができます。個人的な事情になりますが,VSCodeを立ち上げていると,ついコードをいじりたくなって時間がつぶれてしまうのですが,それを防ぐこともできます。

実行ファイル

Windows用の実行ファイルと動作検証用のpptxファイルなどをまとめたzipフォルダは,以下のリンクからダウンロードできます。

フォルダの内容

解凍したフォルダはAddAudio2PPTX_testという名前です。下の図のように2つのフォルダ,AddAudio2PPTX,testが入っています。

AddAudio2PPTX_testの内容

AddAudio2PPTXフォルダの中には2つのファイル,AddAudio2PPTX.exeとconfig.iniがあります。

testフォルダには動作確認用のデータが入っています。

    AddAudio2PPTXフォルダの内容               testフォルダの内容

ナレーションフォルダtestの内容

詳しく説明します。

  • AddAudio2PPTXフォルダ
    実行ファイルと設定ファイルが格納されている。
    • AddAudio2PPTX.exe
      実行ファイル
    • config.ini
      設定ファイル。実行ファイルと同じディレクトリに配置する。
  • testフォルダ
    動作確認用のデータとして,以下のファイルとフォルダが格納されている。
    • test.pptx
      動作確認用pptxファイル。アニメーションは設定済み。ノートのテキストは空白になっている。
    • test.txt
      スクリプトファイル。
    • test(音声ファイルのフォルダ)
      音声ファイルと対応するテキストファイルが格納されている。テキストファイルは「かんたん!AITalk 5」が合成の際に自動的に生成したもので,現時点では利用していない。

動作確認をする場合は以下の点に注意してください。

  • 実行ファイルはWindows 11(64bit)環境で作成
  • 音声合成ソフトは「かんたん!AITalk 5」を使用
    「かんたん!AITalk 5」を持っていない場合は,音声合成ソフトの制御機能は使えない。
  • GUI制御をする場合の注意
    「かんたん!AITalk 5」を制御するときは,他のソフトでの作業はしないこと。他のソフトのウインドウにマウスクリックやホットキーの信号が送られ,編集中のファイルなどに予期しない影響を与える恐れがある。
  • 自作のpptxファイルで動作確認する場合はバックアップを取っておく
    ファイル名の設定によっては,大事なデータが上書きされてしまうかもしれない。(コード上は,その可能性はないと考えているが,見落としがあるかもしれない。)
  • 予期せぬ挙動に注意
    「アプリ」としての完成度は低い。例えば,存在しないファイルやディレクトリを対象として設定した場合の挙動など,起こり得るすべての状況を想定した確認はしていない。

動作確認の手順

テスト用のデータに対する処理を例に,使い方を説明します。

  • ⅰ)実行ファイルと設定ファイルの配置
    • AddAudio2PPTXフォルダに入っているAddAudio2PPTX.exeとconfig.iniを,ローカルディスクの適当な場所に配置する。2つのファイルが同じディレクトリ階層にあればよい。
      ただし,C:\Program Filesフォルダの中に入れるのは避けた方がよいだろう。ここに配置して動作させたところGUI画面からアプリを終了できなくなった。
  • ⅱ)動作確認用データの配置
    • testフォルダごと,適当な場所に配置する
  • ⅲ)起動 
    • 起動:AddAudio2PPTX.exeのアイコンをダブルクリックして起動する。

起動画面

メニューバー > ツール > AITalk 5の実行ファイルのパス を起動

  •  「かんたん!AITalk 5」の実行ファイルのパスを指定(初回のみ)
    • 「かんたん!AITalk 5」を使って音声合成をする場合は,初めての起動の際にAITalkの実行ファイルのパスを指定する。通常のインストールでは,パスは
      C:/Program Files/AI/KantanAITalk/KantanAITalkEditor/KantanAITalk.exe
      となっている。
      操作ウインドウの上部のメニューバー > ツール (図の⓪)から“AITalk 5の実行ファイルのパス”を起動することにより,ファイル指定のダイアログボックスが開くので,KantanAITalk.exeを探して選択し,「開く」ボタンをクリックする。
  • ⅳ)ファイルとフォルダのパスをセット
    • オーディオデータを埋め込む対象となるpptxファイル(①)オーディオデータのファイルを格納したナレーションフォルダ(②)プレゼンテーションの台本を記述したスクリプトファイル(③),の3つのパスを,それぞれの“Set”ボタンをクリックして設定する。個別にセットする場合は,Autoチェックボタン④のチェックを外しておく。
      ①,②,③の3つが同じフォルダの同じ階層に格納されていて,しかも拡張子を除いた名称が同じになっている場合は,④にチェックを入れておき,①のpptxファイルのパスだけを設定すれば,②と③は自動でセットされる。(testフォルダをそのまま使う場合は,この条件を満たしている。)
  • ⅴ)オーディオデータの付加
    • “Add audio”ボタン(⑤)をクリックすると,ナレーションフォルダ内のwav形式ファイルがスクリプトに記述された指示に従ってpptxファイルに埋め込まれる。埋め込まれたファイルは,対象としたpptxファイルの名前に“_A”をつけた新しいファイルとしてフォルダ内に生成される。元のpptxファイルは変更されない。
  • ⅵ)オーディオデータの新規作成(「かんたん!AITalk 5」をインストールしている場合のみ)
    • “Make new audio”ボタン(⑥)をクリックすると,オーディオデータの新規作成が開始される。作成されたデータは,ナレーションフォルダの内容を全て削除してから書き込まれる。
  • ⅶ)作成されたファイル/フォルダの確認
    • zip形式で配布した動作確認用ファイルをそのまま使用した場合,フォルダの中には新たに以下のファイルとフォルダが作成される。
      • test_A.pptx:オーディオデータを埋め込んだpptxファイル
      • test_zip:test_A.pptxに圧縮する前のフォルダ
        この中身を調べると,pptxファイルの内部構造がわかる。学習用に,削除せず残すようなコードにしてある。

オーディオ付加の処理後のtestフォルダの内容

補助用の機能

アプリには基本的な機能に加え,作業の補助用に以下の機能を追加しています。

スクリプトの作成・編集の補助

  • pptxファイルからスクリプトのスケルトンを作成
    • アニメーションを設定したpptxファイルからアニメーションの情報を読み取り,スクリプトのスケルトン(骨組み)を作成する。スケルトンにナレーションのテキストを追加することで,スクリプトを生成できる。
    • メニュー > ファイル > pptxファイルからスクリプトのスケルトンを作成
      により実行される。ファイル名を指定可能。
  • スクリプトをpptxのノートにコピー
    • スクリプトをpptxファイル内のノートのテキストブロックに書き込む。一度,書込んでおくと,次からはpptアプリのみでスクリプトの編集が可能になる。
    • メニュー > ファイル > スクリプトをノートに上書き
      により実行される。元のpptxファイルのノートのテキストブロックにスクリプトが書き込まれる。
  • pptxのノートからスクリプトファイルを作成
    • pptxファイル内のノートのテキストブロックに書かれたスクリプトからスクリプトファイルを作成する。
    • メニュー > ファイル > ノートからスクリプトを作る
      により実行される。作られるスクリプトのファイル名を指定できる。

  スクリプトのスケルトン         ナレーションを付け加えたスクリプト

スクリプトを自動で書き込んだ後のpptxノート

これらの補助機能を使えば,pptxファイルのノートに書いたスクリプトからオーディオデータの自動埋め込みが可能になります。pptxのノートを利用することで,メモ帳などのテキスト編集ソフトが不要になり,スライドの順番の変更も容易になります。(詳しいことはコードの解説のところで触れますが,スクリプトはスライド番号を指定しなくても動作するようにできます。)

その他

動作状態を表示するため以下のようにしています。

  • データ処理中の表示
    “Add audio”と“Make new audio”のボタンはクリックして処理を開始すると,処理が終了するまでボタンの色が赤く変わるようにしている。
  • 処理時間の表示
    ステータス等の表示用にテキストウインドウを追加した。現在,オーディオデータ付加と新規オーディオ作成の処理の終了時に,消費時間を表示するようになっている。

コードについて

アプリのPythonコードの解説については,現在,投稿の準備中です。読んだ人が同じ機能のシステムを再現できるだけでなく,読んだ人の頭の中で「機能する知識」を再現できることを目指したいのですが,心もとない状況です。(そもそも読む人がいるのか,という問題もありますが・・・。)

]]>
https://tamlab.fc2.page/category-materials/1730/feed/ 0
PPTファイルに音声を自動で埋め込む その5 Pythonによる音声合成ソフトの制御 https://tamlab.fc2.page/category-materials/1676/ https://tamlab.fc2.page/category-materials/1676/#respond Sun, 23 Apr 2023 23:04:27 +0000 https://tamlab.fc2.page/?p=1676 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が提供する「ゴミ箱」に移動されるのではなく本当に削除される(と私は理解しています)ので,大変危ないです。

]]>
https://tamlab.fc2.page/category-materials/1676/feed/ 0
PPTファイルに音声を自動で埋め込む その4 GUIを持つアプリ https://tamlab.fc2.page/category-materials/1642/ https://tamlab.fc2.page/category-materials/1642/#respond Thu, 13 Apr 2023 10:17:41 +0000 https://tamlab.fc2.page/?p=1642 pptファイルのスライド切替えとアニメーションにオーディオデータを埋め込むアプリ(といっていいものか・・・)を作りました。初めに作ったAHKでPowerpointのGUIを操作してオーディオデータを設定するソフトは,動作に時間がかかり過ぎました。

そこで,直接pptxファイルにオーディオデータを埋め込む方法に変更しました。試行錯誤の末,何とか講義資料の作成に使えそうなコードができてきました。初めに動作確認のため使っていたものは,動かす度に3回もダイアログボックスを開いてファイルやフォルダを設定するものでした。使いにくいし時間もかかるので,勉強を兼ねてGUIで操作するものを作ることにしました。

アプリの概要

下が作りかけのアプリの操作画面です。必要な最低限の設定しかできないものです。

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

作成の目的

何回か説明しているので,くどいと怒られそうですが,おさらいします。このアプリの目的は講義用のPowerpointプレゼンテーションのスライド切替えとアニメーションに音声ナレーションを自動で挿入することです。1学期15週の講義のために毎週二,三十枚のスライドがあり,各スライドに数個~30個程度のクリック起動のアニメを使います。他に補足説明や演習用のスライドも必要です。とてもたくさんのオーディオ付加の作業が必要になります。

音声データを挿入するため,スライド切替えやクリックの操作と,操作毎に埋め込むナレーションを「台本」(スクリプトと呼んでいます。)として記述したテキストデータを作ります。このテキストデータから,音声合成ソフトのGUIを操作してオーディオデータのファイルを自動生成するAHKコードを作成しました。(スクリプトを作るのは,音声合成の自動実行のためでもありますが,もう1つは,講義の記録・アーカイブのためです。)

次にとりかかったのが,オーディオデータの生成に使ったのと同じスクリプトファイルを使って,pptxファイルにGUIを介せずオーディオデータを埋め込むPython コードの作成です。ある程度行けそうだ,という感触を得たので,GUIの操作コードを追加しました。

ここまで説明しましたように,元々は個人的な限定された目的のために作ったものです。でも,PPTファイルに音声を自動で埋め込む その2 Pythonによる実装の準備 やPPTファイルに音声を自動で埋め込む その3 Pythonによる実装 で紹介したPythonコードを組み合わせることで,様々な用途に合わせたアプリを作ることができるはずです。

現時点での機能

作りかけで,今後,変更していくことになりますが,以下のようにして使います。

GUIの操作

ⅰ)ファイルやフォルダを指定する
 対象となるファイルや,それらが格納されているフォルダ(ディレクトリ)を指定する。ファイルやフォルダはGUI画面上に①~③で示したテキストウインドウに表示される。
 ①対象となるpptxファイル
 ②音声ファイルを格納したフォルダのパス
 ③プレゼンのスクリプトファイル
  これらのファイルやフォルダは,
 ④ファイル/フォルダのセットボタン
  をクリックすることで,個別に設定できる。クリック1回で設定できるように,
 ⑤自動設定のON/OFF を設定するためのチェックボックスがある。
  ここにチェックマークを付けておくと,pptxファイルを指定するだけで,
  事前に決めたルールに従ったファイルやフォルダが自動で設定される。

ⅱ)スクリプトファイルの内容を確認
 スクリプトファイルが指定されると,その内容がテキストウインドウに表示されれる,
 (⑥スクリプトを表示)今のところ,表示するだけ・・・。編集や保存もできるようにしたい。

ⅲ)オーディオ付加の実行
 ⑦スタートボタン をクリックすることで,オーディオを埋め込む作業が開始する。元のpptxファイルの拡張子の前に”_A”が追加されたpptxファイルが同じフォルダ内に作られる。

自動設定用のファイルの配置とルール

pptxファイル,音声データのフォルダ,スクリプトファイルは,以下のようなルールで作っておくと,pptxファイルを1回セットするだけで,自動で設定される。

ファイルとフォルダの配置(朱書きしてあるのは生成されるフォルダとファイル)

  • 3つのファイルとフォルダはディレクトリの同一階層に配置する
    pptxファイル,音声データのフォルダ,スクリプトファイルは同一のフォルダの同一階層に配置する。
  • ファイルとフォルダの名前(pptxファイルの名前を仮にfilenameとする)
    pptxファイルの名前: filename.pptx,
    音声データのフォルダの名前: filename,
    スクリプトファイルの名前: filename.txt

生成されるファイルとフォルダ

音声データを埋め込んだpptxファイルと,元のpptxファイルを展開したフォルダが生成されます。

 生成されるpptxファイルの名前: filename_A.pptx
 展開されるフォルダ:      filename_zip
  このフォルダは削除しても問題ないが,検討のため現在の設定では残すようにしてある。

今後の予定

 実際の講義で使ったpptxファイルに対して使って,不具合や使いにくいところを修正していく予定です。最低限の機能が備わった後はあまり凝らないようにしますが,以下のような機能の追加を考えています。

  • 自動アニメーションへのオーディオデータの付加
    初めに考えた仕様には含まれているが,Pythonコードでは未実装になっている。必要性はそれほど感していないが検討はする。
  • スクリプト作成補助の機能
    pptxファイルのスライドデータからクリック起動や自動のアニメーションの情報を読み取り,スクリプトの基になるテキストを作る。(講義用pptファイルの処理を省力化するためには,この機能の実装を急いだ方がよいかもしれない。
  • pptのノート機能の利用
    スクリプトをテキストファイルではなく,pptのノートにも記述できるようにする。これができるとスライドの順番の変更が容易になる。
  • Pythonによる音声合成ソフトの制御
    AHKで実行している機能をPythonのPyAutoGUIに移行する。AHKで出来ているので,無理にPythonに移行しなくてもよいのだが,PyAutoGUIを試してみたい気持ちはある。

技術情報の調査

Pythonによる一連のコード作成の過程の中で,webを使って技術情報の検索を何度も繰り返しています。Pythonの使い方や文法に関する検索がメインで,たくさんのサイトのお世話になりました。

肝心のpptxファイルの内部構造についての調査は,不十分なままスタートし不十分なままでここまで来てしまいました。システム開発に関して「自分が働いている階層の少なくとも2階層上と2階層下までの知識が必要」なんて学生に言っていたのに,こんなことではダメですね。

pptxに関する技術情報は,初めにちょこっと調べて見つけた「pptxファイルはzip圧縮されている」という短い投稿しか参考にしていません。研究や技術開発では,こういうシンプルな情報が重要になることが多いようです。

<詳しく>

良く言われていることだと思いますが,研究や技術開発では「〇〇ができた」とか「××というやり方で〇〇ができた」という情報がとても重要になるみたいです。実際,複数の組織が競って研究・開発を進めているけれど未だ実現されていない目標があるとき,「できた」というニュースをきっかけに,どんどん成功例が報告されていく,ということがあります。何かの実現を目標にして,つきつめて考えている人がいるときは,具体的な方法に関する情報が無くても「それができる」ということを知るだけで,前に進めるのかもしれません。

<閉じる>

省力化のツールの自作として目途がついたと言える段階になったので,今一度調べてみました。すると,展開されたpptxファイルの中身を操作して数式を挿入するなど,いろいろ検討している方がいらっしゃることを知りました。やっぱり,ちゃんと調べないとダメだと思いました。

追記  処理時間について

実際に講義に使ったPPTファイルで動作確認すると,80個のオーディオファイルを埋め込むのに要した時間は2.6秒でした。Pythonのtime.time()関数を使って,オーディオデータを埋め込む関数の動作開始から終了までの時間を計測した値です。

一方,AHKを使いPPTアプリをGUIを介して操作する方法では,20分ほどかかっています。圧倒的な差です。(正確な時間を知るためには測り直す必要があるのですが,それをする気にもならないくらい時間がかかります。)

というわけで,今後はPythonを使った処理をメインにしていきます。コードの解説をするのは少しお待ちください。もともとお見せできるような出来のコードではないのですが,作っていく中で変更を繰り返していて,なかなか確定しません。

でも,自分の作ったものについて,ある程度の再現性を保って記録しておくためには,コードを残しておくことは必要です。ある程度安定してから解説記事を投稿していきます。

]]>
https://tamlab.fc2.page/category-materials/1642/feed/ 0
PPTファイルに音声を自動で埋め込む その3 Pythonによる実装 https://tamlab.fc2.page/category-materials/1584/ https://tamlab.fc2.page/category-materials/1584/#respond Tue, 04 Apr 2023 10:43:37 +0000 https://tamlab.fc2.page/?p=1584 Pythonを使って,pptファイルのスライド切替えとアニメーションにオーディオデータを埋め込むことができることを確認しました。これから,具体的なPythonコードについて解説していきます。

これらのコードは,とにかく早期に「動くかどうか確認する」ことを優先して作りました。このため汚いし読みにくいし,後々の利用を考えても使いやすいとは言えません。手直ししながら投稿していく予定です。

<余談を読む>

PPTファイルに音声を自動で埋め込む その2 にも書きましたように,pptxファイルの変更の方法は,公開されている技術文書などから得たものではありません。解凍したpptxファイルの中を観測することで仮説を立て,それに基づいてコードを作って確認したものです。(これは格好つけた言い方で,実際は「こうじゃないかなと思ってやってみたら運よく動いた・・・今のところは。」ですね。)

ですから,考え方が間違っている可能性はありますし,複雑なプレゼンのpptxファイルに対しては期待したようには動かないかもしれません。

なお,pptファイルにナレーションを付加する有料ソフトとして,株式会社エーアイさんが「AITalk® 声プラス」と言う製品を販売しています(私は音声合成に「かんたん!AITalk®3」という製品を使っています)。pptプレゼンテーションに合成音でナレーションを付加するソフトへのニーズはあるのでしょう。

商用ソフトがあるくらいですから,pptファイルにオーディオデータを付加する技術に関する資料は,私が方法を知らないだけで,どこかで入手できることは確かでしょう。さらに,Python-pptxライブラリの整備が進んで,私が作ったコードのほとんどが無用のものになる可能性も大だと思います。

でも,面白いし勉強にはなります。それに大量にある講義用のppt資料にナレーションを付けるためには急いで自動化したいし,何といっても年金生活者には高いソフトは導入できません。やれるところまでやってみようと考えています。

<閉じる>

Pythonコード

それでは,作ったPythonコードについて説明していきます。

pptxファイルの展開/再圧縮用のツール

pptxファイルの中身を調べるために,zip展開(unzip)とpptxファイルに再圧縮するためのコードを作りました。Windowsの操作で,拡張子変更やzip/unzipをしても可能なはずですが,私のPCでは再圧縮して作ったpptxファイルを開くときにエラーになり,修復するとオーディオデータが消えてしまうという不具合がありました。また,多数のファイルのuzip/zipを繰り返すので,Pythonコードを作ることにしました。

作ったコードは以下の2つです。

  • PPTX_Decompress.py
    pptxファイルをunzipします。できあがるのは,pptxファイルのファイル名から拡張子を取り除いたものを名前に持つフォルダになります。
  • PPTX_Compress.py
    指定したフォルダをzip圧縮し,拡張子をzipからpptxに戻します。

PPTX_Decompress.py

リストにコードを示します。

# PPTXファイルを解凍
# PPTX decompress
from tkinter import filedialog
import os
from zipfile import ZipFile

# 対象となるPPTXファイルを指定する
# Specify the target PPTX file.
typ_pptx = [("PPTX File","*.pptx")]
fileName = filedialog.askopenfilename(
    title = "PPTXファイルを指定",
    filetypes = typ_pptx, 
    initialdir = dir) 

# ファイル名から拡張子を取り除いた文字列を取得
# Get the string of the file name without the file extension.
name_no_ext = os.path.splitext(os.path.basename(fileName))[0]

# PPTXファイルをzipファイルとして展開
# Unzip the PPTX file.
with ZipFile(fileName, 'r') as zip_file:
    zip_file.extractall(name_no_ext) 
    zip_file.close()

解説は不要かと思います。一応,書いておきます。ほとんどコメントと同じですが。

  • 必要なモジュールをインポート
    from tkinter import filedialog
    import os
    from zipfile import ZipFile
  • ダイアログボックスを開き,変換するpptxファイルを指定
    typ_pptx = [(“PPTX File”,”*.pptx”)]
    fileName = filedialog.askopenfilename(
    title = “PPTXファイルを指定”,
    filetypes = typ_pptx,
    initialdir = dir)
  • ファイル名から拡張子を取り除いた文字列name_no_extを取得
    name_no_ext = os.path.splitext(os.path.basename(fileName))[0]
     この式文の末尾の[0]を[1]にすると,拡張子の文字列が得られます。
  • PPTXファイルをzipファイルとして展開
    with ZipFile(fileName, ‘r’) as zip_file:
    zip_file.extractall(name_no_ext)
    zip_file.close()
     展開したファイルを格納するフォルダ名を引数にしてextractall関数を呼ぶだけです。元のpptxファイル名の拡張子を除いたものをフォルダ名とします。

PPTX_Compress.py

# PPTX_Compress 展開されたPPTXフォルダをzip圧縮してPPTXファイルを作成
# Create PPTX files by zipping the expanded PPTX folder.
from tkinter import filedialog
import os
import shutil

# 非圧縮状態のフォルダを指定
# Specify a folder in uncompressed state.
pptx_dir = filedialog.askdirectory(
    title = "Specify a folder.",
    initialdir = dir) 
pptx_name = pptx_dir + '_R'

# フォルダを再びzipにアーカイブ
# Archive the folder to zip again.
shutil.make_archive(pptx_name, format='zip', root_dir=pptx_dir)

# Change file extension from .zip to .pptx.
name_src = pptx_name + '.zip'
name_dst = pptx_name + '.pptx'
# If the destination file already exists, 
# delete the file to avoid an error.
is_file = os.path.isfile(name_dst)
if is_file:
    os.remove(name_dst)
os.rename(name_src, name_dst) 

こちらも必要な関数を並べているだけです。少し注意が必要なのは一番最後の,再圧縮したzipファイルの拡張子を.zipから.pptxに書き直すos.rename関数のところです。リネームの結果,同じ名前のファイルが存在するとエラーになります。

そこで,このコードでは元のpptxファイル名の後ろに“_R”を付けて異なるファイル名になるようにしています。さらに同じ名前があった場合は,そのファイルを削除してからリネームするようにしています。

  • 必要なモジュールをインポート
    from tkinter import filedialog
    import os
    import shutil
     shutilモジュールをインポートしている。
  • 圧縮するフォルダを指定し,圧縮後のファイル名を設定
    pptx_dir = filedialog.askdirectory(
    title = “Specify a folder.”,
    initialdir = dir)
    pptx_name = pptx_dir + ‘_R’
     ダイアログボックスでフォルダ名pptx_dirを取得する。さらに,これに“_R”を追加して新しいファイル名(拡張子無し)であるpptx_nameにする。
  • フォルダを再びzipにアーカイブ
    shutil.make_archive(pptx_name, format=’zip’, root_dir=pptx_dir)
     shutilのmake_archive関数で,フォルダpptx_dirをpptx_nameという名前のzipファイルに変換する。
  • リネームの操作
    name_src = pptx_name + ‘.zip’
    name_dst = pptx_name + ‘.pptx’
     zipファイルとリネーム後のpptxファイルの名前を拡張子も含めて決定する。
    is_file = os.path.isfile(name_dst)
    if is_file:
    os.remove(name_dst)
     リネーム後のファイルが存在する場合は,削除。
    os.rename(name_src, name_dst)
     name_srcからname_dstにリネーム。

PPTX_Compress.pyは,作成したPythonアプリケーションでは関数として登録しています。

アプリケーションの仕様

作成しようとしているPythonアプリケーションは,私の講義資料のpptファイルを自動で処理することを目的としています。つまり汎用ではなく限定された条件で使うことになります。処理できるpptファイルや,オーディオファイルの付加の方法も細かい設定はできません。

処理するpptファイルやオーディオ付加の条件は以下のとおりです。

  • pptファイルはpptx形式であること
  • pptファイルのアニメーションは既に設定済みであること
    アプリケーションには新たにアニメーションを設定したりタイミングを招請したりする機能は未実装。
  • pptファイルにオーディオデータは付加されていないこと
    現在の仕様では,オーディオデータが付加されていないことを前提に処理している。既にオーディオファイルが埋め込まれている場合,データの関連づけが崩れて正常に動作しなくなる可能性がある。オーディオ関連のxmlコードやオーディオファイルを削除する機能を加えれば対応は可能だが,現時点では未実装。
  • スライド切替えとクリックで起動されるアニメーションにのみオーディオデータを付加
    クリックではなく直前の動作で起動されるアニメーションへのオーディオ付加は未実装。
  • ユーザが用意するオーディオファイルの命名規則がある
    これは,Pythonコードに関する本質的な問題ではない。現在作っているアプリケーションでは,テキスト形式の「台本」(スクリプトと呼んでいる)に従ってpptxファイルのデータ加工を進めていく。付加するオーディオファイルのファイル名は拡張子の前に”×××_01_02.wav”のようにスライド番号とクリック番号をスクリプトの規則にしたがって並べておく必要がある。スライド番号とクリック番号は2桁0パッドの文字列で表している。(つまり最大値は99に限定される。)

講義資料については,上の条件はほとんど問題になりません。講義中に使う場合は,スライドの画面切り替えやアニメーションを起動するためのマウスの操作と口頭での説明は,教員が受講者の反応を見ながらおこないます。pptファイルに音声を埋め込む必要はありません。ただしデモのための音声や効果音を埋め込むことは皆無ではないので,そのようなファイルでは注意が必要です。

一方,学生さんの予習や復習用に,pptファイルをHTML5形式のスライドショーとしてwebサイトに掲載しています。この場合は,教員がしているクリック操作はかなりたくさんあるのですが,閲覧する人にやってもらうことはそんなに問題ではないでしょう(むしろ,閲覧者が自分でスピードをコントロールできる利点になると思います)。

しかし,閲覧する人にとっては,事前にはクリックの意図が不明なので,とまどうかもしれません。何といっても口頭で説明に関するオーディオや文字の情報が無いので,何を説明しているか困ってしまうかもしれません,講義のpptスライドにオーディオデータを付加しようと考えたのは,この問題に対応したいからです。

アプリケーションで使う関数

アプリケーションで使う関数について説明していきます。このアプリケーションはテキストファイルである.xmlファイルや.xml.relsファイルを扱うので,基本的には文字列処理がメインになります。

私は信号処理や数値シミュレーションなど,数値処理のコードの経験は少しあります。しかし,文字列の処理はファイル名の加工くらいしかやったことがありません。webで調べながら作っているのですが,おかしなコードになっているかもしれません。

コーディングをしている中で自作の関数を付け足していって,主なもので15個くらいあります。オーディオ付加に関連する関数について紹介していきます。

スライド切替え時のオーディオデータを付加する関数

add_slide_trans_audio

スライド切替えの際のナレーションです。1枚のスライドにつき1つのオーディオデータを埋め込むことになります。

def add_slide_trans_audio(f_name, wav_dir, pptx_dir, sld, anim_num):
    # f_name : File name of user defined sound(with extention,".wav").
    # wav_dir : Directory of user defned sound files. 
    # pptx_dir : Directory where pptx files are extracted.
    # sld : Slide information. (Instance variable of SlideInfo class)
    # anim_num : Numbers related to the animations in the slide. (AnimInfo class)

    # The function adds the slide transition audio to the slide.
    #   audio_str : Code for adding audio to the slide xml file.
    #   rels_str  : Code for the slide relation file.(ex. slide1.xml.rels)

    #   In the codes, several parameters are replaced with dummy strings;
    #    name of user defined audio file  => "dummy.wav",
    #    string of relation id (rId)  => "rId",
    #    name of the audio file to be stored in the ppt/media  => "audio.wav".
    
    audio_str = \
        r'<mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.'\
        + r'org/markup-compatibility/2006">'\
        + r'<mc:Choice xmlns:p14="http://schemas.microsoft.com/office/'\
        + r'powerpoint/2010/main" Requires="p14">'\
        + r'<p:transition spd="slow" p14:dur="2000"><p:sndAc><p:stSnd>'\
        + r'<p:snd r:embed="rId" name="dummy.wav"/>'\
        + r'</p:stSnd></p:sndAc></p:transition></mc:Choice><mc:Fallback>'\
        + r'<p:transition spd="slow"><p:sndAc><p:stSnd>'\
        + r'<p:snd r:embed="rId" name="dummy.wav"/></p:stSnd></p:sndAc>'\
        + r'</p:transition></mc:Fallback></mc:AlternateContent>'\

    rels_str =  r'<Relationship Id="rId" Type='\
        + r'"http://schemas.openxmlformats.org/officeDocument/2006/'\
        + r'relationships/audio" Target="../media/audio.wav"/>'

    # Delete slide transition code if it remains.
    sld.xml = delete_transition_code(sld.xml) 

    # Find the max value of rId and get the rId string.
    rid_max = find_max_rid(sld.rels)
    rid_str = 'rId' + str(rid_max +1)

    # Replace the dummy codes in the audio_str to determined strings.
    audio_str = audio_str.replace('dummy.wav', f_name)
    audio_str = audio_str.replace('rId', rid_str)  

    # Determine the name of .wav file to be stored in ./ppt/media/.
    audio_file = 'audio' + str(anim_num.audio) + '.wav'

    # Replace the dummy codes in the rels_str to determined strings.
    rels_str = rels_str.replace('rId', rid_str)
    rels_str = rels_str.replace('audio.wav', audio_file)

    # Insert the xml code to xml code of the slide.
    sld.xml, err = insert_xml(sld.xml, '</p:clrMapOvr>', audio_str, 0, 1)

    # Insert the rels_str to xml.rels code of the slide.
    sld.rels, err = insert_xml(sld.rels, 'relationships">', rels_str, 0, 1)

    #  Write the slide information code to the silde.xml and slide.xml.rels.
    write_slide_info(sld, pptx_dir)

    # Copy the user defined audio file to ./ppt/media/
    audio_dir = pptx_dir + '/ppt/media'
    if not os.path.isdir(audio_dir):
        os.mkdir(audio_dir)
    audio_path = pptx_dir + '/ppt/media/' + audio_file
    wav_file_path = wav_dir + '/' + f_name
    shutil.copyfile(wav_file_path, audio_path)
    return
引数
def add_slide_trans_audio(f_name, wav_dir, pptx_dir, sld, anim_num):
  • f_name:ユーザが用意したwav形式のオーディオファイルの名称
    スライド番号,クリック番号,自動アニメーションの番号を文字列として含む。
  • wav_dir:オーディオファイルを格納しているディレクトリのパス
  • pptx_dir:zip展開したpptxファイルのディレクトリ名
    pptxファイルの名前から拡張子を除いた文字列になる。
  • sld:処理するスライドの情報をまとめたクラスSlideInfoのインスタンス
       sld.ind:スライド番号n
       sld.xml:ppt/slides/に格納されているsliden.xml内の文字列
       sld.rels:ppt/slides/_rels/に格納されているsliden.xml.rels内の文字列
  • anim_num :アニメーション番号をまとめたクラスAnimNumのインスタンス
       anim_num.sld:スライドの番号n(sld.indとかぶっています!)
       anim_num.clk:クリックの番号
       anim_num.aut:自動アニメーションの番号
       anim_num.audio:ppt/media/の中に格納するオーディオファイルの番号

クラスを使っていますが,構造体として使っているだけです。あと,Pythonではクラスの引数は参照渡しとなるというので,処理結果を呼び出し側に戻すことも考えてクラスインスタンスを採用しています。

ただし,インスタンスのメンバ変数(と呼ぶのかな?)が重複していたり使い方にはいろいろ問題があるみたいなので,後で変更していくことになるでしょう。

なお,これらのクラスは呼び出し側で次のように定義しています。

class PathInfo:
    def __init__(self):
        self.pptx = ''   # pptx file path.
        self.narr = ''   # Narration folder path.
        self.scpt = ''   # Script file path.
        self.idir = ''   # Initial directory.


class SlideInfo:
    def __init__(self):
        self.ind = 0     # Index of the current slide.
        self.xml = ''   # Content of .xml file.
        self.rels = ''  # Content of .xml.rels file.


class AnimNum:
    def __init__(self):
        self.sld = 0    # Slide number.
        self.clk = 0    # Click number.
        self.aut = 0    # Auto animation number.
        self.audio = 0  # Audio file index in ppt/media/
add_slide_trans_audioがやっていること

PPTファイルに音声を自動で埋め込む その2 Pythonによる実装の準備 で解説したスライド切替えオーディオデータの付加をするための関数です。zip展開したpptxディレクトリの下にある,./ppt/slides/sliden.xmlと./ppt/slides/_rels/sliden.xml.relsの内容を書き換え,./ppt/mediaの下にオーディオファイルをaudiom.wavという名前にして配置します。mはプレゼンテーション内で重ならないような通し番号になります。手順は次のようになります。

ⅰ)rIdの最大値を見つけ,その値に1を加えて新しいrIdを決める

ⅱ)sliden.xmlとliden.xml.relsに挿入する文字列を作る

ⅲ)sliden.xmlとliden.xml.relsに文字列を挿入する

ⅳ)ppt/media/の下に,オーディオファイルをコピーする

それでは,各手順をもう少し詳しくみていきます。

ⅰ)rIdの最大値を見つけ,その値に1を加えて新しいrIdを決める
 sliden.xmlまたはsliden.xml.relsの中を検索して”rIdk“(kは10進数の数字)となっている部分を見つけ,kの最大値を求める。(sliden.xmlとsliden.xml.relsのrIdは共通のはずなので,どちらを使ってもよい。)この値に1を加えて新しいrIdの文字列を決定する。

以下に,オーディオを付加していないシンプルなsliden.xml.relsの例を示します。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml" />
</Relationships>

この例では”rId1″となっている文字列が1つだけ見つかります。”rIdk“のkの最大値は1です。新しいrIdの値は既に使われているものと重ならないように選べばよいので,最大値1に1を加えて得られる”rId2″を新規のrIdとして使うことにします。

<もう少し詳しく読む>

編集を繰り返したpptファイルの場合,rIdの文字列は,”rId1″,”rId3″,”rId4″,・・・のように,1ずつ増加しているのではなく,途中が抜けている可能性があります(確認したわけでがありません)。しかし,その場合でも,最大値を見つけてそれに1を加えることで既存のrId文字列と重ならないものが作れます。それで,最大値に1を加えるというシンプルな方法を採用しました。

<閉じる>

rIdの最大値を見つける部分は,以下のコードになります。

    # Find the max value of rId and get the rId string.
    rid_max = find_max_rid(sld.rels)
    rid_str = 'rId' + str(rid_max +1)

find_max_ridは,自作の関数で,引数に文字列を入れて処理すると,その文字列の中のrId文字列の表している最大の値を整数で返します。

次のrid_str =・・・で,その値に1を加えて新しいrId文字列を作っています。

関数find_max_ridは次のようになっています。

def find_max_rid(str):
    # Find the max of rId number
    rid_num = 0
    rid_max = 0
    pt = 0
    while pt >= 0  :
        (pt, rid_num) = find_rid(str,pt)
        if rid_num >= rid_max :
            rid_max = rid_num
    return rid_max
find_max_rid(str)
  • str:文字列
    この中に含まれているrIdの最大値を求め,整数値で返す。

find_max関数の中では,やはり自作のfind_rid関数を呼んでいます。find_rid(str, pt)は,文字列strの先頭からpt番目の文字から探索を始め,最初に見つかったrId文字列の数値を読み取ります。関数の戻り値は,見つけたrId文字列の直後の位置pt_stopと,読み取った数値rid_numです。rId文字列が存在しない場合は,pt_stopの値は-1にして返します。

これを利用して,find_max_ridではrId文字列の数値を読み取っていき,ptの値が負になるまで繰り返しています。

def find_rid(str,pt):
    pt_start = find_slice_position('"rId',1, pt, str) 
    pt_stop  = find_slice_position('"',-1,pt_start,str)
    if pt_start == -1 or pt_stop == -1 :
        return (-1,0)
    else :
        rid_num = int(str[pt_start:pt_stop])
        return (pt_stop,rid_num)
find_rid(str, pt)
  • str:rId文字列を含んでいる文字列
  • pt:探索を開始する位置(文字列の先頭を0とする)

さらに,find_ridの中でfind_slice_positionを呼んでいます。

def find_slice_position(target_str,d, pt, str) :
    # Finds the target-string and returns sice position.
    # d = 1: after the target,d =-1 : befor the target
    tag_str_len = len(target_str)
    pt_temp = str.find(target_str,pt)
    if pt_temp ==-1 :
        return -1
    else :
        if d == 1 :
          slice_pt = pt_temp + tag_str_len
        else :
          slice_pt= pt_temp
        return slice_pt
find_slice_position(target_str,d, pt, str) 
  • target_str:目標とする文字列
  • d:d>0の場合目標とする文字列の直後,d<0の場合は直前の位置を返す
  • 探索を開始する位置(文字列の先頭を0とする)
  • str:探索の対象とする文字列
  • 戻り値は位置を示す整数値。target_strが見つからないときは,-1を返す。

ⅱ)sliden.xmlとliden.xml.relsに挿入する文字列を作る
 これらの文字列は,関数のコードの中に埋め込まれています。連続した文字列なので,見やすくなるようにところどころにバックスラッシュを入れてあります。

    audio_str = \
        r'<mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.'\
        + r'org/markup-compatibility/2006">'\
        + r'<mc:Choice xmlns:p14="http://schemas.microsoft.com/office/'\
        + r'powerpoint/2010/main" Requires="p14">'\
        + r'<p:transition spd="slow" p14:dur="2000"><p:sndAc><p:stSnd>'\
        + r'<p:snd r:embed="rId" name="dummy.wav"/>'\
        + r'</p:stSnd></p:sndAc></p:transition></mc:Choice><mc:Fallback>'\
        + r'<p:transition spd="slow"><p:sndAc><p:stSnd>'\
        + r'<p:snd r:embed="rId" name="dummy.wav"/></p:stSnd></p:sndAc>'\
        + r'</p:transition></mc:Fallback></mc:AlternateContent>'\

    rels_str =  r'<Relationship Id="rId" Type='\
        + r'"http://schemas.openxmlformats.org/officeDocument/2006/'\
        + r'relationships/audio" Target="../media/audio.wav"/>'
  • audio_str
    スライド切替えのオーディオを付加するためのsliden.xmlに追加する文字列。Pythonコードの中では,書き換える部分(赤い文字で表記)を,次のように仮の文字列にしてある。
      rId文字列:数字部分を抜いた”rId”,
      ユーザが用意したオーディオファイルの名前:”dummy.wav”
  • rels_str
    xmlコードで指示したオーディオデータが実際に埋め込まれたppt/media/下にあるwav形式ファイルのどれに対応するかの関係を表す文字列。書き換えのための仮の文字列は,
      rId文字列:”rId”,audio_strと同じに揃える
      ppt/media/下に書き込むwav形式ファイルの名前:”audio.wav”

文字列の置き換えは,標準の文字列関数で実行しています。

    # Replace the dummy codes in the audio_str to determined strings.
    audio_str = audio_str.replace('dummy.wav', f_name)
    audio_str = audio_str.replace('rId', rid_str)  

文字列audio_strの中のダミーの文字列”dummy.wav”を引数で与えたファイル名のf_nameで置き換えます。次に,”rId”もrIdの最大値+1で計算した値を使ったrId文字列で置き換えます。

以上で,sliden.xmlとsliden.xml.relsに挿入する文字列の準備ができました。次にこの文字列を挿入します。

    # Insert the xml code to xml code of the slide.
    sld.xml = insert_xml(sld.xml, '&lt;/p:clrMapOvr>', audio_str, 0, 1)

    # Insert the rels_str to xml.rels code of the slide.
    sld.rels = insert_xml(sld.rels, 'relationships">', rels_str, 0, 1)

ここで,自作の関数insert_xmlを使っています。

def insert_xml(input_str, target_str, insert_str, start_pt, d):
    # If input_str already exists, do nothing and return.
    slice_pt = find_slice_position(insert_str,1, start_pt, input_str)
    if slice_pt >=0 : 
        return input_str, -1
    # Find the slice position before (d&lt;0) or after (d>0) the tareget_str.
    else : 
        slice_pt = find_slice_position(target_str,d, start_pt, input_str)
    if slice_pt == -1 :
        return input_str, -1
    else :
        input_str_back = input_str[slice_pt : ]
        input_str = input_str[0 : slice_pt] + insert_str + input_str_back
        return input_str, 1

文字列のスライスを使った処理になります。中で自作のfind_slice_positionを使っています。これは文字列オブジェクトのstr.find関数でも問題なくできます。無理に関数にしなくてもよかったのかもしれませんが,やっていることが理解しやすくはなります。

insert_xml(input_str,  target_str,  insert_str,  start_pt,  d)
  • input_str:挿入する対象となる文字列
  • target_str:目印(目標)にする文字列
  • insert_str:
    文字列target_strを見つけたら,その直後または直前にinsert_strを挿入する。
  • d: d>0なら直後,d<0なら直前に挿入
    d<0の場合の処理は,str.find関数だけでよいのですが,同じ関数の形で使えるようにしておきました。でも,target_strの直前に挿入する用途はまだありません。

挿入したら,文字列をファイルsliden.xmlとsliden.xml.relsに書き込みます。

    #  Write the slide information code to the silde.xml and slide.xml.rels.
    write_slide_info(sld, pptx_dir)

2つのファイルに書き込むための関数を作りました。

def write_slide_info(slide, pptx_dir):
    typ = type(slide)
    i_sld = slide.ind
    slide_file = pptx_dir + '/ppt/slides/slide'+ str(i_sld) + '.xml'
    rels_file = pptx_dir + '/ppt/slides/_rels/slide'+ str(i_sld) + '.xml.rels'
    
    write_file(slide_file, slide.xml)
    write_file(rels_file, slide.rels)
write_slide_info(slide, pptx_dir)
  • slide:クラスSlideInfoのインスタンス
  • pptx_dir:pptxファイルをzip展開したトップのディレクトリ

この関数の中では自作のwrite_fileを使っています。中身の説明は不要だと思います。

def write_file(f_name,input):
    f = open(f_name, 'w', encoding = 'UTF-8')
    f.write(input)
    f.close()
    return 1

ⅳ)ppt/media/の下にオーディオファイルをコピーする

    # Copy the user defined audio file to ./ppt/media/
    audio_dir = pptx_dir + '/ppt/media'
    if not os.path.isdir(audio_dir):
        os.mkdir(audio_dir)
    audio_path = pptx_dir + '/ppt/media/' + audio_file
    wav_file_path = wav_dir + '/' + f_name
    shutil.copyfile(wav_file_path, audio_path)
    return

shutil(シューティル,Shell Utilitiesの略だそうです)モジュールの関数,shutil.copyfileを使っています。

クリックで起動されるアニメーションにオーディオを付加する関数

add_click_actv_audioは処理の内容は,スライド切替え用の関数add_slide_trans_audioと,ほとんど同じです。異なっているのは,コードを挿入する位置が複数(1つ以上)ある,sliden.xmlに書き込む文字列が異なる,クリックのIdを読み取って書き込む必要がある,などの点です。

また,この関数add_click_actv_audioでは,指定された順番を持つクリック記述の位置1か所だけにコードを埋め込むようにしてあります。

def add_click_actv_audio(f_name, wav_dir, pptx_dir, sld, anim_num):
    # f_name : File name of user defined sound(with extention,".wav").
    # wav_dir : Directory of user defned sound files. 
    # pptx_dir : Directory where pptx files are extracted.
    # sld : Slide information. (Instance variable of SlideInfo class)
    # anim_num : Numbers related to the animations in the slide. (AnimInfo class)

    # The function adds the click-activated audio to the slide.
    #    audio_str : xml strings describing audio data 
    #    rels_str  : Code for the slide relation file.(ex. slide1.xml.rels)    
    #    name of user defined audio file  => "dummy.wav",
    #    string of relation id (rId)  => "rId",
    #    name of the audio file to be stored in the ppt/media  => "audio.wav".
    audio_str = \
        r'&lt;p:subTnLst>&lt;p:audio>&lt;p:cMediaNode>'\
        + r'&lt;p:cTn display="0" masterRel="sameClick">'\
        + r'&lt;p:stCondLst>&lt;p:cond evt="begin" delay="0">'\
        + r'&lt;p:tn val="dummyNum"/>&lt;/p:cond>&lt;/p:stCondLst>&lt;p:endCondLst>'\
        + r'&lt;p:cond evt="onStopAudio" delay="0">&lt;p:tgtEl>'\
        + r'&lt;p:sldTgt/>&lt;/p:tgtEl>&lt;/p:cond>&lt;/p:endCondLst>&lt;/p:cTn>'\
        + r'&lt;p:tgtEl>&lt;p:sndTgt r:embed="rId" name="dummy.wav"/>'\
        + r'&lt;/p:tgtEl>&lt;/p:cMediaNode>&lt;/p:audio>&lt;/p:subTnLst>'
    rels_str =  r'&lt;Relationship Id="rId" Type='\
        + r'"http://schemas.openxmlformats.org/officeDocument/2006/'\
        + r'relationships/audio" Target="../media/audio.wav"/>'
    
    # Find the max value of rId
    rid_max = find_max_rid(sld.rels)
    rid_str = 'rId' + str(rid_max +1)
    
    (clk_id, pt) = find_nth_click(sld.xml, anim_num.clk)

    audio_str = audio_str.replace('dummy.wav', f_name)
    audio_str = audio_str.replace('rId', rid_str)  
    audio_str = audio_str.replace('dummyNum', clk_id) 

    audio_file = 'audio' + str(anim_num.audio) + '.wav'

    rels_str = rels_str.replace('rId', rid_str)
    rels_str = rels_str.replace('audio.wav', audio_file)

    sld.xml, err = insert_xml(sld.xml, '&lt;/p:childTnLst>', audio_str, pt, 1)
    sld.rels,err = insert_xml(sld.rels, 'relationships">', rels_str, 0, 1)
    write_slide_info(sld, pptx_dir)

    audio_dir = pptx_dir + '/ppt/media'
    if not os.path.isdir(audio_dir):
        os.mkdir(audio_dir)
    audio_path = pptx_dir + '/ppt/media/' + audio_file
    wav_file_path = wav_dir + '/' + f_name
    shutil.copyfile(wav_file_path, audio_path)
    return

関数の引数,処理の流れ,使っている関数などはadd_slide_trans_audioとほぼ同じなので,異なるところだけ説明します。

挿入するコード

sliden.xmlに挿入する文字列と挿入する位置が異なります。文字列は,以下の代入文で定義しています。

 audio_str = \
        r'<p:subTnLst><p:audio><p:cMediaNode>'\
        + r'<p:cTn display="0" masterRel="sameClick">'\
        + r'<p:stCondLst><p:cond evt="begin" delay="0">'\
        + r'<p:tn val="dummyNum"/></p:cond></p:stCondLst><p:endCondLst>'\
        + r'<p:cond evt="onStopAudio" delay="0"><p:tgtEl>'\
        + r'<p:sldTgt/></p:tgtEl></p:cond></p:endCondLst></p:cTn>'\
        + r'<p:tgtEl><p:sndTgt r:embed="rId" name="dummy.wav"/>'\
        + r'</p:tgtEl></p:cMediaNode></p:audio></p:subTnLst>'

見づらいので,整形したものを示します。赤い文字で強調している部分が,ダミーの文字列で,sliden.xmlとsliden.xml.relsから読み取って作った文字列と置き換えてから,ファイルに書き込みます。

        <p:subTnLst>
           <p:audio>
             <p:cMediaNode>
               <p:cTn display="0" masterRel="sameClick">
                 <p:stCondLst>
                   <p:cond evt="begin" delay="0">
                     <p:tn val="dummyNum" />
                   </p:cond>
                 </p:stCondLst>
                 <p:endCondLst>
                   <p:cond evt="onStopAudio" delay="0">
                     <p:tgtEl>
                       <p:sldTgt />
                     </p:tgtEl>
                   </p:cond>
                 </p:endCondLst>
               </p:cTn>
               <p:tgtEl>
                 <p:sndTgt r:embed="rId" name="dummy.wav" />
               </p:tgtEl>
             </p:cMediaNode>
           </p:audio>
         </p:subTnLst>

rId文字列とユーザが用意したwav形式ファイルの名前dummy.wavに関しては,スライド切替えオーディオ用の関数add_slide_trans_audioと同じ処理になります。文字列audio_strの中にあるdummyNumは,クリックに関係するIdで,これは対応するクリックに関する記述から読み取る必要があります。

オーディオを付加していないときのクリックに関する記述の例は次のようになっています。

   <p:childTnLst>
     <p:par>
       <p:cTn id="5" presetID="1" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="clickEffect">
         <p:stCondLst>
           <p:cond delay="0" />
         </p:stCondLst>
         <p:childTnLst>
           <p:set>
             <p:cBhvr>
               <p:cTn id="6" dur="1" fill="hold">
                 <p:stCondLst>
                   <p:cond delay="0" />
                 </p:stCondLst>
               </p:cTn>
               <p:tgtEl>
                 <p:spTgt spid="4" />
               </p:tgtEl>
               <p:attrNameLst>
                 <p:attrName>style.visibility</p:attrName>
               </p:attrNameLst>
             </p:cBhvr>
             <p:to>
               <p:strVal val="visible" />
             </p:to>
           </p:set>
         </p:childTnLst>
       </p:cTn>
     </p:par>
   </p:childTnLst>

“clickEffect”という文字列を見つけ,遡って探索して最初に見つけた”id=”の次にクリックのIdがあります。この例では”5″となっています。

そして,”clickEffect”の後ろに探索していって,初めに見つけた</p:childTnLst>の直後にダミーの文字列を書き換えたaudio_strを挿入します。

異なっているのは,以下の文があることです。

    (clk_id, pt) = find_nth_click(sld.xml, anim_num.audio)

ここで使っている関数find_nth_click(n番目のクリックを見つける関数)が,指定された番号のクリックの記述を見つけ,クリックのId番号を取得して返す関数です。

def find_nth_click(sld_str, n_clk):
    # Find the n-th click-effect description in the slide.xml string.
    # Return the click ID number and the 'clicEffect">' position.
    target_str = 'clickEffect">'
    pt = 0
    i_clk = 0
    strlen = len(target_str)
    while i_clk &lt;= n_clk-1 :
        pt = sld_str.find(target_str, pt) 
        if pt >=0:
            i_clk += 1
            pt = pt + strlen
        else:
            break
    pt_start = find_slice_position('id="',1, pt-120, sld_str) 
    pt_stop = find_slice_position('"',-1, pt_start, sld_str)
    id = sld_str[pt_start : pt_stop]
    return id, pt
find_nth_click(sld_str, n_clk)
  • sld_str:sliden.xmlの内容の文字列
  • n_clk:クリックの番号
  • 関数の戻り値は,クリックのIdを示す文字列と,n番目のクリック記述の中のclicEffect”>という文字列の直後のスライス位置です。

この関数の中身の説明は不要かと思いますが,念のため書き残しておきます。

  • n_clk番目のtarget_str = “<“clickEffect”>を見つける
    pt = 0 
    i_clk = 0
    strlen = len(target_str)
    while i_clk <= n_clk-1 :  
    pt = sld_str.find(target_str, pt)
    if pt >=0:
    i_clk += 1
    pt = pt + strlen
    else:
    break
     whileループを抜けると,ptはn_clk番目の<“clickEffect”>の直後のスライス位置になっています。
  • 遡って探索して初めのId=”を見つける部分
    pt_start = find_slice_position(‘id=”‘,1, pt-120, sld_str)
     n_clk番目の<“clickEffect”>の120文字前から探索を開始し,見つけたid=”の直後をpt_startとする。
    pt_stop = find_slice_position(‘”‘,-1, pt_start, sld_str)
     pt_startの後ろの”を見つけ,その直前をpt_stopとする。
    id = sld_str[pt_start : pt_stop]
     これで数値の文字列が取得できる。

説明が長く,わかりにくくなってきました。

商売柄,「読んだ人が同じ程度のシステムを再構築できる」ような解説を工夫しなければならないのでです。こういうサイトをのぞいてみようとする方であれば,「ヤじるし」よりはコーディングスキルがあるでしょうから,それに頼って雑な書き方をしてしまいました。暇になったら,書き直していくつもりです。

今回の投稿で解説した関数を使って,PPTファイルのスライド切替えとアニメーションにオーディオデータを追加するコードを作成しました。少し複雑でスライド枚数の多いプレゼンテーションに使って,コードを改良していく予定です。

その4 GUIを持つアプリ に続く。

]]>
https://tamlab.fc2.page/category-materials/1584/feed/ 0
PPTファイルに音声を自動で埋め込む その2 Pythonによる実装の準備 https://tamlab.fc2.page/category-materials/1562/ https://tamlab.fc2.page/category-materials/1562/#respond Sat, 01 Apr 2023 23:37:41 +0000 https://tamlab.fc2.page/?p=1562 AHKにより実装したPPT(Powerpoint)ファイルへの音声埋め込みは,処理速度が遅く,しかも動作中はPCを使えなくなってしまいます。PPTのGUI操作をAHKに実行させているからです。これでは,大量のPPTファイルを処理したい用途では使い物になりません。

そこで,Pythonを使ってPPTファイルに直接音声データを埋め込むことを考えました。現在,フリーのPythonモジュールだけ使って音声データ埋め込みができることを確認できています。目標としている「クリックで起動されるアニメーションに音声データを挿入する」ところまではもう少しというところまで漕ぎつけました。この段階でわかったことを,私が理解した範囲で紹介します。

<注意>

この一連の投稿で紹介する技術情報のほとんどは公式のデータを参照にしたものではなく「ヤじるし」の憶測に基づいたものです。信頼性には問題があります。間違っている部分も多いと思います。「・・・です。」と断言している部分があっても,必ず「と,考えられる。」を付けて読んでください。

また,投稿した技術情報やコードに基づいて作られたソフトウェアが,貴重なデータの損失につながる可能性もあります。現在試作しているコードも,お手本になるとはとても呼べない出来です。公開したとしても,参考にする場合は慎重にお願いします。

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

<閉じる>

基本的な考え方

これから紹介する方法は,PPTの標準的なフォーマットであるpptx形式(Windowsの拡張子が“.pptx”)のデータのみを対象としています。“.ppt”などのフォーマットのものは,pptx形式に変換してからでないと対応できないでしょう。

処理の流れは,以下のようになります。

  • pptxファイルをzip形式で展開
    pptxファイル(仮にファイル名をfilename.pptxとする)は,複数のフォルダに格納されたファイルをzip圧縮したものになっている。これをPython標準のモジュールで展開する。展開されたフォルダやファイルは,filenameという名称のフォルダ(ディレクトリ)の下に格納されている。
  • 展開されたファイルに音声データ付加の処理を施す
    スライドの切替えやクリックで起動されるアニメーションに音声データを付ける記述を,filename/ppt/slidesの下のXML(Extended Markup Language)形式のテキストファイルに書き込む。またXMLの記述と実際の音声データを関連づけているテキストファイル(.rels)にも書き加える。埋め込む音声データのファイルをリネームしてsilename/ppt/mediaの下に格納する。
  • 処理されたファイルを再度zip圧縮する
    フォルダfilenameをzip圧縮しファイル名をfilename.zipから新しい名前new_filename.pptxにリネームする。(あるいは,元のfilename.pptxを削除して,同じ名前のpptxファイルで置き換える。)

.xmlや.relsファイルに対する処理は文字列の操作のみになります。あとは,wav形式の音声データのファイルをリネームして展開されたフォルダの下にコピーするだけです。これらの処理は,pptxファイルをzip展開しないままでもできそうで,そうした方がスマートだったかもしれません。しかし,ファイルの書き直しや追加などの際の制約が大きそうだったので,一旦,展開して処理をすることにしました。

ですから処理そのものに技術的に難しい課題はありません。「xmlファイルやrelsファイルをどう書き直すか」というルールがわかってしまえば・・・です。問題は,そのルールに関する情報が,どこを調べても見つからなかったことです。(単に調べ方が下手なだけかもしれませんが。)

<注意:Windowsでのzip展開・圧縮について>

zip展開と再圧縮をWindows11のOSの操作で実行する場合は,以下のようになります。

展開:
ⅰ)filename.pptxの拡張子をzipに変更
ⅱ)zipファイルを右クリックし,「すべて展開」
 filenameという名前のフォルダができる。

再圧縮:
ⅰ)filenameフォルダを選択し右クリック > ZIPファイルに圧縮
ⅱ)filename.zipの拡張子をpptxに変更

ただし,現時点の私のシステムでは,展開したファイルを何もしないで再圧縮して得られるpptxファイルをダブルクリックしてPPTを起動すると,図のようなメッセージが出されます。

「修復」をクリックするとファイルが開かれますが,GUIでアニメーションの効果を確認すると,展開・再圧縮の前にはリンクされていたオーディオデータの情報が消えてしまっています。

少なくても一回は,再圧縮したファイルを正常に開けたという記憶があります。Windowsの更新などの影響かもしれません。zip展開と再圧縮を使う方法では音声データの付加はできないかも・・・と一瞬思いました。

そこで,Pythonでzip展開と圧縮用のコードを作成して試してみました。今度は,再圧縮してリネームしたファイルをPPTで正常に開くことができます。

以上のことから,展開や再圧縮の際にユーザが制御できない「データの損失や追加」が起こるかもしれない,ということがわかりました。将来,OSやPythonの仕様が変更されたときに,開発したコードが正常に動かなくなる可能性があります。

<閉じる>

使ったツール

使っているツールは以下のようになります。

  • PythonとVisual Studio Code
    Pythonの編集や実行にはVisual Studio Code(以下,VSCode)を使った。モジュールは標準に備わってiいたり無償でインストールできたりするものだけを使っている。
  • XMLエディタ,XMLEDITOR.NET
    XMLコードの作成や編集ができるフリーソフト。pptxで使われているxmlファイルはテキストファイルなので,Windowsのメモ帳などで開くことができる。しかし,1行目を除くと改行もインデントも無い文字列なので非常に見づらい。そこで,XMLエディタを使ってファイルを開き,整形されたテキストを印刷して使う。つまり「ビューワ」としての機能のみを利用している。(XMLEDITOR.NETを選定した理由は,“何となく”でした。国産のフリーのXMLエディタの中では使いやすいのかもしれません。)
  • ファイル比較・マージツール,WinMerge
    オープンソースのツール。2組のファイルやフォルダの間の差異を見つけるために使用。同じpptxファイルを基に音声データの無いものと有るものを作り,zip展開して,ファイルのどこが異なるか調べるために使った。

pptxファイルについてわかったこと

現時点で以下のことがわかっています。

pptxファイルの中身

zip展開したpptxファイルをファイルエクスプローラで表示すると図のようになります。

filename.pptxという名前のpptxファイルを展開したときの構成

この図では,オーディオデータを追加する際に書き換えたり追加したりするフォルダやファイルは赤枠や赤文字で表しています。(スライド2枚程度の非常にシンプルなpptxファイルを使って調べた結果なので,実際にはもっと多くの種類のフォルダやファイルがあるかもしれません。)

フォルダ(ディレクトリ)の構成は以下のようになっています。

  • 拡張子“.pptx”を除いた名前を持つフォルダがルートディレクトリとなっている
  • ルートディレクトリの直下に1つのファイルと3つのフォルダがある
    ファイルは[Content_Types].xml,フォルダは,ppt,docProps,_rels。
    オーディオデータの追加で使うのは,[Content_Types].xmlとppt内のファイル
  • ppt/slidesフォルダとppt/mediaフォルダの中のファイルを変更
    mediaフォルダの中にはjpegなどの画像ファイルやwav形式のオーディオファイルを格納する。これらのメディアファイルを使わないpptxファイルではmediaフォルダは存在しない。
    slidesフォルダの中には各スライドの情報を記述するxmlファイルslide1.xml,slide2.xml,・・・がある。さらに_relsというフォルダがあり,その中にあるslides1.xml.rels,slides2.xml.rels,・・・は,対応するslideのxmlファイル内の記述と外部のデータとの関連(relation)を記述するテキストファイルになっている。

xmlファイルにはslide1.xml,slide2.xmlのように,slide + スライド番号 + .xmlというファイル名が付けられています。xml.relsファイルも同様です。以下の説明では,これらのファイルを,“slide.xml”,“slide.xml.rels“のようにスライド番号を略して表すことにします。

どのファイルのどこを変更するか

PPTを使って,音声データを付加している/していない,クリック起動のアニメ有り/無し,など何種類かのpptxファイルを作りました。これらのpptxファイルをzip展開して,中のファイルをXMLエディタで整形して印刷し,何が違うか観察します。しばらく眺めていると,だいたいアタリがついてきます。

この段階でPythonでxmlファイルを書き直すコードを作って,見た目はいい感じにslide.xmlファイルを直してみたのですが,再圧縮したpptxファイルは動作しません。slide.xmの中lで直すべきところがまだありそうです。さらにslide.xml以外にもファイルはたくさんあるので,確認のためファイル比較ツールのWinMeargeを使って,違うところを見つけていきます・・・。

ということを繰り返して,オーディオデータを埋め込むために,どのファイルのどの部分を書き直せばよいかが,少しずつ見えてきました。現時点での結果をまとめると,変更や追加が必要なファイルは以下のようになります。

  • ppt/slides/の中のslide1.xml, slides2.xml, ・・・:
    スライド毎に,切替えやクリックの際の効果(effect)としてオーディオデータを指定する記述を追加する。オーディオデータはユーザが作成したwav形式のファイル名で指定する。また,追加するeffectにはpptxファイルに埋め込まれるオーディオファイルの実体との関連を表すIdである“rId”を与える。
  • ppt/slides/_relsの中のslide1.xml.rels, slides2.xmlrels, ・・・:
    対応するスライドのxmlファイル内の記述と外部のデータの関連(relation)を示すファイル。rIdに対応する実際のオーディオファイルを記述するコードを追加する。
  • ppt/media/を作成し,wav形式のファイルを格納
    wav形式のファイルはプレゼンテーション内部での通し番号を付けたaudio1.wav,audio2.wav,・・・というファイル名に変更してmediaフォルダの中にコピーする。
  • [Content_Types].xml:
    wav形式のオーディオファイルを使うことを示す記述を追加する。この記述はオーディオファイルの名前や個数などによっては変化しない。 

変更の具体的な方法は,「スライド切替えのオーディオデータ」と「クリック起動のアニメへの音声データ」で少し異なっています。まず,動作確認できた「スライド切替えのオーディオデータ」について解説します。

スライド切替えオーディオデータの付加 

PPTのGUIで音声データを設定する場合は,下の図のようになっています。「画面の切り替え」リボンを選択し,サウンドの選択ボックスで音声データを指示します。ここでは,testPy_01_00.wavというwav形式のファイルが音声データとして指定されています。

PPTの操作画面で「画面切り替え」リボン > サウンド > …でオーディオデータを付加

私が講義で使っているPPTでは,画面切り替えの設定はデフォルトのままで,クリックで切り替えるようにしています。口頭での説明のタイミングも,ほぼ切り替えの前後でおこなっています。ですから,画面切り替えのクリックのタイミングで音声を発生させるのは,割と自然だと考えています。

画面切り替えのタイミングにオーディオデータを付加するためのファイル操作は,クリックで動作するアニメへの音声付加のものよりはシンプルです。それで,基礎的なことを確認するための勉強を兼ねて,画面切り替えへの音声付加のコーディングを先に完成させることにしました。

slide.xmlを変更する

画面切り替えのオーディオデータを埋め込んだpptxファイルのslide.xmlの一部を下のリストに示します。XMLエディタで改行とインデントを挿入してあります。

青い文字は,オーディオデータが無い場合のコードです。黒と赤の文字がオーディオデータを埋め込んだときに挿入されていたコードを示します。赤い文字が,埋め込む音声データのファイル名や他のファイルとの関連に応じて変更すべき部分です。

  <p:clrMapOvr>
    <a:masterClrMapping />
  </p:clrMapOvr>
  <mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
    <mc:Choice xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" Requires="p14">
      <p:transition spd="slow" p14:dur="2000">
        <p:sndAc>
          <p:stSnd>
            <p:snd r:embed="rId2" name="testPy_01_00.wav" />
          </p:stSnd>
        </p:sndAc>
      </p:transition>
    </mc:Choice>
    <mc:Fallback>
      <p:transition spd="slow">
        <p:sndAc>
          <p:stSnd>
            <p:snd r:embed="rId2" name="testPy_01_00.wav" />
          </p:stSnd>
        </p:sndAc>
      </p:transition>
    </mc:Fallback>
  </mc:AlternateContent>
</p:sld>
 

オーディオを埋め込んでいない状態のslide.xmlの中で,“</p:clrMapOvr>”という文字列を見つけ,その直後に,”<mc:AlternateContent”で始まり”</mc:AlternateContent>”で終わる文字列を挿入します。

さらに,挿入した文字列の中にある<p:snd r:embed=”rId2″ name=”testPy_01_00.wav” />の部分の変更が必要です。

“rId2”は,ユーザが埋め込む音声データに与えるrelation Id(“関連識別子”)として与えた文字列で,slide.xmlの中にある関連付け要素によってrId3,rId4,・・・のように数字部分が変化していきます。

“testPy_01_00.wav” は,埋め込むオーディオデータとしてユーザが用意したファイルの名前です。

以上の2か所を変更した文字列を“</p:clrMapOvr>”の後ろに挿入すればよいことになります。

slide.xml.relsファイルを変更する

同じpptxファイルのslide.xml.relsの一部を以下に示します。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio" Target="../media/audio1.wav" />
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml" />
</Relationships>

オーディオを埋め込んでいない状態のslide.xml.relsの中で,”relationships”>”という文字列を見つけ,その直後に,”<Relationship Id=”で始まり”audio1.wav” />”で終わる文字列を挿入します。

挿入する文字列は,“Id=”rId2”と,“audio1.wav”の2か所の変更が必要です。

“rId2”は,slide.xmlの中のrIdの文字列と等しくします。一方,“audio1.wav”はpptxファイルに埋め込むwav形式のオーディオファイルの名前になっています。この実体は,ユーザが用意したファイルtestPy_01_00.wavと同じものです。埋め込まれるオーディオファイルの名称は,audio + ファイル番号 + .wavという規則で生成されます。ファイル番号は,プレゼンテーションの中では1から始まり,オーディオデータを追加するたびに1ずつ増加される「通し番号」になっています。このファイル名は一度決めるとスライドの順番を入れ替えても変わらないのかどうかは未確認です。

ppt/mediaフォルダにオーディオファイルを配置

オーディオファイルは,ppt/mediaフォルダの中に配置します。下の図は,ppt/mediaフォルダの中身をファイルエクスプローラで表示したものです。

ppt/mediaフォルダの中にaudio1.wavファイルを配置する

オーディオや画像を埋め込んでいないpptxファイルではppt/mediaフォルダは存在しません。Pythonコードでは,フォルダが無い場合は作成し,そこにユーザが用意したオーディオファイルをリネームして配置するようにします。

[Content_Type].xmlファイルを書き直す

[Content_Type].xmlは,展開したpptxディレクトリの直下に置かれています。その冒頭の部分を以下に示します。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="jpeg" ContentType="image/jpeg" />
  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
  <Default Extension="xml" ContentType="application/xml" />
  <Default Extension="wav" ContentType="audio/x-wav" />
  <Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml" />

[Content_Type].xmlファイルの内容

“<Default Extension=”wav” ContentType=”audio/x-wav” />”の部分は,オーディオファイルを埋め込んでいないpptxファイルでは存在しません。

ファイルの書き換えは簡単です。”application/xml”/>”を見つけ,その直後に
“<Default Extension=”wav” ContentType=”audio/x-wav” />”を挿入すればよいだけです。挿入する文字列を変更する必要はありません。おそらく,この[Content_Type].xmlファイルは,プレゼンテーションにどのような形式のコンテンツが含まれているかを示しているのでしょう。

<Default Extension=”wav” ContentType=”audio/x-wav” />を挿入しない場合,pptxファイルを開いたときにエラーになるのか,あるいは自動て修復してくれるかどうかなどについては未確認です。

クリックで起動されるアニメーションへのオーディオデータの付加 

PPTによるプレゼンテーションでは,文字列やシェイプオブジェクトを出現や消去させたり動かしたりする「アニメーション」が可能です。アニメーションは通常はマウスクリックで開始されます。

私の講義のスライドは,アニメーションを多用しています。1枚のスライドの中で,かなり細かく段階的に表示を変えていきます。このため,クリックの回数の方がスライド切替えよりずっと多くなります。

下の図のスライドは3つのテキストボックスがあり,それぞれがクリックで出現するようになっています。GUIで音声データを設定する場合は,下の図のようアニメーションウインドウを開き,スライド毎に設定していきます。設定はアニメの開始をクリック時にするか,それとも直前の動作と同時または後で起動するのかを選択できます。また,このように前の動作で(自動で)起動する場合の遅れのタイミングなども調整できます。

「アニメーションウインドウ」 オプション効果としてオーディオデータを付加

さらに,「効果のオプション…」を選ぶことでアニメーションに様々な効果をオプションで追加できます。その効果の1つに,サウンドの追加があり,ユーザが作成したwav形式のファイルも使うことができます。PPTへのナレーションの付加は,このアニメーションのオプションを利用しています。

効果オプションの設定ボックス

スライド切替のオーディオはスライド1枚につき高々1回しま発生されません。一方,クリックで起動されるオーディオは,1枚のスライドの中で複数回出現します。また,スライドのxmlファイルから読み取るべきパラメータも1つ多くなります。このため,スライド切替えオーディオの挿入より処理が少し複雑になります。

自動でオーディオを追加するための処理は,スライド切替えのときと同じです。異なるのは,slide.xmlの書き換えの内容と,1つのスライドに対し複数のオーディオファイルを埋め込むことの2つです。

slide.xmlを変更する

クリック起動されるアニメーションにオプション効果としてオーディオデータを付加したときのslide.xmlのクリックに関連する部分を下のリストに示します。

青い文字は,オーディオデータが無い場合のコード,黒と赤の文字がオーディオデータを埋め込んだときに挿入されていたコードを示します。赤い文字が,埋め込む音声データのファイル名や他のファイルとの関連に応じて変更すべき部分です。

   <p:childTnLst>
     <p:par>
       <p:cTn id="5" presetID="1" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="clickEffect">
         <p:stCondLst>
           <p:cond delay="0" />
         </p:stCondLst>
         <p:childTnLst>
           <p:set>
             <p:cBhvr>
               <p:cTn id="6" dur="1" fill="hold">
                 <p:stCondLst>
                   <p:cond delay="0" />
                 </p:stCondLst>
               </p:cTn>
               <p:tgtEl>
                 <p:spTgt spid="4" />
               </p:tgtEl>
               <p:attrNameLst>
                 <p:attrName>style.visibility</p:attrName>
               </p:attrNameLst>
             </p:cBhvr>
             <p:to>
               <p:strVal val="visible" />
             </p:to>
           </p:set>
         </p:childTnLst>
         <p:subTnLst>
           <p:audio>
             <p:cMediaNode>
               <p:cTn display="0" masterRel="sameClick">
                 <p:stCondLst>
                   <p:cond evt="begin" delay="0">
                     <p:tn val="5" />
                   </p:cond>
                 </p:stCondLst>
                 <p:endCondLst>
                   <p:cond evt="onStopAudio" delay="0">
                     <p:tgtEl>
                       <p:sldTgt />
                     </p:tgtEl>
                   </p:cond>
                 </p:endCondLst>
               </p:cTn>
               <p:tgtEl>
                 <p:sndTgt r:embed="rId2" name="testPy_01_01.wav" />
               </p:tgtEl>
             </p:cMediaNode>
           </p:audio>
         </p:subTnLst>
       </p:cTn>
     </p:par>
   </p:childTnLst>

注目していただきたいのは,青い文字で表示された部分の上から3行目です。

<p:cTn id=”5” presetID=”1″ presetClass=”entr” presetSubtype=”0″ fill=”hold” grpId=”0″ nodeType=”clickEffect”>

nodeType = “clickEffect”という記述があることから,クリックに関連した記述であることがわかります。この文字列の初めの方に,id=”5″となっているところがあります。この“5”を緑の文字で強調していますが,これは,このクリックを表すid(indentifier)番号のようです。そして,これと同じ値(“5”)が黒と赤の文字で表した,オーディオ付加に伴って挿入されるXMLコードの7行目に, <p:tn val=”5” />のように現れています。

説明がグダグダになってしまいました。現在,動作確認できているPythonコードでは,slide.xmlに対して以下のような処理をしてn番目のクリックにオーディオを付加しています。

ⅰ)nodeType=”clickEffect”という文字列のn番目のものを見つける

ⅱ)見つけたnodeType=”clickEffect”から遡って<p:cTn id =” を探索する
 初めに見つけた<p:cTn id =” の後にあるクリックのid番号を読み取る。

ⅲ)nodeType=”clickEffect”から後方を探索し,初めの</p:childTnLst>を見つける

ⅳ)</p:childTnLst>の直後に<p:subTnLst>~</p:subTnLst>の文字列を挿入
 黒字で書かれたコードです。挿入する前に赤文字で書かれた3つの部分を書き直しておきます。
 クリックのid,RelationsのrId,ユーザが作成したwav形式ファイルの名前,の3つです。

以上を,オーディオを付加するクリック起動のアニメーションの数だけ繰り返します。

slide.xml.relsファイルの変更とオーディオファイルの配置

slide.xmlファイルの記述と埋め込みデータなどとの関連付けをするslide.xml.relsの書き換えや,ppt/mediフォルダへのwav形式ファイルの配置などは,スライド切替えオーディオの場合と同じです。異なるのは,スライド1枚につき複数回のデータ追加とファイル追加をする可能性がある点だけです。

繰り返しになりますが,ppt/mediフォルダに書き込むwav形式ファイルは,audio1.wav,audio2.wav,・・・のように,プレゼンテーション全体の中での通し番号で識別されていることに注意が必要です。

動作確認

スライド1枚での中に3つのアニメーションで出現するテキストボックスがある,というシンプルなpptxファイルに自動でオーディオデータを付加できることを確認しています。

元々の仕様では,クリックではなく,直前の動作と同時または直後に起動される「自動起動」のアニメーションにオーディオデータを付加するようになっています。私自身が使うには,自動起動のオーディオはほとんど使わないので,このままでもよいのです。でも,使えるようにしておくと便利かもしれないので,それについては,おいおい考えることにします。

PPTファイルに音声を自動で埋め込む その3 Pythonによる実装 に続く

]]>
https://tamlab.fc2.page/category-materials/1562/feed/ 0
PPTファイルに音声を自動で埋め込む その1 AHKによる実装 https://tamlab.fc2.page/category-materials/1511/ https://tamlab.fc2.page/category-materials/1511/#respond Wed, 01 Mar 2023 21:56:53 +0000 https://tamlab.fc2.page/?p=1511 ここしばらく,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の環境をそろえたりするので時間がかかりそうですが,目途がついたら報告します。

]]>
https://tamlab.fc2.page/category-materials/1511/feed/ 0