add Spell Jam project
This commit is contained in:
parent
009abaff09
commit
f5fec0e46b
35 changed files with 2966 additions and 0 deletions
295
Fruit_Jam/Fruit_Jam_Spell_Jam/aws_polly.py
Normal file
295
Fruit_Jam/Fruit_Jam_Spell_Jam/aws_polly.py
Normal file
|
|
@ -0,0 +1,295 @@
|
||||||
|
# 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:
|
||||||
|
# pylint: disable=no-self-use
|
||||||
|
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: # pylint: disable=broad-except
|
||||||
|
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: # pylint: disable=broad-except
|
||||||
|
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"
|
||||||
|
# )
|
||||||
131
Fruit_Jam/Fruit_Jam_Spell_Jam/code.py
Normal file
131
Fruit_Jam/Fruit_Jam_Spell_Jam/code.py
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
# 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')}")
|
||||||
93
Fruit_Jam/Fruit_Jam_Spell_Jam/hmac.py
Normal file
93
Fruit_Jam/Fruit_Jam_Spell_Jam/hmac.py
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
# 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)
|
||||||
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/icon.bmp
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/icon.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
4
Fruit_Jam/Fruit_Jam_Spell_Jam/metadata.json
Normal file
4
Fruit_Jam/Fruit_Jam_Spell_Jam/metadata.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"title": "Spell Jam",
|
||||||
|
"icon": "icon.bmp"
|
||||||
|
}
|
||||||
2432
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/14segment_16.bdf
Normal file
2432
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/14segment_16.bdf
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,8 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/background.bmp
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/background.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
# SPDX-FileCopyrightText: Copyright (c) 2025, Tim Cocks for Adafruit Industries
|
||||||
|
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/A.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/A.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/B.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/B.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/C.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/C.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/D.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/D.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/E.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/E.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/F.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/F.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/G.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/G.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/H.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/H.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/I.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/I.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/J.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/J.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/K.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/K.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/L.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/L.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/M.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/M.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/N.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/N.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/O.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/O.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/P.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/P.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/Q.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/Q.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/R.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/R.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/S.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/S.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/T.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/T.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/U.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/U.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/V.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/V.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/W.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/W.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/X.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/X.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/Y.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/Y.mp3
Normal file
Binary file not shown.
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/Z.mp3
Normal file
BIN
Fruit_Jam/Fruit_Jam_Spell_Jam/spell_jam_assets/letter_mp3s/Z.mp3
Normal file
Binary file not shown.
Loading…
Reference in a new issue