初めに
日本語の音声から、文字起こしを挟まずにAction JSONを生成する小型モデル「KoeAct-JP」を作りました。

音声付きの動画はこちらです。
合成音声とモデルの出力を使った動作イメージです。実機の推論時間を示す動画ではありません。
たとえば「顔を右に45度向けて」という音声から、次の操作を生成します。
[ { "name": "look", "arguments": { "direction": "right", "degrees": 45 } } ]
主な結果は、次の3つです。
- 約259万パラメータで音声からActionを生成。比較した文字起こし経由の構成は、約520万パラメータでした。
- 合成音声でのAction完全一致率(EM)は93.2%。比較相手は83.6%で、いずれも5回の学習の平均です。
- CoreS3で録音終了後の待ち時間を中央値0.53秒に短縮。端末内でActionのピース列を計算し終えるまでの実測値です。
依頼音声の精度は合成音声で評価しています。実際の話者による精度評価はまだ行っておらず、会話や歌に対する誤発火も課題として残っています。
公開モデルは以下です。
ブラウザデモでは、話しかけた音声からActionを生成し、その内容に合わせて3Dのロボットが動きます。
この記事では、音声から直接Actionを出す仕組みと、学習中に分かったこと、実機で待ち時間を短くした工夫を紹介します。
- 初めに
- 開発の背景と目的
- KoeAct-JPのモデル構成
- 合成音声による学習データ
- 声の数と追加学習の効果
- カスケード方式との性能比較
- ESP32-S3への実装と高速化
- 公開モデルの実行方法
- まとめと今後の課題
- 関連モデルとデータ
開発の背景と目的
以前、ESP32-S3で動く日本語音声認識モデル「Ralomi」を作りました。
日本語テキストからAction JSONを生成する「JapaneseTinyAgentLM」もあるので、この2つをつなげば音声操作ができます。
ただ、ロボットに「右を見て」と伝えるために、いったん全文を文字列にする必要があるのかが気になりました。操作名と引数だけが必要なら、音声の特徴から直接取り出すことで、モデルを小さくし、応答も速くできるかもしれません。
そこで、文字起こしを経由するカスケード方式と、音声特徴から直接Actionを生成する構成を比較しました。
カスケード: 音声 → Ralomi → かなの文字起こし → JapaneseTinyAgentLM-kana → Action KoeAct-JP: 音声 → Ralomiの音声特徴 → 小型Actionデコーダ → Action
評価で重視するのは、最終的な操作が正しいか、依頼していない場面で動いてしまわないかです。
対応する操作
顔を向ける look、体を回す turn、うなずく nod、首を振る shake、お辞儀する bow、表情・LED・音量・明るさの変更など、固定の11ツールに対応しています。1発話で生成するActionは最大2つです。
否定や対象外の発話では、何もしないことを表す [] を正解とします。たとえば「やっぱりライトは消さないで」なら [] です。
扱える操作と引数は学習時に固定しています。推論時にツール定義を渡して、新しい操作を追加することはできません。
KoeAct-JPのモデル構成
約205万パラメータのRalomiに、約54万パラメータのActionデコーダを組み合わせています。Ralomiが持つ音声特徴を、小さなデコーダでActionへ変換する構成です。

