sessionvoice 0.3.0__tar.gz → 0.3.2__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 (294) hide show
  1. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/PKG-INFO +72 -159
  2. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/README.md +71 -158
  3. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/pyproject.toml +14 -2
  4. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/_keeper/call-ears +8 -5
  5. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/_keeper/ears-keeper.sh +57 -18
  6. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/_keeper/ears-mail.py +117 -52
  7. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/_keeper/ears-sounds.py +25 -1
  8. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/_keeper/mouth-sounds.py +24 -3
  9. sessionvoice-0.3.2/src/ears/_keeper/phone_roots.py +73 -0
  10. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/_settings/ears.schema.json +1 -1
  11. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/_settings/mouth.schema.json +6 -0
  12. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/_speakers/run.sh +2 -3
  13. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/_speakers/speaker-sidecar2.py +197 -40
  14. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/_speech/build-constraints.txt +7 -6
  15. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/_speech/requirements.lock +70 -4
  16. sessionvoice-0.3.2/src/ears/callrec.py +26 -0
  17. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/eval.py +2 -3
  18. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/heard.py +1 -1
  19. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/home.py +82 -1
  20. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/listen/__main__.py +10 -4
  21. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/listen/loop.py +39 -0
  22. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/listen/sources.py +47 -12
  23. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/listen/watchdog.py +46 -9
  24. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/services.py +927 -117
  25. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/__init__.py +83 -8
  26. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/__main__.py +83 -6
  27. sessionvoice-0.3.2/src/mouth/_separator/requirements.lock +703 -0
  28. sessionvoice-0.3.2/src/mouth/_separator/run.py +302 -0
  29. sessionvoice-0.3.2/src/mouth/autosplit.py +260 -0
  30. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/backends.py +31 -9
  31. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/bed.py +12 -3
  32. sessionvoice-0.3.2/src/mouth/canplay.py +189 -0
  33. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/devices.py +14 -4
  34. sessionvoice-0.3.2/src/mouth/instrumental.py +121 -0
  35. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/notice.py +2 -0
  36. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/output.py +13 -19
  37. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/player.py +67 -8
  38. sessionvoice-0.3.2/src/mouth/separator.py +429 -0
  39. sessionvoice-0.3.2/src/mouth/sidecar.py +318 -0
  40. sessionvoice-0.3.2/src/mouth/split.py +619 -0
  41. sessionvoice-0.3.2/src/mouth/splitwait.py +162 -0
  42. sessionvoice-0.3.2/src/mouth/stems.py +155 -0
  43. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/voices.py +65 -4
  44. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/sessionvoice.egg-info/PKG-INFO +72 -159
  45. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/sessionvoice.egg-info/SOURCES.txt +30 -0
  46. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/sessionvoice.egg-info/entry_points.txt +4 -0
  47. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/talk/cli.py +29 -4
  48. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/talk/index.html +3 -1
  49. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/talk/server.py +119 -9
  50. sessionvoice-0.3.2/src/talk/web.py +76 -0
  51. sessionvoice-0.3.2/tests/test_bin_tool_fallback.py +136 -0
  52. sessionvoice-0.3.2/tests/test_call_sock_search.py +292 -0
  53. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_call_tap_silero.py +1 -1
  54. sessionvoice-0.3.2/tests/test_callrec.py +55 -0
  55. sessionvoice-0.3.2/tests/test_capture_mislabel.py +321 -0
  56. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_clock_check.py +9 -7
  57. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_configuration_docs.py +5 -2
  58. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_conftest_ffmpeg.py +9 -8
  59. sessionvoice-0.3.2/tests/test_dependencies_docs.py +135 -0
  60. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_ears_mail.py +124 -33
  61. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_ears_sounds_stream.py +30 -0
  62. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_ears_wake.py +9 -9
  63. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_echo_cut.py +6 -3
  64. sessionvoice-0.3.2/tests/test_echo_speaker.py +209 -0
  65. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_eval.py +13 -9
  66. sessionvoice-0.3.2/tests/test_instrumental_docs.py +251 -0
  67. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_keeper_deaf.py +1 -1
  68. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_keeper_defaults.py +16 -9
  69. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_keeper_in_wheel.py +14 -4
  70. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_keeper_speakers_deaf.py +84 -1
  71. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_launchd_templates.py +73 -10
  72. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_redecode.py +70 -0
  73. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_watchdog.py +77 -0
  74. sessionvoice-0.3.2/tests/test_log_dir.py +443 -0
  75. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_plugin_install_doc.py +4 -3
  76. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_say_again_e2e.py +3 -1
  77. sessionvoice-0.3.2/tests/test_separator_runner.py +690 -0
  78. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_service_build_constraints.py +7 -6
  79. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_service_hash_pins.py +5 -1
  80. sessionvoice-0.3.2/tests/test_services_adopt_cold.py +271 -0
  81. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_services_install_checks.py +113 -20
  82. sessionvoice-0.3.2/tests/test_services_install_golden.py +168 -0
  83. sessionvoice-0.3.2/tests/test_services_install_load_fail.py +316 -0
  84. sessionvoice-0.3.2/tests/test_services_install_preflight.py +371 -0
  85. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_services_label_move.py +1 -0
  86. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_services_one_keeper.py +1 -0
  87. sessionvoice-0.3.2/tests/test_services_phone_only.py +307 -0
  88. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_services_sounds.py +1 -1
  89. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_services_speakers.py +3 -2
  90. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_services_speech.py +104 -2
  91. sessionvoice-0.3.2/tests/test_services_speech_cache.py +217 -0
  92. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_services_stt_model.py +3 -1
  93. sessionvoice-0.3.2/tests/test_services_stt_row.py +98 -0
  94. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_services_talk.py +1 -1
  95. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_settings_dir.py +13 -1
  96. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_settings_in_wheel.py +18 -1
  97. sessionvoice-0.3.2/tests/test_settings_schema_refresh.py +241 -0
  98. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_speakers_call.py +114 -15
  99. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_speakers_follow.py +331 -15
  100. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_speakers_parity.py +3 -2
  101. sessionvoice-0.3.2/tests/test_talk_kit.py +410 -0
  102. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_talk_launchd.py +6 -3
  103. sessionvoice-0.3.2/tests/test_talk_prefix.py +250 -0
  104. sessionvoice-0.3.2/tests/test_talk_web_room.py +333 -0
  105. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_wheel_build.py +16 -5
  106. sessionvoice-0.3.0/src/ears/callrec.py +0 -34
  107. sessionvoice-0.3.0/tests/test_call_sock_search.py +0 -90
  108. sessionvoice-0.3.0/tests/test_callrec.py +0 -39
  109. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/LICENSE +0 -0
  110. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/setup.cfg +0 -0
  111. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/__init__.py +0 -0
  112. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/_keeper/held_stream.py +0 -0
  113. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/_keeper/input-watch.swift +0 -0
  114. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/_settings/ears-sounds.schema.json +0 -0
  115. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/_speakers/cluster-room.py +0 -0
  116. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/_speakers/make-refs.py +0 -0
  117. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/_speakers/requirements.lock +0 -0
  118. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/backends/__init__.py +0 -0
  119. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/backends/apple.py +0 -0
  120. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/backends/nemotron.py +0 -0
  121. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/backends/proc.py +0 -0
  122. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/backends/whisper.py +0 -0
  123. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/cli.py +0 -0
  124. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/envnames.py +0 -0
  125. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/eval_mlx.py +0 -0
  126. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/events.py +0 -0
  127. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/extras.py +0 -0
  128. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/heard_wait.py +0 -0
  129. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/keys.py +0 -0
  130. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/lag.py +0 -0
  131. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/listen/__init__.py +0 -0
  132. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/listen/aged.py +0 -0
  133. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/listen/chunker.py +0 -0
  134. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/listen/detector.py +0 -0
  135. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/listen/missed.py +0 -0
  136. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/listen/silero.py +0 -0
  137. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/listen/sink.py +0 -0
  138. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/listen/speakers.py +0 -0
  139. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/listen/streaming.py +0 -0
  140. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/listen/stt.py +0 -0
  141. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/listen/turnaudio.py +0 -0
  142. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/maildir.py +0 -0
  143. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/message_bin.py +0 -0
  144. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/operator_config.py +0 -0
  145. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/previews.py +0 -0
  146. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/recording.py +0 -0
  147. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/redecode.py +0 -0
  148. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/serve.py +0 -0
  149. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/smartturn.py +0 -0
  150. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/speakers.py +0 -0
  151. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/speech.py +0 -0
  152. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/turns.py +0 -0
  153. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/vad.py +0 -0
  154. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/ears/wake.py +0 -0
  155. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/audio.py +0 -0
  156. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/bargeswitch.py +0 -0
  157. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/box.py +0 -0
  158. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/coreaudio.py +0 -0
  159. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/duckswitch.py +0 -0
  160. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/hold.py +0 -0
  161. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/inbox.py +0 -0
  162. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/level.py +0 -0
  163. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/maillog.py +0 -0
  164. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/mpv.py +0 -0
  165. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/onset.py +0 -0
  166. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/paths.py +0 -0
  167. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/reply.py +0 -0
  168. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/resolver.py +0 -0
  169. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/mouth/timeline.py +0 -0
  170. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/sessionvoice.egg-info/dependency_links.txt +0 -0
  171. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/sessionvoice.egg-info/requires.txt +0 -0
  172. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/sessionvoice.egg-info/top_level.txt +0 -0
  173. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/talk/__init__.py +0 -0
  174. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/talk/__main__.py +0 -0
  175. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/talk/manifest.json +0 -0
  176. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/src/talk/token_store.py +0 -0
  177. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_anchor.py +0 -0
  178. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_apple_assets_why_not.py +0 -0
  179. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_arm_finds_heard_first.py +0 -0
  180. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_arm_help.py +0 -0
  181. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_audio_devices_schema.py +0 -0
  182. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_audio_fence.py +0 -0
  183. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_call_tap_help.py +0 -0
  184. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_call_tap_listen_off.py +0 -0
  185. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_changelog_union.py +0 -0
  186. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_child_pytest.py +0 -0
  187. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_conftest_order.py +0 -0
  188. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_docs_layout.py +0 -0
  189. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_ears_keys.py +0 -0
  190. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_ears_no_backend.py +0 -0
  191. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_ears_serve_cli.py +0 -0
  192. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_ears_settings_schema.py +0 -0
  193. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_ears_sounds_call.py +0 -0
  194. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_ears_stt_key.py +0 -0
  195. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_emitted_neutral.py +0 -0
  196. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_envnames.py +0 -0
  197. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_export_ignore.py +0 -0
  198. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_extras.py +0 -0
  199. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_github_links.py +0 -0
  200. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_heard_contract.py +0 -0
  201. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_heard_cursor_by.py +0 -0
  202. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_heard_partials_only.py +0 -0
  203. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_heard_wait.py +0 -0
  204. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_heard_writer.py +0 -0
  205. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_home.py +0 -0
  206. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_install_apple_assets.py +0 -0
  207. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_install_apple_optin.py +0 -0
  208. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_install_silero_pin.py +0 -0
  209. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_keeper_adopt.py +0 -0
  210. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_keeper_again.py +0 -0
  211. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_keeper_beat.py +0 -0
  212. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_keeper_calls.py +0 -0
  213. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_keeper_fallback.py +0 -0
  214. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_keeper_lag.py +0 -0
  215. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_keeper_lid.py +0 -0
  216. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_keeper_listen_off.py +0 -0
  217. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_keeper_missed_reply.py +0 -0
  218. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_keeper_model.py +0 -0
  219. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_keeper_room.py +0 -0
  220. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_keeper_stop.py +0 -0
  221. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_keeper_stt_deaf.py +0 -0
  222. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_keeper_swiftc_guard.py +0 -0
  223. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_keeper_zeros.py +0 -0
  224. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_launchd_no_machine_paths.py +0 -0
  225. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_aged.py +0 -0
  226. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_audio_stamp.py +0 -0
  227. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_call_source.py +0 -0
  228. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_clean_exit.py +0 -0
  229. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_hygiene.py +0 -0
  230. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_loop.py +0 -0
  231. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_missed_reply.py +0 -0
  232. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_parts.py +0 -0
  233. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_silero.py +0 -0
  234. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_speaker_agent.py +0 -0
  235. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_speaker_split.py +0 -0
  236. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_stack_dump.py +0 -0
  237. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_stream_feed.py +0 -0
  238. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_streaming.py +0 -0
  239. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_stt_follow.py +0 -0
  240. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_stt_waits.py +0 -0
  241. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_turn_speaker.py +0 -0
  242. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_vad_edges.py +0 -0
  243. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_listen_wedge_scenarios.py +0 -0
  244. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_live_home_guard.py +0 -0
  245. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_lock_wheels.py +0 -0
  246. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_maildir_private.py +0 -0
  247. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_marketplace_description.py +0 -0
  248. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_message_bin.py +0 -0
  249. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_mouth_notice_fence.py +0 -0
  250. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_mouth_schema_plain.py +0 -0
  251. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_mouth_sounds_scoop_by.py +0 -0
  252. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_mouth_sounds_wake.py +0 -0
  253. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_name_hint_bench.py +0 -0
  254. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_no_real_times.py +0 -0
  255. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_no_time_of_day_stamps.py +0 -0
  256. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_no_tracker_ids.py +0 -0
  257. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_no_work_hour_stamps.py +0 -0
  258. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_operator_config.py +0 -0
  259. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_persona_defaults.py +0 -0
  260. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_readme_links.py +0 -0
  261. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_recording.py +0 -0
  262. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_recording_retention.py +0 -0
  263. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_release_script.py +0 -0
  264. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_scanlist.py +0 -0
  265. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_sendmail_tripwire.py +0 -0
  266. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_services_bootstrap.py +0 -0
  267. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_services_dry_run.py +0 -0
  268. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_services_kit_labels.py +0 -0
  269. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_services_legacy_plists.py +0 -0
  270. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_services_prefetch.py +0 -0
  271. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_services_speakers_checks.py +0 -0
  272. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_services_weights.py +0 -0
  273. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_settings_stranded.py +0 -0
  274. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_speakers.py +0 -0
  275. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_speakers_enrol.py +0 -0
  276. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_speakers_in_package.py +0 -0
  277. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_speakers_name.py +0 -0
  278. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_speech.py +0 -0
  279. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_stt_name_hint.py +0 -0
  280. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_system_python_scripts.py +0 -0
  281. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_talk_cli.py +0 -0
  282. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_talk_generalise.py +0 -0
  283. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_talk_host_check.py +0 -0
  284. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_talk_page_pair.py +0 -0
  285. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_talk_pair.py +0 -0
  286. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_talk_server.py +0 -0
  287. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_talk_token.py +0 -0
  288. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_talk_token_gate.py +0 -0
  289. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_talk_unix.py +0 -0
  290. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_turn_audio.py +0 -0
  291. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_turns.py +0 -0
  292. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_version_sync.py +0 -0
  293. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_waiter_shims.py +0 -0
  294. {sessionvoice-0.3.0 → sessionvoice-0.3.2}/tests/test_waiters.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sessionvoice
