echotools 2.4.6__tar.gz → 2.4.8__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 (293) hide show
  1. {echotools-2.4.6 → echotools-2.4.8}/PKG-INFO +3 -2
  2. {echotools-2.4.6 → echotools-2.4.8}/README.md +2 -1
  3. {echotools-2.4.6 → echotools-2.4.8}/pyproject.toml +18 -1
  4. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/__init__.py +18 -2
  5. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_schema/validate.py +0 -2
  6. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/shared/coercion.py +1 -1
  7. echotools-2.4.8/src/echotools/plat/capture/__init__.py +25 -0
  8. echotools-2.4.8/src/echotools/plat/capture/audio_monitor/api.py +88 -0
  9. echotools-2.4.8/src/echotools/plat/capture/audio_monitor/config.py +15 -0
  10. echotools-2.4.8/src/echotools/plat/capture/audio_monitor/linux.py +297 -0
  11. echotools-2.4.8/src/echotools/plat/capture/audio_monitor/mac.py +174 -0
  12. echotools-2.4.8/src/echotools/plat/capture/audio_monitor/types.py +35 -0
  13. echotools-2.4.8/src/echotools/plat/capture/audio_monitor/win.py +206 -0
  14. echotools-2.4.8/src/echotools/plat/capture/audio_record/api.py +45 -0
  15. echotools-2.4.8/src/echotools/plat/capture/audio_record/config.py +33 -0
  16. echotools-2.4.8/src/echotools/plat/capture/audio_record/linux.py +175 -0
  17. echotools-2.4.8/src/echotools/plat/capture/audio_record/mac.py +204 -0
  18. echotools-2.4.8/src/echotools/plat/capture/audio_record/win.py +29 -0
  19. echotools-2.4.8/src/echotools/plat/capture/audio_record/win_wasapi.py +212 -0
  20. echotools-2.4.8/src/echotools/plat/capture/audio_record/win_winmm.py +131 -0
  21. echotools-2.4.8/src/echotools/plat/capture/screenshot/api.py +37 -0
  22. echotools-2.4.8/src/echotools/plat/capture/screenshot/config.py +25 -0
  23. echotools-2.4.8/src/echotools/plat/capture/screenshot/linux.py +312 -0
  24. echotools-2.4.8/src/echotools/plat/capture/screenshot/mac.py +122 -0
  25. echotools-2.4.8/src/echotools/plat/capture/screenshot/win.py +19 -0
  26. echotools-2.4.8/src/echotools/plat/capture/screenshot/win_dxgi.py +236 -0
  27. echotools-2.4.8/src/echotools/plat/capture/screenshot/win_gdi.py +154 -0
  28. echotools-2.4.8/src/echotools/plat/capture/shared/__init__.py +37 -0
  29. echotools-2.4.8/src/echotools/plat/capture/shared/bmp.py +47 -0
  30. echotools-2.4.8/src/echotools/plat/capture/shared/pcm.py +130 -0
  31. echotools-2.4.8/src/echotools/plat/capture/shared/platform.py +27 -0
  32. echotools-2.4.8/src/echotools/plat/capture/shared/wav.py +34 -0
  33. echotools-2.4.8/src/echotools/plat/capture/shared/win_com.py +74 -0
  34. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/sdk/facade.py +2 -2
  35. {echotools-2.4.6 → echotools-2.4.8}/src/echotools.egg-info/PKG-INFO +3 -2
  36. {echotools-2.4.6 → echotools-2.4.8}/src/echotools.egg-info/SOURCES.txt +30 -4
  37. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_coverage_core.py +1 -1
  38. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_ids.py +1 -1
  39. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_multiline_parameter_comprehensive.py +1 -1
  40. echotools-2.4.8/src/tests/test_plat_capture.py +219 -0
  41. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_runtime.py +1 -1
  42. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_scheduler.py +1 -1
  43. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_schema_validate.py +6 -4
  44. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_tool_id.py +0 -2
  45. echotools-2.4.6/src/echotools/plat/runtime/__init__.py +0 -7
  46. echotools-2.4.6/src/echotools/plat/scheduler/__init__.py +0 -7
  47. {echotools-2.4.6 → echotools-2.4.8}/setup.cfg +0 -0
  48. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/cache/__init__.py +0 -0
  49. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/cache/list_cache.py +0 -0
  50. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/cache/memory_cache.py +0 -0
  51. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/config/__init__.py +0 -0
  52. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/config/base.py +0 -0
  53. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/config/center.py +0 -0
  54. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/config/center_notify.py +0 -0
  55. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/config/center_template.py +0 -0
  56. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/config/loader.py +0 -0
  57. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/config/merge.py +0 -0
  58. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/errors/__init__.py +0 -0
  59. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/errors/base.py +0 -0
  60. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/errors/classify.py +0 -0
  61. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/errors/common.py +0 -0
  62. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/errors/http.py +0 -0
  63. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/ids/__init__.py +0 -0
  64. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/ids/generator.py +0 -0
  65. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/io/__init__.py +0 -0
  66. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/io/io_utils.py +0 -0
  67. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/io/printstream.py +0 -0
  68. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/logger/__init__.py +0 -0
  69. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/logger/manager.py +0 -0
  70. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/retry/__init__.py +0 -0
  71. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/retry/retry.py +0 -0
  72. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/dispatch/__init__.py +0 -0
  73. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/dispatch/candidate.py +0 -0
  74. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/dispatch/dispatcher.py +0 -0
  75. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
  76. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/dispatch/race.py +0 -0
  77. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/dispatch/selector.py +0 -0
  78. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/dispatch/usage.py +0 -0
  79. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/__init__.py +1 -1
  80. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
  81. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/parsers/stream.py +0 -0
  82. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/parsers/stream_delta.py +0 -0
  83. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/parsers/stream_feed.py +0 -0
  84. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/parsers/stream_final.py +0 -0
  85. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
  86. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/prompt/behavior_blocks.py +0 -0
  87. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/prompt/history.py +0 -0
  88. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
  89. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/prompt/inject.py +0 -0
  90. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
  91. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
  92. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml.py +0 -0
  93. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_patterns/__init__.py +0 -0
  94. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_patterns/invoke.py +0 -0
  95. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_patterns/params.py +0 -0
  96. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_patterns/regex.py +0 -0
  97. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_patterns/strip.py +0 -0
  98. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_schema/__init__.py +0 -0
  99. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_schema/mangled.py +0 -0
  100. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_stream/__init__.py +0 -0
  101. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_stream/body.py +0 -0
  102. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_stream/snap.py +0 -0
  103. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
  104. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
  105. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_think/filter.py +0 -0
  106. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
  107. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_think/hold.py +0 -0
  108. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_think/parse.py +0 -0
  109. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_tool/__init__.py +0 -0
  110. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_tool/comment.py +0 -0
  111. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_tool/fakemarkup.py +0 -0
  112. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_tool/invoke.py +0 -0
  113. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_tool/tools.py +0 -0
  114. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_tool/values.py +3 -3
  115. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/registry.py +0 -0
  116. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/shared/__init__.py +0 -0
  117. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/shared/entml_format.py +0 -0
  118. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/shared/history_markup.py +0 -0
  119. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
  120. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/shared/normalization.py +0 -0
  121. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
  122. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/tool_id.py +0 -0
  123. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/keys.py +0 -0
  124. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/lifecycle/__init__.py +0 -0
  125. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/lifecycle/manager.py +0 -0
  126. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/lifecycle/updater.py +0 -0
  127. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
  128. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/process/__init__.py +0 -0
  129. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/process/port.py +0 -0
  130. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/protocol/__init__.py +0 -0
  131. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/protocol/base.py +0 -0
  132. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/__init__.py +0 -0
  133. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/conpty.py +0 -0
  134. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/local/__init__.py +0 -0
  135. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/local/core.py +0 -0
  136. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/local/proc.py +0 -0
  137. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/local/unix.py +0 -0
  138. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/local/win.py +0 -0
  139. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/sanitize.py +0 -0
  140. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/session/__init__.py +0 -0
  141. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/session/base.py +0 -0
  142. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/session/client.py +0 -0
  143. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/ssh.py +0 -0
  144. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/tmux.py +0 -0
  145. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/__init__.py +0 -0
  146. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/charmap.py +0 -0
  147. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/spinner.py +0 -0
  148. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/ui.py +0 -0
  149. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
  150. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
  151. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
  152. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
  153. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
  154. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
  155. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/ui_console.py +0 -0
  156. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/ui_io.py +0 -0
  157. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/ui_log.py +0 -0
  158. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/ui_platform.py +0 -0
  159. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/ui_text.py +0 -0
  160. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/ui_types.py +0 -0
  161. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uilayout/ui_bridge.py +0 -0
  162. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
  163. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
  164. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
  165. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
  166. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uilayout/ui_table.py +0 -0
  167. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
  168. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
  169. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
  170. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
  171. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uiwidgets/ui_richcli.py +0 -0
  172. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
  173. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
  174. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uiwidgets/ui_themes.py +0 -0
  175. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/events/__init__.py +0 -0
  176. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/events/bus.py +0 -0
  177. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/events/event.py +0 -0
  178. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/spinner/__init__.py +0 -0
  179. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/spinner/spinner.py +0 -0
  180. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/spinner/spinner_color.py +0 -0
  181. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/tracing/__init__.py +0 -0
  182. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/tracing/context.py +0 -0
  183. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/tracing/span.py +0 -0
  184. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/tracing/tracer.py +0 -0
  185. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/translate.py +0 -0
  186. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/watcher/__init__.py +0 -0
  187. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/watcher/file_watcher.py +0 -0
  188. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/__init__.py +0 -0
  189. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/application.py +0 -0
  190. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/broker.py +0 -0
  191. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/app.js +0 -0
  192. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/components/filezone.js +0 -0
  193. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
  194. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
  195. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
  196. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
  197. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/components/textinput.js +0 -0
  198. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
  199. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
  200. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
  201. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
  202. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/inputbox.css +0 -0
  203. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/inputbox.js +0 -0
  204. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/style.css +0 -0
  205. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/voicegif.js +0 -0
  206. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/middleware.py +0 -0
  207. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/stats.py +0 -0
  208. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/utils.py +0 -0
  209. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/files/__init__.py +0 -0
  210. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/files/file_util.py +0 -0
  211. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/network/__init__.py +0 -0
  212. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/network/http_utils.py +0 -0
  213. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/plugin/__init__.py +0 -0
  214. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/plugin/base.py +0 -0
  215. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/plugin/discovery.py +0 -0
  216. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/plugin/registry.py +0 -0
  217. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/plugin/registry_ops.py +0 -0
  218. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/proxy/__init__.py +0 -0
  219. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/proxy/manager.py +0 -0
  220. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/sdk/__init__.py +0 -0
  221. /echotools-2.4.6/src/echotools/plat/runtime/collector.py → /echotools-2.4.8/src/echotools/plat/sdk/runtime.py +0 -0
  222. {echotools-2.4.6/src/echotools/plat/scheduler → echotools-2.4.8/src/echotools/plat/sdk}/scheduler.py +0 -0
  223. {echotools-2.4.6 → echotools-2.4.8}/src/echotools/version.py +0 -0
  224. {echotools-2.4.6 → echotools-2.4.8}/src/echotools.egg-info/dependency_links.txt +0 -0
  225. {echotools-2.4.6 → echotools-2.4.8}/src/echotools.egg-info/requires.txt +0 -0
  226. {echotools-2.4.6 → echotools-2.4.8}/src/echotools.egg-info/top_level.txt +0 -0
  227. {echotools-2.4.6 → echotools-2.4.8}/src/tests/_check_parity.py +0 -0
  228. {echotools-2.4.6 → echotools-2.4.8}/src/tests/bench_import.py +0 -0
  229. {echotools-2.4.6 → echotools-2.4.8}/src/tests/conftest.py +0 -0
  230. {echotools-2.4.6 → echotools-2.4.8}/src/tests/fixtures/__init__.py +0 -0
  231. {echotools-2.4.6 → echotools-2.4.8}/src/tests/fixtures/entml_golden.py +0 -0
  232. {echotools-2.4.6 → echotools-2.4.8}/src/tests/fixtures/entml_golden_update.py +0 -0
  233. {echotools-2.4.6 → echotools-2.4.8}/src/tests/fixtures/simulated_fake_history_markup_responses.py +0 -0
  234. {echotools-2.4.6 → echotools-2.4.8}/src/tests/fixtures/simulated_fault_thinking_responses.py +0 -0
  235. {echotools-2.4.6 → echotools-2.4.8}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
  236. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_adversarial_stream_splits.py +0 -0
  237. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_cache_retry.py +0 -0
  238. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_completion.py +0 -0
  239. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_config.py +0 -0
  240. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_config_extended.py +0 -0
  241. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_console.py +0 -0
  242. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_dispatch.py +0 -0
  243. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_dispatch_extended.py +0 -0
  244. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_dispatch_race.py +0 -0
  245. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_agent_output_formats.py +0 -0
  246. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_bare_invoke_format.py +0 -0
  247. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_batch_stream_comprehensive.py +0 -0
  248. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_fake_structure_markup.py +0 -0
  249. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_golden.py +0 -0
  250. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_invoke_known_tool_gate.py +0 -0
  251. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_parser_comprehensive.py +0 -0
  252. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_protocol.py +0 -0
  253. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_stream_json.py +0 -0
  254. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
  255. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_thinking_history.py +0 -0
  256. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_thinking_parse.py +0 -0
  257. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_tool_parse_leak.py +0 -0
  258. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_tool_pipeline.py +0 -0
  259. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_tools.py +0 -0
  260. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_errors.py +0 -0
  261. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_errors_common.py +0 -0
  262. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_events.py +0 -0
  263. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_events_event.py +0 -0
  264. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_facade.py +0 -0
  265. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_fake_history_markup_batch.py +0 -0
  266. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_fake_history_markup_invoke_preserve.py +0 -0
  267. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_fake_history_markup_reply_protect.py +0 -0
  268. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_fake_history_markup_stream.py +0 -0
  269. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_fault_thinking_close_batch.py +0 -0
  270. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_fault_thinking_close_stream.py +0 -0
  271. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_fncall.py +0 -0
  272. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_fncall_stream_thinking.py +0 -0
  273. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_history_markup_filter.py +0 -0
  274. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_imports.py +0 -0
  275. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_io.py +0 -0
  276. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_keys.py +0 -0
  277. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_lifecycle.py +0 -0
  278. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_logger.py +0 -0
  279. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_merge.py +0 -0
  280. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_model_branch_matrix.py +0 -0
  281. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_plugin.py +0 -0
  282. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_printstream.py +0 -0
  283. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_protocols.py +0 -0
  284. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_proxy.py +0 -0
  285. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_proxy_selector.py +0 -0
  286. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_simulated_llm_tool_parse.py +0 -0
  287. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_stream_partial_json_parity.py +0 -0
  288. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_stream_realtime_parse.py +0 -0
  289. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_tracing.py +0 -0
  290. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_ui_bridge.py +0 -0
  291. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_usage.py +0 -0
  292. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_web.py +0 -0
  293. {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_web_broker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echotools
3
- Version: 2.4.6
3
+ Version: 2.4.8
4
4
  Summary: 通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链
5
5
  Home-page: https://github.com/nichengfuben/echotools
6
6
  Author: nichengfuben
@@ -58,7 +58,7 @@ Requires-Dist: ruff>=0.4.0; extra == "dev"
58
58
 
59
59
  通用基础设施 SDK:配置、日志、事件、调度、插件、调用链,以及 **entml** LLM 工具调用协议与终端 Console UI。
60
60
 
61
- 完全项目无关,兼容 Python 3.8–3.14。当前版本:**2.4.6**(详见 [CHANGELOG](docs/CHANGELOG.md))。
61
+ 完全项目无关,兼容 Python 3.8–3.14。当前版本:**2.4.8**(详见 [CHANGELOG](docs/CHANGELOG.md))。
62
62
 
63
63
  ## 安装
64
64
 
@@ -174,6 +174,7 @@ choice = run_select(ui, "选择主题", ["ocean", "forest", "sunset"])
174
174
  | `LocalTerminal` / `SSHTerminal` | 终端会话(需 `[terminal]` / `[ssh]`) |
175
175
  | `AutoUpdater` | git 自动更新 |
176
176
  | `FileWatcher` | 轮询文件监视 |
177
+ | `plat.capture` | 跨平台音频监控/录音/截图(纯 ctypes,零第三方依赖) |
177
178
 
178
179
  ## 开发
179
180
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  通用基础设施 SDK:配置、日志、事件、调度、插件、调用链,以及 **entml** LLM 工具调用协议与终端 Console UI。
4
4
 
5
- 完全项目无关,兼容 Python 3.8–3.14。当前版本:**2.4.6**(详见 [CHANGELOG](docs/CHANGELOG.md))。
5
+ 完全项目无关,兼容 Python 3.8–3.14。当前版本:**2.4.8**(详见 [CHANGELOG](docs/CHANGELOG.md))。
6
6
 
7
7
  ## 安装
8
8
 
@@ -118,6 +118,7 @@ choice = run_select(ui, "选择主题", ["ocean", "forest", "sunset"])
118
118
  | `LocalTerminal` / `SSHTerminal` | 终端会话(需 `[terminal]` / `[ssh]`) |
119
119
  | `AutoUpdater` | git 自动更新 |
120
120
  | `FileWatcher` | 轮询文件监视 |
121
+ | `plat.capture` | 跨平台音频监控/录音/截图(纯 ctypes,零第三方依赖) |
121
122
 
122
123
  ## 开发
123
124
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "echotools"
7
- version = "2.4.6"
7
+ version = "2.4.8"
8
8
  description = "通用基础设施 SDK:配置、日志、事件、调度、插件、协议、调用链"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -142,6 +142,23 @@ omit = [
142
142
  "*/web/application.py",
143
143
  "*/web/middleware.py",
144
144
  "*/web/utils.py",
145
+ "*/plat/capture/shared/win_com.py",
146
+ "*/plat/capture/audio_monitor/api.py",
147
+ "*/plat/capture/audio_record/api.py",
148
+ "*/plat/capture/screenshot/api.py",
149
+ "*/plat/capture/audio_monitor/win.py",
150
+ "*/plat/capture/audio_monitor/mac.py",
151
+ "*/plat/capture/audio_monitor/linux.py",
152
+ "*/plat/capture/audio_record/win.py",
153
+ "*/plat/capture/audio_record/win_wasapi.py",
154
+ "*/plat/capture/audio_record/win_winmm.py",
155
+ "*/plat/capture/audio_record/mac.py",
156
+ "*/plat/capture/audio_record/linux.py",
157
+ "*/plat/capture/screenshot/win.py",
158
+ "*/plat/capture/screenshot/win_dxgi.py",
159
+ "*/plat/capture/screenshot/win_gdi.py",
160
+ "*/plat/capture/screenshot/mac.py",
161
+ "*/plat/capture/screenshot/linux.py",
145
162
  ]
146
163
 
147
164
  [tool.coverage.report]
@@ -105,6 +105,14 @@ __all__ = [
105
105
  "ProtocolError",
106
106
  "classify_http_error",
107
107
  "Spinner",
108
+ "AudioProcess",
109
+ "AudioMonitorConfig",
110
+ "get_playing_processes",
111
+ "run_audio_monitor",
112
+ "AudioRecordConfig",
113
+ "record_audio_session",
114
+ "ScreenshotConfig",
115
+ "capture_screenshots",
108
116
  ]
109
117
 
110
118
  _EXPORTS: dict[str, tuple[str, str]] = {
@@ -131,7 +139,7 @@ _EXPORTS: dict[str, tuple[str, str]] = {
131
139
  "make_id": ("echotools.exec.dispatch", "make_id"),
132
140
  "AdaptiveSelector": ("echotools.exec.dispatch", "AdaptiveSelector"),
133
141
  "TaskDispatcher": ("echotools.exec.dispatch", "TaskDispatcher"),
134
- "TaskScheduler": ("echotools.plat.scheduler", "TaskScheduler"),
142
+ "TaskScheduler": ("echotools.plat.sdk.scheduler", "TaskScheduler"),
135
143
  "ProxySelector": ("echotools.exec.dispatch", "ProxySelector"),
136
144
  "ProxyRecord": ("echotools.exec.dispatch", "ProxyRecord"),
137
145
  "Plugin": ("echotools.plat.plugin", "Plugin"),
@@ -147,7 +155,7 @@ _EXPORTS: dict[str, tuple[str, str]] = {
147
155
  "ProxyManager": ("echotools.plat.proxy", "ProxyManager"),
148
156
  "PortReleaseResult": ("echotools.exec.process", "PortReleaseResult"),
149
157
  "ensure_port_available": ("echotools.exec.process", "ensure_port_available"),
150
- "RuntimeCollector": ("echotools.plat.runtime", "RuntimeCollector"),
158
+ "RuntimeCollector": ("echotools.plat.sdk.runtime", "RuntimeCollector"),
151
159
  "WebApplication": ("echotools.media.web.application", "WebApplication"),
152
160
  "json_body": ("echotools.media.web.utils", "json_body"),
153
161
  "safe_flush": ("echotools.media.web.utils", "safe_flush"),
@@ -197,6 +205,14 @@ _EXPORTS: dict[str, tuple[str, str]] = {
197
205
  "ProtocolError": ("echotools.base.errors", "ProtocolError"),
198
206
  "classify_http_error": ("echotools.base.errors", "classify_http_error"),
199
207
  "Spinner": ("echotools.media.spinner", "Spinner"),
208
+ "AudioProcess": ("echotools.plat.capture", "AudioProcess"),
209
+ "AudioMonitorConfig": ("echotools.plat.capture", "AudioMonitorConfig"),
210
+ "get_playing_processes": ("echotools.plat.capture", "get_playing_processes"),
211
+ "run_audio_monitor": ("echotools.plat.capture", "run_audio_monitor"),
212
+ "AudioRecordConfig": ("echotools.plat.capture", "AudioRecordConfig"),
213
+ "record_audio_session": ("echotools.plat.capture", "record_audio_session"),
214
+ "ScreenshotConfig": ("echotools.plat.capture", "ScreenshotConfig"),
215
+ "capture_screenshots": ("echotools.plat.capture", "capture_screenshots"),
200
216
  }
201
217
 
202
218
 
@@ -5,8 +5,6 @@ from __future__ import annotations
5
5
  from dataclasses import dataclass
6
6
  from typing import Any, Dict, List, Optional, Sequence
7
7
 
8
- from echotools.exec.fncall.shared.coercion import is_null_literal, schema_allows_null
9
-
10
8
  _SCALAR_CHECK = {
11
9
  "string": lambda v: isinstance(v, str),
12
10
  "integer": lambda v: isinstance(v, int) and not isinstance(v, bool),
@@ -7,7 +7,7 @@
7
7
  from __future__ import annotations
8
8
 
9
9
  import json
10
- from typing import Any, Dict, List, Optional, Sequence
10
+ from typing import Any, Dict, List, Optional
11
11
 
12
12
  from echotools.base.logger.manager import get_logger
13
13
 
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ """Cross-platform audio/screenshot capture (ctypes + stdlib only)."""
4
+
5
+ from echotools.plat.capture.audio_monitor.api import (
6
+ get_playing_processes,
7
+ run_audio_monitor,
8
+ )
9
+ from echotools.plat.capture.audio_monitor.config import AudioMonitorConfig
10
+ from echotools.plat.capture.audio_monitor.types import AudioProcess
11
+ from echotools.plat.capture.audio_record.api import record_audio_session
12
+ from echotools.plat.capture.audio_record.config import AudioRecordConfig
13
+ from echotools.plat.capture.screenshot.api import capture_screenshots
14
+ from echotools.plat.capture.screenshot.config import ScreenshotConfig
15
+
16
+ __all__ = [
17
+ "AudioMonitorConfig",
18
+ "AudioProcess",
19
+ "AudioRecordConfig",
20
+ "ScreenshotConfig",
21
+ "capture_screenshots",
22
+ "get_playing_processes",
23
+ "record_audio_session",
24
+ "run_audio_monitor",
25
+ ]
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ """Portable audio session monitor API."""
4
+
5
+ import sys
6
+ import time
7
+
8
+ from echotools.plat.capture.audio_monitor.config import AudioMonitorConfig
9
+ from echotools.plat.capture.audio_monitor.types import AudioProcess
10
+ from echotools.plat.capture.shared.platform import _OS, IS_LINUX, IS_MACOS, IS_WINDOWS
11
+
12
+ if IS_WINDOWS:
13
+ from echotools.plat.capture.audio_monitor.win import win_get_playing
14
+ elif IS_MACOS:
15
+ from echotools.plat.capture.audio_monitor.mac import mac_get_playing
16
+ elif IS_LINUX:
17
+ from echotools.plat.capture.audio_monitor.linux import linux_get_playing
18
+
19
+
20
+ def get_playing_processes(
21
+ config: AudioMonitorConfig | None = None,
22
+ ) -> list[AudioProcess]:
23
+ """Return processes currently playing audio."""
24
+ cfg = config or AudioMonitorConfig()
25
+ try:
26
+ if IS_WINDOWS:
27
+ return win_get_playing(cfg)
28
+ if IS_MACOS:
29
+ return mac_get_playing()
30
+ if IS_LINUX:
31
+ return linux_get_playing(cfg)
32
+ raise RuntimeError(f"不支持的平台: {_OS}")
33
+ except Exception as exc:
34
+ print(f"[!] get_playing_processes 异常: {exc}")
35
+ return []
36
+
37
+
38
+ def _format_process(ap: AudioProcess, cfg: AudioMonitorConfig) -> str:
39
+ parts = [f"📢 {ap.name}"]
40
+ if cfg.show_pid and ap.pid > 0:
41
+ parts.append(f"PID={ap.pid}")
42
+ if ap.volume >= 0:
43
+ parts.append(f"vol={ap.volume:.0%}")
44
+ return " " + " | ".join(parts)
45
+
46
+
47
+ def _print_monitor_update(
48
+ procs: list[AudioProcess],
49
+ cfg: AudioMonitorConfig,
50
+ last_set: set[AudioProcess],
51
+ ) -> set[AudioProcess]:
52
+ cur_set = set(procs)
53
+ if cfg.print_on_change and cur_set == last_set:
54
+ return last_set
55
+ ts = (
56
+ f" 时间: {time.strftime('%Y-%m-%d %H:%M:%S')}"
57
+ if cfg.show_timestamp
58
+ else ""
59
+ )
60
+ print(f"\n{'=' * cfg.banner_width}")
61
+ if ts:
62
+ print(ts)
63
+ if cur_set:
64
+ for ap in sorted(cur_set, key=lambda x: x.name):
65
+ print(_format_process(ap, cfg))
66
+ else:
67
+ print(" 🔇 当前没有进程在播放声音")
68
+ return cur_set
69
+
70
+
71
+ def run_audio_monitor(config: AudioMonitorConfig | None = None) -> None:
72
+ """Poll and print playing audio processes until KeyboardInterrupt."""
73
+ cfg = config or AudioMonitorConfig()
74
+ py_ver = sys.version_info
75
+ print("=" * cfg.banner_width)
76
+ print(f" 跨平台音频进程监控器 [{_OS} / Python {py_ver.major}.{py_ver.minor}]")
77
+ print(" 实时显示正在播放声音的进程")
78
+ print(" 按 Ctrl+C 退出")
79
+ print("=" * cfg.banner_width)
80
+
81
+ last_set: set[AudioProcess] = set()
82
+ try:
83
+ while True:
84
+ procs = get_playing_processes(cfg)
85
+ last_set = _print_monitor_update(procs, cfg, last_set)
86
+ time.sleep(cfg.poll_interval_sec)
87
+ except KeyboardInterrupt:
88
+ print("\n\n 监控结束。")
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class AudioMonitorConfig:
8
+ """Configuration for audio session monitoring."""
9
+
10
+ poll_interval_sec: float = 0.2
11
+ print_on_change: bool = True
12
+ show_timestamp: bool = True
13
+ show_pid: bool = True
14
+ active_state_only: bool = True
15
+ banner_width: int = 60
@@ -0,0 +1,297 @@
1
+ from __future__ import annotations
2
+
3
+ """Linux PulseAudio/ALSA audio session monitor with multi-tier fallback."""
4
+
5
+ import ctypes
6
+ import os
7
+ import re
8
+ import subprocess
9
+ import time
10
+ from ctypes import (
11
+ CFUNCTYPE,
12
+ POINTER,
13
+ Structure,
14
+ byref,
15
+ c_char_p,
16
+ c_int,
17
+ c_uint,
18
+ c_void_p,
19
+ )
20
+
21
+ from echotools.plat.capture.audio_monitor.config import AudioMonitorConfig
22
+ from echotools.plat.capture.audio_monitor.types import AudioProcess
23
+ from echotools.plat.capture.shared.platform import load_lib
24
+
25
+ _pulse = load_lib("pulse", "libpulse.so.0", "libpulse.so")
26
+ _linux_backend = "pulse_lib"
27
+
28
+ PA_CONTEXT_READY = 4
29
+ PA_CONTEXT_FAILED = 6
30
+ PA_CONTEXT_TERMINATED = 7
31
+
32
+
33
+ if _pulse:
34
+ class pa_sample_spec(Structure):
35
+ _fields_ = [("format", c_int), ("rate", c_uint), ("channels", ctypes.c_ubyte)]
36
+
37
+ _pulse.pa_mainloop_new.restype = c_void_p
38
+ _pulse.pa_mainloop_get_api.restype = c_void_p
39
+ _pulse.pa_mainloop_get_api.argtypes = [c_void_p]
40
+ _pulse.pa_mainloop_iterate.restype = c_int
41
+ _pulse.pa_mainloop_iterate.argtypes = [c_void_p, c_int, POINTER(c_int)]
42
+ _pulse.pa_mainloop_free.argtypes = [c_void_p]
43
+ _pulse.pa_context_new.restype = c_void_p
44
+ _pulse.pa_context_new.argtypes = [c_void_p, c_char_p]
45
+ _pulse.pa_context_connect.restype = c_int
46
+ _pulse.pa_context_connect.argtypes = [c_void_p, c_char_p, c_int, c_void_p]
47
+ _pulse.pa_context_get_state.restype = c_int
48
+ _pulse.pa_context_get_state.argtypes = [c_void_p]
49
+ _pulse.pa_context_get_sink_input_info_list.restype = c_void_p
50
+ _pulse.pa_context_get_sink_input_info_list.argtypes = [c_void_p, c_void_p, c_void_p]
51
+ _pulse.pa_operation_unref.argtypes = [c_void_p]
52
+ _pulse.pa_context_disconnect.argtypes = [c_void_p]
53
+ _pulse.pa_context_unref.argtypes = [c_void_p]
54
+
55
+
56
+ def _pactl_sink_input_pids() -> list[int]:
57
+ out = subprocess.check_output(
58
+ ["pactl", "list", "sink-inputs"],
59
+ stderr=subprocess.DEVNULL,
60
+ timeout=3,
61
+ )
62
+ pids: list[int] = []
63
+ for line in out.decode(errors="replace").splitlines():
64
+ m = re.search(r'application\.process\.id\s*=\s*"(\d+)"', line)
65
+ if m:
66
+ pids.append(int(m.group(1)))
67
+ return pids
68
+
69
+
70
+ def _wait_pulse_ready(ml, ctx) -> None:
71
+ if _pulse is None:
72
+ raise OSError("libpulse 不可用")
73
+ deadline = time.time() + 5.0
74
+ while time.time() < deadline:
75
+ ret = c_int(0)
76
+ _pulse.pa_mainloop_iterate(ml, 0, byref(ret))
77
+ st = _pulse.pa_context_get_state(ctx)
78
+ if st == PA_CONTEXT_READY:
79
+ return
80
+ if st in (PA_CONTEXT_FAILED, PA_CONTEXT_TERMINATED):
81
+ raise OSError(f"pa_context 失败 state={st}")
82
+ raise OSError("pa_context 超时")
83
+
84
+
85
+ def _parse_sink_input(info_ptr: int) -> AudioProcess | None:
86
+ try:
87
+ idx = c_uint.from_address(info_ptr).value
88
+ off = ctypes.sizeof(c_uint) + (4 if ctypes.sizeof(c_void_p) == 8 else 0)
89
+ name_ptr = c_char_p.from_address(info_ptr + off).value
90
+ name = name_ptr.decode("utf-8", errors="replace") if name_ptr else f"stream_{idx}"
91
+ return AudioProcess(-1, name, "active")
92
+ except Exception:
93
+ return None
94
+
95
+
96
+ def _make_sink_input_cb(result: list[AudioProcess], done_flag: list[bool]):
97
+ SI_CB = CFUNCTYPE(None, c_void_p, c_void_p, c_int, c_void_p)
98
+
99
+ def _si_callback(ctx_ptr, info_ptr, eol, userdata):
100
+ if eol:
101
+ done_flag[0] = True
102
+ return
103
+ if not info_ptr:
104
+ return
105
+ ap = _parse_sink_input(info_ptr)
106
+ if ap:
107
+ result.append(ap)
108
+
109
+ return SI_CB(_si_callback)
110
+
111
+
112
+ def _poll_mainloop(ml, done_flag: list[bool], timeout: float) -> None:
113
+ if _pulse is None:
114
+ raise OSError("libpulse 不可用")
115
+ deadline = time.time() + timeout
116
+ while not done_flag[0] and time.time() < deadline:
117
+ ret = c_int(0)
118
+ _pulse.pa_mainloop_iterate(ml, 0, byref(ret))
119
+
120
+
121
+ def _enrich_pulse_pids(result: list[AudioProcess]) -> None:
122
+ try:
123
+ pids = _pactl_sink_input_pids()
124
+ except Exception:
125
+ return
126
+ for i, ap in enumerate(result):
127
+ if i >= len(pids):
128
+ break
129
+ ap.pid = pids[i]
130
+ if ap.pid <= 0:
131
+ continue
132
+ try:
133
+ with open(f"/proc/{ap.pid}/comm") as f:
134
+ ap.name = f.read().strip()
135
+ except OSError:
136
+ pass
137
+
138
+
139
+ def _pulse_get_playing() -> list[AudioProcess]:
140
+ if not _pulse:
141
+ raise OSError("libpulse 不可用")
142
+ ml = _pulse.pa_mainloop_new()
143
+ api = _pulse.pa_mainloop_get_api(ml)
144
+ ctx = _pulse.pa_context_new(api, b"audio_monitor")
145
+ _pulse.pa_context_connect(ctx, None, 0, None)
146
+ _wait_pulse_ready(ml, ctx)
147
+
148
+ result: list[AudioProcess] = []
149
+ done_flag = [False]
150
+ c_cb = _make_sink_input_cb(result, done_flag)
151
+ op = _pulse.pa_context_get_sink_input_info_list(ctx, c_cb, None)
152
+ _poll_mainloop(ml, done_flag, 3.0)
153
+ if op:
154
+ _pulse.pa_operation_unref(op)
155
+ _pulse.pa_context_disconnect(ctx)
156
+ _pulse.pa_context_unref(ctx)
157
+ _pulse.pa_mainloop_free(ml)
158
+ _enrich_pulse_pids(result)
159
+ return result
160
+
161
+
162
+ def _pactl_get_playing(cfg: AudioMonitorConfig) -> list[AudioProcess]:
163
+ try:
164
+ out = subprocess.check_output(
165
+ ["pactl", "list", "sink-inputs"],
166
+ stderr=subprocess.DEVNULL,
167
+ timeout=3,
168
+ )
169
+ except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.CalledProcessError) as exc:
170
+ raise OSError("pactl 不可用") from exc
171
+
172
+ result: list[AudioProcess] = []
173
+ pid, name, muted = -1, "", False
174
+ for line in out.decode(errors="replace").splitlines():
175
+ line = line.strip()
176
+ m = re.search(r'application\.process\.id\s*=\s*"(\d+)"', line)
177
+ if m:
178
+ pid = int(m.group(1))
179
+ m = re.search(r'application\.name\s*=\s*"(.+)"', line)
180
+ if m:
181
+ name = m.group(1)
182
+ m = re.search(r"Mute:\s*(yes|no)", line)
183
+ if m:
184
+ muted = m.group(1) == "yes"
185
+ if pid > 0 and name:
186
+ proc_name = name
187
+ try:
188
+ with open(f"/proc/{pid}/comm") as f:
189
+ proc_name = f.read().strip()
190
+ except OSError:
191
+ pass
192
+ state = "inactive" if muted else "active"
193
+ if not cfg.active_state_only or state == "active":
194
+ result.append(AudioProcess(pid, proc_name, state))
195
+ pid, name, muted = -1, "", False
196
+ return result
197
+
198
+
199
+ def _lsof_snd_get_playing() -> list[AudioProcess]:
200
+ try:
201
+ out = subprocess.check_output(
202
+ ["lsof", "+D", "/dev/snd"],
203
+ stderr=subprocess.DEVNULL,
204
+ timeout=3,
205
+ )
206
+ except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.CalledProcessError) as exc:
207
+ raise OSError("lsof /dev/snd 失败") from exc
208
+ result: list[AudioProcess] = []
209
+ seen: set[int] = set()
210
+ for line in out.decode(errors="replace").splitlines()[1:]:
211
+ parts = line.split()
212
+ if len(parts) < 2:
213
+ continue
214
+ try:
215
+ pid = int(parts[1])
216
+ except ValueError:
217
+ continue
218
+ if pid in seen:
219
+ continue
220
+ seen.add(pid)
221
+ try:
222
+ with open(f"/proc/{pid}/comm") as f:
223
+ name = f.read().strip()
224
+ except OSError:
225
+ name = parts[0]
226
+ result.append(AudioProcess(pid, name, "unknown"))
227
+ return result
228
+
229
+
230
+ def _read_running_pid(status_path: str) -> AudioProcess | None:
231
+ try:
232
+ with open(status_path) as f:
233
+ content = f.read()
234
+ except OSError:
235
+ return None
236
+ if "RUNNING" not in content:
237
+ return None
238
+ m = re.search(r"owner_pid\s*:\s*(\d+)", content)
239
+ if not m:
240
+ return None
241
+ pid = int(m.group(1))
242
+ try:
243
+ with open(f"/proc/{pid}/comm") as f:
244
+ name = f.read().strip()
245
+ except OSError:
246
+ name = f"pid_{pid}"
247
+ return AudioProcess(pid, name, "active")
248
+
249
+
250
+ def _scan_pcm_dir(pcm_path: str, result: list[AudioProcess]) -> None:
251
+ for sub_entry in os.listdir(pcm_path):
252
+ status_path = os.path.join(pcm_path, sub_entry, "status")
253
+ if not os.path.isfile(status_path):
254
+ continue
255
+ ap = _read_running_pid(status_path)
256
+ if ap:
257
+ result.append(ap)
258
+
259
+
260
+ def _proc_snd_get_playing() -> list[AudioProcess]:
261
+ result: list[AudioProcess] = []
262
+ asound = "/proc/asound"
263
+ if not os.path.isdir(asound):
264
+ return result
265
+ for card_entry in os.listdir(asound):
266
+ card_path = os.path.join(asound, card_entry)
267
+ if not (card_entry.startswith("card") and os.path.isdir(card_path)):
268
+ continue
269
+ for pcm_entry in os.listdir(card_path):
270
+ pcm_path = os.path.join(card_path, pcm_entry)
271
+ if not (pcm_entry.startswith("pcm") and os.path.isdir(pcm_path)):
272
+ continue
273
+ _scan_pcm_dir(pcm_path, result)
274
+ return result
275
+
276
+
277
+ def linux_get_playing(cfg: AudioMonitorConfig) -> list[AudioProcess]:
278
+ global _linux_backend
279
+ if _linux_backend == "pulse_lib":
280
+ try:
281
+ return _pulse_get_playing()
282
+ except Exception as exc:
283
+ print(f"[!] libpulse 失败: {exc} => 切换 pactl")
284
+ _linux_backend = "pactl"
285
+ if _linux_backend == "pactl":
286
+ try:
287
+ return _pactl_get_playing(cfg)
288
+ except Exception as exc:
289
+ print(f"[!] pactl 失败: {exc} => 切换 lsof")
290
+ _linux_backend = "lsof"
291
+ if _linux_backend == "lsof":
292
+ try:
293
+ return _lsof_snd_get_playing()
294
+ except Exception as exc:
295
+ print(f"[!] lsof 失败: {exc} => 切换 /proc/asound")
296
+ _linux_backend = "proc"
297
+ return _proc_snd_get_playing()