Deadmon commited on
Commit
5bbbe8d
·
verified ·
1 Parent(s): 60ddc02

Create main.py

Browse files
Files changed (1) hide show
  1. main.py +216 -0
main.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import os
3
+ import sys
4
+ import time
5
+ import logging
6
+ from pipecat.frames.frames import (
7
+ TextFrame, AudioFrame, CallEndEvent, UserStartedSpeakingFrame,
8
+ UserStoppedSpeakingFrame, LLMResponseStartFrame, LLMResponseEndFrame,
9
+ TTSStartedFrame, TTSEndFrame
10
+ )
11
+ from pipecat.pipeline.pipeline import Pipeline
12
+ from pipecat.pipeline.runner import PipelineRunner
13
+ from pipecat.pipeline.task import PipelineParams
14
+ from pipecat.processors.frame_processor import FrameProcessor, FrameDirection
15
+ from pipecat.services.elevenlabs import ElevenLabsTTSService
16
+ from pipecat.services.deepgram import DeepgramSTTService
17
+ from pipecat.transports.services.daily import DailyParams, DailyTransport
18
+ from pipecat.vad.silero import SileroVADAnalyzer
19
+ from azure_openai import AzureOpenAILLMService
20
+ from elevenlabs import ElevenLabs
21
+
22
+ # Configure logging
23
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Configuration constants
27
+ SILENCE_TIMEOUT_SECONDS = float(os.environ.get("SILENCE_TIMEOUT_SECONDS", 10))
28
+ MAX_SILENCE_PROMPTS = int(os.environ.get("MAX_SILENCE_PROMPTS", 3))
29
+ SILENCE_PROMPT_TEXT = "Are you still there?"
30
+ GOODBYE_PROMPT_TEXT = "It seems you're no longer there. I'm hanging up now. Goodbye."
31
+
32
+ class SilenceAndCallLogicProcessor(FrameProcessor):
33
+ def __init__(self, tts_service, pipeline, app):
34
+ super().__init__()
35
+ self.tts_service = tts_service
36
+ self.pipeline = pipeline
37
+ self.app = app
38
+ self.last_activity_ts = time.time()
39
+ self.silence_prompts_count = 0
40
+ self._bot_is_speaking = False
41
+ self._silence_check_task = None
42
+ self.app.current_call_stats["silence_events"] = 0
43
+
44
+ async def start(self):
45
+ self.last_activity_ts = time.time()
46
+ self.silence_prompts_count = 0
47
+ self._bot_is_speaking = False
48
+ if self._silence_check_task:
49
+ self._silence_check_task.cancel()
50
+ self._silence_check_task = asyncio.create_task(self._check_silence_loop())
51
+
52
+ async def stop(self):
53
+ if self._silence_check_task:
54
+ self._silence_check_task.cancel()
55
+ try:
56
+ await self._silence_check_task
57
+ except asyncio.CancelledError:
58
+ pass
59
+ await self.tts_service.stop()
60
+
61
+ def _reset_activity_timer(self):
62
+ self.last_activity_ts = time.time()
63
+ self.silence_prompts_count = 0
64
+
65
+ async def process_frame(self, frame: Frame, direction: FrameDirection):
66
+ if isinstance(frame, (UserStartedSpeakingFrame, TextFrame)) and direction == FrameDirection.UPSTREAM:
67
+ self._reset_activity_timer()
68
+ if isinstance(frame, (LLMResponseStartFrame, TTSStartedFrame)) and direction == FrameDirection.DOWNSTREAM:
69
+ self._bot_is_speaking = True
70
+ elif isinstance(frame, (LLMResponseEndFrame, TTSEndFrame)) and direction == FrameDirection.DOWNSTREAM:
71
+ self._bot_is_speaking = False
72
+ self.last_activity_ts = time.time()
73
+ await self.push_frame(frame, direction)
74
+
75
+ async def _check_silence_loop(self):
76
+ while True:
77
+ await asyncio.sleep(1)
78
+ if self._bot_is_speaking:
79
+ continue
80
+ if time.time() - self.last_activity_ts > SILENCE_TIMEOUT_SECONDS:
81
+ self.app.current_call_stats["silence_events"] += 1
82
+ self.silence_prompts_count += 1
83
+ self._bot_is_speaking = True
84
+ if self.silence_prompts_count >= MAX_SILENCE_PROMPTS:
85
+ await self.push_frame(TextFrame(GOODBYE_PROMPT_TEXT), FrameDirection.DOWNSTREAM)
86
+ await asyncio.sleep(2)
87
+ await self.pipeline.stop_when_done()
88
+ break
89
+ else:
90
+ await self.push_frame(TextFrame(SILENCE_PROMPT_TEXT), FrameDirection.DOWNSTREAM)
91
+ self.last_activity_ts = time.time()
92
+ self._bot_is_speaking = False
93
+
94
+ class PhoneChatbotApp:
95
+ def __init__(self):
96
+ self.daily_transport = None
97
+ self.pipeline = None
98
+ self.stt_service = None
99
+ self.tts_service = None
100
+ self.llm_service = None
101
+ self.silence_processor = None
102
+ self.call_start_time = None
103
+ self.current_call_stats = {
104
+ "duration_seconds": 0,
105
+ "silence_events": 0,
106
+ "start_time": None,
107
+ "end_time": None,
108
+ "ended_by_silence": False
109
+ }
110
+
111
+ def _reset_call_stats(self):
112
+ self.call_start_time = time.time()
113
+ self.current_call_stats = {
114
+ "duration_seconds": 0,
115
+ "silence_events": 0,
116
+ "start_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.call_start_time)),
117
+ "end_time": None,
118
+ "ended_by_silence": False
119
+ }
120
+
121
+ async def _log_call_summary(self):
122
+ if self.call_start_time:
123
+ call_end_time = time.time()
124
+ self.current_call_stats["duration_seconds"] = round(call_end_time - self.call_start_time, 2)
125
+ self.current_call_stats["end_time"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(call_end_time))
126
+ if self.silence_processor and self.silence_processor.silence_prompts_count >= MAX_SILENCE_PROMPTS:
127
+ self.current_call_stats["ended_by_silence"] = True
128
+ logger.info("--- Post-Call Summary ---")
129
+ for key, value in self.current_call_stats.items():
130
+ logger.info(f" {key.replace('_', ' ').title()}: {value}")
131
+ logger.info("-------------------------")
132
+
133
+ def setup_pipeline_hook(self, pipeline_params: PipelineParams, room_url: str, token: str):
134
+ self._reset_call_stats()
135
+ self.pipeline = Pipeline([self.stt_service, self.llm_service, self.tts_service])
136
+ self.silence_processor = SilenceAndCallLogicProcessor(
137
+ tts_service=self.tts_service,
138
+ pipeline=self.pipeline,
139
+ app=self
140
+ )
141
+ self.pipeline.processors.append(self.silence_processor)
142
+ pipeline_params.pipeline = self.pipeline
143
+ pipeline_params.params = PipelineParams(allow_interruptions=True, enable_metrics=True)
144
+ return pipeline_params
145
+
146
+ def validate_voice_id(self, voice_id: str) -> bool:
147
+ try:
148
+ client = ElevenLabs(api_key=os.environ.get("elevenlabs"))
149
+ client.voices.get(voice_id=voice_id)
150
+ return True
151
+ except Exception as e:
152
+ logger.error(f"Failed to validate ElevenLabs voice ID {voice_id}: {e}")
153
+ return False
154
+
155
+ async def run(self):
156
+ # Validate environment variables (Hugging Face Secrets)
157
+ required_keys = [
158
+ "deepgram", "elevenlabs", "dailyco", "azure_openai"
159
+ ]
160
+ missing_keys = [key for key in required_keys if not os.environ.get(key)]
161
+ if missing_keys:
162
+ logger.error(f"Missing Hugging Face Secrets: {', '.join(missing_keys)}")
163
+ sys.exit(1)
164
+
165
+ # Validate ElevenLabs voice ID
166
+ voice_id = os.environ.get("ELEVENLABS_VOICE_ID", "cgSgspJ2msm6clMCkdW9")
167
+ if not self.validate_voice_id(voice_id):
168
+ logger.error(f"Invalid ElevenLabs voice ID: {voice_id}")
169
+ sys.exit(1)
170
+
171
+ self.stt_service = DeepgramSTTService(
172
+ api_key=os.environ.get("deepgram"),
173
+ input_audio_format="linear16"
174
+ )
175
+ self.tts_service = ElevenLabsTTSService(
176
+ api_key=os.environ.get("elevenlabs"),
177
+ voice_id=voice_id
178
+ )
179
+ self.llm_service = AzureOpenAILLMService(
180
+ preprompt="You are a friendly and helpful phone assistant."
181
+ )
182
+ self.daily_transport = DailyTransport(
183
+ os.environ.get("DAILY_DOMAIN", "your-username.daily.co"),
184
+ os.environ.get("dailyco"),
185
+ None,
186
+ None,
187
+ "Pipecat Phone Demo",
188
+ vad_analyzer=SileroVADAnalyzer(),
189
+ daily_params=DailyParams(
190
+ audio_in_enabled=True,
191
+ audio_out_enabled=True,
192
+ transcription_enabled=False
193
+ )
194
+ )
195
+ self.daily_transport.pipeline_params_hook = self.setup_pipeline_hook
196
+
197
+ runner = PipelineRunner()
198
+ try:
199
+ await runner.run(self.daily_transport)
200
+ except KeyboardInterrupt:
201
+ logger.info("Ctrl+C pressed, shutting down")
202
+ except Exception as e:
203
+ logger.error(f"An error occurred: {e}", exc_info=True)
204
+ finally:
205
+ await self._log_call_summary()
206
+ if self.pipeline:
207
+ await self.pipeline.stop_when_done()
208
+ if self.silence_processor:
209
+ await self.silence_processor.stop()
210
+
211
+ async def main():
212
+ app = PhoneChatbotApp()
213
+ await app.run()
214
+
215
+ if __name__ == "__main__":
216
+ asyncio.run(main())