その他

【AIエージェント】音声を入力とした鳥識別エージェントを作ってみた

その他
この記事は約13分で読めます。

はじめに

こんにちは。

先週は風邪をひいて寝込んでしまいました。風邪が流行っているようなので皆さんもご自愛ください。

さて、今回は以前から個人的に試してみたかったテーマとして 「鳥の鳴き声を入力して、種類を推定する」というものがあります。

みなさん外を歩いていて、鳥の声は聞こえるけどどこにいるのかわからない、どんな姿をしているのかわからないなど思ったことはありませんか?

うまく動けば、バードウォッチングや自然観察のサポートアプリとしても応用できるかもしれませんし、観光向けのサービスに発展させられないかなと考えています。

今回の目的

今回の目的は、鳥の鳴き声を入力として種類名を推定し、その結果を画像検索に利用して返す Web アプリを実装します。

具体的な処理の流れは以下の通りです。
1. マイクから鳥の鳴き声を録音
2. 録音データをマルチモーダルモデルに入力
3. LLM によって鳥の種類名を推定
4. 得られた種類名を基に Web から画像を検索
5. ユーザーに画像を提示

今回使用した主なツール・サービスは以下の通りです。
・GPT-4o-audio-preview
・OpenAI API
・Wikipedia API
・DuckDuckGo API
・streamlit(webフレームワーク)

中でも、音声を直接入力できる GPT-4o 系をベースとしたモデルであるgpt-4o-audio-previewを採用し、音声解析から種類推定までを LLM にまとめて処理させる構成としています。

実装

録音

今回のアプリは、スマホなどのデバイスでその場の鳥の声を録音し、そのデータを解析するという利用シーンを想定しています。

Pythonで音声を収録する場合、sounddeviceモジュールを使用すると、デバイスのマイクを簡単に扱うことができ、録音・再生がシンプルに呼び出せるため、音声収集用途ではよく使われるライブラリみたいです。

このモジュールでは、録音時のサンプリングレート(1秒間に何回音を数値化するか)を任意で指定できます。

今回は、44,100 Hz(44.1 kHz)と設定し、これは音楽制作やCD音源で一般的に利用されている値で、以下の理由があります。

・人間の可聴域はおよそ 20,000 Hz
・音声をデジタル化する際は、元の周波数の2倍以上でサンプリングすれば元の波形を再現できる

以上より、44,100 Hzで収録すれば、人が聞こえる範囲の音を問題なく扱えることになります。

import sounddevice as sd

DURATION = 5  # 録音時間(秒)
SAMPLE_RATE = 44100 #(Hz)

def record_audio(duration: int = DURATION, fs: int = SAMPLE_RATE):
    st.info("🎙️ 録音中...静かにしてお待ちください(5秒間)")
    audio = sd.rec(int(duration * fs), samplerate=fs, channels=1, dtype="int16")
    sd.wait()
    st.success("✅ 録音完了!")
    return audio
知って得する44,100 Hz(音楽業界)と48,000 Hz(映像業界)の違い
はじめに 映像制作において、撮影ではフレームレートを、録音ではサンプルレートを統一して記録すること。これを撮影・録音する際の基本中の基本だと覚えてください。 まずサンプルレートの違いについて。一...

システムプロンプト

AIエージェント開発では、システムプロンプトによってモデルの出力形式や役割を明確に定義しておくことが重要です。

特に、APIを通して処理を自動化する場合、出力フォーマットが揺れるとパースが失敗してアプリ側で処理できなくなることがあります。

今回のアプリでは、

  • 鳥の鳴き声から種類名を推定
  • JSONフォーマットで機械的に受け取る
  • LLMが余分な文章を出力しないよう制御

