A few fixes to ai.py.

This commit is contained in:
Storm Dragon
2025-12-01 02:36:07 -05:00
parent 63be4fc9e7
commit 0e9bc8ae09
+89 -12
View File
@@ -20,6 +20,47 @@ import time
import pyaudio import pyaudio
import wave import wave
class SystemCommands:
"""Check availability of required system commands"""
@staticmethod
def is_command_available(command):
"""Check if a command is available in PATH"""
try:
result = subprocess.run(['which', command],
capture_output=True, text=True, timeout=2)
return result.returncode == 0
except (subprocess.SubprocessError, FileNotFoundError, OSError) as e:
return False
@staticmethod
def check_dependencies():
"""Check for required system commands and return missing ones"""
required_commands = {
'scrot': 'Required for screenshots',
'play': 'Required for audio feedback (sox package)',
'spd-say': 'Required for text-to-speech output',
}
optional_commands = {
'xclip': 'Required for clipboard on X11',
'wl-paste': 'Required for clipboard on Wayland',
'tesseract': 'Required for OCR functionality',
}
missing_required = {}
missing_optional = {}
for cmd, desc in required_commands.items():
if not SystemCommands.is_command_available(cmd):
missing_required[cmd] = desc
for cmd, desc in optional_commands.items():
if not SystemCommands.is_command_available(cmd):
missing_optional[cmd] = desc
return missing_required, missing_optional
class VoiceRecognition: class VoiceRecognition:
"""Voice recognition system for AI assistant""" """Voice recognition system for AI assistant"""
@@ -233,7 +274,7 @@ class OllamaInterface:
try: try:
response = requests.get(f'{self.host}/api/tags', timeout=3) response = requests.get(f'{self.host}/api/tags', timeout=3)
return response.status_code == 200 return response.status_code == 200
except: except (requests.RequestException, ConnectionError, OSError) as e:
return False return False
def send_message(self, message, model, context=None, image_path=None): def send_message(self, message, model, context=None, image_path=None):
@@ -282,7 +323,7 @@ class ClaudeCodeInterface:
result = subprocess.run(['claude', '--version'], result = subprocess.run(['claude', '--version'],
capture_output=True, text=True, timeout=5) capture_output=True, text=True, timeout=5)
return result.returncode == 0 return result.returncode == 0
except: except (subprocess.SubprocessError, FileNotFoundError, OSError) as e:
return False return False
def send_message(self, message, context=None, image_path=None): def send_message(self, message, context=None, image_path=None):
@@ -319,7 +360,7 @@ class WindowContext:
def __init__(self): def __init__(self):
try: try:
self.i3 = i3ipc.Connection() self.i3 = i3ipc.Connection()
except: except (ConnectionError, FileNotFoundError, Exception) as e:
self.i3 = None self.i3 = None
def get_focused_window_info(self): def get_focused_window_info(self):
@@ -1047,7 +1088,8 @@ class AiAssistant(Gtk.Window):
self.contextButton.set_sensitive(False) self.contextButton.set_sensitive(False)
self.actionButton.set_sensitive(False) self.actionButton.set_sensitive(False)
# Play processing sound # Play processing sound if available
if SystemCommands.is_command_available('play'):
subprocess.run(['play', '-qnG', 'synth', '0.1', 'sin', '800'], subprocess.run(['play', '-qnG', 'synth', '0.1', 'sin', '800'],
capture_output=True) capture_output=True)
@@ -1057,7 +1099,8 @@ class AiAssistant(Gtk.Window):
self.contextButton.set_sensitive(True) self.contextButton.set_sensitive(True)
self.actionButton.set_sensitive(True) self.actionButton.set_sensitive(True)
# Play completion sound # Play completion sound if available
if SystemCommands.is_command_available('play'):
subprocess.run(['play', '-qnG', 'synth', '0.05', 'sin', '1200'], subprocess.run(['play', '-qnG', 'synth', '0.05', 'sin', '1200'],
capture_output=True) capture_output=True)
@@ -1238,6 +1281,11 @@ class AiAssistant(Gtk.Window):
def on_describe_image(self, widget): def on_describe_image(self, widget):
"""Handle describe screenshot button click""" """Handle describe screenshot button click"""
def describe_image_in_thread(): def describe_image_in_thread():
# Check if scrot is available
if not SystemCommands.is_command_available('scrot'):
GLib.idle_add(self.set_response_text, "Error: scrot not available. Please install scrot for screenshots.")
return
# Take screenshot # Take screenshot
temp_dir = tempfile.mkdtemp() temp_dir = tempfile.mkdtemp()
screenshot_path = os.path.join(temp_dir, 'screenshot.png') screenshot_path = os.path.join(temp_dir, 'screenshot.png')
@@ -1267,7 +1315,7 @@ class AiAssistant(Gtk.Window):
try: try:
os.unlink(screenshot_path) os.unlink(screenshot_path)
os.rmdir(temp_dir) os.rmdir(temp_dir)
except: except (FileNotFoundError, OSError) as e:
pass pass
threading.Thread(target=describe_image_in_thread, daemon=True).start() threading.Thread(target=describe_image_in_thread, daemon=True).start()
@@ -1278,13 +1326,16 @@ class AiAssistant(Gtk.Window):
try: try:
# First, try to get clipboard content (selected text) # First, try to get clipboard content (selected text)
# Use wl-paste on Wayland, xclip on X11 # Use wl-paste on Wayland, xclip on X11
selected_text = ""
if os.environ.get('WAYLAND_DISPLAY'): if os.environ.get('WAYLAND_DISPLAY'):
if SystemCommands.is_command_available('wl-paste'):
clipboard_result = subprocess.run(['wl-paste', '-p'], clipboard_result = subprocess.run(['wl-paste', '-p'],
capture_output=True, text=True, timeout=5) capture_output=True, text=True, timeout=5)
selected_text = clipboard_result.stdout.strip() if clipboard_result.returncode == 0 else ""
else: else:
if SystemCommands.is_command_available('xclip'):
clipboard_result = subprocess.run(['xclip', '-o', '-selection', 'primary'], clipboard_result = subprocess.run(['xclip', '-o', '-selection', 'primary'],
capture_output=True, text=True, timeout=5) capture_output=True, text=True, timeout=5)
selected_text = clipboard_result.stdout.strip() if clipboard_result.returncode == 0 else "" selected_text = clipboard_result.stdout.strip() if clipboard_result.returncode == 0 else ""
if selected_text: if selected_text:
@@ -1298,6 +1349,11 @@ class AiAssistant(Gtk.Window):
else: else:
# No selected text, fallback to OCR of current screen # No selected text, fallback to OCR of current screen
# Check if scrot is available
if not SystemCommands.is_command_available('scrot'):
GLib.idle_add(self.set_response_text, "Error: No selected text found and scrot not available for screen capture.")
return
# Take screenshot first # Take screenshot first
temp_dir = tempfile.mkdtemp() temp_dir = tempfile.mkdtemp()
screenshot_path = os.path.join(temp_dir, 'screen_analysis.png') screenshot_path = os.path.join(temp_dir, 'screen_analysis.png')
@@ -1349,7 +1405,7 @@ class AiAssistant(Gtk.Window):
try: try:
os.unlink(screenshot_path) os.unlink(screenshot_path)
os.rmdir(temp_dir) os.rmdir(temp_dir)
except: except (FileNotFoundError, OSError) as e:
pass pass
GLib.idle_add(self.set_response_text, response) GLib.idle_add(self.set_response_text, response)
@@ -1362,6 +1418,9 @@ class AiAssistant(Gtk.Window):
def speak_text(self, text): def speak_text(self, text):
"""Use spd-say to speak text if voice output is enabled""" """Use spd-say to speak text if voice output is enabled"""
if self.config.get('voice_output') == 'true': if self.config.get('voice_output') == 'true':
if not SystemCommands.is_command_available('spd-say'):
print("Warning: spd-say not available for text-to-speech")
return
try: try:
subprocess.run(['spd-say', '-P', 'important', text], subprocess.run(['spd-say', '-P', 'important', text],
capture_output=True, timeout=30) capture_output=True, timeout=30)
@@ -1382,14 +1441,16 @@ class AiAssistant(Gtk.Window):
try: try:
self.update_voice_status("🎤 Listening...") self.update_voice_status("🎤 Listening...")
# Play recording start sound # Play recording start sound if available
if SystemCommands.is_command_available('play'):
subprocess.run(['play', '-qnG', 'synth', '0.1', 'sin', '1000', 'vol', '0.3'], subprocess.run(['play', '-qnG', 'synth', '0.1', 'sin', '1000', 'vol', '0.3'],
capture_output=True) capture_output=True)
timeout = int(self.config.get('voice_timeout', '5')) timeout = int(self.config.get('voice_timeout', '5'))
recognized_text = self.voiceRecognition.recognize_speech(timeout=timeout) recognized_text = self.voiceRecognition.recognize_speech(timeout=timeout)
# Play recording end sound # Play recording end sound if available
if SystemCommands.is_command_available('play'):
subprocess.run(['play', '-qnG', 'synth', '0.05', 'sin', '1200', 'vol', '0.3'], subprocess.run(['play', '-qnG', 'synth', '0.05', 'sin', '1200', 'vol', '0.3'],
capture_output=True) capture_output=True)
@@ -1469,7 +1530,8 @@ class AiAssistant(Gtk.Window):
self.speak_text("Yes, what can I help you with?") self.speak_text("Yes, what can I help you with?")
self.update_voice_status(f"🎤 Wake word detected, listening for {ai_name}...") self.update_voice_status(f"🎤 Wake word detected, listening for {ai_name}...")
# Play wake word detection sound # Play wake word detection sound if available
if SystemCommands.is_command_available('play'):
subprocess.run(['play', '-qnG', 'synth', '0.1', 'sin', '800', 'vol', '0.4'], subprocess.run(['play', '-qnG', 'synth', '0.1', 'sin', '800', 'vol', '0.4'],
capture_output=True) capture_output=True)
@@ -1581,10 +1643,25 @@ class AiAssistant(Gtk.Window):
def main(): def main():
"""Main entry point""" """Main entry point"""
# Check system dependencies
missing_required, missing_optional = SystemCommands.check_dependencies()
if missing_required:
print("WARNING: Missing required commands:")
for cmd, desc in missing_required.items():
print(f" - {cmd}: {desc}")
print("\nSome features may not work properly.")
if missing_optional:
print("INFO: Missing optional commands:")
for cmd, desc in missing_optional.items():
print(f" - {cmd}: {desc}")
app = AiAssistant() app = AiAssistant()
app.show_all() app.show_all()
# Play startup sound # Play startup sound if available
if SystemCommands.is_command_available('play'):
subprocess.run(['play', '-qnG', 'synth', '0.1', 'sin', '1000'], subprocess.run(['play', '-qnG', 'synth', '0.1', 'sin', '1000'],
capture_output=True) capture_output=True)