dv-pipecat-ai 0.0.74.dev770__py3-none-any.whl → 0.0.82.dev776__py3-none-any.whl

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.

Potentially problematic release.


This version of dv-pipecat-ai might be problematic. Click here for more details.

Files changed (244) hide show
  1. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/METADATA +137 -93
  2. dv_pipecat_ai-0.0.82.dev776.dist-info/RECORD +340 -0
  3. pipecat/__init__.py +17 -0
  4. pipecat/adapters/base_llm_adapter.py +36 -1
  5. pipecat/adapters/schemas/direct_function.py +296 -0
  6. pipecat/adapters/schemas/function_schema.py +15 -6
  7. pipecat/adapters/schemas/tools_schema.py +55 -7
  8. pipecat/adapters/services/anthropic_adapter.py +22 -3
  9. pipecat/adapters/services/aws_nova_sonic_adapter.py +23 -3
  10. pipecat/adapters/services/bedrock_adapter.py +22 -3
  11. pipecat/adapters/services/gemini_adapter.py +16 -3
  12. pipecat/adapters/services/open_ai_adapter.py +17 -2
  13. pipecat/adapters/services/open_ai_realtime_adapter.py +23 -3
  14. pipecat/audio/filters/base_audio_filter.py +30 -6
  15. pipecat/audio/filters/koala_filter.py +37 -2
  16. pipecat/audio/filters/krisp_filter.py +59 -6
  17. pipecat/audio/filters/noisereduce_filter.py +37 -0
  18. pipecat/audio/interruptions/base_interruption_strategy.py +25 -5
  19. pipecat/audio/interruptions/min_words_interruption_strategy.py +21 -4
  20. pipecat/audio/mixers/base_audio_mixer.py +30 -7
  21. pipecat/audio/mixers/soundfile_mixer.py +53 -6
  22. pipecat/audio/resamplers/base_audio_resampler.py +17 -9
  23. pipecat/audio/resamplers/resampy_resampler.py +26 -1
  24. pipecat/audio/resamplers/soxr_resampler.py +32 -1
  25. pipecat/audio/resamplers/soxr_stream_resampler.py +101 -0
  26. pipecat/audio/utils.py +194 -1
  27. pipecat/audio/vad/silero.py +60 -3
  28. pipecat/audio/vad/vad_analyzer.py +114 -30
  29. pipecat/clocks/base_clock.py +19 -0
  30. pipecat/clocks/system_clock.py +25 -0
  31. pipecat/extensions/voicemail/__init__.py +0 -0
  32. pipecat/extensions/voicemail/voicemail_detector.py +707 -0
  33. pipecat/frames/frames.py +590 -156
  34. pipecat/metrics/metrics.py +64 -1
  35. pipecat/observers/base_observer.py +58 -19
  36. pipecat/observers/loggers/debug_log_observer.py +56 -64
  37. pipecat/observers/loggers/llm_log_observer.py +8 -1
  38. pipecat/observers/loggers/transcription_log_observer.py +19 -7
  39. pipecat/observers/loggers/user_bot_latency_log_observer.py +32 -5
  40. pipecat/observers/turn_tracking_observer.py +26 -1
  41. pipecat/pipeline/base_pipeline.py +5 -7
  42. pipecat/pipeline/base_task.py +52 -9
  43. pipecat/pipeline/parallel_pipeline.py +121 -177
  44. pipecat/pipeline/pipeline.py +129 -20
  45. pipecat/pipeline/runner.py +50 -1
  46. pipecat/pipeline/sync_parallel_pipeline.py +132 -32
  47. pipecat/pipeline/task.py +263 -280
  48. pipecat/pipeline/task_observer.py +85 -34
  49. pipecat/pipeline/to_be_updated/merge_pipeline.py +32 -2
  50. pipecat/processors/aggregators/dtmf_aggregator.py +29 -22
  51. pipecat/processors/aggregators/gated.py +25 -24
  52. pipecat/processors/aggregators/gated_openai_llm_context.py +22 -2
  53. pipecat/processors/aggregators/llm_response.py +398 -89
  54. pipecat/processors/aggregators/openai_llm_context.py +161 -13
  55. pipecat/processors/aggregators/sentence.py +25 -14
  56. pipecat/processors/aggregators/user_response.py +28 -3
  57. pipecat/processors/aggregators/vision_image_frame.py +24 -14
  58. pipecat/processors/async_generator.py +28 -0
  59. pipecat/processors/audio/audio_buffer_processor.py +78 -37
  60. pipecat/processors/consumer_processor.py +25 -6
  61. pipecat/processors/filters/frame_filter.py +23 -0
  62. pipecat/processors/filters/function_filter.py +30 -0
  63. pipecat/processors/filters/identity_filter.py +17 -2
  64. pipecat/processors/filters/null_filter.py +24 -1
  65. pipecat/processors/filters/stt_mute_filter.py +56 -21
  66. pipecat/processors/filters/wake_check_filter.py +46 -3
  67. pipecat/processors/filters/wake_notifier_filter.py +21 -3
  68. pipecat/processors/frame_processor.py +488 -131
  69. pipecat/processors/frameworks/langchain.py +38 -3
  70. pipecat/processors/frameworks/rtvi.py +719 -34
  71. pipecat/processors/gstreamer/pipeline_source.py +41 -0
  72. pipecat/processors/idle_frame_processor.py +26 -3
  73. pipecat/processors/logger.py +23 -0
  74. pipecat/processors/metrics/frame_processor_metrics.py +77 -4
  75. pipecat/processors/metrics/sentry.py +42 -4
  76. pipecat/processors/producer_processor.py +34 -14
  77. pipecat/processors/text_transformer.py +22 -10
  78. pipecat/processors/transcript_processor.py +48 -29
  79. pipecat/processors/user_idle_processor.py +31 -21
  80. pipecat/runner/__init__.py +1 -0
  81. pipecat/runner/daily.py +132 -0
  82. pipecat/runner/livekit.py +148 -0
  83. pipecat/runner/run.py +543 -0
  84. pipecat/runner/types.py +67 -0
  85. pipecat/runner/utils.py +515 -0
  86. pipecat/serializers/base_serializer.py +42 -0
  87. pipecat/serializers/exotel.py +17 -6
  88. pipecat/serializers/genesys.py +95 -0
  89. pipecat/serializers/livekit.py +33 -0
  90. pipecat/serializers/plivo.py +16 -15
  91. pipecat/serializers/protobuf.py +37 -1
  92. pipecat/serializers/telnyx.py +18 -17
  93. pipecat/serializers/twilio.py +32 -16
  94. pipecat/services/ai_service.py +5 -3
  95. pipecat/services/anthropic/llm.py +113 -43
  96. pipecat/services/assemblyai/models.py +63 -5
  97. pipecat/services/assemblyai/stt.py +64 -11
  98. pipecat/services/asyncai/__init__.py +0 -0
  99. pipecat/services/asyncai/tts.py +501 -0
  100. pipecat/services/aws/llm.py +185 -111
  101. pipecat/services/aws/stt.py +217 -23
  102. pipecat/services/aws/tts.py +118 -52
  103. pipecat/services/aws/utils.py +101 -5
  104. pipecat/services/aws_nova_sonic/aws.py +82 -64
  105. pipecat/services/aws_nova_sonic/context.py +15 -6
  106. pipecat/services/azure/common.py +10 -2
  107. pipecat/services/azure/image.py +32 -0
  108. pipecat/services/azure/llm.py +9 -7
  109. pipecat/services/azure/stt.py +65 -2
  110. pipecat/services/azure/tts.py +154 -23
  111. pipecat/services/cartesia/stt.py +125 -8
  112. pipecat/services/cartesia/tts.py +102 -38
  113. pipecat/services/cerebras/llm.py +15 -23
  114. pipecat/services/deepgram/stt.py +19 -11
  115. pipecat/services/deepgram/tts.py +36 -0
  116. pipecat/services/deepseek/llm.py +14 -23
  117. pipecat/services/elevenlabs/tts.py +330 -64
  118. pipecat/services/fal/image.py +43 -0
  119. pipecat/services/fal/stt.py +48 -10
  120. pipecat/services/fireworks/llm.py +14 -21
  121. pipecat/services/fish/tts.py +109 -9
  122. pipecat/services/gemini_multimodal_live/__init__.py +1 -0
  123. pipecat/services/gemini_multimodal_live/events.py +83 -2
  124. pipecat/services/gemini_multimodal_live/file_api.py +189 -0
  125. pipecat/services/gemini_multimodal_live/gemini.py +218 -21
  126. pipecat/services/gladia/config.py +17 -10
  127. pipecat/services/gladia/stt.py +82 -36
  128. pipecat/services/google/frames.py +40 -0
  129. pipecat/services/google/google.py +2 -0
  130. pipecat/services/google/image.py +39 -2
  131. pipecat/services/google/llm.py +176 -58
  132. pipecat/services/google/llm_openai.py +26 -4
  133. pipecat/services/google/llm_vertex.py +37 -15
  134. pipecat/services/google/rtvi.py +41 -0
  135. pipecat/services/google/stt.py +65 -17
  136. pipecat/services/google/test-google-chirp.py +45 -0
  137. pipecat/services/google/tts.py +390 -19
  138. pipecat/services/grok/llm.py +8 -6
  139. pipecat/services/groq/llm.py +8 -6
  140. pipecat/services/groq/stt.py +13 -9
  141. pipecat/services/groq/tts.py +40 -0
  142. pipecat/services/hamsa/__init__.py +9 -0
  143. pipecat/services/hamsa/stt.py +241 -0
  144. pipecat/services/heygen/__init__.py +5 -0
  145. pipecat/services/heygen/api.py +281 -0
  146. pipecat/services/heygen/client.py +620 -0
  147. pipecat/services/heygen/video.py +338 -0
  148. pipecat/services/image_service.py +5 -3
  149. pipecat/services/inworld/__init__.py +1 -0
  150. pipecat/services/inworld/tts.py +592 -0
  151. pipecat/services/llm_service.py +127 -45
  152. pipecat/services/lmnt/tts.py +80 -7
  153. pipecat/services/mcp_service.py +85 -44
  154. pipecat/services/mem0/memory.py +42 -13
  155. pipecat/services/minimax/tts.py +74 -15
  156. pipecat/services/mistral/__init__.py +0 -0
  157. pipecat/services/mistral/llm.py +185 -0
  158. pipecat/services/moondream/vision.py +55 -10
  159. pipecat/services/neuphonic/tts.py +275 -48
  160. pipecat/services/nim/llm.py +8 -6
  161. pipecat/services/ollama/llm.py +27 -7
  162. pipecat/services/openai/base_llm.py +54 -16
  163. pipecat/services/openai/image.py +30 -0
  164. pipecat/services/openai/llm.py +7 -5
  165. pipecat/services/openai/stt.py +13 -9
  166. pipecat/services/openai/tts.py +42 -10
  167. pipecat/services/openai_realtime_beta/azure.py +11 -9
  168. pipecat/services/openai_realtime_beta/context.py +7 -5
  169. pipecat/services/openai_realtime_beta/events.py +10 -7
  170. pipecat/services/openai_realtime_beta/openai.py +37 -18
  171. pipecat/services/openpipe/llm.py +30 -24
  172. pipecat/services/openrouter/llm.py +9 -7
  173. pipecat/services/perplexity/llm.py +15 -19
  174. pipecat/services/piper/tts.py +26 -12
  175. pipecat/services/playht/tts.py +227 -65
  176. pipecat/services/qwen/llm.py +8 -6
  177. pipecat/services/rime/tts.py +128 -17
  178. pipecat/services/riva/stt.py +160 -22
  179. pipecat/services/riva/tts.py +67 -2
  180. pipecat/services/sambanova/llm.py +19 -17
  181. pipecat/services/sambanova/stt.py +14 -8
  182. pipecat/services/sarvam/tts.py +60 -13
  183. pipecat/services/simli/video.py +82 -21
  184. pipecat/services/soniox/__init__.py +0 -0
  185. pipecat/services/soniox/stt.py +398 -0
  186. pipecat/services/speechmatics/stt.py +29 -17
  187. pipecat/services/stt_service.py +47 -11
  188. pipecat/services/tavus/video.py +94 -25
  189. pipecat/services/together/llm.py +8 -6
  190. pipecat/services/tts_service.py +77 -53
  191. pipecat/services/ultravox/stt.py +46 -43
  192. pipecat/services/vision_service.py +5 -3
  193. pipecat/services/websocket_service.py +12 -11
  194. pipecat/services/whisper/base_stt.py +58 -12
  195. pipecat/services/whisper/stt.py +69 -58
  196. pipecat/services/xtts/tts.py +59 -2
  197. pipecat/sync/base_notifier.py +19 -0
  198. pipecat/sync/event_notifier.py +24 -0
  199. pipecat/tests/utils.py +73 -5
  200. pipecat/transcriptions/language.py +24 -0
  201. pipecat/transports/base_input.py +112 -8
  202. pipecat/transports/base_output.py +235 -13
  203. pipecat/transports/base_transport.py +119 -0
  204. pipecat/transports/local/audio.py +76 -0
  205. pipecat/transports/local/tk.py +84 -0
  206. pipecat/transports/network/fastapi_websocket.py +174 -15
  207. pipecat/transports/network/small_webrtc.py +383 -39
  208. pipecat/transports/network/webrtc_connection.py +214 -8
  209. pipecat/transports/network/websocket_client.py +171 -1
  210. pipecat/transports/network/websocket_server.py +147 -9
  211. pipecat/transports/services/daily.py +792 -70
  212. pipecat/transports/services/helpers/daily_rest.py +122 -129
  213. pipecat/transports/services/livekit.py +339 -4
  214. pipecat/transports/services/tavus.py +273 -38
  215. pipecat/utils/asyncio/task_manager.py +92 -186
  216. pipecat/utils/base_object.py +83 -1
  217. pipecat/utils/network.py +2 -0
  218. pipecat/utils/string.py +114 -58
  219. pipecat/utils/text/base_text_aggregator.py +44 -13
  220. pipecat/utils/text/base_text_filter.py +46 -0
  221. pipecat/utils/text/markdown_text_filter.py +70 -14
  222. pipecat/utils/text/pattern_pair_aggregator.py +18 -14
  223. pipecat/utils/text/simple_text_aggregator.py +43 -2
  224. pipecat/utils/text/skip_tags_aggregator.py +21 -13
  225. pipecat/utils/time.py +36 -0
  226. pipecat/utils/tracing/class_decorators.py +32 -7
  227. pipecat/utils/tracing/conversation_context_provider.py +12 -2
  228. pipecat/utils/tracing/service_attributes.py +80 -64
  229. pipecat/utils/tracing/service_decorators.py +48 -21
  230. pipecat/utils/tracing/setup.py +13 -7
  231. pipecat/utils/tracing/turn_context_provider.py +12 -2
  232. pipecat/utils/tracing/turn_trace_observer.py +27 -0
  233. pipecat/utils/utils.py +14 -14
  234. dv_pipecat_ai-0.0.74.dev770.dist-info/RECORD +0 -319
  235. pipecat/examples/daily_runner.py +0 -64
  236. pipecat/examples/run.py +0 -265
  237. pipecat/utils/asyncio/watchdog_async_iterator.py +0 -72
  238. pipecat/utils/asyncio/watchdog_event.py +0 -42
  239. pipecat/utils/asyncio/watchdog_priority_queue.py +0 -48
  240. pipecat/utils/asyncio/watchdog_queue.py +0 -48
  241. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/WHEEL +0 -0
  242. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/licenses/LICENSE +0 -0
  243. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/top_level.txt +0 -0
  244. /pipecat/{examples → extensions}/__init__.py +0 -0
