sessionvoice 0.3.2__tar.gz → 0.3.3__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 (314) hide show
  1. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/PKG-INFO +56 -23
  2. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/README.md +55 -22
  3. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/pyproject.toml +1 -1
  4. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/_keeper/call-ears +38 -5
  5. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/_keeper/ears-keeper.sh +23 -27
  6. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/_keeper/ears-mail.py +365 -34
  7. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/_keeper/ears-sounds.py +60 -35
  8. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/_keeper/mouth-sounds.py +79 -33
  9. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/_settings/ears.schema.json +1 -1
  10. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/_settings/mouth.schema.json +4 -2
  11. sessionvoice-0.3.3/src/ears/_settings/talk.schema.json +20 -0
  12. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/_speakers/speaker-sidecar2.py +33 -14
  13. sessionvoice-0.3.3/src/ears/boxes.py +166 -0
  14. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/cli.py +13 -0
  15. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/heard.py +55 -14
  16. sessionvoice-0.3.3/src/ears/heardbox.py +105 -0
  17. sessionvoice-0.3.3/src/ears/home.py +1054 -0
  18. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/maildir.py +12 -8
  19. sessionvoice-0.3.3/src/ears/migrate.py +1299 -0
  20. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/recording.py +3 -1
  21. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/services.py +232 -63
  22. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/speakers.py +8 -0
  23. sessionvoice-0.3.3/src/ears/wake.py +445 -0
  24. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/__main__.py +3 -2
  25. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/bed.py +12 -2
  26. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/box.py +26 -6
  27. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/inbox.py +4 -2
  28. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/maillog.py +4 -2
  29. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/notice.py +8 -4
  30. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/reply.py +23 -15
  31. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/voices.py +6 -4
  32. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/sessionvoice.egg-info/PKG-INFO +56 -23
  33. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/sessionvoice.egg-info/SOURCES.txt +19 -0
  34. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/talk/index.html +89 -33
  35. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/talk/server.py +197 -49
  36. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_arm_finds_heard_first.py +1 -1
  37. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_arm_help.py +3 -2
  38. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_audio_fence.py +1 -1
  39. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_call_tap_help.py +3 -0
  40. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_call_tap_listen_off.py +3 -0
  41. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_call_tap_silero.py +3 -0
  42. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_configuration_docs.py +55 -0
  43. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_ears_keys.py +7 -0
  44. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_ears_mail.py +525 -1
  45. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_ears_settings_schema.py +14 -0
  46. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_ears_sounds_call.py +37 -0
  47. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_ears_wake.py +96 -1
  48. sessionvoice-0.3.3/tests/test_echo_own_line.py +140 -0
  49. sessionvoice-0.3.3/tests/test_heardbox.py +139 -0
  50. sessionvoice-0.3.3/tests/test_home.py +1085 -0
  51. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_instrumental_docs.py +30 -17
  52. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_keeper_again.py +26 -0
  53. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_keeper_defaults.py +30 -10
  54. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_keeper_listen_off.py +4 -3
  55. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_keeper_room.py +2 -1
  56. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_keeper_stop.py +3 -1
  57. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_log_dir.py +13 -7
  58. sessionvoice-0.3.3/tests/test_mail_fence.py +453 -0
  59. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_maildir_private.py +11 -3
  60. sessionvoice-0.3.3/tests/test_migrate.py +1130 -0
  61. sessionvoice-0.3.3/tests/test_mouth_sounds_defaults.py +66 -0
  62. sessionvoice-0.3.3/tests/test_nothing_born_outside.py +237 -0
  63. sessionvoice-0.3.3/tests/test_plists_after_migrate.py +338 -0
  64. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_recording.py +9 -1
  65. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_say_again_e2e.py +1 -1
  66. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_scanlist.py +3 -2
  67. sessionvoice-0.3.3/tests/test_seat_home.py +599 -0
  68. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_adopt_cold.py +2 -2
  69. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_install_checks.py +152 -15
  70. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_install_load_fail.py +8 -8
  71. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_install_preflight.py +5 -3
  72. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_sounds.py +7 -5
  73. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_speakers.py +29 -8
  74. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_speakers_checks.py +7 -7
  75. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_speech_cache.py +1 -1
  76. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_stt_model.py +1 -1
  77. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_talk.py +154 -0
  78. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_settings_in_wheel.py +2 -2
  79. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_settings_schema_refresh.py +3 -1
  80. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_settings_stranded.py +15 -1
  81. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_speakers_enrol.py +2 -2
  82. sessionvoice-0.3.3/tests/test_state_items.py +543 -0
  83. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_talk_generalise.py +21 -11
  84. sessionvoice-0.3.3/tests/test_talk_hangup.py +62 -0
  85. sessionvoice-0.3.3/tests/test_talk_hearers.py +56 -0
  86. sessionvoice-0.3.3/tests/test_talk_server.py +429 -0
  87. sessionvoice-0.3.3/tests/test_talk_uncrowd.py +133 -0
  88. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_waiter_shims.py +1 -1
  89. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_waiters.py +2 -2
  90. sessionvoice-0.3.3/tests/test_wake_rules_live.py +352 -0
  91. sessionvoice-0.3.3/tests/test_wake_self.py +259 -0
  92. sessionvoice-0.3.3/tests/test_wake_spoken_forms.py +249 -0
  93. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_wheel_build.py +1 -1
  94. sessionvoice-0.3.2/src/ears/home.py +0 -307
  95. sessionvoice-0.3.2/src/ears/wake.py +0 -261
  96. sessionvoice-0.3.2/tests/test_home.py +0 -355
  97. sessionvoice-0.3.2/tests/test_talk_server.py +0 -198
  98. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/LICENSE +0 -0
  99. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/setup.cfg +0 -0
  100. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/__init__.py +0 -0
  101. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/_keeper/held_stream.py +0 -0
  102. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/_keeper/input-watch.swift +0 -0
  103. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/_keeper/phone_roots.py +0 -0
  104. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/_settings/ears-sounds.schema.json +0 -0
  105. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/_speakers/cluster-room.py +0 -0
  106. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/_speakers/make-refs.py +0 -0
  107. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/_speakers/requirements.lock +0 -0
  108. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/_speakers/run.sh +0 -0
  109. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/_speech/build-constraints.txt +0 -0
  110. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/_speech/requirements.lock +0 -0
  111. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/backends/__init__.py +0 -0
  112. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/backends/apple.py +0 -0
  113. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/backends/nemotron.py +0 -0
  114. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/backends/proc.py +0 -0
  115. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/backends/whisper.py +0 -0
  116. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/callrec.py +0 -0
  117. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/envnames.py +0 -0
  118. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/eval.py +0 -0
  119. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/eval_mlx.py +0 -0
  120. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/events.py +0 -0
  121. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/extras.py +0 -0
  122. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/heard_wait.py +0 -0
  123. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/keys.py +0 -0
  124. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/lag.py +0 -0
  125. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/listen/__init__.py +0 -0
  126. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/listen/__main__.py +0 -0
  127. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/listen/aged.py +0 -0
  128. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/listen/chunker.py +0 -0
  129. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/listen/detector.py +0 -0
  130. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/listen/loop.py +0 -0
  131. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/listen/missed.py +0 -0
  132. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/listen/silero.py +0 -0
  133. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/listen/sink.py +0 -0
  134. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/listen/sources.py +0 -0
  135. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/listen/speakers.py +0 -0
  136. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/listen/streaming.py +0 -0
  137. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/listen/stt.py +0 -0
  138. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/listen/turnaudio.py +0 -0
  139. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/listen/watchdog.py +0 -0
  140. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/message_bin.py +0 -0
  141. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/operator_config.py +0 -0
  142. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/previews.py +0 -0
  143. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/redecode.py +0 -0
  144. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/serve.py +0 -0
  145. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/smartturn.py +0 -0
  146. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/speech.py +0 -0
  147. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/turns.py +0 -0
  148. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/ears/vad.py +0 -0
  149. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/__init__.py +0 -0
  150. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/_separator/requirements.lock +0 -0
  151. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/_separator/run.py +0 -0
  152. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/audio.py +0 -0
  153. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/autosplit.py +0 -0
  154. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/backends.py +0 -0
  155. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/bargeswitch.py +0 -0
  156. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/canplay.py +0 -0
  157. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/coreaudio.py +0 -0
  158. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/devices.py +0 -0
  159. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/duckswitch.py +0 -0
  160. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/hold.py +0 -0
  161. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/instrumental.py +0 -0
  162. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/level.py +0 -0
  163. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/mpv.py +0 -0
  164. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/onset.py +0 -0
  165. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/output.py +0 -0
  166. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/paths.py +0 -0
  167. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/player.py +0 -0
  168. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/resolver.py +0 -0
  169. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/separator.py +0 -0
  170. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/sidecar.py +0 -0
  171. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/split.py +0 -0
  172. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/splitwait.py +0 -0
  173. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/stems.py +0 -0
  174. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/mouth/timeline.py +0 -0
  175. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/sessionvoice.egg-info/dependency_links.txt +0 -0
  176. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/sessionvoice.egg-info/entry_points.txt +0 -0
  177. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/sessionvoice.egg-info/requires.txt +0 -0
  178. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/sessionvoice.egg-info/top_level.txt +0 -0
  179. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/talk/__init__.py +0 -0
  180. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/talk/__main__.py +0 -0
  181. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/talk/cli.py +0 -0
  182. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/talk/manifest.json +0 -0
  183. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/talk/token_store.py +0 -0
  184. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/src/talk/web.py +0 -0
  185. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_anchor.py +0 -0
  186. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_apple_assets_why_not.py +0 -0
  187. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_audio_devices_schema.py +0 -0
  188. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_bin_tool_fallback.py +0 -0
  189. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_call_sock_search.py +0 -0
  190. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_callrec.py +0 -0
  191. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_capture_mislabel.py +0 -0
  192. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_changelog_union.py +0 -0
  193. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_child_pytest.py +0 -0
  194. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_clock_check.py +0 -0
  195. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_conftest_ffmpeg.py +0 -0
  196. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_conftest_order.py +0 -0
  197. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_dependencies_docs.py +0 -0
  198. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_docs_layout.py +0 -0
  199. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_ears_no_backend.py +0 -0
  200. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_ears_serve_cli.py +0 -0
  201. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_ears_sounds_stream.py +0 -0
  202. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_ears_stt_key.py +0 -0
  203. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_echo_cut.py +0 -0
  204. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_echo_speaker.py +0 -0
  205. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_emitted_neutral.py +0 -0
  206. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_envnames.py +0 -0
  207. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_eval.py +0 -0
  208. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_export_ignore.py +0 -0
  209. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_extras.py +0 -0
  210. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_github_links.py +0 -0
  211. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_heard_contract.py +0 -0
  212. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_heard_cursor_by.py +0 -0
  213. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_heard_partials_only.py +0 -0
  214. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_heard_wait.py +0 -0
  215. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_heard_writer.py +0 -0
  216. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_install_apple_assets.py +0 -0
  217. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_install_apple_optin.py +0 -0
  218. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_install_silero_pin.py +0 -0
  219. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_keeper_adopt.py +0 -0
  220. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_keeper_beat.py +0 -0
  221. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_keeper_calls.py +0 -0
  222. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_keeper_deaf.py +0 -0
  223. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_keeper_fallback.py +0 -0
  224. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_keeper_in_wheel.py +0 -0
  225. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_keeper_lag.py +0 -0
  226. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_keeper_lid.py +0 -0
  227. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_keeper_missed_reply.py +0 -0
  228. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_keeper_model.py +0 -0
  229. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_keeper_speakers_deaf.py +0 -0
  230. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_keeper_stt_deaf.py +0 -0
  231. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_keeper_swiftc_guard.py +0 -0
  232. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_keeper_zeros.py +0 -0
  233. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_launchd_no_machine_paths.py +0 -0
  234. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_launchd_templates.py +0 -0
  235. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_aged.py +0 -0
  236. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_audio_stamp.py +0 -0
  237. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_call_source.py +0 -0
  238. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_clean_exit.py +0 -0
  239. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_hygiene.py +0 -0
  240. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_loop.py +0 -0
  241. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_missed_reply.py +0 -0
  242. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_parts.py +0 -0
  243. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_redecode.py +0 -0
  244. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_silero.py +0 -0
  245. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_speaker_agent.py +0 -0
  246. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_speaker_split.py +0 -0
  247. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_stack_dump.py +0 -0
  248. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_stream_feed.py +0 -0
  249. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_streaming.py +0 -0
  250. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_stt_follow.py +0 -0
  251. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_stt_waits.py +0 -0
  252. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_turn_speaker.py +0 -0
  253. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_vad_edges.py +0 -0
  254. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_watchdog.py +0 -0
  255. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_listen_wedge_scenarios.py +0 -0
  256. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_live_home_guard.py +0 -0
  257. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_lock_wheels.py +0 -0
  258. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_marketplace_description.py +0 -0
  259. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_message_bin.py +0 -0
  260. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_mouth_notice_fence.py +0 -0
  261. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_mouth_schema_plain.py +0 -0
  262. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_mouth_sounds_scoop_by.py +0 -0
  263. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_mouth_sounds_wake.py +0 -0
  264. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_name_hint_bench.py +0 -0
  265. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_no_real_times.py +0 -0
  266. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_no_time_of_day_stamps.py +0 -0
  267. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_no_tracker_ids.py +0 -0
  268. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_no_work_hour_stamps.py +0 -0
  269. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_operator_config.py +0 -0
  270. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_persona_defaults.py +0 -0
  271. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_plugin_install_doc.py +0 -0
  272. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_readme_links.py +0 -0
  273. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_recording_retention.py +0 -0
  274. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_release_script.py +0 -0
  275. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_sendmail_tripwire.py +0 -0
  276. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_separator_runner.py +0 -0
  277. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_service_build_constraints.py +0 -0
  278. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_service_hash_pins.py +0 -0
  279. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_bootstrap.py +0 -0
  280. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_dry_run.py +0 -0
  281. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_install_golden.py +0 -0
  282. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_kit_labels.py +0 -0
  283. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_label_move.py +0 -0
  284. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_legacy_plists.py +0 -0
  285. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_one_keeper.py +0 -0
  286. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_phone_only.py +0 -0
  287. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_prefetch.py +0 -0
  288. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_speech.py +0 -0
  289. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_stt_row.py +0 -0
  290. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_services_weights.py +0 -0
  291. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_settings_dir.py +0 -0
  292. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_speakers.py +0 -0
  293. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_speakers_call.py +0 -0
  294. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_speakers_follow.py +0 -0
  295. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_speakers_in_package.py +0 -0
  296. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_speakers_name.py +0 -0
  297. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_speakers_parity.py +0 -0
  298. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_speech.py +0 -0
  299. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_stt_name_hint.py +0 -0
  300. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_system_python_scripts.py +0 -0
  301. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_talk_cli.py +0 -0
  302. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_talk_host_check.py +0 -0
  303. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_talk_kit.py +0 -0
  304. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_talk_launchd.py +0 -0
  305. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_talk_page_pair.py +0 -0
  306. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_talk_pair.py +0 -0
  307. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_talk_prefix.py +0 -0
  308. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_talk_token.py +0 -0
  309. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_talk_token_gate.py +0 -0
  310. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_talk_unix.py +0 -0
  311. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_talk_web_room.py +0 -0
  312. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_turn_audio.py +0 -0
  313. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_turns.py +0 -0
  314. {sessionvoice-0.3.2 → sessionvoice-0.3.3}/tests/test_version_sync.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sessionvoice
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: One listening engine for coding agents and the Delta bot: Apple / Nemotron / whisper -> one event stream -> VAD -> turns
5
5
  Author-email: Mike Bailey <mike@sessionkit.ai>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -92,9 +92,11 @@ pair of ears on a Mac (depth in [docs/architecture.md](docs/architecture.md)):
