jupyterlabのwav用MIMEレンダラーを作成しました

概要

jupyterlab内でwavファイルが開けなかったのでチュートリアルを参考にMIMEレンダラーを作成しました。 正直wavファイルの場合は上記チュートリアルと全く同じ操作で作成できてしまいました。

プロジェクトの初期化

cookiecutterを使って雛形を生成します。

❯ cookiecutter https://github.com/jupyterlab/mimerender-cookiecutter-ts.git
author_name []: wrist
author_email []: stoicheia1986@gmail.com
extension_name [myextension]: jupyterlab-wav
viewer_name [My Viewer]: JupyterLab wav viewer
mimetype [application/vnd.my_organization.my_type]: audio/wav
mimetype_name [my_type]: wav
file_extension [.my_type]: .wav
Select data_format:
1 - string
2 - json
Choose from 1, 2 (1, 2) [1]: 1

Extensionのビルドとインストール

Extensionの作成にはyarnのバージョンが固定されたjlpmというjupyterlab付属のツールを用いて行います。 依存パッケージをインストールし、Extensionをビルドし、jupyterlabのextensionとしてインストールするためには下記を実行します。

jlpm install
jlpm run build
jupyter labextension install . --no-build

ここで上記をローカル環境で試したところ、jupyterlabのバージョンが1.2.1の場合は現在のJupyterlabとはバージョンの互換性がないというエラーがでてしまいました(既に2.0.0以上を想定している模様です)。

このためjupyterlabを動作させるために使用しているdocker imageであるwrist/jupyterlab-customを更新したのですが、 jupyterlab-vimは2.x系に対応してなかったので代わりに https://github.com/jwkvam/jupyterlab-vim/pull/115 などを参照し、 @axlair/jupyterlab_vimをインストールしています。 なお、ローカルでdocker imageをビルドする際に当初jupyter lab buildを実行すると このissueと同じようにensure-max-old-space実行時にエラーが出てbuildできなくなりましたが、 osx上でDockerが使用するメモリを4096MBにしたところエラーが生じなくなりました。

コードの監視

jlpm run watchでextensionのコードに変更があるとすぐにrecompileしてくれるようになります。 またjupyterlabをjupyter lab --watchとして立ち上げるとその変更を監視しアプリケーションに取り込んでくれるようになります。 しかし手元ではwebpackがファイル監視に使用しているchokdairを見つけられないというエラーで動作しないため一旦諦めました。 なお、上記watchを施さなくてもjupyterlab自体をreloadするとコードに変更があった場合はbuildを促されるので大きな問題は生じませんでした。

※7/8追記: この監視がうまく行かない件ですがこのissueによればv2.1.3のバグとのことでv2.1.4にjupyterlabのバージョンを上げたら解決しました。

コードの構造

自動生成されたコードのsrc/index.tsを見ると主に3つのデータ構造を含んでいます。

  • OutputWidgetクラス
    • 指定したMIMEタイプのデータを受け取りHTML DOMノードの中にどのように描画するのかを扱うクラス
    • extensionのほとんどのロジックを含む
  • rendererFactoryオブジェクト
    • OutputWidgetクラスのインスタンスをどのようにアプリケーション内で生成するのかを扱うクラス
  • extensionオブジェクト
    • extensionのメインのエントリーポイントとなる部分
    • jupyterlabがextensionをロードする際に必要となるメタデータを書く

コードの編集

下記編集を実施します。

  • OutputWidgetのリネーム
    • src/index.ts内のOutputWidgetWavWidgetへとリネームします(2箇所)
  • extensionオブジェクトのfileTypesmodelNameにbase64エンコードを指定
    • デフォルトだとプレーンテキストとして読もうとするのでbase64エンコードされたものとして読むための指定を追記
    • fileTypefileFormat: 'base64'documentWidgetFactoryOptionsmodelName: 'base64'を追加する
  • レンダー方法の指定
    • WavWidgetクラスを編集
      • コンストラクタにaudioタグを追加するためのコードを追加
      • renderModelメソッド内でaudioタグのsrcを指定

大した量ではないためコード全体を下記に記します。

import { IRenderMime } from '@jupyterlab/rendermime-interfaces';



