Skip to content

Commit 02dc511

Browse files
committed
Adding Spell Jam
1 parent 1edc2c7 commit 02dc511

33 files changed

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

0 commit comments

Comments
 (0)