Streamlitを用いた音響信号処理ダッシュボードの開発(Tokyo BISH Bash #03発表資料)
Streamlitを用いた音響信号処理ダッシュボードの開発(Tokyo BISH Bash #03発表資料)¶
本記事はTokyo BISH Bash #03の発表資料です。
In [1]:
from IPython.display import IFrame
IFrame("https://www.hiromasa.info/slide/22.slides.html", "100%", "450px")
Out[1]:
自己紹介およびPyData Osakaの紹介¶
- 別スライドで説明します
本日の概要¶
- Streamlitの紹介
- 実例を踏まえた使用方法の紹介
- Herokuへのデプロイ方法の紹介
- その他
実行環境¶
- 
https://hub.docker.com/r/wrist/jupyterlab-custom- docker pull wrist/jupyterlab-custom
 
- 
22.ipynbの動作確認に使用- ただしファイルアップロード時に書き込みエラーが生じる場合有
 
Streamlitとは¶
- Webブラウザで動作するダッシュボードを作成するためのライブラリ- 複数のコンポーネントが連携して動作するもの
 
- Pythonのみで記述が可能- 
plotly/dashと比較されることが多い
 
- 
どんな場面で必要か¶
- 実装したアルゴリズムを他者に試してもらいたい- Webフロントエンドがあると便利
 
- パラメータ変更による試行錯誤が必要な場面- 調整ツールとして使用
 
実例¶
- 今回作成したダッシュボードを簡単に紹介
Dashとの違い¶
- 
Towards Data Scienceの記事- Dash- プロダクション/エンタープライズ環境での実行に主眼
 
- Streamlit- ラピッドプロトタイピングに主眼
 
 
- Dash
- 
plotly.comの比較記事- Rのshinyも含めて比較
 
Streamlitの魅力¶
- 手軽にフロントエンドが構築可能- 
javascript不要、スクリプトライクな記述
- 各種pythonライブラリにGUIを付与
 
- 
- 豊富な描画ライブラリ- 
matplotlib,plotly,altair, ...
- Dashはplotlyを前提
 
- 
Streamlitの情報¶
本発表におけるバージョン¶
- 0.68.0- 10/8にリリースされたばかり
- 1月あたり1〜2回マイナーバージョンが更新- まだ不安定
 
 
- 0.66.0も一部で使用
基本機能の紹介¶
導入方法¶
- pip install streamlit
実行方法¶
In [2]:
%%writefile app.py
import streamlit as st
st.write("# Hello, Streamlit!")
In [3]:
!streamlit run app.py
- ノートブックが固まるため以降はターミナル上からstreamlitコマンドを実行
jupyterlabが8888番ポートのみ稼働している環境上でのアクセス¶
- 
jupyter-server-proxyの導入によるproxyアクセス- 
jupyterのURL/proxy/streamlitのport/でアクセス可能
- ex. 127.0.0.1:8888/proxy/8501/
 
- 
In [4]:
%%writefile app.py
import streamlit as st
st.write("# Hello, Streamlit!")
st.write("# Where is Rerun button?")
スクリプトエラーもブラウザ上に表示¶
- デバッグも容易
In [5]:
%%writefile app.py
import streamlit as st
st.write("# Hello, Streamlit!")
raise ValueError
st.write¶
- 引数に与えたオブジェクトを画面に表示
- markdownはHTMLに変換して表示
- listやdictは階層表示
- ndarrayやpandasのDataFrameはテーブルとして表示
In [6]:
%%writefile app.py
import numpy as np
import pandas as pd
import streamlit as st
st.write([1, 2, 3])
st.write({"hello": "world!"})
st.write(np.arange(10).reshape(1,10))
st.write(pd.DataFrame(np.random.randn(10, 4), columns=["1", "2", "3", "4"]))
本日紹介するダッシュボード¶
- 
https://github.com/wrist/streamlit-dsp- 窓関数ビューワ
- デジタルフィルタデザイナ
- pyroomacousticsを用いたシミュレータ
- espnet2のフロントエンド
 
ブランチ¶
- master
- 
heroku- Herokuへのアップロードに必要なファイルを格納
 
- 
espnet2- 3.6.x系で動作させるため別ブランチ
- 
Steramlitのバージョンは0.66.0
 
github上のstreamlitアプリ¶
- 一般的には直接実行可能- streamlit run https://github.com/user/repos.git/master/app.py
 
- 今回のアプリはimportの都合でエラーが生じる
窓関数ビューワの構築¶
- 
scipy.signalを使用
- 
Streamlitによるアプリ構築をライブコーディング的に実施
- 現状のダッシュボード
窓関数の選択インタフェース¶
- サイドバーにselectboxで表示
- サイドバー上への配置- st.sidebar.xxx
- 
st.xxxと本体に配置できるウィジェットは何でも配置可能
 
In [7]:
%%writefile app.py
import streamlit as st
windows = ["boxcar", "triang", "blackman", "hamming", "hann", "bartlett", "flattop",
           "parzen", "bohman", "blackmanharris", "nuttall", "barthann"]
- 
%%writefile -a app.pyでapp.pyに追記
In [8]:
%%writefile -a app.py
win_name = st.sidebar.selectbox("window", windows, 4)
st.write(win_name)
- この時点の見た目
窓関数の窓長およびFFT長の取得¶
- 同様にst.selectboxで2の累乗の値を取得
In [9]:
%%writefile -a app.py
two_powers = [2**i for i in range(16)]
Nx = st.sidebar.selectbox("Window Length", two_powers, 8)
nfft = st.sidebar.selectbox("FFT Length", two_powers, 10)
st.write(win_name, Nx, nfft)
- この時点での見た目
窓関数の取得および描画¶
- 窓関数の取得- scipy.signal.get_window
- 
scipy.fft.fftで周波数分析
 
- 描画- matplotlibをひとまず使用
 
- 必要なライブラリをimport
In [10]:
%%writefile -a app.py
import matplotlib.pyplot as plt
import numpy as np
import scipy.signal as sg
import scipy.fft as fft
- 窓関数を取得し周波数特性を計算
In [11]:
%%writefile -a app.py
eps = 1.e-12
win = sg.get_window(win_name, Nx)
W = 20.0 * np.log10(np.abs(fft.fft(win, nfft)) + eps)
W = fft.fftshift(W)
- 
matplotlibを用いて描画
In [12]:
%%writefile -a app.py
fig, axes = plt.subplots(2, 1)
axes[0].plot(win)
axes[1].plot(W)
st.pyplot(fig)
st.pyplot¶
- 
matplotlibのfigureを描画- st.pyplot(fig)
 
- 使い慣れたmatplotlibを使える- が、javascriptの恩恵が得られない
 
- この時点での見た目
streamlitの描画ライブラリ¶
- 
st.line_chart,st.area_chart,st.bar_chart- 
st.altair_chartのsyntax sugar
- 手軽に使える反面細かい設定は不可能
 
- 
- その他vega,plotly,bokehなども使用可能
In [13]:
%%writefile -a app.py
st.line_chart(win)
st.line_chart(W)
- 
st.line_chartの見た目
この時点でのファイルの内容¶
- 通常の実行スクリプトに近い
- 手軽にフロントエンドの作成が可能
In [14]:
!cat app.py | pygmentize
複数の窓関数を同時に描画したい¶
- 
st.multiselect(ラベル名, 選択肢のリスト, デフォルト項目の要素のリスト)- 複数の選択項目をリストで返す
- 第三引数は選択肢中に存在する要素のリスト
 
- 先程と同じ
In [15]:
%%writefile app.py
import streamlit as st
windows = ["boxcar", "triang", "blackman", "hamming", "hann", "bartlett", "flattop",
           "parzen", "bohman", "blackmanharris", "nuttall", "barthann"]
- 先程と同じ
In [16]:
%%writefile -a app.py
import matplotlib.pyplot as plt
import numpy as np
import scipy.signal as sg
import scipy.fft as fft
- 
st.multiselectを使用
In [17]:
%%writefile -a app.py
# ここから先が異なる
win_names = st.sidebar.multiselect("window", windows, [windows[4]])
st.write(win_names)
- 
win_namesをst.writeする以外は同じ
In [18]:
%%writefile -a app.py
two_powers = [2**i for i in range(16)]
Nx = st.sidebar.selectbox("Window Length", two_powers, 8)
nfft = st.sidebar.selectbox("FFT Length", two_powers, 10)
st.write(win_names, Nx, nfft)
- 
win_namesをループで処理
In [19]:
%%writefile -a app.py
eps = 1.e-12
for win_name in win_names: # ループとして処理
    win = sg.get_window(win_name, Nx)
    W = 20.0 * np.log10(np.abs(fft.fft(win, nfft)) + eps)
    W = fft.fftshift(W)
In [20]:
%%writefile -a app.py
    # ループの続き
    st.write(win_name)
    st.line_chart(win)
    st.line_chart(W)
- 複数の窓が繰り返し描画される
In [21]:
!cat app.py | pygmentize
複数の窓をまとめて描画したい¶
- pandasのDataFrameの複数カラムにデータを押し込む
- 
st.line_chartに渡す
- 先程と同じ
In [22]:
%%writefile app.py
import streamlit as st
windows = ["boxcar", "triang", "blackman", "hamming", "hann", "bartlett", "flattop",
           "parzen", "bohman", "blackmanharris", "nuttall", "barthann"]
- pandasを追加
In [23]:
%%writefile -a app.py
import matplotlib.pyplot as plt
import numpy as np
import scipy.signal as sg
import scipy.fft as fft
import pandas as pd  # 追加
- 先程と同じ
In [24]:
%%writefile -a app.py
win_names = st.sidebar.multiselect("window", windows, [windows[4]])
st.write(win_names)
- 先程と同じ
In [25]:
%%writefile -a app.py
two_powers = [2**i for i in range(16)]
Nx = st.sidebar.selectbox("Window Length", two_powers, 8)
nfft = st.sidebar.selectbox("FFT Length", two_powers, 10)
st.write(win_names, Nx, nfft)
- 窓と周波数特性をlistに格納
In [26]:
%%writefile -a app.py
eps = 1.e-12
win_ary = []; W_ary = []
for win_name in win_names: # ループとして処理
    win = sg.get_window(win_name, Nx)
    W = 20.0 * np.log10(np.abs(fft.fft(win, nfft)) + eps)
    W = fft.fftshift(W)
    win_ary.append(win)
    W_ary.append(W)
- DataFrameに格納
In [27]:
%%writefile -a app.py
fs = 16000.0
t = np.arange(Nx)
f = (fs / nfft) * np.arange(-nfft/2, nfft/2)
df_win = pd.DataFrame(np.array(win_ary).T, index=t, columns=win_names)
df_W   = pd.DataFrame(np.array(W_ary).T,   index=f, columns=win_names)
In [28]:
%%writefile -a app.py
st.line_chart(df_win)
st.line_chart(df_W)
In [28]:
!cat app.py | pygmentize
st.line_chartに軸などを追加したい場合¶
- 
st.altair_chartを用いる必要がある
- 
st_my_line_chartというメソッドを定義(次セル)- 
st.line_chartの代わりに使用可能
- 詳細は省略
 
- 
def st_my_line_chart(xs, ys, columns, xlabel, ylabel, xlim=None, ylim=None, category_name=None):
    df = pd.DataFrame(data=np.array(ys).T,
                      index=xs,
                      columns=columns)
    if category_name is None:
        category_name = "category"
    df_melt = df.reset_index().melt("index", var_name=category_name, value_name="y")
    if xlim:
        x_kwargs = {"title":xlabel, "scale":alt.Scale(domain=xlim)}
    else:
        x_kwargs = {"title":xlabel}
    if ylim:
        y_kwargs = {"title":ylabel, "scale":alt.Scale(domain=ylim)}
    else:
        y_kwargs = {"title":ylabel}
    c = alt.Chart(df_melt).mark_line().encode(
            alt.X("index", **x_kwargs),
            alt.Y("y", **y_kwargs),
            tooltip=["index", "y"],
            color='{0}:N'.format(category_name)
            ).interactive()
    st.altair_chart(c, use_container_width=True)
    return df, df_melt
窓関数ビューワのまとめ¶
- 
st.selectbox,st.multiselectを紹介
- 
st.pyplot,st.line_chartの使い方を紹介
デジタルフィルタデザイナー¶
- 
scipy.signalのデジタルフィルタ設計関数を用いたダッシュボードを作成- 設計したフィルタをアップロードした音声に適用しブラウザ上で視聴
- 処理後の音声のダウンロードリンクを生成
 
現状のダッシュボード¶
- 
filter_designer.py- 
scipy.signal.firwinのみ対応
 
- 
紹介する項目¶
- 数値入力- st.number_input
 
- チェックボックス- st.checkbox
 
- オーディオファイルのアップロード/ダウンロード- st.file_uploader
- ダウンロードリンクは自力で作成
 
- オーディオファイルのブラウザ上での再生- st.audio
 
数値入力¶
- サンプリングレート、タップ長、カットオフ周波数の入力
- 
st.number_input(ラベル名, min_value, max_value, value)- valueなどの型はintからfloatで統一する必要有
 
In [29]:
%%writefile app.py
import streamlit as st
fs = st.sidebar.number_input("Sampling Frequency", min_value=1,
                             max_value=192000, value=16000)
cutoff_hz = st.sidebar.number_input("cutoff [Hz]", min_value=0.0,
                                    max_value=fs/2.0, value=100.0)
チェックボックス¶
- 
st.checkbox(ラベル名, デフォルト真偽値)- チェックが入った場合のみ実行させる動作を記述可能
 
In [30]:
%%writefile -a app.py
show_time_coeff = st.checkbox("time coefficient", value=True)
if show_time_coeff:
    pass  # do something
オーディオファイルのアップロード¶
- 
st.file_uploader(ラベル名, type)- typeは拡張子、リストも可能
- 返り値はBytesIOのサブクラス(UploadedFile)- file-like objectなのでファイルを要求する関数に渡せる
 
 
In [31]:
%%writefile -a app.py
import soundfile as sf
wav_file = st.sidebar.file_uploader("input wave file", type="wav")
if wav_file is not None:
    sig, wav_fs = sf.read(wav_file)  # file-likeなので渡せる
    st.line_chart(sig)
- ファイルブラウズかドラッグアンドドロップでアップロード可能
- Dockerコンテナ上だとエラーが発生(書き込み権限の問題?)
許容アップロードサイズを変えたい場合¶
- APIリファレンスより引用- You can configure this using the server.maxUploadSize config option.
 
オーディオファイルのブラウザ上での再生¶
- 
st.audio(data)- dataはファイル名、BytesIO、ndarrayなど
- rawデータやndarrayの場合はファイルヘッダーが必要
 
In [32]:
%%writefile -a app.py
if wav_file is not None:
    st.audio(wav_file)
chromeでの見た目
オーディオファイルの一括アップロード¶
- 
st.file_uploader(ラベル名, type, encoding=None, accept_multiple_files=True)- 最新版の0.68.0で導入
- 返り値がリストになるため音声データのバッチ処理が可能
- リロード時にうまく読み込めないバグがある?
 
- 最新版の
In [33]:
%%writefile -a app.py
wav_files = st.sidebar.file_uploader("input wave files", encoding=None,
                                     type="wav", accept_multiple_files=True)
if wav_files is not None:
    for wav_fname in wav_files:
        sig, wav_fs = sf.read(wav_fname)
        st.line_chart(sig)
- 
encoding=Noneは付けるとdeprecated errorが出るが抑制可能- st.set_option('deprecation.showfileUploaderEncoding', False)
 
オーディオファイルのダウンロード¶
- 公式コンポーネントはまだない
- 
workaroundが存在- バイナリファイルをbase64エンコード
- aタグを生成しst.markdownで表示
 
- ダウンロードリンクを作成するための関数
In [34]:
%%writefile -a app.py
import os
import base64
def get_binary_file_downloader_html(bin_file, file_label='File', extension=""):
    with open(bin_file, 'rb') as f:
        data = f.read()
    bin_str = base64.b64encode(data).decode()
    href = f'<a href="data:application/octet-stream;base64,{bin_str}" download="{os.path.basename(bin_file)}{extension}">Download {file_label}</a>'
    return href
- 入力ファイルを0.5倍した波形を一時ファイルに保存の上でダウンロードリンクを作成
In [35]:
%%writefile -a app.py
import tempfile
if wav_file is not None:
    sig, wav_fs = sf.read(wav_file)
    hsig = 0.5 * sig
    
    # 名前付き一時ファイルに保存
    fp = tempfile.NamedTemporaryFile()
    sf.write(fp.name, hsig, wav_fs, format="wav")
    st.audio(fp.name)
    
    # ダウンロードリンクを表示
    href = get_binary_file_downloader_html(fp.name, "filtered wave file", ".wav")
    st.markdown(href, unsafe_allow_html=True)
- 最終的なファイル- aタグにclassが自動挿入されているため注意
 
In [36]:
!cat app.py | pygmentize
フィルタデザイナーのまとめ¶
- 数値入力とチェックボックスコンポーネントを紹介
- オーディオファイルのアップロード、再生、ダウンロード方法
pyroomacousticsを用いたシミュレータ¶
- 
pyroomacousticsのroomオブジェクト、音源、マイクをGUIで配置
- RIR生成または伝搬シミュレーションを実行- ※Streamlitのバージョン0.68.0ではシミュレーションでエラーが発生
 
- ※
現状のダッシュボード¶
- 
room_designer.py- 
Shoeboxのみ対応
 
- 
紹介する内容¶
- スライダー- st.slider
 
- ボタン- st.button
 
スライダー¶
- 
st.slider(ラベル名, min_value, max_value, value)- valueの類は省略可能
 
rx = st.sidebar.slider("x", min_value=0.0, max_value=100.0)
アプリケーションのタブ化¶
- 現在タブ用コンポーネントはない
- 
st.radioで代用可能- ただしダッシュボードを切り替えると状態がリセットされる
 
ret = st.sidebar.radio("app", ["window viewer", "filter designer", "room designer"])
print(ret)
if ret == "window viewer":
    window_viewer.main()
elif ret == "filter designer":
    filter_designer.main()
elif ret == "room designer":
    room_designer.main()
espnet2のフロントエンド¶
- 
espnet2- pythonのみで処理可能
- pre-trainedモデルをダウンロードする仕組みが存在
- https://github.com/espnet/espnet_model_zoo
 
- ダッシュボード- espnet2_loader.py
- asrのみ対応
 
音声ファイルをアップロードすると波形を描画¶
デコードボタンクリックで音声認識を実行¶
モデルの情報を表示¶
Herokuへのデプロイ¶
- 実行環境として手軽に利用可能- git push heroku local_branch:main
 
- 実際に稼働中- https://streamlit-dsp.herokuapp.com
- free-dynoなので遅い
 
必要となるファイル¶
- Procfile
- Aptfile
- requirements.txt
- runtime.txt
Procfile¶
- 実行時のコマンドを記述
- 内容: web: streamlit run --server.enableCORS false --server.port $PORT streamlit_dsp/app.py
runtime.txt¶
- ランタイムバージョンを指定する場合に必要
- 使用可能なランタイム
- 内容: Python 3.8.6
Aptfile¶
- 
apt installする対象を記述- 
soundfileライブラリがlibsndfileに依存
 
- 
- buildpackが必要- heroku buildpacks:add heroku-community/apt
 
- 内容: libsndfile-dev
requirements.txt¶
- 依存ライブラリを記述
- HerokuはPoetryに対応していない- 
python-poetry-buildpack- 
pyproject.tomlからpush時に動的にrequirements.txtを生成
- しかしエラー時に対処できないのでローカルで予め生成しておく
 
- 
- poetry export -f requirements.txt --output requirements.txt
 
- 
python-poetry-buildpack
最終的な実行コマンド¶
- 必要なファイルが揃った上で下記を実行
$ heroku buildpacks:clear
$ heroku buildpacks:add heroku-community/apt
$ heroku buildpacks:add heroku/python
$ heroku create
$ git push heroku heroku:main
$ heroku open
その他の話題¶
ASTの書き換えによるコード生成¶
- ほぼスクリプトライクに書けるため代入の上書きにより通常のスクリプトとして実行できるようにすることも可能
- 標準ライブラリastでコードを書き換えastorライブラリでコードを生成
- そのまま実行できるものが出来上がるわけではないが取っ掛かりに便利
まだ足りないと感じる要素(1)¶
- タブ
- デフォルトの可視化コンポーネント- 軸ラベルぐらい指定したい
 
- 複雑なレイアウト
まだ足りないと感じる要素(2)¶
- 編集可能なテーブル- フィルタ係数を入力して可視化することなどが困難
 
- ユーザー認証および状態の永続化- 
SessionState.pyを使う例
- https://discuss.streamlit.io/t/alternative-implementation-of-session-state/799
 
- 
また足りないと感じる要素(3)¶
- 標準出力のキャプチャ- 処理進捗を標準出力に出すコマンドで困る
- 
https://github.com/streamlit/streamlit/issues/268- 
contextlibでredirectは可能だが、非同期実行ができない
 
- 
 
- 仕様の安定性- 複数ファイルアップローダにおいて再読み込み時にエラーが発生
 
コンポーネントの自作¶
- 必要なコンポーネントは自作することが可能とのこと
- https://docs.streamlit.io/en/stable/publish_streamlit_components.html
まとめ¶
- 自作のダッシュボードを通じてStreamlitについて紹介
- デプロイ方法およびその他の話題を紹介
コメント
Comments powered by Disqus