A few fixes to ai.py.
This commit is contained in:
+89
-12
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user