pipecat/utils/string.py CHANGED
@@ -4,44 +4,92 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
- import re
8
- from typing import Optional, Sequence, Tuple
9
-
10
- ENDOFSENTENCE_PATTERN_STR = r"""
11
- (?<![A-Z]) # Negative lookbehind: not preceded by an uppercase letter (e.g., "U.S.A.")
12
- (?<!\d\.\d) # Not preceded by a decimal number (e.g., "3.14159")
13
- (?<!^\d\.) # Not preceded by a numbered list item (e.g., "1. Let's start")
14
- (?<!\d\s[ap]) # Negative lookbehind: not preceded by time (e.g., "3:00 a.m.")
15
- (?<!Mr|Ms|Dr) # Negative lookbehind: not preceded by Mr, Ms, Dr (combined bc. length is the same)
16
- (?<!Mrs) # Negative lookbehind: not preceded by "Mrs"
17
- (?<!Prof) # Negative lookbehind: not preceded by "Prof"
18
- (\.\s*\.\s*\.|[\.\?\!;])| # Match a period, question mark, exclamation point, or semicolon
19
- (\。\s*\。\s*\。|[。?!;।]) # the full-width version (mainly used in East Asian languages such as Chinese, Hindi)
20
- $ # End of string
21
- """
7
+ """Text processing utilities for sentence boundary detection and tag parsing.
22
8
 
23
- ENDOFSENTENCE_PATTERN = re.compile(ENDOFSENTENCE_PATTERN_STR, re.VERBOSE)
9
+ This module provides utilities for natural language text processing including
10
+ sentence boundary detection, email and number pattern handling, and XML-style
11
+ tag parsing for structured text content.
24
12
 
25
- EMAIL_PATTERN = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")
13
+ Dependencies:
14
+ This module uses NLTK (Natural Language Toolkit) for robust sentence
15
+ tokenization. NLTK is licensed under the Apache License 2.0.
16
+ See: https://www.nltk.org/
17
+ Source: https://www.nltk.org/api/nltk.tokenize.punkt.html
18
+ """
26
19
 
