From 8e51eb94f60ccaa5d80c44eb180bc75138b92858 Mon Sep 17 00:00:00 2001 From: Hugo Saporetti Junior Date: Tue, 10 Sep 2024 15:57:51 -0300 Subject: [PATCH] Add Scheduler Demo --- dependencies.hspd | 2 +- src/demo/components/camera_demo.py | 16 +- src/demo/components/scheduler_demo.py | 68 ++++++ src/demo/others/screenshot_demo.py | 2 +- src/demo/utils.py | 11 +- src/main/askai/core/askai_cli.py | 6 +- src/main/askai/core/askai_configs.py | 4 + .../core/commander/commands/settings_cmd.py | 19 +- src/main/askai/core/component/recorder.py | 45 ++-- src/main/askai/core/component/scheduler.py | 210 +++++++++++++----- src/main/askai/tui/askai_app.py | 4 +- src/main/requirements.txt | 3 +- 12 files changed, 289 insertions(+), 101 deletions(-) create mode 100644 src/demo/components/scheduler_demo.py diff --git a/dependencies.hspd b/dependencies.hspd index dfd1f5ac..ca56202e 100644 --- a/dependencies.hspd +++ b/dependencies.hspd @@ -8,7 +8,7 @@ */ /* HSPyLib projects */ -package: hspylib, version: 1.12.46, mode: ge +package: hspylib, version: 1.12.47, mode: ge package: hspylib-clitt, version: 0.9.132, mode: ge package: hspylib-setman, version: 0.10.35, mode: ge diff --git a/src/demo/components/camera_demo.py b/src/demo/components/camera_demo.py index c5521361..020ac648 100644 --- a/src/demo/components/camera_demo.py +++ b/src/demo/components/camera_demo.py @@ -1,14 +1,14 @@ -from askai.core.component.camera import camera -from askai.core.component.image_store import ImageMetadata, store -from askai.core.features.router.tools.terminal import open_command -from askai.core.support.utilities import display_text +import os +from textwrap import dedent + from clitt.core.term.cursor import cursor from clitt.core.tui.line_input.line_input import line_input from hspylib.core.tools.text_tools import strip_escapes -from textwrap import dedent -from utils import init_context -import os +from askai.core.component.camera import camera +from askai.core.component.image_store import ImageMetadata, store +from askai.core.features.router.tools.terminal import open_command +from utils import init_context MENU = dedent( f"""Camera Demo options @@ -27,7 +27,7 @@ if __name__ == "__main__": - init_context(log_name="camera-demo") + init_context("camera-demo") photo: ImageMetadata while opt := line_input(MENU, placeholder="Select an option"): cursor.write() diff --git a/src/demo/components/scheduler_demo.py b/src/demo/components/scheduler_demo.py new file mode 100644 index 00000000..c86644b6 --- /dev/null +++ b/src/demo/components/scheduler_demo.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + @project: HsPyLib-AskAI + @package: demo.components + @file: scheduler-demo.py + @created: Tue, 14 May 2024 + @author: Hugo Saporetti Junior + @site: https://github.com/yorevs/askai + @license: MIT - Please refer to + + Copyright (c) 2024, HomeSetup +""" +import logging as log + +from askai.core.component.scheduler import scheduler +from hspylib.core.zoned_datetime import now, SIMPLE_DATETIME_FORMAT + +from utils import init_context + + +def echo(msg: str): + log.info(f"{msg} {now('[%H:%M:%S]')}") + + +@scheduler.every(2000, 2000) +def every_2_seconds(): + echo("EVERY-2") + + +@scheduler.every(4000, 2000) +def every_4_seconds(): + echo("EVERY-4") + + +@scheduler.every(8000, 2000) +def every_8_seconds(): + echo("EVERY-8") + + +# @scheduler.at(16, 26, 0) +# def at_1(): +# echo("AT-1") +# +# +# @scheduler.at(16, 26, 10) +# def at_2(): +# echo("AT-2") +# +# +# @scheduler.at(16, 26, 20) +# def at_3(): +# echo("AT-3") + + +@scheduler.after(second=20) +def after_20_seconds(): + echo("AFTER-20s") + + +if __name__ == "__main__": + init_context("scheduler-demo", rich_logging=False, console_enable=True, log_level=log.INFO) + log.info("-=" * 40) + log.info(f"AskAI Scheduler Demo - {scheduler.now.strftime(SIMPLE_DATETIME_FORMAT)}") + log.info("-=" * 40) + scheduler.start() + scheduler.join() diff --git a/src/demo/others/screenshot_demo.py b/src/demo/others/screenshot_demo.py index d8d0feca..6db675a2 100644 --- a/src/demo/others/screenshot_demo.py +++ b/src/demo/others/screenshot_demo.py @@ -4,5 +4,5 @@ from utils import init_context if __name__ == "__main__": - init_context(log_name="camera-demo") + init_context("camera-demo") sysout(take_screenshot("gabiroba.jpeg")) diff --git a/src/demo/utils.py b/src/demo/utils.py index 6e45544a..7fb0d554 100644 --- a/src/demo/utils.py +++ b/src/demo/utils.py @@ -22,15 +22,22 @@ def init_context( log_name: str | None = None, - context_size: int = 1000, log_level: int = log.NOTSET, + rich_logging: bool = False, + console_enable: bool = False, + context_size: int = 1000, engine_name: Literal["openai"] = "openai", model_name: Literal["gpt-3.5-turbo", "gpt-4", "gpt-4o"] = "gpt-3.5-turbo", ) -> None: """Initialize AskAI context and startup components.""" if log_name: log_dir: str = os.environ.get("HHS_LOG_DIR", os.getcwd()) - log_init(f"{os.path.join(log_dir, ensure_endswith(log_name, '.log'))}", level=log_level) + log_init( + filename=f"{os.path.join(log_dir, ensure_endswith(log_name, '.log'))}", + level=log_level, + rich_logging=rich_logging, + console_enable=console_enable, + ) KeyboardInput.preload_history(cache.load_input_history(commands())) shared.create_engine(engine_name=engine_name, model_name=model_name) shared.create_context(context_size) diff --git a/src/main/askai/core/askai_cli.py b/src/main/askai/core/askai_cli.py index 67d1ffa7..1bff768a 100644 --- a/src/main/askai/core/askai_cli.py +++ b/src/main/askai/core/askai_cli.py @@ -58,7 +58,7 @@ def __init__( query_string: QueryString, ): - configs.is_interactive = configs.is_interactive if not query_prompt else False + configs.is_interactive = interactive if not query_prompt else False super().__init__(interactive, speak, debug, cacheable, tempo, engine_name, model_name) os.environ["ASKAI_APP"] = (self.RunModes.ASKAI_CLI if interactive else self.RunModes.ASKAI_CMD).value self._ready: bool = False @@ -136,14 +136,14 @@ def _cb_mic_listening_event(self, ev: Event) -> None: :param ev: The event object representing the microphone listening event. """ if ev.args.listening: - self._reply(msg.listening()) + self._reply(AIReply.info(msg.listening())) def _cb_device_changed_event(self, ev: Event) -> None: """Callback to handle audio input device change events. :param ev: The event object representing the device change. """ cursor.erase_line() - self._reply(msg.device_switch(str(ev.args.device))) + self._reply(AIReply.info(msg.device_switch(str(ev.args.device)))) def _splash(self, interval: int = 250) -> None: """Display the AskAI splash screen until the system is fully started and ready. This method shows the splash diff --git a/src/main/askai/core/askai_configs.py b/src/main/askai/core/askai_configs.py index 2bd0d8bf..441560db 100644 --- a/src/main/askai/core/askai_configs.py +++ b/src/main/askai/core/askai_configs.py @@ -205,5 +205,9 @@ def remove_device(self, device_name: str) -> None: self._recorder_devices.remove(device_name) settings.put("askai.recorder.devices", ", ".join(self._recorder_devices)) + def clear_devices(self) -> None: + """Remove all devices from the configuration list.""" + self._recorder_devices.clear() + assert (configs := AskAiConfigs().INSTANCE) is not None diff --git a/src/main/askai/core/commander/commands/settings_cmd.py b/src/main/askai/core/commander/commands/settings_cmd.py index 80e267ea..995a4170 100644 --- a/src/main/askai/core/commander/commands/settings_cmd.py +++ b/src/main/askai/core/commander/commands/settings_cmd.py @@ -14,13 +14,14 @@ """ from abc import ABC +from typing import Any, Optional + +from askai.core.askai_configs import configs from askai.core.askai_settings import settings -from askai.core.component.recorder import recorder from askai.core.support.text_formatter import text_formatter from askai.core.support.utilities import display_text from hspylib.core.tools.commons import sysout from setman.settings.settings_entry import SettingsEntry -from typing import Any, Optional class SettingsCmd(ABC): @@ -65,6 +66,16 @@ def get(key: str) -> Optional[SettingsEntry]: @staticmethod def reset() -> None: """Reset all settings to their default values.""" + # Command arguments settings must be kept as it was. + is_interactive: bool = settings.get_bool("askai.interactive.enabled") + is_speak: bool = settings.get_bool("askai.speak.enabled") + is_debug: bool = settings.get_bool("askai.debug.enabled") + is_cache: bool = settings.get_bool("askai.cache.enabled") settings.defaults() - # Include the current audio input. - settings.put("askai.recorder.devices", recorder.input_device[1] or "") + configs.clear_devices() + # Put back the command argument settings. + settings.put("askai.interactive.enabled", is_interactive) + settings.put("askai.speak.enabled", is_speak) + settings.put("askai.debug.enabled", is_debug) + settings.put("askai.cache.enabled", is_cache) + text_formatter.cmd_print(f"%GREEN%Factory settings reset!%NC%") diff --git a/src/main/askai/core/component/recorder.py b/src/main/askai/core/component/recorder.py index a81e10b8..9f6b124c 100644 --- a/src/main/askai/core/component/recorder.py +++ b/src/main/askai/core/component/recorder.py @@ -12,11 +12,17 @@ Copyright (c) 2024, HomeSetup """ +import logging as log +import operator +import sys +from pathlib import Path +from typing import Callable, Optional, TypeAlias + from askai.core.askai_configs import configs from askai.core.askai_events import events from askai.core.askai_messages import msg from askai.core.component.cache_service import REC_DIR -from askai.core.component.scheduler import Scheduler +from askai.core.component.scheduler import scheduler from askai.core.model.ai_reply import AIReply from askai.core.support.utilities import display_text, seconds from askai.exception.exceptions import InvalidInputDevice, InvalidRecognitionApiError @@ -27,13 +33,7 @@ from hspylib.core.preconditions import check_argument, check_state from hspylib.core.zoned_datetime import now_ms from hspylib.modules.application.exit_status import ExitStatus -from pathlib import Path from speech_recognition import AudioData, Microphone, Recognizer, RequestError, UnknownValueError, WaitTimeoutError -from typing import Callable, Optional, TypeAlias - -import logging as log -import operator -import sys InputDevice: TypeAlias = tuple[int, str] @@ -63,21 +63,7 @@ def get_device_list(cls) -> list[InputDevice]: devices.append((index, name)) return devices - def __init__(self): - self._rec: Recognizer = Recognizer() - self._devices: list[InputDevice] = [] - self._device_index: int | None = None - self._input_device: InputDevice | None = None - self._old_device = None - - def setup(self) -> None: - """Setup the microphone recorder.""" - self._devices = self.get_device_list() - log.debug("Available audio devices:\n%s", "\n".join([f"{d[0]} - {d[1]}" for d in self._devices])) - self._select_device() - @staticmethod - @Scheduler.every(3000, 5000) def __device_watcher() -> None: """Monitor audio input devices for being plugged in or unplugged. This method periodically checks the status of audio input devices to detect any changes. @@ -96,6 +82,19 @@ def __device_watcher() -> None: break recorder.devices = new_list + def __init__(self): + self._rec: Recognizer = Recognizer() + self._devices: list[InputDevice] = [] + self._device_index: int | None = None + self._input_device: InputDevice | None = None + + def setup(self) -> None: + """Setup the microphone recorder.""" + self._devices = self.get_device_list() + log.debug("Available audio devices:\n%s", "\n".join([f"{d[0]} - {d[1]}" for d in self._devices])) + self._select_device() + scheduler.set_interval(3000, self.__device_watcher, 5000) + @property def devices(self) -> list[InputDevice]: return sorted(self._devices if self._devices else [], key=lambda x: x[0]) @@ -238,10 +237,10 @@ def _select_device(self) -> None: """Select a device for recording.""" available: list[str] = list(filter(lambda d: d, map(str.strip, configs.recorder_devices))) device: InputDevice | None = None - devices: list[InputDevice] = list(reversed(self.devices)) + devices: list[InputDevice] = self.devices while devices and not device: if available: - for dev in devices: + for dev in list(reversed(devices)): # Reverse to get any headphone or external device if dev[1] in available and self.set_device(dev): device = dev break diff --git a/src/main/askai/core/component/scheduler.py b/src/main/askai/core/component/scheduler.py index 53049983..d72625eb 100644 --- a/src/main/askai/core/component/scheduler.py +++ b/src/main/askai/core/component/scheduler.py @@ -12,16 +12,18 @@ Copyright (c) 2024, HomeSetup """ - +import inspect +import os +import threading from datetime import datetime, timedelta -from hspylib.core.metaclass.singleton import Singleton -from hspylib.core.preconditions import check_argument from threading import Thread from time import monotonic from typing import Any, Callable, Iterable, Mapping import pause -import threading +from hspylib.core.metaclass.singleton import Singleton +from hspylib.core.preconditions import check_argument +from hspylib.core.zoned_datetime import SIMPLE_DATETIME_FORMAT class Scheduler(Thread, metaclass=Singleton): @@ -32,40 +34,42 @@ class Scheduler(Thread, metaclass=Singleton): INSTANCE: "Scheduler" - _done = False - - def __init__(self): - super().__init__() - self._today = datetime.today() - self._threads: list[Thread] = [] - self._start_time = monotonic() + _DONE: bool = False @staticmethod - def every(interval_ms: int, delay_ms: int): - """Decorator to schedule a function to be run periodically. The decorated function will be executed every + def every(interval_ms: int, delay_ms: int = 0): + """ + Decorator to schedule a function to be run periodically. The decorated function will be executed every `interval_ms` milliseconds, with an initial delay of `delay_ms` milliseconds before the first execution. - Note: - - This decorator cannot be used for instance methods (methods with `self`). - - For scheduling instance methods, use the `set_interval` method. + Can be used with both instance methods (methods with `self`) and static or standalone functions. + :param interval_ms: The interval in milliseconds between consecutive executions of the decorated function. :param delay_ms: The initial delay in milliseconds before the first execution of the decorated function. :return: The decorated function. """ - def every_wrapper(func: Callable, *fargs, **fkwargs): - """'every' function wrapper.""" - return scheduler.set_interval(interval_ms, func, delay_ms, *fargs, **fkwargs) + def every_wrapper(func: Callable): + """Wrapper to handle both instance methods and static functions.""" + + def wrapped_function(*args, **kwargs): + # Check if the first argument is likely to be 'self' (i.e., method bound to an instance) + if len(args) > 0 and inspect.isclass(type(args[0])): + self = args[0] # The first argument is 'self' + return scheduler.set_interval(interval_ms, func, delay_ms, self, *args[1:], **kwargs) + else: + # It's either a static method or a standalone function + return scheduler.set_interval(interval_ms, func, delay_ms, *args, **kwargs) + + return wrapped_function() return every_wrapper @staticmethod - def at(hour: int, minute: int, second: int, millis: int): - """Decorator to schedule a function to be run periodically at a specific time each day. This decorator - schedules the decorated function to execute at the given hour, minute, second, and millisecond every day. It is - useful for tasks that need to be performed at a specific time daily. - Note: - - This decorator cannot be used to decorate instance methods (with `self`). For instance methods, - use the `schedule` method. + def at(hour: int, minute: int, second: int, millis: int = 0): + """ + Decorator to schedule a function to be run periodically at a specific time each day. This can handle both + instance methods (with `self`) and standalone functions. + :param hour: The hour of the day (0-23) when the function should run. :param minute: The minute of the hour (0-59) when the function should run. :param second: The second of the minute (0-59) when the function should run. @@ -73,30 +77,88 @@ def at(hour: int, minute: int, second: int, millis: int): :return: A decorator that schedules the function to run at the specified time. """ - def at_wrapper(func: Callable, *fargs, **fkwargs): - """'at' function wrapper.""" - return scheduler.schedule(hour, minute, second, millis, func, *fargs, **fkwargs) + def at_wrapper(func: Callable): + """Wrapper to handle both instance methods and static functions.""" + + def wrapped_function(*args, **kwargs): + # Check if the first argument is likely to be 'self' (i.e., method bound to an instance) + if len(args) > 0 and inspect.isclass(type(args[0])): + self = args[0] # The first argument is 'self' + return scheduler.schedule(hour, minute, second, millis, func, self, *args[1:], **kwargs) + else: + # It's either a static method or a standalone function + return scheduler.schedule(hour, minute, second, millis, func, *args, **kwargs) + + return wrapped_function() return at_wrapper + @staticmethod + def after(hour: int = 0, minute: int = 0, second: int = 0, microsecond: int = 0): + """ + Decorator to schedule a function to be run after the specified hour, minute, second, and microsecond delay. + Can be used for both instance methods (with `self`) and static or standalone functions. + + :param hour: Hours to delay + :param minute: Minutes to delay + :param second: Seconds to delay + :param microsecond: Microseconds to delay + :return: A decorator that schedules the function to run after the specified delay. + """ + + def after_wrapper(func: Callable): + """Wrapper to handle both instance methods and static functions.""" + + def wrapped_function(*args, **kwargs): + # Check if the first argument is likely to be 'self' (i.e., method bound to an instance) + if len(args) > 0 and inspect.isclass(type(args[0])): + self = args[0] # The first argument is 'self' + return scheduler.scheduler_after(hour, minute, second, microsecond, func, self, *args[1:], **kwargs) + else: + # It's either a static method or a standalone function + return scheduler.scheduler_after(hour, minute, second, microsecond, func, *args, **kwargs) + + return wrapped_function() + + return after_wrapper + + def __init__(self): + super().__init__() + self._relief_interval_ms: int = 100 + self._now: datetime = datetime.now() + self._not_started: list[Thread] = [] + self._threads: dict[str, Thread] = {} + self._start_time: float = monotonic() + + def __str__(self): + return ( + f"Started: {self.now.strftime(SIMPLE_DATETIME_FORMAT)}\n" + f"jobs:\n" + f"{' |-' + ' |-'.join([j + os.linesep for j, _ in self._threads.items()]) if self._threads else ''}" + ) + + @property + def now(self) -> datetime: + return self._now + def run(self) -> None: - while not self._done and threading.main_thread().is_alive(): - not_started = next((th for th in self._threads if not th.is_alive()), None) - if not_started: + while not self._DONE and threading.main_thread().is_alive(): + if not_started := next((th for th in self._not_started if not th.is_alive()), None): not_started.start() - self._threads.remove(not_started) - pause.milliseconds(500) + self._remove(not_started) + pause.milliseconds(self._relief_interval_ms) def start(self) -> None: if not self.is_alive(): super().start() + self._now = datetime.now() def schedule( self, - hh: int, - mm: int, - ss: int, - us: int, + hour: int, + minute: int, + second: int, + microsecond: int, callback: Callable, cb_fn_args: Iterable | None = None, cb_fn_kwargs: Mapping[str, Any] | None = None, @@ -104,33 +166,60 @@ def schedule( """Schedule a task to run at a specific time in the future. This method schedules the provided callback function to execute at the specified hour, minute, second, and microsecond on the current day. The function will be invoked with the specified arguments and keyword arguments. - :param hh: The hour of the day (0-23) when the task should run. - :param mm: The minute of the hour (0-59) when the task should run. - :param ss: The second of the minute (0-59) when the task should run. - :param us: The microsecond of the second (0-999999) when the task should run. + :param hour: The hour of the day (0-23) when the task should run. + :param minute: The minute of the hour (0-59) when the task should run. + :param second: The second of the minute (0-59) when the task should run. + :param microsecond: The microsecond of the second (0-999999) when the task should run. :param callback: The callback function to be executed at the scheduled time. :param cb_fn_args: The positional arguments to pass to the callback function. Defaults to None. :param cb_fn_kwargs: The keyword arguments to pass to the callback function. Defaults to None. """ - run_at: datetime = self._today.replace(day=self._today.day, hour=hh, minute=mm, second=ss, microsecond=us) - delta_t: timedelta = run_at - self._today + run_at: datetime = self.now.replace( + day=self.now.day, hour=hour, minute=minute, second=second, microsecond=microsecond + ) + delta_t: timedelta = run_at - self.now check_argument(delta_t.total_seconds() > 0, ">> Time is in the past <<") secs: float = max(0, delta_t.seconds) + 1 - def _call_it_back(): + def _call_it_back() -> None: """Continuously checks if the scheduled time has been reached and executes the callback function. The - method uses a 100ms pause between checks to avoid excessive CPU usage. + method uses a pause between checks to avoid excessive CPU usage. """ - while not self._done and threading.main_thread().is_alive(): - elapsed = monotonic() - self._start_time - if elapsed >= secs: + while not self._DONE and threading.main_thread().is_alive(): + if monotonic() - self._start_time >= secs: args = cb_fn_args if cb_fn_args else [] xargs = cb_fn_kwargs if cb_fn_kwargs else {} callback(*args, **xargs) return - pause.milliseconds(100) + pause.milliseconds(self._relief_interval_ms) - self._threads.append(Thread(name=f"Schedule-{callback.__name__}", target=_call_it_back)) + self._add(f"Scheduled-{callback.__name__}", _call_it_back) + + def scheduler_after( + self, + hh: int, + mm: int, + ss: int, + us: int, + callback: Callable, + cb_fn_args: Iterable | None = None, + cb_fn_kwargs: Mapping[str, Any] | None = None, + ) -> None: + """Schedule a function to be run after the specified hour, minute, second, and microsecond. + :param hh: Hours to delay + :param mm: Minutes to delay + :param ss: Seconds to delay + :param us: microsecond delay + :param callback: Function to be executed + :param cb_fn_args: Optional arguments to pass to the callback function + :param cb_fn_kwargs: Optional keyword arguments to pass to the callback function + """ + check_argument(any(num > 0 for num in [hh, mm, ss]), ">> Delay must be positive <<") + # fmt: off + delta_t: timedelta = timedelta(hours=hh, minutes=mm, seconds=ss, microseconds=us) + # fmt: on + run_at: datetime = self.now + delta_t + self.schedule(run_at.hour, run_at.minute, run_at.second, run_at.microsecond, callback, cb_fn_args, cb_fn_kwargs) def set_interval( self, @@ -150,19 +239,28 @@ def set_interval( :param cb_fn_kwargs: The keyword arguments to pass to the callback function. """ - def _call_it_back(): + def _call_it_back() -> None: """Internal method to repeatedly invoke the callback function at specified intervals. It uses the `pause.milliseconds()` method to handle the waiting periods between each invocation. """ - if delay_ms > 0: - pause.milliseconds(interval_ms) - while not self._done and threading.main_thread().is_alive(): + pause.milliseconds(interval_ms if delay_ms > 0 else 0) + while not self._DONE and threading.main_thread().is_alive(): args = cb_fn_args if cb_fn_args else [] xargs = cb_fn_kwargs if cb_fn_kwargs else {} callback(*args, **xargs) pause.milliseconds(interval_ms) - self._threads.append(Thread(name=f"SetInterval-{callback.__name__}", target=_call_it_back)) + self._add(f"Every-{callback.__name__}", _call_it_back) + + def _add(self, thread_name: str, callback: Callable, *args, **kwargs) -> None: + """TODO""" + th_new: Thread = Thread(name=thread_name, target=callback, args=args, kwargs=kwargs) + self._not_started.append(th_new) + self._threads[thread_name] = th_new + + def _remove(self, not_started: Thread) -> None: + """TODO""" + self._not_started.remove(not_started) assert (scheduler := Scheduler().INSTANCE) is not None diff --git a/src/main/askai/tui/askai_app.py b/src/main/askai/tui/askai_app.py index 33269ddf..c7bce9ca 100644 --- a/src/main/askai/tui/askai_app.py +++ b/src/main/askai/tui/askai_app.py @@ -341,13 +341,13 @@ def _cb_mic_listening_event(self, ev: Event) -> None: """ self.header.notifications.listening = ev.args.listening if ev.args.listening: - self._reply(msg.listening()) + self._reply(AIReply.info(msg.listening())) def _cb_device_changed_event(self, ev: Event) -> None: """Callback to handle audio input device change events. :param ev: The event object representing the device change. """ - self._reply(msg.device_switch(str(ev.args.device))) + self._reply(AIReply.info(msg.device_switch(str(ev.args.device)))) def _cb_mode_changed_event(self, ev: Event) -> None: """Callback to handle mode change events. diff --git a/src/main/requirements.txt b/src/main/requirements.txt index 37846749..4adf4726 100644 --- a/src/main/requirements.txt +++ b/src/main/requirements.txt @@ -1,6 +1,6 @@ ###### AUTO-GENERATED Requirements file for: AskAI ###### -hspylib>=1.12.46 +hspylib>=1.12.47 hspylib-clitt>=0.9.132 hspylib-setman>=0.10.35 retry2>=0.9.5 @@ -28,6 +28,7 @@ soundfile>=0.12.1 PyAudio>=0.2.14 SpeechRecognition>=3.10.4 opencv-python>=4.10.0.84 +pyautogui>=0.9.54 torch>=2.2.0 torchvision>=0.17.2 open-clip-torch