GPUが使えて仮想環境を認識するJupyter環境のDockerfileを書いた

Jupyterが立ち上げるだけでプログラミング環境ができあがる各種の便利なDockerイメージを出してくれているが、GPUが使えない。ので、無理やり使えるようにしてみた。 Dockerfileの継承元のFROMを書き換える必要があったので強引な感じになった。 以下ではcudaのDockerイメージを元にしているが、jupyterを元にcudaをインストールしたほうが完了までの時間が短いはず。

なお、TensorflowがGPUの使えるJupyter入りのDockerイメージをリリースしてくれている。なので、Jupyter本家に拘らなければこんなことをする必要はほぼない…が、まあせっかく書いたので。

適当に継ぎ接ぎしてDockerfileを作る

mkdir ~/Documents/cuda-based-jupyter-datascience-notebook
cd ~/Documents/cuda-based-jupyter-datascience-notebook

wget https://raw.githubusercontent.com/jupyter/docker-stacks/master/base-notebook/Dockerfile -O base-notebook
wget https://raw.githubusercontent.com/jupyter/docker-stacks/master/minimal-notebook/Dockerfile -O minimal-notebook
wget https://raw.githubusercontent.com/jupyter/docker-stacks/master/scipy-notebook/Dockerfile -O scipy-notebook
wget https://raw.githubusercontent.com/jupyter/docker-stacks/master/datascience-notebook/Dockerfile -O datascience-notebook

cat << "EOF" > Dockerfile
# Merging nvidia/cuda and jupyter/datascience-notebook DockerFiles.
FROM nvidia/cuda:8.0-cudnn6-devel-ubuntu16.04

MAINTAINER rubyu

EOF
cat << "EOF" >> Dockerfile

# jupter/base-notebook
EOF
sed -e '1,8 s/^/# /g' base-notebook >> Dockerfile
cat << "EOF" >> Dockerfile

# jupter/minimal-notebook
EOF
sed -e '1,6 s/^/# /g' minimal-notebook >> Dockerfile
cat << "EOF" >> Dockerfile

# jupter/scipy-notebook
EOF
sed -e '1,5 s/^/# /g' scipy-notebook >> Dockerfile
cat << "EOF" >> Dockerfile

# jupter/datascience-notebook
EOF
sed -e '1,5 s/^/# /g' datascience-notebook >> Dockerfile
cat << "EOF" >> Dockerfile

USER $NB_USER

RUN jupyter notebook --generate-config && \
    jupyter serverextension enable --py jupyterlab --sys-prefix && \
    pip install environment_kernels && \
    echo "c.NotebookApp.kernel_spec_manager_class = 'environment_kernels.EnvironmentKernelSpecManager'" >> ~/.jupyter/jupyter_notebook_config.py
EOF

最後のRUNの部分がJupyter Labとconda createで作る仮想環境のための設定。

Dockerイメージ作成に必要なファイルをダウンロードしておく

wget https://github.com/jupyter/docker-stacks/raw/master/base-notebook/fix-permissions -O fix-permissions
wget https://github.com/jupyter/docker-stacks/raw/master/base-notebook/jupyter_notebook_config.py -O jupyter_notebook_config.py
wget https://github.com/jupyter/docker-stacks/raw/master/base-notebook/start-notebook.sh -O start-notebook.sh
wget https://github.com/jupyter/docker-stacks/raw/master/base-notebook/start-singleuser.sh -O start-singleuser.sh
wget https://github.com/jupyter/docker-stacks/raw/master/base-notebook/start.sh -O start.sh
sudo chmod ugo+x fix-permissions
sudo chmod ugo+x jupyter_notebook_config.py
sudo chmod ugo+x start-notebook.sh
sudo chmod ugo+x start-singleuser.sh
sudo chmod ugo+x start.sh

実行できる権限がないとダメ。ここではwgetしているが、普通はgit cloneするのでハマることはないはず。

コンテナを立ち上げる

sudo nvidia-docker build -t rubyu/datascience-notebook-gpu:1.0 .

sudo nvidia-docker run -d --name notebook-gpu -p 8888:8888 --restart=always -v /storage/samba:/home/jovyan rubyu/datascience-notebook-gpu:1.0 start.sh jupyter lab --NotebookApp.token='XXXX'

これで8888ポートでJupyter Labが起動している。

本題には関係ない話ですが、Jupyterのhome(/home/jovyan)をまるごとホストのディレクトリに割り当てて、かつそれをSamba経由でアクセスできるようにするのが好きです。Windowsで処理する必要がある時にいちいち転送する必要がなくなるので。なんやかんやでGUIWindowsが使いやすい(個人の感想です)

アタッチしてconda createで仮想環境を作る

docker exec -it notebook-gpu /bin/bash

conda create -n your-new-envronment python=3.5 jupyter tensorflow-gpu && conda clean -tipsy

作成された仮想環境はJupyterから見えて、カーネルとして選択することができる。

Dockerfile

# Merging nvidia/cuda and jupyter/datascience-notebook DockerFiles.
FROM nvidia/cuda:8.0-cudnn6-devel-ubuntu16.04

MAINTAINER rubyu


# jupter/base-notebook
# # Copyright (c) Jupyter Development Team.
# # Distributed under the terms of the Modified BSD License.
# 
# # Ubuntu 16.04 (xenial) from 2017-07-23
# # https://github.com/docker-library/official-images/commit/0ea9b38b835ffb656c497783321632ec7f87b60c
# FROM ubuntu@sha256:84c334414e2bfdcae99509a6add166bbb4fa4041dc3fa6af08046a66fed3005f
# 
# MAINTAINER Jupyter Project <jupyter@googlegroups.com>

USER root

