-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
25 changed files
with
304 additions
and
143 deletions.
There are no files selected for viewing
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import os | ||
from pathlib import Path | ||
from typing import TypedDict | ||
|
||
PROJECT_ROOT=Path(os.getenv('EC_ROOT') or '') | ||
DATA_DIR=Path(os.getenv('EC_DATA_DIR') or '') | ||
|
||
class _Config(TypedDict): | ||
CHEETAH_MODEL_FILE: str | ||
PICOVOICE_API_KEY: str | ||
FRAME_LENGTH: int | ||
ENDPOINT_DURATION_SEC: float | ||
TRANSCRIPT_BEGIN_WAV: str | ||
TRANSCRIPT_SUCCESS_WAV: str | ||
SOCKET_PATH: str | ||
|
||
|
||
Config: _Config = dict( | ||
CHEETAH_MODEL_FILE=str(DATA_DIR/"speech-command-cheetah-v1.pv"), | ||
PICOVOICE_API_KEY=os.environ.get('PICOVOICE_API_KEY', ''), | ||
FRAME_LENGTH=512, | ||
ENDPOINT_DURATION_SEC=1.3, | ||
TRANSCRIPT_BEGIN_WAV=str(DATA_DIR/"transcript_begin.wav"), | ||
TRANSCRIPT_SUCCESS_WAV=str(DATA_DIR/"transcript_success.wav"), | ||
SOCKET_PATH=str(Path(os.getenv('EC_SOCKET_FILE') or '/tmp/echo-crafter.sock')) | ||
) | ||
|
||
__all__ = ['Config'] |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
from echo_crafter.listener.utils import ( | ||
socket_connection, | ||
Microphone | ||
) | ||
from echo_crafter.logger import setup_logger | ||
from echo_crafter.config import Config | ||
|
||
import subprocess | ||
import traceback | ||
|
||
|
||
def play_sound(wav_file): | ||
"""Play a ding sound to indicate that the wake word was detected.""" | ||
subprocess.Popen(["aplay", "-q", str(wav_file)]) | ||
|
||
|
||
def main(): | ||
"""Upon detection of a wake word, transcribe speech until endpoint is detected.""" | ||
logger = setup_logger() | ||
|
||
with Microphone() as mic: | ||
try: | ||
while True: | ||
mic.wait_for_wake_word() | ||
play_sound(Config['TRANSCRIPT_BEGIN_WAV']) | ||
|
||
with socket_connection(Config['SOCKET_PATH']) as client: | ||
mic.process_and_transmit_utterance(client) | ||
play_sound(Config['TRANSCRIPT_SUCCESS_WAV']) | ||
|
||
except KeyboardInterrupt: | ||
pass | ||
|
||
except Exception as e: | ||
logger.error(f"An error occured:\n{e}") | ||
logger.error(traceback.format_exc()) | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from .porcupine import Microphone | ||
from .sockets import socket_connection | ||
|
||
__all__ = [ | ||
'socket_connection', | ||
'Microphone' | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
from echo_crafter.config import Config | ||
|
||
from contextlib import contextmanager | ||
import pvcheetah | ||
import pvporcupine | ||
import pvrecorder | ||
|
||
@contextmanager | ||
def porcupine_context_manager(): | ||
"""Create a Porcupine instance and yield it. Delete the instance upon exit.""" | ||
porcupine_instance = None | ||
try: | ||
porcupine_instance = pvporcupine.create( | ||
keywords=['computer'], | ||
sensitivities=[0.1], | ||
access_key=Config['PICOVOICE_API_KEY'] | ||
) | ||
yield porcupine_instance | ||
finally: | ||
if porcupine_instance is not None: | ||
porcupine_instance.delete() | ||
|
||
|
||
@contextmanager | ||
def cheetah_context_manager(): | ||
"""Create a Cheetah instance and yield it. Delete the instance upon exit.""" | ||
cheetah_instance = None | ||
try: | ||
cheetah_instance = pvcheetah.create( | ||
access_key=Config['PICOVOICE_API_KEY'], | ||
endpoint_duration_sec=Config['ENDPOINT_DURATION_SEC'], | ||
model_path=Config['CHEETAH_MODEL_FILE'] | ||
) | ||
yield cheetah_instance | ||
finally: | ||
if cheetah_instance is not None: | ||
cheetah_instance.delete() | ||
|
||
|
||
@contextmanager | ||
def recorder_context_manager(): | ||
"""Create a PvRecorder instance and yield it. Delete the instance upon exit.""" | ||
recorder_instance = None | ||
try: | ||
recorder_instance = pvrecorder.PvRecorder( | ||
frame_length=Config['FRAME_LENGTH'] | ||
) | ||
recorder_instance.start() | ||
yield recorder_instance | ||
finally: | ||
if recorder_instance is not None: | ||
recorder_instance.delete() | ||
|
||
|
||
class _Microphone: | ||
"""A context manager for the recorder_context_manager.""" | ||
|
||
def __init__(self, recorder, porcupine, cheetah): | ||
self.recorder = recorder | ||
self.porcupine = porcupine | ||
self.cheetah = cheetah | ||
|
||
def wait_for_wake_word(self): | ||
"""Wait for the wake word to be detected.""" | ||
while True: | ||
pcm_frame = self.recorder.read() | ||
keyword_index = self.porcupine.process(pcm_frame) | ||
if keyword_index >= 0: | ||
break | ||
|
||
def process_and_transmit_utterance(self, client): | ||
"""Process the utterance and transmit the partial transcript to the client.""" | ||
while True: | ||
pcm_frame = self.recorder.read() | ||
partial_transcript, is_endpoint = self.cheetah.process(pcm_frame) | ||
client.sendall((partial_transcript).encode()) | ||
if is_endpoint: | ||
partial_transcript = self.cheetah.flush() | ||
client.sendall((partial_transcript + 'STOP').encode()) | ||
break | ||
|
||
|
||
@contextmanager | ||
def Microphone(): | ||
"""Create a Microphone instance and yield it. Delete the instance upon exit.""" | ||
with recorder_context_manager() as recorder, porcupine_context_manager() as porcupine, cheetah_context_manager() as cheetah: | ||
mic = _Microphone(recorder, porcupine, cheetah) | ||
yield mic |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import socket | ||
import contextlib | ||
|
||
# Define a context manager for the socket connection | ||
@contextlib.contextmanager | ||
def socket_connection(socket_path): | ||
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||
try: | ||
client.connect(socket_path) | ||
yield client | ||
finally: | ||
client.close() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
"""Logging module for the Echo Crafter application.""" | ||
|
||
import logging | ||
import json | ||
import time | ||
import os | ||
from pathlib import Path | ||
|
||
LOG_FILE = Path(os.environ.get('EC_LOG_DIR', '')) / "transcripts.jsonl" | ||
|
||
class CustomRecord(logging.LogRecord): | ||
"""Custom LogRecord class with a timestamp attribute.""" | ||
|
||
def __init__(self, name, level, pathname, lineno, msg, args, | ||
exc_info, func=None, sinfo=None, **kwargs): | ||
"""Initialize a CustomRecord instance.""" | ||
super().__init__(name, level, pathname, lineno, msg, args, | ||
exc_info, func=func, sinfo=sinfo) | ||
self.timestamp = time.time() | ||
self.intent = kwargs.get('intent', '') | ||
self.slots = kwargs.get('slots', {}) | ||
|
||
|
||
class JsonFormatter(logging.Formatter): | ||
"""JSON formatter for log records.""" | ||
|
||
def format(self, record): | ||
"""Format a log record as a JSON string.""" | ||
log_dict = record.__dict__.copy() | ||
log_dict['msg'] = record.getMessage() | ||
return json.dumps(log_dict) | ||
|
||
|
||
def setup_logger(name=__name__, level=logging.INFO): | ||
"""Set up a logger with a JSON formatter.""" | ||
logger = logging.getLogger(name) | ||
logger.setLevel(level) | ||
logging.setLogRecordFactory(CustomRecord) | ||
handler = logging.FileHandler(LOG_FILE, encoding='utf-8') | ||
handler.setFormatter(JsonFormatter()) | ||
logger.addHandler(handler) | ||
return logger | ||
|
||
|
||
__all__ = ['setup_logger'] |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Oops, something went wrong.