Debian 10 busterを初代Mac mini (A1176; 2006)にインストール

経緯

前回の記事を書いた後にUbuntu 18.04がインストールされていたMacBook Pro 2009のトラックパッドがバッテリー膨張による圧力によって割れるという惨事が発生してしまった。 このMacBook Proは以前に一度ファンとバッテリーを自力で換装したことがあるものであり、もう限界と判断して押入れに眠っていたMac mini 2006(大学の先輩から2万円で譲ってもらったもの)を復活させることを決意したのであった。

白昼夢の惨劇

ディストリビューションの選択

当初はarch linuxを入れようかと思い、x86_64のISOイメージをUSBに焼いてインストールしようとしたが、このタイミングでこのマシンのCPUがx86_64ではなくi686、つまり32bit CPUであるということが発覚した。 2020年にもなり32bit CPUのマシンを稼働させることに躊躇いもあったが、調べたところUbuntuが32bit対応の廃止を表明している一方でDebianは32bit対応のイメージを今でも配布しているということもありインストールすることを決めた。

マシンのスペック

ここに載っている1.66GHzのCPUを積んでいるものである。

  • CPU
    • 1.66GHz Core Duo
  • メモリ
    • 2GB(DDR2; 667MHz)
  • ディスク容量
    • 60GB

ちなみにこの記事によればファームウェアをアップデートした場合CPUの換装や4GBまでのメモリの増設が可能になるらしい。

想定使用用途

  • ファイルサーバ
    • 外付けHDDにもともと保存していたデータに対してsambaでアクセスしたいため
  • VPNサーバ
    • 移行元マシンで稼働していたSoftEtherの設定を引き継ぐ

実際の作業

全般的に自分用のメモ書きである。

DebianのISOをUSBに焼く

ここではdebian-10.6.0-i386-netinst.isoを公式から落としてきてUSBメモリに書き込んだ。 この作業は今回使用するMac miniではない別のマシンで行っている。

$ df -hか何かでUSBメモリのデバイスのパスを確認する(ここでは`/dev/disk4`)
$ diskutil umountDisk /dev/disk4
$ sudo dd if=debian-10.6.0-i386-netinst.iso of=/dev/rdisk4  # /dev/rdisk4, とrを付けると早く書き込まれるらしい

Mac mini上での作業

rEFIndのインストール

mac miniのosx側で予めrEFIndをインストールしておく。 rEFIndに同梱のインストールスクリプトを実行した上で再起動すると起動時にrEFIndが立ち上がるはずである。 ちなみにmac miniに入っていたosxのバージョンはLepardであった。

mac mini側のディスクユーティリティでosxのパーティションサイズを縮小しておく

HFS+フォーマットのosxがインストールされた領域のサイズを20GB程度に縮小した。 残りの領域についてはこの時点でフォーマットなどは実施しない(GUIインストーラの実行時にやってくれるため)。

GUIインストーラによるDebianのインストール

rEFIndが使えるようになるとUSBメモリに焼いたbootableイメージからインストーラを起動することが可能となる。 GUIインストーラで提示された項目を選んでいくだけで基本的には問題なくインストールは完了した。 パーティションは予めosx側で分割した残りの空き領域に対しインストーラが自動的にroot, var, tmp, swap, homeを分割してくれたのでその通りにした。LVMは使っていない。ファイルシステムはext4になっている。 デスクトップ環境はメモリが2GBしかなく必要かどうか悩んだがxfce4とした。 SSHサーバもインストーラの選択肢で選べたためインストールしておく。 よって基本的にこれ以降の作業はSSH越しの作業である。


ルーターの固定IP割当設定

以降でローカルLAN内でIPアドレスを固定化するためにルーターの割当設定を行っておく。 Debian上でip addrでMACアドレスを表示させ、有線LANのNICとWiFiアダプタに対して固定IP割当を設定。


Debian起動直後における設定

sudoグループへの所属

インストール時に登録したユーザーがデフォルトではsudoを使えないため、一度suでrootユーザになった上でsudoグループに所属させる。 $は一般ユーザ、#はrootユーザでの実行を意味する。

$ su
# gpasswd -a ${username} sudo
# exit
SSHの設定

鍵認証による接続を行うために公開鍵の登録をした後に/etc/ssh/sshd_configを編集する。 公開鍵はクライアント側からssh-copy-id -i id_rsa.pub ${ipaddr}で転送しても良いが、GitHubに登録してある公開鍵を使う場合はwget https://github.com/${username}.keys -qO - >> ~/.ssh/authorized_keysauthorized_keysに追記しても良い。

次にsudo vi /etc/ssh/sshd_configに下記項目を設定する。

Port ポート番号  # 変更したい場合
PubkeyAuthentication yes
PermitRootLogin no
PermitEmptyPasswords no
PasswordAuthentication no

設定後、sudo systemctl restart sshdでsshdを再起動する。

このタイミングで他のターミナルからsshしてパスワードログインができなくなっていることを確認する。 (元々接続していたターミナルでexitしてしまうと再度接続できなくなってしまったときに困る)。

その後に、クライアント側で~/.ssh/configを下記のように編集しておくとssh hostnameで接続できるようになるため便利である。

Host hostname
    HostName ${ipaddr}
    Port ポート番号
    preferredauthentications publickey
    IdentityFile "/path/to/id_rsa"
ホームディレクトリ配下の日本語ディレクトリの英語化

セットアップ時に日本語を選択するとホームディレクトリ配下の各種ディレクトリが日本語になってしまっている場合がある。 その場合はLC_ALL=C xdg-user-dirs-gtk-update --forceを実行して英語ディレクトリを作成する。 日本語ディレクトリはそのまま残ってしまうため不要であれば消す。

aptで色々入れる

とりあえず最低限のパッケージと前回の記事で書いたように最新版のsambaを使うためのppaを追加する。 最初にインストールするsoftware-properties-commonはdebianでadd-apt-repositoryを使うために必要である。

$ sudo apt update
$ sudo apt upgrade
$ sudo apt install -y software-properties-common
$ sudo add-apt-repository ppa:linux-schools/samba-latest
$ sudo apt dist-upgrade
$ sudo apt install -y git vim zsh tmux curl htop
$ sudo apt install -y samba
$ sudo apt install -y build-essential autotools-dev libtool automake autoconf autogen

しかしリポジトリにppaを追加した後にupdateを掛けたところ無効である旨が表示されたためDebianでは使用不可能であった。


シェル環境周りの設定

tmuxの設定

.tmux.confを作成しC-tをprefixキーとするために下記のような.tmux.conf~以下に配置した。

unbind C-b
set-option -g prefix C-t
set-window-option -g mode-keys vi
bind C-t send-prefix
シェルをzshに変える
$ chsh  # /bin/zshを指定

再ログインすると色々zshの設定に関する対話スクリプトが走るが、blankファイルだけ生成して脱出した。 その上で下記のような設定を.zshrcに記述した。 内容自体は他マシンにおける設定の使い回しであるため各自好きに変更すると良いと思う。

setopt autopushd
setopt auto_cd

WORDCHARS='*?_-.[]~=&;!#$%^(){}<>'

compctl -M 'm:{a-z}={A-Z}'

setopt IGNOREEOF

umask 002

HISTFILE=$HOME/.zsh-history
HISTSIZE=10000
SAVEHIST=10000

setopt hist_ignore_dups # ignore duplication command history
setopt extended_history
setopt share_history
function history-all { history -E 1 }

# bind
bindkey -e
bindkey '^P' history-beginning-search-backward
bindkey '^N' history-beginning-search-forward

setopt no_check_jobs
setopt no_hup

setopt print_eight_bit

export LSCOLORS=gxfxcxdxbxegedadagacad
zstyle ':completion:*:default' list-colors $LSCOLORS

alias ls="ls -vG"
alias la='ls -aF'
alias ll='ls -h -laF'

alias mv='mv -v'
alias cp='cp -v'
alias rm='rm -vi'
alias grep='grep --color'

alias du='du -h'
alias df='df -h'

autoload -U compinit
compinit -u

autoload colors
colors

# VCS settings
# http://liosk.blog103.fc2.com/blog-entry-209.html
autoload -Uz vcs_info
precmd() {
    psvar=()
    LANG=en_US.UTF-8 vcs_info
    psvar[1]=$vcs_info_msg_0_
}

PROMPT="%{${fg[green]}%}[%n@%m %*] %{${fg[magenta]}%}%(!.#.>) %{${reset_color}%}"
PROMPT2="%{${fg[cyan]}%}%_> %{${reset_color}%}"
SPROMPT="%{${fg[red]}%}correct: %R -> %r %{${reset_color}%}"
RPROMPT="%{${fg[green]}%}[%{${fg[red]}%}%~%{${fg[white]}%}%1v%{${fg[green]}%}]%{${reset_color}%}"

ちなみに最近はプロンプトにstarshipを使っているが、このマシンでインストールしようとしても失敗したので独自に設定している。


ファイルサーバ(samba)関連の設定

fstabの設定

外付けHDDのUUIDを調べてシステム起動時に自動的にマウントされるように設定する。

$ sudo blkid -o list  # UUIDを調べる
$ sudo mkdir /mnt/hdd /mnt/hdd2  # マウントポイントを作成
$ sudo vim /etc/fstab

fstabには下記のような感じで書く。${uuidN}の部分は上記で調べたものに置き換えること。

UUID="${uuid1}" /mnt/hdd hfsplus nofail,defaults,force 0 0
UUID="${uuid2}" /mnt/hdd2 ext4 nofail,defaults 0 0

記述後、再起動して無事にマウントされているかを確認する。 nofailの記述は仮にマウントできなかった場合であってもエラーとして扱わないための指定である。 (実際、hfsplusフォーマットのディスクのマウントはsudo apt install hfsprogsが必要であったためこの記述を付けていない場合にemergency modeに突入してしまった。)

UUIDは外付けHDDに固有であるためか以前と変わっていなかったため、引き継ぎ元のマシンのfstabの追記内容をそのまま使い回すことができた。

/etc/smb.confの設定およびavahi-daemonの設定をする

詳細は割愛するが前回の記事を参考のこと。 ここもsmb.conf内のinterface名を変えたぐらいで基本的に元の設定ファイルをそのまま使い回すことができた。

sambaアクセス用のユーザーを追加する

guest okにしていてもTimeMachineバックアップの時には書き込み権限を持ったユーザーが登録されている必要があったので下記で設定しておく。

sudo smbpasswd -a ${username}


VPNサーバーの設定

SoftEtherの32bit対応版をダウンロードしてビルドする

ここから条件に合うものを落としてきてmakeする。makeの際にライセンスの同意などを求められる。 ビルドした後は/usr/localに移動しておく。

$ wget https://jp.softether-download.com/files/softether/v4.34-9745-rtm-2020.04.05-tree/Linux/SoftEther_VPN_Server/32bit_-_Intel_x86/softether-vpnserver-v4.34-9745-rtm-2020.04.05-linux-x86-32bit.tar.gz
$ tar zxvf softether-vpnserver-v4.34-9745-rtm-2020.04.05-linux-x86-32bit.tar.gz
$ cd vpnserver
$ make
$ cd ..
$ mv vpnserver /usr/local/
systemdの設定

/etc/systemd/system/vpnserver.serviceとして下記ファイルを作成する。

[Unit]
Description=SoftEther VPN Server
After=network.target network-online.target

[Service]
ExecStart=/usr/local/vpnserver/vpnserver start
ExecStop=/usr/local/vpnserver/vpnserver stop
Type=forking
RestartSec=3s

[Install]
WantedBy=multi-user.target
bridgeとtapデバイスの作成

下記のようなスクリプトを作成しsudoで実行する。

#!/bin/bash
# vim:fileencoding=utf-8

ip_addr="192.168.xxx.xxx/xx"
gateway_addr="192.168.xxx.xxx"
interface="enp1s0"

bridge_name="br0"
tap_device_name="tap_softether"

echo "create bridge interface"
/usr/bin/nmcli connection add type bridge ifname ${bridge_name}
echo "disable STP"
/usr/bin/nmcli connection modify bridge-${bridge_name} bridge.stp no
echo "modify bridge"
/usr/bin/nmcli connection modify bridge-${bridge_name} ipv4.method manual ipv4.addresses ${ip_addr} ipv4.gateway ${gateway_addr}
echo "connect ${interface} to bridge"
/usr/bin/nmcli connection add type bridge-slave ifname ${interface} master bridge-${bridge_name}
echo "connect tap to bridge"
/usr/bin/nmcli connection add type bridge-slave ifname ${tap_device_name} master bridge-${bridge_name}
echo "up tap device"
/usr/bin/nmcli connection up bridge-${bridge_name}

上記を実行すると下記のような出力が出る。

