chartremotely 0.2.14__tar.gz → 0.2.16__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 (63) hide show
  1. {chartremotely-0.2.14 → chartremotely-0.2.16}/PKG-INFO +1 -1
  2. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/assets/shortcut.plist +4 -0
  3. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/shortcut.py +5 -2
  4. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/voice.py +16 -8
  5. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely.egg-info/PKG-INFO +1 -1
  6. {chartremotely-0.2.14 → chartremotely-0.2.16}/pyproject.toml +1 -1
  7. {chartremotely-0.2.14 → chartremotely-0.2.16}/tests/test_hearing.py +3 -0
  8. {chartremotely-0.2.14 → chartremotely-0.2.16}/tests/test_voice.py +14 -0
  9. {chartremotely-0.2.14 → chartremotely-0.2.16}/LICENSE +0 -0
  10. {chartremotely-0.2.14 → chartremotely-0.2.16}/README.md +0 -0
  11. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/__init__.py +0 -0
  12. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/answer.py +0 -0
  13. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/ax.py +0 -0
  14. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/cli.py +0 -0
  15. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/config.py +0 -0
  16. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/displays.py +0 -0
  17. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/doctor.py +0 -0
  18. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/guilock.py +0 -0
  19. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/hearing.py +0 -0
  20. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/keystore.py +0 -0
  21. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/layout.py +0 -0
  22. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/mcpclient.py +0 -0
  23. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/patron.py +0 -0
  24. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/popups.py +0 -0
  25. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/push.py +0 -0
  26. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/recent.py +0 -0
  27. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/registry.py +0 -0
  28. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/relay.py +0 -0
  29. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/requestlog.py +0 -0
  30. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/resolve.py +0 -0
  31. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/scales.py +0 -0
  32. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/screen.py +0 -0
  33. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/server.py +0 -0
  34. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/services.py +0 -0
  35. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/setup.py +0 -0
  36. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/snapshot.py +0 -0
  37. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/studies.py +0 -0
  38. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/symbol.py +0 -0
  39. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/tailnet.py +0 -0
  40. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/timeframe.py +0 -0
  41. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/understand.py +0 -0
  42. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/vocab.py +0 -0
  43. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely/window.py +0 -0
  44. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely.egg-info/SOURCES.txt +0 -0
  45. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely.egg-info/dependency_links.txt +0 -0
  46. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely.egg-info/entry_points.txt +0 -0
  47. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely.egg-info/requires.txt +0 -0
  48. {chartremotely-0.2.14 → chartremotely-0.2.16}/chartremotely.egg-info/top_level.txt +0 -0
  49. {chartremotely-0.2.14 → chartremotely-0.2.16}/setup.cfg +0 -0
  50. {chartremotely-0.2.14 → chartremotely-0.2.16}/tests/test_patron.py +0 -0
  51. {chartremotely-0.2.14 → chartremotely-0.2.16}/tests/test_popups.py +0 -0
  52. {chartremotely-0.2.14 → chartremotely-0.2.16}/tests/test_push.py +0 -0
  53. {chartremotely-0.2.14 → chartremotely-0.2.16}/tests/test_recent.py +0 -0
  54. {chartremotely-0.2.14 → chartremotely-0.2.16}/tests/test_relay.py +0 -0
  55. {chartremotely-0.2.14 → chartremotely-0.2.16}/tests/test_requestlog.py +0 -0
  56. {chartremotely-0.2.14 → chartremotely-0.2.16}/tests/test_resolve.py +0 -0
  57. {chartremotely-0.2.14 → chartremotely-0.2.16}/tests/test_scales.py +0 -0
  58. {chartremotely-0.2.14 → chartremotely-0.2.16}/tests/test_screen.py +0 -0
  59. {chartremotely-0.2.14 → chartremotely-0.2.16}/tests/test_setup.py +0 -0
  60. {chartremotely-0.2.14 → chartremotely-0.2.16}/tests/test_snapshot.py +0 -0
  61. {chartremotely-0.2.14 → chartremotely-0.2.16}/tests/test_stamp_changelog.py +0 -0
  62. {chartremotely-0.2.14 → chartremotely-0.2.16}/tests/test_timeframe.py +0 -0
  63. {chartremotely-0.2.14 → chartremotely-0.2.16}/tests/test_understand.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chartremotely