# Install all OS dependencies for notebook server that starts but lacks all
# features (e.g., download as all possible file formats)
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get -yq dist-upgrade \
 && apt-get install -yq --no-install-recommends \
    wget \
    bzip2 \
    ca-certificates \
    sudo \
    locales \
    fonts-liberation \
 && apt-get clean \
 && rm -rf /var/lib/apt/lists/*

RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \
    locale-gen

# Install Tini
RUN wget --quiet https://github.com/krallin/tini/releases/download/v0.10.0/tini && \
    echo "1361527f39190a7338a0b434bd8c88ff7233ce7b9a4876f3315c22fce7eca1b0 *tini" | sha256sum -c - && \
    mv tini /usr/local/bin/tini && \
    chmod +x /usr/local/bin/tini

# Configure environment
ENV CONDA_DIR=/opt/conda \
    SHELL=/bin/bash \
    NB_USER=jovyan \
    NB_UID=1000 \
    NB_GID=100 \
    LC_ALL=en_US.UTF-8 \
    LANG=en_US.UTF-8 \
    LANGUAGE=en_US.UTF-8
ENV PATH=$CONDA_DIR/bin:$PATH \
    HOME=/home/$NB_USER

ADD fix-permissions /usr/local/bin/fix-permissions
# Create jovyan user with UID=1000 and in the 'users' group
# and make sure these dirs are writable by the `users` group.
RUN useradd -m -s /bin/bash -N -u $NB_UID $NB_USER && \
    mkdir -p $CONDA_DIR && \
    chown $NB_USER:$NB_GID $CONDA_DIR && \
    fix-permissions $HOME && \
    fix-permissions $CONDA_DIR

USER $NB_USER

# Setup work directory for backward-compatibility
RUN mkdir /home/$NB_USER/work && \
    fix-permissions /home/$NB_USER

# Install conda as jovyan and check the md5 sum provided on the download site
ENV MINICONDA_VERSION 4.3.21
RUN cd /tmp && \
    wget --quiet https://repo.continuum.io/miniconda/Miniconda3-${MINICONDA_VERSION}-Linux-x86_64.sh && \
    echo "c1c15d3baba15bf50293ae963abef853 *Miniconda3-${MINICONDA_VERSION}-Linux-x86_64.sh" | md5sum -c - && \
    /bin/bash Miniconda3-${MINICONDA_VERSION}-Linux-x86_64.sh -f -b -p $CONDA_DIR && \
    rm Miniconda3-${MINICONDA_VERSION}-Linux-x86_64.sh && \
    $CONDA_DIR/bin/conda config --system --prepend channels conda-forge && \
    $CONDA_DIR/bin/conda config --system --set auto_update_conda false && \
    $CONDA_DIR/bin/conda config --system --set show_channel_urls true && \
    $CONDA_DIR/bin/conda update --all --quiet --yes && \
    conda clean -tipsy && \
    fix-permissions $CONDA_DIR

# Install Jupyter Notebook and Hub
RUN conda install --quiet --yes \
    'notebook=5.2.*' \
    'jupyterhub=0.8.*' \
    'jupyterlab=0.28.*' \
    && conda clean -tipsy && \
    fix-permissions $CONDA_DIR

USER root

EXPOSE 8888
WORKDIR $HOME

# Configure container startup
ENTRYPOINT ["tini", "--"]
CMD ["start-notebook.sh"]

# Add local files as late as possible to avoid cache busting
COPY start.sh /usr/local/bin/
COPY start-notebook.sh /usr/local/bin/
COPY start-singleuser.sh /usr/local/bin/
COPY jupyter_notebook_config.py /etc/jupyter/
RUN fix-permissions /etc/jupyter/

# Switch back to jovyan to avoid accidental container runs as root
USER $NB_USER

# jupter/minimal-notebook
# # Copyright (c) Jupyter Development Team.
# # Distributed under the terms of the Modified BSD License.
# 
# FROM jupyter/base-notebook
# 
# MAINTAINER Jupyter Project <jupyter@googlegroups.com>

USER root

# Install all OS dependencies for fully functional notebook server
RUN apt-get update && apt-get install -yq --no-install-recommends \
    build-essential \
    emacs \
    git \
    inkscape \
    jed \
    libsm6 \
    libxext-dev \
    libxrender1 \
    lmodern \
    pandoc \
    python-dev \
    texlive-fonts-extra \
    texlive-fonts-recommended \
    texlive-generic-recommended \
    texlive-latex-base \
    texlive-latex-extra \
    texlive-xetex \
    vim \
    unzip \
    && apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# Switch back to jovyan to avoid accidental container runs as root
USER $NB_USER

# jupter/scipy-notebook
# # Copyright (c) Jupyter Development Team.
# # Distributed under the terms of the Modified BSD License.
# FROM jupyter/minimal-notebook
# 
# MAINTAINER Jupyter Project <jupyter@googlegroups.com>

USER root

# libav-tools for matplotlib anim
RUN apt-get update && \
    apt-get install -y --no-install-recommends libav-tools && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

USER $NB_USER

# Install Python 3 packages
# Remove pyqt and qt pulled in for matplotlib since we're only ever going to
# use notebook-friendly backends in these images
RUN conda install --quiet --yes \
    'nomkl' \
    'ipywidgets=7.0*' \
    'pandas=0.19*' \
    'numexpr=2.6*' \
    'matplotlib=2.0*' \
    'scipy=0.19*' \
    'seaborn=0.7*' \
    'scikit-learn=0.18*' \
    'scikit-image=0.12*' \
    'sympy=1.0*' \
    'cython=0.25*' \
    'patsy=0.4*' \
    'statsmodels=0.8*' \
    'cloudpickle=0.2*' \
    'dill=0.2*' \
    'numba=0.31*' \
    'bokeh=0.12*' \
    'sqlalchemy=1.1*' \
    'hdf5=1.8.17' \
    'h5py=2.6*' \
    'vincent=0.4.*' \
    'beautifulsoup4=4.5.*' \
    'protobuf=3.*' \
    'xlrd'  && \
    conda remove --quiet --yes --force qt pyqt && \
    conda clean -tipsy && \
    # Activate ipywidgets extension in the environment that runs the notebook server
    jupyter nbextension enable --py widgetsnbextension --sys-prefix && \
    fix-permissions $CONDA_DIR

# Install facets which does not have a pip or conda package at the moment
RUN cd /tmp && \
    git clone https://github.com/PAIR-code/facets.git && \
    cd facets && \
    jupyter nbextension install facets-dist/ --sys-prefix && \
    rm -rf facets && \
    fix-permissions $CONDA_DIR

# Import matplotlib the first time to build the font cache.
ENV XDG_CACHE_HOME /home/$NB_USER/.cache/
RUN MPLBACKEND=Agg python -c "import matplotlib.pyplot" && \
    fix-permissions /home/$NB_USER

USER $NB_USER

# jupter/datascience-notebook
# # Copyright (c) Jupyter Development Team.
# # Distributed under the terms of the Modified BSD License.
# FROM jupyter/scipy-notebook
# 
# MAINTAINER Jupyter Project <jupyter@googlegroups.com>

USER root

# R pre-requisites
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    fonts-dejavu \
    gfortran \
    gcc && apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# Julia dependencies
# install Julia packages in /opt/julia instead of $HOME
ENV JULIA_PKGDIR=/opt/julia

RUN . /etc/os-release && \
    echo "deb http://ppa.launchpad.net/staticfloat/juliareleases/ubuntu $VERSION_CODENAME main" > /etc/apt/sources.list.d/julia.list && \
    apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3D3D3ACC && \
    apt-get update && \
    apt-get install -y --no-install-recommends \
    julia && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* && \
    # Show Julia where conda libraries are \
    echo "push!(Libdl.DL_LOAD_PATH, \"$CONDA_DIR/lib\")" >> /usr/etc/julia/juliarc.jl && \
    # Create JULIA_PKGDIR \
    mkdir $JULIA_PKGDIR && \
    chown $NB_USER $JULIA_PKGDIR && \
    fix-permissions $JULIA_PKGDIR

USER $NB_USER

# R packages including IRKernel which gets installed globally.
RUN conda config --system --add channels r && \
    conda install --quiet --yes \
    'rpy2=2.8*' \
    'r-base=3.3.2' \
    'r-irkernel=0.7*' \
    'r-plyr=1.8*' \
    'r-devtools=1.12*' \
    'r-tidyverse=1.0*' \
    'r-shiny=0.14*' \
    'r-rmarkdown=1.2*' \
    'r-forecast=7.3*' \
    'r-rsqlite=1.1*' \
    'r-reshape2=1.4*' \
    'r-nycflights13=0.2*' \
    'r-caret=6.0*' \
    'r-rcurl=1.95*' \
    'r-crayon=1.3*' \
    'r-randomforest=4.6*' && \
    conda clean -tipsy && \
    fix-permissions $CONDA_DIR

# Add Julia packages
# Install IJulia as jovyan and then move the kernelspec out
# to the system share location. Avoids problems with runtime UID change not
# taking effect properly on the .local folder in the jovyan home dir.
RUN julia -e 'Pkg.init()' && \
    julia -e 'Pkg.update()' && \
    julia -e 'Pkg.add("HDF5")' && \
    julia -e 'Pkg.add("Gadfly")' && \
    julia -e 'Pkg.add("RDatasets")' && \
    julia -e 'Pkg.add("IJulia")' && \
    # Precompile Julia packages \
    julia -e 'using HDF5' && \
    julia -e 'using Gadfly' && \
    julia -e 'using RDatasets' && \
    julia -e 'using IJulia' && \
    # move kernelspec out of home \
    mv $HOME/.local/share/jupyter/kernels/julia* $CONDA_DIR/share/jupyter/kernels/ && \
    chmod -R go+rx $CONDA_DIR/share/jupyter && \
    rm -rf $HOME/.local && \
    fix-permissions $JULIA_PKGDIR $CONDA_DIR/share/jupyter


USER $NB_USER

RUN jupyter notebook --generate-config && \
    jupyter serverextension enable --py jupyterlab --sys-prefix && \
    pip install environment_kernels && \
    echo "c.NotebookApp.kernel_spec_manager_class = 'environment_kernels.EnvironmentKernelSpecManager'" >> ~/.jupyter/jupyter_notebook_config.py

Logicoolマウスの敏感なホイールをソフトウェアでどうにかする

小型かつ、標準でL, M, R, W系x4, X1, X2に割り当てがあるLogicool m546をポチったんですが、ホイールが敏感すぎて、美少女紙芝居ゲーで遊ぶのと、ホイールでのマウスジェスチャが辛かったので対策しました。ブラウジングにはいいんですが。もうちょっとホイールのカチカチ感が強いといいバランスかもしれません。

var VWManager = new VerticalWheelManager(
    400, // Duration in milliseconds for suppression of wheel events given after once it have emitted.
    6    // Max number of the wheel events which will be suppressed in the duration for suppression.
);
@do((ctx) =>
{
    if (VWManager.Up.Check())
    {
        // do something
    }
}

VerticalWheelManagerのコンストラクタに渡す値は「連続するホイールメッセージを無効にする期間(ミリ秒)」, 「その期間内でも一定数以上連続すれば有効にする」です。あとはVerticalWheelManager.Up.Check()VerticalWheelManager.Down.Check()を叩けば、truefalseの判定の結果が返ります。

以下、マウスジェスチャツール CreviceAppの設定ファイルサンプルです。M325, M545, M546あたりの軽いホイール。または高速スクロール対応機種にも便利かもしれません。

gist6de4bf1c5f7d936bc2df2873173dec65

このぐらいの処理をマウスジェスチャツール本体ではなく、設定側でどうにかできるというのはなかなか便利です。

LogicoolのSetPointとVMwareの相性が悪く、マウスボタンに割り当てたコマンドの最後が連続入力される問題

例えばホストのSetPointにて、特定のボタンにWin+Tab(アプリケーションスイッチャ)を割り当てて、VMwareでゲストの操作をしているときにそのボタンを押すと、コマンドはゲストに送信されますが、Win+Tab, Tab, Tab ...のような連続したものになってしまい、とっても悲しいです。

ここで、SetPointにはX1, X2ボタンを割り当てて、それらのボタン入力をマウスジェスチャツールで任意のコマンドに割り当てると、Win10 on Win10の環境ではうまく動くようです。

ホストもゲストもこれでうまく動くので、以前Win8だか8.1だかの頃に話題になった、ソフトウェアが送信したAlt+TabをOSが無視する問題とか、緩和されたんでしょうか?(調べてない)

マウスジェスチャーツール CreviceApp 1.0 をリリースしました

2013年10月上旬に、突如としてかざぐるマウスが消えてしまってから乗り換え先を探していましたが、あまりよいものが見つからなかったので自作しました。先月から実際に使ってバグを潰していましたが、そろそろ安定したかなーという感じなので1.0をリリースします。

rubyu/CreviceApp

特徴

必須要件

良い点

  • ジェスチャの取りこぼしがない
  • CSharp Scriptでジェスチャを定義できる

悪い点

  • GUIがショボい

ジェスチャの取りこぼしがないこと、というのを最重要な要件として開発しました。私の必要としない、例えばGUIでの設定機能とか、ジェスチャ軌跡の描写機能とかはバッサリ切ってます。C#で設定を書けて、ジェスチャの取りこぼしなく、安定して動けばいいかなーと。

機能追加にはあんまり興味がないですが、安定性の向上には興味があります。問題などあれば、報告いただければと思います。

夏休みの宿題「Ankiを捗らせるためのjsビューア」提出しました

AnkiDroidでひながいちにち遊ぶのに、ストレスを感じていました。以前の記事(市販のデータを用いてAnkiデッキを生成する方法 Epwing辞書編)の方法で辞書からデッキを生成したためカードのサイズが大きく、頻繁にスクロールのためにタッチ操作をしなければいけなかったためです。Ankiを可能な限り高速に単語とその語義を頭に叩き込むゲームとして考えたとき、このUIには改善の余地があると思いました。幸い、カードはHTMLそのものであり、JavaScriptを読みこませれば、カード閲覧時のUIはカスタマイズ可能です。そこで、いくばくかの時間を割いて、UIの改善を試みることにしました。この改善により学習効率が向上すれば、十分にペイすると考えたからです。

開発中のできごと

当初の方針

当初、DOMを走査してページ位置を割り出せるかな、と見込んでいました。可能なら、それを元に仮想的なページ位置を前後できればビューアが完成するはず…でした。

方針転換

DOMの走査は高コストで、かつ、要素によってはオフセット位置を取れなかったりしました。そこで、CSS3 Multiple Columnsを使って、カラム幅 = ウィンドウ幅に設定し、レンダリングの段階でページごとに描写されるようにしました。この方法だと、単にX方向のスクロール位置をウィンドウ幅ぶん前後させるだけで、ページの前後が実現できます。

生JSを諦める

ひとまず動く、というところまでは生JavaScriptを書きましたが、型がないことに耐えられず、途中でScala.jsに切り替えました。初めて触ったのですが、Scala.jsは思っていたより完成度が高く、十分実用になるのではという印象を持ちました。

AnkiDroidにプルリクを送る

  • Cookieが永続せず消える
  • ユーザーアクションなしでaudio/videoをloadできない

の2点が問題になったため、改善するためのパッチを書いて送りました。無事マージされたので、自前ビルド地獄に落ちずにすみました。Playストアからダウンロードできると楽でいいです。

できあがったもの

全体をスクロールするのではなく、ページ単位の戻り/送りでAnkiのカードを眺めるためのビューアです。シンプルなものですが、これで十分実用になります。

操作方法

操作 動作
左右スワイプ ページ戻り/送り
左右タップ ページ戻り/送り
左右ロングスワイプ チャプター戻り/送り
中央タップ 音声再生
タップ&ホールド 音声リピート再生

成果物

アプリケーションはScalaで記述されており、全部で900行ほど、レポジトリはrubyu/anki-iframe-viewerです。 gitsbtがインストールされている環境であれば、以下の手順でコンパイルできます。

$ git clone https://github.com/rubyu/anki-iframe-viewer
$ cd anki-iframe-viewer
$ sbt prod/dist

現時点で1.0が最新です。リリース一覧からコンパイル済みのjsファイル、cssファイルがダウンロードできます。

  • anki-iframe-viewer-opt.js

ビューア本体。このビューアはページを左右にスライドさせることでページ送りを実現しているので、以下のcssの適用が必要です。

  • anki-iframe-viewer.css

CSS3 multi-column layout をカードに適用するためのCSS#container内にコンテンツが入るようにすると、うまく表示されると思います。

anki-iframe-viewer.cssは標準的な1カラム構成のデザインです。タブレットなどでは、複数カラム構成のデザインのほうが見やすいかもしれません。例えば2カラム構成ではanki-iframe-viewer.cssに続けて、次のコードを適用してください:

#container {
  -webkit-column-width: 50vw;
}

注1: コンテンツの横のグラデーションは、このビューアには関係がない、現在どのあたりを読んでいるのかわかりやすくするためのハックです。適当なHTMLに対し、

<div class="outer">
  <div class="inner"> ... </div>
</div>
.outer {
  /* berry-juice from http://codepen.io/tumanova/pen/tkvmi?editors=0100 */
  background-image: linear-gradient(to bottom, #c5d4d7 6%, #d6b98d 34%, #c99262 57%, #8c5962 80%, #43577e 100%);
}
.inner {
  margin: 0 0.5rem 0 0;
  padding: 0 0.5rem 0 0.5rem;
  background-color: white;
}