27
- NUMBER_PATTERN = re.compile(r"[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?")
20
+ import re
21
+ from typing import FrozenSet, Optional, Sequence, Tuple
22
+
23
+ import nltk
24
+ from nltk.tokenize import sent_tokenize
25
+
26
+ # Ensure punkt_tab tokenizer data is available
27
+ try:
28
+ nltk.data.find("tokenizers/punkt_tab")
29
+ except LookupError:
30
+ nltk.download("punkt_tab", quiet=True)
31
+
32
+ SENTENCE_ENDING_PUNCTUATION: FrozenSet[str] = frozenset(
33
+ {
34
+ # Latin script punctuation (most European languages, Filipino, etc.)
35
+ ".",
36
+ "!",
37
+ "?",
38
+ ";",
39
+ # East Asian punctuation (Chinese (Traditional & Simplified), Japanese, Korean)
40
+ "。", # Ideographic full stop
41
+ "?", # Full-width question mark
42
+ "!", # Full-width exclamation mark
43
+ ";", # Full-width semicolon
44
+ ".", # Full-width period
45
+ "。", # Halfwidth ideographic period
46
+ # Indic scripts punctuation (Hindi, Sanskrit, Marathi, Nepali, Bengali, Tamil, Telugu, Kannada, Malayalam, Gujarati, Punjabi, Oriya, Assamese)
47
+ "।", # Devanagari danda (single vertical bar)
48
+ "॥", # Devanagari double danda (double vertical bar)
49
+ # Arabic script punctuation (Arabic, Persian, Urdu, Pashto)
50
+ "؟", # Arabic question mark
51
+ "؛", # Arabic semicolon
52
+ "۔", # Urdu full stop
53
+ "؏", # Arabic sign misra (classical texts)
54
+ # Thai
55
+ "।", # Thai uses Devanagari-style punctuation in some contexts
56
+ # Myanmar/Burmese
57
+ "၊", # Myanmar sign little section
58
+ "။", # Myanmar sign section
59
+ # Khmer
60
+ "។", # Khmer sign khan
61
+ "៕", # Khmer sign bariyoosan
62
+ # Lao
63
+ "໌", # Lao cancellation mark (used as period)
64
+ "༎", # Tibetan mark delimiter tsheg bstar (also used in Lao contexts)
65
+ # Tibetan
66
+ "།", # Tibetan mark intersyllabic tsheg
67
+ "༎", # Tibetan mark delimiter tsheg bstar
68
+ # Armenian
69
+ "։", # Armenian full stop
70
+ "՜", # Armenian exclamation mark
71
+ "՞", # Armenian question mark
72
+ # Ethiopic script (Amharic)
73
+ "።", # Ethiopic full stop
74
+ "፧", # Ethiopic question mark
75
+ "፨", # Ethiopic paragraph separator
76
+ }
77
+ )
28
78
 
