JavaScript の Jupyter Notebook を Google Cloud Shell の Docker で 動かす

いつも通り、Google Cloud Shell での作業。 JavaScript で Firestore を使うのにあれこれ書いて試したかった。


Google Cloud Shell で Jupyter Notebook を Dockerコンテナとして動かす

Jupyter プロジェクト公式のイメージが Docker Hub に置いてある。

jupyter-docker-stacks.readthedocs.io

今回は、jupyter/base-notebook を使う。 ここの Dockerイメージは、jovyan1というユーザーで Jupyter が起動するようにセットアップしてある。

docker run -p 8080:8888 jupyter/base-notebook

Google Cloud Shell のプレビュー機能でアクセスできるけど、新しいノートブック作ったりできない。リクエストのホスト名チェックではじかれてしまう。

こんなエラーメッセージ Blocking Cross Origin API request for /api/contents.

これは Jupyter の起動オプション2で待ち受けホスト名を指定すればいい。あとついでに、token 認証も無効にする。Google Cloud Shell のプレビューだと認証済みのリクエストしか通ってこないから Jupyter の認証を無効にしてもよい。起動オプションを指定するときは、起動スクリプトも指定する必要がある。

docker run 
    -p 8080:8888 
    jupyter/base-notebook 
    start-notebook.sh --NotebookApp.allow_origin="*" --NotebookApp.token=""

jupyter/base-notebook からカスタムイメージを作る

同じことは Dockerfile でもできるが、今回はコンテナにアタッチして手動で設定した。

IJavascript をイントールする

IJavascript のコードリポジトリにある Dockerfile3 を参考にした。ついでに firebase の Web SDK も追加しておく。

export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/conda/bin"
npm install -g ijavascript firebase
ijsinstall

実際コードで require するときには node のグローバルモジュールのパスも追加する必要がある。それは後で Dockerイメージに ENV で書き込む。モジュールがインストールされるパスは npm root -g で表示できる。

Jupyter Notebook を設定する

Notebook をアップデートする

いま(2019/11/21)の latest イメージだと、Notebook の keyboard shortcut エディターが使えないなどの不具合がある。

サーバー側ではこんなエラーメッセージ 404 GET /static/components/react/react-dom.production.min.js

notebook のバージョンを最新では解決されてる。コンテナの中で conda コマンドを使ってアップデートする。

conda install notebook

Notebook の起動オプションを設定する

起動オプションを設定ファイルで設定する。/home/jovyan/.jupyter/juypter_notebook_config.pyに書く。

c.NotebookApp.allow_origin='*'
c.NotebookApp.token=''

Notebook のショートカットキーを設定する

