oinume journal

Scratchpad of what I learned

whisper.cppで文字起こしをやってみた

自分の備忘録としてのやってみた系の話。OpenAIが提供するモデルを使って文字起こしをするC++実装のwhisper.cppを試してみた。

環境

Apple M1 MacBook Air 2020 (Memory 16GB)

uname -a

Darwin hogehoge.local 22.3.0 Darwin Kernel Version 22.3.0: Mon Jan 30 20:39:35 PST 2023; root:xnu-8792.81.3~2/RELEASE_ARM64_T8103 x86_64

whisper.cppのビルド

以下のコマンドを実行してビルドする。

git clone https://github.com/ggerganov/whisper.cpp.git
cd whisper.cpp
make

ビルドが成功すると、カレントディレクトリに main という名前のバイナリが出来上がる。

モデルのダウンロード

次にモデルのダウンロードをしておく。

./models/download-ggml-model.sh large

文字起こしする音声ファイルのwavファイルを用意

  • whisper.cppはwavファイルしか受け付けないので、ffmpegで動画ファイルをwavにする。
  • ちょうど手元に会社で公開している勉強会の動画があったので、これを使ってみる
  • あと長いので最初の2分間だけを抽出。
ffmpeg -i ~/Downloads/2023-02-13.mp4  -ar 16000 -to 120 2023-02-13.wav

whisper.cppで文字起こし

先ほど出力したwavファイルからwhisper.cppを使って文字起こしする。

./main -m models/ggml-large.bin -f 2023-02-13.wav -l auto

注意点

  • -m でダウンロードしたモデルファイルを使う。largeだと精度が高い
  • -l auto を指定しないと日本語の文字起こししてくれないので指定する。もしくは-l jaでもOK

文字起こしの結果。ちなみに2分の音声ファイルを文字起こしするのに82秒ぐらいかかっている。

[00:00:00.000 --> 00:00:01.000]  お願いします
[00:00:01.000 --> 00:00:02.000]  お願いします
[00:00:02.000 --> 00:00:11.120]  はい 本日 Dpエンジニアリングまで 第60回始めていきたいと思います
[00:00:11.120 --> 00:00:12.120]  お願いします
[00:00:12.120 --> 00:00:13.120]  お願いします
[00:00:13.120 --> 00:00:14.120]  お願いします
[00:00:14.120 --> 00:00:21.840]  今日は React 18かな 18から新しく 出たディファードバリューっていう
[00:00:21.840 --> 00:00:28.440]  やつの紹介ですね もともとデバウンス とかスロットルみたいな感じで
[00:00:28.440 --> 00:00:33.840]  各種ライブラリーが似たような やつやってたんですけど そうじゃ
[00:00:33.840 --> 00:00:38.800]  なくて結構UIのアップデートで 最適化したやつが出ましたよっていう
[00:00:38.800 --> 00:00:44.320]  ので もともとデバウンスっていう 本当はスロットルも出したかったん
[00:00:44.320 --> 00:00:49.200]  ですけど React 18でスロットルのやつ がバグっていてとあるライブラリー
[00:00:49.200 --> 00:00:54.000]  でパッてデモが用意できなかったん ですけど 一応デバウンスだけ紹介
[00:00:54.000 --> 00:01:04.120]  しておくと 18入力した後に時間差 で18入力されるという 今これ1秒
[00:01:04.120 --> 00:01:09.680]  かな 1秒ってやってるので 最後に 入力した後1秒後に値が更新される
[00:01:09.680 --> 00:01:15.400]  ってやつですね 連続で入力してる 間はデバウンスバリューは変わん
[00:01:15.400 --> 00:01:20.840]  ないけれど 止めてから1秒すると デバウンスバリューは変わる これ
[00:01:20.840 --> 00:01:27.120]  何使いたいかっていうと インクリメンタル サーチとか オリジンのところを
[00:01:27.120 --> 00:01:31.600]  見てもらうと 一文字変わるごとに サーチ検索API叩きまくってると
[00:01:31.600 --> 00:01:38.320]  重すぎるし 重すぎるので デバウンス みたいにユーザーの操作が終わった
[00:01:38.320 --> 00:01:44.560]  後にAPI一回叩くみたいな感じで やってあげるっていう スロットル
[00:01:44.560 --> 00:01:50.280]  の場合は1秒ごとに発火される っていうイメージなので 例えば
[00:01:50.280 --> 00:01:55.240]  デバウンスバリューはこれ 最後 操作終わるまではデバウンスバリュー
[00:01:55.240 --> 00:02:00.040]  更新されないですけど スロットル だった場合は1秒ごとに発火されて

