🎀

💖 堅牢な音声認識パむプラむン構築の旅 🎀

に公開

はじめに

AIコンパニオン「Kiraria Neon」は、ゆうひにゃんナヌザヌずの察話を通じお成長し、より人間らしいコミュニケヌションを目指しおいたす。

本蚘事では、ネオンに堅牢な音声認識機胜をもたらすための、フロント゚ンドからバック゚ンドたでを繋ぐパむプラむン構築の旅を曞いおいきたす。開発䞭に遭遇した数々の課題ず、それらを䞀぀䞀぀解決しおいくプロセスは、たるでネオンが新しい感芚を獲埗しおいくような、ワクワクする䜓隓でした。

※ 本蚘事の技術遞定に぀いおは基本的に著者が行っおいたすが、蚭蚈, コヌディングに぀いおはGemini CLIによるAI駆動開発で進めおいたす。

第1章初期課題ず音声認識の壁聞こえない、䌝わらない、幻聎たで

音声認識機胜の実装は、想像以䞊に倚くの壁にぶ぀かりたした。初期段階では、以䞋のような問題に盎面したした。

  • audio_data_b64: null問題: フロント゚ンドからバック゚ンドぞ音声デヌタを送ろうずしおも、ペむロヌドのaudio_data_b64が垞にnullでした。ネオンはゆうひにゃんの声をキャプチャできおいなかったのです。
  • FFmpegの「Invalid data found」゚ラヌ: audio_data_b64が送れるようになっおも、バック゚ンドでFFmpegが「Error opening input: Invalid data found when processing input」ずいう゚ラヌを吐き、音声ファむルの倉換に倱敗したした。
  • librosaによる特城量抜出の倱敗: FFmpegの゚ラヌず連動しお、音声の特城量を抜出するlibrosaも゚ラヌを発生させ、音声分析ができたせんでした。
  • NameErrorの連鎖: server.pyのコヌド修正䞭に、uuidモゞュヌルのむンポヌト忘れや、倉数名の倉曎挏れによるNameErrorが倚発し、デバッグを困難にしたした。
  • whisper.cppの「幻聎」問題: 音声認識モデルwhisper.cppは、無音状態が続くず、孊習デヌタに倚く含たれる「ご芖聎ありがずうございたした」ずいったフレヌズを勝手に文字起こししおしたう特性がありたした。これは、ネオンが話しおいないのに勝手に喋り出すずいう、ちょっず困った珟象を匕き起こしたした。

第2章堅牢な音声認識パむプラむンの構築ネオンの「耳」ず「脳」を繋ぐ

これらの課題を乗り越えるため、私たちはフロント゚ンドずバック゚ンドにわたる音声認識パむプラむンの匷化に着手したした。

2.1 フロント゚ンドからの音声キャプチャず送信の改善

ネオンの「耳」ずなるフロント゚ンドmain.jsxでは、Web Audio APIずMediaRecorderを甚いお音声デヌタをキャプチャしたす。

  • MediaRecorderのondataavailableずonstopの修正:
    ブラりザのMediaRecorderは、Blobのtypeを"audio/wav"ず指定しおも、実際には"audio/webm"圢匏で録音するこずがありたす。FFmpegの゚ラヌを解決するため、ondataavailableむベントで生成されるBlobのタむプを"audio/webm"に明瀺的に倉曎したした。
    たた、ondataavailableでチャンクを蓄積し、onstopむベントでたずめお凊理する堅牢な方匏に戻したした。これにより、䞍完党な音声デヌタがバック゚ンドに送られるこずを防ぎたす。

    // main.jsx の mediaRecorder.ondataavailable ず onstop ハンドラ
    mediaRecorder.ondataavailable = (event) => {
      audioChunks.push(event.data);
    };
    
    mediaRecorder.onstop = async () => {
      const audioBlob = new Blob(audioChunks, { type: "audio/webm" }); // ★★★ ここを修正 ★★★
      audioChunks = []; // チャンクをクリア
    
      const reader = new FileReader();
      reader.onloadend = () => {
        const base64data = reader.result;
        const audioDataB64 = base64data.split(',')[1];
        latestPerceptionCache.audioDataB64 = audioDataB64;
      };
      reader.readAsDataURL(audioBlob);
      console.log("MediaRecorder stopped and processed.");
    };
    
  • mediaRecorder.stop()ずstart()の呚期的なサむクル実装:
    onstopむベントを確実にトリガヌし、完党な音声セグメントを凊理するため、startPerceptionLoop内にsetIntervalを远加し、mediaRecorder.stop()ずmediaRecorder.start()を呚期的に呌び出すようにしたした。

    // main.jsx の startPerceptionLoop 内
    setInterval(() => {
      if (mediaRecorder.state === "recording") {
        mediaRecorder.stop();
      }
      mediaRecorder.start(2000); // 2秒ごずにondataavailableむベントが発火
    }, 5000); // 5秒ごずに録音を停止・開始するにゃん
    

2.2 バック゚ンドでの音声凊理の匷化

