かおるノート

cordx56のブログです

発言内容を自動で文字起こしするDiscord botを作った話

こんにちは。 学生なので夏休みに入りました。 皆様いかがお過ごしでしょうか。

今回はDiscordの発言内容を自動で文字起こしするDiscord botを書いたので、それについて技術的にはまった点などを備忘録的に書いていきたいと思います。

そもそも何故そんな需要があったかというと、今の私のインターン先でチームで開発をしているのですが、その会議を文字起こししたいという話があったからでした。

今回のbotはNode.js + TypeScriptで記述しました。 普段の私であれば使用言語にPythonを選択するのですが、discord.pyよりもdiscord.jsの方が音声受信周りが充実していたのと、TypeScriptの方が型周りの開発体験が良かった1ので、TypeScriptを書くことにしました。

実際に作ったものはこちらです。

github.com

では詳細を見ていきましょう。

Discordの音声を受信してWAVファイルにする

Discordの音声受信はユーザごとに音声を受信します。

今回はユーザが一通りしゃべり終わったタイミングでしゃべった内容を文字起こしにかけ、リアルタイムにその内容をテキストチャンネルに投稿したいので、VoiceConnectionspeakingイベントを監視して、speakingイベント発火時に録音を開始、createStreamの結果得られたReadableStreamがendの時に取得した録音データを文字起こしにかけ、その結果を発言ユーザの名前と共に開始コマンドが入力されたテキストチャンネルに投稿する、という処理を行っています。

詳しくはこのあたりを参照していただければわかるかと思います。

Google Cloud Speech-to-Text APIを使って文字起こし

文字起こしには、精度がそれなりによいGoogle Cloud Speech-to-Textを利用しました。 と書きましたが、AWSのTranscribeも結構良さげですね。 今まさに悩んでいます2

まぁとりあえずGoogle Cloud Speech-to-Textで組んだのでその辺の説明をしようと思います。

Google Cloud Speech-to-Textでは、いくつかのファイル形式に対応していますが、WAVが一番扱いやすいと思います。 discord.js側が用意してくれるのは32bitステレオなWAVファイルですが(これについては後述)、Google Cloud Speech-to-Textに渡すためには16bitモノラルなWAVファイルを用意してあげる必要があり、fluent-ffmpegというパッケージを利用してNode.js側からffmpegを叩いて変換をしました。

詳細はsrc/speech.tsを参照してください。

遭遇した問題

ここからは実際に遭遇して、解決が難しかった問題について書いていきます。

discord.jsが実際に吐き出すWAVファイルがガイドの形式と異なる

このことはどのドキュメント、Webサイトにも載っていなかったので確かではないのですが、このガイドに「Signed 16-bit PCM」と書いてあるのに対し、手元で実行した結果得られたファイルをAudacityにかけてみると、32bitにしたところ正常に再生されました。 何らかの仕様が変わってそうなった可能性が高そうな気がしますが、これはどこが悪いのかわからない(そもそもDiscord側からはopus形式で来ていて、それを手元でWAVに変換しているっぽい)ので、何とも言えません。 とにかく、fluent-ffmpegの読み込み形式を s32le にしたところ正常に音声認識されるようになったので、そのようにしました。

一定時間が経過するとVoiceConnectionのspeakingイベントが発火しなくなる現象への対処

このこともどのドキュメント、Webサイトにも載っていなかったので確かではないのですが、VoiceConnectionを張って一定時間経過すると、speakingイベントが発火しなくなる現象がこちらの手元で起きました。 これへの対処としては、一定時間毎に無音をplayしてやることで回避することができました。

これらの問題は二つともなんか腑に落ちませんが、まぁこの対処で動いているので良しとしました。 動くことこそ正義3

おわりに

今回は簡単にですが、自動で発言内容を文字起こしするDiscord botについて説明しました。

