まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

本ブログエントリは IBM Cloud Advent Calendar 2019 に参加しています。12/03 ぶんのネタです。経緯を紹介する導入部分がちと長めですが、お付き合いください。

あまり公に宣伝したりしてなかったのですが、以前に「空耳アワー・マシーン」を作ったことがありました。「空耳アワー」はテレビ番組「タモリ倶楽部」の人気ミニコーナーのあれです。 「外国語で歌われた歌詞が日本語だとこう言っているように聞こえる・・・」という空耳にフィーチャーした紹介コーナーです。ちょっとググってみただけで多くの傑作作品動画が見つかります:
2019112100


この空耳は、曲に合わせて歌われる外国語の歌詞の中で、「日本語だとこう聞こえる」という特徴ある部分を見つけることで成立しています。ふと「・・・これって自動化できるんじゃないか?」と閃いたのが「空耳アワー・マシーン」誕生のきっかけでした。いわゆる "Speech to Text"(音声のテキスト化)機能を使い、外国語の歌唱部分を(その言語ではなく)強制的に日本語でテキスト化する、というロジックです。実装にあたっては IBM Cloud から提供されているWatson Speech to Text API を採用しました。ちなみに Watson Speech to Text はライトプランと呼ばれるプランで利用する場合であれば、各種カスタマイズを行うことはできませんが、毎月 500 分ぶんの音声データ変換までは無料で利用することができます(2019年11月現在)。

実装したソースコード(Node.js 用)はこちらで公開しています:
https://github.com/dotnsf/soramimi

2019112801


ソースコードを git clone するかダウンロードして、settings.js 内で Watson Speech to Text API 用の API キーを指定すると実際にウェブブラウザで動かすことができるようになります。


ウェブブラウザを起動してアプリケーションにアクセスすると、最初にブラウザがマイクの使用許可を求めてくるので「許可」します:
2019112101


画面を下の方にスクロールすると「どの言語でテキスト化するか」を選択するボックスがあります。「空耳アワー」の場合ここは日本語固定ですが、機能としてはここで指定する別言語でも認識→テキスト化を行うこともできるようになっています:
2019112102


言語を指定したらマイクを持った人のアイコン部分を1回クリックして、枠が赤になる(録音モードになる)ことを確認します。枠が赤になっている間に英語の歌をマイクから「歌ってください」(笑)。歌い終わったらもう一度人のアイコンをクリックします。枠は水色に戻り、録音モードも解除されます:
2019112103


録音モードからモードが解除されると同時に、録音された内容が Watson Speech to Text API に渡され、「日本語でどう聞こえたか?」の結果が表示されます:
2019112804



一応、動くには動いています(よね?)。このアプリではマイクを使ってその場で録音した音データを使って日本語テキスト化を行っていますが、理屈の上ではこの入力を外国曲の MP3 データなどに切り替えればいいはず・・・と考えていました。

・・・が、いざそのように使ってみると、仕組みとしては動くのですが、期待していたほどの精度では認識してくれませんでした。もともと Speech to Text 機能は「喋った内容のテキスト化」に最適化されたものであって、「歌った内容のテキスト化」ではないという違いに加えて、「BGMによるノイズ」の影響が多分にあるように感じられました。要するに曲の中で楽器で演奏される部分が Speech to Text 的にはノイズとなってしまい、思っていたほどの精度でテキスト化できなくなっているようだったのでした。 まあネタとしてはある程度使えるが、正しくテキスト化できる箇所が少なすぎて実用(?)にはほど遠い、というものになってしまったのでした。これも公開を控えていた理由の1つでもあります:
2019112802


さて、長い導入部分が終わりました。ここまでが「空耳アワー・マシーン」を「作ってみた」という話で、ここからが本エントリのタイトルでもある「改良してみた」部分です。最近、音楽ストリーミング事業を展開している Deezer 社から spleeter というものすっごいライブラリが公開されました:

無料で曲からボーカルが抜ける音楽素材分離エンジン「Spleeter」公開
2019112104


