@domdhi/claude-code-tts 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
  claude-code-tts — TTS daemon
4
4
  Persistent background process. Keeps model loaded. Serves speech via TCP.
5
5
 
6
- Protocol (JSON lines over localhost:6254):
6
+ Protocol (JSON lines over localhost — port derived from install dir):
7
7
  {"cmd": "speak", "text": "...", "voice": "af_heart", "speed": 1.0, "project": "repo-name"}
8
8
  {"cmd": "stop"}
9
9
  {"cmd": "ping"}
@@ -30,8 +30,12 @@ os.environ.setdefault('OMP_NUM_THREADS', '6')
30
30
  os.environ.setdefault('ONNXRUNTIME_NUM_THREADS', '6')
31
31
 
32
32
  HOST = '127.0.0.1'
33
- PORT = 6254
34
33
  DAEMON_DIR = os.path.dirname(os.path.abspath(__file__))
34
+
35
+ # Port is derived from install location — each install gets its own daemon port,
36
+ # so multiple projects with local installs don't collide.
37
+ import hashlib as _hashlib
38
+ PORT = 49152 + (int(_hashlib.md5(DAEMON_DIR.encode()).hexdigest(), 16) % 16384)
35
39
  PID_FILE = os.path.join(DAEMON_DIR, 'daemon.pid')
36
40
  MODEL_PATH = os.path.join(DAEMON_DIR, 'models', 'kokoro-v1.0.onnx')
37
41
  VOICES_PATH = os.path.join(DAEMON_DIR, 'models', 'voices-v1.0.bin')
@@ -20,7 +20,8 @@ VOICES_FILE = os.path.join(HOOK_DIR, 'voices.json')
20
20
  DAEMON_SCRIPT = os.path.join(HOOK_DIR, 'daemon.py')
21
21
 
22
22
  DAEMON_HOST = '127.0.0.1'
23
- DAEMON_PORT = 6254
23
+ import hashlib as _hashlib
24
+ DAEMON_PORT = 49152 + (int(_hashlib.md5(HOOK_DIR.encode()).hexdigest(), 16) % 16384)
24
25
 
25
26
 
26
27
  def _start_daemon():
@@ -21,7 +21,8 @@ VOICES_FILE = os.path.join(HOOK_DIR, 'voices.json')
21
21
  DAEMON_SCRIPT = os.path.join(HOOK_DIR, 'daemon.py')
22
22
 
23
23
  DAEMON_HOST = '127.0.0.1'
24
- DAEMON_PORT = 6254
24
+ import hashlib as _hashlib
25
+ DAEMON_PORT = 49152 + (int(_hashlib.md5(HOOK_DIR.encode()).hexdigest(), 16) % 16384)
25
26
 
26
27
  READ_ALL_CHAIN = False # True = speak all assistant messages in chain, False = last only
27
28
 
@@ -19,7 +19,8 @@ VOICES_FILE = os.path.join(HOOK_DIR, 'voices.json')
19
19
  DAEMON_SCRIPT = os.path.join(HOOK_DIR, 'daemon.py')
20
20
 
21
21
  DAEMON_HOST = '127.0.0.1'
22
- DAEMON_PORT = 6254
22
+ import hashlib as _hashlib
23
+ DAEMON_PORT = 49152 + (int(_hashlib.md5(HOOK_DIR.encode()).hexdigest(), 16) % 16384)
23
24
 
24
25
 
25
26
  def strip_markdown(text):
package/INSTALL.md CHANGED
@@ -43,8 +43,8 @@ The installer automatically:
43
43
  7. Patches `settings.json` / `settings.local.json` with hook entries (backs up original first)
44
44
 
45
45
  **Local vs global:**
46
- - Default (no flag): commands go to `./.claude/commands/voice/`, hooks registered in `./.claude/settings.local.json` (gitignored safe to commit the project)
47
- - `--global`: commands go to `~/.claude/commands/voice/`, hooks registered in `~/.claude/settings.json`
46
+ - Default (no flag): everything goes into `./.claude/` hook scripts, commands, and `settings.local.json` (gitignored). Good for shipping TTS config alongside a project. Each install gets its own daemon port, so multiple local-installed projects run independently without conflict.
47
+ - `--global`: everything goes into `~/.claude/` hook scripts, commands, and `settings.json`. Recommended for personal use across all projects.
48
48
 
49
49
  ### Option B — installer script
50
50
 
@@ -290,7 +290,9 @@ Type in the Claude Code prompt:
290
290
 
291
291
  ## Daemon Protocol
292
292
 
293
- The daemon runs on `localhost:6254` and accepts JSON lines:
293
+ The daemon runs on `localhost` on a port derived from the install directory (range 49152–65535). Each install location gets a stable, unique port — so multiple local-installed projects run independent daemons without conflict. Hook scripts and the daemon compute the same port, so they always find each other.
294
+
295
+ Accepts JSON lines:
294
296
 
295
297
  | Command | Effect |
296
298
  |---------|--------|
