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