Skip to content

Commit 4e82953

Browse files
DaoudSabatclaude
andcommitted
Refactor: wrap Jarvis functions into VirtualAssistant class
- VirtualAssistant encapsulates speech I/O (pyttsx3 + SpeechRecognition) - Command pattern: run() dispatches voice queries to action methods - Remove committed .venv from tracking - 6 pytest unit tests with mocked hardware - Professional README with architecture and design pattern table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7554a1f commit 4e82953

9 files changed

Lines changed: 183 additions & 0 deletions

File tree

.gitignore

282 Bytes
Binary file not shown.

README.md

2.08 KB
Binary file not shown.

core/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .assistant import VirtualAssistant
2+
3+
__all__ = ["VirtualAssistant"]

core/assistant.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""Virtual assistant — speech I/O and command dispatch."""
2+
from __future__ import annotations
3+
4+
import datetime
5+
import webbrowser
6+
7+
import pyttsx3
8+
import speech_recognition as sr
9+
import wikipedia
10+
11+
12+
class VirtualAssistant:
13+
"""Jarvis-style voice assistant with pluggable command handlers."""
14+
15+
def __init__(self, name: str = "Jarvis") -> None:
16+
self.name = name
17+
self._engine = pyttsx3.init()
18+
self._recognizer = sr.Recognizer()
19+
20+
# ------------------------------------------------------------------
21+
# Speech I/O
22+
# ------------------------------------------------------------------
23+
24+
def speak(self, text: str) -> None:
25+
"""Convert text to speech."""
26+
self._engine.say(text)
27+
self._engine.runAndWait()
28+
29+
def listen(self) -> str | None:
30+
"""Listen via microphone and return recognised text (or None)."""
31+
try:
32+
with sr.Microphone() as source:
33+
print("Listening...")
34+
self._recognizer.adjust_for_ambient_noise(source, duration=1)
35+
self._recognizer.pause_threshold = 1
36+
audio = self._recognizer.listen(source)
37+
query = self._recognizer.recognize_google(audio, language="en-in")
38+
print(f"Command: {query}")
39+
return query
40+
except sr.UnknownValueError:
41+
self.speak("Sorry, I didn't catch that.")
42+
except sr.RequestError as e:
43+
self.speak("Speech service unavailable.")
44+
print(f"RequestError: {e}")
45+
except Exception as e:
46+
self.speak("An unexpected error occurred.")
47+
print(f"Error: {e}")
48+
return None
49+
50+
# ------------------------------------------------------------------
51+
# Command handlers
52+
# ------------------------------------------------------------------
53+
54+
def greet(self) -> None:
55+
self.speak(f"Hello! I'm {self.name}. How can I assist you?")
56+
57+
def tell_day(self) -> None:
58+
days = {1: "Monday", 2: "Tuesday", 3: "Wednesday", 4: "Thursday",
59+
5: "Friday", 6: "Saturday", 7: "Sunday"}
60+
day = datetime.datetime.today().weekday() + 1
61+
self.speak(f"Today is {days[day]}")
62+
63+
def tell_time(self) -> None:
64+
t = datetime.datetime.now().strftime("%H:%M")
65+
self.speak(f"The time is {t}")
66+
67+
def open_google(self, search: str) -> None:
68+
webbrowser.open(f"https://www.google.com/search?q={search}")
69+
70+
def open_url(self, url: str) -> None:
71+
webbrowser.open(url)
72+
73+
def wikipedia_search(self, query: str) -> None:
74+
self.speak("Checking Wikipedia")
75+
try:
76+
result = wikipedia.summary(query, sentences=4)
77+
self.speak("According to Wikipedia")
78+
self.speak(result)
79+
except wikipedia.exceptions.DisambiguationError:
80+
self.speak("Multiple entries found. Please be more specific.")
81+
except wikipedia.exceptions.PageError:
82+
self.speak("No Wikipedia page found for that topic.")
83+
84+
# ------------------------------------------------------------------
85+
# Main loop
86+
# ------------------------------------------------------------------
87+
88+
def run(self) -> None:
89+
"""Start the assistant and listen for commands indefinitely."""
90+
self.greet()
91+
while True:
92+
query = self.listen()
93+
if query is None:
94+
continue
95+
query = query.lower()
96+
97+
if "open google" in query:
98+
self.speak("What do you want to search for?")
99+
term = self.listen()
100+
if term:
101+
self.open_google(term)
102+
elif "open geeksforgeeks" in query:
103+
self.speak("Opening Geeks for Geeks")
104+
self.open_url("https://www.geeksforgeeks.com")
105+
elif "which day is it" in query:
106+
self.tell_day()
107+
elif "tell me the time" in query:
108+
self.tell_time()
109+
elif "from wikipedia" in query:
110+
self.wikipedia_search(query.replace("from wikipedia", "").strip())
111+
elif "your name" in query:
112+
self.speak(f"I am {self.name}, your desktop assistant.")
113+
elif "exit" in query or "bye" in query:
114+
self.speak("Goodbye! Have a great day!")
115+
break
116+
else:
117+
self.speak("Sorry, I didn't understand that command.")

main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Entry point — launches the Jarvis virtual assistant."""
2+
from core.assistant import VirtualAssistant
3+
4+
if __name__ == "__main__":
5+
assistant = VirtualAssistant(name="Jarvis")
6+
assistant.run()

requirements.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pyttsx3>=2.90
2+
SpeechRecognition>=3.10.0
3+
wikipedia>=1.4.0
4+
pyaudio>=0.2.13
5+
pytest>=7.4.0

tests/__init__.py

Whitespace-only changes.

tests/test_assistant.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Unit tests for VirtualAssistant — mocks all hardware (mic, speaker, browser)."""
2+
import datetime
3+
from unittest.mock import MagicMock, patch
4+
5+
import pytest
6+
7+
from core.assistant import VirtualAssistant
8+
9+
10+
@pytest.fixture
11+
def assistant():
12+
with patch("core.assistant.pyttsx3.init"):
13+
a = VirtualAssistant(name="TestBot")
14+
a._engine = MagicMock()
15+
return a
16+
17+
18+
def test_speak_calls_engine(assistant):
19+
assistant.speak("Hello")
20+
assistant._engine.say.assert_called_once_with("Hello")
21+
assistant._engine.runAndWait.assert_called_once()
22+
23+
24+
def test_tell_day_speaks_a_day(assistant):
25+
with patch.object(assistant, "speak") as mock_speak:
26+
assistant.tell_day()
27+
called_text = mock_speak.call_args[0][0]
28+
assert "day is" in called_text.lower()
29+
30+
31+
def test_tell_time_format(assistant):
32+
with patch.object(assistant, "speak") as mock_speak:
33+
assistant.tell_time()
34+
called_text = mock_speak.call_args[0][0]
35+
assert ":" in called_text
36+
37+
38+
@patch("core.assistant.webbrowser.open")
39+
def test_open_google(mock_open, assistant):
40+
assistant.open_google("Python tutorials")
41+
mock_open.assert_called_once()
42+
assert "Python+tutorials" in mock_open.call_args[0][0] or "Python" in mock_open.call_args[0][0]
43+
44+
45+
@patch("core.assistant.webbrowser.open")
46+
def test_open_url(mock_open, assistant):
47+
assistant.open_url("https://example.com")
48+
mock_open.assert_called_once_with("https://example.com")
49+
50+
51+
def test_assistant_name(assistant):
52+
assert assistant.name == "TestBot"

utils/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)