🎼

[実践ADK] ADKとLyriaとChainlitで音楽生成エージェント - おまけ Lyria Realtimeによる長めの音楽生成

に公開

こんにちは、サントリーこと大橋です。

このシリーズでは、ADK (Agent Development Kit) とLyria、Chainlitを使って音楽生成エージェントを作成する方法を解説してきました。

https://zenn.dev/soundtricker/articles/fba90dc901ab46
https://zenn.dev/soundtricker/articles/e039f39e84fa80
https://zenn.dev/soundtricker/articles/25324dfd702883

前回までで、このシリーズとして実践的にADKをGoogle Cloud上でChatUIとともに扱うにはどうすればよいのかという部分については、書き終えています。

今回はおまけ回ということで、Lyria RealTimeについて触れ、音楽生成の別アプローチについて書きたいと思います。

Vertex AIのLyria2 APIの課題

一回目の記事で使用した Vertex AIのLyria2 では以下のような課題がありました。

https://zenn.dev/soundtricker/articles/fba90dc901ab46

  • 30秒以上の音楽が作成できない。
  • ちょこちょこ失敗する

今回はこの課題を解決していくために Lyria RealTime を利用したAI Agentを作成したいと思います。

Lyria RealTime とは?

Lyria RealTimeは Interactive real-time music creation modelと紹介された音楽生成モデルで、
具体的にはWebSocketを利用して、永続的にプロンプトとパラメータを与え続けることで音楽を変化させながら生成し続けることができるモデルです。

https://deepmind.google/models/lyria/realtime
https://ai.google.dev/gemini-api/docs/music-generation?hl=ja

Lyria RealTimeは以下のサンプルで試すことができます。

https://labs.google/fx/ja/tools/music-fx-dj
PromptDJ
PromptDJ MIDI

Lyria RealTimeでの音楽生成

Lyria RealTimeは、現状Vertex AIでは利用できずGenerative AI API経由で利用します。
またAPI自体も専用のAPIとなるため以下のように実行方法が異なります。

 import asyncio
  from google import genai
  from google.genai import types

  client = genai.Client(api_key=API_KEY, http_options={'api_version': 'v1alpha'})

  async def main():
      async def receive_audio(session):
        """Example background task to process incoming audio."""
        while True:
          async for message in session.receive():
            audio_data = message.server_content.audio_chunks[0].data
            # Process audio...
            await asyncio.sleep(10**-12)

      async with (
        client.aio.live.music.connect(model='models/lyria-realtime-exp') as session,
        asyncio.TaskGroup() as tg,
      ):
        # Set up task to receive server messages.
        tg.create_task(receive_audio(session))

        # Send initial prompts and config
        await session.set_weighted_prompts(
          prompts=[
            types.WeightedPrompt(text='minimal techno', weight=1.0),
          ]
        )
        await session.set_music_generation_config(
          config=types.LiveMusicGenerationConfig(bpm=90, temperature=1.0)
        )

        # Start streaming music
        await session.play()
  if __name__ == "__main__":
      asyncio.run(main())

※公式ドキュメントより引用

具体的には上記のように、「音楽データをストリーミングで受信するための非同期タスク」と「Lyria RealTimeにプロンプトを渡し続ける非同期タスク」の2つの処理が必要です。

データ受信非同期タスクでは、WebSocketを通じて送られてくる音楽データを受信し続ける処理を行います。
公式ドキュメントでは永続的に処理を行うためにwhile Trueを指定して、永続的にループを実行していますが、実際にはこのwhile Trueの部分は必須ではありません。
sessionが継続している限りは音楽は生成され続けます。

while True:
  async for message in session.receive():
    audio_data = message.server_content.audio_chunks[0].data
    # Process audio...
    await asyncio.sleep(10**-12)

