summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/config.py37
-rw-r--r--modules/input_handlers/__init__.py0
-rw-r--r--modules/input_handlers/input_handler.py11
-rw-r--r--modules/input_handlers/pipewire_record.py72
-rw-r--r--modules/input_handlers/stdin_input.py16
-rw-r--r--modules/intents/process.py6
-rw-r--r--modules/intents/sway.py8
-rw-r--r--modules/responses/__init__.py0
-rw-r--r--modules/responses/libnotify.py15
-rw-r--r--modules/responses/response_handler.py6
-rw-r--r--modules/responses/response_manager.py32
-rw-r--r--modules/responses/responses.py29
12 files changed, 224 insertions, 8 deletions
diff --git a/modules/config.py b/modules/config.py
index 602ec8d..c5e8d1d 100644
--- a/modules/config.py
+++ b/modules/config.py
@@ -1,20 +1,48 @@
+import sys
+
import __main__
import os.path
import yaml
class Config:
+ INPUT_PW = "pw-record"
+ INPUT_STDIN = "stdin"
+ INPUT_MODES = [INPUT_PW, INPUT_STDIN]
+
def __init__(self):
+ self.input_mode = ""
+
self.intents_dir = ""
+ self.responses_dir = ""
+ self.resources_dir = ""
self.applications_dir = ""
- def update(self, **entries):
+ self.lock = ""
+
+ self.hestia_images = ""
+
+ def update(self, **entries) -> None:
self.__dict__.update(entries)
- if not self.intents_dir.startswith("/"):
- self.intents_dir = os.path.join(os.path.dirname(__main__.__file__), self.intents_dir)
+ self.intents_dir = Config.__convert_to_absolute_path(self.intents_dir)
+ self.responses_dir = Config.__convert_to_absolute_path(self.responses_dir)
+ self.resources_dir = Config.__convert_to_absolute_path(self.resources_dir)
+
+ self.hestia_images = os.path.join(self.resources_dir, "hestia_images")
+
+ @staticmethod
+ def __convert_to_absolute_path(path: str) -> str:
+ if not path.startswith("/"):
+ path = os.path.join(os.path.dirname(__main__.__file__), path)
+
+ return path
+
+ def validate(self) -> None:
+ if self.input_mode not in self.INPUT_MODES:
+ sys.exit(f"Invalid input_mode '{self.input_mode}', valid options: {', '.join(self.INPUT_MODES)}")
-def load_config():
+def load_config() -> Config:
config = Config()
with open(os.path.join(os.path.dirname(__main__.__file__), "config.yaml")) as stream:
config.update(**yaml.safe_load(stream))
@@ -24,4 +52,5 @@ def load_config():
with open(user_config) as stream:
config.update(**yaml.safe_load(stream))
+ config.validate()
return config \ No newline at end of file
diff --git a/modules/input_handlers/__init__.py b/modules/input_handlers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/modules/input_handlers/__init__.py
diff --git a/modules/input_handlers/input_handler.py b/modules/input_handlers/input_handler.py
new file mode 100644
index 0000000..3db042c
--- /dev/null
+++ b/modules/input_handlers/input_handler.py
@@ -0,0 +1,11 @@
+from abc import ABC, abstractmethod
+
+class InputHandler(ABC):
+
+ @abstractmethod
+ def get_input(self) -> str:
+ pass
+
+ @abstractmethod
+ def cleanup(self) -> None:
+ pass \ No newline at end of file
diff --git a/modules/input_handlers/pipewire_record.py b/modules/input_handlers/pipewire_record.py
new file mode 100644
index 0000000..0a767e1
--- /dev/null
+++ b/modules/input_handlers/pipewire_record.py
@@ -0,0 +1,72 @@
+import subprocess
+import os.path
+import signal
+from time import sleep
+
+import whisper
+
+from .input_handler import InputHandler
+
+FIFO_PATH = "/tmp/hestia-listening"
+RECORD_PATH = "/tmp/hestia-record.mp3"
+
+class PipeWireRecord(InputHandler):
+ def cleanup(self) -> None:
+ if os.path.exists(FIFO_PATH):
+ os.remove(FIFO_PATH)
+
+
+ def get_input(self) -> str:
+ device = PipeWireRecord.get_device()
+
+ self.cleanup()
+ os.mkfifo(FIFO_PATH)
+
+ while True:
+ with open(FIFO_PATH):
+ pass
+ # TODO "I'm listening"
+
+ try:
+ ps = subprocess.Popen((f"pw-record --target {device} {RECORD_PATH}",), shell=True)
+ with open(FIFO_PATH):
+ print("finished")
+ ps.send_signal(signal.SIGINT)
+ # TODO "acknowledged"
+ except:
+ if "ps" in locals():
+ ps.kill()
+ # TODO "error"
+ # TODO exit gracefully or try to recover
+ raise StopIteration
+
+ model = whisper.load_model("base")
+
+ audio = whisper.load_audio(RECORD_PATH)
+ audio = whisper.pad_or_trim(audio)
+
+ mel = whisper.log_mel_spectrogram(audio).to(model.device)
+ options = whisper.DecodingOptions(language="en", fp16=False)
+ result = whisper.decode(model, mel, options)
+ result_text = result.text.replace(",", "").replace(".", "").lower()
+
+ print(result_text)
+
+ yield result_text
+
+ @staticmethod
+ def get_device() -> str:
+ already_warned = False
+
+ while True:
+ ps = subprocess.Popen(('pw-cli ls | \\grep -Poi "(?<=node.name = \\").*mic.*(?=\\")"',), shell=True, stdout=subprocess.PIPE)
+ ps.wait()
+
+ if ps.returncode == 0:
+ return ps.stdout.read().decode().strip()
+
+ elif not already_warned:
+ already_warned = True
+ # TODO warn about device not found
+
+ sleep(3) \ No newline at end of file
diff --git a/modules/input_handlers/stdin_input.py b/modules/input_handlers/stdin_input.py
new file mode 100644
index 0000000..3d338c1
--- /dev/null
+++ b/modules/input_handlers/stdin_input.py
@@ -0,0 +1,16 @@
+import sys
+
+from .input_handler import InputHandler
+
+
+class StdinInput(InputHandler):
+ def cleanup(self) -> None:
+ pass
+
+ def get_input(self) -> str:
+ for line in sys.stdin:
+ line = line.strip()
+ if not line:
+ continue
+
+ yield line \ No newline at end of file
diff --git a/modules/intents/process.py b/modules/intents/process.py
index bc834b6..48ade48 100644
--- a/modules/intents/process.py
+++ b/modules/intents/process.py
@@ -1,9 +1,11 @@
import os
from typing import Dict
from .sway import HesNavigate
+from ..config import Config
-def HesExecuteProcess(data: Dict):
+
+def HesExecuteProcess(data: Dict, config: Config) -> None:
if "workspace" in data:
- HesNavigate(data)
+ HesNavigate(data, config)
os.popen(data["process"].strip())
diff --git a/modules/intents/sway.py b/modules/intents/sway.py
index 5ab1b25..b5bd9fe 100644
--- a/modules/intents/sway.py
+++ b/modules/intents/sway.py
@@ -1,5 +1,9 @@
import os
from typing import Dict
+from ..config import Config
-def HesNavigate(data: Dict):
- os.popen(f"swaymsg workspace {data["workspace"]}")
+def HesNavigate(data: Dict, config: Config) -> None:
+ os.popen(f"swaymsg workspace {data['workspace']}")
+
+def HesLock(data: Dict, config: Config) -> None:
+ os.popen(config.lock) \ No newline at end of file
diff --git a/modules/responses/__init__.py b/modules/responses/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/modules/responses/__init__.py
diff --git a/modules/responses/libnotify.py b/modules/responses/libnotify.py
new file mode 100644
index 0000000..0bc774f
--- /dev/null
+++ b/modules/responses/libnotify.py
@@ -0,0 +1,15 @@
+import os
+
+from .response_handler import ResponseHandler
+from ..config import Config
+
+
+class LibNotify(ResponseHandler):
+ def __init__(self, config: Config):
+ self.config = config
+ self.hestia_images = {}
+ for path in os.listdir(self.config.hestia_images):
+ self.hestia_images[os.path.splitext(os.path.basename(path))[0]] = os.path.join(self.config.hestia_images, path)
+
+ def respond(self, text: str) -> None:
+ os.popen(f'notify-send "Hestia " "{text}" -i "{self.hestia_images["hestia"]}" --app-name=hestia') \ No newline at end of file
diff --git a/modules/responses/response_handler.py b/modules/responses/response_handler.py
new file mode 100644
index 0000000..9cc0cf0
--- /dev/null
+++ b/modules/responses/response_handler.py
@@ -0,0 +1,6 @@
+from abc import ABC, abstractmethod
+
+class ResponseHandler(ABC):
+ @abstractmethod
+ def respond(self, response: str):
+ pass \ No newline at end of file
diff --git a/modules/responses/response_manager.py b/modules/responses/response_manager.py
new file mode 100644
index 0000000..34c0a41
--- /dev/null
+++ b/modules/responses/response_manager.py
@@ -0,0 +1,32 @@
+import yaml
+
+from pathlib import Path
+
+from ..config import Config
+from ..responses.responses import Responses
+from ..hassil.util import merge_dict
+from .response_handler import ResponseHandler
+from .libnotify import LibNotify
+
+class ResponseManager:
+ def __init__(self, config: Config):
+ response_dict = {"responses": {}}
+
+ response_yaml_path = Path(config.responses_dir)
+ response_yaml_file_paths = response_yaml_path.glob("*.yaml")
+ for yaml_file_path in response_yaml_file_paths:
+ with open(yaml_file_path, "r", encoding="utf-8") as yaml_file:
+ merge_dict(response_dict, yaml.safe_load(yaml_file))
+
+ self.responses = Responses.from_dict(response_dict)
+ self.respond_handlers: list[ResponseHandler] = [LibNotify(config)]
+
+ def respond(self, response: str, intent_name: str):
+ response_key = intent_name if response == "default" else response
+ if response_key not in self.responses.responses:
+ print(f"No response found for: {response_key}")
+ return
+
+ response_text = self.responses.responses[response_key].sentence_texts[0]
+ for handler in self.respond_handlers:
+ handler.respond(response_text)
diff --git a/modules/responses/responses.py b/modules/responses/responses.py
new file mode 100644
index 0000000..f89a1fe
--- /dev/null
+++ b/modules/responses/responses.py
@@ -0,0 +1,29 @@
+from dataclasses import dataclass, field
+from typing import List, Dict, Any
+
+@dataclass(frozen=True)
+class Response:
+ name: str
+ sentence_texts: List[str]
+
+@dataclass(frozen=True)
+class Responses:
+ responses: Dict[str, Response]
+
+ @staticmethod
+ def from_dict(input_dict: Dict[str, Any]) -> "Responses":
+ # responses:
+ # ResponseName:
+ # data:
+ # - sentences:
+ # - "<sentence>"
+ return Responses(
+ responses={
+ response_name: Response(
+ name=response_name,
+ sentence_texts=response_dict["sentences"]
+ )
+ for response_name, response_dict in input_dict["responses"].items()
+ },
+ )
+