diff --git a/echo_crafter/__init__.py b/echo_crafter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/echo_crafter/listener/listener_with_wake_word.py b/echo_crafter/listener/listener_with_wake_word.py index 1693edf..5be9afe 100644 --- a/echo_crafter/listener/listener_with_wake_word.py +++ b/echo_crafter/listener/listener_with_wake_word.py @@ -11,7 +11,7 @@ def play_sound(wav_file): """Play a ding sound to indicate that the wake word was detected.""" - subprocess.Popen(["aplay", "-q", str(wav_file)]) + subprocess.Popen(["aplay", "-q", wav_file]) def main(): diff --git a/echo_crafter/listener/utils/porcupine.py b/echo_crafter/listener/utils/porcupine.py index a75d412..dba0796 100644 --- a/echo_crafter/listener/utils/porcupine.py +++ b/echo_crafter/listener/utils/porcupine.py @@ -53,7 +53,7 @@ def recorder_context_manager(): class _Microphone: - """A context manager for the recorder_context_manager.""" + """A class implementing the various microphone actions we use.""" def __init__(self, recorder, porcupine, cheetah): self.recorder = recorder @@ -82,7 +82,7 @@ def process_and_transmit_utterance(self, client): @contextmanager def Microphone(): - """Create a Microphone instance and yield it. Delete the instance upon exit.""" + """A context manager taking care of never leaking any resource.""" with recorder_context_manager() as recorder, porcupine_context_manager() as porcupine, cheetah_context_manager() as cheetah: mic = _Microphone(recorder, porcupine, cheetah) yield mic diff --git a/requirements.txt b/requirements.txt index 2e04ae4..f2cf8e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,4 @@ -annotated-types==0.6.0 -anyio==3.7.1 -certifi==2023.11.17 -distro==1.8.0 -h11==0.14.0 -httpcore==1.0.2 -httpx==0.25.2 -idna==3.6 openai==1.3.7 -PyAudio==0.2.14 -pydantic==2.5.2 -pydantic_core==2.14.5 -sniffio==1.3.0 -tqdm==4.66.1 -typing_extensions==4.8.0 -websocket-client==1.6.4 +pvcheetah==2.0.0 +pvporcupine==3.0.2 +pvrecorder==1.2.2 diff --git a/setup.py b/setup.py index 7feab56..7808de6 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,72 @@ -#!/usr/bin/env python3 -from setuptools import setup, find_packages +import platform +import os +from pathlib import Path +from setuptools import setup, find_packages, Command +from setuptools.command.build_py import build_py +import sys + +project_root = Path(__file__).parent +venv_dir = os.getenv('VIRTUAL_ENV') + +def get_os(): + os_name = platform.system() + return 'macOS' if os_name == 'Darwin' else os_name + +def is_virtual_env(): + return venv_dir is not None + +if not is_virtual_env(): + print('This script should be run in a virtual environment') + print("Please activate a virtual environment before running this script.") + venv_dir = project_root / '.venv' if (project_root / '.venv').exists() else project_root / 'venv' if (project_root / 'venv').exists() else None + if not venv_dir: + print("You can create a virtual environment using:") + if get_os() == 'Windows': + print("python -m venv venv") + venv_dir = project_root / 'venv' + else: + print("python -m venv .venv") + venv_dir = project_root / '.venv' + print("From the root of the project.") + + print("You can activate your virtual environment using:") + if get_os() == 'Windows': + print(f"{venv_dir.relative_to(project_root)}\\Scripts\\activate.bat") + else: + print(f"source {venv_dir.relative_to(project_root)}/bin/activate") + sys.exit(1) + +class BuildDocsCommand(Command): + description = 'Build Sphinx documentation' + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + import subprocess + docs_dir = project_root / 'docs' + if not docs_dir.exists() or not docs_dir.is_dir() or not (docs_dir / 'Makefile').exists(): + raise FileNotFoundError('Documentation not found') + venv_activate = project_root / '.venv/bin/activate' + if venv_activate.exists(): + subprocess.run(['source', str(venv_activate)], shell=True) + subprocess.run(['make', 'html'], cwd=docs_dir) setup( name='echo-crafter', version='0.1.0', packages=find_packages(), + cmdclass={ + 'build_docs': BuildDocsCommand, + 'build_py': build_py + }, entry_points={ 'console_scripts': [ - 'microphone_listen = echo_crafter.listener.listener_with_wake_word:main', - 'transcripts_collect = echo_crafter.listener.socket_read:main' + 'restart_daemons=scripts.restart_daemons:main', ] } ) diff --git a/start-listener.sh b/start-listener.sh new file mode 100755 index 0000000..1e1c87a --- /dev/null +++ b/start-listener.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env zsh + +DIR="$(dirname "$(readlink -f "$0")")" + +if pgrep -f "listener_with_wake_word" >/dev/null || pgrep -f "socket_read" >/dev/null; then + echo "The process is already running" + exit 1 +else + source "$DIR/.venv/bin/activate" + python "$DIR/echo_crafter/listener/socket_read.py" & + echo "Transcripts collector started" + python "$DIR/echo_crafter/listener/listener_with_wake_word.py" & + echo "Microphone listener started" +fi diff --git a/stop-listener.sh b/stop-listener.sh new file mode 100755 index 0000000..7714c70 --- /dev/null +++ b/stop-listener.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env zsh + +DIR="$(dirname "$(readlink -f "$0")")" + +microphone_listener=$(pgrep -f "listener_with_wake_word") 2>/dev/null +transcripts_collector=$(pgrep -f "socket_read") 2>/dev/null + +if [ -n "$microphone_listener" ]; then + kill -15 "$microphone_listener" + echo "Microphone listener stopped" +fi +if [ -n "$transcripts_collector" ]; then + kill -15 "$transcripts_collector" + echo "Transcripts collector stopped" +fi