プロンプトを送信する非同期タスクではLyria RealTime用のプロンプトを作成し、Lyria RealTimeへ送信しています。
Lyria RealTimeではどのような音楽を生成するかを指定するためにWeightedPromptのリストをパラメータとして渡します。

  await session.set_weighted_prompts(
    prompts=[
      {"text": "Piano", "weight": 2.0},
      types.WeightedPrompt(text="Meditation", weight=0.5),
      types.WeightedPrompt(text="Live Performance", weight=1.0),
    ]
  )

このWeightedPromptはどのプロンプトをどれぐらいの重さ(強さ?)にするかを定義したパラメータです。
Lyria RealTimeでは連続的にパラメータを渡しますが、急激なプロンプトの変化は不自然な音楽を生成することになります。このため、プロンプトを変化させる場合はweightを徐々に変化させることで、より自然な音楽の変化を生み出すことができます。
それ以外にもスケールやBPM、ドラムやベースの有無などを調整することが可能です。

具体的な実装例は以下にもありますので参照してみてください。

https://github.com/google-gemini/cookbook/blob/cb04a04359ac7937c4b22e8b4c381451ba1e5d93/quickstarts/Get_started_LyriaRealTime.py#L50

今回作成するAI Agent LongComposerAgentLongComposerFlowAgent

Lyria RealTime用パラメータ生成Agent LongComposerAgent

今回作成するAI Agent(LongComposerAgent)は以下のタスクをもちます。

  1. ユーザーによって渡された音楽イメージと大まかな時間から、Stanza(節、連)という単位に音楽を分割
  2. そのStanza毎にユーザの指定した音楽イメージに合っったプロンプト(群)を生成する
    • この時生成するプロンプトはLyria RealTimeに沿ったWeightedPromptのリストにする
    • Lyria RealTimeのプロンプトはLyriaとは異なるため注意が必要
  3. Lyria RealTimeに連続的に渡すためのパラメータをJSON Arrayで生成する。

なおLongComposerAgentは前回作成したComposerAgentのように直接toolを利用して音楽生成はさせず、別のAgentに任せたいと思います。

これはLyria RealTimeに渡すためのパラメータ群が少し複雑で、「こういうパラメータのtoolがあるから呼び出して」としてしまうとパラメータの生成や呼び出しに失敗することが多いためです。

この詳細については、ADKのドキュメントのToolのベストプラクティスを参照してください。
https://google.github.io/adk-docs/tools/function-tools/#best-practices

上記を実現するLongComposerAgent用のプロンプトが以下です。

https://github.com/soundTricker/composer-agent/blob/main/apps/agents/composer/sub_agents/long_composer/prompts.py#L3-L101

上記のタスクと、Lyria RealTimeのパラメータについての説明が記載されています。
Agentの作成は以下です。

https://github.com/soundTricker/composer-agent/blob/main/apps/agents/composer/sub_agents/long_composer/agent.py#L22-L39

MusicPlanというpydanticモデルを生成させmusic_planというsession keyに保存させる仕様になっています。

実際にJSONを作成すると以下のようになります。

プロンプト

1分ぐらいの朝に聞く爽やかなプログレッシブハウス 最初はドラムのみで始まって、途中から次第にベースが追加され、 中盤はピアノベースの爽やかなメロディライン 終わりはドラムとベースを消してピアノのみで

生成されるJSON

