diff --git a/README.md b/README.md index cc80f3a..6454573 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ https://github.com/user-attachments/assets/8aa729ff-c431-4a61-a9ef-d17050a27d02 ### Prerequisites - Python 3.9 or higher +- Conda (Miniconda or Anaconda) ### Setup @@ -47,14 +48,28 @@ https://github.com/user-attachments/assets/8aa729ff-c431-4a61-a9ef-d17050a27d02 pip install -r requirements.txt ``` -4. Set up your Anthropic API key: - - Set up a developer account with Anthropic and get an API key at https://console.anthropic.com/dashboard - - You'll need to pre-fund your account to cover API costs. Current costs (as of Sept 15, 2024) are $0.003 and $0.015 per 1000 tokens for input and output, respectively. Long conversations will obviously cost more. Fund your account with something small (maybe $5) to start with, then add more if you find this tool useful. - - Create a `.env` file in the project root - - Add your API key to the file: - ``` - ANTHROPIC_API_KEY="your_api_key_here" # make sure the key is in quotes - ``` +Note: These instructions should work for most systems (macOS, including Apple Silicon, Windows, and Linux). If you encounter any architecture-specific issues, please refer to the troubleshooting section below or open an issue on GitHub. + +### Troubleshooting + +If you experience architecture-specific issues (e.g., on Apple Silicon Macs), try the following: + +1. Ensure Conda is using the correct architecture: + ``` + conda info + ``` + Look for the "platform" field to confirm it matches your system architecture. + +2. If needed, you can force Conda to use a specific architecture: + ``` + CONDA_SUBDIR=osx-arm64 conda create -n codeaide python=3.11 # For Apple Silicon + conda activate codeaide + conda config --env --set subdir osx-arm64 # For Apple Silicon + ``` + +3. Then proceed with step 3 of the regular installation process. + +For other architecture-specific issues, please open an issue on GitHub with details about your system and the problem you're encountering. ## Usage diff --git a/build_scripts/build_codeaide.sh b/build_scripts/build_codeaide.sh index a2e8264..8950476 100755 --- a/build_scripts/build_codeaide.sh +++ b/build_scripts/build_codeaide.sh @@ -3,29 +3,7 @@ # Exit on any error set -e -# Function to check if a command exists -command_exists () { - type "$1" &> /dev/null ; -} - -# Check prerequisites -if ! command_exists conda ; then - echo "conda is not installed. Please install it and try again." - exit 1 -fi - -if ! command_exists brew ; then - echo "Homebrew is not installed. Please install it and try again." - exit 1 -fi - -# Check if notarization is requested -NOTARIZE=false -if [ "$1" = "--notarize" ]; then - NOTARIZE=true -fi - -# Activate conda environment +# Activate the correct conda environment eval "$(conda shell.bash hook)" conda activate codeaide @@ -43,50 +21,32 @@ mkdir -p ~/Library/Logs/CodeAide echo "Ensuring application data directory exists..." mkdir -p ~/Library/Application\ Support/CodeAide -# Step 1: Install Required Python Packages and Download Whisper Model +# Install required packages echo "Installing required Python packages..." $PYTHON_PATH -m pip install --upgrade pip -$PYTHON_PATH -m pip install PyQt5 pyinstaller whisper +$PYTHON_PATH -m pip install -r requirements.txt -echo "Downloading Whisper model..." -$PYTHON_PATH </dev/null || true - diskutil eject force $disk 2>/dev/null || true - done - rm -f "${TEMP_DMG}" -} - -# Run cleanup before starting -cleanup - # Create a temporary directory for DMG contents TEMP_DIR=$(mktemp -d) cp -R "${SOURCE_DIR}" "${TEMP_DIR}" ln -s /Applications "${TEMP_DIR}" -# Create the DMG directly -echo "Creating DMG..." +# Create the DMG hdiutil create -volname "${APP_NAME}" -srcfolder "${TEMP_DIR}" -ov -format UDZO "${FINAL_DMG}" # Clean up the temporary directory @@ -100,32 +60,5 @@ else exit 1 fi -if [ "$NOTARIZE" = true ]; then - # Prompt for Apple Developer information - read -p "Enter your Developer ID Application certificate name (e.g., 'Developer ID Application: Your Name'): " DEVELOPER_NAME - read -p "Enter your Apple Developer Team ID: " TEAM_ID - read -p "Enter your Apple ID: " APPLE_ID - read -s -p "Enter your app-specific password: " APP_PASSWORD - echo - - # Step 4: Code Sign the Application - echo "Code signing the application..." - codesign --force --options runtime --entitlements build_scripts/entitlements.plist --sign "$DEVELOPER_NAME" "dist/CodeAide.app" - - # Step 5: Notarize the DMG - echo "Notarizing the DMG..." - xcrun notarytool submit "${FINAL_DMG}" --wait --apple-id "$APPLE_ID" --password "$APP_PASSWORD" --team-id "$TEAM_ID" - - # Step 6: Staple the notarization ticket to the DMG - echo "Stapling the notarization ticket..." - xcrun stapler staple "${FINAL_DMG}" - - echo "Build process completed successfully!" - echo "The signed and notarized DMG is ready for distribution." -else - echo "Build process completed successfully!" - echo "The DMG is ready for testing. (Not signed or notarized)" -fi - -# Final cleanup -cleanup +echo "Build process completed successfully!" +echo "The DMG is ready for testing. (Not signed or notarized)" diff --git a/build_scripts/codeaide.spec b/build_scripts/codeaide.spec index 5870ff4..fb46ef5 100644 --- a/build_scripts/codeaide.spec +++ b/build_scripts/codeaide.spec @@ -1,19 +1,64 @@ # -*- mode: python ; coding: utf-8 -*- + import os +import sys +from PyInstaller.utils.hooks import collect_data_files + +# Add the path to your conda environment's site-packages +sys.path.append('/Users/dollerenshaw/opt/anaconda3/envs/codeaide_arm64_new/lib/python3.11/site-packages') + +import whisper block_cipher = None -whisper_path = '/Users/dollerenshaw/opt/anaconda3/envs/codeaide/lib/python3.11/site-packages/whisper' +whisper_path = os.path.dirname(whisper.__file__) +whisper_assets = os.path.join(whisper_path, 'assets') + +# Determine the path to your project root +project_root = os.path.abspath(os.path.join(SPECPATH, '..')) + +# Collect data files +datas = [ + (os.path.join(project_root, 'codeaide', 'examples.yaml'), 'codeaide'), + (os.path.join(project_root, 'codeaide', 'assets'), 'codeaide/assets'), + (whisper_assets, 'whisper/assets'), +] + +# Add assets directory +assets_dir = os.path.join(project_root, 'codeaide', 'assets') +for root, dirs, files in os.walk(assets_dir): + for file in files: + file_path = os.path.join(root, file) + relative_path = os.path.relpath(root, project_root) + datas.append((file_path, relative_path)) a = Analysis( - ['../codeaide.py'], + ['../codeaide/__main__.py'], pathex=[], binaries=[], - datas=[('../codeaide/examples.yaml', 'codeaide'), - ('../codeaide/assets/*', 'codeaide/assets'), - ('../models', 'models'), - (os.path.join(whisper_path, 'assets'), 'whisper/assets')], - hiddenimports=['whisper'], + datas=datas + collect_data_files('whisper'), + hiddenimports=[ + 'PyQt5.QtCore', + 'PyQt5.QtGui', + 'PyQt5.QtWidgets', + 'anthropic', + 'google.generativeai', + 'decouple', + 'numpy', + 'keyring', + 'openai', + 'hjson', + 'yaml', + 'pygments', + 'sounddevice', + 'scipy', + 'openai-whisper', + 'whisper', + 'whisper.tokenizer', + 'whisper.audio', + 'whisper.model', + 'whisper.transcribe', + ], hookspath=[], hooksconfig={}, runtime_hooks=[], @@ -22,42 +67,41 @@ a = Analysis( win_private_assemblies=False, cipher=block_cipher, noarchive=False, - optimize=0, ) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE( pyz, a.scripts, - a.binaries, - a.zipfiles, - a.datas, [], + exclude_binaries=True, name='CodeAide', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, - upx_exclude=[], - runtime_tmpdir=None, console=False, disable_windowed_traceback=False, + argv_emulation=True, target_arch=None, codesign_identity=None, entitlements_file=None, ) -info_plist = { - 'NSHighResolutionCapable': True, - 'NSRequiresAquaSystemAppearance': False, # For dark mode support -} +coll = COLLECT( + exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='CodeAide', +) app = BUNDLE( - exe, + coll, name='CodeAide.app', - bundle_identifier='com.codeaide', - info_plist={ - 'NSMicrophoneUsageDescription': 'CodeAide needs access to your microphone for speech-to-text functionality.', - }, - entitlements_file='build_scripts/entitlements.plist', + icon=None, + bundle_identifier=None, ) diff --git a/codeaide/__main__.py b/codeaide/__main__.py index 35e59b2..14cbc23 100644 --- a/codeaide/__main__.py +++ b/codeaide/__main__.py @@ -1,17 +1,12 @@ import sys from PyQt5.QtWidgets import QApplication, QMessageBox -from PyQt5.QtCore import QTimer, Qt +from PyQt5.QtCore import QTimer, Qt, QSharedMemory from codeaide.logic.chat_handler import ChatHandler from codeaide.utils import api_utils from codeaide.ui.splash_screen import SplashScreen from codeaide.ui.chat_window import ChatWindow import traceback -if hasattr(Qt, "AA_EnableHighDpiScaling"): - QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) -if hasattr(Qt, "AA_UseHighDpiPixmaps"): - QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) - def exception_hook(exctype, value, tb): error_msg = "".join(traceback.format_exception(exctype, value, tb)) @@ -27,9 +22,31 @@ def exception_hook(exctype, value, tb): def main(): + # Create QApplication instance app = QApplication(sys.argv) + + # Check for existing instance + shared_memory = QSharedMemory("CodeAideUniqueKey") + if shared_memory.attach(): + print("Application is already running.") + sys.exit(1) + + if not shared_memory.create(1): + print("Failed to create shared memory.") + sys.exit(1) + sys.excepthook = exception_hook + if hasattr(Qt, "AA_EnableHighDpiScaling"): + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) + if hasattr(Qt, "AA_UseHighDpiPixmaps"): + QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) + + # Show the splash screen immediately + splash = SplashScreen() + splash.show() + app.processEvents() # This ensures the splash screen is displayed immediately + if len(sys.argv) > 1 and sys.argv[1] == "test": success, message = api_utils.check_api_connection() if success: @@ -38,14 +55,12 @@ def main(): else: print("Connection failed.") print("Error:", message) + sys.exit(0) # Exit after test else: - # Show the splash screen - splash = SplashScreen() - splash.show() - # Function to update progress def update_progress(value, message): splash.update_progress(value, message) + app.processEvents() # Ensure UI updates # Create ChatHandler (this might take some time) update_progress(10, "Initializing ChatHandler...") diff --git a/codeaide/__main__streamlined.py b/codeaide/__main__streamlined.py new file mode 100644 index 0000000..e07b125 --- /dev/null +++ b/codeaide/__main__streamlined.py @@ -0,0 +1,62 @@ +import sys +import os +import time + + +def log(message): + log_path = os.path.expanduser("~/Desktop/codeaide_startup_log.txt") + with open(log_path, "a") as f: + f.write(f"{time.time()}: {message}\n") + + +log(f"Script started. Python version: {sys.version}") +log(f"Executable path: {sys.executable}") +log(f"Current working directory: {os.getcwd()}") + + +def main(): + log("Main function started") + from PyQt5.QtWidgets import ( + QApplication, + QMainWindow, + QLineEdit, + QVBoxLayout, + QWidget, + ) + + log("Modules imported") + + app = QApplication(sys.argv) + log("QApplication created") + + class SimpleApp(QMainWindow): + def __init__(self): + super().__init__() + log("SimpleApp init started") + self.initUI() + log("SimpleApp init completed") + + def initUI(self): + central_widget = QWidget() + self.setCentralWidget(central_widget) + layout = QVBoxLayout(central_widget) + + self.text_entry = QLineEdit(self) + layout.addWidget(self.text_entry) + + self.setGeometry(300, 300, 300, 200) + self.setWindowTitle("Simple CodeAide") + + ex = SimpleApp() + log("SimpleApp instance created") + ex.show() + log("SimpleApp shown") + sys.exit(app.exec_()) + + +if __name__ == "__main__": + try: + main() + except Exception as e: + log(f"Error in main: {str(e)}") + raise diff --git a/codeaide/ui/chat_window.py b/codeaide/ui/chat_window.py index 0d41afb..495daf2 100644 --- a/codeaide/ui/chat_window.py +++ b/codeaide/ui/chat_window.py @@ -46,13 +46,23 @@ import sounddevice as sd import numpy as np from scipy.io import wavfile -import whisper import tempfile from codeaide.utils.general_utils import get_resource_path import os import traceback import subprocess from codeaide.utils.general_utils import get_most_recent_log_file +import logging + +try: + import whisper + + WHISPER_AVAILABLE = True +except ImportError: + WHISPER_AVAILABLE = False + print( + "Whisper module not available. Speech-to-text functionality will be disabled." + ) class AudioRecorder(QThread): @@ -728,27 +738,43 @@ def on_recording_finished(self, filename, recording_duration): f"Total time from recording stop to transcription complete: {transcription_end - transcription_start:.2f} seconds" ) - def transcribe_audio(self, filename): - self.logger.info("Transcribing audio") - progress_dialog = QProgressDialog("Transcribing audio...", None, 0, 0, self) - progress_dialog.setWindowTitle("Please Wait") - progress_dialog.setWindowModality(Qt.WindowModal) - progress_dialog.setAutoClose(True) - progress_dialog.setAutoReset(True) - progress_dialog.setMinimumDuration(0) - progress_dialog.setValue(0) - progress_dialog.setMaximum(0) # This makes it an indeterminate progress dialog - progress_dialog.show() - - self.transcription_thread = TranscriptionThread( - self.whisper_model, filename, self.logger - ) - self.transcription_thread.finished.connect(self.on_transcription_finished) - self.transcription_thread.error.connect(self.on_transcription_error) - self.transcription_thread.finished.connect(progress_dialog.close) - self.transcription_thread.error.connect(progress_dialog.close) - self.transcription_thread.start() - self.logger.info("Transcription thread started") + def transcribe_audio(self, audio_file): + try: + logging.info(f"Whisper package location: {whisper.__file__}") + logging.info(f"Current working directory: {os.getcwd()}") + logging.info(f"Python path: {sys.path}") + logging.info(f"Contents of current directory: {os.listdir('.')}") + whisper_dir = os.path.dirname(whisper.__file__) + logging.info(f"Contents of whisper directory: {os.listdir(whisper_dir)}") + logging.info( + f"Contents of whisper assets directory: {os.listdir(os.path.join(whisper_dir, 'assets'))}" + ) + + self.logger.info("Transcribing audio") + progress_dialog = QProgressDialog("Transcribing audio...", None, 0, 0, self) + progress_dialog.setWindowTitle("Please Wait") + progress_dialog.setWindowModality(Qt.WindowModal) + progress_dialog.setAutoClose(True) + progress_dialog.setAutoReset(True) + progress_dialog.setMinimumDuration(0) + progress_dialog.setValue(0) + progress_dialog.setMaximum( + 0 + ) # This makes it an indeterminate progress dialog + progress_dialog.show() + + self.transcription_thread = TranscriptionThread( + self.whisper_model, audio_file, self.logger + ) + self.transcription_thread.finished.connect(self.on_transcription_finished) + self.transcription_thread.error.connect(self.on_transcription_error) + self.transcription_thread.finished.connect(progress_dialog.close) + self.transcription_thread.error.connect(progress_dialog.close) + self.transcription_thread.start() + self.logger.info("Transcription thread started") + except Exception as e: + self.logger.error(f"Error in transcription: {str(e)}") + self.logger.error(traceback.format_exc()) def on_transcription_finished(self, transcribed_text): self.logger.info("on_transcription_finished method called") @@ -803,6 +829,10 @@ def scroll_to_bottom(self): scrollbar.setValue(scrollbar.maximum()) def load_whisper_model(self): + if not WHISPER_AVAILABLE: + self.logger.warning("Whisper is not available. Speech-to-text is disabled.") + return + try: self.logger.info("Loading Whisper model...") model_path = get_resource_path("models/whisper") diff --git a/codeaide/utils/terminal_manager.py b/codeaide/utils/terminal_manager.py index 07cc036..2712987 100644 --- a/codeaide/utils/terminal_manager.py +++ b/codeaide/utils/terminal_manager.py @@ -148,9 +148,7 @@ def process_line(self, line): def show_traceback_if_any(self): if self.traceback_buffer: traceback_text = "\n".join(self.traceback_buffer) - self.logger.info( - f"ScriptRunner: Traceback detected: {traceback_text[:50]}..." - ) + self.logger.info(f"ScriptRunner: Traceback detected: {traceback_text}...") if self.traceback_callback: self.logger.info("ScriptRunner: Calling traceback callback") self.traceback_callback(traceback_text) @@ -207,7 +205,7 @@ def _create_script_content(self, script_path, activation_command, new_packages): script_content = f""" clear # Clear the terminal - echo "Activating environment..." + echo "Activating environment with {activation_command}" {activation_command} """ diff --git a/requirements.txt b/requirements.txt index cd96d20..9cbf83e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,3 +20,4 @@ autoflake openai-whisper sounddevice scipy +PyInstaller