図1:Ralomiのlayer 4とlayer 8の特徴からActionを生成する構成。CoreS3では、最後の棄却判定とJSON化をPC側で行います。
| 項目 | 構成 |
|---|---|
| 音声エンコーダ | Ralomi、8層のcausal DFSMN、約2.05M |
| 利用する特徴 | layer 4と、追加学習したlayer 8 |
| Actionデコーダ | 2層のTransformerデコーダ、約0.54M |
| デコーダの次元 | hidden size 128、4 heads、FFN size 384 |
| 出力 | 62種類のピース、最大24ステップ |
| モデル全体 | 約2.59Mパラメータ |
音声特徴からのAction生成
Ralomiのlayer 4とlayer 8の特徴を、学習した重みで混合します。それを320次元から128次元へ射影し、causalな畳み込みを通して、デコーダのcross-attentionで参照するmemoryにします。
最終構成では、入力層とblock 1〜4を凍結し、block 5〜8をAction生成に合わせて追加学習しました。学習時には、layer 8から日本語の読みを予測する補助CTC損失も加えています。
学習設定:補助CTC損失の重みは0.3。追加学習するRalomi側の学習率は、デコーダ側の0.1倍です。
推論時には、これらの特徴をそのままAction生成に使います。CTCで文字列を復号して、後段へ渡す処理は行いません。
文法制約付きデコーディング
デコーダは、JSONのまとまりや引数の値を、固定の「ピース」として順番に出します。冒頭の例なら、look の操作と引数のキーを含む部分、right、数字の 4 と 5、閉じ括弧などです。
生成中は、そこまでのピース列に続けられる候補を文法で絞り、その中で最も確率の高いものを選びます。これにより、スキーマに合うJSONを生成できます。
ただし、JSONの形式が合っていても、操作の内容まで正しいとは限りません。音声の意味を取り違えれば、操作名や引数を誤ることがあります。
スコアによる棄却
依頼していない会話や環境音での動作を減らすため、出力を [] に切り替える処理を入れました。判定には、最初の生成ステップでの [] の確率を使います。
p_act = 1 − P(最初のピースが [])
p_act が閾値を下回ったら、Actionを出さずに [] を返します。公開モデルの閾値は0.97です。このスコアが高くても、選んだ操作や引数まで正しいとは限りません。
閾値の決め方:validationの対象外発話に対する誤発火率が2%以下になるよう、モデルごとに固定しました。
合成音声による学習データ
日本語テキストとAction JSONのペアを用意し、Irodori-TTS VoiceDesignで音声化しました。単一の操作だけでなく、数値指定、複数Action、否定、言い直しも含めています。
テキストにはJapaneseTinyAgentLM-Action-Synthと数値・否定のテンプレートを使い、対象外の発話にはAmazon MASSIVEの日本語データなどを使いました。
| 項目 | 最終構成の学習データ |
|---|---|
| 学習用テキスト | 54,500文 |
| 合成音声の声 | 120種類 |
| 学習用の音声コピー | 107,964件。cleanと増強したコピーの合計 |
合成音声は品質チェックを通し、そのままの音声と、雑音・残響などを加えた音声を用意しました。この記事では、雑音などの増強を加えていない条件をcleanと呼びます。
さらに、操作の依頼ではない話し声や雑音も負例として学習に加えました。これらには、ReazonSpeechやOpenSLR SLR28由来の実録音を使っています。
学習・評価データの分離
学習に使った文や声を覚えているだけにならないよう、validation・testには学習に使っていないテキストと声を割り当てました。
数値についても、角度の25・37・75・105・135・165などを学習から外しています。これらは評価だけに使い、学習にない値を生成できるかを確認しました。
前処理の影響
ブラウザデモを作る際、同じモデルでも、入力の前処理を省くとEMが約4.5ポイント下がることが分かりました。前処理をそろえると、記録した値に近い結果へ戻ります。
行っているのは、音量を−23 dBFSにそろえ、先頭に0.5秒、末尾に0.3秒の無音を加える処理です。clean条件でも、この処理は入れています。
先頭からすぐに発話が始まると、Ralomiが最初の語を取りこぼしやすいことが原因と考えられます。公開runtimeもv0.1.1から、学習・評価と同じ前処理を標準で行うようにしました。
比較に使ったデータ:合成validation。前処理を省いた場合と、そろえた場合を同じ公開モデルで比較しました。
声の数と追加学習の効果
最初はRalomiを凍結し、Actionデコーダだけを学習しました。Actionは出せるようになりましたが、初期の合成testでは、cleanの単一ActionのEMが77.8%で、目標の80%に届きませんでした。
改善のため、学習用テキストを増やし、声の数とRalomiの追加学習を比較しました。ここからの構成選択は、データを増やした条件で、合成validationを使って行っています。
声の数による精度比較
声を5種類から120種類に増やしても、EMはほとんど変わりませんでした。 5・20・60・120種類を比較しましたが、全体EMは88.0〜88.1%に収まっています。
評価条件:Ralomiを凍結した構成、合成validation、棄却前。乱数seedを変えた3回の学習の平均です。
凍結したRalomiの特徴が、合成音声の声の違いをある程度吸収している可能性があります。実際の話者でも同じかは未確認ですが、今回の条件では、声を増やすだけでは大きな改善につながりませんでした。
最終構成には、比較した範囲で声の多様性が最も大きい120声を使いました。
上位ブロックの追加学習
一方、Ralomiの上位ブロックを追加学習すると、精度が改善しました。 同じ120声条件のデータで、最後の2ブロックを学習する構成と、4ブロックを学習する構成を比較した結果です。
| 構成 | 全体EM | 増強条件のEM |
|---|---|---|
| Ralomiを凍結 | 88.0% | 84.1% |
| 最後の2ブロックを追加学習 | 92.2% | 89.3% |
| 最後の4ブロックを追加学習 | 92.9% | 90.0% |
評価条件:同じ合成validation、棄却前、3回の学習の平均。
最後の4ブロックを学習した構成は、凍結した構成より全体EMが4.9ポイント高く、特に増強条件での伸びが大きくなりました。
この結果から、最終構成を「120声、block 5〜8を追加学習」に決めました。その後、構成選択時とは別の乱数seedで5回学習し、validationで棄却閾値を固定して最終比較を行いました。公開する重みも、testを見る前にvalidationのEMで選んでいます。
カスケード方式との性能比較
比較相手は、Ralomiのかなの文字起こしをJapaneseTinyAgentLM-kanaへ渡す構成です。後段のモデルも、拡張した学習データに対応するRalomiの認識結果で学習し直しました。
この節の精度と誤発火の比較には、乱数seedを変えて5回学習した平均を使います。両方式で棄却スコアと閾値の選び方が異なるため、棄却処理を含めたシステム全体として比べています。
合成音声での精度と雑音耐性
合成test全体のEMは、KoeAct-JPが93.2%、カスケードが83.6%でした。 特に、雑音が強い条件で差が開きました。