@@ -347,8 +349,8 @@ The daemon auto-restarts on the next Claude response.
347
349
  - sounddevice doesn't always pull in cffi automatically
348
350
 
349
351
  ### Daemon keeps restarting / won't stay up
350
- - Check for port conflict: `lsof -i :6254` (Mac/Linux) or `netstat -ano | findstr 6254` (Windows)
351
- - Check `~/.claude/hooks/tts/daemon.log`
352
+ - Each install uses a unique port derived from its directory path port conflicts between installs are not possible
353
+ - Check `daemon.log` in your hooks directory for errors
352
354
 
353
355
  ### Audio cuts off mid-sentence (kokoro fallback)
354
356
  - kokoro-onnx has a 510-token (~1500 char) hard limit
package/README.md CHANGED
@@ -18,7 +18,7 @@ npx @domdhi/claude-code-tts
18
18
  npx @domdhi/claude-code-tts --global
19
19
  ```
20
20
 
21
- That's it. The installer copies the hook files, enables TTS, optionally installs the offline fallback, and automatically patches `settings.json` and installs the slash commands.
21
+ That's it. The installer copies the hook files into `.claude/hooks/tts/`, enables TTS, optionally installs the offline fallback, installs slash commands, and patches `settings.local.json` all in the current project directory. Use `--global` to install once for all projects.
22
22
 
23
23
  **Requirements:** Node.js 16+ and Python 3.10+ must both be installed. The hooks run in Python — Node is only used for the install command.
24
24
 
package/install.py CHANGED
@@ -33,15 +33,16 @@ SOURCE_DIR = os.path.dirname(os.path.abspath(__file__))
33
33
  HOOKS_SOURCE = os.path.join(SOURCE_DIR, '.claude', 'hooks', 'tts')
34
34
  COMMANDS_SOURCE = os.path.join(SOURCE_DIR, '.claude', 'commands', 'voice')
35
35
 
36
- INSTALL_DIR = os.path.join(os.path.expanduser('~'), '.claude', 'hooks', 'tts')
37
- MODELS_DIR = os.path.join(INSTALL_DIR, 'models')
36
+ _home_claude = os.path.join(os.path.expanduser('~'), '.claude')
37
+ _proj_claude = os.path.join(os.getcwd(), '.claude')
38
38
 
39
- # Default: project-local install (current directory's .claude/)
40
- # Overridden to ~/.claude/ when --global is passed
41
- _project_claude = os.path.join(os.getcwd(), '.claude')
42
- COMMANDS_INSTALL_DIR = os.path.join(_project_claude, 'commands', 'voice')
43
- # Use settings.local.json for local scope — gitignored, machine-specific (absolute Python paths)
44
- SETTINGS_FILE = os.path.join(_project_claude, 'settings.local.json')
39
+ # Default: project-local all files go into ./.claude/ (current project)
40
+ # --global overrides all of these to ~/.claude/
41
+ INSTALL_DIR = os.path.join(_proj_claude, 'hooks', 'tts')
42
+ MODELS_DIR = os.path.join(INSTALL_DIR, 'models')
43
+ COMMANDS_INSTALL_DIR = os.path.join(_proj_claude, 'commands', 'voice')
44
+ # settings.local.json is gitignored — safe for machine-specific absolute paths
45
+ SETTINGS_FILE = os.path.join(_proj_claude, 'settings.local.json')
45
46
 
46
47
  GLOBAL_SCOPE = False
47
48
 
@@ -294,7 +295,7 @@ def main():
294
295
  parser = argparse.ArgumentParser(description='claude-code-tts installer')
295
296
  parser.add_argument(
296
297
  '--dir', metavar='PATH',
297
- help='Install hooks to PATH instead of ~/.claude/hooks/tts/ (for testing)'
298
+ help='Override hook install directory (for testing only)'
298
299
  )
299
300
  parser.add_argument(
300
301
  '--global', dest='global_scope', action='store_true',
@@ -309,9 +310,10 @@ def main():
309
310
 
310
311
  if args.global_scope:
311
312
  GLOBAL_SCOPE = True
312
- global_claude = os.path.join(os.path.expanduser('~'), '.claude')
313
- COMMANDS_INSTALL_DIR = os.path.join(global_claude, 'commands', 'voice')
314
- SETTINGS_FILE = os.path.join(global_claude, 'settings.json')
313
+ INSTALL_DIR = os.path.join(_home_claude, 'hooks', 'tts')
314
+ MODELS_DIR = os.path.join(INSTALL_DIR, 'models')
315
+ COMMANDS_INSTALL_DIR = os.path.join(_home_claude, 'commands', 'voice')
316
+ SETTINGS_FILE = os.path.join(_home_claude, 'settings.json')
315
317
 
316
318
  scope_label = 'global (~/.claude/)' if args.global_scope else 'project-local (.claude/)'
317
319
  print(f'\nclaude-code-tts installer [{scope_label}]\n')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@domdhi/claude-code-tts",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Neural TTS hook system for Claude Code. Reads Claude's responses aloud as they finish.",
5
5
  "bin": {
6
6
  "claude-code-tts": "bin/install.js"