みなさんこんにちは。株式会社はてなで働いている
id:ma2saka です。普段はクラウドサービスの利用状況の詳細データにSQLを書いたり、壊れたスプレッドシートを直したり、更新されたエンドユーザーライセンスのdiffを目grepする仕事をしています。
この記事は はてなエンジニア Advent Calendar 2025 の29日目です。昨日の記事は、
id:walnuts1018 さんの KustomizeのJSON Patchに苦しんでいる方、Jsonnetを使ってみませんか? - Walnutsでした。Jsonnetいいですよね。importstrは特に大好きです。なんでも外から持ってきて結合したい。
さて。
はじめに
よくあるあれを作ってみたい、ありますよね。みんなAIとキャッキャウフフしている。していないのはお前だけ。ギギギ、自分も欲しい。ということで、書いてたのはこれとなります。環境に対するポータビリティはゼロです。x86 / Nvidia環境向け。コード自体は 99% が codex により生産されていますが、3回くらい作り直しています。 作り直すのは楽になりましたねえほんと。
動作イメージは以下です。リアルタイムに近い感覚で発話して結果を得ることができます。利用モデルは medium です。

GIFではもちろんわからないけど、音声入力のみで対話しています。
後半見るとわかるように、無音というかノイズしかないような音声データに対して、「字幕をご覧いただきまして」「ご視聴ありがとうございます」が出現していますね。openai-whisper で有名なこれ、回避できなかったんだよな~。(多様な音声と字幕のセットのデータが死ぬほどあるとはいえ、YouTubeの音声と字幕で学習するとはこういうことか...という気がする。次の世代では「これは製作者のあてたテロップだな」という字幕は無視して教師データを作ってほしい!!!!)
ということで作業メモ
WSL2セットアップしたらVSCodeとWindowsの連携とかNVidiaのドライバとかノートラブルだった
すごい。便利すぎる。これにつきる。みんなWindows使いましょう。
偶然手元にある、そこそこのグラボ積んでるPCはたいていゲーム用途であろうから、WSL2有効にしたくないという人がいるのはわかるけど、めっちゃ快適だった。
Windows 11の Snipping Toolめっちゃ進化していて動画も撮れる。GIFにもできる
しかし手元ではなぜか、スクリーンショットモードでしか起動してくれず... なんでだろ。
上の動画を用意する際は ScreenToGif を利用しました。感謝。
pythonは3.13を利用しました
めでたくCTranslate2 さんが 4.6.1でpython 3.14をサポートしたのだけど、faster-whisper が依存する onnxruntime のバージョンがcp314をサポートしないため。次のリリースでサポートするぜ、というやりとりがあったので、もうちょっとかなー。
惜しい。
Whisperのモデルファイルは場所決めておいてキャッシュしよう
基本ではありますがファイルでかいので音声処理いろいろ手元ですることがあるならプロジェクト外に置いておくとよいですね。
def create_model(config: WhisperConfig, model_name: str) -> WhisperModel: download_root = str(Path(config.download_root).expanduser()) return WhisperModel( model_name, device=config.device, compute_type=config.compute_type, download_root=download_root, ) def transcribe_audio( model: WhisperModel, audio, language: str, beam_size: int, ) -> str: # TODO: ご視聴ありがとうございました対策として vad_filter をあとで試す segments, _info = model.transcribe( audio, language=language, beam_size=beam_size, ) return "".join(segment.text for segment in segments).strip()
だいたい mediumで1.5GBでした。smallは450MBくらい。large-v3で3GB。
$ ls -lah ~/data/models/models--Systran--faster-whisper-medium/blobs/ total 1.5G drwxr-xr-x 2 ma2saka ma2saka 4.0K Dec 28 18:44 . drwxr-xr-x 5 ma2saka ma2saka 4.0K Dec 28 18:44 .. -rw-r--r-- 1 ma2saka ma2saka 2.3K Dec 28 18:43 242aa06a480a7b5509375c645097e87af5136774 -rw-r--r-- 1 ma2saka ma2saka 2.2M Dec 28 18:43 7818adb6de9fa3064d3ff81226fdd675be1f6344 -rw-r--r-- 1 ma2saka ma2saka 1.5G Dec 28 18:44 9b45e1009dcc4ab601eff815b61d80e60ce3fd8c74c1a14f4a282258286b51ae -rw-r--r-- 1 ma2saka ma2saka 450K Dec 28 18:43 c9074644d9d1205686f16d411564729461324b75
gpu利用のためにLD_LIBRARY_PATHを追加する
手元の環境にセットアップするCUDAのライブラリのバージョンがWhisper で要求されるものと細かく違っていて合わせるのが面倒だった。
pipで nvidia-cublas-cu12, nvidia-cudnn-cu12 をインストールすると、site-packages/nvidia/{cudnn,cublas}/lib*に必要なファイルを含んで配布されていた。便利すぎる。
def _append_cuda_lib_paths() -> None: candidates = _cuda_lib_dirs() if not candidates: return existing = os.environ.get("LD_LIBRARY_PATH", "") parts = [p for p in existing.split(":") if p] for path in candidates: path_str = str(path) if path_str not in parts: parts.append(path_str) os.environ["LD_LIBRARY_PATH"] = ":".join(parts) def _cuda_lib_dirs() -> list[Path]: base = Path(".venv/lib") if not base.exists(): base = Path("~/.venv/lib").expanduser() candidates: list[Path] = [] for root in base.glob("python*/site-packages/nvidia"): for lib_dir in root.glob("*/lib*"): if lib_dir.is_dir(): candidates.append(lib_dir) return candidates
冷静になると .venv/site-packages/ の下のファイルを直参照するというのは相当に荒々しい。コンテナとかでビルドするときは素直に環境に置くと思う。
wsl2のUbuntuからマイク入力を取得してレベルを測る
環境側で以下しておく。
uv add sounddevice uv add numpy apt -y install pulseaudio
で、以下のようなコードを書いて動作確認してました。
import sounddevice as sd import numpy as np device = None # ここに入力デバイス番号を指定できる duration = 2.0 sample_rate = 16000 audio = sd.rec( int(duration * sample_rate), samplerate=sample_rate, channels=1, dtype="float32", device=device, ) sd.wait() rms = float(np.sqrt(np.mean(np.square(audio)))) if audio.size else 0.0 print(f"rms={rms:.6f}")
rmsはRoot Mean Squareで平均実効値で、波形データの中の標準的な音量レベルを計測する、というようなもの。無音室ではないのでマイクが有効であれば多少のノイズが入るのでこの値は0にはならない。
実際の音声処理では2秒などのウインドウでオーディオデータのrmsを測って、いっていレベル以下であれば書き起こしをしない、というようなことをした。GPUの処理の節約ということもあるけど、Whisperは無音またはノイズに対して「ご視聴ありがとうございました」という書き起こしを生成するので。
しかし入力レベルがいってい以上あっても、やっぱりノイズや環境音楽みたいなのが入り込むと、依然として「ご視聴ありがとうございました」は発生します。
openaiのAPIでチャットを呼ぶ
APIキーを取得する。ChatGPTとOpenAI Platformは別サービスみたいな感じなので、ChatGPTのサブスクリプションに参加していてもAPI利用のためには別に登録しないといけない。事前課金でここでは$100入れておいた。
公式のsdkを使う。uv add openaiしておく。
Chat Completions | OpenAI API Reference
動作確認。
import asyncio from openai import AsyncOpenAI async def main() -> None: client = AsyncOpenAI() response = await client.chat.completions.create( model="gpt-5-chat-latest", messages=[{"role": "user", "content": "筋子といくらの違いはなんですか"}], ) print(response.choices[0].message.content or "") asyncio.run(main())
金額感
ゲームマスター役に gpt-5.2を使わせたらそこそこかさむ感じになった。gpt-5-chat だけなら全然いかない。

