diff --git a/AGENTS.md b/AGENTS.md index 2b1b90c..25ae7e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,9 +2,9 @@ ## Project Structure & Module Organization -- `src/leld.py` is the primary entrypoint (X11 STT daemon). +- `src/aman.py` is the primary entrypoint (X11 STT daemon). - `src/recorder.py` handles audio capture using PortAudio via `sounddevice`. -- `src/leld.py` owns Whisper setup and transcription. +- `src/aman.py` owns Whisper setup and transcription. - `src/aiprocess.py` runs the in-process Llama-3.2-3B cleanup. - `src/desktop_x11.py` encapsulates X11 hotkeys, tray, and injection. - `src/desktop_wayland.py` scaffolds Wayland support (exits with a message). @@ -13,7 +13,7 @@ - Install deps (X11): `uv sync --extra x11`. - Install deps (Wayland scaffold): `uv sync --extra wayland`. -- Run daemon: `uv run python3 src/leld.py --config ~/.config/lel/config.json`. +- Run daemon: `uv run python3 src/aman.py --config ~/.config/aman/config.json`. System packages (example names): @@ -39,6 +39,6 @@ System packages (example names): - Audio input is configured via the `recording.input` field in `config.json`. - STT model and device are configured via the `stt` section in `config.json`. -- LLM model settings are locked; model downloads to `~/.cache/lel/models/`. +- LLM model settings are locked; model downloads to `~/.cache/aman/models/`. - `-v/--verbose` enables verbose logs (including llama.cpp) with `llama::` prefix. - Press `Esc` while recording to cancel without processing. diff --git a/Makefile b/Makefile index bb2fcbf..85be59f 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,9 @@ -CONFIG := $(HOME)/.config/lel/config.json +CONFIG := $(HOME)/.config/aman/config.json .PHONY: run install sync test check run: - uv run python3 src/leld.py --config $(CONFIG) + uv run python3 src/aman.py --config $(CONFIG) sync: uv sync @@ -16,9 +16,9 @@ check: $(MAKE) test install: - mkdir -p $(HOME)/.local/share/lel/src/assets - cp src/*.py $(HOME)/.local/share/lel/src/ - cp src/assets/*.png $(HOME)/.local/share/lel/src/assets/ - cp systemd/lel.service $(HOME)/.config/systemd/user/lel.service + mkdir -p $(HOME)/.local/share/aman/src/assets + cp src/*.py $(HOME)/.local/share/aman/src/ + cp src/assets/*.png $(HOME)/.local/share/aman/src/assets/ + cp systemd/aman.service $(HOME)/.config/systemd/user/aman.service systemctl --user daemon-reload - systemctl --user enable --now lel + systemctl --user enable --now aman diff --git a/README.md b/README.md index 969c397..2819273 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# lel +# aman Python X11 STT daemon that records audio, runs Whisper, applies local AI cleanup, and injects text. @@ -78,12 +78,12 @@ uv sync --extra wayland Run: ```bash -uv run python3 src/leld.py --config ~/.config/lel/config.json +uv run python3 src/aman.py --config ~/.config/aman/config.json ``` ## Config -Create `~/.config/lel/config.json`: +Create `~/.config/aman/config.json`: ```json { @@ -111,7 +111,7 @@ Recording input can be a device index (preferred) or a substring of the device name. AI cleanup is always enabled and uses the locked local Llama-3.2-3B GGUF model -downloaded to `~/.cache/lel/models/` on first use. +downloaded to `~/.cache/aman/models/` on first use. Use `-v/--verbose` to enable DEBUG logs, including recognized/processed transcript text and llama.cpp logs (`llama::` prefix). Without `-v`, logs are @@ -139,12 +139,12 @@ STT hinting: ## systemd user service ```bash -mkdir -p ~/.local/share/lel/src/assets -cp src/*.py ~/.local/share/lel/src/ -cp src/assets/*.png ~/.local/share/lel/src/assets/ -cp systemd/lel.service ~/.config/systemd/user/lel.service +mkdir -p ~/.local/share/aman/src/assets +cp src/*.py ~/.local/share/aman/src/ +cp src/assets/*.png ~/.local/share/aman/src/assets/ +cp systemd/aman.service ~/.config/systemd/user/aman.service systemctl --user daemon-reload -systemctl --user enable --now lel +systemctl --user enable --now aman ``` ## Usage diff --git a/pyproject.toml b/pyproject.toml index 8114ec4..affb2d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "lel" +name = "aman" version = "0.0.0" description = "X11 STT daemon with faster-whisper and optional AI cleanup" readme = "README.md" diff --git a/src/leld.py b/src/aman.py similarity index 98% rename from src/leld.py rename to src/aman.py index bdc2ec2..c560583 100755 --- a/src/leld.py +++ b/src/aman.py @@ -319,9 +319,9 @@ def _read_lock_pid(lock_file) -> str: def _lock_single_instance(): - runtime_dir = Path(os.getenv("XDG_RUNTIME_DIR", "/tmp")) / "lel" + runtime_dir = Path(os.getenv("XDG_RUNTIME_DIR", "/tmp")) / "aman" runtime_dir.mkdir(parents=True, exist_ok=True) - lock_path = runtime_dir / "lel.lock" + lock_path = runtime_dir / "aman.lock" lock_file = open(lock_path, "a+", encoding="utf-8") try: import fcntl @@ -360,7 +360,7 @@ def main(): logging.basicConfig( stream=sys.stderr, level=logging.DEBUG if args.verbose else logging.INFO, - format="lel: %(asctime)s %(levelname)s %(message)s", + format="aman: %(asctime)s %(levelname)s %(message)s", ) cfg = load(args.config) _LOCK_HANDLE = _lock_single_instance() @@ -368,7 +368,7 @@ def main(): logging.info("hotkey: %s", cfg.daemon.hotkey) logging.info( "config (%s):\n%s", - args.config or str(Path.home() / ".config" / "lel" / "config.json"), + args.config or str(Path.home() / ".config" / "aman" / "config.json"), json.dumps(redacted_dict(cfg), indent=2), ) diff --git a/src/constants.py b/src/constants.py index 8b20634..df06122 100644 --- a/src/constants.py +++ b/src/constants.py @@ -1,7 +1,7 @@ from pathlib import Path -DEFAULT_CONFIG_PATH = Path.home() / ".config" / "lel" / "config.json" +DEFAULT_CONFIG_PATH = Path.home() / ".config" / "aman" / "config.json" RECORD_TIMEOUT_SEC = 300 STT_LANGUAGE = "en" TRAY_UPDATE_MS = 250 @@ -12,5 +12,5 @@ MODEL_URL = ( "https://huggingface.co/bartowski/Llama-3.2-3B-Instruct-GGUF/resolve/main/" "Llama-3.2-3B-Instruct-Q4_K_M.gguf" ) -MODEL_DIR = Path.home() / ".cache" / "lel" / "models" +MODEL_DIR = Path.home() / ".cache" / "aman" / "models" MODEL_PATH = MODEL_DIR / MODEL_NAME diff --git a/src/desktop.py b/src/desktop.py index 40c885d..23ac5f0 100644 --- a/src/desktop.py +++ b/src/desktop.py @@ -31,7 +31,7 @@ def get_desktop_adapter() -> DesktopAdapter: session_type = os.getenv("XDG_SESSION_TYPE", "").lower() if session_type == "wayland" or os.getenv("WAYLAND_DISPLAY"): raise SystemExit( - "Wayland is not supported yet. Run under X11 (XDG_SESSION_TYPE=x11) to use lel." + "Wayland is not supported yet. Run under X11 (XDG_SESSION_TYPE=x11) to use aman." ) from desktop_x11 import X11Adapter diff --git a/src/desktop_x11.py b/src/desktop_x11.py index 59dd189..9f60a83 100644 --- a/src/desktop_x11.py +++ b/src/desktop_x11.py @@ -44,7 +44,7 @@ class X11Adapter: self.menu = None if AppIndicator3 is not None: self.indicator = AppIndicator3.Indicator.new( - "lel", + "aman", self._icon_path("idle"), AppIndicator3.IndicatorCategory.APPLICATION_STATUS, ) diff --git a/systemd/aman.service b/systemd/aman.service new file mode 100644 index 0000000..4a8132c --- /dev/null +++ b/systemd/aman.service @@ -0,0 +1,13 @@ +[Unit] +Description=aman X11 STT daemon +After=default.target + +[Service] +Type=simple +WorkingDirectory=%h/.local/share/aman +ExecStart=/usr/bin/uv run python3 %h/.local/share/aman/src/aman.py --config %h/.config/aman/config.json +Restart=on-failure +RestartSec=2 + +[Install] +WantedBy=default.target diff --git a/systemd/lel.service b/systemd/lel.service deleted file mode 100644 index ef31fd1..0000000 --- a/systemd/lel.service +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Description=lel X11 STT daemon -After=default.target - -[Service] -Type=simple -WorkingDirectory=%h/.local/share/lel -ExecStart=/usr/bin/uv run python3 %h/.local/share/lel/src/leld.py --config %h/.config/lel/config.json -Restart=on-failure -RestartSec=2 - -[Install] -WantedBy=default.target diff --git a/tests/test_leld.py b/tests/test_aman.py similarity index 75% rename from tests/test_leld.py rename to tests/test_aman.py index dbc23cb..d377aae 100644 --- a/tests/test_leld.py +++ b/tests/test_aman.py @@ -10,7 +10,7 @@ SRC = ROOT / "src" if str(SRC) not in sys.path: sys.path.insert(0, str(SRC)) -import leld +import aman from config import Config, VocabularyReplacement @@ -87,12 +87,12 @@ class DaemonTests(unittest.TestCase): cfg = Config() return cfg - @patch("leld.stop_audio_recording", return_value=FakeAudio(8)) - @patch("leld.start_audio_recording", return_value=(object(), object())) + @patch("aman.stop_audio_recording", return_value=FakeAudio(8)) + @patch("aman.start_audio_recording", return_value=(object(), object())) def test_toggle_start_stop_injects_text(self, _start_mock, _stop_mock): desktop = FakeDesktop() - with patch("leld._build_whisper_model", return_value=FakeModel()): - daemon = leld.Daemon(self._config(), desktop, verbose=False) + with patch("aman._build_whisper_model", return_value=FakeModel()): + daemon = aman.Daemon(self._config(), desktop, verbose=False) daemon.ai_processor = FakeAIProcessor() daemon._start_stop_worker = ( lambda stream, record, trigger, process_audio: daemon._stop_and_process( @@ -101,19 +101,19 @@ class DaemonTests(unittest.TestCase): ) daemon.toggle() - self.assertEqual(daemon.get_state(), leld.State.RECORDING) + self.assertEqual(daemon.get_state(), aman.State.RECORDING) daemon.toggle() - self.assertEqual(daemon.get_state(), leld.State.IDLE) + self.assertEqual(daemon.get_state(), aman.State.IDLE) self.assertEqual(desktop.inject_calls, [("hello world", "clipboard", False)]) - @patch("leld.stop_audio_recording", return_value=FakeAudio(8)) - @patch("leld.start_audio_recording", return_value=(object(), object())) + @patch("aman.stop_audio_recording", return_value=FakeAudio(8)) + @patch("aman.start_audio_recording", return_value=(object(), object())) def test_shutdown_stops_recording_without_injection(self, _start_mock, _stop_mock): desktop = FakeDesktop() - with patch("leld._build_whisper_model", return_value=FakeModel()): - daemon = leld.Daemon(self._config(), desktop, verbose=False) + with patch("aman._build_whisper_model", return_value=FakeModel()): + daemon = aman.Daemon(self._config(), desktop, verbose=False) daemon.ai_processor = FakeAIProcessor() daemon._start_stop_worker = ( lambda stream, record, trigger, process_audio: daemon._stop_and_process( @@ -122,22 +122,22 @@ class DaemonTests(unittest.TestCase): ) daemon.toggle() - self.assertEqual(daemon.get_state(), leld.State.RECORDING) + self.assertEqual(daemon.get_state(), aman.State.RECORDING) self.assertTrue(daemon.shutdown(timeout=0.2)) - self.assertEqual(daemon.get_state(), leld.State.IDLE) + self.assertEqual(daemon.get_state(), aman.State.IDLE) self.assertEqual(desktop.inject_calls, []) - @patch("leld.stop_audio_recording", return_value=FakeAudio(8)) - @patch("leld.start_audio_recording", return_value=(object(), object())) + @patch("aman.stop_audio_recording", return_value=FakeAudio(8)) + @patch("aman.start_audio_recording", return_value=(object(), object())) def test_dictionary_replacement_applies_after_ai(self, _start_mock, _stop_mock): desktop = FakeDesktop() model = FakeModel(text="good morning martha") cfg = self._config() cfg.vocabulary.replacements = [VocabularyReplacement(source="Martha", target="Marta")] - with patch("leld._build_whisper_model", return_value=model): - daemon = leld.Daemon(cfg, desktop, verbose=False) + with patch("aman._build_whisper_model", return_value=model): + daemon = aman.Daemon(cfg, desktop, verbose=False) daemon.ai_processor = FakeAIProcessor() daemon._start_stop_worker = ( lambda stream, record, trigger, process_audio: daemon._stop_and_process( @@ -156,8 +156,8 @@ class DaemonTests(unittest.TestCase): cfg = self._config() cfg.vocabulary.terms = ["Docker", "Systemd"] - with patch("leld._build_whisper_model", return_value=model): - daemon = leld.Daemon(cfg, desktop, verbose=False) + with patch("aman._build_whisper_model", return_value=model): + daemon = aman.Daemon(cfg, desktop, verbose=False) result = daemon._transcribe(object()) @@ -172,8 +172,8 @@ class DaemonTests(unittest.TestCase): cfg.vocabulary.terms = ["Systemd"] cfg.vocabulary.replacements = [VocabularyReplacement(source="docker", target="Docker")] - with patch("leld._build_whisper_model", return_value=model): - daemon = leld.Daemon(cfg, desktop, verbose=False) + with patch("aman._build_whisper_model", return_value=model): + daemon = aman.Daemon(cfg, desktop, verbose=False) result = daemon._transcribe(object()) @@ -186,24 +186,24 @@ class DaemonTests(unittest.TestCase): desktop = FakeDesktop() cfg = self._config() - with patch("leld._build_whisper_model", return_value=FakeModel()): - daemon = leld.Daemon(cfg, desktop, verbose=False) + with patch("aman._build_whisper_model", return_value=FakeModel()): + daemon = aman.Daemon(cfg, desktop, verbose=False) self.assertFalse(daemon.log_transcript) - with patch("leld._build_whisper_model", return_value=FakeModel()): - daemon_verbose = leld.Daemon(cfg, desktop, verbose=True) + with patch("aman._build_whisper_model", return_value=FakeModel()): + daemon_verbose = aman.Daemon(cfg, desktop, verbose=True) self.assertTrue(daemon_verbose.log_transcript) - @patch("leld.stop_audio_recording", return_value=FakeAudio(8)) - @patch("leld.start_audio_recording", return_value=(object(), object())) + @patch("aman.stop_audio_recording", return_value=FakeAudio(8)) + @patch("aman.start_audio_recording", return_value=(object(), object())) def test_passes_clipboard_remove_option_to_desktop(self, _start_mock, _stop_mock): desktop = FakeDesktop() model = FakeModel(text="hello world") cfg = self._config() cfg.injection.remove_transcription_from_clipboard = True - with patch("leld._build_whisper_model", return_value=model): - daemon = leld.Daemon(cfg, desktop, verbose=False) + with patch("aman._build_whisper_model", return_value=model): + daemon = aman.Daemon(cfg, desktop, verbose=False) daemon.ai_processor = FakeAIProcessor() daemon._start_stop_worker = ( lambda stream, record, trigger, process_audio: daemon._stop_and_process( @@ -218,11 +218,11 @@ class DaemonTests(unittest.TestCase): def test_state_changes_are_debug_level(self): desktop = FakeDesktop() - with patch("leld._build_whisper_model", return_value=FakeModel()): - daemon = leld.Daemon(self._config(), desktop, verbose=False) + with patch("aman._build_whisper_model", return_value=FakeModel()): + daemon = aman.Daemon(self._config(), desktop, verbose=False) with self.assertLogs(level="DEBUG") as logs: - daemon.set_state(leld.State.RECORDING) + daemon.set_state(aman.State.RECORDING) self.assertTrue( any("DEBUG:root:state: idle -> recording" in line for line in logs.output) @@ -233,10 +233,10 @@ class LockTests(unittest.TestCase): def test_lock_rejects_second_instance(self): with tempfile.TemporaryDirectory() as td: with patch.dict(os.environ, {"XDG_RUNTIME_DIR": td}, clear=False): - first = leld._lock_single_instance() + first = aman._lock_single_instance() try: with self.assertRaises(SystemExit) as ctx: - leld._lock_single_instance() + aman._lock_single_instance() self.assertIn("already running", str(ctx.exception)) finally: first.close() diff --git a/uv.lock b/uv.lock index 3b3a54a..efaa525 100644 --- a/uv.lock +++ b/uv.lock @@ -404,7 +404,7 @@ wheels = [ ] [[package]] -name = "lel" +name = "aman" version = "0.0.0" source = { virtual = "." } dependencies = [