などとすれば、最近のブラウザなら再現できるはずです。

注2: anki-iframe-viewerという名前のアプリケーションですが、 iframe内でしか動作しないわけではありません。後述するiframeデッキに移行する際に導入することを目指して開発を始めたため、この名前になりました。

制約事項

以下の制約は、私が必要としておらず、コスト的にサポートできなかった部分です。

  • 画面をタッチして、文字列を選択したりすることができない
  • 複数のaudio要素が見つかった場合、最初のものだけを再生してしまう

設定方法

Scala.jsにてApplicationを以下のように定義しています。

@JSExport("AnkiIframeViewerApp")
object App extends Logger {
...
  @JSExport def minLongSwipeSize(d: Double)                      = { userMinLongSwipeSize = Option(d); this }
  @JSExport def minSwipeSize(d: Double)                          = { userMinSwipeSize = Option(d); this }
  @JSExport def minLongTouchMillis(d: Double)                    = { userMinLongTouchMillis = Option(d); this }
  @JSExport def maxGestureMillis(d: Double)                      = { userMaxGestureMillis = Option(d); this }
  @JSExport def dispatcherDuplicateEventWindowMillis(d: Double)  = { userDispatcherDuplicateEventWindowMillis = Option(d); this }
  @JSExport def tapCenterRatio(d: Double)                        = { userTapCenterRatio = Option(d); this }
  @JSExport def autoPlayAudio(b: Boolean)                        = { userAutoPlayAudio = Option(b); this }
  @JSExport def holdReplayAudio(b: Boolean)                      = { userHoldReplayAudio = Option(b); this }
  @JSExport def audio(query: String)                             = { userAudioQueries += query; this }
  @JSExport def chapter(query: String)                           = { userChapterQueries += query; this }
...
  @JSExport
  def run(): Unit = {
...
  }
}

よって、カード自体は

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<link rel="stylesheet" type="text/css" href="anki-iframe-viewer.css">
<script type="text/javascript" src="anki-iframe-viewer-opt.js"></script>
<script type="text/javascript">
  AnkiIframeViewerApp()
    .audio("#chapter-00 audio")
    .chapter("#chapter-01")
    .chapter("#chapter-02")
    .run();
</script>
</head>
<body>
<div id="container">
  <div id="chapter-00"> ... </div>
  <div id="chapter-01"> ... </div>
  <div id="chapter-02"> ... </div>
</div>
</body>
</html>

のようなHTMLにすべきです。

ここで、AnkiIframeViewerApp()audio()chapter()をコールして設定を行い、run()でアプリケーションを開始していることに注意してください。anki-iframe-viewer-opt.jsを読み込んだだけではアプリケーションは開始されません。

まずは適当なhtmlファイルを作り、ローカルサーバでビューアが動作することを確認してみてください。正しく動作すれば、それをAnkiに移植してみましょう。

Ankiへの導入方法

Ankiへの導入にはいくつかの問題があり、それらを回避しなければなりません。以下には問題とその対処法を記します。

注1: 何か問題が発生したとき、あるいは何も起こらなかったとき(動きさえしない!)に責任を負うことができないため、実際にデッキにこのビューアを組み込むことは推奨しませんが、なにかの役に立つこともあるだろう、ということで本稿を公開しています。ご注意を。お役に立てたなら嬉しいです。

注2: 2016/07/19現在、このビューアはAnkiDroidでのみ動作確認が取れています。Anki Mobileは不明。Anki Desktopでは動作しません。Anki Desktopについては、組み込んであるPyQt 4.8.4がちょー古いためです。ただし、PyQt 5.5への移行が進行中なので、そのうち動くようになるのではないかと思います。

AnkiDroidとその他の環境で併用したい

動作しない環境では、cssとjsファイルを削除すれば、従来通りのスワイプクロールUIに簡単にフォールバックできます。これは、将来的にこのビューアに問題が発生した場合も、この方法で簡単にフォールバックが行えるということを意味します。

カードのheadをカスタマイズできない

JavaScriptを使って改変できます。カードのテンプレートに、例えば以下のように記述します。

<script>
  var meta = document.createElement("meta");
  meta.setAttribute("name", "viewport");
  meta.setAttribute("content", "width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no");
  document.getElementsByTagName("head")[0].appendChild(meta);
</script>

Ankiのデッキサイズに100MBの制約がある

Collections on AnkiWeb are limited to 100MB, not including media.

Are there limits on file sizes on AnkiWeb? / Anki Ecosystem / Knowledge Base - Anki Support

これは実に絶望的な制約で、例えばCOCA60K(6万単語)コーパスからデッキを生成すると、平均して単語あたり1000文字ほどが上限になってしまいます。このための回避策として、カード自体をhtmlファイルに切り出して、カードからはそのhtmlをiframeとして参照するという方法をなんとか編み出しました。以下の手順で、既存のデッキに適用できます。

準備

まず、次の記事を参考に、データを処理する環境を準備してください: 市販のデータを用いてAnkiデッキを生成する方法 Epwing辞書編 - ruby-U's blog

次にhttps://github.com/rubyu/wok-scripts/archive/v0.2.zipをダウンロードして、同様に配置してください。

