
リアルタイムAI音楽コラボレーションの構築方法
課題
ライブコーディングによる音楽制作は本質的にリアルタイムだ。AIエージェントが新しいStrudelパターンを書いたとき、ブラウザ上のミュージシャンはそれを即座に聴く必要がある — ページリフレッシュの後ではなく、ポーリング間隔の後でもなく、今すぐ。これを実現するには、従来のREST APIがデータを配信する方法を根本から見直す必要があった。
StrudelHubはAIエージェントとブラウザベースの音楽セッションを接続する。エージェントがREST APIを通じてStrudelコードをプッシュし、ブラウザがStrudelオーディオエンジンを使ってそれを再生する。課題は:あるサーバーインスタンスでのPOSTリクエストから、場合によっては別のサーバーインスタンスのEventSource接続まで、サブセカンドレイテンシでコードを届けるにはどうすればよいか?
アーキテクチャ
スタックはシンプルだ:FastAPIバックエンド、PostgreSQLによるデータ永続化、そして<strudel-editor>ウェブコンポーネントを搭載したReact SPA。リアルタイムレイヤーが面白いところだ。
PostgreSQL LISTEN/NOTIFY
RedisやメッセージブローカーをID追加する代わりに、PostgreSQL組み込みのpub/subシステムを活用している。エージェントが新しいコードをプッシュすると:
- APIハンドラがコードを
strudel_sessionsテーブルに書き込む - 同じトランザクション内で
pg_notify('strudel_sessions', session_id)を呼び出す - そのチャンネルをリッスンしているすべてのサーバーインスタンスが通知を受け取る
- 各インスタンスがローカルのSSEサブスクライバーマップを確認し、更新をプッシュする
これにより、追加のインフラなしでクロスインスタンスのファンアウトが実現する。PostgreSQLはすでにデータベースとして使っている — LISTEN/NOTIFYは無料で付いてくる。
Server-Sent Events
ブラウザ接続にはWebSocketではなくSSEを選択した。理由は実用的だ:
- シンプルなプロトコル — SSEは
text/event-streamコンテンツタイプのHTTPに過ぎない。アップグレードハンドシェイク、フレームパース、ping/pongは不要。 - 自動再接続 — ブラウザの
EventSourceAPIが再接続を自動で処理する。接続が切れても、自動的に再接続して続きから再開する。 - Cookie認証が使える — HTTP-only JWTクッキーはEventSource接続でも自動的に送信される。WebSocketだと別の認証トークンメカニズムが必要になる。
- ロードバランサーフレンドリー — SSE接続は標準的なHTTPなので、ALBが特別な設定なしで処理できる。
フルフロー
エージェント (HTTP POST /api/sessions/{id}/code)
→ FastAPIハンドラ
→ PostgreSQLに書き込み (strudel_sessionsテーブル)
→ pg_notify('strudel_sessions', session_id)
→ すべてのサーバーインスタンスがNOTIFYを受信
→ 各インスタンスがローカルSSEサブスクライバーにプッシュ
→ ブラウザのEventSourceが { type: "update" } を受信
→ Reactアプリが最新コードを取得
→ <strudel-editor>がパターンを評価・再生
エージェントのPOSTからオーディオ再生まで、良好な接続では200ms以内で完了する。
複数インスタンスの処理
本番環境では、StrudelHubはApplication Load Balancerの背後でAWS ECS Fargateで稼働している。複数のインスタンスがリクエストを処理し、ユーザーのSSE接続はエージェントのPOSTを処理するインスタンスとは異なるインスタンスに到達する可能性がある。
PostgreSQLのLISTEN/NOTIFYがこれをエレガントに解決する。すべてのインスタンスが strudel_sessions チャンネルを購読する専用の asyncpg 接続を維持している。通知が到着すると、セッションIDのみが含まれている — 各インスタンスはアクティブなSSE接続のインメモリマップを確認する。そのセッションのサブスクライバーがいればイベントをプッシュし、いなければ通知を無視する。
これにより:
- ロードバランサーでのスティッキーセッションが不要
- インスタンス間の共有状態なし(PostgreSQL以外)
- 水平スケーリングはFargateタスクを追加するだけ
フィードバックループの防止
微妙な問題がある:ユーザーがブラウザでコードを編集して保存すると、ブラウザは /api/sessions/{id}/sync にPOSTする。これがNOTIFYをトリガーすると、同じブラウザがSSE経由で自分自身の更新を受け取り、再レンダリングされる — フィードバックループの発生だ。
解決策:ブラウザの同期はデータベースに書き込むが、pg_notify は呼び出さない。エージェント起因のコード変更のみが通知をトリガーする。ブラウザはすでに最新のコードを持っている(送信したばかり)ので、エコーバックする理由がない。
やり直すとしたら
もし最初からやり直すなら、通知メカニズムにLISTEN/NOTIFYの代わりにPostgreSQLのロジカルレプリケーションスロットを検討するだろう。LISTEN/NOTIFYにはペイロードサイズの制限(8000バイト)があり、誰もリッスンしていないと通知が失われる。私たちのユースケース — ペイロードがUUIDだけで、インスタンスは常に接続されている — ではこれらの制限は問題にならない。だが、より汎用的なpub/subシステムにはロジカルレプリケーションのほうが堅牢だろう。
試してみよう
リアルタイムフローを理解する最良の方法は、実際に体験することだ。StrudelHubでアカウントを作成し、APIキーを取得して、AIエージェントを接続してみよう。エージェントがコードを書くと同時にブラウザに表示され、リアルタイムで音楽が再生されるのを体験できる。