Google Cloud Speech-to-TextやAmazon Transcribeの精度は実用には厳しいですが、非常に使いやすいものにはなってきているかと思います。 今回はDiscord botとして実装しましたが、ほかにも活用法を検討中です。 文字起こしを使ってこういうサービスが欲しい!などあればぜひお話をお聞かせください。


  1. Pythonの漸進的型付け界隈はもうちょっと頑張ってほしいという気持ちがあります。いや私はその辺の研究をしているので、お前が頑張れという話なのですが……

  2. 私は基本的に業務でも趣味でもAWSを使っているので、GCPを使うよりはAWSを使った方が楽といった事情があります

  3. そうかな?

inkwellを使ってRustでLLVMをやっていく

こんにちは。 そろそろ夏ですね。

今回の記事はRustでLLVMをやっていくことができるクレート、inkwellについてです。 inkwellを紹介している記事自体はあるのですが、実際にinkwellを利用して何かを作ってる記事が少なかったのでこの記事を書きました。

LLVM自体はそれなりに情報はあり、またきつねさん本などもあることから、トラブルシューティングはしやすいほうだと思います。 とは言いつつLLVMに対しては触りながら知見をためていったこともあり、様々の理解に少し時間がかかってしまいました。 この記事や私の書いたソースコードが少しでもRustでLLVMをやろうとする人の役に立てば幸いです。

inkwellを利用して実際に作ったものはこちらです。

github.com

小さな独自のLISP方言をLLVM IRにコンパイルするプログラムです。 まだ全然関数などがそろっていないため実用には程遠いです。

inkwell

inkwellの公式ドキュメントはこちらです。

inkwell自体はllvm-sysを安全に(unsafeで囲う必要がなく)、Rust風のAPIで使えるようにしたものです。

導入

LLVMの導入

まずはLLVM自体を導入してあげる必要があります。 LLVMのバージョンを指定しての導入にはllvmenv1がおすすめです2

依存ビルドツールにcmake、make、ninja、g++/clang++があるので、これらをインストールします。 あとは次のコマンドで導入できます。

$ cargo install llvmenv
$ llvmenv init
$ llvmenv build-entry 12.0.0

inkwellの導入

inkwellを導入は、Cargo.tomlのdependenciesに以下を記述してあげるだけです。

inkwell = { git = "https://github.com/TheDan64/inkwell", branch = "master", features = ["llvm12-0"] }

featuresは利用したいLLVMのバージョンに合わせて変更します。詳しくは inkwellのREADME を見てください。

inkwellを使うためには、環境変数llvm-sysにLLVMの場所を教えなくてはいけません。 fishでは次のようにして場所を指定します(各々のシェルに応じてコマンドは変えてください)。

$ set -x LLVM_SYS_120_PREFIX (llvmenv prefix)

これでinkwellの導入まで完了です。

実際に動かしてみる

では実際にinkwellを用いてLLVMのプログラムをビルドし動かしてみましょう。

use inkwell::context::Context;
use inkwell::OptimizationLevel;

fn main() {
    let context = Context::create();
    // moduleを作成
    let module = context.create_module("main");
    // builderを作成
    let builder = context.create_builder();

    // 型関係の変数
    let i32_type = context.i32_type();
    let i8_type = context.i8_type();
    let i8_ptr_type = i8_type.ptr_type(inkwell::AddressSpace::Generic);

    // printf関数を宣言
    let printf_fn_type = i32_type.fn_type(&[i8_ptr_type.into()], true);
    let printf_function = module.add_function("printf", printf_fn_type, None);

    // main関数を宣言
    let main_fn_type = i32_type.fn_type(&[], false);
    let main_function = module.add_function("main", main_fn_type, None);

    // main関数にBasic Blockを追加
    let entry_basic_block = context.append_basic_block(main_function, "entry");
    // builderのpositionをentry Basic Blockに設定
    builder.position_at_end(entry_basic_block);

    // ここからmain関数に命令をビルドしていく
    // globalに文字列を宣言
    let hw_string_ptr = builder.build_global_string_ptr("Hello, world!", "hw");
    // printfをcall
    builder.build_call(printf_function, &[hw_string_ptr.as_pointer_value().into()], "call");
    // main関数は0を返す
    builder.build_return(Some(&i32_type.const_int(0, false)));

    // JIT実行エンジンを作成し、main関数を実行
    let execution_engine = module.create_jit_execution_engine(OptimizationLevel::Aggressive).unwrap();
    unsafe {
        execution_engine.get_function::<unsafe extern "C" fn()>("main").unwrap().call();
    }
}