iframeデッキの構築

以下では、チュートリアルで生成したres2.tsvに手を加えて、カードのデータをhtmlファイルとして切り出します。

> java -jar wok-0.1.0.jar -f wok-scripts-0.2\col-detach.wok -v@str src=1,2,3 -v dst=1 -v@rawstr media=media res2.tsv > res3.tsv
> type res3.tsv
"apple" "<iframe src=""4CC39A56AE79DCFDA45E47D76C6C4D94.html""></iframe>"
"book"  "<iframe src=""1B764C2ED28D05BD3759E0ADAAF56557.html""></iframe>"
"car"   "<iframe src=""8D5A6E213A3F1BC101840112AABE2D0F.html""></iframe>"
> dir *.html /b media
4CC39A56AE79DCFDA45E47D76C6C4D94.html
1B764C2ED28D05BD3759E0ADAAF56557.html
8D5A6E213A3F1BC101840112AABE2D0F.html

stcに複数の列が指定されていることに注意してください。これらの列はまとめて一つのhtmlファイルに変換され、dstでそのhtmlを参照するiframeの出力先の列を指定しています。mediaに出力されたhtmlファイルは以下のようなフォーマットになっています。

<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="col-detach.css">
<script type="text/javascript" src="col-detach.js"></script>
</head>
<body>
<div id="detached">
  <div id="detached-00"> ... </div>
  <div id="detached-01"> ... </div>
  <div id="detached-02"> ... </div>
</div>
</body>
</html>
Ankiへの導入

この例の場合、iframeから読み込まれるhtmlは col-detach.jscol-detach.cssを読み込みます。必要であれば、 anki-iframe-viewer-opt.jsにビューアを起動するためのスクリプトを追記したものをcol-detach.jsanki-iframe-viewer.csscol-detach.cssとリネームし、collection.mediaに配置してください。

ひとつのディレクトリに大量のメディアファイルを置くと、メディアスキャンがバッテリードレインを起こすことがある

Anki/AnkiDroidではcollection.media内にファイルを配置することを推奨していますが、その数が膨大になると、パフォーマンス上の問題が発生します。アプリケーションの動作のみならず、端末自体のパフォーマンスが低下することもあるため、Epwingから生成したデッキには以下の手順を適用するといいかもしれません。

バッテリードレインの例:

参考: Issue 37199 - android - android.process.media draining battery at boot - Android Open Source Project - Issue Tracker - Google Project Hosting

tsvファイルにディレクトリ構造を適用する
> java -jar wok-0.1.0.jar -f wok-scripts-0.2\apply-directory-structure-to-tsv.wok -v idx=1 -v depth=2 -v@rawstr prefix=Foo res3.tsv > Foo.tsv
> type Foo.tsv
"apple" "<iframe src=""Foo/4/C/4CC39A56AE79DCFDA45E47D76C6C4D94.html""></iframe>"
"book"  "<iframe src=""Foo/1/B/1B764C2ED28D05BD3759E0ADAAF56557.html""></iframe>"
"car"   "<iframe src=""Foo/8/D/8D5A6E213A3F1BC101840112AABE2D0F.html""></iframe>"
htmlファイルにディレクトリ構造を適用する
> java -jar wok-0.1.0.jar -f wok-scripts-0.2\apply-directory-structure-to-html.wok -v depth=2 -v@rawstr target=media

mediaで指定されたディレクトリ内のhtmlファイルについて、head内にbaseタグを追加し、また、メディアファイル(mp3, png)のパスを書き換えます。 ここで、depthで指定された数だけparentへと遡るbaseタグが設定されることに注意してください。

mediaディレクトリ内の全てのファイルにディレクトリ構造を適用する
> java -jar wok-0.1.0.jar -f wok-scripts-0.2\apply-directory-structure-to-media.wok -v depth=2 -v@rawstr target=media
> dir /b media
0
1
2
...
> dir /b media\4
0
1
2
...
> dir *.html /b media\4\C
4CC39A56AE79DCFDA45E47D76C6C4D94.html

指定のdepthの、ファイル名のprefixから生成されたディレクトリ構造が適用されていることを確認してください。 問題がなければ、mediaディレクトリをFooapply-directory-structure-to-tsv.wokprefixで指定した値)にリネームして、これをcollection.mediaにコピーすれば手順は完了です。Foo.tsvをAnkiに読み込み、動作を確認してください。

この例の場合、iframeから読み込まれるhtmlは Foo/col-detach.jsFoo/col-detach.cssを参照します。必要であれば、 anki-iframe-viewer-opt.jsにビューアを起動するためのスクリプトを追記したものをcol-detach.jsanki-iframe-viewer.csscol-detach.cssとリネームし、collection.media/Fooに配置してください。 同様に、iframeから参照されるその他のリソースファイルが存在すれば、それらもcollection.media/Fooに配置してください。cssファイルから参照されるフォントなど、間接的な参照もこの場合に含まれることに注意してください。

大量のファイルを含むアーカイブファイルを扱えないファイラーがある

前述の手順で生成されたディレクトリをAndroid上にコピーするには、圧縮したあと、Android上でアーカイブファイルを解凍するという手順が簡単かつ高速なのですが、ESファイルエクスプローラーなど有名なアプリでも、大量のファイルを含むアーカイブファイルを扱えない場合があります。RARというアプリでは問題なく展開できるようなので、このような場合には試してみてください。

実際の例

私はCOCA60Kコーパスから、語義がEpwing辞書なデッキを作り、さらに前述の方法で語義をiframeに変換して、ディレクトリ構造を適用したものを実際に運用しています。以下に設定を記載します。

ノートタイプ

ノートタイプは「Front」「Back」の二つからなる、標準のノートタイプです。

テンプレート

表面のテンプレート
<div id="anki-front">{{Front}}</div>
書式
@font-face { font-family: _myfont00; src: url('_Lucida_Sans_Unicode.ttf'); }
@font-face { font-family: _myfont01; src: url('_yugothic.ttf'); }
@font-face { font-family: _myfont02; src: url('_HanaMinA.ttf'); }
@font-face { font-family: _myfont03; src: url('_HanaMinB.ttf'); }

html {
  font-size: 100%;
}
html body,
html body * {
  padding: 0;
  margin: 0;
}
body {
  overflow: hidden;
}
#outer {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
}
iframe {
  width: 100%;
  height: 100%;
  border: none;
}
#anki-front {
  margin: 1rem;
  display: block;
  font-size: 1rem;
  font-family: _myfont00, _myfont01, _myfont02, _myfont03, sans-serif, serif;
  text-align: left;
  line-height: 1.5rem;
  color: black;
  background-color: white;
}
.win #anki-front {
  font-size: 200%;
}
裏面のテンプレート
<script>
  document.createElement("meta");
  meta.setAttribute("name", "viewport");
  meta.setAttribute("content", "width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no");
  document.getElementsByTagName("head")[0].appendChild(meta);

  document.addEventListener("touchstart", function(event) {
    event.preventDefault();
  }, false);
  document.addEventListener("touchmove", function(event) {
    event.preventDefault();
  }, false);
  document.addEventListener("touchend", function(event) {
    event.preventDefault();
  }, false);
  document.addEventListener("DOMContentLoaded", function() {
    document.getElementsByTagName("iframe")[0].focus();
  }, false);
