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

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

gTTSとmpg123で作るテキスト読み上げアプリ【Python】

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

テキストを音声合成し、音声再生するPython アプリを作成しました。
gTTS ライブラリ、mpg123 ライブラリを使用し、一時ファイルを使いません。
ライブラリの使い方やソースコードの説明をします。

はてなブログの記事を読み上げることができます。
併せてテキストファイルやクリップボードのテキストも読み上げられます。

▽アプリの画面

アプリはお使いいただけます 📖 テキスト読み上げアプリ【フリー】 🔗
アプリの画面や機能などはこちらの記事で確認できます。

まず、音声合成の gTTS ライブラリ、音声再生の mpg123 ライブラリについて紹介します。
次に本アプリのコードについて説明します。
使用しているプログラミング言語Python です。

目次

音声合成 gTTS ライブラリ

音声合成のライブラリには、gTTS を使用しました。
これは、google API を使用した音声合成ライブラリで、そのままで日本語対応されています。
インターネットに接続した状態で使用します。
音声の種類を選択することはできず、音声は女性の音声です。

【gTTS ライブラリ】

◇gTTS クラスの仕様

基本的な使い方は、コンストラクタで読ませたいテキストを指定してオブジェクトを作成し、出力メソッドでファイルなどに出力します。

◎コンストラク

  • 【構文】gTTS(text, tld='com', lang='en', slow=False, lang_check=True, ...)
  • 例:gTTS("おはよう", lang = "ja")
  • 主な引数
    • text:読ませるテキスト
    • lang:テキストを読み取る言語(IETF 言語タグ)
      デフォルト:en、日本語:ja
    • slow:テキストを読むのを遅くする(bool)
      ※個人的には False でも遅い、速くする方法は見つかっていません

◎メソッド

合成された音声は、MP3 データで作成されます。
MP3 データを出力するメソッドが用意されています。

  • write_to_fp(fp):ファイルライクオブジェクトに合成結果を書き込む
    • 引数
      • fp:MP3 データを書き込むファイルタイプオブジェクト
  • sve(savefile):合成結果をファイルに書き込む
    • 引数
      • savefile:MP3 データを保存するファイル名を含むパス

◇免責

gTTS のサイトに次のような免責事項が明示されています。本アプリも準拠します。

This project is not affiliated with Google or Google Cloud. Breaking upstream changes can occur without notice. This project is leveraging the undocumented Google Translate speech functionality and is different from Google Cloud Text-to-Speech.
【翻訳】このプロジェクトは Google または Google Cloud とは提携していません。アップストリームの重大な変更は予告なく発生する可能性があります。このプロジェクトは文書化されていない Google 翻訳の音声機能を活用しており、Google Cloud Text-to-Speech とは異なります。 gTTS · PyPI

◇検討した他の音声合成ライブラリ

gTTS ライブラリを採用する時に検討した他のライブラリを紹介します。

◎pyttsx4

pyttsx3 は、GPL だったので対象外としていたのですが、pyttsx4 で MPL になったので検討しました。
私の動作環境だと日本語対応できないようなので使用をあきらめました。

◆音声再生 mpg123 ライブラリ

音声合成で作成した音声データは MP3 型式の音響データです。
MP3 音響データを再生するライブラリとして mpg123 を採用しました。
mpg123 は、MP3 音響データをファイルに保存しなくても再生できます。
その場合、ファイルライクオブジェクトとして入力します。
mpg123 を採用した理由は、この点と外部プログラムではなくライブラリとして呼ばれる点が望んでいたものだったからです。

Python の mpg123 は、ctypes の mpg123 ライブラリのラッパーです。
(ctypes とは Python で C などで書かれた動的リンク(.dll)/共有ライブラリ(.lib)などの関数呼び出しを可能にするもの)
したがって使用には元の mpg123 ライブラリ(libmpg123, libout123)が必要です。

Python mpg123】