create bridge interface
接続 'bridge-br0' (0aa0ae96-6428-4f7e-bc1c-fa6f22f98cea) が正常に追加されました。
disable STP
modify bridge
connect enp1s0 to bridge
接続 'bridge-slave-enp1s0' (ec87d050-f6ae-4ca8-adbf-7b1f0e4eb1b6) が正常に追加されました。
connect tap to bridge
接続 'bridge-slave-tap_softether' (f262c26c-2799-4c9b-b149-9d1b3c44f8ce) が正常に追加されました。
up tap device
接続が正常にアクティベートされました (master waiting for slaves) (D-Bus アクティブパス: /org/freedesktop/NetworkManager/ActiveConnection/3)

この後に再起動を行い、ip addrでIPアドレスが割り振られていたり、ip link showでbr0のSTATEがUpになっていれば問題ないと思われる。

なおsudo apt install bridge-utilsを実行すると /sbin/brctlが使えるようになるが、今回は直接は不要であった。 また上記スクリプトではnmcliを使っているがipコマンドでもどうやら作成できるらしい。

VPNサーバーの設定の読み込み

移行元で予めvpncmdのConfigGetを実行し保存しておく。 これは

$ /usr/local/vpnserver/vpncmd
  > * 1. VPN Server または VPN Bridgeの管理 を実行
  > * 何も指定せずにEnterを押すことでローカルのサーバーに接続
  > * 更に何も指定せずにEnterを押すことでサーバー管理モードに入る
  > * 管理パスワードを入力
  VPN Server> ConfigGet 保存先パス

と実行することで保存が可能である。この設定ファイルを引き継ぎ先のマシンで読み込ませることを考える。

まず、sudo systemctl enable vpnserverを実行し前述のsystemd用の設定によるサービスを有効化する。 続いてsudo systemctl start vpnserverでVPNサーバーを起動する。

この上で/usr/local/vpnserver/vpncmdを実行し、移行元での保存時と同様に1を指定の上で無指定でEnterを2回実行することでサーバー管理モードに入る。 ここでConfigSetを実行すると設定ファイルのパスを尋ねられるので、入力することで設定の読み込みが可能である。 ここによればこの時点で元マシンの環境は復元できるとのことである。

ルーターで通信に必要なポートを開放する

50, 51, 500, 4500番ポートの通信をホストマシンのIPアドレスに対して許可するようにルーターの設定を施した。

接続できない原因を探す

元マシンでの設定が正しければ上記までの設定で接続できるようになるはずだが案の定接続できない。 そこで/usr/local/vpnserver以下に存在するsecure_logserver_logといったログ見て原因を推察する。 ここではserver_log以下に存在するlogにおける

2020-11-21 xx:xx:xx.xxx L2TP PPP セッション [xxx.xxx.xxx.xxx:xxxx]:
  DHCP サーバーからの IP アドレスの取得に失敗しました
  PPP の通信を受諾するためには DHCP サーバーが必要です
  仮想 HUB  Ethernet セグメント上で DHCP サーバーが正しく動作しているかどうか確認してください
  DHCP サーバーを用意することができない場合は仮想 HUB  SecureNAT 機能を用いることもできます

という記述からSecureNatが有効化されていないことが分かったため、vpncmdを実行し仮想ハブの管理からSecureNatEnableを実行したところ、無事に接続されるようになった。

SecureNATの利用を使用しない設定への変更

前節の設定のままだとVPNでSecureNAT機能によりIPアドレスが割り振られるが、このままだとホストマシンにSSHアクセスできなかったため元マシンの設定を見返したところ/etc/network/interfacesを明示的に指定していたことが分かった。 よってvpncmdSecureNATDisableを実行した上で再度SecureNatを停止した上で下記のような設定を/etc/network/interfacesに追記した。

auto enp1s0
iface enp1s0 inet manual

auto br0
iface br0 inet static
  address 192.168.xxx.xxx
  netmask 255.255.xxx.xxx
  gateway 192.168.xxx.xxx
  bridge_ports enp1s0 tap_softether
  dns-nameservers 192.168.xxx.xxx

NetworkManagerは/etc/NetworkManager/NetworkManager.confにおいて下記のような設定

[ifupdown]
managed=false

となっている場合、/etc/network/interfaceによって管理されているデバイスはNetworkManagerで制御を行わないとのことである。 この上でsudo systemctl restart networkingを行うとSecureNATがdisableとなっている場合でも接続が可能となった。

しかし、この状態で再起動を行うとやはり接続できない状態に戻ってしまう。 調べたところsudo systemctl restart networkingを行った後は下記状態

$ /sbin/brctl show
bridge name     bridge id               STP enabled     interfaces
br0             8000.0016cba5df15       no              enp1s0
                                                        tap_softether