</script>
<div id="outer">{{Back}}</div>

バイスごとの設定

SO-03G
collection.media/COCA/col-detach.js

anki-iframe-viewer-opt.jsに以下を結合したもの。

AnkiIframeViewerApp()
  .audio("#detached-02 audio")
  .chapter("#detached-03")
  .chapter("#detached-04")
  .chapter("#detached-05")
  .chapter("#detached-06")
  .chapter("#detached-07")
  .chapter("#detached-08")
  .chapter("#detached-09")
  .chapter("#detached-10")
  .run();
collection.media/COCA/col-detach.css
@font-face { font-family: _myfont00; src: url('_Lucida_Sans_Unicode.ttf'); }
@font-face { font-family: _myfont01; src: url('_yugothic.ttf'); }
@font-face { font-family: _myfont02; src: url('_HanaMinA.ttf'); }
@font-face { font-family: _myfont03; src: url('_HanaMinB.ttf'); }

html {
  overflow: hidden;
  font-size: 2vmin;
}

html * {
  font-size: 1rem;
}

body {
  margin: 1rem 0 1rem 0;
  height: calc(100% - 2rem);
  font-family: _myfont00, _myfont01, _myfont02, _myfont03, sans-serif, serif;
  text-align: justify;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: subpixel–antialiased;
  -webkit-text-size-adjust: 100%;
  -webkit-user-select: none;
  cursor: default;
  line-height: 1.4;
  color: black;
  background-color: white;
}

#detached {
  height: 100%;
  -webkit-column-width: 100vw;
  -webkit-column-gap: 0;
}

#horizontal-bar {
  position: fixed;
  left: 0;
  bottom: 0.05rem;
  width: 100vw;
}
#horizontal-bar-icon1-A,
#horizontal-bar-icon1-B,
#horizontal-bar-icon1-C {
  visibility: hidden;
  position: absolute;
  right: 0.5rem;
  bottom: 0.15rem;
  width: 1rem;
  text-align: center;
  vertical-align: bottom;
  font-family: san-serif, selif;
  font-size: 0.5rem;
  line-height: 0.5;
  -webkit-filter: grayscale(100%) opacity(70%);
}
#horizontal-bar-progress-bar {
  position: absolute;
  left: 0.5rem;
  bottom: 0.35rem;
  width: calc(100vw - 2.5rem);
}
#horizontal-bar-progress-total {
  position: absolute;
  top: 0;
  width: 100%;
  border-bottom: 0.05rem solid #f0f0f0;
}
#horizontal-bar-progress-current {
  position: absolute;
  top: 0;
  width: 0;
  border-bottom: 0.05rem solid #505050;
}

#detached-00 {
  display: inline;
  float: left;
  margin: 0 auto 0 0.5rem;
  font-size: 1.2rem;
  line-height: 1;
}
#detached-01 {
  display: inline;
}
#detached-01 table {
  float: right;
  margin: 0 1rem 0.5rem 0.5rem;
  max-width: 50vw;
  border-collapse: collapse;
  border: 0;
}
#detached-01 table * {
  font-weight: normal;
  line-height: 1.1rem;
}
#detached-01 td {
  margin: 2rem 0 2rem 0;
  text-align: center;
  min-width: 10vw;
}
#detached-01 thead th {
  background-color: #f0f0f0;
  font-size: 0.4rem;
}
#detached-01 tbody td {
  font-size: 0.6rem;
}
#detached-01 tbody:nth-child(2n) td {
  background-color: #fefefe;
}
#detached-01 tbody:nth-child(2n+1) td {
  background-color: #f9f9f9;
}
#detached-02 {
  display: none;
}
#detached-03 .ebquery:first-child::before {
  content: "\A";
  white-space: pre;
}
#detached-03:not(:empty),
#detached-04:not(:empty),
#detached-05:not(:empty),
#detached-06:not(:empty),
#detached-07:not(:empty),
#detached-08:not(:empty),
#detached-09:not(:empty),
#detached-10:not(:empty) {
  /* berry-juice from http://codepen.io/tumanova/pen/tkvmi?editors=0100 */
  background-image: linear-gradient(to bottom, #c5d4d7 6%, #d6b98d 34%, #c99262 57%, #8c5962 80%, #43577e 100%);
}
#detached-03 .ebquery:last-child::after,
#detached-04 .ebquery:last-child::after,
#detached-05 .ebquery:last-child::after,
#detached-06 .ebquery:last-child::after,
#detached-07 .ebquery:last-child::after,
#detached-08 .ebquery:last-child::after,
#detached-09 .ebquery:last-child::after,
#detached-10 .ebquery:last-child::after {
  display: block;
  padding-bottom: 0.2rem;
  text-align: right;
  font-size: 0.8rem;
}
#detached-03 .ebquery:last-child::after {
  content: "【ランダムハウス英語辞典】";
}
#detached-04 .ebquery:last-child::after {
  content: "【研究社 新英和大辞典】";
}
#detached-05 .ebquery:last-child::after {
  content: "【リーダーズ・プラス】";
}
#detached-06 .ebquery:last-child::after {
  content: "【英辞郎】";
}
#detached-07 .ebquery:last-child::after {
  content: "【ロングマン現代英英辞典】";
}
#detached-08 .ebquery:last-child::after {
  content: "【オックスフォード現代英英辞典】";
}
#detached-09 .ebquery:last-child::after {
  content: "【斎藤和英大辞典】";
}
#detached-10 .ebquery:last-child::after {
  content: "【ロングマン現代英英辞典】";
}
#detached-03 .ebquery:last-child,
#detached-04 .ebquery:last-child,
#detached-05 .ebquery:last-child,
#detached-06 .ebquery:last-child,
#detached-07 .ebquery:last-child,
#detached-08 .ebquery:last-child,
#detached-09 .ebquery:last-child,
#detached-10 .ebquery:last-child {
  box-shadow: 0 -5rem 1rem -5rem #909090 inset;
}
.ebquery:first-child {
  padding-top: 0.5rem;
}
.ebquery:not(:last-child) {
  padding-bottom: 0.2rem;
}
.ebquery:not(:first-child) {
  margin-top: 0.1rem;
  padding-top: 0.3rem;
}
.ebquery {
  margin: 0 0.5rem 0 0;
  padding: 0 0.5rem 0 0.5rem;
  display: block;
  word-wrap: break-word;
  background-color: white;
}
.ebkw {
  font-weight: bold;
  background-color: #f0f0f0;
}
.ebsb {  vertical-align: sub;  }
.ebsp {  vertical-align: super;  }
.ebec {
  vertical-align: text-bottom;
  height: 1.1rem;
  padding-bottom: 0.25rem;
}
.ebkw,
.pos,
.sense1,
.sense2,
.sense3,
.LDOCE5 .sup {
  padding-left: 0.2rem;
  padding-right: 0.2rem;
  line-height: 1.7;
}
.SRD      .sense1 {
 padding-left: 0.6rem;
}
.SRD      .sense2,
.BODY     .sense1,
.KENE7J5  .sense1,
.KENE7J5  .sense2,
.KENE7J5  .sense3,
.KQNEWEJ6 .sense1,
.KQNEWEJ6 .sense2,
.KQNEWEJ6 .sense3,
.PLUS     .sense1,
.PLUS     .sense2,
.eijiro   .sense1,
.LDOCE5   .sense1,
.LDOCE5   .sense2,
.OALD7    .sense1 {
 padding-left: 1.2rem;
}
.SRD      .pos,
.KENE7J5  .pos,
.KQNEWEJ6 .pos,
.PLUS     .pos,
.eijiro   .pos,
.BODY     .pos {
 background-color: #e0ffe0;
}
.LDOCE5   .sup {
 background-color: #ffead5;
}
.SRD      .sense1,
.KENE7J5  .sense2,
.KQNEWEJ6 .sense1,
.KQNEWEJ6 .sense3,
.PLUS     .sense1,
.eijiro   .sense1,
.BODY     .sense1,
.LDOCE5   .sense1,
.OALD7    .sense1 {
 background-color: #ffe8e8;
}
.SRD      .sense2,
.KENE7J5  .sense3,
.KQNEWEJ6 .sense2,
.PLUS     .sense2,
.LDOCE5   .sense2 {
 background-color: #e8ffff;
}