Cell の編集モードで Shift-Enter を押したときに Cell が実行されるのを無効にしたい。チャットツールとかで改行が Shift-Enter だから反対の癖がつくとやっかいだから。それと、ctrl - [で編集モードから抜けるショートカットキーを追加したい。

Keyboard Shortcut Customization — Jupyter Notebook 6.0.2 documentation

/home/jovyan/.jupyter/custom/custom.jsに書く。書く内容は notebook の cell に マジックコマンド%%javascript を使うか、ブラウザの デベロッパーツールのコンソールで実行して確認できる。4

Cell の編集モードは CodeMirror でできている。Jupyter が管理するショートカットキーよりも CodeMirror の持つキー設定が勝つ。ctrl - [は、CodeMirror ではインデント解除に割り当てられているのでそれを削除する。

Jupyter.keyboard_manager.edit_shortcuts.remove_shortcut('Shift-Enter');
Jupyter.keyboard_manager.edit_shortcuts.add_shortcut("ctrl-[", "jupyter-notebook:enter-command-mode")
delete CodeMirror.keyMap.default["Ctrl-["]

編集した Dockerイメージを保存する

上でいじったコンテナをもとに Dockerイメージを作る。ついでに、環境変数と起動オプションを設定する。

docker commit 
    -c "ENV NODE_PATH /opt/conda/lib/node_modules" 
    -c 'CMD ["start-notebook.sh"]' 
    <ここにコンテナid>
    <ここに作成するイメージ名>

Docker Hub にアカウントがあってdocker loginコマンドでログインしてあれば docker pushで自分のイメージをDocker Hub に置ける。

Firestore local emulator に Firebase JavaScript SDK (Web) で接続する

admin SDK ではなくて、Webブラウザで使う firebase-js-sdk で firestore ローカルエミュレーターに接続する方法。

const db = firebase.firestore();
db.settings({
    host: "172.18.0.1:18080",
    ssl: false
});

Connect your app and start prototyping  |  Firebase

他の SDK では環境変数 FIRESTORE_EMULATOR_HOST で設定できるが、Web SDK環境変数を読まない。

Can't connect to local Firestore emulator · Issue #1721 · firebase/firebase-js-sdk · GitHub

Firestore local emulator に Docker コンテナから接続する

$ firebase setup:emulators:firestore

firebase.json

{
  "firestore": {}
  "emulators": {
    "firestore": {
      "host": "0.0.0.0",
      "port": "18080"
    }
  }
} 

"0.0.0.0"で全てのIPで待ち受けをして、リクエストのホスト名チェックも無効になる。 今回はgoogle cloud shell の中で実行する。認証なしでは外からアクセスできないから大丈夫。

Docker コンテナから Docker ホストへは設定なしでアクセス可能。ただし、コンテナから見たホストのIPアドレスが必要になる。

こんな感じでコンテナ作成時にIPアドレスを渡して置く

docker container run -e "DOCKER_HOST=$(ip -4 addr show docker0 | grep -Po 'inet \K[\d.]+')" ...

Docker Tip #65: Get Your Docker Host's IP Address from in a Container — Nick Janetakis

$ curl "$DOCKER_HOST":8080 で "OK"が返ってきたら成功。

文字起こしと発声時刻で動画に文字を表示してみた

WSL の debian で作業した。 apt install ffmpegFFmpegをインストールした。

音声ファイルを静止画の動画にする

ffmpeg
  -loop 1
  -r 30
  -i image.png
  -i audio.mp3
  -pix_fmg yuv420p
  movie.mp4

文字起こしと文節ごとの発声時刻データを作る

とりあえず、Google の Cloud Speech-to-Text を使った。他の音声認識サービスでも同じようなものだろう。Cloud Speech-to-Text では単語ごとの発声時刻も取得できる。文字を表示させるのは単語単位よりも文節単位のほうがよさそうに思った。文字起こしした文章を GiNZA を使って文節に分けた。

文字起こしと単語とごの発声時刻

gcloud beta ml speech recognize
  audio.mp3
  --language-code="ja-JP"
  --encoding="mp3"
  --sample-rate=44100
  --include-word-time-offsets

サンプルレートが分からないときはffmpeg -i audio.mp3で確認できる。

文節に分ける

GiNZA のインストールを手元でやると時間がかかるから、Google Colaboratory を使った。

pip install "https://github.com/megagonlabs/ginza/releases/download/latest/ginza-latest.tar.gz"
import spacy
nlp = spacy.load('ja_ginza')
doc = nlp('まずは心を鎮めます。目を閉じて。全身の力をすーっと抜いていきます')
for sent in doc.sents:
    for token in sent:
        print(token.i, token._.bunsetu_index, token.orth_)
    print('EOS')

token._.bunsetu_indexで文節のインデックスが取得できる。

動画に文字を書き込む

今回は手っ取り早く FFMpeg の drawtext を使った。

日本語フォントをインストールする

IPA フォントにした。

sudo apt install fonts-ipafont

フォントのパスの確認方法

fc-list

文字を書き込む位置と場所とタイミングを決める

今回は適当に手で書いた。表示するフレームは発声時刻のデータを使う。

drawtext
    =text='まずは'
    :fontfile='/usr/share/fonts/opentype/ipafont-gothic/ipagp.ttf'
    :fontsize=60
    :fontcolor='#FFF100'
    :enable='between(n, trunc(0 * 30), trunc((43/10) * 30))'
    :y=(th * 1)
,drawtext
    =text='心を'
    :fontfile='/usr/share/fonts/opentype/ipafont-gothic/ipagp.ttf'
    :fontsize=60
    :fontcolor='#FFF100'
    :enable='between(n, trunc((7/10) * 30), trunc((43/10) * 30))'
    :y=(th * 2) + 10
,drawtext
    =text='静めます'
    :fontfile='/usr/share/fonts/opentype/ipafont-gothic/ipagp.ttf'
    :fontsize=60
    :fontcolor='#FFF100'
    :enable='between(n, trunc((22/10) * 30), trunc((43/10) * 30))'
    :y=(th * 3) + 10


,drawtext
    =text='目を'
    :fontfile='/usr/share/fonts/opentype/ipafont-gothic/ipagp.ttf'
    :fontsize=60
    :fontcolor='#FFF100'
    :enable='between(n, trunc((43/10) * 30), trunc((83/10) * 30))'
    :y=(th * 1)
,drawtext
    =text='閉じて'
    :fontfile='/usr/share/fonts/opentype/ipafont-gothic/ipagp.ttf'
    :fontsize=60
    :fontcolor='#FFF100'
    :enable='between(n, trunc((49/10) * 30), trunc((83/10) * 30))'
    :y=(th * 2) + 10

......

書き込む

ffmpeg -i movie.mp4 -vf "`cat draw.txt`" result.mp4

その他

字幕を SVG アニメーションで作ったら Web の編集ツールとか工夫できそうで面白そう。

音の波形で文字の装飾とかできそう。

音声を字幕動画にするのに、たぶん一番面倒そうな発声タイミングと文字表示の同期が簡単にできることがわかった。

Google Cloud Shellのカスタム環境をGoogle Cloud Shellでcloudshell envコマンドを使って用意する

Google Cloud Shell は Cloud Registory に登録した Dockerイメージ をシェルとして利用できる。これをカスタム環境 [en]と呼ぶ。

カスタム環境の開発には Google Cloud Shell に予めインストールされているcloudshell envコマンドが便利。 cloudshell env コマンドを使えば Cloud Shell 上で Dockerイメージをビルド、実行して試した後に GCP 上で自動ビルド、コンテナレジストリへのプッシュまでできる。

自動ビルド、デプロイには、Cloud Build と Cloud Source Repositories、Container Registory が使われる。

用意するもの

課金が有効になった Google Cloud Platform のプロジェクトが必要。

以下の API を有効にしておく。

  • Cloud Source Repositories
  • Cloud Build
  • Container Registory

開発環境を作る

以下は Google Cloud Shell での作業。

# 現在のセッションにGCPプロジェクトを設定する
$ gcloud config set project $PROJECT_ID

$ cloudshell env create-custom-image $REPO_NAME

$ cd $REPO_NAME

cloudshell env コマンドを実行するには、現在のセッションが GCPプロジェクトにログインしている必要がある。gcloud コマンドで現在のセッションを GCPプロジェクトにログインする。$REPO_NAMEは作成するカスタム環境の名前になる。

env create-custom-imageコマンドによって Git リポジトリが作成される

  • Cloud Source Repositories に新規 Git リポジトリが作成される
    • Cloud Shell の ~/$REPO_NAME に クローンされて origin として設定される
    • Dockerfile と Cloud Build のビルド構成ファイルが含まれる
  • Cloud Build に自動ビルドのトリガーが2つ作成される
    • このリポジトリが更新で発火
    • Goolgeが提供する Cloud Shell のベースイメージの更新で発火

作成されたことによって最初の自動ビルドが Cloud Build で実行される。ビルドが完了したら Container Registory にプッシュされる。ビルドの詳細は作成された cloudbuild.yaml に書かれている。

create-custom-image 以外の cloudshell env コマンドはこのリポジトリディレクトリで実行する必要がある。

Cloud Shellの「環境構成.イメージの場所」を変更する

cloudshell env update-default-imageで Cloud Shellインスタンスの Dockerイメージの取得元をカスタム環境イメージに設定する。作業ディレクトリのコードから自動ビルド、プッシュされる先の Container Registory のパスは cloudshell env get-image-nameで表示できる。

Dockerfileを編集する

Dockerfile を編集したらcloudshell env build-localしてビルドできるか確認する。ビルドできたらcloudshell env runでコンテナの中に入って内容を確認する、exit でコンテナからログアウトできる。

Dockerイメージを更新する

Cloud Source Repositories のリポジトリが origin として設定済みになっているので git push origin master すれば自動ビルドが実行されて Dockerイメージが更新される。

cloudshell env pushを使えば cloudshell env build-local でビルドした Dockerイメージを直接 Container Registory にプッシュすることもできる。

料金

Container Registory が使用する Cloud Storage でイメージサイズに応じた料金がかかる。Cloud Shell のベースイメージが現時点で4.5GBなので最低でも月に13円ぐらいかかる。

Cloud Build が120分/日までは無料で使える。参考として、monoのランタイムだけを追加でインストールした場合、1回のビルドが10分以内に終了した。

monoランタイムとPaketを実行できるだけのdllをインストールしたGoogle Cloud ShellのDockerイメージを作った

Google Cloud Shell で F# スクリプトを書きたかった。Google Cloud Shellには .NET Core SDK がインストールされているのでdotnet fsiコマンドで F# の REPL が使える。

nuget パッケージを使うときに Paket があると便利だが、Paket の実行には mono が必要。mono は apt でインストールができる。

Cloud Shell のインスタンスに毎回インストールしてもいいが時間がかかるので、Cloud Shell のカスタム環境として用意することにした。

カスタム環境は Google Container Registory に置いた Docker イメージを Cloud Shell として利用する。Google によって提供される Docker イメージをベースにする必要がある。提供されるベースイメージは debian 9 (strech)。

カスタム環境用の Docker イメージをビルドする Dockerfile を書いた。 mono のパッケージリポジトリと鍵を追加して、apt install で mono-runtime と必要なライブラリをインストールする。

FROM gcr.io/cloudshell-images/cloudshell:latest

RUN apt-get update \
     && apt-get install -y --no-install-recommends gnupg dirmngr \
     && export GNUPGHOME="$(mktemp -d)" \
     && gpg --batch --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF \
     && gpg --batch --export --armor 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF > /etc/apt/trusted.gpg.d/mono.gpg.asc \
     && gpgconf --kill all \
     && rm -rf "$GNUPGHOME" \
     && APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn apt-key list | grep Xamarin \
     && apt-get purge -y --auto-remove gnupg dirmngr \
     && apt-get clean \
     && rm -rf /var/lib/apt/lists/*

RUN echo "deb http://download.mono-project.com/repo/debian stable-stretch main" > /etc/apt/sources.list.d/mono-official-stable.list \
     && apt-get update \
     && apt-get install -y \
       mono-runtime \
       libmono-system-data4.0-cil \
       libmono-system-io-compression-filesystem4.0-cil \
       libmono-system-net-http4.0-cil \
       libmono-system-runtime-serialization4.0-cil \
       libmono-system-xml-linq4.0-cil \
     && apt-get clean \
     && rm -rf /var/lib/apt/lists/* /tmp/*

Paketを実行するためのライブラリ

Paket.exe を実行するには mono-runtime だけでは足りない。mono-complete パッケージであれば十分だけど使わないライブラリもインストールされる。mono-runtime で Paket を実行してみてエラー表示から足りないdllを確認した。以下はとりあえずパッケージダウンロードを試してエラーになったものだけ。これらのライブラリがあれば nuget からライブラリのダウンロードはできるようになる。

  • System.Data.dll
  • System.IO.Compression.FileSystem.dll
  • System.Net.Http.dll
  • System.Runtime.Serialization.dll
  • System.Xml.Linq.dll

必要になったmonoプロジェクトの情報

鍵IDとパッケージリポジトリURL
https://www.mono-project.com/download/stable/#download-lin-debian
パッケージリポジトリの追加のしかた
https://github.com/mono/docker/blob/1d31220a290b2b4d19654a8cdb4ba13888e29717/6.4.0.198/slim/Dockerfile
dllとパッケージ名の対応
https://github.com/mono/linux-packaging-mono/blob/master/debian/control

Dockerfileのベストプラクティ

docs.docker.jp

apt update, install, clean を同じ RUN で実行する。

RUN apt-get update \
     && apt-get install ... \
     ...
     && apt-get clean \
     && rm -rf /var/lib/apt/lists/*