図2:雑音の強さごとのEM。学習にない声の合成音声で評価した、棄却後・5回の学習の平均値です。
評価には、3,425文のそれぞれにcleanと雑音ありの音声を用意した、計6,850件を使いました。EMは操作名・引数・複数Actionの順序まで一致した割合で、JSONの空白やキー順は正規化しています。
全体の差は、丸め前の値で9.5ポイントです。SNR 0 dBでは17ポイントの差になり、学習にない数値だけのカテゴリでも、89.6%対63.5%と差がありました。
全体EMの差の95%区間:+8.7〜+10.4ポイント。テキスト単位の対応bootstrapで計算。
ただし、すべての条件で同じように改善したわけではありません。cleanの複数Actionと言い直しは、両方式でほぼ同じでした。また、KoeAct-JPではエンコーダも追加学習しているため、改善の原因を文字起こしの省略だけに分けることはできません。
非依頼音声での誤発火
否定された操作を出してしまう誤作動率は、KoeAct-JPが0.05%、カスケードが0.60%でした。一方、操作を依頼していない音声での誤発火は、KoeAct-JPのほうが多くなりました。
| 音声 | KoeAct-JP | カスケード |
|---|---|---|
| 全体の誤発火 | 1.57回/時 | 1.38回/時 |
| 人の話し声・歌のカテゴリ | 4.44回/時 | 4.28回/時 |
測定条件:MUSANとFSD50Kの収録音声14.02時間をVADで区切り、Actionが出た回数を元の音声の時間で割って集計。値は5回の学習の平均です。
「誤発火をカスケード以下にする」という事前の基準には届きませんでした。依頼の音声では正しくActionを出せても、会話や歌を操作として拾ってしまう問題が残っています。実際に使う際は、ボタンやウェイクワードで入力を限定する工夫が必要です。
評価条件と制限
依頼音声の正解率は、すべて合成音声で評価しています。実際の話者や、マイクとの距離・部屋の響きによって、どの程度精度が変わるかはまだ測っていません。
評価と公開モデルについては、次の点も補足しておきます。
- モデルの選択:最終構成と閾値はvalidationで選び、最終比較の評価方法は予測を作る前に固定しました。ただし、合成testは初期の別構成の評価にも一度使っています。
- 公開する重み:5モデルのうちseed 14です。単体のtest EMは93.6%、非依頼音声での誤発火は1.07回/時でした。上の比較に使った5回の平均とは区別しています。
seedごとの結果やカテゴリ別の詳細は、公開モデルのModel Cardにまとめています。
PCでの推論時間
PCでは、KoeAct-JPの推論時間の中央値が7.1 ms、カスケードが16.6 msでした。約2.3倍の差はありますが、どちらも音声長の1%以下の時間で処理できています。
計測条件:Apple M1のCPU、1スレッド、合成validationの200発話、平均4.75秒。音声全体を受け取ってからActionを出すまでを計測し、録音時間は含みません。入力は一律15秒にpaddingせず、実際の長さで処理しています。
待ち時間の差が大きく効いたのは、CoreS3側でした。
ESP32-S3への実装と高速化
実機にはM5Stack CoreS3を使いました。RalomiはESP-DL向けにW16A16へ量子化し、ActionデコーダはCで実装しています。
現在は、CoreS3で音声からActionのピース列まで計算し、接続したPCで棄却とJSON化を行います。
CoreS3:音声 → 特徴抽出 → Ralomi → デコーダ → ピースID列とp_act PC :p_actによる棄却 → ピース列をJSONへ変換
モーターを動かす処理は、現在のファームウェアには含めていません。
最初に、float32のデコーダと1秒単位の処理で測ってみると、録音終了後に中央値2.52秒かかりました。この待ち時間を短くするため、同じ学習済みモデルに対して3つの変更を加えました。
- デコーダの行列の重みをint16にする:行ごとのスケールを持たせ、float32より重みの読み出し量を減らす
- デコーダのencodeを録音中に進める:音声特徴をmemoryへ変換する処理を、区間ごとに終えておく
- Ralomiを0.4秒単位にする:DFSMNの状態を引き継ぎ、録音終了後に残る最後の区間を短くする
録音中に特徴抽出・Ralomi・デコーダのencodeまで進めておけば、録音後に待つのは、最後に残った区間とピース生成の処理だけになります。
区間を短くすると呼び出し回数は増えるので、計算の総量が減るとは限りません。今回は、録音が終わった後の待ち時間を短くすることを重視しました。