この spleeter は TensorFlow を用いた機械学習によって音源からボーカルやドラム、ベースといったパーツを抜き出して分離することができるようになる、というオープンソースなライブラリです。音楽ストリーミング事業を展開している Deezer 社の立場を最大限活用し、大量のサンプリングデータを所有している状態で機械学習を行うことができ、その成果をオープンソース化して公開していただいたことになります。ウェブ上のパフォーマンスや精度に関する評判もよいものばかりという印象です。

この spleeter を使うことで上述の空耳アワー・マシーンの BGM ノイズ問題を解決できるのではないか、と考えました。BGM が Speech to Text のノイズとなっていた問題に対して、spleeter で曲データからボーカルデータだけを抜き出しておいて、そのボーカルデータに対して Speech to Text を実行することでノイズのない音声データからのテキスト化が実現できるのではないか!? と考え、これも実装してみました:
2019112803


この機能を実装するためには、あらかじめサーバー上に spleeter を導入しておく必要があります。そして spleeter をインストールする際にはパッケージ管理ツールである conda が必要になります。というわけでまずは conda (今回は簡易版の miniconda)を導入します。このページから自分のシステム環境にあった miniconda のインストーラーを選択してダウンロードし、実行して導入します:
2019112101
(↑画面は Linux 版 Miniconda のダウンロードリンクを選択する箇所)

miniconda 導入後に spleeter を導入します。コードは github で公開されているので、以下の手順を実行します:
$ git clone https://github.com/deezer/spleeter

$ conda env create -f spleeter/conda/spleeter-cpu.yaml

これで spleeter の導入が完了しました。spleeter を利用するにはまず以下のコマンドで spleeter をアクティベートし、・・: 
$ conda activate spleeter-cpu

続けて spleeter をテスト実行します。spleeter フォルダの中にサンプルの mp3 ファイル(spleeter/audio_example.mp3)があるので、これを使って実行してみます。今回はボーカルとそれ以外(2stems)に分割し、実行結果を output フォルダ以下に作成する、というオプションを指定して実行してみました:
$ spleeter separate -i spleeter/audio_example.mp3 -p spleeter:2stems -o output

成功すると ./output/audio_example/ というフォルダ(フォルダ名はファイル名から生成される)が作成され、その中に vocals.wav と accompaniment.wav という2つのファイルが生成されています。vocals.wav がボーカルだけを抜き出したもの、accompaniment.wav がボーカルを抜き出した残りの音源です:
2019112102


実際に audio_example.mp3(元の曲)と、vocals.wav(ボーカルのみ) / accompaniment.wav(ボーカル以外)を聴き比べてみるとわかるのですが、信じられないくらいキレイにボーカルが分離されています。これが無料でできるとは驚き!

audio_example.mp3(元の曲)
vocals.wav(ボーカルのみ)
accompaniment.wav(ボーカル以外)



そしてこれを上述の空耳アワー・マシーンに組み込んでブラウザから実行できるように改良してみました。この拡張機能は上述のソースコードにも含まれているので、こちらの機能を試す場合はアプリケーション実行後に /spleeter.html にアクセスすると、mp3 の音楽ファイルを指定して実行することができます(結果は out/ フォルダ内に作成されます):


そして実行してみましたが・・・ 想定外だったのは処理時間でした。例えば5分程度の曲を対象にした場合、これまでは単に Watson Speech to Text を実行するだけで、その処理時間が1分を超えるようなことはなかったのですが、spleeter でのボーカル抜き出し処理だけで5分前後かかるようになりました(ちなみに自分の PC のスペックは i5-8350 の8コアCPUでメモリは 8GB)。これは処理内容を考えるとかなり高速な処理であるとは思いますが、これまでと比べると、1曲単位での処理にかなり長い時間がかかるようになった、という印象です。 またその長い処理の間、CPU やメモリはほぼフル稼働となります。仮にネット上から利用できるサービスとして作ろうとすると、かなり高いスペックのサーバーインスタンスを用意する必要があることに加え、タイムアウトとの戦いも意識して実装する必要がある、という印象を持っています。 といった背景もあって、現時点ではバッチでまとめて処理して後から結果を確認する、という方向での実装が中心です。


