Skip to content

Commit 6245324

Browse files
authored
Merge pull request #3114 from FoamyGuy/add_spell_jam
Add Spell Jam project
2 parents 009abaf + 52c757a commit 6245324

35 files changed

+2966
-0
lines changed
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
import hmac
4+
import json
5+
6+
from adafruit_datetime import datetime
7+
import adafruit_hashlib as hashlib
8+
9+
10+
def url_encode(string, safe=""):
11+
"""
12+
Minimal URL encoding function to replace urllib.parse.quote
13+
14+
Args:
15+
string (str): String to encode
16+
safe (str): Characters that should not be encoded
17+
18+
Returns:
19+
str: URL encoded string
20+
"""
21+
# Characters that need to be encoded (RFC 3986)
22+
unreserved = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"
23+
24+
# Add safe characters to unreserved set
25+
allowed = unreserved + safe
26+
27+
encoded = ""
28+
for char in string:
29+
if char in allowed:
30+
encoded += char
31+
else:
32+
# Convert to percent-encoded format
33+
encoded += f"%{ord(char):02X}"
34+
35+
return encoded
36+
37+
38+
def _zero_pad(num, count=2):
39+
padded = str(num)
40+
while len(padded) < count:
41+
padded = "0" + padded
42+
return padded
43+
44+
45+
class PollyHTTPClient:
46+
# pylint: disable=no-self-use
47+
def __init__(self, requests_instance, access_key, secret_key, region="us-east-1"):
48+
self._requests = requests_instance
49+
self.access_key = access_key
50+
self.secret_key = secret_key
51+
self.region = region
52+
self.service = "polly"
53+
self.host = f"polly.{region}.amazonaws.com"
54+
self.endpoint = f"https://{self.host}"
55+
56+
def _sign(self, key, msg):
57+
"""Helper function for AWS signature"""
58+
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
59+
60+
def _get_signature_key(self, date_stamp):
61+
"""Generate AWS signature key"""
62+
k_date = self._sign(("AWS4" + self.secret_key).encode("utf-8"), date_stamp)
63+
k_region = self._sign(k_date, self.region)
64+
k_service = self._sign(k_region, self.service)
65+
k_signing = self._sign(k_service, "aws4_request")
66+
return k_signing
67+
68+
def _create_canonical_request(self, method, uri, query_string, headers, payload):
69+
"""Create canonical request for AWS Signature V4"""
70+
canonical_uri = url_encode(uri, safe="/")
71+
canonical_querystring = query_string
72+
73+
# Create canonical headers
74+
canonical_headers = ""
75+
signed_headers = ""
76+
header_names = sorted(headers.keys())
77+
for name in header_names:
78+
canonical_headers += f"{name.lower()}:{headers[name].strip()}\n"
79+
signed_headers += f"{name.lower()};"
80+
signed_headers = signed_headers[:-1] # Remove trailing semicolon
81+
82+
# Create payload hash
83+
payload_hash = hashlib.sha256(payload.encode("utf-8")).hexdigest()
84+
85+
# Create canonical request
86+
canonical_request = (f"{method}\n{canonical_uri}\n{canonical_querystring}\n"
87+
f"{canonical_headers}\n{signed_headers}\n{payload_hash}")
88+
89+
return canonical_request, signed_headers
90+
91+
def _create_string_to_sign(self, timestamp, credential_scope, canonical_request):
92+
"""Create string to sign for AWS Signature V4"""
93+
algorithm = "AWS4-HMAC-SHA256"
94+
canonical_request_hash = hashlib.sha256(
95+
canonical_request.encode("utf-8")
96+
).hexdigest()
97+
string_to_sign = (
98+
f"{algorithm}\n{timestamp}\n{credential_scope}\n{canonical_request_hash}"
99+
)
100+
return string_to_sign
101+
102+
def synthesize_speech( # pylint: disable=too-many-locals
103+
self,
104+
text,
105+
voice_id="Joanna",
106+
output_format="mp3",
107+
engine="standard",
108+
text_type="text",
109+
):
110+
"""
111+
Synthesize speech using Amazon Polly via direct HTTP request
112+
113+
Args:
114+
text (str): Text to convert to speech
115+
voice_id (str): Voice to use (e.g., 'Joanna', 'Matthew', 'Amy')
116+
output_format (str): Audio format ('mp3', 'ogg_vorbis', 'pcm')
117+
engine (str): Engine type ('standard' or 'neural')
118+
text_type (str): 'text' or 'ssml'
119+
120+
Returns:
121+
bytes: Audio data if successful, None if failed
122+
"""
123+
124+
# Prepare request
125+
method = "POST"
126+
uri = "/v1/speech"
127+
128+
# Create request body
129+
request_body = {
130+
"Text": text,
131+
"OutputFormat": output_format,
132+
"VoiceId": voice_id,
133+
"Engine": engine,
134+
"TextType": text_type,
135+
}
136+
payload = json.dumps(request_body)
137+
138+
# Get current time
139+
now = datetime.now()
140+
# amzdate = now.strftime('%Y%m%dT%H%M%SZ')
141+
amzdate = (f"{now.year}{_zero_pad(now.month)}{_zero_pad(now.day)}"
142+
f"T{_zero_pad(now.hour)}{_zero_pad(now.minute)}{_zero_pad(now.second)}Z")
143+
# datestamp = now.strftime('%Y%m%d')
144+
datestamp = f"{now.year}{_zero_pad(now.month)}{_zero_pad(now.day)}"
145+
146+
# Create headers
147+
headers = {
148+
"Content-Type": "application/x-amz-json-1.0",
149+
"Host": self.host,
150+
"X-Amz-Date": amzdate,
151+
"X-Amz-Target": "AWSPollyService.SynthesizeSpeech",
152+
}
153+
154+
# Create canonical request
155+
canonical_request, signed_headers = self._create_canonical_request(
156+
method, uri, "", headers, payload
157+
)
158+
159+
# Create string to sign
160+
credential_scope = f"{datestamp}/{self.region}/{self.service}/aws4_request"
161+
string_to_sign = self._create_string_to_sign(
162+
amzdate, credential_scope, canonical_request
163+
)
164+
165+
# Create signature
166+
signing_key = self._get_signature_key(datestamp)
167+
signature = hmac.new(
168+
signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
169+
).hexdigest()
170+
171+
# Add authorization header
172+
authorization_header = (
173+
f"AWS4-HMAC-SHA256 "
174+
f"Credential={self.access_key}/{credential_scope}, "
175+
f"SignedHeaders={signed_headers}, "
176+
f"Signature={signature}"
177+
)
178+
headers["Authorization"] = authorization_header
179+
180+
# Make request
181+
try:
182+
url = f"{self.endpoint}{uri}"
183+
response = self._requests.post(url, headers=headers, data=payload)
184+
185+
if response.status_code == 200:
186+
return response.content
187+
else:
188+
print(f"Error: HTTP {response.status_code}")
189+
print(f"Response: {response.text}")
190+
return None
191+
192+
except Exception as e: # pylint: disable=broad-except
193+
print(f"Request failed: {e}")
194+
return None
195+
196+
197+
def text_to_speech_polly_http(
198+
requests_instance,
199+
text,
200+
access_key,
201+
secret_key,
202+
output_file="/saves/awspollyoutput.mp3",
203+
voice_id="Joanna",
204+
region="us-east-1",
205+
output_format="mp3",
206+
):
207+
"""
208+
Simple function to convert text to speech using Polly HTTP API
209+
210+
Args:
211+
text (str): Text to convert
212+
access_key (str): AWS Access Key ID
213+
secret_key (str): AWS Secret Access Key
214+
output_file (str): Output file path
215+
voice_id (str): Polly voice ID
216+
region (str): AWS region
217+
output_format (str): Audio format
218+
219+
Returns:
220+
bool: True if successful, False otherwise
221+
"""
222+
223+
# Create Polly client
224+
client = PollyHTTPClient(requests_instance, access_key, secret_key, region)
225+
226+
# Synthesize speech
227+
audio_data = client.synthesize_speech(
228+
text=text, voice_id=voice_id, output_format=output_format
229+
)
230+
231+
if audio_data:
232+
# Save to file
233+
try:
234+
with open(output_file, "wb") as f:
235+
f.write(audio_data)
236+
print(f"Audio saved to: {output_file}")
237+
return True
238+
except Exception as e: # pylint: disable=broad-except
239+
print(f"Failed to save file: {e}")
240+
return False
241+
else:
242+
print("Failed to synthesize speech")
243+
return False
244+
245+
246+
def text_to_speech_with_ssml(
247+
requests_instance,
248+
text,
249+
access_key,
250+
secret_key,
251+
speech_rate="medium",
252+
output_file="output.mp3",
253+
):
254+
"""
255+
Example with SSML for speech rate control
256+
"""
257+
# Wrap text in SSML
258+
ssml_text = f'<speak><prosody rate="{speech_rate}">{text}</prosody></speak>'
259+
260+
client = PollyHTTPClient(requests_instance, access_key, secret_key)
261+
audio_data = client.synthesize_speech(
262+
text=ssml_text, voice_id="Joanna", text_type="ssml"
263+
)
264+
265+
if audio_data:
266+
with open(output_file, "wb") as f:
267+
f.write(audio_data)
268+
print(f"SSML audio saved to: {output_file}")
269+
return True
270+
return False
271+
272+
273+
# Example usage
274+
# if __name__ == "__main__":
275+
# # Replace with your actual AWS credentials
276+
# AWS_ACCESS_KEY = os.getenv('AWS_ACCESS_KEY')
277+
# AWS_SECRET_KEY = os.getenv('AWS_SECRET_KEY')
278+
#
279+
# # Basic usage
280+
# success = text_to_speech_polly_http(
281+
# text="Hello from CircuitPython! This is Amazon Polly speaking.",
282+
# access_key=AWS_ACCESS_KEY,
283+
# secret_key=AWS_SECRET_KEY,
284+
# voice_id="Joanna"
285+
# )
286+
287+
# SSML example
288+
# if success:
289+
# text_to_speech_with_ssml(
290+
# text="This speech has a custom rate using SSML markup.",
291+
# access_key=AWS_ACCESS_KEY,
292+
# secret_key=AWS_SECRET_KEY,
293+
# speech_rate="slow",
294+
# output_file="ssml_example.mp3"
295+
# )

0 commit comments

Comments
 (0)