EmacsにおけるLaTeX執筆環境構築(3) pdf-toolsについて

はじめに

pdf-toolsを使うことで、Emacs上で使い勝手の良いPDFビューワーが実現できるので、その設定を残しておく。

インストール

基本は公式ページを参照。epdinfoを使うので、そのコンパイルに必要なライブラリ(popplerなど)を色々とインストールする必要が出てくる。

OS別のインストール手順

Linux
sudo aptitude install libpng-dev zlib1g-dev
sudo aptitude install libpoppler-glib-dev
sudo aptitude install libpoppler-private-dev
MacOS

Homebrewは必要なので事前にインストールしておく。

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

次にpopplerとautomakeをインストールする。

 brew install poppler automake

poppler, automakeはそれぞれ依存ライブラリを持つので、コンパイルには時間がかかる。

pdf-toolsのインストール

Lispパッケージのインストール

Emacsのパッケージシステムからpdf-toolsをインストールする。

M-x package-install pdf-tools RET
epdfinfoのインストール

exec-path-from-shellをEmacsのパッケージシステムからインストールしておく。そして以下の設定を追記する。

(exec-path-from-shell-initialize)
(exec-path-from-shell-copy-env "PATH")

その後、

M-x pdf-tools-install

と実行するとepdinfoのビルドが始まる。もしくは設定ファイルに

(add-hook 'after-init-hook
          (lambda ()
            (pdf-tools-install)))

と書いておくと、Emacs起動後にepdinfoのビルドが始まる。このとき、Emacsの内部からシェルを起動してepdinfoのコンパイルをしているのだが、popplerやautomakeのインストールも一緒に行われる。ただ依存ライブラリはEmacsの外側で事前にコンパイル・インストールしておくのがスムーズだろう。さらにEmacs上からepdinfoのビルドに成功するためには、環境変数Emacsに適切に引き継ぐ必要がある。そのためにexec-path-from-shellをインストールしたのであった。

最終的に以下のメッセージを得て、ビルド完了となる。これでようやくpdf-toolsが使えるようになる。

---------------------------
       Installing
---------------------------
make -s install
 /usr/local/bin/gmkdir -p '/usr/local/bin'
  /usr/local/bin/ginstall -c epdfinfo '/usr/local/bin'