一方、この処理によって得られた結果は spleeter で分離する前とは比較にならないほど良質でした。Watson Speech to Text API は日本語としてある程度の確信度をもって認識出来なかった部分は結果に含まれない(つまり正しく認識できた部分のみが結果として得られる)のですが、分離前は「あー」とか「うー」とかいうコーラス部分を除くとほんの数箇所だけが識別できていたのですが、分離後の結果のサイズは数倍になりました。つまりボーカルだけを対象とすることで識別可能な音声が数倍になった、ということになります。なお上述の vocals.wav (ボーカルのみ抜き出したオーディオデータ)を Speech to Text で変換した結果は以下になりました:
{
  "status": true,
  "results": [
    {
      "alternatives": [
        {
          "timestamps": [
            [
              "令和",
              2.37,
              2.77
            ],
            [
              "OSS",
              2.77,
              3.77
            ],
            [
              "は",
              3.77,
              3.88
            ],
            [
              "萌え",
              5.96,
              6.25
            ]
          ],
          "confidence": 0.38,
          "transcript": "令和 OSS は 萌え "
        },
        {
          "transcript": "令和 OSS は 親 "
        },
        {
          "transcript": "映画 を SNS は 萌え "
        }
      ],
      "final": true
    }
  ]
}

↑確信度は 0.38 とあまり高くありませんが、データの 2.37 秒から 6.25 秒部分が赤字のように聞こえた、という結果です。


「本当に空耳アワーでそのまま即採用されるレベルの空耳が検出できるか?」という観点においては残念ながら難しそうな印象を持っています。特に Watson はエロネタは不得意っぽい(?)のか、結果にエロなワードは皆無でした。まあこのあたりは有料版の Watson Speech to Text でカスタマイズすると変わってくるのかもしれませんが・・・ ただそのまま採用されるようなレベルでの検出はできなかったとはいえ、ここで検出された結果をヒントにそれっぽいワードを混ぜて改良すればもしや・・・ という期待は充分に持てる結果になりました。一ヶ月に 500 分、1曲5分とすれば一ヶ月で約 100 曲ほど無料で空耳を調査できることになります。あのTシャツを狙って・・・はまだ遠い夢かもしれませんが、(個人的に欲しい)耳かきあたりは狙えるんじゃないか・・・と期待が膨らむ結果となりました。

Node.js + Express で作るウェブアプリケーションを HTTPS 対応し、更に「HTTPS 以外の通信を認めない」ように設定してみます。なお SSL 対応用のドメイン取得や証明書の取得は完了しているものとし、ドメインは mydomain.com 、秘密鍵ファイルと証明書ファイルがそれぞれ、
 ・/etc/letsencrypt/live/mydomain.com/privkey.pem (秘密鍵)
 ・/etc/letsencrypt/live/mydomain.com/cert.pem (証明書)
に存在しているものと仮定して以下を説明します。


【Node.js + Express で HTTPS 通信】
Node.js + Express で HTTPS 通信を行うには https モジュールを利用します:
var express = require( 'express' );
var app = express();

var fs = require( 'fs' );
var https = require( 'https' );
var options = {
  key: fs.readFileSync( "/etc/letsencrypt/live/mydomain.com/privkey.pem" ),
  cert: fs.readFileSync( "/etc/letsencrypt/live/mydomain.com/cert.pem" )
};
var https_server = https.createServer( options, app );

app.get( '/hello', function( req, res ){
  res.contentType( "text/plain" );
  res.writeHead( 200 );
  res.end( "Hello World." );
});

var https_port = 8443;

https_server.listen( https_port );

console.log( "server starging on " + https_port + ' ...' );

https モジュールを使って 8443 番ポートで待ち受ける https サーバーを作ります。その際に秘密鍵や証明書を指定することで SSL 通信を行うことができるようにしています。

このファイルを Node.js で実行して、ウェブブラウザや curl コマンドで https://mydomain.com:8443/hello にアクセスすることで動作を確認することができます(curl コマンドで localhost など、異なるドメインにアクセスする場合は $ curl https://localhost:8443/hello --insecure といった具合に --insecure オプションを付けて実行することで確認できます)。

また、上記の方法だと https のみアクセスできます(http だと接続エラー)が、http / https 両方でのアクセスに対応するには以下のように改良します:
var express = require( 'express' );
var app = express();
var settings = require( './settings' );

var fs = require( 'fs' );
var http = require( 'http' );
var https = require( 'https' );
var options = {
  key: fs.readFileSync( "/etc/letsencrypt/live/mydomain.com/privkey.pem" ),
  cert: fs.readFileSync( "/etc/letsencrypt/live/mydomain.com/cert.pem" )
};
var https_server = https.createServer( options, app );
var http_server = http.createServer( app );

app.get( '/hello', function( req, res ){
  res.contentType( "text/plain" );
  res.writeHead( 200 );
  res.end( "Hello World." );
});

var http_port = 8080;
var https_port = 8443;

https_server.listen( https_port );
http_server.listen( http_port );

console.log( "server starging on " + http_port + ' / ' + https_port + ' ...' );

これを実行すると、http を 8080 番ポートで、https を 8443 番ポートで同時に待受けてレスポンスを返すことができるようになりました。ウェブブラウザや curl コマンドで http://mydomain.com:8080/hello や https://mydomain.com:8443/hello にアクセスして挙動を確認してください。


【HTTP 通信をしない(HTTPS へ転送)】
最後に、上記プログラムを更に改良して HTTP 通信をしない(強制的に HTTPS 通信に転送する)ための設定を加えます。それには Strict-Transport-Security ヘッダを付けてレスポンスを返すことで実現できます:
var express = require( 'express' );
var app = express();
var settings = require( './settings' );

var fs = require( 'fs' );
var http = require( 'http' );
var https = require( 'https' );
var options = {
  key: fs.readFileSync( "/etc/letsencrypt/live/mydomain.com/privkey.pem" ),
  cert: fs.readFileSync( "/etc/letsencrypt/live/mydomain.com/cert.pem" )
};
var https_server = https.createServer( options, app );
var http_server = http.createServer( app );

//add HSTS
app.use( function( req, res, next ){
  res.setHeader( 'Strict-Transport-Security', 'max-age=15552000' );
  next();
});

app.get( '/hello', function( req, res ){
  res.contentType( "text/plain" );
  res.writeHead( 200 );
  res.end( "Hello World." );
});

var http_port = 8080;
var https_port = 8443;

https_server.listen( https_port );
http_server.listen( http_port );

console.log( "server starging on " + http_port + ' / ' + https_port + ' ...' );

HSTS(HTTP Strict Transport Security) という仕組みを強制することで HTTP でリクエストを受け取っても HTTPS に強制的に変更(リダイレクト)されて通信を続ける、というものです。リダイレクトさせるには HTTP そのものを閉じるわけにはいかない(リダイレクト直前のリクエストを受け取らないといけない)ので、このようなレスポンスヘッダによって実現しています:

2019112500



【参考】
良い感じにHTTPS対応したexpress(Node.js)サーバを立てる
node.jsによるHTTPSサーバの作り方

HTML5 Canvas を使って1年間かけて一周する「年間時計」を作ってみました:
https://dotnsf.github.io/yearclock/
2019112300


表示すると1分で一周する秒針と、1年で一周する年針(?)が表示されます。年針は針というよりはパイチャートの要領で面積で表示されます。ちなみに上図は 11 月後半に表示したものです。もうかなり進んでいますね。。

仕組みはそれほど複雑ではなく、HTML5 Canvas 内に円と弧を描画した上で秒針を setInterval() を使って1秒おきに描画しています。画像の透明度をうまく使って秒針の残像が残る形にしています。パックマン型に弧を描画する技術については先日公開したこちらのエントリを参照ください:
Canvas でパックマン型に塗りつぶした弧を描く


計算上では(うるう年でない年は)毎年 11月25日 の正午が一年間の 90% を経過する瞬間になります。今年も残すはあと1割!

このページのトップヘ