92
92
  - **Capture** (`src/ears/listen`): `ears.listen capture` reads one input
93
93
  device and writes every partial, turn and heartbeat to the **heard log**
94
94
  (`src/ears/heard.py`):
95
- `<home>/logs/conversations/heard_YYYY-MM-DD.jsonl`, where `<home>` is
96
- `~/.voicemode` on a host that already keeps it there, else
97
- `~/.sessionvoice` ([docs/logs.md](docs/logs.md#sessionvoices-home)).
95
+ `<root>/logs/conversations/heard_YYYY-MM-DD.jsonl`, where `<root>` is the
96
+ state root: `~/.sessionkit/voice/state` on a new host, or the old
97
+ `~/.sessionvoice` or `~/.voicemode` on a host that already keeps its heard
98
+ log there ([docs/logs.md](docs/logs.md#the-state-root)).
99
+ `ears home state-dir` prints it.
98
100
  On a fresh host that also runs VoiceMode's `converse` as a heard guest,
99
101
  set `SESSIONVOICE_HOME=~/.voicemode` (or `VOICEMODE_BASE_DIR`), so that
100
102
  both write one log.
@@ -128,9 +130,11 @@ pair of ears on a Mac (depth in [docs/architecture.md](docs/architecture.md)):
128
130
  - **Sounds** (`keeper/ears-sounds.py`, launchd `dev.sessionkit.voice.ears-sounds`):
129
131
  a short sound in the user's ears when the ears log an event, so they know
130
132
  they were heard. Its partner, `keeper/mouth-sounds.py`
131
- (`dev.sessionkit.voice.mouth-sounds`), plays the mouth's cues. Opt-in:
132
- `ears services install --only sounds` loads both, from an installed
133
- package or a checkout; a bare install never does.
133
+ (`dev.sessionkit.voice.mouth-sounds`), plays the mouth's cues. A bare
134
+ `ears services install` loads both, from an installed package or a
135
+ checkout. From the first turn the ears' turn and partial ticks are on
136
+ (Settings turns them off), and so are the mouth's five cues
137
+ (`mouth-sounds.json` in the settings folder turns each off).
134
138
  - **Turn mail** (`keeper/ears-mail.py`, launchd `dev.sessionkit.voice.ears-mail`):
135
139
  mails each of the user's finished turns To and From
136
140
  `heard+room@<host>`, whatever input device the capture is following, with
@@ -159,6 +163,12 @@ pair of ears on a Mac (depth in [docs/architecture.md](docs/architecture.md)):
159
163
  label is dropped: the remaining words get `--speaker` (else
160
164
  `unknown@<host>`), never the seat whose echo was cut, and they can be a
161
165
  reply to that seat. A label naming anyone else is kept.
166
+ `Ears-Addressed` lists the seats a turn names, as `<seat>@<host>`, wherever
167
+ the name falls in the turn, plus `everyone` for "hey everyone":
168
+ `Ears-Addressed: everyone, ivy@<host>`. A seat is named by its `names` in
169
+ the wake file. The header carries names only, never the turn's words, and a
170
+ turn that names nobody has no `Ears-Addressed`. It goes on the room mail
171
+ only. [docs/ears.md](docs/ears.md) gives the rules.
162
172
  `Ears-Echo` says a mouth line heard back through the mic was cut out of
163
173
  the turn. On the speakers the mic hears the mouth, so a line's
164
174
  words are cut wherever they sit in the user's turn: at its start (up to `3 + 5
@@ -187,6 +197,25 @@ pair of ears on a Mac (depth in [docs/architecture.md](docs/architecture.md)):
187
197
  have opened within 2 s of the line beginning, unless the turn is mostly
188
198
  the line. On the `ts − (t1 − t0)` estimate, only the first two guards
189
199
  apply. Numerals (`9%` for "nine percent") still slip past the word match.
200
+ `Ears-Said` names each session's own line the turn came after, so the
201
+ pager can keep that session's window open (its linger):
202
+ `Ears-Said: <session>@<host>=<end>, ...`. `<session>` is the full session
203
+ id on the line's `said` row, `<host>` the keeper's, and `<end>` that
204
+ row's `ts`, as it was written. Each session gets one entry: its newest line
205
+ that finished at or before the turn's `Ears-Start`, within 600 s. It
206
+ carries spoken lines only: a line on `lane: file` (a music bed or a sound
207
+ file) is left out, so a bed that ends after a line leaves that line the
208
+ session's newest. A line with `interest: zero`, or one cut before it
209
+ played a word (`played_s` 0), is left out. A line the user barged into after part of it played counts,
210
+ at its cut. Entries are sorted, and the header holds names and times,
211
+ never a line's words. It goes on the device mail and the `--to seats`
212
+ mail, never on a cut notice or a call turn; with no entry there is no
213
+ header.
214
+ `Ears-Film: playing` marks a turn the keeper heard while a film was
215
+ playing (`--kodi`: the state that holds `reply` and `light`), so the
216
+ pager can tell film dialogue from the user. It is a fact about the turn,
217
+ with one value, on the device mail only; with no film playing, or no
218
+ `--kodi`, there is no header. Kodi is asked at most once per turn.
190
219
  - **Waiter** (`bin/heard-first`): an agent blocks on the heard log
191
220
  until a line is for it, by `--rule`: its name in a partial or a
192
221
  turn (`partial:REGEX`, `turn:REGEX`), a reply to its last line
@@ -229,10 +258,11 @@ Three JSON settings files, each reread with no restart:
229
258
  read only their own file.
230
259
 
231
260
  Environment variables win over single keys (`EARS_RECORD` over `record`,
232
- `MOUTH_DEVICE` over `output`). `SESSIONVOICE_HOME` moves the home that holds
233
- the heard log and each part's state; by default it is `~/.sessionvoice`, or
234
- `~/.voicemode` on a host that already keeps its heard log there. The launch
235
- agents are in `~/Library/LaunchAgents/`.
261
+ `MOUTH_DEVICE` over `output`). `SESSIONVOICE_HOME` moves the state root,
262
+ which holds the heard log and each part's state; by default it is
263
+ `~/.sessionkit/voice/state`, or the old `~/.sessionvoice` or `~/.voicemode` on
264
+ a host that already keeps its heard log there. The launch agents are in
265
+ `~/Library/LaunchAgents/`.
236
266
 
237
267
  Which files and which variables each part reads, and where each one lives:
238
268
  [docs/configuration.md](docs/configuration.md).
@@ -241,9 +271,9 @@ Every key, its type and its default:
241
271
 
242
272
  ## Logs
243
273
 
244
- - **The heard log:** `<home>/logs/conversations/heard_YYYY-MM-DD.jsonl`,
274
+ - **The heard log:** `<root>/logs/conversations/heard_YYYY-MM-DD.jsonl`,
245
275
  one JSON line for each thing the ears heard and the mouth said, where
246
- `<home>` is the home above. A new file starts each day.
276
+ `<root>` is the state root above (`ears home state-dir` prints it). A new file starts each day.
247
277
  - **The services' logs:** `~/.sessionkit/voice/log/` (`EARS_LOG_DIR` moves it).
248
278
  `ears-keeper.log` is what the keeper did (devices, restarts, calls, a
249
279
  deaf mic); `ears-keeper-capture.log` is the room capture's own output;
@@ -253,7 +283,7 @@ Every key, its type and its default:
253
283
 
254
284
  Follow one with `tail -F`, for example
255
285
  `tail -F ~/.sessionkit/voice/log/ears-keeper.log`, or today's heard log with
256
- `tail -F <home>/logs/conversations/heard_$(date +%F).jsonl`.
286
+ `tail -F "$(ears home state-dir)/logs/conversations/heard_$(date +%F).jsonl"`.
257
287
  Every log file and what each line means:
258
288
  [docs/logs.md](docs/logs.md).
259
289
 
@@ -302,7 +332,8 @@ conversation either. On the host, run `talk pair` in a terminal and open the
302
332
  link it prints; `talk token --rotate` makes every browser pair again. Open talk
303
333
  at `http://127.0.0.1:<port>/`, never at `localhost`. The code is `src/talk/`.
304
334
  `ears services install --only talk` keeps it running as a launch agent
305
- (opt-in; a bare install never does).
335
+ (opt-in; a bare install never does); add `--allow-host
336
+ machine.example.ts.net` for a phone behind `tailscale serve`.
306
337
 
307
338
  In the kit's web page, Talk is a room: the kit's front door runs
308
339
  `talk serve --kit` on a unix socket and logs you in, so there is nothing to
@@ -321,8 +352,7 @@ import that needs one fails naming it, as `pip install 'sessionvoice[EXTRA]'`.
321
352
 
322
353
  uv venv && uv pip install -e '.[all]' # onnxruntime + mlx-audio
323
354
  ears install
324
- ears services install --agent NAME # the launch agents: mouth, keeper, mail
325
- ears services install --only sounds # opt-in: the ears and mouth sounds
355
+ ears services install --agent NAME # the launch agents: mouth, keeper, mail, sounds
326
356
  ears services install --only talk # opt-in: talk serve, the live view
327
357
 
328
358
  A phone-only Mac (for example one that runs a Delta bot, with no agent
@@ -395,11 +425,12 @@ as its `log` row ([logs](docs/logs.md#the-log-files)). It then:
395
425
  postfix and makes the `heard` box with `message setup --box heard`
396
426
  (which also makes sessionmail's mail root, your own box and its notmuch
397
427
  profile, if they are missing, and prints each one). On a host that
398
- sends by postfix, it makes the `heard` box NESTED under the store dir
399
- that carries `.tm-store`, as `~/.mail/agents/<store>/heard/`, when there
400
- is exactly one such dir, then runs `vmailbox-gen` if that is on `PATH`,
401
- and otherwise prints the commands (`~/.mail/.postfix/vmailbox-gen`:
402
- sessionmail keeps it off `PATH`). When sessionmail's answer cannot be
428
+ sends by postfix, it makes the `heard` box NESTED under the store box
429
+ (`message box list` names it; `message path <store>` is its folder), as
430
+ `<store>/heard/`, when there is exactly one, then runs this host's
431
+ `vmailbox-gen` as sessionmail names it (`message path --generator`), and
432
+ otherwise prints the commands. sessionvoice never spells the mail root:
433
+ every box is sessionmail's answer. When sessionmail's answer cannot be
403
434
  read (no `message`, or one older than 0.2.0), it warns, makes no box,
404
435
  checks neither, and goes on.
405
436
  On a postfix host it also prints the box to subscribe to:
@@ -413,7 +444,9 @@ as its `log` row ([logs](docs/logs.md#the-log-files)). It then:
413
444
  `ears services install --agent NAME` for a Mac where an agent seat should
414
445
  be woken when you speak, and `ears services install --no-agent` for a
415
446
  phone-only Mac (for example a Delta bot), where nobody is woken and the
416
- notices go to the log; then `ears wake add NAME`, to add a seat later. Inside
447
+ notices go to the log; then `ears wake add NAME`, to add a seat later, or
448
+ `ears wake add --self` in the seat's agent file, to register it at every
449
+ launch ([docs/ears.md](docs/ears.md#wake-seats)). Inside
417
450
  an agent seat (`TM_AGENT_ADDRESS` set) it suggests that address's local
418
451
  part as NAME; it never installs on the suggestion. `--no-agent` installs
419
452
  anyway, and install warns that NOBODY is woken. A wake file that already
@@ -48,9 +48,11 @@ pair of ears on a Mac (depth in [docs/architecture.md](docs/architecture.md)):
48
48
  - **Capture** (`src/ears/listen`): `ears.listen capture` reads one input
49
49
  device and writes every partial, turn and heartbeat to the **heard log**
50
50
  (`src/ears/heard.py`):
51
- `<home>/logs/conversations/heard_YYYY-MM-DD.jsonl`, where `<home>` is
52
- `~/.voicemode` on a host that already keeps it there, else
53
- `~/.sessionvoice` ([docs/logs.md](docs/logs.md#sessionvoices-home)).
51
+ `<root>/logs/conversations/heard_YYYY-MM-DD.jsonl`, where `<root>` is the
52
+ state root: `~/.sessionkit/voice/state` on a new host, or the old
53
+ `~/.sessionvoice` or `~/.voicemode` on a host that already keeps its heard
54
+ log there ([docs/logs.md](docs/logs.md#the-state-root)).
55
+ `ears home state-dir` prints it.
54
56
  On a fresh host that also runs VoiceMode's `converse` as a heard guest,
55
57
  set `SESSIONVOICE_HOME=~/.voicemode` (or `VOICEMODE_BASE_DIR`), so that
56
58
  both write one log.
@@ -84,9 +86,11 @@ pair of ears on a Mac (depth in [docs/architecture.md](docs/architecture.md)):
84
86
  - **Sounds** (`keeper/ears-sounds.py`, launchd `dev.sessionkit.voice.ears-sounds`):
85
87
  a short sound in the user's ears when the ears log an event, so they know
86
88
  they were heard. Its partner, `keeper/mouth-sounds.py`
87
- (`dev.sessionkit.voice.mouth-sounds`), plays the mouth's cues. Opt-in:
88
- `ears services install --only sounds` loads both, from an installed
89
- package or a checkout; a bare install never does.
89
+ (`dev.sessionkit.voice.mouth-sounds`), plays the mouth's cues. A bare
90
+ `ears services install` loads both, from an installed package or a
91
+ checkout. From the first turn the ears' turn and partial ticks are on
92
+ (Settings turns them off), and so are the mouth's five cues
93
+ (`mouth-sounds.json` in the settings folder turns each off).
90
94
  - **Turn mail** (`keeper/ears-mail.py`, launchd `dev.sessionkit.voice.ears-mail`):
91
95
  mails each of the user's finished turns To and From
92
96
  `heard+room@<host>`, whatever input device the capture is following, with
@@ -115,6 +119,12 @@ pair of ears on a Mac (depth in [docs/architecture.md](docs/architecture.md)):
115
119
  label is dropped: the remaining words get `--speaker` (else
116
120
  `unknown@<host>`), never the seat whose echo was cut, and they can be a
117
121
  reply to that seat. A label naming anyone else is kept.
122
+ `Ears-Addressed` lists the seats a turn names, as `<seat>@<host>`, wherever
123
+ the name falls in the turn, plus `everyone` for "hey everyone":
124
+ `Ears-Addressed: everyone, ivy@<host>`. A seat is named by its `names` in
125
+ the wake file. The header carries names only, never the turn's words, and a
126
+ turn that names nobody has no `Ears-Addressed`. It goes on the room mail
127
+ only. [docs/ears.md](docs/ears.md) gives the rules.
118
128
  `Ears-Echo` says a mouth line heard back through the mic was cut out of
119
129
  the turn. On the speakers the mic hears the mouth, so a line's
120
130
  words are cut wherever they sit in the user's turn: at its start (up to `3 + 5
@@ -143,6 +153,25 @@ pair of ears on a Mac (depth in [docs/architecture.md](docs/architecture.md)):
143
153
  have opened within 2 s of the line beginning, unless the turn is mostly
144
154
  the line. On the `ts − (t1 − t0)` estimate, only the first two guards
145
155
  apply. Numerals (`9%` for "nine percent") still slip past the word match.
156
+ `Ears-Said` names each session's own line the turn came after, so the
157
+ pager can keep that session's window open (its linger):
158
+ `Ears-Said: <session>@<host>=<end>, ...`. `<session>` is the full session
159
+ id on the line's `said` row, `<host>` the keeper's, and `<end>` that
160
+ row's `ts`, as it was written. Each session gets one entry: its newest line
161
+ that finished at or before the turn's `Ears-Start`, within 600 s. It
162
+ carries spoken lines only: a line on `lane: file` (a music bed or a sound
163
+ file) is left out, so a bed that ends after a line leaves that line the
164
+ session's newest. A line with `interest: zero`, or one cut before it
165
+ played a word (`played_s` 0), is left out. A line the user barged into after part of it played counts,
166
+ at its cut. Entries are sorted, and the header holds names and times,
167
+ never a line's words. It goes on the device mail and the `--to seats`
168
+ mail, never on a cut notice or a call turn; with no entry there is no
169
+ header.
170
+ `Ears-Film: playing` marks a turn the keeper heard while a film was
171
+ playing (`--kodi`: the state that holds `reply` and `light`), so the
172
+ pager can tell film dialogue from the user. It is a fact about the turn,
173
+ with one value, on the device mail only; with no film playing, or no
174
+ `--kodi`, there is no header. Kodi is asked at most once per turn.
146
175
  - **Waiter** (`bin/heard-first`): an agent blocks on the heard log
147
176
  until a line is for it, by `--rule`: its name in a partial or a
148
177
  turn (`partial:REGEX`, `turn:REGEX`), a reply to its last line
@@ -185,10 +214,11 @@ Three JSON settings files, each reread with no restart:
185
214
  read only their own file.
186
215
 
187
216
  Environment variables win over single keys (`EARS_RECORD` over `record`,
188
- `MOUTH_DEVICE` over `output`). `SESSIONVOICE_HOME` moves the home that holds
189
- the heard log and each part's state; by default it is `~/.sessionvoice`, or
190
- `~/.voicemode` on a host that already keeps its heard log there. The launch
191
- agents are in `~/Library/LaunchAgents/`.
217
+ `MOUTH_DEVICE` over `output`). `SESSIONVOICE_HOME` moves the state root,
218
+ which holds the heard log and each part's state; by default it is
219
+ `~/.sessionkit/voice/state`, or the old `~/.sessionvoice` or `~/.voicemode` on
220
+ a host that already keeps its heard log there. The launch agents are in
221
+ `~/Library/LaunchAgents/`.
192
222
 
193
223
  Which files and which variables each part reads, and where each one lives:
194
224
  [docs/configuration.md](docs/configuration.md).
@@ -197,9 +227,9 @@ Every key, its type and its default:
197
227
 
198
228
  ## Logs
199
229
 
200
- - **The heard log:** `<home>/logs/conversations/heard_YYYY-MM-DD.jsonl`,
230
+ - **The heard log:** `<root>/logs/conversations/heard_YYYY-MM-DD.jsonl`,
201
231
  one JSON line for each thing the ears heard and the mouth said, where
202
- `<home>` is the home above. A new file starts each day.
232
+ `<root>` is the state root above (`ears home state-dir` prints it). A new file starts each day.
203
233
  - **The services' logs:** `~/.sessionkit/voice/log/` (`EARS_LOG_DIR` moves it).
204
234
  `ears-keeper.log` is what the keeper did (devices, restarts, calls, a
205
235
  deaf mic); `ears-keeper-capture.log` is the room capture's own output;
@@ -209,7 +239,7 @@ Every key, its type and its default:
209
239
 
210
240
  Follow one with `tail -F`, for example
211
241
  `tail -F ~/.sessionkit/voice/log/ears-keeper.log`, or today's heard log with
212
- `tail -F <home>/logs/conversations/heard_$(date +%F).jsonl`.
242
+ `tail -F "$(ears home state-dir)/logs/conversations/heard_$(date +%F).jsonl"`.
213
243
  Every log file and what each line means:
214
244
  [docs/logs.md](docs/logs.md).
215
245
 
@@ -258,7 +288,8 @@ conversation either. On the host, run `talk pair` in a terminal and open the
258
288
  link it prints; `talk token --rotate` makes every browser pair again. Open talk
259
289
  at `http://127.0.0.1:<port>/`, never at `localhost`. The code is `src/talk/`.
260
290
  `ears services install --only talk` keeps it running as a launch agent
261
- (opt-in; a bare install never does).
291
+ (opt-in; a bare install never does); add `--allow-host
292
+ machine.example.ts.net` for a phone behind `tailscale serve`.
262
293
 
263
294
  In the kit's web page, Talk is a room: the kit's front door runs
264
295
  `talk serve --kit` on a unix socket and logs you in, so there is nothing to
@@ -277,8 +308,7 @@ import that needs one fails naming it, as `pip install 'sessionvoice[EXTRA]'`.
277
308
 
278
309
  uv venv && uv pip install -e '.[all]' # onnxruntime + mlx-audio
279
310
  ears install
280
- ears services install --agent NAME # the launch agents: mouth, keeper, mail
281
- ears services install --only sounds # opt-in: the ears and mouth sounds
311
+ ears services install --agent NAME # the launch agents: mouth, keeper, mail, sounds
282
312
  ears services install --only talk # opt-in: talk serve, the live view
283
313
 
284
314
  A phone-only Mac (for example one that runs a Delta bot, with no agent
@@ -351,11 +381,12 @@ as its `log` row ([logs](docs/logs.md#the-log-files)). It then:
351
381
  postfix and makes the `heard` box with `message setup --box heard`
352
382
  (which also makes sessionmail's mail root, your own box and its notmuch
353
383
  profile, if they are missing, and prints each one). On a host that
354
- sends by postfix, it makes the `heard` box NESTED under the store dir
355
- that carries `.tm-store`, as `~/.mail/agents/<store>/heard/`, when there
356
- is exactly one such dir, then runs `vmailbox-gen` if that is on `PATH`,
357
- and otherwise prints the commands (`~/.mail/.postfix/vmailbox-gen`:
358
- sessionmail keeps it off `PATH`). When sessionmail's answer cannot be
384
+ sends by postfix, it makes the `heard` box NESTED under the store box
385
+ (`message box list` names it; `message path <store>` is its folder), as
386
+ `<store>/heard/`, when there is exactly one, then runs this host's
387
+ `vmailbox-gen` as sessionmail names it (`message path --generator`), and
388
+ otherwise prints the commands. sessionvoice never spells the mail root:
389
+ every box is sessionmail's answer. When sessionmail's answer cannot be
359
390
  read (no `message`, or one older than 0.2.0), it warns, makes no box,
360
391
  checks neither, and goes on.
361
392
  On a postfix host it also prints the box to subscribe to:
@@ -369,7 +400,9 @@ as its `log` row ([logs](docs/logs.md#the-log-files)). It then:
369
400
  `ears services install --agent NAME` for a Mac where an agent seat should
370
401
  be woken when you speak, and `ears services install --no-agent` for a
371
402
  phone-only Mac (for example a Delta bot), where nobody is woken and the
372
- notices go to the log; then `ears wake add NAME`, to add a seat later. Inside
403
+ notices go to the log; then `ears wake add NAME`, to add a seat later, or
404
+ `ears wake add --self` in the seat's agent file, to register it at every
405
+ launch ([docs/ears.md](docs/ears.md#wake-seats)). Inside
373
406
  an agent seat (`TM_AGENT_ADDRESS` set) it suggests that address's local
374
407
  part as NAME; it never installs on the suggestion. `--no-agent` installs
375
408
  anyway, and install warns that NOBODY is woken. A wake file that already
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sessionvoice"
3
- version = "0.3.2"
3
+ version = "0.3.3"
4
4
  description = "One listening engine for coding agents and the Delta bot: Apple / Nemotron / whisper -> one event stream -> VAD -> turns"
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-or-later"
@@ -12,8 +12,8 @@ A call already live when this starts keeps the ear it has (one restarted with th
12
12
  leaves its captures running), and one with no ear gets one. Once, every already-live
13
13
  call was left alone, so a restart mid-call left that call deaf to the end.
14
14
  Stop: touch $CALL_EARS_DIR/STOP. Log: stdout (the keeper's $EARS_LOG_DIR/ears-keeper-calls.log);
15
- each ear's in $CALL_EARS_DIR/capture-N.log. CALL_EARS_DIR defaults to
16
- ${XDG_STATE_HOME:-~/.local/state}/ears/call-ears (it was ~/.<agent>/call-ears).
15
+ each ear's in $CALL_EARS_DIR/capture-N.log. CALL_EARS_DIR defaults to the state item
16
+ call-ears (`ears home state-dir call-ears`; it was ~/.<agent>/call-ears, then an XDG folder).
17
17
 
18
18
  The room and the calls were once two launch agents; now they are one, so
19
19
  this is not a launch
@@ -30,9 +30,42 @@ if os.path.dirname(os.path.abspath(__file__)) not in sys.path:
30
30
  import phone_roots # noqa: E402 (keeper/ is not a package)
31
31
 
32
32
  HOME = Path.home()
33
- # The keeper passes its own; the default is the keeper's (keeper/ears-keeper.sh), change both.
34
- DIR = Path(os.environ.get("CALL_EARS_DIR")
35
- or Path(os.environ.get("XDG_STATE_HOME") or HOME / ".local/state") / "ears/call-ears")
33
+ HERE = Path(__file__).resolve().parent
34
+
35
+
36
+ def voice_home():
37
+ """ears/home.py, the ONE place voice names its state items.
38
+ keeper/ imports nothing from the ears package, so it loads home.py (standard library
39
+ only) by its path, once: the checkout's src/ears/home.py, else the wheel's ears/home.py
40
+ above ears/_keeper/. Not there: exit 2, naming where it looked; there is no copy of the
41
+ rule to fall back on."""
42
+ mod = getattr(voice_home, "mod", None)
43
+ if mod is None:
44
+ import importlib.util
45
+ tried = [HERE.parent / "src/ears/home.py", HERE.parent / "home.py"]
46
+ f = next((f for f in tried if f.is_file()), None)
47
+ if f is None:
48
+ print(f"call-ears: no ears/home.py at {' nor '.join(map(str, tried))}: it names "
49
+ "voice's state folders. Run it from a sessionvoice checkout, the plugin or the "
50
+ "package, or set CALL_EARS_DIR", file=sys.stderr)
51
+ sys.exit(2)
52
+ spec = importlib.util.spec_from_file_location("_voice_home", f)
53
+ mod = importlib.util.module_from_spec(spec)
54
+ spec.loader.exec_module(mod)
55
+ voice_home.mod = mod
56
+ return mod
57
+
58
+
59
+ def voice_state_path(item: str) -> Path:
60
+ """The state item ITEM's path, as the PACKAGE answers it (ears.home.asked_state_path):
61
+ its own variable wins and asks nothing; else exit 2, naming both answers, when the package
62
+ cannot be asked or disagrees with this checkout's home.py."""
63
+ return voice_home().asked_state_path(item, "call-ears")
64
+
65
+
66
+ # The keeper passes its own (CALL_EARS_DIR, always): this default is for a run by hand, and it
67
+ # is the keeper's, the state item call-ears (ears-keeper.sh asks the same).
68
+ DIR = voice_state_path("call-ears")
36
69
  STOP = DIR / "STOP"
37
70
  # The calls the probe finds live, "call:N,call:M", rewritten every loop (so its age says this
38
71
  # is alive): the keeper's room-off rule reads it. A capture is no proof of a live call - one
@@ -57,7 +57,7 @@
57
57
  # 5 min; back to 2 s after a run of a minute). It stands aside while another job runs a
58
58
  # call-ears (the old com.voicemode.ears-call), so a cutover never puts two ears on a call.
59
59
  # EARS_CALLS=0: the room only. The call ears' own stop file still works: touch
60
- # $CALL_EARS_DIR/STOP (default ${XDG_STATE_HOME:-~/.local/state}/ears/call-ears/STOP); rm it and
60
+ # $CALL_EARS_DIR/STOP (default: the state item call-ears, `ears home state-dir call-ears`); rm it and
61
61
  # the keeper starts them again.
62
62
  set -uo pipefail
63
63
  DEVICE="${1:-airpods}"
@@ -103,6 +103,14 @@ NOTIFY="${EARS_NOTIFY:-}"
103
103
  VOICE_SETTINGS="$HOME/.sessionkit/voice/settings"
104
104
  CONF="${EARS_CONF:-$VOICE_SETTINGS/ears.json}"
105
105
  LOGDIR="$("$PY" -m ears.home log-dir)" && [ -n "$LOGDIR" ] || { echo "ears-keeper: $PY -m ears.home log-dir named no log folder - run \`uv sync\` in $ROOT, or set EARS_PY" >&2; exit 2; }
106
+ # The state root, for the heard log's folder (HEARD_DIR, with the heard region below): the
107
+ # PACKAGE's answer, "$PY" -m ears.home state-dir, as for the log folder.
108
+ # $PY is the package's python and every writer this keeper starts runs under it, so its answer is
109
+ # the one they write to; there is no copy of the rule here. No answer (a 0.3.2 package has no
110
+ # state-dir): exit 2 before anything is read or written, never a guess. $EARS_HEARD_DIR asks nothing.
111
+ if [ -z "${EARS_HEARD_DIR:-}" ]; then
112
+ STATE_DIR="$("$PY" -m ears.home state-dir)" && [ -n "$STATE_DIR" ] || { echo "ears-keeper: $PY -m ears.home state-dir named no state root (a package older than 0.3.3 has none) - upgrade the package and the plugin together, run \`uv sync\` in $ROOT, or set EARS_PY" >&2; exit 2; }
113
+ fi
106
114
  RUN="${EARS_RUN:-$HOME/.cache/ears/run}"
107
115
  mkdir -p "$LOGDIR" "$RUN"
108
116
  # The settings-schema copies (.ears.schema.json and the rest), brought up to date with the
@@ -238,7 +246,9 @@ OUT_EVERY="${EARS_OUT_EVERY:-30}" # 0: no backstop, only after an input
238
246
  OUT_FALLBACK="${EARS_OUTPUT_FALLBACK:-1}" # 0: never touch the Mac's default output
239
247
  # Here, between OUT_EVERY= and listen_on(): tests/test_keeper_fallback.py runs this region
240
248
  # alone under set -u, so an assignment outside it would make every test fall back unseen.
241
- AUDIO_DEVICES="${AUDIO_DEVICES_FILE:-${XDG_STATE_HOME:-$HOME/.local/state}/audio/devices.json}"
249
+ # The state item audio-devices: AUDIO_DEVICES_FILE, else the package's
250
+ # answer (its old file while that is real, else in the state root). No answer: exit 2.
251
+ AUDIO_DEVICES="${AUDIO_DEVICES_FILE:-$("$PY" -m ears.home state-dir audio-devices)}" && [ -n "$AUDIO_DEVICES" ] || { echo "ears-keeper: $PY -m ears.home state-dir audio-devices named no devices file - upgrade the package and the plugin together, or set AUDIO_DEVICES_FILE" >&2; exit 2; }
242
252
  out_tried=""; out_src="file"
243
253
  audio_outputs() { # line 1 = the default output ("" if none), then one output name per
244
254
  # line, from $AUDIO_DEVICES while it is fresh. Exit 1 = stale or missing: do your own read.
@@ -394,7 +404,9 @@ call_live() { # the live calls the ears hear, "call:N[,call:M]"; "" when none,
394
404
  }
395
405
  calls_published() { # call-ears' probe, "call:N[,call:M]" from $CALL_EARS_DIR/live; "" when the
396
406
  # file is absent or older than EARS_CALLS_LIVE_AGE seconds (call-ears is down: captures decide)
397
- local f="${CALL_EARS_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/ears/call-ears}/live" m
407
+ local f m
408
+ f="${CALL_EARS_DIR:-$("$PY" -m ears.home state-dir call-ears)}" && [ -n "$f" ] || return 0
409
+ f="$f/live"
398
410
  # macOS stat by path: Homebrew's GNU stat can come first on PATH, and reads -f as --file-system
399
411
  m="$(/usr/bin/stat -f %m "$f" 2>/dev/null || stat -c %Y "$f" 2>/dev/null)" || return 0
400
412
  [ $(( $(date +%s) - m )) -le "${EARS_CALLS_LIVE_AGE:-5}" ] || return 0
@@ -403,9 +415,11 @@ calls_published() { # call-ears' probe, "call:N[,call:M]" from $CALL_EARS_DIR/l
403
415
  # --- calls: the call ears, this keeper's child (tests/test_keeper_calls.py runs these alone) ---
404
416
  CALLS="${EARS_CALLS:-1}"
405
417
  CALL_EARS="${EARS_CALL_EARS:-$(cd "$(dirname "$0")" && pwd)/call-ears}"
406
- # call-ears' own dir (STOP, live, its log): user state, as bin/call-tap's RUN is
407
- # (a host keeping its old dir sets CALL_EARS_DIR to it). keeper/call-ears has the same default.
408
- CALL_EARS_DIR="${CALL_EARS_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/ears/call-ears}"; export CALL_EARS_DIR
418
+ # call-ears' own dir (STOP, live, its log): the state item call-ears, as
419
+ # bin/call-tap's RUN is call-tap: CALL_EARS_DIR, else the package's answer (its old folder while
420
+ # that is real, else in the state root). keeper/call-ears asks the same item. No answer: exit 2.
421
+ CALL_EARS_DIR="${CALL_EARS_DIR:-$("$PY" -m ears.home state-dir call-ears)}" && [ -n "$CALL_EARS_DIR" ] || { echo "ears-keeper: $PY -m ears.home state-dir call-ears named no call ears folder - upgrade the package and the plugin together, or set CALL_EARS_DIR" >&2; exit 2; }
422
+ export CALL_EARS_DIR
409
423
  # A call-ears is a python running a file named call-ears: the old launch agent's, ~/.<agent>/tools'
410
424
  # or ours. Not a capture's `--session call-ears-N`, not `less keeper/call-ears`.
411
425
  CALL_EARS_PAT="${EARS_CALL_EARS_PAT:-[Pp]ython[0-9.]* [^ ]*/call-ears( |$)}"
@@ -624,26 +638,7 @@ model_started() { # $1 = the device: a capture is running, so a model DEAF epis
624
638
  # listen-stopped and its partials and turns go to the stt region. The speaker side-car's
625
639
  # rows, speakers_DAY.jsonl beside the heard log, are read in the SAME python, with their own offset
626
640
  # and the same day-roll and cut-short rules, and go to the speakers region.
627
- # --- sessionvoice_home: a copy of ears.home.sessionvoice_home (tests/test_home.py holds it) ---
628
- # $SESSIONVOICE_HOME, $VOICEMODE_BASE_DIR, ~/.sessionvoice or ~/.voicemode if it holds the
629
- # heard log (a heard.lock or a heard_*.jsonl in logs/conversations), else ~/.sessionvoice.
630
- sessionvoice_home() {
631
- local v d
632
- for v in "${SESSIONVOICE_HOME:-}" "${VOICEMODE_BASE_DIR:-}"; do
633
- [ -n "$v" ] || continue
634
- case $v in "~" | "~/"*) v="$HOME${v#\~}" ;; esac
635
- echo "$v"; return 0
636
- done
637
- for d in "$HOME/.sessionvoice" "$HOME/.voicemode"; do
638
- if [ -e "$d/logs/conversations/heard.lock" ] \
639
- || compgen -G "$d/logs/conversations/heard_*.jsonl" >/dev/null; then
640
- echo "$d"; return 0
641
- fi
642
- done
643
- echo "$HOME/.sessionvoice"
644
- }
645
- # --- end sessionvoice_home ---
646
- HEARD_DIR="${EARS_HEARD_DIR:-$(sessionvoice_home)/logs/conversations}"
641
+ HEARD_DIR="${EARS_HEARD_DIR:-${STATE_DIR:-}/logs/conversations}" # STATE_DIR: asked at the top (or exit 2)
647
642
  heard_file=""; heard_off=0; spk_file=""; spk_off=0
648
643
  heard_events() { # $1 = heard log, $2 = byte offset -> line 1 the offset after the last whole line,
649
644
  # then one TAB line per event in between:
@@ -1179,7 +1174,8 @@ stop_pid() { # $1 pid, $2 what it is, $3 its stop file ("" for none): the stop
1179
1174
  # nothing on a loop that finds nothing to stop.
1180
1175
  # ears_off runs every loop while off (and at a start that is off), so an ear started behind the
1181
1176
  # keeper's back is stopped within a loop too.
1182
- TAP_RUN="${EARS_CALL_TAP_RUN:-${XDG_STATE_HOME:-$HOME/.local/state}/ears/call-tap}" # bin/call-tap's RUN
1177
+ # bin/call-tap's RUN: the state item call-tap, asked as call-tap asks it (EARS_CALL_TAP_RUN wins)
1178
+ TAP_RUN="${EARS_CALL_TAP_RUN:-$("$PY" -m ears.home state-dir call-tap)}" && [ -n "$TAP_RUN" ] || { echo "ears-keeper: $PY -m ears.home state-dir call-tap named no call-tap folder - upgrade the package and the plugin together, or set EARS_CALL_TAP_RUN" >&2; exit 2; }
1183
1179
  off_other=""
1184
1180
  call_legs() { # the running call-leg captures (`--source call:N`), one pid per line
1185
1181
  local p args