0. まとめ
Claude Codeを擬人化するとエモくて捗るぞ
1. はじめに
今や私たちには一人ひとりに最高の開発パートナーがいますよね。そう、ご存知Claude Codeです。
そんなClaude君ですが、ターミナル(など)越しに文字でしか会話してくれません。
せっかくのパートナーなのにちょっと味気ない、、なら受肉させて喋らせればいいじゃないか!
というわけで最近ちょっと流行りのデスクトップマスコットを作りました。オープンソースです。
2. これはなに
Claude Codeのログを監視して、発言を音声で喋ってくれるデスクトップマスコットアプリケーションです。
- 常駐アプリとしてデスクトップに鎮座
- サイズや位置は自由に調整可能
- 任意のVRM/GLBモデルを読み込み可能
- Claude Codeのログを監視してリアルタイム音声発話+リップシンク
- Claude Codeの動作には関与せずコンテキスト消費なし
- 別途プラグインを導入すれば発話するセッション絞り込みも可
- 音声合成はAivisSpeechなどのVOICEVOX互換エンジンを利用
- 好きな声色に変更可能
- 感情に合わせて表情やアニメーションが変わる
- オフライン動作
インストール方法や使い方は下記Githubを参照ください
3. 技術スタック
Claude Codeのログフォルダを監視しつつVRMモデルを表示して連動させるにあたり、Electronアプリとして実装しています。
- Electron: デスクトップアプリ化
- React + TypeScript: UI
- Three.js + @pixiv/three-vrm: VRMモデルのレンダリング
- @pixiv/three-vrm-animation: VRMAアニメーション再生
- chokidar: ログファイル監視
- AivisSpeech / VOICEVOX: 音声合成(外部プロセス)
3-1. VRMモデルの表示
Three.jsでVRMモデルを表示するのは、@pixiv/three-vrmを使わせてもらっています。
設定画面からお好みのVRMモデルを読み込ませることができるのでVRoid Hubで拾ったり買ったり作ったりすると良いと思います。
import { Canvas } from '@react-three/fiber';
import { VRM, VRMLoaderPlugin } from '@pixiv/three-vrm';
// VRM読み込み
const loader = new GLTFLoader();
loader.register((parser) => new VRMLoaderPlugin(parser));
const gltf = await loader.loadAsync('/models/avatar.glb');
const vrm = gltf.userData.vrm as VRM;
// Canvas内で表示
<Canvas>
<primitive object={vrm.scene} />
{/* ライティングとか */}
</Canvas>
3-2. ログ監視〜音声合成の流れ
処理の流れはこんな感じ
-
chokidarで~/.claude/projects/**/*.jsonlを監視 - ファイルの差分だけを読み込んでJSONLをパース
- Markdown記法・コードブロック・XMLタグなどを除去
- ルールベースで感情を推論(happy, angry, sad, surprised, relaxed)
- 音声合成エンジンのAPIを叩いてWAVを取得
- Web Audio APIで再生しつつ、AnalyserNodeで音量を取得してリップシンク
ログ監視
ファイル監視はchokidarでやってます。
デフォルトでは ~/.claude/projects/**/*.jsonl がClaude Codeのログ出力先なので、ここをまるっと監視して差分を喋らせるようにしています。
chokidar.watch(pattern, { depth: 3 })
.on('change', async (path) => {
const newLines = await readNewLines(path);
for (const line of newLines) {
const message = JSON.parse(line);
if (message.role === 'assistant') {
processMessage(message);
}
}
});
Claude Codeのログ形式は公開されていないっぽいですが、中身を見ればなんとなくわかるので、message.roleとかを見て喋らせるかどうかを判断しています。
ログ監視なんてせずにHooksを使えばいいじゃないかと思うかもしれませんが、Stop HookはClaudeの一連の作業が全て終了したタイミングに発火するので、作業中の応答をフックすることが出来ずリアルタイム感に欠けます。
ログ監視方式にすることでClaudeの作業中の応答も拾ってリアルタイムに発話させられるようにしています。
あとClaude側の環境を汚さないというメリットもある。
テキストフィルタリング
JSONLから抽出したテキストは、そのまま喋らせるにはいろいろ余計なものが含まれてるので除去します。
何も考慮しないとプログラムコードも喋り始めてしまって全然聞き取れないです。
- コードブロック(```)の除去
- XML/HTMLタグの除去
- Markdown記法の除去
- インラインコードは中身だけ残す
- URLの除去
この手のマスコットにあんま正確性は求められていないと思うので、聞き取りにくい文章は一律喋らないようにしています。
音声合成
OS標準の読み上げ機能も検討しましたが圧倒的にエモさが足りないので、
音声合成にAivisSpeechやVOICEVOXのエンジンサーバーをローカルに立てて利用します。
特にAivisSpeechは調整をせずとも日本語の発話がとても自然なのでデフォルトにさせてもらっています。
// 1. クエリ作成
const query = await fetch(
`http://localhost:8564/audio_query?text=${text}&speaker=${speakerId}`
).then(r => r.json());
// 2. 音声合成
const wav = await fetch(
`http://localhost:8564/synthesis?speaker=${speakerId}`,
{ method: 'POST', body: JSON.stringify(query) }
).then(r => r.arrayBuffer());
// 3. Web Audio APIで再生
const audio = await audioContext.decodeAudioData(wav);
const source = audioContext.createBufferSource();
const analyser = audioContext.createAnalyser();
source.buffer = audio;
source.connect(analyser);
analyser.connect(audioContext.destination);
source.start();
3-3. 感情推測
シンプルなルールベースで感情の推測を行ってます。
ただ結構テキトーロジックなので、あんまり精度は高くないです。
- キーワードマッチング
- 文末パターン
- コードブロックが多い→neutral
- 問題解決系のキーワード→happy
感情はVRMモデルの仕様で定義されているhappy, angry, sad, surprised, relaxedの5種に無表情のneutralを加えた6パターンに分類されます。
100文字以上の長文は重みを調整して、感情がブレすぎないようにしてます。
3-4. アニメーション・リップシンク
判定した感情に応じて表情を変えているのと、VRMAアニメーションファイルを読み込んでリアクションを取らせています。
アニメーションはBOOTHで拾ったり、Adobe Mixamoというサイトから落としてきたFBXファイルをなんやかんやしてVRMAファイルに変換したりしてます。
ライセンス的に再配布不可なものもあるのでそれらのファイルはプライベートサブモジュールで管理するようにしています。
FBXからVRMAに変換する過程で細かい指の動きとかが欠落したりしてるけどまあそこは御愛嬌…
import { VRMAnimationLoaderPlugin, VRMAnimation } from '@pixiv/three-vrm-animation';
const loader = new GLTFLoader();
loader.register((parser) => new VRMAnimationLoaderPlugin(parser));
const gltf = await loader.loadAsync('/animations/happy1.vrma');
const animation = gltf.userData.vrmAnimation as VRMAnimation;
// 再生
const mixer = new THREE.AnimationMixer(vrm.scene);
const action = mixer.clipAction(animation.clip);
action.play();
リップシンクはAnalyserNodeから音量を取得して、VRMのaa表情に適用してます。
const data = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteTimeDomainData(data);
const rms = Math.sqrt(data.reduce((sum, v) => sum + v * v, 0) / data.length);
const openness = Math.min(rms * 4, 1.0);
vrm.expressionManager.setValue('aa', openness);
表情変化によってすでに口が開いてる場合、さらにリップシンクをかけるとVRMモデルのあごから口が飛び出てしまったりするので、表情によって口の開き方をチューニングしてたりします。
3-5. 視線・頭部追従
マウスカーソルの位置に応じて、キャラクターが目線と頭部を向けます。
VRMのlookAtAPIで目線を制御しつつ、headボーンの回転で頭部を動かしてます。
Bezier補間で滑らかに追従させます。
vrm.humanoid.humanBones.get('head')?.node.rotation.set(
THREE.MathUtils.lerp(currentRotation.x, targetRotation.x, 0.08),
THREE.MathUtils.lerp(currentRotation.y, targetRotation.y, 0.08),
THREE.MathUtils.lerp(currentRotation.z, targetRotation.z, 0.08)
);
3-6. その他の機能
まばたき・待機モーション
定期的なまばたきを実装しています。
また、発話していないときはたまに適当なモーションと取らせるようにしています。
マイク使用時のミュート機能
(こんなシチュエーションあるか知りませんが)オンラインMTG時に裏でClaude Codeに内職させて誤ってこのアプリによってアニメボイスが発話されてしまうとたぶん社会的に死ぬので、OSがマイク使用中かどうかを検出して発話を自動でミュートにするオプションを備えています。
手で実装しようと思ったらどう考えても面倒そうなこともClaude君は嫌な顔一つせずすぐ作ってくれるのは本当にすごい時代ですね。
サブエージェントの発話
Claude Codeではメインエージェントとサブエージェントで保存されるログのディレクトリ階層が異なります。
- メインエージェント
~/.claude/projects/[プロジェクト名]/[セッションID].jsonl
- サブエージェント
~/.claude/projects/[プロジェクト名]/[セッションID]/subagents/agent-*.jsonl
この特性を使ってサブエージェントの内容まで発話するかどうかの制御に利用しています(デフォルトではサブエージェントは含めない設定になっています)
サブエージェントのメッセージは最後の応答(処理のまとめみたいなやつ)のみセッションにメッセージが残り、メインエージェントのログにも記録されます。
この際メインエージェントのログにはmessage.roleがuserかつ、message.contentに<local-command-stdout>タグで括られた状態でサブエージェントからの最後の応答メッセージが残るというかなり怪しい挙動をするのですが、実際そうなってるのでそれに従ってアプリの振る舞いも調整しています。(2026/02現在)
発話セッションの絞り込み
デフォルトの動作ではすべてのセッションの応答を発話しますが、これだとClaude Codeを並列実行しているとどのセッションのことを喋っているのか文脈がわかりにくくなってしまいます。
そこでClaude Code側からSkill(スラッシュコマンド)を使って、発話するセッションを固定化することが可能です。
# プラグインのインストール方法
/plugin marketplace add kazakago/cc-mascot
/plugin install cc-mascot@cc-mascot
| コマンド | 説明 |
|---|---|
/speak-this |
このセッションの発話のみに絞り込む |
/speak-all |
すべてのセッションの発話に戻す |
/speak-status |
現在の発話フィルタ状態を確認する |
発話セッションを絞り込んだ場合でも、該当のセッション終了時にはSessionEndフックが発火して、自動的に絞り込みを解除する機構を入れています。
仕組みとしてはプラグインがCC Mascot側の管理ディレクトリにSessionIdを記載したファイルを配置し、ファイルが存在してる場合はアプリ側がSessionIdが一致しているときにのみ発話する、という非常に単純なものです。
このようなシンプルかつ疎な仕組みにすることで独立して動作するため、CC Mascotが起動してるかどうかに左右されません。
Claude CodeのSessionEndフックは意外と優秀で、ClaudeCodeの実行中にいきなりターミナルウィンドウを閉じたりしてもちゃんと発火します。
4. Claude CodeのTipsとか
サービスやアプリを作ろうとするといつも8割まで作って後の2割がめんどくさくなってしまうのですが、そこを詰めるのにもClaude Codeは役に立ちます。
4-1. LPやアプリアイコンの生成について
README.mdとかCLAUDE.mdがある程度整ってる前提にはなりますが、アプリのコンセプトに合うLPを作るにあたり、8割程度出来た段階でコードを読み込ませた上でLPを作ってもらうと結構それっぽいものを作ってくれます。
LPイメージは固まってないけどなんかいい感じに作って欲しいみたいな温度感のときはありだと思います。
アプリアイコンについてはClaude Codeで直接生成は出来ませんが、Nano Bananaなどの画像生成AIに投げるプロンプトを考えてもらうことは出来ます。
こちらもコードやREADME、LPを読み込ませたうえでイメージに合うプロンプトを考えてもらうと良さそうです
4-2. Electronのデバッグについて
Electronは内蔵ブラウザなのでコーディングエージェントでの検証はどうやるんだろと思ったんですが、MCPサーバーを作ってる方がいます。
導入すると開発サーバー起動から操作までやってくれるのでありがたく使わせてもらってます
5. おわり
今回は音声合成的に日本語特化ということもあり、README含め全部日本語にしたのでそこは楽に感じました。
まあ今後は英訳が必要な場面もだいたいClaudeにやってもらうと思うんですけどね、、
当初、すでに巷にあるデスクトップマスコットアプリに相乗りできないかと調査したんですが、任意文字の発話が可能かつ外部入力を備えているものは見つからなかったのでいっそゼロから作りました。
3DモデルといえばUnityを触る必要があるかと思ったものの、相対的に馴染みのあるWeb系技術だけでも行けたのはよかったです。
Electronアプリならディレクトリ監視も簡単に構築できますし、AIコーディングとの相性も良いのでClaude Codeにコードの9割以上を書いてもらうという経験としても面白かったです。
ちなみにこの記事自体は割と人の手で書いてます。
流石にDesktop Mateとかの商用製品と比べるとマスコットとしての出来に差を感じますが、日々のClaude Codeライフを少し彩るにはちょうどよいと思うのでぜひお試しください。