.KENE7J5  .sense1 {
 background-color: #e8e8ff;
}
.KQNEWEJ6 .ebul,
.PLUS     .ebbo,
.PLUS     .ebul,
.PLUS     .ebit,
.eijiro   .ebbo,
.eijiro   .ebit,
.LDOCE5   .ebbo,
.OALD7    .ebbo,
.BODY     .ebbo {
  font-weight: bold;
}

html {
  font-size: 10.5px;
}
Nexus7
collection.media/COCA/col-detach.js

SO-03Gと同じ。

collection.media/COCA/col-detach.css

SO-03Gと以下だけ異なる。

html {
  font-size: 13.5px;
}

#detached {
  -webkit-column-width: 50vw;
}

#detached-00,
#detached-01 {
  display: block;
  width: 100%;
  margin-bottom: 0.2rem;
}
#detached-01 table {
  float: none;
  margin: 0 auto 0 auto;
}
#detached-03 .ebquery:first-child::before {
  content: "";
  white-space: pre;
}
PC
collection.media/COCA/col-detach.js

使用しないので空のファイルを配置する。

collection.media/COCA/col-detach.css

SO-03Gと以下だけ異なる。

html {
  overflow: scroll;
  font-size: 16px;
}

市販のデータを用いてAnkiデッキを生成する方法 Epwing辞書編

以前から、Ankiデッキを効率的に各種のデータから作成する方法を開拓してきたが、ここではEpwing辞書からAnkiデッキを生成する方法を紹介する。

まえがき

Epwingとはかつて、一度データを購入すれば、自身が所有するあらゆる端末上で利用することができるようになるという、夢のような、自由で便利な辞書データの規格(JIS X 4081)であった。 しかし現在では、旧式化、クローズドな規格への切り替えなどにより、ほぼ絶滅しており、オリジナルのEpwing辞書の入手は困難になっている。 一方で、デファクト・スタンダードとして一度覇権を握った影響は大きく、現在でも各種プラットフォーム向けのEpwing辞書ビューアは生き長らえており、各種の辞書ソフトのデータをEpwing形式に変換するようなツールも有志により開発が続けられている。 中でもロゴヴィスタの電子辞書シリーズは、Epwingから派生したフォーマットを使用していることもあり、かなりの辞書がdessedによりEpwing形式に変換可能である。 ITの基礎知識を持つ語学学習者が、これらのEpwing辞書から好きなものを選び、高品質なAnkiデッキを容易に作成できるようになることが、本稿の意図するところである。

準備

おおよそ以下のものが必要である。

  • Javaランタイム

Wokを動作させるために必要になる。多くの環境ではデフォルトでインストールされているため、新たに導入する必要はない。必要があれば、https://java.com/ja/download/ からダウンロードする。

  • Wok

TSVファイルを加工するためのツールhttps://github.com/rubyu/wok/releases/download/v0.1.0/wok-0.1.0.jar をダウンロードする。

Wokが行う処理を記述したファイル。 https://github.com/rubyu/wok-scripts/archive/v0.1.zip からダウンロードする。

前述のEpwing辞書。好みのものを用意すること。

  • 単語のリスト

Ankiデッキに変換する元となる、単語が列挙されたファイル。ASCII、またはUTF-8で記述されていること。

ツアー

以下では次に示されるシンプルな単語のリスト list.txt を出発点として、僅かな手順で、十分に実用になるデッキを生成していく。

> type list.txt
apple
book
car

環境構築

まだ作業を始めたばかり、フォルダの中身はlist.txtだけだ。

> dir /b
list.txt

Java環境は整っているだろうか。確かめてみよう。

> java -version
java version "1.8.0_77"
Java(TM) SE Runtime Environment (build 1.8.0_77-b03)
Java HotSpot(TM) Client VM (build 25.77-b03, mixed mode, sharing)

ではここで、Wok、そしてWok-Scriptsをダウンロードしよう。Wok-Scriptsはzipで圧縮されているので解凍しておく。フォルダの中身はこうだ。

> dir /b
list.txt
wok-0.1.0.jar
wok-scripts-0.1
wok-scripts-0.1.zip

Wok-Scriptsには動作テスト用に、挨拶をするだけのスクリプト hello.wok が含まれている。早速試してみよう。

> java -jar wok-0.1.0.jar -f wok-scripts-0.1\hello.wok
Hello!

フィールドを追加する

list.txtにフィールドを追加することで、Ankiのデッキとして意味を持つTSVファイル(フィールドがタブで句切られているファイル)を構築していく。

ランクフィールド

まず簡単な例を示す。 ある単語が、ある単語リストの中で何番目であるかを示すフィールドを構築するためのスクリプトrank.wokを用いて以下のように:

> java -jar wok-0.1.0.jar -f wok-scripts-0.1\rank.wok -v src=0 -v dst=1 -v grp=1 -v@rawstr list=list.txt -v update=true list.txt
"apple" "1"
"book"  "2"
"car"   "3"

ここで、-v src=0 は処理の元になるフィールドの番号を指定している。list.txtにはまだ1つのフィールドしかない。そのため0と置く。

-v dst=1 は処理の結果を書き込むフィールド番号を指定している。1を指定しているため、2番めのフィールドに処理結果が書き込まれる。

-v grp=1 は何件ごとに1つのランクとするか。ここでは1件ごとにランクは繰り上がる。

-v@rawstr list=list.txt はランクの基準となるリストを指定している。ここでは入力と同じファイルを指定している。

-v update=truedstで指定したフィールドに値があった場合、それを上書きすると指定している。

list.txt を処理するため、一番最後に与えている。

また、Wokを実行した結果は、リダイレクトを用いて任意のファイルとして保存することができる。ここではres0.tsvとして結果を保存する。