最後に会社の勉強会の宣伝

ドクターズプライムでは、DP Engineering Mondayという名前で毎週勉強会をやってます。月に1回、社外向けに開催したものをYouTubeで配信しているので、よかったらチャンネル登録してください。技術スタックはReact, TypeScript, ChakraUI, Go, GCP, Hasura, PostgreSQLなので、その周辺の話が多いです。

www.youtube.com

2023年に読みたい本

最近読書量が減ってきたので、今年は読書を頑張ろう!と思ったのでまずは読みたい本をリストアップしてみる。

プロを目指す人のためのTypeScript入門

仕事でTypeScript書いてるんだけど、型システムが難しすぎて詰まっているので、体系的に理解したい。

データ指向アプリケーションデザイン

分厚いけど良書だと噂の本。

モダン・ソフトウェアエンジニアリング

これも良書だと噂なので。

A Philosophy of Software Design, 2nd Edition

いい加減読み終わりたい...

詳解 システム・パフォーマンス 第2版

めちゃくちゃ評判がいい本なので。

LIFESPAN(ライフスパン): 老いなき世界

最近老いが気になってきたので...

AlfredからRaycastに移行した

最近はRaycastが巷で流行っているので、自分もAlfredから乗り換えてみた。Alfredはv5への移行をずっと保留にしていてタイミング的にも良かったというのが一番大きい。ちなみに自分はAlfredの有料課金のライトユーザーで、以下の機能を使っていた。

  1. 通常のランチャー機能
  2. HotKey - よく使うアプリをHotKeyとして登録
  3. Clipboard history - クリップボードの履歴
  4. ブラウザのブックマーク検索

ライトユーザーなので、とてもスムーズに移行できた。

また、Raycastの拡張機能でいいなと思ったのは

  1. Screenshotの検索とコピーが簡単にできる
  2. Emojiの検索が簡単にできる
  3. カレンダー連携すると直近のMeeting URLにRaycastから飛べる
    • なので、「あ、Meetingだ!」って思ったらRaycastを開いておけばいい、という体験が良い
  4. Quicklinkの機能で簡単に指定のURLに飛ぶリンクが作成できる

の2点。とりあえず色んな拡張がStoreにあるので、有用そうなものを日々探してインストールしている。まだ使い始めて3日ぐらいだけど、そろそろAlfredはアンインストールしようかなと思うぐらい馴染んでいる。

OpenTelemetryとhttptrace.ClientTraceを使ってHTTPリクエストのlatencyを可視化する

この記事は OpenCensusとhttptrace.ClientTraceを使ってHTTPリクエストのlatencyを可視化する - oinume journal のOpenTelemetry版。OpenTelemetryについては OpenTelemetryとは何か、そしてなぜそれが計装器の未来なのか? | New Relic を見てもらうのが手っ取り早くて、httptrace.ClientTraceについては先のブログを見てもらえればと。

OpenTelemetry Tracing + httptrace.ClientTrace

早速本題に入ってしまうと、以下のようなコードを書くことでhttp.ClientでHTTPリクエストを送った際のlatencyを可視化することが可能になる。

以下のように otelhttptrace.NewClientTraceで生成したClientTraceをctxにセットし、そのctxをhttp.Requestにセットするだけで良い。

clientTrace := otelhttptrace.NewClientTrace(ctx)
ctx = httptrace.WithClientTrace(ctx, clientTrace)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
...

Jaeger UIでの確認

https://github.com/oinume/go-http-client-trace-sample/ のリポジトリをcloneしてdocker-compose up -dするとJaegerが立ち上げることができる。

$ git clone https://github.com/oinume/go-http-client-trace-sample
$ docker-compose up -d
$ go run ./examples/otelhttp_client_trace/main.go

のように、Jaegerを立ち上げてサンプルコードを実行するとJaegerのexporterにトレースの情報が送られる。そのトレースの情報は http://localhost:16686/search にブラウザでアクセスすると見ることができる。以下のスクショを見てもらうとわかるように、http.getconnhttp.dnshttp.tlsなど、httptrace.ClientTraceのコールバックメソッドごとにSpanが作成され、それがどのぐらい時間がかかっているのかがわかるようになっている。

OpenCensusとOpenTelemetryの違い

httptrace.ClientTraceに関わるところで、OpenCensusとOpenTelemetryで微妙に挙動が違うところがあったので、メモしておく。

