Bumblebeeのwhisperを使ってSpeech to Text

# はじめに
本記事はLivebook上でBumblebeeからwhiperのネットワークを構築して、実際に文字起こしを行う方法を解説します。

# Livebookとは

https://livebook.dev/

Elixirで実装されたインタラクティブなノートブックアプリケーションで
コードやデータ処理の自動化を目標としています

> Automate code & data workflows with interactive notebooks

対話型ノートブックでコードとデータのワークフローを自動化する

> Get rid of scripts, manual steps, and outdated docs. Start using Elixir and Livebook to share knowledge, explore code, visualize data, run machine learning models, and much more!

スクリプト、手作業による手順、古くなったドキュメントから解放されましょう。ElixirとLivebookを使って、知識の共有、コードの探索、データの可視化、機械学習モデルの実行など、さまざまなことを始めましょう！

最新の0.9.0ではノートブックをWebアプリケーションとしてデプロイできるそうです

https://qiita.com/RyoWakabayashi/items/9cd0032d1bd4ddad5d6c

# Bumblebeeとは

https://github.com/elixir-nx/bumblebee

Elixrの深層学習フレームワークのAxonでhuggingfaceの学習済みTransformerを簡単に動かすライブラリ

# whsiperとは

https://openai.com/research/whisper

Whisperは、OpenAIが文字起こしサービスとして公開した無料の音声認識モデルです。WhisperはWebから収集した68万時間分の多言語音声データを教師付きデータで学習させており、高い精度で入力した音声を文字起こしすることが可能になっています。
by https://aismiley.co.jp/ai_news/what-is-whisper/

# 文字起こしノートブックの作成
ではLivebook上で文字起こしを行うプログラムを作成していきます

## ライブラリのインストール
setupセルに以下を追加してください

```elixir
Mix.install(
  [
    {:kino_bumblebee, "~> 0.3.0"},
    {:exla, "~> 0.5.3"}
  ],
  config: [nx: [default_backend: EXLA.Backend]]
)
```

## SmartCellでSpeech to text
LivebookにはSmartCellという機能があっていろいろな便利ウィジェットを提供しています

SmartCellのNeural Network taskを選択して 
taskを Speech to text
モデルを Whisper(small multilingual)
を選択して、英語の音声ファイルをアップロード
Runを実行すると10秒ほどたってテキストが出力されます

![スクリーンショット 2023-03-28 13.33.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/b1457105-24ad-d56b-1f62-dcba53b888ad.png)

テキスト書き起こしてができました！

日本語はどうでしょうか？

![スクリーンショット 2023-03-28 13.44.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/8052ae51-1b18-d50e-3482-b3f9b9108aa8.png)

英語ではないとは認識できてますが、出力はしてくれないようです・・・

## 日本語でtransscribe(書き起こし)

このissueによるとトークナイザーに細工をして英語以外で書き起こしをしてくれるようになります
この例はスペイン語なので日本語のjaにすれば日本語になります

※ 2023/05/23更新
`speech_to_text`に直接トークンの設定を入れていましたが、　0.3からできなくなっていて
`Bumblebee.configure`で設定を行います

https://github.com/elixir-nx/bumblebee/pull/107#issuecomment-1435388630

```elixir
{:ok, whisper} = Bumblebee.load_model({:hf, "openai/whisper-small"})
{:ok, featurizer} = Bumblebee.load_featurizer({:hf, "openai/whisper-small"})
{:ok, tokenizer} = Bumblebee.load_tokenizer({:hf, "openai/whisper-small"})
{:ok, generation_config} = Bumblebee.load_generation_config({:hf, "openai/whisper-small"})

generation_config = Bumblebee.configure(
  generation_config,
  max_new_tokens: 100,
  forced_token_ids: [
      {1, Bumblebee.Tokenizer.token_to_id(tokenizer, "
")},
      {2, Bumblebee.Tokenizer.token_to_id(tokenizer, "
")},
      {3, Bumblebee.Tokenizer.token_to_id(tokenizer, "
")}
    ]
)

serving =
  Bumblebee.Audio.speech_to_text(whisper, featurizer, tokenizer, generation_config,
    compile: [batch_size: 1],
    defn_options: [compiler: EXLA]
  )
```

