Metadata-Version: 2.5
Name: livekit-plugins-murf-bvc
Version: 0.2.2
Summary: Murf background voice cancellation for LiveKit Agents
Requires-Python: >=3.10
Requires-Dist: livekit-agents>=1.2
Requires-Dist: livekit>=1.0.12
Requires-Dist: numpy
Description-Content-Type: text/markdown

# Murf BVC for LiveKit Agents

Background voice cancellation for voice agents. Murf BVC keeps the person talking to your agent and
removes everyone and everything else (TV, other people in the room, street and office noise) before
your agent's VAD, turn detection and speech-to-text see the audio. Fewer false interruptions, fewer
transcripts of someone else's words.

It runs on CPU inside your agent process, like LiveKit's own noise-cancellation plugin. No GPU and no
extra services; audio never leaves your process.

## Requirements

- Python 3.10+ and `livekit-agents` 1.2+
- Linux (glibc 2.28+: Ubuntu 20.04+, Debian 10+, RHEL/Rocky/Alma 8+, Amazon Linux 2023) on x86_64 or
  aarch64, or macOS 11+ (Apple Silicon) / 10.15+ (Intel)
- x86_64 CPUs need AVX2 and FMA (Intel Haswell / 2013 or newer, any AMD Zen); on ARM, AWS Graviton2 or
  newer, or any Apple Silicon Mac
- A Murf API key with BVC enabled, or an offline license key for on-premise and air-gapped
  deployments (contact Murf)

## Install

```bash
pip install livekit-plugins-murf-bvc
```

## Quick start

Set `MURF_API_KEY` (the same Murf API key the Murf TTS plugin uses; in `.env.local` like your other
keys), call `murf_bvc.prewarm()` in the server's setup function, and pass `murf_bvc.BVC()` as the noise
cancellation for the room's audio input:

   ```python
   from livekit.agents import Agent, AgentServer, AgentSession, JobContext, JobProcess, cli, room_io
   from livekit.plugins import murf_bvc

   server = AgentServer()


   def prewarm(proc: JobProcess):
       murf_bvc.prewarm()  # license + models, before any call is assigned


   server.setup_fnc = prewarm


   @server.rtc_session()
   async def entrypoint(ctx: JobContext):
       session = AgentSession(...)  # your STT, LLM, TTS, VAD, turn detection
       await session.start(
           room=ctx.room,
           agent=Agent(instructions="You are a helpful assistant."),
           room_options=room_io.RoomOptions(
               audio_input=room_io.AudioInputOptions(noise_cancellation=murf_bvc.BVC()),
           ),
       )


   if __name__ == "__main__":
       cli.run_app(server)
   ```

That's all most agents need. The defaults are what we recommend for production. On-premise or
air-gapped, set `MURF_BVC_LICENSE="mbvc1...."` (an offline license from Murf) instead of
`MURF_API_KEY`.

### Why prewarm

`BVC()` never blocks your agent. Without `prewarm()`, though, the first call in a process waits for
work that `prewarm()` does up front: fetching the first lease for an API key (a request to Murf), and decrypting
and loading the models (about 0.3 s, which would otherwise happen inside LiveKit's audio pipeline when
the first participant's audio arrives). Until the lease arrives, audio passes through unprocessed.

LiveKit runs the setup function once per worker process while the process waits in the idle pool, so
the time it takes is never on a call. It is the same place agents load their VAD model. `prewarm()`
takes the same arguments as `BVC()`, returns `True` when ready, and returns `False` (logging why) if
there is no valid license within `timeout` seconds (default 5); it never raises.

## Keys

Like LiveKit's other plugins, BVC reads its key from the environment unless you pass one:

| Key | Environment variable | How it is checked |
| --- | --- | --- |
| `api_key`: Murf API key | `MURF_API_KEY` | The plugin exchanges it with Murf for a short-lived license (a lease, valid for an hour) and renews every 15 minutes, reporting how many minutes of audio it processed. A key you revoke on Murf stops working within the hour. Needs outbound HTTPS to `api.murf.ai`. |
| `license_key`: offline license (`mbvc1....`) | `MURF_BVC_LICENSE` | Checked locally (signature and expiry); no network access. For on-premise and air-gapped deployments. Takes precedence over an API key. |

If you'd rather not put the key in the environment, pass it in code to both calls, for example
`murf_bvc.prewarm(api_key=key)` and `murf_bvc.BVC(api_key=key)` with the key from your secrets manager.
A key passed in code applies only to that `BVC` value and the streams it creates (a `BVC()` without a
key is not licensed by it), never enters LiveKit's options, and is not inherited by subprocesses or
visible in the process environment, where crash reporters and debugging tools often collect it.

Any code running in your agent process (other plugins included) can read memory it shares with the
agent, so no way of passing a key hides it from them. Treat the BVC key like the agent's other API
keys.

## Options: when to change them

```python
murf_bvc.BVC(api_key=None, license_key=None, restore=True, normalize=True, native=True)
```

| Option | Default | Change it when |
| --- | --- | --- |
| `api_key` | `MURF_API_KEY` | You keep the key out of the environment: see [Keys](#keys). |
| `license_key` | `MURF_BVC_LICENSE` | You run on-premise or air-gapped with an offline license from Murf (or keep it out of the environment). Takes precedence over `api_key`. |
| `restore` | `True` | Leave on. While only the caller is talking, it gives back anything the model removed, so the caller's own voice is never thinned and STT accuracy holds up on clean calls. It runs a speaker check every 0.5 s of speech. Set `False` only if you'd rather have maximum removal at all times, or need to save that bit of CPU. |
| `normalize` | `True` | Leave on. It levels quiet and loud callers before the model so both get the same quality. There is no production reason to turn it off. |
| `native` | `True` | Leave on. It runs BVC inside LiveKit's audio pipeline. It falls back to a Python processor on its own if your `livekit` version predates audio-filter plugins, so you only set `False` when debugging. |

### Rooms with several participants

Nothing to change: one `BVC()` value serves every participant, each with its own independent state.

If you use `native=False` (or `BVCProcessor` directly), each processor handles one track, so pass a
selector instead:

```python
noise_cancellation=lambda params: murf_bvc.BVC(native=False)
```

## What to expect

- **Latency:** 10 ms at 16 kHz, about 13.6 ms at 24 or 48 kHz (resampling included).
- **CPU:** about 3% of one core per active participant on an AWS AMD EPYC instance, plus a speaker
  check every 0.5 s of speech while caller restore is on. Processing is single-threaded per stream, so
  it scales with the number of concurrent calls.
- **Audio:** any sample rate LiveKit delivers (8–96 kHz); stereo input is mixed to mono.

## License behaviour

Without a valid license BVC does not stop your agent: it logs an error and passes audio through
unchanged, delayed by the same few milliseconds, so calls keep working and a license expiring
mid-call never knocks audio out of sync.

With an API key, the first lease is fetched in the background (or by `prewarm()`); calls that start
before it arrives pass audio through, then are processed once it does, without reconnecting. If Murf
cannot be reached, processing continues on the current lease for at least 45 minutes; after it expires
audio passes through until a renewal succeeds, then processing resumes on its own.

Check a key at startup with the result of `prewarm()`:

```python
if not murf_bvc.prewarm():   # the reason is logged
    ...
```

## Troubleshooting

All messages, including those from the native library inside LiveKit's audio pipeline, go to the
Python logger `livekit.plugins.murf_bvc`, so they follow your agent's log level and format (JSON in
production). License events carry `license_sub` and `license_expires_at` as structured fields.

| You see | Meaning |
| --- | --- |
| `no license: pass BVC(api_key=...) or BVC(license_key=...), or set MURF_API_KEY or MURF_BVC_LICENSE` | No key was passed and neither variable is set. |
| `HTTP 401 invalid_api_key ...; lease renewal stopped` | The Murf API key is wrong or revoked. |
| `HTTP 403 not_entitled ...; lease renewal stopped` | The key's Murf account does not include BVC; contact Murf. |
| `lease request failed (...); retrying` | Murf can't be reached (outbound HTTPS blocked, or an outage). Processing continues until the current lease expires. |
| `prewarm: no valid license after 5.0 s` | The first lease did not arrive in time (or the key was refused: see the line before). Audio passes through until it arrives. |
| `offline license for '...' expires in N days` | Ask Murf for a new offline license before that date. |
| `license for '...' has expired` | Renew the key with Murf. |
| `this license does not carry a model key; ask Murf to reissue it` | An older-format key; ask Murf for a reissued one. |
| `Illegal instruction` when the agent starts | The CPU lacks AVX2/FMA (very old x86, or some emulated/virtualized CPUs). Use a newer instance type. |
| `pip` finds no matching distribution | Python older than 3.10, an unsupported platform, or glibc older than 2.28. |