3
- Version: 0.3.0
3
+ Version: 0.3.2
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
@@ -76,143 +76,13 @@ streams transcription, and decides when the speaker has finished.
76
76
 
77
77
  ## Requirements
78
78
 
79
- - **macOS** for the always-on parts. The keeper follows devices through
80
- CoreAudio (it builds `keeper/input-watch.swift` with `swiftc`,
81
- `keeper/ears-keeper.sh:134`, when the Command Line Tools are installed;
82
- without them it polls `system_profiler` and never runs the `swiftc`
83
- install stub), and `ears services install` writes launchd
84
- agents to `~/Library/LaunchAgents` (`src/ears/services.py:93`). The
85
- `apple` backend needs macOS 27, Xcode's `swift` and a source checkout
86
- (`uv run ears install --apple`: the wheel does not ship `swift/`, and
87
- `--apple` without it refuses). The engine alone (`ears file`, `ears
88
- listen` with `whisper`) needs no Mac.
89
- - **Python 3.10-3.14** (`requires-python` in `pyproject.toml`) and
90
- **[uv](https://docs.astral.sh/uv/)**: `make test` runs `uv run --locked`.
91
- - **A speech server on `127.0.0.1:8890`**: mlx-audio's server, which serves
92
- both STT and TTS. It is the ears' default STT (Parakeet;
93
- `src/ears/listen/stt.py:86`, `src/ears/services.py:530`) and the
94
- mouth's Kokoro voices (`src/mouth/backends.py:289`). `ears services
95
- install` finds one or installs its own (`src/ears/speech.py`), and asks
96
- before it acts, in this order:
97
- - **Opt-out.** `final.url` in `~/.sessionkit/voice/settings/ears.json` (or
98
- `EARS_STT_URL` at install) names another engine, and the mouth is not
99
- being installed: `:8890` is not sessionvoice's to manage.
100
- - **Use.** Something listens on `:8890` and says it is mlx-audio: `GET /`
101
- returns mlx-audio's welcome line, or `/openapi.json` lists its audio
102
- routes. sessionvoice uses that server, whoever installed it (VoiceMode,
103
- for example), and starts nothing.
104
- - **Refuse.** Something else holds `:8890`, or holds it and does not
105
- answer. Install starts nothing, replaces nothing, and names the holder.
106
- When the ears hear through `:8890` it exits 2 and says what to run;
107
- when only the mouth does, it warns and the mouth uses `say`. Free the
108
- port, or point `final.url` at another speech server.
109
- - **Never a second.** Nothing answers on `:8890`, but a loaded launch
110
- agent runs `mlx_audio.server` (under any label, so another installer's
111
- job between restarts counts). Install starts nothing. When that job is
112
- sessionvoice's own, run `ears services uninstall --only speech`, then
113
- install again.
114
- - **Install.** Nothing on `:8890` and no such job. On an Apple-silicon
115
- Mac (macOS 14 or later), install builds sessionvoice's own venv,
116
- `~/.local/share/ears/speech/venv`, from the shipped
117
- `speech/requirements.lock` (`mlx-audio==0.5.8`), and loads it as the
118
- launch agent `dev.sessionkit.voice.mlx-audio`. `EARS_SPEECH_PY` names a
119
- python that already has it, and no venv is built. On any other host it
120
- installs nothing: it refuses and says what to run when the ears need
121
- `:8890`, and warns when only the mouth does.
122
-
123
- `--dry-run` says which of these install would take, and builds and writes
124
- nothing. `ears services status` has a `speech` row that names who serves
125
- `:8890`. `ears services uninstall --only speech` removes only
126
- `dev.sessionkit.voice.mlx-audio`, never a server someone else installed, and
127
- keeps its venv. While sessionvoice's own server runs, a re-install just
128
- uses it, so to upgrade it, uninstall it and install again: install
129
- re-applies the lock's pins to the venv it finds. The lock carries a hash
130
- for every pin, and install refuses an artifact that does not match it.
131
- Two pins, `docopt` and `webrtcvad`, ship only as source, so uv builds them,
132
- and the setuptools it builds them with is hash-checked too, from the
133
- shipped `speech/build-constraints.txt`. A new venv is checked in full. A
134
- re-apply checks only what it installs, not what is already in the venv, so
135
- to check an existing venv, remove it and install again. Install then
136
- fetches the server's weights, about 2.9 GB, so the first call does not wait
137
- on them. It prints the size first, skips any already cached and prints
138
- progress while it fetches. `--no-prefetch` (or `EARS_NO_PREFETCH=1`) skips
139
- this on a host that must not download, and `--dry-run` prints the size and
140
- fetches nothing. A fetch that fails does not fail the install. It says why
141
- and how much is left. Whatever is not fetched at install is downloaded by
142
- the server on first use: the first transcription waits on Parakeet, and the
143
- first line spoken waits on Kokoro and its voices.
144
-
145
- **These weights are not pinned.** mlx-audio's server takes whatever is on
146
- Hugging Face `main` at first use, and it has no way to ask for a revision,
147
- so a later first use (a new Mac, a cleared cache) can load different
148
- weights. `ears services weights` (and the `weight` rows of `ears services
149
- status`) reads the three, `mlx-community/parakeet-tdt-0.6b-v3`,
150
- `mlx-community/Kokoro-82M-bf16` and `prince-canuma/Kokoro-82M` (the
151
- voices), from the Hugging Face cache: `cached`, `downloading 412 MB of
152
- 2.5 GB`, or `not cached`, and `DIFFERS` when the cache's `main` is not the
153
- commit sessionvoice expects (`SPEECH_WEIGHTS` in `src/ears/services.py`).
154
- `--json` prints the same for a program, schema `ears.weights/1`
155
- ([docs/models.md](docs/models.md)). It reads the cache's files and never
156
- fetches. It only reports; it does not stop or change what the server loads.
157
- A real pin, fixed at install, is not built yet.
158
-
159
- sessionvoice can promise "never two servers" only from its own side:
160
- VoiceMode's installer does not ask, so `voicemode service install
161
- mlx-audio` run after sessionvoice's install loads a second mlx-audio job,
162
- and one of the two crash-loops.
163
-
164
- Without a speech server, point the ears at another OpenAI-compatible STT
165
- (`EARS_LISTEN_STT_URL`, or `url` in `~/.sessionkit/voice/settings/ears.json`;
166
- `src/ears/listen/stt.py:105`) and the mouth at another TTS
167
- (`MOUTH_KOKORO_URL`, or `tts` in `~/.sessionkit/voice/settings/mouth.json`),
168
- a hosted one included, with `MOUTH_TTS_KEY` or the key file
169
- `~/.sessionkit/voice/settings/tts.key` for a keyed one: see
170
- [A hosted speech endpoint](docs/models.md#a-hosted-speech-endpoint-instead).
171
- On a Mac the mouth's `auto` falls back to `say`
172
- (`src/mouth/backends.py:200`).
173
- - **ffmpeg** on `PATH`: `mouth play` (`src/mouth/backends.py:261`), the
174
- mouth's `.mp3`/`.m4a` output (`src/mouth/audio.py:137`), `ears file`
175
- (`src/ears/cli.py:92`) and `ears eval` (`src/ears/eval.py:169`).
176
- - **SwitchAudioSource** on `PATH` (Homebrew `switchaudio-osx`): the keeper
177
- sets the default output with it, and reads with it only as a fallback
178
- (`keeper/ears-keeper.sh`): `check_output` reads the default output and the
179
- output list from input-watch's JSON file (`docs/audio-devices.md`) and asks
180
- SwitchAudioSource only when that file is missing or stale; `default_input`
181
- reads the legacy `input-watch.state` while the watcher is running and asks
182
- SwitchAudioSource only when it is not. `keeper/speak:13-17` still reads with it.
183
- - **sessionmail's `message`** only if you use mail: the mouth's notices and
184
- the keeper's turn mail (`keeper/ears-mail.py`) go out by `message send`,
185
- never straight to an MTA. With no `sendmail` key in
186
- sessionmail's settings file (`~/.sessionkit/mail/settings/mail.json`)
187
- and no `SESSIONMAIL_SENDMAIL`, sessionmail (0.2.0 and later) delivers
188
- by its mail drop: it writes each mail straight into your own mailbox,
189
- as you, with no mail server. That is how the launch agents deliver on
190
- a Mac with no postfix. The drop never makes a mailbox, so the `heard`
191
- box must exist first: `message setup --box heard` makes it. A turn
192
- mailed to a box that is not there is refused, nothing is delivered,
193
- and the turn is lost. To send by postfix instead, set
194
- `"sendmail": "/usr/sbin/sendmail"` in that file: the launch agents do
195
- not see a `SESSIONMAIL_SENDMAIL` set in your shell.
196
- On a drop host, `ears services install` runs sessionmail's first-run setup, `message setup --box heard`.
197
- Mail to `mouth@<host>` reaches the speech queue only if sessionmail
198
- delivers it there (`src/mouth/box.py`). The mouth's notices go through
199
- `MOUTH_SENDMAIL` (`src/mouth/notice.py`): only the mouth service (`mouth serve`, `mouth mail`,
200
- `mouth retract`, `mouth again`) uses the default `message`; any other process
201
- must name its own. ears-mail's command is `EARS_MAIL_MESSAGE`, else
202
- `message` on PATH, else where uv installs it (`~/.local/bin`, then uv's
203
- tool dir; `src/ears/message_bin.py`).
204
- - **Models.** `ears install` fetches only Silero VAD, to
205
- `~/.cache/ears/silero_vad.onnx`, pinned to one upstream commit and checked
206
- against its SHA-256 (`install()` in `src/ears/cli.py`; the pin is in
207
- `src/ears/vad.py`, and `docs/models.md` names it). **It does not
208
- fetch the Smart Turn model** that `--turns smart` needs
209
- (`src/ears/smartturn.py:48-72`). With mlx-audio installed, the `mlx`
210
- backend loads `~/.cache/ears/smart-turn-v3-mlx` if it is there, and
211
- otherwise `mlx-community/smart-turn-v3` from Hugging Face on first use.
212
- Without mlx-audio, the `onnx` backend needs `smart-turn-v3.2-gpu.onnx` at
213
- `~/.cache/ears/` (or the path in `EARS_SMART_TURN_ONNX`). Download it
214
- yourself from
215
- [pipecat-ai/smart-turn-v3](https://huggingface.co/pipecat-ai/smart-turn-v3).
79
+ - [x] macOS (tested on Apple silicon)
80
+ - [ ] Linux
81
+ - [ ] WSL
82
+ - [ ] Windows
83
+
84
+ What sessionvoice depends on, what needs each one, and how to upgrade it:
85
+ [dependencies](docs/dependencies.md).
216
86
 
217
87
  ## Components
218
88
 
@@ -263,10 +133,15 @@ pair of ears on a Mac (depth in [docs/architecture.md](docs/architecture.md)):
263
133
  package or a checkout; a bare install never does.
264
134
  - **Turn mail** (`keeper/ears-mail.py`, launchd `dev.sessionkit.voice.ears-mail`):
265
135
  mails each of the user's finished turns To and From
266
- `heard+<device>@<host>`, with the `Ears-*` headers of the pager genie's
267
- contract (sessionmail `openspec/changes/pager-genie`). A session hears a
268
- device by subscribing to that address. The box is `heard` (`--sender`),
269
- because `ears` is this project's own box.
136
+ `heard+room@<host>`, whatever input device the capture is following, with
137
+ the `Ears-*` headers of the pager genie's contract (sessionmail
138
+ `openspec/changes/pager-genie`). The device is named by the `Ears-Device`
139
+ header. A session hears the room, on every device, by subscribing to that
140
+ one address. While `--device-copy` is on (the default), the same send (one
141
+ message, one Message-ID) also goes Cc `heard+<device>@<host>`, so a
142
+ session subscribed to a device's address keeps hearing that device. The
143
+ room's tag is `--room` (`EARS_MAIL_ROOM`, default `room`). The box is
144
+ `heard` (`--sender`), because `ears` is this project's own box.
270
145
  sessionmail trusts only its keeper's From (`_voice.KEEPER`), so the
271
146
  plist loads only once that says `heard` too. `--to seats` is the older mail, To
272
147
  each seat a turn is for. **Never run `--to seats` and `--to device` for
@@ -278,6 +153,12 @@ pair of ears on a Mac (depth in [docs/architecture.md](docs/architecture.md)):
278
153
  one whose label is empty or names nobody, keeps `--speaker`
279
154
  (`$EARS_SPEAKER`, else `unknown@<host>`: a voice the matcher could not
280
155
  place is never mailed under a person's name).
156
+ When the user speaks over a seat's line, the capture may label the whole
157
+ turn with that seat, since the line's voice fills most of its audio. Once
158
+ the echo cut has taken that seat's line out and left the user's words, the
159
+ label is dropped: the remaining words get `--speaker` (else
160
+ `unknown@<host>`), never the seat whose echo was cut, and they can be a
161
+ reply to that seat. A label naming anyone else is kept.
281
162
  `Ears-Echo` says a mouth line heard back through the mic was cut out of
282
163
  the turn. On the speakers the mic hears the mouth, so a line's
283
164
  words are cut wherever they sit in the user's turn: at its start (up to `3 + 5
