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.
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/METADATA +137 -93
- dv_pipecat_ai-0.0.82.dev776.dist-info/RECORD +340 -0
- pipecat/__init__.py +17 -0
- pipecat/adapters/base_llm_adapter.py +36 -1
- pipecat/adapters/schemas/direct_function.py +296 -0
- pipecat/adapters/schemas/function_schema.py +15 -6
- pipecat/adapters/schemas/tools_schema.py +55 -7
- pipecat/adapters/services/anthropic_adapter.py +22 -3
- pipecat/adapters/services/aws_nova_sonic_adapter.py +23 -3
- pipecat/adapters/services/bedrock_adapter.py +22 -3
- pipecat/adapters/services/gemini_adapter.py +16 -3
- pipecat/adapters/services/open_ai_adapter.py +17 -2
- pipecat/adapters/services/open_ai_realtime_adapter.py +23 -3
- pipecat/audio/filters/base_audio_filter.py +30 -6
- pipecat/audio/filters/koala_filter.py +37 -2
- pipecat/audio/filters/krisp_filter.py +59 -6
- pipecat/audio/filters/noisereduce_filter.py +37 -0
- pipecat/audio/interruptions/base_interruption_strategy.py +25 -5
- pipecat/audio/interruptions/min_words_interruption_strategy.py +21 -4
- pipecat/audio/mixers/base_audio_mixer.py +30 -7
- pipecat/audio/mixers/soundfile_mixer.py +53 -6
- pipecat/audio/resamplers/base_audio_resampler.py +17 -9
- pipecat/audio/resamplers/resampy_resampler.py +26 -1
- pipecat/audio/resamplers/soxr_resampler.py +32 -1
- pipecat/audio/resamplers/soxr_stream_resampler.py +101 -0
- pipecat/audio/utils.py +194 -1
- pipecat/audio/vad/silero.py +60 -3
- pipecat/audio/vad/vad_analyzer.py +114 -30
- pipecat/clocks/base_clock.py +19 -0
- pipecat/clocks/system_clock.py +25 -0
- pipecat/extensions/voicemail/__init__.py +0 -0
- pipecat/extensions/voicemail/voicemail_detector.py +707 -0
- pipecat/frames/frames.py +590 -156
- pipecat/metrics/metrics.py +64 -1
- pipecat/observers/base_observer.py +58 -19
- pipecat/observers/loggers/debug_log_observer.py +56 -64
- pipecat/observers/loggers/llm_log_observer.py +8 -1
- pipecat/observers/loggers/transcription_log_observer.py +19 -7
- pipecat/observers/loggers/user_bot_latency_log_observer.py +32 -5
- pipecat/observers/turn_tracking_observer.py +26 -1
- pipecat/pipeline/base_pipeline.py +5 -7
- pipecat/pipeline/base_task.py +52 -9
- pipecat/pipeline/parallel_pipeline.py +121 -177
- pipecat/pipeline/pipeline.py +129 -20
- pipecat/pipeline/runner.py +50 -1
- pipecat/pipeline/sync_parallel_pipeline.py +132 -32
- pipecat/pipeline/task.py +263 -280
- pipecat/pipeline/task_observer.py +85 -34
- pipecat/pipeline/to_be_updated/merge_pipeline.py +32 -2
- pipecat/processors/aggregators/dtmf_aggregator.py +29 -22
- pipecat/processors/aggregators/gated.py +25 -24
- pipecat/processors/aggregators/gated_openai_llm_context.py +22 -2
- pipecat/processors/aggregators/llm_response.py +398 -89
- pipecat/processors/aggregators/openai_llm_context.py +161 -13
- pipecat/processors/aggregators/sentence.py +25 -14
- pipecat/processors/aggregators/user_response.py +28 -3
- pipecat/processors/aggregators/vision_image_frame.py +24 -14
- pipecat/processors/async_generator.py +28 -0
- pipecat/processors/audio/audio_buffer_processor.py +78 -37
- pipecat/processors/consumer_processor.py +25 -6
- pipecat/processors/filters/frame_filter.py +23 -0
- pipecat/processors/filters/function_filter.py +30 -0
- pipecat/processors/filters/identity_filter.py +17 -2
- pipecat/processors/filters/null_filter.py +24 -1
- pipecat/processors/filters/stt_mute_filter.py +56 -21
- pipecat/processors/filters/wake_check_filter.py +46 -3
- pipecat/processors/filters/wake_notifier_filter.py +21 -3
- pipecat/processors/frame_processor.py +488 -131
- pipecat/processors/frameworks/langchain.py +38 -3
- pipecat/processors/frameworks/rtvi.py +719 -34
- pipecat/processors/gstreamer/pipeline_source.py +41 -0
- pipecat/processors/idle_frame_processor.py +26 -3
- pipecat/processors/logger.py +23 -0
- pipecat/processors/metrics/frame_processor_metrics.py +77 -4
- pipecat/processors/metrics/sentry.py +42 -4
- pipecat/processors/producer_processor.py +34 -14
- pipecat/processors/text_transformer.py +22 -10
- pipecat/processors/transcript_processor.py +48 -29
- pipecat/processors/user_idle_processor.py +31 -21
- pipecat/runner/__init__.py +1 -0
- pipecat/runner/daily.py +132 -0
- pipecat/runner/livekit.py +148 -0
- pipecat/runner/run.py +543 -0
- pipecat/runner/types.py +67 -0
- pipecat/runner/utils.py +515 -0
- pipecat/serializers/base_serializer.py +42 -0
- pipecat/serializers/exotel.py +17 -6
- pipecat/serializers/genesys.py +95 -0
- pipecat/serializers/livekit.py +33 -0
- pipecat/serializers/plivo.py +16 -15
- pipecat/serializers/protobuf.py +37 -1
- pipecat/serializers/telnyx.py +18 -17
- pipecat/serializers/twilio.py +32 -16
- pipecat/services/ai_service.py +5 -3
- pipecat/services/anthropic/llm.py +113 -43
- pipecat/services/assemblyai/models.py +63 -5
- pipecat/services/assemblyai/stt.py +64 -11
- pipecat/services/asyncai/__init__.py +0 -0
- pipecat/services/asyncai/tts.py +501 -0
- pipecat/services/aws/llm.py +185 -111
- pipecat/services/aws/stt.py +217 -23
- pipecat/services/aws/tts.py +118 -52
- pipecat/services/aws/utils.py +101 -5
- pipecat/services/aws_nova_sonic/aws.py +82 -64
- pipecat/services/aws_nova_sonic/context.py +15 -6
- pipecat/services/azure/common.py +10 -2
- pipecat/services/azure/image.py +32 -0
- pipecat/services/azure/llm.py +9 -7
- pipecat/services/azure/stt.py +65 -2
- pipecat/services/azure/tts.py +154 -23
- pipecat/services/cartesia/stt.py +125 -8
- pipecat/services/cartesia/tts.py +102 -38
- pipecat/services/cerebras/llm.py +15 -23
- pipecat/services/deepgram/stt.py +19 -11
- pipecat/services/deepgram/tts.py +36 -0
- pipecat/services/deepseek/llm.py +14 -23
- pipecat/services/elevenlabs/tts.py +330 -64
- pipecat/services/fal/image.py +43 -0
- pipecat/services/fal/stt.py +48 -10
- pipecat/services/fireworks/llm.py +14 -21
- pipecat/services/fish/tts.py +109 -9
- pipecat/services/gemini_multimodal_live/__init__.py +1 -0
- pipecat/services/gemini_multimodal_live/events.py +83 -2
- pipecat/services/gemini_multimodal_live/file_api.py +189 -0
- pipecat/services/gemini_multimodal_live/gemini.py +218 -21
- pipecat/services/gladia/config.py +17 -10
- pipecat/services/gladia/stt.py +82 -36
- pipecat/services/google/frames.py +40 -0
- pipecat/services/google/google.py +2 -0
- pipecat/services/google/image.py +39 -2
- pipecat/services/google/llm.py +176 -58
- pipecat/services/google/llm_openai.py +26 -4
- pipecat/services/google/llm_vertex.py +37 -15
- pipecat/services/google/rtvi.py +41 -0
- pipecat/services/google/stt.py +65 -17
- pipecat/services/google/test-google-chirp.py +45 -0
- pipecat/services/google/tts.py +390 -19
- pipecat/services/grok/llm.py +8 -6
- pipecat/services/groq/llm.py +8 -6
- pipecat/services/groq/stt.py +13 -9
- pipecat/services/groq/tts.py +40 -0
- pipecat/services/hamsa/__init__.py +9 -0
- pipecat/services/hamsa/stt.py +241 -0
- pipecat/services/heygen/__init__.py +5 -0
- pipecat/services/heygen/api.py +281 -0
- pipecat/services/heygen/client.py +620 -0
- pipecat/services/heygen/video.py +338 -0
- pipecat/services/image_service.py +5 -3
- pipecat/services/inworld/__init__.py +1 -0
- pipecat/services/inworld/tts.py +592 -0
- pipecat/services/llm_service.py +127 -45
- pipecat/services/lmnt/tts.py +80 -7
- pipecat/services/mcp_service.py +85 -44
- pipecat/services/mem0/memory.py +42 -13
- pipecat/services/minimax/tts.py +74 -15
- pipecat/services/mistral/__init__.py +0 -0
- pipecat/services/mistral/llm.py +185 -0
- pipecat/services/moondream/vision.py +55 -10
- pipecat/services/neuphonic/tts.py +275 -48
- pipecat/services/nim/llm.py +8 -6
- pipecat/services/ollama/llm.py +27 -7
- pipecat/services/openai/base_llm.py +54 -16
- pipecat/services/openai/image.py +30 -0
- pipecat/services/openai/llm.py +7 -5
- pipecat/services/openai/stt.py +13 -9
- pipecat/services/openai/tts.py +42 -10
- pipecat/services/openai_realtime_beta/azure.py +11 -9
- pipecat/services/openai_realtime_beta/context.py +7 -5
- pipecat/services/openai_realtime_beta/events.py +10 -7
- pipecat/services/openai_realtime_beta/openai.py +37 -18
- pipecat/services/openpipe/llm.py +30 -24
- pipecat/services/openrouter/llm.py +9 -7
- pipecat/services/perplexity/llm.py +15 -19
- pipecat/services/piper/tts.py +26 -12
- pipecat/services/playht/tts.py +227 -65
- pipecat/services/qwen/llm.py +8 -6
- pipecat/services/rime/tts.py +128 -17
- pipecat/services/riva/stt.py +160 -22
- pipecat/services/riva/tts.py +67 -2
- pipecat/services/sambanova/llm.py +19 -17
- pipecat/services/sambanova/stt.py +14 -8
- pipecat/services/sarvam/tts.py +60 -13
- pipecat/services/simli/video.py +82 -21
- pipecat/services/soniox/__init__.py +0 -0
- pipecat/services/soniox/stt.py +398 -0
- pipecat/services/speechmatics/stt.py +29 -17
- pipecat/services/stt_service.py +47 -11
- pipecat/services/tavus/video.py +94 -25
- pipecat/services/together/llm.py +8 -6
- pipecat/services/tts_service.py +77 -53
- pipecat/services/ultravox/stt.py +46 -43
- pipecat/services/vision_service.py +5 -3
- pipecat/services/websocket_service.py +12 -11
- pipecat/services/whisper/base_stt.py +58 -12
- pipecat/services/whisper/stt.py +69 -58
- pipecat/services/xtts/tts.py +59 -2
- pipecat/sync/base_notifier.py +19 -0
- pipecat/sync/event_notifier.py +24 -0
- pipecat/tests/utils.py +73 -5
- pipecat/transcriptions/language.py +24 -0
- pipecat/transports/base_input.py +112 -8
- pipecat/transports/base_output.py +235 -13
- pipecat/transports/base_transport.py +119 -0
- pipecat/transports/local/audio.py +76 -0
- pipecat/transports/local/tk.py +84 -0
- pipecat/transports/network/fastapi_websocket.py +174 -15
- pipecat/transports/network/small_webrtc.py +383 -39
- pipecat/transports/network/webrtc_connection.py +214 -8
- pipecat/transports/network/websocket_client.py +171 -1
- pipecat/transports/network/websocket_server.py +147 -9
- pipecat/transports/services/daily.py +792 -70
- pipecat/transports/services/helpers/daily_rest.py +122 -129
- pipecat/transports/services/livekit.py +339 -4
- pipecat/transports/services/tavus.py +273 -38
- pipecat/utils/asyncio/task_manager.py +92 -186
- pipecat/utils/base_object.py +83 -1
- pipecat/utils/network.py +2 -0
- pipecat/utils/string.py +114 -58
- pipecat/utils/text/base_text_aggregator.py +44 -13
- pipecat/utils/text/base_text_filter.py +46 -0
- pipecat/utils/text/markdown_text_filter.py +70 -14
- pipecat/utils/text/pattern_pair_aggregator.py +18 -14
- pipecat/utils/text/simple_text_aggregator.py +43 -2
- pipecat/utils/text/skip_tags_aggregator.py +21 -13
- pipecat/utils/time.py +36 -0
- pipecat/utils/tracing/class_decorators.py +32 -7
- pipecat/utils/tracing/conversation_context_provider.py +12 -2
- pipecat/utils/tracing/service_attributes.py +80 -64
- pipecat/utils/tracing/service_decorators.py +48 -21
- pipecat/utils/tracing/setup.py +13 -7
- pipecat/utils/tracing/turn_context_provider.py +12 -2
- pipecat/utils/tracing/turn_trace_observer.py +27 -0
- pipecat/utils/utils.py +14 -14
- dv_pipecat_ai-0.0.74.dev770.dist-info/RECORD +0 -319
- pipecat/examples/daily_runner.py +0 -64
- pipecat/examples/run.py +0 -265
- pipecat/utils/asyncio/watchdog_async_iterator.py +0 -72
- pipecat/utils/asyncio/watchdog_event.py +0 -42
- pipecat/utils/asyncio/watchdog_priority_queue.py +0 -48
- pipecat/utils/asyncio/watchdog_queue.py +0 -48
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/WHEEL +0 -0
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/licenses/LICENSE +0 -0
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/top_level.txt +0 -0
- /pipecat/{examples → extensions}/__init__.py +0 -0
|
@@ -4,11 +4,18 @@
|
|
|
4
4
|
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
5
|
#
|
|
6
6
|
|
|
7
|
+
"""Frame processing pipeline infrastructure for Pipecat.
|
|
8
|
+
|
|
9
|
+
This module provides the core frame processing system that enables building
|
|
10
|
+
audio/video processing pipelines. It includes frame processors, pipeline
|
|
11
|
+
management, and frame flow control mechanisms.
|
|
12
|
+
"""
|
|
13
|
+
|
|
7
14
|
import asyncio
|
|
8
15
|
import traceback
|
|
9
16
|
from dataclasses import dataclass
|
|
10
17
|
from enum import Enum
|
|
11
|
-
from typing import Awaitable, Callable, Coroutine, List, Optional, Sequence
|
|
18
|
+
from typing import Any, Awaitable, Callable, Coroutine, List, Optional, Sequence, Tuple
|
|
12
19
|
|
|
13
20
|
from loguru import logger
|
|
14
21
|
|
|
@@ -28,51 +35,132 @@ from pipecat.frames.frames import (
|
|
|
28
35
|
SystemFrame,
|
|
29
36
|
)
|
|
30
37
|
from pipecat.metrics.metrics import LLMTokenUsage, MetricsData
|
|
31
|
-
from pipecat.observers.base_observer import BaseObserver, FramePushed
|
|
38
|
+
from pipecat.observers.base_observer import BaseObserver, FrameProcessed, FramePushed
|
|
32
39
|
from pipecat.processors.metrics.frame_processor_metrics import FrameProcessorMetrics
|
|
33
40
|
from pipecat.utils.asyncio.task_manager import BaseTaskManager
|
|
34
|
-
from pipecat.utils.asyncio.watchdog_event import WatchdogEvent
|
|
35
|
-
from pipecat.utils.asyncio.watchdog_queue import WatchdogQueue
|
|
36
41
|
from pipecat.utils.base_object import BaseObject
|
|
37
42
|
|
|
38
43
|
|
|
39
44
|
class FrameDirection(Enum):
|
|
45
|
+
"""Direction of frame flow in the processing pipeline.
|
|
46
|
+
|
|
47
|
+
Parameters:
|
|
48
|
+
DOWNSTREAM: Frames flowing from input to output.
|
|
49
|
+
UPSTREAM: Frames flowing back from output to input.
|
|
50
|
+
"""
|
|
51
|
+
|
|
40
52
|
DOWNSTREAM = 1
|
|
41
53
|
UPSTREAM = 2
|
|
42
54
|
|
|
43
55
|
|
|
56
|
+
FrameCallback = Callable[["FrameProcessor", Frame, FrameDirection], Awaitable[None]]
|
|
57
|
+
|
|
58
|
+
|
|
44
59
|
@dataclass
|
|
45
60
|
class FrameProcessorSetup:
|
|
61
|
+
"""Configuration parameters for frame processor initialization.
|
|
62
|
+
|
|
63
|
+
Parameters:
|
|
64
|
+
clock: The clock instance for timing operations.
|
|
65
|
+
task_manager: The task manager for handling async operations.
|
|
66
|
+
observer: Optional observer for monitoring frame processing events.
|
|
67
|
+
"""
|
|
68
|
+
|
|
46
69
|
clock: BaseClock
|
|
47
70
|
task_manager: BaseTaskManager
|
|
48
71
|
observer: Optional[BaseObserver] = None
|
|
49
|
-
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class FrameProcessorQueue(asyncio.PriorityQueue):
|
|
75
|
+
"""A priority queue for systems frames and other frames.
|
|
76
|
+
|
|
77
|
+
This is a specialized queue for frame processors that separates and
|
|
78
|
+
prioritizes system frames over other frames. It ensures that `SystemFrame`
|
|
79
|
+
objects are processed before any other frames by using a priority queue.
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
HIGH_PRIORITY = 1
|
|
84
|
+
LOW_PRIORITY = 2
|
|
85
|
+
|
|
86
|
+
def __init__(self):
|
|
87
|
+
"""Initialize the FrameProcessorQueue.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
manager (BaseTaskManager): The task manager used by the internal watchdog queues.
|
|
91
|
+
|
|
92
|
+
"""
|
|
93
|
+
super().__init__()
|
|
94
|
+
self.__high_counter = 0
|
|
95
|
+
self.__low_counter = 0
|
|
96
|
+
|
|
97
|
+
async def put(self, item: Tuple[Frame, FrameDirection, FrameCallback]):
|
|
98
|
+
"""Put an item into the priority queue.
|
|
99
|
+
|
|
100
|
+
System frames (`SystemFrame`) have higher priority than any other
|
|
101
|
+
frames. If a non-frame item (e.g. a watchdog cancellation sentinel) is
|
|
102
|
+
provided it will have the highest priority.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
item (Any): The item to enqueue.
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
frame, _, _ = item
|
|
109
|
+
if isinstance(frame, SystemFrame):
|
|
110
|
+
self.__high_counter += 1
|
|
111
|
+
await super().put((self.HIGH_PRIORITY, self.__high_counter, item))
|
|
112
|
+
else:
|
|
113
|
+
self.__low_counter += 1
|
|
114
|
+
await super().put((self.LOW_PRIORITY, self.__low_counter, item))
|
|
115
|
+
|
|
116
|
+
async def get(self) -> Any:
|
|
117
|
+
"""Retrieve the next item from the queue.
|
|
118
|
+
|
|
119
|
+
System frames are prioritized. If both queues are empty, this method
|
|
120
|
+
waits until an item is available.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Any: The next item from the system or main queue.
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
_, _, item = await super().get()
|
|
127
|
+
return item
|
|
50
128
|
|
|
51
129
|
|
|
52
130
|
class FrameProcessor(BaseObject):
|
|
131
|
+
"""Base class for all frame processors in the pipeline.
|
|
132
|
+
|
|
133
|
+
Frame processors are the building blocks of Pipecat pipelines, they can be
|
|
134
|
+
linked to form complex processing pipelines. They receive frames, process
|
|
135
|
+
them, and pass them to the next or previous processor in the chain. Each
|
|
136
|
+
frame processor guarantees frame ordering and processes frames in its own
|
|
137
|
+
task. System frames are also processed in a separate task which guarantees
|
|
138
|
+
frame priority.
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
|
|
53
142
|
def __init__(
|
|
54
143
|
self,
|
|
55
144
|
*,
|
|
56
145
|
name: Optional[str] = None,
|
|
57
|
-
|
|
58
|
-
enable_watchdog_timers: Optional[bool] = None,
|
|
146
|
+
enable_direct_mode: bool = False,
|
|
59
147
|
metrics: Optional[FrameProcessorMetrics] = None,
|
|
60
|
-
watchdog_timeout_secs: Optional[float] = None,
|
|
61
148
|
**kwargs,
|
|
62
149
|
):
|
|
63
|
-
|
|
64
|
-
|
|
150
|
+
"""Initialize the frame processor.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
name: Optional name for this processor instance.
|
|
154
|
+
enable_direct_mode: Whether to process frames immediately or use internal queues.
|
|
155
|
+
metrics: Optional metrics collector for this processor.
|
|
156
|
+
**kwargs: Additional arguments passed to parent class.
|
|
157
|
+
"""
|
|
158
|
+
super().__init__(name=name, **kwargs)
|
|
65
159
|
self._prev: Optional["FrameProcessor"] = None
|
|
66
160
|
self._next: Optional["FrameProcessor"] = None
|
|
67
161
|
|
|
68
|
-
# Enable
|
|
69
|
-
self.
|
|
70
|
-
|
|
71
|
-
# Enable watchdog logging for all tasks created by this frame processor.
|
|
72
|
-
self._enable_watchdog_logging = enable_watchdog_logging
|
|
73
|
-
|
|
74
|
-
# Allow this frame processor to control their tasks timeout.
|
|
75
|
-
self._watchdog_timeout_secs = watchdog_timeout_secs
|
|
162
|
+
# Enable direct mode to skip queues and process frames right away.
|
|
163
|
+
self._enable_direct_mode = enable_direct_mode
|
|
76
164
|
|
|
77
165
|
# Clock
|
|
78
166
|
self._clock: Optional[BaseClock] = None
|
|
@@ -104,201 +192,396 @@ class FrameProcessor(BaseObject):
|
|
|
104
192
|
self._metrics = metrics or FrameProcessorMetrics()
|
|
105
193
|
self._metrics.set_processor_name(self.name)
|
|
106
194
|
|
|
107
|
-
# Processors have an input queue
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
195
|
+
# Processors have an input priority queue which stores any type of
|
|
196
|
+
# frames in order. System frames have higher priority than any other
|
|
197
|
+
# frames, so they will be returned first from the queue.
|
|
198
|
+
#
|
|
199
|
+
# If a system frame is obtained it will be processed immediately any
|
|
200
|
+
# other type of frame (data and control) will be put in a separate queue
|
|
201
|
+
# for later processing. This guarantees that each frame processor will
|
|
202
|
+
# always process system frames before any other frame in the queue.
|
|
203
|
+
|
|
204
|
+
# The input task that handles all types of frames. It processes system
|
|
205
|
+
# frames right away and queues non-system frames for later processing.
|
|
206
|
+
self.__should_block_system_frames = False
|
|
207
|
+
self.__input_event: Optional[asyncio.Event] = None
|
|
113
208
|
self.__input_frame_task: Optional[asyncio.Task] = None
|
|
114
209
|
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
|
|
210
|
+
# The process task processes non-system frames. Non-system frames will
|
|
211
|
+
# be processed as soon as they are received by the processing task
|
|
212
|
+
# (default) or they will block if `pause_processing_frames()` is
|
|
213
|
+
# called. To resume processing frames we need to call
|
|
214
|
+
# `resume_processing_frames()` which will wake up the event.
|
|
215
|
+
self.__should_block_frames = False
|
|
216
|
+
self.__process_event: Optional[asyncio.Event] = None
|
|
217
|
+
self.__process_frame_task: Optional[asyncio.Task] = None
|
|
119
218
|
self.logger = logger # Will later be replaced with a bound logger
|
|
120
219
|
|
|
121
220
|
@property
|
|
122
221
|
def id(self) -> int:
|
|
222
|
+
"""Get the unique identifier for this processor.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
The unique integer ID of this processor.
|
|
226
|
+
"""
|
|
123
227
|
return self._id
|
|
124
228
|
|
|
125
229
|
@property
|
|
126
230
|
def name(self) -> str:
|
|
231
|
+
"""Get the name of this processor.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
The name of this processor instance.
|
|
235
|
+
"""
|
|
127
236
|
return self._name
|
|
128
237
|
|
|
238
|
+
@property
|
|
239
|
+
def processors(self) -> List["FrameProcessor"]:
|
|
240
|
+
"""Return the list of sub-processors contained within this processor.
|
|
241
|
+
|
|
242
|
+
Only compound processors (e.g. pipelines and parallel pipelines) have
|
|
243
|
+
sub-processors. Non-compound processors will return an empty list.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
The list of sub-processors if this is a compound processor.
|
|
247
|
+
"""
|
|
248
|
+
return []
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def entry_processors(self) -> List["FrameProcessor"]:
|
|
252
|
+
"""Return the list of entry processors for this processor.
|
|
253
|
+
|
|
254
|
+
Entry processors are the first processors in a compound processor
|
|
255
|
+
(e.g. pipelines, parallel pipelines). Note that pipelines can also be an
|
|
256
|
+
entry processor as pipelines are processors themselves. Non-compound
|
|
257
|
+
processors will simply return an empty list.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
The list of entry processors.
|
|
261
|
+
"""
|
|
262
|
+
return []
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def next(self) -> Optional["FrameProcessor"]:
|
|
266
|
+
"""Get the next processor.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
The next processor, or None if there's no next processor.
|
|
270
|
+
"""
|
|
271
|
+
return self._next
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def previous(self) -> Optional["FrameProcessor"]:
|
|
275
|
+
"""Get the previous processor.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
The previous processor, or None if there's no previous processor.
|
|
279
|
+
"""
|
|
280
|
+
return self._prev
|
|
281
|
+
|
|
129
282
|
@property
|
|
130
283
|
def interruptions_allowed(self):
|
|
284
|
+
"""Check if interruptions are allowed for this processor.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
True if interruptions are allowed.
|
|
288
|
+
"""
|
|
131
289
|
return self._allow_interruptions
|
|
132
290
|
|
|
133
291
|
@property
|
|
134
292
|
def metrics_enabled(self):
|
|
293
|
+
"""Check if metrics collection is enabled.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
True if metrics collection is enabled.
|
|
297
|
+
"""
|
|
135
298
|
return self._enable_metrics
|
|
136
299
|
|
|
137
300
|
@property
|
|
138
301
|
def usage_metrics_enabled(self):
|
|
302
|
+
"""Check if usage metrics collection is enabled.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
True if usage metrics collection is enabled.
|
|
306
|
+
"""
|
|
139
307
|
return self._enable_usage_metrics
|
|
140
308
|
|
|
141
309
|
@property
|
|
142
310
|
def report_only_initial_ttfb(self):
|
|
311
|
+
"""Check if only initial TTFB should be reported.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
True if only initial time-to-first-byte should be reported.
|
|
315
|
+
"""
|
|
143
316
|
return self._report_only_initial_ttfb
|
|
144
317
|
|
|
145
318
|
@property
|
|
146
319
|
def interruption_strategies(self) -> Sequence[BaseInterruptionStrategy]:
|
|
320
|
+
"""Get the interruption strategies for this processor.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Sequence of interruption strategies.
|
|
324
|
+
"""
|
|
147
325
|
return self._interruption_strategies
|
|
148
326
|
|
|
149
327
|
@property
|
|
150
328
|
def task_manager(self) -> BaseTaskManager:
|
|
329
|
+
"""Get the task manager for this processor.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
The task manager instance.
|
|
333
|
+
|
|
334
|
+
Raises:
|
|
335
|
+
Exception: If the task manager is not initialized.
|
|
336
|
+
"""
|
|
151
337
|
if not self._task_manager:
|
|
152
338
|
raise Exception(f"{self} TaskManager is still not initialized.")
|
|
153
339
|
return self._task_manager
|
|
154
340
|
|
|
341
|
+
def processors_with_metrics(self):
|
|
342
|
+
"""Return processors that can generate metrics.
|
|
343
|
+
|
|
344
|
+
Recursively collects all processors that support metrics generation,
|
|
345
|
+
including those from nested processors.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
List of frame processors that can generate metrics.
|
|
349
|
+
"""
|
|
350
|
+
return []
|
|
351
|
+
|
|
155
352
|
def can_generate_metrics(self) -> bool:
|
|
353
|
+
"""Check if this processor can generate metrics.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
True if this processor can generate metrics.
|
|
357
|
+
"""
|
|
156
358
|
return False
|
|
157
359
|
|
|
158
360
|
def set_core_metrics_data(self, data: MetricsData):
|
|
361
|
+
"""Set core metrics data for this processor.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
data: The metrics data to set.
|
|
365
|
+
"""
|
|
159
366
|
self._metrics.set_core_metrics_data(data)
|
|
160
367
|
|
|
161
368
|
async def start_ttfb_metrics(self):
|
|
369
|
+
"""Start time-to-first-byte metrics collection."""
|
|
162
370
|
if self.can_generate_metrics() and self.metrics_enabled:
|
|
163
371
|
await self._metrics.start_ttfb_metrics(self._report_only_initial_ttfb)
|
|
164
372
|
|
|
165
373
|
async def stop_ttfb_metrics(self):
|
|
374
|
+
"""Stop time-to-first-byte metrics collection and push results."""
|
|
166
375
|
if self.can_generate_metrics() and self.metrics_enabled:
|
|
167
376
|
frame = await self._metrics.stop_ttfb_metrics()
|
|
168
377
|
if frame:
|
|
169
378
|
await self.push_frame(frame)
|
|
170
379
|
|
|
171
380
|
async def start_processing_metrics(self):
|
|
381
|
+
"""Start processing metrics collection."""
|
|
172
382
|
if self.can_generate_metrics() and self.metrics_enabled:
|
|
173
383
|
await self._metrics.start_processing_metrics()
|
|
174
384
|
|
|
175
385
|
async def stop_processing_metrics(self):
|
|
386
|
+
"""Stop processing metrics collection and push results."""
|
|
176
387
|
if self.can_generate_metrics() and self.metrics_enabled:
|
|
177
388
|
frame = await self._metrics.stop_processing_metrics()
|
|
178
389
|
if frame:
|
|
179
390
|
await self.push_frame(frame)
|
|
180
391
|
|
|
181
392
|
async def start_llm_usage_metrics(self, tokens: LLMTokenUsage):
|
|
393
|
+
"""Start LLM usage metrics collection.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
tokens: Token usage information for the LLM.
|
|
397
|
+
"""
|
|
182
398
|
if self.can_generate_metrics() and self.usage_metrics_enabled:
|
|
183
399
|
frame = await self._metrics.start_llm_usage_metrics(tokens)
|
|
184
400
|
if frame:
|
|
185
401
|
await self.push_frame(frame)
|
|
186
402
|
|
|
187
403
|
async def start_tts_usage_metrics(self, text: str):
|
|
404
|
+
"""Start TTS usage metrics collection.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
text: The text being processed by TTS.
|
|
408
|
+
"""
|
|
188
409
|
if self.can_generate_metrics() and self.usage_metrics_enabled:
|
|
189
410
|
frame = await self._metrics.start_tts_usage_metrics(text)
|
|
190
411
|
if frame:
|
|
191
412
|
await self.push_frame(frame)
|
|
192
413
|
|
|
193
414
|
async def stop_all_metrics(self):
|
|
415
|
+
"""Stop all active metrics collection."""
|
|
194
416
|
await self.stop_ttfb_metrics()
|
|
195
417
|
await self.stop_processing_metrics()
|
|
196
418
|
|
|
197
|
-
def create_task(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
419
|
+
def create_task(self, coroutine: Coroutine, name: Optional[str] = None) -> asyncio.Task:
|
|
420
|
+
"""Create a new task managed by this processor.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
coroutine: The coroutine to run in the task.
|
|
424
|
+
name: Optional name for the task.
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
The created asyncio task.
|
|
428
|
+
"""
|
|
206
429
|
if name:
|
|
207
430
|
name = f"{self}::{name}"
|
|
208
431
|
else:
|
|
209
432
|
name = f"{self}::{coroutine.cr_code.co_name}"
|
|
210
|
-
return self.task_manager.create_task(
|
|
211
|
-
coroutine,
|
|
212
|
-
name,
|
|
213
|
-
enable_watchdog_logging=(
|
|
214
|
-
enable_watchdog_logging
|
|
215
|
-
if enable_watchdog_logging
|
|
216
|
-
else self._enable_watchdog_logging
|
|
217
|
-
),
|
|
218
|
-
enable_watchdog_timers=(
|
|
219
|
-
enable_watchdog_timers if enable_watchdog_timers else self._enable_watchdog_timers
|
|
220
|
-
),
|
|
221
|
-
watchdog_timeout=(
|
|
222
|
-
watchdog_timeout_secs if watchdog_timeout_secs else self._watchdog_timeout_secs
|
|
223
|
-
),
|
|
224
|
-
)
|
|
433
|
+
return self.task_manager.create_task(coroutine, name)
|
|
225
434
|
|
|
226
435
|
async def cancel_task(self, task: asyncio.Task, timeout: Optional[float] = None):
|
|
436
|
+
"""Cancel a task managed by this processor.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
task: The task to cancel.
|
|
440
|
+
timeout: Optional timeout for task cancellation.
|
|
441
|
+
"""
|
|
227
442
|
await self.task_manager.cancel_task(task, timeout)
|
|
228
443
|
|
|
229
444
|
async def wait_for_task(self, task: asyncio.Task, timeout: Optional[float] = None):
|
|
230
|
-
|
|
445
|
+
"""Wait for a task to complete.
|
|
446
|
+
|
|
447
|
+
.. deprecated:: 0.0.81
|
|
448
|
+
This function is deprecated, use `await task` or
|
|
449
|
+
`await asyncio.wait_for(task, timeout) instead.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
task: The task to wait for.
|
|
453
|
+
timeout: Optional timeout for waiting.
|
|
454
|
+
"""
|
|
455
|
+
import warnings
|
|
456
|
+
|
|
457
|
+
warnings.warn(
|
|
458
|
+
"`FrameProcessor.wait_for_task()` is deprecated. "
|
|
459
|
+
"Use `await task` or `await asyncio.wait_for(task, timeout)` instead.",
|
|
460
|
+
DeprecationWarning,
|
|
461
|
+
stacklevel=2,
|
|
462
|
+
)
|
|
231
463
|
|
|
232
|
-
|
|
233
|
-
|
|
464
|
+
if timeout:
|
|
465
|
+
await asyncio.wait_for(task, timeout)
|
|
466
|
+
else:
|
|
467
|
+
await task
|
|
234
468
|
|
|
235
469
|
async def setup(self, setup: FrameProcessorSetup):
|
|
470
|
+
"""Set up the processor with required components.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
setup: Configuration object containing setup parameters.
|
|
474
|
+
"""
|
|
236
475
|
self._clock = setup.clock
|
|
237
476
|
self._task_manager = setup.task_manager
|
|
238
477
|
self._observer = setup.observer
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
)
|
|
478
|
+
|
|
479
|
+
# Create processing tasks.
|
|
480
|
+
self.__create_input_task()
|
|
481
|
+
|
|
244
482
|
if self._metrics is not None:
|
|
245
483
|
await self._metrics.setup(self._task_manager)
|
|
246
484
|
|
|
247
485
|
async def cleanup(self):
|
|
486
|
+
"""Clean up processor resources."""
|
|
248
487
|
await super().cleanup()
|
|
249
488
|
await self.__cancel_input_task()
|
|
250
|
-
await self.
|
|
489
|
+
await self.__cancel_process_task()
|
|
251
490
|
if self._metrics is not None:
|
|
252
491
|
await self._metrics.cleanup()
|
|
253
492
|
|
|
254
493
|
def link(self, processor: "FrameProcessor"):
|
|
494
|
+
"""Link this processor to the next processor in the pipeline.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
processor: The processor to link to.
|
|
498
|
+
"""
|
|
255
499
|
self._next = processor
|
|
256
500
|
processor._prev = self
|
|
257
501
|
self.logger.debug(f"Linking {self} -> {self._next}")
|
|
258
502
|
|
|
259
|
-
def
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
def set_parent(self, parent: "FrameProcessor"):
|
|
263
|
-
self._parent = parent
|
|
503
|
+
def get_clock(self) -> BaseClock:
|
|
504
|
+
"""Get the clock used by this processor.
|
|
264
505
|
|
|
265
|
-
|
|
266
|
-
|
|
506
|
+
Returns:
|
|
507
|
+
The clock instance.
|
|
267
508
|
|
|
268
|
-
|
|
509
|
+
Raises:
|
|
510
|
+
Exception: If the clock is not initialized.
|
|
511
|
+
"""
|
|
269
512
|
if not self._clock:
|
|
270
513
|
raise Exception(f"{self} Clock is still not initialized.")
|
|
271
514
|
return self._clock
|
|
272
515
|
|
|
516
|
+
def get_event_loop(self) -> asyncio.AbstractEventLoop:
|
|
517
|
+
"""Get the event loop used by this processor.
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
The asyncio event loop.
|
|
521
|
+
"""
|
|
522
|
+
return self.task_manager.get_event_loop()
|
|
523
|
+
|
|
273
524
|
async def queue_frame(
|
|
274
525
|
self,
|
|
275
526
|
frame: Frame,
|
|
276
527
|
direction: FrameDirection = FrameDirection.DOWNSTREAM,
|
|
277
|
-
callback: Optional[
|
|
278
|
-
Callable[["FrameProcessor", Frame, FrameDirection], Awaitable[None]]
|
|
279
|
-
] = None,
|
|
528
|
+
callback: Optional[FrameCallback] = None,
|
|
280
529
|
):
|
|
530
|
+
"""Queue a frame for processing.
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
frame: The frame to queue.
|
|
534
|
+
direction: The direction of frame flow.
|
|
535
|
+
callback: Optional callback to call after processing.
|
|
536
|
+
"""
|
|
281
537
|
# If we are cancelling we don't want to process any other frame.
|
|
282
538
|
if self._cancelling:
|
|
283
539
|
return
|
|
284
540
|
|
|
285
|
-
if
|
|
286
|
-
|
|
287
|
-
await self.process_frame(frame, direction)
|
|
541
|
+
if self._enable_direct_mode:
|
|
542
|
+
await self.__process_frame(frame, direction, callback)
|
|
288
543
|
else:
|
|
289
|
-
# We queue everything else.
|
|
290
544
|
await self.__input_queue.put((frame, direction, callback))
|
|
291
545
|
|
|
292
546
|
async def pause_processing_frames(self):
|
|
547
|
+
"""Pause processing of queued frames."""
|
|
293
548
|
self.logger.trace(f"{self}: pausing frame processing")
|
|
294
549
|
self.__should_block_frames = True
|
|
295
550
|
|
|
551
|
+
async def pause_processing_system_frames(self):
|
|
552
|
+
"""Pause processing of queued system frames."""
|
|
553
|
+
logger.trace(f"{self}: pausing system frame processing")
|
|
554
|
+
self.__should_block_system_frames = True
|
|
555
|
+
|
|
296
556
|
async def resume_processing_frames(self):
|
|
557
|
+
"""Resume processing of queued frames."""
|
|
297
558
|
self.logger.trace(f"{self}: resuming frame processing")
|
|
559
|
+
if self.__process_event:
|
|
560
|
+
self.__process_event.set()
|
|
561
|
+
|
|
562
|
+
async def resume_processing_system_frames(self):
|
|
563
|
+
"""Resume processing of queued system frames."""
|
|
564
|
+
self.logger.trace(f"{self}: resuming system frame processing")
|
|
298
565
|
if self.__input_event:
|
|
299
566
|
self.__input_event.set()
|
|
300
567
|
|
|
301
568
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
|
569
|
+
"""Process a frame.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
frame: The frame to process.
|
|
573
|
+
direction: The direction of frame flow.
|
|
574
|
+
"""
|
|
575
|
+
if self._observer:
|
|
576
|
+
timestamp = self._clock.get_time() if self._clock else 0
|
|
577
|
+
data = FrameProcessed(
|
|
578
|
+
processor=self,
|
|
579
|
+
frame=frame,
|
|
580
|
+
direction=direction,
|
|
581
|
+
timestamp=timestamp,
|
|
582
|
+
)
|
|
583
|
+
await self._observer.on_process_frame(data)
|
|
584
|
+
|
|
302
585
|
if isinstance(frame, StartFrame):
|
|
303
586
|
await self.__start(frame)
|
|
304
587
|
elif isinstance(frame, StartInterruptionFrame):
|
|
@@ -314,18 +597,33 @@ class FrameProcessor(BaseObject):
|
|
|
314
597
|
await self.__resume(frame)
|
|
315
598
|
|
|
316
599
|
async def push_error(self, error: ErrorFrame):
|
|
600
|
+
"""Push an error frame upstream.
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
error: The error frame to push.
|
|
604
|
+
"""
|
|
605
|
+
if not error.processor:
|
|
606
|
+
error.processor = self
|
|
317
607
|
await self.push_frame(error, FrameDirection.UPSTREAM)
|
|
318
608
|
|
|
319
609
|
async def push_frame(self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM):
|
|
610
|
+
"""Push a frame to the next processor in the pipeline.
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
frame: The frame to push.
|
|
614
|
+
direction: The direction to push the frame.
|
|
615
|
+
"""
|
|
320
616
|
if not self._check_started(frame):
|
|
321
617
|
return
|
|
322
618
|
|
|
323
|
-
|
|
324
|
-
await self.__internal_push_frame(frame, direction)
|
|
325
|
-
else:
|
|
326
|
-
await self.__push_queue.put((frame, direction))
|
|
619
|
+
await self.__internal_push_frame(frame, direction)
|
|
327
620
|
|
|
328
621
|
async def __start(self, frame: StartFrame):
|
|
622
|
+
"""Handle the start frame to initialize processor state.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
frame: The start frame containing initialization parameters.
|
|
626
|
+
"""
|
|
329
627
|
self.__started = True
|
|
330
628
|
self._allow_interruptions = frame.allow_interruptions
|
|
331
629
|
self._enable_metrics = frame.enable_metrics
|
|
@@ -336,19 +634,32 @@ class FrameProcessor(BaseObject):
|
|
|
336
634
|
self.logger = logger.bind(call_id=frame.metadata["call_id"])
|
|
337
635
|
self._metrics.set_logger(self.logger)
|
|
338
636
|
self._report_only_initial_ttfb = frame.report_only_initial_ttfb
|
|
339
|
-
self.
|
|
340
|
-
self.__create_push_task()
|
|
637
|
+
self.__create_process_task()
|
|
341
638
|
|
|
342
639
|
async def __cancel(self, frame: CancelFrame):
|
|
640
|
+
"""Handle the cancel frame to stop processor operation.
|
|
641
|
+
|
|
642
|
+
Args:
|
|
643
|
+
frame: The cancel frame.
|
|
644
|
+
"""
|
|
343
645
|
self._cancelling = True
|
|
344
|
-
await self.
|
|
345
|
-
await self.__cancel_push_task()
|
|
646
|
+
await self.__cancel_process_task()
|
|
346
647
|
|
|
347
648
|
async def __pause(self, frame: FrameProcessorPauseFrame | FrameProcessorPauseUrgentFrame):
|
|
649
|
+
"""Handle pause frame to pause processor operation.
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
frame: The pause frame.
|
|
653
|
+
"""
|
|
348
654
|
if frame.processor.name == self.name:
|
|
349
655
|
await self.pause_processing_frames()
|
|
350
656
|
|
|
351
657
|
async def __resume(self, frame: FrameProcessorResumeFrame | FrameProcessorResumeUrgentFrame):
|
|
658
|
+
"""Handle resume frame to resume processor operation.
|
|
659
|
+
|
|
660
|
+
Args:
|
|
661
|
+
frame: The resume frame.
|
|
662
|
+
"""
|
|
352
663
|
if frame.processor.name == self.name:
|
|
353
664
|
await self.resume_processing_frames()
|
|
354
665
|
|
|
@@ -357,29 +668,31 @@ class FrameProcessor(BaseObject):
|
|
|
357
668
|
#
|
|
358
669
|
|
|
359
670
|
async def _start_interruption(self):
|
|
671
|
+
"""Start handling an interruption by cancelling current tasks."""
|
|
360
672
|
try:
|
|
361
|
-
# Cancel the
|
|
362
|
-
await self.
|
|
363
|
-
|
|
364
|
-
# Cancel the input task. This will stop processing queued frames.
|
|
365
|
-
await self.__cancel_input_task()
|
|
673
|
+
# Cancel the process task. This will stop processing queued frames.
|
|
674
|
+
await self.__cancel_process_task()
|
|
366
675
|
except Exception as e:
|
|
367
676
|
self.logger.exception(
|
|
368
677
|
f"Uncaught exception in {self} when handling _start_interruption: {e}"
|
|
369
678
|
)
|
|
370
679
|
await self.push_error(ErrorFrame(str(e)))
|
|
371
680
|
|
|
372
|
-
# Create a new
|
|
373
|
-
self.
|
|
374
|
-
|
|
375
|
-
# Create a new output queue and task.
|
|
376
|
-
self.__create_push_task()
|
|
681
|
+
# Create a new process queue and task.
|
|
682
|
+
self.__create_process_task()
|
|
377
683
|
|
|
378
684
|
async def _stop_interruption(self):
|
|
685
|
+
"""Stop handling an interruption."""
|
|
379
686
|
# Nothing to do right now.
|
|
380
687
|
pass
|
|
381
688
|
|
|
382
689
|
async def __internal_push_frame(self, frame: Frame, direction: FrameDirection):
|
|
690
|
+
"""Internal method to push frames to adjacent processors.
|
|
691
|
+
|
|
692
|
+
Args:
|
|
693
|
+
frame: The frame to push.
|
|
694
|
+
direction: The direction to push the frame.
|
|
695
|
+
"""
|
|
383
696
|
try:
|
|
384
697
|
timestamp = self._clock.get_time() if self._clock else 0
|
|
385
698
|
if direction == FrameDirection.DOWNSTREAM and self._next:
|
|
@@ -415,60 +728,104 @@ class FrameProcessor(BaseObject):
|
|
|
415
728
|
await self.push_error(ErrorFrame(str(e)))
|
|
416
729
|
|
|
417
730
|
def _check_started(self, frame: Frame):
|
|
731
|
+
"""Check if the processor has been started.
|
|
732
|
+
|
|
733
|
+
Args:
|
|
734
|
+
frame: The frame being processed.
|
|
735
|
+
|
|
736
|
+
Returns:
|
|
737
|
+
True if the processor has been started.
|
|
738
|
+
"""
|
|
418
739
|
if not self.__started:
|
|
419
740
|
logger.error(f"{self} Trying to process {frame} but StartFrame not received yet")
|
|
420
741
|
return self.__started
|
|
421
742
|
|
|
422
743
|
def __create_input_task(self):
|
|
744
|
+
"""Create the frame input processing task."""
|
|
745
|
+
if self._enable_direct_mode:
|
|
746
|
+
return
|
|
747
|
+
|
|
423
748
|
if not self.__input_frame_task:
|
|
424
|
-
self.
|
|
425
|
-
|
|
426
|
-
self.__input_event = WatchdogEvent(self.task_manager)
|
|
427
|
-
self.__input_event.clear()
|
|
428
|
-
self.__input_queue = WatchdogQueue(self.task_manager)
|
|
749
|
+
self.__input_event = asyncio.Event()
|
|
750
|
+
self.__input_queue = FrameProcessorQueue()
|
|
429
751
|
self.__input_frame_task = self.create_task(self.__input_frame_task_handler())
|
|
430
752
|
|
|
431
753
|
async def __cancel_input_task(self):
|
|
754
|
+
"""Cancel the frame input processing task."""
|
|
432
755
|
if self.__input_frame_task:
|
|
433
756
|
await self.cancel_task(self.__input_frame_task)
|
|
434
757
|
self.__input_frame_task = None
|
|
435
758
|
|
|
759
|
+
def __create_process_task(self):
|
|
760
|
+
"""Create the non-system frame processing task."""
|
|
761
|
+
if self._enable_direct_mode:
|
|
762
|
+
return
|
|
763
|
+
|
|
764
|
+
if not self.__process_frame_task:
|
|
765
|
+
self.__should_block_frames = False
|
|
766
|
+
self.__process_event = asyncio.Event()
|
|
767
|
+
self.__process_queue = asyncio.Queue()
|
|
768
|
+
self.__process_frame_task = self.create_task(self.__process_frame_task_handler())
|
|
769
|
+
|
|
770
|
+
async def __cancel_process_task(self):
|
|
771
|
+
"""Cancel the non-system frame processing task."""
|
|
772
|
+
if self.__process_frame_task:
|
|
773
|
+
await self.cancel_task(self.__process_frame_task)
|
|
774
|
+
self.__process_frame_task = None
|
|
775
|
+
|
|
776
|
+
async def __process_frame(
|
|
777
|
+
self, frame: Frame, direction: FrameDirection, callback: Optional[FrameCallback]
|
|
778
|
+
):
|
|
779
|
+
try:
|
|
780
|
+
# Process the frame.
|
|
781
|
+
await self.process_frame(frame, direction)
|
|
782
|
+
# If this frame has an associated callback, call it now.
|
|
783
|
+
if callback:
|
|
784
|
+
await callback(self, frame, direction)
|
|
785
|
+
except Exception as e:
|
|
786
|
+
logger.exception(f"{self}: error processing frame: {e}")
|
|
787
|
+
await self.push_error(ErrorFrame(str(e)))
|
|
788
|
+
|
|
436
789
|
async def __input_frame_task_handler(self):
|
|
437
|
-
"""Handle frames from the input queue.
|
|
438
|
-
|
|
790
|
+
"""Handle frames from the input queue.
|
|
791
|
+
|
|
792
|
+
It only processes system frames. Other frames are queue for another task
|
|
793
|
+
to execute.
|
|
794
|
+
|
|
795
|
+
"""
|
|
439
796
|
while True:
|
|
440
|
-
if self.
|
|
441
|
-
logger.trace(f"{self}: frame processing paused")
|
|
797
|
+
if self.__should_block_system_frames and self.__input_event:
|
|
798
|
+
logger.trace(f"{self}: system frame processing paused")
|
|
442
799
|
await self.__input_event.wait()
|
|
443
800
|
self.__input_event.clear()
|
|
801
|
+
self.__should_block_system_frames = False
|
|
802
|
+
logger.trace(f"{self}: system frame processing resumed")
|
|
803
|
+
|
|
804
|
+
(frame, direction, callback) = await self.__input_queue.get()
|
|
805
|
+
|
|
806
|
+
if isinstance(frame, SystemFrame):
|
|
807
|
+
await self.__process_frame(frame, direction, callback)
|
|
808
|
+
elif self.__process_queue:
|
|
809
|
+
await self.__process_queue.put((frame, direction, callback))
|
|
810
|
+
else:
|
|
811
|
+
raise RuntimeError(
|
|
812
|
+
f"{self}: __process_queue is None when processing frame {frame.name}"
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
self.__input_queue.task_done()
|
|
816
|
+
|
|
817
|
+
async def __process_frame_task_handler(self):
|
|
818
|
+
"""Handle non-system frames from the process queue."""
|
|
819
|
+
while True:
|
|
820
|
+
if self.__should_block_frames and self.__process_event:
|
|
821
|
+
logger.trace(f"{self}: frame processing paused")
|
|
822
|
+
await self.__process_event.wait()
|
|
823
|
+
self.__process_event.clear()
|
|
444
824
|
self.__should_block_frames = False
|
|
445
825
|
logger.trace(f"{self}: frame processing resumed")
|
|
446
826
|
|
|
447
|
-
(frame, direction, callback) = await
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
if callback:
|
|
453
|
-
await callback(self, frame, direction)
|
|
454
|
-
except Exception as e:
|
|
455
|
-
self.logger.exception(f"{self}: error processing frame: {e}")
|
|
456
|
-
await self.push_error(ErrorFrame(str(e)))
|
|
457
|
-
finally:
|
|
458
|
-
my_queue.task_done()
|
|
459
|
-
|
|
460
|
-
def __create_push_task(self):
|
|
461
|
-
if not self.__push_frame_task:
|
|
462
|
-
self.__push_queue = WatchdogQueue(self.task_manager)
|
|
463
|
-
self.__push_frame_task = self.create_task(self.__push_frame_task_handler())
|
|
464
|
-
|
|
465
|
-
async def __cancel_push_task(self):
|
|
466
|
-
if self.__push_frame_task:
|
|
467
|
-
await self.cancel_task(self.__push_frame_task)
|
|
468
|
-
self.__push_frame_task = None
|
|
469
|
-
|
|
470
|
-
async def __push_frame_task_handler(self):
|
|
471
|
-
while True:
|
|
472
|
-
(frame, direction) = await self.__push_queue.get()
|
|
473
|
-
await self.__internal_push_frame(frame, direction)
|
|
474
|
-
self.__push_queue.task_done()
|
|
827
|
+
(frame, direction, callback) = await self.__process_queue.get()
|
|
828
|
+
|
|
829
|
+
await self.__process_frame(frame, direction, callback)
|
|
830
|
+
|
|
831
|
+
self.__process_queue.task_done()
|