Metadata-Version: 2.4
Name: jupyterlab_voice_capture_extension
Version: 1.0.6
Summary: JupyterLab extension that captures microphone audio in the browser and streams it to a server-side bridge, exposing it as a virtual audio source so terminal applications running in the container (such as Claude Code voice mode) can record from the user's microphone
Project-URL: Homepage, https://github.com/stellarshenson/jupyterlab_voice_capture_extension
Project-URL: Bug Tracker, https://github.com/stellarshenson/jupyterlab_voice_capture_extension/issues
Project-URL: Repository, https://github.com/stellarshenson/jupyterlab_voice_capture_extension.git
Author-email: Stellars Henson <konrad.jelen+github@gmail.com>
License: BSD 3-Clause License
        
        Copyright (c) 2026, Stellars Henson
        All rights reserved.
        
        Redistribution and use in source and binary forms, with or without
        modification, are permitted provided that the following conditions are met:
        
        1. Redistributions of source code must retain the above copyright notice, this
           list of conditions and the following disclaimer.
        
        2. Redistributions in binary form must reproduce the above copyright notice,
           this list of conditions and the following disclaimer in the documentation
           and/or other materials provided with the distribution.
        
        3. Neither the name of the copyright holder nor the names of its
           contributors may be used to endorse or promote products derived from
           this software without specific prior written permission.
        
        THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
        AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
        IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
        DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
        FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
        DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
        SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
        CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
        OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
        OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
License-File: LICENSE
Keywords: jupyter,jupyterlab,jupyterlab-extension
Classifier: Framework :: Jupyter
Classifier: Framework :: Jupyter :: JupyterLab
Classifier: Framework :: Jupyter :: JupyterLab :: 4
Classifier: Framework :: Jupyter :: JupyterLab :: Extensions
Classifier: Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt
Classifier: License :: OSI Approved :: BSD License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.10
Requires-Dist: jupyter-server<3,>=2.4.0
Requires-Dist: traitlets>=5.0
Provides-Extra: dev
Requires-Dist: jupyter-builder>=1.0.0; extra == 'dev'
Requires-Dist: jupyterlab>=4; extra == 'dev'
Provides-Extra: test
Requires-Dist: coverage; extra == 'test'
Requires-Dist: pytest; extra == 'test'
Requires-Dist: pytest-asyncio; extra == 'test'
Requires-Dist: pytest-cov; extra == 'test'
Requires-Dist: pytest-jupyter[server]>=0.6.0; extra == 'test'
Description-Content-Type: text/markdown

# jupyterlab_voice_capture_extension

[![GitHub Actions](https://github.com/stellarshenson/jupyterlab_voice_capture_extension/actions/workflows/build.yml/badge.svg)](https://github.com/stellarshenson/jupyterlab_voice_capture_extension/actions/workflows/build.yml)
[![npm version](https://img.shields.io/npm/v/jupyterlab_voice_capture_extension.svg)](https://www.npmjs.com/package/jupyterlab_voice_capture_extension)
[![PyPI version](https://img.shields.io/pypi/v/jupyterlab-voice-capture-extension.svg)](https://pypi.org/project/jupyterlab-voice-capture-extension/)
[![Total PyPI downloads](https://static.pepy.tech/badge/jupyterlab-voice-capture-extension)](https://pepy.tech/project/jupyterlab-voice-capture-extension)
[![JupyterLab 4](https://img.shields.io/badge/JupyterLab-4-orange.svg)](https://jupyterlab.readthedocs.io/en/stable/)
[![Brought To You By KOLOMOLO](https://img.shields.io/badge/Brought%20To%20You%20By-KOLOMOLO-00ffff?style=flat)](https://kolomolo.com)
[![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-blue?style=flat)](https://www.paypal.com/donate/?hosted_button_id=B4KPBJDLLXTSA)

Capture microphone audio in the JupyterLab browser tab and stream it to a server-side FIFO, so terminal applications running inside the container - notably Claude Code voice mode - can record from a microphone the container itself has no access to.

The container has no capture device; the browser does. This extension bridges that gap: the browser captures the mic, ships the audio over an authenticated websocket to a Jupyter server handler, and the handler writes raw PCM to a named pipe. A separate, out-of-scope plumbing layer (PulseAudio `module-pipe-source` + SoX) turns that pipe into the system default audio source.

## How it works

- **Capture** - a microphone toggle in the status bar calls `getUserMedia`; an AudioWorklet resamples to 16 kHz mono and encodes signed 16-bit little-endian PCM off the UI thread
- **Transport** - 20 ms PCM frames (640 bytes) are sent as binary websocket messages to `…/jupyterlab-voice-capture-extension/stream`, which lives under the Jupyter base URL and inherits Jupyter token auth - no new port is opened
- **Sink** - the server handler writes each frame, in order, to a FIFO (default `/run/pulseaudio.fifo`); it creates the pipe if absent and tolerates a not-yet-attached reader without blocking the server

Chain: browser mic → AudioWorklet (16 kHz mono s16le) → websocket → server handler → FIFO → (PulseAudio + SoX, out of scope) → terminal app.

> [!IMPORTANT]
> Out of scope - the extension does not manage PulseAudio, invoke SoX or any recorder, or perform speech-to-text; its responsibility ends at delivering correct PCM to the FIFO.

## Requirements

- JupyterLab >= 4.0.0
- A secure context (https or `localhost`) - browsers only expose the microphone over a secure origin

## Install

```bash
pip install jupyterlab-voice-capture-extension
```

## Dependencies

- **Python**: `jupyter_server` and `traitlets`, installed automatically with the package
- **System** (only for the full voice chain into a terminal app): PulseAudio + SoX. Provision and verify them with the bundled CLI:

```bash
jupyterlab_voice_capture_extension install    # apt packages + client.conf + Jupyter config line (does NOT start the daemon)
jupyterlab_voice_capture_extension start -d   # start the PulseAudio daemon + pipe-source (run after install and each restart)
jupyterlab_voice_capture_extension validate   # check every component, print what to fix (--json for machine output)
jupyterlab_voice_capture_extension stop       # kill the PulseAudio daemon
```

See [docs/jupyterlab-enable-claude-voice.md](docs/jupyterlab-enable-claude-voice.md) for the full setup and troubleshooting.

## Usage

- Click the microphone icon in the status bar (or run **Toggle Voice Capture** from the command palette) to start capture
- On the first start the browser asks for microphone permission; the status label moves Disconnected → Connecting → Connected, the icon glows green while streaming, and the browser shows its active-microphone indicator
- Click again to stop - capture tracks are released and the browser indicator clears
- Only one tab streams at a time: starting capture in a second tab takes over and stops the first

## Configuration

The sink FIFO path defaults to `/run/pulseaudio.fifo` and is overridable via Jupyter server config:

```python
c.VoiceCapture.sink_path = "/run/pulseaudio.fifo"
```

Settings → **Voice Capture** has one option, **Auto-connect on startup** (`autoConnect`, default off): when enabled, capture starts automatically as JupyterLab loads instead of waiting for a click.

## Uninstall

```bash
pip uninstall jupyterlab-voice-capture-extension
```

## Troubleshoot

If you see the frontend extension but it is not working, check that the server extension is enabled:

```bash
jupyter server extension list
```

If the server extension is installed and enabled but you do not see the frontend extension, check the frontend extension is installed:

```bash
jupyter labextension list
```

## Contributing

If you would like to contribute to this extension, please refer to the [Contributing Guide](CONTRIBUTING.md).
