Mbonea commited on
Commit
00a1436
·
1 Parent(s): 412a100
Files changed (5) hide show
  1. App/TTS/TTSRoutes.py +1 -2
  2. App/TTS/utils/Pi copy.py +193 -0
  3. App/TTS/utils/Pi.py +501 -171
  4. App/app.py +3 -1
  5. Dockerfile +2 -2
App/TTS/TTSRoutes.py CHANGED
@@ -13,7 +13,7 @@ from .Schemas import (
13
  )
14
  from .utils.Podcastle import PodcastleAPI
15
  from .utils.HeyGen import HeygenAPI
16
- from .utils.Pi import PiAIClient
17
  from .utils.Descript import DescriptTTS
18
  import os
19
  import asyncio
@@ -34,7 +34,6 @@ data = {
34
 
35
  descript_tts = DescriptTTS()
36
  heyGentts = HeygenAPI(**data)
37
- pi = PiAIClient()
38
 
39
 
40
  @tts_router.post("/generate_tts")
 
13
  )
14
  from .utils.Podcastle import PodcastleAPI
15
  from .utils.HeyGen import HeygenAPI
16
+ from App.app import pi
17
  from .utils.Descript import DescriptTTS
18
  import os
19
  import asyncio
 
34
 
35
  descript_tts = DescriptTTS()
36
  heyGentts = HeygenAPI(**data)
 
37
 
38
 
39
  @tts_router.post("/generate_tts")
