プログラムでおかえしできるかな

定年を過ぎて何かの役に立てないかなと始めた元SEのブログです

音声入力で文章作成するアプリの作り方【Python】

このエントリーをはてなブックマークに追加

パソコンのマイクに話した内容を文字にするアプリを作成しました。
SpeechRecognition の使い方、pyaudio のインストール方法、マルチスレッドの使い方、ウィジェット変数のトレースなどを説明します。

アプリはお使いいただけます 📔 音声入力で文章作成するアプリ【フリー】 🔗
目次

音声認識Python 向けパッケージには複数ありますが、SpeechRecognition を使用することにしました。 1

◆SpeechRecognition で音声認識

SpeechRecognition の使い方は非常に簡単です。

音声ファイルを使用するならメソッドを一つ動かすだけです。

ここでは、マイクから入力した音声を認識します。
そのため、2つのフェーズがあり、それぞれ該当するメソッドがあります。
それぞれのメソッドは単に実行するだけです。

  • マイクから入力された音声を音声データにする(音声入力
    • Microphone オブジェクトを作成
    • Recognizer クラスの listen() メソッドを実行
      引数に Microphone オブジェクトを指定
    • ※ Microphone オブジェクトを使う時は with 文と合わせて使うと PyAudioを通したマイクの open と後処理を実施してくれます
  • 音声データを解析して文字にする(音声認識
    Recognizer クラスの recognize_google() メソッドを実行
     (Google Speech Recognition 用)

▽音声入力するコード

import speech_recognition as sr

        self.r = sr.Recognizer()
        self.mic = sr.Microphone()

    def listen_voice(self, i):
        with self.mic as source:
            # マイク入力
            audio = self.r.listen(source)

※実際のソースコードからデバッグ用コードなどを除いて表示しています。

▽音声データを認識するコード

    def recognize_voice(self, audio, i) -> str:
        """
        音声認識(Google Speech Recognition)
        Args:
            audio:  音声データ
            int:    処理順
        Returns:
            str:    認識した文字列
        """
        text = ""
        # recognize speech using Google Speech Recognition
        try:
            text = self.r.recognize_google(audio, language='ja-JP')
            self.my_frame.insert_msg(f"\n{i}===>{text}")
        except sr.UnknownValueError:
            self.my_frame.insert_msg("❓")
        except sr.RequestError as e:
            self.my_frame.insert_msg("❓")
        else:
            if text == "ストップ":
                self.do_break = True    # 終了フラグをオン
        return text

基本的には、recognize_google() メソッドを呼び出すだけです。
他はエラー処理と終了のためのフラグ設定です。

※ SpeechRecognition パッケージについては「◆SpeechRecognition パッケージ」節を参照してください。

◆連続音声入力のためのマルチスレッド

音声認識では、先ほども触れたように2つのフェーズがあります。

  • マイクから入力された音声を音声データにする(音声入力)
  • 音声データを解析して文字にする(音声認識

音声入力では、音声の切れ目を検出して音声データを作ります。
音声認識では、その音声データを受けて音声解析をはじめます。
音声認識には時間が掛かります。
音声認識に音声データを渡したらすぐに次の音声入力を始める必要があります。
そうしないと連続した会話に対応できません。

そのため、音声認識はスレッドにして呼び出します。
また、音声認識は音声データの長さによって解析時間が異なります。
したがって、前の音声認識のスレッドの終了を待たずに次の音声認識のスレッドを起動します。

音声認識は I/O バウンドなタスクなのでマルチスレッド(マルチプロセスではなく)で対応します。

マルチスレッド用のライブラリには、threading もありますが、ここでは concurrent.futures の ThreadPoolExecutor を使用します。

理由は、音声認識スレッドの結果を認識を開始した順に受け取れるようにするためです。

※ concurrent.futures モジュールについては、「concurrent.futures モジュール ⤵」節を参照
※ ソースでは、threading モジュールのスレッドで音声入力を動かしていますが、画面(tkinter)が止まらないようにスレッドにしています。

【処理】音声入力の結果を持って音声認識を起動します

  1. ThreadPoolExecutor オブジェクトを作成
  2. 以下を繰り返す
    1. submit() メソッドで音声認識メソッドをスケジュール
    2. submit() メソッドの戻り値 Futrue オブジェクトを self.futures に追加
      ※スレッドの終了を待たずに処理できます
  3. すべての音声入力が終わったら self.futures の要素を追加した順に取り出し、認識した文字列をファイルに出力
    認識した文字列の取得は、Futrue オブジェクトの result() メソッドで行います
    ※このメソッドは音声認識のスレッドの終了を待ちます
  4. shutdown() メソッドで終了処理

※ ThreadPoolExecutor オブジェクトは、with 文と一緒に使うと後処理が必要なく便利ですが、起動したスレッドの終了を内部的に待つ( shutdown(wait=true) を呼んだように)ため、ここでは使用していません。

▽スレッドプールで音声データを認識するコードを呼び出す

    def __init__(self, my_frame) -> None:
        ...中略
        self.futures = []

        # スレッドプールの作成
        self.pool = ThreadPoolExecutor(thread_name_prefix="Rec Thread")

    def recognize_voice_thread_pool(self, audio, i, event=None):
        """
        スレッドプールで音声認識メソッドをスケジュール
        作成された Futureオブジェクトをself.futuresに追加する
        Args:
            audio:  音声データ
            int:    処理順
        """
        future = self.pool.submit(self.recognize_voice, audio, i)
        self.futures.append(future)

▽認識した文字列をファイルに出力

        # 同じファイルに追記する
        # 順番通り取得できるか
        self.insert_msg("▽結果")
        with open("音声入力.txt", "a", encoding="utf_8_sig") as f:
            f.write(f"\n◆{datetime.now().strftime('%Y年%m月%d日 %H:%M:%S')}\n")
            for future in self.vr.futures[:-1]: # 最後の「ストップ」を除く
                s = future.result()
                logger.debug("get future result")
                if not s:continue   # 空文字は認識できなかった時なので出力しない
                # print(f"{s}")
                self.insert_msg(f"{s}")
                f.write(s + "\n")
        self.insert_msg("△", "\n\n")

▽スレッドプールの後処理

    def delete_window(self):
        """
        ウィンドウの☓が押された時の処理
        """
        self.vr.pool.shutdown()         # ThreadPoolExcutorの終了処理
        self.master.destroy()           # 自分でウィンドウを削除する

◆ScrolledText ウィジェットで結果表示

音声認識できたテキストを表示するために ScrolledText ウィジェットを使います。
表示するテキストが複数行に渡るための選択です。

ScrolledText ウィジェットは、Text ウィジェットにスクロールバーが付いたものです。
基本的な機能は Text ウィジェットと同じです。

Text ウィジェットは、エディタなどでテキストを編集する部分を実現するウィジェットと考えていただければ良いです。

今回は編集機能などは使用せず、テキストを追加して表示するだけのために使用しています。

Text ウィジェットにテキストを追加するには、insert() メソッドを使います。
その時に、どこに追加するのかをインデックスで指定します。
今回は常に末尾に追加するので tk.END を使います。
ところですが、tk.INSERT を使用しています。
テキストを追加すると tk.INSERT の位置も動くのでこちらを使いました。
tk.INSERT は、テキストカーソルの位置を示します。

更新:2022-12-28

   self.txt.insert(tk.END, msg1)

※ ScrolledText ウィジェットの仕様は「◆ScrolledText ウィジェット」節で簡単に説明しています。

▽インポートとコンストラク

from tkinter import scrolledtext as tk_scrolledtext

        self.txt = tk_scrolledtext.ScrolledText(master, font=self.font4txt)

▽ScrolledTextウィジェットの作成(フォントの指定を含む)

        self.v_font = tk.IntVar(master, args.font)
        self.v_font.trace_add("write", self.change_font_size)   # v_fontが変更された時の処理を登録

        self.c_font = tk.Entry(self.f_top, textvariable=self.v_font, width=3, justify=tk.RIGHT
                                    , validate="key", vcmd=(self.register(self.entry_validate), "%S"))

        self.font4txt = font.Font(size=self.v_font.get())
        self.txt = tk_scrolledtext.ScrolledText(master, font=self.font4txt)
        self.txt.pack(fill=tk.BOTH, expand=True)

▽ScrolledText ウィジェットへメッセージの挿入

    def insert_msg(self, msg:str, end:str="\n"):
        """
        ScrolledText ウィジェットへメッセージの挿入
        Args:
            str:    挿入する文字列
            str:    終端文字
        """
        msg1 = msg + end
        self.txt.insert(tk.INSERT, msg1)
        self.txt.see(tk.INSERT)
        # self.txt.update_idletasks()   # see()メソッドを実行すると必要ない

ウィジェット変数の監視

今回、しきい値とフォントサイズを変更できるように画面に Entry ウィジェットを作成しました。
Entry ウィジェットの数値を変更したら、対応する処理を実行します。
その方法としてウィジェット変数の監視を選択しました。
その理由は次のようなことです。

つまり、Entry ウィジェットにイベントを割り当ててしまうと検証結果が False でも動いてしまいます。
検証の結果、ウィジェット変数が変更されたらイベントが発生するようにしました。

ウィジェット変数のトレース設定

        self.v_threshold.trace_add("write", self.mic_init)   # v_thresholdが変更された時の処理を登録

        self.v_font.trace_add("write", self.change_font_size)   # v_fontが変更された時の処理を登録

トレース設定にはウィジェット変数オブジェクトの trace_add() メソッドを使用します。
引数に変更のモードと実行する関数(コールバック関数)を指定します。
関数には3つの引数が定義されている必要があるので注意が必要です。
コールバック関数内で引数の値を明示的に使っていないので、定義だけしてあります。

▽コールバック関数の定義部分

    def mic_init(self, var=None, index=None, mode=None):

    def change_font_size(self, var, index, mode):

◎メソッド

【trace_add】コールバック関数の登録

  • 【構文】trace_add(mode, callback)
  • 引数
    • mode:モード
      • "write":変更された
      • "read":取得された
      • "unset":破棄された
      • "array":tclの配列コマンドを呼び出す(Tkinterでは呼び出せない)
    • callback:コールバック関数
      • 関数に必要な引数
        • var:発生したウィジェット変数名
        • index:varがリスト変数を表す場合、そのリストへのインデックス(Tkinterではほとんどない)
        • mode:発生した動作('write', 'read' or 'unset')

【trace_remove】コールバック関数の登録を解除

  • 【構文】trace_remove(mode, cbname)

【trace_info】登録されているコールバック関数名を返す

  • 【構文】trace_info()

◆SpeechRecognition ライブラリ

SpeechRecognition は、音声認識を実行するための Python ライブラリです。

特徴は、複数の認識エンジンをサポートしていることです。
認識エンジンは、オンラインまたはオフラインで動作するものがあります。

◇対応している音声認識エンジン/API

SpeechRecognition では以下の音声認識エンジン/API に対応しています。
それぞれ特徴があります。
他のサイトでの評価結果 2 などを基に、ここでは「Google Speech Recognition」を使用します。

エンジン サン
プル
日本語 無料 オフ
ライン
備考
CMU Sphinx
Google Speech Recognition
Google Cloud Speech API
Wit.ai 人工知能API
Microsoft Bing Voice Recognition
Houndify API 音楽認識検索
IBM Speech to Text
Snowboy Hotword Detection ホットワード検出
開発元は2020/12/31終了Githubは残る Win非対応

※サンプル:サンプルコードが提供されているかどうか 3
※ホットワード検出:発話された時にデバイスをアクティブにすることを目的とした特別な単語またはフレーズの検出

◇依存関係

  • PyAudio 0.2.11+ (マイク入力のために必要)

◇インストール

  • pip install pyaudio
    pyaudio のインストールに失敗する場合は「◆pyaudioのインストール ⤵」節を参照
  • pip install SpeechRecognition

  • インポート:import speech_recognition as sr

◆SpeechRecognition パッケージ

◇Recognizerクラス

音声認識のためのクラスです。

◎メソッド

【listen】 オーディオソースからの音を録音しオーディオデータを返します

  • 【構文】listen(source: AudioSource, timeout: Union[float, None] = None, phrase_time_limit: Union[float, None] = None, snowboy_configuration: Union[Tuple[str, Iterable[str]], None] = None) -> AudioData
  • よく使う使い方(サンプル)
    【構文】listen(source)
  • 主な引数
    • source:オーディオソースオブジェクト(マイクなど)
    • timeout:フレーズの開始を待機してから、WaitTimeoutError 例外をスローするまでの最大秒数
      Noneの場合、待機タイムアウトなし
    • phrase_time_limit:フレーズを継続できる最大秒数
      結果として得られる音声は、制限時間にカットオフされたフレーズ
      Noneの場合、フレーズの時間制限はない
  • 戻り値:オーディオソースからの単一のフレーズを記録した AudioData オブジェクト
  • 記録開始:オーディオの大きさが energy_threshold 属性値を超える(ユーザーが話し始めた)までは待機
    energy_threshold 属性値のデフォルトは、300
     ただしdynamic_energy_threshold(デフォルトは有効)が有効なら自動調節
  • 記録終了:pause_threshold 秒の無音と判断する状態が発生するか、
    オーディオ入力がなくなると終了

【adjust_for_ambient_noise】 外部雑音に合わせてしきい値を調整します

  • 【構文】adjust_for_ambient_noise(source: AudioSource, duration: float = 1) -> None
    オーディオソースを通して周囲のノイズを考慮した しきい値を動的に調整
    本メソッドは音声のない期間に実行する
    音声が検出されると早期に停止
  • source:オーディオソースオブジェクト(マイクなど)
  • durationしきい値の動的調整時間の最大値(秒)(デフォルト1秒) 少なくとも0.5以上が望ましい。

【recognize_google Google Speech Recognition を使用して音声認識します

  • 【構文】recognize_google(self, audio_data, key=None, language="en-US", show_all=False)
  • よく使う使い方(サンプル)
    【構文】recognize_google(audio, language='ja-JP'))
  • 主な引数
    • audio_data:オーディオデータ
    • language:日本語は"ja-JP"

◎属性

  • energy_threshold:デフォルト 300
    無音と音声を区別する音の大きさのレベルのしきい値
    このしきい値を下回る値は無音と見なされます
    このしきい値を超える値は音声と見なされます
    動的しきい値dynamic_energy_threshold を参照)が有効になっている場合、自動調整します
    適切な開始値は、自動調整を早く終わらせます
    静かな部屋での一般的な値は 0〜100 です
    話し声がしているような環境での一般的な値は 150〜3500 です
    無音とみなして欲しいのに音を拾ってしまう場合、より高い値に調整してみてください
  • pause_threshold:デフォルト 0.8 秒
    この長さ分の無音状態が続くとフレーズの終わりと判断します
  • dynamic_energy_threshold:デフォルト True
    energy_threshold を、リスニング中に自動調整するかどうかを表すフラグ
    周囲のノイズレベルが予測できない状況の場合に使うと良いです

◇Microphoneクラス

PC 上の物理マイクを表すクラスです。

◎コンストラク

  • 【構文】Microphone(device_index: Union[int, None] = None, sample_rate: int = 16000, chunk_size: int = 1024) -> Microphone
    with文の使用が可能。使用した場合、PyAudioを通してマイクのopenと後処理を実施
  • 【使い方】
with Microphone() as source:    # open the microphone and start recording
    pass                        # do things here - ``source`` is the Microphone
                                # instance created above
                                # the microphone is automatically released at this point

◆concurrent.futures モジュール

concurrent.futures モジュールは、非同期に実行できる呼び出し可能オブジェクトの高水準のインターフェースを提供します。

使用にはインポートが必要です。

   from concurrent.futures import ThreadPoolExecutor

◇クラス

  • ThreadPoolExecutor
    • スレッドを使って並列タスクを実行
    • ネットワークアクセスなど CPU に負荷がかからない(I/O バウンドな)処理の並列実行向き
  • ProcessPoolExecutor
    • プロセスを使って並列タスクを実行
    • CPU に負荷がかかる計算(CPU バウンドな)処理などの並列実行向き
  • Future
    • カプセル化された呼び出し可能オブジェクトの非同期実行状態
      submit() メソッドで生成される
    • 呼び出し可能オブジェクトのキャンセルや実行結果の取得が可能

◇メソッド

以下は、ThreadPoolExecutor クラス、ProcessPoolExecutor クラスで使えるメソッドです。

【submit】関数の実行をスケジュールします

  • 【構文】submit(fn, *args, **kwargs)
  • 引数
    • fnfn(*args, **kwargs) をスケジュール
  • 戻り値
    • Future オブジェクト

【shuttdown】 Executor(ThreadPoolExecutor オブジェクトなど)の終了処理

  • 【構文】shuttdown(wait=True, *, cancel_futures=False)
  • 引数
    • wait:待ち方
      wait の値に関係なく、すべての未完了の Future の実行が完了するまで Pythonプログラム全体は終了しない
      • True:すべての未完了の Future の実行が完了して Executor に 関連付けられたリソースが解放されたら、このメソッドが返る
      • False:このメソッドはすぐに返る
        すべての未完了の Future の実行が完了したときに、Executorに関連付けられたリソースが解放される
  • with 文
    with 文を使用すると、このメソッドを明示的に呼ばなくてすむ
    with 文は Executor をシャットダウンする (wait を True にセットして Executor.shutdown() が呼ばれたかのように待つ)

Tkinterの使い方

Tkinter(ティーキンター)は Python が標準で提供している GUI パッケージです。
他のプログラムの開発ツールで提供されているようなルック&フィールな GUI ツールではありません。
従って、画面を構成するには、すべてコードを書く必要があります。

Tkinter を使用するには import が必要です。

   import tkinter as tk

慣例的に tk と別名を付けるようです。

Tkinterの使用ウィジェット

Tkinter がサポートする GUI の部品をウィジェット (widget) と呼びます。

ここでは次のウィジェットを使用しています。

◇使用ウィジェット

ウィジェットは基本的にインスタンスを作成し、表示テキストや表示方法などを指定して使用します。

今回使用しているウィジェットです。

パッケージ ウィジェット 用途 見た目
tkinter Frame
フレーム
ウィジェットの受け皿
tkinter Label
ラベル
文字の表示
tkinter Button
ボタン
押すと処理が動くボタン
tkinter Checkbutton
チェックボックス
チェックでオン/オフを設定
tkinter Entry
エントリー
テキストを入力するボックス
tkinter ScrolledText
スクロールバー付テキスト
テキストの編集

◇画面構成 - フレーム構造

プログラムを作るにあたって、どのような画面にするか決めなくてはなりません。画面構成を考えるということですね。
画面を構成するというのは、GUI 部品(ウィジェット)を画面に配置することです。
ウィジェットは設定したテキストに合わせてサイズが決まるので、そのまま使用しても画面構成上問題ないものと、広げないと見栄えが良くないものが出てきます。
ウィジェットを広げる場合、他のウィジェットとの位置関係で思ったように配置できないことがあります。
そのようなときに Frame ウィジェットを使用して区分けをして希望する位置に配置します。
引き出しの整理に使うトレイのようなイメージでしょうか。

今回の画面構成です。

  • f_top (frame)
    • b_start (Button)
    • l_threshold (Label)
    • e_threshold (Entry)
    • c_adjust (Checkbutton)
    • l_font (Label)
    • c_font (Entry)
  • txt (ScrolledText)

※「f_top」などはプログラムで使用した変数名です。()内はクラス名です。

入力をする部分と出力をする部分で大きくウィジェットを分けています。
結果を表示する部分は余った領域をすべて使用して、できるだけ大きく表示させています。

ウィジェットの配置 - pack

▶説明は別記事を参照してください
 📔 ◇ウィジェットの配置 - pack - SQLクライアントアプリの作り方(Tkinterで表)【Python】 🔗

◇動作をウィジェットに割り当てる - バインド

▶説明は別記事を参照してください
 📔 □動作をウィジェットに割り当てる - Excel viewerアプリの作り方(Tkinterでタブと表)【Python】 🔗

◆ScrolledText ウィジェット

Text ウィジェットを継承してスクロールバーが付加されたウィジェットです。
ここでは、使用している insert() メソッドに関する仕様を説明します。

使用するには tkinter とは別にインポートが必要です。

from tkinter import scrolledtext as tk_scrolledtext

◇インデックス

インデックスとは、Text ウィジェットに挿入したコンテンツの中の位置を指します。
インデックスを指定して挿入や削除する位置を指定します。

【主なインデックス】

  • 行.桁:行(1から数えて)、列(0から数えて)
  • 行.end:指定された行の末尾の改行の直前の位置
  • tk.INSERT:テキストウィジェットの挿入カーソルの位置。"insert" と同じ
  • tk.CURRENTマウスポインターに最も近い文字の位置。"current" と同じ
  • tk.END:テキストの最後の文字の後の位置。"end" と同じ
  • tk.SEL_FIRST:選択範囲の先頭の位置。未選択だとエラー
  • tk.SEL_LAST:選択範囲の末尾の位置。未選択だとエラー

◇メソッド

【insert】テキストの挿入

  • 【構文】insert(index, text, tags=None)
  • 引数
    • index:挿入位置
    • text:テキスト
    • tags:タグを設定する場合に指定
      ※タグについては説明を省略します

pyaudioのインストール

PyAudio のインストールは pip で行います。

pip install pyaudoi

しかし、次のようなエラーが出る場合があります。
その場合の対処方法を説明します。

pyaudioがインストールできない場合

C++ビルドツールがない

Microsoft Visual C++ 14.0 or greater is required.」 が出た場合 4

C++コンパイラがなくてビルドできないので、コンパイラをインストールします。

.whl ファイルがない

「package 'wheel' is not installed」 が出た場合 5

whl ファイルを取得します。

GitHub - Uberi/speech_recognition 』で紹介している 『speech_recognition/third-party at master · Uberi/speech_recognition · GitHub 』 には古いファイルしかないので、下記から取得します。

◆ソースの取得

全体のソースはこちらから取得できます。

※ソースにはデバッグ用のコードが含まれていますのでご容赦ください。
デバッグには logging モジュールを使用しています。
 logging モジュールの使い方については、別の記事で解説する予定です。

◆さいごに

単発での音声入力、認識は、SpeechRecognition が優秀なので直ぐに動きました。
それよりも、pyaudio のインストールやマイクの動作確認(結果的にマイクのコードが不安定だっただけ)、マルチスレッド対応で時間が掛かりました。

マルチスレッドをそれなりに使うのは初めてで、思ったように動いているのかどうかを確認したくて、logger でスレッド名を出力したデバッグ情報を出せるようにしました。

logger の使い方も少し調べると作法があるようで、自分なりに良いと思った方法を取り入れています。(この記事で説明はしていませんが・・・🙇)

更に、MVC(Model-View-Controller) なるソフトウェアアーキテクチャーの存在を遅まきながら知ることとなり、試してみました。

こじんまりしたソースですが、いろいろとチャレンジしてみました。

あわせて読みたい 📔 はてなブログ向け画像ツールの作り方【Python】 🔗
画像に対し、サイズ変更、反転、文字透かし挿入ができ、はてなフォトライフにアップロードできるツールの作り方の説明記事です。

◇ご注意

本記事は次のバージョンの下で動作した内容を基に記述しています。

◇免責事項

ご利用に際しては、『免責事項』をご確認ください。
お気づきの点がございましたら『お問い合わせ』からお問い合わせください。

◆参考

投稿: 、更新: