memcat 1.0.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.
- memcat-1.0.0/LICENSE +24 -0
- memcat-1.0.0/PKG-INFO +198 -0
- memcat-1.0.0/README.md +157 -0
- memcat-1.0.0/pyproject.toml +155 -0
- memcat-1.0.0/pyproject.toml.orig +116 -0
- memcat-1.0.0/src/memcat/__init__.py +46 -0
- memcat-1.0.0/src/memcat/cli.py +608 -0
- memcat-1.0.0/src/memcat/config.py +120 -0
- memcat-1.0.0/src/memcat/core/__init__.py +0 -0
- memcat-1.0.0/src/memcat/core/audio.py +247 -0
- memcat-1.0.0/src/memcat/core/breaker.py +113 -0
- memcat-1.0.0/src/memcat/core/chunker.py +505 -0
- memcat-1.0.0/src/memcat/core/codec.py +310 -0
- memcat-1.0.0/src/memcat/core/index.py +361 -0
- memcat-1.0.0/src/memcat/core/keys.py +406 -0
- memcat-1.0.0/src/memcat/core/lookup.py +215 -0
- memcat-1.0.0/src/memcat/core/metrics.py +89 -0
- memcat-1.0.0/src/memcat/core/pcm.py +24 -0
- memcat-1.0.0/src/memcat/core/runtime.py +252 -0
- memcat-1.0.0/src/memcat/core/store/__init__.py +0 -0
- memcat-1.0.0/src/memcat/core/store/base.py +94 -0
- memcat-1.0.0/src/memcat/core/store/memory.py +233 -0
- memcat-1.0.0/src/memcat/core/store/redis.py +456 -0
- memcat-1.0.0/src/memcat/core/store/s3.py +527 -0
- memcat-1.0.0/src/memcat/core/worker.py +577 -0
- memcat-1.0.0/src/memcat/pipecat/__init__.py +0 -0
- memcat-1.0.0/src/memcat/pipecat/adapters/__init__.py +0 -0
- memcat-1.0.0/src/memcat/pipecat/adapters/base.py +172 -0
- memcat-1.0.0/src/memcat/pipecat/adapters/providers.py +190 -0
- memcat-1.0.0/src/memcat/pipecat/compat.py +161 -0
- memcat-1.0.0/src/memcat/pipecat/contexts.py +94 -0
- memcat-1.0.0/src/memcat/pipecat/mixin.py +1614 -0
- memcat-1.0.0/src/memcat/pipecat/render.py +331 -0
- memcat-1.0.0/src/memcat/pipecat/wrap.py +36 -0
- memcat-1.0.0/src/memcat/py.typed +0 -0
- memcat-1.0.0/src/memcat/serve.py +562 -0
memcat-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Nous Labs
|
|
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.
|
memcat-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: memcat
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Clause-level text-to-speech cache for Pipecat voice bots.
|
|
5
|
+
Keywords: pipecat,tts,text-to-speech,cache,voice,voice-ai,telephony
|
|
6
|
+
Author: Umesh Yadav
|
|
7
|
+
Author-email: Umesh Yadav <git@umesh.dev>
|
|
8
|
+
License-Expression: BSD-2-Clause
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Dist: pipecat-ai>=1.10,<1.14
|
|
21
|
+
Requires-Dist: aiohttp>=3.11.12
|
|
22
|
+
Requires-Dist: audioop-lts>=0.2.1 ; python_full_version >= '3.13'
|
|
23
|
+
Requires-Dist: loguru>=0.7.3
|
|
24
|
+
Requires-Dist: numpy>=1.26.4
|
|
25
|
+
Requires-Dist: soxr>=1.0.0
|
|
26
|
+
Requires-Dist: memcat[redis,opus,s3] ; extra == 'all'
|
|
27
|
+
Requires-Dist: av>=14 ; extra == 'opus'
|
|
28
|
+
Requires-Dist: redis>=5.0.1 ; extra == 'redis'
|
|
29
|
+
Requires-Dist: boto3>=1.34 ; extra == 's3'
|
|
30
|
+
Requires-Python: >=3.11
|
|
31
|
+
Project-URL: Homepage, https://github.com/imumesh18/memcat
|
|
32
|
+
Project-URL: Source, https://github.com/imumesh18/memcat
|
|
33
|
+
Project-URL: Documentation, https://github.com/imumesh18/memcat/tree/main/docs
|
|
34
|
+
Project-URL: Changelog, https://github.com/imumesh18/memcat/blob/main/CHANGELOG.md
|
|
35
|
+
Project-URL: Issues, https://github.com/imumesh18/memcat/issues
|
|
36
|
+
Provides-Extra: all
|
|
37
|
+
Provides-Extra: opus
|
|
38
|
+
Provides-Extra: redis
|
|
39
|
+
Provides-Extra: s3
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# Memcat
|
|
43
|
+
|
|
44
|
+
**A text-to-speech cache for [Pipecat](https://github.com/pipecat-ai/pipecat) voice bots.**
|
|
45
|
+
When your agent says a line it has said before (or part of one), Memcat replays the
|
|
46
|
+
stored audio instead of paying the provider to synthesize it again, and the audio
|
|
47
|
+
starts in milliseconds.
|
|
48
|
+
|
|
49
|
+
| | Measured |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| Speech served from cache, scripted support calls ([Harper Valley Bank](https://github.com/cricketclub/gridspace-stanford-harper-valley), 1,446 calls) | **54.0% of characters, 59.0% of responses** |
|
|
52
|
+
| Same, typed support chats ([ABCD](https://arxiv.org/abs/2104.00783), 10,042 chats) | 32.4% of characters, 38.5% of responses |
|
|
53
|
+
| First audio of a sentence that opens with a cached clause, recorded ElevenLabs traffic | **2–13 ms** with Memcat vs 346 ms without (median, HTTP and WebSocket) |
|
|
54
|
+
| What a listener hears, recorded ElevenLabs traffic, cached and live clauses mixed | the same words in the same order: 0.3% word error vs 0.0% without Memcat, by speech recognition |
|
|
55
|
+
| First audio on a hit, local Kokoro voice | 1.0 ms from memory, 10 ms from Redis (vs 745 ms live) |
|
|
56
|
+
| Added time on a miss | none measurable: 488 ms vs 495 ms without Memcat (median, within run-to-run noise) |
|
|
57
|
+
| Storage per cached minute | 464 KB at 8 kHz (mu-law), 236 KB at 24 kHz (Opus) |
|
|
58
|
+
| Cache keys containing a digit (order numbers, prices, dates, phone numbers) | **0** on all corpora |
|
|
59
|
+
| Redis killed mid-call | every turn spoken, no error reaches the pipeline |
|
|
60
|
+
|
|
61
|
+
Hit rates come from replaying public dialogue corpora through Memcat's own chunker,
|
|
62
|
+
privacy rules and admission policy (`bench/hitrate.py`); no TTS was called. Net of
|
|
63
|
+
what filling the cache costs, savings are within 2 points of these (54.0% and 31.9%).
|
|
64
|
+
LLM agents whose every sentence carries an order number or a price cache less: 6–17%
|
|
65
|
+
of characters on [τ²-bench](https://github.com/sierra-research/tau2-bench) traces,
|
|
66
|
+
where those clauses stay live. The ElevenLabs rows replay traffic recorded from the
|
|
67
|
+
ElevenLabs API (Flash v2.5) through Pipecat's ElevenLabs services
|
|
68
|
+
(`bench/elevenlabs_replay.py`). The recordings are not in the repository; anyone with
|
|
69
|
+
an ElevenLabs key can make their own and rerun it.
|
|
70
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uv add memcat # or: pip install memcat
|
|
75
|
+
uv add "memcat[redis,opus]" # shared cache across bot processes, compact audio
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Python 3.11–3.13 and `pipecat-ai` 1.10–1.13. Extras: `redis` (`RedisStore`), `opus`
|
|
79
|
+
(Opus storage), `s3` (`S3Store`), `all`.
|
|
80
|
+
|
|
81
|
+
## Use
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from pipecat.services.cartesia.tts import CartesiaTTSService
|
|
85
|
+
from memcat import cached, MemcatConfig, RedisStore
|
|
86
|
+
|
|
87
|
+
CachedCartesia = cached(CartesiaTTSService) # once, at import time
|
|
88
|
+
|
|
89
|
+
tts = CachedCartesia(
|
|
90
|
+
api_key=CARTESIA_API_KEY,
|
|
91
|
+
voice_id=VOICE_ID,
|
|
92
|
+
memcat=MemcatConfig(store=RedisStore(url=REDIS_URL), namespace="acme-support"),
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Use `tts` exactly as you used the unwrapped service. Nothing else in the pipeline
|
|
97
|
+
changes. `await tts.memcat_stats()` reports hits, characters saved and dollars saved.
|
|
98
|
+
|
|
99
|
+
No API keys at hand? From a clone, `uv run examples/kokoro_local.py` speaks three lines twice
|
|
100
|
+
with a local voice; the second time they come from the cache.
|
|
101
|
+
|
|
102
|
+
## How it works
|
|
103
|
+
|
|
104
|
+
1. **Clauses, not just sentences.** Each sentence Pipecat hands to the TTS is split
|
|
105
|
+
at commas, semicolons, colons and dashes. "Thanks for waiting, your order 48213
|
|
106
|
+
has shipped, and it should arrive soon." is three clauses; the first and last are
|
|
107
|
+
reusable, the middle one is spoken live.
|
|
108
|
+
2. **Privacy first.** A clause with any digit, a number in words, an email, a URL,
|
|
109
|
+
money, a date, a time or a spelled-out code is never cached. A clause is cached
|
|
110
|
+
only after it has been heard in 3 distinct calls, so a line said in one call never
|
|
111
|
+
enters the cache.
|
|
112
|
+
3. **One lookup per sentence.** An in-process index answers misses with no network
|
|
113
|
+
call; hits come from memory or one Redis `MGET`.
|
|
114
|
+
4. **Exact order on every provider.** On HTTP services cached and live audio
|
|
115
|
+
interleave inside one generator. On WebSocket services Memcat keeps Pipecat's
|
|
116
|
+
audio context and gives each run of live clauses its own provider stream,
|
|
117
|
+
releasing segments strictly in order, so cached clauses play before, between and
|
|
118
|
+
after live ones. Transcripts and word timestamps stay correct.
|
|
119
|
+
5. **Joins matched to live speech.** Clips are stored tight and Memcat sets the pause
|
|
120
|
+
at each join the way live speech does (short after "Sure,", longer after a full
|
|
121
|
+
clause), matches loudness, and fades edges. Against live synthesis of the same
|
|
122
|
+
sentences, pauses differ by 45 ms at the median and loudness by at most 0.62 LU
|
|
123
|
+
(`bench/abx.py`).
|
|
124
|
+
6. **Fill in the background.** A clause admitted from a live run that synthesized
|
|
125
|
+
it alone is stored as is, for free; others are rendered once by a background
|
|
126
|
+
copy of the same service. Nothing on the call ever waits for the cache.
|
|
127
|
+
|
|
128
|
+
If the store, a codec or Memcat itself fails, the sentence goes to the provider as
|
|
129
|
+
if Memcat were not installed: Memcat can lose savings, but the call goes on.
|
|
130
|
+
|
|
131
|
+
## Providers
|
|
132
|
+
|
|
133
|
+
| Provider | WebSocket | HTTP |
|
|
134
|
+
| --- | --- | --- |
|
|
135
|
+
| Cartesia | cached clauses anywhere in a sentence | cached clauses anywhere |
|
|
136
|
+
| ElevenLabs | cached clauses anywhere | cached clauses anywhere, `previous_text` kept |
|
|
137
|
+
| Deepgram Aura | cached clauses anywhere, within Deepgram's 20-flush/minute limit | cached clauses anywhere |
|
|
138
|
+
| Sarvam | cached clauses before the first live one | cached clauses anywhere |
|
|
139
|
+
| Any other Pipecat TTS service | cached clauses before the first live one (if it pushes its own start frames) | cached clauses anywhere (if it returns audio inline) |
|
|
140
|
+
|
|
141
|
+
Every listed provider is tested with its Pipecat service class against a local
|
|
142
|
+
emulator of the provider's wire protocol (`tests/emulators/`). ElevenLabs is also
|
|
143
|
+
tested against recordings of its API, replayed with their original timing
|
|
144
|
+
(`tests/test_replay_elevenlabs.py`). To record your own, see
|
|
145
|
+
[CONTRIBUTING.md](https://github.com/imumesh18/memcat/blob/main/CONTRIBUTING.md).
|
|
146
|
+
|
|
147
|
+
## Configure
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
MemcatConfig(
|
|
151
|
+
store=RedisStore("redis://..."), # default: one MemoryStore per process; RedisStore, S3Store
|
|
152
|
+
namespace="acme-support", # tenant prefix on every key
|
|
153
|
+
admit_after_calls=3, # distinct calls before a clause is cached
|
|
154
|
+
warm=["Thank you for calling ABC Shop, how can I help you today?"], # cached at start
|
|
155
|
+
block_patterns=[r"\bACME-\w+"], # extra text that must never be cached
|
|
156
|
+
ttl_s=30 * 24 * 3600, # refreshed when served
|
|
157
|
+
codec="auto", # mu-law at 8 kHz, Opus when installed, else PCM16
|
|
158
|
+
enabled=True, # kill switch: False means a pure pass-through
|
|
159
|
+
)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
All fields are described in [docs/configuration.md](https://github.com/imumesh18/memcat/blob/main/docs/configuration.md).
|
|
163
|
+
|
|
164
|
+
## Operate
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
memcat warm --config memcat.toml --from calls.jsonl --top 500 # render your most repeated clauses
|
|
168
|
+
memcat stats --config memcat.toml # entries, minutes, codecs
|
|
169
|
+
memcat purge --config memcat.toml --text "..." # erase a text everywhere
|
|
170
|
+
memcat export --config memcat.toml --to backup/ ; memcat import --config memcat.toml --from backup/
|
|
171
|
+
memcat serve --config memcat.toml --port 8080 # TTS endpoint for Vapi and other platforms
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`memcat serve` puts the cache in front of hosted platforms that call a custom TTS
|
|
175
|
+
URL (Vapi's custom voice): it answers with raw 16-bit PCM, streamed, and counts
|
|
176
|
+
admission per platform call. It listens on 127.0.0.1 unless you set a secret (or
|
|
177
|
+
pass `--insecure-no-auth` behind your own authentication).
|
|
178
|
+
|
|
179
|
+
Every command except `serve` and `warm --dry-run` needs a Redis or S3 `[store]`.
|
|
180
|
+
`purge --text` erases the text and each of its clauses in every voice, setting and
|
|
181
|
+
sample rate; `purge --all` asks for confirmation (or `--yes`).
|
|
182
|
+
|
|
183
|
+
See [docs/operations.md](https://github.com/imumesh18/memcat/blob/main/docs/operations.md) for `memcat.toml`, metrics and OpenTelemetry.
|
|
184
|
+
|
|
185
|
+
## Benchmarks
|
|
186
|
+
|
|
187
|
+
`bench/` reproduces every measured number above: `hitrate.py` (ABCD, Harper Valley
|
|
188
|
+
Bank, τ²-bench), `elevenlabs_replay.py` (recorded ElevenLabs traffic), `latency.py`
|
|
189
|
+
(Kokoro + Redis), `storage.py`, `hotpath.py` (run nightly, which fails at 3x
|
|
190
|
+
its baseline), `soak.py` (100 concurrent calls) and `abx.py`, which builds a
|
|
191
|
+
30-sentence listening test and measures each join against live speech. The Redis
|
|
192
|
+
row is `tests/test_store_outage.py`. Results live in `bench/results/`.
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
BSD 2-Clause, the same as Pipecat. Keep one cache (or one `namespace`) per
|
|
197
|
+
customer so audio is never shared across customers, and check your provider's
|
|
198
|
+
terms for anything else.
|
memcat-1.0.0/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Memcat
|
|
2
|
+
|
|
3
|
+
**A text-to-speech cache for [Pipecat](https://github.com/pipecat-ai/pipecat) voice bots.**
|
|
4
|
+
When your agent says a line it has said before (or part of one), Memcat replays the
|
|
5
|
+
stored audio instead of paying the provider to synthesize it again, and the audio
|
|
6
|
+
starts in milliseconds.
|
|
7
|
+
|
|
8
|
+
| | Measured |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| Speech served from cache, scripted support calls ([Harper Valley Bank](https://github.com/cricketclub/gridspace-stanford-harper-valley), 1,446 calls) | **54.0% of characters, 59.0% of responses** |
|
|
11
|
+
| Same, typed support chats ([ABCD](https://arxiv.org/abs/2104.00783), 10,042 chats) | 32.4% of characters, 38.5% of responses |
|
|
12
|
+
| First audio of a sentence that opens with a cached clause, recorded ElevenLabs traffic | **2–13 ms** with Memcat vs 346 ms without (median, HTTP and WebSocket) |
|
|
13
|
+
| What a listener hears, recorded ElevenLabs traffic, cached and live clauses mixed | the same words in the same order: 0.3% word error vs 0.0% without Memcat, by speech recognition |
|
|
14
|
+
| First audio on a hit, local Kokoro voice | 1.0 ms from memory, 10 ms from Redis (vs 745 ms live) |
|
|
15
|
+
| Added time on a miss | none measurable: 488 ms vs 495 ms without Memcat (median, within run-to-run noise) |
|
|
16
|
+
| Storage per cached minute | 464 KB at 8 kHz (mu-law), 236 KB at 24 kHz (Opus) |
|
|
17
|
+
| Cache keys containing a digit (order numbers, prices, dates, phone numbers) | **0** on all corpora |
|
|
18
|
+
| Redis killed mid-call | every turn spoken, no error reaches the pipeline |
|
|
19
|
+
|
|
20
|
+
Hit rates come from replaying public dialogue corpora through Memcat's own chunker,
|
|
21
|
+
privacy rules and admission policy (`bench/hitrate.py`); no TTS was called. Net of
|
|
22
|
+
what filling the cache costs, savings are within 2 points of these (54.0% and 31.9%).
|
|
23
|
+
LLM agents whose every sentence carries an order number or a price cache less: 6–17%
|
|
24
|
+
of characters on [τ²-bench](https://github.com/sierra-research/tau2-bench) traces,
|
|
25
|
+
where those clauses stay live. The ElevenLabs rows replay traffic recorded from the
|
|
26
|
+
ElevenLabs API (Flash v2.5) through Pipecat's ElevenLabs services
|
|
27
|
+
(`bench/elevenlabs_replay.py`). The recordings are not in the repository; anyone with
|
|
28
|
+
an ElevenLabs key can make their own and rerun it.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv add memcat # or: pip install memcat
|
|
34
|
+
uv add "memcat[redis,opus]" # shared cache across bot processes, compact audio
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Python 3.11–3.13 and `pipecat-ai` 1.10–1.13. Extras: `redis` (`RedisStore`), `opus`
|
|
38
|
+
(Opus storage), `s3` (`S3Store`), `all`.
|
|
39
|
+
|
|
40
|
+
## Use
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from pipecat.services.cartesia.tts import CartesiaTTSService
|
|
44
|
+
from memcat import cached, MemcatConfig, RedisStore
|
|
45
|
+
|
|
46
|
+
CachedCartesia = cached(CartesiaTTSService) # once, at import time
|
|
47
|
+
|
|
48
|
+
tts = CachedCartesia(
|
|
49
|
+
api_key=CARTESIA_API_KEY,
|
|
50
|
+
voice_id=VOICE_ID,
|
|
51
|
+
memcat=MemcatConfig(store=RedisStore(url=REDIS_URL), namespace="acme-support"),
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Use `tts` exactly as you used the unwrapped service. Nothing else in the pipeline
|
|
56
|
+
changes. `await tts.memcat_stats()` reports hits, characters saved and dollars saved.
|
|
57
|
+
|
|
58
|
+
No API keys at hand? From a clone, `uv run examples/kokoro_local.py` speaks three lines twice
|
|
59
|
+
with a local voice; the second time they come from the cache.
|
|
60
|
+
|
|
61
|
+
## How it works
|
|
62
|
+
|
|
63
|
+
1. **Clauses, not just sentences.** Each sentence Pipecat hands to the TTS is split
|
|
64
|
+
at commas, semicolons, colons and dashes. "Thanks for waiting, your order 48213
|
|
65
|
+
has shipped, and it should arrive soon." is three clauses; the first and last are
|
|
66
|
+
reusable, the middle one is spoken live.
|
|
67
|
+
2. **Privacy first.** A clause with any digit, a number in words, an email, a URL,
|
|
68
|
+
money, a date, a time or a spelled-out code is never cached. A clause is cached
|
|
69
|
+
only after it has been heard in 3 distinct calls, so a line said in one call never
|
|
70
|
+
enters the cache.
|
|
71
|
+
3. **One lookup per sentence.** An in-process index answers misses with no network
|
|
72
|
+
call; hits come from memory or one Redis `MGET`.
|
|
73
|
+
4. **Exact order on every provider.** On HTTP services cached and live audio
|
|
74
|
+
interleave inside one generator. On WebSocket services Memcat keeps Pipecat's
|
|
75
|
+
audio context and gives each run of live clauses its own provider stream,
|
|
76
|
+
releasing segments strictly in order, so cached clauses play before, between and
|
|
77
|
+
after live ones. Transcripts and word timestamps stay correct.
|
|
78
|
+
5. **Joins matched to live speech.** Clips are stored tight and Memcat sets the pause
|
|
79
|
+
at each join the way live speech does (short after "Sure,", longer after a full
|
|
80
|
+
clause), matches loudness, and fades edges. Against live synthesis of the same
|
|
81
|
+
sentences, pauses differ by 45 ms at the median and loudness by at most 0.62 LU
|
|
82
|
+
(`bench/abx.py`).
|
|
83
|
+
6. **Fill in the background.** A clause admitted from a live run that synthesized
|
|
84
|
+
it alone is stored as is, for free; others are rendered once by a background
|
|
85
|
+
copy of the same service. Nothing on the call ever waits for the cache.
|
|
86
|
+
|
|
87
|
+
If the store, a codec or Memcat itself fails, the sentence goes to the provider as
|
|
88
|
+
if Memcat were not installed: Memcat can lose savings, but the call goes on.
|
|
89
|
+
|
|
90
|
+
## Providers
|
|
91
|
+
|
|
92
|
+
| Provider | WebSocket | HTTP |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| Cartesia | cached clauses anywhere in a sentence | cached clauses anywhere |
|
|
95
|
+
| ElevenLabs | cached clauses anywhere | cached clauses anywhere, `previous_text` kept |
|
|
96
|
+
| Deepgram Aura | cached clauses anywhere, within Deepgram's 20-flush/minute limit | cached clauses anywhere |
|
|
97
|
+
| Sarvam | cached clauses before the first live one | cached clauses anywhere |
|
|
98
|
+
| Any other Pipecat TTS service | cached clauses before the first live one (if it pushes its own start frames) | cached clauses anywhere (if it returns audio inline) |
|
|
99
|
+
|
|
100
|
+
Every listed provider is tested with its Pipecat service class against a local
|
|
101
|
+
emulator of the provider's wire protocol (`tests/emulators/`). ElevenLabs is also
|
|
102
|
+
tested against recordings of its API, replayed with their original timing
|
|
103
|
+
(`tests/test_replay_elevenlabs.py`). To record your own, see
|
|
104
|
+
[CONTRIBUTING.md](https://github.com/imumesh18/memcat/blob/main/CONTRIBUTING.md).
|
|
105
|
+
|
|
106
|
+
## Configure
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
MemcatConfig(
|
|
110
|
+
store=RedisStore("redis://..."), # default: one MemoryStore per process; RedisStore, S3Store
|
|
111
|
+
namespace="acme-support", # tenant prefix on every key
|
|
112
|
+
admit_after_calls=3, # distinct calls before a clause is cached
|
|
113
|
+
warm=["Thank you for calling ABC Shop, how can I help you today?"], # cached at start
|
|
114
|
+
block_patterns=[r"\bACME-\w+"], # extra text that must never be cached
|
|
115
|
+
ttl_s=30 * 24 * 3600, # refreshed when served
|
|
116
|
+
codec="auto", # mu-law at 8 kHz, Opus when installed, else PCM16
|
|
117
|
+
enabled=True, # kill switch: False means a pure pass-through
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
All fields are described in [docs/configuration.md](https://github.com/imumesh18/memcat/blob/main/docs/configuration.md).
|
|
122
|
+
|
|
123
|
+
## Operate
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
memcat warm --config memcat.toml --from calls.jsonl --top 500 # render your most repeated clauses
|
|
127
|
+
memcat stats --config memcat.toml # entries, minutes, codecs
|
|
128
|
+
memcat purge --config memcat.toml --text "..." # erase a text everywhere
|
|
129
|
+
memcat export --config memcat.toml --to backup/ ; memcat import --config memcat.toml --from backup/
|
|
130
|
+
memcat serve --config memcat.toml --port 8080 # TTS endpoint for Vapi and other platforms
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`memcat serve` puts the cache in front of hosted platforms that call a custom TTS
|
|
134
|
+
URL (Vapi's custom voice): it answers with raw 16-bit PCM, streamed, and counts
|
|
135
|
+
admission per platform call. It listens on 127.0.0.1 unless you set a secret (or
|
|
136
|
+
pass `--insecure-no-auth` behind your own authentication).
|
|
137
|
+
|
|
138
|
+
Every command except `serve` and `warm --dry-run` needs a Redis or S3 `[store]`.
|
|
139
|
+
`purge --text` erases the text and each of its clauses in every voice, setting and
|
|
140
|
+
sample rate; `purge --all` asks for confirmation (or `--yes`).
|
|
141
|
+
|
|
142
|
+
See [docs/operations.md](https://github.com/imumesh18/memcat/blob/main/docs/operations.md) for `memcat.toml`, metrics and OpenTelemetry.
|
|
143
|
+
|
|
144
|
+
## Benchmarks
|
|
145
|
+
|
|
146
|
+
`bench/` reproduces every measured number above: `hitrate.py` (ABCD, Harper Valley
|
|
147
|
+
Bank, τ²-bench), `elevenlabs_replay.py` (recorded ElevenLabs traffic), `latency.py`
|
|
148
|
+
(Kokoro + Redis), `storage.py`, `hotpath.py` (run nightly, which fails at 3x
|
|
149
|
+
its baseline), `soak.py` (100 concurrent calls) and `abx.py`, which builds a
|
|
150
|
+
30-sentence listening test and measures each join against live speech. The Redis
|
|
151
|
+
row is `tests/test_store_outage.py`. Results live in `bench/results/`.
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
BSD 2-Clause, the same as Pipecat. Keep one cache (or one `namespace`) per
|
|
156
|
+
customer so audio is never shared across customers, and check your provider's
|
|
157
|
+
terms for anything else.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "memcat"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Clause-level text-to-speech cache for Pipecat voice bots."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "BSD-2-Clause"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
keywords = [
|
|
10
|
+
"pipecat",
|
|
11
|
+
"tts",
|
|
12
|
+
"text-to-speech",
|
|
13
|
+
"cache",
|
|
14
|
+
"voice",
|
|
15
|
+
"voice-ai",
|
|
16
|
+
"telephony",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 5 - Production/Stable",
|
|
20
|
+
"Framework :: AsyncIO",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Multimedia :: Sound/Audio :: Speech",
|
|
28
|
+
"Typing :: Typed",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"pipecat-ai>=1.10,<1.14",
|
|
32
|
+
"aiohttp>=3.11.12",
|
|
33
|
+
"audioop-lts>=0.2.1; python_version >= '3.13'",
|
|
34
|
+
"loguru>=0.7.3",
|
|
35
|
+
"numpy>=1.26.4",
|
|
36
|
+
"soxr>=1.0.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[[project.authors]]
|
|
40
|
+
name = "Umesh Yadav"
|
|
41
|
+
email = "git@umesh.dev"
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
redis = ["redis>=5.0.1"]
|
|
45
|
+
opus = ["av>=14"]
|
|
46
|
+
s3 = ["boto3>=1.34"]
|
|
47
|
+
all = ["memcat[redis,opus,s3]"]
|
|
48
|
+
|
|
49
|
+
[project.scripts]
|
|
50
|
+
memcat = "memcat.cli:main"
|
|
51
|
+
|
|
52
|
+
[project.urls]
|
|
53
|
+
Homepage = "https://github.com/imumesh18/memcat"
|
|
54
|
+
Source = "https://github.com/imumesh18/memcat"
|
|
55
|
+
Documentation = "https://github.com/imumesh18/memcat/tree/main/docs"
|
|
56
|
+
Changelog = "https://github.com/imumesh18/memcat/blob/main/CHANGELOG.md"
|
|
57
|
+
Issues = "https://github.com/imumesh18/memcat/issues"
|
|
58
|
+
|
|
59
|
+
[dependency-groups]
|
|
60
|
+
asr = ["faster-whisper>=1.2.1"]
|
|
61
|
+
dev = [
|
|
62
|
+
"pytest>=8",
|
|
63
|
+
"pytest-asyncio>=0.24",
|
|
64
|
+
"pytest-cov>=5",
|
|
65
|
+
"hypothesis>=6.100",
|
|
66
|
+
"ruff>=0.6",
|
|
67
|
+
"pyright>=1.1.380",
|
|
68
|
+
"redis>=5.0.1",
|
|
69
|
+
"av>=14",
|
|
70
|
+
"boto3>=1.34",
|
|
71
|
+
"moto[s3]>=5",
|
|
72
|
+
"opentelemetry-api>=1.20",
|
|
73
|
+
"opentelemetry-sdk>=1.20",
|
|
74
|
+
"pipecat-ai[kokoro]>=1.13.0,<1.14",
|
|
75
|
+
"psutil>=7.2.2",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
[build-system]
|
|
79
|
+
requires = ["uv_build>=0.13.1,<0.14.0"]
|
|
80
|
+
build-backend = "uv_build"
|
|
81
|
+
|
|
82
|
+
[tool.ruff]
|
|
83
|
+
line-length = 100
|
|
84
|
+
target-version = "py311"
|
|
85
|
+
|
|
86
|
+
[tool.ruff.format]
|
|
87
|
+
exclude = ["*.md"]
|
|
88
|
+
|
|
89
|
+
[tool.ruff.lint]
|
|
90
|
+
select = [
|
|
91
|
+
"E",
|
|
92
|
+
"F",
|
|
93
|
+
"W",
|
|
94
|
+
"I",
|
|
95
|
+
"B",
|
|
96
|
+
"UP",
|
|
97
|
+
"SIM",
|
|
98
|
+
"RUF",
|
|
99
|
+
"ASYNC",
|
|
100
|
+
"PERF",
|
|
101
|
+
"S",
|
|
102
|
+
]
|
|
103
|
+
ignore = [
|
|
104
|
+
"S101",
|
|
105
|
+
"S311",
|
|
106
|
+
"RUF012",
|
|
107
|
+
"RUF001",
|
|
108
|
+
"RUF002",
|
|
109
|
+
"RUF003",
|
|
110
|
+
"ASYNC109",
|
|
111
|
+
"ASYNC110",
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
[tool.ruff.lint.per-file-ignores]
|
|
115
|
+
"tests/**" = [
|
|
116
|
+
"S",
|
|
117
|
+
"B",
|
|
118
|
+
"PERF",
|
|
119
|
+
"RUF",
|
|
120
|
+
"ASYNC",
|
|
121
|
+
]
|
|
122
|
+
"bench/**" = [
|
|
123
|
+
"S",
|
|
124
|
+
"PERF",
|
|
125
|
+
"RUF",
|
|
126
|
+
"ASYNC",
|
|
127
|
+
]
|
|
128
|
+
"examples/**" = ["S"]
|
|
129
|
+
|
|
130
|
+
[tool.pyright]
|
|
131
|
+
include = ["src"]
|
|
132
|
+
pythonVersion = "3.11"
|
|
133
|
+
typeCheckingMode = "strict"
|
|
134
|
+
reportMissingTypeStubs = false
|
|
135
|
+
reportUnknownMemberType = false
|
|
136
|
+
reportUnknownVariableType = false
|
|
137
|
+
reportUnknownArgumentType = false
|
|
138
|
+
reportUnknownParameterType = false
|
|
139
|
+
reportMissingParameterType = false
|
|
140
|
+
reportPrivateUsage = false
|
|
141
|
+
reportUnnecessaryIsInstance = false
|
|
142
|
+
|
|
143
|
+
[tool.pytest.ini_options]
|
|
144
|
+
asyncio_mode = "auto"
|
|
145
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
146
|
+
testpaths = ["tests"]
|
|
147
|
+
filterwarnings = ["ignore::DeprecationWarning"]
|
|
148
|
+
|
|
149
|
+
[tool.coverage.run]
|
|
150
|
+
source = ["memcat"]
|
|
151
|
+
branch = true
|
|
152
|
+
|
|
153
|
+
[tool.coverage.report]
|
|
154
|
+
show_missing = true
|
|
155
|
+
skip_covered = true
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "memcat"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Clause-level text-to-speech cache for Pipecat voice bots."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "BSD-2-Clause"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "Umesh Yadav", email = "git@umesh.dev" }]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
keywords = ["pipecat", "tts", "text-to-speech", "cache", "voice", "voice-ai", "telephony"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 5 - Production/Stable",
|
|
13
|
+
"Framework :: AsyncIO",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Multimedia :: Sound/Audio :: Speech",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"pipecat-ai>=1.10,<1.14", # each Pipecat minor is tested before it is allowed
|
|
25
|
+
# Imported directly by memcat; Pipecat 1.10 requires the same floors.
|
|
26
|
+
"aiohttp>=3.11.12",
|
|
27
|
+
"audioop-lts>=0.2.1; python_version >= '3.13'",
|
|
28
|
+
"loguru>=0.7.3",
|
|
29
|
+
"numpy>=1.26.4",
|
|
30
|
+
"soxr>=1.0.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
redis = ["redis>=5.0.1"]
|
|
35
|
+
opus = ["av>=14"]
|
|
36
|
+
s3 = ["boto3>=1.34"]
|
|
37
|
+
all = ["memcat[redis,opus,s3]"]
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
memcat = "memcat.cli:main"
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/imumesh18/memcat"
|
|
44
|
+
Source = "https://github.com/imumesh18/memcat"
|
|
45
|
+
Documentation = "https://github.com/imumesh18/memcat/tree/main/docs"
|
|
46
|
+
Changelog = "https://github.com/imumesh18/memcat/blob/main/CHANGELOG.md"
|
|
47
|
+
Issues = "https://github.com/imumesh18/memcat/issues"
|
|
48
|
+
|
|
49
|
+
[dependency-groups]
|
|
50
|
+
asr = [
|
|
51
|
+
"faster-whisper>=1.2.1",
|
|
52
|
+
]
|
|
53
|
+
dev = [
|
|
54
|
+
"pytest>=8",
|
|
55
|
+
"pytest-asyncio>=0.24",
|
|
56
|
+
"pytest-cov>=5",
|
|
57
|
+
"hypothesis>=6.100",
|
|
58
|
+
"ruff>=0.6",
|
|
59
|
+
"pyright>=1.1.380",
|
|
60
|
+
"redis>=5.0.1",
|
|
61
|
+
"av>=14",
|
|
62
|
+
"boto3>=1.34",
|
|
63
|
+
"moto[s3]>=5",
|
|
64
|
+
"opentelemetry-api>=1.20",
|
|
65
|
+
"opentelemetry-sdk>=1.20",
|
|
66
|
+
"pipecat-ai[kokoro]>=1.13.0,<1.14",
|
|
67
|
+
"psutil>=7.2.2",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
[build-system]
|
|
71
|
+
requires = ["uv_build>=0.13.1,<0.14.0"]
|
|
72
|
+
build-backend = "uv_build"
|
|
73
|
+
|
|
74
|
+
[tool.ruff]
|
|
75
|
+
line-length = 100
|
|
76
|
+
target-version = "py311"
|
|
77
|
+
|
|
78
|
+
[tool.ruff.format]
|
|
79
|
+
# Ruff also formats Python blocks in Markdown; the docs keep hand-aligned comments.
|
|
80
|
+
exclude = ["*.md"]
|
|
81
|
+
|
|
82
|
+
[tool.ruff.lint]
|
|
83
|
+
select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF", "ASYNC", "PERF", "S"]
|
|
84
|
+
ignore = ["S101", "S311", "RUF012", "RUF001", "RUF002", "RUF003", "ASYNC109", "ASYNC110"]
|
|
85
|
+
|
|
86
|
+
[tool.ruff.lint.per-file-ignores]
|
|
87
|
+
"tests/**" = ["S", "B", "PERF", "RUF", "ASYNC"]
|
|
88
|
+
"bench/**" = ["S", "PERF", "RUF", "ASYNC"]
|
|
89
|
+
"examples/**" = ["S"]
|
|
90
|
+
|
|
91
|
+
[tool.pyright]
|
|
92
|
+
include = ["src"]
|
|
93
|
+
pythonVersion = "3.11"
|
|
94
|
+
typeCheckingMode = "strict"
|
|
95
|
+
reportMissingTypeStubs = false
|
|
96
|
+
reportUnknownMemberType = false
|
|
97
|
+
reportUnknownVariableType = false
|
|
98
|
+
reportUnknownArgumentType = false
|
|
99
|
+
reportUnknownParameterType = false
|
|
100
|
+
reportMissingParameterType = false
|
|
101
|
+
reportPrivateUsage = false
|
|
102
|
+
reportUnnecessaryIsInstance = false
|
|
103
|
+
|
|
104
|
+
[tool.pytest.ini_options]
|
|
105
|
+
asyncio_mode = "auto"
|
|
106
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
107
|
+
testpaths = ["tests"]
|
|
108
|
+
filterwarnings = ["ignore::DeprecationWarning"]
|
|
109
|
+
|
|
110
|
+
[tool.coverage.run]
|
|
111
|
+
source = ["memcat"]
|
|
112
|
+
branch = true
|
|
113
|
+
|
|
114
|
+
[tool.coverage.report]
|
|
115
|
+
show_missing = true
|
|
116
|
+
skip_covered = true
|