Skip to content

Commit

Permalink
add porcupine wakeword
Browse files Browse the repository at this point in the history
  • Loading branch information
MarkParker5 committed Sep 21, 2023
1 parent e7102a5 commit d155e27
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 38 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "stark-place"
version = "1.0.2"
version = "1.1.0"
description = "S.T.A.R.K. Platform Library And Community Extensions"
authors = ["Mark Parker <[email protected]>"]
license = "CC BY-NC-SA 4.0"
Expand Down
8 changes: 4 additions & 4 deletions stark_place/examples/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
from stark.interfaces.silero import SileroSpeechSynthesizer


VOSK_MODEL_URL = "YOUR_CHOSEN_VOSK_MODEL_URL"
SILERO_MODEL_URL = "YOUR_CHOSEN_SILERO_MODEL_URL"
vosk_model_ur = 'YOUR_CHOSEN_VOSK_MODEL_URL'
silero_model_ur = 'YOUR_CHOSEN_SILERO_MODEL_URL'

recognizer = VoskSpeechRecognizer(model_url=VOSK_MODEL_URL)
synthesizer = SileroSpeechSynthesizer(model_url=SILERO_MODEL_URL)
recognizer = VoskSpeechRecognizer(model_url=vosk_model_ur)
synthesizer = SileroSpeechSynthesizer(model_url=silero_model_ur)

manager = CommandsManager()

Expand Down
8 changes: 4 additions & 4 deletions stark_place/examples/custom_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
from stark.general.blockage_detector import BlockageDetector


VOSK_MODEL_URL = "YOUR_CHOSEN_VOSK_MODEL_URL"
SILERO_MODEL_URL = "YOUR_CHOSEN_SILERO_MODEL_URL"
vosk_model_ur = 'YOUR_CHOSEN_VOSK_MODEL_URL'
silero_model_ur = 'YOUR_CHOSEN_SILERO_MODEL_URL'

recognizer = VoskSpeechRecognizer(model_url=VOSK_MODEL_URL)
synthesizer = SileroSpeechSynthesizer(model_url=SILERO_MODEL_URL)
recognizer = VoskSpeechRecognizer(model_url=vosk_model_ur)
synthesizer = SileroSpeechSynthesizer(model_url=silero_model_ur)

manager = CommandsManager()

Expand Down
8 changes: 4 additions & 4 deletions stark_place/examples/hello_world.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
from stark.interfaces.silero import SileroSpeechSynthesizer


VOSK_MODEL_URL = "YOUR_CHOSEN_VOSK_MODEL_URL"
SILERO_MODEL_URL = "YOUR_CHOSEN_SILERO_MODEL_URL"
vosk_model_ur = 'YOUR_CHOSEN_VOSK_MODEL_URL'
silero_model_ur = 'YOUR_CHOSEN_SILERO_MODEL_URL'

recognizer = VoskSpeechRecognizer(model_url=VOSK_MODEL_URL)
synthesizer = SileroSpeechSynthesizer(model_url=SILERO_MODEL_URL)
recognizer = VoskSpeechRecognizer(model_url=vosk_model_ur)
synthesizer = SileroSpeechSynthesizer(model_url=silero_model_ur)

manager = CommandsManager()

Expand Down
20 changes: 15 additions & 5 deletions stark_place/examples/keyboard_trigger.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
'''
Requirements except stark:
- pynput
'''

import asyncer
from stark import CommandsContext, CommandsManager, Response
from stark.general.blockage_detector import BlockageDetector
Expand All @@ -7,13 +12,14 @@
from stark.voice_assistant import VoiceAssistant, Mode

from stark_place.triggers import keyboard_key # import the trigger
from stark_place.notifications import sound


VOSK_MODEL_URL = "YOUR_CHOSEN_VOSK_MODEL_URL"
SILERO_MODEL_URL = "YOUR_CHOSEN_SILERO_MODEL_URL"
vosk_model_ur = 'YOUR_CHOSEN_VOSK_MODEL_URL'
silero_model_ur = 'YOUR_CHOSEN_SILERO_MODEL_URL'

recognizer = VoskSpeechRecognizer(model_url=VOSK_MODEL_URL)
synthesizer = SileroSpeechSynthesizer(model_url=SILERO_MODEL_URL)
recognizer = VoskSpeechRecognizer(model_url=vosk_model_ur)
synthesizer = SileroSpeechSynthesizer(model_url=silero_model_ur)

manager = CommandsManager()

