> ## Documentation Index
> Fetch the complete documentation index at: https://wb-21fd5541-feature-automate-reference-docs-generation.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Audio with weave

<Note>
  これはインタラクティブなノートブックです。ローカルで実行するか、以下のリンクを使用できます：

  * [Google Colabで開く](https://colab.research.google.com/github/wandb/weave/blob/master/docs/notebooks/audio_with_weave.ipynb)
  * [GitHubでソースを表示](https://github.com/wandb/weave/blob/master/docs/notebooks/audio_with_weave.ipynb)
</Note>

##

# 音声データでWeaveを使用する方法：OpenAIの例

このデモでは、OpenAIのチャット完了APIとGPT 4o Audio Previewを使用して、テキストプロンプトに対する音声応答を生成し、Weaveでこれらを追跡します。

<img src="https://i.imgur.com/OUfsZ2x.png" />

高度なユースケースでは、OpenAI Realtime APIを活用してリアルタイムで音声をストリーミングします。以下のサムネイルをクリックしてビデオデモを表示するか、[ここ](https://www.youtube.com/watch?v=lnnd73xDElw)をクリックしてください。

[![Everything Is AWESOME](https://img.youtube.com/vi/lnnd73xDElw/0.jpg)](https://www.youtube.com/watch?v=lnnd73xDElw "Everything Is AWESOME")

## セットアップ

OpenAI（`openai`）とWeave（`weave`）の依存関係、およびAPIキー管理の依存関係`set-env`をインストールすることから始めます。

```python
%%capture
!pip install openai
!pip install weave
!pip install set-env-colab-kaggle-dotenv -q # for env var
python
%%capture
# Temporary workaround to fix bug in openai:
# TypeError: Client.__init__() got an unexpected keyword argument 'proxies'
# See https://community.openai.com/t/error-with-openai-1-56-0-client-init-got-an-unexpected-keyword-argument-proxies/1040332/15
!pip install "httpx<0.28"
```

次に、OpenAIとWeaveに必要なAPIキーを読み込みます。ここでは、Google Colabのシークレットキーマネージャーと互換性のあるset\_envを使用します。これはColabの特定の`google.colab.userdata`の代替です。参照：[ここ](https://pypi.org/project/set-env-colab-kaggle-dotenv/)で使用方法の説明を確認できます。

```python
# Set environment variables.
from set_env import set_env

_ = set_env("OPENAI_API_KEY")
_ = set_env("WANDB_API_KEY")
```

最後に必要なライブラリをインポートします。

```python
import base64
import os
import time
import wave

import numpy as np
from IPython.display import display
from openai import OpenAI

import weave
```

## 音声ストリーミングと保存の例

ここでは、音声モダリティを有効にしたOpenAIの完了エンドポイントへの呼び出しをセットアップします。まず、OpenAIクライアントを作成し、Weaveプロジェクトを開始します。

```python
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
weave.init("openai-audio-chat")
```

次に、OpenAIの完了リクエストを定義し、Weaveデコレータ（op）を追加します。

ここでは、関数`prompt_endpont_and_log_trace`を定義します。この関数には主に3つのステップがあります：

1. テキストと音声の入出力をサポートする`GPT 4o Audio Preview`モデルを使用して完了オブジェクトを作成します。

   * モデルにさまざまなアクセントでゆっくりと13まで数えるよう指示します。
   * 完了を「ストリーム」に設定します。

2. ストリーミングデータがチャンクごとに書き込まれる新しい出力ファイルを開きます。

3. Weaveがトレースに音声データを記録できるように、音声ファイルへのオープンファイルハンドラを返します。

```python
SAMPLE_RATE = 22050

@weave.op()
def prompt_endpoint_and_log_trace(system_prompt=None, user_prompt=None):
    if not system_prompt:
        system_prompt = "You're the fastest counter in the world"
    if not user_prompt:
        user_prompt = "Count to 13 super super slow, enunciate each number with a dramatic flair, changing up accents as you go along. British, French, German, Spanish, etc."
    # Request from the OpenAI API with audio modality
    completion = client.chat.completions.create(
        model="gpt-4o-audio-preview",
        modalities=["text", "audio"],
        audio={"voice": "fable", "format": "pcm16"},
        stream=True,
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt},
        ],
    )

    # Open a wave file for writing
    with wave.open("./output.wav", "wb") as wav_file:
        wav_file.setnchannels(1)  # Mono
        wav_file.setsampwidth(2)  # 16-bit
        wav_file.setframerate(SAMPLE_RATE)  # Sample rate (adjust if needed)

        # Write chunks as they are streamed in from the API
        for chunk in completion:
            if (
                hasattr(chunk, "choices")
                and chunk.choices is not None
                and len(chunk.choices) > 0
                and hasattr(chunk.choices[0].delta, "audio")
                and chunk.choices[0].delta.audio.get("data") is not None
            ):
                # Decode the base64 audio data
                audio_data = base64.b64decode(chunk.choices[0].delta.audio.get("data"))

                # Write the current chunk to the wave file
                wav_file.writeframes(audio_data)

    # Return the file to Weave op
    return wave.open("output.wav", "rb")
```

## テスト

以下のセルを実行します。システムとユーザープロンプトは出力音声とともにWeaveトレースに保存されます。
セルを実行した後、「🍩」絵文字の横にあるリンクをクリックしてトレースを表示します。

```python
from IPython.display import Audio

# Call the function to write the audio stream
prompt_endpoint_and_log_trace(
    system_prompt="You're the fastest counter in the world",
    user_prompt="Count to 13 super super slow, enunciate each number with a dramatic flair, changing up accents as you go along. British, French, German, Spanish, etc.",
)

# Display the updated audio stream
display(Audio("output.wav", rate=SAMPLE_RATE, autoplay=True))
```

# 高度な使用法：WeaveによるリアルタイムオーディオAPI

<img src="https://i.imgur.com/ZiW3IVu.png" />

<details>
  <summary>（高度）WeaveによるリアルタイムオーディオAPI</summary>
  OpenAIのリアルタイムAPIは、リアルタイムの音声およびテキストアシスタントを構築するための高機能で信頼性の高い会話APIです。

  注意事項：

  * 以下のセルを確認してください：[マイク設定](#microphone-configuration)
  * Google Colab実行環境の制限により、**これはホストマシン上で実行する必要があります**（Jupyter Notebookとして）。ブラウザでは実行できません。
    * MacOSでは、`portaudio`をBrew経由でインストールする必要があります（[ここ](https://formulae.brew.sh/formula/portaudio)を参照）。これはPyaudioが機能するために必要です。
  * OpenAIのPython SDKはまだリアルタイムAPIをサポートしていません。より読みやすくするために、OAIリアルタイムAPIスキーマ全体をPydanticで実装しています。公式サポートがリリースされたら廃止される可能性があります。
  * この`enable_audio_playback`トグルを有効にすると、アシスタントが出力した音声が再生されます。このオプションを有効にする場合は、**ヘッドフォンが必要です**。エコー検出には非常に複雑な実装が必要なためです。

  ## 要件のセットアップ

  ```python
  %%capture
  !pip install numpy==2.0
  !pip install weave
  !pip install pyaudio # On mac, you may need to install portaudio first with `brew install portaudio`
  !pip install websocket-client
  !pip install set-env-colab-kaggle-dotenv -q # for env var
  !pip install resampy
  python
  import io
  import json
  import os
  import threading
  from typing import Optional

  import pyaudio
  import resampy
  import websocket
  from set_env import set_env

  import weave
  python
  # Set environment variables.
  # See: https://pypi.org/project/set-env-colab-kaggle-dotenv/ for usage instructions.
  _ = set_env("OPENAI_API_KEY")
  _ = set_env("WANDB_API_KEY")
  ```

  ## マイク設定

  以下のセルを実行して、利用可能なすべてのオーディオデバイスを確認します。次に、リストされたデバイスに基づいて`INPUT_DEVICE_INDEX`と`OUTPUT_DEVICE_INDEX`を設定します。入力デバイスには少なくとも1つの入力チャンネルがあり、出力デバイスには少なくとも1つの出力チャンネルがあります。

  ```python
  # Get device list from pyaudio so we can configure the next cell
  p = pyaudio.PyAudio()
  devices_data = {i: p.get_device_info_by_index(i) for i in range(p.get_device_count())}
  for i, device in devices_data.items():
      print(
          f"Found device @{i}: {device['name']} with sample rate: {device['defaultSampleRate']} and input channels: {device['maxInputChannels']} and output channels: {device['maxOutputChannels']}"
      )
  python
  INPUT_DEVICE_INDEX = 3  # @param                                                 # Choose based on device list above. Make sure device has > 0 input channels.
  OUTPUT_DEVICE_INDEX = 12  # @param                                                # Chose based on device list above. Make sure device has > 0 output channels.
  enable_audio_playback = True  # @param {type:"boolean"}                           # Toggle on assistant audio playback. Requires headphones.

  # Audio recording and streaming parameters
  INPUT_DEVICE_CHANNELS = devices_data[INPUT_DEVICE_INDEX][
      "maxInputChannels"
  ]  # From device list above
  SAMPLE_RATE = int(
      devices_data[INPUT_DEVICE_INDEX]["defaultSampleRate"]
  )  # From device list above
  CHUNK = int(SAMPLE_RATE / 10)  # Samples per frame
  SAMPLE_WIDTH = p.get_sample_size(pyaudio.paInt16)  # Samples per frame for the format
  CHUNK_DURATION = 0.3  # Seconds of audio per chunk sent to OAI API
  OAI_SAMPLE_RATE = (
      24000  # OAI Sample Rate is 24kHz, we need this to play or save assistant audio
  )
  OUTPUT_DEVICE_CHANNELS = 1  # Set to 1 for mono output
  ```

  ## OpenAI リアルタイムAPI スキーマの実装

  OpenAI Python SDKはまだリアルタイムAPIをサポートしていません。より読みやすくするために、OAIリアルタイムAPIスキーマ全体をPydanticで実装しています。公式サポートがリリースされたら廃止される可能性があります。

  <details>
    <summary>OpenAI リアルタイムAPI用のPydanticスキーマ（OpenAIのSDKにはリアルタイムAPIサポートがありません）</summary>

    ```python
    from enum import Enum
    from typing import Any, Literal, Union

    from pydantic import BaseModel, Field, ValidationError

    class BaseEvent(BaseModel):
        type: Union["ClientEventTypes", "ServerEventTypes"]
        event_id: Optional[str] = None  # Add event_id as an optional field for all events

        # def model_dump_json(self, *args, **kwargs):
        #     # Only include non-None fields
        #     return super().model_dump_json(*args, exclude_none=True, **kwargs)

    class ChatMessage(BaseModel):
        role: Literal["user", "assistant"]
        content: str
        timestamp: float

    """ CLIENT EVENTS """

    class ClientEventTypes(str, Enum):
        SESSION_UPDATE = "session.update"
        CONVERSATION_ITEM_CREATE = "conversation.item.create"
        CONVERSATION_ITEM_TRUNCATE = "conversation.item.truncate"
        CONVERSATION_ITEM_DELETE = "conversation.item.delete"
        RESPONSE_CREATE = "response.create"
        RESPONSE_CANCEL = "response.cancel"
        INPUT_AUDIO_BUFFER_APPEND = "input_audio_buffer.append"
        INPUT_AUDIO_BUFFER_COMMIT = "input_audio_buffer.commit"
        INPUT_AUDIO_BUFFER_CLEAR = "input_audio_buffer.clear"
        ERROR = "error"

    #### Session Update
    class TurnDetection(BaseModel):
        type: Literal["server_vad"]
        threshold: float = Field(..., ge=0.0, le=1.0)
        prefix_padding_ms: int
        silence_duration_ms: int

    class InputAudioTranscription(BaseModel):
        model: Optional[str] = None

    class ToolParameterProperty(BaseModel):
        type: str

    class ToolParameter(BaseModel):
        type: str
        properties: dict[str, ToolParameterProperty]
        required: list[str]

    class Tool(BaseModel):
        type: Literal["function", "code_interpreter", "file_search"]
        name: Optional[str] = None
        description: Optional[str] = None
        parameters: Optional[ToolParameter] = None

    class Session(BaseModel):
        modalities: Optional[list[str]] = None
        instructions: Optional[str] = None
        voice: Optional[str] = None
        input_audio_format: Optional[str] = None
        output_audio_format: Optional[str] = None
        input_audio_transcription: Optional[InputAudioTranscription] = None
        turn_detection: Optional[TurnDetection] = None
        tools: Optional[list[Tool]] = None
        tool_choice: Optional[str] = None
        temperature: Optional[float] = None
        max_output_tokens: Optional[int] = None

    class SessionUpdate(BaseEvent):
        type: Literal[ClientEventTypes.SESSION_UPDATE] = ClientEventTypes.SESSION_UPDATE
        session: Session

    #### Audio Buffers
    class InputAudioBufferAppend(BaseEvent):
        type: Literal[ClientEventTypes.INPUT_AUDIO_BUFFER_APPEND] = (
            ClientEventTypes.INPUT_AUDIO_BUFFER_APPEND
        )
        audio: str

    class InputAudioBufferCommit(BaseEvent):
        type: Literal[ClientEventTypes.INPUT_AUDIO_BUFFER_COMMIT] = (
            ClientEventTypes.INPUT_AUDIO_BUFFER_COMMIT
        )

    class InputAudioBufferClear(BaseEvent):
        type: Literal[ClientEventTypes.INPUT_AUDIO_BUFFER_CLEAR] = (
            ClientEventTypes.INPUT_AUDIO_BUFFER_CLEAR
        )

    #### Messages
    class MessageContent(BaseModel):
        type: Literal["input_audio"]
        audio: str

    class ConversationItemContent(BaseModel):
        type: Literal["input_text", "input_audio", "text", "audio"]
        text: Optional[str] = None
        audio: Optional[str] = None
        transcript: Optional[str] = None

    class FunctionCallContent(BaseModel):
        call_id: str
        name: str
        arguments: str

    class FunctionCallOutputContent(BaseModel):
        output: str

    class ConversationItem(BaseModel):
        id: Optional[str] = None
        type: Literal["message", "function_call", "function_call_output"]
        status: Optional[Literal["completed", "in_progress", "incomplete"]] = None
        role: Literal["user", "assistant", "system"]
        content: list[
            Union[ConversationItemContent, FunctionCallContent, FunctionCallOutputContent]
        ]
        call_id: Optional[str] = None
        name: Optional[str] = None
        arguments: Optional[str] = None
        output: Optional[str] = None

    class ConversationItemCreate(BaseEvent):
        type: Literal[ClientEventTypes.CONVERSATION_ITEM_CREATE] = (
            ClientEventTypes.CONVERSATION_ITEM_CREATE
        )
        item: ConversationItem

    class ConversationItemTruncate(BaseEvent):
        type: Literal[ClientEventTypes.CONVERSATION_ITEM_TRUNCATE] = (
            ClientEventTypes.CONVERSATION_ITEM_TRUNCATE
        )
        item_id: str
        content_index: int
        audio_end_ms: int

    class ConversationItemDelete(BaseEvent):
        type: Literal[ClientEventTypes.CONVERSATION_ITEM_DELETE] = (
            ClientEventTypes.CONVERSATION_ITEM_DELETE
        )
        item_id: str

    #### Responses
    class ResponseCreate(BaseEvent):
        type: Literal[ClientEventTypes.RESPONSE_CREATE] = ClientEventTypes.RESPONSE_CREATE

    class ResponseCancel(BaseEvent):
        type: Literal[ClientEventTypes.RESPONSE_CANCEL] = ClientEventTypes.RESPONSE_CANCEL

    # Update the Event union to include all event types
    ClientEvent = Union[
        SessionUpdate,
        InputAudioBufferAppend,
        InputAudioBufferCommit,
        InputAudioBufferClear,
        ConversationItemCreate,
        ConversationItemTruncate,
        ConversationItemDelete,
        ResponseCreate,
        ResponseCancel,
    ]

    """ SERVER EVENTS """

    class ServerEventTypes(str, Enum):
        ERROR = "error"
        RESPONSE_AUDIO_TRANSCRIPT_DONE = "response.audio_transcript.done"
        RESPONSE_AUDIO_TRANSCRIPT_DELTA = "response.audio_transcript.delta"
        RESPONSE_AUDIO_DELTA = "response.audio.delta"
        SESSION_CREATED = "session.created"
        SESSION_UPDATED = "session.updated"
        CONVERSATION_CREATED = "conversation.created"
        INPUT_AUDIO_BUFFER_COMMITTED = "input_audio_buffer.committed"
        INPUT_AUDIO_BUFFER_CLEARED = "input_audio_buffer.cleared"
        INPUT_AUDIO_BUFFER_SPEECH_STARTED = "input_audio_buffer.speech_started"
        INPUT_AUDIO_BUFFER_SPEECH_STOPPED = "input_audio_buffer.speech_stopped"
        CONVERSATION_ITEM_CREATED = "conversation.item.created"
        CONVERSATION_ITEM_INPUT_AUDIO_TRANSCRIPTION_COMPLETED = (
            "conversation.item.input_audio_transcription.completed"
        )
        CONVERSATION_ITEM_INPUT_AUDIO_TRANSCRIPTION_FAILED = (
            "conversation.item.input_audio_transcription.failed"
        )
        CONVERSATION_ITEM_TRUNCATED = "conversation.item.truncated"
        CONVERSATION_ITEM_DELETED = "conversation.item.deleted"
        RESPONSE_CREATED = "response.created"
        RESPONSE_DONE = "response.done"
        RESPONSE_OUTPUT_ITEM_ADDED = "response.output_item.added"
        RESPONSE_OUTPUT_ITEM_DONE = "response.output_item.done"
        RESPONSE_CONTENT_PART_ADDED = "response.content_part.added"
        RESPONSE_CONTENT_PART_DONE = "response.content_part.done"
        RESPONSE_TEXT_DELTA = "response.text.delta"
        RESPONSE_TEXT_DONE = "response.text.done"
        RESPONSE_AUDIO_DONE = "response.audio.done"
        RESPONSE_FUNCTION_CALL_ARGUMENTS_DELTA = "response.function_call_arguments.delta"
        RESPONSE_FUNCTION_CALL_ARGUMENTS_DONE = "response.function_call_arguments.done"
        RATE_LIMITS_UPDATED = "rate_limits.updated"

    #### Errors
    class ErrorDetails(BaseModel):
        type: Optional[str] = None
        code: Optional[str] = None
        message: Optional[str] = None
        param: Optional[str] = None

    class ErrorEvent(BaseEvent):
        type: Literal[ServerEventTypes.ERROR] = ServerEventTypes.ERROR
        error: ErrorDetails

    #### Session
    class SessionCreated(BaseEvent):
        type: Literal[ServerEventTypes.SESSION_CREATED] = ServerEventTypes.SESSION_CREATED
        session: Session

    class SessionUpdated(BaseEvent):
        type: Literal[ServerEventTypes.SESSION_UPDATED] = ServerEventTypes.SESSION_UPDATED
        session: Session

    #### Conversation
    class Conversation(BaseModel):
        id: str
        object: Literal["realtime.conversation"]

    class ConversationCreated(BaseEvent):
        type: Literal[ServerEventTypes.CONVERSATION_CREATED] = (
            ServerEventTypes.CONVERSATION_CREATED
        )
        conversation: Conversation

    class ConversationItemCreated(BaseEvent):
        type: Literal[ServerEventTypes.CONVERSATION_ITEM_CREATED] = (
            ServerEventTypes.CONVERSATION_ITEM_CREATED
        )
        previous_item_id: Optional[str] = None
        item: ConversationItem

    class ConversationItemInputAudioTranscriptionCompleted(BaseEvent):
        type: Literal[
            ServerEventTypes.CONVERSATION_ITEM_INPUT_AUDIO_TRANSCRIPTION_COMPLETED
        ] = ServerEventTypes.CONVERSATION_ITEM_INPUT_AUDIO_TRANSCRIPTION_COMPLETED
        item_id: str
        content_index: int
        transcript: str

    class ConversationItemInputAudioTranscriptionFailed(BaseEvent):
        type: Literal[
            ServerEventTypes.CONVERSATION_ITEM_INPUT_AUDIO_TRANSCRIPTION_FAILED
        ] = ServerEventTypes.CONVERSATION_ITEM_INPUT_AUDIO_TRANSCRIPTION_FAILED
        item_id: str
        content_index: int
        error: dict[str, Any]

    class ConversationItemTruncated(BaseEvent):
        type: Literal[ServerEventTypes.CONVERSATION_ITEM_TRUNCATED] = (
            ServerEventTypes.CONVERSATION_ITEM_TRUNCATED
        )
        item_id: str
        content_index: int
        audio_end_ms: int

    class ConversationItemDeleted(BaseEvent):
        type: Literal[ServerEventTypes.CONVERSATION_ITEM_DELETED] = (
            ServerEventTypes.CONVERSATION_ITEM_DELETED
        )
        item_id: str

    #### Response
    class ResponseUsage(BaseModel):
        total_tokens: int
        input_tokens: int
        output_tokens: int
        input_token_details: Optional[dict[str, int]] = None
        output_token_details: Optional[dict[str, int]] = None

    class ResponseOutput(BaseModel):
        id: str
        object: Literal["realtime.item"]
        type: str
        status: str
        role: str
        content: list[dict[str, Any]]

    class ResponseContentPart(BaseModel):
        type: str
        text: Optional[str] = None

    class ResponseOutputItemContent(BaseModel):
        type: str
        text: Optional[str] = None

    class ResponseStatusDetails(BaseModel):
        type: str
        reason: str

    class ResponseOutputItem(BaseModel):
        id: str
        object: Literal["realtime.item"]
        type: str
        status: str
        role: str
        content: list[ResponseOutputItemContent]

    class Response(BaseModel):
        id: str
        object: Literal["realtime.response"]
        status: str
        status_details: Optional[ResponseStatusDetails] = None
        output: list[ResponseOutput]
        usage: Optional[ResponseUsage]

    class ResponseCreated(BaseEvent):
        type: Literal[ServerEventTypes.RESPONSE_CREATED] = ServerEventTypes.RESPONSE_CREATED
        response: Response

    class ResponseDone(BaseEvent):
        type: Literal[ServerEventTypes.RESPONSE_DONE] = ServerEventTypes.RESPONSE_DONE
        response: Response

    class ResponseOutputItemAdded(BaseEvent):
        type: Literal[ServerEventTypes.RESPONSE_OUTPUT_ITEM_ADDED] = (
            ServerEventTypes.RESPONSE_OUTPUT_ITEM_ADDED
        )
        response_id: str
        output_index: int
        item: ResponseOutputItem

    class ResponseOutputItemDone(BaseEvent):
        type: Literal[ServerEventTypes.RESPONSE_OUTPUT_ITEM_DONE] = (
            ServerEventTypes.RESPONSE_OUTPUT_ITEM_DONE
        )
        response_id: str
        output_index: int
        item: ResponseOutputItem

    class ResponseContentPartAdded(BaseEvent):
        type: Literal[ServerEventTypes.RESPONSE_CONTENT_PART_ADDED] = (
            ServerEventTypes.RESPONSE_CONTENT_PART_ADDED
        )
        response_id: str
        item_id: str
        output_index: int
        content_index: int
        part: ResponseContentPart

    class ResponseContentPartDone(BaseEvent):
        type: Literal[ServerEventTypes.RESPONSE_CONTENT_PART_DONE] = (
            ServerEventTypes.RESPONSE_CONTENT_PART_DONE
        )
        response_id: str
        item_id: str
        output_index: int
        content_index: int
        part: ResponseContentPart

    #### Response Text
    class ResponseTextDelta(BaseEvent):
        type: Literal[ServerEventTypes.RESPONSE_TEXT_DELTA] = (
            ServerEventTypes.RESPONSE_TEXT_DELTA
        )
        response_id: str
        item_id: str
        output_index: int
        content_index: int
        delta: str

    class ResponseTextDone(BaseEvent):
        type: Literal[ServerEventTypes.RESPONSE_TEXT_DONE] = (
            ServerEventTypes.RESPONSE_TEXT_DONE
        )
        response_id: str
        item_id: str
        output_index: int
        content_index: int
        text: str

    #### Response Audio
    class ResponseAudioTranscriptDone(BaseEvent):
        type: Literal[ServerEventTypes.RESPONSE_AUDIO_TRANSCRIPT_DONE] = (
            ServerEventTypes.RESPONSE_AUDIO_TRANSCRIPT_DONE
        )
        transcript: str

    class ResponseAudioTranscriptDelta(BaseEvent):
        type: Literal[ServerEventTypes.RESPONSE_AUDIO_TRANSCRIPT_DELTA] = (
            ServerEventTypes.RESPONSE_AUDIO_TRANSCRIPT_DELTA
        )
        delta: str

    class ResponseAudioDelta(BaseEvent):
        type: Literal[ServerEventTypes.RESPONSE_AUDIO_DELTA] = (
            ServerEventTypes.RESPONSE_AUDIO_DELTA
        )
        response_id: str
        item_id: str
        delta: str

    class ResponseAudioDone(BaseEvent):
        type: Literal[ServerEventTypes.RESPONSE_AUDIO_DONE] = (
            ServerEventTypes.RESPONSE_AUDIO_DONE
        )
        response_id: str
        item_id: str
        output_index: int
        content_index: int

    class InputAudioBufferCommitted(BaseEvent):
        type: Literal[ServerEventTypes.INPUT_AUDIO_BUFFER_COMMITTED] = (
            ServerEventTypes.INPUT_AUDIO_BUFFER_COMMITTED
        )
        previous_item_id: Optional[str] = None
        item_id: Optional[str] = None
        event_id: Optional[str] = None

    class InputAudioBufferCleared(BaseEvent):
        type: Literal[ServerEventTypes.INPUT_AUDIO_BUFFER_CLEARED] = (
            ServerEventTypes.INPUT_AUDIO_BUFFER_CLEARED
        )

    class InputAudioBufferSpeechStarted(BaseEvent):
        type: Literal[ServerEventTypes.INPUT_AUDIO_BUFFER_SPEECH_STARTED] = (
            ServerEventTypes.INPUT_AUDIO_BUFFER_SPEECH_STARTED
        )
        audio_start_ms: int
        item_id: str

    class InputAudioBufferSpeechStopped(BaseEvent):
        type: Literal[ServerEventTypes.INPUT_AUDIO_BUFFER_SPEECH_STOPPED] = (
            ServerEventTypes.INPUT_AUDIO_BUFFER_SPEECH_STOPPED
        )
        audio_end_ms: int
        item_id: str

    #### Function Calls
    class ResponseFunctionCallArgumentsDelta(BaseEvent):
        type: Literal[ServerEventTypes.RESPONSE_FUNCTION_CALL_ARGUMENTS_DELTA] = (
            ServerEventTypes.RESPONSE_FUNCTION_CALL_ARGUMENTS_DELTA
        )
        response_id: str
        item_id: str
        output_index: int
        call_id: str
        delta: str

    class ResponseFunctionCallArgumentsDone(BaseEvent):
        type: Literal[ServerEventTypes.RESPONSE_FUNCTION_CALL_ARGUMENTS_DONE] = (
            ServerEventTypes.RESPONSE_FUNCTION_CALL_ARGUMENTS_DONE
        )
        response_id: str
        item_id: str
        output_index: int
        call_id: str
        arguments: str

    #### Rate Limits
    class RateLimit(BaseModel):
        name: str
        limit: int
        remaining: int
        reset_seconds: float

    class RateLimitsUpdated(BaseEvent):
        type: Literal[ServerEventTypes.RATE_LIMITS_UPDATED] = (
            ServerEventTypes.RATE_LIMITS_UPDATED
        )
        rate_limits: list[RateLimit]

    ServerEvent = Union[
        ErrorEvent,
        ConversationCreated,
        ResponseAudioTranscriptDone,
        ResponseAudioTranscriptDelta,
        ResponseAudioDelta,
        ResponseCreated,
        ResponseDone,
        ResponseOutputItemAdded,
        ResponseOutputItemDone,
        ResponseContentPartAdded,
        ResponseContentPartDone,
        ResponseTextDelta,
        ResponseTextDone,
        ResponseAudioDone,
        ConversationItemInputAudioTranscriptionCompleted,
        SessionCreated,
        SessionUpdated,
        InputAudioBufferCleared,
        InputAudioBufferSpeechStarted,
        InputAudioBufferSpeechStopped,
        ConversationItemCreated,
        ConversationItemInputAudioTranscriptionFailed,
        ConversationItemTruncated,
        ConversationItemDeleted,
        RateLimitsUpdated,
    ]

    EVENT_TYPE_TO_MODEL = {
        ServerEventTypes.ERROR: ErrorEvent,
        ServerEventTypes.RESPONSE_AUDIO_TRANSCRIPT_DONE: ResponseAudioTranscriptDone,
        ServerEventTypes.RESPONSE_AUDIO_TRANSCRIPT_DELTA: ResponseAudioTranscriptDelta,
        ServerEventTypes.RESPONSE_AUDIO_DELTA: ResponseAudioDelta,
        ServerEventTypes.CONVERSATION_ITEM_INPUT_AUDIO_TRANSCRIPTION_COMPLETED: ConversationItemInputAudioTranscriptionCompleted,
        ServerEventTypes.SESSION_CREATED: SessionCreated,
        ServerEventTypes.SESSION_UPDATED: SessionUpdated,
        ServerEventTypes.CONVERSATION_CREATED: ConversationCreated,
        ServerEventTypes.INPUT_AUDIO_BUFFER_COMMITTED: InputAudioBufferCommitted,
        ServerEventTypes.INPUT_AUDIO_BUFFER_CLEARED: InputAudioBufferCleared,
        ServerEventTypes.INPUT_AUDIO_BUFFER_SPEECH_STARTED: InputAudioBufferSpeechStarted,
        ServerEventTypes.INPUT_AUDIO_BUFFER_SPEECH_STOPPED: InputAudioBufferSpeechStopped,
        ServerEventTypes.CONVERSATION_ITEM_CREATED: ConversationItemCreated,
        ServerEventTypes.CONVERSATION_ITEM_INPUT_AUDIO_TRANSCRIPTION_FAILED: ConversationItemInputAudioTranscriptionFailed,
        ServerEventTypes.CONVERSATION_ITEM_TRUNCATED: ConversationItemTruncated,
        ServerEventTypes.CONVERSATION_ITEM_DELETED: ConversationItemDeleted,
        ServerEventTypes.RESPONSE_CREATED: ResponseCreated,
        ServerEventTypes.RESPONSE_DONE: ResponseDone,
        ServerEventTypes.RESPONSE_OUTPUT_ITEM_ADDED: ResponseOutputItemAdded,
        ServerEventTypes.RESPONSE_OUTPUT_ITEM_DONE: ResponseOutputItemDone,
        ServerEventTypes.RESPONSE_CONTENT_PART_ADDED: ResponseContentPartAdded,
        ServerEventTypes.RESPONSE_CONTENT_PART_DONE: ResponseContentPartDone,
        ServerEventTypes.RESPONSE_TEXT_DELTA: ResponseTextDelta,
        ServerEventTypes.RESPONSE_TEXT_DONE: ResponseTextDone,
        ServerEventTypes.RESPONSE_AUDIO_DONE: ResponseAudioDone,
        ServerEventTypes.RATE_LIMITS_UPDATED: RateLimitsUpdated,
    }

    def parse_server_event(event_data: dict) -> ServerEvent:
        event_type = event_data.get("type")
        if not event_type:
            raise ValueError("Event data is missing 'type' field")

        model_class = EVENT_TYPE_TO_MODEL.get(event_type)
        if not model_class:
            raise ValueError(f"Unknown event type: {event_type}")

        try:
            return model_class(**event_data)
        except ValidationError as e:
            raise ValueError(f"Failed to parse event of type {event_type}: {str(e)}") from e
    ```
  </details>

  ## オーディオストリームライター（ディスクおよびメモリ内）

  ```python
  class StreamingWavWriter:
      """Writes audio integer or byte array chunks to a WAV file."""

      wav_file = None
      buffer = None
      in_memory = False

      def __init__(
          self,
          filename=None,
          channels=INPUT_DEVICE_CHANNELS,
          sample_width=SAMPLE_WIDTH,
          framerate=SAMPLE_RATE,
      ):
          self.in_memory = filename is None
          if self.in_memory:
              self.buffer = io.BytesIO()
              self.wav_file = wave.open(self.buffer, "wb")
          else:
              self.wav_file = wave.open(filename, "wb")

          self.wav_file.setnchannels(channels)
          self.wav_file.setsampwidth(sample_width)
          self.wav_file.setframerate(framerate)

      def append_int16_chunk(self, int16_data):
          if int16_data is not None:
              self.wav_file.writeframes(
                  int16_data.tobytes()
                  if isinstance(int16_data, np.ndarray)
                  else int16_data
              )

      def close(self):
          self.wav_file.close()

      def get_wav_buffer(self):
          assert self.in_memory, "Buffer only available if stream is in memory."
          return self.buffer
  ```

  ## リアルタイムオーディオモデル

  リアルタイム（RT）オーディオモデルはウェブソケットを使用して、OpenAIのリアルタイムオーディオAPIにイベントを送信します。これは次のように機能します：

  1. **init:**ローカルバッファ（入力オーディオ）とストリーム（アシスタント再生ストリーム、ユーザーオーディオディスクライターストリーム）を初期化し、リアルタイムAPIへの接続を開きます。
  2. **receive\_messages\_thread**：スレッドがAPIからのメッセージの受信を処理します。主に4つのイベントタイプが処理されます：- RESPONSE\_AUDIO\_TRANSCRIPT\_DONE：

     サーバーがアシスタントの応答が完了したことを示し、トランスクリプトを提供します。

     * CONVERSATION\_ITEM\_INPUT\_AUDIO\_TRANSCRIPTION\_COMPLETED：

       サーバーがユーザーの音声が文字起こしされたことを示し、ユーザーの音声のトランスクリプトを送信します。トランスクリプトをWeaveに記録し、ユーザーに表示します。

     * RESPONSE\_AUDIO\_DELTA：

       サーバーがアシスタント応答音声の新しいチャンクを送信します。これをレスポンスIDを介して進行中のレスポンスデータに追加し、再生用の出力ストリームに追加します。

     * RESPONSE\_DONE：

       サーバーがアシスタント応答の完了を示します。レスポンスに関連するすべての音声チャンクとトランスクリプトを取得し、Weaveに記録します。

     3.**send\_audio**：ハンドラーがユーザーの音声チャンクをバッファに追加し、音声バッファが一定のサイズに達したときに音声チャンクを送信します。

  ```python
  class RTAudioModel(weave.Model):
      """Model class for realtime e2e audio OpenAI model interaction with Whisper user transcription for logging."""

      realtime_model_name: str = "gpt-4o-realtime-preview-2024-10-01"  # realtime e2e audio only model interaction

      stop_event: Optional[threading.Event] = threading.Event()  # Event to stop the model
      ws: Optional[websocket.WebSocket] = None  # Websocket for OpenAI communications

      user_wav_writer: Optional[StreamingWavWriter] = (
          None  # Stream for writing user output to file
      )
      input_audio_buffer: Optional[np.ndarray] = None  # Buffer for user audio chunks
      assistant_outputs: dict[str, StreamingWavWriter] = (
          None  # Assistant outputs aggregated to send to weave
      )
      playback_stream: Optional[pyaudio.Stream] = (
          None  # Playback stream for playing assistant responses
      )

      def __init__(self):
          super().__init__()
          self.stop_event.clear()
          self.user_wav_writer = StreamingWavWriter(
              filename="user_audio.wav", framerate=SAMPLE_RATE
          )
          self.input_audio_buffer = np.array([], dtype=np.int16)
          self.ws = websocket.WebSocket()
          self.assistant_outputs = {}

          # Open the assistant audio playback stream if enabled
          if enable_audio_playback:
              self.playback_stream = pyaudio.PyAudio().open(
                  format=pyaudio.paInt16,
                  channels=OUTPUT_DEVICE_CHANNELS,
                  rate=OAI_SAMPLE_RATE,
                  output=True,
                  output_device_index=OUTPUT_DEVICE_INDEX,
              )

          # Connect Websocket
          try:
              self.ws.connect(
                  f"wss://api.openai.com/v1/realtime?model={self.realtime_model_name}",
                  header={
                      "Authorization": f"Bearer {os.environ.get('OPENAI_API_KEY')}",
                      "OpenAI-Beta": "realtime=v1",
                  },
              )

              # Send config msg
              config_event = SessionUpdate(
                  session=Session(
                      modalities=["text", "audio"],  # modalities to use
                      input_audio_transcription=InputAudioTranscription(
                          model="whisper-1"
                      ),  # whisper-1 for transcription
                      turn_detection=TurnDetection(
                          type="server_vad",
                          threshold=0.3,
                          prefix_padding_ms=300,
                          silence_duration_ms=600,
                      ),  # server VAD to detect silence
                  )
              )
              self.ws.send(config_event.model_dump_json(exclude_none=True))
              self.log_ws_message(config_event.model_dump_json(exclude_none=True), "Sent")

              # Start listener
              websocket_thread = threading.Thread(target=self.receive_messages_thread)
              websocket_thread.daemon = True
              websocket_thread.start()

          except Exception as e:
              print(f"Error connecting to WebSocket: {e}")

      ##### Weave Integration and Message Handlers #####
      def handle_assistant_response_audio_delta(self, data: ResponseAudioDelta):
          if data.response_id not in self.assistant_outputs:
              self.assistant_outputs[data.response_id] = StreamingWavWriter(
                  framerate=OAI_SAMPLE_RATE
              )

          data_bytes = base64.b64decode(data.delta)
          self.assistant_outputs[data.response_id].append_int16_chunk(data_bytes)

          if enable_audio_playback:
              self.playback_stream.write(data_bytes)

          return {"assistant_audio": data_bytes}

      @weave.op()
      def handle_assistant_response_done(self, data: ResponseDone):
          wave_file_stream = self.assistant_outputs[data.response.id]
          wave_file_stream.close()
          wave_file_stream.buffer.seek(0)
          weave_payload = {
              "assistant_audio": wave.open(wave_file_stream.get_wav_buffer(), "rb"),
              "assistant_transcript": data.response.output[0]
              .content[0]
              .get("transcript", "Transcript Unavailable."),
          }
          return weave_payload

      @weave.op()
      def handle_user_transcription_done(
          self, data: ConversationItemInputAudioTranscriptionCompleted
      ):
          return {"user_transcript": data.transcript}

      ##### Message Receiver and Sender #####
      def receive_messages_thread(self):
          while not self.stop_event.is_set():
              try:
                  data = json.loads(self.ws.recv())
                  self.log_ws_message(json.dumps(data, indent=2))

                  parsed_event = parse_server_event(data)

                  if parsed_event.type == ServerEventTypes.RESPONSE_AUDIO_TRANSCRIPT_DONE:
                      print("Assistant: ", parsed_event.transcript)
                  elif (
                      parsed_event.type
                      == ServerEventTypes.CONVERSATION_ITEM_INPUT_AUDIO_TRANSCRIPTION_COMPLETED
                  ):
                      print("User: ", parsed_event.transcript)
                      self.handle_user_transcription_done(parsed_event)
                  elif parsed_event.type == ServerEventTypes.RESPONSE_AUDIO_DELTA:
                      self.handle_assistant_response_audio_delta(parsed_event)
                  elif parsed_event.type == ServerEventTypes.RESPONSE_DONE:
                      self.handle_assistant_response_done(parsed_event)
                  elif parsed_event.type == ServerEventTypes.ERROR:
                      print(
                          f"\nError from server: {parsed_event.error.model_dump_json(exclude_none=True)}"
                      )
              except websocket.WebSocketConnectionClosedException:
                  print("\nWebSocket connection closed")
                  break
              except json.JSONDecodeError:
                  continue
              except Exception as e:
                  print(f"\nError in receive_messages: {e}")
                  break

      def send_audio(self, audio_chunk):
          if self.ws and self.ws.connected:
              self.input_audio_buffer = np.append(
                  self.input_audio_buffer, np.frombuffer(audio_chunk, dtype=np.int16)
              )
              if len(self.input_audio_buffer) >= SAMPLE_RATE * CHUNK_DURATION:
                  try:
                      # Resample audio to OAI sample rate
                      resampled_audio = (
                          resampy.resample(
                              self.input_audio_buffer, SAMPLE_RATE, OAI_SAMPLE_RATE
                          )
                          if SAMPLE_RATE != OAI_SAMPLE_RATE
                          else self.input_audio_buffer
                      )

                      # Send audio chunk to OAI API
                      audio_event = InputAudioBufferAppend(
                          audio=base64.b64encode(
                              resampled_audio.astype(np.int16).tobytes()
                          ).decode("utf-8")  # Convert audio array to b64 bytes
                      )
                      self.ws.send(audio_event.model_dump_json(exclude_none=True))
                      self.log_ws_message(
                          audio_event.model_dump_json(exclude_none=True), "Sent"
                      )
                  finally:
                      self.user_wav_writer.append_int16_chunk(self.input_audio_buffer)

                      # Clear the audio buffer
                      self.input_audio_buffer = np.array([], dtype=np.int16)
          else:
              print("Error sending audio: websocket not initialized.")

      ##### General Utility Functions #####
      def log_ws_message(self, message, direction="Received"):
          with open("websocket_log.txt", "a") as log_file:
              log_file.write(
                  f"{time.strftime('%Y-%m-%d %H:%M:%S')} - {direction}: {message}\n"
              )

      def stop(self):
          self.stop_event.set()

          if self.ws:
              self.ws.close()

          self.user_wav_writer.close()
  ```

  ## 音声レコーダー

  RTAudioモデルの`send_audio`メソッドにリンクされたハンドラーを持つpyaudio入力ストリームを使用します。プログラム完了時に安全に終了できるように、ストリームはメインスレッドに返されます。

  ```python
  # Audio capture stream
  def record_audio(realtime_model: RTAudioModel) -> pyaudio.Stream:
      """Setup a Pyaudio input stream and use the RTAudioModel as a callback for streaming data."""

      def audio_callback(in_data, frame_count, time_info, status):
          realtime_model.send_audio(in_data)
          return (None, pyaudio.paContinue)

      p = pyaudio.PyAudio()
      stream = p.open(
          format=pyaudio.paInt16,
          channels=INPUT_DEVICE_CHANNELS,
          rate=SAMPLE_RATE,
          input=True,
          input_device_index=INPUT_DEVICE_INDEX,
          frames_per_buffer=CHUNK,
          stream_callback=audio_callback,
      )
      stream.start_stream()

      print("Recording started. Please begin speaking to your personal assistant...")
      return stream
  ```

  ## メインスレッド（実行してください！）

  メインスレッドはWeaveを統合したリアルタイムオーディオモデルを開始します。次に、録音が開始され、ユーザーからのキーボード割り込みを待ちます。

  ```python
  weave.init(project_name="realtime-oai-audio-testing")

  realtime_model = RTAudioModel()

  if realtime_model.ws and realtime_model.ws.connected:
      recording_stream: pyaudio.Stream = record_audio(realtime_model)

      try:
          while not realtime_model.stop_event.is_set():
              time.sleep(1)
      except KeyboardInterrupt:
          pass
      except Exception as e:
          print(f"Error in main loop: {e}")
          import traceback

          traceback.print_exc()
      finally:
          print("Exiting...")
          realtime_model.stop()
          if recording_stream and recording_stream.is_active():
              recording_stream.stop_stream()
              recording_stream.close()
  else:
      print(
          "WebSocket connection failed. Please check your API key and internet connection."
      )
  ```
</details>