@@ -363,21 +244,23 @@ Every key, its type and its default:
363
244
  - **The heard log:** `<home>/logs/conversations/heard_YYYY-MM-DD.jsonl`,
364
245
  one JSON line for each thing the ears heard and the mouth said, where
365
246
  `<home>` is the home above. A new file starts each day.
366
- - **The services' logs:** `~/Library/Logs/ears/` (`EARS_LOG_DIR` moves it).
247
+ - **The services' logs:** `~/.sessionkit/voice/log/` (`EARS_LOG_DIR` moves it).
367
248
  `ears-keeper.log` is what the keeper did (devices, restarts, calls, a
368
249
  deaf mic); `ears-keeper-capture.log` is the room capture's own output;
369
250
  `ears-mail.log` is each turn mail sent or skipped; `ears-mouth.log` is
370
- the mouth's player.
251
+ the mouth's player. Logs written before the move stay where they
252
+ were, and nothing deletes them. `ears services status` prints the folder.
371
253
 
372
254
  Follow one with `tail -F`, for example
373
- `tail -F ~/Library/Logs/ears/ears-keeper.log`, or today's heard log with
255
+ `tail -F ~/.sessionkit/voice/log/ears-keeper.log`, or today's heard log with
374
256
  `tail -F <home>/logs/conversations/heard_$(date +%F).jsonl`.