{
    "title": "Morning Progressive House",
    "stanzas": [
        {
            "prompts": [
                {
                    "text": "Upbeat Progressive House",
                    "weight": 2.0
                },
                {
                    "text": "Driving",
                    "weight": 1.0
                },
                {
                    "text": "Upbeat",
                    "weight": 1.0
                },
                {
                    "text": "Danceable",
                    "weight": 1.0
                },
                {
                    "text": "TR-909 Drum Machine",
                    "weight": 1.0
                }
            ],
            "seconds": 15,
            "config": {
                "bpm": 125,
                "mute_bass": True
            }
        },
        {
            "prompts": [
                {
                    "text": "Upbeat Progressive House",
                    "weight": 2.0
                },
                {
                    "text": "Driving",
                    "weight": 1.0
                },
                {
                    "text": "Upbeat",
                    "weight": 1.0
                },
                {
                    "text": "Danceable",
                    "weight": 1.0
                },
                {
                    "text": "TR-909 Drum Machine",
                    "weight": 1.0
                },
                {
                    "text": "Boomy Bass",
                    "weight": 1.0
                }
            ],
            "seconds": 15,
            "config": {
                "bpm": 125
            }
        },
        {
            "prompts": [
                {
                    "text": "Upbeat Progressive House",
                    "weight": 2.0
                },
                {
                    "text": "Driving",
                    "weight": 1.0
                },
                {
                    "text": "Upbeat",
                    "weight": 1.0
                },
                {
                    "text": "Danceable",
                    "weight": 1.0
                },
                {
                    "text": "Refreshing Piano Melody",
                    "weight": 1.5
                },
                {
                    "text": "Smooth Pianos",
                    "weight": 1.0
                }
            ],
            "seconds": 15,
            "config": {
                "bpm": 125
            }
        },
        {
            "prompts": [
                {
                    "text": "Upbeat Progressive House",
                    "weight": 2.0
                },
                {
                    "text": "Refreshing Piano Melody",
                    "weight": 1.5
                },
                {
                    "text": "Smooth Pianos",
                    "weight": 1.0
                }
            ],
            "seconds": 15,
            "config": {
                "bpm": 125,
                "mute_bass": True,
                "mute_drums": True
            }
        }
    ]
}

指示通り序盤はドラムのみで、だんだんベースを入れて、途中からピアノ、終盤はベースとドラムをミュートして終わるようになっています。

Lyria RealTimeで実際に音楽を生成する LongComposerFlowAgent

次に、以下のLongComposerFlowAgentを作成します。
LongComposerFlowAgentLlmAgent(Agent)ではなくBaseAgentを継承し、任意のフロー制御を行うAgentクラスです。

https://github.com/soundTricker/composer-agent/blob/main/apps/agents/composer/sub_agents/long_composer/agent.py#L41-L135

やっていることは

  1. LongComposerAgentを作成し、MusicPlan(音楽生成プラン 上記のStanzaの配列を持つ)を生成
  2. 生成された音楽生成プランを元に、genrate_musicで音楽データを作成 save_musicでArtifactへ保存(細かな処理はComposerAgentと同様)
    です。

音楽の生成処理は後で細かく説明します。

今回のLongComposerFlowAgentのようなBaseAgentを直接継承したAgentは各種LLMAgentを制御するワークフローAgentを作成する場合に有用です。
今回はLLMAgentを呼び出しと音楽生成をしているのみですが、ADKのWorkflow Agentで説明されているSequentialAgentParallelAgentLoopAgentを作成し、複雑なワークフローを生成することが可能です。

https://google.github.io/adk-docs/agents/workflow-agents/

より複雑なワークフロー制御を行いたい場合はADK公式の以下のドキュメントがわかりやすいので参照してみてください。動的なAgent生成等、非常に重要なワークフロー制御が可能になります。

https://google.github.io/adk-docs/agents/custom-agents/#part-1-simplified-custom-agent-initialization

Lyria RealTimeを利用した音楽の生成

Lyria RealTimeクライアントの初期化

LyriaRealTimeを使うためには、まずLyria Realtimeのクライアントを作成します。

https://github.com/soundTricker/composer-agent/blob/main/apps/agents/composer/sub_agents/long_composer/agent.py#L57

ここはGeminiのクライアントを作成する方法とほぼ同じです。
普通と異なる点は、他のAgentがVertex AI経由で呼び出しているため、環境変数にGOOGLE_GENAI_USE_VERTEXAIを指定しているため、vertexai=Falseというパラメータを渡している点と、現状ではLyria RealTimeはアルファバージョンのAPIであるため、{'api_version': 'v1alpha'}を渡す必要があります。