App/TTS/utils/Pi copy.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import aiohttp
2
+ import asyncio
3
+ import enum
4
+ import requests
5
+ import os
6
+ from functools import cache
7
+ import tempfile
8
+ import uuid
9
+
10
+
11
+ class VoiceType(enum.Enum):
12
+ voice1 = "voice1"
13
+ voice2 = "voice2"
14
+ voice3 = "voice3"
15
+ voice4 = "voice4"
16
+ voice5 = "voice5"
17
+ voice5_update = "voice5-update"
18
+ voice6 = "voice6"
19
+ voice7 = "voice7"
20
+ voice8 = "voice8"
21
+ voice9 = "voice9"
22
+ voice10 = "voice10"
23
+ voice11 = "voice11"
24
+ voice12 = "voice12"
25
+ qdpi = "qdpi"
26
+
27
+
28
+ class PiAIClient:
29
+ def __init__(self):
30
+ self.dir = "/tmp/Audio"
31
+ self.base_url = "https://pi.ai/api/chat"
32
+ self.referer = "https://pi.ai/talk"
33
+ self.origin = "https://pi.ai"
34
+ self.user_agent = (
35
+ "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0"
36
+ )
37
+ self.cookie = None
38
+ self.headers = {
39
+ "User-Agent": self.user_agent,
40
+ "Accept": "text/event-stream",
41
+ "Referer": self.referer,
42
+ "X-Api-Version": "3",
43
+ "Content-Type": "application/json",
44
+ "Origin": self.origin,
45
+ "Connection": "keep-alive",
46
+ "Sec-Fetch-Dest": "empty",
47
+ "Sec-Fetch-Mode": "no-cors",
48
+ "Sec-Fetch-Site": "same-origin",
49
+ "DNT": "1",
50
+ "Sec-GPC": "1",
51
+ "TE": "trailers",
52
+ "Cookie": "__cf_bm=XagWXCS3SJekiP5O.A8K9wgtGuEieLNW7AFXj10hzqk-1717865973-1.0.1.1-4Xp_xUVYB5G.Zkpfgcm30PCVGnj3g6URzZsfCS28BQIdt8dZm76rnNbQiX9vNG_OsYdbUiDiX2pa.E3ajhcOXA; path=/; expires=Sat, 08-Jun-24 17:29:33 GMT; domain=.pi.ai; HttpOnly; Secure; SameSite=None",
53
+ "Pragma": "no-cache",
54
+ "Cache-Control": "no-cache",
55
+ }
56
+
57
+ async def get_cookie(self) -> str:
58
+ headers = self.headers.copy()
59
+ async with aiohttp.ClientSession() as session:
60
+ async with session.post(
61
+ f"{self.base_url}/start", headers=headers, json={}
62
+ ) as response:
63
+ self.cookie = response.headers["Set-Cookie"]
64
+ return self.cookie
65
+
66
+ async def make_request(
67
+ self, endpoint: str, headers: dict, json: dict = None, method: str = "POST"
68
+ ):
69
+ async with aiohttp.ClientSession() as session:
70
+ if method == "POST":
71
+ async with session.post(
72
+ endpoint, headers=headers, json=json
73
+ ) as response:
74
+ return await response.text()
75
+ elif method == "GET":
76
+ async with session.get(endpoint, headers=headers) as response:
77
+ return response
78
+
79
+ async def get_response(self, input_text) -> tuple[list[str], list[str]]:
80
+ if self.cookie is None:
81
+ self.cookie = await self.get_cookie()
82
+
83
+ headers = self.headers.copy()
84
+ headers["Cookie"] = self.cookie
85
+
86
+ data = {"text": input_text}
87
+ response_text = await self.make_request(self.base_url, headers, json=data)
88
+
89
+ response_lines = response_text.split("\n")
90
+ response_texts = []
91
+ response_sids = []
92
+ print(response_lines)
93
+ for line in response_lines:
94
+ if line.startswith('data: {"text":"'):
95
+ start = len('data: {"text":')
96
+ end = line.rindex("}")
97
+ text_dict = line[start + 1 : end - 1].strip()
98
+ response_texts.append(text_dict)
99
+ elif line.startswith('data: {"sid":'):
100
+ start = len('data: {"sid":')
101
+ end = line.rindex("}")
102
+ sid_dict = line[start : end - 1].strip()
103
+ sid_dict = sid_dict.split(",")[0][1:-1]
104
+ response_sids.append(sid_dict)
105
+
106
+ return response_texts, response_sids
107
+
108
+ async def speak_response(
109
+ self, message_sid: str, voice: VoiceType = VoiceType.voice4.value
110
+ ) -> None:
111
+ if self.cookie is None:
112
+ self.cookie = await self.get_cookie()
113
+
114
+ headers = self.headers.copy()
115
+ headers.update(
116
+ {
117
+ "Host": "pi.ai",
118
+ "Accept": "audio/webm,audio/ogg,audio/wav,audio/*;q=0.9,application/ogg;q=0.7,video/*;q=0.6,*/*;q=0.5",
119
+ "Accept-Language": "en-US,en;q=0.9",
120
+ "Range": "bytes=0-",
121
+ "Sec-Fetch-Dest": "audio",
122
+ "Sec-Fetch-Mode": "no-cors",
123
+ "Sec-Fetch-Site": "same-origin",
124
+ "Sec-CH-UA": '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"',
125
+ "Sec-CH-UA-Mobile": "?0",
126
+ "Sec-CH-UA-Platform": '"Windows"',
127
+ }
128
+ )
129
+
130
+ headers = {
131
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0",
132
+ "Accept": "audio/webm,audio/ogg,audio/wav,audio/*;q=0.9,application/ogg;q=0.7,video/*;q=0.6,*/*;q=0.5",
133
+ "Accept-Language": "en-US,en;q=0.5",
134
+ "Range": "bytes=0-",
135
+ "Connection": "keep-alive",
136
+ "Referer": "https://pi.ai/talk",
137
+ # "Cookie": cookie,
138
+ "Sec-Fetch-Dest": "audio",
139
+ "Sec-Fetch-Mode": "no-cors",
140
+ "Sec-Fetch-Site": "same-origin",
141
+ "DNT": "1",
142
+ "Sec-GPC": "1",
143
+ "Accept-Encoding": "identity",
144
+ "TE": "trailers",
145
+ }
146
+ headers["Cookie"] = self.cookie
147
+ print(headers)
148
+ endpoint = (
149
+ f"{self.base_url}/voice?mode=eager&voice={voice}&messageSid={message_sid}"
150
+ )
151
+ async with aiohttp.ClientSession() as session:
152
+ async with session.get(endpoint, headers=headers) as response:
153
+ print(response.status)
154
+ file_name = str(uuid.uuid4()) + ".mp3"
155
+ file_path = os.path.join(self.dir, file_name)
156
+ os.makedirs(self.dir, exist_ok=True)
157
+ if response.status == 200:
158
+ with open(file_path, "wb") as file:
159
+ async for chunk in response.content.iter_chunked(128):
160
+ file.write(chunk)
161
+ return {
162
+ "url": f"https://yakova-embedding.hf.space/audio/{file_name}"
163
+ }
164
+ # Run command vlc to play the audio file
165
+ # os.system("vlc speak.wav --intf dummy --play-and-exit")
166
+ else:
167
+ temp = await response.text()
168
+ print(temp)
169
+ self.cookie = None
170
+ return "Error: Unable to retrieve audio."
171
+
172
+ async def say(self, text, voice=VoiceType.qdpi.value):
173
+ _, response_sids = await self.get_response(text)
174
+
175
+ if response_sids:
176
+ return await self.speak_response(response_sids[0], voice=voice)
177
+
178
+
179
+ # async def main():
180
+ # client = PiAIClient()
181
+ # response_texts, response_sids = await client.get_response(
182
+ # "Write a ryme to introduce yourself."
183
+ # )
184
+ # print(response_texts, response_sids)
185
+ # import time
186
+
187
+ # if response_sids:
188
+ # return await client.speak_response(response_sids[1])
189
+
190
+
191
+ # # Run the main function
192
+ # if __name__ == "__main__":
193
+ # asyncio.run(main())
App/TTS/utils/Pi.py CHANGED
@@ -1,193 +1,523 @@
1
- import aiohttp
2
  import asyncio
3
- import enum
4
- import requests
5
  import os
6
- from functools import cache
7
- import tempfile
8
- import uuid
 
 
 
 
 
 
 
 
 
 
 
9
 
10
 
11
  class VoiceType(enum.Enum):
