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