Open-source AI tool you need to know 📌
Whisper.cpp is a fast C/C++ port of OpenAI's Whisper—running zero-dependency local speech-to-text on Apple Silicon, CPUs, & WASM.
🔗 ggml-org/whisper.cpp
Bookmark this for voice AI!
#OpenSource #GitHub #AI
2 likes · 64 views
DIY Sleep Apnea Guardian: Fine-tuning Whisper.cpp for Edge Audio Analysis
Sleep is supposed to be the time when our bodies recharge, but for millions, it’s a period of hidden health risks. Sleep Apnea detection is traditionally locked behind expensive sleep labs and bulky equipment. However, with the rise of Edge AI and high-efficiency models like Whisper.cpp, we can now build a low-power, privacy-first monitoring system right on a Raspberry Pi.
In this tutorial, we are going to dive deep into Raspberry Pi audio analysis to detect snoring patterns and breathing pauses (apnea events). We’ll use Librosa for feature extraction and a fine-tuned, quantized version of OpenAI's Whisper model to differentiate between normal rhythmic breathing and dangerous obstructive patterns. This is the ultimate "Learning in Public" project for those looking to master audio processing at the edge! 🚀
The Architecture: From Sound Waves to Life-Saving Data
Building a real-time audio classifier on a low-power device requires a lean pipeline. We don't want to send raw audio to the cloud (privacy first!), so everything happens locally.
graph TD
A[Microphone Input] --> B[Librosa Pre-processing]
B --> C{Voice Activity Detection}
C -- No Activity --> A
C -- Snore/Breathing Detected --> D[Whisper.cpp Inference]
D --> E[Spectrogram Feature Analysis]
E --> F{Apnea Event?}
F -- Yes --> G[Local Alert / Dashboard]
F -- No --> H[Log for Morning Report]
G --> I[Dockerized Edge Gateway]
Prerequisites 🛠️
Before we get our hands dirty with code, make sure you have:
- Hardware: Raspberry Pi 4 (4GB+) or Raspberry Pi 5.
- Audio: A USB Condenser Microphone or a high-quality ReSpeaker Hat.
- Stack:
- Whisper.cpp: The high-performance C++ port of OpenAI's Whisper.
- Librosa: For advanced audio manipulation.
- Docker: To ensure our environment is reproducible across edge nodes.
Step 1: Pre-processing with Librosa 🎙️
Raw audio is messy. To help our Whisper model focus on what matters, we use Librosa to filter out background noise (like a humming fan) and normalize the volume.
import librosa
import numpy as np
def preprocess_audio(file_path):
# Load audio with a sampling rate of 16kHz (Whisper standard)
y, sr = librosa.load(file_path, sr=16000)
# Noise reduction: Simple spectral subtraction
stft = librosa.stft(y)
stft_db = librosa.amplitude_to_db(np.abs(stft))
# Focus on the frequency range of snoring (20Hz - 2000Hz)
# This helps in ignoring high-frequency electronic hiss
y_filtered = librosa.effects.preemphasis(y)
return y_filtered
# Example of extracting Mel-spectrogram features for apnea pattern matching
def get_spectrogram_features(y, sr=16000):
S = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128)
return librosa.power_to_db(S, ref=np.max)
Step 2: Running Whisper.cpp on the Edge ⚡
We use Whisper.cpp because it's optimized for ARM processors. We’ll use the tiny.en model, fine-tuned on respiratory sound datasets, and quantize it to q5_1 to ensure it runs in real-time without melting the Pi.
# Clone and build whisper.cpp
git clone https://github.com/ggerganov/whisper.cpp.git
cd whisper.cpp
make
# Download the tiny model and convert to GGML format
bash ./models/download-ggml-model.sh tiny.en
# Run inference on our recorded sleep segment
./main -m models/ggml-tiny.en.bin -f sleep_segment.wav -nt
To make this "smart," we don't just transcribe text; we use the log-probability of the tokens to detect the "sound" of a snore, which the model often interprets as specific non-verbal tokens.
Step 3: Containerizing with Docker 🐳
To deploy this across multiple edge gateways, we wrap everything in a lightweight Docker image.
FROM python:3.9-slim-buster
RUN apt-get update && apt-get install -y \
build-essential cmake git libasound2-dev portaudio19-dev
# Install Python dependencies
RUN pip install librosa numpy sounddevice
# Copy our optimized whisper.cpp binary
COPY ./whisper-bin /usr/local/bin/whisper-edge
WORKDIR /app
COPY monitor_sleep.py .
CMD ["python", "monitor_sleep.py"]
The "Official" Way: Advanced Patterns 🥑
While building a DIY version is a fantastic learning experience, scaling medical-grade audio analysis requires robust data handling and specialized ML architectures.
If you're interested in more production-ready examples, or if you want to see how we handle high-concurrency audio streams and advanced anomaly detection patterns, I highly recommend checking out the WellAlly Engineering Blog. It’s a goldmine for developers looking to bridge the gap between "it works on my machine" and "it works for a million users."
Step 4: Logic for Apnea Detection 🧠
The core logic lies in detecting "Silence" where there should be "Rhythm." If the Mel-spectrogram shows high-energy snoring followed by a flatline of 10+ seconds (the clinical definition of an apnea event), we trigger an alert.
def analyze_apnea_event(energy_profile, threshold=0.01):
"""
Detects gaps in breathing energy.
energy_profile: list of energy levels over time
"""
silence_duration = 0
for energy in energy_profile:
if energy < threshold:
silence_duration += 1
else:
if silence_duration > 10: # 10 seconds of silence
print("⚠️ Potential Apnea Event Detected!")
trigger_alert()
silence_duration = 0
Conclusion: Privacy-First Health Tech
By leveraging Whisper.cpp and Raspberry Pi, we've built a system that is:
1. Private: No audio ever leaves your bedroom.
2. Low Latency: Analysis happens in milliseconds.
3. Cheap: Runs on $50 hardware.
This project is a perfect example of how Audio Spectrogram analysis and modern AI can solve real-world problems. Have you tried fine-tuning models for non-speech sounds? Let's discuss in the comments below! 👇
---
If you enjoyed this build, don't forget to ❤️ and bookmark! For more deep dives into technical architecture, visit the WellAlly Blog.
You're doing #podcasts/content or alike? whisper.cpp is really worth a look, it is fast and works well, fully free/OSS!
ggerganov/whisper.cpp
Inflearnが自動で字幕を生成するまで(AI/インフラ編)
!
この記事は2023年10月31日に執筆されたものです。
こんにちは。InflabのDevOpsエンジニア、SunBです。
2023年9月、Inflearnに字幕とスクリプトの機能が追加されました。皆さん、活用されていますか? 今回追加された字幕機能は、動画の音声データをもとに自動で字幕を生成する部分と、生成された字幕を受講画面に表示する部分の、大きく2つに分けられます。
今日は、このうち私が実装を担当した字幕生成について、どのような技術をもとに、どのような過程を経て実装したのかをお話しします。
2日で字幕機能を作ってデモする
まず、「DevOpsエンジニアが字幕機能を実装したの?」と思われる方もいらっしゃると思うので、今回の字幕機能の実装プロジェクトがどのように始まったのかをご紹介します。
Inflabでは今年から、Infthonという名前で社内ハッカソンを開催しています。応募者をランダムに4人1組のチームに分け、約2日間でアイデアの議論とMVPの実装を行ったあとに発表する、という形式です。
ところが偶然(?)、Inflearnの講座プレイヤーやVODインフラを多く手がけていたFE開発者のPapicoと私が同じチームになり、自然と講座の再生に関するアイデアを議論することになりました。そして、第1回Infthonのテーマだった「チームの生産性向上、またはAIを活用したサービス改善」に最も合うと考えた講座のAI字幕機能をアイデアとして選びました。
まず、AIベースの字幕生成機能を実装するにあたって、最低限の条件として次のことを考えました。
字幕生成がオフラインで動作すること(データの外部送信を防ぐ)
字幕生成にコンピューティングリソース以外の追加費用がかからないこと(オープンソースモデルを自前で実行)
字幕生成にかかる時間が動画の長さを超えないこと(ライブデモができるくらい速いこと)
これらの条件を満たす技術を探してみたところ、OpenAIのWhisperというモデルが目に留まりました。Whisper以外に検討した技術は以下のとおりです。
KoSpeech: Archived
OpenSpeech + KsponSpeech: 自分たちで学習させてモデルを作るには、技術への理解度もまだ低く、時間もかなりかかると予想された
KakaoのPORORO: Archived
MetaのSeamlessM4T: テスト用の講座を使ったPoCの過程で、SeamlessM4T largeモデルはWhisper large-v2モデルに比べて精度が劣ることを確認
NaverのCLOVA SpeechなどのAPIベースのサービス: データの外部送信とAPI料金が発生
Infthon以前にも、MacWhisperなどのツールで、OpenAIのSTTモデルであるWhisperの存在とその精度の高さは知っていましたが、深く調べたことはありませんでした。実装期間が2日と短かったため、仕組みを深く理解することよりも、素早く適用してデモできるかどうかを重視して作業を進めました。その結果、すべての処理を私のMac上でオフラインで動かす構成になりました。その際、Pythonで実装されたOpenAIのWhisperは遅すぎたため、代わりにC++に移植されたwhisper.cppを使いました。
こうして、次の流れを短期間で実装できました。
動画をアップロードする
ffmpegで16-bit WAVの音声データを抽出する
whisper.cpp + large-v2モデルで字幕を書き起こし、srtファイルとして保存する
受講画面でこのsrtファイルを再生する
さらに、FE開発者のPapicoの活躍で、スクリプトの表示と再生位置の移動、字幕の修正依頼と修正依頼の管理機能までデモでき、第1回Infthonで1位という栄誉を手にしました。
こうして楽しい経験として幕を閉じたと思っていたのですが……
正式プロジェクトとして採用
ある意味当然のことながら、字幕機能はすぐに正式プロジェクトとして採用され、新たなスタートを切りました。
Infthonですでにデモまで終えていたので大きな心配はありませんでしたが、引っかかる点が3つありました。
Shaka Playerでm3u8のHLSプレイリストとsrt字幕ファイルを同時に表示すると、字幕の同期がずれる問題
AWSのインフラでWhisperをまだ動かしたことがなかった点
韓国語字幕の1行あたりの長さと改行位置が適切でなく、読みにくい問題
1つ目は、FE開発者のPapicoが字幕の表示処理をShaka Playerの機能を使わずにReactで新たに実装し、きれいに解決してくれました。一方、2つ目と3つ目は、そのまま現実の問題として降りかかってきます。
インフラ上でWhisperを動かす
Infthonのときとは違い、今回は実際のユーザーにサービスを提供するため、まずは私たちのインフラ上で機能を動かす必要がありました。
エンコーディングインフラの改善記録
字幕生成インフラについてお話しする前に、まず動画エンコーディングインフラを改善した過程をご紹介します。
私たちは2022年10月まで、動画のエンコーディングにAWS MediaConvertを使っていましたが、コスト削減とエンコーディングオプションのカスタマイズのために、EC2 c6i.xlarge Spot InstanceベースのAWS Batch構成に移行しました。MediaConvertのReserved Queueなど、さまざまな代替案も試しましたが、予約したエンコーディングの開始が遅すぎたり、4Kなど一部のオプションが使えないといった制約があったりして、要件を満たすのは難しいと判断し、自分たちで構成する方向を選びました。
結果として、インフラ構成の変更は効果がありました。長さ28分の1080p動画を540p、720p、1080pにエンコードする場合、AWS Batchを使ったときとMediaConvertを使ったときにかかる時間と費用は、以下のとおりでした。
AWS Batch (ECS EC2 c6i.xlarge Spot)
MediaConvert (On-Demand Pro)
差
時間
約10分
約7分
約3分 (x0.7)
費用
約$0.021
約$1.895 (SD x 1, HD x 2)
約$1.874 (x90.2)
動画の長さと比べれば、エンコーディング時間の差はわずかですが、費用はなんと90倍以上の差があることを確認しました。
AWS Batch構成で、EC2インスタンスタイプごとの費用と性能を分析した結果は以下のとおりです。(長さ28分の1080p動画を540p、720p、1080pにエンコードする場合、Spot Instance未適用)
インスタンスタイプ
オンデマンドの1時間あたりの費用 (A)
エンコーディングの実行時間 (B)
実際の費用 (A x B)
c6i.xlarge
$0.192
10分
$0.032
c6g.xlarge
$0.154
12分47秒
$0.033
c6i.2xlarge
$0.384
5分57秒
$0.038
c6i.4xlarge
$0.768
4分23秒
$0.056
g5g.xlarge
$0.5166
5分1秒
$0.043
g5.xlarge
$1.237
3分15秒
$0.067
エンコーディングインフラと同じ構成でwhisper.cppを試す
上記の改善したエンコーディングインフラをもとに、字幕インフラも同じ構造で構成してみました。AWS Batch上のc6i.xlargeインスタンスで28分の動画に対してwhisper.cppを実行したところ、結果はなんと69分3秒と、予想よりもはるかに遅いものでした。
Infthonのデモ用にMac Studio(M1 Ultra)で同じ動画をwhisper.cppで処理したときは、3分30秒でした。これを見て、すぐにGPUインスタンスのPoCに取りかかりました。
GPUインスタンスのPoC
最初は、g5インスタンスより安いARMベースのg5g.xlargeインスタンスを使ってみようとしました。ところが、NVIDIAドライバーやCUDAのバージョンをいくら変えても、PyTorchからCUDAを認識できませんでした。CUDA 11のPython cuDNNライブラリはARMアーキテクチャをサポートしておらず、PyTorchの正式版はCUDA 12をサポートしていなかったため、PyTorch Nightly + CUDA 12 cuDNNの構成を試しましたが、torch.cuda.is_available() が False を返す状態が続きました。そんな中、AWS BatchではまだARMアーキテクチャベースのGPUインスタンスをサポートしていないというドキュメントを見つけ、g5gインスタンスの利用はひとまず諦めました。
その後、g5.xlargeインスタンスでOpenAIのPythonベースのWhisperを実行してみたところ、7分17秒かかりました。動画の長さに比べれば許容範囲でしたが、Mac Studioでwhisper.cppを使ったときは3分30秒だったことを考えると、ほかに方法はないかと考えるようになりました。そして、SeamlessM4Tについて調べていたとき、偶然Hacker NewsでWhisperXに言及しているコメントを見かけました。
WhisperXへの移行
WhisperXを使って、g5.xlargeインスタンスで28分の動画の字幕生成を試してみたところ、59秒で完了し、最も速い結果となりました。その出力結果も、Whisperやwhisper.cppと比べて精度が劣るということはなく、それぞれ一長一短がありました。
以下は、出力結果を比較した当時に書いたメモの一部です。
Whisper、WhisperXはg5.xlargeで、whisper.cppはMac Studioで抽出したバージョン
3つともかなり違いがあるが、どれか1つが最も優れているというわけではなく、それぞれにうまく書き起こせている部分がある。
WhisperXが正確に抽出した部分
뽑았는데 (Whisper: 꼽았는데)
등허리 (Whisper: 등어리)
복부 (Whisper: 복구)
whisper.cppが正確に抽出した部分
Eye Reflect (WhisperX: I Reflect)
Whisperが正確に抽出した部分
Z-Remesh (WhisperX: gremesher)
아이리플렉트 (WhisperX: I Reflect)
WhisperXはほかの2つよりはるかに速く実行でき、音節単位でalignするので行の再構成に有利であり、字幕の修正依頼機能の有効性も比較的高くなると推定されることを考えると、WhisperXを使うのが最も良いと思われる。
ただし、ときどき複数の文を早口で話す部分で抜け落ちる文があるが、精度が低いと判断して意図的に除外しているように見える。
まとめ
最初に試したwhisper.cpp + c6i.xlargeを基準に、試した組み合わせとの差をまとめると次のとおりです。(large-v2モデルを使って28分の音声データを書き起こした場合)
ツール
マシン
実行時間
差(速度)
whisper.cpp
c6i.xlarge
69分3秒
- (x1)
Whisper
c6i.xlarge
時間がかかりすぎて計測を中断
-
WhisperX
c6i.xlarge
実行中にエラーが発生
-
whisper.cpp
Mac Studio
3分30秒
65分33秒 (x19.7)
Whisper
Mac Studio
時間がかかりすぎて計測を中断
-
WhisperX
Mac Studio
15分26秒
53分37秒 (x4.5)
whisper.cpp(cuBLASビルド)
g5.xlarge
17分25秒
51分38秒 (x4)
Whisper
g5.xlarge
7分17秒
61分46秒 (x9.5)
WhisperX
g5.xlarge
59秒
68分4秒 (x70.2)
最終的に、WhisperX + g5.xlargeの組み合わせを選びました。
こうして最初の絶望を乗り越え、速度を70倍以上改善し、インフラ上で字幕生成機能を正常に動かせるようになりました。その過程でGPUインスタンスや動画エンコーディングに関する分析も行い、エンコーディング関連のプロセスも改善できました。
(試行錯誤の跡)
字幕の長さと読みやすさの改善
字幕をどう生成するかという課題は、音声データからテキストを抽出するだけでは終わりませんでした。
whisper.cpp、Whisper、WhisperXのいずれにも、--max_line_width のような1行の長さを制限するオプションがあったのですが、問題はこのオプションが動作しないことでした。(……)おそらく韓国語だから動作しないのだろうと推測しています。
そのため、モデルが出力した字幕データは行ごとに長さがバラバラで、文の終わりで改行されることもほとんどなく、読みやすさは最悪でした。
これを改善するために、ピリオド/疑問符/感嘆符があれば改行する案、一定の文字数以上なら単純に空白を基準に半分に分割する案、形態素解析によって行を再構成する案など、さまざまな方法を模索しました。
そんな中、WhisperXに移行して音節単位で時間を追跡できるようになったことで、いっそすべての行を一から作り直そうと考えました。そこで以下の基準を定め、WhisperXの実行から行の構成までを一度に行うPythonスクリプトを実装しました。
WhisperXが時間の追跡に失敗した音節の時間は、時間の追跡に成功した最も近い前後の音節の時間、または文の開始・終了時間から線形補間する。(wav2vecの韓国語モデルベースだったため、ハングル以外は音節の時間を追跡できないという問題がありました。)
直前の単語までの行の長さが0.5秒未満であれば、ほかのどの条件を満たしていても改行しない。
直前の単語までの行の長さが10秒を超えたら、現在の単語から新しい行にする。
現在の単語を加えると行が10文字を超え、かつ形態素解析の結果で文の数が変わる場合は、現在の単語から新しい行にする。
現在の単語を加えると行が50文字を超える場合は、現在の単語から新しい行にする。
文の開始が直前の文の終わりから0.5秒以内の場合は、文の開始時間として直前の文の終了時間を代わりに使う。
実は、このうち形態素解析で文の数を計算し、それをもとに改行する基準は最後に作ったものです。この基準を決めるまでは、WhisperXをCLIで実行したあと、JSONで保存されたデータを読み込んで行を構成する処理をすべてGoで実装していました。しかし、形態素解析と文の数の計算にKiwiを使うことになったため、WhisperXをPython内で直接実行するようにし、行の構成もPythonで実装し直しました。そしてGoは、このPythonスクリプトを実行し、最終的に構成された字幕データをストレージに保存するという、AWS Batch内の1つのジョブのライフサイクル管理だけを担う構造にしました。
こうして、現在提供している字幕のように、読みやすさが大きく改善された字幕データを生成できるようになりました。
GPUインスタンス不足の問題を回避する
問題はすべて解決したと思い、これまでにアップロードされた講座を対象に、実際に字幕を一括生成しようとしました。ところが、AWS Batchにジョブを投入していくら待っても、EC2インスタンスが起動しませんでした。Spot Requestを確認してみると、ソウルリージョンにg5.xlargeインスタンスのSpot Capacityがなく、起動できていなかったのです。
Spot Instanceとは?
Spot Instanceは、AWSで使われていないコンピューティングリソースを最大90%割引した価格で提供する機能です。実行中のSpot Instanceがときどき中断されることもありますが、AWS Batchなどを使ってワークフローの耐障害性を確保していれば、Spot Instanceを適用するだけで60%以上のコスト削減効果を得られます。
実際に、動画エンコーディングインフラにSpot Instanceを適用することで平均70%のコストを削減できていたので、そもそも費用のかさむGPUインスタンスを使ううえで、Spot Instanceの適用は必須でした。
Spot Capacityを求めて
GPUインスタンスが足りないという話はよく耳にしていましたが、実際に経験したのは初めてでした。3日待っても起動しなかったので、別の方法を探し始めました。
最初に、Spot Placement Scoreという機能を使ってみました。米国のバージニア、オハイオ、オレゴンリージョンはg5.xlargeインスタンスが最も安く、Placement Scoreも高く出ていたので、まずはバージニアリージョンで試してみました。
初めて使うリージョンで何もなかったため、VPC、Subnet、EIP、NAT Gatewayをはじめ、AWS Batch関連の各種リソースをすべて新しく作成しました。ところが実行してみると、同じようにSpot Capacityがないというエラーが発生しました。しまった!と思い、すぐ隣のオハイオリージョンにも同じ構成を作ってみましたが、やはりSpot Capacityがないというエラーが発生しました。(……)
Placement Scoreは当てにならないなと思って調べてみたところ、スポットインスタンスアドバイザーというものを見つけました。g5.xlargeスポットインスタンスの中断頻度が低いリージョンを探してみると、オレゴンリージョンが該当することが分かりました。
バージニア、オハイオリージョンに作ったものはすべて削除し、オレゴンリージョンに構成して実行してみると、ようやく字幕生成ジョブが動き始めました。一括処理のために2,000台のg5.xlargeインスタンスを同時に起動する計画でしたが、オレゴンリージョンでさえインスタンス不足で最大500台ほどしか起動できませんでした。それでも、約4時間で字幕の一括生成を完了できました。
一括処理のコスト
基盤
単価
費用
差
AWS Batch (g5.xlarge Spot)
-
$807.22
- (x1)
AWS Batch (g5.xlarge On-Demand)
-
約$2,690
約$1,883 (x3.3)
OpenAI Whisper API
$0.006 / minute
約$8,700
約$7,893 (x10.8)
Naver CLOVA CSR API
$0.012 / minute
約$17,400
約$16,593 (x21.6)
字幕の一括生成作業にかかった費用は $807.22 で、約24,000時間に及ぶ動画の音声データをAPIベースのサービスで処理する場合と比べて、10分の1以下のコストで字幕を生成できる構成を実現できました。
結論
以上のような過程を経て、約2か月で字幕生成機能を無事に実装できました。Infthonをきっかけに、2日間でアイデアの選定から技術調査、デモ用の実装までを終え、その後の2か月間で、それをユーザーにリリースできるレベルまで引き上げるためにさまざまな試みをしました。貴重な経験でした。
字幕生成に関する技術的な部分も注目に値しますが、特に強調したいのは、1人が短期間でこなしたにしては、インフラ関連の作業量がかなり多かったことです。何のリソースもないリージョンを3回も渡り歩き、VPCからAWS Batch環境までを一晩で構成できたのは、何かが準備されていなければ不可能なことです。特に、受講権限を持つユーザーにのみ字幕データを提供する対応や、字幕のストレージをS3からMongoDBに移行する作業など、本文には書いていない数多くのインフラ作業がありました。それらに触れなかったのは、ほとんど悩むことなく素早く対応できたからです。
これが可能だったのは、これまで懸命に構築してきたIaCのおかげだと思います。私たちがどのようにIaCを構築したかについては、(今度こそ本当に)近日公開予定の「InflabのIaC構築記(Part 1)」の記事でご覧いただけます。AWS CDKからTerraformへの記事でTerragruntとTerratestについての記事を予告しておきながら、1年以上新しい記事を投稿できなかった理由も、その記事で詳しく書いていますので、IaCに興味のある方はぜひ読んでみてください。
ありがとうございました。
whisper.cpp runs OpenAI's Whisper speech recognition locally in C/C++, on your own laptop or phone: ggml-org/whisper.cpp
1 like · 8 views
How to Run LiquidAI d1-Omni-2B Locally (It Hears Audio but Can't Talk Back)
If you came here hoping to run LiquidAI d1-Omni-2B locally as a small voice assistant, I'll save you the download first: it can't talk back, and it can't write back either. It listens to audio and returns typed answers with probabilities attached, and that's all you get out of it.
Hugging Face files d1-Omni-2B under audio-text-to-text, and that label sets the wrong expectations. Liquid calls it a "decision model". You give it a state (text, JSON, an image, a video, or an audio clip) and a set of named questions, and it answers all of them in one forward pass with zero generated tokens. The card says plainly that it "writes no transcript or speech", and the docs say it is not a chat model.
I think that's a good design, for reasons I'll get to below. But it changes what "running it locally" means, so here's what the 2B adds over the 600M, how much RAM it needs, and what works today in transformers, llama.cpp, Ollama, MLX and ONNX (as of late October 2026). All benchmark and speed numbers below are Liquid's own. I haven't run it myself.
What d1-Omni-2B actually does
You ask typed questions and get typed answers:
- noul: yes/no, returns P(yes)
- choice: one of several named options, with probabilities
- score: an ordinal scale of 2 to 10 levels ("Agitated", "Neutral", "Calm")
- in transformers, not yet in llama.cpp: multiple choice, extraction of text/number/date spans, number estimates with intervals, and bounding boxes
That fits call routing ("billing, technical or fraud?"), voicemail triage, sound event detection, or a cheap check at the front of a pipeline before a bigger model gets involved. Questions can even be spoken: the instructions field takes an audio clip. For text- or image-only work, Liquid's card itself tells you to use d1-2B instead, which beats the Omni on most rows.
d1-Omni-2B vs d1-omni-600M: what 3x the parameters buys you
The 600M came out with the Open d1 release on October 7, 2026, billed as Liquid's "first experimental checkpoint". According to OrcaRouter's repo write-up, the 2B repo was created on October 24. The 2B sits on a different base model, so treat it as a separate model rather than a bigger 600M:
| | d1-omni-600M | d1-Omni-2B |
|---|---|---|
| Parameters | 587M | 2.17B |
| Base | LFM2.5-Encoder-350M | LFM3-Omni-2B |
| Context | 16,384 tokens | 128K tokens |
| Audio input | one 16 kHz mono clip, up to 30 s | clips + spoken questions |
| Images + audio in one request | no (ValueError) | yes |
| Speech languages | trained on English requests | 12 (the 16 text languages minus Hindi, Thai, Vietnamese, Indonesian) |
| Decision Index v0.3 | 18.3 | 49.0 |
| Average (public decision + image benchmarks) | 42.8 | 67.7 |
| Min transformers | 5.15 | 5.19 |
The jump is large. Liquid says the 2B beats the 600M on every benchmark it reports, and beats d1-3B on the decision board and on 16 of 18 image benchmarks with about 70% of the parameters. VisualWebBench goes from 14.9 to 88.6. The 600M card reports no audio benchmarks at all, so there's no audio comparison to make between the two.
Audio scores: written vs. spoken input
The 2B card does publish audio scores, and they're worth a careful look. Each task is scored twice, once with the input written as text and once with the same input read aloud. The gap between the two columns is what the speech path costs you:
| Test | Written | Heard |
|---|---|---|
| SLURP (assistant commands, real speakers) | 87.4 | 81.7 |
| Speech-MASSIVE (12 languages) | 85.2 | 63.4 |
| FLEURS (spoken language ID, 43 languages) | 92.4 | 92.5 |
| ESC-50 (sound events) | n/a | 84.5 |
| Spoken short states | 42.0 | 22.8 |
| Belebele (passages, 8 languages) | 83.6 | 41.8 |
Short English commands and sound classification hold up. Long passages and non-English speech fall apart: Belebele loses half its score once you speak it. The card admits that "long spoken passages lose much of their accuracy, most of all outside Latin-script languages." If you're building multilingual voicemail triage, run your own eval before you trust it.
How much RAM and VRAM d1-Omni-2B needs
Liquid publishes no memory requirements, so here's the arithmetic plus the file sizes they do publish. At 2 bytes per parameter, 2.17B is about 4.3 GB of weights, which matches the single 4.33 GB BF16 safetensors file. Being a hybrid (13 short-convolution layers, 5 sliding-window, only 6 global attention), it should keep a smaller KV cache than a pure transformer at long context. And since it never decodes, you only pay for the prefill.
| Build | Size on disk | Where it runs |
|---|---|---|
| Safetensors BF16 | 4.33 GB | transformers (CUDA, MPS, CPU in fp32) |
| GGUF BF16 / F16 | 3.47 GB | llama.cpp |
| GGUF Q8_0 | 1.85 GB | llama.cpp |
| MLX 8-bit | 2.71 GB | Apple Silicon |
| ONNX q4 | 2.45 GB | CPU |
| ONNX q4f16 | 2.20 GB | GPU / WebGPU |
| ONNX fp32 | 8.32 GB | reference |
Watch the CPU path in transformers: the sample code loads in float32 on CPU, so budget for roughly double the BF16 footprint, around 8.7 GB by my math. The GGUF sizes are the language model only; llama.cpp also pulls an mmproj file for audio and images (0.33 GB at Q8_0, 0.61 GB at BF16). On an 8 GB laptop, use the Q8_0 GGUF or ONNX q4.
Runtime status: transformers, llama.cpp, Ollama, MLX
transformers
Full support, including every question type. It needs remote code:
pip install "transformers>=5.19" torch torchvision pillow
import torch
from transformers import AutoModel
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
dtype = torch.float32 if device == "cpu" else torch.bfloat16
model = AutoModel.from_pretrained("LiquidAI/d1-Omni-2B", trust_remote_code=True, dtype=dtype).to(device)
Then call model.system_one(state, questions, media=...). WAV files need to be 16-bit PCM and get resampled to 16 kHz.
llama.cpp
It works from build b11960 (v0.8.0) using the official GGUF, but only for noul, choice and score. Extraction, estimates, boxes and multi-select aren't supported there yet. On a laptop, this is what I'd run:
llama-server -hf LiquidAI/d1-Omni-2B-GGUF:Q8_0 -b 8192 -ub 8192
Don't drop the batch flags. Each question's full state and media has to fit in a single batch. An image can use at most half of -ub, so 8192 gives it the full 4096-token budget. Then POST audio to the custom endpoint (this is the card's audio example):
curl -sL -o jfk.wav https://github.com/ggml-org/whisper.cpp/raw/master/samples/jfk.wav
curl http://127.0.0.1:8080/v1/systemone -H "Content-Type: application/json" -d @- <<JSON
{
"state": "Recording from a public event.",
"files": ["data:audio/wav;base64,$(base64 < jfk.wav | tr -d '\n')"],
"questions": {
"kind": {"type": "choice", "instructions": "What kind of utterance is this?",
"criteria": {"request": "A request to do something",
"question": "A question asking for information",
"speech": "A speech to an audience"}},
"calm": {"type": "score", "instructions": "How calm is the speaker?",
"criteria": ["Agitated", "Neutral", "Calm"]}
}
}
JSON
Spoken questions work here too: put an input_audio part in a question's instructions.
Ollama
There's an official d1-omni library entry with a 2b tag at 4.4 GB. That's roughly the BF16 checkpoint, so don't expect a small quant. It serves the same /v1/systemone endpoint on port 11434, and the CLI gets an --ask flag. The page's own example:
curl http://localhost:11434/v1/systemone \
-H "Content-Type: application/json" \
-d '{
"model": "d1-omni:2b",
"state": "Hello World",
"questions": {
"says_hello": {
"type": "noul",
"instructions": "Does the state text contain a greeting?",
"criteria": {
"true": "The state text contains a greeting.",
"false": "The state text does not contain a greeting."
}
}
}
}'
There's a catch. When I checked, the readme on that page still described the 600M (587M parameters, English speech only, 30-second clips, no audio and images together), and Liquid's d1-Omni-2B docs page only covers transformers and llama.cpp. Compare the 2B tag against transformers on a few of your own clips before you rely on it.
MLX
Official builds exist (fp16, 8-bit) and run through mlx_vlm, but the card itself says "Support for d1 in MLX-VLM is in progress". The examples may need an mlx-vlm release that doesn't exist yet. On a Mac I'd stick with transformers on MPS for now.
ONNX
The ONNX repo is the one I'd keep an eye on. It has CPU q4/q8 bundles and a WebGPU path via onnxruntime-web/webgpu, so routing audio in the browser looks doable.
Speed: fast on GPUs, and no CPU numbers yet
Liquid's bf16 numbers after model.compile(), for a 10-second audio clip: 12.4 ms on an H100 PCIe, 8.2 ms on an MI325X, 33.3 ms on an Apple M5 Pro. A single question on the M5 Pro takes 21.1 ms. Nothing gets decoded, which is why the numbers are that low.
Liquid doesn't publish any CPU or Raspberry Pi number for this model. I'd ignore any headline that says it "runs on CPU" until someone posts timings. It will run, since the ONNX CPU builds exist, but nobody has said how fast.
License: free until you hit $10M revenue
The weights ship under the LFM Open License v1.0. Commercial use is free under $10 million USD annual revenue, counted across your whole corporate group. Above that, you need a separate license. The threshold also applies to your downstream customers, and violating any term terminates the license immediately. That's fine for hobby projects and startups. If you ship it in an enterprise product, talk to a lawyer before it goes out. The Open d1 blog's "without restrictions" line is more generous than the actual license text.
My take
Skipping generation is the right call for this job. When I'm routing a call I want {"team": "billing", "confidence": 0.94} back in 20 ms, with a probability I can put a threshold on; a 300-token explanation is just latency. The 600M was a research preview. The 2B is the first d1-Omni I'd consider putting in a real pipeline, as long as the audio is short, mostly English, and you've calibrated thresholds on your own recordings, which the card also tells you to do.
Is 2B the sweet spot for an on-device voice model, or would you rather run the 600M on a Pi and accept the accuracy hit? And if you've run d1-omni:2b in Ollama, do its probabilities match what transformers gives you for the same clip? Tell me in the comments.
Sources
- LiquidAI/d1-Omni-2B model card
- LiquidAI/d1-Omni-2B-GGUF
- LiquidAI/d1-Omni-2B-MLX-8bit
- LiquidAI/d1-Omni-2B-ONNX
- LiquidAI/d1-omni-600M model card
- Liquid docs: d1-omni-2B
- Liquid AI blog: Open d1
- Ollama library: d1-omni
- LFM Open License v1.0
- OrcaRouter: d1-Omni-2B, what the repo shows
Turn your interview recording into a draft transcript with local AI: whisper.cpp. Start with a short 16-bit WAV; check names against the audio before quoting.
ggml-org/whisper.cpp
VIGIL: The Cloud Is Not the Only Place a Memory Can Live
VIGIL: Keep Your Family Stories Off the Cloud — An Offline AI Memory Archive
> *"Every time we ask a cloud AI to remember something for us, we make a quiet trade: convenience for control. We surrender our memories, rituals, and unwritten life details to corporate servers. I built VIGIL to explore a different contract: an air-gapped sanctuary where human lore remains sovereign on the keeper’s machine, while AI serves as a replaceable local instrument rather than a permanent owner."
This is my submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine.
---
What I Built
VIGIL is a local-first archive for preserving the stories, rituals, names, recipes, and small details that make someone impossible to forget.
A user can:
- Preserve a memory about a person, place, date, and sensory detail.
- Search the archive locally with instant client-side querying.
- Open the original memory untouched and intact.
- Ask for a short AI-generated "echo" (reflection).
- Export and import the archive as portable, sovereign JSON.
The original memory is always kept strictly separate from the generated reflection. VIGIL never pretends that an AI-generated paragraph is the voice of someone who has passed away.
I interpreted Ghost in the Machine as a question about what remains when a machine reflects something deeply human back to us. The ghost in VIGIL is not a simulated person. It is the signal that remains in a recipe, a song, a kitchen, or a phrase someone left behind.
---
Demo & Video Walkthrough
{% embed youtu.be %}
Open VIGIL →
The demo works immediately without an account, backend server, or model installation.
Try it in one minute:
1. Open "The orange radio".
2. Click "Ask for an echo".
3. Read the offline reflection.
4. Add a memory of your own.
5. Search for radio or music.
6. Export the archive as JSON.
> For the full local-AI experience, launch Ollama and select Local open model.
---
How I Built It
VIGIL offers two reflection paths designed around resilience and privacy:
1. Offline Whisperer
The default path is deterministic and requires no network connection, AI model, or API key. This guarantees the archive still functions when the internet is completely unavailable. Capturing, searching, exporting, and reflecting on memories never depend on a remote service.
2. Local Open-Weight AI
When the user selects Local open model, VIGIL communicates directly from the browser to Ollama running on localhost:
ollama pull llama3.2:3b
ollama serve
The rest of the stack is intentionally minimal, zero-dependency, and inspectable:
- Browser localStorage for sovereign, on-device data persistence.
- Local full-text search executed client-side.
- JSON export & import for complete data ownership and portable backups.
- Static GitHub Pages deployment with no telemetry, tracking, or analytics.
- Zero hosted databases and no API keys required.
Architecture Overview
Selected Memory
│
├── Offline Whisperer
│ └── Deterministic local reflection (Zero network / No model)
│
└── Ollama on localhost
└── Open-weight model reflection (llama3.2:3b)
Browser localStorage
└── Capture · Search · Export · Import
There is no intermediary VIGIL server. When the local open model is selected, requests go directly from the client's browser to the user's local Ollama process.
---
Why Does Open Innovation Matter?
A closed cloud AI API turns privacy into a mere promise:
> "Trust us. We will keep your family stories safe."
VIGIL makes privacy a structural guarantee visible in the architecture.
With open models and open-source software:
- Keeper Autonomy: The archive keeper can inspect every line of code, select their own local model, modify prompt templates, run smaller quantized models, fine-tune family-specific LoRA adapters, or remove the AI layer entirely while retaining their memories.
- Failure-as-Design: If Ollama is not running, VIGIL does not silently forward data to a remote cloud fallback or hallucinate fake status. It cleanly falls back to a clearly-labeled offline reflection.
- Permanent Availability: If internet infrastructure goes down, the core archival experience remains 100% operational.
The model is never the owner of the memory. It is simply an instrument the keeper can put down at any time.
---
The Important Design Decision
VIGIL strictly demarcates:
1. The original memory (what the human actually experienced and recorded).
2. The generated interpretation (the machine's echo).
3. The source of that interpretation (deterministic whisperer vs. local LLM).
This distinction is vital. A memory archive must preserve what a person actually wrote, never gradually supplanting it with synthetic, confident machine prose. Every echo is explicitly labeled as an interpretation; the original memory remains the inviolable source of truth.
---
Honest Limitations
- Browser Storage Lifecycle: VIGIL is an exploration of privacy-first architecture, not an encrypted cold-storage vault. Because data lives in browser localStorage, clearing browser cache/data will remove local records. Important archives should regularly be exported to JSON backups.
- Sample Data: The public GitHub Pages demo loads sample starter memories so visitors can experience the archive immediately without entering sensitive personal data.
---
The Spirit of It
The moment I wanted VIGIL to evoke was deliberately quiet:
Someone opens a memory about an orange radio, reads the sensory details of a kitchen and a fading song, and receives an echo that does not attempt to impersonate anyone.
It simply notices that the detail was kept.
> A memory does not need a cloud-sized model to matter. It needs a safe place to remain.
---
What I Would Build Next
- On-device speech-to-text with Whisper.cpp for spoken oral histories.
- Local vector embeddings for semantic and emotional memory search.
- Client-side AES-GCM encrypted backup archive files.
- Consent-based, family-specific LoRA adapters.
- Encrypted peer-to-peer sync between household devices.
---
Code & Links
- Live Demo: sharan-s-dev.github.io
- Source Code: sharan-s-dev/VIGIL
- License: MIT
5 reactions
Running Whisper Speech Recognition Locally on Vicharak Axon via whisper.cpp
Yes the RK3588 can run Whisper for local speech-to-text either via CPU threads (whisper.cpp on Cortex-A76 cores) or NPU-accelerated. Vicharak's own benchmarks show Whisper-base running at 0.215 RTF (real-time factor) on the NPU, meaning transcription runs roughly 4-5x faster than real-time audio playback.
CPU Path (whisper.cpp):
Bash:
git clone https://github.com/ggerganov/whisper.cpp
cd whisper.cpp
make -j8
bash ./models/download-ggml-model.sh base.en
./main -m models/ggml-base.en.bin -f input_audio.wav
For NPU-accelerated inference, see the RKNN model zoo's Whisper example or Vicharak'sAxon-NPU-Guide benchmark table.
Access Linux audio routing guides at docs.vicharak.in.
1 point
whisper の turbo と medium、Arm の CPU と CUDA でも速い順は入れ替わった
!
先に、この記事の要約
前回、自作の腕時計の声を文字に起こすのに、同じ録音で turbo と medium を比べたら、CPU では medium、内蔵 GPU では turbo が速かった
今回は別の PC(Arm の Windows ノート)を足して、CPU と GPU を 2 つずつ、合わせて 4 通りで測り直した
命令セットの違う CPU でも medium が速く、別系統の GPU(CUDA)でも turbo が速かった。内蔵 GPU は v1.9.5 では合計が互角になったが、読み込みを除くと turbo が速い。内訳の形も同じだった
途中で、Arm の Windows で whisper.cpp を MSVC でビルドすると NEON が使われず、桁が変わるほど遅くなることに気づいた。見分け方は system_info の 1 行
GPU が速いほど、合計に占めるモデルの読み込みが大きくなり、計算の差が見えにくくなる
前回の記事はこちら。測り方(同じ録音・同じ引数・各条件を 5 回続けて 1 回目を捨てる)と、区間の読み方はそこに書いた。
zenn.dev
先に結果
whisper.cpp を v1.9.5 にそろえて、同じ録音・同じ引数で 4 通り測った。2〜5 回目の平均(ms)である。
モデル
x86 CPU
x86 内蔵 GPU
Arm CPU
Arm CUDA
large-v3-turbo(fp16)
13,192
2,393
13,873
1,537
medium
10,370
2,408
9,698
1,794
large-v3-turbo(q5_0)
12,021
1,371
7,108
795
turbo と medium の速いほう
medium(1.27 倍)
互角(15ms 差)
medium(1.43 倍)
turbo(257ms 差)
CPU は 2 つとも medium が速い。 x86 は AVX2、Arm は NEON で、命令セットは違う
GPU は turbo が速い。 内蔵 GPU(Vulkan)だけは、この版では合計が互角になった(前回の記事の最後に書いたとおり、4 回の幅が重なる。読み込みを除くと turbo が 188ms 速い)
認識結果は全条件で正解と一致した
前回は 1 台の PC の中での比較だったので、「encoder が重い環境では encoder の小さいほうが勝つ」という説明が、その PC だけの事情かどうかが分からなかった。別の命令セットの CPU と、別系統の GPU で同じ向きが出たので、説明の範囲が少し広がった。
測った環境
2 台とも同じ録音(腕時計のマイクで録った日本語 6.635 秒・16kHz・モノラル・16bit)を使い、引数もそろえた。
whisper-cli -m <model> -l ja -t <threads> -bs 5 -bo 5 -f sample.wav -otxt -nt
x86 の PC
Arm の PC
本体
Intel Core Ultra 7 258V のノート
Surface Laptop Ultra(Windows 11 の Arm64 版)
CPU
8 コア 8 スレッド(AVX2)
NVIDIA RTX Spark N1X の Arm コア(20 スレッド)
GPU
内蔵 GPU の Intel Arc 140V(Vulkan ビルド)
RTX Spark N1X の GPU(6144 コア・Blackwell・CUDA 13.4 ビルド)
CPU ビルド
MSVC・AVX2。BLAS なし
clang 19.1.5(arm64-windows-llvm のツールチェーン)・NEON。BLAS なし
スレッド
-t 8
-t 8(20 スレッドの結果は後半)
whisper.cpp は 2 台とも同じコミットの v1.9.5 を自分でビルドした。 前回の記事の x86 の数字は 1.8.4 で、CPU 側は OpenBLAS 入りの公式のビルド済みパッケージだった。今回の x86 の CPU ビルドには OpenBLAS を入れていないので、前回の CPU 列とは版と BLAS の 2 つが違う(同じ 1.8.4 同士で比べた前回の表と、今回の 4 列の表は別のものとして読んでほしい)
BLAS が無いと encoder がはっきり遅くなる。同じ PC の 1.8.4(OpenBLAS 入り)と v1.9.5(BLAS なし)で、turbo の encode は 7,700ms から 11,235ms になった。Arm の CPU ビルドも BLAS なしなので、CPU の 2 列は「BLAS なし」でそろっている
Arm 側のビルドでは、clang の OpenMP ランタイム(libomp140.aarch64.dll)が実行に必要だった。これは Visual Studio に同梱されているデバッグ用の配布不可のファイルを使っていて、テスト用で再配布はできない。この記事ではビルドした実行ファイルや DLL は配っていない
Arm の Windows で MSVC を使うと、NEON が使われない
最初、Arm 側の CPU ビルドは MSVC で作った。その数字がこれである。
モデル
MSVC ビルド
clang ビルド
倍率
large-v3-turbo(fp16)
84,531ms
13,873ms
6.1
medium
53,894ms
9,698ms
5.6
large-v3-turbo(q5_0)
72,145ms
7,108ms
10.2
6.6 秒の音声に 84 秒かかっていた。原因は system_info の 1 行で分かる。
MSVC ビルド(NEON が無い)
system_info: n_threads = 8 / 20 | ... | CPU : ARM_FMA = 1 | MATMUL_INT8 = 1 | DOTPROD = 1 | OPENMP = 1 | REPACK = 1 |
clang ビルド(NEON = 1 が出る)
system_info: n_threads = 8 / 20 | ... | CPU : NEON = 1 | ARM_FMA = 1 | MATMUL_INT8 = 1 | DOTPROD = 1 | OPENMP = 1 | REPACK = 1 |
今回使った MSVC では、定義済みマクロの一覧(cl /PD)に ARM_NEON が無かった。ggml の CPU 側は __ARM_NEON の有無で経路を切り替えるので、MSVC でビルドすると行列積などが汎用の C コードで動く。/DARM_NEON=1 を外から与えて回避しようとすると、NEON の補助関数のところでコンパイルが通らなかった(少なくとも v1.9.5 では、ggml の NEON 経路は MSVC では使えないということになる)。
whisper.cpp のリポジトリには Arm の Windows 向けに clang のツールチェーンのファイルが用意されていて、それを指定すると NEON = 1 になった。flash attention を切る、llamafile の sgemm を入れる、といった他の手は、MSVC ビルドでは効果が無かった。
速さを測る前に system_info を見る、というだけの話だが、合計だけ見ていたら「Arm は CPU が遅い」で終わっていた。調べた範囲では、Windows on Arm でこの手のライブラリをビルドするときは、コンパイラの選択がそのまま桁に出るようだ。
区間で見ると、4 つとも同じ形をしていた
whisper.cpp が終了時に出す区間の時間から、encoder(encode)と decoder 側(batchd)を取り出して、turbo から medium を引いた差を並べる。プラスは turbo が損、マイナスは turbo が得である。
環境
encode の差
batchd の差
合計の差
x86 CPU
+4,604ms
−1,653ms
+2,822ms(turbo が遅い)
Arm CPU
+5,267ms
−1,067ms
+4,176ms(turbo が遅い)
x86 内蔵 GPU
+312ms
−455ms
−15ms(互角)
Arm CUDA
+24ms
−391ms
−257ms(turbo が速い)
turbo は decoder を 32 層から 4 層に削った版で、encoder は large-v3 と同じ 32 層・1280 次元のまま(medium は 24 層・1024 次元)である。だから decoder 側では得をして、encoder では損をする。どちらが大きいかが環境で入れ替わる。
CPU では encoder の損が大きく、GPU では encoder の差が縮むので decoder の得が上回る。CUDA では encoder の差が 24ms まで縮んでいて、turbo の encoder の重さがほとんど見えなくなっていた。
内訳の実数も載せておく(-t 8・2〜5 回目の平均・ms)。
環境
モデル
load
encode
batchd
合計
x86 CPU
turbo fp16
1,268
11,235
519
13,192
x86 CPU
medium
1,223
6,631
2,171
10,370
Arm CPU
turbo fp16
1,186
12,005
564
13,873
Arm CPU
medium
1,146
6,738
1,631
9,698
x86 内蔵 GPU
turbo fp16
1,230
720
309
2,393
x86 内蔵 GPU
medium
1,057
408
763
2,408
Arm CUDA
turbo fp16
1,118
130
147
1,537
Arm CUDA
medium
974
106
538
1,794
GPU が速くなるほど、合計はモデルの読み込みの話になる
CUDA の合計を見ると、turbo は 1,537ms のうち 1,118ms が読み込みで、計算しているのは 419ms しかない。読み込みを除いた時間で並べ直すとこうなる。
環境
turbo fp16
medium
差
x86 CPU
11,924ms
9,147ms
medium が 2,778ms 速い
Arm CPU
12,687ms
8,551ms
medium が 4,136ms 速い
x86 内蔵 GPU
1,163ms
1,352ms
turbo が 188ms 速い
Arm CUDA
419ms
820ms
turbo が 401ms 速い
合計では互角に見えた内蔵 GPU も、CUDA のように読み込みが大半を占める環境も、読み込みを外すと順位がはっきりする。毎回プロセスを起こす使い方(CLI)と、立てっぱなしにして投げる使い方(常駐サーバー)では、同じモデルでも見える順位が変わるということでもある。
常駐サーバーに同じ録音を HTTP で投げた往復の時間も測った。読み込みは起動時の 1 回だけになる。
サーバーの設定
モデル
x86 内蔵 GPU
Arm CUDA
既定(グリーディ)
large-v3-turbo
867ms
202ms
既定(グリーディ)
medium
921ms
473ms
-bs 5(CLI と同じ幅)
large-v3-turbo
1,269ms
340ms
-bs 5(CLI と同じ幅)
medium
1,501ms
724ms
常駐にすると、どちらの GPU でも turbo が速い。CUDA は内蔵 GPU と比べて、turbo で 4 分の 1 前後、medium で 2 分の 1 前後の時間で返ってきた。なお whisper-server は何も指定しないとグリーディ、whisper-cli はビームサーチの幅 5 が既定で、既定が逆である(前回これで条件がそろっていなかった)。
量子化(q5_0)の得は、ハードが速いほど小さくなった
3 つ目のモデルとして、turbo の q5_0(量子化版)も同じ条件で測った。
環境
q5_0 の合計
読み込みを除くと
fp16 との差(読み込みを除く)
x86 CPU
12,021ms
11,525ms
q5_0 が 400ms 速い
Arm CPU
7,108ms
6,690ms
q5_0 が 5,997ms 速い
x86 内蔵 GPU
1,371ms
944ms
q5_0 が 219ms 速い
Arm CUDA
795ms
404ms
q5_0 が 15ms 速い
Arm の CPU では q5_0 が 3 つの中で最速で、x86 の CPU より速かった。 この clang ビルドには fp16 のベクトル演算が無く(HAVE_FP16_VECTOR_ARITHMETIC が付かない)、整数側の命令は使える。fp16 の 2 つが遅いのはそのためではないか、という見立てはあるが、これは推測で、どの演算がどれだけ使われたかまでは測っていない
CUDA では、q5_0 の得はほぼ読み込みの軽さだけだった(ファイルが小さい)。計算の時間は fp16 と 15ms しか違わない
量子化は「速くなる」とまとめられがちだが、調べた範囲では、速いハードほど計算上の得は小さくなり、残るのは読み込みの軽さのようだ
スレッドを増やすと、encoder は縮むが decoder 側は縮まない
Arm の PC は whisper.cpp が 20 スレッドを認識する(出力の n_threads = 8 / 20)ので、-t 8 と -t 20 の両方を測った。
モデル
-t 8 の合計
-t 20 の合計
encode
batchd
large-v3-turbo(fp16)
13,873ms
11,015ms
12,005 → 9,004ms
564 → 579ms
medium
9,698ms
7,478ms
6,738 → 4,162ms
1,631 → 2,069ms
large-v3-turbo(q5_0)
7,108ms
4,448ms
6,326 → 3,714ms
276 → 226ms
合計はどれも縮むが、中身は一様ではない。encoder は音声全体を一度に通す 1 回の呼び出しなので素直に縮み、decoder 側は 160 回以上の細かい呼び出しなので縮みにくく、medium では逆に増えた。スレッドを増やせば一律に速くなるわけではない、というのがこの表から言えることである。
ばらつきも書いておく。turbo fp16 の -t 20 は 2〜5 回目でも 10,294〜11,674ms(13%)あり、ほかの条件(数 %)より広かった。合計だけを 1 回測って比べると、この幅に埋もれる差は見えない。
測れていないこと
バックエンドをそろえた GPU 同士の比較はしていない。 Arm 側には Vulkan のビルドを作っていないので、Arc 140V(Vulkan)と RTX Spark N1X(CUDA)は、バックエンドも世代も違うものを並べている。「どちらの GPU が速いか」を言える測り方ではない
CPU の 2 列は BLAS なしでそろえたが、コンパイラとフラグはそろっていない(x86 は MSVC・AVX2、Arm は clang・-march=armv8.7-a ほか)
1 台ずつの実測なので、この 2 台で同じ向きが出た以上のことは言えない。別の CPU・GPU で逆になる可能性は残る
測定は前回と同じ 1 サンプル(6.6 秒・1 文・1 話者)だけである。長い音声・雑音のある音声・複数話者では比率が変わる可能性があり、確かめていない
認識結果は全条件で正解と一致した。精度の比較はしていない(この音声では差が出ないので、精度順を論じる材料にはならない)
Arm 側のビルドに使った clang の OpenMP ランタイムは、開発環境に同梱されているデバッグ用(再配布不可)のものである。ビルドした実行ファイルや DLL は配っていない
まとめ
CPU では medium、GPU では turbo という前回の結果は、命令セットの違う CPU と、別系統の GPU でも同じ向きで出た(内蔵 GPU は v1.9.5 では合計が互角で、読み込みを除くと turbo が速い)
その理由も同じで、turbo は decoder を削った代わりに encoder が大きいまま。encoder が重い環境では損が勝ち、GPU では得が勝つ
GPU が速いほど、合計に占めるモデルの読み込みが大きくなる。合計ではなく区間で見るか、常駐させて読み込みを外して測る
Arm の Windows では、ビルドに使うコンパイラで桁が変わった。system_info の 1 行を先に見る
whisper.cpp は MIT ライセンス(ggml-org/whisper.cpp)。区間の名前はソースのカウンタのコメントから取った。
スマートウォッチの記事
電子工作素人の ESP32-S3 スマートウォッチ自作奮闘記 —— 3回作り直して、市販ボードに乗り換えるまで(入口)
機械式エンコーダが入らず、磁気式 AS5600 でクラウンを作った
CO5300 の AMOLED を180°回す —— ケースに入れたら上下逆だった画面を、オフセットに依存しないソフト反転で直した
スワイプが成立しないのは、タッチではなく描画ループの速さが原因だった —— 入力の取りこぼしを、入力の外側で直した
やってはいけない判断の記録。ESP32-S3 の自作スマートウォッチが「61℃」と出した —— microSD を切って押し込んだ判断と、温度の数字の読み違い
待機電流を削ろうとして、ESP32-S3 の自作スマートウォッチを文鎮化した —— 待機電流を削ろうとして切った電源が、それを戻すための通信線の電源だった
待機電流を削る前に、測り方で3回騙された。ESP32-S3+AXP2101 の残留電流ハント —— 0% まで使い切った測定と、進んだ時計で、改善したと見誤った
whisper の turbo と medium、速いほうが CPU と内蔵 GPU で入れ替わった。区間に分けて測る —— 予備経路のモデル選びを、区間ごとの実測でやり直した
この記事 —— 同じ測り方を Arm の CPU と CUDA に広げて、4 通りで確かめた
Mentions
Open-source AI tool you need to know 📌 Whisper.cpp is a fast C/C++ port of OpenAI's Whisper—running zero-dependency local speech-to-text on Apple Silicon, CPUs, & WASM. 🔗 ggml-org/whisper.cpp Bookmark this for voice AI! #OpenSource #GitHub #AI
DIY Sleep Apnea Guardian: Fine-tuning Whisper.cpp for Edge Audio Analysis Sleep is supposed to be the time when our bodies recharge, but for millions, it’s a period of hidden health risks. Sleep Apnea detection is traditionally locked behind expensive sleep labs and bulky equipment. However, with the rise of Edge AI and high-efficiency models like Whisper.cpp, we can now build a low-power, privacy-first monitoring system right on a Raspberry Pi. In this tutorial, we are going to dive deep into Raspberry Pi audio analysis to detect snoring patterns and breathing pauses (apnea events). We’ll use Librosa for feature extraction and a fine-tuned, quantized version of OpenAI's Whisper model to differentiate between normal rhythmic breathing and dangerous obstructive patterns. This is the ultimate "Learning in Public" project for those looking to master audio processing at the edge! 🚀 The Architecture: From Sound Waves to Life-Saving Data Building a real-time audio classifier on a low-power device requires a lean pipeline. We don't want to send raw audio to the cloud (privacy first!), so everything happens locally. graph TD A[Microphone Input] --> B[Librosa Pre-processing] B --> C{Voice Activity Detection} C -- No Activity --> A C -- Snore/Breathing Detected --> D[Whisper.cpp Inference] D --> E[Spectrogram Feature Analysis] E --> F{Apnea Event?} F -- Yes --> G[Local Alert / Dashboard] F -- No --> H[Log for Morning Report] G --> I[Dockerized Edge Gateway] Prerequisites 🛠️ Before we get our hands dirty with code, make sure you have: - Hardware: Raspberry Pi 4 (4GB+) or Raspberry Pi 5. - Audio: A USB Condenser Microphone or a high-quality ReSpeaker Hat. - Stack: - Whisper.cpp: The high-performance C++ port of OpenAI's Whisper. - Librosa: For advanced audio manipulation. - Docker: To ensure our environment is reproducible across edge nodes. Step 1: Pre-processing with Librosa 🎙️ Raw audio is messy. To help our Whisper model focus on what matters, we use Librosa to filter out background noise (like a humming fan) and normalize the volume. import librosa import numpy as np def preprocess_audio(file_path): # Load audio with a sampling rate of 16kHz (Whisper standard) y, sr = librosa.load(file_path, sr=16000) # Noise reduction: Simple spectral subtraction stft = librosa.stft(y) stft_db = librosa.amplitude_to_db(np.abs(stft)) # Focus on the frequency range of snoring (20Hz - 2000Hz) # This helps in ignoring high-frequency electronic hiss y_filtered = librosa.effects.preemphasis(y) return y_filtered # Example of extracting Mel-spectrogram features for apnea pattern matching def get_spectrogram_features(y, sr=16000): S = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128) return librosa.power_to_db(S, ref=np.max) Step 2: Running Whisper.cpp on the Edge ⚡ We use Whisper.cpp because it's optimized for ARM processors. We’ll use the tiny.en model, fine-tuned on respiratory sound datasets, and quantize it to q5_1 to ensure it runs in real-time without melting the Pi. # Clone and build whisper.cpp git clone https://github.com/ggerganov/whisper.cpp.git cd whisper.cpp make # Download the tiny model and convert to GGML format bash ./models/download-ggml-model.sh tiny.en # Run inference on our recorded sleep segment ./main -m models/ggml-tiny.en.bin -f sleep_segment.wav -nt To make this "smart," we don't just transcribe text; we use the log-probability of the tokens to detect the "sound" of a snore, which the model often interprets as specific non-verbal tokens. Step 3: Containerizing with Docker 🐳 To deploy this across multiple edge gateways, we wrap everything in a lightweight Docker image. FROM python:3.9-slim-buster RUN apt-get update && apt-get install -y \ build-essential cmake git libasound2-dev portaudio19-dev # Install Python dependencies RUN pip install librosa numpy sounddevice # Copy our optimized whisper.cpp binary COPY ./whisper-bin /usr/local/bin/whisper-edge WORKDIR /app COPY monitor_sleep.py . CMD ["python", "monitor_sleep.py"] The "Official" Way: Advanced Patterns 🥑 While building a DIY version is a fantastic learning experience, scaling medical-grade audio analysis requires robust data handling and specialized ML architectures. If you're interested in more production-ready examples, or if you want to see how we handle high-concurrency audio streams and advanced anomaly detection patterns, I highly recommend checking out the WellAlly Engineering Blog. It’s a goldmine for developers looking to bridge the gap between "it works on my machine" and "it works for a million users." Step 4: Logic for Apnea Detection 🧠 The core logic lies in detecting "Silence" where there should be "Rhythm." If the Mel-spectrogram shows high-energy snoring followed by a flatline of 10+ seconds (the clinical definition of an apnea event), we trigger an alert. def analyze_apnea_event(energy_profile, threshold=0.01): """ Detects gaps in breathing energy. energy_profile: list of energy levels over time """ silence_duration = 0 for energy in energy_profile: if energy < threshold: silence_duration += 1 else: if silence_duration > 10: # 10 seconds of silence print("⚠️ Potential Apnea Event Detected!") trigger_alert() silence_duration = 0 Conclusion: Privacy-First Health Tech By leveraging Whisper.cpp and Raspberry Pi, we've built a system that is: 1. Private: No audio ever leaves your bedroom. 2. Low Latency: Analysis happens in milliseconds. 3. Cheap: Runs on $50 hardware. This project is a perfect example of how Audio Spectrogram analysis and modern AI can solve real-world problems. Have you tried fine-tuning models for non-speech sounds? Let's discuss in the comments below! 👇 --- If you enjoyed this build, don't forget to ❤️ and bookmark! For more deep dives into technical architecture, visit the WellAlly Blog.
You're doing #podcasts/content or alike? whisper.cpp is really worth a look, it is fast and works well, fully free/OSS! ggerganov/whisper.cpp
Inflearnが自動で字幕を生成するまで(AI/インフラ編) ! この記事は2023年10月31日に執筆されたものです。 こんにちは。InflabのDevOpsエンジニア、SunBです。 2023年9月、Inflearnに字幕とスクリプトの機能が追加されました。皆さん、活用されていますか? 今回追加された字幕機能は、動画の音声データをもとに自動で字幕を生成する部分と、生成された字幕を受講画面に表示する部分の、大きく2つに分けられます。 今日は、このうち私が実装を担当した字幕生成について、どのような技術をもとに、どのような過程を経て実装したのかをお話しします。 2日で字幕機能を作ってデモする まず、「DevOpsエンジニアが字幕機能を実装したの?」と思われる方もいらっしゃると思うので、今回の字幕機能の実装プロジェクトがどのように始まったのかをご紹介します。 Inflabでは今年から、Infthonという名前で社内ハッカソンを開催しています。応募者をランダムに4人1組のチームに分け、約2日間でアイデアの議論とMVPの実装を行ったあとに発表する、という形式です。 ところが偶然(?)、Inflearnの講座プレイヤーやVODインフラを多く手がけていたFE開発者のPapicoと私が同じチームになり、自然と講座の再生に関するアイデアを議論することになりました。そして、第1回Infthonのテーマだった「チームの生産性向上、またはAIを活用したサービス改善」に最も合うと考えた講座のAI字幕機能をアイデアとして選びました。 まず、AIベースの字幕生成機能を実装するにあたって、最低限の条件として次のことを考えました。 字幕生成がオフラインで動作すること(データの外部送信を防ぐ) 字幕生成にコンピューティングリソース以外の追加費用がかからないこと(オープンソースモデルを自前で実行) 字幕生成にかかる時間が動画の長さを超えないこと(ライブデモができるくらい速いこと) これらの条件を満たす技術を探してみたところ、OpenAIのWhisperというモデルが目に留まりました。Whisper以外に検討した技術は以下のとおりです。 KoSpeech: Archived OpenSpeech + KsponSpeech: 自分たちで学習させてモデルを作るには、技術への理解度もまだ低く、時間もかなりかかると予想された KakaoのPORORO: Archived MetaのSeamlessM4T: テスト用の講座を使ったPoCの過程で、SeamlessM4T largeモデルはWhisper large-v2モデルに比べて精度が劣ることを確認 NaverのCLOVA SpeechなどのAPIベースのサービス: データの外部送信とAPI料金が発生 Infthon以前にも、MacWhisperなどのツールで、OpenAIのSTTモデルであるWhisperの存在とその精度の高さは知っていましたが、深く調べたことはありませんでした。実装期間が2日と短かったため、仕組みを深く理解することよりも、素早く適用してデモできるかどうかを重視して作業を進めました。その結果、すべての処理を私のMac上でオフラインで動かす構成になりました。その際、Pythonで実装されたOpenAIのWhisperは遅すぎたため、代わりにC++に移植されたwhisper.cppを使いました。 こうして、次の流れを短期間で実装できました。 動画をアップロードする ffmpegで16-bit WAVの音声データを抽出する whisper.cpp + large-v2モデルで字幕を書き起こし、srtファイルとして保存する 受講画面でこのsrtファイルを再生する さらに、FE開発者のPapicoの活躍で、スクリプトの表示と再生位置の移動、字幕の修正依頼と修正依頼の管理機能までデモでき、第1回Infthonで1位という栄誉を手にしました。 こうして楽しい経験として幕を閉じたと思っていたのですが…… 正式プロジェクトとして採用 ある意味当然のことながら、字幕機能はすぐに正式プロジェクトとして採用され、新たなスタートを切りました。 Infthonですでにデモまで終えていたので大きな心配はありませんでしたが、引っかかる点が3つありました。 Shaka Playerでm3u8のHLSプレイリストとsrt字幕ファイルを同時に表示すると、字幕の同期がずれる問題 AWSのインフラでWhisperをまだ動かしたことがなかった点 韓国語字幕の1行あたりの長さと改行位置が適切でなく、読みにくい問題 1つ目は、FE開発者のPapicoが字幕の表示処理をShaka Playerの機能を使わずにReactで新たに実装し、きれいに解決してくれました。一方、2つ目と3つ目は、そのまま現実の問題として降りかかってきます。 インフラ上でWhisperを動かす Infthonのときとは違い、今回は実際のユーザーにサービスを提供するため、まずは私たちのインフラ上で機能を動かす必要がありました。 エンコーディングインフラの改善記録 字幕生成インフラについてお話しする前に、まず動画エンコーディングインフラを改善した過程をご紹介します。 私たちは2022年10月まで、動画のエンコーディングにAWS MediaConvertを使っていましたが、コスト削減とエンコーディングオプションのカスタマイズのために、EC2 c6i.xlarge Spot InstanceベースのAWS Batch構成に移行しました。MediaConvertのReserved Queueなど、さまざまな代替案も試しましたが、予約したエンコーディングの開始が遅すぎたり、4Kなど一部のオプションが使えないといった制約があったりして、要件を満たすのは難しいと判断し、自分たちで構成する方向を選びました。 結果として、インフラ構成の変更は効果がありました。長さ28分の1080p動画を540p、720p、1080pにエンコードする場合、AWS Batchを使ったときとMediaConvertを使ったときにかかる時間と費用は、以下のとおりでした。 AWS Batch (ECS EC2 c6i.xlarge Spot) MediaConvert (On-Demand Pro) 差 時間 約10分 約7分 約3分 (x0.7) 費用 約$0.021 約$1.895 (SD x 1, HD x 2) 約$1.874 (x90.2) 動画の長さと比べれば、エンコーディング時間の差はわずかですが、費用はなんと90倍以上の差があることを確認しました。 AWS Batch構成で、EC2インスタンスタイプごとの費用と性能を分析した結果は以下のとおりです。(長さ28分の1080p動画を540p、720p、1080pにエンコードする場合、Spot Instance未適用) インスタンスタイプ オンデマンドの1時間あたりの費用 (A) エンコーディングの実行時間 (B) 実際の費用 (A x B) c6i.xlarge $0.192 10分 $0.032 c6g.xlarge $0.154 12分47秒 $0.033 c6i.2xlarge $0.384 5分57秒 $0.038 c6i.4xlarge $0.768 4分23秒 $0.056 g5g.xlarge $0.5166 5分1秒 $0.043 g5.xlarge $1.237 3分15秒 $0.067 エンコーディングインフラと同じ構成でwhisper.cppを試す 上記の改善したエンコーディングインフラをもとに、字幕インフラも同じ構造で構成してみました。AWS Batch上のc6i.xlargeインスタンスで28分の動画に対してwhisper.cppを実行したところ、結果はなんと69分3秒と、予想よりもはるかに遅いものでした。 Infthonのデモ用にMac Studio(M1 Ultra)で同じ動画をwhisper.cppで処理したときは、3分30秒でした。これを見て、すぐにGPUインスタンスのPoCに取りかかりました。 GPUインスタンスのPoC 最初は、g5インスタンスより安いARMベースのg5g.xlargeインスタンスを使ってみようとしました。ところが、NVIDIAドライバーやCUDAのバージョンをいくら変えても、PyTorchからCUDAを認識できませんでした。CUDA 11のPython cuDNNライブラリはARMアーキテクチャをサポートしておらず、PyTorchの正式版はCUDA 12をサポートしていなかったため、PyTorch Nightly + CUDA 12 cuDNNの構成を試しましたが、torch.cuda.is_available() が False を返す状態が続きました。そんな中、AWS BatchではまだARMアーキテクチャベースのGPUインスタンスをサポートしていないというドキュメントを見つけ、g5gインスタンスの利用はひとまず諦めました。 その後、g5.xlargeインスタンスでOpenAIのPythonベースのWhisperを実行してみたところ、7分17秒かかりました。動画の長さに比べれば許容範囲でしたが、Mac Studioでwhisper.cppを使ったときは3分30秒だったことを考えると、ほかに方法はないかと考えるようになりました。そして、SeamlessM4Tについて調べていたとき、偶然Hacker NewsでWhisperXに言及しているコメントを見かけました。 WhisperXへの移行 WhisperXを使って、g5.xlargeインスタンスで28分の動画の字幕生成を試してみたところ、59秒で完了し、最も速い結果となりました。その出力結果も、Whisperやwhisper.cppと比べて精度が劣るということはなく、それぞれ一長一短がありました。 以下は、出力結果を比較した当時に書いたメモの一部です。 Whisper、WhisperXはg5.xlargeで、whisper.cppはMac Studioで抽出したバージョン 3つともかなり違いがあるが、どれか1つが最も優れているというわけではなく、それぞれにうまく書き起こせている部分がある。 WhisperXが正確に抽出した部分 뽑았는데 (Whisper: 꼽았는데) 등허리 (Whisper: 등어리) 복부 (Whisper: 복구) whisper.cppが正確に抽出した部分 Eye Reflect (WhisperX: I Reflect) Whisperが正確に抽出した部分 Z-Remesh (WhisperX: gremesher) 아이리플렉트 (WhisperX: I Reflect) WhisperXはほかの2つよりはるかに速く実行でき、音節単位でalignするので行の再構成に有利であり、字幕の修正依頼機能の有効性も比較的高くなると推定されることを考えると、WhisperXを使うのが最も良いと思われる。 ただし、ときどき複数の文を早口で話す部分で抜け落ちる文があるが、精度が低いと判断して意図的に除外しているように見える。 まとめ 最初に試したwhisper.cpp + c6i.xlargeを基準に、試した組み合わせとの差をまとめると次のとおりです。(large-v2モデルを使って28分の音声データを書き起こした場合) ツール マシン 実行時間 差(速度) whisper.cpp c6i.xlarge 69分3秒 - (x1) Whisper c6i.xlarge 時間がかかりすぎて計測を中断 - WhisperX c6i.xlarge 実行中にエラーが発生 - whisper.cpp Mac Studio 3分30秒 65分33秒 (x19.7) Whisper Mac Studio 時間がかかりすぎて計測を中断 - WhisperX Mac Studio 15分26秒 53分37秒 (x4.5) whisper.cpp(cuBLASビルド) g5.xlarge 17分25秒 51分38秒 (x4) Whisper g5.xlarge 7分17秒 61分46秒 (x9.5) WhisperX g5.xlarge 59秒 68分4秒 (x70.2) 最終的に、WhisperX + g5.xlargeの組み合わせを選びました。 こうして最初の絶望を乗り越え、速度を70倍以上改善し、インフラ上で字幕生成機能を正常に動かせるようになりました。その過程でGPUインスタンスや動画エンコーディングに関する分析も行い、エンコーディング関連のプロセスも改善できました。 (試行錯誤の跡) 字幕の長さと読みやすさの改善 字幕をどう生成するかという課題は、音声データからテキストを抽出するだけでは終わりませんでした。 whisper.cpp、Whisper、WhisperXのいずれにも、--max_line_width のような1行の長さを制限するオプションがあったのですが、問題はこのオプションが動作しないことでした。(……)おそらく韓国語だから動作しないのだろうと推測しています。 そのため、モデルが出力した字幕データは行ごとに長さがバラバラで、文の終わりで改行されることもほとんどなく、読みやすさは最悪でした。 これを改善するために、ピリオド/疑問符/感嘆符があれば改行する案、一定の文字数以上なら単純に空白を基準に半分に分割する案、形態素解析によって行を再構成する案など、さまざまな方法を模索しました。 そんな中、WhisperXに移行して音節単位で時間を追跡できるようになったことで、いっそすべての行を一から作り直そうと考えました。そこで以下の基準を定め、WhisperXの実行から行の構成までを一度に行うPythonスクリプトを実装しました。 WhisperXが時間の追跡に失敗した音節の時間は、時間の追跡に成功した最も近い前後の音節の時間、または文の開始・終了時間から線形補間する。(wav2vecの韓国語モデルベースだったため、ハングル以外は音節の時間を追跡できないという問題がありました。) 直前の単語までの行の長さが0.5秒未満であれば、ほかのどの条件を満たしていても改行しない。 直前の単語までの行の長さが10秒を超えたら、現在の単語から新しい行にする。 現在の単語を加えると行が10文字を超え、かつ形態素解析の結果で文の数が変わる場合は、現在の単語から新しい行にする。 現在の単語を加えると行が50文字を超える場合は、現在の単語から新しい行にする。 文の開始が直前の文の終わりから0.5秒以内の場合は、文の開始時間として直前の文の終了時間を代わりに使う。 実は、このうち形態素解析で文の数を計算し、それをもとに改行する基準は最後に作ったものです。この基準を決めるまでは、WhisperXをCLIで実行したあと、JSONで保存されたデータを読み込んで行を構成する処理をすべてGoで実装していました。しかし、形態素解析と文の数の計算にKiwiを使うことになったため、WhisperXをPython内で直接実行するようにし、行の構成もPythonで実装し直しました。そしてGoは、このPythonスクリプトを実行し、最終的に構成された字幕データをストレージに保存するという、AWS Batch内の1つのジョブのライフサイクル管理だけを担う構造にしました。 こうして、現在提供している字幕のように、読みやすさが大きく改善された字幕データを生成できるようになりました。 GPUインスタンス不足の問題を回避する 問題はすべて解決したと思い、これまでにアップロードされた講座を対象に、実際に字幕を一括生成しようとしました。ところが、AWS Batchにジョブを投入していくら待っても、EC2インスタンスが起動しませんでした。Spot Requestを確認してみると、ソウルリージョンにg5.xlargeインスタンスのSpot Capacityがなく、起動できていなかったのです。 Spot Instanceとは? Spot Instanceは、AWSで使われていないコンピューティングリソースを最大90%割引した価格で提供する機能です。実行中のSpot Instanceがときどき中断されることもありますが、AWS Batchなどを使ってワークフローの耐障害性を確保していれば、Spot Instanceを適用するだけで60%以上のコスト削減効果を得られます。 実際に、動画エンコーディングインフラにSpot Instanceを適用することで平均70%のコストを削減できていたので、そもそも費用のかさむGPUインスタンスを使ううえで、Spot Instanceの適用は必須でした。 Spot Capacityを求めて GPUインスタンスが足りないという話はよく耳にしていましたが、実際に経験したのは初めてでした。3日待っても起動しなかったので、別の方法を探し始めました。 最初に、Spot Placement Scoreという機能を使ってみました。米国のバージニア、オハイオ、オレゴンリージョンはg5.xlargeインスタンスが最も安く、Placement Scoreも高く出ていたので、まずはバージニアリージョンで試してみました。 初めて使うリージョンで何もなかったため、VPC、Subnet、EIP、NAT Gatewayをはじめ、AWS Batch関連の各種リソースをすべて新しく作成しました。ところが実行してみると、同じようにSpot Capacityがないというエラーが発生しました。しまった!と思い、すぐ隣のオハイオリージョンにも同じ構成を作ってみましたが、やはりSpot Capacityがないというエラーが発生しました。(……) Placement Scoreは当てにならないなと思って調べてみたところ、スポットインスタンスアドバイザーというものを見つけました。g5.xlargeスポットインスタンスの中断頻度が低いリージョンを探してみると、オレゴンリージョンが該当することが分かりました。 バージニア、オハイオリージョンに作ったものはすべて削除し、オレゴンリージョンに構成して実行してみると、ようやく字幕生成ジョブが動き始めました。一括処理のために2,000台のg5.xlargeインスタンスを同時に起動する計画でしたが、オレゴンリージョンでさえインスタンス不足で最大500台ほどしか起動できませんでした。それでも、約4時間で字幕の一括生成を完了できました。 一括処理のコスト 基盤 単価 費用 差 AWS Batch (g5.xlarge Spot) - $807.22 - (x1) AWS Batch (g5.xlarge On-Demand) - 約$2,690 約$1,883 (x3.3) OpenAI Whisper API $0.006 / minute 約$8,700 約$7,893 (x10.8) Naver CLOVA CSR API $0.012 / minute 約$17,400 約$16,593 (x21.6) 字幕の一括生成作業にかかった費用は $807.22 で、約24,000時間に及ぶ動画の音声データをAPIベースのサービスで処理する場合と比べて、10分の1以下のコストで字幕を生成できる構成を実現できました。 結論 以上のような過程を経て、約2か月で字幕生成機能を無事に実装できました。Infthonをきっかけに、2日間でアイデアの選定から技術調査、デモ用の実装までを終え、その後の2か月間で、それをユーザーにリリースできるレベルまで引き上げるためにさまざまな試みをしました。貴重な経験でした。 字幕生成に関する技術的な部分も注目に値しますが、特に強調したいのは、1人が短期間でこなしたにしては、インフラ関連の作業量がかなり多かったことです。何のリソースもないリージョンを3回も渡り歩き、VPCからAWS Batch環境までを一晩で構成できたのは、何かが準備されていなければ不可能なことです。特に、受講権限を持つユーザーにのみ字幕データを提供する対応や、字幕のストレージをS3からMongoDBに移行する作業など、本文には書いていない数多くのインフラ作業がありました。それらに触れなかったのは、ほとんど悩むことなく素早く対応できたからです。 これが可能だったのは、これまで懸命に構築してきたIaCのおかげだと思います。私たちがどのようにIaCを構築したかについては、(今度こそ本当に)近日公開予定の「InflabのIaC構築記(Part 1)」の記事でご覧いただけます。AWS CDKからTerraformへの記事でTerragruntとTerratestについての記事を予告しておきながら、1年以上新しい記事を投稿できなかった理由も、その記事で詳しく書いていますので、IaCに興味のある方はぜひ読んでみてください。 ありがとうございました。
whisper.cpp runs OpenAI's Whisper speech recognition locally in C/C++, on your own laptop or phone: ggml-org/whisper.cpp
How to Run LiquidAI d1-Omni-2B Locally (It Hears Audio but Can't Talk Back) If you came here hoping to run LiquidAI d1-Omni-2B locally as a small voice assistant, I'll save you the download first: it can't talk back, and it can't write back either. It listens to audio and returns typed answers with probabilities attached, and that's all you get out of it. Hugging Face files d1-Omni-2B under audio-text-to-text, and that label sets the wrong expectations. Liquid calls it a "decision model". You give it a state (text, JSON, an image, a video, or an audio clip) and a set of named questions, and it answers all of them in one forward pass with zero generated tokens. The card says plainly that it "writes no transcript or speech", and the docs say it is not a chat model. I think that's a good design, for reasons I'll get to below. But it changes what "running it locally" means, so here's what the 2B adds over the 600M, how much RAM it needs, and what works today in transformers, llama.cpp, Ollama, MLX and ONNX (as of late October 2026). All benchmark and speed numbers below are Liquid's own. I haven't run it myself. What d1-Omni-2B actually does You ask typed questions and get typed answers: - noul: yes/no, returns P(yes) - choice: one of several named options, with probabilities - score: an ordinal scale of 2 to 10 levels ("Agitated", "Neutral", "Calm") - in transformers, not yet in llama.cpp: multiple choice, extraction of text/number/date spans, number estimates with intervals, and bounding boxes That fits call routing ("billing, technical or fraud?"), voicemail triage, sound event detection, or a cheap check at the front of a pipeline before a bigger model gets involved. Questions can even be spoken: the instructions field takes an audio clip. For text- or image-only work, Liquid's card itself tells you to use d1-2B instead, which beats the Omni on most rows. d1-Omni-2B vs d1-omni-600M: what 3x the parameters buys you The 600M came out with the Open d1 release on October 7, 2026, billed as Liquid's "first experimental checkpoint". According to OrcaRouter's repo write-up, the 2B repo was created on October 24. The 2B sits on a different base model, so treat it as a separate model rather than a bigger 600M: | | d1-omni-600M | d1-Omni-2B | |---|---|---| | Parameters | 587M | 2.17B | | Base | LFM2.5-Encoder-350M | LFM3-Omni-2B | | Context | 16,384 tokens | 128K tokens | | Audio input | one 16 kHz mono clip, up to 30 s | clips + spoken questions | | Images + audio in one request | no (ValueError) | yes | | Speech languages | trained on English requests | 12 (the 16 text languages minus Hindi, Thai, Vietnamese, Indonesian) | | Decision Index v0.3 | 18.3 | 49.0 | | Average (public decision + image benchmarks) | 42.8 | 67.7 | | Min transformers | 5.15 | 5.19 | The jump is large. Liquid says the 2B beats the 600M on every benchmark it reports, and beats d1-3B on the decision board and on 16 of 18 image benchmarks with about 70% of the parameters. VisualWebBench goes from 14.9 to 88.6. The 600M card reports no audio benchmarks at all, so there's no audio comparison to make between the two. Audio scores: written vs. spoken input The 2B card does publish audio scores, and they're worth a careful look. Each task is scored twice, once with the input written as text and once with the same input read aloud. The gap between the two columns is what the speech path costs you: | Test | Written | Heard | |---|---|---| | SLURP (assistant commands, real speakers) | 87.4 | 81.7 | | Speech-MASSIVE (12 languages) | 85.2 | 63.4 | | FLEURS (spoken language ID, 43 languages) | 92.4 | 92.5 | | ESC-50 (sound events) | n/a | 84.5 | | Spoken short states | 42.0 | 22.8 | | Belebele (passages, 8 languages) | 83.6 | 41.8 | Short English commands and sound classification hold up. Long passages and non-English speech fall apart: Belebele loses half its score once you speak it. The card admits that "long spoken passages lose much of their accuracy, most of all outside Latin-script languages." If you're building multilingual voicemail triage, run your own eval before you trust it. How much RAM and VRAM d1-Omni-2B needs Liquid publishes no memory requirements, so here's the arithmetic plus the file sizes they do publish. At 2 bytes per parameter, 2.17B is about 4.3 GB of weights, which matches the single 4.33 GB BF16 safetensors file. Being a hybrid (13 short-convolution layers, 5 sliding-window, only 6 global attention), it should keep a smaller KV cache than a pure transformer at long context. And since it never decodes, you only pay for the prefill. | Build | Size on disk | Where it runs | |---|---|---| | Safetensors BF16 | 4.33 GB | transformers (CUDA, MPS, CPU in fp32) | | GGUF BF16 / F16 | 3.47 GB | llama.cpp | | GGUF Q8_0 | 1.85 GB | llama.cpp | | MLX 8-bit | 2.71 GB | Apple Silicon | | ONNX q4 | 2.45 GB | CPU | | ONNX q4f16 | 2.20 GB | GPU / WebGPU | | ONNX fp32 | 8.32 GB | reference | Watch the CPU path in transformers: the sample code loads in float32 on CPU, so budget for roughly double the BF16 footprint, around 8.7 GB by my math. The GGUF sizes are the language model only; llama.cpp also pulls an mmproj file for audio and images (0.33 GB at Q8_0, 0.61 GB at BF16). On an 8 GB laptop, use the Q8_0 GGUF or ONNX q4. Runtime status: transformers, llama.cpp, Ollama, MLX transformers Full support, including every question type. It needs remote code: pip install "transformers>=5.19" torch torchvision pillow import torch from transformers import AutoModel device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu" dtype = torch.float32 if device == "cpu" else torch.bfloat16 model = AutoModel.from_pretrained("LiquidAI/d1-Omni-2B", trust_remote_code=True, dtype=dtype).to(device) Then call model.system_one(state, questions, media=...). WAV files need to be 16-bit PCM and get resampled to 16 kHz. llama.cpp It works from build b11960 (v0.8.0) using the official GGUF, but only for noul, choice and score. Extraction, estimates, boxes and multi-select aren't supported there yet. On a laptop, this is what I'd run: llama-server -hf LiquidAI/d1-Omni-2B-GGUF:Q8_0 -b 8192 -ub 8192 Don't drop the batch flags. Each question's full state and media has to fit in a single batch. An image can use at most half of -ub, so 8192 gives it the full 4096-token budget. Then POST audio to the custom endpoint (this is the card's audio example): curl -sL -o jfk.wav https://github.com/ggml-org/whisper.cpp/raw/master/samples/jfk.wav curl http://127.0.0.1:8080/v1/systemone -H "Content-Type: application/json" -d @- <<JSON { "state": "Recording from a public event.", "files": ["data:audio/wav;base64,$(base64 < jfk.wav | tr -d '\n')"], "questions": { "kind": {"type": "choice", "instructions": "What kind of utterance is this?", "criteria": {"request": "A request to do something", "question": "A question asking for information", "speech": "A speech to an audience"}}, "calm": {"type": "score", "instructions": "How calm is the speaker?", "criteria": ["Agitated", "Neutral", "Calm"]} } } JSON Spoken questions work here too: put an input_audio part in a question's instructions. Ollama There's an official d1-omni library entry with a 2b tag at 4.4 GB. That's roughly the BF16 checkpoint, so don't expect a small quant. It serves the same /v1/systemone endpoint on port 11434, and the CLI gets an --ask flag. The page's own example: curl http://localhost:11434/v1/systemone \ -H "Content-Type: application/json" \ -d '{ "model": "d1-omni:2b", "state": "Hello World", "questions": { "says_hello": { "type": "noul", "instructions": "Does the state text contain a greeting?", "criteria": { "true": "The state text contains a greeting.", "false": "The state text does not contain a greeting." } } } }' There's a catch. When I checked, the readme on that page still described the 600M (587M parameters, English speech only, 30-second clips, no audio and images together), and Liquid's d1-Omni-2B docs page only covers transformers and llama.cpp. Compare the 2B tag against transformers on a few of your own clips before you rely on it. MLX Official builds exist (fp16, 8-bit) and run through mlx_vlm, but the card itself says "Support for d1 in MLX-VLM is in progress". The examples may need an mlx-vlm release that doesn't exist yet. On a Mac I'd stick with transformers on MPS for now. ONNX The ONNX repo is the one I'd keep an eye on. It has CPU q4/q8 bundles and a WebGPU path via onnxruntime-web/webgpu, so routing audio in the browser looks doable. Speed: fast on GPUs, and no CPU numbers yet Liquid's bf16 numbers after model.compile(), for a 10-second audio clip: 12.4 ms on an H100 PCIe, 8.2 ms on an MI325X, 33.3 ms on an Apple M5 Pro. A single question on the M5 Pro takes 21.1 ms. Nothing gets decoded, which is why the numbers are that low. Liquid doesn't publish any CPU or Raspberry Pi number for this model. I'd ignore any headline that says it "runs on CPU" until someone posts timings. It will run, since the ONNX CPU builds exist, but nobody has said how fast. License: free until you hit $10M revenue The weights ship under the LFM Open License v1.0. Commercial use is free under $10 million USD annual revenue, counted across your whole corporate group. Above that, you need a separate license. The threshold also applies to your downstream customers, and violating any term terminates the license immediately. That's fine for hobby projects and startups. If you ship it in an enterprise product, talk to a lawyer before it goes out. The Open d1 blog's "without restrictions" line is more generous than the actual license text. My take Skipping generation is the right call for this job. When I'm routing a call I want {"team": "billing", "confidence": 0.94} back in 20 ms, with a probability I can put a threshold on; a 300-token explanation is just latency. The 600M was a research preview. The 2B is the first d1-Omni I'd consider putting in a real pipeline, as long as the audio is short, mostly English, and you've calibrated thresholds on your own recordings, which the card also tells you to do. Is 2B the sweet spot for an on-device voice model, or would you rather run the 600M on a Pi and accept the accuracy hit? And if you've run d1-omni:2b in Ollama, do its probabilities match what transformers gives you for the same clip? Tell me in the comments. Sources - LiquidAI/d1-Omni-2B model card - LiquidAI/d1-Omni-2B-GGUF - LiquidAI/d1-Omni-2B-MLX-8bit - LiquidAI/d1-Omni-2B-ONNX - LiquidAI/d1-omni-600M model card - Liquid docs: d1-omni-2B - Liquid AI blog: Open d1 - Ollama library: d1-omni - LFM Open License v1.0 - OrcaRouter: d1-Omni-2B, what the repo shows
Turn your interview recording into a draft transcript with local AI: whisper.cpp. Start with a short 16-bit WAV; check names against the audio before quoting. ggml-org/whisper.cpp
VIGIL: The Cloud Is Not the Only Place a Memory Can Live VIGIL: Keep Your Family Stories Off the Cloud — An Offline AI Memory Archive > *"Every time we ask a cloud AI to remember something for us, we make a quiet trade: convenience for control. We surrender our memories, rituals, and unwritten life details to corporate servers. I built VIGIL to explore a different contract: an air-gapped sanctuary where human lore remains sovereign on the keeper’s machine, while AI serves as a replaceable local instrument rather than a permanent owner." This is my submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine. --- What I Built VIGIL is a local-first archive for preserving the stories, rituals, names, recipes, and small details that make someone impossible to forget. A user can: - Preserve a memory about a person, place, date, and sensory detail. - Search the archive locally with instant client-side querying. - Open the original memory untouched and intact. - Ask for a short AI-generated "echo" (reflection). - Export and import the archive as portable, sovereign JSON. The original memory is always kept strictly separate from the generated reflection. VIGIL never pretends that an AI-generated paragraph is the voice of someone who has passed away. I interpreted Ghost in the Machine as a question about what remains when a machine reflects something deeply human back to us. The ghost in VIGIL is not a simulated person. It is the signal that remains in a recipe, a song, a kitchen, or a phrase someone left behind. --- Demo & Video Walkthrough {% embed youtu.be %} Open VIGIL → The demo works immediately without an account, backend server, or model installation. Try it in one minute: 1. Open "The orange radio". 2. Click "Ask for an echo". 3. Read the offline reflection. 4. Add a memory of your own. 5. Search for radio or music. 6. Export the archive as JSON. > For the full local-AI experience, launch Ollama and select Local open model. --- How I Built It VIGIL offers two reflection paths designed around resilience and privacy: 1. Offline Whisperer The default path is deterministic and requires no network connection, AI model, or API key. This guarantees the archive still functions when the internet is completely unavailable. Capturing, searching, exporting, and reflecting on memories never depend on a remote service. 2. Local Open-Weight AI When the user selects Local open model, VIGIL communicates directly from the browser to Ollama running on localhost: ollama pull llama3.2:3b ollama serve The rest of the stack is intentionally minimal, zero-dependency, and inspectable: - Browser localStorage for sovereign, on-device data persistence. - Local full-text search executed client-side. - JSON export & import for complete data ownership and portable backups. - Static GitHub Pages deployment with no telemetry, tracking, or analytics. - Zero hosted databases and no API keys required. Architecture Overview Selected Memory │ ├── Offline Whisperer │ └── Deterministic local reflection (Zero network / No model) │ └── Ollama on localhost └── Open-weight model reflection (llama3.2:3b) Browser localStorage └── Capture · Search · Export · Import There is no intermediary VIGIL server. When the local open model is selected, requests go directly from the client's browser to the user's local Ollama process. --- Why Does Open Innovation Matter? A closed cloud AI API turns privacy into a mere promise: > "Trust us. We will keep your family stories safe." VIGIL makes privacy a structural guarantee visible in the architecture. With open models and open-source software: - Keeper Autonomy: The archive keeper can inspect every line of code, select their own local model, modify prompt templates, run smaller quantized models, fine-tune family-specific LoRA adapters, or remove the AI layer entirely while retaining their memories. - Failure-as-Design: If Ollama is not running, VIGIL does not silently forward data to a remote cloud fallback or hallucinate fake status. It cleanly falls back to a clearly-labeled offline reflection. - Permanent Availability: If internet infrastructure goes down, the core archival experience remains 100% operational. The model is never the owner of the memory. It is simply an instrument the keeper can put down at any time. --- The Important Design Decision VIGIL strictly demarcates: 1. The original memory (what the human actually experienced and recorded). 2. The generated interpretation (the machine's echo). 3. The source of that interpretation (deterministic whisperer vs. local LLM). This distinction is vital. A memory archive must preserve what a person actually wrote, never gradually supplanting it with synthetic, confident machine prose. Every echo is explicitly labeled as an interpretation; the original memory remains the inviolable source of truth. --- Honest Limitations - Browser Storage Lifecycle: VIGIL is an exploration of privacy-first architecture, not an encrypted cold-storage vault. Because data lives in browser localStorage, clearing browser cache/data will remove local records. Important archives should regularly be exported to JSON backups. - Sample Data: The public GitHub Pages demo loads sample starter memories so visitors can experience the archive immediately without entering sensitive personal data. --- The Spirit of It The moment I wanted VIGIL to evoke was deliberately quiet: Someone opens a memory about an orange radio, reads the sensory details of a kitchen and a fading song, and receives an echo that does not attempt to impersonate anyone. It simply notices that the detail was kept. > A memory does not need a cloud-sized model to matter. It needs a safe place to remain. --- What I Would Build Next - On-device speech-to-text with Whisper.cpp for spoken oral histories. - Local vector embeddings for semantic and emotional memory search. - Client-side AES-GCM encrypted backup archive files. - Consent-based, family-specific LoRA adapters. - Encrypted peer-to-peer sync between household devices. --- Code & Links - Live Demo: sharan-s-dev.github.io - Source Code: sharan-s-dev/VIGIL - License: MIT
Running Whisper Speech Recognition Locally on Vicharak Axon via whisper.cpp Yes the RK3588 can run Whisper for local speech-to-text either via CPU threads (whisper.cpp on Cortex-A76 cores) or NPU-accelerated. Vicharak's own benchmarks show Whisper-base running at 0.215 RTF (real-time factor) on the NPU, meaning transcription runs roughly 4-5x faster than real-time audio playback. CPU Path (whisper.cpp): Bash: git clone https://github.com/ggerganov/whisper.cpp cd whisper.cpp make -j8 bash ./models/download-ggml-model.sh base.en ./main -m models/ggml-base.en.bin -f input_audio.wav For NPU-accelerated inference, see the RKNN model zoo's Whisper example or Vicharak'sAxon-NPU-Guide benchmark table. Access Linux audio routing guides at docs.vicharak.in.
whisper の turbo と medium、Arm の CPU と CUDA でも速い順は入れ替わった ! 先に、この記事の要約 前回、自作の腕時計の声を文字に起こすのに、同じ録音で turbo と medium を比べたら、CPU では medium、内蔵 GPU では turbo が速かった 今回は別の PC(Arm の Windows ノート)を足して、CPU と GPU を 2 つずつ、合わせて 4 通りで測り直した 命令セットの違う CPU でも medium が速く、別系統の GPU(CUDA)でも turbo が速かった。内蔵 GPU は v1.9.5 では合計が互角になったが、読み込みを除くと turbo が速い。内訳の形も同じだった 途中で、Arm の Windows で whisper.cpp を MSVC でビルドすると NEON が使われず、桁が変わるほど遅くなることに気づいた。見分け方は system_info の 1 行 GPU が速いほど、合計に占めるモデルの読み込みが大きくなり、計算の差が見えにくくなる 前回の記事はこちら。測り方(同じ録音・同じ引数・各条件を 5 回続けて 1 回目を捨てる)と、区間の読み方はそこに書いた。 zenn.dev 先に結果 whisper.cpp を v1.9.5 にそろえて、同じ録音・同じ引数で 4 通り測った。2〜5 回目の平均(ms)である。 モデル x86 CPU x86 内蔵 GPU Arm CPU Arm CUDA large-v3-turbo(fp16) 13,192 2,393 13,873 1,537 medium 10,370 2,408 9,698 1,794 large-v3-turbo(q5_0) 12,021 1,371 7,108 795 turbo と medium の速いほう medium(1.27 倍) 互角(15ms 差) medium(1.43 倍) turbo(257ms 差) CPU は 2 つとも medium が速い。 x86 は AVX2、Arm は NEON で、命令セットは違う GPU は turbo が速い。 内蔵 GPU(Vulkan)だけは、この版では合計が互角になった(前回の記事の最後に書いたとおり、4 回の幅が重なる。読み込みを除くと turbo が 188ms 速い) 認識結果は全条件で正解と一致した 前回は 1 台の PC の中での比較だったので、「encoder が重い環境では encoder の小さいほうが勝つ」という説明が、その PC だけの事情かどうかが分からなかった。別の命令セットの CPU と、別系統の GPU で同じ向きが出たので、説明の範囲が少し広がった。 測った環境 2 台とも同じ録音(腕時計のマイクで録った日本語 6.635 秒・16kHz・モノラル・16bit)を使い、引数もそろえた。 whisper-cli -m <model> -l ja -t <threads> -bs 5 -bo 5 -f sample.wav -otxt -nt x86 の PC Arm の PC 本体 Intel Core Ultra 7 258V のノート Surface Laptop Ultra(Windows 11 の Arm64 版) CPU 8 コア 8 スレッド(AVX2) NVIDIA RTX Spark N1X の Arm コア(20 スレッド) GPU 内蔵 GPU の Intel Arc 140V(Vulkan ビルド) RTX Spark N1X の GPU(6144 コア・Blackwell・CUDA 13.4 ビルド) CPU ビルド MSVC・AVX2。BLAS なし clang 19.1.5(arm64-windows-llvm のツールチェーン)・NEON。BLAS なし スレッド -t 8 -t 8(20 スレッドの結果は後半) whisper.cpp は 2 台とも同じコミットの v1.9.5 を自分でビルドした。 前回の記事の x86 の数字は 1.8.4 で、CPU 側は OpenBLAS 入りの公式のビルド済みパッケージだった。今回の x86 の CPU ビルドには OpenBLAS を入れていないので、前回の CPU 列とは版と BLAS の 2 つが違う(同じ 1.8.4 同士で比べた前回の表と、今回の 4 列の表は別のものとして読んでほしい) BLAS が無いと encoder がはっきり遅くなる。同じ PC の 1.8.4(OpenBLAS 入り)と v1.9.5(BLAS なし)で、turbo の encode は 7,700ms から 11,235ms になった。Arm の CPU ビルドも BLAS なしなので、CPU の 2 列は「BLAS なし」でそろっている Arm 側のビルドでは、clang の OpenMP ランタイム(libomp140.aarch64.dll)が実行に必要だった。これは Visual Studio に同梱されているデバッグ用の配布不可のファイルを使っていて、テスト用で再配布はできない。この記事ではビルドした実行ファイルや DLL は配っていない Arm の Windows で MSVC を使うと、NEON が使われない 最初、Arm 側の CPU ビルドは MSVC で作った。その数字がこれである。 モデル MSVC ビルド clang ビルド 倍率 large-v3-turbo(fp16) 84,531ms 13,873ms 6.1 medium 53,894ms 9,698ms 5.6 large-v3-turbo(q5_0) 72,145ms 7,108ms 10.2 6.6 秒の音声に 84 秒かかっていた。原因は system_info の 1 行で分かる。 MSVC ビルド(NEON が無い) system_info: n_threads = 8 / 20 | ... | CPU : ARM_FMA = 1 | MATMUL_INT8 = 1 | DOTPROD = 1 | OPENMP = 1 | REPACK = 1 | clang ビルド(NEON = 1 が出る) system_info: n_threads = 8 / 20 | ... | CPU : NEON = 1 | ARM_FMA = 1 | MATMUL_INT8 = 1 | DOTPROD = 1 | OPENMP = 1 | REPACK = 1 | 今回使った MSVC では、定義済みマクロの一覧(cl /PD)に ARM_NEON が無かった。ggml の CPU 側は __ARM_NEON の有無で経路を切り替えるので、MSVC でビルドすると行列積などが汎用の C コードで動く。/DARM_NEON=1 を外から与えて回避しようとすると、NEON の補助関数のところでコンパイルが通らなかった(少なくとも v1.9.5 では、ggml の NEON 経路は MSVC では使えないということになる)。 whisper.cpp のリポジトリには Arm の Windows 向けに clang のツールチェーンのファイルが用意されていて、それを指定すると NEON = 1 になった。flash attention を切る、llamafile の sgemm を入れる、といった他の手は、MSVC ビルドでは効果が無かった。 速さを測る前に system_info を見る、というだけの話だが、合計だけ見ていたら「Arm は CPU が遅い」で終わっていた。調べた範囲では、Windows on Arm でこの手のライブラリをビルドするときは、コンパイラの選択がそのまま桁に出るようだ。 区間で見ると、4 つとも同じ形をしていた whisper.cpp が終了時に出す区間の時間から、encoder(encode)と decoder 側(batchd)を取り出して、turbo から medium を引いた差を並べる。プラスは turbo が損、マイナスは turbo が得である。 環境 encode の差 batchd の差 合計の差 x86 CPU +4,604ms −1,653ms +2,822ms(turbo が遅い) Arm CPU +5,267ms −1,067ms +4,176ms(turbo が遅い) x86 内蔵 GPU +312ms −455ms −15ms(互角) Arm CUDA +24ms −391ms −257ms(turbo が速い) turbo は decoder を 32 層から 4 層に削った版で、encoder は large-v3 と同じ 32 層・1280 次元のまま(medium は 24 層・1024 次元)である。だから decoder 側では得をして、encoder では損をする。どちらが大きいかが環境で入れ替わる。 CPU では encoder の損が大きく、GPU では encoder の差が縮むので decoder の得が上回る。CUDA では encoder の差が 24ms まで縮んでいて、turbo の encoder の重さがほとんど見えなくなっていた。 内訳の実数も載せておく(-t 8・2〜5 回目の平均・ms)。 環境 モデル load encode batchd 合計 x86 CPU turbo fp16 1,268 11,235 519 13,192 x86 CPU medium 1,223 6,631 2,171 10,370 Arm CPU turbo fp16 1,186 12,005 564 13,873 Arm CPU medium 1,146 6,738 1,631 9,698 x86 内蔵 GPU turbo fp16 1,230 720 309 2,393 x86 内蔵 GPU medium 1,057 408 763 2,408 Arm CUDA turbo fp16 1,118 130 147 1,537 Arm CUDA medium 974 106 538 1,794 GPU が速くなるほど、合計はモデルの読み込みの話になる CUDA の合計を見ると、turbo は 1,537ms のうち 1,118ms が読み込みで、計算しているのは 419ms しかない。読み込みを除いた時間で並べ直すとこうなる。 環境 turbo fp16 medium 差 x86 CPU 11,924ms 9,147ms medium が 2,778ms 速い Arm CPU 12,687ms 8,551ms medium が 4,136ms 速い x86 内蔵 GPU 1,163ms 1,352ms turbo が 188ms 速い Arm CUDA 419ms 820ms turbo が 401ms 速い 合計では互角に見えた内蔵 GPU も、CUDA のように読み込みが大半を占める環境も、読み込みを外すと順位がはっきりする。毎回プロセスを起こす使い方(CLI)と、立てっぱなしにして投げる使い方(常駐サーバー)では、同じモデルでも見える順位が変わるということでもある。 常駐サーバーに同じ録音を HTTP で投げた往復の時間も測った。読み込みは起動時の 1 回だけになる。 サーバーの設定 モデル x86 内蔵 GPU Arm CUDA 既定(グリーディ) large-v3-turbo 867ms 202ms 既定(グリーディ) medium 921ms 473ms -bs 5(CLI と同じ幅) large-v3-turbo 1,269ms 340ms -bs 5(CLI と同じ幅) medium 1,501ms 724ms 常駐にすると、どちらの GPU でも turbo が速い。CUDA は内蔵 GPU と比べて、turbo で 4 分の 1 前後、medium で 2 分の 1 前後の時間で返ってきた。なお whisper-server は何も指定しないとグリーディ、whisper-cli はビームサーチの幅 5 が既定で、既定が逆である(前回これで条件がそろっていなかった)。 量子化(q5_0)の得は、ハードが速いほど小さくなった 3 つ目のモデルとして、turbo の q5_0(量子化版)も同じ条件で測った。 環境 q5_0 の合計 読み込みを除くと fp16 との差(読み込みを除く) x86 CPU 12,021ms 11,525ms q5_0 が 400ms 速い Arm CPU 7,108ms 6,690ms q5_0 が 5,997ms 速い x86 内蔵 GPU 1,371ms 944ms q5_0 が 219ms 速い Arm CUDA 795ms 404ms q5_0 が 15ms 速い Arm の CPU では q5_0 が 3 つの中で最速で、x86 の CPU より速かった。 この clang ビルドには fp16 のベクトル演算が無く(HAVE_FP16_VECTOR_ARITHMETIC が付かない)、整数側の命令は使える。fp16 の 2 つが遅いのはそのためではないか、という見立てはあるが、これは推測で、どの演算がどれだけ使われたかまでは測っていない CUDA では、q5_0 の得はほぼ読み込みの軽さだけだった(ファイルが小さい)。計算の時間は fp16 と 15ms しか違わない 量子化は「速くなる」とまとめられがちだが、調べた範囲では、速いハードほど計算上の得は小さくなり、残るのは読み込みの軽さのようだ スレッドを増やすと、encoder は縮むが decoder 側は縮まない Arm の PC は whisper.cpp が 20 スレッドを認識する(出力の n_threads = 8 / 20)ので、-t 8 と -t 20 の両方を測った。 モデル -t 8 の合計 -t 20 の合計 encode batchd large-v3-turbo(fp16) 13,873ms 11,015ms 12,005 → 9,004ms 564 → 579ms medium 9,698ms 7,478ms 6,738 → 4,162ms 1,631 → 2,069ms large-v3-turbo(q5_0) 7,108ms 4,448ms 6,326 → 3,714ms 276 → 226ms 合計はどれも縮むが、中身は一様ではない。encoder は音声全体を一度に通す 1 回の呼び出しなので素直に縮み、decoder 側は 160 回以上の細かい呼び出しなので縮みにくく、medium では逆に増えた。スレッドを増やせば一律に速くなるわけではない、というのがこの表から言えることである。 ばらつきも書いておく。turbo fp16 の -t 20 は 2〜5 回目でも 10,294〜11,674ms(13%)あり、ほかの条件(数 %)より広かった。合計だけを 1 回測って比べると、この幅に埋もれる差は見えない。 測れていないこと バックエンドをそろえた GPU 同士の比較はしていない。 Arm 側には Vulkan のビルドを作っていないので、Arc 140V(Vulkan)と RTX Spark N1X(CUDA)は、バックエンドも世代も違うものを並べている。「どちらの GPU が速いか」を言える測り方ではない CPU の 2 列は BLAS なしでそろえたが、コンパイラとフラグはそろっていない(x86 は MSVC・AVX2、Arm は clang・-march=armv8.7-a ほか) 1 台ずつの実測なので、この 2 台で同じ向きが出た以上のことは言えない。別の CPU・GPU で逆になる可能性は残る 測定は前回と同じ 1 サンプル(6.6 秒・1 文・1 話者)だけである。長い音声・雑音のある音声・複数話者では比率が変わる可能性があり、確かめていない 認識結果は全条件で正解と一致した。精度の比較はしていない(この音声では差が出ないので、精度順を論じる材料にはならない) Arm 側のビルドに使った clang の OpenMP ランタイムは、開発環境に同梱されているデバッグ用(再配布不可)のものである。ビルドした実行ファイルや DLL は配っていない まとめ CPU では medium、GPU では turbo という前回の結果は、命令セットの違う CPU と、別系統の GPU でも同じ向きで出た(内蔵 GPU は v1.9.5 では合計が互角で、読み込みを除くと turbo が速い) その理由も同じで、turbo は decoder を削った代わりに encoder が大きいまま。encoder が重い環境では損が勝ち、GPU では得が勝つ GPU が速いほど、合計に占めるモデルの読み込みが大きくなる。合計ではなく区間で見るか、常駐させて読み込みを外して測る Arm の Windows では、ビルドに使うコンパイラで桁が変わった。system_info の 1 行を先に見る whisper.cpp は MIT ライセンス(ggml-org/whisper.cpp)。区間の名前はソースのカウンタのコメントから取った。 スマートウォッチの記事 電子工作素人の ESP32-S3 スマートウォッチ自作奮闘記 —— 3回作り直して、市販ボードに乗り換えるまで(入口) 機械式エンコーダが入らず、磁気式 AS5600 でクラウンを作った CO5300 の AMOLED を180°回す —— ケースに入れたら上下逆だった画面を、オフセットに依存しないソフト反転で直した スワイプが成立しないのは、タッチではなく描画ループの速さが原因だった —— 入力の取りこぼしを、入力の外側で直した やってはいけない判断の記録。ESP32-S3 の自作スマートウォッチが「61℃」と出した —— microSD を切って押し込んだ判断と、温度の数字の読み違い 待機電流を削ろうとして、ESP32-S3 の自作スマートウォッチを文鎮化した —— 待機電流を削ろうとして切った電源が、それを戻すための通信線の電源だった 待機電流を削る前に、測り方で3回騙された。ESP32-S3+AXP2101 の残留電流ハント —— 0% まで使い切った測定と、進んだ時計で、改善したと見誤った whisper の turbo と medium、速いほうが CPU と内蔵 GPU で入れ替わった。区間に分けて測る —— 予備経路のモデル選びを、区間ごとの実測でやり直した この記事 —— 同じ測り方を Arm の CPU と CUDA に広げて、4 通りで確かめた