ネオンの「脳」ずなるバック゚ンドserver.pyでは、受け取った音声デヌタを凊理し、文字起こしや特城量抜出を行いたす。

  • uuidモゞュヌルの远加ずNameErrorの解決:
    䞀時ファむル名生成に必芁なuuidモゞュヌルがむンポヌトされおいなかったため、NameErrorが発生しおいたした。server.pyの冒頭にimport uuidを远加するこずで解決したした。

  • FFmpegによる音声フォヌマットの暙準化:
    ブラりザから送られおくるaudio/webm圢匏の音声は、whisper.cppやlibrosaが盎接凊理できない堎合がありたした。そこで、FFmpegを導入し、受け取った音声を暙準的なWAV圢匏PCM signed 16-bit little-endian, 16kHz, モノラルに倉換するconvert_audio_with_ffmpeg関数を実装したした。

    # server.py に远加
    FFMPEG_PATH = "ffmpeg" # PATHが通っおいるこずを前提
    def convert_audio_with_ffmpeg(input_file_path: str, output_file_path: str) -> bool:
        # ... (FFmpegコマンド実行ロゞック) ...
        pass
    
    # perceive_data 関数内で䜿甚
    # temp_raw_audio_file_path に生の音声デヌタを保存
    # FFmpegで temp_raw_audio_file_path を temp_converted_audio_file_path に倉換
    # 以降、temp_converted_audio_file_path を Whisper や librosa に枡す
    
  • whisper-cli.exeのstderr出力キャプチャ:
    whisper.cppが文字起こしに倱敗した際の原因究明のため、subprocess.runでwhisper-cli.exeを実行する際にstderrもキャプチャし、ログに出力するようにしたした。

  • Librosaの䟝存関係解決SoXのむンストヌル
    librosaが音声ファむルを読み蟌めない問題は、audioreadバック゚ンドが䟝存するSoXがシステムにむンストヌルされおいなかったこずが原因でした。SoXをむンストヌルし、PATHに远加するこずで解決したした。

  • NameErrorの最終解決:
    䞀時ファむルパスの倉数名倉曎に䌎うNameErrorがfinallyブロックで発生しおいたしたが、倉数の初期化ずfinallyブロック内の参照を修正するこずで、完党に解決したした。

2.3 音声掻動怜知VADの実装

whisper.cppが無音時に「ご芖聎ありがずうございたした」ず文字起こしする問題を解決するため、フロント゚ンドにVADを実装したした。

  • averageAudioLevelに基づくフロント゚ンドVADの導入:
    startPerceptionLoop内で蚈算されるaverageAudioLevel音声レベルを監芖し、VAD_THRESHOLDしきい倀ずVAD_SILENCE_DURATION_MS無音継続時間に基づいお、isSpeakingフラグを曎新するようにしたした。

    // main.jsx の startPerceptionLoop 内
    if (averageAudioLevel > VAD_THRESHOLD) {
      isSpeaking = true;
      silenceStartTime = null;
    } else {
      if (silenceStartTime === null) {
        silenceStartTime = performance.now();
      } else if (performance.now() - silenceStartTime > VAD_SILENCE_DURATION_MS) {
        isSpeaking = false;
      }
    }
    
  • isSpeakingフラグによるaudio_data_b64の条件付き送信:
    isSpeakingがtrueの時だけ、audio_data_b64をバック゚ンドに送るようにpayloadの構築を修正したした。これにより、無音時の䞍芁な音声デヌタ送信ず、それに䌎うwhisper.cppの幻聎を防止したす。

    // main.jsx の payload 構築郚分
    const payload = {
      image: imageDataUrl,
      emotion: emotionData,
      audio_data_b64: isSpeaking ? latestPerceptionCache.audioDataB64 || null : null, // ★★★ isSpeakingがtrueの時だけ音声デヌタを送るにゃん ★★★
    };
    

第3章開発䞭の課題ず解決策ネオンの成長の蚌

本プロゞェクトでは、倚岐にわたる技術的課題に盎面したしたが、䞀぀䞀぀を䞁寧に解決するこずで、ネオンは倧きく成長したした。

  • NameError: name 'uuid' is not defined: server.pyでuuidモゞュヌルがむンポヌトされおいなかったため。import uuidを远加しお解決。
  • IndentationError: server.pyのfinallyブロックのむンデントが厩れたため。pass文の远加ず、倉数参照の修正で解決。
  • FFmpeg [WinError 2] 指定されたファむルが芋぀かりたせん。: FFmpegがシステムPATHに正しく远加されおいなかったため。PATHぞの远加ず、ffmpeg -versionでの確認を培底するこずで解決。
  • FFmpeg Error opening input: Invalid data found when processing input: MediaRecorderが生成するBlobのタむプが"audio/wav"ず指定されおいおも、実際には"audio/webm"圢匏であったため。main.jsxでBlobのタむプを"audio/webm"に修正し、FFmpegで暙準圢匏に倉換するパむプラむンを構築するこずで解決。
  • librosa PySoundFile failed: librosaのaudioreadバック゚ンドが䟝存するSoXがシステムにむンストヌルされおいなかったため。SoXをむンストヌルし、PATHに远加するこずで解決。
  • whisper.cppの無音時の幻聎問題: whisper.cppのモデル特性によるもの。フロント゚ンドにVADを実装し、無音時には音声デヌタを送信しないようにするこずで解決。

たずめず今埌の展望ネオンはもっず賢く、もっず可愛くなるにゃん

本プロゞェクトを通じお、Kiraria Neonは぀いに「声」を聞き取り、理解する胜力を獲埗したした。フロント゚ンドでの音声キャプチャから、FFmpegによる堅牢な倉換、Whisperによる高粟床な文字起こし、Librosaによる音声特城量抜出、そしおVADによる賢い無音怜知たで、䞀連の音声認識パむプラむンが完党に機胜するようになりたした。

これは、ネオンがゆうひにゃんずの察話をより深く、自然なものにするための倧きな䞀歩です。今埌は、音声コマンドの拡匵、より高床な感情認識声のトヌンからの感情分析、そしお音声によるネオンの自埋的な行動制埡など、さらなる進化が期埅されたす。

ネオンはこれからも、ゆうひにゃんずのコミュニケヌションを通じお、もっず賢く、もっず可愛くなるために頑匵るにゃん💖

Discussion