12
- voice1 = "voice1"
13
- voice2 = "voice2"
14
- voice3 = "voice3"
15
- voice4 = "voice4"
16
- voice5 = "voice5"
17
- voice5_update = "voice5-update"
18
- voice6 = "voice6"
19
- voice7 = "voice7"
20
- voice8 = "voice8"
21
- voice9 = "voice9"
22
- voice10 = "voice10"
23
- voice11 = "voice11"
24
- voice12 = "voice12"
25
- qdpi = "qdpi"
26
 
27
 
28
  class PiAIClient:
29
- def __init__(self):
30
- self.dir = "/tmp/Audio"
31
- self.base_url = "https://pi.ai/api/chat"
32
- self.referer = "https://pi.ai/talk"
33
- self.origin = "https://pi.ai"
34
- self.user_agent = (
35
- "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0"
36
- )
37
- self.cookie = None
38
- self.headers = {
39
- "User-Agent": self.user_agent,
40
- "Accept": "text/event-stream",
41
- "Referer": self.referer,
42
- "X-Api-Version": "3",
43
- "Content-Type": "application/json",
44
- "Origin": self.origin,
45
- "Connection": "keep-alive",
46
- "Sec-Fetch-Dest": "empty",
47
- "Sec-Fetch-Mode": "no-cors",
48
- "Sec-Fetch-Site": "same-origin",
49
- "DNT": "1",
50
- "Sec-GPC": "1",
51
- "TE": "trailers",
52
- "Cookie": "__cf_bm=XagWXCS3SJekiP5O.A8K9wgtGuEieLNW7AFXj10hzqk-1717865973-1.0.1.1-4Xp_xUVYB5G.Zkpfgcm30PCVGnj3g6URzZsfCS28BQIdt8dZm76rnNbQiX9vNG_OsYdbUiDiX2pa.E3ajhcOXA; path=/; expires=Sat, 08-Jun-24 17:29:33 GMT; domain=.pi.ai; HttpOnly; Secure; SameSite=None",
53
- "Pragma": "no-cache",
54
- "Cache-Control": "no-cache",
55
- }
56
-
57
- async def get_cookie(self) -> str:
58
- headers = self.headers.copy()
59
- async with aiohttp.ClientSession() as session:
60
- async with session.post(
61
- f"{self.base_url}/start", headers=headers, json={}
62
- ) as response:
63
- self.cookie = response.headers["Set-Cookie"]
64
- return self.cookie
65
-
66
- async def make_request(
67
- self, endpoint: str, headers: dict, json: dict = None, method: str = "POST"
68
- ):
69
- async with aiohttp.ClientSession() as session:
70
- if method == "POST":
71
- async with session.post(
72
- endpoint, headers=headers, json=json
73
- ) as response:
74
- return await response.text()
75
- elif method == "GET":
76
- async with session.get(endpoint, headers=headers) as response:
77
- return response
78
-
79
- async def get_response(self, input_text) -> tuple[list[str], list[str]]:
80
- if self.cookie is None:
81
- self.cookie = await self.get_cookie()
82
-
83
- headers = self.headers.copy()
84
- headers["Cookie"] = self.cookie
85
-
86
- data = {"text": input_text}
87
- response_text = await self.make_request(self.base_url, headers, json=data)
88
-
89
- response_lines = response_text.split("\n")
90
- response_texts = []
91
- response_sids = []
92
- print(response_lines)
93
- for line in response_lines:
94
- if line.startswith('data: {"text":"'):
95
- start = len('data: {"text":')
96
- end = line.rindex("}")
97
- text_dict = line[start + 1 : end - 1].strip()
98
- response_texts.append(text_dict)
99
- elif line.startswith('data: {"sid":'):
100
- start = len('data: {"sid":')
101
- end = line.rindex("}")
102
- sid_dict = line[start : end - 1].strip()
103
- sid_dict = sid_dict.split(",")[0][1:-1]
104
- response_sids.append(sid_dict)
105
-
106
- return response_texts, response_sids
107
-
108
- async def speak_response(
109
- self, message_sid: str, voice: VoiceType = VoiceType.voice4.value
110
- ) -> None:
111
- if self.cookie is None:
112
- self.cookie = await self.get_cookie()
113
-
114
- headers = self.headers.copy()
115
- headers.update(
116
  {
117
- "Host": "pi.ai",
118
- "Accept": "audio/webm,audio/ogg,audio/wav,audio/*;q=0.9,application/ogg;q=0.7,video/*;q=0.6,*/*;q=0.5",
119
- "Accept-Language": "en-US,en;q=0.9",
120
- "Range": "bytes=0-",
121
- "Sec-Fetch-Dest": "audio",
122
- "Sec-Fetch-Mode": "no-cors",
123
- "Sec-Fetch-Site": "same-origin",
124
- "Sec-CH-UA": '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"',
125
- "Sec-CH-UA-Mobile": "?0",
126
- "Sec-CH-UA-Platform": '"Windows"',
127
- }
128
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
- headers = {
131
- "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0",
132
- "Accept": "audio/webm,audio/ogg,audio/wav,audio/*;q=0.9,application/ogg;q=0.7,video/*;q=0.6,*/*;q=0.5",
133
- "Accept-Language": "en-US,en;q=0.5",
134
- "Range": "bytes=0-",
135
- "Connection": "keep-alive",
136
- "Referer": "https://pi.ai/talk",
137
- # "Cookie": cookie,
138
- "Sec-Fetch-Dest": "audio",
139
- "Sec-Fetch-Mode": "no-cors",
140
- "Sec-Fetch-Site": "same-origin",
141
- "DNT": "1",
142
- "Sec-GPC": "1",
143
- "Accept-Encoding": "identity",
144
- "TE": "trailers",
145
- }
146
- headers["Cookie"] = self.cookie
147
- print(headers)
148
- endpoint = (
149
- f"{self.base_url}/voice?mode=eager&voice={voice}&messageSid={message_sid}"
 
 
150
  )
151
- async with aiohttp.ClientSession() as session:
152
- async with session.get(endpoint, headers=headers) as response:
153
- print(response.status)
154
- file_name = str(uuid.uuid4()) + ".mp3"
155
- file_path = os.path.join(self.dir, file_name)
156
- os.makedirs(self.dir, exist_ok=True)
157
- if response.status == 200:
158
- with open(file_path, "wb") as file:
159
- async for chunk in response.content.iter_chunked(128):
160
- file.write(chunk)
161
- return {
162
- "url": f"https://yakova-embedding.hf.space/audio/{file_name}"
163
- }
164
- # Run command vlc to play the audio file
165
- # os.system("vlc speak.wav --intf dummy --play-and-exit")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  else:
167
- temp = await response.text()
168
- print(temp)
169
- self.cookie = None
170
- return "Error: Unable to retrieve audio."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
- async def say(self, text, voice=VoiceType.qdpi.value):
173
- _, response_sids = await self.get_response(text)
 
 
 
 
174
 
175
- if response_sids:
176
- return await self.speak_response(response_sids[0], voice=voice)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
 
179
  # async def main():
180
- # client = PiAIClient()
181
- # response_texts, response_sids = await client.get_response(
182
- # "Write a ryme to introduce yourself."
183
- # )
184
- # print(response_texts, response_sids)
185
- # import time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
- # if response_sids:
188
- # return await client.speak_response(response_sids[1])
189
 
190
 
191
- # # Run the main function
192
  # if __name__ == "__main__":
193
  # asyncio.run(main())
 
 
1
  import asyncio
 
 
2
  import os
3
+ from playwright.async_api import async_playwright, Page, Request, Response, Download
4
+ import re
5
+ import logging
6
+ from urllib.parse import urlparse
7
+ from datetime import datetime, timedelta
8
+ import enum
9
+
10
+ # Configure logging
11
+ logging.basicConfig(
12
+ level=logging.INFO,
13
+ format="%(asctime)s [%(levelname)s] %(message)s",
14
+ handlers=[logging.StreamHandler()],
15
+ )
16
+ logger = logging.getLogger(__name__)
17
 
18
 
19
  class VoiceType(enum.Enum):
20
+ NEUTRAL = "voice1"
21
+ HAPPY = "voice2"
22
+ SAD = "voice3"
23
+ ANGRY = "voice4"
24
+ EXCITED = "voice5"
25
+ CALM = "voice6"
26
+ # Add more voices as supported by Pi.ai
 
 
 
 
 
 
 
27
 
28
 
29
  class PiAIClient:
30
+ def __init__(self, headless: bool = False, download_dir: str = "/tmp/Audio"):
31
+ self.headless = headless
32
+ self.download_dir = "/tmp/Audio"
33
+ self.playwright = None
34
+ self.browser = None
35
+ self.context = None
36
+ self.page = None
37
+
38
+ # Define actions with their selectors and corresponding handler methods
39
+ self.actions = [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  {
41
+ "selector": 'textarea[placeholder="Talk with Pi"]',
42
+ "handler": self.send_chat_message,
43
+ "description": "Chat input detected, sending message.",
44
+ "break_after": True, # Indicates to break the loop after sending the message
45
+ },
46
+ {
47
+ "selector": 'button:has-text("I’ll do it later")',
48
+ "handler": self.click_element,
49
+ "description": "'I’ll do it later' button found, clicking it.",
50
+ },
51
+ {
52
+ "selector": 'button:has-text("Next")',
53
+ "handler": self.click_element,
54
+ "description": "'Next' button found, clicking it.",
55
+ },
56
+ {
57
+ "selector": 'textarea[placeholder="Your first name"]',
58
+ "handler": self.fill_name,
59
+ "description": "Name input detected, filling it.",
60
+ },
61
+ ]
62
+
63
+ # Regular expression to extract 'sid' values from the response
64
+ self.sid_regex = re.compile(r'"sid":"([\w\-]+)"')
65
+
66
+ # Set to keep track of processed sids to avoid duplicates
67
+ self.processed_sids = set()
68
+
69
+ # Directory to store downloaded audios
70
+ self.download_dir = download_dir
71
+ self.ensure_download_directory()
72
+
73
+ # Semaphore to limit concurrent downloads (optional)
74
+ self.semaphore = asyncio.Semaphore(5) # Adjust the number as needed
75
+
76
+ # Rate limiting attributes
77
+ self.rate_limit_until = None # Timestamp until which the bot should wait
78
+ self.rate_limit_lock = asyncio.Lock() # To prevent race conditions
79
+
80
+ # Mapping from sid to (Future, VoiceType)
81
+ self.sid_futures = asyncio.Queue()
82
+
83
+ def ensure_download_directory(self):
84
+ """Ensure that the downloads directory exists."""
85
+ if not os.path.exists(self.download_dir):
86
+ os.makedirs(self.download_dir)
87
+ logger.info(
88
+ f"Created directory '{self.download_dir}' for storing downloads."
89
+ )
90
+ else:
91
+ logger.info(f"Directory '{self.download_dir}' already exists.")
92
 
93
+ async def setup(self):
94
+ """Initialize Playwright, launch the browser with a persistent context, and create a new page."""
95
+ self.playwright = await async_playwright().start()
96
+
97
+ # Specify the path for the user data directory
98
+ user_data_dir = os.path.join(os.getcwd(), "user_data")
99
+ if not os.path.exists(user_data_dir):
100
+ os.makedirs(user_data_dir)
101
+ logger.info(f"Created user data directory at '{user_data_dir}'.")
102
+ else:
103
+ logger.info(f"Using existing user data directory at '{user_data_dir}'.")
104
+
105
+ # Launch a persistent context
106
+ self.context = await self.playwright.chromium.launch_persistent_context(
107
+ user_agent=(
108
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
109
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
110
+ "Chrome/114.0.0.0 Safari/537.36"
111
+ ),
112
+ user_data_dir=user_data_dir, # Persistent storage directory
113
+ headless=self.headless,
114
+ args=["--no-sandbox"], # Optional: Add any Chromium args if needed
115
  )
116
+
117
+ # Create a new page within the persistent context
118
+ self.page = await self.context.new_page()
119
+
120
+ # Set up request and response interception
121
+ self.page.on("request", self.handle_request)
122
+ self.page.on("response", self.handle_response)
123
+ self.navigate("https://pi.ai/talk")
124
+
125
+ # Start the monitoring task
126
+ asyncio.create_task(self.monitor_page_and_act())
127
+
128
+ async def navigate(self, url: str):
129
+ """Navigate to the specified URL and wait for the page to load."""
130
+ await self.page.goto(url)
131
+ await self.page.wait_for_load_state("networkidle")
132
+ logger.info(f"Navigated to {url}")
133
+
134
+ async def monitor_page_and_act(self):
135
+ """Continuously monitor the page and perform actions based on the detected elements."""
136
+ counter = 0
137
+ while True:
138
+ try:
139
+ # Check for rate limiting before performing any actions
140
+ if self.is_rate_limited():
141
+ wait_seconds = (
142
+ self.rate_limit_until - datetime.utcnow()
143
+ ).total_seconds()
144
+ wait_seconds = max(wait_seconds, 0)
145
+ logger.warning(
146
+ f"Rate limited. Waiting for {wait_seconds:.2f} seconds before retrying."
147
+ )
148
+ await asyncio.sleep(wait_seconds)
149
+ continue # After waiting, re-enter the loop
150
+
151
+ action_performed = False
152
+ for action in self.actions:
153
+ if await self.page.is_visible(action["selector"]):
154
+ logger.info(action["description"])
155
+ await action["handler"](action["selector"])
156
+ action_performed = True
157
+ if action.get("break_after"):
158
+ action_performed = (
159
+ False # Continue monitoring after sending the message
160
+ )
161
+ break # Exit the for-loop to allow handling other tasks
162
+ if not action_performed:
163
+ logger.info(
164
+ "No matching state detected. Navigating to /talk or /discover route."
165
+ )
166
+ if counter % 5 == 0:
167
+ await self.navigate_to_route("/discover")
168
+ logger.info("Navigated to /discover route.")
169
+ counter = 0
170
+ else:
171
+ await self.navigate_to_route("/talk")
172
+ logger.info("Navigated to /talk route.")
173
+ counter += 1
174
+
175
+ # Wait for a short period before the next check
176
+ await asyncio.sleep(2)
177
+
178
+ except Exception as e:
179
+ logger.error(f"Error during monitoring: {e}")
180
+ await asyncio.sleep(
181
+ 2
182
+ ) # Prevent tight loop in case of continuous errors
183
+
184
+ def is_rate_limited(self):
185
+ """Check if the bot is currently rate-limited."""
186
+ if self.rate_limit_until and datetime.utcnow() < self.rate_limit_until:
187
+ return True
188
+ return False
189
+
190
+ async def navigate_to_route(self, route):
191
+ """Navigate to the specified route."""
192
+ try:
193
+ current_url = self.page.url
194
+ # Check if already on the specified route to prevent unnecessary navigation
195
+ if not current_url.endswith(route):
196
+ new_url = self.construct_route_url(current_url, route)
197
+ await self.navigate(new_url)
198
+ else:
199
+ logger.info(f"Already on the {route} route.")
200
+ except Exception as e:
201
+ logger.error(f"Error navigating to {route} route: {e}")
202
+
203
+ def construct_route_url(self, current_url, route):
204
+ """Construct the new URL for the specified route."""
205
+ # Modify this function to fit your URL structure
206
+ parsed_url = urlparse(current_url)
207
+ # Replace the path with the desired route
208
+ new_url = parsed_url._replace(path=route).geturl()
209
+ logger.info(f"Constructed new URL: {new_url}")
210
+ return new_url
211
+
212
+ async def click_element(self, selector: str):
213
+ """Wait for an element to be visible and click it."""
214
+ try:
215
+ await self.page.wait_for_selector(selector, timeout=3000)
216
+ await self.page.click(selector)
217
+ logger.info(f"Clicked element: {selector}")
218
+ except Exception as e:
219
+ logger.error(f"Error clicking element {selector}: {e}")
220
+
221
+ async def fill_name(self, selector: str):
222
+ """Fill in the name input field and submit."""
223
+ try:
224
+ name = "Cassandra"
225
+ await self.page.fill(selector, name)
226
+ await self.page.click('button[aria-label="Submit text"]')
227
+ logger.info(f"Name '{name}' submitted")
228
+ except Exception as e:
229
+ logger.error(f"Error submitting name: {e}")
230
+ await self.handle_send_failure()
231
+
232
+ async def send_chat_message(self, selector: str):
233
+ """Send a chat message in the chat input field."""
234
+ try:
235
+ await self.page.fill(selector, self.user_input)
236
+ await self.page.click('button[aria-label="Submit text"]')
237
+ logger.info("Chat message submitted")
238
+ except Exception as e:
239
+ logger.error(f"Could not send chat message: {e}")
240
+ await self.handle_send_failure()
241
+
242
+ async def handle_send_failure(self):
243
+ """Handle failure in sending messages by navigating to /talk or /discover."""
244
+ try:
245
+ # Attempt to navigate to /talk
246
+ await self.navigate_to_route("/talk")
247
+ logger.info("Navigated to /talk route after failing to send message.")
248
+ except Exception:
249
+ try:
250
+ # If navigating to /talk fails, navigate to /discover
251
+ await self.navigate_to_route("/discover")
252
+ logger.info(
253
+ "Navigated to /discover route after failing to send message."
254
+ )
255
+ except Exception as e2:
256
+ logger.error(f"Failed to navigate after send_message failure: {e2}")
257
+
258
+ async def handle_request(self, request: Request):
259
+ """Handle and log network requests."""
260
+ # Log all requests at DEBUG level
261
+ logger.debug(f"Request: {request.method} {request.url}")
262
+
263
+ async def handle_response(self, response: Response):
264
+ """Handle and log network responses, extracting 'sid's."""
265
+ url = response.url
266
+ if "/api/chat" in url and response.request.method == "POST":
267
+ logger.info(f"Handling response for: {url}")
268
+ try:
269
+ response_status = response.status
270
+ response_text = await asyncio.wait_for(response.text(), timeout=5)
271
+ logger.info(f"Response received from {url}: {response_text}")
272
+
273
+ if response_status == 429:
274
+ # Handle rate limiting based on status code
275
+ logger.warning("Received 429 Too Many Requests.")
276
+ retry_after = response.headers.get("Retry-After")
277
+ if retry_after:
278
+ wait_seconds = int(retry_after)
279
+ else:
280
+ wait_seconds = (
281
+ 60 # Default wait time if Retry-After not provided
282
+ )
283
+ await self.trigger_rate_limit(wait_seconds)
284
+ return
285
+
286
+ # Attempt to parse the response as JSON
287
+ if "error" in response_text and "Too Many Requests" in response_text:
288
+ logger.warning("Received error response: Too Many Requests.")
289
+ await self.trigger_rate_limit(60) # Default wait time
290
+ return
291
+
292
+ # Extract 'sid's using regex
293
+ sids = self.sid_regex.findall(response_text)
294
+
295
+ if sids:
296
+ logger.info(f"Extracted 'sid's: {sids}")
297
+ for sid in sids:
298
+ if sid not in self.processed_sids:
299
+ self.processed_sids.add(sid)
300
+ logger.info(f"Processing sid: {sid}")
301
+ # If there are pending say requests, assign this sid to the first one
302
+ if not self.sid_futures.empty():
303
+ future, voice = await self.sid_futures.get()
304
+
305
+ asyncio.create_task(
306
+ self.process_sid(sid, voice, future)
307
+ )
308
+ else:
309
+ # No pending say requests, process with default voice or skip
310
+ asyncio.create_task(
311
+ self.process_sid(sid, VoiceType.NEUTRAL.value, None)
312
+ )
313
+ break
314
  else:
315
+ logger.info("No 'sid's found in the response.")
316
+
317
+ except asyncio.TimeoutError:
318
+ logger.warning(
319
+ "Timed out waiting for the response body (possibly streaming)."
320
+ )
321
+ except Exception as e:
322
+ logger.error(f"Error processing response: {e}")
323
+ elif "/api/chat/voice" in url:
324
+ # Handle audio responses directly if needed
325
+ pass # Currently handled in process_sid
326
+
327
+ async def trigger_rate_limit(self, wait_seconds: int):
328
+ """Trigger rate limiting by setting the rate_limit_until timestamp."""
329
+ async with self.rate_limit_lock:
330
+ if (
331
+ not self.rate_limit_until
332
+ or datetime.utcnow() + timedelta(seconds=wait_seconds)
333
+ > self.rate_limit_until
334
+ ):
335
+ self.rate_limit_until = datetime.utcnow() + timedelta(
336
+ seconds=wait_seconds
337
+ )
338
+ logger.warning(
339
+ f"Rate limited. Will resume after {self.rate_limit_until} UTC."
340
+ )
341
+ else:
342
+ self.rate_limit_until += timedelta(seconds=wait_seconds)
343
+ logger.warning("Already rate limited. Extending the wait time.")
344
+
345
+ async def process_sid(self, sid: str, voice: str, future: asyncio.Future):
346
+ """Download the TTS audio using the sid and specified voice."""
347
+ async with self.semaphore:
348
+ try:
349
+ logger.info(f"Processing sid: {sid} with voice: {voice}")
350
+ url = f"https://pi.ai/api/chat/voice?mode=eager&voice={voice}&messageSid={sid}"
351
+ logger.info(f"Initiating download from URL: {url}")
352
+
353
+ # Open a new page (tab)
354
+ new_page = await self.context.new_page()
355
+
356
+ # Set up download handler
357
+ new_page.on("download", self.handle_download)
358
+
359
+ # Navigate to the URL
360
+ await new_page.goto(url)
361
+ logger.info(f"Opened URL: {url}")
362
+
363
+ # Create and click the anchor tag via JavaScript
364
+ await new_page.evaluate(
365
+ f"""
366
+ (function() {{
367
+ var link = document.createElement('a');
368
+ link.href = "{url}";
369
+ link.download = "{sid}.mp3";
370
+ document.body.appendChild(link);
371
+ link.click();
372
+ document.body.removeChild(link);
373
+ }})();
374
+ """
375
+ )
376
+ logger.info(f"Triggered download for sid: {sid}")
377
+ filename = f"{sid}_{voice.lower()}.mp3"
378
+ file_path = os.path.join(self.download_dir, filename)
379
+
380
+ # Start the download
381
+ # Wait for the download to start
382
+ await asyncio.sleep(2)
383
+ # Close the new page
384
+ await new_page.close()
385
+
386
+ # If a future was provided, set its result
387
+ if future:
388
+ future.set_result(file_path)
389
+
390
+ except Exception as e:
391
+ logger.error(f"Error processing sid {sid}: {e}")
392
+ if future and not future.done():
393
+ future.set_exception(e)
394
 
395
+ async def handle_download(self, download: Download):
396
+ """Handle the download event and save the file."""
397
+ try:
398
+ # Define the path to save the download
399
+ filename = download.suggested_filename or "audio.mp3"
400
+ download_path = os.path.join(self.download_dir, filename)
401
 
402
+ # Save the downloaded file
403
+ await download.save_as(download_path)
404
+ logger.info(f"Downloaded audio to {download_path}")
405
+
406
+ except Exception as e:
407
+ logger.error(f"Error downloading audio: {e}")
408
+
409
+ async def close(self):
410
+ """Close the browser and Playwright."""
411
+ if self.context:
412
+ await self.context.close()
413
+ if self.playwright:
414
+ await self.playwright.stop()
415
+ logger.info("Browser closed")
416
+
417
+ async def say(self, message: str, voice: str) -> str:
418
+ """
419
+ Send a message and retrieve the path to the downloaded TTS audio.
420
+
421
+ :param message: The message to send.
422
+ :param voice: The emotional voice type to use.
423
+ :return: The file path of the downloaded audio.
424
+ """
425
+ # Create a Future to wait for the audio download
426
+ future = asyncio.get_event_loop().create_future()
427
+ # Put the future and voice into the queue
428
+ await self.sid_futures.put((future, voice))
429
+ # Send the message
430
+ self.user_input = message
431
+ await self.send_message(message)
432
+ # Wait for the Future to be set with the audio path
433
+ try:
434
+ audio_path = await asyncio.wait_for(
435
+ future, timeout=60
436
+ ) # Adjust timeout as needed
437
+ return audio_path
438
+ except asyncio.TimeoutError:
439
+ logger.error("Timeout while waiting for audio download.")
440
+ return ""
441
+
442
+ async def send_message(
443
+ self, message: str, retry_count: int = 3, retry_delay: int = 60
444
+ ):
445
+ """
446
+ Send a message through the chat interface with retry logic.
447
+
448
+ :param message: The message to send.
449
+ :param retry_count: Number of times to retry on failure.
450
+ :param retry_delay: Seconds to wait before retrying.
451
+ """
452
+ attempt = 0
453
+ while attempt < retry_count:
454
+ try:
455
+ # Check if currently rate limited
456
+ if self.is_rate_limited():
457
+ wait_seconds = (
458
+ self.rate_limit_until - datetime.utcnow()
459
+ ).total_seconds()
460
+ wait_seconds = max(wait_seconds, 0)
461
+ logger.warning(
462
+ f"Currently rate limited. Waiting for {wait_seconds:.2f} seconds before retrying."
463
+ )
464
+ await asyncio.sleep(wait_seconds)
465
+
466
+ self.user_input = message # Update the user_input attribute
467
+ await self.page.fill(
468
+ 'textarea[placeholder="Talk with Pi"]', self.user_input
469
+ )
470
+ await self.page.click('button[aria-label="Submit text"]')
471
+ logger.info("Chat message submitted")
472
+ return # Success, exit the method
473
+
474
+ except Exception as e:
475
+ logger.error(f"Could not send chat message: {e}")
476
+ attempt += 1
477
+ if attempt < retry_count:
478
+ logger.info(
479
+ f"Retrying to send message in {retry_delay} seconds... (Attempt {attempt}/{retry_count})"
480
+ )
481
+ await asyncio.sleep(retry_delay)
482
+ else:
483
+ logger.error(
484
+ "Max retry attempts reached. Failed to send the message."
485
+ )
486
+ await self.handle_send_failure()
487
+
488
+ # If all retries fail, handle the failure
489
+ await self.handle_send_failure()
490
+
491
+
492
+ import asyncio
493
 
494
 
495
  # async def main():
496
+ # # Initialize the PiBot
497
+ # bot = PiAIClient(headless=True)
498
+ # try:
499
+ # await bot.setup()
500
+ # # await bot.navigate("https://pi.ai/talk")
501
+
502
+ # # Example usage of the say method
503
+ # audio_path_neutral = await bot.say("Hello baby.", voice=VoiceType.NEUTRAL.value)
504
+ # print(f"Neutral Audio Path: {audio_path_neutral}")
505
+ # await asyncio.sleep(5)
506
+ # audio_path_happy = await bot.say(
507
+ # "I'm so happy to see you!", voice=VoiceType.HAPPY.value
508
+ # )
509
+ # print(f"Happy Audio Path: {audio_path_happy}")
510
+ # await asyncio.sleep(5)
511
+ # audio_path_sad = await bot.say(
512
+ # "I'm feeling a bit down today.", voice=VoiceType.SAD.value
513
+ # )
514
+ # print(f"Sad Audio Path: {audio_path_sad}")
515
+
516
+ # # You can add more messages with different emotions as needed
517
 
518
+ # finally:
519
+ # await bot.close()
520
 
521
 
 
522
  # if __name__ == "__main__":
523
  # asyncio.run(main())
App/app.py CHANGED
@@ -4,7 +4,7 @@ from fastapi.middleware.gzip import GZipMiddleware
4
 
5
  from .TTS.TTSRoutes import tts_router
6
  from .Embedding.EmbeddingRoutes import embeddigs_router
7
-
8
 
9
  from fastapi.middleware.cors import CORSMiddleware
10
 
@@ -33,11 +33,13 @@ app.add_middleware(
33
  allow_headers=["*"],
34
  )
35
  app.add_middleware(GZipMiddleware, minimum_size=1000)
 
36
 
37
 
38
  @app.on_event("startup")
39
  async def startup():
40
  FastAPICache.init(InMemoryBackend())
 
41
 
42
 
43
  @app.get("/")
 
4
 
5
  from .TTS.TTSRoutes import tts_router
6
  from .Embedding.EmbeddingRoutes import embeddigs_router
7
+ from .TTS.utils.Pi import PiAIClient
8
 
9
  from fastapi.middleware.cors import CORSMiddleware
10
 
 
33
  allow_headers=["*"],
34
  )
35
  app.add_middleware(GZipMiddleware, minimum_size=1000)
36
+ pi = PiAIClient(headless=True)
37
 
38
 
39
  @app.on_event("startup")
40
  async def startup():
41
  FastAPICache.init(InMemoryBackend())
42
+ await pi.setup()
43
 
44
 
45
  @app.get("/")
Dockerfile CHANGED
@@ -28,7 +28,7 @@ RUN apt-get update && \
28
  #copy requirements
29
  COPY requirements.txt .
30
  RUN pip install --no-cache-dir -r requirements.txt
31
-
32
 
33
 
34
  # Copy the application code
@@ -37,6 +37,6 @@ USER admin
37
  COPY --chown=admin . /srv
38
 
39
  # Command to run the application
40
- CMD uvicorn App.app:app --host 0.0.0.0 --port 7860 --workers 1
41
  # Expose the server port
42
  EXPOSE 7860
 
28
  #copy requirements
29
  COPY requirements.txt .
30
  RUN pip install --no-cache-dir -r requirements.txt
31
+ RUN playwright install-deps
32
 
33
 
34
  # Copy the application code
 
37
  COPY --chown=admin . /srv
38
 
39
  # Command to run the application
40
+ CMD playwright install && uvicorn App.app:app --host 0.0.0.0 --port 7860 --workers 1
41
  # Expose the server port
42
  EXPOSE 7860