lesson 21 (#81)
* Adding content * Update en.json * Update README.md * Update TRANSLATIONS.md * Adding lesson tempolates * Fixing code files with each others code in * Update README.md * Adding lesson 16 * Adding virtual camera * Adding Wio Terminal camera capture * Adding wio terminal code * Adding SBC classification to lesson 16 * Adding challenge, review and assignment * Adding images and using new Azure icons * Update README.md * Update iot-reference-architecture.png * Adding structure for JulyOT links * Removing icons * Sketchnotes! * Create lesson-1.png * Starting on lesson 18 * Updated sketch * Adding virtual distance sensor * Adding Wio Terminal image classification * Update README.md * Adding structure for project 6 and wio terminal distance sensor * Adding some of the smart timer stuff * Updating sketchnotes * Adding virtual device speech to text * Adding chapter 21pull/83/head
@ -0,0 +1,5 @@
|
||||
.pio
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"platformio.platformio-ide"
|
||||
]
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
|
||||
This directory is intended for project header files.
|
||||
|
||||
A header file is a file containing C declarations and macro definitions
|
||||
to be shared between several project source files. You request the use of a
|
||||
header file in your project source file (C, C++, etc) located in `src` folder
|
||||
by including it, with the C preprocessing directive `#include'.
|
||||
|
||||
```src/main.c
|
||||
|
||||
#include "header.h"
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Including a header file produces the same results as copying the header file
|
||||
into each source file that needs it. Such copying would be time-consuming
|
||||
and error-prone. With a header file, the related declarations appear
|
||||
in only one place. If they need to be changed, they can be changed in one
|
||||
place, and programs that include the header file will automatically use the
|
||||
new version when next recompiled. The header file eliminates the labor of
|
||||
finding and changing all the copies as well as the risk that a failure to
|
||||
find one copy will result in inconsistencies within a program.
|
||||
|
||||
In C, the usual convention is to give header files names that end with `.h'.
|
||||
It is most portable to use only letters, digits, dashes, and underscores in
|
||||
header file names, and at most one dot.
|
||||
|
||||
Read more about using header files in official GCC documentation:
|
||||
|
||||
* Include Syntax
|
||||
* Include Operation
|
||||
* Once-Only Headers
|
||||
* Computed Includes
|
||||
|
||||
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
|
@ -0,0 +1,46 @@
|
||||
|
||||
This directory is intended for project specific (private) libraries.
|
||||
PlatformIO will compile them to static libraries and link into executable file.
|
||||
|
||||
The source code of each library should be placed in a an own separate directory
|
||||
("lib/your_library_name/[here are source files]").
|
||||
|
||||
For example, see a structure of the following two libraries `Foo` and `Bar`:
|
||||
|
||||
|--lib
|
||||
| |
|
||||
| |--Bar
|
||||
| | |--docs
|
||||
| | |--examples
|
||||
| | |--src
|
||||
| | |- Bar.c
|
||||
| | |- Bar.h
|
||||
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
||||
| |
|
||||
| |--Foo
|
||||
| | |- Foo.c
|
||||
| | |- Foo.h
|
||||
| |
|
||||
| |- README --> THIS FILE
|
||||
|
|
||||
|- platformio.ini
|
||||
|--src
|
||||
|- main.c
|
||||
|
||||
and a contents of `src/main.c`:
|
||||
```
|
||||
#include <Foo.h>
|
||||
#include <Bar.h>
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
PlatformIO Library Dependency Finder will find automatically dependent
|
||||
libraries scanning project source files.
|
||||
|
||||
More information about PlatformIO Library Dependency Finder
|
||||
- https://docs.platformio.org/page/librarymanager/ldf.html
|
@ -0,0 +1,16 @@
|
||||
; PlatformIO Project Configuration File
|
||||
;
|
||||
; Build options: build flags, source filter
|
||||
; Upload options: custom upload port, speed and extra flags
|
||||
; Library options: dependencies, extra library storages
|
||||
; Advanced options: extra scripting
|
||||
;
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[env:seeed_wio_terminal]
|
||||
platform = atmelsam
|
||||
board = seeed_wio_terminal
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
seeed-studio/Grove Ranging sensor - VL53L0X @ ^1.1.1
|
@ -0,0 +1,31 @@
|
||||
#include <Arduino.h>
|
||||
#include "Seeed_vl53l0x.h"
|
||||
|
||||
Seeed_vl53l0x VL53L0X;
|
||||
|
||||
void setup()
|
||||
{
|
||||
Serial.begin(9600);
|
||||
|
||||
while (!Serial)
|
||||
; // Wait for Serial to be ready
|
||||
|
||||
delay(1000);
|
||||
|
||||
VL53L0X.VL53L0X_common_init();
|
||||
VL53L0X.VL53L0X_high_accuracy_ranging_init();
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
VL53L0X_RangingMeasurementData_t RangingMeasurementData;
|
||||
memset(&RangingMeasurementData, 0, sizeof(VL53L0X_RangingMeasurementData_t));
|
||||
|
||||
VL53L0X.PerformSingleRangingMeasurement(&RangingMeasurementData);
|
||||
|
||||
Serial.print("Distance = ");
|
||||
Serial.print(RangingMeasurementData.RangeMilliMeter);
|
||||
Serial.println(" mm");
|
||||
|
||||
delay(1000);
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
|
||||
This directory is intended for PlatformIO Unit Testing and project tests.
|
||||
|
||||
Unit Testing is a software testing method by which individual units of
|
||||
source code, sets of one or more MCU program modules together with associated
|
||||
control data, usage procedures, and operating procedures, are tested to
|
||||
determine whether they are fit for use. Unit testing finds problems early
|
||||
in the development cycle.
|
||||
|
||||
More information about PlatformIO Unit Testing:
|
||||
- https://docs.platformio.org/page/plus/unit-testing.html
|
@ -1,5 +1,20 @@
|
||||
# Consumer IoT - build a smart voice assistant
|
||||
|
||||
The fod has been grown, driven to a processing plant, sorted for quality, sold in the store and now it's time to cook! One of the core pieces of any kitchen is a timer. Initially these started as simple hour glasses - your food was cooked when all the sand trickled down into the bottom bulb. They then went clockwork, then electric.
|
||||
|
||||
The latest iterations are now part of our smart devices. In kitchens all throughout the world you'll head chefs shouting "Hey Siri - set a 10 minute timer", or "Alexa - cancel my bread timer". No longer do you have to walk back to the kitchen to check on a timer, you can do it from your phone, or a call out across the room.
|
||||
|
||||
In these 4 lessons you'll learn how to build a smart timer, using AI to recognize your voice, understand what you are asking for, and reply with information about your timer. You'll also add support for multiple languages.
|
||||
|
||||
> 💁 These lessons will use some cloud resources. If you don't complete all the lessons in this project, make sure you [Clean up your project](../clean-up.md).
|
||||
|
||||
## Topics
|
||||
|
||||
1. [Recognize speech with an IoT device](./lessons/1-speech-recognition/README.md)
|
||||
1. [Understand language](./lessons/2-language-understanding/README.md)
|
||||
1. [Provide spoken feedback](./lessons/3-spoken-feedback/README.md)
|
||||
1. [Support multiple languages](./lessons/4-multiple-language-support/README.md)
|
||||
|
||||
## Credits
|
||||
|
||||
All the lessons were written with ♥️ by [Jim Bennett](https://GitHub.com/JimBobBennett)
|
||||
|
@ -0,0 +1,9 @@
|
||||
#
|
||||
|
||||
## Instructions
|
||||
|
||||
## Rubric
|
||||
|
||||
| Criteria | Exemplary | Adequate | Needs Improvement |
|
||||
| -------- | --------- | -------- | ----------------- |
|
||||
| | | | |
|
@ -0,0 +1,94 @@
|
||||
import io
|
||||
import json
|
||||
import pyaudio
|
||||
import requests
|
||||
import time
|
||||
import wave
|
||||
|
||||
from azure.iot.device import IoTHubDeviceClient, Message
|
||||
|
||||
from grove.factory import Factory
|
||||
button = Factory.getButton('GPIO-HIGH', 5)
|
||||
|
||||
connection_string = '<connection_string>'
|
||||
|
||||
device_client = IoTHubDeviceClient.create_from_connection_string(connection_string)
|
||||
|
||||
print('Connecting')
|
||||
device_client.connect()
|
||||
print('Connected')
|
||||
|
||||
audio = pyaudio.PyAudio()
|
||||
microphone_card_number = 1
|
||||
speaker_card_number = 1
|
||||
rate = 48000
|
||||
|
||||
def capture_audio():
|
||||
stream = audio.open(format = pyaudio.paInt16,
|
||||
rate = rate,
|
||||
channels = 1,
|
||||
input_device_index = microphone_card_number,
|
||||
input = True,
|
||||
frames_per_buffer = 4096)
|
||||
|
||||
frames = []
|
||||
|
||||
while button.is_pressed():
|
||||
frames.append(stream.read(4096))
|
||||
|
||||
stream.stop_stream()
|
||||
stream.close()
|
||||
|
||||
wav_buffer = io.BytesIO()
|
||||
with wave.open(wav_buffer, 'wb') as wavefile:
|
||||
wavefile.setnchannels(1)
|
||||
wavefile.setsampwidth(audio.get_sample_size(pyaudio.paInt16))
|
||||
wavefile.setframerate(rate)
|
||||
wavefile.writeframes(b''.join(frames))
|
||||
wav_buffer.seek(0)
|
||||
|
||||
return wav_buffer
|
||||
|
||||
api_key = '<key>'
|
||||
location = '<location>'
|
||||
language = '<language>'
|
||||
|
||||
def get_access_token():
|
||||
headers = {
|
||||
'Ocp-Apim-Subscription-Key': api_key
|
||||
}
|
||||
|
||||
token_endpoint = f'https://{location}.api.cognitive.microsoft.com/sts/v1.0/issuetoken'
|
||||
response = requests.post(token_endpoint, headers=headers)
|
||||
return str(response.text)
|
||||
|
||||
def convert_speech_to_text(buffer):
|
||||
url = f'https://{location}.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1'
|
||||
|
||||
headers = {
|
||||
'Authorization': 'Bearer ' + get_access_token(),
|
||||
'Content-Type': f'audio/wav; codecs=audio/pcm; samplerate={rate}',
|
||||
'Accept': 'application/json;text/xml'
|
||||
}
|
||||
|
||||
params = {
|
||||
'language': language
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, params=params, data=buffer)
|
||||
response_json = json.loads(response.text)
|
||||
|
||||
if response_json['RecognitionStatus'] == 'Success':
|
||||
return response_json['DisplayText']
|
||||
else:
|
||||
return ''
|
||||
|
||||
while True:
|
||||
while not button.is_pressed():
|
||||
time.sleep(.1)
|
||||
|
||||
buffer = capture_audio()
|
||||
text = convert_speech_to_text(buffer)
|
||||
if len(text) > 0:
|
||||
message = Message(json.dumps({ 'speech': text }))
|
||||
device_client.send_message(message)
|
@ -0,0 +1,33 @@
|
||||
import json
|
||||
import time
|
||||
from azure.cognitiveservices.speech import SpeechConfig, SpeechRecognizer
|
||||
from azure.iot.device import IoTHubDeviceClient, Message
|
||||
|
||||
api_key = '<key>'
|
||||
location = '<location>'
|
||||
language = '<language>'
|
||||
connection_string = '<connection_string>'
|
||||
|
||||
device_client = IoTHubDeviceClient.create_from_connection_string(connection_string)
|
||||
|
||||
print('Connecting')
|
||||
device_client.connect()
|
||||
print('Connected')
|
||||
|
||||
speech_config = SpeechConfig(subscription=api_key,
|
||||
region=location,
|
||||
speech_recognition_language=language)
|
||||
|
||||
recognizer = SpeechRecognizer(speech_config=speech_config)
|
||||
|
||||
def recognized(args):
|
||||
if len(args.result.text) > 0:
|
||||
message = Message(json.dumps({ 'speech': args.result.text }))
|
||||
device_client.send_message(message)
|
||||
|
||||
recognizer.recognized.connect(recognized)
|
||||
|
||||
recognizer.start_continuous_recognition()
|
||||
|
||||
while True:
|
||||
time.sleep(1)
|
@ -0,0 +1,61 @@
|
||||
import io
|
||||
import pyaudio
|
||||
import time
|
||||
import wave
|
||||
|
||||
from grove.factory import Factory
|
||||
button = Factory.getButton('GPIO-HIGH', 5)
|
||||
|
||||
audio = pyaudio.PyAudio()
|
||||
microphone_card_number = 1
|
||||
speaker_card_number = 1
|
||||
rate = 48000
|
||||
|
||||
def capture_audio():
|
||||
stream = audio.open(format = pyaudio.paInt16,
|
||||
rate = rate,
|
||||
channels = 1,
|
||||
input_device_index = microphone_card_number,
|
||||
input = True,
|
||||
frames_per_buffer = 4096)
|
||||
|
||||
frames = []
|
||||
|
||||
while button.is_pressed():
|
||||
frames.append(stream.read(4096))
|
||||
|
||||
stream.stop_stream()
|
||||
stream.close()
|
||||
|
||||
wav_buffer = io.BytesIO()
|
||||
with wave.open(wav_buffer, 'wb') as wavefile:
|
||||
wavefile.setnchannels(1)
|
||||
wavefile.setsampwidth(audio.get_sample_size(pyaudio.paInt16))
|
||||
wavefile.setframerate(rate)
|
||||
wavefile.writeframes(b''.join(frames))
|
||||
wav_buffer.seek(0)
|
||||
|
||||
return wav_buffer
|
||||
|
||||
def play_audio(buffer):
|
||||
stream = audio.open(format = pyaudio.paInt16,
|
||||
rate = rate,
|
||||
channels = 1,
|
||||
output_device_index = speaker_card_number,
|
||||
output = True)
|
||||
|
||||
with wave.open(buffer, 'rb') as wf:
|
||||
data = wf.readframes(4096)
|
||||
|
||||
while len(data) > 0:
|
||||
stream.write(data)
|
||||
data = wf.readframes(4096)
|
||||
|
||||
stream.close()
|
||||
|
||||
while True:
|
||||
while not button.is_pressed():
|
||||
time.sleep(.1)
|
||||
|
||||
buffer = capture_audio()
|
||||
play_audio(buffer)
|
@ -0,0 +1,82 @@
|
||||
import io
|
||||
import json
|
||||
import pyaudio
|
||||
import requests
|
||||
import time
|
||||
import wave
|
||||
|
||||
from grove.factory import Factory
|
||||
button = Factory.getButton('GPIO-HIGH', 5)
|
||||
|
||||
audio = pyaudio.PyAudio()
|
||||
microphone_card_number = 1
|
||||
speaker_card_number = 1
|
||||
rate = 48000
|
||||
|
||||
def capture_audio():
|
||||
stream = audio.open(format = pyaudio.paInt16,
|
||||
rate = rate,
|
||||
channels = 1,
|
||||
input_device_index = microphone_card_number,
|
||||
input = True,
|
||||
frames_per_buffer = 4096)
|
||||
|
||||
frames = []
|
||||
|
||||
while button.is_pressed():
|
||||
frames.append(stream.read(4096))
|
||||
|
||||
stream.stop_stream()
|
||||
stream.close()
|
||||
|
||||
wav_buffer = io.BytesIO()
|
||||
with wave.open(wav_buffer, 'wb') as wavefile:
|
||||
wavefile.setnchannels(1)
|
||||
wavefile.setsampwidth(audio.get_sample_size(pyaudio.paInt16))
|
||||
wavefile.setframerate(rate)
|
||||
wavefile.writeframes(b''.join(frames))
|
||||
wav_buffer.seek(0)
|
||||
|
||||
return wav_buffer
|
||||
|
||||
api_key = '<key>'
|
||||
location = '<location>'
|
||||
language = '<language>'
|
||||
|
||||
def get_access_token():
|
||||
headers = {
|
||||
'Ocp-Apim-Subscription-Key': api_key
|
||||
}
|
||||
|
||||
token_endpoint = f'https://{location}.api.cognitive.microsoft.com/sts/v1.0/issuetoken'
|
||||
response = requests.post(token_endpoint, headers=headers)
|
||||
return str(response.text)
|
||||
|
||||
def convert_speech_to_text(buffer):
|
||||
url = f'https://{location}.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1'
|
||||
|
||||
headers = {
|
||||
'Authorization': 'Bearer ' + get_access_token(),
|
||||
'Content-Type': f'audio/wav; codecs=audio/pcm; samplerate={rate}',
|
||||
'Accept': 'application/json;text/xml'
|
||||
}
|
||||
|
||||
params = {
|
||||
'language': language
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, params=params, data=buffer)
|
||||
response_json = json.loads(response.text)
|
||||
|
||||
if response_json['RecognitionStatus'] == 'Success':
|
||||
return response_json['DisplayText']
|
||||
else:
|
||||
return ''
|
||||
|
||||
while True:
|
||||
while not button.is_pressed():
|
||||
time.sleep(.1)
|
||||
|
||||
buffer = capture_audio()
|
||||
text = convert_speech_to_text(buffer)
|
||||
print(text)
|
@ -0,0 +1,22 @@
|
||||
import time
|
||||
from azure.cognitiveservices.speech import SpeechConfig, SpeechRecognizer
|
||||
|
||||
api_key = '<key>'
|
||||
location = '<location>'
|
||||
language = '<language>'
|
||||
|
||||
speech_config = SpeechConfig(subscription=api_key,
|
||||
region=location,
|
||||
speech_recognition_language=language)
|
||||
|
||||
recognizer = SpeechRecognizer(speech_config=speech_config)
|
||||
|
||||
def recognized(args):
|
||||
print(args.result.text)
|
||||
|
||||
recognizer.recognized.connect(recognized)
|
||||
|
||||
recognizer.start_continuous_recognition()
|
||||
|
||||
while True:
|
||||
time.sleep(1)
|
@ -0,0 +1,213 @@
|
||||
# Capture audio - Raspberry Pi
|
||||
|
||||
In this part of the lesson, you will write code to capture audio on your Raspberry Pi. Audio capture will be controlled by a button.
|
||||
|
||||
## Hardware
|
||||
|
||||
The Raspberry Pi needs a button to control the audio capture.
|
||||
|
||||
The button you will use is a Grove button. This is a digital sensor that turns a signal on or off. These buttons can be configured to send a high signal when the button is pressed, and low when it is not, or low when pressed and high when not.
|
||||
|
||||
If you are using a ReSpeaker 2-Mics Pi HAT as a microphone, then there is no need to connect a button as this hat has one fitted already. Skip to the next section.
|
||||
|
||||
### Connect the button
|
||||
|
||||
The button can be connected to the Grove base hat.
|
||||
|
||||
#### Task - connect the button
|
||||
|
||||

|
||||
|
||||
1. Insert one end of a Grove cable into the socket on the button module. It will only go in one way round.
|
||||
|
||||
1. With the Raspberry Pi powered off, connect the other end of the Grove cable to the digital socket marked **D5** on the Grove Base hat attached to the Pi. This socket is the second from the left, on the row of sockets next to the GPIO pins.
|
||||
|
||||

|
||||
|
||||
## Capture audio
|
||||
|
||||
You can capture audio from the microphone using Python code.
|
||||
|
||||
### Task - capture audio
|
||||
|
||||
1. Power up the Pi and wait for it to boot
|
||||
|
||||
1. Launch VS Code, either directly on the Pi, or connect via the Remote SSH extension.
|
||||
|
||||
1. The PyAudio Pip package has functions to record and play back audio. This package depends on some audio libraries that need to be installed first. Run the following commands in the terminal to install these:
|
||||
|
||||
```sh
|
||||
sudo apt update
|
||||
sudo apt install libportaudio0 libportaudio2 libportaudiocpp0 portaudio19-dev libasound2-plugins --yes
|
||||
```
|
||||
|
||||
1. Install the PyAudio Pip package.
|
||||
|
||||
```sh
|
||||
pip3 install pyaudio
|
||||
```
|
||||
|
||||
1. Create a new folder called `smart-timer` and add a file called `app.py` to this folder.
|
||||
|
||||
1. Add the following imports to the top of this file:
|
||||
|
||||
```python
|
||||
import io
|
||||
import pyaudio
|
||||
import time
|
||||
import wave
|
||||
|
||||
from grove.factory import Factory
|
||||
```
|
||||
|
||||
This imports the `pyaudio` module, some standard Python modules to handle wave files, and the `grove.factory` module to import a `Factory` to create a button class.
|
||||
|
||||
1. Below this, add code to create a Grove button.
|
||||
|
||||
If you are using the ReSpeaker 2-Mics Pi HAT, use the following code:
|
||||
|
||||
```python
|
||||
# The button on the ReSpeaker 2-Mics Pi HAT
|
||||
button = Factory.getButton("GPIO-LOW", 17)
|
||||
```
|
||||
|
||||
This creates a button on port **D17**, the port that the button on the ReSpeaker 2-Mics Pi HAT is connected to. This button is set to send a low signal when pressed.
|
||||
|
||||
If you are not using the ReSpeaker 2-Mics Pi HAT, and are using a Grove button connected to the base hat, use this code.
|
||||
|
||||
```python
|
||||
button = Factory.getButton("GPIO-HIGH", 5)
|
||||
```
|
||||
|
||||
This creates a button on port **D5** that is set to send a high signal when pressed.
|
||||
|
||||
1. Below this, create an instance of the PyAudio class to handle audio:
|
||||
|
||||
```python
|
||||
audio = pyaudio.PyAudio()
|
||||
```
|
||||
|
||||
1. Declare the hardware card number for the microphone and speaker. This will be the number of the card you found by running `arecord -l` and `aplay -l` earlier in this lesson.
|
||||
|
||||
```python
|
||||
microphone_card_number = <microphone card number>
|
||||
speaker_card_number = <speaker card number>
|
||||
```
|
||||
|
||||
Replace `<microphone card number>` with the number of your microphones card.
|
||||
|
||||
Replace `<speaker card number>` with the number of your speakers card, the same number you set in the `alsa.conf` file.
|
||||
|
||||
1. Below this, declare the sample rate to use for the audio capture and playback. You may need to change this depending on the hardware you are using.
|
||||
|
||||
```python
|
||||
rate = 48000 #48KHz
|
||||
```
|
||||
|
||||
If you get sample rate errors when running this code later, change this value to `44100` or `16000`. The higher the value, the better the quality of the sound.
|
||||
|
||||
1. Below this, create a new function called `capture_audio`. This will be called to capture audio from the microphone:
|
||||
|
||||
```python
|
||||
def capture_audio():
|
||||
```
|
||||
|
||||
1. Inside this function, add the following to capture the audio:
|
||||
|
||||
```python
|
||||
stream = audio.open(format = pyaudio.paInt16,
|
||||
rate = rate,
|
||||
channels = 1,
|
||||
input_device_index = microphone_card_number,
|
||||
input = True,
|
||||
frames_per_buffer = 4096)
|
||||
|
||||
frames = []
|
||||
|
||||
while button.is_pressed():
|
||||
frames.append(stream.read(4096))
|
||||
|
||||
stream.stop_stream()
|
||||
stream.close()
|
||||
```
|
||||
|
||||
This code opens an audio input stream using the PyAudio object. This stream will capture audio from the microphone at 16KHz, capturing it in buffers of 4096 bytes in size.
|
||||
|
||||
The code then loops whilst the Grove button is pressed, reading these 4096 byte buffers into an array each time.
|
||||
|
||||
> 💁 You can read more on the options passed to the `open` method in the [PyAudio documentation](https://people.csail.mit.edu/hubert/pyaudio/docs/).
|
||||
|
||||
Once the button is released, the stream is stopped and closed.
|
||||
|
||||
1. Add the following to the end of this function:
|
||||
|
||||
```python
|
||||
wav_buffer = io.BytesIO()
|
||||
with wave.open(wav_buffer, 'wb') as wavefile:
|
||||
wavefile.setnchannels(1)
|
||||
wavefile.setsampwidth(audio.get_sample_size(pyaudio.paInt16))
|
||||
wavefile.setframerate(rate)
|
||||
wavefile.writeframes(b''.join(frames))
|
||||
wav_buffer.seek(0)
|
||||
|
||||
return wav_buffer
|
||||
```
|
||||
|
||||
This code creates a binary buffer, and writes all the captured audio to it as a [WAV file](https://wikipedia.org/wiki/WAV). This is a standard way to write uncompressed audio to a file. This buffer is then returned.
|
||||
|
||||
1. Add the following `play_audio` function to play back the audio buffer:
|
||||
|
||||
```python
|
||||
def play_audio(buffer):
|
||||
stream = audio.open(format = pyaudio.paInt16,
|
||||
rate = rate,
|
||||
channels = 1,
|
||||
output_device_index = speaker_card_number,
|
||||
output = True)
|
||||
|
||||
with wave.open(buffer, 'rb') as wf:
|
||||
data = wf.readframes(4096)
|
||||
|
||||
while len(data) > 0:
|
||||
stream.write(data)
|
||||
data = wf.readframes(4096)
|
||||
|
||||
stream.close()
|
||||
```
|
||||
|
||||
This function opens another audio stream, this time for output - to play the audio. It uses the same settings as the input stream. The buffer is then opened as a wave file and written to the output stream in 4096 byte chunks, playing the audio. The stream is then closed.
|
||||
|
||||
1. Add the following code below the `capture_audio` function to loop until the button is pressed. Once the button is pressed, the audio is captured, then played.
|
||||
|
||||
```python
|
||||
while True:
|
||||
while not button.is_pressed():
|
||||
time.sleep(.1)
|
||||
|
||||
buffer = capture_audio()
|
||||
play_audio(buffer)
|
||||
```
|
||||
|
||||
1. Run the code. Press the button and speak into the microphone. Release the button when you are done, and you will hear the recording.
|
||||
|
||||
You may see some ALSA errors when the PyAudio instance is created. This is due to configuration on the Pi for audio devices you don't have. You can ignore these errors.
|
||||
|
||||
```output
|
||||
pi@raspberrypi:~/smart-timer $ python3 app.py
|
||||
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.front
|
||||
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.rear
|
||||
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.center_lfe
|
||||
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.side
|
||||
```
|
||||
|
||||
If you see the following error:
|
||||
|
||||
```output
|
||||
OSError: [Errno -9997] Invalid sample rate
|
||||
```
|
||||
|
||||
then change the `rate` to either 44100 or 16000.
|
||||
|
||||
> 💁 You can find this code in the [code-record/pi](code-record/pi) folder.
|
||||
|
||||
😀 Your audio recording program was a success!
|
@ -0,0 +1,143 @@
|
||||
# Configure your microphone and speakers - Raspberry Pi
|
||||
|
||||
In this part of the lesson, you will add a microphone and speakers to your Raspberry Pi.
|
||||
|
||||
## Hardware
|
||||
|
||||
The Raspberry Pi needs a microphone.
|
||||
|
||||
The Pi doesn't have a microphone built in, you will need to add an external microphone. There are multiple ways to do this:
|
||||
|
||||
* USB microphone
|
||||
* USB headset
|
||||
* USB all in one speakerphone
|
||||
* USB audio adapter and microphone with a 3.5mm jack
|
||||
* [ReSpeaker 2-Mics Pi HAT](https://www.seeedstudio.com/ReSpeaker-2-Mics-Pi-HAT.html)
|
||||
|
||||
> 💁 Bluetooth microphones are not all supported on the Raspberry Pi, so if you have a bluetooth microphone or headset, you may have issues pairing or capturing audio.
|
||||
|
||||
Raspberry Pis come with a 3.5mm headphone jack. You can use this to connect headphones, a headset or a speaker. You can also add speakers using:
|
||||
|
||||
* HDMI audio through a monitor or TV
|
||||
* USB speakers
|
||||
* USB headset
|
||||
* USB all in one speakerphone
|
||||
* [ReSpeaker 2-Mics Pi HAT](https://www.seeedstudio.com/ReSpeaker-2-Mics-Pi-HAT.html) with a speaker attached, either to the 3.5mm jack or to the JST port
|
||||
|
||||
## Connect and configure the microphone and speakers
|
||||
|
||||
The microphone and speakers need to be connected, and configured.
|
||||
|
||||
### Task - connect and configure the microphone
|
||||
|
||||
1. Connect the microphone using the appropriate method. For example, connect it via one of the USB ports.
|
||||
|
||||
1. If you are using the ReSpeaker 2-Mics Pi HAT, you can remove the Grove base hat, then fit the ReSpeaker hat in it's place.
|
||||
|
||||

|
||||
|
||||
You will need a Grove button later in this lesson, but one is built into this hat, so the Grove base hat is not needed.
|
||||
|
||||
Once the hat is fitted, you will need to install some drivers. Refer to the [Seeed getting started instructions](https://wiki.seeedstudio.com/ReSpeaker_2_Mics_Pi_HAT_Raspberry/#getting-started) for driver installation instructions.
|
||||
|
||||
> ⚠️ The instructions use `git` to clone a repository. If you don't have `git` installed on your Pi, you can install it by running the following command:
|
||||
>
|
||||
> ```sh
|
||||
> sudo apt install git --yes
|
||||
> ```
|
||||
|
||||
1. Run the following command in your Terminal either on the Pi, or connected using VS Code and a remote SSH session to see information about the connected microphone:
|
||||
|
||||
```sh
|
||||
arecord -l
|
||||
```
|
||||
|
||||
You will see a list of connected microphones. It will be something like the following:
|
||||
|
||||
```output
|
||||
pi@raspberrypi:~ $ arecord -l
|
||||
**** List of CAPTURE Hardware Devices ****
|
||||
card 1: M0 [eMeet M0], device 0: USB Audio [USB Audio]
|
||||
Subdevices: 1/1
|
||||
Subdevice #0: subdevice #0
|
||||
```
|
||||
|
||||
Assuming you only have one microphone, you should only see one entry. Configuration of mics can be tricky on Linux, so it is easiest to only use one microphone and unplug any others.
|
||||
|
||||
Note down the card number, as you will need this later. In the output above the card number is 1.
|
||||
|
||||
### Task - connect and configure the speaker
|
||||
|
||||
1. Connect the speakers using the appropriate method.
|
||||
|
||||
1. Run the following command in your Terminal either on the Pi, or connected using VS Code and a remote SSH session to see information about the connected speakers:
|
||||
|
||||
```sh
|
||||
aplay -l
|
||||
```
|
||||
|
||||
You will see a list of connected speakers. It will be something like the following:
|
||||
|
||||
```output
|
||||
pi@raspberrypi:~ $ aplay -l
|
||||
**** List of PLAYBACK Hardware Devices ****
|
||||
card 0: Headphones [bcm2835 Headphones], device 0: bcm2835 Headphones [bcm2835 Headphones]
|
||||
Subdevices: 8/8
|
||||
Subdevice #0: subdevice #0
|
||||
Subdevice #1: subdevice #1
|
||||
Subdevice #2: subdevice #2
|
||||
Subdevice #3: subdevice #3
|
||||
Subdevice #4: subdevice #4
|
||||
Subdevice #5: subdevice #5
|
||||
Subdevice #6: subdevice #6
|
||||
Subdevice #7: subdevice #7
|
||||
card 1: M0 [eMeet M0], device 0: USB Audio [USB Audio]
|
||||
Subdevices: 1/1
|
||||
Subdevice #0: subdevice #0
|
||||
```
|
||||
|
||||
You will always see `card 0: Headphones` as this is the built-in headphone jack. If you have added additional speakers, such as a USB speaker, you will see this listed as well.
|
||||
|
||||
1. If you are using an additional speaker, and not a speaker or headphones connected to the built-in headphone jack, you need to configure it as the default. To do this run the following command:
|
||||
|
||||
```sh
|
||||
sudo nano /usr/share/alsa/alsa.conf
|
||||
```
|
||||
|
||||
This will open a configuration file in `nano`, a terminal-based text editor. Scroll down using the arrow keys on your keyboard until you find the following line:
|
||||
|
||||
```output
|
||||
defaults.pcm.card 0
|
||||
```
|
||||
|
||||
Change the value from `0` to the card number of the card you want to use from the list that came back from the call to `aplay -l`. For example, in the output above there is a second sound card called `card 1: M0 [eMeet M0], device 0: USB Audio [USB Audio]`, using card 1. To use this, I would update the line to be:
|
||||
|
||||
```output
|
||||
defaults.pcm.card 1
|
||||
```
|
||||
|
||||
Set this value to the appropriate card number. You can navigate to the number using the arrow keys on your keyboard, then delete and type the new number as normal when editing text files.
|
||||
|
||||
1. Save the changes and close the file by pressing `Ctrl+x`. Press `y` to save the file, then `return` to select the file name.
|
||||
|
||||
### Task - test the microphone and speaker
|
||||
|
||||
1. Run the following command to record 5 seconds of audio through the microphone:
|
||||
|
||||
```sh
|
||||
arecord --format=S16_LE --duration=5 --rate=16000 --file-type=wav out.wav
|
||||
```
|
||||
|
||||
Whilst this command is running, make noise into the microphone such as by speaking, singing, beat boxing, playing an instrument or whatever takes your fancy.
|
||||
|
||||
1. After 5 seconds, the recording will stop. Run the following command to play back the audio:
|
||||
|
||||
```sh
|
||||
aplay --format=S16_LE --rate=16000 out.wav
|
||||
```
|
||||
|
||||
You will hear the audio bing played back through the speakers. Adjust the output volume on your speaker as necessary.
|
||||
|
||||
1. If you need to adjust the volume of the built-in microphone port, or adjust the gain of the microphone, you can use the `alsamixer` utility. You can read more on this utility on thw [Linux alsamixer man page](https://linux.die.net/man/1/alsamixer)
|
||||
|
||||
1. If you get errors playing back the audio, check the card you set as the `defaults.pcm.card` in the `alsa.conf` file.
|
@ -0,0 +1,106 @@
|
||||
# Speech to text - Raspberry Pi
|
||||
|
||||
In this part of the lesson, you will write code to convert speech in the captured audio to text using the speech service.
|
||||
|
||||
## Send the audio to the speech service
|
||||
|
||||
The audio can be sent to the speech service using the REST API. To use the speech service, first you need to request an access token, then use that token to access the REST API. These access tokens expire after 10 minutes, so your code should request them on a regular basis to ensure they are always up to date.
|
||||
|
||||
### Task - get an access token
|
||||
|
||||
1. Open the `smart-timer` project on your Pi.
|
||||
|
||||
1. Remove the `play_audio` function. This is no longer needed as you don't want a smart timer to repeat back to you what you said.
|
||||
|
||||
1. Add the following imports to the top of the `app.py` file:
|
||||
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
```
|
||||
|
||||
1. Add the following code above the `while True` loop to declare some settings for the speech service:
|
||||
|
||||
```python
|
||||
api_key = '<key>'
|
||||
location = '<location>'
|
||||
language = '<language>'
|
||||
```
|
||||
|
||||
Replace `<key>` with the API key for your speech service. Replace `<location>` with the location you used when you created the speech service resource.
|
||||
|
||||
Replace `<language>` with the locale name for language you will be speaking in, for example `en-GB` for English, or `zn-HK` for Cantonese. You can find a list of the supported languages and their locale names in the [Language and voice support documentation on Microsoft docs](https://docs.microsoft.com/azure/cognitive-services/speech-service/language-support?WT.mc_id=academic-17441-jabenn#speech-to-text).
|
||||
|
||||
1. Below this, add the following function to get an access token:
|
||||
|
||||
```python
|
||||
def get_access_token():
|
||||
headers = {
|
||||
'Ocp-Apim-Subscription-Key': api_key
|
||||
}
|
||||
|
||||
token_endpoint = f'https://{location}.api.cognitive.microsoft.com/sts/v1.0/issuetoken'
|
||||
response = requests.post(token_endpoint, headers=headers)
|
||||
return str(response.text)
|
||||
```
|
||||
|
||||
This calls a token issuing endpoint, passing the API key as a header. This call returns an access token that can be used to call the speech services.
|
||||
|
||||
1. Below this, declare a function to convert speech in the captured audio to text using the REST API:
|
||||
|
||||
```python
|
||||
def convert_speech_to_text(buffer):
|
||||
```
|
||||
|
||||
1. Inside this function, set up the REST API URL and headers:
|
||||
|
||||
```python
|
||||
url = f'https://{location}.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1'
|
||||
|
||||
headers = {
|
||||
'Authorization': 'Bearer ' + get_access_token(),
|
||||
'Content-Type': f'audio/wav; codecs=audio/pcm; samplerate={rate}',
|
||||
'Accept': 'application/json;text/xml'
|
||||
}
|
||||
|
||||
params = {
|
||||
'language': language
|
||||
}
|
||||
```
|
||||
|
||||
This builds a URL using the location of the speech services resource. It then populates the headers with the access token from the `get_access_token` function, as well as the sample rate used to capture the audio. Finally it defines some parameters to be passed with the URL containing the language in the audio.
|
||||
|
||||
1. Below this, add the following code to call the REST API and get back the text:
|
||||
|
||||
```python
|
||||
response = requests.post(url, headers=headers, params=params, data=buffer)
|
||||
response_json = json.loads(response.text)
|
||||
|
||||
if response_json['RecognitionStatus'] == 'Success':
|
||||
return response_json['DisplayText']
|
||||
else:
|
||||
return ''
|
||||
```
|
||||
|
||||
This calls the URL and decodes the JSON value that comes in the response. The `RecognitionStatus` value in the response indicates if the call was able to extract speech into text successfully, and if this is `Success` then the text is returned from the function, otherwise an empty string is returned.
|
||||
|
||||
1. Finally replace the call to `play_audio` in the `while True` loop with a call to the `convert_speech_to_text` function, as well as printing the text to the console:
|
||||
|
||||
```python
|
||||
text = convert_speech_to_text(buffer)
|
||||
print(text)
|
||||
```
|
||||
|
||||
1. Run the code. Press the button and speak into the microphone. Release the button when you are done, and you will see the audio converted to text in the output.
|
||||
|
||||
```output
|
||||
pi@raspberrypi:~/smart-timer $ python3 app.py
|
||||
Hello world.
|
||||
Welcome to IoT for beginners.
|
||||
```
|
||||
|
||||
Try different types of sentences, along with sentences where words sound the same but have different meanings. For example, if you are speaking in English, say 'I want to buy two bananas and an apple too', and notice how it will use the correct to, two and too based on the context of the word, not just it's sound.
|
||||
|
||||
> 💁 You can find this code in the [code-speech-to-text/pi](code-speech-to-text/pi) folder.
|
||||
|
||||
😀 Your speech to text program was a success!
|
@ -0,0 +1,3 @@
|
||||
# Capture audio - Virtual IoT device
|
||||
|
||||
The Python libraries that you will be using later in this lesson to convert speech to text have built-in audio capture on Windows, macOS and Linux. You don't need to do anything here.
|
@ -0,0 +1,12 @@
|
||||
# Configure your microphone and speakers - Virtual IoT Hardware
|
||||
|
||||
The virtual IoT hardware will use a microphone and speakers attached to your computer.
|
||||
|
||||
If your computer doesn't have a microphone and speakers built in, you will need to attach these using hardware of your choice, such as:
|
||||
|
||||
* USB microphone
|
||||
* USB speakers
|
||||
* Speakers built into your monitor and connected over HDMI
|
||||
* Bluetooth headset
|
||||
|
||||
Refer to your hardware manufacturers instructions to install and configure this hardware.
|
@ -0,0 +1,95 @@
|
||||
# Speech to text - Virtual IoT device
|
||||
|
||||
In this part of the lesson, you will write code to convert speech captured from your microphone to text using the speech service.
|
||||
|
||||
## Convert speech to text
|
||||
|
||||
On Windows, Linux, and macOS, the speech services Python SDK can be used to listen to your microphone and convert any speech that is detected to text. It will listen continuously, detecting the audio levels and sending the speech for conversion to text when the audio level drops, such as at the end of a block of speech.
|
||||
|
||||
### Task - convert speech to text
|
||||
|
||||
1. Create a new Python app on your computer in a folder called `smart-timer` with a single file called `app.py` and a Python virtual environment.
|
||||
|
||||
1. Install the Pip package for the speech services. Make sure you are installing this from a terminal with the virtual environment activated.
|
||||
|
||||
```sh
|
||||
pip install azure-cognitiveservices-speech
|
||||
```
|
||||
|
||||
> ⚠️ If you see the following error:
|
||||
>
|
||||
> ```output
|
||||
> ERROR: Could not find a version that satisfies the requirement azure-cognitiveservices-speech (from versions: none)
|
||||
> ERROR: No matching distribution found for azure-cognitiveservices-speech
|
||||
> ```
|
||||
>
|
||||
> You will need to update Pip. Do this with the following command, then try to install the package again
|
||||
>
|
||||
> ```sh
|
||||
> pip install --upgrade pip
|
||||
> ```
|
||||
|
||||
1. Add the following imports to the `app,py` file:
|
||||
|
||||
```python
|
||||
import time
|
||||
from azure.cognitiveservices.speech import SpeechConfig, SpeechRecognizer
|
||||
```
|
||||
|
||||
This imports some classes used to recognize speech.
|
||||
|
||||
1. Add the following code to declare some configuration:
|
||||
|
||||
```python
|
||||
api_key = '<key>'
|
||||
location = '<location>'
|
||||
language = '<language>'
|
||||
|
||||
speech_config = SpeechConfig(subscription=api_key,
|
||||
region=location,
|
||||
speech_recognition_language=language)
|
||||
```
|
||||
|
||||
Replace `<key>` with the API key for your speech service. Replace `<location>` with the location you used when you created the speech service resource.
|
||||
|
||||
Replace `<language>` with the locale name for language you will be speaking in, for example `en-GB` for English, or `zn-HK` for Cantonese. You can find a list of the supported languages and their locale names in the [Language and voice support documentation on Microsoft docs](https://docs.microsoft.com/azure/cognitive-services/speech-service/language-support?WT.mc_id=academic-17441-jabenn#speech-to-text).
|
||||
|
||||
This configuration is then used to create a `SpeechConfig` object that will be used to configure the speech services.
|
||||
|
||||
1. Add the following code to create a speech recognizer:
|
||||
|
||||
```python
|
||||
recognizer = SpeechRecognizer(speech_config=speech_config)
|
||||
```
|
||||
|
||||
1. The speech recognizer runs on a background thread, listening for audio and converting any speech in it to text. You can get the text using a callback function - a function you define and pass to the recognizer. Every time speech is detected, the callback is called. Add the following code to define a callback that prints the text to the console, and pass this callback to the recognizer:
|
||||
|
||||
```python
|
||||
def recognized(args):
|
||||
print(args.result.text)
|
||||
|
||||
recognizer.recognized.connect(recognized)
|
||||
```
|
||||
|
||||
1. The recognizer only starts listening when you explicitly start it. Add the following code to start the recognition. This runs in the background, so your application will also need an infinite loop that sleeps to keep the application running.
|
||||
|
||||
```python
|
||||
recognizer.start_continuous_recognition()
|
||||
|
||||
while True:
|
||||
time.sleep(1)
|
||||
```
|
||||
|
||||
1. Run this app. Speak into your microphone and you will see the audio converted to text in the console.
|
||||
|
||||
```output
|
||||
(.venv) ➜ smart-timer python3 app.py
|
||||
Hello world.
|
||||
Welcome to IoT for beginners.
|
||||
```
|
||||
|
||||
Try different types of sentences, along with sentences where words sound the same but have different meanings. For example, if you are speaking in English, say 'I want to buy two bananas and an apple too', and notice how it will use the correct to, two and too based on the context of the word, not just it's sound.
|
||||
|
||||
> 💁 You can find this code in the [code-speech-to-text/virtual-iot-device](code-speech-to-text/virtual-iot-device) folder.
|
||||
|
||||
😀 Your speech to text program was a success!
|
@ -0,0 +1,3 @@
|
||||
# Capture audio - Wio Terminal
|
||||
|
||||
Coming soon!
|
@ -0,0 +1,3 @@
|
||||
# Configure your microphone and speakers - Wio Terminal
|
||||
|
||||
Coming soon!
|
@ -0,0 +1,3 @@
|
||||
# Speech to text - Wio Terminal
|
||||
|
||||
Coming soon!
|
@ -0,0 +1,33 @@
|
||||
# Understand language
|
||||
|
||||
Add a sketchnote if possible/appropriate
|
||||
|
||||

|
||||
|
||||
## Pre-lecture quiz
|
||||
|
||||
[Pre-lecture quiz](https://brave-island-0b7c7f50f.azurestaticapps.net/quiz/33)
|
||||
|
||||
## Introduction
|
||||
|
||||
In this lesson you will learn about
|
||||
|
||||
In this lesson we'll cover:
|
||||
|
||||
* [Thing 1](#thing-1)
|
||||
|
||||
## Thing 1
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Challenge
|
||||
|
||||
## Post-lecture quiz
|
||||
|
||||
[Post-lecture quiz](https://brave-island-0b7c7f50f.azurestaticapps.net/quiz/34)
|
||||
|
||||
## Review & Self Study
|
||||
|
||||
## Assignment
|
||||
|
||||
[](assignment.md)
|
@ -0,0 +1,9 @@
|
||||
#
|
||||
|
||||
## Instructions
|
||||
|
||||
## Rubric
|
||||
|
||||
| Criteria | Exemplary | Adequate | Needs Improvement |
|
||||
| -------- | --------- | -------- | ----------------- |
|
||||
| | | | |
|
@ -0,0 +1,33 @@
|
||||
# Provide spoken feedback
|
||||
|
||||
Add a sketchnote if possible/appropriate
|
||||
|
||||

|
||||
|
||||
## Pre-lecture quiz
|
||||
|
||||
[Pre-lecture quiz](https://brave-island-0b7c7f50f.azurestaticapps.net/quiz/33)
|
||||
|
||||
## Introduction
|
||||
|
||||
In this lesson you will learn about
|
||||
|
||||
In this lesson we'll cover:
|
||||
|
||||
* [Thing 1](#thing-1)
|
||||
|
||||
## Thing 1
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Challenge
|
||||
|
||||
## Post-lecture quiz
|
||||
|
||||
[Post-lecture quiz](https://brave-island-0b7c7f50f.azurestaticapps.net/quiz/34)
|
||||
|
||||
## Review & Self Study
|
||||
|
||||
## Assignment
|
||||
|
||||
[](assignment.md)
|
@ -0,0 +1,9 @@
|
||||
#
|
||||
|
||||
## Instructions
|
||||
|
||||
## Rubric
|
||||
|
||||
| Criteria | Exemplary | Adequate | Needs Improvement |
|
||||
| -------- | --------- | -------- | ----------------- |
|
||||
| | | | |
|
@ -0,0 +1,33 @@
|
||||
# Support multiple languages
|
||||
|
||||
Add a sketchnote if possible/appropriate
|
||||
|
||||

|
||||
|
||||
## Pre-lecture quiz
|
||||
|
||||
[Pre-lecture quiz](https://brave-island-0b7c7f50f.azurestaticapps.net/quiz/33)
|
||||
|
||||
## Introduction
|
||||
|
||||
In this lesson you will learn about
|
||||
|
||||
In this lesson we'll cover:
|
||||
|
||||
* [Thing 1](#thing-1)
|
||||
|
||||
## Thing 1
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Challenge
|
||||
|
||||
## Post-lecture quiz
|
||||
|
||||
[Post-lecture quiz](https://brave-island-0b7c7f50f.azurestaticapps.net/quiz/34)
|
||||
|
||||
## Review & Self Study
|
||||
|
||||
## Assignment
|
||||
|
||||
[](assignment.md)
|
@ -0,0 +1,9 @@
|
||||
#
|
||||
|
||||
## Instructions
|
||||
|
||||
## Rubric
|
||||
|
||||
| Criteria | Exemplary | Adequate | Needs Improvement |
|
||||
| -------- | --------- | -------- | ----------------- |
|
||||
| | | | |
|
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 176 KiB |
After Width: | Height: | Size: 290 KiB |
After Width: | Height: | Size: 387 KiB |
After Width: | Height: | Size: 371 KiB |
After Width: | Height: | Size: 76 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 301 KiB |
Before Width: | Height: | Size: 4.3 MiB After Width: | Height: | Size: 4.2 MiB |
After Width: | Height: | Size: 5.9 MiB |