29
79
  StartEndTags = Tuple[str, str]
30
80
 
31
81
 
32
82
  def replace_match(text: str, match: re.Match, old: str, new: str) -> str:
33
- """Replace occurrences of a substring within a matched section of a given
34
- text.
83
+ """Replace occurrences of a substring within a matched section of text.
35
84
 
36
85
  Args:
37
- text (str): The input text in which replacements will be made.
38
- match (re.Match): A regex match object representing the section of text to modify.
39
- old (str): The substring to be replaced.
40
- new (str): The substring to replace `old` with.
86
+ text: The input text in which replacements will be made.
87
+ match: A regex match object representing the section of text to modify.
88
+ old: The substring to be replaced.
89
+ new: The substring to replace `old` with.
41
90
 
42
91
  Returns:
43
- str: The modified text with the specified replacements made within the matched section.
44
-
92
+ The modified text with the specified replacements made within the matched section.
45
93
  """
46
94
  start = match.start()
47
95
  end = match.end()
@@ -51,37 +99,47 @@ def replace_match(text: str, match: re.Match, old: str, new: str) -> str:
51
99
 
52
100
 
53
101
  def match_endofsentence(text: str) -> int:
54
- """Finds the position of the end of a sentence in the provided text string.
102
+ """Find the position of the end of a sentence in the provided text.
55
103
 
56
- This function processes the input text by replacing periods in email
57
- addresses and numbers with ampersands to prevent them from being
58
- misidentified as sentence terminals. It then searches for the end of a
59
- sentence using a specified regex pattern.
104
+ This function uses NLTK's sentence tokenizer to detect sentence boundaries
105
+ in the input text, combined with punctuation verification to ensure that
106
+ single tokens without proper sentence endings aren't considered complete sentences.
60
107
 
61
108
  Args:
62
- text (str): The input text in which to find the end of the sentence.
109
+ text: The input text in which to find the end of the sentence.
63
110
 
64
111
  Returns:
65
- int: The position of the end of the sentence if found, otherwise 0.
66
-
112
+ The position of the end of the sentence if found, otherwise 0.
67
113
  """