OpenAI Platformの管理画面はUsage見やすくていいですね。
まとめ
ということで、ローカルGPUで書き起こしを動かして音声入力で対話することに成功しました。
実はCPUのみでも実時間程度の速度で書き起こしできるっちゃできるんですけども、ある程度の長さのある文章をまとめてやらないと書き起こし精度が出なくて、でもある程度の長さのある音声データをまとめて書き起こししようとすると実時間と同じくらいの速度だとぜんぜん足りない。それこそ、「月の裏側と電話してるの?」って感じになってしまう。
今回はRTX5060tiを使いました。
$ uv run -m app ## アシスタントと接続できました。どうぞ対話を楽しんでください。 ## User: はい、こんにちはこんにちは。どうぞよろしくお願いします。 今日はゆっくりとやっていきましょう。ここで一つお願いです。 ちょっと俳句を一つ読んでください。 (transcribe: 0.49 sec, chunk 7.94 sec) Assistant: こんにちは、こちらこそよろしくお願いします。ゆっくりやっていきましょうね。 では、俳句を一つ—— ――― 冬の陽に 湯気のぼりて 時止まる ――― 年の瀬っぽい静けさをイメージしてみました。どうでしょう、ちょっと落ち着く感じになったかな?
と、だいたい実時間の16倍速で処理できている。16秒を1秒で処理できる。けっこう早い。このくらい速度が出ると、普段のビデオ会議とそんなに変わらないレスポンス感になります。
OpenAIの gpt-5-chat-latest も非常にレスポンス速度が速くてよかった。いわゆる生成AIの待っている感が全くないため、対話している感じが出てきます。試してみるとわかるのですが、音声入力でなにか機械と対話しようとするとき、反応速度が少し遅いとめっちゃやりづらいんです。ビデオ会議をしているときに相手の反応が遅いと話しはじめにかぶったりしてつっかかるのがあると思うんですが、それです。ところが、ローカルの faster-whisper + gpt-5-chat-latest の組み合わせであれば、これもうちょっとしたら実用できるのでは、と思わされました。
体感速度向上には単に書き起こし速度だけではなく、2秒ウインドウでの部分書き起こしもユーザーへのフィードバックとして出力していて、これもけっこう効いている気がします。メモリ上に smallとmedium両方展開しているのはなんか無駄にも思うのですが、smallはやっぱり早い。話していることが虚空に消えている感覚がなくなるのは重要です。
ちなみに、音声入力だけでなく手元ではVoiceboxを利用した音声発話もいちど実装したんですが、発話の衝突をどう自然に回避したもんか思いつかずにやめました。なんていうんだろう。動かしてみたところ、「こっちの話を聞かずに発話している」という感じが強くなっちゃって気持ちが悪かったんですよね。ChatGPTとかGeminiの「高度な音声モード」はさすがにうまくて、こちらが話しはじめるとしゅっと口をつぐむのだけど、とはいえこう、「あ、衝突したな、黙ろう」っていう一瞬のコンタクトが感じられない不自然さがあって。これも不気味の谷ってやつなんだろうか。そんなわけで、人間がテキストでしゃべってAIがテキストで返す、というかたちにいったん落ち着いております。最初気持ち悪いかなと思ったけど、レスポンス早くなるにつれて気にならなくなった。自分の感覚が興味深い。
GitHubにあげたリポジトリの実装には「定期的に状態を更新する」っていうフィーチャーがこっそり含まれています。チャット相手がこちらのチャットからのみ状態変化を受けるってなんか面白くないよな、と思ったからなんですが、割とよいと思う。githubのソースは当たり障りのないことが書いてありますが、チャットのログとは独立した状態管理をさせて解説役・ゲームマスター役をやらせると、あら不思議、おもしろロールプレイができるという寸法です。なお、ChatGPTに実装方針の相談をしたとき、「AI同士が会話するのってみんなやりたがるけどだいたい破滅するからやめとけ」って言われました。
ということで、AIと対話してみたネタでございました。
Advent Calendar 2025 延長戦、30日目は
id:chaya2z さんです。
追記: Macbook Pro 2021(M1 Max)での性能
だいたい実時間と同じくらいでした。
% uv run -m app ## アシスタントと接続できました。どうぞ対話を楽しんでください。 ## User: こんにちはよろしくお願いします (transcribe: 1.69 sec, chunk 1.66 sec) Assistant: こんにちは、こちらこそよろしくお願いします。 サポート窓口みたいな立ち位置でお話し聞く係をやってます。(ちょっと事務っぽい始まりで失礼します) 今日はどんな感じで使いたいですか? ・勉強や仕事の相談 ・雑談で暇つぶし ・アイデア出し(企画、文章、プレゼント案とか) ・モヤモヤの整理 このあたりじゃなくても全然OKなので、「とりあえず〇〇の話したい」みたいにざっくり教えてもらえると助かります。 User: 【字幕視聴ありがとうございました】 (transcribe: 1.91 sec, chunk 0.13 sec) Assistant: あ、そのテロップどこかで見たことあるやつですね(笑)