diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1ea5834 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +GROQ_API_KEY=your_groq_api_key_here +LLM_PROVIDER=groq diff --git a/README.MD b/README.MD index 158ecf0..cf37a48 100644 --- a/README.MD +++ b/README.MD @@ -14,13 +14,26 @@ BonziAssist revives the classic BonziBuddy assistant in a modern, voice-activate ### Installation 1. Clone this repository. -2. Ensure you have [Python](https://www.python.org/downloads/) and [Visual CPP Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/). installed, and then run `pip install -r requirements.txt` to install the necessary packages. -4. Set up your environment variables. Edit the `.env.example` file in `helpers\` and follow the instructions there. `[INSTRUCTIONS='Remove this line of instructions. Fill out the info below. Rename this file, and remove the .example. It should just be ".env" now.']` +2. Ensure you have [Python](https://www.python.org/downloads/) and [Visual CPP Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) installed. +3. Install the required packages: +```bash +python -m pip install -r requirements.txt +``` +4. Set up your environment variables: +```bash +copy .env.example .env +``` +Then open `.env` and set `GROQ_API_KEY` and `LLM_PROVIDER`. ### Usage -To start BonziAssist: +Before starting BonziAssist, run the setup check: +```bash +python start.py --check +``` + +If the check passes, start BonziAssist: ```bash -python main.py +python start.py ``` You will be prompted to configure your microphone settings upon the first run or if you choose to be prompted every time by the configuration settings. @@ -60,4 +73,4 @@ First of all, I appreciate anyone being excited about this project. That being s - **SimpleAudio**: Used for playing back audio responses generated by the system. - **PyAudio**: Handles microphone input and audio stream management. - **Python-dotenv**: For managing environment variables in a .env file, which helps secure API keys and other sensitive settings. -- **GitHub Community**: For all the open-source contributors who have developed libraries and tools that made this project feasible. \ No newline at end of file +- **GitHub Community**: For all the open-source contributors who have developed libraries and tools that made this project feasible. diff --git a/helpers/llm.py b/helpers/llm.py index 3aba156..3854606 100644 --- a/helpers/llm.py +++ b/helpers/llm.py @@ -1,17 +1,20 @@ import os +from pathlib import Path + from dotenv import load_dotenv import litellm -load_dotenv() +PROJECT_ROOT = Path(__file__).resolve().parents[1] +load_dotenv(PROJECT_ROOT / ".env") groq_api_key = os.getenv("GROQ_API_KEY") llm_provider = os.getenv("LLM_PROVIDER") if groq_api_key is None: - raise ValueError("GROQ_API_KEY environment variable not found. Please set it in your .env file.") + raise ValueError("GROQ_API_KEY environment variable not found. Copy .env.example to .env and set your key.") if llm_provider is None: - raise ValueError("LLM_PROVIDER environment variable not found. Please set it in your .env file.") + raise ValueError("LLM_PROVIDER environment variable not found. Copy .env.example to .env and set your provider.") os.environ["GROQ_API_KEY"] = groq_api_key @@ -34,4 +37,4 @@ def request(user_query, model="llama3-8b-8192", temperature=0.7, max_tokens=150) max_tokens=max_tokens ) - return response.choices[0].message.content \ No newline at end of file + return response.choices[0].message.content diff --git a/helpers/mic.py b/helpers/mic.py index 5101544..04a0f70 100644 --- a/helpers/mic.py +++ b/helpers/mic.py @@ -1,8 +1,11 @@ -import pyaudio -import os import json +from pathlib import Path + +import pyaudio -CONFIG_FILE = "config\mic_config.json" +PROJECT_ROOT = Path(__file__).resolve().parents[1] +CONFIG_FILE = PROJECT_ROOT / "config" / "mic_config.json" +LEGACY_CONFIG_FILE = PROJECT_ROOT / "mic_config.json" def list_microphones(): p = pyaudio.PyAudio() @@ -34,13 +37,17 @@ def get_device_index(num_devices, default_device_index, default_device_name): print("Invalid input. Please enter a valid number.") def load_config(): - if os.path.exists(CONFIG_FILE): - with open(CONFIG_FILE, "r") as f: + if CONFIG_FILE.exists(): + with CONFIG_FILE.open("r") as f: + return json.load(f) + if LEGACY_CONFIG_FILE.exists(): + with LEGACY_CONFIG_FILE.open("r") as f: return json.load(f) return None def save_config(config): - with open(CONFIG_FILE, "w") as f: + CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True) + with CONFIG_FILE.open("w") as f: json.dump(config, f, indent=4) def configure_microphone(): @@ -57,4 +64,4 @@ def configure_microphone(): "prompt_every_time": prompt_every_time == 'y' } save_config(config) - return config \ No newline at end of file + return config diff --git a/main.py b/main.py index c312325..0a4b82f 100644 --- a/main.py +++ b/main.py @@ -71,9 +71,12 @@ def listen_for_bonzi(device_index=None): time.sleep(.45) command_active = True # Enable command capture -if __name__ == "__main__": +def main(): config = mic.load_config() if config is None or config.get("prompt_every_time", False): config = mic.configure_microphone() device_index = config['device_index'] listen_for_bonzi(device_index) + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt index 0564ce1..6de2b72 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ pyaudio python-dotenv litellm +requests simpleaudio vosk diff --git a/start.py b/start.py new file mode 100644 index 0000000..bbc83cf --- /dev/null +++ b/start.py @@ -0,0 +1,103 @@ +import argparse +import importlib.util +import os +import sys +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parent +REQUIRED_PACKAGES = { + "dotenv": "python-dotenv", + "litellm": "litellm", + "pyaudio": "pyaudio", + "requests": "requests", + "simpleaudio": "simpleaudio", + "vosk": "vosk", +} + + +def missing_packages(): + missing = [] + for import_name, package_name in REQUIRED_PACKAGES.items(): + if importlib.util.find_spec(import_name) is None: + missing.append(package_name) + return missing + + +def load_env_file(): + env_path = PROJECT_ROOT / ".env" + values = {} + if not env_path.exists(): + return values + + for line in env_path.read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + values[key.strip()] = value.strip() + return values + + +def check_setup(): + errors = [] + warnings = [] + + missing = missing_packages() + if missing: + errors.append( + "Install missing packages with: " + + f"{sys.executable} -m pip install -r requirements.txt" + + f" (missing: {', '.join(missing)})" + ) + + env_path = PROJECT_ROOT / ".env" + env_values = load_env_file() + if not env_path.exists(): + errors.append("Create .env by copying .env.example, then set GROQ_API_KEY and LLM_PROVIDER.") + else: + for key in ("GROQ_API_KEY", "LLM_PROVIDER"): + if not env_values.get(key) or env_values[key].startswith("your_"): + errors.append(f"Set {key} in .env.") + + model_path = PROJECT_ROOT / "vosk" / "vosk-model-small-en-us-0.15" + if not model_path.exists(): + errors.append("The Vosk model is missing. Expected: vosk/vosk-model-small-en-us-0.15") + + if not (PROJECT_ROOT / "canned_responses").exists(): + errors.append("The canned_responses folder is missing.") + + if not (PROJECT_ROOT / "config").exists(): + warnings.append("The config folder will be created on first microphone setup.") + + return errors, warnings + + +def main(): + parser = argparse.ArgumentParser(description="Check and start BonziAssist.") + parser.add_argument("--check", action="store_true", help="Only check setup and exit.") + args = parser.parse_args() + + os.chdir(PROJECT_ROOT) + errors, warnings = check_setup() + + for warning in warnings: + print(f"Warning: {warning}") + + if errors: + print("BonziAssist is not ready to start:") + for error in errors: + print(f"- {error}") + raise SystemExit(1) + + print("BonziAssist setup looks ready.") + if args.check: + return + + from main import main as run_bonzi + + run_bonzi() + + +if __name__ == "__main__": + main()