【mpg123 ライブラリ】

Python mpg123 から呼ばれるライブラリです。

◇必要な設定

動作には次の設定が必要です。

  • 環境変数 PATH:リリース物を保存したパスを追加
  • 環境変数 MPG123_MODDIR:上記パスの plugins フォルダを指定

設定しないと次のエラーが出ます。

  • error: Failure getting module directory! (Perhaps set MPG123_MODDIR?)

本アプリでは、ソースの中で環境変数を設定しています。
本ソースをそのまま使用する場合は、個別に環境変数の設定をしないでください。

◇Mpg123 クラスの仕様

◎コンストラク

  • 【構文】Mpg123(filename=None, library_path=None)
  • 例:gTTS('tests/bensound-scifi.mp3')
  • 主な引数
    • filename:MP3 ファイルのパス
    • library_path:mpg123のパス
      ※ここでパスを指定しても環境変数の指定が必要

◎メソッド

  • iter_frames(new_format_callback=None)
    次のMPEGフレームをデコードしてイテレータで返します
    またはフレームを呼んで新しいフォーマットを設定します

    • 引数
      • new_format_callback=None
        読み取りを開始するための関数(動作前にフォーマット情報が読まれます)
  • feed(data):引数 data のデータを Mpg123 オブジェクトに与えます

◇Out123 クラスの仕様

◎コンストラク

  • 【構文】Out123(library_path=None)
  • 主な引数
    • library_path:out123のパス
      ※ここでパスを指定しても環境変数の指定が必要

◎メソッド

  • paly(data):引数 data のデータを再生
  • start()  :再生を開始。内部的にフォーマット情報を取得します。

◇再生サンプルコード

from mpg123 import Mpg123, Out123

mp3 = Mpg123('tests/bensound-scifi.mp3')

out = Out123()

for frame in mp3.iter_frames(out.start):
    out.play(frame)

◇検討した他の音声再生ライブラリ

mpg123 ライブラリを採用する時に検討した他のライブラリを紹介します。

  • playsound
    MP3, WAV 再生ライブラリ。音響ファイルは保存されていること。
    1.3.0だとコマンドを認識できないエラーになります
    ⇒1.2.2ならOK。pip install playsound==1.2.2 としてインストールします
  • pydub
    内部的に ffmpeg を subprocess で起動します
  • PyGame
    MP3 をサポートしていません

◆コードの説明

作成したプログラムは、テキストを gTTS で音声合成して、mpg123 で再生します。
この部分はシンプルです。

GUI として画面を作成し、ドラッグアンドドロップクリップボードからの貼り付けでテキストを取得するようにしています。
取得したテキストは読み上げるだけでなく、画面に表示もしています。

◇テキストの読み上げ

読み上げたいテキストを gTTS ライブラリで音声合成し、mpg123 ライブラリで音声再生します。

gTTS ライブラリで出力されるデータをファイルライクオブジェクトに出力し、
それを入力にして mpg123 ライブラリで再生します。

したがって、外部に一時ファイルなどを作成しません。

▽手順

  1. ファイルライクオブジェクト(バイナリストリーム)を作成: BytesIO()
  2. gTTS オブジェクトを作成(音声合成データ作成):     gTTS()
  3. 音声合成データをファイルライクオブジェクトに出力:     write_to_fp()
  4. Mpg123 オブジェクトを作成:               Mpg123()
  5. Mpg123 オブジェクトにファイルライクオブジェクトを与える:feed()
  6. Out123 オブジェクトを作成:                Out123()
  7. Mpg123 オブジェクトのフレームが終わるまでフレームを再生:play()
    iter_frames() メソッドでフレーム分回します