Lyria RealTimeとの非同期双方向通信セッションの作成

次にLyria RealTimeと非同期双方向通信するための、Sessionを作成します。

https://github.com/soundTricker/composer-agent/blob/main/apps/agents/composer/sub_agents/long_composer/agent.py#L90-L93

次はLyria RealTimeとの通信処理です。

上にも書きましたが、Lyria RealTimeでは「生成された音楽データを受信する非同期タスク」と「プロンプトを送信するタスク」が必要です。

Lyria RealTimeとの非同期双方向通信処理 受信側

今回の「生成された音楽データを受信する非同期タスク」ではリアルタイムで音楽を流す必要が無いため、bytearrayに受信したデータを貯めて行くことのみを行います。

https://github.com/soundTricker/composer-agent/blob/main/apps/agents/composer/sub_agents/long_composer/agent.py#L64-L87

なおリアルタイムでブラウザなどに配信する場合はWebSocketを利用したり、何かしらのストリームへの出力が必要です。
この辺りは以下を参考にしてください。

https://github.com/google-gemini/cookbook/blob/cb04a04359ac7937c4b22e8b4c381451ba1e5d93/quickstarts/Get_started_LyriaRealTime.py#L68-L86

Lyria RealTimeとの非同期双方向通信処理 送信側

「プロンプトを送信するタスク」では

  1. LLMにより生成されたMusicPlan内にあるMusicStanzaのリストを順々に送信
  2. 送信後、MusicStanzaの時間分待機

ということをしていきます。

https://github.com/soundTricker/composer-agent/blob/main/apps/agents/composer/sub_agents/long_composer/agent.py#L96-L116

BPMやスケールがわかった場合はコンテキストのリセットが必要らしくその処理も記載しています。

MP3化

全てのMusicStanzaを送信し、待機した後、セッション(音楽の生成)とタスクを止めます。
※現在のコードではこの処理がうまく行っていなくて、データ受信側でエラーになっていますが、握りつぶしています。

その後、音楽データのMP3化とArtifactへの保存処理を行っています。
音楽データは以下の形式で送信されており、それに合わせてパラメータを設定、mp3化します。

出力形式: 未加工の 16 ビット PCM 音声
サンプルレート: 48 kHz
チャンネル: 2(ステレオ)

https://github.com/soundTricker/composer-agent/blob/main/apps/agents/composer/sub_agents/long_composer/agent.py#L126-L136

それ以外のArtifactへの保存処理は以前書いたComposerAgentと同じにすることによって、ディレクターエージェント(オーケストレーターエージェント)側の処理を共通化しています。

ディレクターエージェントの書き換え

最後にLongComposerAgentをディレクターエージェントに登録して、振り分け処理を行うようにします。

https://github.com/soundTricker/composer-agent/blob/main/apps/agents/composer/prompts.py#L3-L25

ユーザーの求める長さによってComposerAgentLongComposerAgentかを振り分けます。

実際に作ってみる

では実際に使ってみましょう。面倒なのでChainlitのChatUIではなくadk webで動かします。

できた曲が以下です。

https://drive.google.com/file/d/1iRN5EQ6647ZGGQkRnaJqhaHffMzg8MF-/view?usp=drive_link

Lyriaに比べて長い曲が生成できました。

まとめ

今回はLyria RealTimeを利用して、ちょっと眺めの音楽をAI Agent経由で自動生成してみました。
Lyria RealTimeを使うことで、音楽の知識がそれほどなくても、インタラクティブに思ったような音楽が生成できました。

なお、Lyria RealTimeの音楽生成はリアルタイムなので生成する音楽と同じ時間だけ必要です。
ADKを利用する場合、このような長い処理を指せる場合は、 LongRunningFunctionToolを利用するほうが正しいかなとは思います。

https://google.github.io/adk-docs/tools/function-tools/#2-long-running-function-tool

Discussion