vocalize-cli 0.5.0__tar.gz → 0.6.0__tar.gz

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.
Files changed (32) hide show
  1. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/CHANGELOG.md +11 -0
  2. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/PKG-INFO +5 -1
  3. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/README.md +4 -0
  4. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/hooks/claude_stop_hook.py +13 -0
  5. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/tests/test_claude_stop_hook.py +20 -0
  6. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/tests/test_cli.py +28 -0
  7. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/vocalize/__init__.py +1 -1
  8. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/vocalize/cli.py +19 -0
  9. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/.env.example +0 -0
  10. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/.github/workflows/ci.yml +0 -0
  11. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/.gitignore +0 -0
  12. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/LICENSE +0 -0
  13. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/hooks/install_hook.py +0 -0
  14. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/hooks/speak_url_gate.py +0 -0
  15. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/pyproject.toml +0 -0
  16. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/tests/conftest.py +0 -0
  17. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/tests/test_audio.py +0 -0
  18. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/tests/test_auth.py +0 -0
  19. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/tests/test_config.py +0 -0
  20. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/tests/test_install_hook.py +0 -0
  21. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/tests/test_preprocess.py +0 -0
  22. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/tests/test_speak_url_gate.py +0 -0
  23. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/tests/test_tts.py +0 -0
  24. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/tests/test_wizard.py +0 -0
  25. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/vocalize/__main__.py +0 -0
  26. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/vocalize/audio.py +0 -0
  27. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/vocalize/auth.py +0 -0
  28. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/vocalize/config.py +0 -0
  29. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/vocalize/exceptions.py +0 -0
  30. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/vocalize/preprocess.py +0 -0
  31. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/vocalize/tts.py +0 -0
  32. {vocalize_cli-0.5.0 → vocalize_cli-0.6.0}/vocalize/wizard.py +0 -0
@@ -3,6 +3,17 @@
3
3
  All notable changes to this project are documented here. Format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## 0.6.0 - 2026-08-31
7
+
8
+ ### Added
9
+
10
+ - `vocalize settings` — prints the resolved settings, one key=value per
11
+ line, so wrapper scripts (like the /speak slash command) can read the
12
+ effective `overflow` and `max_chars` instead of hardcoding them.
13
+ - `--print-length` on the Stop hook: prints the response's character count
14
+ instead of speaking, so a wrapper can decide to ask about truncation
15
+ interactively before any audio is spent.
16
+
6
17
  ## 0.5.0 - 2026-08-31
7
18
 
8
19
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vocalize-cli
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: A CLI that turns text, markdown, or piped stdin into speech via the ElevenLabs API, with markdown-table-aware preprocessing.
5
5
  Project-URL: Homepage, https://github.com/matthager12-collab/vocalize
6
6
  Project-URL: Repository, https://github.com/matthager12-collab/vocalize
@@ -225,6 +225,10 @@ regardless. With no cap set anywhere there is no overflow, so the mode
225
225
  never fires. Hook-triggered speech still lives under the hook's 15-minute
226
226
  watchdog described below, whatever the mode.
227
227
 
228
+ `vocalize settings` prints the resolved values (one `key=value` per
229
+ line, env and config applied) — handy for wrapper scripts and for checking
230
+ which source won.
231
+
228
232
  Not having a config file is normal and silent. A file that isn't valid TOML
229
233
  is an error naming the file; a key that isn't recognised is a warning on
230
234
  stderr, so a typo doesn't pass unnoticed but doesn't stop the run either.
@@ -191,6 +191,10 @@ regardless. With no cap set anywhere there is no overflow, so the mode
191
191
  never fires. Hook-triggered speech still lives under the hook's 15-minute
192
192
  watchdog described below, whatever the mode.
193
193
 
194
+ `vocalize settings` prints the resolved values (one `key=value` per
195
+ line, env and config applied) — handy for wrapper scripts and for checking
196
+ which source won.
197
+
194
198
  Not having a config file is normal and silent. A file that isn't valid TOML
195
199
  is an error naming the file; a key that isn't recognised is a warning on
196
200
  stderr, so a typo doesn't pass unnoticed but doesn't stop the run either.
@@ -9,6 +9,10 @@
9
9
  that response. Run it when you want speech instead of installing the
10
10
  hook and getting it after every turn.
11
11
 
12
+ Add `--print-length` to either mode to print the response's character
13
+ count instead of speaking — for wrappers deciding whether to ask about
14
+ truncation before committing to audio.
15
+
12
16
  Either way it pulls the most recent assistant text message out of the
13
17
  transcript and pipes it through the `vocalize` CLI (the same one used
14
18
  directly from the command line), so there's exactly one code path for
@@ -125,6 +129,15 @@ def main() -> int:
125
129
 
126
130
  text = _extract_last_assistant_text(transcript_path)
127
131
  if not text.strip():
132
+ if "--print-length" in sys.argv[1:]:
133
+ print(0)
134
+ return 0
135
+
136
+ if "--print-length" in sys.argv[1:]:
137
+ # Report the response's size without speaking — the /speak command
138
+ # uses this to decide whether to ask about truncation in-app. Raw
139
+ # (pre-flattening) length: close enough for an ask threshold.
140
+ print(len(text))
128
141
  return 0
129
142
 
130
143
  # VOCALIZE_BIN wins over PATH so a venv install still resolves when the