## SmartCellのカスタマイズ
SmartCellは実際のコードをtoggle souceを押してUIとコードの表示を切り替えができます

![スクリーンショット 2023-03-28 14.53.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/2505215f-5d20-641a-09af-fdbe497030e3.png)

もちろんコピーできるので新しいセルにコピペして日本語書き起こしができるようにします

```diff_elixir
{:ok, model_info} = Bumblebee.load_model({:hf, "openai/whisper-tiny"})
{:ok, featurizer} = Bumblebee.load_featurizer({:hf, "openai/whisper-tiny"})
{:ok, tokenizer} = Bumblebee.load_tokenizer({:hf, "openai/whisper-tiny"})
{:ok, generation_config} = Bumblebee.load_generation_config({:hf, "openai/whisper-tiny"})
- generation_config = Bumblebee.configure(generation_config, max_new_tokens: 100)
+ generation_config = Bumblebee.configure(
+  generation_config,
+  max_new_tokens: 100,
+  forced_token_ids: [
+      {1, Bumblebee.Tokenizer.token_to_id(tokenizer, "
")},
+      {2, Bumblebee.Tokenizer.token_to_id(tokenizer, "
")},
+      {3, Bumblebee.Tokenizer.token_to_id(tokenizer, "
")}
+    ]
+)


serving =
  Bumblebee.Audio.speech_to_text(model_info, featurizer, tokenizer, generation_config,
    compile: [batch_size: 1],
    defn_options: [compiler: EXLA]
  )

audio_input = Kino.Input.audio("Audio", sampling_rate: featurizer.sampling_rate)
form = Kino.Control.form([audio: audio_input], submit: "Run")
frame = Kino.Frame.new()

Kino.listen(form, fn %{data: %{audio: audio}} ->
  if audio do
    Kino.Frame.render(frame, Kino.Text.new("Running..."))

    audio =
      audio.data
      |> Nx.from_binary(:f32)
      |> Nx.reshape({:auto, audio.num_channels})
      |> Nx.mean(axes: [1])

    %{results: [%{text: generated_text}]} = Nx.Serving.run(serving, audio)
    Kino.Frame.render(frame, Kino.Text.new(generated_text))
  end
end)

Kino.Layout.grid([form, frame], boxed: true, gap: 16)
```

![スクリーンショット 2023-03-28 15.00.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/b8f9d422-de2f-c013-49ec-aca8cfc454e9.png)

無事日本語書き起こしができるようになりました！

## シーケンシャルに全文を書き起こし
whisperは最初の30秒の1文しか文字起こしを行ってくれないので、ffmpegでぶつ切りにして文字起こしをすることで1ファイル全ての文字起こしを行います。

こちらを参考にしていきます。

https://phoenixframework.org/blog/whisper-speech-to-text-phoenix
### モデルの構築
small, ja, transcribeでservingを作ります
```elixir
{:ok, whisper} = Bumblebee.load_model({:hf, "openai/whisper-small"})
{:ok, featurizer} = Bumblebee.load_featurizer({:hf, "openai/whisper-small"})
{:ok, tokenizer} = Bumblebee.load_tokenizer({:hf, "openai/whisper-small"})
{:ok, generation_config} = Bumblebee.load_generation_config({:hf, "openai/whisper-small"})

generation_config = Bumblebee.configure(
  generation_config,
  max_new_tokens: 100,
  forced_token_ids: [
      {1, Bumblebee.Tokenizer.token_to_id(tokenizer, "
")},
      {2, Bumblebee.Tokenizer.token_to_id(tokenizer, "
")},
      {3, Bumblebee.Tokenizer.token_to_id(tokenizer, "
")}
    ]
)

serving =
  Bumblebee.Audio.speech_to_text(whisper, featurizer, tokenizer, generation_config,
    compile: [batch_size: 1],
    defn_options: [compiler: EXLA]
  )
```