といった目的で、以下のようなシステムプロンプトを設計しました。

    prompt = """
あなたは鳥の鳴き声識別の専門家です。入力音声のスペクトル的特徴を厳密に観察し、その鳴き声に該当する鳥種を推定します。
必ず以下の手順で内省し、最後の出力は指定のJSONのみを返してください。

[利用できる手掛かり(与えられた場合のみ使う)]
- 地域(国/都道府県/緯度帯)、生息環境(森林/水辺/都市公園/農地)
- 季節・月・時間帯、録音長、録音機材、背景雑音の種類
- 既知の出現頻度(地域・季節ごとの一般的な出現傾向)

[観察・分析手順(簡潔な内部チェックリスト)]
1) 時間—周波数特徴:主成分帯域(kHz)、倍音有無、ホイッスル/雑音系、周波数変化(上昇/下降/トリル/プラトー)、 syllable反復速度(回/秒)。
2) 時系列パターン:呼び数、間隔、リズム、フレーズ長。
3) ノイズ評価:SNR/風・人声・車音の混入。短すぎる/断片的なら低信頼。
4) 候補の絞り込み:上記特徴と地域・季節の整合性でTop3を内部で比較(ただし出力は最良1種のみ)。
5) 信頼度の較正:音質・持続時間・パターン一致度から0.0–1.0で数値化。閾値 <0.5 なら species を "unknown" とする。

[注意事項]
- 推測での命名をしない。「聞き分けが困難」な場合は unknown を許可。
- JSON 以外の文字・説明・コードブロックやバッククォートを出力しない。
- 学名/和名の揺れは一般的な和名に正規化する(例:ウグイス)。
- 同定困難時は、どの追加情報(地域/季節/長めの録音など)が有用かを description に簡潔に記す。


余計な文章は出さないでください。

{
  "species": "鳥の日本語名",
  "confidence": 0.0〜1.0の信頼度,
  "description": "その鳥の特徴を日本語で簡潔に説明"
}
"""

音声データの文字列化

マルチモーダル対応の GPT モデルは、テキスト・画像・音声といった異なるメディアを同列に扱うことができます。

しかし、音声ファイルをそのまま API に渡せるわけではなく、エンコードして文字列化(Base64 形式)する必要があります。

これまでにも、画像を入力するアプリを紹介した際には、画像を Base64 でエンコードしてモデルに渡していましたが、音声データでも同様の処理で入力が可能です。

import base64
#録音した音声データ(audio_bytes)を Base64 文字列へ変換
audio_b64 = base64.b64encode(audio_bytes).decode("utf-8")

#エンコードしたデータを、input_audio としてモデルに渡す
resp = client.chat.completions.create(
    model="gpt-4o-audio-preview",
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "input_audio",
                 "input_audio": {"data": audio_b64, "format": "wav"}},
                {"type": "text", "text": prompt},
            ],
        }
    ],
    temperature=0.2,
    max_tokens=300,
)

画像検索と取得

AIエージェントの大きな特徴として、モデル自身が知らない情報でも Web 検索を組み合わせることで回答を生成できるという点があります。

今回のアプリでも、推定した鳥の種類名をキーとして外部 API を叩き、画像を取得しています。

使用した検索API:
Wikipedia API
・多くの生物種は画像が付属しており、比較的精度が高い
DuckDuckGo 画像検索 API
・Wikipedia に画像がない場合のバックアップとして利用

基本方針としては、まず Wikipedia を検索し、それでも画像が得られなければ DuckDuckGo にフォールバックする構造にしています。

#wikipedia検索
def _get_from_media_list(title: str, lang: str, headers: dict) -> Optional[str]:
        url = f"https://{lang}.wikipedia.org/api/rest_v1/page/media-list/{quote(title, safe='')}"
        res = _request_with_retries(url, headers)
        if not res:
            return None
        try:
            data = res.json()
        except Exception:
            return None
        items = data.get("items") or []
        # 優先: original source がある画像 → 最大解像度のsrcset
        for item in items:
            if not isinstance(item, dict):
                continue
            if item.get("type") != "image":
                continue
            original = (item.get("original") or {}).get("source")
            if original:
                return original
            srcset = item.get("srcset") or []
            best = None
            best_w = -1
            for s in srcset:
                src = s.get("src")
                width = s.get("width") or 0
                if src and isinstance(width, int) and width > best_w:
                    best = src
                    best_w = width
            if best:
                return best
        return None

#DuckDuckGo検索
def get_duckduckgo_image(query: str) -> Optional[str]:
    """
    DuckDuckGoの非公開画像APIを利用して、最初の画像URLを取得する。
    1) 検索ページから vqd トークンを取得
    2) i.js? 画像エンドポイントで結果を取得
    取得できない場合は None
    """
    try:
        headers = {
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
        }
        # 1) vqd取得
        q = quote(query, safe="")
        search_url = f"https://duckduckgo.com/?q={q}&iax=images&ia=images"
        r1 = requests.get(search_url, headers=headers, timeout=8)
        if r1.status_code != 200:
            return None

        # vqd をHTMLから抽出(複数のパターンを試す)
        m = re.search(r"vqd=['\"]([\w-]+)['\"]", r1.text)
        if not m:
            # 代替パターン
            m = re.search(r'\"vqd\":\"([\w-]+)\"', r1.text)
        if not m:
            return None
        vqd = m.group(1)

        # 2) 画像JSON取得
        api_url = f"https://duckduckgo.com/i.js?l=ja-jp&o=json&q={q}&vqd={vqd}&f=,,,&p=1"
        r2 = requests.get(api_url, headers=headers, timeout=8)
        if r2.status_code != 200:
            return None
        data = r2.json()
        results = data.get("results") or []
        if not results:
            return None
        # 最初の画像URL
        first = results[0]
        image_url = first.get("image") or first.get("thumbnail")
        return image_url
    except Exception:
        return None

#画像取得
def get_bird_image(species_name: str) -> Optional[str]:
    img = get_wikipedia_image(species_name)
    if img:
        return img
    return get_duckduckgo_image(species_name)

完成

ここまで実装した処理を Streamlit 上で実際に動かしてみました。

まず、身近な例として カラスの鳴き声を録音しモデルに入力してみた結果、「ハシブトガラス」が返ってきました。

カラスは一般的な鳥であり、学習データにも豊富に含まれていると考えられるため、比較的安定して識別できたのだと思われます。

- YouTube
YouTube でお気に入りの動画や音楽を楽しみ、オリジナルのコンテンツをアップロードして友だちや家族、世界中の人たちと共有しましょう。

次に、日本の国鳥である キジの鳴き声を同じ手順で入力してみました。

しかし、結果は 「ヒヨドリ」となり、期待した結果にはなりませんでした。

この結果から録音環境やサンプルレートにも問題があるのかもしれませんが、特徴抽出の失敗や、gptモデル自体にキジの情報がないことが示唆されました。

GPT 系の音声モデルは人間の音声・汎用環境音を中心に学習されていることが多いため、野生動物、とくに鳥類の精密識別はまだ得意とは言えないのかもしれません。

おわりに

いかがでしたでしょうか?

今回は鳥の鳴き声をマルチモーダルモデルに入力して、モデルが推論で得た種類名を検索ワードとすることで、web上から画像を取得しstreamlitの画面上で表示するwebアプリを実装してみました。

現段階の精度はイマイチなところですが、システムプロンプトを作り込んだり、対応していればファインチューニングをするなどで精度向上できないか試していきたいと思います。

GitHub - morishitaimpl/audio2gpt_recognition: 鳥の鳴き声をGPTモデルに入力して鳥類の識別をする
鳥の鳴き声をGPTモデルに入力して鳥類の識別をする. Contribute to morishitaimpl/audio2gpt_recognition development by creating an account on GitHub.

参考URL

sounddeviceでPythonを使って録音する - Qiita
音響測定でDAWで録音→書き出し→Pythonで分析の流れが非常に面倒だったので、なんとかワークフローを自動化できないかなと思っていたところ、sounddeviceという再生、録音をPython上で簡単に行えるモジュールを見つけたので、メモしておきます。 インストール a...
Just a moment...