webscout 6.4__py3-none-any.whl → 6.5__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 webscout might be problematic. Click here for more details.
- webscout/AIutel.py +7 -54
- webscout/DWEBS.py +48 -26
- webscout/{YTdownloader.py → Extra/YTToolkit/YTdownloader.py} +990 -1103
- webscout/Extra/YTToolkit/__init__.py +3 -0
- webscout/{transcriber.py → Extra/YTToolkit/transcriber.py} +1 -1
- webscout/Extra/YTToolkit/ytapi/__init__.py +6 -0
- webscout/Extra/YTToolkit/ytapi/channel.py +307 -0
- webscout/Extra/YTToolkit/ytapi/errors.py +13 -0
- webscout/Extra/YTToolkit/ytapi/extras.py +45 -0
- webscout/Extra/YTToolkit/ytapi/https.py +88 -0
- webscout/Extra/YTToolkit/ytapi/patterns.py +61 -0
- webscout/Extra/YTToolkit/ytapi/playlist.py +59 -0
- webscout/Extra/YTToolkit/ytapi/pool.py +8 -0
- webscout/Extra/YTToolkit/ytapi/query.py +37 -0
- webscout/Extra/YTToolkit/ytapi/stream.py +60 -0
- webscout/Extra/YTToolkit/ytapi/utils.py +62 -0
- webscout/Extra/YTToolkit/ytapi/video.py +102 -0
- webscout/Extra/__init__.py +2 -1
- webscout/Extra/autocoder/rawdog.py +679 -680
- webscout/Extra/gguf.py +441 -441
- webscout/Extra/markdownlite/__init__.py +862 -0
- webscout/Extra/weather_ascii.py +2 -2
- webscout/Provider/PI.py +292 -221
- webscout/Provider/Perplexity.py +6 -14
- webscout/Provider/Reka.py +0 -1
- webscout/Provider/TTS/__init__.py +5 -1
- webscout/Provider/TTS/deepgram.py +183 -0
- webscout/Provider/TTS/elevenlabs.py +137 -0
- webscout/Provider/TTS/gesserit.py +151 -0
- webscout/Provider/TTS/murfai.py +139 -0
- webscout/Provider/TTS/parler.py +134 -107
- webscout/Provider/TTS/streamElements.py +360 -275
- webscout/Provider/TTS/utils.py +280 -0
- webscout/Provider/TTS/voicepod.py +116 -116
- webscout/Provider/__init__.py +146 -146
- webscout/Provider/meta.py +794 -779
- webscout/Provider/typegpt.py +1 -2
- webscout/__init__.py +24 -28
- webscout/litprinter/__init__.py +831 -830
- webscout/optimizers.py +269 -269
- webscout/prompt_manager.py +279 -279
- webscout/scout/__init__.py +11 -0
- webscout/scout/core.py +884 -0
- webscout/scout/element.py +459 -0
- webscout/scout/parsers/__init__.py +69 -0
- webscout/scout/parsers/html5lib_parser.py +172 -0
- webscout/scout/parsers/html_parser.py +236 -0
- webscout/scout/parsers/lxml_parser.py +178 -0
- webscout/scout/utils.py +38 -0
- webscout/update_checker.py +125 -125
- webscout/version.py +1 -1
- webscout/zeroart/__init__.py +55 -0
- webscout/zeroart/base.py +61 -0
- webscout/zeroart/effects.py +99 -0
- webscout/zeroart/fonts.py +816 -0
- webscout/zerodir/__init__.py +225 -0
- {webscout-6.4.dist-info → webscout-6.5.dist-info}/METADATA +12 -68
- {webscout-6.4.dist-info → webscout-6.5.dist-info}/RECORD +62 -37
- webscout/Agents/Onlinesearcher.py +0 -182
- webscout/Agents/__init__.py +0 -2
- webscout/Agents/functioncall.py +0 -248
- webscout/Bing_search.py +0 -251
- webscout/gpt4free.py +0 -666
- webscout/requestsHTMLfix.py +0 -775
- webscout/webai.py +0 -2590
- {webscout-6.4.dist-info → webscout-6.5.dist-info}/LICENSE.md +0 -0
- {webscout-6.4.dist-info → webscout-6.5.dist-info}/WHEEL +0 -0
- {webscout-6.4.dist-info → webscout-6.5.dist-info}/entry_points.txt +0 -0
- {webscout-6.4.dist-info → webscout-6.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Text processing utilities for TTS providers.
|
|
3
|
+
"""
|
|
4
|
+
from typing import List, Dict, Tuple, Set, Optional, Pattern
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SentenceTokenizer:
|
|
9
|
+
"""Advanced sentence tokenizer with support for complex cases and proper formatting."""
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
# Common abbreviations by category
|
|
13
|
+
self.TITLES: Set[str] = {
|
|
14
|
+
'mr', 'mrs', 'ms', 'dr', 'prof', 'rev', 'sr', 'jr', 'esq',
|
|
15
|
+
'hon', 'pres', 'gov', 'atty', 'supt', 'det', 'rev', 'col','maj', 'gen', 'capt', 'cmdr',
|
|
16
|
+
'lt', 'sgt', 'cpl', 'pvt'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
self.ACADEMIC: Set[str] = {
|
|
20
|
+
'ph.d', 'phd', 'm.d', 'md', 'b.a', 'ba', 'm.a', 'ma', 'd.d.s', 'dds',
|
|
21
|
+
'm.b.a', 'mba', 'b.sc', 'bsc', 'm.sc', 'msc', 'llb', 'll.b', 'bl'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
self.ORGANIZATIONS: Set[str] = {
|
|
25
|
+
'inc', 'ltd', 'co', 'corp', 'llc', 'llp', 'assn', 'bros', 'plc', 'cos',
|
|
26
|
+
'intl', 'dept', 'est', 'dist', 'mfg', 'div'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
self.MONTHS: Set[str] = {
|
|
30
|
+
'jan', 'feb', 'mar', 'apr', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
self.UNITS: Set[str] = {
|
|
34
|
+
'oz', 'pt', 'qt', 'gal', 'ml', 'cc', 'km', 'cm', 'mm', 'ft', 'in',
|
|
35
|
+
'kg', 'lb', 'lbs', 'hz', 'khz', 'mhz', 'ghz', 'kb', 'mb', 'gb', 'tb'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
self.TECHNOLOGY: Set[str] = {
|
|
39
|
+
'v', 'ver', 'app', 'sys', 'dir', 'exe', 'lib', 'api', 'sdk', 'url',
|
|
40
|
+
'cpu', 'gpu', 'ram', 'rom', 'hdd', 'ssd', 'lan', 'wan', 'sql', 'html'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
self.MISC: Set[str] = {
|
|
44
|
+
'vs', 'etc', 'ie', 'eg', 'no', 'al', 'ca', 'cf', 'pp', 'est', 'st',
|
|
45
|
+
'approx', 'appt', 'apt', 'dept', 'depts', 'min', 'max', 'avg'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Combine all abbreviations
|
|
49
|
+
self.all_abbreviations: Set[str] = (
|
|
50
|
+
self.TITLES | self.ACADEMIC | self.ORGANIZATIONS |
|
|
51
|
+
self.MONTHS | self.UNITS | self.TECHNOLOGY | self.MISC
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Special patterns
|
|
55
|
+
self.ELLIPSIS: str = r'\.{2,}|…'
|
|
56
|
+
self.URL_PATTERN: str = (
|
|
57
|
+
r'(?:https?:\/\/|www\.)[\w\-\.]+\.[a-zA-Z]{2,}(?:\/[^\s]*)?'
|
|
58
|
+
)
|
|
59
|
+
self.EMAIL_PATTERN: str = r'[\w\.-]+@[\w\.-]+\.\w+'
|
|
60
|
+
self.NUMBER_PATTERN: str = (
|
|
61
|
+
r'\d+(?:\.\d+)?(?:%|°|km|cm|mm|m|kg|g|lb|ft|in|mph|kmh|hz|mhz|ghz)?'
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Quote and bracket pairs
|
|
65
|
+
self.QUOTE_PAIRS: Dict[str, str] = {
|
|
66
|
+
'"': '"', "'": "'", '"': '"', "「": "」", "『": "』",
|
|
67
|
+
"«": "»", "‹": "›", "'": "'", "‚": "'"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
self.BRACKETS: Dict[str, str] = {
|
|
71
|
+
'(': ')', '[': ']', '{': '}', '⟨': '⟩', '「': '」',
|
|
72
|
+
'『': '』', '【': '】', '〖': '〗', '「': '」'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Compile regex patterns
|
|
76
|
+
self._compile_patterns()
|
|
77
|
+
|
|
78
|
+
def _compile_patterns(self) -> None:
|
|
79
|
+
"""Compile regex patterns for better performance."""
|
|
80
|
+
# Pattern for finding potential sentence boundaries
|
|
81
|
+
self.SENTENCE_END: Pattern = re.compile(
|
|
82
|
+
r'''
|
|
83
|
+
# Group for sentence endings
|
|
84
|
+
(?:
|
|
85
|
+
# Standard endings with optional quotes/brackets
|
|
86
|
+
(?<=[.!?])[\"\'\)\]\}»›」』\s]*
|
|
87
|
+
|
|
88
|
+
# Ellipsis
|
|
89
|
+
|(?:\.{2,}|…)
|
|
90
|
+
|
|
91
|
+
# Asian-style endings
|
|
92
|
+
|(?<=[。!?」』】\s])
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Must be followed by whitespace and capital letter or number
|
|
96
|
+
(?=\s+(?:[A-Z0-9]|["'({[\[「『《‹〈][A-Z]))
|
|
97
|
+
''',
|
|
98
|
+
re.VERBOSE
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Pattern for abbreviations
|
|
102
|
+
abbrev_pattern = '|'.join(re.escape(abbr) for abbr in self.all_abbreviations)
|
|
103
|
+
self.ABBREV_PATTERN: Pattern = re.compile(
|
|
104
|
+
fr'\b(?:{abbrev_pattern})\.?',
|
|
105
|
+
re.IGNORECASE
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def _protect_special_cases(self, text: str) -> Tuple[str, Dict[str, str]]:
|
|
109
|
+
"""Protect URLs, emails, and other special cases from being split."""
|
|
110
|
+
protected = text
|
|
111
|
+
placeholders: Dict[str, str] = {}
|
|
112
|
+
counter = 0
|
|
113
|
+
|
|
114
|
+
# Protect URLs and emails
|
|
115
|
+
for pattern in [self.URL_PATTERN, self.EMAIL_PATTERN]:
|
|
116
|
+
for match in re.finditer(pattern, protected):
|
|
117
|
+
placeholder = f'__PROTECTED_{counter}__'
|
|
118
|
+
placeholders[placeholder] = match.group()
|
|
119
|
+
protected = protected.replace(match.group(), placeholder)
|
|
120
|
+
counter += 1
|
|
121
|
+
|
|
122
|
+
# Protect quoted content
|
|
123
|
+
stack = []
|
|
124
|
+
protected_chars = list(protected)
|
|
125
|
+
i = 0
|
|
126
|
+
while i < len(protected_chars):
|
|
127
|
+
char = protected_chars[i]
|
|
128
|
+
if char in self.QUOTE_PAIRS:
|
|
129
|
+
stack.append((char, i))
|
|
130
|
+
elif stack and char == self.QUOTE_PAIRS[stack[-1][0]]:
|
|
131
|
+
start_quote, start_idx = stack.pop()
|
|
132
|
+
content = ''.join(protected_chars[start_idx:i + 1])
|
|
133
|
+
placeholder = f'__PROTECTED_{counter}__'
|
|
134
|
+
placeholders[placeholder] = content
|
|
135
|
+
protected_chars[start_idx:i + 1] = list(placeholder)
|
|
136
|
+
counter += 1
|
|
137
|
+
i += 1
|
|
138
|
+
|
|
139
|
+
return ''.join(protected_chars), placeholders
|
|
140
|
+
|
|
141
|
+
def _restore_special_cases(self, text: str, placeholders: Dict[str, str]) -> str:
|
|
142
|
+
"""Restore protected content."""
|
|
143
|
+
restored = text
|
|
144
|
+
for placeholder, original in placeholders.items():
|
|
145
|
+
restored = restored.replace(placeholder, original)
|
|
146
|
+
return restored
|
|
147
|
+
|
|
148
|
+
def _handle_abbreviations(self, text: str) -> str:
|
|
149
|
+
"""Handle abbreviations to prevent incorrect sentence splitting."""
|
|
150
|
+
def replace_abbrev(match: re.Match) -> str:
|
|
151
|
+
abbr = match.group().lower().rstrip('.')
|
|
152
|
+
if abbr in self.all_abbreviations:
|
|
153
|
+
return match.group().replace('.', '__DOT__')
|
|
154
|
+
return match.group()
|
|
155
|
+
|
|
156
|
+
return self.ABBREV_PATTERN.sub(replace_abbrev, text)
|
|
157
|
+
|
|
158
|
+
def _normalize_whitespace(self, text: str) -> str:
|
|
159
|
+
"""Normalize whitespace while preserving paragraph breaks."""
|
|
160
|
+
# Replace multiple newlines with special marker
|
|
161
|
+
text = re.sub(r'\n\s*\n', ' __PARA__ ', text)
|
|
162
|
+
# Normalize remaining whitespace
|
|
163
|
+
text = re.sub(r'\s+', ' ', text)
|
|
164
|
+
return text.strip()
|
|
165
|
+
|
|
166
|
+
def _restore_formatting(self, sentences: List[str]) -> List[str]:
|
|
167
|
+
"""Restore original formatting and clean up sentences."""
|
|
168
|
+
restored = []
|
|
169
|
+
for sentence in sentences:
|
|
170
|
+
# Restore dots in abbreviations
|
|
171
|
+
sentence = sentence.replace('__DOT__', '.')
|
|
172
|
+
|
|
173
|
+
# Restore paragraph breaks
|
|
174
|
+
sentence = sentence.replace('__PARA__', '\n\n')
|
|
175
|
+
|
|
176
|
+
# Clean up whitespace
|
|
177
|
+
sentence = re.sub(r'\s+', ' ', sentence).strip()
|
|
178
|
+
|
|
179
|
+
# Capitalize first letter if it's lowercase and not an abbreviation
|
|
180
|
+
words = sentence.split()
|
|
181
|
+
if words and words[0].lower() not in self.all_abbreviations:
|
|
182
|
+
sentence = sentence[0].upper() + sentence[1:]
|
|
183
|
+
|
|
184
|
+
if sentence:
|
|
185
|
+
restored.append(sentence)
|
|
186
|
+
|
|
187
|
+
return restored
|
|
188
|
+
|
|
189
|
+
def tokenize(self, text: str) -> List[str]:
|
|
190
|
+
"""
|
|
191
|
+
Split text into sentences while handling complex cases.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
text (str): Input text to split into sentences.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
List[str]: List of properly formatted sentences.
|
|
198
|
+
"""
|
|
199
|
+
if not text or not text.strip():
|
|
200
|
+
return []
|
|
201
|
+
|
|
202
|
+
# Step 1: Protect special cases
|
|
203
|
+
protected_text, placeholders = self._protect_special_cases(text)
|
|
204
|
+
|
|
205
|
+
# Step 2: Normalize whitespace
|
|
206
|
+
protected_text = self._normalize_whitespace(protected_text)
|
|
207
|
+
|
|
208
|
+
# Step 3: Handle abbreviations
|
|
209
|
+
protected_text = self._handle_abbreviations(protected_text)
|
|
210
|
+
|
|
211
|
+
# Step 4: Split into potential sentences
|
|
212
|
+
potential_sentences = self.SENTENCE_END.split(protected_text)
|
|
213
|
+
|
|
214
|
+
# Step 5: Process and restore formatting
|
|
215
|
+
sentences = self._restore_formatting(potential_sentences)
|
|
216
|
+
|
|
217
|
+
# Step 6: Restore special cases
|
|
218
|
+
sentences = [self._restore_special_cases(s, placeholders) for s in sentences]
|
|
219
|
+
|
|
220
|
+
# Step 7: Post-process sentences
|
|
221
|
+
final_sentences = []
|
|
222
|
+
current_sentence = []
|
|
223
|
+
|
|
224
|
+
for sentence in sentences:
|
|
225
|
+
# Skip empty sentences
|
|
226
|
+
if not sentence.strip():
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
# Check if sentence might be continuation of previous
|
|
230
|
+
if current_sentence and sentence[0].islower():
|
|
231
|
+
current_sentence.append(sentence)
|
|
232
|
+
else:
|
|
233
|
+
if current_sentence:
|
|
234
|
+
final_sentences.append(' '.join(current_sentence))
|
|
235
|
+
current_sentence = [sentence]
|
|
236
|
+
|
|
237
|
+
# Add last sentence if exists
|
|
238
|
+
if current_sentence:
|
|
239
|
+
final_sentences.append(' '.join(current_sentence))
|
|
240
|
+
|
|
241
|
+
return final_sentences
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def split_sentences(text: str) -> List[str]:
|
|
245
|
+
"""
|
|
246
|
+
Convenience function to split text into sentences using SentenceTokenizer.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
text (str): Input text to split into sentences.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
List[str]: List of properly formatted sentences.
|
|
253
|
+
"""
|
|
254
|
+
tokenizer = SentenceTokenizer()
|
|
255
|
+
return tokenizer.tokenize(text)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
if __name__ == "__main__":
|
|
259
|
+
# Test text with various challenging cases
|
|
260
|
+
test_text: str = """
|
|
261
|
+
Dr. Smith (Ph.D., M.D.) visited Washington D.C. on Jan. 20, 2024! He met with Prof. Johnson at 3:30 p.m.
|
|
262
|
+
They discussed A.I. and machine learning... "What about the U.S. market?" asked Dr. Smith.
|
|
263
|
+
The meeting ended at 5 p.m. Later, they went to Mr. Wilson's house (located at 123 Main St.) for dinner.
|
|
264
|
+
|
|
265
|
+
Visit our website at https://www.example.com or email us at test@example.com!
|
|
266
|
+
The temperature was 72.5°F (22.5°C). The company's Q3 2023 revenue was $12.5M USD.
|
|
267
|
+
|
|
268
|
+
「これは日本語の文章です。」This is a mixed-language text! How cool is that?
|
|
269
|
+
|
|
270
|
+
Some technical specs: CPU: 3.5GHz, RAM: 16GB, Storage: 2TB SSD.
|
|
271
|
+
Common abbreviations: etc., i.e., e.g., vs., cf., approx. 100 units.
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
# Process and print each sentence
|
|
275
|
+
sentences: List[str] = split_sentences(test_text)
|
|
276
|
+
print("Detected sentences:")
|
|
277
|
+
print("-" * 80)
|
|
278
|
+
for i, sentence in enumerate(sentences, 1):
|
|
279
|
+
print(f"{i}. {sentence}")
|
|
280
|
+
print("-" * 80)
|
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
import requests
|
|
2
|
-
import json
|
|
3
|
-
import time
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Generator
|
|
6
|
-
from playsound import playsound
|
|
7
|
-
from webscout import exceptions
|
|
8
|
-
from webscout.AIbase import TTSProvider
|
|
9
|
-
|
|
10
|
-
class Voicepods(TTSProvider):
|
|
11
|
-
"""
|
|
12
|
-
A class to interact with the Voicepods text-to-speech API.
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
def __init__(self, timeout: int = 20, proxies: dict = None):
|
|
16
|
-
"""
|
|
17
|
-
Initializes the Voicepods API client.
|
|
18
|
-
"""
|
|
19
|
-
self.api_endpoint = "https://voicepods-stream.vercel.app/api/resemble"
|
|
20
|
-
self.headers = {
|
|
21
|
-
'Accept': '*/*',
|
|
22
|
-
'Accept-Encoding': 'gzip, deflate, br, zstd',
|
|
23
|
-
'Accept-Language': 'en-US,en;q=0.9,en-IN;q=0.8',
|
|
24
|
-
'Content-Type': 'application/json',
|
|
25
|
-
'DNT': '1',
|
|
26
|
-
'Origin': 'https://voicepods-stream.vercel.app',
|
|
27
|
-
'Referer': 'https://voicepods-stream.vercel.app/',
|
|
28
|
-
'Sec-CH-UA': '"Chromium";v="128", "Not;A=Brand";v="24", "Microsoft Edge";v="128"',
|
|
29
|
-
'Sec-CH-UA-Mobile': '?0',
|
|
30
|
-
'Sec-CH-UA-Platform': '"Windows"',
|
|
31
|
-
'Sec-Fetch-Dest': 'empty',
|
|
32
|
-
'Sec-Fetch-Mode': 'cors',
|
|
33
|
-
'Sec-Fetch-Site': 'same-origin',
|
|
34
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0',
|
|
35
|
-
}
|
|
36
|
-
self.session = requests.Session()
|
|
37
|
-
self.session.headers.update(self.headers)
|
|
38
|
-
if proxies:
|
|
39
|
-
self.session.proxies.update(proxies)
|
|
40
|
-
self.timeout = timeout
|
|
41
|
-
self.audio_cache_dir = Path("./audio_cache")
|
|
42
|
-
|
|
43
|
-
def tts(self, text: str) -> str:
|
|
44
|
-
"""
|
|
45
|
-
Converts text to speech using the Voicepods API.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
text (str): The text to be converted to speech.
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
str: The filename of the saved audio file.
|
|
52
|
-
|
|
53
|
-
Raises:
|
|
54
|
-
exceptions.FailedToGenerateResponseError: If there is an error generating or saving the audio.
|
|
55
|
-
"""
|
|
56
|
-
payload = json.dumps({"query": text})
|
|
57
|
-
filename = self.audio_cache_dir / f"{int(time.time())}.wav" # Using timestamp for filename
|
|
58
|
-
|
|
59
|
-
try:
|
|
60
|
-
response = self.session.post(self.api_endpoint, data=payload, timeout=self.timeout)
|
|
61
|
-
response.raise_for_status()
|
|
62
|
-
|
|
63
|
-
content_type = response.headers.get('Content-Type', '')
|
|
64
|
-
if 'audio' not in content_type.lower():
|
|
65
|
-
raise ValueError(f"Unexpected content type: {content_type}")
|
|
66
|
-
|
|
67
|
-
audio_data = response.content
|
|
68
|
-
self._save_audio(audio_data, filename)
|
|
69
|
-
return filename.as_posix() # Return the filename as a string
|
|
70
|
-
|
|
71
|
-
except requests.exceptions.RequestException as e:
|
|
72
|
-
raise exceptions.FailedToGenerateResponseError(f"Error generating audio: {e}")
|
|
73
|
-
|
|
74
|
-
def _save_audio(self, audio_data: bytes, filename: Path):
|
|
75
|
-
"""Saves the audio data to a WAV file in the audio cache directory."""
|
|
76
|
-
try:
|
|
77
|
-
# Create the audio_cache directory if it doesn't exist
|
|
78
|
-
self.audio_cache_dir.mkdir(parents=True, exist_ok=True)
|
|
79
|
-
|
|
80
|
-
riff_start = audio_data.find(b'RIFF')
|
|
81
|
-
if riff_start == -1:
|
|
82
|
-
raise ValueError("RIFF header not found in audio data")
|
|
83
|
-
|
|
84
|
-
trimmed_audio_data = audio_data[riff_start:]
|
|
85
|
-
|
|
86
|
-
with open(filename, "wb") as f:
|
|
87
|
-
f.write(trimmed_audio_data)
|
|
88
|
-
|
|
89
|
-
except Exception as e:
|
|
90
|
-
raise exceptions.FailedToGenerateResponseError(f"Error saving audio: {e}")
|
|
91
|
-
|
|
92
|
-
def play_audio(self, filename: str):
|
|
93
|
-
"""
|
|
94
|
-
Plays an audio file using playsound.
|
|
95
|
-
|
|
96
|
-
Args:
|
|
97
|
-
filename (str): The path to the audio file.
|
|
98
|
-
|
|
99
|
-
Raises:
|
|
100
|
-
RuntimeError: If there is an error playing the audio.
|
|
101
|
-
"""
|
|
102
|
-
try:
|
|
103
|
-
playsound(filename)
|
|
104
|
-
except Exception as e:
|
|
105
|
-
raise RuntimeError(f"Error playing audio: {e}")
|
|
106
|
-
|
|
107
|
-
# Example usage
|
|
108
|
-
if __name__ == "__main__":
|
|
109
|
-
|
|
110
|
-
voicepods = Voicepods()
|
|
111
|
-
text = "Hello, this is a test of the Voicepods text-to-speech system."
|
|
112
|
-
|
|
113
|
-
print("Generating audio...")
|
|
114
|
-
audio_file = voicepods.tts(text)
|
|
115
|
-
|
|
116
|
-
print("Playing audio...")
|
|
1
|
+
import requests
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Generator
|
|
6
|
+
from playsound import playsound
|
|
7
|
+
from webscout import exceptions
|
|
8
|
+
from webscout.AIbase import TTSProvider
|
|
9
|
+
|
|
10
|
+
class Voicepods(TTSProvider):
|
|
11
|
+
"""
|
|
12
|
+
A class to interact with the Voicepods text-to-speech API.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, timeout: int = 20, proxies: dict = None):
|
|
16
|
+
"""
|
|
17
|
+
Initializes the Voicepods API client.
|
|
18
|
+
"""
|
|
19
|
+
self.api_endpoint = "https://voicepods-stream.vercel.app/api/resemble"
|
|
20
|
+
self.headers = {
|
|
21
|
+
'Accept': '*/*',
|
|
22
|
+
'Accept-Encoding': 'gzip, deflate, br, zstd',
|
|
23
|
+
'Accept-Language': 'en-US,en;q=0.9,en-IN;q=0.8',
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
'DNT': '1',
|
|
26
|
+
'Origin': 'https://voicepods-stream.vercel.app',
|
|
27
|
+
'Referer': 'https://voicepods-stream.vercel.app/',
|
|
28
|
+
'Sec-CH-UA': '"Chromium";v="128", "Not;A=Brand";v="24", "Microsoft Edge";v="128"',
|
|
29
|
+
'Sec-CH-UA-Mobile': '?0',
|
|
30
|
+
'Sec-CH-UA-Platform': '"Windows"',
|
|
31
|
+
'Sec-Fetch-Dest': 'empty',
|
|
32
|
+
'Sec-Fetch-Mode': 'cors',
|
|
33
|
+
'Sec-Fetch-Site': 'same-origin',
|
|
34
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0',
|
|
35
|
+
}
|
|
36
|
+
self.session = requests.Session()
|
|
37
|
+
self.session.headers.update(self.headers)
|
|
38
|
+
if proxies:
|
|
39
|
+
self.session.proxies.update(proxies)
|
|
40
|
+
self.timeout = timeout
|
|
41
|
+
self.audio_cache_dir = Path("./audio_cache")
|
|
42
|
+
|
|
43
|
+
def tts(self, text: str) -> str:
|
|
44
|
+
"""
|
|
45
|
+
Converts text to speech using the Voicepods API.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
text (str): The text to be converted to speech.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
str: The filename of the saved audio file.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
exceptions.FailedToGenerateResponseError: If there is an error generating or saving the audio.
|
|
55
|
+
"""
|
|
56
|
+
payload = json.dumps({"query": text})
|
|
57
|
+
filename = self.audio_cache_dir / f"{int(time.time())}.wav" # Using timestamp for filename
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
response = self.session.post(self.api_endpoint, data=payload, timeout=self.timeout)
|
|
61
|
+
response.raise_for_status()
|
|
62
|
+
|
|
63
|
+
content_type = response.headers.get('Content-Type', '')
|
|
64
|
+
if 'audio' not in content_type.lower():
|
|
65
|
+
raise ValueError(f"Unexpected content type: {content_type}")
|
|
66
|
+
|
|
67
|
+
audio_data = response.content
|
|
68
|
+
self._save_audio(audio_data, filename)
|
|
69
|
+
return filename.as_posix() # Return the filename as a string
|
|
70
|
+
|
|
71
|
+
except requests.exceptions.RequestException as e:
|
|
72
|
+
raise exceptions.FailedToGenerateResponseError(f"Error generating audio: {e}")
|
|
73
|
+
|
|
74
|
+
def _save_audio(self, audio_data: bytes, filename: Path):
|
|
75
|
+
"""Saves the audio data to a WAV file in the audio cache directory."""
|
|
76
|
+
try:
|
|
77
|
+
# Create the audio_cache directory if it doesn't exist
|
|
78
|
+
self.audio_cache_dir.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
|
|
80
|
+
riff_start = audio_data.find(b'RIFF')
|
|
81
|
+
if riff_start == -1:
|
|
82
|
+
raise ValueError("RIFF header not found in audio data")
|
|
83
|
+
|
|
84
|
+
trimmed_audio_data = audio_data[riff_start:]
|
|
85
|
+
|
|
86
|
+
with open(filename, "wb") as f:
|
|
87
|
+
f.write(trimmed_audio_data)
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
raise exceptions.FailedToGenerateResponseError(f"Error saving audio: {e}")
|
|
91
|
+
|
|
92
|
+
def play_audio(self, filename: str):
|
|
93
|
+
"""
|
|
94
|
+
Plays an audio file using playsound.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
filename (str): The path to the audio file.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
RuntimeError: If there is an error playing the audio.
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
playsound(filename)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
raise RuntimeError(f"Error playing audio: {e}")
|
|
106
|
+
|
|
107
|
+
# Example usage
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
|
|
110
|
+
voicepods = Voicepods()
|
|
111
|
+
text = "Hello, this is a test of the Voicepods text-to-speech system."
|
|
112
|
+
|
|
113
|
+
print("Generating audio...")
|
|
114
|
+
audio_file = voicepods.tts(text)
|
|
115
|
+
|
|
116
|
+
print("Playing audio...")
|
|
117
117
|
voicepods.play_audio(audio_file)
|