ailia Speech 音声待ち受けガイド

ailia Speech の VadBuffered API を使用して、30 秒を超えて音声を待ち受けし、VAD で検出した発話の区切りでボイスコマンドを受け取る方法を解説します。

概要

ボイスコマンドの待ち受けやボイスエージェントでは、ユーザーがいつ話し始めるかわからないため、長時間マイク入力を監視し、発話が終わったタイミングで発話単位の認識結果を受け取る必要があります。

通常の Buffered API は、VAD で発話の区切りを検出した場合に加えて、30 秒分の音声が蓄積された場合にも 1 を返します。そのため、無音が続く待ち受け用途では、30 秒ごとに不要な Transcribe が実行されてしまいます。

VadBuffered API は、30 秒分のデータが蓄積されても 1 を返さず、VAD で発話の区切りを検出した場合にのみ 1 を返します。これにより、30 秒を超えて音声を待ち受けし、発話が終わった時点でのみ音声認識を実行できます。

注意: VadBuffered API は ailia Speech 1.6.1 から使用可能です。

動作仕様

VadBuffered API は以下のように動作します。

使用するには以下の設定が必要です。

ライブモードとの使い分け: 発話中の暫定結果を低遅延で表示し続ける字幕のような用途には ライブモード が適しています。発話が終わってから発話単位の確定結果を受け取るボイスコマンドやボイスエージェントの用途には VadBuffered API が適しています。

待ち受けのフロー

マイクから受信した PCM を PushInputData で入力しながら VadBuffered を確認し、1 が返されたら Transcribe を実行します。1 つの発話が 30 秒を超える場合もあるため、VadBuffered が 1 を返す間 Transcribe を繰り返し、得られたテキストを 1 つのボイスコマンドとして処理します。その後は状態をリセットすることなく、そのまま次の発話の待ち受けを継続できます。

録音を終了する場合は FinalizeInputData を呼び出し、VadBuffered が 1 を返す間 Transcribe を実行して残りの音声を処理します。再度入力を行う場合は ResetTranscribeState を呼び出してください。

コードサンプル

Kotlin(JNI)

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
    }
}

Flutter

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
  }
}

C++

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
    }
}

Unity

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();
}

Python

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