▽コード:音声合成して音声再生する

    def speak_with_mpeg123(self, text:str, lang:str = 'ja'):
        """
        テキストを音声出力する
        Args:
            str:    読み上げるテキスト
            str:    言語(日本語:"ja")
        """
        # gTTSで文章を音声合成し、結果をファイルライクオブジェクト(mp3)に書き込み
        f = BytesIO()
        gTTS(text = text, lang = lang).write_to_fp(f)
        f.seek(0)

        # mp3形式のファイルライクオブジェクトを再生
        mp3 = Mpg123()
        mp3.feed(f.read())
        out = Out123()

        for frame in mp3.iter_frames(out.start):
            out.play(frame)

mpg123 を使用する場合、環境変数の設定が必要です。
Python の os.environ を使用して設定します。
これは、アプリ実行中のみ有効な設定です。
また、設定ファイル settings_speech_text.py を作成しています。
内容については「設定ファイル」⤵を参照してください。

▽コード:mpg123 の環境変数設定

import settings_speech_text as settings

# mpg123をインストールした場所
path_mpg123 = settings.path_mpg123
# MPG123を使用するための環境設定
os.environ['PATH'] = os.environ.get('PATH', '') + os.pathsep + path_mpg123
os.environ['MPG123_MODDIR'] = os.path.join(path_mpg123, 'plugins')

◇テキストの取得

本アプリでは、次の動作でテキストを指定できます。

  • テキストファイル、HTML ファイルをドラッグ・アンド・ドロップ
  • クリップボードにコピーしたテキストをペースト

これらの違いを判断してテキストを取得します。

更に、パスなのかURLなのかテキストなのか、
パスの場合はテキストファイルか HTML ファイルか
を判断して更にテキストを取得します。

MIMEタイプで HTML かテキストかを判断

MIMEタイプとは、「タイプ名/サブタイプ名」の形式で記述され、タイプ名でデータの種類(テキスト、画像、動画など)を、サブタイプ名で具体的なデータ形式を指定します。

【主なMIMEタイプ】

  • テキスト  :.txt    :text/plain
  • HTML文書:.htm .html :text/html
  • XML文書 :.xml    :text/xml
  • JPEG画像:.jpg .jpeg :image/jpeg

◇標準ライブラリ mimetypes を使用

  • インポート:import mimetypes

◇主なメソッド

  • guess_type(url, strict=True)
    引数 url で与えられるファイル名あるいは URL に基づいて、ファイルの型を推定
    • 戻り値:タプル (type, encoding)
      • type:None あるいは type/subtype 形の文字列

▽テキストが url かどうか判断

◇標準ライブラリ urllib.parse を使用

  • インポート:from urllib.parse import urlparse

◇主なメソッド

  • urlparse(urlstring, scheme='', allow_fragments=True)
    引数 url で与えられるファイル名あるいは URL に基づいて、ファイルの型を推定
    • 戻り値:タプル (type, encoding)
      • type:None あるいは type/subtype 形の文字列