@@ -172,6 +172,26 @@ def test_env_max_chars_is_left_for_the_cli_to_resolve(monkeypatch, tmp_path):
172
172
  assert calls[0][2:4] == ["--default-max-chars", "500"]
173
173
 
174
174
 
175
+ def test_print_length_reports_count_without_speaking(monkeypatch, tmp_path, capsys):
176
+ path = _write_transcript(tmp_path, [_assistant([_text("hello there")])])
177
+ calls = _patch_main(monkeypatch, {"transcript_path": path})
178
+ monkeypatch.setattr(hook.sys, "argv", ["claude_stop_hook.py", "--print-length"])
179
+
180
+ assert hook.main() == 0
181
+ assert capsys.readouterr().out.strip() == "11"
182
+ assert calls == [] # nothing was spoken
183
+
184
+
185
+ def test_print_length_reports_zero_for_an_empty_response(monkeypatch, tmp_path, capsys):
186
+ path = _write_transcript(tmp_path, ['{"type":"user"}'])
187
+ calls = _patch_main(monkeypatch, {"transcript_path": path})
188
+ monkeypatch.setattr(hook.sys, "argv", ["claude_stop_hook.py", "--print-length"])
189
+
190
+ assert hook.main() == 0
191
+ assert capsys.readouterr().out.strip() == "0"
192
+ assert calls == []
193
+
194
+
175
195
  def test_speech_timeout_scales_with_text_and_is_capped():
176
196
  assert hook._speech_timeout("") == 60
177
197
  assert hook._speech_timeout("x" * 1200) == 60 + 100
@@ -583,6 +583,34 @@ def test_ask_to_truncate_returns_none_on_tty_eof(monkeypatch):
583
583
  assert cli_module._ask_to_truncate(5000, 100) is None
584
584
 
585
585
 
586
+ def test_settings_prints_resolved_config_values(monkeypatch, tmp_path):
587
+ _isolate_overflow_env(monkeypatch, tmp_path)
588
+ for var in ("VOCALIZE_VOICE", "VOCALIZE_MODEL", "VOCALIZE_SPEED"):
589
+ monkeypatch.delenv(var, raising=False)
590
+ cfg = tmp_path / "vocalize" / "config.toml"
591
+ cfg.parent.mkdir(parents=True, exist_ok=True)
592
+ cfg.write_text('max_chars = 1000\noverflow = "ask"\n', encoding="utf-8")
593
+
594
+ result = CliRunner().invoke(main, ["settings"])
595
+
596
+ assert result.exit_code == 0, result.output
597
+ assert "max_chars=1000" in result.output
598
+ assert "overflow=ask" in result.output
599
+
600
+
601
+ def test_settings_prints_defaults_when_nothing_is_configured(monkeypatch, tmp_path):
602
+ _isolate_overflow_env(monkeypatch, tmp_path)
603
+ for var in ("VOCALIZE_VOICE", "VOCALIZE_MODEL", "VOCALIZE_SPEED"):
604
+ monkeypatch.delenv(var, raising=False)
605
+
606
+ result = CliRunner().invoke(main, ["settings"])
607
+
608
+ assert result.exit_code == 0, result.output
609
+ assert "max_chars=unset" in result.output
610
+ assert "overflow=truncate" in result.output
611
+ assert "speed=unset" in result.output
612
+
613
+
586
614
  def test_stop_command_reports_a_stopped_player(monkeypatch):
587
615
  monkeypatch.setattr(cli_module, "stop_playback", lambda: True)
588
616
  result = CliRunner().invoke(main, ["stop"])
@@ -7,4 +7,4 @@ formatting into something that actually sounds good spoken aloud
7
7
  which is close to useless).
8
8
  """
9
9
 
10
- __version__ = "0.5.0"
10
+ __version__ = "0.6.0"
@@ -4,6 +4,7 @@
4
4
  vocalize speak-file report.md --play
5
5
  cat notes.md | vocalize speak-file - --play
6
6
  vocalize stop
7
+ vocalize settings
7
8
  vocalize voices
8
9
  vocalize usage
9
10
  vocalize config
@@ -181,6 +182,24 @@ def speak_file(path, api_key, voice_id, model_id, speed, output_path, play, raw,
181
182
  chunk_chars=chunk_chars, overflow=overflow, default_max_chars=default_max_chars)
182
183
 
183
184
 
185
+ @main.command()
186
+ def settings() -> None:
187
+ """Print the resolved settings, env and config precedence applied.
188
+
189
+ One key=value per line, made for wrapper scripts (the /speak slash
190
+ command reads overflow and max_chars from here) — no API call, no key
191
+ needed.
192
+ """
193
+ file_config = load_config_file()
194
+ resolved = resolve_settings(file_config=file_config)
195
+ mode, cap = resolve_overflow(file_config=file_config)
196
+ click.echo(f"voice={resolved.voice_id}")
197
+ click.echo(f"model={resolved.model_id}")
198
+ click.echo(f"speed={resolved.speed if resolved.speed is not None else 'unset'}")
199
+ click.echo(f"max_chars={cap if cap is not None else 'unset'}")
200
+ click.echo(f"overflow={mode}")
201
+
202
+
184
203
  @main.command()
185
204
  def stop() -> None:
186
205
  """Stop any audio vocalize is currently playing."""
File without changes
File without changes
File without changes