Kaynağa Gözat

Fixed bugs and removed use of outside directories

Efren Yevale Varela 1 ay önce
ebeveyn
işleme
f53900b21c
6 değiştirilmiş dosya ile 38 ekleme ve 53 silme
  1. 2 2
      AGENTS.md
  2. 7 8
      README.md
  3. 3 13
      src/stt_daemon/state.py
  4. 4 25
      src/stt_daemon/supervisor.py
  5. 3 5
      tests/test_state.py
  6. 19 0
      tests/test_supervisor.py

+ 2 - 2
AGENTS.md

@@ -39,8 +39,8 @@ Stateful logic belongs in the assistant. Transcription runs on a single worker t
   annotated reference. Keep all three in sync when adding a key.
 - Mapping is **manual** — no pydantic/dynaconf. Adding a key means editing the dataclass *and*
   `load_config()` in `config.py`.
-- Env vars: `STT_DAEMON_STATE` / `XDG_STATE_HOME` (provisioning state at
-  `~/.local/state/stt-daemon/audio.json`), `XDG_RUNTIME_DIR` (sockets), `HF_HOME` (Whisper cache).
+- Env vars: `STT_DAEMON_STATE` (provisioning state; default `audio/audio.json` in the
+  cwd), `XDG_RUNTIME_DIR` (sockets), `HF_HOME` (Whisper cache).
 - Never treat the live `config.toml` as a source of defaults; it is gitignored user state.
 
 ## Testing

+ 7 - 8
README.md

