This commit is contained in:
Charles Wambua
2026-05-25 16:43:05 +03:00
parent b7b7397876
commit 1dd5b757be
3 changed files with 337 additions and 213 deletions

140
main.py
View File

@@ -7,7 +7,9 @@ import sys
import os
import yaml
import threading
import logging
from pathlib import Path
from datetime import datetime
sys.path.insert(0, str(Path(__file__).parent))
@@ -34,38 +36,57 @@ _NOISE_PHRASES = (
class MeetingAssistant:
def __init__(self, config_path="config.yaml"):
def __init__(self):
"""Initialize all components"""
print("🚀 Starting Meeting Assistant...")
config_file = Path(__file__).parent / config_path
with open(config_file, "r") as f:
self.config = yaml.safe_load(f)
self.logger = setup_logging()
print(f"💻 Platform: {get_platform()}")
self.context_manager = ContextManager(self.config)
# Load config
config_path = Path(__file__).parent / "config.yaml"
with open(config_path, 'r') as f:
self.config = yaml.safe_load(f)
# Setup logging
self.logger = setup_logging()
# Initialize components
self.ai_engine = AIEngine(self.config)
self.context_manager = ContextManager(self.config)
self.overlay = InvisibleOverlay(self.config)
# Audio listener with callback
self.audio_listener = AudioListener(self.config, self.on_audio_transcript)
self._last_question = "" # Simple debounce
# State management
self.current_answer = None
self.answering = False
self.answer_lock = threading.Lock()
self._last_question = ""
# ------------------------------------------------------------------
# Audio callback — runs in the audio listener thread
# ------------------------------------------------------------------
print("✅ Meeting Assistant Ready!")
print("==================================================")
print("🎤 Listening for questions via microphone")
print(" Answers appear in the overlay (top-right)")
print(" Type a question here + Enter to test AI")
print(" Ctrl+C to stop")
print("==================================================")
def on_audio_transcript(self, text, timestamp):
"""Audio callback — runs in the audio listener thread"""
if not text or len(text.strip()) < 2:
return
self.context_manager.add_audio_context(text, timestamp)
if self._is_question(text):
print(f"❓ Question: {text}")
print(f"\n❓ Question detected: {text}")
self._generate_answer(text)
else:
print(f" Context: {text}")
print(f" Context (not a question): {text}")
def _is_question(self, text):
if len(text.split()) < 3:
"""Enhanced question detection for faster responses"""
if len(text.split()) < 2: # Reduced from 3 for shorter questions
return False
text_lower = text.lower().strip()
@@ -78,21 +99,78 @@ class MeetingAssistant:
if text.rstrip().endswith("?"):
return True
# Check for math expressions (2+2, two plus two)
import re
math_patterns = [
r'\d+\s*[\+\-\*\/]\s*\d+', # 2+2, 5-3
r'\d+\s*plus\s*\d+', # 2 plus 2
r'\d+\s*minus\s*\d+', # 5 minus 3
r'\d+\s*times\s*\d+', # 4 times 5
r'\d+\s*divided by\s*\d+', # 10 divided by 2
]
for pattern in math_patterns:
if re.search(pattern, text_lower):
return True
# Check for short implicit questions (2-5 words without filler)
words = text_lower.split()
if 2 <= len(words) <= 5:
filler = {'um', 'uh', 'like', 'so', 'well', 'actually', 'basically'}
content_words = [w for w in words if w not in filler]
if len(content_words) >= 2:
# "two plus two", "capital france", "python example"
return True
# Starts with or contains a question word
return any(text_lower.startswith(w) or f" {w} " in text_lower for w in _QUESTION_WORDS)
if any(text_lower.startswith(w) or f" {w} " in text_lower for w in _QUESTION_WORDS):
return True
# Check for command starters
command_starters = ['tell me', 'explain', 'describe', 'show me', 'give me', 'find', 'search']
for starter in command_starters:
if text_lower.startswith(starter):
return True
return False
def _generate_answer(self, question):
"""Generate answer with interruption support"""
# Debounce: skip exact repeat within same run
if question.lower() == self._last_question.lower():
return
self._last_question = question
context = self.context_manager.get_context()
answer = self.ai_engine.answer_question(question, context)
self.overlay.show_answer(answer, question)
self.logger.info(f"Q: {question}")
self.logger.info(f"A: {answer}")
print(f"💡 Answer: {answer}\n")
# Check if we should interrupt current answer
with self.answer_lock:
if self.answering:
print("🔄 Interrupting previous answer...")
if hasattr(self.ai_engine, 'interrupt'):
self.ai_engine.interrupt()
# Start answer generation in background thread
threading.Thread(target=self._answer_worker, args=(question,), daemon=True).start()
def _answer_worker(self, question):
"""Worker thread for answer generation"""
with self.answer_lock:
self.answering = True
try:
# Get context and generate answer
context = self.context_manager.get_context()
answer = self.ai_engine.answer_question(question, context)
# Show answer in overlay
self.overlay.show_answer(answer, question)
self.logger.info(f"Q: {question}")
self.logger.info(f"A: {answer}")
print(f"\n💡 Answer: {answer}\n")
except Exception as e:
print(f"❌ Error generating answer: {e}")
finally:
with self.answer_lock:
self.answering = False
# ------------------------------------------------------------------
# Manual terminal input for testing
@@ -114,14 +192,7 @@ class MeetingAssistant:
# ------------------------------------------------------------------
def run(self):
print("\n✅ Meeting Assistant Ready!")
print("=" * 50)
print("🎤 Listening for questions via microphone")
print(" Answers appear in the overlay (top-right)")
print(" Type a question here + Enter to test AI")
print(" Ctrl+C to stop")
print("=" * 50)
print("\n🎤 Starting audio listener...")
self.overlay.start()
self.audio_listener.start()
@@ -137,9 +208,12 @@ class MeetingAssistant:
def shutdown(self):
print("\n🛑 Shutting down...")
self.audio_listener.stop()
self.overlay.stop()
if hasattr(self, 'audio_listener'):
self.audio_listener.stop()
if hasattr(self, 'overlay'):
self.overlay.stop()
print("👋 Goodbye!")
sys.exit(0)
def main():
@@ -148,4 +222,4 @@ def main():
if __name__ == "__main__":
main()
main()