pipecat-backchannel 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pipecat_backchannel-0.2.0/.gitignore +10 -0
- pipecat_backchannel-0.2.0/CLAUDE.md +86 -0
- pipecat_backchannel-0.2.0/LICENSE +24 -0
- pipecat_backchannel-0.2.0/PKG-INFO +215 -0
- pipecat_backchannel-0.2.0/README.md +194 -0
- pipecat_backchannel-0.2.0/examples/.env.template +14 -0
- pipecat_backchannel-0.2.0/examples/bot.py +150 -0
- pipecat_backchannel-0.2.0/examples/demo-script.md +76 -0
- pipecat_backchannel-0.2.0/examples/voice-test-script.md +194 -0
- pipecat_backchannel-0.2.0/pyproject.toml +70 -0
- pipecat_backchannel-0.2.0/src/pipecat_backchannel/__init__.py +49 -0
- pipecat_backchannel-0.2.0/src/pipecat_backchannel/cache.py +114 -0
- pipecat_backchannel-0.2.0/src/pipecat_backchannel/clips.py +288 -0
- pipecat_backchannel-0.2.0/src/pipecat_backchannel/frames.py +17 -0
- pipecat_backchannel-0.2.0/src/pipecat_backchannel/library.py +165 -0
- pipecat_backchannel-0.2.0/src/pipecat_backchannel/placement.py +112 -0
- pipecat_backchannel-0.2.0/src/pipecat_backchannel/player.py +110 -0
- pipecat_backchannel-0.2.0/src/pipecat_backchannel/plugin.py +160 -0
- pipecat_backchannel-0.2.0/src/pipecat_backchannel/prewarm.py +108 -0
- pipecat_backchannel-0.2.0/src/pipecat_backchannel/processor.py +324 -0
- pipecat_backchannel-0.2.0/src/pipecat_backchannel/py.typed +0 -0
- pipecat_backchannel-0.2.0/src/pipecat_backchannel/recorder.py +258 -0
- pipecat_backchannel-0.2.0/src/pipecat_backchannel/synth.py +45 -0
- pipecat_backchannel-0.2.0/src/pipecat_backchannel/turn.py +89 -0
- pipecat_backchannel-0.2.0/tests/conftest.py +251 -0
- pipecat_backchannel-0.2.0/tests/test_clips.py +296 -0
- pipecat_backchannel-0.2.0/tests/test_gate.py +516 -0
- pipecat_backchannel-0.2.0/tests/test_plugin.py +158 -0
- pipecat_backchannel-0.2.0/tests/test_startup.py +306 -0
- pipecat_backchannel-0.2.0/uv.lock +3171 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# pipecat-backchannel
|
|
2
|
+
|
|
3
|
+
## The idea
|
|
4
|
+
|
|
5
|
+
Human conversation runs on two channels at once. One person holds the floor and talks. The other, without ever taking the floor, keeps feeding back short signals — "mhm", "yeah", "right". These are **backchannels**, and they mean *keep going, I'm with you*.
|
|
6
|
+
|
|
7
|
+
They are not turns. The speaker doesn't stop for them, doesn't answer them, and doesn't remember them as things the listener said. They're a parallel stream of evidence that someone is still on the other end.
|
|
8
|
+
|
|
9
|
+
Voice agents have no such channel. They are silent for as long as you speak, then reply once you stop. From the user's side that silence is indistinguishable from a hung connection, so they compensate: trailing off, repeating themselves, cutting a thought short to check the agent is still there. The conversation stops feeling live.
|
|
10
|
+
|
|
11
|
+
This library adds that missing channel to a voice agent, and nothing else. It is not a feature of the agent's intelligence — the agent never knows it happened. It's a thin behavioral layer that makes the agent *sound* like it's listening while it listens.
|
|
12
|
+
|
|
13
|
+
## Why it's shaped the way it is
|
|
14
|
+
|
|
15
|
+
The problem decomposes into decisions that are genuinely independent, and the architecture's whole job is to keep them that way:
|
|
16
|
+
|
|
17
|
+
- **When to signal.** A timing decision, made from audio alone. This is the hard part and the only one that can ruin the experience.
|
|
18
|
+
- **What to signal.** Which flavor of acknowledgment fits the moment.
|
|
19
|
+
- **Where the sound comes from.** Synthesis, recording, a vendor — an infrastructure concern.
|
|
20
|
+
- **How it reaches the user.** Turning a decision into audio in the pipeline.
|
|
21
|
+
|
|
22
|
+
Collapsing any two of these together is the mistake this design exists to prevent. They change for different reasons, at different rates, under different constraints.
|
|
23
|
+
|
|
24
|
+
Three properties fall out of the domain and are not negotiable:
|
|
25
|
+
|
|
26
|
+
**Timing is a hard deadline, not a goal.** A backchannel is only correct inside a narrow window. Slightly late is worse than never — an acknowledgment that lands after the moment has passed sounds like an interruption. Anything slow must therefore happen ahead of time, never in the moment. Work is precomputed at startup so the live decision is pure and instant.
|
|
27
|
+
|
|
28
|
+
**The decisions differ wildly in stakes.** Getting the timing wrong makes the agent talk over people. Getting the flavor wrong makes it sound very slightly off. These must not be coupled, and the cheap, sloppy decision must never be allowed to influence the expensive, careful one.
|
|
29
|
+
|
|
30
|
+
**Listening is not speaking.** A backchannel must leave no trace in the agent's memory of the conversation. The moment it's recorded as something the agent said, it has become a turn, and the whole premise is gone.
|
|
31
|
+
|
|
32
|
+
Read those three as the acceptance criteria for any change here. A change that's elegant but violates one of them is wrong.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
# Engineering principles
|
|
37
|
+
|
|
38
|
+
## Dependency inversion — the one that matters most
|
|
39
|
+
|
|
40
|
+
**Depend on abstractions, never on concretions.** High-level policy must not know which low-level implementation serves it. If a module names a vendor, a library, or a driver, it is welded to it.
|
|
41
|
+
|
|
42
|
+
**Inject dependencies; never construct them inside.** A component that builds its own collaborators has silently made them mandatory and untestable. Pass them in, with a sensible default where there's an obvious one.
|
|
43
|
+
|
|
44
|
+
This is what buys everything else:
|
|
45
|
+
|
|
46
|
+
- **Swappability.** A new provider is a new implementation, not an edit.
|
|
47
|
+
- **Testability.** Inject a fake and the test is fast, offline, deterministic. If something is hard to test, that's the design telling you it's too coupled — fix the coupling, don't mock harder.
|
|
48
|
+
- **Blast radius.** Concrete details stay at the edges. A change out there doesn't reach the core.
|
|
49
|
+
|
|
50
|
+
The abstraction must be defined by the **consumer's** need, not the implementation's shape. An interface that mirrors one vendor's API is that vendor wearing a hat — the second implementation will not fit it.
|
|
51
|
+
|
|
52
|
+
## Simple first
|
|
53
|
+
|
|
54
|
+
Write the obvious thing. Complexity should arrive because something demanded it, not because you predicted it would. Speculative generality is paid for now, for a benefit that usually never comes.
|
|
55
|
+
|
|
56
|
+
You cannot guess the right abstraction from one use case. Wait for the second. Duplication is cheaper than the wrong abstraction — it's easy to merge later, while a bad abstraction is hard to remove once everything depends on it.
|
|
57
|
+
|
|
58
|
+
## Architecturally correct first
|
|
59
|
+
|
|
60
|
+
Simple is not sloppy. Get the **seams** right early — where things plug together is cheap to place now and expensive to move later. Everything else can be crude and improved in place.
|
|
61
|
+
|
|
62
|
+
A wrong abstraction is worse than none.
|
|
63
|
+
|
|
64
|
+
## SOLID
|
|
65
|
+
|
|
66
|
+
- **S** — one reason to change. If describing a component needs "and", split it.
|
|
67
|
+
- **O** — extend by adding, not by editing. New behavior shouldn't mean touching working code.
|
|
68
|
+
- **L** — a subtype must be usable anywhere its base is, without surprises.
|
|
69
|
+
- **I** — many small interfaces beat one fat one. Don't force a caller to depend on methods it ignores.
|
|
70
|
+
- **D** — see above.
|
|
71
|
+
|
|
72
|
+
## DRY
|
|
73
|
+
|
|
74
|
+
Deduplicate **knowledge, not text.** Two pieces of code that look alike but change for different reasons are not duplicates — merging them couples them forever. One fact should have one home; that's the point.
|
|
75
|
+
|
|
76
|
+
## Abstraction
|
|
77
|
+
|
|
78
|
+
An abstraction earns its place by hiding something the caller genuinely shouldn't know. If it only forwards calls, delete it — it's a layer, not an abstraction.
|
|
79
|
+
|
|
80
|
+
Name things for what they mean, not how they work. The name is the abstraction; if you can't name it clearly, the boundary is wrong.
|
|
81
|
+
|
|
82
|
+
## KISS
|
|
83
|
+
|
|
84
|
+
The reader is the constraint. Code is read far more than written, and the next reader has none of your context. Clever costs more than it saves.
|
|
85
|
+
|
|
86
|
+
Prefer boring, obvious, explicit. Fewer moving parts, fewer states, fewer ways to hold it wrong.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Taras Maister
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
19
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
20
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
23
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
24
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pipecat-backchannel
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Make voice AI conversations feel live — Pipecat agents that listen back with natural "mhm"s and "yeah"s while the user is still talking.
|
|
5
|
+
Project-URL: Homepage, https://github.com/maisterr/pipecat-backchannel
|
|
6
|
+
Project-URL: Repository, https://github.com/maisterr/pipecat-backchannel
|
|
7
|
+
Project-URL: Issues, https://github.com/maisterr/pipecat-backchannel/issues
|
|
8
|
+
Author: Taras Maister
|
|
9
|
+
License-Expression: BSD-2-Clause
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: backchannel,conversational-ai,pipecat,turn-taking,voice-ai
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: pipecat-ai<2,>=1.6.0
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
<div align="center">
|
|
23
|
+
<a href="https://github.com/pipecat-ai/pipecat">
|
|
24
|
+
<img alt="Pipecat" width="220px" height="auto" src="https://raw.githubusercontent.com/pipecat-ai/pipecat/main/pipecat.png">
|
|
25
|
+
</a>
|
|
26
|
+
<p><em>A community integration for <a href="https://github.com/pipecat-ai/pipecat">Pipecat</a></em></p>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
# pipecat-backchannel
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/pipecat-backchannel)
|
|
32
|
+
[](https://pypi.org/project/pipecat-backchannel)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
[](https://github.com/pipecat-ai/pipecat)
|
|
35
|
+
|
|
36
|
+
Your Pipecat agent listens back: quiet "mhm"s and "yeah"s while the user is
|
|
37
|
+
still talking.
|
|
38
|
+
|
|
39
|
+
## Why
|
|
40
|
+
|
|
41
|
+
Listen to two people on a phone call. While one tells a story, the other keeps
|
|
42
|
+
feeding back small signals: "mhm", "yeah". Linguists call these
|
|
43
|
+
**backchannels**. They mean _keep going, I'm with you_, and the speaker talks
|
|
44
|
+
straight through them.
|
|
45
|
+
|
|
46
|
+
Talk to a voice agent and you get silence until you finish. Silence on a call
|
|
47
|
+
reads as a dropped connection, so you hesitate, trail off, or ask "hello?".
|
|
48
|
+
|
|
49
|
+
This library adds the listening sounds. The agent keeps the floor with you the
|
|
50
|
+
whole time: a backchannel takes no turn and leaves no trace in your LLM
|
|
51
|
+
context.
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
uv add pipecat-backchannel
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from pipecat_backchannel import Backchannel
|
|
63
|
+
|
|
64
|
+
backchannel = Backchannel()
|
|
65
|
+
|
|
66
|
+
pipeline = Pipeline(backchannel([
|
|
67
|
+
transport.input(),
|
|
68
|
+
stt,
|
|
69
|
+
context_aggregator.user(),
|
|
70
|
+
llm,
|
|
71
|
+
tts,
|
|
72
|
+
transport.output(),
|
|
73
|
+
context_aggregator.assistant(),
|
|
74
|
+
]))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
One wrapper call is the whole setup. The default configuration works without
|
|
78
|
+
an API key, a voice ID, or a sample rate, and places every processor for you.
|
|
79
|
+
|
|
80
|
+
The first run records the clips through **your own TTS**, so they come out in
|
|
81
|
+
your bot's voice, and caches them in `.clip_cache/`. Every later run starts
|
|
82
|
+
from the cache. Delete that directory after changing voice.
|
|
83
|
+
|
|
84
|
+
## Record before the first call
|
|
85
|
+
|
|
86
|
+
The first session waits a few seconds while the recorder fills the clip cache.
|
|
87
|
+
Move that wait to app startup if you want to avoid delay for first caller:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
backchannel = Backchannel() # one per process
|
|
91
|
+
|
|
92
|
+
async def run_bot(...):
|
|
93
|
+
pipeline = Pipeline(backchannel([... tts ...]))
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
asyncio.run(backchannel.prewarm(tts=make_tts())) # before the server opens
|
|
97
|
+
main()
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Tuning
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
Backchannel(
|
|
104
|
+
volume=0.6, # how loud, relative to the recording
|
|
105
|
+
params=BackchannelParams(
|
|
106
|
+
fire_probability=0.8, # chance of speaking up at a good moment
|
|
107
|
+
cooldown_s=2.5, # minimum gap between backchannels
|
|
108
|
+
min_speech_before_eligible_s=0.7, # ignore pauses right after a turn starts
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`fire_probability` and `cooldown_s` set how present the listener feels. Raise
|
|
114
|
+
the first and lower the second for a more active one. Keep some slack: a bot
|
|
115
|
+
that reacts to each eligible pause sounds mechanical, because human listeners
|
|
116
|
+
skip some of them.
|
|
117
|
+
|
|
118
|
+
`volume` sits below 1.0 by default so clips play under the user's voice.
|
|
119
|
+
|
|
120
|
+
Set `loguru` to `DEBUG` to see each fire/skip decision and its reason.
|
|
121
|
+
|
|
122
|
+
### Going quiet mid-session
|
|
123
|
+
|
|
124
|
+
Some stretches of a call want a silent listener: the user dictating a card
|
|
125
|
+
number, or the bot walking through a form where each pause belongs to a
|
|
126
|
+
field, and an "mhm" would read as confirmation.
|
|
127
|
+
|
|
128
|
+
The component that decides when to fire is the `BackchannelProcessor`, one
|
|
129
|
+
per pipeline. `backchannel([...])` returns your processor list with it
|
|
130
|
+
inserted, so keep that list, build the `Pipeline` from it, and pull the gate
|
|
131
|
+
out of it:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from pipecat_backchannel.processor import BackchannelProcessor
|
|
135
|
+
|
|
136
|
+
processors = backchannel([transport.input(), stt, llm, tts, transport.output()])
|
|
137
|
+
pipeline = Pipeline(processors)
|
|
138
|
+
|
|
139
|
+
gate = next(p for p in processors if isinstance(p, BackchannelProcessor))
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Then flip it from any event handler, as many times as the call needs:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
gate.enabled = False # stop firing; every frame still passes through
|
|
146
|
+
...
|
|
147
|
+
gate.enabled = True # resume
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`enabled` is a plain attribute, safe to set at any moment. While it is
|
|
151
|
+
`False` the processor keeps passing audio through and keeps tracking speech,
|
|
152
|
+
so nothing downstream notices and re-enabling picks up mid-turn. It fires no
|
|
153
|
+
clips and runs no end-of-turn classification in that state. The pipeline, the
|
|
154
|
+
LLM context, and the other sessions' gates stay untouched.
|
|
155
|
+
|
|
156
|
+
## Custom clips
|
|
157
|
+
|
|
158
|
+
The library groups clips by conversational function: continuer, agreement,
|
|
159
|
+
hesitation, surprise.
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
Backchannel(clip_groups={"continue": ["Mhm.", "Right."], "affirm": ["Yeah.", "Yep."]})
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Two rules for a custom inventory:
|
|
166
|
+
|
|
167
|
+
- **Give each group at least two clips.** A one-clip group repeats itself.
|
|
168
|
+
- **Get variety from punctuation, and keep the vocabulary small.** Real
|
|
169
|
+
backchannels draw on a handful of sounds: _uh-huh, yeah, mm-hmm, right,
|
|
170
|
+
okay, oh, huh, hm_. A written phrase like "absolutely" reads fine on the
|
|
171
|
+
page, then sounds like an answer when spoken over someone mid-sentence.
|
|
172
|
+
Punctuation is the handle your TTS gives you: `"Mhm."` falls, `"Mhm,"` stays
|
|
173
|
+
up, `"Hm..."` draws out.
|
|
174
|
+
|
|
175
|
+
Pass `synthesizer=` to use a different voice, or `cache=` to supply your own
|
|
176
|
+
recorded `.wav` files. See `pipecat_backchannel.synth` and `.cache`.
|
|
177
|
+
|
|
178
|
+
## Run the example
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
cp examples/.env.template examples/.env # DEEPGRAM_API_KEY, CARTESIA_API_KEY, CEREBRAS_API_KEY
|
|
182
|
+
uv run examples/bot.py
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Open `http://localhost:7860/client`, talk in mid-length sentences, and pause
|
|
186
|
+
mid-thought. A quiet "mhm" lands on clause-complete pauses. Word-search pauses
|
|
187
|
+
("we need the, uh...") and finished questions get silence, and the questions
|
|
188
|
+
get a real reply.
|
|
189
|
+
|
|
190
|
+
## Limitations
|
|
191
|
+
|
|
192
|
+
- **Pauses only.** Humans backchannel during speech too, cued by pitch. This
|
|
193
|
+
library reacts to pauses. The roadmap covers the rest.
|
|
194
|
+
- **About 2× inference while the user speaks.** It runs a second VAD and
|
|
195
|
+
turn-detector on purpose: the pipeline's copies are tuned for turn-taking,
|
|
196
|
+
and pause timing needs the opposite settings. A handful of concurrent
|
|
197
|
+
sessions runs fine; measure before a large deployment.
|
|
198
|
+
- **English only.** The clips and the regexes are English. The underlying
|
|
199
|
+
models handle other languages.
|
|
200
|
+
|
|
201
|
+
## Roadmap
|
|
202
|
+
|
|
203
|
+
- [ ] **Fire during speech.** Real listeners backchannel mid-utterance, cued
|
|
204
|
+
by pitch and rhythm rather than silence. That takes Voice Activity
|
|
205
|
+
Projection; [MaAI](https://github.com/MaAI-Kyoto/MaAI) ships a usable
|
|
206
|
+
model. It reads a different signal, so it slots in beside the pause
|
|
207
|
+
gate rather than replacing it, and it is the largest quality jump left.
|
|
208
|
+
- [ ] **Match the speaker's prosody.** The selector picks a clip at random
|
|
209
|
+
within its group. Picking the one whose pitch and energy fit the last
|
|
210
|
+
utterance adds no runtime cost, because the clips already sit on disk.
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
BSD 2-Clause © Taras Maister, matching Pipecat's own license. Not affiliated
|
|
215
|
+
with Daily or the Pipecat core team.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://github.com/pipecat-ai/pipecat">
|
|
3
|
+
<img alt="Pipecat" width="220px" height="auto" src="https://raw.githubusercontent.com/pipecat-ai/pipecat/main/pipecat.png">
|
|
4
|
+
</a>
|
|
5
|
+
<p><em>A community integration for <a href="https://github.com/pipecat-ai/pipecat">Pipecat</a></em></p>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
# pipecat-backchannel
|
|
9
|
+
|
|
10
|
+
[](https://pypi.org/project/pipecat-backchannel)
|
|
11
|
+
[](https://pypi.org/project/pipecat-backchannel)
|
|
12
|
+
[](LICENSE)
|
|
13
|
+
[](https://github.com/pipecat-ai/pipecat)
|
|
14
|
+
|
|
15
|
+
Your Pipecat agent listens back: quiet "mhm"s and "yeah"s while the user is
|
|
16
|
+
still talking.
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
Listen to two people on a phone call. While one tells a story, the other keeps
|
|
21
|
+
feeding back small signals: "mhm", "yeah". Linguists call these
|
|
22
|
+
**backchannels**. They mean _keep going, I'm with you_, and the speaker talks
|
|
23
|
+
straight through them.
|
|
24
|
+
|
|
25
|
+
Talk to a voice agent and you get silence until you finish. Silence on a call
|
|
26
|
+
reads as a dropped connection, so you hesitate, trail off, or ask "hello?".
|
|
27
|
+
|
|
28
|
+
This library adds the listening sounds. The agent keeps the floor with you the
|
|
29
|
+
whole time: a backchannel takes no turn and leaves no trace in your LLM
|
|
30
|
+
context.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv add pipecat-backchannel
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from pipecat_backchannel import Backchannel
|
|
42
|
+
|
|
43
|
+
backchannel = Backchannel()
|
|
44
|
+
|
|
45
|
+
pipeline = Pipeline(backchannel([
|
|
46
|
+
transport.input(),
|
|
47
|
+
stt,
|
|
48
|
+
context_aggregator.user(),
|
|
49
|
+
llm,
|
|
50
|
+
tts,
|
|
51
|
+
transport.output(),
|
|
52
|
+
context_aggregator.assistant(),
|
|
53
|
+
]))
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
One wrapper call is the whole setup. The default configuration works without
|
|
57
|
+
an API key, a voice ID, or a sample rate, and places every processor for you.
|
|
58
|
+
|
|
59
|
+
The first run records the clips through **your own TTS**, so they come out in
|
|
60
|
+
your bot's voice, and caches them in `.clip_cache/`. Every later run starts
|
|
61
|
+
from the cache. Delete that directory after changing voice.
|
|
62
|
+
|
|
63
|
+
## Record before the first call
|
|
64
|
+
|
|
65
|
+
The first session waits a few seconds while the recorder fills the clip cache.
|
|
66
|
+
Move that wait to app startup if you want to avoid delay for first caller:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
backchannel = Backchannel() # one per process
|
|
70
|
+
|
|
71
|
+
async def run_bot(...):
|
|
72
|
+
pipeline = Pipeline(backchannel([... tts ...]))
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
asyncio.run(backchannel.prewarm(tts=make_tts())) # before the server opens
|
|
76
|
+
main()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Tuning
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
Backchannel(
|
|
83
|
+
volume=0.6, # how loud, relative to the recording
|
|
84
|
+
params=BackchannelParams(
|
|
85
|
+
fire_probability=0.8, # chance of speaking up at a good moment
|
|
86
|
+
cooldown_s=2.5, # minimum gap between backchannels
|
|
87
|
+
min_speech_before_eligible_s=0.7, # ignore pauses right after a turn starts
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`fire_probability` and `cooldown_s` set how present the listener feels. Raise
|
|
93
|
+
the first and lower the second for a more active one. Keep some slack: a bot
|
|
94
|
+
that reacts to each eligible pause sounds mechanical, because human listeners
|
|
95
|
+
skip some of them.
|
|
96
|
+
|
|
97
|
+
`volume` sits below 1.0 by default so clips play under the user's voice.
|
|
98
|
+
|
|
99
|
+
Set `loguru` to `DEBUG` to see each fire/skip decision and its reason.
|
|
100
|
+
|
|
101
|
+
### Going quiet mid-session
|
|
102
|
+
|
|
103
|
+
Some stretches of a call want a silent listener: the user dictating a card
|
|
104
|
+
number, or the bot walking through a form where each pause belongs to a
|
|
105
|
+
field, and an "mhm" would read as confirmation.
|
|
106
|
+
|
|
107
|
+
The component that decides when to fire is the `BackchannelProcessor`, one
|
|
108
|
+
per pipeline. `backchannel([...])` returns your processor list with it
|
|
109
|
+
inserted, so keep that list, build the `Pipeline` from it, and pull the gate
|
|
110
|
+
out of it:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from pipecat_backchannel.processor import BackchannelProcessor
|
|
114
|
+
|
|
115
|
+
processors = backchannel([transport.input(), stt, llm, tts, transport.output()])
|
|
116
|
+
pipeline = Pipeline(processors)
|
|
117
|
+
|
|
118
|
+
gate = next(p for p in processors if isinstance(p, BackchannelProcessor))
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Then flip it from any event handler, as many times as the call needs:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
gate.enabled = False # stop firing; every frame still passes through
|
|
125
|
+
...
|
|
126
|
+
gate.enabled = True # resume
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`enabled` is a plain attribute, safe to set at any moment. While it is
|
|
130
|
+
`False` the processor keeps passing audio through and keeps tracking speech,
|
|
131
|
+
so nothing downstream notices and re-enabling picks up mid-turn. It fires no
|
|
132
|
+
clips and runs no end-of-turn classification in that state. The pipeline, the
|
|
133
|
+
LLM context, and the other sessions' gates stay untouched.
|
|
134
|
+
|
|
135
|
+
## Custom clips
|
|
136
|
+
|
|
137
|
+
The library groups clips by conversational function: continuer, agreement,
|
|
138
|
+
hesitation, surprise.
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
Backchannel(clip_groups={"continue": ["Mhm.", "Right."], "affirm": ["Yeah.", "Yep."]})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Two rules for a custom inventory:
|
|
145
|
+
|
|
146
|
+
- **Give each group at least two clips.** A one-clip group repeats itself.
|
|
147
|
+
- **Get variety from punctuation, and keep the vocabulary small.** Real
|
|
148
|
+
backchannels draw on a handful of sounds: _uh-huh, yeah, mm-hmm, right,
|
|
149
|
+
okay, oh, huh, hm_. A written phrase like "absolutely" reads fine on the
|
|
150
|
+
page, then sounds like an answer when spoken over someone mid-sentence.
|
|
151
|
+
Punctuation is the handle your TTS gives you: `"Mhm."` falls, `"Mhm,"` stays
|
|
152
|
+
up, `"Hm..."` draws out.
|
|
153
|
+
|
|
154
|
+
Pass `synthesizer=` to use a different voice, or `cache=` to supply your own
|
|
155
|
+
recorded `.wav` files. See `pipecat_backchannel.synth` and `.cache`.
|
|
156
|
+
|
|
157
|
+
## Run the example
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
cp examples/.env.template examples/.env # DEEPGRAM_API_KEY, CARTESIA_API_KEY, CEREBRAS_API_KEY
|
|
161
|
+
uv run examples/bot.py
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Open `http://localhost:7860/client`, talk in mid-length sentences, and pause
|
|
165
|
+
mid-thought. A quiet "mhm" lands on clause-complete pauses. Word-search pauses
|
|
166
|
+
("we need the, uh...") and finished questions get silence, and the questions
|
|
167
|
+
get a real reply.
|
|
168
|
+
|
|
169
|
+
## Limitations
|
|
170
|
+
|
|
171
|
+
- **Pauses only.** Humans backchannel during speech too, cued by pitch. This
|
|
172
|
+
library reacts to pauses. The roadmap covers the rest.
|
|
173
|
+
- **About 2× inference while the user speaks.** It runs a second VAD and
|
|
174
|
+
turn-detector on purpose: the pipeline's copies are tuned for turn-taking,
|
|
175
|
+
and pause timing needs the opposite settings. A handful of concurrent
|
|
176
|
+
sessions runs fine; measure before a large deployment.
|
|
177
|
+
- **English only.** The clips and the regexes are English. The underlying
|
|
178
|
+
models handle other languages.
|
|
179
|
+
|
|
180
|
+
## Roadmap
|
|
181
|
+
|
|
182
|
+
- [ ] **Fire during speech.** Real listeners backchannel mid-utterance, cued
|
|
183
|
+
by pitch and rhythm rather than silence. That takes Voice Activity
|
|
184
|
+
Projection; [MaAI](https://github.com/MaAI-Kyoto/MaAI) ships a usable
|
|
185
|
+
model. It reads a different signal, so it slots in beside the pause
|
|
186
|
+
gate rather than replacing it, and it is the largest quality jump left.
|
|
187
|
+
- [ ] **Match the speaker's prosody.** The selector picks a clip at random
|
|
188
|
+
within its group. Picking the one whose pitch and energy fit the last
|
|
189
|
+
utterance adds no runtime cost, because the clips already sit on disk.
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
BSD 2-Clause © Taras Maister, matching Pipecat's own license. Not affiliated
|
|
194
|
+
with Daily or the Pipecat core team.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
DEEPGRAM_API_KEY=your_deepgram_api_key_here
|
|
2
|
+
CARTESIA_API_KEY=your_cartesia_api_key_here
|
|
3
|
+
CARTESIA_VOICE_ID=71a7ad14-091c-4e8e-a314-022ece01c121
|
|
4
|
+
CEREBRAS_API_KEY=your_cerebras_api_key_here
|
|
5
|
+
CEREBRAS_MODEL=gemma-4-31b
|
|
6
|
+
|
|
7
|
+
# Daily WebRTC room (leave unset to run in local "webrtc" test mode instead)
|
|
8
|
+
DAILY_API_KEY=your_daily_api_key_here
|
|
9
|
+
|
|
10
|
+
# Main-pipeline VAD start_secs (barge-in/interruption sensitivity). Higher =
|
|
11
|
+
# fewer spurious interruptions from brief sounds, slower to react to real
|
|
12
|
+
# barge-in. Independent of backchannel pause timing, which BackchannelProcessor
|
|
13
|
+
# runs its own VAD for (BackchannelParams.vad_start_secs, default 0.2).
|
|
14
|
+
MAIN_VAD_START_SECS=0.4
|