68
114
  text = text.rstrip()
69
115
 
70
- # Replace email dots by ampersands so we can find the end of sentence. For
71
- # example, first.last@email.com becomes first&last@email&com.
72
- emails = list(EMAIL_PATTERN.finditer(text))
73
- for email_match in emails:
74
- text = replace_match(text, email_match, ".", "&")
116
+ if not text:
117
+ return 0
75
118
 
76
- # Replace number dots by ampersands so we can find the end of sentence.
77
- numbers = list(NUMBER_PATTERN.finditer(text))
78
- for number_match in numbers:
79
- text = replace_match(text, number_match, ".", "&")
119
+ # Use NLTK's sentence tokenizer to find sentence boundaries
120
+ sentences = sent_tokenize(text)
80
121
 
81
- # Match against the new text.
82
- match = ENDOFSENTENCE_PATTERN.search(text)
122
+ if not sentences:
123
+ return 0
83
124
 
84
- return match.end() if match else 0
125
+ first_sentence = sentences[0]
126
+
127
+ # If there's only one sentence that equals the entire text,
128
+ # verify it actually ends with sentence-ending punctuation.
129
+ # This is required as NLTK may return a single sentence for
130
+ # text that's a single word. In the case of LLM tokens, it's
131
+ # common for text to be single words, so we need to ensure
132
+ # sentence-ending punctuation is present.
133
+ if len(sentences) == 1 and first_sentence == text:
134
+ return len(text) if text and text[-1] in SENTENCE_ENDING_PUNCTUATION else 0
135
+
136
+ # If there are multiple sentences, the first one is complete by definition
137
+ # (NLTK found a boundary, so there must be proper punctuation)
138
+ if len(sentences) > 1:
139
+ return len(first_sentence)
140
+
141
+ # Single sentence that doesn't equal the full text means incomplete
142
+ return 0
85
143
 
86
144
 
