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.
Files changed (36) hide show
  1. memcat-1.0.0/LICENSE +24 -0
  2. memcat-1.0.0/PKG-INFO +198 -0
  3. memcat-1.0.0/README.md +157 -0
  4. memcat-1.0.0/pyproject.toml +155 -0
  5. memcat-1.0.0/pyproject.toml.orig +116 -0
  6. memcat-1.0.0/src/memcat/__init__.py +46 -0
  7. memcat-1.0.0/src/memcat/cli.py +608 -0
  8. memcat-1.0.0/src/memcat/config.py +120 -0
  9. memcat-1.0.0/src/memcat/core/__init__.py +0 -0
  10. memcat-1.0.0/src/memcat/core/audio.py +247 -0
  11. memcat-1.0.0/src/memcat/core/breaker.py +113 -0
  12. memcat-1.0.0/src/memcat/core/chunker.py +505 -0
  13. memcat-1.0.0/src/memcat/core/codec.py +310 -0
  14. memcat-1.0.0/src/memcat/core/index.py +361 -0
  15. memcat-1.0.0/src/memcat/core/keys.py +406 -0
  16. memcat-1.0.0/src/memcat/core/lookup.py +215 -0
  17. memcat-1.0.0/src/memcat/core/metrics.py +89 -0
  18. memcat-1.0.0/src/memcat/core/pcm.py +24 -0
  19. memcat-1.0.0/src/memcat/core/runtime.py +252 -0
  20. memcat-1.0.0/src/memcat/core/store/__init__.py +0 -0
  21. memcat-1.0.0/src/memcat/core/store/base.py +94 -0
  22. memcat-1.0.0/src/memcat/core/store/memory.py +233 -0
  23. memcat-1.0.0/src/memcat/core/store/redis.py +456 -0
  24. memcat-1.0.0/src/memcat/core/store/s3.py +527 -0
  25. memcat-1.0.0/src/memcat/core/worker.py +577 -0
  26. memcat-1.0.0/src/memcat/pipecat/__init__.py +0 -0
  27. memcat-1.0.0/src/memcat/pipecat/adapters/__init__.py +0 -0
  28. memcat-1.0.0/src/memcat/pipecat/adapters/base.py +172 -0
  29. memcat-1.0.0/src/memcat/pipecat/adapters/providers.py +190 -0
  30. memcat-1.0.0/src/memcat/pipecat/compat.py +161 -0
  31. memcat-1.0.0/src/memcat/pipecat/contexts.py +94 -0
  32. memcat-1.0.0/src/memcat/pipecat/mixin.py +1614 -0
  33. memcat-1.0.0/src/memcat/pipecat/render.py +331 -0
  34. memcat-1.0.0/src/memcat/pipecat/wrap.py +36 -0
  35. memcat-1.0.0/src/memcat/py.typed +0 -0
  36. 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