> java -jar wok-0.1.0.jar -f wok-scripts-0.1\rank.wok -v src=0 -v dst=1 -v grp=1 -v@rawstr list=list.txt -v update=true list.txt > res0.tsv

うまく保存できただろうか。確認してみよう。

> type res0.tsv
"apple" "1"
"book"  "2"
"car"   "3"
音声フィールド

ここでは研究社新英和(第7版)・和英(第5版)中辞典(KENE7J5)をデッキ生成に用いる。

Epwing辞書を検索し、音声ファイルのフィールドを構築するためのスクリプトeb-voice-anki.wokを用いて、以下のように:

> java -jar wok-0.1.0.jar -f wok-scripts-0.1\eb-voice-anki.wok -v src=0 -v dst=2 -v idx=0 -v update=true -v@rawstr dic=KENE7J5 -v@rawstr media=media res0.tsv > res1.tsv

-v idx=0 は検索結果の何番目の音声ファイルを使用するかを指定している。(例えば UK、USなどの順で、)複数の音声が含まれている場合に、望ましいインデックス番号を指定すことができる。

-v@rawstr dic=KENE7J5Epwing辞書のパス(CATALOGSを子に持つフォルダ)を指定している。環境に合わせて適切な値を設定すること。

-v@rawstr media=media は音声ファイルを保存するフォルダを指定している。このフォルダが存在しない場合、処理の中で自動的に生成される。

このスクリプトは処理を実行するのに必要なebquery-0.3.1.jarを自動的に保存する。見覚えがないファイルが存在することに驚かないこと。

さて、結果を見てみよう。

> type res1.tsv
"apple" "1"     "[sound:D7E4E20774DDB06FBA354BD6B91FE24F.wav]"
"book"  "2"     "[sound:9669237A5749E31144688F7F24C2C6D9.wav]"
"car"   "3"     "[sound:4C7A322536A21791D5F559F3E199C77B.wav]"

[]で囲まれたAnki形式のsoundタグを持つ3番めのフィールドが追加されている。

> dir /b media
4C7A322536A21791D5F559F3E199C77B.wav
9669237A5749E31144688F7F24C2C6D9.wav
D7E4E20774DDB06FBA354BD6B91FE24F.wav

音声ファイルもきちんとmediaフォルダ内に作られている。

語義フィールド

さて、最後のフィールド。音声フィールドと同様に、研究社新英和(第7版)・和英(第5版)中辞典(KENE7J5)を用いることとする。

このフィールドを作成するために、必要なものがある。それはMapファイルで、ここで必要になるのはKENE7J5.mapEBWin4をインストールすると、%APPDATA%\EBWin4\GAIJIに自動的に生成される。あるいは空のmapファイルを用いてもよい。その場合はtype nul > KENE7J5.mapとして用意する。

念のため、KENE7J5.mapが存在するか確認しておこう。

> dir /b
ebquery-0.3.1.jar
KENE7J5.map
list.txt
media
res0.tsv
res1.tsv
wok-0.1.0.jar
wok-scripts-0.1
wok-scripts-0.1.zip

準備ができれば、Epwing辞書を検索し、語義のフィールドを構築するためのスクリプトeb-html-min.wokを用いて、以下のように:

> java -jar wok-0.1.0.jar -f wok-scripts-0.1\eb-html-min.wok -v src=0 -v dst=3 -v update=true -v@rawstr dic=KENE7J5 -v@rawstr ebmap=KENE7J5.map -v@rawstr media=media res1.tsv > res2.tsv

-v@rawstr ebmap=KENE7J5.map は前述のmapファイルの場所を指定している。

-v@rawstr media=media 音声フィールドのときと同様。ここでは外字を表示するための画像ファイルを保存する。

さて、結果を見てみよう。

> type res2.tsv
"apple" "1"     "[sound:D7E4E20774DDB06FBA354BD6B91FE24F.wav]"  "<span class=""e
bquery KENE7J5""><span class=""ebkw""><img alt=""hA132"" class=""ebec"" src=""40
CB34C31B79B8D7EF231201333C4468.png""><span class=""ebul"">ap繝サple</span> </span
><br>/ヌスpl/竊帝浹螢ー<br>笏・img alt=""zB128"" class=""ebec"" src=""ACD7681805EBF
0F9BD3345E9F56C122F.png""><br><span class=""ebul"">1</span> <span class=""ebul""
~略~

ちょっと見づらいが、4番目のフィールド、HTMLで記述された語義が生成されている。 各フィールドの文字数でもチェックしてみよう。

> java -jar wok-0.1.0.jar -f wok-scripts-0.1\cell-convert.wok -v@rawstr expr=v.size res2.tsv
"5"     "1"     "44"    "1081"
"4"     "1"     "44"    "10742"
"3"     "1"     "44"    "1536"

うまく生成できたようだ。これでデッキは完成。res2.tsvをAnkiで読み込み、mediaフォルダの中のファイルをAnkiのcollection.mediaにコピーすれば、作業は完了となる。

なお、その際、Anki側では適切なノートタイプを予め定義しておく必要がある。ここでは「単語」「ランク」「音声」「語義」からなるデッキを生成したので、ノートタイプも同様に。

おすすめの辞書コンテンツ

dessed にて変換可能なもの

音声あり、中辞典。Weblioを引くと表示されるのはだいたいこれ。

大辞典。英和。

研究社 新英和大辞典第6版
ロゴヴィスタ (2007-02-17)
売り上げランキング: 432

大辞典。和英。

コーパスとして。

コーパスとして。

NEW 斎藤和英大辞典
NEW 斎藤和英大辞典
posted with amazlet at 16.05.02
ロゴヴィスタ (2013-05-24)
売り上げランキング: 11,426

Androidの「メディア」によるバッテリードレインかどうかを確認する方法

Android 6.0にアップデートしたXperia Z4くんがバッテリードレイン病にかかってしまった。とりあえず原因を探り、対処方法を見つけたのでメモしておく。

原因を調べる

まず「設定」→「バッテリー」→「電池使用量」を調べる。 f:id:ruby-U:20160502123522p:plain

この時点で明らかに「メディア」が怪しい。

もう少し詳細に調べる

PCが手元にあれば、adbを使ってtopを見ることができる。 f:id:ruby-U:20160502123738p:plain

ここで、/system/bin/sdcardとあるので、外付けSDカードが原因だ、というわけではないことに注意。これはそういう名前のサービスで、詳しくは Configuration Examples | Android Open Source Project などを参照されたし。

さらに詳細に調べる

端末がrootedなら、adbからsuしてlsofなどを叩くこともできる。詳しくは Finding open files with lsof などを参照されたし。

今回の原因

/storage/sdcard0/DCIM/.thumbnailsを消すと、android.process.mediaの暴走が止まった。思い当たることといえば、

  • 最近Googleの「フォト」アプリを更新した
  • BitTorrentSyncを使用して、DCIMフォルダをPCと同期している
  • 同期したファイルに、破損したものがあった(Lightroomへのインポート時に警告が表示された)

ぐらい。ひとまず問題があれば、怪しいフォルダを探して、PCやクラウドにバックアップした後、top | grep media を眺めながら、サクッと消してみるのがよさそう