87
145
  def parse_start_end_tags(
@@ -90,24 +148,22 @@ def parse_start_end_tags(
90
148
  current_tag: Optional[StartEndTags],
91
149
  current_tag_index: int,
92
150
  ) -> Tuple[Optional[StartEndTags], int]:
93
- """Parses the given text to identify a pair of start/end tags.
151
+ """Parse text to identify start and end tag pairs.
94
152
 
95
- If a start tag was previously found (i.e. current_tags is valid), wait for
96
- the corresponding end tag. Otherwise, wait for a start tag.
153
+ If a start tag was previously found (i.e., current_tag is valid), wait for
154
+ the corresponding end tag. Otherwise, wait for a start tag.
97
155
 
98
- This function will return the index in the text that we should start parsing
156
+ This function returns the index in the text where parsing should continue
99
157
  in the next call and the current or new tags.
100
158
 
101
- Parameters:
102
- - text (str): The text to be parsed.
103
- - tags (Sequence[StartEndTags]): List of tuples containing start and end tags.
104
- - current_tags (Optional[StartEndTags]): The currently active tags, if any.
105
- - current_tags_index (int): The current index in the text.
159
+ Args:
160
+ text: The text to be parsed.
161
+ tags: List of tuples containing start and end tags.
162
+ current_tag: The currently active tags, if any.
163
+ current_tag_index: The current index in the text.
106
164
 
107
165
  Returns:
108
- Tuple[Optional[StartEndTags], int]: A tuple containing None or the current
109
- tag and the index of the text.
110
-
166
+ A tuple containing None or the current tag and the index of the text.
111
167
  """
112
168
  # If we are already inside a tag, check if the end tag is in the text.
113
169
  if current_tag:
@@ -4,54 +4,85 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
+ """Base text aggregator interface for Pipecat text processing.
8
+
9
+ This module defines the abstract base class for text aggregators that accumulate
10
+ and process text tokens, typically used by TTS services to determine when
11
+ aggregated text should be sent for speech synthesis.
12
+ """
13
+
7
14
  from abc import ABC, abstractmethod
8
15
  from typing import Optional
9
16
 
10
17
 
11
18
  class BaseTextAggregator(ABC):
12
- """This is the base class for text aggregators. Text aggregators are usually
13
- used by the TTS service to aggregate LLM tokens and decide when the
14
- aggregated text should be pushed to the TTS service.
19
+ """Base class for text aggregators in the Pipecat framework.
20
+
21
+ Text aggregators are usually used by the TTS service to aggregate LLM tokens
22
+ and decide when the aggregated text should be pushed to the TTS service.
15
23
 
16
24
  Text aggregators can also be used to manipulate text while it's being
17
25
  aggregated (e.g. reasoning blocks can be removed).
18
26
 
27
+ Subclasses must implement all abstract methods to define specific aggregation
28
+ logic, text manipulation behavior, and state management for interruptions.
19
29
  """
20
30
 
21
31
  @property
22
32
  @abstractmethod
23
33
  def text(self) -> str:
24
- """Returns the currently aggregated text."""
34
+ """Get the currently aggregated text.
35
+
36
+ Subclasses must implement this property to return the text that has
37
+ been accumulated so far in their internal buffer or storage.
38
+
39
+ Returns:
40
+ The text that has been accumulated so far.
41
+ """
25
42
  pass
26
43
 
27
44
  @abstractmethod
28
45
  async def aggregate(self, text: str) -> Optional[str]:
29
- """Aggregates the specified text with the currently accumulated text.
46
+ """Aggregate the specified text with the currently accumulated text.
30
47
 
31
48
  This method should be implemented to define how the new text contributes
32
49
  to the aggregation process. It returns the updated aggregated text if
33
50
  it's ready to be processed, or None otherwise.
34
51
 
52
+ Subclasses should implement their specific logic for:
53
+
54
+ - How to combine new text with existing accumulated text
55
+ - When to consider the aggregated text ready for processing
56
+ - What criteria determine text completion (e.g., sentence boundaries)
57
+
35
58
  Args:
36
- text (str): The text to be aggregated.
59
+ text: The text to be aggregated.
37
60
 
38
61
  Returns:
39
- Optional[str]: The updated aggregated text or None if aggregated
40
- text is not ready.
41
-
62
+ The updated aggregated text if ready for processing, or None if more
63
+ text is needed before the aggregated content is ready.
42
64
  """
43
65
  pass
44
66
 
45
67
  @abstractmethod
46
68
  async def handle_interruption(self):
47
- """Handles interruptions. When an interruption occurs it is possible
48
- that we might want to discard the aggregated text or do some internal
49
- modifications to the aggregated text.
69
+ """Handle interruptions in the text aggregation process.
50
70
 
71
+ When an interruption occurs it is possible that we might want to discard
72
+ the aggregated text or do some internal modifications to the aggregated text.
73
+
74
+ Subclasses should implement this method to define how they respond to
75
+ interruptions, such as clearing buffers, resetting state, or preserving
76
+ partial content.
51
77
  """
52
78
  pass
53
79
 
54
80
  @abstractmethod
55
81
  async def reset(self):
56
- """Clears the internally aggregated text."""
82
+ """Clear the internally aggregated text and reset to initial state.
83
+
84
+ Subclasses should implement this method to return the aggregator to its
85
+ initial state, discarding any previously accumulated text content and
86
+ resetting any internal tracking variables.
87
+ """
57
88
  pass
@@ -4,23 +4,69 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
+ """Base text filter interface for Pipecat text processing.
8
+
9
+ This module defines the abstract base class for text filters that can modify
10
+ text content in the processing pipeline, including support for settings updates
11
+ and interruption handling.
12
+ """
13
+
7
14
  from abc import ABC, abstractmethod
8
15
  from typing import Any, Mapping
9
16
 
10
17
 
11
18
  class BaseTextFilter(ABC):
19
+ """Abstract base class for text filters in the Pipecat framework.
20
+
21
+ Text filters are responsible for modifying text content as it flows through
22
+ the processing pipeline. They support dynamic settings updates and can handle
23
+ interruptions to reset their internal state.
24
+
25
+ Subclasses must implement all abstract methods to define specific filtering
26
+ behavior, settings management, and interruption handling logic.
27
+ """
28
+
12
29
  @abstractmethod
13
30
  async def update_settings(self, settings: Mapping[str, Any]):
31
+ """Update the filter's configuration settings.
32
+
33
+ Subclasses should implement this method to handle dynamic configuration
34
+ updates during runtime, updating internal state as needed.
35
+
36
+ Args:
37
+ settings: Dictionary of setting names to values for configuration.
38
+ """
14
39
  pass
15
40
 
16
41
  @abstractmethod
17
42
  async def filter(self, text: str) -> str:
43
+ """Apply filtering transformations to the input text.
44
+
45
+ Subclasses must implement this method to define the specific text
46
+ transformations that should be applied to the input.
47
+
48
+ Args:
49
+ text: The input text to be filtered.
50
+
51
+ Returns:
52
+ The filtered text after applying transformations.
53
+ """
18
54
  pass
19
55
 
20
56
  @abstractmethod
21
57
  async def handle_interruption(self):
58
+ """Handle interruption events in the processing pipeline.
59
+
60
+ Subclasses should implement this method to reset internal state,
61
+ clear buffers, or perform other cleanup when an interruption occurs.
62
+ """
22
63
  pass
23
64
 
24
65
  @abstractmethod
25
66
  async def reset_interruption(self):
67
+ """Reset the filter state after an interruption has been handled.
68
+
69
+ Subclasses should implement this method to restore the filter to normal
70
+ operation after an interruption has been processed and resolved.
71
+ """
26
72
  pass
@@ -4,6 +4,12 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
+ """Markdown text filter for removing Markdown formatting from text.
8
+
9
+ This module provides a text filter that converts Markdown content to plain text
10
+ while preserving structure and handling special cases like code blocks and tables.
11
+ """
12
+
7
13
  import re
8
14
  from typing import Any, Mapping, Optional
9
15
 
@@ -14,19 +20,34 @@ from pipecat.utils.text.base_text_filter import BaseTextFilter
14
20
 
15
21
 
16
22
  class MarkdownTextFilter(BaseTextFilter):
17
- """Removes Markdown formatting from text in TextFrames.
23
+ """Text filter that removes Markdown formatting from text content.
18
24
 
19
25
  Converts Markdown to plain text while preserving the overall structure,
20
26
  including leading and trailing spaces. Handles special cases like
21
- asterisks and table formatting.
27
+ asterisks and table formatting. Supports selective filtering of code
28
+ blocks and tables based on configuration.
22
29
  """
23
30
 
24
31
  class InputParams(BaseModel):
32
+ """Configuration parameters for Markdown text filtering.
33
+
34
+ Parameters:
35
+ enable_text_filter: Whether to apply Markdown filtering. Defaults to True.
36
+ filter_code: Whether to remove code blocks from the text. Defaults to False.
37
+ filter_tables: Whether to remove table content from the text. Defaults to False.
38
+ """
39
+
25
40
  enable_text_filter: Optional[bool] = True
26
41
  filter_code: Optional[bool] = False
27
42
  filter_tables: Optional[bool] = False
28
43
 
29
44
  def __init__(self, params: Optional[InputParams] = None, **kwargs):
45
+ """Initialize the Markdown text filter.
46
+
47
+ Args:
48
+ params: Configuration parameters for filtering behavior.
49
+ **kwargs: Additional keyword arguments passed to parent class.
50
+ """
30
51
  super().__init__(**kwargs)
31
52
  self._settings = params or MarkdownTextFilter.InputParams()
32
53
  self._in_code_block = False
@@ -34,11 +55,24 @@ class MarkdownTextFilter(BaseTextFilter):
34
55
  self._interrupted = False
35
56
 
36
57
  async def update_settings(self, settings: Mapping[str, Any]):
58
+ """Update the filter's configuration settings.
59
+
60
+ Args:
61
+ settings: Dictionary of setting names to values for configuration.
62
+ """
37
63
  for key, value in settings.items():
38
64
  if hasattr(self._settings, key):
39
65
  setattr(self._settings, key, value)
40
66
 
41
67
  async def filter(self, text: str) -> str:
68
+ """Apply Markdown filtering transformations to the input text.
69
+
70
+ Args:
71
+ text: The input text containing Markdown formatting to be filtered.
72
+
73
+ Returns:
74
+ The filtered text with Markdown formatting removed or converted.
75
+ """
42
76
  if self._settings.enable_text_filter:
43
77
  # Remove newlines and replace with a space only when there's no text before or after
44
78
  filtered_text = re.sub(r"^\s*\n", " ", text, flags=re.MULTILINE)
@@ -108,11 +142,20 @@ class MarkdownTextFilter(BaseTextFilter):
108
142
  return text
109
143
 
110
144
  async def handle_interruption(self):
145
+ """Handle interruption events in the processing pipeline.
146
+
147
+ Resets the filter state and clears any tracking variables for
148
+ code blocks and tables.
149
+ """
111
150
  self._interrupted = True
112
151
  self._in_code_block = False
113
152
  self._in_table = False
114
153
 
115
154
  async def reset_interruption(self):
155
+ """Reset the filter state after an interruption has been handled.
156
+
157
+ Clears the interrupted flag to restore normal operation.
158
+ """
116
159
  self._interrupted = False
117
160
 
118
161
  #
@@ -120,8 +163,10 @@ class MarkdownTextFilter(BaseTextFilter):
120
163
  #
121
164
 
122
165
  def _remove_code_blocks(self, text: str) -> str:
123
- """Main method to remove code blocks from the input text.
124
- Handles interruptions and delegates to specific methods based on the current state.
166
+ """Remove code blocks from the input text.
167
+
168
+ Handles interruptions and delegates to specific methods based on the
169
+ current state.
125
170
  """
126
171
  if self._interrupted:
127
172
  self._in_code_block = False
@@ -137,8 +182,10 @@ class MarkdownTextFilter(BaseTextFilter):
137
182
  return self._handle_not_in_code_block(match, text, code_block_pattern)
138
183
 
139
184
  def _handle_in_code_block(self, match, text):
140
- """Handle text when we're currently inside a code block.
141
- If we find the end of the block, return text after it. Otherwise, skip the content.
185
+ """Handle text when not currently inside a code block.
186
+
187
+ If we find the end of the block, return text after it. Otherwise, skip
188
+ the content.
142
189
  """
143
190
  if match:
144
191
  self._in_code_block = False
@@ -147,9 +194,7 @@ class MarkdownTextFilter(BaseTextFilter):
147
194
  return "" # Skip content inside code block
148
195
 
149
196
  def _handle_not_in_code_block(self, match, text, code_block_pattern):
150
- """Handle text when we're not currently inside a code block.
151
- Delegate to specific methods based on whether we find a code block delimiter.
152
- """
197
+ """Handle text when not currently inside a code block."""
153
198
  if not match:
154
199
  return text # No code block found, return original text
155
200
 
@@ -159,14 +204,17 @@ class MarkdownTextFilter(BaseTextFilter):
159
204
  return self._handle_code_block_within_text(text, code_block_pattern)
160
205
 
161
206
  def _handle_start_of_code_block(self, text, start_index):
162
- """Handle the case where we find the start of a code block.
163
- Return any text before the code block and set the state to inside a code block.
207
+ """Handle the case where a code block starts.
208
+
209
+ Return any text before the code block and set the state to inside a
210
+ code block.
164
211
  """
165
212
  self._in_code_block = True
166
213
  return text[:start_index].strip()
167
214
 
168
215
  def _handle_code_block_within_text(self, text, code_block_pattern):
169
- """Handle the case where we find a code block within the text.
216
+ """Handle code blocks found within text content.
217
+
170
218
  If it's a complete code block, remove it and return surrounding text.
171
219
  If it's the start of a code block, return text before it and set state.
172
220
  """
@@ -180,8 +228,16 @@ class MarkdownTextFilter(BaseTextFilter):
180
228
  # Filter tables
181
229
  #
182
230
  def remove_tables(self, text: str) -> str:
183
- """Remove tables from the input text, handling cases where
184
- both start and end tags are in the same input.
231
+ """Remove HTML tables from the input text.
232
+
233
+ Handles cases where both start and end tags are in the same input,
234
+ as well as tables that span multiple text chunks.
235
+
236
+ Args:
237
+ text: The text containing HTML tables to remove.
238
+
239
+ Returns:
240
+ The text with tables removed.
185
241
  """
186
242
  if self._interrupted:
187
243
  self._in_table = False
@@ -4,6 +4,13 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
+ """Pattern pair aggregator for processing structured content in streaming text.
8
+
9
+ This module provides an aggregator that identifies and processes content between
10
+ pattern pairs (like XML tags or custom delimiters) in streaming text, with
11
+ support for custom handlers and configurable pattern removal.
12
+ """
13
+
7
14
  import re
8
15
  from typing import Awaitable, Callable, Optional, Tuple
9
16
 
@@ -20,20 +27,15 @@ class PatternMatch:
20
27
  in the text. It contains information about which pattern was matched,
21
28
  the full matched text (including start and end patterns), and the
22
29
  content between the patterns.
23
-
24
- Attributes:
25
- pattern_id: The identifier of the matched pattern pair.
26
- full_match: The complete text including start and end patterns.
27
- content: The text content between the start and end patterns.
28
30
  """
29
31
 
30
32
  def __init__(self, pattern_id: str, full_match: str, content: str):
31
33
  """Initialize a pattern match.
32
34
 
33
35
  Args:
34
- pattern_id: ID of the pattern pair.
35
- full_match: Complete matched text including start and end patterns.
36
- content: Content between the start and end patterns.
36
+ pattern_id: The identifier of the matched pattern pair.
37
+ full_match: The complete text including start and end patterns.
38
+ content: The text content between the start and end patterns.
37
39
  """
38
40
  self.pattern_id = pattern_id
39
41
  self.full_match = full_match
@@ -43,7 +45,7 @@ class PatternMatch:
43
45
  """Return a string representation of the pattern match.
44
46
 
45
47
  Returns:
46
- A string describing the pattern match.
48
+ A descriptive string showing the pattern ID and content.
47
49
  """
48
50
  return f"PatternMatch(id={self.pattern_id}, content={self.content})"
49
51
 
@@ -66,6 +68,7 @@ class PatternPairAggregator(BaseTextAggregator):
66
68
  """Initialize the pattern pair aggregator.
67
69
 
68
70
  Creates an empty aggregator with no patterns or handlers registered.
71
+ Text buffering and pattern detection will begin when text is aggregated.
69
72
  """
70
73
  self._text = ""
71
74
  self._patterns = {}
@@ -76,7 +79,7 @@ class PatternPairAggregator(BaseTextAggregator):
76
79
  """Get the currently buffered text.
77
80
 
78
81
  Returns:
79
- The current text buffer content.
82
+ The current text buffer content that hasn't been processed yet.
80
83
  """
81
84
  return self._text
82
85
 
@@ -115,7 +118,7 @@ class PatternPairAggregator(BaseTextAggregator):
115
118
 
116
119
  Args:
117
120
  pattern_id: ID of the pattern pair to match.
118
- handler: Function to call when pattern is matched.
121
+ handler: Async function to call when pattern is matched.
119
122
  The function should accept a PatternMatch object.
120
123
 
121
124
  Returns:
@@ -131,10 +134,11 @@ class PatternPairAggregator(BaseTextAggregator):
131
134
  appropriate handlers, and optionally removes the matches.
132
135
 
133
136
  Args:
134
- text: The text to process.
137
+ text: The text to process for pattern matches.
135
138
 
136
139
  Returns:
137
140
  Tuple of (processed_text, was_modified) where:
141
+
138
142
  - processed_text is the text after processing patterns
139
143
  - was_modified indicates whether any changes were made
140
144
  """
@@ -185,7 +189,7 @@ class PatternPairAggregator(BaseTextAggregator):
185
189
  matching end patterns, which would indicate incomplete content.
186
190
 
187
191
  Args:
188
- text: The text to check.
192
+ text: The text to check for incomplete patterns.
189
193
 
190
194
  Returns:
191
195
  True if there are incomplete patterns, False otherwise.
@@ -257,6 +261,6 @@ class PatternPairAggregator(BaseTextAggregator):
257
261
  """Clear the internally aggregated text.
258
262
 
259
263
  Resets the aggregator to its initial state, discarding any
260
- buffered text.
264
+ buffered text and clearing pattern tracking state.
261
265
  """
262
266
  self._text = ""