となっているが、マシンの再起動直後は

$ /sbin/brctl show
bridge name     bridge id               STP enabled     interfaces
br0             8000.0016cba5df15       no              enp1s0

という状態になっていることが分かった。 つまりbr0に対して起動直後は何らかの理由でtap_softetherのtapデバイスがbr0に接続されていないことが原因となっていることが考えられ、 実際に/sbin/brctl addif br0 tap_softetherを実行することで明示的に追加を行った場合はVPN接続が可能となった。

これを受けて/etc/systemd/system/vpnserver.serviceに下記のようにExecStartPostを追加した。

[Service]
ExecStart=/usr/local/vpnserver/vpnserver start
ExecStartPost=/bin/sleep 10 ; /sbin/brctl addif br0 tap_softether

sleepの秒数の後のセミコロンは10とセミコロンの間にスペースを空けないとエラーが生じるため注意が必要である。 これによりマシン再起動後も手動での操作なしにVPN接続が可能な状態となった。


まとめ

古いMac miniにDebianを入れてファイルサーバ・VPNサーバとして復活させる手順のメモを記した。 稼働させたばかりであり、そもそも古いマシンであるため、いつまで持つか不明であるがこのまましばらく運用してみようと思う。

Ubuntu 18.04のSambaサーバー上にTimeMachineのバックアップ環境を構築

普段使用しているMacBook Proのバタフライキーボードにおいて特定のキーが繰り返し入力されたりバッテリーが膨張しつつある問題が発生したため修理に出す前にTimeMachineを用いてバックアップを取ろうと思ったが、調べたところ最近のSambaでは特定の条件が揃えばネットワーク越しにTimeMachineを用いてバックアップを取ることが可能であることが分かったため、その際に設定した環境構築のメモを下記に記す。

手順

  1. ppaを追加し新しめのバージョンのsambaを入れる
  2. /etc/samba/smbd.confを編集する
  3. avahi-daemonの設定をする
  4. sambaのdaemonをrestartする

1.ppaを追加し新しめのバージョンのsambaを入れる

2020/10/31現在、aptでインストールされるsambaはバージョン4.7.6であるが、 バージョン4.8以降でなければTimeMachineバックアップ用の設定ができないのでリポジトリを追加した上で新しいバージョンのsambaをインストールする。 このQAのAnswerを参考に下記のようにppaを追加する。

$ sudo add-apt-repository ppa:linux-schools/samba-latest
$ sudo apt-get dist-upgrade
$ sudo apt-get install samba

バージョンは下記のように確認可能である。

$ smbd -V
> Version 4.10.18

2. /etc/samba/smbd.confを編集する

様々なサイトを参考に下記のような項目を追加した(が不要な設定もあるかもしれない)。 pathは適宜共有したいディレクトリに書き換えること。またfruit:time machine max sizeの項目は適切なサイズを設定しておかないとTimeMachineはディスクをフルに使おうとしてしまうので注意が必要である。 正確な設定項目についてはマニュアルを見たほうが良い。

[global]

# Special configuration for Apple's Time Machine
vfs objects = catia fruit streams_xattr 
fruit:model = MacPro
fruit:advertise_fullsync = true
fruit:aapl = yes

[TimeMachine]
  comment = Backup for Mac Computers
  path = /path/to/share
  writable = yes
  browsable = yes
  guest ok = yes
  fruit:time machine = yes
  fruit:time machine max size = 600G
  durable handles = yes
  kernel oplocks = no
  kernel share modes = no
  posix locking = no

3. avahi-daemonの設定をする

上記設定を施したのちにsambaのデーモンを再起動した時点で使えるかと思ったがどうやら別途avahi-daemon(mDNS)の設定をしなければならなかったことが分かった。 この記事を参考に下記のような設定を記述したファイルを/etc/avahi/services/timemachine.serviceとして作成した。 なおここではavahi-daemon自体はaptで別途インストール済みであることを想定している。

<?xml version="1.0" standalone='no'?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
 <name replace-wildcards="yes">%h</name>
 <service>
   <type>_smb._tcp</type>
   <port>445</port>
 </service>
 <service>
   <type>_adisk._tcp</type>
   <txt-record>dk0=adVN=TimeMachine,adVF=0x82</txt-record>
 </service>
</service-group>