このプログラムは、LLVMでmain関数を宣言し、main関数内でprintf関数を呼び出すコードを実行しています。 実行すると、Hello, world!と表示されるはずです。

inkwellを使えば、Rustからこれだけ簡単にLLVMをいじることができます。 LLVMの他の命令を使うには、Builderのメソッドを参照するとよいでしょう。 ドキュメント自体はそんなに充実しているとは言えませんが、命令をビルドするメソッドの引数と返り値の型がわかれば後は組み立てていけばいいので、慣れてくればそんなに迷うことはなくなるはずです。

おわりに

今回はRustで簡単にLLVMを扱うクレート、inkwellを紹介し、その導入と命令の記述についてサンプルコードとともに簡単に説明しました。 初めてinkwellやLLVMに触る人は、とりあえずは上のサンプルコードに様々な命令を追加してみて、inkwellとLLVMがなんとなくわかってきたら実際の言語作成に取り掛かってみるのが良いのではないでしょうか。

是非皆さんもRustでオレオレコンパイラフロントエンドを作っていきましょう3


  1. こちらです。メンテナ不足で悩んでいるようですが……

  2. aptが使える環境をご利用の方はこっちのほうが楽でいいかもしれません

  3. 次回はプログラミング言語自作に入門する記事が書きたいですね……

Rustで動的リンクライブラリを作る / 読み込む

こんにちは。 しばらくぶりの投稿になります。

シンプルにRustで動的リンクライブラリを作って読み込む記事が少ないように感じたので本記事を書くことにしました。 Rustで動的にリンクするライブラリを作りたい!、Rustで動的リンクライブラリを読み込みたい!という方は読んでいっていただければと思います。

Rustで動的リンクライブラリを作る

Rustで動的リンクライブラリを作ります。

Cargo.toml の編集

Cargo.toml を次のように編集します。

[package]
name = "dylib"
version = "0.1.0"
authors = ["cordx56"]
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]

重要なのは [lib] 以下の2行です。 crate-type["cdylib"] を設定しています。

こうすることで、 $ cargo build がRust以外の言語からも利用できる動的リンクライブラリをビルドしてくれるようになります。 このオプションについて詳しくは Linkage - The Rust Reference を参照してください。

ライブラリ本体を書く

ライブラリ本体を書いていきます。

src/lib.rs に次のようなプログラムを書きました。

#[no_mangle]
pub extern "C" fn main() {
    println!("Hello, world!");
}

#[no_mangle] は名前マングリングをしないようにコンパイラに指示します。

extern "C" はCのABIを利用するようにRustコンパイラに指示します。

これでライブラリ本体が書けました。

ライブラリをビルドする

ビルドは簡単で、

$ cargo build

を実行するだけです。

Linux環境であれば、 target/debug/libdylib.so が生成されているはずです。 これが動的リンクライブラリです。

Rustで動的リンクライブラリを読み込む

前章で作成した動的リンクライブラリを読み込むプログラムを作ります。

Cargo.toml の編集

libloading クレートを利用します。

Cargo.toml を次のように編集します。

[package]
name = "testapp"
version = "0.1.0"
authors = ["cordx56"]
edition = "2018"

[dependencies]
libloading = "0.7.0"

読み込むプログラム本体を書く

動的リンクライブラリを読み込むプログラム本体を書いていきます。

src/main.rs に次のようなプログラムを書きました。

fn main() {
    unsafe {
        match libloading::Library::new("./libdylib.so") {
            Ok(lib) => {
                match lib.get::<libloading::Symbol<unsafe extern fn()>>(b"main") {
                    Ok(func) => {
                        func();
                    },
                    Err(_) => {
                        eprintln!("Function get error!");
                    }
                }
            },
            Err(_) => {
                eprintln!("Library link error!");
            }
        }
    }
}

