偉大なる先人のプログラムを参考に書いてみたということ。
参考: Real Time PyAudio wave plot and FFT transform plots from microphone · GitHub
偉大なる先人のプログラムを参考に書いてみたということ。
参考: Real Time PyAudio wave plot and FFT transform plots from microphone · GitHub
pdf-toolsを使うことで、Emacs上で使い勝手の良いPDFビューワーが実現できるので、その設定を残しておく。
基本は公式ページを参照。epdinfoを使うので、そのコンパイルに必要なライブラリ(popplerなど)を色々とインストールする必要が出てくる。
sudo aptitude install libpng-dev zlib1g-dev sudo aptitude install libpoppler-glib-dev sudo aptitude install libpoppler-private-dev
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 |
2値が-1と1とした場合、こう書く。
import numpy numpy.random.choice([-1, 1], 10) # 10個生成
PySimpleGUIの応用シリーズ。スクリプトの動作の様子は以下の通り。
wavファイルを音声認識して、複数話者で音声合成するデモ(最初はデフォルトのテキストで合成) pic.twitter.com/CYX5pw0qiX
— mat (@ballforest) September 5, 2021
本記事の趣旨は今回作成したスクリプトで実現された機能や利用したライブラリ群ほか、関連情報を補足的に説明することである。
スクリプトへの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の関数群を用いて、ボタンやフレームを配置してみた。現時点では話者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))
「誰でも100人の声に変えられる声変換システム」Seiren Voiceが発表された。
解説記事によれば、Seiren Voiceは収録された音声を「音素」(=言語情報のひとつ)と「音高」の成分にわけ、それを「学習済みの音声合成モデル」に渡して音声合成を行っている。モデルへの入力において、発話内容に対応する音素は「文字の情報」なので、話者に依存する成分(=声色)が含まれていないという点が重要である。音声合成モデル自体は、話者の数だけ事前に学習しておけばよい(実はこれが大変)。
収録済み音声を言語情報に変換し、それに基づいて音声波形を生成している点においては今回のアプリと関連している。違いの1つはモデルへの入力であり、Seiren Voiceは「音素」と「音高」に分離した状態となる。2つ目の違いは音声波形生成モデルであり、Seiren VoiceはWaveRNNを用いている。
今回利用したTactron + HiFi-GANのttslearn実装では、合成時に声の高さや話速のコントロールはできない。あくまで選択された話者について、テキストデータを渡して音声波形を手に入れるのみである。もしこれをコントロール可能となるよう望むならば、機械学習モデル自体に手を入れなければならない。
そのような声の高さや話速のコントロールまで含めた音声合成システムを提供しているのが、VOICEVOXである。
その合成エンジンのソースコードを紐解けば、上記の意味でコントロール可能な枠組みが実装されていることが分かる。
github.com
VOICEVOXのようなアクセント変更向けのGUIをPySimpleGUIで実現することは(おそらく)かなりの工数を要する。未来のスーパーマンに託したい(他力本願!)。
表題の通り。
音声認識結果に基づく天気予報の結果を音声合成によりしゃべらせた、ということ。
pipでインストール可能である。
pip3 install numpy pip3 install pyopenjtalk pip3 install speech_recognition pip3 install urllib3 pip3 install beautifulsoup4
Yahoo天気予報のスクレイピングは以下の記事を参考にした。
Google Homeリスペクトで音声認識の結果に「Ok Google」が含まれ、 かつ「天気」「天気予報」が含まれていた場合にスクレイピングおよび音声合成を発動する。
例えば「Ok Google 今日の名古屋市の天気は」と音声認識されたら、実際に名古屋の天気をしゃべるわけだ。