図3:同じ学習済みモデルに、重みのint16化・録音中のencode・0.4秒への区切り変更を適用した結果です。
この変更で、録音終了後の待ち時間は中央値0.53秒になりました。 実機とPCのActionも、600件中597件で一致しました。
| 項目 | 結果 |
|---|---|
| 録音終了からピース生成まで、P50/P95 | 0.53秒/0.79秒 |
| PCとのActionの一致 | 99.5%:597/600件 |
| 動作待機中の内部RAM空き最小値 | 約56 KB |
| デコーダによるPSRAMの追加使用量 | 約1.11 MB |
| Ralomiのモデル/デコーダの重み | 4.81 MB/1.13 MB |
遅延の測定条件:Macのスピーカーから合成音声を再生し、CoreS3のマイクで4秒録音する操作を50回実施。4秒の録音終了から、端末内のピース計算完了までを計測しています。シリアル転送・PC側のJSON化は含みません。
録音後に残る処理の中央値は、Ralomiの最後の区間が約0.26秒、最後のencodeが約0.03秒、ピース生成が約0.24秒でした。
PCとの一致率は、合成validationの600発話を実機に送り、同じ入力で照合した値です。また、内部RAMの空きは動作待機中の値で、モデル読み込み中には一時的にほぼなくなります。機能を追加する際は、起動時のメモリ使用量も確認する必要があります。
カスケードとの遅延比較
カスケード方式とも、同じCoreS3で比較しました。
| 構成 | 録音終了後の遅延P50 | 条件 |
|---|---|---|
| KoeAct-JP | 0.53秒 | 実測、0.4秒区切り |
| カスケード | 1.54秒 | 実測、1秒区切り |
| カスケード | 1.30〜1.44秒 | 見積もり、0.4秒区切り相当 |
カスケードの0.4秒区切り版は実測していません。表の見積もりは、KoeAct-JPで測ったRalomiの最後の区間の時間に、JapaneseTinyAgentLM-kanaの実測時間を足したものです。
この見積もりと比べると、KoeAct-JPの待ち時間は約2.5〜2.7分の1です。主な差は後段の処理にあり、KoeAct-JPのピース生成が約0.24秒なのに対し、JapaneseTinyAgentLM-kanaは約1.0〜1.2秒でした。
公開モデルの実行方法
PCでの実行方法
以下は、uvを使うmacOS・Linux向けの手順です。前処理が標準で入るv0.1.1を取得します。
uvx --from huggingface-hub hf download ayousanz/KoeAct-JP \ --revision v0.1.1 --local-dir koeact-jp cd koeact-jp uv venv --python 3.13 uv pip install -r code/requirements.txt
WAVファイルを渡す場合は、次のコマンドでActionを生成できます。
PYTHONPATH=code .venv/bin/python -m koeact.runtime \ --model . a.wav
マイクを使う場合は sounddevice を追加して、--mic を指定します。Enterで録音を開始し、もう一度Enterで停止します。
uv pip install sounddevice PYTHONPATH=code .venv/bin/python -m koeact.runtime \ --model . --mic
入力の前処理は標準で有効です。モデルが扱う長さは前処理後15秒までで、先頭に加える0.5秒の無音を含むため、元の音声の約14.5秒より後は使われません。
CoreS3での実行方法
公開モデルの cores3/ に、変換済みモデル、デコーダの重み、ファームウェアのソースを置いています。ESP-IDF v5.5.5の環境で、取得したモデルのフォルダから以下を実行します。
cd cores3/firmware/e05 idf.py \ -DRALOMI_ESPDL="$PWD/../../ralomi_e13_stream40.espdl" \ -DA2_WEIGHTS="$PWD/../../a2_weights.bin" \ build idf.py -p YOUR_SERIAL_PORT flash
YOUR_SERIAL_PORT はCoreS3の接続先に置き換えてください。書き込み後、画面をタッチすると4秒録音し、録音しながら推論します。
PCでピース列を受け取り、JSONへ変換するには、次のコマンドを使います。
cd ../../.. uv pip install pyserial PYTHONPATH=code .venv/bin/python -m koeact.runtime \ --model . --serial YOUR_SERIAL_PORT
まとめと今後の課題
約259万パラメータのモデルで、日本語音声から直接Actionを生成し、CoreS3でピース列を出力するところまで動かしました。
学習では、声を増やすだけでは精度がほとんど変わらず、Ralomiの上位層を追加学習することで改善しました。実機では、録音中に計算を進めることで、録音後の待ち時間を短くできました。
一方、依頼ではない音声での誤発火は、カスケード以下という基準に届いていません。実話者での精度評価と誤発火の改善、端末内での棄却・JSON化、ロボットの動作との接続が、今後の課題です。
関連モデルとデータ
KoeAct-JPの重み・推論コード・評価の詳細は、冒頭の公開モデルにあります。ベースにしたモデルとテキストデータは以下です。