import { Widget } from '@lumino/widgets';

/**
 * The default mime type for the extension.
 */
const MIME_TYPE = 'audio/wav';

/**
 * The class name added to the extension.
 */
const CLASS_NAME = 'mimerenderer-wav';

/**
 * A widget for rendering wav.
 */
export class WavWidget extends Widget implements IRenderMime.IRenderer {
  /**
   * Construct a new output widget.
   */
  constructor(options: IRenderMime.IRendererOptions) {
    super();
    this._mimeType = options.mimeType;
    this.addClass(CLASS_NAME);
    /* 追加 */
    this._audio = document.createElement('audio');
    this._audio.setAttribute('controls', '');
    this.node.appendChild(this._audio);
  }

  /**
   * Render wav into this widget's node.
   */
  renderModel(model: IRenderMime.IMimeModel): Promise<void> {

    let data = model.data[this._mimeType] as string;
    /* 元コードを削除し下記を追加 */
    this._audio.src = `data:${MIME_TYPE};base64,${data}`

    return Promise.resolve();
  }

  private _audio: HTMLAudioElement;
  private _mimeType: string;
}

/**
 * A mime renderer factory for wav data.
 */
export const rendererFactory: IRenderMime.IRendererFactory = {
  safe: true,
  mimeTypes: [MIME_TYPE],
  createRenderer: options => new WavWidget(options)
};

/**
 * Extension definition.
 */
const extension: IRenderMime.IExtension = {
  id: 'jupyterlab-wav:plugin',
  rendererFactory,
  rank: 0,
  dataType: 'string',
  fileTypes: [
    {
      name: 'wav',
      fileFormat: 'base64',  // 追加
      mimeTypes: [MIME_TYPE],
      extensions: ['.wav']
    }
  ],
  documentWidgetFactoryOptions: {
    name: 'JupyterLab wav viewer',
    primaryFileType: 'wav',
    modelName: 'base64',  // 追加
    fileTypes: ['wav'],
    defaultFor: ['wav']
  }
};

export default extension;

上記コードを再度ビルドしてjupyterlabを再読み込みすれば完了です。

実際の表示の例

左のファイルブラウザからwavファイルをダブルクリックすると下記のようにaudio要素が表示されます。

audio要素

ちなみにwavファイルを右クリックして表示される「Open in New Browser tab」を実行するとこのExtensionがなくてもブラウザの別タブで再生は元々可能です。

npmjsへの投稿

Extension Developer GUidShipping Packagesの項を読むと extensionは単一のJavascriptパッケージであり、npmjs.orgから配布できることや、 jupyterlab-extensionというキーワードがpackage.jsonに含まれている場合JupyterLabのextension managerが見つけ出すことができるとの記載があります。 このキーワードはcookiecutterで自動生成された場合既に含まれているため、自ら追加する必要はありません。

よってnpmjs.orgへと登録することを考えます。 まずコマンドラインからnpmjsにログインしておきます。

$ npm adduser  # ユーザをnpmjs上に作成していない場合
$ npm login

次にpackage.jsonを編集し、nameの値をjupyterlab-wavから@wrist/jupyterlab-wavというように@username/extension-nameへと変えます。 更に必要に応じてhomepagelicenserepositoryなどのフィールドを追加します。

最後に、README.mdを編集してjupyter labextension installの部分を@wrist/jupyterlab-wavのように変更します。

上記を終えたら下記でnpmjsへとアップロードして終了です。

$ npm publish --access=public

無事にhttps://www.npmjs.com/package/@wrist/jupyterlab-wavへとアップロードされていれば、以後

$ jupyter labextension install @wrist/jupyterlab-wav

と打つことでインストールできるようになります。 実際に新規にコンテナを生成し直して上記を試したところ使えるようになりました。

今後

  • 他のMIMEタイプにも対応させる
    • 人によってはmp3も同様に開きたいこともあるかもしれませんが、MIME_TYPEの部分が配列になっていることから複数拡張子に対して同じような実装を使いませせるような気がしています
  • Web Audio APIを用いて波形描画やスペクトログラムを描画する

nbmediasplitというipynbファイルから画像・音声を抽出するためのスクリプトをPyPIで公開しました

nbmediasplitというipynbファイルから画像・音声を抽出するためのスクリプトをPyPIで公開しました

nbmediasplitというipynbファイルにbase64エンコードされて埋め込まれた画像・音声を抽出するためのスクリプトをPyPIで公開しました。

この記事では使い方と使用しているツールについて簡単に記述します。

インストール・使い方

  • pip install nbmediasplitを実行
    • nbmediasplitコマンドが使用可能となる
In [1]:
!pip install nbmediasplit
Collecting nbmediasplit
  Downloading https://files.pythonhosted.org/packages/fd/e2/02c36a9c5322bd24b43b3d2abe7b5ea80827e793a6b2f35b9b5460a7b4f0/nbmediasplit-0.2.0-py3-none-any.whl
Collecting click<8.0,>=7.1
  Downloading https://files.pythonhosted.org/packages/d2/3d/fa76db83bf75c4f8d338c2fd15c8d33fdd7ad23a9b5e57eb6c5de26b430e/click-7.1.2-py2.py3-none-any.whl (82kB)
     |████████████████████████████████| 92kB 798kB/s eta 0:00:01
Collecting beautifulsoup4<5.0,>=4.9
  Downloading https://files.pythonhosted.org/packages/66/25/ff030e2437265616a1e9b25ccc864e0371a0bc3adb7c5a404fd661c6f4f6/beautifulsoup4-4.9.1-py3-none-any.whl (115kB)
     |████████████████████████████████| 122kB 5.4MB/s eta 0:00:01
Requirement already satisfied: lxml<5.0,>=4.5 in /opt/conda/lib/python3.7/site-packages (from nbmediasplit) (4.5.0)
Requirement already satisfied: soupsieve>1.2 in /opt/conda/lib/python3.7/site-packages (from beautifulsoup4<5.0,>=4.9->nbmediasplit) (1.9.4)
ERROR: distributed 2.5.2 has requirement dask>=2.3, but you'll have dask 2.2.0 which is incompatible.
Installing collected packages: click, beautifulsoup4, nbmediasplit
  Found existing installation: Click 7.0
    Uninstalling Click-7.0:
      Successfully uninstalled Click-7.0
  Found existing installation: beautifulsoup4 4.8.1
    Uninstalling beautifulsoup4-4.8.1:
      Successfully uninstalled beautifulsoup4-4.8.1
Successfully installed beautifulsoup4-4.9.1 click-7.1.2 nbmediasplit-0.2.0
In [2]:
!nbmediasplit --help
Usage: nbmediasplit [OPTIONS] IPYNB_FILE

  extract base64 encoded image and pcm and save them into specified
  directories.

Options:
  -i, --imgdir TEXT    directory to store image
  -w, --wavdir TEXT    directory to store audio
  -o, --output TEXT    output ipynb file path
  -e, --encoding TEXT  input ipynb encoding  [default: utf-8]
  -d, --debug          use debug mode
  --img-prefix TEXT    path prefix for src attribute of img tag
  --wav-prefix TEXT    path prefix for src attribute of source tag under audio
                       tag

  --help               Show this message and exit.

ipynbファイルから画像を抽出

In [3]:
!nbmediasplit work/test.ipynb -i work/img
In [4]:
!ls work/img
0.png  1.png  2.png  3.png  4.png  5.png

現在のスクリプトではコードセルのoutputに埋め込まれた画像だけでなく、markdownセルに埋め込まれたattachmentも同時に抽出されます。

In [5]:
from IPython.display import Image
Image("work/img/0.png")
Out[5]:

ipynbファイルから音声を抽出

In [6]:
!nbmediasplit work/test.ipynb -w work/wav
In [7]:
!ls work/wav
0.wav  1.wav  2.wav

上記はコードセルのoutputに埋め込まれた音声を抽出します。0.wavは1kHz、FS48kHzの正弦波です。振幅が大きい(0dBFS)のでヘッドホンで再生する場合はご注意ください。 なお-iオプションと-wを同時に付けることで画像と音声を同時に抽出することもできます。

In [10]:
from IPython.display import Audio
Audio("work/wav/0.wav")
Out[10]: