revert accidental commit
This commit is contained in:
parent
f34bab8fd8
commit
009abaff09
35 changed files with 0 additions and 2965 deletions
|
|
@ -1,294 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
|
||||
# SPDX-License-Identifier: MIT
|
||||
import hmac
|
||||
import json
|
||||
|
||||
from adafruit_datetime import datetime
|
||||
import adafruit_hashlib as hashlib
|
||||
|
||||
|
||||
def url_encode(string, safe=""):
|
||||
"""
|
||||
Minimal URL encoding function to replace urllib.parse.quote
|
||||
|
||||
Args:
|
||||
string (str): String to encode
|
||||
safe (str): Characters that should not be encoded
|
||||
|
||||
Returns:
|
||||
str: URL encoded string
|
||||
"""
|
||||
# Characters that need to be encoded (RFC 3986)
|
||||
unreserved = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"
|
||||
|
||||
# Add safe characters to unreserved set
|
||||
allowed = unreserved + safe
|
||||
|
||||
encoded = ""
|
||||
for char in string:
|
||||
if char in allowed:
|
||||
encoded += char
|
||||
else:
|
||||
# Convert to percent-encoded format
|
||||
encoded += f"%{ord(char):02X}"
|
||||
|
||||
return encoded
|
||||
|
||||
|
||||
def _zero_pad(num, count=2):
|
||||
padded = str(num)
|
||||
while len(padded) < count:
|
||||
padded = "0" + padded
|
||||
return padded
|
||||
|
||||
|
||||
class PollyHTTPClient:
|
||||
def __init__(self, requests_instance, access_key, secret_key, region="us-east-1"):
|
||||
self._requests = requests_instance
|
||||
self.access_key = access_key
|
||||
self.secret_key = secret_key
|
||||
self.region = region
|
||||
self.service = "polly"
|
||||
self.host = f"polly.{region}.amazonaws.com"
|
||||
self.endpoint = f"https://{self.host}"
|
||||
|
||||
def _sign(self, key, msg):
|
||||
"""Helper function for AWS signature"""
|
||||
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
|
||||
|
||||
def _get_signature_key(self, date_stamp):
|
||||
"""Generate AWS signature key"""
|
||||
k_date = self._sign(("AWS4" + self.secret_key).encode("utf-8"), date_stamp)
|
||||
k_region = self._sign(k_date, self.region)
|
||||
k_service = self._sign(k_region, self.service)
|
||||
k_signing = self._sign(k_service, "aws4_request")
|
||||
return k_signing
|
||||
|
||||
def _create_canonical_request(self, method, uri, query_string, headers, payload):
|
||||
"""Create canonical request for AWS Signature V4"""
|
||||
canonical_uri = url_encode(uri, safe="/")
|
||||
canonical_querystring = query_string
|
||||
|
||||
# Create canonical headers
|
||||
canonical_headers = ""
|
||||
signed_headers = ""
|
||||
header_names = sorted(headers.keys())
|
||||
for name in header_names:
|
||||
canonical_headers += f"{name.lower()}:{headers[name].strip()}\n"
|
||||
signed_headers += f"{name.lower()};"
|
||||
signed_headers = signed_headers[:-1] # Remove trailing semicolon
|
||||
|
||||
# Create payload hash
|
||||
payload_hash = hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||
|
||||
# Create canonical request
|
||||
canonical_request = (f"{method}\n{canonical_uri}\n{canonical_querystring}\n"
|
||||
f"{canonical_headers}\n{signed_headers}\n{payload_hash}")
|
||||
|
||||
return canonical_request, signed_headers
|
||||
|
||||
def _create_string_to_sign(self, timestamp, credential_scope, canonical_request):
|
||||
"""Create string to sign for AWS Signature V4"""
|
||||
algorithm = "AWS4-HMAC-SHA256"
|
||||
canonical_request_hash = hashlib.sha256(
|
||||
canonical_request.encode("utf-8")
|
||||
).hexdigest()
|
||||
string_to_sign = (
|
||||
f"{algorithm}\n{timestamp}\n{credential_scope}\n{canonical_request_hash}"
|
||||
)
|
||||
return string_to_sign
|
||||
|
||||
def synthesize_speech( # pylint: disable=too-many-locals
|
||||
self,
|
||||
text,
|
||||
voice_id="Joanna",
|
||||
output_format="mp3",
|
||||
engine="standard",
|
||||
text_type="text",
|
||||
):
|
||||
"""
|
||||
Synthesize speech using Amazon Polly via direct HTTP request
|
||||
|
||||
Args:
|
||||
text (str): Text to convert to speech
|
||||
voice_id (str): Voice to use (e.g., 'Joanna', 'Matthew', 'Amy')
|
||||
output_format (str): Audio format ('mp3', 'ogg_vorbis', 'pcm')
|
||||
engine (str): Engine type ('standard' or 'neural')
|
||||
text_type (str): 'text' or 'ssml'
|
||||
|
||||
Returns:
|
||||
bytes: Audio data if successful, None if failed
|
||||
"""
|
||||
|
||||
# Prepare request
|
||||
method = "POST"
|
||||
uri = "/v1/speech"
|
||||
|
||||
# Create request body
|
||||
request_body = {
|
||||
"Text": text,
|
||||
"OutputFormat": output_format,
|
||||
"VoiceId": voice_id,
|
||||
"Engine": engine,
|
||||
"TextType": text_type,
|
||||
}
|
||||
payload = json.dumps(request_body)
|
||||
|
||||
# Get current time
|
||||
now = datetime.now()
|
||||
# amzdate = now.strftime('%Y%m%dT%H%M%SZ')
|
||||
amzdate = (f"{now.year}{_zero_pad(now.month)}{_zero_pad(now.day)}"
|
||||
f"T{_zero_pad(now.hour)}{_zero_pad(now.minute)}{_zero_pad(now.second)}Z")
|
||||
# datestamp = now.strftime('%Y%m%d')
|
||||
datestamp = f"{now.year}{_zero_pad(now.month)}{_zero_pad(now.day)}"
|
||||
|
||||
# Create headers
|
||||
headers = {
|
||||
"Content-Type": "application/x-amz-json-1.0",
|
||||
"Host": self.host,
|
||||
"X-Amz-Date": amzdate,
|
||||
"X-Amz-Target": "AWSPollyService.SynthesizeSpeech",
|
||||
}
|
||||
|
||||
# Create canonical request
|
||||
canonical_request, signed_headers = self._create_canonical_request(
|
||||
method, uri, "", headers, payload
|
||||
)
|
||||
|
||||
# Create string to sign
|
||||
credential_scope = f"{datestamp}/{self.region}/{self.service}/aws4_request"
|
||||
string_to_sign = self._create_string_to_sign(
|
||||
amzdate, credential_scope, canonical_request
|
||||
)
|
||||
|
||||
# Create signature
|
||||
signing_key = self._get_signature_key(datestamp)
|
||||
signature = hmac.new(
|
||||
signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
# Add authorization header
|
||||
authorization_header = (
|
||||
f"AWS4-HMAC-SHA256 "
|
||||
f"Credential={self.access_key}/{credential_scope}, "
|
||||
f"SignedHeaders={signed_headers}, "
|
||||
f"Signature={signature}"
|
||||
)
|
||||
headers["Authorization"] = authorization_header
|
||||
|
||||
# Make request
|
||||
try:
|
||||
url = f"{self.endpoint}{uri}"
|
||||
response = self._requests.post(url, headers=headers, data=payload)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.content
|
||||
else:
|
||||
print(f"Error: HTTP {response.status_code}")
|
||||
print(f"Response: {response.text}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"Request failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def text_to_speech_polly_http(
|
||||
requests_instance,
|
||||
text,
|
||||
access_key,
|
||||
secret_key,
|
||||
output_file="/saves/awspollyoutput.mp3",
|
||||
voice_id="Joanna",
|
||||
region="us-east-1",
|
||||
output_format="mp3",
|
||||
):
|
||||
"""
|
||||
Simple function to convert text to speech using Polly HTTP API
|
||||
|
||||
Args:
|
||||
text (str): Text to convert
|
||||
access_key (str): AWS Access Key ID
|
||||
secret_key (str): AWS Secret Access Key
|
||||
output_file (str): Output file path
|
||||
voice_id (str): Polly voice ID
|
||||
region (str): AWS region
|
||||
output_format (str): Audio format
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
|
||||
# Create Polly client
|
||||
client = PollyHTTPClient(requests_instance, access_key, secret_key, region)
|
||||
|
||||
# Synthesize speech
|
||||
audio_data = client.synthesize_speech(
|
||||
text=text, voice_id=voice_id, output_format=output_format
|
||||
)
|
||||
|
||||
if audio_data:
|
||||
# Save to file
|
||||
try:
|
||||
with open(output_file, "wb") as f:
|
||||
f.write(audio_data)
|
||||
print(f"Audio saved to: {output_file}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Failed to save file: {e}")
|
||||
return False
|
||||
else:
|
||||
print("Failed to synthesize speech")
|
||||
return False
|
||||
|
||||
|
||||
def text_to_speech_with_ssml(
|
||||
requests_instance,
|
||||
text,
|
||||
access_key,
|
||||
secret_key,
|
||||
speech_rate="medium",
|
||||
output_file="output.mp3",
|
||||
):
|
||||
"""
|
||||
Example with SSML for speech rate control
|
||||
"""
|
||||
# Wrap text in SSML
|
||||
ssml_text = f'<speak><prosody rate="{speech_rate}">{text}</prosody></speak>'
|
||||
|
||||
client = PollyHTTPClient(requests_instance, access_key, secret_key)
|
||||
audio_data = client.synthesize_speech(
|
||||
text=ssml_text, voice_id="Joanna", text_type="ssml"
|
||||
)
|
||||
|
||||
if audio_data:
|
||||
with open(output_file, "wb") as f:
|
||||
f.write(audio_data)
|
||||
print(f"SSML audio saved to: {output_file}")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# Example usage
|
||||
# if __name__ == "__main__":
|
||||
# # Replace with your actual AWS credentials
|
||||
# AWS_ACCESS_KEY = os.getenv('AWS_ACCESS_KEY')
|
||||
# AWS_SECRET_KEY = os.getenv('AWS_SECRET_KEY')
|
||||
#
|
||||
# # Basic usage
|
||||
# success = text_to_speech_polly_http(
|
||||
# text="Hello from CircuitPython! This is Amazon Polly speaking.",
|
||||
# access_key=AWS_ACCESS_KEY,
|
||||
# secret_key=AWS_SECRET_KEY,
|
||||
# voice_id="Joanna"
|
||||
# )
|
||||
|
||||
# SSML example
|
||||
# if success:
|
||||
# text_to_speech_with_ssml(
|
||||
# text="This speech has a custom rate using SSML markup.",
|
||||
# access_key=AWS_ACCESS_KEY,
|
||||
# secret_key=AWS_SECRET_KEY,
|
||||
# speech_rate="slow",
|
||||
# output_file="ssml_example.mp3"
|
||||
# )
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
|
||||
# SPDX-License-Identifier: MIT
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import supervisor
|
||||
from adafruit_fruitjam import FruitJam
|
||||
from adafruit_fruitjam.peripherals import request_display_config
|
||||
import adafruit_connection_manager
|
||||
import adafruit_requests
|
||||
from displayio import OnDiskBitmap, TileGrid, Group
|
||||
from adafruit_bitmap_font import bitmap_font
|
||||
from adafruit_display_text.bitmap_label import Label
|
||||
|
||||
from aws_polly import text_to_speech_polly_http
|
||||
|
||||
# constants
|
||||
LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
# local variables
|
||||
curword = ""
|
||||
lastword = ""
|
||||
|
||||
# setup display
|
||||
request_display_config(320, 240)
|
||||
display = supervisor.runtime.display
|
||||
|
||||
# setup background image
|
||||
main_group = Group()
|
||||
background = OnDiskBitmap("spell_jam_assets/background.bmp")
|
||||
background_tg = TileGrid(background, pixel_shader=background.pixel_shader)
|
||||
main_group.append(background_tg)
|
||||
|
||||
# setup 14-segment label used to display words
|
||||
font = bitmap_font.load_font("spell_jam_assets/14segment_16.bdf")
|
||||
screen_lbl = Label(text="Type a word", font=font, color=0x00FF00)
|
||||
screen_lbl.anchor_point = (0.5, 0)
|
||||
screen_lbl.anchored_position = (display.width // 2, 100)
|
||||
main_group.append(screen_lbl)
|
||||
|
||||
# initialize Fruit Jam built-in hardware
|
||||
fj = FruitJam()
|
||||
fj.neopixels.brightness = 0.1
|
||||
fj.peripherals.volume = 9
|
||||
|
||||
# AWS auth requires us to have accurate date/time
|
||||
now = fj.sync_time()
|
||||
|
||||
# setup adafruit_requests session
|
||||
# pylint: disable=protected-access
|
||||
pool = adafruit_connection_manager.get_radio_socketpool(fj.network._wifi.esp)
|
||||
ssl_context = adafruit_connection_manager.get_radio_ssl_context(fj.network._wifi.esp)
|
||||
requests = adafruit_requests.Session(pool, ssl_context)
|
||||
|
||||
# read AWS keys from settings.toml
|
||||
AWS_ACCESS_KEY = os.getenv("AWS_ACCESS_KEY")
|
||||
AWS_SECRET_KEY = os.getenv("AWS_SECRET_KEY")
|
||||
|
||||
|
||||
def fetch_word(word, voice="Joanna"):
|
||||
"""
|
||||
Fetch an MP3 saying a word from AWS Polly
|
||||
:param word: The word to speak
|
||||
:param voice: The AWS Polly voide ID to use
|
||||
:return: Boolean, whether the request was successful.
|
||||
"""
|
||||
fj.neopixels.fill(0xFFFF00)
|
||||
success = text_to_speech_polly_http(
|
||||
requests,
|
||||
text=word,
|
||||
access_key=AWS_ACCESS_KEY,
|
||||
secret_key=AWS_SECRET_KEY,
|
||||
voice_id=voice,
|
||||
)
|
||||
fj.neopixels.fill(0x00FF00)
|
||||
return success
|
||||
|
||||
|
||||
def say_and_spell_lastword():
|
||||
"""
|
||||
Say the last word, then spell it out one letter at a time, finally say it once more.
|
||||
"""
|
||||
fj.play_mp3_file("saves/awspollyoutput.mp3")
|
||||
time.sleep(0.2)
|
||||
for letter in lastword:
|
||||
fj.play_mp3_file(f"spell_jam_assets/letter_mp3s/{letter.upper()}.mp3")
|
||||
time.sleep(0.2)
|
||||
fj.play_mp3_file("saves/awspollyoutput.mp3")
|
||||
fj.neopixels.fill(0x000000)
|
||||
|
||||
|
||||
display.root_group = main_group
|
||||
while True:
|
||||
# check how many bytes are available
|
||||
available = supervisor.runtime.serial_bytes_available
|
||||
|
||||
# if there are some bytes available
|
||||
if available:
|
||||
# read data from the keyboard input
|
||||
c = sys.stdin.read(available)
|
||||
# print the data that was read
|
||||
print(c, end="")
|
||||
|
||||
if c in LETTERS:
|
||||
curword += c
|
||||
screen_lbl.text = curword
|
||||
elif c in {"\x7f", "\x08"}: # backspace
|
||||
curword = curword[:-1]
|
||||
screen_lbl.text = curword
|
||||
elif c == "\n":
|
||||
if curword:
|
||||
lastword = curword
|
||||
fetch_word(lastword)
|
||||
say_and_spell_lastword()
|
||||
curword = ""
|
||||
else:
|
||||
# repeat last word
|
||||
say_and_spell_lastword()
|
||||
elif c.encode("utf-8") == b"\x1b[B":
|
||||
# down arrow
|
||||
fj.peripherals.volume = max(1, fj.peripherals.volume - 1)
|
||||
print(f"Volume: {fj.peripherals.volume}")
|
||||
elif c.encode("utf-8") == b"\x1b[A":
|
||||
# up arrow
|
||||
fj.peripherals.volume = min(
|
||||
fj.peripherals.safe_volume_limit, fj.peripherals.volume + 1
|
||||
)
|
||||
print(f"Volume: {fj.peripherals.volume}")
|
||||
else:
|
||||
print(f"unused key: {c.encode('utf-8')}")
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2015 Paul Sokolovsky
|
||||
# SPDX-License-Identifier: MIT
|
||||
# https://github.com/micropython/micropython-lib
|
||||
# https://github.com/micropython/micropython-lib/blob/master/LICENSE
|
||||
|
||||
# Implements the hmac module from the Python standard library.
|
||||
|
||||
|
||||
class HMAC:
|
||||
# pylint: disable=protected-access, import-outside-toplevel,unnecessary-lambda-assignment
|
||||
def __init__(self, key, msg=None, digestmod=None):
|
||||
if not isinstance(key, (bytes, bytearray)):
|
||||
raise TypeError("key: expected bytes/bytearray")
|
||||
|
||||
import adafruit_hashlib as hashlib
|
||||
|
||||
if digestmod is None:
|
||||
# TODO: Default hash algorithm is now deprecated.
|
||||
digestmod = hashlib.md5
|
||||
|
||||
if callable(digestmod):
|
||||
# A hashlib constructor returning a new hash object.
|
||||
make_hash = digestmod # A
|
||||
elif isinstance(digestmod, str):
|
||||
# A hash name suitable for hashlib.new().
|
||||
make_hash = lambda d=b"": getattr(hashlib, digestmod)(d)
|
||||
else:
|
||||
# A module supporting PEP 247.
|
||||
make_hash = digestmod.new # C
|
||||
|
||||
self._outer = make_hash()
|
||||
self._inner = make_hash()
|
||||
|
||||
self.digest_size = getattr(self._inner, "digest_size", None)
|
||||
# If the provided hash doesn't support block_size (e.g. built-in
|
||||
# hashlib), 64 is the correct default for all built-in hash
|
||||
# functions (md5, sha1, sha256).
|
||||
self.block_size = getattr(self._inner, "block_size", 64)
|
||||
|
||||
# Truncate to digest_size if greater than block_size.
|
||||
if len(key) > self.block_size:
|
||||
key = make_hash(key).digest()
|
||||
|
||||
# Pad to block size.
|
||||
key = key + bytes(self.block_size - len(key))
|
||||
|
||||
self._outer.update(bytes(x ^ 0x5C for x in key))
|
||||
self._inner.update(bytes(x ^ 0x36 for x in key))
|
||||
|
||||
if msg is not None:
|
||||
self.update(msg)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return "hmac-" + getattr(self._inner, "name", type(self._inner).__name__)
|
||||
|
||||
def update(self, msg):
|
||||
self._inner.update(msg)
|
||||
|
||||
def copy(self):
|
||||
if not hasattr(self._inner, "copy"):
|
||||
# Not supported for built-in hash functions.
|
||||
raise NotImplementedError()
|
||||
# Call __new__ directly to avoid the expensive __init__.
|
||||
other = self.__class__.__new__(self.__class__)
|
||||
other.block_size = self.block_size
|
||||
other.digest_size = self.digest_size
|
||||
other._inner = self._inner.copy()
|
||||
other._outer = self._outer.copy()
|
||||
return other
|
||||
|
||||
def _current(self):
|
||||
h = self._outer
|
||||
if hasattr(h, "copy"):
|
||||
# built-in hash functions don't support this, and as a result,
|
||||
# digest() will finalise the hmac and further calls to
|
||||
# update/digest will fail.
|
||||
h = h.copy()
|
||||
h.update(self._inner.digest())
|
||||
return h
|
||||
|
||||
def digest(self):
|
||||
h = self._current()
|
||||
return h.digest()
|
||||
|
||||
def hexdigest(self):
|
||||
import binascii
|
||||
|
||||
return str(binascii.hexlify(self.digest()), "utf-8")
|
||||
|
||||
|
||||
def new(key, msg=None, digestmod=None):
|
||||
return HMAC(key, msg, digestmod)
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.9 KiB |
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"title": "Spell Jam",
|
||||
"icon": "icon.bmp"
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +0,0 @@
|
|||
# SPDX-FileCopyrightText: Copyright (c) 2020, keshikan (https://www.keshikan.net), with Reserved Font Name "DSEG".
|
||||
|
||||
# SPDX-License-Identifier: OFL-1.1-RFN
|
||||
|
||||
# This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
# This license is copied below, and is also available with a FAQ at:
|
||||
# http://scripts.sil.org/OFL
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 75 KiB |
|
|
@ -1,3 +0,0 @@
|
|||
# SPDX-FileCopyrightText: Copyright (c) 2025, Tim Cocks for Adafruit Industries
|
||||
|
||||
# SPDX-License-Identifier: MIT
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in a new issue