3
- Version: 0.2.14
3
+ Version: 0.2.16
4
4
  Summary: Remote control of desktop charting applications on macOS
5
5
  Author: Lonnie VanZandt
6
6
  License:
@@ -669,6 +669,8 @@
669
669
  <string>is.workflow.actions.speaktext</string>
670
670
  <key>WFWorkflowActionParameters</key>
671
671
  <dict>
672
+ <key>WFSpeakTextRate</key>
673
+ <real>0.6</real>
672
674
  <key>WFSpeakTextWait</key>
673
675
  <true/>
674
676
  <key>WFText</key>
@@ -751,6 +753,8 @@
751
753
  <string>is.workflow.actions.speaktext</string>
752
754
  <key>WFWorkflowActionParameters</key>
753
755
  <dict>
756
+ <key>WFSpeakTextRate</key>
757
+ <real>0.6</real>
754
758
  <key>WFSpeakTextWait</key>
755
759
  <true/>
756
760
  <key>WFText</key>
@@ -5,8 +5,8 @@ placeholders where its address and token go. Talk, or type when it asks:
5
5
 
6
6
  * It records one sentence - "Palantir, half, on mac mini" - posts it to this
7
7
  Mac, which hears it with Whisper (:mod:`voice`), and speaks the reply. When
8
- the reply is a question (it ends "I'm listening.") it records again, at
9
- most three times.
8
+ the reply is a question (it ends "I'm listening.") it records again; the
9
+ third spoken miss asks its question in the text box instead.
10
10
  * When nothing usable was recorded (no microphone, silence, noise) the reply
11
11
  is ``TYPE:`` and a prompt: the Shortcut shows that prompt in a one-line
12
12
  text box and posts the typed sentence instead. On a Mac it goes straight
@@ -32,6 +32,9 @@ NAME = "ChartRemotely"
32
32
  #: The Shortcut older setups made, removed from the library by setup. The
33
33
  #: ``shortcuts`` command can list but not delete; "Shortcuts Events" can.
34
34
  RETIRED = "ChartRemotely Ask"
35
+ #: Speak Text's rate (0 to 1; Siri's own is 0.5). Replies are short and
36
+ #: expected, so a person follows them brisker than Siri's default.
37
+ SPEAK_RATE = 0.6
35
38
 
36
39
 
37
40
  def template() -> bytes:
@@ -10,7 +10,9 @@ PLTR at half scale sent to mac mini. Good luck."
10
10
  A follow-up question ends with :data:`LISTENING`. The Shortcut records again
11
11
  when it hears that word, and what was understood the first time is held for
12
12
  :data:`FOLLOW_UP_SECONDS`: "Palantir" then "half" is one request, as is
13
- "Palantir half" then "PLTR" after "Did you mean PLTR or PLTX?".
13
+ "Palantir half" then "PLTR" after "Did you mean PLTR or PLTX?". Speech gets
14
+ :data:`VOICE_TRIES` chances, as Siri gives: the last spoken miss asks its
15
+ question in the text box instead, with what was heard so far still held.
14
16
 
15
17
  When nothing usable was recorded - no microphone, silence, noise - the
16
18
  reply is not a sentence but :data:`TYPE` and a prompt. The Shortcut shows
@@ -42,25 +44,28 @@ TYPED_MAX = 200
42
44
  #: The word every follow-up question ends with; the Shortcut listens again on it.
43
45
  LISTENING = "I'm listening."
44
46
  FOLLOW_UP_SECONDS = 45.0
47
+ #: Spoken tries at one request before the question moves to the text box.
48
+ VOICE_TRIES = 3
45
49
 
46
50
  _pending: dict = {}
47
51
  _pending_lock = threading.Lock()
48
52
 
49
53
 
50
- def _held(now: float) -> Understanding | None:
54
+ def _held(now: float) -> tuple[Understanding | None, int]:
55
+ """The request being completed and how many spoken tries it has had."""
51
56
  with _pending_lock:
52
57
  got = _pending.get("u")
53
58
  if got and now - _pending.get("at", 0.0) <= FOLLOW_UP_SECONDS:
54
- return got
59
+ return got, _pending.get("misses", 0)
55
60
  _pending.clear()
56
- return None
61
+ return None, 0
57
62
 
58
63
 
59
- def _hold(u: Understanding | None, now: float) -> None:
64
+ def _hold(u: Understanding | None, now: float, misses: int = 0) -> None:
60
65
  with _pending_lock:
61
66
  _pending.clear()
62
67
  if u is not None:
63
- _pending.update(u=u, at=now)
68
+ _pending.update(u=u, at=now, misses=misses)
64
69
 
65
70
 
66
71
  def merge(new: Understanding, old: Understanding | None) -> Understanding:
@@ -110,11 +115,14 @@ def command(u: Understanding) -> str:
110
115
  def _answer(text: str, rows: list[dict], lately: list[str], places: list[str],
111
116
  send: Callable[[str, str], str], at: float, typed: bool) -> str:
112
117
  """Understand one request, spoken or typed: ask for what is missing, or send it."""
113
- got = merge(understand(text, recent=lately, displays=places, rows=rows), _held(at))
118
+ old, misses = _held(at)
119
+ got = merge(understand(text, recent=lately, displays=places, rows=rows), old)
114
120
  names = {t: resolve.spoken_name(r["n"]) for r in rows if (t := r["t"]) in got.ambiguous}
121
+ misses = misses if typed else misses + 1
122
+ typed = typed or misses >= VOICE_TRIES
115
123
  ask = question(got, names, "Type" if typed else "Say")
116
124
  if ask:
117
- _hold(got, at)
125
+ _hold(got, at, misses)
118
126
  requestlog.request("type" if typed else "hear", "ask", got.where or "", "")
119
127
  return typing(ask) if typed else f"{ask} {LISTENING}"
120
128
  _hold(None, at)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chartremotely
3
- Version: 0.2.14
3
+ Version: 0.2.16
4
4
  Summary: Remote control of desktop charting applications on macOS
5
5
  Author: Lonnie VanZandt
6
6
  License:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "chartremotely"
3
- version = "0.2.14"
3
+ version = "0.2.16"
4
4
  description = "Remote control of desktop charting applications on macOS"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -246,6 +246,9 @@ def test_the_shortcut_records_posts_branches_on_the_marker_asks_one_line_posts_t
246
246
  strip = actions[11]["WFWorkflowActionParameters"]
247
247
  assert (strip["WFReplaceTextFind"], strip["WFReplaceTextReplace"]) == (voice.TYPE, "")
248
248
 
249
+ speaks = _params(actions, "speaktext")
250
+ assert [s["WFSpeakTextRate"] for s in speaks] == [shortcut.SPEAK_RATE] * 2, "brisk, not slow"
251
+
249
252
  asks = _params(actions, "ask")
250
253
  assert len(asks) == 1, "one text box"
251
254
  ask = asks[0]
@@ -97,6 +97,20 @@ def test_a_follow_up_expires():
97
97
  assert ear.respond() == f"Which company at daily? {voice.LISTENING}"
98
98
 
99
99
 
100
+ def test_the_third_spoken_miss_asks_in_the_text_box_and_keeps_what_was_heard():
101
+ ear = Ear("mumble", "Nvidia", "grumble")
102
+ assert ear.respond().endswith(voice.LISTENING)
103
+ assert ear.respond() == f"I heard Nvidia but no scale. Say the scale, like half or daily. {voice.LISTENING}"
104
+ assert ear.respond() == "TYPE:I heard Nvidia but no scale. Type the scale, like half or daily."
105
+ assert ear.typed("daily") == "sent set NVDA | daily to this Mac"
106
+
107
+
108
+ def test_a_heard_request_resets_the_spoken_tries():
109
+ ear = Ear("mumble", "mumble", "Palantir half", "mumble")
110
+ ear.respond(), ear.respond(), ear.respond()
111
+ assert ear.respond().endswith(voice.LISTENING)
112
+
113
+
100
114
  def test_a_completed_request_is_not_carried_into_the_next():
101
115
  ear = Ear("Palantir half", "daily")
102
116
  ear.respond()
File without changes
File without changes
File without changes