diff --git a/.gitignore b/.gitignore index 0e92774..a5adf8f 100644 --- a/.gitignore +++ b/.gitignore @@ -28,17 +28,17 @@ CodeAide.app/ # Ignore CodeAide.dmg file CodeAide.dmg -# Ignore build directory +# Build artifacts build/ - -# Ignore codeaide.spec file -codeaide.spec - -# Ignore dist directory dist/ +*.spec +!build_scripts/codeaide.spec # Ignore .dmg files *.dmg # Ignore .egg-info directories *.egg-info/ + +# Ignore the models folder +models/ diff --git a/BUILD.md b/BUILD.md index 6d61a3e..3d4ed07 100644 --- a/BUILD.md +++ b/BUILD.md @@ -6,113 +6,74 @@ CodeAide is a chat application that leverages LLMs to generate code based on use Follow these steps to build CodeAide as a standalone application for macOS: -Prerequisites +### Prerequisites -- Python 3.7 or higher -- pip (Python package installer) +- Python 3.11 or higher +- Conda (for managing the Python environment) - Homebrew (for installing create-dmg) -### Step 1: Install Required Python Packages - -``` -pip install PyQt5 pyinstaller -``` - -### Step 2: Package the Application - -1. Navigate to your project directory: - ``` - cd path/to/CodeAIde - ``` - -2. Run PyInstaller: - ``` - pyinstaller --windowed --onefile --add-data "codeaide/examples.yaml:codeaide" codeaide.py - ``` - This command creates a single executable file in the `dist` folder. - - Note: Make sure the path to examples.yaml is correct. If you're in the root of your project, it should be "codeaide/examples.yaml" as shown above. - -### Step 3: Create an Application Bundle - -1. Create the necessary directories: - ``` - mkdir -p CodeAide.app/Contents/MacOS - mkdir -p CodeAide.app/Contents/Resources - ``` - -2. Move your executable: - ``` - mv dist/codeaide CodeAide.app/Contents/MacOS/CodeAide - ``` - -3. Create an `Info.plist` file in `CodeAide.app/Contents/`: - ``` - - - - - CFBundleExecutable - CodeAide - CFBundleIconFile - icon.icns - CFBundleIdentifier - com.yourcompany.codeaide - CFBundleName - CodeAide - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0.0 - - +### Step 1: Set Up the Environment + +1. Create and activate a new conda environment: + ``` + conda create -n codeaide python=3.11 + conda activate codeaide + ``` + +2. Ensure you're in the project root directory. + +### Step 2: Run the Build Script + +1. Make the build script executable: + ``` + chmod +x build_codeaide.sh + ``` + +2. Run the build script: + + For local testing (without notarization): + ``` + ./build_codeaide.sh + ``` + + For building a distributable version (with notarization): + ``` + ./build_codeaide.sh --notarize ``` -4. (Optional) Add an icon: - - Create a .icns file for your app icon - - Place it in `CodeAide.app/Contents/Resources/icon.icns` - -### Step 4: Create the DMG - -1. Install create-dmg: - ``` - brew install create-dmg - ``` - -2. Create the DMG: - ``` - create-dmg \ - --volname "CodeAide Installer" \ - --window-pos 200 120 \ - --window-size 800 400 \ - --icon-size 100 \ - --icon "CodeAide.app" 200 190 \ - --hide-extension "CodeAide.app" \ - --app-drop-link 600 185 \ - "CodeAide.dmg" \ - "CodeAide.app" - ``` - -### Step 5: Test The Application + If you choose to notarize, you will be prompted to enter: + - Your Developer ID Application certificate name + - Your Apple Developer Team ID + - Your Apple ID + - Your app-specific password + + Make sure you have these details ready before running the script with the --notarize option. + +### Step 3: Test The Application Always test the DMG on a clean macOS installation to ensure it works as expected. -Troubleshooting +## Troubleshooting -If you encounter issues with resource files not being found: +If you encounter issues: -1. Ensure all necessary files (like `examples.yaml`) are included in the PyInstaller command with the correct path. -2. Check the console output for any error messages. -3. Verify that the paths in `general_utils.py` are correct for both development and packaged environments. +1. Check the console output for any error messages. +2. Ensure all necessary files are included in the `codeaide.spec` file. +3. Verify that the paths in your code are correct for both development and packaged environments. +4. If you're having issues with PyInstaller, try updating it to the latest version: + ``` + pip install --upgrade pyinstaller + ``` -Updating the Application +## Updating the Application When updating the application: -1. Increment the version number in `Info.plist`. -2. Rebuild the application following the steps above. -3. Create a new DMG with the updated version. +1. Update the version number in the `build_codeaide.sh` script (look for `CFBundleShortVersionString`). +2. Rebuild the application by running the build script. -Notes +## Notes -- This README assumes you're building on macOS. The process may differ for other operating systems. \ No newline at end of file +- This build process is designed for macOS. The process may differ for other operating systems. +- Ensure you're in the correct conda environment (`codeaide`) when running these commands. +- The `codeaide.spec` file is crucial for correct packaging. If you make changes to your project structure or dependencies, update the spec file accordingly. 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 new file mode 100755 index 0000000..8950476 --- /dev/null +++ b/build_scripts/build_codeaide.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# Exit on any error +set -e + +# Activate the correct conda environment +eval "$(conda shell.bash hook)" +conda activate codeaide + +# Get the path to the Python interpreter in the conda environment +PYTHON_PATH=$(which python) + +# Change directory to the project root +cd "$(dirname "$0")/.." + +# Ensure log directory exists +echo "Ensuring log directory exists..." +mkdir -p ~/Library/Logs/CodeAide + +# Ensure application data directory exists +echo "Ensuring application data directory exists..." +mkdir -p ~/Library/Application\ Support/CodeAide + +# Install required packages +echo "Installing required Python packages..." +$PYTHON_PATH -m pip install --upgrade pip +$PYTHON_PATH -m pip install -r requirements.txt + +# Check if Whisper is installed +echo "Checking Whisper installation..." +$PYTHON_PATH -c "import whisper; print('Whisper version:', whisper.__version__)" + +# Package the application +echo "Packaging the application..." +yes | $PYTHON_PATH -m PyInstaller build_scripts/codeaide.spec + +# Create the DMG +echo "Creating DMG..." +APP_NAME="CodeAide" +DMG_NAME="${APP_NAME}.dmg" +SOURCE_DIR="dist/${APP_NAME}.app" +FINAL_DMG="${DMG_NAME}" + +# 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 +hdiutil create -volname "${APP_NAME}" -srcfolder "${TEMP_DIR}" -ov -format UDZO "${FINAL_DMG}" + +# Clean up the temporary directory +rm -rf "${TEMP_DIR}" + +# Verify that the DMG was created +if [ -f "${FINAL_DMG}" ]; then + echo "DMG created successfully: ${FINAL_DMG}" +else + echo "Error: DMG creation failed" + exit 1 +fi + +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 new file mode 100644 index 0000000..fb46ef5 --- /dev/null +++ b/build_scripts/codeaide.spec @@ -0,0 +1,107 @@ +# -*- 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 = 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/__main__.py'], + pathex=[], + binaries=[], + 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=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='CodeAide', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, + disable_windowed_traceback=False, + argv_emulation=True, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) + +coll = COLLECT( + exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='CodeAide', +) + +app = BUNDLE( + coll, + name='CodeAide.app', + icon=None, + bundle_identifier=None, +) diff --git a/build_scripts/entitlements.plist b/build_scripts/entitlements.plist new file mode 100644 index 0000000..d459cb2 --- /dev/null +++ b/build_scripts/entitlements.plist @@ -0,0 +1,8 @@ + + + + + com.apple.security.device.audio-input + + + diff --git a/codeaide/__main__.py b/codeaide/__main__.py index fb83584..14cbc23 100644 --- a/codeaide/__main__.py +++ b/codeaide/__main__.py @@ -1,13 +1,51 @@ import sys - -from PyQt5.QtWidgets import QApplication - +from PyQt5.QtWidgets import QApplication, QMessageBox +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 + + +def exception_hook(exctype, value, tb): + error_msg = "".join(traceback.format_exception(exctype, value, tb)) + print(error_msg, file=sys.stderr) + sys.stderr.flush() + msg_box = QMessageBox() + msg_box.setIcon(QMessageBox.Critical) + msg_box.setText("An unexpected error occurred.") + msg_box.setInformativeText(str(value)) + msg_box.setDetailedText(error_msg) + msg_box.setWindowTitle("Error") + msg_box.exec_() def main(): - chat_handler = ChatHandler() + # 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() @@ -17,9 +55,29 @@ def main(): else: print("Connection failed.") print("Error:", message) + sys.exit(0) # Exit after test else: - app = QApplication(sys.argv) - chat_handler.start_application() + # 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...") + chat_handler = ChatHandler() + + # Create main window + update_progress(50, "Creating main window...") + main_window = ChatWindow(chat_handler) + + # Function to finish startup + def finish_startup(): + main_window.show() + splash.finish(main_window) + + # Use a timer to allow the splash screen to update + QTimer.singleShot(100, finish_startup) + sys.exit(app.exec_()) 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/logic/chat_handler.py b/codeaide/logic/chat_handler.py index 05beafd..03ab628 100644 --- a/codeaide/logic/chat_handler.py +++ b/codeaide/logic/chat_handler.py @@ -21,6 +21,7 @@ from codeaide.utils.general_utils import generate_session_id from codeaide.utils.logging_config import get_logger, setup_logger from PyQt5.QtCore import QObject, pyqtSignal +from codeaide.ui.chat_window import ChatWindow class ChatHandler(QObject): @@ -69,14 +70,9 @@ def __init__(self): self.logger.info(f"Session directory: {self.session_dir}") self.chat_window = None - def start_application(self): - from codeaide.ui.chat_window import ( - ChatWindow, - ) # Import here to avoid circular imports - + def create_chat_window(self): self.chat_window = ChatWindow(self) - self.connect_signals() - self.chat_window.show() + return self.chat_window def connect_signals(self): self.update_chat_signal.connect(self.chat_window.add_to_chat) diff --git a/codeaide/ui/chat_window.py b/codeaide/ui/chat_window.py index 97d9776..495daf2 100644 --- a/codeaide/ui/chat_window.py +++ b/codeaide/ui/chat_window.py @@ -1,4 +1,5 @@ import signal +import sys # Add this import from PyQt5.QtCore import ( Qt, QTimer, @@ -19,6 +20,7 @@ QComboBox, QLabel, QProgressDialog, + QAction, ) from codeaide.ui.code_popup import CodePopup from codeaide.ui.example_selection_dialog import show_example_dialog @@ -44,8 +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): @@ -62,32 +79,50 @@ def run(self): RATE = 16000 # 16kHz to match Whisper's expected input self.is_recording = True self.start_time = time.time() - with sd.InputStream(samplerate=RATE, channels=1) as stream: - frames = [] - while self.is_recording: - data, overflowed = stream.read(RATE) - if not overflowed: + self.logger.info(f"Starting audio recording with rate: {RATE}") + + try: + with sd.InputStream(samplerate=RATE, channels=1) as stream: + self.logger.info("Audio stream opened successfully") + frames = [] + while self.is_recording: + data, overflowed = stream.read(RATE) + if overflowed: + self.logger.warning("Audio buffer overflowed") frames.append(data) + self.logger.debug(f"Recorded frame with shape: {data.shape}") - audio_data = np.concatenate(frames, axis=0) - self.logger.info(f"Raw audio data shape: {audio_data.shape}") - self.logger.info( - f"Raw audio data range: {audio_data.min()} to {audio_data.max()}" - ) + self.logger.info(f"Recording stopped. Total frames: {len(frames)}") + audio_data = np.concatenate(frames, axis=0) + self.logger.info(f"Raw audio data shape: {audio_data.shape}") + self.logger.info( + f"Raw audio data range: {audio_data.min()} to {audio_data.max()}" + ) + self.logger.info(f"Raw audio data mean: {audio_data.mean()}") - # Ensure audio data is in the correct range for int16 - audio_data = np.clip(audio_data * 32768, -32768, 32767).astype(np.int16) + # Ensure audio data is in the correct range for int16 + audio_data = np.clip(audio_data * 32768, -32768, 32767).astype(np.int16) + self.logger.info( + f"Processed audio data range: {audio_data.min()} to {audio_data.max()}" + ) - wavfile.write(self.filename, RATE, audio_data) - end_time = time.time() - self.finished.emit(self.filename, end_time - self.start_time) + wavfile.write(self.filename, RATE, audio_data) + self.logger.info(f"Audio file written to: {self.filename}") + + end_time = time.time() + self.finished.emit(self.filename, end_time - self.start_time) + except Exception as e: + self.logger.error(f"Error during audio recording: {str(e)}") + self.logger.error(traceback.format_exc()) def stop(self): self.is_recording = False + self.logger.info("Stop recording requested") class TranscriptionThread(QThread): finished = pyqtSignal(str) + error = pyqtSignal(str) def __init__(self, whisper_model, filename, logger): super().__init__() @@ -96,33 +131,45 @@ def __init__(self, whisper_model, filename, logger): self.logger = logger def run(self): - self.logger.info("Transcribing audio...") - read_start = time.time() - # Read the WAV file - sample_rate, audio_data = wavfile.read(self.filename) - read_end = time.time() - self.logger.info(f"Time to read WAV file: {read_end - read_start:.2f} seconds") - - self.logger.info(f"Audio shape: {audio_data.shape}, Sample rate: {sample_rate}") - self.logger.info(f"Audio duration: {len(audio_data) / sample_rate:.2f} seconds") - - # Convert to float32 and normalize - audio_data = audio_data.astype(np.float32) / 32768.0 - - self.logger.info(f"Audio data range: {audio_data.min()} to {audio_data.max()}") - - # Transcribe - transcribe_start = time.time() - result = self.whisper_model.transcribe(audio_data) - transcribe_end = time.time() - transcribed_text = result["text"].strip() - self.logger.info(f"Transcription: {transcribed_text}") - self.logger.info( - f"Time for Whisper to transcribe: {transcribe_end - transcribe_start:.2f} seconds" - ) - self.logger.info("Transcription complete.") - self.logger.info(f"Emitting finished signal with text: {transcribed_text}") - self.finished.emit(transcribed_text) + try: + self.logger.info("Starting transcription") + read_start = time.time() + # Read the WAV file + sample_rate, audio_data = wavfile.read(self.filename) + read_end = time.time() + self.logger.info( + f"Time to read WAV file: {read_end - read_start:.2f} seconds" + ) + + self.logger.info( + f"Audio shape: {audio_data.shape}, Sample rate: {sample_rate}" + ) + self.logger.info( + f"Audio duration: {len(audio_data) / sample_rate:.2f} seconds" + ) + + # Convert to float32 and normalize + audio_data = audio_data.astype(np.float32) / 32768.0 + + self.logger.info( + f"Audio data range: {audio_data.min()} to {audio_data.max()}" + ) + + # Transcribe + transcribe_start = time.time() + result = self.whisper_model.transcribe(audio_data) + transcribe_end = time.time() + self.logger.info( + f"Time to transcribe: {transcribe_end - transcribe_start:.2f} seconds" + ) + + transcribed_text = result["text"] + self.logger.info(f"Transcribed text: {transcribed_text}") + self.finished.emit(transcribed_text) + except Exception as e: + self.logger.error(f"Error in transcription: {str(e)}") + self.logger.error(traceback.format_exc()) + self.error.emit(str(e)) class ChatWindow(QMainWindow): @@ -139,21 +186,15 @@ def __init__(self, chat_handler): self.is_recording = False # Load microphone icons - self.green_mic_icon = QIcon( - general_utils.get_resource_path("codeaide/assets/green_mic.png") - ) - self.red_mic_icon = QIcon( - general_utils.get_resource_path("codeaide/assets/red_mic.png") - ) + self.green_mic_icon = QIcon(get_resource_path("codeaide/assets/green_mic.png")) + self.red_mic_icon = QIcon(get_resource_path("codeaide/assets/red_mic.png")) self.setup_ui() self.setup_input_placeholder() self.update_submit_button_state() # Initialize Whisper model - print("Loading Whisper model...") - self.whisper_model = whisper.load_model("tiny") - print("Whisper model loaded.") + QTimer.singleShot(100, self.load_whisper_model) # Check API key status if not self.chat_handler.api_key_valid: @@ -171,6 +212,15 @@ def __init__(self, chat_handler): self.logger.info("Chat window initialized") + # Create menu bar + menubar = self.menuBar() + file_menu = menubar.addMenu("File") + + # Add "Show Logs" to the File menu + show_logs_action = QAction("Show Logs", self) + show_logs_action.triggered.connect(self.show_logs) + file_menu.addAction(show_logs_action) + def setup_ui(self): central_widget = QWidget(self) self.setCentralWidget(central_widget) @@ -688,34 +738,50 @@ 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("transcribe_audio method called") - 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.finished.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") self.logger.info(f"Transcribed text: {transcribed_text}") self.logger.info(f"Original HTML: {self.original_html}") - transcribed_text = ( - transcribed_text.strip() - ) # Remove any leading/trailing whitespace + transcribed_text = transcribed_text.strip() if not self.original_html.strip(): self.logger.info("No original text, setting transcribed text directly") @@ -744,6 +810,14 @@ def on_transcription_finished(self, transcribed_text): # Clear the original HTML self.original_html = "" + def on_transcription_error(self, error_message): + self.logger.error(f"Transcription error: {error_message}") + QMessageBox.critical( + self, + "Transcription Error", + f"An error occurred during transcription: {error_message}", + ) + def scroll_to_bottom(self): # Move cursor to the end of the text cursor = self.input_text.textCursor() @@ -753,3 +827,54 @@ def scroll_to_bottom(self): # Scroll to the bottom scrollbar = self.input_text.verticalScrollBar() 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") + model_file = os.path.join(model_path, "tiny.pt") + + if not os.path.exists(model_file): + self.logger.warning( + f"Whisper model not found at {model_file}. Attempting to download..." + ) + whisper.load_model("tiny", download_root=model_path) + + self.whisper_model = whisper.load_model("tiny", download_root=model_path) + self.logger.info("Whisper model loaded successfully.") + except Exception as e: + self.logger.error(f"Error loading Whisper model: {str(e)}") + self.logger.error(traceback.format_exc()) + self.show_error_message( + f"Failed to load Whisper model. Speech-to-text may not work. Error: {str(e)}" + ) + + def show_error_message(self, message): + QMessageBox.critical(self, "Error", message) + + def show_logs(self): + try: + if getattr(sys, "frozen", False): + # We are running in a bundle + log_file = get_most_recent_log_file() + else: + # We are running in a normal Python environment + log_file = os.path.join(self.chat_handler.session_dir, "codeaide.log") + + if log_file and os.path.exists(log_file): + self.logger.info(f"Opening log file: {log_file}") + subprocess.run(["open", log_file]) + else: + self.logger.warning("No log file found") + QMessageBox.information(self, "Logs Not Found", "No log file found.") + except Exception as e: + self.logger.error(f"Error opening log file: {str(e)}") + QMessageBox.critical( + self, + "Error", + f"An error occurred while trying to open the log file: {str(e)}", + ) diff --git a/codeaide/ui/splash_screen.py b/codeaide/ui/splash_screen.py new file mode 100644 index 0000000..9144806 --- /dev/null +++ b/codeaide/ui/splash_screen.py @@ -0,0 +1,29 @@ +from PyQt5.QtWidgets import QSplashScreen, QProgressBar +from PyQt5.QtGui import QPixmap +from PyQt5.QtCore import Qt + + +class SplashScreen(QSplashScreen): + def __init__(self): + super().__init__() + + # Create a pixmap for the splash screen (you can replace this with your own image) + pixmap = QPixmap(400, 200) + pixmap.fill(Qt.white) + self.setPixmap(pixmap) + + # Add a progress bar + self.progress_bar = QProgressBar(self) + self.progress_bar.setGeometry(10, 150, 380, 20) + self.progress_bar.setRange(0, 100) + self.progress_bar.setValue(0) + + # Add some text + self.setStyleSheet("QSplashScreen { color: black; font-size: 14px; }") + self.showMessage( + "Loading CodeAIde...", Qt.AlignCenter | Qt.AlignBottom, Qt.black + ) + + def update_progress(self, value, message): + self.progress_bar.setValue(value) + self.showMessage(message, Qt.AlignCenter | Qt.AlignBottom, Qt.black) diff --git a/codeaide/utils/api_utils.py b/codeaide/utils/api_utils.py index 0a53e99..8bf29f3 100644 --- a/codeaide/utils/api_utils.py +++ b/codeaide/utils/api_utils.py @@ -1,8 +1,6 @@ -import os import anthropic import openai import google.generativeai as genai -from decouple import AutoConfig import hjson import re from google.generativeai.types import GenerationConfig @@ -14,8 +12,10 @@ SYSTEM_PROMPT, ) from codeaide.utils.logging_config import get_logger +from codeaide.utils.config_manager import ConfigManager logger = get_logger() +config_manager = ConfigManager() class MissingAPIKeyException(Exception): @@ -28,18 +28,8 @@ def __init__(self, service): def get_api_client(provider=DEFAULT_PROVIDER, model=None): try: - root_dir = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - ) - - # Use AutoConfig to automatically find and load the .env file in the project root - config = AutoConfig(search_path=root_dir) - - api_key_name = AI_PROVIDERS[provider]["api_key_name"] - api_key = config(api_key_name, default=None) - logger.info( - f"Attempting to get API key for {provider} with key name: {api_key_name}" - ) + api_key = config_manager.get_api_key(provider) + logger.info(f"Attempting to get API key for {provider}") logger.info(f"API key found: {'Yes' if api_key else 'No'}") if api_key is None or api_key.strip() == "": @@ -64,30 +54,7 @@ def get_api_client(provider=DEFAULT_PROVIDER, model=None): def save_api_key(service, api_key): try: cleaned_key = api_key.strip().strip("'\"") # Remove quotes and whitespace - root_dir = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - ) - env_path = os.path.join(root_dir, ".env") - - if os.path.exists(env_path): - with open(env_path, "r") as file: - lines = file.readlines() - - key_exists = False - for i, line in enumerate(lines): - if line.startswith(f"{service.upper()}_API_KEY="): - lines[i] = f'{service.upper()}_API_KEY="{cleaned_key}"\n' - key_exists = True - break - - if not key_exists: - lines.append(f'{service.upper()}_API_KEY="{cleaned_key}"\n') - else: - lines = [f'{service.upper()}_API_KEY="{cleaned_key}"\n'] - - with open(env_path, "w") as file: - file.writelines(lines) - + config_manager.set_api_key(service, cleaned_key) return True except Exception as e: logger.error(f"Error saving API key: {str(e)}") diff --git a/codeaide/utils/config_manager.py b/codeaide/utils/config_manager.py new file mode 100644 index 0000000..c1b138d --- /dev/null +++ b/codeaide/utils/config_manager.py @@ -0,0 +1,63 @@ +import os +import platform +import sys +from pathlib import Path +from decouple import Config, RepositoryEnv + + +class ConfigManager: + def __init__(self): + self.is_packaged_app = getattr(sys, "frozen", False) + if self.is_packaged_app: + self.config_dir = self._get_app_config_dir() + self.keyring_service = "CodeAIde" + else: + self.config_dir = Path(__file__).parent.parent.parent + self.env_file = self.config_dir / ".env" + + def _get_app_config_dir(self): + system = platform.system() + if system == "Darwin": # macOS + return Path.home() / "Library" / "Application Support" / "CodeAIde" + elif system == "Windows": + return Path(os.getenv("APPDATA")) / "CodeAIde" + else: # Linux and others + return Path.home() / ".config" / "codeaide" + + def get_api_key(self, provider): + if self.is_packaged_app: + import keyring + + return keyring.get_password( + self.keyring_service, f"{provider.upper()}_API_KEY" + ) + else: + config = Config(RepositoryEnv(self.env_file)) + return config(f"{provider.upper()}_API_KEY", default=None) + + def set_api_key(self, provider, api_key): + if self.is_packaged_app: + import keyring + + keyring.set_password( + self.keyring_service, f"{provider.upper()}_API_KEY", api_key + ) + else: + with open(self.env_file, "a") as f: + f.write(f'\n{provider.upper()}_API_KEY="{api_key}"\n') + + def delete_api_key(self, provider): + if self.is_packaged_app: + import keyring + + keyring.delete_password(self.keyring_service, f"{provider.upper()}_API_KEY") + else: + # Read the .env file, remove the line with the API key, and write it back + if self.env_file.exists(): + lines = self.env_file.read_text().splitlines() + lines = [ + line + for line in lines + if not line.startswith(f"{provider.upper()}_API_KEY=") + ] + self.env_file.write_text("\n".join(lines) + "\n") diff --git a/codeaide/utils/general_utils.py b/codeaide/utils/general_utils.py index cb2e007..57a106e 100644 --- a/codeaide/utils/general_utils.py +++ b/codeaide/utils/general_utils.py @@ -4,6 +4,7 @@ from datetime import datetime from codeaide.utils.logging_config import get_logger import sys +from pathlib import Path logger = get_logger() @@ -121,3 +122,15 @@ def generate_session_id(): session_id = datetime.now().strftime("%Y%m%d_%H%M%S") logger.info(f"Generated new session ID: {session_id}") return session_id + + +def get_most_recent_log_file(): + app_data_dir = Path.home() / "Library" / "Application Support" / "CodeAide" + if not app_data_dir.exists(): + return None + + log_files = list(app_data_dir.rglob("codeaide*.log")) + if not log_files: + return None + + return str(max(log_files, key=os.path.getmtime)) diff --git a/codeaide/utils/logging_config.py b/codeaide/utils/logging_config.py index 4b77956..e48c2ad 100644 --- a/codeaide/utils/logging_config.py +++ b/codeaide/utils/logging_config.py @@ -1,14 +1,19 @@ import logging from logging.handlers import RotatingFileHandler import os +import sys def setup_logger(session_dir, level=logging.INFO): - log_dir = os.path.join(session_dir) - os.makedirs(log_dir, exist_ok=True) - - # Set up the log file path - log_file = os.path.join(log_dir, "codeaide.log") + if getattr(sys, "frozen", False): + # We are running in a bundle + log_dir = os.path.expanduser("~/Library/Application Support/CodeAide") + os.makedirs(log_dir, exist_ok=True) + log_file = os.path.join(log_dir, f"codeaide_{os.getpid()}.log") + else: + # We are running in a normal Python environment + log_dir = session_dir + log_file = os.path.join(log_dir, "codeaide.log") # Create a formatter formatter = logging.Formatter( 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 0afdf9b..9cbf83e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ google-generativeai==0.8.3 python-decouple==3.8 virtualenv==20.16.2 numpy==1.26.4 -numpy==1.26.4 +keyring openai hjson pyyaml @@ -20,4 +20,4 @@ autoflake openai-whisper sounddevice scipy -ffmpeg-python +PyInstaller