Expand Down Expand Up @@ -42,7 +48,11 @@ async def run(

voice_assistant.mode = Mode.external() # stop listening after first response

main_task_group.soonify(keyboard_key.start)(main_task_group.soonify(speech_recognizer.start_listening)) # add trigger to the main loop
def on_hotkey():
sound.play() # optional: play a sound when the hotkey is pressed, check the realisation in stark_place/notifications/sound.py
main_task_group.soonify(speech_recognizer.start_listening)

main_task_group.soonify(keyboard_key.start)(on_hotkey) # add trigger to the main loop
# main_task_group.soonify(speech_recognizer.start_listening)() # don't start listening until the hotkey is pressed
main_task_group.soonify(context.handle_responses)()

Expand Down
92 changes: 92 additions & 0 deletions stark_place/examples/porcupine_wakeword_trigger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
'''
Requirements except stark:
- pvporcupine
'''

import asyncer
from stark import CommandsContext, CommandsManager, Response
from stark.general.blockage_detector import BlockageDetector
from stark.interfaces.protocols import SpeechRecognizer, SpeechSynthesizer
from stark.interfaces.vosk import VoskSpeechRecognizer
from stark.interfaces.silero import SileroSpeechSynthesizer
from stark.voice_assistant import VoiceAssistant, Mode

from stark_place.triggers import porcupine # import the trigger
from stark_place.notifications import sound


vosk_model_ur = 'YOUR_CHOSEN_VOSK_MODEL_URL'
silero_model_ur = 'YOUR_CHOSEN_SILERO_MODEL_URL'

# register at https://console.picovoice.ai and copy the access key
# train and download a model for the your platform at https://console.picovoice.ai/ppn
# don't rename either the model file or the keyword files
access_key = 'YOUR_ACCESS_KEY'
keyword_paths = ['YOUR_KEYWORD_PATH.ppn',]
model_path = 'YOUR_MODEL_PATH.pv'

recognizer = VoskSpeechRecognizer(model_url = vosk_model_ur)
synthesizer = SileroSpeechSynthesizer(model_url = silero_model_ur)

manager = CommandsManager()

@manager.new('hello')
async def hello_command() -> Response:
text = voice = 'Hello, world!'
return Response(text=text, voice=voice)

async def run(
manager: CommandsManager,
speech_recognizer: SpeechRecognizer,
speech_synthesizer: SpeechSynthesizer
):
async with asyncer.create_task_group() as main_task_group:
context = CommandsContext(
task_group = main_task_group,
commands_manager = manager
)
voice_assistant = VoiceAssistant(
speech_recognizer = speech_recognizer,
speech_synthesizer = speech_synthesizer,
commands_context = context
)
speech_recognizer.delegate = voice_assistant
context.delegate = voice_assistant

voice_assistant.mode = Mode.external() # stop listening after first response

# trigger-listener setup

def add_porcupine_listener():
# soonify returns immediately, but the wrapped function is added to the task group (main loop)
# porcuping and speech recognizer use the same microphone device, so they can't run at the same time
# porcupine.start runs until the first wake-word is detected and needs to be restarted after the speech recognizer is stopped
def on_wake_word():
sound.play() # optional: play a sound when the wake-word is detected, check the realisation in stark_place/notifications/sound.py
main_task_group.soonify(start_speech_recognizer)()

main_task_group.soonify(porcupine.start)(
access_key = access_key,
keyword_paths = keyword_paths,
model_path = model_path,
callback = on_wake_word
)

async def start_speech_recognizer():
await speech_recognizer.start_listening() # awaits until the speech recognizer is stopped
add_porcupine_listener() # start listening for the wake-word after the speech recognizer is stopped

add_porcupine_listener() # start listening for the wake-word

# other tasks

main_task_group.soonify(context.handle_responses)()

detector = BlockageDetector()
main_task_group.soonify(detector.monitor)()

async def main():
await run(manager, recognizer, synthesizer)

if __name__ == '__main__':
asyncer.runnify(main)() # or anyio.run(main), same thing
15 changes: 15 additions & 0 deletions stark_place/notifications/sound.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import os


def play():
'''
play all sounds in macos using shell: sh`for s in /System/Library/Sounds/*; do echo "$s" && afplay "$s"; done`
for linux: check the `/usr/share/sounds/` directory and use `aplay` instead of `afplay`
as an alternative, you can use the cross-platform system-sounds pypi library to list and play sounds
pypi.org/project/system-sounds
or github.com/MarkParker5/system-sounds
or use the a SpeechSynthesizer to say something like "Yes, sir?"
'''
os.system('afplay /System/Library/Sounds/Blow.aiff &') # play the sound in the background (non-blocking, immediate return) (macos)
26 changes: 6 additions & 20 deletions stark_place/triggers/keyboard_key.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from typing import Callable, Any
import os
from pynput.keyboard import Key, KeyCode, Listener, HotKey
from pynput.keyboard import Key, KeyCode, Listener
import anyio
from asyncer import asyncify
from threading import Event


Expand All @@ -19,22 +17,10 @@ def on_release(key: Key):
listener.start() # start listening in a separate thread

while True: # this loop runs in the main thread

await anyio.sleep(0.1) # release the thread for other tasks

if hotkey_event.is_set():
'''
optional: play a sound to indicate that the hotkey was pressed
play all sounds in macos using shell: sh`for s in /System/Library/Sounds/*; do echo "$s" && afplay "$s"; done`
for linux: check the `/usr/share/sounds/` directory and use `aplay` instead of `afplay`
as an alternative, you can use the system-sounds pypi library to list and play sounds
pypi.org/project/system-sounds
or github.com/MarkParker5/system-sounds
or use the a SpeechSynthesizer to say something like "Yes, sir?"
'''
os.system('afplay /System/Library/Sounds/Blow.aiff &') # play the sound in the background (macos)
callback()
hotkey_event.clear()
if not hotkey_event.is_set():
continue

callback()
hotkey_event.clear()
43 changes: 43 additions & 0 deletions stark_place/triggers/porcupine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from typing import Any, Callable
import anyio
import pvporcupine
from pvrecorder import PvRecorder


async def start(
access_key: str,
keyword_paths: list[str],
model_path: str,
callback: Callable[[], Any | None]
):
# register at https://console.picovoice.ai and copy the access key
# train and download a model for the your platform at https://console.picovoice.ai/ppn
# don't rename either the model file or the keyword files
porcupine = pvporcupine.create(
access_key = access_key,
keyword_paths = keyword_paths,
model_path = model_path
)

recorder = PvRecorder(
frame_length = porcupine.frame_length,
device_index = 0
)
recorder.start()

try:
while True: # this loop runs in the main thread
await anyio.sleep(0.01) # release the thread for other tasks

pcm = recorder.read()
keyword_index = porcupine.process(pcm)

if keyword_index == -1:
continue

callback()
break # stop listening after the first keyword is detected to release the microphone device
finally:
# stop the recorder, delete the porcupine instances, and release the microphone device
porcupine.delete()
recorder.delete()

0 comments on commit d155e27

Please sign in to comment.