OpenCensusの場合はデフォルトではhttp.getconnhttp.dnsなどの情報がSpanにならないが、OpenTelemetryの場合はそれぞれSpanになっている。この挙動で注意した方が良いのは、例えばGCPのCloud Traceだとスパン単位で従量課金されるので、場合によっては金額が大きくなってしまうかもしれない。しかし、以下のようにWithoutSubSpansを指定することでSpanを生成しないようにすることができる。

clientTrace := otelhttptrace.NewClientTrace(ctx, otelhttptrace.WithoutSubSpans())

最後に

OpenTelemetryのTracingについては各言語のSDKも揃ってきて、クラウドベンダーの対応もされてきているので、そろそろ実戦投入していこうと思った。

Chapter 5 - Information Hiding (and Leakage) / A Philosophy of Software Design

第5章はInformation Hiding (and Leakage)というタイトル。いいモジュールを作るには情報の隠蔽(カプセル化)を行うことが大事だよ、という話。

5.1 Information hiding

  • Deep Moduleを作る上で最も大事なのは情報の隠蔽
  • モジュールを実装する上で必要なメカニズムをinterfaceとして表には出さないこと
    • ex)
      • B-treeにどういう情報を保存するか
      • ファイルの中身が物理的なディスクのブロックのどこに存在するかを認識する方法
      • TCPプロトコルの実装の詳細
      • マルチコアCPUでのスレッドの実装
      • JSONをどうやってパースしているか
  • 情報の隠蔽は2つの方法で複雑性を減らす
    • 情報の隠蔽はモジュールのインターフェースをシンプルにする
      • 具体例) B-tree classを開発者が使うときに、理想的なノードの広がりやツリー内部の均衡化については、利用者は気にしなくて良い
    • 情報の隠蔽はシステムの発展を容易にする
      • 情報がモジュール外には隠蔽されている=(モジュール外からみて)その情報への依存性がなくなっているので、その情報に関する内部的な設計の変更の影響はモジュール内部のみである
        • 例えば、TCPでネットワーク混雑時の制御についてプロトコルの仕様が新しく追加されても、TCPのデータの送受信のコードには影響がないはずである

5.2 Information leakage

  • 直訳すると情報の漏洩。情報の隠蔽の反対の概念
  • これはモジュール間の依存を作り出してしまう
  • 情報の漏洩はインターフェースだけで起こるものではない
    • 例えば特定のファイルフォーマットを扱う2つのクラスがあり、片方がファイルを読み込み、もう片方が書き込みをするような場合。ファイルのフォーマットが変更された時は両方のクラスを修正する必要がある。
  • 情報の漏洩もまたred flag(危険信号)
  • 情報の漏洩をどうやって防ぐか?
    • 先ほどの2つのクラスの例であれば、クラスが小さくて密接に関連しているのであればマージして1つにしてしまうのが良い
    • 情報をpull(取得)してカプセル化するクラスを新しく作る、という方法もある。
      • Another possible approach is to pull the information out of all of the affected classes and create a new class that encapsulates just that information. However, this approach will be effective only if you can find a simple interface that abstracts away from the details; if the new class exposes most of the knowledge through its interface, then it won’t provide much value (you’ve simply replaced back-door leakage with leakage through an interface).

5.3 Temporal decomposition

  • 時間による分解
  • Temporal decompositionの考え方で「ファイルを読み込んで、その内容を変更して、新しいバージョンを書き出す」というアプリケーションを考えると、3つのクラスに分解される。
    • ファイルを読むクラスと新しいバージョンを書き出すクラスはそれぞれファイルのフォーマットを知っている必要があるので、ここで情報の漏洩が発生する
    • 情報の漏洩を防ぐには、ファイルを読むクラスと書き込むクラスは同じものにする必要がある
  • 時間による処理の順番でクラスを分解するのではなく、そのクラスが行う処理が知識として何を持っているべきか(先程の例だとファイルのフォーマット)に着目してクラスを分解するべき

5.4  Example: HTTP server

  • 生徒がHTTPプロトコルの実装をした際のデザインの意思決定について、実際の例を交えて考察
  • HTTPプロトコル説明の図

