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.
- {echotools-2.4.6 → echotools-2.4.8}/PKG-INFO +3 -2
- {echotools-2.4.6 → echotools-2.4.8}/README.md +2 -1
- {echotools-2.4.6 → echotools-2.4.8}/pyproject.toml +18 -1
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/__init__.py +18 -2
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_schema/validate.py +0 -2
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/shared/coercion.py +1 -1
- echotools-2.4.8/src/echotools/plat/capture/__init__.py +25 -0
- echotools-2.4.8/src/echotools/plat/capture/audio_monitor/api.py +88 -0
- echotools-2.4.8/src/echotools/plat/capture/audio_monitor/config.py +15 -0
- echotools-2.4.8/src/echotools/plat/capture/audio_monitor/linux.py +297 -0
- echotools-2.4.8/src/echotools/plat/capture/audio_monitor/mac.py +174 -0
- echotools-2.4.8/src/echotools/plat/capture/audio_monitor/types.py +35 -0
- echotools-2.4.8/src/echotools/plat/capture/audio_monitor/win.py +206 -0
- echotools-2.4.8/src/echotools/plat/capture/audio_record/api.py +45 -0
- echotools-2.4.8/src/echotools/plat/capture/audio_record/config.py +33 -0
- echotools-2.4.8/src/echotools/plat/capture/audio_record/linux.py +175 -0
- echotools-2.4.8/src/echotools/plat/capture/audio_record/mac.py +204 -0
- echotools-2.4.8/src/echotools/plat/capture/audio_record/win.py +29 -0
- echotools-2.4.8/src/echotools/plat/capture/audio_record/win_wasapi.py +212 -0
- echotools-2.4.8/src/echotools/plat/capture/audio_record/win_winmm.py +131 -0
- echotools-2.4.8/src/echotools/plat/capture/screenshot/api.py +37 -0
- echotools-2.4.8/src/echotools/plat/capture/screenshot/config.py +25 -0
- echotools-2.4.8/src/echotools/plat/capture/screenshot/linux.py +312 -0
- echotools-2.4.8/src/echotools/plat/capture/screenshot/mac.py +122 -0
- echotools-2.4.8/src/echotools/plat/capture/screenshot/win.py +19 -0
- echotools-2.4.8/src/echotools/plat/capture/screenshot/win_dxgi.py +236 -0
- echotools-2.4.8/src/echotools/plat/capture/screenshot/win_gdi.py +154 -0
- echotools-2.4.8/src/echotools/plat/capture/shared/__init__.py +37 -0
- echotools-2.4.8/src/echotools/plat/capture/shared/bmp.py +47 -0
- echotools-2.4.8/src/echotools/plat/capture/shared/pcm.py +130 -0
- echotools-2.4.8/src/echotools/plat/capture/shared/platform.py +27 -0
- echotools-2.4.8/src/echotools/plat/capture/shared/wav.py +34 -0
- echotools-2.4.8/src/echotools/plat/capture/shared/win_com.py +74 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/sdk/facade.py +2 -2
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools.egg-info/PKG-INFO +3 -2
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools.egg-info/SOURCES.txt +30 -4
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_coverage_core.py +1 -1
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_ids.py +1 -1
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_multiline_parameter_comprehensive.py +1 -1
- echotools-2.4.8/src/tests/test_plat_capture.py +219 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_runtime.py +1 -1
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_scheduler.py +1 -1
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_schema_validate.py +6 -4
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_tool_id.py +0 -2
- echotools-2.4.6/src/echotools/plat/runtime/__init__.py +0 -7
- echotools-2.4.6/src/echotools/plat/scheduler/__init__.py +0 -7
- {echotools-2.4.6 → echotools-2.4.8}/setup.cfg +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/config/base.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/config/center.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/__init__.py +1 -1
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/parsers/stream.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/parsers/stream_delta.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/parsers/stream_feed.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/parsers/stream_final.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/prompt/behavior_blocks.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/prompt/history.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/prompt/inject.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_patterns/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_patterns/invoke.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_patterns/params.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_patterns/regex.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_patterns/strip.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_schema/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_schema/mangled.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_stream/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_stream/body.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_stream/snap.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_think/filter.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_think/hold.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_think/parse.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_tool/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_tool/comment.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_tool/fakemarkup.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_tool/invoke.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_tool/tools.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_tool/values.py +3 -3
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/shared/entml_format.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/shared/history_markup.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/tool_id.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/keys.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/protocol/base.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/charmap.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/spinner.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/ui.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/ui_console.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/ui_io.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/ui_log.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/ui_platform.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/ui_text.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uicore/ui_types.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uilayout/ui_bridge.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uilayout/ui_table.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uiwidgets/ui_richcli.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/console/uiwidgets/ui_themes.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/events/event.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/translate.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/application.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/components/textinput.js +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/inputbox.js +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/plat/sdk/__init__.py +0 -0
- /echotools-2.4.6/src/echotools/plat/runtime/collector.py → /echotools-2.4.8/src/echotools/plat/sdk/runtime.py +0 -0
- {echotools-2.4.6/src/echotools/plat/scheduler → echotools-2.4.8/src/echotools/plat/sdk}/scheduler.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools/version.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/_check_parity.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/bench_import.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/conftest.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/fixtures/__init__.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/fixtures/entml_golden.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/fixtures/entml_golden_update.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/fixtures/simulated_fake_history_markup_responses.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/fixtures/simulated_fault_thinking_responses.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_adversarial_stream_splits.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_completion.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_config.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_config_extended.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_console.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_dispatch.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_agent_output_formats.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_bare_invoke_format.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_batch_stream_comprehensive.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_fake_structure_markup.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_golden.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_invoke_known_tool_gate.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_parser_comprehensive.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_protocol.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_stream_json.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_thinking_history.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_thinking_parse.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_tool_parse_leak.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_tool_pipeline.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_entml_tools.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_errors.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_errors_common.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_events.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_events_event.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_facade.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_fake_history_markup_batch.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_fake_history_markup_invoke_preserve.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_fake_history_markup_reply_protect.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_fake_history_markup_stream.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_fault_thinking_close_batch.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_fault_thinking_close_stream.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_fncall.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_fncall_stream_thinking.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_history_markup_filter.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_imports.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_io.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_keys.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_logger.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_merge.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_model_branch_matrix.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_plugin.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_printstream.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_protocols.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_proxy.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_simulated_llm_tool_parse.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_stream_partial_json_parity.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_stream_realtime_parse.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_tracing.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_ui_bridge.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_usage.py +0 -0
- {echotools-2.4.6 → echotools-2.4.8}/src/tests/test_web.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
{echotools-2.4.6 → echotools-2.4.8}/src/echotools/exec/fncall/protocols/entml_schema/validate.py
RENAMED
|
@@ -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),
|
|
@@ -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()
|