先週Microsoft Foundry Local 1.1.0 がリリースされました。
Microsoft Foundry LocalはLLMなどをクラウドではなくローカルで動かして利用、提供するためのライブラリです。面倒なモデルの管理や起動、Webサービス化など簡単にできます。Microsoft Foundry Local CLIのほうを使えばモデルのロードやWebサービス化は簡単にできますが、SDKを使えばWebサービスにしなくても利用できます。Ollamaのように外部プロセスで別途Webサービスを立ち上げて~みたいな手間が不要で良いです。
今回ライブラリが更新されて 1.1 になったことで、リアルタイムのASR(自動音声認識)やResponses API、テキスト埋め込み、WebGPU実行プロバイダのプラグイン化(分離)などが実現できました。
ASRとか最近ちょっと触ってたので、どんな感じか実際に触ってみようと思います。ベースとなるサンプルは こちら にあるので参考にしながら見てください。Foundry Local 1.1 では色々検証した結果、 NVIDIAのNemotron Speech Streaming 0.6b を採用したようです。(残念ながら英語サポートのみのモデルだけです)
まずは適当に .NET コンソールアプリを作りましょう。作ったら Microsoft.AI.Foundry.Local と Microsoft.AI.Foundry.Local.Core 、それからオーディオデバイスを触るので NAudio のNuGetパッケージを追加します。
※ 今回使用するモデルがCPUで動作するみたいな感じなのでMicrosoft.AI.Foundry.Localでも問題ないですが、GPUサポートなどが必要な場合は Microsoft.AI.Foundry.Local.WinML を使うといいと思います。その場合、Microsoft.ML.OnnxRuntime.Gpu.Linuxも追加したりWindows環境ならフレームワークをWindows指定したりサポートOSバージョンを指定したりする必要があります。(net10.0-windows10.0.26100.0とか)
Microsoft Foundry LocalはManager経由でインスタンスを生成し、そちらに必要な実行プロバイダー(ExecutionProviders)やモデルのダウンロードや登録/ロードをして各機能を利用する、という感じです。
(実行プロバイダーはモデルによっては必要になるCUDAだったりWebGPUだったり上で実行するために必要なプロバイダーです。)
ではでは実際にコードにしていきましょう。
最初にFoundry Localの構成を作っておきます。
var config = new Configuration
{
AppName = "foundry_local_samples",
LogLevel = Microsoft.AI.Foundry.Local.LogLevel.Information,
//AppDataDir = "____",
//ModelCacheDir = "____",
//LogsDir = "____",
};
AppDataDirを省略するとホームディレクトリ(Windowsなら %userprofile% )配下に ドット始まりの AppName のフォルダが作られ、そのフォルダ以下に実行プロバイダーやモデルキャッシュ、ログなどが保存されます。上記のコードなら C:\Users\___\.foundry_local_samples のようになります。実行プロバイダーやモデルキャッシュが他のアプリとは独立して保存できるので良いのですが、CUDAとか割とファイルがでかい(2.3GB弱とか)ですしモデルもものによってはでかいので必要に応じてパスを変えるなり、他のアプリなどと共有するなりしましょう。(今回のNemotron Speech Streaming 0.6bは700MB弱ぐらい)
フォルダ構成例(今回の実行後)
C:\Users\__\.foundry_local_samples
├─cache
│ └─models
│ └─Microsoft
│ └─nemotron-speech-streaming-en-0.6b-generic-cpu-3
│ └─v3
├─ep
│ ├─cuda-ep
│ └─webgpu-ep
└─logs
次にLoggerも作ってFoundry Local Managerのインスタンスを取得します。
var logger = loggerFactory.CreateLogger<Program>();
await FoundryLocalManager.CreateAsync(config, logger);
var mgr = FoundryLocalManager.Instance;
インスタンスに対して DownloadAndRegisterEpsAsync で実行プロバイダーのダウンロードと登録を行います。初回は結構なサイズをダウンロードするので時間がかかります。
await mgr.DownloadAndRegisterEpsAsync();
利用するモデルについてもモデルカタログから指定してダウンロードします。モデルによりますが容量に応じたダウンロード時間がかかります。 LoadAsync呼べば利用できる状態になります。(モデルの構成などによりますがGPUやメモリが相応に消費されます。今回のnemotron-speech-streaming-en-0.6bはGPUは使わずCPUベースなのと、サイズも小さいのでメモリの消費もかなり少ないです。)
var catalog = await mgr.GetCatalogAsync();
var model = await catalog.GetModelAsync("nemotron-speech-streaming-en-0.6b");
await model.DownloadAsync();
await model.LoadAsync();
指定するモデル名は ListModelsAsync を呼べば取得できます。
これでFoundry Localとモデルの準備が整いました。今回は音声用のクライアントを生成して文字起こし用セッションを作成して開始します。セッションに対してサンプルレートや音声チャンネル数、認識言語などを指定します。(言語は現状”en”のみ)
var audioClient = await model.GetAudioClientAsync();
var session = audioClient.CreateLiveTranscriptionSession();
session.Settings.SampleRate = 16000;
session.Settings.Channels = 1;
session.Settings.Language = "en";
await session.StartAsync();
セッション開始後、GetStream() で音声認識された結果が流れてくるのでそれを処理する部分を作ります。
var readTask = Task.Run(async () =>
{
try
{
await foreach (var result in session.GetStream())
{
var text = result.Content?[0]?.Text;
if (result.IsFinal)
{
Console.WriteLine();
Console.WriteLine($" [FINAL] {text}");
Console.Out.Flush();
}
else if (!string.IsNullOrEmpty(text))
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write(text);
Console.ResetColor();
Console.Out.Flush();
}
}
}
catch (OperationCanceledException) { }
});
これでモデルが音声を認識して結果を受け取る部分はできました。ただまだ認識するべき音声をモデルに渡してません。なので次は音声デバイスから音声データを取得してモデルに渡す部分を実装します。ここではNAudioを使ってマイクまたはスピーカーのデバイスを表示させて選択できるようにしてます。※ スピーカーも選択できるようにしてるのは例えば英語の動画を見てるときにその音声を認識させるとかできるからです。
var devices = new MMDeviceEnumerator()
.EnumerateAudioEndPoints(DataFlow.All, DeviceState.Active);
foreach (var d in devices)
{
Console.WriteLine($"{d.DataFlow}\t{d.FriendlyName}\t{d.ID}");
}
Console.Write("Input use device id: ");
var useDevice = Console.ReadLine();
var device = devices.FirstOrDefault(x => x.ID == useDevice);
using var waveIn = device.DataFlow == DataFlow.Capture ? new WasapiCapture(device) : new WasapiLoopbackCapture(device);
waveIn.WaveFormat = new WaveFormat(rate: 16000, bits: 16, channels: 1);
サンプルレートはモデルのセッションと合わせる必要があります。もっといえばモデルでサポートされる値にする必要があります。次は音声データがデバイスから流れてきたときにセッションに投げる部分を実装します。
音声データをセッションのAppendAsyncに直接渡してもいい気はしますが非同期チャンネル使って良しなにしてる感じですね。DataAvailableで生データを得られるので都度放り込めばOKです。極端な話サンプルレートとチャンネルが合致してる生データをモデル(セッション)に放りこめれればNAudioである必要はないです。
var audioChannel = System.Threading.Channels.Channel.CreateBounded<byte[]>(
new System.Threading.Channels.BoundedChannelOptions(50)
{
FullMode = System.Threading.Channels.BoundedChannelFullMode.DropOldest
});
var appendTask = Task.Run(async () =>
{
await foreach (var chunk in audioChannel.Reader.ReadAllAsync())
{
await session.AppendAsync(chunk);
}
});
waveIn.DataAvailable += (sender, e) =>
{
if (e.BytesRecorded > 0)
{
var buffer = new byte[e.BytesRecorded];
Buffer.BlockCopy(e.Buffer, 0, buffer, 0, e.BytesRecorded);
audioChannel.Writer.TryWrite(buffer);
}
};
waveIn.StartRecording();
これで音声データがきたらモデルに流して、モデルが認識して文字起こしできたら結果が得られるまでできました。
終了するときは音声デバイスの録音を止めてセッションの停止、モデルのアンロードを行えばOKです。
waveIn.StopRecording();
audioChannel.Writer.Complete();
await appendTask;
await session.StopAsync();
await readTask;
await model.UnloadAsync();
実行例
CPUもGPUも全然使ってませんね。メモリは 0.6bのモデルをロードして1.1GBぐらい消費してる感じです。ASR(STT)の速度や精度は結構いいんじゃないでしょうか。まぁ英語だからというのもありますけど、、
とはいえこの内容が他のアプリやサービスに依存せず .NET やSDKがサポートしてる言語でアプリ単体で完結して動作するのがいいですね。
まとめ
利用できるモデルの種類とかはまだまだな点はあるけど、 .NET 単体で完結させてローカルで実行したいケースとかには良さそうなので、使いどころ見つけて活用できればなと思います。
まぁダラダラと書きましたがほんとは AppDataDir の件だけ忘れないようにメモするためだけのPostでした。