make[1]: Nothing to be done for `install-data-am'.
===========================
   Build succeeded. :O)
===========================
Comint finished at Sat Apr  3 11:51:45

設定

コードを表示する

(require 'pdf-tools)
(add-hook 'after-init-hook
          (lambda ()
            (pdf-tools-install)))

;; 左右見開き時のスクロールに関する設定
;; 参考 https://github.com/politza/pdf-tools/issues/303#issuecomment-397744326
(defun my-pdf-view-double-scroll-down-or-previous-page (&optional arg)
  "Scroll page down ARG lines if possible, else go to the previous page.

When `pdf-view-continuous' is non-nil, scrolling downward at the
top edge of the page moves to the previous page.  Otherwise, go
to previous page only on typing DEL (ARG is nil)."
  (interactive "P")
  (if (or pdf-view-continuous (null arg))
      (let ((hscroll (window-hscroll))
            (cur-page (pdf-view-current-page)))
        (when (or (= (window-vscroll)
                     (image-scroll-down arg))
                  ;; Workaround rounding/off-by-one issues.
                  (memq pdf-view-display-size
                        '(fit-height fit-page)))
          (pdf-view-previous-page 2)
          (when (/= cur-page (pdf-view-current-page))
            (image-eob)
            (image-bol 1))
          (set-window-hscroll (selected-window) hscroll)))
    (image-scroll-down arg)))
(defun my-pdf-view-double-scroll-up-or-next-page (&optional arg)
  "Scroll page up ARG lines if possible, else go to the next page.

When `pdf-view-continuous' is non-nil, scrolling upward at the
bottom edge of the page moves to the next page.  Otherwise, go to
next page only on typing SPC (ARG is nil)."
  (interactive "P")
  (if (or pdf-view-continuous (null arg))
      (let ((hscroll (window-hscroll))
            (cur-page (pdf-view-current-page)))
        (when (or (= (window-vscroll) (image-scroll-up arg))
                  ;; Workaround rounding/off-by-one issues.
                  (memq pdf-view-display-size
                        '(fit-height fit-page)))
          (pdf-view-next-page 2)
          (when (/= cur-page (pdf-view-current-page))
            (image-bob)
            (image-bol 1))
          (set-window-hscroll (selected-window) hscroll)))
    (image-scroll-up arg)))

;; 左右見開きページ送り:水平に分割したPDFページを並べ、左右は1ページずらしておく
(defun my-pdf-view-double-scroll-up-horizontal-view ()
  (interactive)
  (if (eq (buffer-local-value 'major-mode (current-buffer)) 'pdf-view-mode)
      (progn
        (my-pdf-view-double-scroll-up-or-next-page)
        (other-window 1)
        (if (eq (buffer-local-value 'major-mode (current-buffer)) 'pdf-view-mode)
            (progn
              (my-pdf-view-double-scroll-up-or-next-page)
              (other-window 1))))))

;; 左右見開きページ戻し:水平に分割したPDFページを並べ、左右は1ページずらしておく
(defun my-pdf-view-double-scroll-down-horizontal-view ()
  (interactive)
  (if (eq (buffer-local-value 'major-mode (current-buffer)) 'pdf-view-mode)
      (progn
        (my-pdf-view-double-scroll-down-or-previous-page)
        (other-window 1)
        (if (eq (buffer-local-value 'major-mode (current-buffer)) 'pdf-view-mode)
            (progn
              (my-pdf-view-double-scroll-down-or-previous-page)
              (other-window 1))))))

(add-hook 'pdf-view-mode-hook
          (lambda()

            ;; 必須
            (linum-mode -1)

            (setq pdf-annot-activate-created-annotations t)

            ;; use normal isearch
            (define-key pdf-view-mode-map (kbd "C-s") 'isearch-forward)

            ;; SPCキーでページ送りスクロール
            (define-key pdf-view-mode-map (kbd "l")
              'my-pdf-view-double-scroll-up-horizontal-view)

            ;; Shift + SPCキーでページ戻しスクロール
            (define-key pdf-view-mode-map (kbd "j")
              'my-pdf-view-double-scroll-down-horizontal-view)

            ;; more fine-grained zooming
            (setq pdf-view-resize-factor 1.1)
            ))


解説と補足

pdf-toolsはAUCTeXのPDFビューワーとして利用することができる。
具体的には以下の設定を済ませておく。

(add-hook 'TeX-mode-hook
          #'(lambda ()
	      (pdf-tools-install)
              (setq TeX-view-program-selection
                    '((output-pdf "PDF Tools")))
              (setq TeX-view-program-list
                    '(("PDF Tools" TeX-pdf-tools-sync-view)))))

もしdisplay-line-numbers-modeを使っているならば,それをOFFにするようにしておく.

(add-hook 'pdf-view-mode-hook
          #'(lambda()
              (display-line-numbers-mode -1)))

さらに、「forward search」「backward search」のためには以下の設定が必要である。

(add-hook 'TeX-mode-hook
          #'(lambda ()
              (setq TeX-source-correlate-method 'synctex)
              (setq TeX-source-correlate-start-server t)
              (setq TeX-source-correlate-mode t)
              (with-eval-after-load "pdf-sync"
                (define-key TeX-source-correlate-map (kbd "C-c C-g")
                  'pdf-sync-forward-search))))
(add-hook 'TeX-mode-hook 'TeX-source-correlate-mode)

ちなみに「forward search」とは、latexソースコードの編集中に`C-c C-g`(後述)とすると、PDFの対応する行までジャンプする機能である。つまり「latexソースコードからビューワー上の頭出しができる」。「backward search」はその逆で、PDFの当該位置に対応するソースコードまでジャンプする機能である。つまりPDFを閲覧しながら、「このあたりってどんな感じでlatex書いたっけ」という確認のために「ビューワー上からlatexソースコードの頭出しができる」機能である。

これらの設定はすでに記事の中に見つけることができる。
tam5917.hatenablog.com

キーバインド

主なキーバインドは以下の通り。

入力 機能
C-n ページを1行単位で上スクロール
C-p ページの1行単位で下スクロール
C-f ページを右スクロール
C-p ページを左スクロール
< 先頭ページに移動
> 最終ページに移動
n 次のページに移動
p 前のページに移動
M-s o occur 発動(入力したクエリがヒットした行を別ウィンドウに抽出)
RET occur ジャンプ (occur ウィンドウ)
F ページ中のハイパーリンクを抽出し、指定箇所にジャンプ
f ページ中のハイパーリンクに対するインクリメンタルサーチ
B ジャンプを含めた過去の移動履歴を戻る
N ジャンプを含めた過去の移動履歴を進む
o (目次付きPDFの場合)目次を表示
RET 目次にジャンプ
M-g g 指定したページにジャンプ
+ ズームイン
- ズームアウト
0 ズームをもとに戻す
H PDFのページをウィンドウの高さにフィットさせる
W PDFのページをウィンドウの幅にフィットさせる
P PDFのページをウィンドウにフィットさせる
s b 余白をカット
s r 余白カットをもとに戻す
s m マウスを使って余白をカット
s f フレーム(Emacsの「枠」)のサイズをPDFに合わせて変更する
C-c C-g forward search
Ctrl + 左クリック backward search

短時間フーリエ変換入門に関する記事へのリンク

早稲田大学の矢田部先生による、短時間フーリエ変換に関する入門記事。

勉強になります。

音声認識結果に基づき複数話者でテキスト音声合成する簡易音声変換アプリをPythonで書いた話

はじめに

PySimpleGUIの応用シリーズ。スクリプトの動作の様子は以下の通り。

本記事の趣旨は今回作成したスクリプトで実現された機能や利用したライブラリ群ほか、関連情報を補足的に説明することである。

作成したスクリプト

スクリプトへのURLを置く。

以下は機能の覚え書きなどである。

動作確認環境

必要なPythonライブラリはコマンドラインからインストールできる(はず)。

pip install numpy
pip install pysimplegui
pip install sounddevice
pip install soundfile
pip install speech_recognition
pip install ttslearn
pip install torch

PySimpleGUI

PySimpleGUIの関数群を用いて、ボタンやフレームを配置してみた。現時点では話者10人分のボタンを直接押すことで話者選択を行っているが、話者数が増えるとしんどいので(実は合成モデルは100人分用意されている)、プルダウンメニュー式の話者選択UIはそのうち作る予定である(やれたらやる)。

PySimpleGUI初学者向けのサンプルプログラムを公開している方がおり、私もこれで最初勉強した。とても参考になった。
PySimpleGUI 基礎解説 | K-TechLaboゼミ用学習ノート

PySimpleGUIの勉強にあたり、以下のWeb記事から概要を知るとよい。

基本的には各ボタンに「key」を紐付けることで、イベントループ内でボタン押下などのイベントを回収し、それぞれの処理を行う構造となる(「音声認識」や「話者変更」など)。

音声収録・音声再生機能

sounddeviceライブラリを用いることにした。

再生にはplay関数、録音にはrec関数を用いる。

再生の際には音割れ防止のための正規化をかけておいた(play_wav関数)。

def play_wav():
    """WAVを再生する"""
    if VARS["audio"] is None or len(VARS["audio"]) == 0:
        raise ValueError("Audio data must not be empty!")

    # 振幅の正規化
    audio = VARS["audio"] / np.abs(VARS["audio"]).max()
    audio = audio * (np.iinfo(np.int16).max / 2 - 1)
    audio = audio.astype(np.int16)

    sd.play(audio, SAMPLE_RATE)

    # 再生は非同期に行われるので、明示的にsleepさせる
    sd.sleep(int(1000 * len(VARS["audio"]) / SAMPLE_RATE))

音声収録の際にはプログレスバーをつけるようにした(listen関数)。sounddeviceのrec関数自体は非同期で動くので、プログレスバーの進行はthreadingを用いた並行処理によった。

def listen():
    """リッスンする関数"""

    # 音声録音の非同期実行
    VARS["audio"] = sd.rec(
        int(DURATION * SAMPLE_RATE), samplerate=SAMPLE_RATE, channels=N_CHANNEL
    )
    time.sleep(BUFFER)  # 少しだけ間を置く

    # 録音している間、プログレスバーを非同期表示
    record_thread = threading.Thread(target=progress_bar, daemon=True)
    record_thread.start()  # 終了すると自動でterminateする

https://gist.github.com/tam17aki/4f11c904832ce270b0fd4e11894bb1aa#file-pysimplegui_recog_tts-py-L204-L215
rec関数の戻り値は(収録サンプル数、チャネル)のshapeをもつarrayであり、モノラルであればチャネル数は1であるが2次元arrayである。

音声認識機能

本記事での音声認識とは、音声データをテキストデータに変換する技術を指す。

実装にはSpeechRecognitionライブラリを用いた。マイク入力で音声取得を行う場合は以前記事を書いたことがある。

今回はマイク入力した直後の音声データだけではなく、既存のwavファイルから音声データを読み込むことのできる仕様にしたいため、音声認識部のスクリプトにも多少の修正が加わっている。音声認識をかける前に、常に一時ファイルに保存したうえで、その一時ファイルをロードする形で音データを取得する。

ちょうどプログラムのこの辺りである(recog関数)。

    # 一旦ファイルに書き込む
    sf.write(
        file=OUTPUT_FILE,
        data=VARS["audio"],
        samplerate=SAMPLE_RATE,
        format="WAV",
        subtype="PCM_16",
    )

    r = sr.Recognizer()
    with sr.AudioFile(OUTPUT_FILE) as source:
        r.adjust_for_ambient_noise(source)
        audio = r.listen(source)  # 音声取得
        try:
            text = r.recognize_google(audio, language="ja-JP")
            VARS["window"]["-RECOG_TEXT-"].Update(text)
        except sr.UnknownValueError:
            VARS["window"]["-RECOG_TEXT-"].Update("認識に失敗しました")

音声合成機能・話者選択機能

本実装で扱う音声合成技術は深層学習に基づく「テキスト音声合成」である。システムにテキストデータを入力し、テキストに対応する合成音声データを出力として得る技術を指す。今回は入力テキストが音声認識結果により得られたものを想定している。

テキスト音声合成に関する機械学習モデルの発展も手伝って、高度な音声合成システムを比較的容易に、各自のローカル計算機環境下で構築・実現できるようになった。最近では『Pythonで学ぶ音声合成』という書籍も出版されており、機械学習・深層学習に基づく高度な音声合成技術をPythonプログラムとともに1冊で学ぶことができる。書籍全体を通しての難易度は専門性の高さから中級者向けであると言える。

今回の実装では、この書籍に付属のPythonライブラリ ttslearn で提供されている、音声合成のための事前学習済モデルを利用することにした。特にテキストから音響特徴量への変換モデルとしてTacotron、また音響特徴量から音声波形への変換モデル(ニューラルボコーダ)としてHiFi-GANに基づく事前学習済モデルを利用する。この事前学習済モデルの使い方は以下に示すページからJupyter notebookを見ることで確認できる。またGoogle Colaboratory上で実際にプログラムを動かし、合成音声を試聴することもできる。

上記リンクより、利用したのは"multspk_tacotron2_hifipwg_jvs24k"(JVSコーパスで訓練された複数話者Tacotoron with HiFi-GAN、サンプリング周波数24kHz) である。16kHzバージョンも利用できるが、多少音質が落ちる。

話者選択機能については、各話者に対応する「ボタン押下イベント」に話者IDを紐づけ、その話者IDを合成モジュールの引数に与えればよい。

def event_synth(event, values):
    """音声合成系のイベント処理"""
    if event == "-SYNTH-":
        text = VARS["window"]["-RECOG_TEXT-"].DisplayText

        # テキスト音声合成
        wav, sr = PWG_ENGINE.tts(text, spk_id=SPK_ID)  # SPK_IDが話者ID

        # 音割れ防止
        wav = (wav / np.abs(wav).max()) * (np.iinfo(np.int16).max / 2 - 1)

        # 再生
        sd.play(wav.astype(np.int16), sr)
        sd.sleep(int(1000 * len(wav) / sr))

関連事例

Seiren Voice

「誰でも100人の声に変えられる声変換システム」Seiren Voiceが発表された。

解説記事によれば、Seiren Voiceは収録された音声を「音素」(=言語情報のひとつ)と「音高」の成分にわけ、それを「学習済みの音声合成モデル」に渡して音声合成を行っている。モデルへの入力において、発話内容に対応する音素は「文字の情報」なので、話者に依存する成分(=声色)が含まれていないという点が重要である。音声合成モデル自体は、話者の数だけ事前に学習しておけばよい(実はこれが大変)。

収録済み音声を言語情報に変換し、それに基づいて音声波形を生成している点においては今回のアプリと関連している。違いの1つはモデルへの入力であり、Seiren Voiceは「音素」と「音高」に分離した状態となる。2つ目の違いは音声波形生成モデルであり、Seiren VoiceはWaveRNNを用いている。

VOICEVOX

今回利用したTactron + HiFi-GANのttslearn実装では、合成時に声の高さや話速のコントロールはできない。あくまで選択された話者について、テキストデータを渡して音声波形を手に入れるのみである。もしこれをコントロール可能となるよう望むならば、機械学習モデル自体に手を入れなければならない。

そのような声の高さや話速のコントロールまで含めた音声合成システムを提供しているのが、VOICEVOXである。


その合成エンジンのソースコードを紐解けば、上記の意味でコントロール可能な枠組みが実装されていることが分かる。
github.com
VOICEVOXのようなアクセント変更向けのGUIをPySimpleGUIで実現することは(おそらく)かなりの工数を要する。未来のスーパーマンに託したい(他力本願!)。

おわりに

今回はGUI構築にPySimpleGUIを用い、音声認識音声合成を直列につないだ非リアルタイム音声変換アプリをPythonで作成した。PySimpleGUIのおかげでGUI構築はかなり楽になったといえる。

Pythonで学ぶ音声合成』はオススメの1冊なので、気になった方はぜひ。

Yahoo天気予報をスクレイピングしてしゃべらせるPythonプログラム

表題の通り。

音声認識結果に基づく天気予報の結果を音声合成によりしゃべらせた、ということ。

必要なライブラリ

pipでインストール可能である。

pip3 install numpy
pip3 install pyopenjtalk
pip3 install speech_recognition
pip3 install urllib3
pip3 install beautifulsoup4

コード

Yahoo天気予報のスクレイピングは以下の記事を参考にした。

Google Homeリスペクトで音声認識の結果に「Ok Google」が含まれ、 かつ「天気」「天気予報」が含まれていた場合にスクレイピングおよび音声合成を発動する。

例えば「Ok Google 今日の名古屋市の天気は」と音声認識されたら、実際に名古屋の天気をしゃべるわけだ。

gist.github.com

参考

Pythonによるウェブスクレイピングについて、ばんくし氏の記事が参考になる。

vaaaaaanquish.hatenablog.com