@@ -338,7 +338,7 @@ uv run stt-daemon clear --yes
 ```
 
 This unloads all tracked modules and removes the state file. If
-`~/.local/state/stt-daemon/audio.json` is already gone (reboot, manual
+`audio/audio.json` is already gone (reboot, manual
 delete, crash), `clear --yes` still finds orphaned modules by name
 (`VirtualMicSink`/`VirtualMic` via `pactl list short modules`) and unloads
 them in reverse order (loopbacks → remap-source → null-sink), so the
@@ -362,10 +362,9 @@ The supervisor manages their lifetimes: if any child exits or Ctrl+C is pressed,
 remaining children are cleanly shut down. Without `--with-tui`, you can still run
 a separate `uv run stt-tui` in another terminal to view and control the assistant.
 
-With `--with-tui`, the daemon and assistant log to
-`~/.local/state/stt-daemon/up.log` (or `$XDG_STATE_HOME/stt-daemon/up.log`) so their
-output cannot corrupt the screen, while the TUI keeps the terminal. Without `--with-tui`,
-all output goes to the terminal.
+With `--with-tui`, the daemon and assistant output is discarded so it cannot corrupt
+the screen, while the TUI keeps the terminal. Without `--with-tui`, all output goes to
+the terminal. The supervisor writes nothing to disk.
 
 Like all subcommands, `up` also accepts `--config`, `--device`, `--backend`,
 `--model`, `--language`, and `-v` (verbose) flags.
@@ -486,7 +485,7 @@ uv run stt-daemon up --with-tui
 
 This starts daemon, assistant, and TUI as child processes under a supervisor.
 When you quit the TUI, everything stops cleanly. Daemon and assistant output is
-logged to `~/.local/state/stt-daemon/up.log`.
+discarded while the TUI owns the screen; nothing is written to disk.
 
 #### Traditional: three terminals
 
@@ -590,7 +589,7 @@ The daemon uses PulseAudio's module system to create a virtual microphone from m
 
 When you run `uv run stt-daemon setup`, the daemon:
 - Creates all modules and records their numeric IDs
-- Saves the state to `~/.local/state/stt-daemon/audio.json` (or `$STT_DAEMON_STATE`)
+- Saves the state to `audio/audio.json` (or `$STT_DAEMON_STATE`)
 - Allows `run` to re-provision if the sink is missing (e.g., after reboot)
 - Allows `clear` to unload the modules it created — and falls back to
   name-based discovery (`sink_name`/`remap_source_name` in
@@ -791,7 +790,7 @@ src/stt_daemon/
 ├── audio.py             - Audio capture (parec, sounddevice)
 ├── pulse.py             - PulseAudio/pactl wrapper (devices, modules, sink checks)
 ├── provisioner.py       - Virtual sink + loopback lifecycle (setup/ensure/teardown)
-├── state.py             - Provisioning state file (~/.local/state/stt-daemon/audio.json)
+├── state.py             - Provisioning state file (audio/audio.json)
 ├── picker.py            - Interactive device picker (questionary / numbered menu)
 ├── segmenter.py         - VAD + pre-roll buffer + utterance segmentation (core)
 ├── transcriber.py       - Faster-Whisper wrapper

+ 3 - 13
src/stt_daemon/state.py

@@ -32,22 +32,12 @@ class StateStore:
         """Initialize the state store.
 
         Args:
-            path: Path to the state file. If None, uses XDG_STATE_HOME or ~/.local/state.
+            path: Path to the state file. If None, uses `$STT_DAEMON_STATE` or
+                `audio/audio.json` relative to the current directory.
         """
         if path is None:
-            # Try STT_DAEMON_STATE env var first
             env_path = os.environ.get("STT_DAEMON_STATE")
-            if env_path:
-                path = Path(env_path)
-            else:
-                # XDG default: $XDG_STATE_HOME/stt-daemon/audio.json
-            # or ~/.local/state/stt-daemon/audio.json
-                state_home = os.environ.get("XDG_STATE_HOME")
-                if not state_home:
-                    home = os.environ.get("HOME", "~")
-                    state_home = str(Path(home) / ".local" / "state")
-                path = Path(state_home) / "stt-daemon" / "audio.json"
-
+            path = Path(env_path) if env_path else Path("audio") / "audio.json"
         self.path = path
 
     def load(self) -> AudioState | None:

+ 4 - 25
src/stt_daemon/supervisor.py

@@ -16,7 +16,6 @@ terminated and a non-zero code is returned. Silent headless fallback is not allo
 """
 
 import logging
-import os
 import signal
 import subprocess
 import sys
@@ -39,15 +38,6 @@ if termios is not None:
     _TERMIOS_ERRORS = _TERMIOS_ERRORS + (termios.error,)
 
 
-def _get_state_home() -> Path:
-    """Return $XDG_STATE_HOME/stt-daemon or ~/.local/state/stt-daemon."""
-    env_path = os.environ.get("XDG_STATE_HOME")
-    if env_path:
-        return Path(env_path) / "stt-daemon"
-    home = os.environ.get("HOME", "~")
-    return Path(home).expanduser() / ".local" / "state" / "stt-daemon"
-
-
 class Supervisor:
     """Orchestrate daemon, assistant, and optional TUI as child processes."""
 
@@ -64,7 +54,6 @@ class Supervisor:
         wait_timeout_s: float = 15.0,
         grace_s: float = 5.0,
         poll_s: float = 0.1,
-        log_path: str | None = None,
     ):
         """Initialize the supervisor.
 
@@ -75,14 +64,12 @@ class Supervisor:
                 'model', 'language' (all strings). None/missing keys are omitted.
             verbose: If True, pass -v to daemon and assistant.
             spawn: Function spawn(argv, *, redirect_output=False) -> Popen.
-                Default wraps subprocess.Popen, handling log file open/close.
+                Default discards child output when redirect_output is set.
             sleep: Callable for sleeping (injected for testing).
             clock: Callable for monotonic time (injected for testing).
             wait_timeout_s: Timeout for waiting on assistant control socket.
             grace_s: Time to wait for children to exit after terminate().
             poll_s: Time between child poll() checks.
-            log_path: File path for child stdout/stderr. Default is
-                $XDG_STATE_HOME/stt-daemon/up.log.
         """
         self.cfg = cfg
         self.with_tui = with_tui
@@ -98,22 +85,14 @@ class Supervisor:
         if spawn is None:
             def default_spawn(argv, *, redirect_output=False):
                 if redirect_output:
-                    log_file = open(self.log_path, "a")
-                    proc = subprocess.Popen(argv, stdout=log_file, stderr=log_file)
-                    log_file.close()
-                    return proc
+                    return subprocess.Popen(
+                        argv, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
+                    )
                 return subprocess.Popen(argv)
             self.spawn = default_spawn
         else:
             self.spawn = spawn
 
-        # Resolve log path
-        if log_path is None:
-            log_dir = _get_state_home()
-            log_dir.mkdir(parents=True, exist_ok=True)
-            log_path = str(log_dir / "up.log")
-        self.log_path = log_path
-
     def run(self) -> int:
         """Run the supervisor until a child exits or a signal is received.
 

+ 3 - 5
tests/test_state.py

@@ -87,15 +87,13 @@ def test_state_store_default_path_env_var(tmp_path, monkeypatch):
     assert store.path == state_file
 
 
-def test_state_store_default_path_xdg(tmp_path, monkeypatch):
-    """Test that StateStore uses XDG_STATE_HOME for default path."""
+def test_state_store_default_path_is_project_local(tmp_path, monkeypatch):
+    """Without overrides the state lives in ./audio/audio.json, inside the project."""
     monkeypatch.delenv("STT_DAEMON_STATE", raising=False)
-    monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path))
 
     store = StateStore()
 
-    expected_path = tmp_path / "stt-daemon" / "audio.json"
-    assert store.path == expected_path
+    assert store.path == Path("audio") / "audio.json"
 
 
 def test_state_store_creates_parent_dirs(tmp_path):

+ 19 - 0
tests/test_supervisor.py

@@ -1,6 +1,7 @@
 """Tests for the Supervisor class (daemon, assistant, TUI orchestration)."""
 
 import signal
+import subprocess
 import sys
 import types
 
@@ -407,6 +408,24 @@ def test_supervisor_run_signal_shutdown_returns_zero(monkeypatch):
         assert proc._terminated or proc._killed
 
 
+def test_default_spawn_discards_output_when_redirected(monkeypatch):
+    """The default spawn must discard child output, never write a log file."""
+    calls = []
+    monkeypatch.setattr(
+        "stt_daemon.supervisor.subprocess.Popen",
+        lambda argv, **kwargs: calls.append(kwargs) or FakePopen(argv),
+    )
+
+    supervisor = Supervisor(_cfg(), with_tui=False)
+    supervisor._spawn_with_output(["x"], redirect_output=True)
+    supervisor._spawn_with_output(["y"], redirect_output=False)
+
+    assert calls[0]["stdout"] is subprocess.DEVNULL
+    assert calls[0]["stderr"] is subprocess.DEVNULL
+    assert "stdout" not in calls[1]
+    assert "stderr" not in calls[1]
+
+
 def test_supervisor_restores_terminal_after_tui(monkeypatch, tmp_path):
     """A killed TUI must not leave the terminal in raw mode."""
     cfg = _cfg()