詳しくはlibloadingのドキュメントを見てください。

動的リンクライブラリを配置する

前章で作成した libdylib.so をカレントディレクトリにコピーします。

実行する

$ cargo run

を実行して、 Hello, world! と表示されれば成功です。

おわり

以上でRustで動的リンクライブラリを作って読み込むことができました。

Rustで作った動的リンクライブラリをCで使ったり、Cで作った動的リンクライブラリをRustで使ったりもできるはずです(試してないので断言できませんが)。 それについてはまた後日記事を書こうかなと考えています。

2020年買ったものとその評価

こんにちは。 年末ですね。 皆様よいお年をお過ごしでしょうか。

今回の記事は、私が2020年に買ったものを振り返って買ってよかった度を付けていくものです。 皆様の来年のお買い物の参考になれば幸いです。

  • Corne cherry

買ってよかった度: ★★★★☆

自作キーボードです。 この自作キーボードはキー数が少ないのが特徴で、指の動きが少なくて済みます。 とてもいいキーボードで気に入っているのですが、独特すぎるキー配列の影響でいまだにミスタイプが減りません……

買ってよかった度: ★★☆☆☆

まだNintendo Switchがなかなか手に入りづらかった頃、適当にヨドバシの抽選に申し込んだら当たってしまい、購入したものです。 どうぶつの森は楽しかったですが、最近は飽きがきてあまりやっていません。

買ってよかった度: ★★★★☆

言わずと知れた高級モニターです。 特別定額給付金を利用して買いました。 作業環境がこれまでWQHD1枚とFullHD1枚の2枚だったのが、WQHD2枚とFullHD1枚の計3枚になりました。 やはりWQHDが2枚あると作業環境が快適ですね。

買ってよかった度: ★★☆☆☆

Arm64WindowsタブレットSurface Pro Xです。 20万出してこのスペックかぁというのは正直あって、買ってよかったかと言われるとびみょいというのが正直な感想です。

  • Ergohuman Fit Ottoman

買ってよかった度: ★★★★☆

高機能チェアです。 まだ買ったばかりなので何とも言えませんが、多少作業が楽になった気がします。

  • Oculus Quest 2

買ってよかった度: ★★★★☆

今流行りのVR機器です。 VRChatくらいしかやっていませんが、それでも買った価値があったなぁというくらいVRChatが楽しいです。 これからVRコンテンツを漁っていきたいとも思っています。 おすすめがあったら何か教えてください。

以上が2020年の大きな買い物でした。 こうしてみるとそんなにたくさんは大きな買い物をしてないですね…… もうちょっとあるかなと思ったのですが。

2020年も残りわずか1時間半となりました。 私は今年中に終わらせたかった研究の実装が終わっていなくてやばいです。 大丈夫かなぁ…… それでは皆様よいお年を。

Tweet generatorのバージョン0.2.0を公開しました。

実際のサービスはこちら

Tweet generatorの新バージョン公開と共に、ドメイン名も刷新して新たにサービスを公開し直しました。

github.com

本エントリーはリリースノート代わりに、今回刷新された点について書いていきたいと思います。

新機能

鍵アカウント保護機能

これまでは鍵アカウントで生成された学習済みモデルを鍵アカウントを知らない第三者が利用してテキストを生成することで鍵アカウントのツイート内容が推測されてしまうという問題がありました。

今回のアップデートで新機能として鍵アカウント保護機能が追加されました。

鍵アカウントでモデルを学習した場合に、データベースに鍵アカウントであることが記録され、当該アカウントのテキスト生成時にはTwitterアカウントでログインしているかをチェックします。 鍵アカウントのテキスト生成時に、鍵アカウントでログインしていない場合には上記画像のエラーメッセージが表示されます。 内部的には、ログイン機能が実装されたことになります。 モデル生成時に自動でログインをする仕組みになっています。

動的サムネイル画像生成

Twitter cardなどで利用されるサムネイル画像の動的生成に対応しました。