ここで<txt-record>dk0=adVN=TimeMachine,adVF=0x82</txt-record>TimeMachineとなっている部分は/etc/samba/smb.confの共有名と共通にしておく必要がある。

4. sambaのdaemonをrestartしMacOSからTimeMachineを実行

3までを実施した後で、sudo systemctl restart avahi-daemonおよびsudo systemctl restart smbdを実行しavahi-daemonとsmbdをrestartさせると MacOS(手元のマシンはCatalina 10.15.7)のシステム環境設定にあるTimeMachineのディスクを選択の項目から設定したディスクが探せるようになっているはずである。 保存先として設定するとIDとパスワードを入力するダイアログが出るため書き込み権限のあるユーザーIDを打ち込むとバックアップが進行するようになった。

参考にさせていただいたサイト

  • https://askubuntu.com/questions/1166875/unable-to-built-samba-4-10-6-from-source
    • ppaからのsambaをインストールする際の参考にしました
  • https://ideal-reality.com/computer/server/time-machine-samba/
    • 設定の参考にしました
  • https://qiita.com/upsilon/items/c368726845f89cb0ffe9
    • avahiの設定を参考にしました

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へのデプロイ方法の紹介
  • その他

実行環境

Streamlitとは

  • Webブラウザで動作するダッシュボードを作成するためのライブラリ
    • 複数のコンポーネントが連携して動作するもの
  • Pythonのみで記述が可能
    • plotly/dashと比較されることが多い

どんな場面で必要か

  • 実装したアルゴリズムを他者に試してもらいたい
    • Webフロントエンドがあると便利
  • パラメータ変更による試行錯誤が必要な場面
    • 調整ツールとして使用

実例

  • 今回作成したダッシュボードを簡単に紹介

Dashとの違い

  • Towards Data Scienceの記事
    • Dash
      • プロダクション/エンタープライズ環境での実行に主眼
    • Streamlit
      • ラピッドプロトタイピングに主眼

image.png

Streamlitの魅力

  • 手軽にフロントエンドが構築可能
    • javascript不要、スクリプトライクな記述
    • 各種pythonライブラリにGUIを付与
  • 豊富な描画ライブラリ
    • matplotlib, plotly, altair, ...
    • Dashはplotlyを前提

本発表におけるバージョン

基本機能の紹介

導入方法

  • pip install streamlit

実行方法

In [2]:
%%writefile app.py
import streamlit as st

st.write("# Hello, Streamlit!")
Overwriting app.py
In [3]:
!streamlit run app.py
  You can now view your Streamlit app in your browser.

  Network URL: http://172.17.0.2:8501
  External URL: http://49.251.189.62:8501

^C
  Stopping...

image.png

  • ノートブックが固まるため以降はターミナル上からstreamlitコマンドを実行

jupyterlabが8888番ポートのみ稼働している環境上でのアクセス

  • jupyter-server-proxyの導入によるproxyアクセス

アプリ更新時の反映が容易

  • ファイルの上書きを検知しRerunボタンを自動表示

image.png

In [4]:
%%writefile app.py
import streamlit as st

st.write("# Hello, Streamlit!")
st.write("# Where is Rerun button?")
Overwriting app.py

image.png

スクリプトエラーもブラウザ上に表示

  • デバッグも容易
In [5]:
%%writefile app.py
import streamlit as st

st.write("# Hello, Streamlit!")
raise ValueError
Overwriting app.py

image.png

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"]))
Overwriting app.py

image.png

本日紹介するダッシュボード

ブランチ

  • 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によるアプリ構築をライブコーディング的に実施

image.png

窓関数の選択インタフェース

  • サイドバーにselectboxで表示
  • サイドバー上への配置
    • st.sidebar.xxx
    • st.xxxと本体に配置できるウィジェットは何でも配置可能

st.selectbox

st.selectbox(ラベル名, 選択肢のリスト, デフォルト項目のインデックス)

image.png

In [7]:
%%writefile app.py
import streamlit as st

windows = ["boxcar", "triang", "blackman", "hamming", "hann", "bartlett", "flattop",
           "parzen", "bohman", "blackmanharris", "nuttall", "barthann"]
Overwriting app.py
  • %%writefile -a app.pyapp.pyに追記
In [8]:
%%writefile -a app.py

win_name = st.sidebar.selectbox("window", windows, 4)
st.write(win_name)
Appending to app.py
  • この時点の見た目

image.png

窓関数の窓長および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)
Appending to app.py
  • この時点での見た目

image.png

窓関数の取得および描画

  • 窓関数の取得
    • 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
Appending to app.py
  • 窓関数を取得し周波数特性を計算
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)
Appending to app.py
  • matplotlibを用いて描画
In [12]:
%%writefile -a app.py
fig, axes = plt.subplots(2, 1)
axes[0].plot(win)
axes[1].plot(W)
st.pyplot(fig)
Appending to app.py

st.pyplot

  • matplotlibのfigureを描画
    • st.pyplot(fig)
  • 使い慣れたmatplotlibを使える
    • が、javascriptの恩恵が得られない
  • この時点での見た目

image.png

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)
Appending to app.py
  • st.line_chartの見た目

image.png

この時点でのファイルの内容

  • 通常の実行スクリプトに近い
  • 手軽にフロントエンドの作成が可能
In [14]:
!cat app.py | pygmentize
import streamlit as st

windows = ["boxcar", "triang", "blackman", "hamming", "hann", "bartlett", "flattop",
           "parzen", "bohman", "blackmanharris", "nuttall", "barthann"]

win_name = st.sidebar.selectbox("window", windows, 4)
st.write(win_name)
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)
import matplotlib.pyplot as plt
import numpy as np
import scipy.signal as sg
import scipy.fft as fft
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)
fig, axes = plt.subplots(2, 1)
axes[0].plot(win)
axes[1].plot(W)
st.pyplot(fig)
st.line_chart(win)
st.line_chart(W)

複数の窓関数を同時に描画したい

  • st.multiselect(ラベル名, 選択肢のリスト, デフォルト項目の要素のリスト)
    • 複数の選択項目をリストで返す
    • 第三引数は選択肢中に存在する要素のリスト

image.png

  • 先程と同じ
In [15]:
%%writefile app.py
import streamlit as st

windows = ["boxcar", "triang", "blackman", "hamming", "hann", "bartlett", "flattop",
           "parzen", "bohman", "blackmanharris", "nuttall", "barthann"]
Overwriting app.py
  • 先程と同じ
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
Appending to app.py
  • st.multiselectを使用
In [17]:
%%writefile -a app.py

# ここから先が異なる
win_names = st.sidebar.multiselect("window", windows, [windows[4]])
st.write(win_names)
Appending to app.py
  • win_namesst.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)
Appending to app.py
  • 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)
Appending to app.py
In [20]:
%%writefile -a app.py
    # ループの続き
    st.write(win_name)
    st.line_chart(win)
    st.line_chart(W)
Appending to app.py
  • 複数の窓が繰り返し描画される

image.png

In [21]:
!cat app.py | pygmentize
import streamlit as st

windows = ["boxcar", "triang", "blackman", "hamming", "hann", "bartlett", "flattop",
           "parzen", "bohman", "blackmanharris", "nuttall", "barthann"]

import matplotlib.pyplot as plt
import numpy as np
import scipy.signal as sg
import scipy.fft as fft

# ここから先が異なる
win_names = st.sidebar.multiselect("window", windows, [windows[4]])
st.write(win_names)
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)
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)
    # ループの続き
    st.write(win_name)
    st.line_chart(win)
    st.line_chart(W)

複数の窓をまとめて描画したい

  • 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"]
Overwriting app.py
  • 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  # 追加
Appending to app.py
  • 先程と同じ
In [24]:
%%writefile -a app.py

win_names = st.sidebar.multiselect("window", windows, [windows[4]])
st.write(win_names)
Appending to app.py
  • 先程と同じ
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)
Appending to app.py
  • 窓と周波数特性を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)
Appending to app.py
  • 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)
Appending to app.py
In [28]:
%%writefile -a app.py
st.line_chart(df_win)
st.line_chart(df_W)
Appending to app.py

image.png

In [28]:
!cat app.py | pygmentize
import streamlit as st

windows = ["boxcar", "triang", "blackman", "hamming", "hann", "bartlett", "flattop",
           "parzen", "bohman", "blackmanharris", "nuttall", "barthann"]
import matplotlib.pyplot as plt
import numpy as np
import scipy.signal as sg
import scipy.fft as fft
import pandas as pd  # 追加

win_names = st.sidebar.multiselect("window", windows, [windows[4]])
st.write(win_names)
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)
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)
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)
st.line_chart(df_win)
st.line_chart(df_W)

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のデジタルフィルタ設計関数を用いたダッシュボードを作成
    • 設計したフィルタをアップロードした音声に適用しブラウザ上で視聴
    • 処理後の音声のダウンロードリンクを生成

