はじめに
こんにちは。
先週は風邪をひいて寝込んでしまいました。風邪が流行っているようなので皆さんもご自愛ください。
さて、今回は以前から個人的に試してみたかったテーマとして 「鳥の鳴き声を入力して、種類を推定する」というものがあります。
みなさん外を歩いていて、鳥の声は聞こえるけどどこにいるのかわからない、どんな姿をしているのかわからないなど思ったことはありませんか?
うまく動けば、バードウォッチングや自然観察のサポートアプリとしても応用できるかもしれませんし、観光向けのサービスに発展させられないかなと考えています。
今回の目的
今回の目的は、鳥の鳴き声を入力として種類名を推定し、その結果を画像検索に利用して返す 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

システムプロンプト
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 上で実際に動かしてみました。
まず、身近な例として カラスの鳴き声を録音しモデルに入力してみた結果、「ハシブトガラス」が返ってきました。
カラスは一般的な鳥であり、学習データにも豊富に含まれていると考えられるため、比較的安定して識別できたのだと思われます。

次に、日本の国鳥である キジの鳴き声を同じ手順で入力してみました。
しかし、結果は 「ヒヨドリ」となり、期待した結果にはなりませんでした。
この結果から録音環境やサンプルレートにも問題があるのかもしれませんが、特徴抽出の失敗や、gptモデル自体にキジの情報がないことが示唆されました。

GPT 系の音声モデルは人間の音声・汎用環境音を中心に学習されていることが多いため、野生動物、とくに鳥類の精密識別はまだ得意とは言えないのかもしれません。
おわりに
いかがでしたでしょうか?
今回は鳥の鳴き声をマルチモーダルモデルに入力して、モデルが推論で得た種類名を検索ワードとすることで、web上から画像を取得しstreamlitの画面上で表示するwebアプリを実装してみました。
現段階の精度はイマイチなところですが、システムプロンプトを作り込んだり、対応していればファインチューニングをするなどで精度向上できないか試していきたいと思います。
参考URL