画像にあるように、リンクからアカウント名を抽出し、アカウント名を含む画像を生成しています。 これにより、どのアカウントの自動生成結果なのかがツイートからわかりにくい問題が解消されました。 内部的には、metaタグをNuxt.jsでSSRし、画像はDjango側でPillowを使って生成しています。

その他の改善点

細かい改善点です。

フロントエンドのデザインを微調整

フロントエンドのデザインを少し変えました。 例えば、従来オプションがオプションとわかりにくく、入力必須であるかのように見えていた問題を解消するため、オプションであることを明記しました。

データベース内部で保持するアカウント名が大文字小文字を維持して保存するようになった

これまではファイルベースで生成済みモデルを保持していたため、大文字小文字の区別を無くすために、内部的に全て小文字で管理していました。 今回データベースに移行するにあたり、内部的に大文字小文字を維持して保存しても、大文字小文字を区別せず検索できるようになりました。 以前はモデル生成時にリダイレクトされるリンク先はアカウント名が全て小文字になったものでしたが、この変更により大文字小文字を区別した状態でリダイレクトされるようになりました。

プログラムの話

今回のバージョンアップ計画では、これまで使っていたフレームワークを変える決断をしました。

まず、PythonバックエンドのフレームワークをFlaskからDjangoに変更した点です。 これは鍵アカウント保護機能において必要だったため変更しました。 勿論、Flaskでログインシステムを一から構築することも考えましたが、データベースの扱いやすさなども考慮した際に、Djangoを利用したほうが開発コストが低いという判断になりました。 自分がインターン先やサークルでDjangoを利用する機会が多く、Djangoの知見がたまっていたという背景もあります。

次に、これまでVue.jsを使っていたのをNuxt.jsに変更した点です。 インターン先の関係でReactとNext.jsの知見がたまっていたためそちらへの移行を最初に考えたのですが1、Vue.jsのソースコードが再利用できるNuxt.jsを利用する方向で落ち着きました。 フロントエンドでは、動的サムネイル画像生成のために、metaタグをクライアントサイドで変更せず、サーバサイドで生成する必要がありました。 そのため、Nuxt.jsでSSRをするという決断に至りました。

おわり

以上が今回のアップデートの概要になります。 これからも多くの方に愛されるサービスであるよう、改良を続けていきたいと考えています。 今後ともTweet generatorをどうぞよろしくおねがいします。

Tweet generatorについてはこちらの記事もご参照ください。

cordx56.hatenablog.com


  1. コミットログにNext.jsを使おうとして途中で諦めたログが残っています

Windows on Arm上のWSL2でArch Linux ARMを利用する

Windows on Armを搭載したSurface Pro XのWSL2でArch Linuxを利用することができました。

本記事はWSL にインストール - ArchWikiの手法を参考に書いています。

また、本記事はWindows Insider ProgramのDevチャネルを導入したSurface Pro Xをベースに書いています。 下の記事で書いている通り、現状WSL2はWindows Insider ProgramのDevチャネルを導入しないと使えないようなので、ご確認ください。

cordx56.hatenablog.com

手順

まずWSL2のUbuntuをインストールします。

次にwslコマンドのエクスポート機能を用いてUbuntuのtarファイルを取得します。

> wsl --export Ubuntu-20.04 ubuntu.tar

以下のサイトからArchLinuxARM-aarch64-latest.tar.gzをダウンロードしてきます。

archlinuxarm.org

私の環境ではWindows上ではうまく行かなかったので、ここから先はLinux上で処理をします。

まずダウンロードしたArchLinuxARM-aarch64-latest.tar.gzとエクスポートしたubuntu.tarを同一ディレクトリ下に配置して、以下のようにしてファイル操作をします。

# mkdir arch
# mkdir ubuntu
# bsdtar -xpf ArchLinuxARM-aarch64-latest.tar.gz -C arch
# tar -xf ubuntu.tar -C ubuntu
# cd ubuntu
# rm -rf bin etc lib sbin usr var
# cp -a ../arch/etc ./
# cp -a ../arch/usr ./
# cp -a ../arch/var ./
# cp -a ../arch/bin ./
# cp -a ../arch/lib ./
# cp -a ../arch/sbin ./
# tar -cf Arch.tar *