▽手順

  1. ドラッグアンドドロップで呼び出されたかを判断
    D&D の場合、event 引数が存在し、data 属性が存在します
    • その場合、パスを取得:splitlist()
      複数 D&D されても先頭だけを処理します
    • そうでない場合、クリップボードからテキストを取得:clipboard_get()
  2. 取得した文字列でファイルの存在確認
    • 存在する場合
      • MIME タイプがテキストか判断
        • テキストなら
          • ファイルを読み込みます(テキストファイル
          • MIME タイプが HTML か判断
            • そうならテキスト情報だけを抜き出します(HTML ファイル
        • テキストでないならメッセージ出力して戻ります
    • 存在しない場合
      • 文字列が URL かどうか判断します:urlparse()
        • URL なら URL を読んでテキスト情報だけを抜き出します(URL
          読み込みでエラーがあればメッセージ出力して戻ります
      • 文字列が URL でない場合、文字列をそのまま使用します(テキスト

▽コード:テキストの取得

from urllib.parse import urlparse
import mimetypes
mimetypes.add_type('text/markdown', '.md')  # Windows環境では存在しないので追加

    def check_string_and_get_text_and_speech(self, event=None):
        """
        ペーストされたテキストがパスかURLかその他かをチェックし、
        パス、URLなら対象を読み込み、読み上げを開始
        """
        # ペースされたものを取得
        if event and hasattr(event, 'data'):        # D&Dの場合
            s1 = self.tk.splitlist(event.data)[0]   # 先頭のみ対象
        else:                                       # ペーストの場合
            s1 = self.clipboard_get()               # クリップボードから取得
        # ファイルが存在したらファイルの内容を読み込む
        if os.path.isfile(s1):
            # MIMEタイプでテキストかどうか判断
            m_t = mimetypes.guess_type(s1)
            if m_t[0].startswith('text/'):
                doc = self.speech_text.get_text_from_file(s1)
                # MIMEタイプがhtml場合テキストだけを取得
                if m_t[0] == 'text/html':
                    doc = self.hatena_blog.get_text_from_html(doc, settings.select_tag, settings.select_class)
            else:
                self.var_strings.set('テキストファイルではありません')
                return
        else:
            # URLだったらサイトから読み込む
            r1 = urlparse(s1)
            if r1.scheme in ('https', 'http'):
                # urlを使ってHTML文書を取得 
                html, err = self.hatena_blog.get_html(s1)
                if err: # HTML文書取得でエラーがあればエラー表示して終了
                    self.var_strings.set(err)
                    return
                # HTML文書からテキストを取得
                doc = self.hatena_blog.get_text_from_html(html, settings.select_tag, settings.select_class)
            else:
                # テキストをそのまま使用
                doc = s1
        # スレッドで読み上げ処理を起動
        th = threading.Thread(target=self.speech_text.do, args=(doc,), daemon=True)
        th.start()

◇Webの取得

貼り付けられたテキストが URL だった場合、その URL の Web サイトからテキストを取得します。

取得には別に作成した www ライブラリを使用します。
ライブラリの使い方の説明は「wwwライブラリについて⤵」を参照してください。
ここでは、処理の内容について説明します。

▽手順

  1. requests の get() メソッドで URL から手データを取得
  2. エラーが無ければ text 属性の内容を取得
  3. 取得した html を BeautifulSoup で解析
  4. 解析結果からタグとクラス名で検索
  5. 見つかったら text 属性を取得
  6. 見つからなかったら body タグを検索して text 属性を取得

▽コード:web からテキストを取得

from www_juu7g.get_web_text import WebSite

    def get_html(self, url:str) -> Tuple[str, str]:
        """
        指定サイトのHTML取得
        Args:
            str:    url
        Returns:
            str:    html
            str:    エラー内容(エラーがない時は空文字)
        """
        result = ""
        err = ""
        endpoint = url
        
        try:
            r = requests.get(endpoint)

            print(f'--request result-- status code={r.status_code}')
            r.raise_for_status()    # 200番代以外は例外を発生
            # 例外がないのでhtmlを返す
            result = r.text
        except requests.exceptions.ConnectionError:
            print('Connection Error!')
            err = "Connection Error!"
        except requests.exceptions.HTTPError:
            err = "エラー:URLが存在しないか、アクセス権限がありません"
        except Exception as ex:
            print(ex)
            result = "Error"
            if r:
                print(f'Error!\nstatus_code: {r.status_code}')
                err = f'Error!\nstatus_code: {r.status_code}'
        return result, err
        
    def get_text_from_html(self, html:str, tag:str="div", cls:str="hatenablog-entry") -> str:
        """
        HTMLをBSで解析してテキストを返す
        タグとクラスを指定してHTML内を検索しそのタグ以下のテキストを返す
        引数のデフォルトははてなブログ用
        Args:
            str:    HTML形式のテキスト
            str:    HTML内を検索するタグ(デフォルト:"div")
            str:    HTML内を検索するクラス(デフォルト:"hatenablog-entry")
        Returns:
            str:    テキスト
        """
        # htmlをBeautifulSoupで解析
        soup = BeautifulSoup(html, "html.parser")

        # タグを検索し、文字列を取得
        _found = soup.find(tag, cls)
        if _found:
            _str = _found.text
        else:
            # 指定された条件のタグが見つからなかった場合、bodyタグ以下のテキストを取得
            _str = soup.find("body").text

        return _str

◇テキストの分割

gTTS で音声合成する時にどのくらいの文字数が良いのか試行錯誤しました。
gTTSは、はてなブログの一つの記事を丸ごと(例えば5000文字位)渡しても音声合成してくれます。
しかし、この場合、音声合成に時間が掛かり、再生を待たされます。

音声合成で待たされたと感じない範囲で、できるだけ多くの文字を音声合成してもらうには、
試行錯誤の結果、128 文字位で処理するのが良いのではないかと至りました。
128 は、2のべき乗なので選びました。

文章を128文字程度に分割するために、まず文章を文単位で分割し、その後128文字以内になるよう結合します。
128文字より長い文はそのまま使います。
分割は、改行または句点で分割します。
句点の場合、句点自体を残します。(句点がなくなると音声合成のされ方が変わるため)
結合は、文字数を見ながら結合するメソッドが見つからなかったので、for 文で対応しました。

▽手順

  1. 正規表現で文章を改行または句点の右で分割
  2. 1文を先読み
  3. 残りの分割された文を読んで以下を実施
    1. 先読みした文と対象の文の文字数をチェックして128より大きいなら
      1. 先読みした文を出力リストに追加
      2. 対象の文を先読みの文として保存
    2. そうでないなら先読みの文と対象の文を空白を挟んで結合
  4. 先読みの文を出力リストに追加

▽コード:文章を分割

    def text_to_list(self, text:str) -> list:
        """
        文章を改行または句点で分割。ただし、短い文はまとめる。
        1要素は128文字以内。ただし、改行がない場合を除く。
        Args:
            str:    読み上げるテキスト
        Returns:
            list:   読み上げる単位に区切ったテキストのリスト
        """
        # 改行、または「。」の右側で分割する。「。」は残す
        texts = re.split(r"\n|(?<=。)", text)

        strs = []
        pre_s = texts[0]
        for cur_s in texts[1:]:
            if len(pre_s) + len(cur_s) > 128 :
                if pre_s:
                    strs.append(pre_s)
                pre_s = cur_s
            else:
                pre_s = pre_s + " " + cur_s # 空白で区切りをつけておく
        strs.append(pre_s)
        return strs

正規表現の後読み・先読みアサーション

「句点を残して句点の右側で文章を分割する」処理は、正規表現を使えば実装可能です。
それには、正規表現の後読み・先読みアサーション機能を使います。

アサーション機能は指定したパターンとマッチした位置の先頭または末尾をマッチ位置とします。
文字の境目がマッチ位置となります。
否定の場合はパターンと一致しない場合がマッチとなります。

今回の場合、パターンに句点を指定して後読みアサーションとすれば、句点の右側がマッチ位置となり、そこで分割されます。

 コード:texts = re.split(r"(?<=。)", text)

 結果 :"こんにちは。元気ですか"["こんにちは。", "元気ですか"]

コンテキストメニュー

コンテキストメニューは、Tkinter の menu ウィジェットで作成します。

▽手順

  1. menu ウィジェット作成:tk.Menu(master, option) コンストラク
    • オプション
      • tearoff:メニューの切り離し可否
  2. メニュー要素の追加:widget.add_command(option) メソッド
    • オプション
      • label:ラベル
      • command:関数
      • menu:サブメニュー
  3. メニュー選択時の動作関数指定:widget.bind(シーケンス, 関数, addオプション)
    ※メニュー要素の追加の add_command() メソッドの command オプションでも指定可能
  4. メニュー表示関数の作成
    表示用の関数を作成します
    関数の引数に event を指定し、event オブジェクトの x、y 属性でマウスのクリックされた座標を取得します 関数内で post() メソッドで位置を指定してメニューを表示します
  5. メニュー表示:widget.post(x, y) メソッド
    • 引数
      • x:表示 x 座標
      • y:表示 y 座標

▽コード:メニュー作成、要素追加、表示(MyFrameクラス)

    def __init__(self, master) -> None:
        # コンテキストメニュー(貼り付けのみ)
        self.cmenu = tk.Menu(self, tearoff=False)   # メニュー作成
        # 要素追加
        self.cmenu.add_command(label='貼り付け'
            , command=self.check_string_and_get_text_and_speech)

    def show_cmenu(self, event):
        """
        コンテキストメニュー表示
        """
        self.cmenu.post(event.x_root, event.y_root)

▽コード:表示用関数のバインド(アプリケーションクラス(Tk クラスを継承))

    def __init__(self) -> None:
        # ペーストの設定
        self.bind('<Button-3>', my_frame.show_cmenu)    # 右クリックでコンテキストメニュー表示

▽コード:ペースト(MyFrameクラス)

    def check_string_and_get_text_and_speech(self, event=None):
        # ペースされたものを取得
        if event and hasattr(event, 'data'):        # D&Dの場合
            s1 = self.tk.splitlist(event.data)[0]   # 先頭のみ対象
        else:                                       # ペーストの場合
            s1 = self.clipboard_get()               # クリップボードから取得
        ... 以下略 ...

◇中断、一時停止

読み上げ中に中断、あるいは一時停止して再開ができるように対応します。
読み上げる文章は、音声合成に適した文字数の文章に分割しています。
読み上げ処理は、分割した文章を逐次、音声合成して音声再生します。
この逐次処理の途中で中断、一時停止をサポートします。

中断処理は、中断ボタンが押されたら中断用フラグをオンし、逐次処理の中でフラグを監視し、フラグがオンになっていたら処理を中断します。

一時停止/再開処理は、threading の Event クラスを使用します。

▽コード:ボタン作成、ボタンが押された時の処理

    def __init__(self, master) -> None:
        self.aborting = False     # 中断処理用フラグの初期化
        self.in_pause = False     # 一時停止/再開フラグの初期化
        self.event = threading.Event()

        # 中断ボタン
        self.btn_stop = tk.Button(self, text='中断', command=self.abort_run)
        self.btn_stop.pack(fill=tk.X)
        # 一時停止/再開
        self.var_pause =tk.StringVar()
        self.var_pause.set('一時停止')
        self.btn_pause = tk.Button(self, textvariable=self.var_pause, command=self.pause_run)
        self.btn_pause.pack(fill=tk.X)


    def abort_run(self):
        """
        中断処理
        """
        self.aborting = True
        if self.in_pause:
            self.pause_run()    # 一時停止中に中断ボタンが押された場合再開させて中断する
        # ボタンが押された状態にする(読み上げ終わるまで)
        self.btn_stop.config(relief=tk.SUNKEN, state=tk.DISABLED)
        

    def pause_run(self):
        """
        一時停止/再開処理
        """
        if self.in_pause:
            # 再開ボタンが押されたので表示を「一時停止」にしてイベントの内部フラグをセットしてスレッドを再開させる
            self.var_pause.set('一時停止')
            self.event.set()
        else:
            # ボタンが押された状態にする(読み上げ終わるまで)
            self.btn_pause.config(relief=tk.SUNKEN, state=tk.DISABLED)
            # 一時停止ボタンが押されたので表示を「再開」に替えてイベントの内部フラグをクリアする
            self.var_pause.set('再開')
            self.event.clear()
        self.in_pause = not self.in_pause           # フラグを反転

▽コード:MyFrameクラス内

    def check_string_and_get_text_and_speech(self, event=None):
        ... 中略 ...
        # スレッドで読み上げ処理を起動
        th = threading.Thread(target=self.speech_text.do, args=(doc,), daemon=True)
        th.start()

▽コード:中断や一時停止の処理

    def do(self, doc:str):
        """
        テキストを読み上げる
        中断や一時停止を処理する
        Args:
            str:    読み上げるテキスト
        """
        strs = self.text_to_list(doc)   # 文章を読み上げ単位に分割

        for text in strs:
            # 一時停止処理
            if self.view.in_pause:
                self.view.event.wait()      # イベントの内部フラグがセットされるまで待つ
            # 中断処理
            if self.view.aborting:
                self.view.var_strings.set('中断しました')
                self.view.aborting = False
                break
            
            # print(f'start:{text[:40]}')
            # 読み上げる設定なら読み上げ、そうでない(表示のみ)なら2秒待つ
            self.view.var_strings.set(text)     # 文章を画面表示
            if settings.do_speech:
                self.speak_with_mpeg123(text)   # 文章の読み上げ
            else:
                time.sleep(5)
            # ボタンが押された状態を戻す
            self.view.btn_pause.config(relief=tk.RAISED, state=tk.NORMAL)
            self.view.btn_stop.config(relief=tk.RAISED, state=tk.NORMAL)

◇Eventクラス

マルチスレッドでイベント発生まで待機、スレッド再開を実現するクラスです。
使用にはインポートが必要です。

  • インポート  :import threading
  • コンストラクタ:threading.Event()

◎メソッド

  • wait() :イベントが発生するかタイムアウトになるまで現在のスレッドを待機
  • set()  :イベントを発生させ、待機スレッドを再開させます(内部フラグをTrueにセット)
  • clear() :イベントのクリア(内部フラグをFalseにリセット)
  • is_set():内部フラグを返します

◎Eventを繰り返し使う

Event クラスを使用してスレッドを待機したり再開したりする場合、clear() メソッドを呼び忘れないように注意が必要です。

◇設定ファイル

動作に必要な mpg123 ライブラリの保存場所などを環境変数に設定する必要があります。

本アプリでは設定ファイル settings_speech_text.py を作成してそこに記述しています。

【設定項目】

  • path_mpg123
    mpg123 をインストールしたフォルダ(mpg123.exe があるフォルダ)を設定します
    フルパスで指定します
  • do_speech
    読み上げをするかどうかを設定します
    True の場合読み上げます(True / False )
  • select_tag
    HTML の本文を区別するタグを設定します
    通常、変更する必要はありません
  • select_class
    HTML の本文を区別するタグのクラスを設定します
    はてなブログの記事用の設定にしてあります
    タグの知識があって、他のサイトの記事を読み上げたい場合に変更してください

▽初期状態の設定ファイル

"""
貼って読み上げ用設定
"""

# mpg123をインストールした場所
path_mpg123 = r'C:\mpg123'

# テスト用に発声をしないで画面表示だけにする
do_speech = True

# HTMLから文章を抽出する時のセレクタ
select_tag = "div"
select_class = "article_body"   # Yahooニュース用
select_class = "hatenablog-entry"   # はてなブログ用

参考に Yahoo ニュースの記事のセレクタを入れてあります。
設定は後に書かれているものが有効です。

◆wwwライブラリについて

Web サイトからデータを取得するコードは今後も良く使うと考え、ライブラリにしました。

せっかくなのでパッケージ化して、GitHub から直接インストールできるようにしました。
 参考記事:📖 自作ライブラリをコピーせずpipして使えるようにする方法【Python】 🔗

◇インストールとインポート

  • インストール:pip install git+https://github.com/juu7g/Python-www.git
    GitHub から直接インストールできます
    依存関係にあるパッケージも併せてインストールされます
  • インポート :from www_juu7g.get_web_text import WebSite

WebSiteクラス

Web を操作するクラスです。

◎メソッド

  • get_html(url:str):指定したurlのhtml文書を取得します
    エラーがあった場合、エラー内容を返します

    • 引数
      • url:url
    • 戻り値
      • str:html
      • str:エラー内容(エラーがない時は空文字)
  • get_text_from_html(html:str, tag:str="div", cls:str="hatenablog-entry")
    指定したhtml文書からテキストを取得します
    clsクラスを持つtagタグの子孫のテキストを取得します

    • 引数
      • html:html文書を指定
      • tag:テキストを取得する対象のタグ(このタグの子孫のテキストが取得対象)
      • cls:テキストを取得する対象のクラス
    • 戻り値
      • str:テキスト
  • output_to_file(text:str, ext:str="txt"):textをファイルに出力します
    ファイル名は兄弟のtestsディレクトリに「html_yymmddHHMM.txt」

    • 引数
      • text:テキスト
      • ext:ファイル拡張子

Tkinterの使い方

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

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

  • インポート:import tkinter as tk

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

Tkinterの使用ウィジェット

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

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

◇使用ウィジェット

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

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

パッケージ ウィジェット 用途 見た目
tkinter Frame
フレーム
ウィジェットの受け皿
tkinter Label
ラベル
文字、画像の表示
tkinter Button
ボタン
押すと処理が動くボタン
tkinter Menu
メニュー
コンテキストメニュー

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

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

今回の画面構成です。

  • lbl_strings (Label) 文章を表示
  • lbl_icon (Label) 画像を表示
    画像は提供していません。提供しているソースのままだとエラーになります
    画像を用意してソースを修正してください
  • btn_stop (Button) 中断ボタン
  • btn_pause (Button) 一時停止/再開ボタン
  • cmenu (Menu) コンテキストメニュー

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

ウィジェットの配置 - pack

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

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

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

ドラッグアンドドロップの実装(TkinterDnD2の使い方)

▶説明は別記事を参照してください
 📖 ドラッグアンドドロップの実装(TkinterDnD2の使い方) - CSV viewerアプリの作り方(ドラッグアンドドロップ)【Python】 🔗

本アプリでは、少し変えました。
ベースとしてアプリケーションクラスを作成しました。
画面を持つアプリケーションなので、Tk を継承しました。
次にドラッグアンドドロップを使用したい場合、継承元を Tk から TkinterDnD.Tk に変更しました。

▽コード:ドラッグアンドドロップの設定

class App(TkinterDnD.Tk):
    """
    アプリケーションクラス
    """
    def __init__(self) -> None:
        """
        コンストラクタ:操作画面クラスと制御クラスを作成し関連付ける
        """
        super().__init__()
        ...中略...
        # ペーストの設定
        self.bind('<Control-v>', my_frame.check_string_and_get_text_and_speech)
        self.bind('<Button-3>', my_frame.show_cmenu)    # 右クリックでコンテキストメニュー表示

        # ドラッグ&ドロップの設定
        self.drop_target_register(DND_FILES)       # ドロップを受け付け
        self.dnd_bind('<<Drop>>', my_frame.check_string_and_get_text_and_speech)

◆ソースの取得

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

wwwライブラリのソースはこちらから取得できます。

◆さいごに

音声入力に続いて音声合成のアプリを作りました。
これらを合わせて更に ChatGPT API を組み合わせれば、チャットアプリができますね。
既にあるのでしょうが、独り者の話し相手ができそうで作ってみたい気もします。


あわせて読みたい 📖 音声入力で文章作成するアプリの作り方【Python】 🔗

◇ご注意

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

  • Python 3.8.5
  • mpg123(Python) 0.4
  • mpg123 1.31.3
  • beautifulsoup4 4.12.2
  • requests 2.31.0
  • tkinterdnd2 0.3.0
  • gTTS 2.3.2
  • www_juu7g 1.0.0

◇免責事項

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

◆参考

投稿: