ロリポップ!AIゲートウェイが対応した判定特化モデル「Jev」を使って、音楽ファイルのジャンルやムードを判定する CLI を作りました。librosa で測った特徴を Jev に渡す 2 段構成で、精度を上げていく過程で見えてきた「Jev は渡したデータをどう読んでいるのか」を、実験結果と合わせて紹介します。
はじめに
GMOペパボの黒瀧です。ロリポップ!AIゲートウェイが TypeSafe AI の判定特化モデル Jev に対応しました(お知らせ)。
Jev は「生成」ではなく「判定」に特化したモデルで、選択肢から選ぶ・段階で評価する・Yes/No を判定する、といったことを 0.1〜0.5 秒程度で返してくれます。せっかくなので触ってみようと思い、音楽ファイルからジャンルやムードを判定する CLI を作ってみました。
作る過程で「Jev は渡したデータをどう読んでいるのか」が少し見えてきたので、その実験結果も合わせて紹介します。作ったものは GitHub で公開しています。
kurotaky/jev-music
Jev の呼び出し方
ロリポップ!AIゲートウェイの API キーがあれば、すぐに使えます。ほかのモデルで使う /v1/chat/completions ではなく、Jev 専用の /v1/systemone を使います。
リクエストには、判定の材料となる state と、聞きたいことを並べた questions を渡します。
AI_GATEWAY_BASE_URL=https://ai-gateway.lolipop.jp
curl -sS "$AI_GATEWAY_BASE_URL/v1/systemone" \
-H "Authorization: Bearer $AI_GATEWAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "typesafe/jev-latest",
"state": {
"inquiry": "先週注文したマグカップがまだ届きません。注文番号は12345です。急ぎで使いたいので早く対応してほしいです。"
},
"questions": {
"category": {
"type": "choice",
"instructions": "問い合わせの種類はどれか?",
"criteria": {
"shipping": "配送・到着に関する問い合わせ",
"refund": "返品・返金に関する問い合わせ",
"product": "商品仕様に関する質問"
}
},
"urgency": {
"type": "score",
"instructions": "問い合わせの緊急度は?",
"criteria": ["低い", "やや低い", "普通", "やや高い", "高い"]
},
"angry": {
"type": "noul",
"instructions": "問い合わせ者は怒っているか?"
}
}
}'
questions の type は 3 種類あります。
type用途criteria返ってくるものchoice選択肢から 1 つ選ぶ{キー: 説明文} のオブジェクトchoice・confidence・全選択肢の probabilitiesscore段階で評価する低い順に並べた配列確率で重み付けした score(連続値)・probabilitiesnoulYes/No を判定する不要0〜1 の noul
上のリクエストの結果は、category が shipping(確率 1.0)、urgency が 3.16(「やや高い」78%・「高い」19%)、angry が 0.49 でした。1 回のリクエストで複数の判定をまとめて聞けて、応答は 0.42 秒でした。
作ったもの: jev-music
Jev の state はテキスト(JSON)なので、音声ファイルをそのまま渡すことはできません。そこで、次の 2 段構成にしました。
mp3/wav ──librosa──▶ 特徴量(BPM, キー, 音圧, 音色…)──テキスト化──▶ Jev ──▶ ジャンル・ムード
「測定」 「判定」
測定(テンポ・キー・音圧・音色など)は librosa に任せる。信号処理で正確な数値が出るものを、わざわざ AI に推測させる必要はありません判定(ジャンル・ムード・踊れるか など)は Jev に任せる。正解が 1 つに決まらず、確率で答えるのが自然なものです
uv run jev-music song.mp3
🎵 guitar_obsession-80s-radio-tune-113798.mp3
[librosa] 測定
テンポ 117.5 BPM(ビート 239 個 / 拍の明確さ 0.576 / 2・4拍子系)
キー F major(確からしさ 0.138)
長さ 120.0 秒 / 音圧 -16.3 dBFS / 抑揚 20.2 dB
音色 重心 2711 Hz / 低音比 0.444 / 高音比 0.0428 / ノイズ感 0.0342
リズム 打楽器比 0.221 / 毎秒 2.71 音
[Jev] 判定
ジャンル rock(rock 80% / pop 19% / house_techno 1%)
ムード energetic(energetic 85% / happy 13% / calm 1%)
明るさ 2.62 / 4
激しさ 2.58 / 4
踊れる 65%
作業BGM 54%
電子音中心 58%
⏱ 解析 4.58s / Jev 0.49s
librosa で測っているもの
特徴中身テンポlibrosa.beat.beat_track拍の明確さオンセット強度の自己相関のピーク(0〜1)拍子自己相関の 3 拍目と 4 拍目の強さを比べて 3 拍子系 / 2・4 拍子系を推定キークロマ特徴と Krumhansl のキープロファイルの相関音圧・抑揚RMS の平均と、上位 5% と下位 5% の差音色スペクトル重心、150Hz 未満・4kHz 超のエネルギー比、スペクトル平坦度打楽器比HPSS(調波音と打楽器音の分離)でのエネルギー比
Jev に渡しているもの
数値だけだと多いのか少ないのかが分かりにくいので、目安の言葉を添えて state にしています。
"state": {
"audio_features": {
"tempo": "117.5 BPM(中くらい。ビート検出は倍・半分に誤ることがある)",
"pulse_clarity": "0.576(拍の感じやすさ。はっきりしている)",
"key": "F major(長調、推定の確からしさ 0.138)",
"loudness": "平均 -16.3 dBFS(音圧高い)",
"bass": "150Hz未満のエネルギー比 0.444(ベース・キックなど低音が中くらい)",
"treble": "4kHz超のエネルギー比 0.0428(シンバル・ハイハット・歪みなど高音成分が多い)",
"noisiness": "スペクトル平坦度 0.0342(ノイズ・歪み・シンセ成分が多い)",
"percussive_ratio": "打楽器成分の割合 0.221(打楽器成分中くらい)"
}
}
questions には、ジャンル(17 種の choice)、ムード(7 種の choice)、明るさ・激しさ(score)、踊れるか・作業 BGM 向きか・電子音中心か(noul)の 7 つを並べています。
精度を上げる: 2/7 → 6/7
librosa には、ジャンルがはっきりしたサンプル曲(ブラームス、ドラムンベース、ワルツ、ラグタイムなど)が付属しています。この 7 曲を正解データにして、改善していきました。
最初の版は 2/7 正解。7 曲中 5 曲が house_techno(ハウス/テクノ)と判定されていました。
原因 1: どの曲でも「ビートが非常に一定」になっていた
最初はビート間隔のばらつきを「規則性」として渡していたのですが、これが全曲 0.93〜0.96 になっていました。ビート検出器は等間隔にビートを置こうとするので、ビート間隔からテンポの揺れを測っても差が出ないのです。
その結果、全曲に「非常に一定(機械的)」という言葉が付き、4 つ打ちのハウス/テクノに引っ張られていました。
オンセット強度の自己相関から「拍の明確さ」を取るように変えると、テンポを揺らして演奏するブラームスは 0.17、機械的なワルツは 0.9 と、ちゃんと差が出るようになりました。
原因 2: ジャンルの説明と特徴量の言葉がかみ合っていなかった
低音比・高音比・平坦度を追加しても 3/7 どまり。効いたのは criteria(ジャンルの説明文)の書き直し でした。
- "classical": "クラシック。オーケストラ・ピアノ、打楽器が少なくダイナミクスが大きい",
+ "classical": "クラシック。オーケストラ・ピアノ・弦楽・室内楽。ドラムもベースラインも無いので低音と打楽器成分が少ない、澄んだ楽音、録音の音圧は控えめで抑揚が大きい、3拍子のワルツも多い",
- "jazz": "ジャズ。スウィング、即興、ビートの揺らぎ",
+ "jazz": "ジャズ。ウッドベースが鳴り続けるため低音が多い一方、ドラムはブラシやシンバル中心で打楽器成分は少ない。ピアノ・管楽器、スウィング",
state で使っている言葉(低音・打楽器成分・音圧・抑揚)と同じ言葉でジャンルを説明するようにしたところ、6/7 正解 になりました。
曲改善前改善後brahms(クラシック)classical ✅classical 84% ✅choice(ドラムンベース)house_techno ❌drum_and_bass 95% ✅nutcracker(クラシック)lofi ❌classical 82% ✅pistachio(ラグタイム)house_techno ❌classical 81% ✅sweetwaltz(ワルツ)house_techno ❌classical 99% ✅vibeace(ジャズ寄りラウンジ)lofi ✅jazz 60% ✅fishin(カントリーポップ)house_techno ❌pop 79% ❌
残った fishin は歌もののカントリーポップで、特徴量の上ではポップスそのものです。バンジョーの音色は今の特徴量では見分けられないので、この 1 曲に合わせて調整するのはやめました。
実験: Jev は数値を読んでいるのか、言葉を見ているのか
ここまでの経緯から、「Jev は state と criteria の言葉を照らし合わせているのでは?」という疑問が湧きました。jev-music は入力を完全に制御できるので、これを確かめる実験台にしてみました。
実験 1: 数値+言葉 / 数値だけ / 言葉だけ
同じ 7 曲で、state の渡し方だけを変えました。
state の渡し方例正解数正解ジャンルへの確率(平均)数値+言葉0.794(低音が非常に多い)6/774%数値だけ0.7946/757%言葉だけ低音が非常に多い6/772%
数値だけでも正解数は変わりません。 Jev は数値の大小もちゃんと判定に使えています。ただし確信度は下がり、それを押し上げているのは主に言葉のほうでした。
実験 2: 特徴を 1 つずつ動かす
基準曲(くるみ割り人形。低音比 0.087)の低音比だけを書き換えて、ジャンルの確率がどう動くかを見ました。
低音比数値だけ: classicaljazz数値+言葉: classicaljazz0.172%2%84%·0.263%4%16%3%0.349%6%12%4%0.431%10%12%5%0.520%13%12%5%0.611%32%2%54%
(数値+言葉の 0.2〜0.5 は folk_acoustic が 1 位)
数値だけ: classical が 72 → 63 → 49 → 31% と、なめらかに下がっていく数値+言葉: 言葉が「少ない → 中くらい」に切り替わる 0.2 で、84% → 16% に急落。0.2〜0.5 の間はほぼ横ばい
言葉があると、Jev は数値よりも言葉に従います。つまり、こちらが決めた言葉のしきい値が、そのまま判定の境界になります。
BPM でも面白い結果が出ました。
BPM数値だけ: classicaldrum_and_bass数値+言葉: classicaldrum_and_bass13551%12%83%3%15066%10%88%1%16543%49%82%7%18026%67%83%8%
数値だけのときは、drum_and_bass の説明文に書いた「BPM 160-180 前後」の範囲に入ったところで逆転しました。state の数値と criteria に書いた数値の範囲を照らし合わせている ようです。一方、言葉を添えると 135 以上はすべて「速い」になるため、180 でもほとんど反応しませんでした。言葉が数値の情報を隠してしまったわけです。
state 設計の指針
やりたいこと渡し方判定をはっきりさせたい(確信度を上げたい)言葉を添える。ただし境界は自分で決めることになる境界付近の値をきめ細かく扱いたい数値だけを渡し、criteria 側にも数値の目安を書く特定の範囲で判定を切り替えたいcriteria に数値の範囲を明記する(例: BPM 160-180)
7 曲・基準曲 1 曲・各条件 1 回の小さな実験なので傾向として見てほしいのですが、「state と criteria を同じ言葉で書く」「言葉のしきい値は判定の境界になる」は、Jev を使うときに意識しておくとよさそうです。実験の詳細は docs/jev-probe.md にまとめています。
実際の曲でも試してみる
サンプル以外の曲でも試してみました。ファイル名に「edm」「rock」などが入っていますが、Jev に渡しているのは librosa の特徴量だけで、ファイル名は渡していません。
EDM×ロック系のビート80s ラジオ風の曲ジャンルhiphop 25% / drum_and_bass 23% / rock 17% / edm 16% / house_techno 14%rock 81% / pop 18%genre の confidence0.190.79ムードdark 74%energetic 86%電子音中心75%58%
80s ラジオ風の曲は、「高音成分が多い」「ノイズ・歪み成分が多い」「音圧高い」という state が、rock の説明文とそのまま重なって rock 81% になりました。
EDM×ロックの曲は、1 位の hiphop でも 25% しかなく、電子系とロックに確率が割れました。電子音とロックが混ざった曲なので、この迷い方はむしろ正直です。こういう曲では 1 位だけでなく、確率の上位をまとめて「ジャンル候補」として見せるほうが、Jev の出力に合っていると感じました。
速度とコスト
応答時間: 7 つの質問をまとめて聞いても、毎回 0.4〜0.5 秒トークン: 1 回あたり入力約 2,270 トークン。その大半は 17 ジャンルの説明文で、state は 1 割程度料金: 入力 $0.042 / 100 万トークン、出力は無料。1 回あたり約 $0.0001(2026年9月18日〜24日は無料キャンペーン)
同じ入力で 2 回呼び出しても、確率の差は 1〜2% でした。判定が安定していて、速く、安いので、大量のデータを捌く処理や、LLM を呼ぶ前の振り分けに向いていそうです。
まとめ
Jev は音声を直接は受け取れないので、「測定」は librosa、「判定」は Jev と役割を分けるとうまくはまった精度に一番効いたのは、特徴量を増やすことより、criteria を state と同じ言葉で書くこと だったJev は数値も読んでいるが、言葉を添えると言葉に従う。言葉のしきい値がそのまま判定の境界になる
今後は、4 ビート・8 ビート・16 ビートといったリズムの刻みやテンポの揺れの判定、mp3 のタグ(曲名・アーティスト)をヒントとして渡す機能を試してみたいと思っています。
参考
kurotaky/jev-music(今回作った CLI と実験スクリプト)JevにLLMモデルの使い分けを任せる仕組みをロリポップ!AIゲートウェイで作ってみた(Jev の別の使い方の例)判定特化型AIモデル Jev が登場!(ロリポップ!AIゲートウェイのお知らせ)