-
Notifications
You must be signed in to change notification settings - Fork 280
Expand file tree
/
Copy pathmain.py
More file actions
215 lines (174 loc) · 9.12 KB
/
main.py
File metadata and controls
215 lines (174 loc) · 9.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
import random
from src.agent.capability import MatchingCapability
from src.main import AgentWorker
from src.agent.capability_worker import CapabilityWorker
# =============================================================================
# CONFIGURATION & CONSTANTS
# =============================================================================
# Voice ID for the sleep lecture voice (calm, soothing tone)
SLEEP_VOICE_ID = "pNInz6obpgDQGcFmaJgB" # Use the same voice as the sound ability, or change to your preferred voice
INTRO_PROMPT = (
"I'll talk quietly for a while. "
"There's nothing you need to do, and nothing you need to remember. "
"You can relax and just listen. "
"If you want to stop at any time, just say stop."
)
TOPIC_PROMPT = (
"What would you like me to talk about? "
"You can name a topic, or say 'none' for a random topic."
)
DURATION_PROMPT = (
"How long would you like me to keep you company? "
"You can say short, medium, or long."
)
LECTURE_TOPICS = [
"the slow formation of sedimentary rock layers",
"the history of library cataloging systems",
"the life cycle of moss in temperate forests",
"the physics of wave patterns in still water",
"the development of postal systems through history",
"the chemistry of slow-burning candles",
"the taxonomy of common household dust",
"the gradual weathering of stone monuments",
"the principles of long-distance telegraph systems",
"the process of paper manufacturing in the 19th century"
]
# Duration mapped to lecture segments
DURATION_SEGMENTS = {
"short": 3, # ~3-5 minutes per segment
"medium": 8, # ~8-15 minutes
"long": 15 # ~15-25 minutes
}
EXIT_WORDS = {"stop", "exit", "quit", "done", "cancel", "goodbye", "that's enough"}
CLOSING_STATEMENTS = [
"Rest well. I'll fade out now.",
"Sleep peacefully. Good night.",
"I'll let you drift off now. Good night.",
"Rest easy. I'll be quiet now.",
"Sweet dreams. I'll go now."
]
class QuietCompanyCapability(MatchingCapability):
# --- REQUIRED FIELD DEFINITIONS ---
worker: AgentWorker = None
capability_worker: CapabilityWorker = None
current_topic: str = None
segment_count: int = 0
should_stop: bool = False
# ----------------------------------
# {{register capability}}
def call(self, worker: AgentWorker):
self.worker = worker
self.capability_worker = CapabilityWorker(self.worker)
# Reset state
self.current_topic = None
self.segment_count = 0
self.should_stop = False
self.worker.session_tasks.create(self.run_quiet_lecture())
async def speak(self, text: str):
"""Use text_to_speech with the sleep voice"""
await self.capability_worker.text_to_speech(text, SLEEP_VOICE_ID)
async def run_quiet_lecture(self):
"""Main lecture loop with stop functionality"""
try:
# Intro
await self.speak(INTRO_PROMPT)
# --- Topic selection ---
await self.speak(TOPIC_PROMPT)
topic_response = await self.capability_worker.user_response()
# Clean up the response - sometimes it includes the trigger phrase
if topic_response:
topic_response = topic_response.lower()
# Remove trigger phrases that might have been captured
for trigger in ["quiet company", "sleep lecture", "help me sleep"]:
topic_response = topic_response.replace(trigger, "").strip()
topic_response = topic_response.strip(". ,")
# Check for immediate exit
if topic_response and any(word in topic_response.lower() for word in EXIT_WORDS):
await self.speak(random.choice(CLOSING_STATEMENTS))
self.capability_worker.resume_normal_flow()
return
# Handle "none" or empty response
if not topic_response or topic_response.strip() in ["none", "."]:
self.current_topic = random.choice(LECTURE_TOPICS)
else:
self.current_topic = topic_response.strip()
await self.speak(f"I'll talk quietly about {self.current_topic}.")
# --- Duration selection ---
await self.speak(DURATION_PROMPT)
duration_response = await self.capability_worker.user_response()
# Check for exit during duration selection
if duration_response and any(word in duration_response.lower() for word in EXIT_WORDS):
await self.speak(random.choice(CLOSING_STATEMENTS))
self.capability_worker.resume_normal_flow()
return
duration_key = duration_response.lower().strip() if duration_response else "short"
# Map variations to standard keys
if "short" in duration_key or "quick" in duration_key or "brief" in duration_key:
duration_key = "short"
elif "long" in duration_key or "extended" in duration_key:
duration_key = "long"
else:
duration_key = "medium"
self.segment_count = DURATION_SEGMENTS.get(duration_key, DURATION_SEGMENTS["short"])
self.worker.editor_logging_handler.info(
f"[SleepLectures] Topic: {self.current_topic}, Segments: {self.segment_count}"
)
# --- Lecture session with interruptible segments ---
for i in range(self.segment_count):
# Check if user wants to stop
if self.should_stop:
break
is_final_segment = (i == self.segment_count - 1)
if is_final_segment:
# FINAL SEGMENT: natural fade-out
lecture_prompt = (
f"You are giving a slow, calm, sleep-inducing lecture about {self.current_topic}. "
"Speak in long, gentle sentences with soft repetition. "
"Avoid excitement, urgency, questions, or dramatic conclusions. "
"This is the final segment. Gently wind down the topic and naturally "
"fade out with a soft closing like 'And with that, I'll let you rest. Good night.' "
"Keep it around 250-300 words. The tone should be like a bedtime story that slowly ends."
)
else:
lecture_prompt = (
f"You are giving a slow, calm, sleep-inducing lecture about {self.current_topic}. "
"Speak in long, gentle sentences with soft repetition. "
"Avoid excitement, urgency, questions, or dramatic points. "
"This should feel like quiet background audio that helps someone drift off to sleep. "
"Keep it around 250-300 words. Maintain a steady, unhurried pace."
)
self.worker.editor_logging_handler.info(
f"[SleepLectures] Generating segment {i+1}/{self.segment_count}"
)
try:
lecture_text = self.capability_worker.text_to_text_response(lecture_prompt)
except Exception as e:
self.worker.editor_logging_handler.error(f"[SleepLectures] LLM failed: {e}")
await self.speak("I lost my train of thought. Let me try again.")
continue
# Speak the lecture segment
await self.speak(lecture_text)
# Check if user interrupted during this segment
# With auto-interrupt, user speech gets captured even during TTS
if not is_final_segment:
# Check for user input (captured by auto-interrupt)
user_input = await self.capability_worker.user_response()
# Only stop if user said a stop word, otherwise continue
if user_input:
self.worker.editor_logging_handler.info(f"[SleepLectures] User said: {user_input}")
if any(word in user_input.lower() for word in EXIT_WORDS):
self.worker.editor_logging_handler.info("[SleepLectures] Stop detected, exiting")
await self.speak(random.choice(CLOSING_STATEMENTS))
self.capability_worker.resume_normal_flow()
return
else:
# User interrupted but didn't say stop - replay this segment
self.worker.editor_logging_handler.info("[SleepLectures] Non-stop interruption, replaying segment")
await self.speak(lecture_text)
# If we completed all segments naturally (not stopped early)
if self.should_stop:
await self.speak(random.choice(CLOSING_STATEMENTS))
except Exception as e:
self.worker.editor_logging_handler.error(f"[SleepLectures] Unexpected error: {e}")
await self.speak("Something went wrong. Sorry about that.")
self.capability_worker.resume_normal_flow()