ailia Speech の VadBuffered API を使用して、30 秒を超えて音声を待ち受けし、VAD で検出した発話の区切りでボイスコマンドを受け取る方法を解説します。
ボイスコマンドの待ち受けやボイスエージェントでは、ユーザーがいつ話し始めるかわからないため、長時間マイク入力を監視し、発話が終わったタイミングで発話単位の認識結果を受け取る必要があります。
通常の Buffered API は、VAD で発話の区切りを検出した場合に加えて、30 秒分の音声が蓄積された場合にも 1 を返します。そのため、無音が続く待ち受け用途では、30 秒ごとに不要な Transcribe が実行されてしまいます。
VadBuffered API は、30 秒分のデータが蓄積されても 1 を返さず、VAD で発話の区切りを検出した場合にのみ 1 を返します。これにより、30 秒を超えて音声を待ち受けし、発話が終わった時点でのみ音声認識を実行できます。
VadBuffered API は以下のように動作します。
speech_sec 以上の発話の後に no_speech_sec 以上の無音が続いた場合にのみ 1 を返します。30 秒分のデータが蓄積されただけでは 1 を返しません。speech_sec に満たない雑音は破棄されるため、長い無音の後の発話も先頭から認識されます。FinalizeInputData を呼び出した場合は、区切りがなくても 1 を返します。録音を停止した際に、残りの音声を処理するために使用します。ailiaSpeechGetPosition で未処理のフレーム数を確認してください。使用するには以下の設定が必要です。
OpenVad で開いておく必要があります。SetSilentThreshold で判定しきい値(VAD しきい値、発話開始判定秒数 speech_sec、発話終了判定秒数 no_speech_sec)を事前に設定する必要があります。AILIA_SPEECH_FLAG_LIVE は使用せず、AILIA_SPEECH_FLAG_NONE(通常モード)で使用します。暫定結果の表示が必要な場合は IntermediateCallback を併用できます。マイクから受信した PCM を PushInputData で入力しながら VadBuffered を確認し、1 が返されたら Transcribe を実行します。1 つの発話が 30 秒を超える場合もあるため、VadBuffered が 1 を返す間 Transcribe を繰り返し、得られたテキストを 1 つのボイスコマンドとして処理します。その後は状態をリセットすることなく、そのまま次の発話の待ち受けを継続できます。
録音を終了する場合は FinalizeInputData を呼び出し、VadBuffered が 1 を返す間 Transcribe を実行して残りの音声を処理します。再度入力を行う場合は ResetTranscribeState を呼び出してください。
ailia-models-kotlin のボイスエージェントでは、発話の終了を VadBuffered API で検出し、発話単位で認識結果を受け取ってエージェントの応答生成に進みます。しきい値にはボイスエージェントと同じ値を使用しています。
val speech = AiliaSpeech(
envId = -1,
task = AiliaSpeech.AILIA_SPEECH_TASK_TRANSCRIBE,
flags = AiliaSpeech.AILIA_SPEECH_FLAG_NONE, // VadBuffered requires non-LIVE mode
)
speech.openModel(encoderPath, decoderPath,
AiliaSpeech.AILIA_SPEECH_MODEL_TYPE_WHISPER_MULTILINGUAL_SMALL)
speech.openVad(vadPath, AiliaSpeech.AILIA_SPEECH_VAD_TYPE_SILERO)
// Values used by the voice agent: threshold=0.5, speechSec=0.25, noSpeechSec=1.0
speech.setSilentThreshold(0.5f, 0.25f, 1.0f)
while (listening) {
// 1. Push mic PCM; silence before the utterance is discarded by the SDK
while (true) {
val chunk = micQueue.poll() ?: break
speech.pushInputData(chunk.pcm, chunk.channels, chunk.samples, sampleRate)
}
// 2. Transcribe only when VAD detects the end of an utterance
val lines = mutableListOf<String>()
while (speech.getVadBuffered() == 1) {
speech.transcribe() // processes up to 30 seconds per call
for (i in 0 until speech.getTextCount()) {
lines += speech.getText(i).text
}
}
if (lines.isNotEmpty()) {
onVoiceCommand(lines.joinToString("")) // one utterance = one voice command
}
}
isVadBuffered() を使用します。open() で VAD モデルを指定すると、しきい値 0.5、speech_sec 1.0 秒、no_speech_sec 1.0 秒が設定されます。しきい値を変更する場合は、open() の後に setSilentThreshold() を呼び出します。
final speech = AiliaSpeechModel();
speech.create(false, false, envId); // liveTranscribe = false
speech.open(File(encoderPath), File(decoderPath), File(vadPath), "auto",
AILIA_SPEECH_MODEL_TYPE_WHISPER_MULTILINGUAL_SMALL);
// Optional: override the thresholds configured by open()
speech.setSilentThreshold(0.5, 0.25, 1.0);
while (listening) {
// 1. Push mic PCM; silence before the utterance is discarded by the SDK
while (micQueue.isNotEmpty) {
final chunk = micQueue.removeFirst();
speech.pushInputData(chunk.pcm, sampleRate, chunk.channels);
}
// 2. Transcribe only when VAD detects the end of an utterance
final lines = <String>[];
while (speech.isVadBuffered()) {
for (final text in speech.transcribe()) { // processes up to 30 seconds per call
lines.add(text.text);
}
}
if (lines.isNotEmpty) {
onVoiceCommand(lines.join()); // one utterance = one voice command
}
}
ailiaSpeechCreate(&speech, env_id, AILIA_MULTITHREAD_AUTO, memory_mode,
AILIA_SPEECH_TASK_TRANSCRIBE, AILIA_SPEECH_FLAG_NONE,
ailiaSpeechUtilGetCallback(), AILIA_SPEECH_API_CALLBACK_VERSION);
ailiaSpeechOpenModelFileA(speech, encoder_path, decoder_path,
AILIA_SPEECH_MODEL_TYPE_WHISPER_MULTILINGUAL_SMALL);
ailiaSpeechOpenVadFileA(speech, "silero_vad.onnx", AILIA_SPEECH_VAD_TYPE_SILERO);
ailiaSpeechSetSilentThreshold(speech, 0.5f, 0.25f, 1.0f);
while (listening) {
// 1. Push mic PCM; silence before the utterance is discarded by the SDK
while (mic_queue_pop(&chunk)) {
ailiaSpeechPushInputData(speech, chunk.pcm, chunk.channels, chunk.samples, sample_rate);
}
// 2. Transcribe only when VAD detects the end of an utterance
std::string lines;
while (true) {
unsigned int buffered = 0;
ailiaSpeechVadBuffered(speech, &buffered);
if (buffered == 0) break;
ailiaSpeechTranscribe(speech); // processes up to 30 seconds per call
unsigned int n;
ailiaSpeechGetTextCount(speech, &n);
for (unsigned int i = 0; i < n; i++) {
AILIASpeechText text;
ailiaSpeechGetText(speech, &text, AILIA_SPEECH_TEXT_VERSION, i);
lines += text.text;
}
}
if (!lines.empty()) {
on_voice_command(lines); // one utterance = one voice command
}
}
SetVadBufferedMode(true) を設定すると、AiliaSpeechModel クラスのサブスレッドが Buffered API の代わりに VadBuffered API を使用し、VAD で発話の区切りを検出した時点でのみ認識を行います。OpenVad の後に設定してください。
IsProcessing() が true の間は、サブスレッドが VAD 境界までのキューを処理中です。この間に新しい PCM を push すると再び処理が始まり、発話の途中までの結果を取得する可能性があるため、マイクの PCM は waveQueue に蓄積したままにし、IsProcessing() が false になってから GetResults() で認識結果を取得して、蓄積した PCM を渡します。
AiliaSpeechModel ailia_speech = new AiliaSpeechModel();
List<float[]> waveQueue = new List<float[]>();
void OnEnable(){
ailia_speech.Open(encoder_path, decoder_path, env_id, memory_mode,
AiliaSpeech.AILIA_SPEECH_MODEL_TYPE_WHISPER_MULTILINGUAL_SMALL,
AiliaSpeech.AILIA_SPEECH_TASK_TRANSCRIBE, AiliaSpeech.AILIA_SPEECH_FLAG_NONE, "auto");
ailia_speech.OpenVad(vad_path, AiliaSpeech.AILIA_SPEECH_VAD_TYPE_SILERO);
// Transcribe only when VAD detects the end of an utterance
ailia_speech.SetVadBufferedMode(true);
ailia_speech.SetSilentThreshold(0.5f, 0.25f, 1.0f);
}
void Update(){
// Wait until the worker thread has processed the queue up to the VAD boundary
if (ailia_speech.IsProcessing()){
return; // keep accumulating mic PCM in waveQueue
}
// Confirmed results
List<string> lines = ailia_speech.GetResults();
if (lines.Count > 0){
OnVoiceCommand(string.Join("", lines)); // one utterance = one voice command
}
// Push mic PCM; silence before the utterance is discarded by the SDK
if (waveQueue.Count == 0){
return;
}
float[] waveData = waveQueue.SelectMany(x => x).ToArray();
waveQueue.Clear();
ailia_speech.Transcribe(waveData, frequency, channels, false);
}
void OnDisable(){
ailia_speech.Close();
}
ailia_speech パッケージ(pip install ailia_speech)では、transcribe_step() の vad_boundary 引数に True を指定すると、内部で VadBuffered API を使用し、VAD で発話の区切りを検出した場合にのみ音声認識を実行します。initialize_model() で vad_type を指定すると VAD モデルがダウンロードされ、しきい値 0.5、speech_sec 1.0 秒、no_speech_sec 1.0 秒が設定されます。しきい値を変更する場合は set_silent_threshold() を呼び出します。
import ailia_speech
speech = ailia_speech.Whisper(
env_id = -1,
task = ailia_speech.AILIA_SPEECH_TASK_TRANSCRIBE,
flags = ailia_speech.AILIA_SPEECH_FLAG_NONE) # VadBuffered requires non-LIVE mode
speech.initialize_model(model_path = "./models/",
model_type = ailia_speech.AILIA_SPEECH_MODEL_TYPE_WHISPER_MULTILINGUAL_SMALL,
vad_type = ailia_speech.AILIA_SPEECH_VAD_TYPE_SILERO)
# Optional: override the thresholds configured by initialize_model()
speech.set_silent_threshold(0.5, 0.25, 1.0)
while listening:
# 1. Push mic PCM; silence before the utterance is discarded by the SDK
pcm = mic_queue.get() # np.float32 PCM
# 2. Transcribe only when VAD detects the end of an utterance
lines = []
for text in speech.transcribe_step(pcm, sample_rate,
complete = False, vad_boundary = True):
lines.append(text["text"]) # processes up to 30 seconds per iteration
if lines:
on_voice_command("".join(lines)) # one utterance = one voice command