375
257
  Every log file and what each line means:
376
258
  [docs/logs.md](docs/logs.md).
377
259
 
378
260
  - **Health:** `ears services status` prints one row per launch agent
379
- (`running, pid N`, `loaded, not running` or `not installed`), the
380
- keeper's `mode` (`input-watch`, or `polling system_profiler` and why),
261
+ (`running, pid N`, `loaded, not running` or `not installed`; the
262
+ `stt` row, the optional `--apple` server, also names what the ears hear
263
+ through), the keeper's `mode` (`input-watch`, or `polling system_profiler` and why),
381
264
  the one that serves speech on `127.0.0.1:8890`, and the home in use.
382
265
 
383
266
  ## Use
@@ -421,11 +304,17 @@ at `http://127.0.0.1:<port>/`, never at `localhost`. The code is `src/talk/`.
421
304
  `ears services install --only talk` keeps it running as a launch agent
422
305
  (opt-in; a bare install never does).
423
306
 
307
+ In the kit's web page, Talk is a room: the kit's front door runs
308
+ `talk serve --kit` on a unix socket and logs you in, so there is nothing to
309
+ pair and you never run `--kit` by hand
310
+ ([docs/talk.md](docs/talk.md#in-the-kits-web-page)).
311
+
424
312
  ## Install
425
313
 
426
314
  The distribution is `sessionvoice` (`sessionvoice[nemotron]`, `sessionvoice[all]`);
427
- it installs the `ears`, `ears-serve`, `mouth` and `talk` commands. A bare
428
- `pip install sessionvoice` carries what `mouth say` needs. The extras are
315
+ it installs the `ears`, `ears-serve`, `mouth`, `talk` and `sessionvoice-web`
316
+ commands. A bare `pip install sessionvoice` carries what `mouth say` needs.
317
+ The extras are
429
318
  `capture` (the mic and call ears), `silero` (the Silero VAD), `smart-turn`
430
319
  (the Smart Turn gate's ONNX backend), `nemotron` (mlx-audio) and `all`. An
431
320
  import that needs one fails naming it, as `pip install 'sessionvoice[EXTRA]'`.
@@ -436,6 +325,15 @@ import that needs one fails naming it, as `pip install 'sessionvoice[EXTRA]'`.
436
325
  ears services install --only sounds # opt-in: the ears and mouth sounds
437
326
  ears services install --only talk # opt-in: talk serve, the live view
438
327
 
328
+ A phone-only Mac (for example one that runs a Delta bot, with no agent
329
+ seat to wake) installs with `--no-agent`: nobody is woken when you speak,
330
+ and the keeper's notices go to the log. `--agent NAME` is for a Mac where
331
+ an agent seat should be woken when you speak. Before it writes anything,
332
+ install says how big the speech models are (about 2.9 GB, fetched once,
333
+ after the speech server loads) and that the speech server runs as a login
334
+ service; `--dry-run` prints the same line, and `--no-prefetch` leaves the
335
+ download to the first call.
336
+
439
337
  ### The proof of a working install: you speak, and the agent WAKES and answers
440
338
 
441
339
  **Not "a heard line appears".** The keeper alone writes the heard line, and
@@ -447,11 +345,19 @@ agent deaf to its person, and one that retires room mode on that proof goes
447
345
  deaf for good. So the proof is: say "hey NAME", and NAME wakes and
448
346
  answers.
449
347
 
450
- `ears services install` (`--dry-run` lists the same checks and writes nothing):
348
+ `ears services install` (`--dry-run` lists the same checks and writes nothing).
349
+ Before it writes anything, it checks every prerequisite it can check without
350
+ building or fetching. When any are missing, it lists them all in one run, each
351
+ with its fix, writes nothing and exits 2. A launch agent that fails to load
352
+ also makes it exit 2, once every other service has been tried: one block on
353
+ stderr names each service that did not load, the install log and `ears
354
+ services status`. Every real run appends its output and launchctl's answers,
355
+ timestamped, to `install.log` in the log folder, `~/.sessionkit/voice/log/`, and prints that path
356
+ as its `log` row ([logs](docs/logs.md#the-log-files)). It then:
451
357
 
452
358
  - first finds the mlx-audio speech server on `:8890`, or installs its own
453
- (`dev.sessionkit.voice.mlx-audio`), as "A speech server" under Requirements
454
- says, and prints which step it took. When it loads its own, the new
359
+ (`dev.sessionkit.voice.mlx-audio`), as [the speech server](docs/speech-server.md)
360
+ page says, and prints which step it took. When it loads its own, the new
455
361
  `final` below is written without the probe: that server has not started
456
362
  yet;
457
363
  - installs `dev.sessionkit.voice.ears-mail` beside the mouth and the keeper, unless
@@ -497,14 +403,17 @@ answers.
497
403
  read (no `message`, or one older than 0.2.0), it warns, makes no box,
498
404
  checks neither, and goes on.
499
405
  On a postfix host it also prints the box to subscribe to:
500
- `message subscription add heard+macbook`;
406
+ `message subscription add heard+room`;
501
407
  - `--agent NAME` adds NAME's wake seat (`ears wake add NAME`) and sends the
502
408
  keeper's notices to NAME (`EARS_NOTIFY`; `--notify ADDR` sends them
503
409
  elsewhere). Without it, and when the wake file names no seat, nobody would
504
410
  be woken, so an install that includes `mail` REFUSES (exit 2, before
505
411
  anything is written; `--dry-run` says `REFUSED`, still lists, and exits 2
506
- too). The refusal prints both forms, `ears services install --agent NAME`
507
- and `ears services install --no-agent`, and `ears wake add NAME`. Inside
412
+ too). The refusal prints both forms and what each means:
413
+ `ears services install --agent NAME` for a Mac where an agent seat should
414
+ be woken when you speak, and `ears services install --no-agent` for a
415
+ 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
508
417
  an agent seat (`TM_AGENT_ADDRESS` set) it suggests that address's local
509
418
  part as NAME; it never installs on the suggestion. `--no-agent` installs
510
419
  anyway, and install warns that NOBODY is woken. A wake file that already
@@ -519,9 +428,13 @@ answers.
519
428
  missing: a checkout's `settings/`, else the wheel's `ears/_settings/`. It
520
429
  then copies each beside its settings file as a dot file
521
430
  (`.ears.schema.json`, `.ears-sounds.schema.json`, `.mouth.schema.json`);
522
- - refuses, before any plist is written, when the Silero VAD model is missing,
523
- unless VoiceMode's copy (`~/.voicemode/models/silero/`) is there to link.
524
- Run `ears install` first. A keeper that finds no model says so once and
431
+ - fetches the Silero VAD model, before any plist is written, when it is
432
+ missing and VoiceMode's copy (`~/.voicemode/models/silero/`) is not there to
433
+ link. It is the file `ears install` fetches, from the same pinned source,
434
+ checked against the same SHA-256, and install names the version, the source
435
+ and the destination. A failed fetch or a checksum mismatch is refused, and no
436
+ file is left behind. `--dry-run` says what it would fetch (the version and
437
+ the size) and fetches nothing. A keeper that finds no model says so once and
525
438
  holds, and does not restart its capture every few seconds;
526
439
  - probes a new `final` engine with one short clip before writing it to
527
440
  `ears.json`, and falls back to whisper `:2022` (and says so) when it does
@@ -555,7 +468,7 @@ the agent's next tool call. `EARS_HEARD_HOOK=off` silences it without
555
468
  editing the file.
556
469
 
557
470
  `ears heard hook` is the canonical heard hook. A seat that also gets heard
558
- mail (it subscribes to `heard+<device>`) gets each finished turn by mail, so
471
+ mail (it subscribes to `heard+room`) gets each finished turn by mail, so
559
472
  give it `ears heard hook --partials-only || exit 1` instead: the hook then
560
473
  shows only the partial preview of a turn still being spoken and the shown
561
474
  events (wake word, barge-in, device change), never the finished turn a
@@ -32,143 +32,13 @@ streams transcription, and decides when the speaker has finished.
32
32
 
33
33
  ## Requirements
34
34
 
35
- - **macOS** for the always-on parts. The keeper follows devices through
36
- CoreAudio (it builds `keeper/input-watch.swift` with `swiftc`,
37
- `keeper/ears-keeper.sh:134`, when the Command Line Tools are installed;
38
- without them it polls `system_profiler` and never runs the `swiftc`
39
- install stub), and `ears services install` writes launchd
40
- agents to `~/Library/LaunchAgents` (`src/ears/services.py:93`). The
41
- `apple` backend needs macOS 27, Xcode's `swift` and a source checkout
42
- (`uv run ears install --apple`: the wheel does not ship `swift/`, and
43
- `--apple` without it refuses). The engine alone (`ears file`, `ears
44
- listen` with `whisper`) needs no Mac.
45
- - **Python 3.10-3.14** (`requires-python` in `pyproject.toml`) and
46
- **[uv](https://docs.astral.sh/uv/)**: `make test` runs `uv run --locked`.
47
- - **A speech server on `127.0.0.1:8890`**: mlx-audio's server, which serves
48
- both STT and TTS. It is the ears' default STT (Parakeet;
49
- `src/ears/listen/stt.py:86`, `src/ears/services.py:530`) and the
50
- mouth's Kokoro voices (`src/mouth/backends.py:289`). `ears services
51
- install` finds one or installs its own (`src/ears/speech.py`), and asks
52
- before it acts, in this order:
53
- - **Opt-out.** `final.url` in `~/.sessionkit/voice/settings/ears.json` (or
54
- `EARS_STT_URL` at install) names another engine, and the mouth is not
55
- being installed: `:8890` is not sessionvoice's to manage.
56
- - **Use.** Something listens on `:8890` and says it is mlx-audio: `GET /`
57
- returns mlx-audio's welcome line, or `/openapi.json` lists its audio
58
- routes. sessionvoice uses that server, whoever installed it (VoiceMode,
59
- for example), and starts nothing.
60
- - **Refuse.** Something else holds `:8890`, or holds it and does not
61
- answer. Install starts nothing, replaces nothing, and names the holder.
62
- When the ears hear through `:8890` it exits 2 and says what to run;
63
- when only the mouth does, it warns and the mouth uses `say`. Free the
64
- port, or point `final.url` at another speech server.
65
- - **Never a second.** Nothing answers on `:8890`, but a loaded launch
66
- agent runs `mlx_audio.server` (under any label, so another installer's
67
- job between restarts counts). Install starts nothing. When that job is
68
- sessionvoice's own, run `ears services uninstall --only speech`, then
69
- install again.
70
- - **Install.** Nothing on `:8890` and no such job. On an Apple-silicon
71
- Mac (macOS 14 or later), install builds sessionvoice's own venv,
72
- `~/.local/share/ears/speech/venv`, from the shipped
73
- `speech/requirements.lock` (`mlx-audio==0.5.8`), and loads it as the
74
- launch agent `dev.sessionkit.voice.mlx-audio`. `EARS_SPEECH_PY` names a
75
- python that already has it, and no venv is built. On any other host it
76
- installs nothing: it refuses and says what to run when the ears need
77
- `:8890`, and warns when only the mouth does.
78
-
79
- `--dry-run` says which of these install would take, and builds and writes
80
- nothing. `ears services status` has a `speech` row that names who serves
81
- `:8890`. `ears services uninstall --only speech` removes only
82
- `dev.sessionkit.voice.mlx-audio`, never a server someone else installed, and
83
- keeps its venv. While sessionvoice's own server runs, a re-install just
84
- uses it, so to upgrade it, uninstall it and install again: install
85
- re-applies the lock's pins to the venv it finds. The lock carries a hash
86
- for every pin, and install refuses an artifact that does not match it.
87
- Two pins, `docopt` and `webrtcvad`, ship only as source, so uv builds them,
88
- and the setuptools it builds them with is hash-checked too, from the
89
- shipped `speech/build-constraints.txt`. A new venv is checked in full. A
90
- re-apply checks only what it installs, not what is already in the venv, so
91
- to check an existing venv, remove it and install again. Install then
92
- fetches the server's weights, about 2.9 GB, so the first call does not wait
93
- on them. It prints the size first, skips any already cached and prints
94
- progress while it fetches. `--no-prefetch` (or `EARS_NO_PREFETCH=1`) skips
95
- this on a host that must not download, and `--dry-run` prints the size and
96
- fetches nothing. A fetch that fails does not fail the install. It says why
97
- and how much is left. Whatever is not fetched at install is downloaded by
98
- the server on first use: the first transcription waits on Parakeet, and the
99
- first line spoken waits on Kokoro and its voices.
100
-
101
- **These weights are not pinned.** mlx-audio's server takes whatever is on
102
- Hugging Face `main` at first use, and it has no way to ask for a revision,
103
- so a later first use (a new Mac, a cleared cache) can load different
104
- weights. `ears services weights` (and the `weight` rows of `ears services
105
- status`) reads the three, `mlx-community/parakeet-tdt-0.6b-v3`,
106
- `mlx-community/Kokoro-82M-bf16` and `prince-canuma/Kokoro-82M` (the
107
- voices), from the Hugging Face cache: `cached`, `downloading 412 MB of
108
- 2.5 GB`, or `not cached`, and `DIFFERS` when the cache's `main` is not the
109
- commit sessionvoice expects (`SPEECH_WEIGHTS` in `src/ears/services.py`).
110
- `--json` prints the same for a program, schema `ears.weights/1`
111
- ([docs/models.md](docs/models.md)). It reads the cache's files and never
112
- fetches. It only reports; it does not stop or change what the server loads.
113
- A real pin, fixed at install, is not built yet.
114
-
115
- sessionvoice can promise "never two servers" only from its own side:
116
- VoiceMode's installer does not ask, so `voicemode service install
117
- mlx-audio` run after sessionvoice's install loads a second mlx-audio job,
118
- and one of the two crash-loops.
119
-
120
- Without a speech server, point the ears at another OpenAI-compatible STT
121
- (`EARS_LISTEN_STT_URL`, or `url` in `~/.sessionkit/voice/settings/ears.json`;
122
- `src/ears/listen/stt.py:105`) and the mouth at another TTS
123
- (`MOUTH_KOKORO_URL`, or `tts` in `~/.sessionkit/voice/settings/mouth.json`),
124
- a hosted one included, with `MOUTH_TTS_KEY` or the key file
125
- `~/.sessionkit/voice/settings/tts.key` for a keyed one: see
126
- [A hosted speech endpoint](docs/models.md#a-hosted-speech-endpoint-instead).
127
- On a Mac the mouth's `auto` falls back to `say`
128
- (`src/mouth/backends.py:200`).
129
- - **ffmpeg** on `PATH`: `mouth play` (`src/mouth/backends.py:261`), the
130
- mouth's `.mp3`/`.m4a` output (`src/mouth/audio.py:137`), `ears file`
131
- (`src/ears/cli.py:92`) and `ears eval` (`src/ears/eval.py:169`).
132
- - **SwitchAudioSource** on `PATH` (Homebrew `switchaudio-osx`): the keeper
133
- sets the default output with it, and reads with it only as a fallback
134
- (`keeper/ears-keeper.sh`): `check_output` reads the default output and the
135
- output list from input-watch's JSON file (`docs/audio-devices.md`) and asks
136
- SwitchAudioSource only when that file is missing or stale; `default_input`
137
- reads the legacy `input-watch.state` while the watcher is running and asks
138
- SwitchAudioSource only when it is not. `keeper/speak:13-17` still reads with it.
139
- - **sessionmail's `message`** only if you use mail: the mouth's notices and
140
- the keeper's turn mail (`keeper/ears-mail.py`) go out by `message send`,
141
- never straight to an MTA. With no `sendmail` key in
142
- sessionmail's settings file (`~/.sessionkit/mail/settings/mail.json`)
143
- and no `SESSIONMAIL_SENDMAIL`, sessionmail (0.2.0 and later) delivers
144
- by its mail drop: it writes each mail straight into your own mailbox,
145
- as you, with no mail server. That is how the launch agents deliver on
146
- a Mac with no postfix. The drop never makes a mailbox, so the `heard`
147
- box must exist first: `message setup --box heard` makes it. A turn
148
- mailed to a box that is not there is refused, nothing is delivered,
149
- and the turn is lost. To send by postfix instead, set
150
- `"sendmail": "/usr/sbin/sendmail"` in that file: the launch agents do
151
- not see a `SESSIONMAIL_SENDMAIL` set in your shell.
152
- On a drop host, `ears services install` runs sessionmail's first-run setup, `message setup --box heard`.
153
- Mail to `mouth@<host>` reaches the speech queue only if sessionmail
154
- delivers it there (`src/mouth/box.py`). The mouth's notices go through
155
- `MOUTH_SENDMAIL` (`src/mouth/notice.py`): only the mouth service (`mouth serve`, `mouth mail`,
156
- `mouth retract`, `mouth again`) uses the default `message`; any other process
157
- must name its own. ears-mail's command is `EARS_MAIL_MESSAGE`, else
158
- `message` on PATH, else where uv installs it (`~/.local/bin`, then uv's
159
- tool dir; `src/ears/message_bin.py`).
160
- - **Models.** `ears install` fetches only Silero VAD, to
161
- `~/.cache/ears/silero_vad.onnx`, pinned to one upstream commit and checked
162
- against its SHA-256 (`install()` in `src/ears/cli.py`; the pin is in
163
- `src/ears/vad.py`, and `docs/models.md` names it). **It does not
164
- fetch the Smart Turn model** that `--turns smart` needs
165
- (`src/ears/smartturn.py:48-72`). With mlx-audio installed, the `mlx`
166
- backend loads `~/.cache/ears/smart-turn-v3-mlx` if it is there, and
167
- otherwise `mlx-community/smart-turn-v3` from Hugging Face on first use.
168
- Without mlx-audio, the `onnx` backend needs `smart-turn-v3.2-gpu.onnx` at
169
- `~/.cache/ears/` (or the path in `EARS_SMART_TURN_ONNX`). Download it
170
- yourself from
171
- [pipecat-ai/smart-turn-v3](https://huggingface.co/pipecat-ai/smart-turn-v3).
35
+ - [x] macOS (tested on Apple silicon)
36
+ - [ ] Linux
37
+ - [ ] WSL
38
+ - [ ] Windows
39
+
40
+ What sessionvoice depends on, what needs each one, and how to upgrade it:
41
+ [dependencies](docs/dependencies.md).
172
42
 
173
43
  ## Components
174
44
 
@@ -219,10 +89,15 @@ pair of ears on a Mac (depth in [docs/architecture.md](docs/architecture.md)):
219
89
  package or a checkout; a bare install never does.
220
90
  - **Turn mail** (`keeper/ears-mail.py`, launchd `dev.sessionkit.voice.ears-mail`):
221
91
  mails each of the user's finished turns To and From
222
- `heard+<device>@<host>`, with the `Ears-*` headers of the pager genie's
223
- contract (sessionmail `openspec/changes/pager-genie`). A session hears a
224
- device by subscribing to that address. The box is `heard` (`--sender`),
225
- because `ears` is this project's own box.
92
+ `heard+room@<host>`, whatever input device the capture is following, with
93
+ the `Ears-*` headers of the pager genie's contract (sessionmail
94
+ `openspec/changes/pager-genie`). The device is named by the `Ears-Device`
95
+ header. A session hears the room, on every device, by subscribing to that
96
+ one address. While `--device-copy` is on (the default), the same send (one
97
+ message, one Message-ID) also goes Cc `heard+<device>@<host>`, so a
98
+ session subscribed to a device's address keeps hearing that device. The
99
+ room's tag is `--room` (`EARS_MAIL_ROOM`, default `room`). The box is
100
+ `heard` (`--sender`), because `ears` is this project's own box.
226
101
  sessionmail trusts only its keeper's From (`_voice.KEEPER`), so the
227
102
  plist loads only once that says `heard` too. `--to seats` is the older mail, To
228
103
  each seat a turn is for. **Never run `--to seats` and `--to device` for
@@ -234,6 +109,12 @@ pair of ears on a Mac (depth in [docs/architecture.md](docs/architecture.md)):
234
109
  one whose label is empty or names nobody, keeps `--speaker`
235
110
  (`$EARS_SPEAKER`, else `unknown@<host>`: a voice the matcher could not
236
111
  place is never mailed under a person's name).
112
+ When the user speaks over a seat's line, the capture may label the whole
113
+ turn with that seat, since the line's voice fills most of its audio. Once
114
+ the echo cut has taken that seat's line out and left the user's words, the
115
+ label is dropped: the remaining words get `--speaker` (else
116
+ `unknown@<host>`), never the seat whose echo was cut, and they can be a
117
+ reply to that seat. A label naming anyone else is kept.
237
118
  `Ears-Echo` says a mouth line heard back through the mic was cut out of
238
119
  the turn. On the speakers the mic hears the mouth, so a line's
239
120
  words are cut wherever they sit in the user's turn: at its start (up to `3 + 5
@@ -319,21 +200,23 @@ Every key, its type and its default:
319
200
  - **The heard log:** `<home>/logs/conversations/heard_YYYY-MM-DD.jsonl`,
320
201
  one JSON line for each thing the ears heard and the mouth said, where
321
202
  `<home>` is the home above. A new file starts each day.
322
- - **The services' logs:** `~/Library/Logs/ears/` (`EARS_LOG_DIR` moves it).
203
+ - **The services' logs:** `~/.sessionkit/voice/log/` (`EARS_LOG_DIR` moves it).
323
204
  `ears-keeper.log` is what the keeper did (devices, restarts, calls, a
324
205
  deaf mic); `ears-keeper-capture.log` is the room capture's own output;
325
206
  `ears-mail.log` is each turn mail sent or skipped; `ears-mouth.log` is
326
- the mouth's player.
207
+ the mouth's player. Logs written before the move stay where they
208
+ were, and nothing deletes them. `ears services status` prints the folder.
327
209
 
328
210
  Follow one with `tail -F`, for example
329
- `tail -F ~/Library/Logs/ears/ears-keeper.log`, or today's heard log with
211
+ `tail -F ~/.sessionkit/voice/log/ears-keeper.log`, or today's heard log with
330
212
  `tail -F <home>/logs/conversations/heard_$(date +%F).jsonl`.
331
213
  Every log file and what each line means:
332
214
  [docs/logs.md](docs/logs.md).
333
215
 
334
216
  - **Health:** `ears services status` prints one row per launch agent
335
- (`running, pid N`, `loaded, not running` or `not installed`), the
336
- keeper's `mode` (`input-watch`, or `polling system_profiler` and why),
217
+ (`running, pid N`, `loaded, not running` or `not installed`; the
218
+ `stt` row, the optional `--apple` server, also names what the ears hear
219
+ through), the keeper's `mode` (`input-watch`, or `polling system_profiler` and why),
337
220
  the one that serves speech on `127.0.0.1:8890`, and the home in use.
338
221
 
339
222
  ## Use
@@ -377,11 +260,17 @@ at `http://127.0.0.1:<port>/`, never at `localhost`. The code is `src/talk/`.
377
260
  `ears services install --only talk` keeps it running as a launch agent
378
261
  (opt-in; a bare install never does).
379
262
 
263
+ In the kit's web page, Talk is a room: the kit's front door runs
264
+ `talk serve --kit` on a unix socket and logs you in, so there is nothing to
265
+ pair and you never run `--kit` by hand
266
+ ([docs/talk.md](docs/talk.md#in-the-kits-web-page)).
267
+
380
268
  ## Install
381
269
 
382
270
  The distribution is `sessionvoice` (`sessionvoice[nemotron]`, `sessionvoice[all]`);
383
- it installs the `ears`, `ears-serve`, `mouth` and `talk` commands. A bare
384
- `pip install sessionvoice` carries what `mouth say` needs. The extras are
271
+ it installs the `ears`, `ears-serve`, `mouth`, `talk` and `sessionvoice-web`
272
+ commands. A bare `pip install sessionvoice` carries what `mouth say` needs.
273
+ The extras are
385
274
  `capture` (the mic and call ears), `silero` (the Silero VAD), `smart-turn`
386
275
  (the Smart Turn gate's ONNX backend), `nemotron` (mlx-audio) and `all`. An
387
276
  import that needs one fails naming it, as `pip install 'sessionvoice[EXTRA]'`.
@@ -392,6 +281,15 @@ import that needs one fails naming it, as `pip install 'sessionvoice[EXTRA]'`.
392
281
  ears services install --only sounds # opt-in: the ears and mouth sounds
393
282
  ears services install --only talk # opt-in: talk serve, the live view
394
283
 
284
+ A phone-only Mac (for example one that runs a Delta bot, with no agent
285
+ seat to wake) installs with `--no-agent`: nobody is woken when you speak,
286
+ and the keeper's notices go to the log. `--agent NAME` is for a Mac where
287
+ an agent seat should be woken when you speak. Before it writes anything,
288
+ install says how big the speech models are (about 2.9 GB, fetched once,
289
+ after the speech server loads) and that the speech server runs as a login
290
+ service; `--dry-run` prints the same line, and `--no-prefetch` leaves the
291
+ download to the first call.
292
+
395
293
  ### The proof of a working install: you speak, and the agent WAKES and answers
396
294
 
397
295
  **Not "a heard line appears".** The keeper alone writes the heard line, and
@@ -403,11 +301,19 @@ agent deaf to its person, and one that retires room mode on that proof goes
403
301
  deaf for good. So the proof is: say "hey NAME", and NAME wakes and
404
302
  answers.
405
303
 
406
- `ears services install` (`--dry-run` lists the same checks and writes nothing):
304
+ `ears services install` (`--dry-run` lists the same checks and writes nothing).
305
+ Before it writes anything, it checks every prerequisite it can check without
306
+ building or fetching. When any are missing, it lists them all in one run, each
307
+ with its fix, writes nothing and exits 2. A launch agent that fails to load
308
+ also makes it exit 2, once every other service has been tried: one block on
309
+ stderr names each service that did not load, the install log and `ears
310
+ services status`. Every real run appends its output and launchctl's answers,
311
+ timestamped, to `install.log` in the log folder, `~/.sessionkit/voice/log/`, and prints that path
312
+ as its `log` row ([logs](docs/logs.md#the-log-files)). It then:
407
313
 
408
314
  - first finds the mlx-audio speech server on `:8890`, or installs its own
409
- (`dev.sessionkit.voice.mlx-audio`), as "A speech server" under Requirements
410
- says, and prints which step it took. When it loads its own, the new
315
+ (`dev.sessionkit.voice.mlx-audio`), as [the speech server](docs/speech-server.md)
316
+ page says, and prints which step it took. When it loads its own, the new
411
317
  `final` below is written without the probe: that server has not started
412
318
  yet;
413
319
  - installs `dev.sessionkit.voice.ears-mail` beside the mouth and the keeper, unless
@@ -453,14 +359,17 @@ answers.
453
359
  read (no `message`, or one older than 0.2.0), it warns, makes no box,
454
360
  checks neither, and goes on.
455
361
  On a postfix host it also prints the box to subscribe to:
456
- `message subscription add heard+macbook`;
362
+ `message subscription add heard+room`;
457
363
  - `--agent NAME` adds NAME's wake seat (`ears wake add NAME`) and sends the
458
364
  keeper's notices to NAME (`EARS_NOTIFY`; `--notify ADDR` sends them
459
365
  elsewhere). Without it, and when the wake file names no seat, nobody would
460
366
  be woken, so an install that includes `mail` REFUSES (exit 2, before
461
367
  anything is written; `--dry-run` says `REFUSED`, still lists, and exits 2
462
- too). The refusal prints both forms, `ears services install --agent NAME`
463
- and `ears services install --no-agent`, and `ears wake add NAME`. Inside
368
+ too). The refusal prints both forms and what each means:
369
+ `ears services install --agent NAME` for a Mac where an agent seat should
370
+ be woken when you speak, and `ears services install --no-agent` for a
371
+ 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
464
373
  an agent seat (`TM_AGENT_ADDRESS` set) it suggests that address's local
465
374
  part as NAME; it never installs on the suggestion. `--no-agent` installs
466
375
  anyway, and install warns that NOBODY is woken. A wake file that already
@@ -475,9 +384,13 @@ answers.
475
384
  missing: a checkout's `settings/`, else the wheel's `ears/_settings/`. It
476
385
  then copies each beside its settings file as a dot file
477
386
  (`.ears.schema.json`, `.ears-sounds.schema.json`, `.mouth.schema.json`);
478
- - refuses, before any plist is written, when the Silero VAD model is missing,
479
- unless VoiceMode's copy (`~/.voicemode/models/silero/`) is there to link.
480
- Run `ears install` first. A keeper that finds no model says so once and
387
+ - fetches the Silero VAD model, before any plist is written, when it is
388
+ missing and VoiceMode's copy (`~/.voicemode/models/silero/`) is not there to
389
+ link. It is the file `ears install` fetches, from the same pinned source,
390
+ checked against the same SHA-256, and install names the version, the source
391
+ and the destination. A failed fetch or a checksum mismatch is refused, and no
392
+ file is left behind. `--dry-run` says what it would fetch (the version and
393
+ the size) and fetches nothing. A keeper that finds no model says so once and
481
394
  holds, and does not restart its capture every few seconds;
482
395
  - probes a new `final` engine with one short clip before writing it to
483
396
  `ears.json`, and falls back to whisper `:2022` (and says so) when it does
@@ -511,7 +424,7 @@ the agent's next tool call. `EARS_HEARD_HOOK=off` silences it without
511
424
  editing the file.
512
425
 
513
426
  `ears heard hook` is the canonical heard hook. A seat that also gets heard
514
- mail (it subscribes to `heard+<device>`) gets each finished turn by mail, so
427
+ mail (it subscribes to `heard+room`) gets each finished turn by mail, so
515
428
  give it `ears heard hook --partials-only || exit 1` instead: the hook then
516
429
  shows only the partial preview of a turn still being spoken and the shown
517
430
  events (wake word, barge-in, device change), never the finished turn a