5.5 Example: too many classes

  • 生徒がよくやってしまう間違いとしては、たくさんのShallow Classに分割してそのクラス間での情報漏洩をもたらしてしまうこと
    • 例) ネットワークコネクションからHTTPリクエストを読み込んでstringに変換するクラスを作り、別のクラスでそのstringをパースする
      • Temporal Decompositionのダメな例
      • 例えばContent-Length headerでbodyの長さが指定されているので、Bodyの長さを計算するにはまずContent-Lengthをパースしないといけない
        • stringになったリクエストをパースする時にBodyの長さがわからないとパースできない
        • コードの具体例がなかったのでちょっとここはよくわからなかった
      • 結果として、2つのクラスに分割してもそれぞれがHTTPリクエストの構造を知っていないとダメな実装になっている
      • さらに呼び出し側も2つのクラスの使い方を知っている必要が出てくるので、呼び出し側に余計な認知負荷がかかる
    • よって、上の例ではリクエストを読み込むクラスとパースするクラスは一つにマージした方が良い
      • HTTPリクエストをパースという処理を1クラスに集約することで情報の漏洩が発生しない
      • クラスの数を減らすことで利用者から見たインターフェースはシンプルになる

5.6 Example: HTTP parameter handling

  • HTTP requestがサーバーに来ると、サーバーはリクエストの情報を参照する必要がある
  • 例えば、以前のFigure 5.1で言うとphoto_idというリクエストパラメーター
    • パラメーターはURLのQuery Stringとして送られてくることもあれば、ボディで送られてくることもある
    • Query Stringの場合はURLEncodeされているが、処理する時にはこれはデコードされている必要がある
  • 著者の生徒の大半は以下の良い情報の隠蔽を行なっていた
    • パラメータはQuery StringまたはBody経由で送信されてくるが、それを利用する人にはどちらから来ているかは重要ではないのでマージしている
    • 2つ目の良いところは、パラメーターのデコードについて利用者が意識しなくて済むようにしていること
  • ただし、生徒が作るモジュールは以下の点でShallow Moduleだった
    • HTTPRequestというクラスを作ってその中に Map<String, String> getParams() というメソッドを定義していた
    public Map<String, String> getParams() {
      return this.params;
    }
  • Map<String, String> という内部のデータ型を利用者に公開するのではなく、String getParam(String name) という形にした方が、仮に内部のデータ構造を変えたいときに利用者に影響を与えずに済む
  • パフォーマンスチューニングのために内部のデータ構造を変更することもあるので、なるべく内部のデータ構造は利用者からは見えないようにした方が良い
  • あとはこのMapを利用者側で変更できないようにImmutableにする必要があるなどの問題もある

5.7  Example: defaults in HTTP responses

  • HTTP Responseに関する、生徒たちがよくやる不適切なデフォルト値について説明
  • HTTP ResponseのHTTPのバージョンに関して
    • (生徒の)あるチームはメソッドの呼び出し元にHTTPのバージョンを指定させるようにしていた
    • しかし、HTTPのバージョンはリクエストで指定されているものと一致しているべき
    • なので、リクエストのHTTPバージョンをそのままレスポンスのバージョンとすることが望ましい
    • 呼び出し元がHTTPのバージョンを指定すると、このHTTPのライブラリと呼び出し元の間で情報の漏洩が発生してしまう
  • HTTPレスポンスに必要なものとしてDateヘッダーもある
    • これもHTTPライブラリが気の利いたデフォルト値を提供すると良い
    • もし呼び出し元が何かしらの理由でDateヘッダーをオーバーライドしたい場合は、専用のメソッドを用意する
    • クラス or モジュールはできる限りいい感じのデフォルト値を用意する
    • P.26のJavaのIOバッファリングは、この点でよくない例となっている
      • IOライブラリのほとんどのユーザーはバッファリングするのでそれがデフォルトになっているべきだが、Javaの標準ライブラリではバッファリングのために別のクラスを使ってラップする必要がある
    FileInputStream fileStream = new FileInputStream(fileName);
    BufferedInputStream bufferedStream = new BuffferedInputStream(fileStream);
  • Red Flag: Overexposure(過度の露出)
    • よく使われるAPIが、たまにしか使われない機能もユーザーに覚えさせるようにしてしまうと、認知負荷を増大させてしまう(意味がよくわからなかった)

5.10 Conclusion

  • 情報の隠蔽とDeep Modulesは密接に関連している
  • モジュールがたくさんの情報を隠蔽することで、インタフェースをシンプルにしつつ多くの機能が提供される
  • システムをモジュールに分解するときに、実行時に行われる処理の順番を意識しないようにすること
    • 以前に説明したTemporal decompositionが発生し情報の漏洩やShallow modulesの問題を引き起こす
    • モジュールが知っているべき知識を考慮してそれを各モジュールがカプセル化することで、Deep modulesを作ることができる