Skip to content

examples/realtime/push_to_talk_app.py is grossly outdated #2715

Closed

Description

@master0v

Confirm this is an issue with the Python library and not an underlying OpenAI API

  • This is an issue with the Python library

Describe the bug

why are you confusing users? remove it entirely, or replace with an up-to-date version

and why is it "push-to-talk" anyway? push-to-talk is not realtime!

To Reproduce

just try to run it with the latest openai library. you will get a bunch of errors because the library has evolved since that example was written.

Code snippets

OS

MacOS (Apple Silicon)

Python version

3.13.5

Library version

latest

Activity

  1. karpetrosyan commented on Oct 29, 2025

    @karpetrosyan
    Contributor

    It's push to talk because you need to press a button to start real-time communication.

    I guess there was just an outdated import. Does this fixed version work for you?

    Code here:
    #!/usr/bin/env uv run
    # /// script
    # requires-python = ">=3.9"
    # dependencies = [
    #     "textual",
    #     "numpy",
    #     "pyaudio",
    #     "pydub",
    #     "sounddevice",
    #     "openai[realtime]",
    # ]
    #
    # [tool.uv.sources]
    # openai = { path = "../../", editable = true }
    # ///
    ####################################################################
    # Sample TUI app with a push to talk interface to the Realtime API #
    # If you have `uv` installed and the `OPENAI_API_KEY`              #
    # environment variable set, you can run this example with just     #
    #                                                                  #
    # `./examples/realtime/push_to_talk_app.py`                        #
    #                                                                  #
    # On Mac, you'll also need `brew install portaudio ffmpeg`         #
    ####################################################################
    from __future__ import annotations
    
    import base64
    import asyncio
    from typing import Any, cast
    from typing_extensions import override
    
    from textual import events
    from audio_util import CHANNELS, SAMPLE_RATE, AudioPlayerAsync
    from textual.app import App, ComposeResult
    from textual.widgets import Button, Static, RichLog
    from textual.reactive import reactive
    from textual.containers import Container
    
    from openai import AsyncOpenAI
    from openai.resources.realtime.realtime import AsyncRealtimeConnection
    from openai.types.realtime.session_update_event import Session
    
    
    class SessionDisplay(Static):
        """A widget that shows the current session ID."""
    
        session_id = reactive("")
    
        @override
        def render(self) -> str:
            return f"Session ID: {self.session_id}" if self.session_id else "Connecting..."
    
    
    class AudioStatusIndicator(Static):
        """A widget that shows the current audio recording status."""
    
        is_recording = reactive(False)
    
        @override
        def render(self) -> str:
            status = (
                "🔴 Recording... (Press K to stop)" if self.is_recording else "⚪ Press K to start recording (Q to quit)"
            )
            return status
    
    
    class RealtimeApp(App[None]):
        CSS = """
            Screen {
                background: #1a1b26;  /* Dark blue-grey background */
            }
    
            Container {
                border: double rgb(91, 164, 91);
            }
    
            Horizontal {
                width: 100%;
            }
    
            #input-container {
                height: 5;  /* Explicit height for input container */
                margin: 1 1;
                padding: 1 2;
            }
    
            Input {
                width: 80%;
                height: 3;  /* Explicit height for input */
            }
    
            Button {
                width: 20%;
                height: 3;  /* Explicit height for button */
            }
    
            #bottom-pane {
                width: 100%;
                height: 82%;  /* Reduced to make room for session display */
                border: round rgb(205, 133, 63);
                content-align: center middle;
            }
    
            #status-indicator {
                height: 3;
                content-align: center middle;
                background: #2a2b36;
                border: solid rgb(91, 164, 91);
                margin: 1 1;
            }
    
            #session-display {
                height: 3;
                content-align: center middle;
                background: #2a2b36;
                border: solid rgb(91, 164, 91);
                margin: 1 1;
            }
    
            Static {
                color: white;
            }
        """
    
        client: AsyncOpenAI
        should_send_audio: asyncio.Event
        audio_player: AudioPlayerAsync
        last_audio_item_id: str | None
        connection: AsyncRealtimeConnection | None
        session: Session | None
        connected: asyncio.Event
    
        def __init__(self) -> None:
            super().__init__()
            self.connection = None
            self.session = None
            self.client = AsyncOpenAI()
            self.audio_player = AudioPlayerAsync()
            self.last_audio_item_id = None
            self.should_send_audio = asyncio.Event()
            self.connected = asyncio.Event()
    
        @override
        def compose(self) -> ComposeResult:
            """Create child widgets for the app."""
            with Container():
                yield SessionDisplay(id="session-display")
                yield AudioStatusIndicator(id="status-indicator")
                yield RichLog(id="bottom-pane", wrap=True, highlight=True, markup=True)
    
        async def on_mount(self) -> None:
            self.run_worker(self.handle_realtime_connection())
            self.run_worker(self.send_mic_audio())
    
        async def handle_realtime_connection(self) -> None:
            async with self.client.realtime.connect(model="gpt-realtime") as conn:
                self.connection = conn
                self.connected.set()
    
                # note: this is the default and can be omitted
                # if you want to manually handle VAD yourself, then set `'turn_detection': None`
                await conn.session.update(
                    session={
                        "audio": {
                            "input": {"turn_detection": {"type": "server_vad"}},
                        },
                        "model": "gpt-realtime",
                        "type": "realtime",
                    }
                )
    
                acc_items: dict[str, Any] = {}
    
                async for event in conn:
                    if event.type == "session.created":
                        self.session = event.session
                        session_display = self.query_one(SessionDisplay)
                        # id comes with the response, but doesn't appear in the type definition
                        assert event.session.id is not None  # type: ignore
                        session_display.session_id = event.session.id  # type: ignore
                        continue
    
                    if event.type == "session.updated":
                        self.session = event.session
                        continue
    
                    if event.type == "response.output_audio.delta":
                        if event.item_id != self.last_audio_item_id:
                            self.audio_player.reset_frame_count()
                            self.last_audio_item_id = event.item_id
    
                        bytes_data = base64.b64decode(event.delta)
                        self.audio_player.add_data(bytes_data)
                        continue
    
                    if event.type == "response.output_audio_transcript.delta":
                        try:
                            text = acc_items[event.item_id]
                        except KeyError:
                            acc_items[event.item_id] = event.delta
                        else:
                            acc_items[event.item_id] = text + event.delta
    
                        # Clear and update the entire content because RichLog otherwise treats each delta as a new line
                        bottom_pane = self.query_one("#bottom-pane", RichLog)
                        bottom_pane.clear()
                        bottom_pane.write(acc_items[event.item_id])
                        continue
    
        async def _get_connection(self) -> AsyncRealtimeConnection:
            await self.connected.wait()
            assert self.connection is not None
            return self.connection
    
        async def send_mic_audio(self) -> None:
            import sounddevice as sd  # type: ignore
    
            sent_audio = False
    
            device_info = sd.query_devices()
            print(device_info)
    
            read_size = int(SAMPLE_RATE * 0.02)
    
            stream = sd.InputStream(
                channels=CHANNELS,
                samplerate=SAMPLE_RATE,
                dtype="int16",
            )
            stream.start()
    
            status_indicator = self.query_one(AudioStatusIndicator)
    
            try:
                while True:
                    if stream.read_available < read_size:
                        await asyncio.sleep(0)
                        continue
    
                    await self.should_send_audio.wait()
                    status_indicator.is_recording = True
    
                    data, _ = stream.read(read_size)
    
                    connection = await self._get_connection()
                    if not sent_audio:
                        asyncio.create_task(connection.send({"type": "response.cancel"}))
                        sent_audio = True
    
                    await connection.input_audio_buffer.append(audio=base64.b64encode(cast(Any, data)).decode("utf-8"))
    
                    await asyncio.sleep(0)
            except KeyboardInterrupt:
                pass
            finally:
                stream.stop()
                stream.close()
    
        async def on_key(self, event: events.Key) -> None:
            """Handle key press events."""
            if event.key == "enter":
                self.query_one(Button).press()
                return
    
            if event.key == "q":
                self.exit()
                return
    
            if event.key == "k":
                status_indicator = self.query_one(AudioStatusIndicator)
                if status_indicator.is_recording:
                    self.should_send_audio.clear()
                    status_indicator.is_recording = False
    
                    if self.session and hasattr(self.session, "turn_detection") and self.session.turn_detection is None:
                        # The default in the API is that the model will automatically detect when the user has
                        # stopped talking and then start responding itself.
                        #
                        # However if we're in manual `turn_detection` mode then we need to
                        # manually tell the model to commit the audio buffer and start responding.
                        conn = await self._get_connection()
                        await conn.input_audio_buffer.commit()
                        await conn.response.create()
                else:
                    self.should_send_audio.set()
                    status_indicator.is_recording = True
    
    
    if __name__ == "__main__":
        app = RealtimeApp()
        app.run()
  2. Rishirandhawa commented on Mar 3, 2026

    @Rishirandhawa
    Contributor

    Opened a fix PR: https://github.com/openai/openai-python/pull/2926\n\nIt updates the broken import path in and adds a small guard for session variants that may not expose .

  3. Rishirandhawa commented on Mar 3, 2026

    @Rishirandhawa
    Contributor

    Correction: PR #2926 updates the broken Session import path in examples/realtime/push_to_talk_app.py and adds a turn_detection attribute guard for session variants where that field may not exist.

  4. marcuswood-oai commented on Sep 14, 2026

    @marcuswood-oai
    Contributor

    Thanks for flagging this, and sorry the example broke as the SDK evolved! #2926 fixes the stale Session import and updates turn-detection handling to the current session layout. Once it merges, please pull the updated example from main.

    We’re closing this to track the remaining work in that PR. If the updated example still fails, or we’ve closed this by mistake, please flag it here with the traceback and SDK version—we’re happy to revisit.

  5. added a commit that references this issue on Sep 14, 2026
    0b7ad38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions