Metadata-Version: 2.5
Name: livekit-plugins-murf-bvc
Version: 0.2.1
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

Pass `murf_bvc.BVC(...)` with your key as the noise cancellation for the room's audio input, and call
`murf_bvc.prewarm(...)` with the same key in the worker's prewarm function:

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

   MURF_API_KEY = load_murf_api_key()  # from your secrets manager


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


   async def entrypoint(ctx: JobContext):
       session = AgentSession(...)  # your STT, LLM, TTS, VAD, turn detection
       bvc = murf_bvc.BVC(api_key=MURF_API_KEY)

       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=bvc),
           ),
       )


   if __name__ == "__main__":
       cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint, prewarm_fnc=prewarm))
   ```

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

### Why prewarm

`BVC()` never blocks your agent. On its own, 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 prewarm 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

Pass the key in code, as above, rather than through the environment. The key then applies only to that
`BVC` value and the streams it creates, 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. A `BVC()` created without a key is not licensed by another one's key.

| Key | How it is checked |
| --- | --- |
| `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....`) | Checked locally (signature and expiry); no network access. For on-premise and air-gapped deployments. |

Without a key argument, `BVC()` falls back to the environment: `MURF_BVC_LICENSE`, then `MURF_API_KEY`.

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` | Pass it: see [Keys](#keys). |
| `license_key` | `MURF_BVC_LICENSE` | You run on-premise or air-gapped with an offline license from Murf. 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(api_key=key):   # or license_key=...; the reason is logged
    ...
```

## Troubleshooting

| You see | Meaning |
| --- | --- |
| `murf_bvc: no license: pass BVC(license_key=...) or BVC(api_key=...)` | No key was passed and neither `MURF_BVC_LICENSE` nor `MURF_API_KEY` is set. |
| `murf_bvc: HTTP 401 invalid_api_key ...; lease renewal stopped` | The Murf API key is wrong or revoked. |
| `murf_bvc: HTTP 403 not_entitled ...; lease renewal stopped` | The key's Murf account does not include BVC; contact Murf. |
| `murf_bvc: lease request failed (...); retrying` | Murf can't be reached (outbound HTTPS blocked, or an outage). Processing continues until the current lease expires. |
| `murf_bvc: 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. |
| `murf_bvc: license for '...' has expired` | Renew the key with Murf. |
| `this license does not carry a model key; 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. |