### 音声ファイルの再生時間を取得する
最初にファイルの時間を取得します。
`Kino.Input.audio`にアップロードすると表示されるのでそれでもいいですが、
プログラム的に算出する方法は以下になります。
動画の方はLiveBeatsのモジュール使ってるのですがライブラリ化されていないのとコピーするにもコードが長いです。

Kino.Input.audioで音声ファイルのアップロードフォームを作ります。
単体のセルでもいいですし上のモデルのセルに足しても良いです

```elixir
audio_input = Kino.Input.audio("audio", sampling_rate: featurizer.sampling_rate)
```

```elixir
value = Kino.Input.read(audio_input)
sec = 
  value.data
  |> Nx.from_binary(:f32)
  |> Nx.shape
  |> elem(0)
  |> div(featurizer.sampling_rate)
  |> div(value.num_channels)
```

### ファイルパスを取得する

ffmpegで処理をするためファイルパスを取得します

```elixir
file_input = Kino.Input.file("audio")
```

```elixir
value = Kino.Input.read(file_input)
path = Kino.Input.file_path(value.file_ref)
# ファイル名が表示されなかったら絶対パスを貼り付ける
# path = "/Users/shou/phoenix/livebook/piyopiyo_nako.mp3"
```

ファイルによってはアップロードしても反応しない場合があるので、そのときは絶対パスで指定してください

`Kino.Input.audio`でアップロードした場合はアップロード後ファイルが削除されるようなので、
新た`にKino.Input.file`でアップロードするようにしています

### ffmpegで分割して書き起こし

`Task.async_stream`を使って4つずつ並行して書き起こしを行います
`System.cmd`で`ffmpeg`を実行し
返ってきたバイナリデータでBumblebeeのwhiperを`Nx.Serving.run`で実行します
おまけに書き起こし中のデータを tap内のinspectで表示していきます
最後に全体の結果をソートし、`Enum.map`でテキストだけ抽出しています。

```elixir
0..sec//10
|> Task.async_stream(
  fn ss ->
    args = ~w(-ac 1 -ar 16k -f f32le -ss #{ss} -t 10 -v quiet -)
    {data, 0} = System.cmd("ffmpeg", ["-i", path] ++ args)

    {ss, Nx.Serving.run(serving, Nx.from_binary(data, :f32))}
    |> tap(fn {ss, res} ->
      res
      |> Map.get(:results)
      |> List.first()
      |> Map.get(:text)
      |> then(&IO.inspect({ss, &1}))
    end)
  end,
  max_concurrency: 4,
  timeout: :infinity
)
|> Enum.map(fn {:ok, {ss, %{results: [%{text: text}]}}} -> {ss, text} end)
|> Enum.sort_by(&elem(&1, 0))
|> Enum.map(&elem(&1,1))
```

# demo
それでは実際に実行してみましょう

![14deb8ec8c8eb897d4439d15d36835aa.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/d78b5d32-e6ff-b4d0-d425-d71ec442dc85.gif)

CPUを使用してsmallのモデルで9分の音声が3分ほどで書き起こしが完了しました

# 最後に
Bumblebeeのwhisperで日本語音声の文字起こしを行うプログラムを作成しました

今回はsmallかつマルチリンガルなので制度はそこまで良くないですがチューニング　+　large　+　GPUで動かせば結構良さそうなので出来そうですね

今回はKino.Inputを使って音声ファイルのデータ等を取得していましたが

https://github.com/lawik/membrane_transcription

こちらのプロジェクトでマルチメディアに特化したライブラリのMembraneを使用していたりします
大きなライブラリなのでキャッチアップにも時間がかかりそうで今回は使いませんでしたが、今後機会があれば使ってみたいですね

https://membrane.stream/

本記事は以上になりますありがとうございました

# 参考サイト
https://livebook.dev/
https://phoenixframework.org/blog/whisper-speech-to-text-phoenix
https://qiita.com/RyoWakabayashi/items/9cd0032d1bd4ddad5d6c
https://github.com/elixir-nx/bumblebee
https://openai.com/research/whisper
https://aismiley.co.jp/ai_news/what-is-whisper/
https://membrane.stream/
https://github.com/lawik/membrane_transcription