現状のダッシュボード

image.png

紹介する項目

  • 数値入力
    • st.number_input
  • チェックボックス
    • st.checkbox
  • オーディオファイルのアップロード/ダウンロード
    • st.file_uploader
    • ダウンロードリンクは自力で作成
  • オーディオファイルのブラウザ上での再生
    • st.audio

個人的な最終目標

  • pyfda
    • PyQtで作られたmatlabのfdatoolみたいなもの
    • 非常に便利だがフィルタ長が長い時などに動作が不安定化

数値入力

  • サンプリングレート、タップ長、カットオフ周波数の入力
  • 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)
Overwriting app.py

image.png

チェックボックス

  • st.checkbox(ラベル名, デフォルト真偽値)
    • チェックが入った場合のみ実行させる動作を記述可能
In [30]:
%%writefile -a app.py

show_time_coeff = st.checkbox("time coefficient", value=True)
if show_time_coeff:
    pass  # do something
Appending to app.py

image.png

オーディオファイルのアップロード

  • 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)
Appending to app.py
  • ファイルブラウズかドラッグアンドドロップでアップロード可能

image.png

  • 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)
Appending to app.py

chromeでの見た目

image.png

オーディオファイルの一括アップロード

  • 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)
Appending to app.py
  • encoding=Noneは付けるとdeprecated errorが出るが抑制可能
    • st.set_option('deprecation.showfileUploaderEncoding', False)

image.png

オーディオファイルのダウンロード

  • 公式コンポーネントはまだない
  • 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
Appending to app.py
  • 入力ファイルを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)
Appending to app.py

image.png

  • 最終的なファイル
    • aタグにclassが自動挿入されているため注意
In [36]:
!cat app.py | pygmentize
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)

show_time_coeff = st.checkbox("time coefficient", value=True)
if show_time_coeff:
    pass  # do something

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)

if wav_file is not None:
    st.audio(wav_file)

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)

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

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)

フィルタデザイナーのまとめ

  • 数値入力とチェックボックスコンポーネントを紹介
  • オーディオファイルのアップロード、再生、ダウンロード方法

pyroomacousticsを用いたシミュレータ

  • pyroomacousticsのroomオブジェクト、音源、マイクをGUIで配置
  • RIR生成または伝搬シミュレーションを実行
    • Streamlitのバージョン0.68.0ではシミュレーションでエラーが発生

現状のダッシュボード

image.png

紹介する内容

  • スライダー
    • 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)

image.png

ボタン

  • st.button(ラベル名)
ret = st.button("Simulate")
if ret:
    pass

image.png


アプリケーションのタブ化

  • 現在タブ用コンポーネントはない
  • st.radioで代用可能
    • ただしダッシュボードを切り替えると状態がリセットされる

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()

image.png


espnet2のフロントエンド

モデルのダウンロード

  • table.csv記載のnameを打ち込むとモデルをダウンロード
    • 問題点:標準出力がキャプチャできないためダウンロード進捗が不明

image.png

音声ファイルをアップロードすると波形を描画

image.png

デコードボタンクリックで音声認識を実行

image.png

モデルの情報を表示

image.png


Herokuへのデプロイ

必要となるファイル

  • Procfile
  • Aptfile
  • requirements.txt
  • runtime.txt

Procfile

  • 実行時のコマンドを記述
  • 内容: web: streamlit run --server.enableCORS false --server.port $PORT streamlit_dsp/app.py

runtime.txt

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

最終的な実行コマンド

  • 必要なファイルが揃った上で下記を実行
$ 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ライブラリでコードを生成
  • そのまま実行できるものが出来上がるわけではないが取っ掛かりに便利

st.cacheによるキャッシュ機構

  • 時間のかかる計算や大きなモデルのダウンロードに対してキャッシュ動作を実現
  • ドキュメント
    • キャッシュ機能を付与したい関数に@st.cacheというデコレータを付与
    • 引数、返り値、本体内部の変化を監視

まだ足りないと感じる要素(1)

まだ足りないと感じる要素(2)

また足りないと感じる要素(3)

  • 標準出力のキャプチャ
  • 仕様の安定性
    • 複数ファイルアップローダにおいて再読み込み時にエラーが発生

コンポーネントの自作


まとめ

  • 自作のダッシュボードを通じてStreamlitについて紹介
  • デプロイ方法およびその他の話題を紹介