From cc42e9fb05bc59a6e0043bd23848622515c5a2c5 Mon Sep 17 00:00:00 2001 From: AmanDevelops Date: Sun, 9 Feb 2025 19:41:26 +0530 Subject: [PATCH 01/12] [optimization]: optimized email --- src/assistant.py | 7 ++-- src/commands/__init__.py | 0 src/{ => commands}/config/email_config.json | 0 src/commands/send_email.py | 39 +++++++++++++++++++++ src/{ => commands}/utils.py | 0 src/{ => commands}/voice_interface.py | 0 src/{commands.py => commands_temp.py} | 9 ++--- 7 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 src/commands/__init__.py rename src/{ => commands}/config/email_config.json (100%) create mode 100644 src/commands/send_email.py rename src/{ => commands}/utils.py (100%) rename src/{ => commands}/voice_interface.py (100%) rename src/{commands.py => commands_temp.py} (98%) diff --git a/src/assistant.py b/src/assistant.py index d36922c..d71a4ef 100755 --- a/src/assistant.py +++ b/src/assistant.py @@ -15,8 +15,9 @@ import commands from infra import clear_screen -from utils import load_email_config -from voice_interface import VoiceInterface +from commands.utils import load_email_config +from commands.voice_interface import VoiceInterface +from commands.send_email import send_email LISTENING_ERROR = "Say that again please..." MAX_FETCHED_HEADLINES = ( @@ -191,7 +192,7 @@ def execute_query(self, query: str) -> None: response = self.listen_for_query() if "yes" in response.lower() or "sure" in response.lower(): self.__voice_interface.speak("Sending the email") - commands.send_email(self.__voice_interface, receiver, subject, body) + send_email(self.__voice_interface, receiver, subject, body) else: self.__voice_interface.speak("Request aborted by user") diff --git a/src/commands/__init__.py b/src/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/config/email_config.json b/src/commands/config/email_config.json similarity index 100% rename from src/config/email_config.json rename to src/commands/config/email_config.json diff --git a/src/commands/send_email.py b/src/commands/send_email.py new file mode 100644 index 0000000..9f11b47 --- /dev/null +++ b/src/commands/send_email.py @@ -0,0 +1,39 @@ +from email.message import EmailMessage +from .voice_interface import VoiceInterface +from .utils import load_email_config +import smtplib +import ssl +from dotenv import dotenv_values + + +ENVIRONMENT_VARIABLES = dotenv_values(".env") + + +def send_email(vi: VoiceInterface, toEmail: str, subject: str, body: str): + """ + Send an email to the specified recipient. + + Args: + vi (VoiceInterface): VoiceInterface instance used to speak. + toEmail (str): The recipient's email address. + subject (str): The subject of the email. + body (str): The body content of the email. + + Raises: + ValueError: If any required parameters are missing or invalid. + """ + + data = load_email_config() + CONTEXT = ssl.create_default_context() + msg = EmailMessage() + msg["Subject"] = subject + msg["From"] = data.get("username") + msg["To"] = [toEmail] + msg.set_content(body) + server = smtplib.SMTP_SSL(data.get("server"), data.get("port"), context=CONTEXT) + server.login( + data.get("username"), ENVIRONMENT_VARIABLES.get("DESKTOP_ASSISTANT_SMTP_PWD") + ) + server.send_message(msg) + server.quit() + vi.speak(f"Email sent to {toEmail}") diff --git a/src/utils.py b/src/commands/utils.py similarity index 100% rename from src/utils.py rename to src/commands/utils.py diff --git a/src/voice_interface.py b/src/commands/voice_interface.py similarity index 100% rename from src/voice_interface.py rename to src/commands/voice_interface.py diff --git a/src/commands.py b/src/commands_temp.py similarity index 98% rename from src/commands.py rename to src/commands_temp.py index 96b7321..2163f8e 100644 --- a/src/commands.py +++ b/src/commands_temp.py @@ -9,13 +9,11 @@ """ -import smtplib -import ssl + import subprocess import threading import time from datetime import datetime -from email.message import EmailMessage from subprocess import CalledProcessError, TimeoutExpired import feedparser @@ -26,13 +24,12 @@ import wikipedia import wmi from comtypes import CLSCTX_ALL -from dotenv import dotenv_values from PIL import ImageGrab from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume from infra import __is_darwin, __is_posix, __is_windows, __system_os -from utils import load_email_config -from voice_interface import VoiceInterface +from commands.utils import load_email_config +from commands.voice_interface import VoiceInterface SUPPORTED_FEATURES = { "search your query in google and return upto 10 results", From feb22fa684b19f7d1c326163f7643c4e60231386 Mon Sep 17 00:00:00 2001 From: AmanDevelops Date: Sun, 9 Feb 2025 19:45:06 +0530 Subject: [PATCH 02/12] fixing blacklines --- src/assistant.py | 4 ++-- src/commands/send_email.py | 7 ++++--- src/commands_temp.py | 32 +------------------------------- 3 files changed, 7 insertions(+), 36 deletions(-) diff --git a/src/assistant.py b/src/assistant.py index d71a4ef..97915db 100755 --- a/src/assistant.py +++ b/src/assistant.py @@ -14,10 +14,10 @@ from datetime import datetime import commands -from infra import clear_screen +from commands.send_email import send_email from commands.utils import load_email_config from commands.voice_interface import VoiceInterface -from commands.send_email import send_email +from infra import clear_screen LISTENING_ERROR = "Say that again please..." MAX_FETCHED_HEADLINES = ( diff --git a/src/commands/send_email.py b/src/commands/send_email.py index 9f11b47..d8d618c 100644 --- a/src/commands/send_email.py +++ b/src/commands/send_email.py @@ -1,10 +1,11 @@ -from email.message import EmailMessage -from .voice_interface import VoiceInterface -from .utils import load_email_config import smtplib import ssl +from email.message import EmailMessage + from dotenv import dotenv_values +from .utils import load_email_config +from .voice_interface import VoiceInterface ENVIRONMENT_VARIABLES = dotenv_values(".env") diff --git a/src/commands_temp.py b/src/commands_temp.py index 2163f8e..01bc6cc 100644 --- a/src/commands_temp.py +++ b/src/commands_temp.py @@ -27,9 +27,9 @@ from PIL import ImageGrab from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume -from infra import __is_darwin, __is_posix, __is_windows, __system_os from commands.utils import load_email_config from commands.voice_interface import VoiceInterface +from infra import __is_darwin, __is_posix, __is_windows, __system_os SUPPORTED_FEATURES = { "search your query in google and return upto 10 results", @@ -455,33 +455,3 @@ def weather_reporter(vi: VoiceInterface, city_name: str) -> None: f"The wind speed is expected to be {weather_data.get('wind_speed_10m')}{weather_units.get('wind_speed_10m')}, " "so plan accordingly." ) - - -def send_email(vi: VoiceInterface, toEmail: str, subject: str, body: str): - """ - Send an email to the specified recipient. - - Args: - vi (VoiceInterface): VoiceInterface instance used to speak. - toEmail (str): The recipient's email address. - subject (str): The subject of the email. - body (str): The body content of the email. - - Raises: - ValueError: If any required parameters are missing or invalid. - """ - - data = load_email_config() - CONTEXT = ssl.create_default_context() - msg = EmailMessage() - msg["Subject"] = subject - msg["From"] = data.get("username") - msg["To"] = [toEmail] - msg.set_content(body) - server = smtplib.SMTP_SSL(data.get("server"), data.get("port"), context=CONTEXT) - server.login( - data.get("username"), ENVIRONMENT_VARIABLES.get("DESKTOP_ASSISTANT_SMTP_PWD") - ) - server.send_message(msg) - server.quit() - vi.speak(f"Email sent to {toEmail}") From 7f2b068a9cb16661a2502320ee9a20d4554814ab Mon Sep 17 00:00:00 2001 From: AmanDevelops Date: Mon, 10 Feb 2025 19:47:55 +0530 Subject: [PATCH 03/12] [optimization]: optimized brightness control --- src/assistant.py | 5 +++-- src/commands/brightness_control.py | 34 +++++++++++++++++++++++++++++ src/commands_temp.py | 35 +----------------------------- 3 files changed, 38 insertions(+), 36 deletions(-) create mode 100644 src/commands/brightness_control.py diff --git a/src/assistant.py b/src/assistant.py index 97915db..f7dab45 100755 --- a/src/assistant.py +++ b/src/assistant.py @@ -16,6 +16,7 @@ import commands from commands.send_email import send_email from commands.utils import load_email_config +from commands.brightness_control import brightness_control from commands.voice_interface import VoiceInterface from infra import clear_screen @@ -205,11 +206,11 @@ def execute_query(self, query: str) -> None: else: value = min(max(0, int(value[0])), 100) if "set" in query: - commands.brightness_control(value, False, False) + brightness_control(value, False, False) else: toDecrease = "decrease" in query or "reduce" in query relative = "by" in query - commands.brightness_control(value, relative, toDecrease) + brightness_control(value, relative, toDecrease) elif "volume" in query: query = query.lower() diff --git a/src/commands/brightness_control.py b/src/commands/brightness_control.py new file mode 100644 index 0000000..755edf4 --- /dev/null +++ b/src/commands/brightness_control.py @@ -0,0 +1,34 @@ +import wmi + + +def brightness_control(value: int, relative: bool, toDecrease: bool): + """ + Adjusts the brightness of the monitor. + + Args: + value (int): The brightness level to set or adjust by. Should be between 0 and 100. + relative (bool): If True, the brightness change is relative to the current brightness. + If False, the brightness is set to the specified value. + toDecrease (bool): If True, decreases the brightness by the specified value. + If False, increases the brightness by the specified value. Only applicable when `relative` is True. + + Raises: + RuntimeError: If there is an issue with accessing the brightness control methods. + + Returns: + None + """ + + brightness_ctrl = wmi.WMI(namespace="root\\wmi") + methods = brightness_ctrl.WmiMonitorBrightnessMethods()[0] + + if relative: + current_brightness = brightness_ctrl.WmiMonitorBrightness()[0].CurrentBrightness + set_brightnes = ( + current_brightness - int(value) + if toDecrease + else current_brightness + int(value) + ) + methods.WmiSetBrightness(set_brightnes, 0) + else: + methods.WmiSetBrightness(value, 0) diff --git a/src/commands_temp.py b/src/commands_temp.py index 01bc6cc..14b4881 100644 --- a/src/commands_temp.py +++ b/src/commands_temp.py @@ -22,7 +22,7 @@ import pygetwindow import requests import wikipedia -import wmi + from comtypes import CLSCTX_ALL from PIL import ImageGrab from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume @@ -304,39 +304,6 @@ def simple_scroll(direction: str) -> None: print("Invalid direction") -def brightness_control(value: int, relative: bool, toDecrease: bool): - """ - Adjusts the brightness of the monitor. - - Args: - value (int): The brightness level to set or adjust by. Should be between 0 and 100. - relative (bool): If True, the brightness change is relative to the current brightness. - If False, the brightness is set to the specified value. - toDecrease (bool): If True, decreases the brightness by the specified value. - If False, increases the brightness by the specified value. Only applicable when `relative` is True. - - Raises: - RuntimeError: If there is an issue with accessing the brightness control methods. - - Returns: - None - """ - - brightness_ctrl = wmi.WMI(namespace="root\\wmi") - methods = brightness_ctrl.WmiMonitorBrightnessMethods()[0] - - if relative: - current_brightness = brightness_ctrl.WmiMonitorBrightness()[0].CurrentBrightness - set_brightnes = ( - current_brightness - int(value) - if toDecrease - else current_brightness + int(value) - ) - methods.WmiSetBrightness(set_brightnes, 0) - else: - methods.WmiSetBrightness(value, 0) - - def volume_control(value: int, relative: bool, toDecrease: bool): """ Adjusts the master volume of the system. From 15276201d262f2091651c9f86d7360a81a0e243c Mon Sep 17 00:00:00 2001 From: AmanDevelops Date: Mon, 10 Feb 2025 19:54:07 +0530 Subject: [PATCH 04/12] [optimization]: optimized volume control --- src/assistant.py | 5 +++-- src/commands/volume_control.py | 35 ++++++++++++++++++++++++++++++++++ src/commands_temp.py | 32 ------------------------------- 3 files changed, 38 insertions(+), 34 deletions(-) create mode 100644 src/commands/volume_control.py diff --git a/src/assistant.py b/src/assistant.py index f7dab45..7d05d32 100755 --- a/src/assistant.py +++ b/src/assistant.py @@ -17,6 +17,7 @@ from commands.send_email import send_email from commands.utils import load_email_config from commands.brightness_control import brightness_control +from commands.volume_control import volume_control from commands.voice_interface import VoiceInterface from infra import clear_screen @@ -221,11 +222,11 @@ def execute_query(self, query: str) -> None: else: value = min(max(0, int(value[0])), 100) if "set" in query: - commands.volume_control(value, False, False) + volume_control(value, False, False) else: toDecrease = "decrease" in query or "reduce" in query relative = "by" in query - commands.volume_control(value, relative, toDecrease) + volume_control(value, relative, toDecrease) elif "shutdown" in query or "shut down" in query: self.__voice_interface.speak("Are you sure you want to shut down your PC?") diff --git a/src/commands/volume_control.py b/src/commands/volume_control.py new file mode 100644 index 0000000..5e1d9be --- /dev/null +++ b/src/commands/volume_control.py @@ -0,0 +1,35 @@ +from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume +from comtypes import CLSCTX_ALL + + +def volume_control(value: int, relative: bool, toDecrease: bool): + """ + Adjusts the master volume of the system. + + Args: + value (int): The volume level to set or adjust by. Should be between 0 and 100. + relative (bool): If True, the volume change is relative to the current volume. + If False, the volume is set to the specified value. + toDecrease (bool): If True, decreases the volume by the specified value. + If False, increases the volume by the specified value. Only applicable when `relative` is True. + + Raises: + RuntimeError: If there is an issue with accessing the audio endpoint. + + Returns: + None + """ + + devices = AudioUtilities.GetSpeakers() + interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None) + volume = interface.QueryInterface(IAudioEndpointVolume) + + if relative: + current_volume = volume.GetMasterVolumeLevelScalar() * 100 + set_volume = ( + current_volume - int(value) if toDecrease else current_volume + int(value) + ) + print(set_volume) + volume.SetMasterVolumeLevelScalar(min(max(0, set_volume), 100) / 100, None) + else: + volume.SetMasterVolumeLevelScalar(min(max(0, value), 100) / 100, None) diff --git a/src/commands_temp.py b/src/commands_temp.py index 14b4881..03a75bf 100644 --- a/src/commands_temp.py +++ b/src/commands_temp.py @@ -23,9 +23,7 @@ import requests import wikipedia -from comtypes import CLSCTX_ALL from PIL import ImageGrab -from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume from commands.utils import load_email_config from commands.voice_interface import VoiceInterface @@ -304,37 +302,7 @@ def simple_scroll(direction: str) -> None: print("Invalid direction") -def volume_control(value: int, relative: bool, toDecrease: bool): - """ - Adjusts the master volume of the system. - - Args: - value (int): The volume level to set or adjust by. Should be between 0 and 100. - relative (bool): If True, the volume change is relative to the current volume. - If False, the volume is set to the specified value. - toDecrease (bool): If True, decreases the volume by the specified value. - If False, increases the volume by the specified value. Only applicable when `relative` is True. - - Raises: - RuntimeError: If there is an issue with accessing the audio endpoint. - Returns: - None - """ - - devices = AudioUtilities.GetSpeakers() - interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None) - volume = interface.QueryInterface(IAudioEndpointVolume) - - if relative: - current_volume = volume.GetMasterVolumeLevelScalar() * 100 - set_volume = ( - current_volume - int(value) if toDecrease else current_volume + int(value) - ) - print(set_volume) - volume.SetMasterVolumeLevelScalar(min(max(0, set_volume), 100) / 100, None) - else: - volume.SetMasterVolumeLevelScalar(min(max(0, value), 100) / 100, None) def fetch_news(vi: VoiceInterface, max_fetched_headlines: int) -> None: From b5e706c11846ed3960ac3dba90f3e7ea7656dc94 Mon Sep 17 00:00:00 2001 From: AmanDevelops Date: Mon, 10 Feb 2025 20:09:25 +0530 Subject: [PATCH 05/12] [optimisation]: optimised weather functionality --- src/assistant.py | 3 +- src/commands/weather_reporter.py | 58 ++++++++++++++++++++++++++++++++ src/commands_temp.py | 55 ------------------------------ 3 files changed, 60 insertions(+), 56 deletions(-) create mode 100644 src/commands/weather_reporter.py diff --git a/src/assistant.py b/src/assistant.py index 7d05d32..d6f3eca 100755 --- a/src/assistant.py +++ b/src/assistant.py @@ -18,6 +18,7 @@ from commands.utils import load_email_config from commands.brightness_control import brightness_control from commands.volume_control import volume_control +from commands.weather_reporter import weather_reporter from commands.voice_interface import VoiceInterface from infra import clear_screen @@ -137,7 +138,7 @@ def execute_query(self, query: str) -> None: cities = re.findall( r"\b(?:of|in|at)\s+(\w+)", query ) # Extract the city name just after the word 'of' - commands.weather_reporter(self.__voice_interface, cities[0]) + weather_reporter(self.__voice_interface, cities[0]) elif "email" in query: query = query.lower() diff --git a/src/commands/weather_reporter.py b/src/commands/weather_reporter.py new file mode 100644 index 0000000..e4789b9 --- /dev/null +++ b/src/commands/weather_reporter.py @@ -0,0 +1,58 @@ +import requests +from .voice_interface import VoiceInterface + + +def weather_reporter(vi: VoiceInterface, city_name: str) -> None: + """ + Fetches and reports the weather conditions for a given city. + + This function retrieves the latitude and longitude of the specified city using the API Ninjas City API. + It then fetches the current weather data from the Open-Meteo API and reports the temperature, humidity, + apparent temperature, rain probability, cloud cover, and wind speed using the VoiceInterface instance. + + Args: + vi (VoiceInterface): The VoiceInterface instance used to speak the weather report. + city_name (str): The name of the city for which to fetch weather data. + + Raises: + requests.exceptions.RequestException: If there is an issue with the API request. + IndexError: If the city name is not found in the API response. + KeyError: If expected weather data fields are missing from the response. + """ + # Fetch latitude and longitude for the given city to be used by open-metro api + params = { + "name": city_name, + } + geo_codes = requests.get( + "https://api.api-ninjas.com/v1/city", + params=params, + headers={"origin": "https://www.api-ninjas.com"}, + ).json() + + # Fetch weather data from Open-Meteo using the obtained coordinates + weather_data_response = requests.get( + f'https://api.open-meteo.com/v1/forecast?latitude={geo_codes[0].get("latitude")}&longitude={geo_codes[0].get("longitude")}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,rain,showers,cloud_cover,wind_speed_10m&forecast_days=1' + ).json() + + weather_data = weather_data_response.get("current") + weather_units = weather_data_response.get("current_units") + + vi.speak( + f"The current temperature in {city_name} is {weather_data.get('temperature_2m')}{weather_units.get('temperature_2m')}. " + f"However, due to a relative humidity of {weather_data.get('relative_humidity_2m')}{weather_units.get('relative_humidity_2m')}, " + f"it feels like {weather_data.get('apparent_temperature')}{weather_units.get('apparent_temperature')}." + ) + + if weather_data.get("rain") == 0: + vi.speak("The skies will be clear, with no chance of rain.") + else: + cloud_cover = weather_data.get("cloud_cover") + vi.speak( + f"The sky will be {cloud_cover}{weather_units.get('cloud_cover')} cloudy, " + f"and there's a predicted rainfall of {weather_data.get('rain')}{weather_units.get('rain')}." + ) + + vi.speak( + f"The wind speed is expected to be {weather_data.get('wind_speed_10m')}{weather_units.get('wind_speed_10m')}, " + "so plan accordingly." + ) diff --git a/src/commands_temp.py b/src/commands_temp.py index 03a75bf..a87446f 100644 --- a/src/commands_temp.py +++ b/src/commands_temp.py @@ -20,7 +20,6 @@ import googlesearch import pyautogui as pag import pygetwindow -import requests import wikipedia from PIL import ImageGrab @@ -336,57 +335,3 @@ def fetch_news(vi: VoiceInterface, max_fetched_headlines: int) -> None: vi.speak("Failed to fetch the news.") -def weather_reporter(vi: VoiceInterface, city_name: str) -> None: - """ - Fetches and reports the weather conditions for a given city. - - This function retrieves the latitude and longitude of the specified city using the API Ninjas City API. - It then fetches the current weather data from the Open-Meteo API and reports the temperature, humidity, - apparent temperature, rain probability, cloud cover, and wind speed using the VoiceInterface instance. - - Args: - vi (VoiceInterface): The VoiceInterface instance used to speak the weather report. - city_name (str): The name of the city for which to fetch weather data. - - Raises: - requests.exceptions.RequestException: If there is an issue with the API request. - IndexError: If the city name is not found in the API response. - KeyError: If expected weather data fields are missing from the response. - """ - # Fetch latitude and longitude for the given city to be used by open-metro api - params = { - "name": city_name, - } - geo_codes = requests.get( - "https://api.api-ninjas.com/v1/city", - params=params, - headers={"origin": "https://www.api-ninjas.com"}, - ).json() - - # Fetch weather data from Open-Meteo using the obtained coordinates - weather_data_response = requests.get( - f'https://api.open-meteo.com/v1/forecast?latitude={geo_codes[0].get("latitude")}&longitude={geo_codes[0].get("longitude")}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,rain,showers,cloud_cover,wind_speed_10m&forecast_days=1' - ).json() - - weather_data = weather_data_response.get("current") - weather_units = weather_data_response.get("current_units") - - vi.speak( - f"The current temperature in {city_name} is {weather_data.get('temperature_2m')}{weather_units.get('temperature_2m')}. " - f"However, due to a relative humidity of {weather_data.get('relative_humidity_2m')}{weather_units.get('relative_humidity_2m')}, " - f"it feels like {weather_data.get('apparent_temperature')}{weather_units.get('apparent_temperature')}." - ) - - if weather_data.get("rain") == 0: - vi.speak("The skies will be clear, with no chance of rain.") - else: - cloud_cover = weather_data.get("cloud_cover") - vi.speak( - f"The sky will be {cloud_cover}{weather_units.get('cloud_cover')} cloudy, " - f"and there's a predicted rainfall of {weather_data.get('rain')}{weather_units.get('rain')}." - ) - - vi.speak( - f"The wind speed is expected to be {weather_data.get('wind_speed_10m')}{weather_units.get('wind_speed_10m')}, " - "so plan accordingly." - ) From 64dda7433b227049e23da2614315251c7e3892be Mon Sep 17 00:00:00 2001 From: AmanDevelops Date: Mon, 10 Feb 2025 21:29:09 +0530 Subject: [PATCH 06/12] [optimization]: optimized news fetching functionality --- src/assistant.py | 13 ++++++++----- src/commands/news_reporter.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 src/commands/news_reporter.py diff --git a/src/assistant.py b/src/assistant.py index d6f3eca..c510479 100755 --- a/src/assistant.py +++ b/src/assistant.py @@ -20,8 +20,11 @@ from commands.volume_control import volume_control from commands.weather_reporter import weather_reporter from commands.voice_interface import VoiceInterface +from commands.news_reporter import fetch_news +from commands import scroller from infra import clear_screen + LISTENING_ERROR = "Say that again please..." MAX_FETCHED_HEADLINES = ( 10 # Maximum number of news headlines to fetch when news function is called @@ -118,20 +121,20 @@ def execute_query(self, query: str) -> None: self.__scrolling_thread is None ): # Only start if not already scrolling self.__scrolling_thread, self.__stop_scrolling_event = ( - commands.start_scrolling(direction) + scroller.start_scrolling(direction) ) elif "stop scrolling" in query: if self.__scrolling_thread is None: # Only stop if already scrolling return - commands.stop_scrolling( + scroller.stop_scrolling( self.__scrolling_thread, self.__stop_scrolling_event ) del self.__scrolling_thread self.__scrolling_thread = None elif re.search(r"scroll to (up|down|left|right|top|bottom)", query): - commands.scroll_to(direction) + scroller.scroll_to(direction) elif re.search(r"scroll (up|down|left|right)", query): - commands.simple_scroll(direction) + scroller.simple_scroll(direction) else: print("Scroll command not recognized") elif "weather" in query: @@ -251,7 +254,7 @@ def execute_query(self, query: str) -> None: else: self.__voice_interface.speak("Request aborted by user") elif "news" in query: - commands.fetch_news(self.__voice_interface, MAX_FETCHED_HEADLINES) + fetch_news(self.__voice_interface, MAX_FETCHED_HEADLINES) else: self.__voice_interface.speak("could not interpret the query") diff --git a/src/commands/news_reporter.py b/src/commands/news_reporter.py new file mode 100644 index 0000000..f939e39 --- /dev/null +++ b/src/commands/news_reporter.py @@ -0,0 +1,33 @@ +import feedparser +from .voice_interface import VoiceInterface + + +def fetch_news(vi: VoiceInterface, max_fetched_headlines: int) -> None: + """ + Fetches and reads out the top 5 headlines from the Google News RSS feed. + + This function fetches news headlines from the Google News RSS feed (specific to India in English). + It then reads out the top 5 headlines using the provided VoiceInterface instance. If the feed fetch is successful, + it reads the headlines one by one. If the fetch fails, it informs the user that the news couldn't be fetched. + + Args: + vi (VoiceInterface): The VoiceInterface instance used to speak the news headlines. + + Raises: + requests.exceptions.RequestException: If there is an issue while fetching the RSS feed. + AttributeError: If the feed does not contain expected attributes or entries. + """ + + feed_url = "https://news.google.com/rss?hl=en-IN&gl=IN&ceid=IN:en" + + vi.speak("Fetching news from servers.") + feed = feedparser.parse(feed_url) + if feed.status == 200: + headlines_list = [] + for entry in feed.entries[:max_fetched_headlines]: + headlines_list.append((entry.title).split(" -")[0]) + vi.speak("Here are some recent news headlines.") + for headline in headlines_list: + vi.speak(headline) + else: + vi.speak("Failed to fetch the news.") From 340beaac277445d219921692526a269e3febc6ce Mon Sep 17 00:00:00 2001 From: AmanDevelops Date: Mon, 10 Feb 2025 21:30:41 +0530 Subject: [PATCH 07/12] [optimisation]: optimised scroller functionality --- src/commands/scroller.py | 78 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/commands/scroller.py diff --git a/src/commands/scroller.py b/src/commands/scroller.py new file mode 100644 index 0000000..1adeb2a --- /dev/null +++ b/src/commands/scroller.py @@ -0,0 +1,78 @@ +import pygetwindow +import pyautogui as pag +import time +import threading +from PIL import ImageGrab + + +def start_gradual_scroll(direction: str, stop_event: threading.Event) -> None: + """Gradually scroll in the given direction until stop_event is set.""" + active_window = pygetwindow.getActiveWindow() + if not active_window: + return + + # Capture a portion of the window to ensure scrolling + left, top, right, bottom = 0, 0, 100, 100 + width = right - left + height = bottom - top + previous_image = ImageGrab.grab(bbox=(left, top, left + width, top + height)) + while not stop_event.is_set(): + pag.scroll(clicks=1) + current_image = ImageGrab.grab(bbox=(left, top, left + width, top + height)) + + if current_image.getdata() == previous_image.getdata(): + print("Reached to extreme") + stop_event.set() + break + previous_image = current_image + + print(f"Stopped scrolling {direction}.") + + +def start_scrolling(direction: str) -> tuple[threading.Thread, threading.Event]: + """Start a new scroll thread.""" + stop_scrolling_event = threading.Event() + scrolling_thread = threading.Thread( + target=start_gradual_scroll, args=(direction, stop_scrolling_event) + ) + scrolling_thread.start() + return scrolling_thread, stop_scrolling_event + + +def stop_scrolling( + scrolling_thread: threading.Thread, scrolling_thread_event: threading.Event +) -> None: + """Stop the scrolling thread if not already stopped.""" + if scrolling_thread is not None: + scrolling_thread_event.set() + scrolling_thread.join() + + +def scroll_to(direction: str) -> None: + """Scroll to the extreme in the given direction.""" + active_window = pygetwindow.getActiveWindow() + if not active_window: + return + time.sleep(0.5) + if direction == "top": + pag.press("home") + elif direction == "bottom": + pag.press("end") + elif direction == "right": + pag.press("right", presses=9999) + elif direction == "left": + pag.press("left", presses=9999) + else: + print("Invalid Command") + + +def simple_scroll(direction: str) -> None: + """Simple scroll in the given direction by a fixed number of steps.""" + active_window = pygetwindow.getActiveWindow() + if not active_window: + return + time.sleep(0.5) + if direction in ["up", "down", "left", "right"]: + pag.press(keys=direction, presses=25) + else: + print("Invalid direction") From 8814ee457e215edebbf8f730e77bfcbba82926fb Mon Sep 17 00:00:00 2001 From: AmanDevelops Date: Mon, 10 Feb 2025 21:31:00 +0530 Subject: [PATCH 08/12] refactor: replace commands.stop_scrolling with scroller.stop_scrolling --- src/assistant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assistant.py b/src/assistant.py index c510479..b8f3713 100755 --- a/src/assistant.py +++ b/src/assistant.py @@ -264,7 +264,7 @@ def close(self): self.__voice_interface.close() del self.__voice_interface if self.__scrolling_thread: - commands.stop_scrolling( + scroller.stop_scrolling( self.__scrolling_thread, self.__stop_scrolling_event ) del self.__scrolling_thread From 114ae8029a853fcb3f2337fedcd8403dbaef50ed Mon Sep 17 00:00:00 2001 From: AmanDevelops Date: Mon, 10 Feb 2025 21:39:52 +0530 Subject: [PATCH 09/12] [optimisation]: optimised basic features like explain featues, run search query, tell time, wikipedia search. --- src/assistant.py | 9 +- src/commands/basic_features.py | 89 ++++++++++++++ src/commands_temp.py | 205 --------------------------------- 3 files changed, 94 insertions(+), 209 deletions(-) create mode 100644 src/commands/basic_features.py diff --git a/src/assistant.py b/src/assistant.py index b8f3713..9246c55 100755 --- a/src/assistant.py +++ b/src/assistant.py @@ -21,6 +21,7 @@ from commands.weather_reporter import weather_reporter from commands.voice_interface import VoiceInterface from commands.news_reporter import fetch_news +from commands.basic_features import * from commands import scroller from infra import clear_screen @@ -76,19 +77,19 @@ def execute_query(self, query: str) -> None: print("No query detected. Please provide an input.") elif "what can you do" in query: - commands.explain_features(self.__voice_interface) + explain_features(self.__voice_interface) elif re.search(r"search .* (in google)?", query): # to convert to a generalized format query = query.replace(" in google", "") search_query = re.findall(r"search (.*)", query)[0] - commands.run_search_query(self.__voice_interface, search_query) + run_search_query(self.__voice_interface, search_query) elif "wikipedia" in query: # replace it only once to prevent changing the query query = query.replace("wikipedia", "", 1) search_query = query.replace("search", "", 1) - commands.wikipedia_search(self.__voice_interface, search_query, 3) + wikipedia_search(self.__voice_interface, search_query, 3) elif re.search("open .*", query): application = re.findall(r"open (.*)", query) @@ -107,7 +108,7 @@ def execute_query(self, query: str) -> None: ) elif any(text in query for text in ["the time", "time please"]): - commands.tell_time(self.__voice_interface) + tell_time(self.__voice_interface) elif "scroll" in query: direction = re.search(r"(up|down|left|right|top|bottom)", query) diff --git a/src/commands/basic_features.py b/src/commands/basic_features.py new file mode 100644 index 0000000..ce42f6f --- /dev/null +++ b/src/commands/basic_features.py @@ -0,0 +1,89 @@ +from .voice_interface import VoiceInterface +import googlesearch +from datetime import datetime + +import wikipedia + +SUPPORTED_FEATURES = { + "search your query in google and return upto 10 results", + "get a wikipedia search summary of upto 3 sentences", + "open applications or websites", + "tell you the time of the day", + "scroll the screen with active cursor", +} + + +def explain_features(vi: VoiceInterface) -> None: + """Explains the features available + + Args: + vi (VoiceInterface): The voice interface instance used to speak the text + """ + vi.speak("Here's what I can do...\n") + for feature in SUPPORTED_FEATURES: + vi.speak(f"--> {feature}") + + +def run_search_query(vi: VoiceInterface, search_query: str) -> None: + """Performs google search based on some terms + + Args: + vi (VoiceInterface): VoiceInterface instance used to speak + search_query (str): the query term to be searched in google + """ + if not search_query: + vi.speak("Invalid Google Search Query Found!!") + return + + results = googlesearch.search(term=search_query) + if not results: + vi.speak("No Search Result Found!!") + else: + results = list(results) + vi.speak("Found Following Results: ") + for i, result in enumerate(results): + print(i + 1, ")", result.title) + + +def tell_time(vi: VoiceInterface) -> None: + """Tells the time of the day with timezone + + Args: + vi (VoiceInterface): Voice interface instance used to speak + """ + date_time = datetime.now() + hour, minute, second = date_time.hour, date_time.minute, date_time.second + tmz = date_time.tzname() + + vi.speak(f"Current time is {hour}:{minute}:{second} {tmz}") + + +def wikipedia_search( + vi: VoiceInterface, search_query: str, sentence_count: int = 3 +) -> None: + """Searches wikipedia for the given query and returns fixed number of statements in response. + Disambiguation Error due to multiple similar results is handled. + Speaks the options in this case. + + Args: + vi (VoiceInterface): VoiceInterface instance used to speak. + search_query (str): The query term to search in wikipedia + sentence_count (int, optional): The number of sentences to speak in case of direct match. + Default is 3. + """ + try: + vi.speak("Searching Wikipedia...") + results = wikipedia.summary(search_query, sentences=sentence_count) + + vi.speak("According to wikipedia...") + vi.speak(results) + except wikipedia.DisambiguationError as de: + vi.speak(f"\n{de.__class__.__name__}") + options = str(de).split("\n") + if len(options) < 7: + for option in options: + vi.speak(option) + else: + for option in options[0:6]: + vi.speak(option) + vi.speak("... and more") diff --git a/src/commands_temp.py b/src/commands_temp.py index a87446f..590e14e 100644 --- a/src/commands_temp.py +++ b/src/commands_temp.py @@ -11,101 +11,18 @@ import subprocess -import threading -import time -from datetime import datetime from subprocess import CalledProcessError, TimeoutExpired -import feedparser -import googlesearch -import pyautogui as pag -import pygetwindow -import wikipedia -from PIL import ImageGrab - -from commands.utils import load_email_config from commands.voice_interface import VoiceInterface from infra import __is_darwin, __is_posix, __is_windows, __system_os -SUPPORTED_FEATURES = { - "search your query in google and return upto 10 results", - "get a wikipedia search summary of upto 3 sentences", - "open applications or websites", - "tell you the time of the day", - "scroll the screen with active cursor", -} ########## Conditional Imports ########## if __is_windows(): from AppOpener import open as open_app ########## Conditional Imports ########## -ENVIRONMENT_VARIABLES = dotenv_values(".env") - - -def explain_features(vi: VoiceInterface) -> None: - """Explains the features available - - Args: - vi (VoiceInterface): The voice interface instance used to speak the text - """ - vi.speak("Here's what I can do...\n") - for feature in SUPPORTED_FEATURES: - vi.speak(f"--> {feature}") - - -def run_search_query(vi: VoiceInterface, search_query: str) -> None: - """Performs google search based on some terms - - Args: - vi (VoiceInterface): VoiceInterface instance used to speak - search_query (str): the query term to be searched in google - """ - if not search_query: - vi.speak("Invalid Google Search Query Found!!") - return - - results = googlesearch.search(term=search_query) - if not results: - vi.speak("No Search Result Found!!") - else: - results = list(results) - vi.speak("Found Following Results: ") - for i, result in enumerate(results): - print(i + 1, ")", result.title) - - -def wikipedia_search( - vi: VoiceInterface, search_query: str, sentence_count: int = 3 -) -> None: - """Searches wikipedia for the given query and returns fixed number of statements in response. - Disambiguation Error due to multiple similar results is handled. - Speaks the options in this case. - - Args: - vi (VoiceInterface): VoiceInterface instance used to speak. - search_query (str): The query term to search in wikipedia - sentence_count (int, optional): The number of sentences to speak in case of direct match. - Default is 3. - """ - try: - vi.speak("Searching Wikipedia...") - results = wikipedia.summary(search_query, sentences=sentence_count) - - vi.speak("According to wikipedia...") - vi.speak(results) - except wikipedia.DisambiguationError as de: - vi.speak(f"\n{de.__class__.__name__}") - options = str(de).split("\n") - if len(options) < 7: - for option in options: - vi.speak(option) - else: - for option in options[0:6]: - vi.speak(option) - vi.speak("... and more") - def open_application_website(vi: VoiceInterface, search_query: str) -> None: """ @@ -213,125 +130,3 @@ def __open_application_website_posix(vi: VoiceInterface, search_query: str) -> N except TimeoutExpired as error: vi.speak(f"Error: {error}: Call to open {search_query} timed out.") - - -def tell_time(vi: VoiceInterface) -> None: - """Tells the time of the day with timezone - - Args: - vi (VoiceInterface): Voice interface instance used to speak - """ - date_time = datetime.now() - hour, minute, second = date_time.hour, date_time.minute, date_time.second - tmz = date_time.tzname() - - vi.speak(f"Current time is {hour}:{minute}:{second} {tmz}") - - -def start_gradual_scroll(direction: str, stop_event: threading.Event) -> None: - """Gradually scroll in the given direction until stop_event is set.""" - active_window = pygetwindow.getActiveWindow() - if not active_window: - return - - # Capture a portion of the window to ensure scrolling - left, top, right, bottom = 0, 0, 100, 100 - width = right - left - height = bottom - top - previous_image = ImageGrab.grab(bbox=(left, top, left + width, top + height)) - while not stop_event.is_set(): - pag.scroll(clicks=1) - current_image = ImageGrab.grab(bbox=(left, top, left + width, top + height)) - - if current_image.getdata() == previous_image.getdata(): - print("Reached to extreme") - stop_event.set() - break - previous_image = current_image - - print(f"Stopped scrolling {direction}.") - - -def start_scrolling(direction: str) -> tuple[threading.Thread, threading.Event]: - """Start a new scroll thread.""" - stop_scrolling_event = threading.Event() - scrolling_thread = threading.Thread( - target=start_gradual_scroll, args=(direction, stop_scrolling_event) - ) - scrolling_thread.start() - return scrolling_thread, stop_scrolling_event - - -def stop_scrolling( - scrolling_thread: threading.Thread, scrolling_thread_event: threading.Event -) -> None: - """Stop the scrolling thread if not already stopped.""" - if scrolling_thread is not None: - scrolling_thread_event.set() - scrolling_thread.join() - - -def scroll_to(direction: str) -> None: - """Scroll to the extreme in the given direction.""" - active_window = pygetwindow.getActiveWindow() - if not active_window: - return - time.sleep(0.5) - if direction == "top": - pag.press("home") - elif direction == "bottom": - pag.press("end") - elif direction == "right": - pag.press("right", presses=9999) - elif direction == "left": - pag.press("left", presses=9999) - else: - print("Invalid Command") - - -def simple_scroll(direction: str) -> None: - """Simple scroll in the given direction by a fixed number of steps.""" - active_window = pygetwindow.getActiveWindow() - if not active_window: - return - time.sleep(0.5) - if direction in ["up", "down", "left", "right"]: - pag.press(keys=direction, presses=25) - else: - print("Invalid direction") - - - - - -def fetch_news(vi: VoiceInterface, max_fetched_headlines: int) -> None: - """ - Fetches and reads out the top 5 headlines from the Google News RSS feed. - - This function fetches news headlines from the Google News RSS feed (specific to India in English). - It then reads out the top 5 headlines using the provided VoiceInterface instance. If the feed fetch is successful, - it reads the headlines one by one. If the fetch fails, it informs the user that the news couldn't be fetched. - - Args: - vi (VoiceInterface): The VoiceInterface instance used to speak the news headlines. - - Raises: - requests.exceptions.RequestException: If there is an issue while fetching the RSS feed. - AttributeError: If the feed does not contain expected attributes or entries. - """ - - feed_url = "https://news.google.com/rss?hl=en-IN&gl=IN&ceid=IN:en" - - vi.speak("Fetching news from servers.") - feed = feedparser.parse(feed_url) - if feed.status == 200: - headlines_list = [] - for entry in feed.entries[:max_fetched_headlines]: - headlines_list.append((entry.title).split(" -")[0]) - vi.speak("Here are some recent news headlines.") - for headline in headlines_list: - vi.speak(headline) - else: - vi.speak("Failed to fetch the news.") - - From 8dffbdc06396670673573285d7f4e8cf5d96bd7d Mon Sep 17 00:00:00 2001 From: AmanDevelops Date: Mon, 10 Feb 2025 21:45:16 +0530 Subject: [PATCH 10/12] [optimisation]: optimised opener functionality and clear screen support --- src/assistant.py | 5 +++-- src/{ => commands}/infra.py | 0 .../website_oppener.py} | 16 +--------------- 3 files changed, 4 insertions(+), 17 deletions(-) rename src/{ => commands}/infra.py (100%) rename src/{commands_temp.py => commands/website_oppener.py} (92%) diff --git a/src/assistant.py b/src/assistant.py index 9246c55..8a7412d 100755 --- a/src/assistant.py +++ b/src/assistant.py @@ -23,7 +23,8 @@ from commands.news_reporter import fetch_news from commands.basic_features import * from commands import scroller -from infra import clear_screen +from commands.infra import clear_screen +from commands.website_oppener import * LISTENING_ERROR = "Say that again please..." @@ -98,7 +99,7 @@ def execute_query(self, query: str) -> None: return application = application[0] try: - commands.open_application_website(self.__voice_interface, application) + open_application_website(self.__voice_interface, application) except ValueError as ve: print( f"Error occurred while opening {application}: {ve.__class__.__name__}: {ve}" diff --git a/src/infra.py b/src/commands/infra.py similarity index 100% rename from src/infra.py rename to src/commands/infra.py diff --git a/src/commands_temp.py b/src/commands/website_oppener.py similarity index 92% rename from src/commands_temp.py rename to src/commands/website_oppener.py index 590e14e..819a573 100644 --- a/src/commands_temp.py +++ b/src/commands/website_oppener.py @@ -1,27 +1,13 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Features -=============== - -This module contains all the functions pertaining to implementing the -individual features of the Assistant. - -""" - - import subprocess from subprocess import CalledProcessError, TimeoutExpired from commands.voice_interface import VoiceInterface -from infra import __is_darwin, __is_posix, __is_windows, __system_os +from commands.infra import __is_darwin, __is_posix, __is_windows, __system_os -########## Conditional Imports ########## if __is_windows(): from AppOpener import open as open_app -########## Conditional Imports ########## def open_application_website(vi: VoiceInterface, search_query: str) -> None: From 1ea15d65f8b5d679b2d9480fdf4e35c77037d4d0 Mon Sep 17 00:00:00 2001 From: AmanDevelops Date: Mon, 10 Feb 2025 21:51:28 +0530 Subject: [PATCH 11/12] [optimisation]: minor update --- src/assistant.py | 4 +- src/commands/basic_features.py | 25 +++++++++ src/commands/brightness_control.py | 34 ------------ src/commands/modify_settings.py | 83 ++++++++++++++++++++++++++++++ src/commands/volume_control.py | 35 ------------- src/commands/website_oppener.py | 21 ++++++++ 6 files changed, 130 insertions(+), 72 deletions(-) delete mode 100644 src/commands/brightness_control.py create mode 100644 src/commands/modify_settings.py delete mode 100644 src/commands/volume_control.py diff --git a/src/assistant.py b/src/assistant.py index 8a7412d..e71e449 100755 --- a/src/assistant.py +++ b/src/assistant.py @@ -13,11 +13,9 @@ import subprocess from datetime import datetime -import commands from commands.send_email import send_email from commands.utils import load_email_config -from commands.brightness_control import brightness_control -from commands.volume_control import volume_control +from commands.modify_settings import * from commands.weather_reporter import weather_reporter from commands.voice_interface import VoiceInterface from commands.news_reporter import fetch_news diff --git a/src/commands/basic_features.py b/src/commands/basic_features.py index ce42f6f..dafd452 100644 --- a/src/commands/basic_features.py +++ b/src/commands/basic_features.py @@ -1,3 +1,28 @@ +""" +Basic Features +=============== + +This module contains basic features for the desktop assistant, including: + +- Explaining supported features +- Running Google search queries +- Telling the current time +- Performing Wikipedia searches + +Functions: + explain_features(vi: VoiceInterface) -> None: + Explains the features available. + + run_search_query(vi: VoiceInterface, search_query: str) -> None: + Performs a Google search based on some terms. + + tell_time(vi: VoiceInterface) -> None: + Tells the current time of the day with timezone. + + wikipedia_search(vi: VoiceInterface, search_query: str, sentence_count: int = 3) -> None: + Searches Wikipedia for the given query and returns a fixed number of sentences in response. +""" + from .voice_interface import VoiceInterface import googlesearch from datetime import datetime diff --git a/src/commands/brightness_control.py b/src/commands/brightness_control.py deleted file mode 100644 index 755edf4..0000000 --- a/src/commands/brightness_control.py +++ /dev/null @@ -1,34 +0,0 @@ -import wmi - - -def brightness_control(value: int, relative: bool, toDecrease: bool): - """ - Adjusts the brightness of the monitor. - - Args: - value (int): The brightness level to set or adjust by. Should be between 0 and 100. - relative (bool): If True, the brightness change is relative to the current brightness. - If False, the brightness is set to the specified value. - toDecrease (bool): If True, decreases the brightness by the specified value. - If False, increases the brightness by the specified value. Only applicable when `relative` is True. - - Raises: - RuntimeError: If there is an issue with accessing the brightness control methods. - - Returns: - None - """ - - brightness_ctrl = wmi.WMI(namespace="root\\wmi") - methods = brightness_ctrl.WmiMonitorBrightnessMethods()[0] - - if relative: - current_brightness = brightness_ctrl.WmiMonitorBrightness()[0].CurrentBrightness - set_brightnes = ( - current_brightness - int(value) - if toDecrease - else current_brightness + int(value) - ) - methods.WmiSetBrightness(set_brightnes, 0) - else: - methods.WmiSetBrightness(value, 0) diff --git a/src/commands/modify_settings.py b/src/commands/modify_settings.py new file mode 100644 index 0000000..58f6f95 --- /dev/null +++ b/src/commands/modify_settings.py @@ -0,0 +1,83 @@ +""" +Modify Settings +=============== + +This module contains functions to adjust system settings such as brightness and volume. + +Functions: + brightness_control(value: int, relative: bool, toDecrease: bool) -> None: + Adjusts the brightness of the monitor. + + volume_control(value: int, relative: bool, toDecrease: bool) -> None: + Adjusts the master volume of the system. +""" + +import wmi +from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume +from comtypes import CLSCTX_ALL + + +def brightness_control(value: int, relative: bool, toDecrease: bool): + """ + Adjusts the brightness of the monitor. + + Args: + value (int): The brightness level to set or adjust by. Should be between 0 and 100. + relative (bool): If True, the brightness change is relative to the current brightness. + If False, the brightness is set to the specified value. + toDecrease (bool): If True, decreases the brightness by the specified value. + If False, increases the brightness by the specified value. Only applicable when `relative` is True. + + Raises: + RuntimeError: If there is an issue with accessing the brightness control methods. + + Returns: + None + """ + + brightness_ctrl = wmi.WMI(namespace="root\\wmi") + methods = brightness_ctrl.WmiMonitorBrightnessMethods()[0] + + if relative: + current_brightness = brightness_ctrl.WmiMonitorBrightness()[0].CurrentBrightness + set_brightnes = ( + current_brightness - int(value) + if toDecrease + else current_brightness + int(value) + ) + methods.WmiSetBrightness(set_brightnes, 0) + else: + methods.WmiSetBrightness(value, 0) + + +def volume_control(value: int, relative: bool, toDecrease: bool): + """ + Adjusts the master volume of the system. + + Args: + value (int): The volume level to set or adjust by. Should be between 0 and 100. + relative (bool): If True, the volume change is relative to the current volume. + If False, the volume is set to the specified value. + toDecrease (bool): If True, decreases the volume by the specified value. + If False, increases the volume by the specified value. Only applicable when `relative` is True. + + Raises: + RuntimeError: If there is an issue with accessing the audio endpoint. + + Returns: + None + """ + + devices = AudioUtilities.GetSpeakers() + interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None) + volume = interface.QueryInterface(IAudioEndpointVolume) + + if relative: + current_volume = volume.GetMasterVolumeLevelScalar() * 100 + set_volume = ( + current_volume - int(value) if toDecrease else current_volume + int(value) + ) + print(set_volume) + volume.SetMasterVolumeLevelScalar(min(max(0, set_volume), 100) / 100, None) + else: + volume.SetMasterVolumeLevelScalar(min(max(0, value), 100) / 100, None) diff --git a/src/commands/volume_control.py b/src/commands/volume_control.py deleted file mode 100644 index 5e1d9be..0000000 --- a/src/commands/volume_control.py +++ /dev/null @@ -1,35 +0,0 @@ -from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume -from comtypes import CLSCTX_ALL - - -def volume_control(value: int, relative: bool, toDecrease: bool): - """ - Adjusts the master volume of the system. - - Args: - value (int): The volume level to set or adjust by. Should be between 0 and 100. - relative (bool): If True, the volume change is relative to the current volume. - If False, the volume is set to the specified value. - toDecrease (bool): If True, decreases the volume by the specified value. - If False, increases the volume by the specified value. Only applicable when `relative` is True. - - Raises: - RuntimeError: If there is an issue with accessing the audio endpoint. - - Returns: - None - """ - - devices = AudioUtilities.GetSpeakers() - interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None) - volume = interface.QueryInterface(IAudioEndpointVolume) - - if relative: - current_volume = volume.GetMasterVolumeLevelScalar() * 100 - set_volume = ( - current_volume - int(value) if toDecrease else current_volume + int(value) - ) - print(set_volume) - volume.SetMasterVolumeLevelScalar(min(max(0, set_volume), 100) / 100, None) - else: - volume.SetMasterVolumeLevelScalar(min(max(0, value), 100) / 100, None) diff --git a/src/commands/website_oppener.py b/src/commands/website_oppener.py index 819a573..678f9cd 100644 --- a/src/commands/website_oppener.py +++ b/src/commands/website_oppener.py @@ -1,3 +1,24 @@ +""" +Website Opener +=============== + +This module contains functions to open applications or websites based on the user's query. +It supports different operating systems including Windows, Darwin (macOS), and POSIX (Linux). + +Functions: + open_application_website(vi: VoiceInterface, search_query: str) -> None: + Opens the application or website using a matching path from AppPath/WebPath dictionaries. + + __open_application_website_windows(vi: VoiceInterface, search_query: str) -> None: + Handles the opening of application/website for Windows OS. + + __open_application_website_darwin(vi: VoiceInterface, search_query: str) -> None: + Handles the opening of application/website for Darwin OS. + + __open_application_website_posix(vi: VoiceInterface, search_query: str) -> None: + Handles the opening of application/website for POSIX OS. +""" + import subprocess from subprocess import CalledProcessError, TimeoutExpired From 23bc52705cfdf994afa59189e34b9530c09ca10c Mon Sep 17 00:00:00 2001 From: AmanDevelops Date: Mon, 10 Feb 2025 21:55:47 +0530 Subject: [PATCH 12/12] added docstring --- src/assistant.py | 13 ++++++------- src/commands/basic_features.py | 7 +++++-- src/commands/modify_settings.py | 4 +++- src/commands/news_reporter.py | 21 +++++++++++++++++++++ src/commands/scroller.py | 32 +++++++++++++++++++++++++++++--- src/commands/send_email.py | 22 ++++++++++++++++++++++ src/commands/utils.py | 16 ++++++++++++++++ src/commands/weather_reporter.py | 23 +++++++++++++++++++++++ src/commands/website_oppener.py | 6 +++--- 9 files changed, 128 insertions(+), 16 deletions(-) diff --git a/src/assistant.py b/src/assistant.py index e71e449..0809cde 100755 --- a/src/assistant.py +++ b/src/assistant.py @@ -13,18 +13,17 @@ import subprocess from datetime import datetime +from commands import scroller +from commands.basic_features import * +from commands.infra import clear_screen +from commands.modify_settings import * +from commands.news_reporter import fetch_news from commands.send_email import send_email from commands.utils import load_email_config -from commands.modify_settings import * -from commands.weather_reporter import weather_reporter from commands.voice_interface import VoiceInterface -from commands.news_reporter import fetch_news -from commands.basic_features import * -from commands import scroller -from commands.infra import clear_screen +from commands.weather_reporter import weather_reporter from commands.website_oppener import * - LISTENING_ERROR = "Say that again please..." MAX_FETCHED_HEADLINES = ( 10 # Maximum number of news headlines to fetch when news function is called diff --git a/src/commands/basic_features.py b/src/commands/basic_features.py index dafd452..47f216a 100644 --- a/src/commands/basic_features.py +++ b/src/commands/basic_features.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- """ Basic Features =============== @@ -23,12 +25,13 @@ Searches Wikipedia for the given query and returns a fixed number of sentences in response. """ -from .voice_interface import VoiceInterface -import googlesearch from datetime import datetime +import googlesearch import wikipedia +from .voice_interface import VoiceInterface + SUPPORTED_FEATURES = { "search your query in google and return upto 10 results", "get a wikipedia search summary of upto 3 sentences", diff --git a/src/commands/modify_settings.py b/src/commands/modify_settings.py index 58f6f95..34c6ae9 100644 --- a/src/commands/modify_settings.py +++ b/src/commands/modify_settings.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- """ Modify Settings =============== @@ -13,8 +15,8 @@ """ import wmi -from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume from comtypes import CLSCTX_ALL +from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume def brightness_control(value: int, relative: bool, toDecrease: bool): diff --git a/src/commands/news_reporter.py b/src/commands/news_reporter.py index f939e39..94eafc2 100644 --- a/src/commands/news_reporter.py +++ b/src/commands/news_reporter.py @@ -1,4 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +News Reporter +=============== + +This module contains functions to fetch and read out the top news headlines. + +Functions: + fetch_news(vi: VoiceInterface, max_fetched_headlines: int) -> None: + Fetches and reads out the top headlines from the Google News RSS feed. + + Args: + vi (VoiceInterface): The VoiceInterface instance used to speak the news headlines. + max_fetched_headlines (int): The maximum number of headlines to fetch and read out. + + Raises: + requests.exceptions.RequestException: If there is an issue while fetching the RSS feed. + AttributeError: If the feed does not contain expected attributes or entries. +""" import feedparser + from .voice_interface import VoiceInterface diff --git a/src/commands/scroller.py b/src/commands/scroller.py index 1adeb2a..952b202 100644 --- a/src/commands/scroller.py +++ b/src/commands/scroller.py @@ -1,7 +1,33 @@ -import pygetwindow -import pyautogui as pag -import time +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Scroller +=============== + +This module contains functions to handle scrolling actions for the desktop assistant. + +Functions: + start_gradual_scroll(direction: str, stop_event: threading.Event) -> None: + Gradually scrolls in the given direction until the stop_event is set. + + start_scrolling(direction: str) -> tuple[threading.Thread, threading.Event]: + Starts a new thread to handle gradual scrolling. + + stop_scrolling(scrolling_thread: threading.Thread, scrolling_thread_event: threading.Event) -> None: + Stops the scrolling thread if it is not already stopped. + + scroll_to(direction: str) -> None: + Scrolls to the extreme in the given direction. + + simple_scroll(direction: str) -> None: + Performs a simple scroll in the given direction by a fixed number of steps. +""" + import threading +import time + +import pyautogui as pag +import pygetwindow from PIL import ImageGrab diff --git a/src/commands/send_email.py b/src/commands/send_email.py index d8d618c..240de52 100644 --- a/src/commands/send_email.py +++ b/src/commands/send_email.py @@ -1,3 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Send Email +=============== + +This module contains functions to send emails using SMTP. + +Functions: + send_email(vi: VoiceInterface, toEmail: str, subject: str, body: str) -> None: + Sends an email to the specified recipient. + + Args: + vi (VoiceInterface): VoiceInterface instance used to speak. + toEmail (str): The recipient's email address. + subject (str): The subject of the email. + body (str): The body content of the email. + + Raises: + ValueError: If any required parameters are missing or invalid. +""" + import smtplib import ssl from email.message import EmailMessage diff --git a/src/commands/utils.py b/src/commands/utils.py index 09cbd27..93d7e37 100644 --- a/src/commands/utils.py +++ b/src/commands/utils.py @@ -1,3 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Utils +=============== + +This module contains utility functions for the desktop assistant. + +Functions: + load_email_config() -> dict: + Loads the email configuration from a JSON file. + + Returns: + dict: The email configuration data. +""" + import json import os diff --git a/src/commands/weather_reporter.py b/src/commands/weather_reporter.py index e4789b9..72721b3 100644 --- a/src/commands/weather_reporter.py +++ b/src/commands/weather_reporter.py @@ -1,4 +1,27 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Weather Reporter +=============== + +This module contains functions to fetch and report the weather conditions for a given city. + +Functions: + weather_reporter(vi: VoiceInterface, city_name: str) -> None: + Fetches and reports the weather conditions for a given city. + + Args: + vi (VoiceInterface): The VoiceInterface instance used to speak the weather report. + city_name (str): The name of the city for which to fetch weather data. + + Raises: + requests.exceptions.RequestException: If there is an issue with the API request. + IndexError: If the city name is not found in the API response. + KeyError: If expected weather data fields are missing from the response. +""" + import requests + from .voice_interface import VoiceInterface diff --git a/src/commands/website_oppener.py b/src/commands/website_oppener.py index 678f9cd..b7c1f1a 100644 --- a/src/commands/website_oppener.py +++ b/src/commands/website_oppener.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- """ Website Opener =============== @@ -22,10 +24,8 @@ import subprocess from subprocess import CalledProcessError, TimeoutExpired - -from commands.voice_interface import VoiceInterface from commands.infra import __is_darwin, __is_posix, __is_windows, __system_os - +from commands.voice_interface import VoiceInterface if __is_windows(): from AppOpener import open as open_app