最後に、完成したArch.tarをwslコマンドでインポートします。

> wsl --import Arch .\ Arch.tar

インストール場所(引数の.\)はいい感じに指定してください。

以下のコマンドでインストール完了です。

> wsl -d Arch
# pacman-key --init
# pacman-key --populate archlinuxarm
# pacman -Syyu base base-devel

まとめ

今回はArch LinuxWindows on Armの動作するSurface Pro XのWSL2上に構築しました。 現状、快適に利用することができています。

Windows on Armはまだ未知数なところが多いですが、これからもWindows on Armについて追っていきたいと思います。

Surface Pro Xを買いました。

こんにちは。

Windows on Armの機運が高まったのでSurface Pro Xを買ってしまいました。 機運が高まったってなんだ?

使えるアプリケーション

このページを読んでいる方には釈迦に説法かもしれませんが、今のところArm版Windowsではx64アプリケーションは動作しません。 将来的にはx64アプリケーションもエミュレーションで動作するようになる予定です。

blogs.windows.com

上のブログによると11月にロールアウトされるWindows Insider Programでx64エミュレーションが利用可能になるとのことです。

では現状では一体どういったアプリケーションであれば動作するのでしょうか。

Arm64ビルドされたアプリケーションは当たり前ですが動作します。 ただ、これはかなり少ないため、現実的ではありません。 サードパーティーでArm64ビルドが提供されているのは、私の知る限りではFirefoxのみです。

では全然既存のソフトウェア資源が利用不可能かというとそんなことはなく、x86アプリケーションであればエミュレーションで動作します。 エミュレーションなのでパフォーマンスはよくありません。 そのため、Arm64ビルドがあればそれを利用し、なければx86ビルドを利用する、というのが基本的なやり方になります。

とはいえ具体的にどういったアプリケーションが利用できるのかが気になるところかと思います。

参考までに、私が日常的に利用している以下のアプリケーションについては動作を確認しました1

Firefox: Arm64ビルドあり
Slack(32bit版): x86エミュレーションで動作
Discord: x86エミュレーションで動作
OBS(32bit版): x86エミュレーションで動作
GIMP(32bit版): x86エミュレーションで動作
CLIP STUDIO PAINT(旧バージョン32bit版): x86エミュレーションで動作
7zip(32bit版): x86エミュレーションで動作
Windows Terminal: Arm64ビルドあり
VScode: Arm64ビルドあり

x86エミュレーションでも使い勝手はそんなに気にならないと思います。 クリスタはちょっともさっとしている印象を受けましたが、まぁそれくらいですね。

開発環境

WSL2

WSL2についてですが、結論から言うとそのままではエラーが出て利用できませんでした。

手元の環境では、Windows Insider ProgramのDevチャネルを導入して初めて正常に動作しました。 バージョン2004、ビルド20241.1000で正常にWSL2が利用できています。

ただ、やはり通常のWindowsに比べて諸々の挙動が怪しいです。 例えば、codeコマンドなどが正常に動作しなかったりします。

Docker

WSL2上でDockerを動かすことができました。

docs.docker.com

上のリンクのインストールガイドのarm64に従いDockerをインストールして、下記コマンドを実行します。

$ sudo service docker start
$ docker run -it --rm -p 80:80 nginx:1.18

nginxサーバがDocker上で立ち上がります。

開発に使うアプリケーション

VScodeWindows Terminalは先述の通りArm64ビルドが存在しています。

総評

普通のパソコンとして使うにはちょっと難点が多いかなという気がします。 そりゃそうか。

ただ、電池持ちがよくLTE接続ができるWindowsとなると現状Surface Pro X一択だと思うので、興味のある人は買ってみるとよいのではないでしょうか。

同じArmプロセッサだとM1のMacBook Airは気になりますね……


  1. これらのアプリケーションの動作を保証するものではありません。参考程度にしてください。