echotools 2.4.5__tar.gz → 2.4.7__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.5 → echotools-2.4.7}/PKG-INFO +3 -2
- {echotools-2.4.5 → echotools-2.4.7}/README.md +2 -1
- {echotools-2.4.5 → echotools-2.4.7}/pyproject.toml +1 -1
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/__init__.py +18 -2
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/prompt/behavior_blocks.py +3 -1
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_schema/validate.py +0 -2
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/shared/coercion.py +1 -1
- echotools-2.4.7/src/echotools/plat/capture/__init__.py +25 -0
- echotools-2.4.7/src/echotools/plat/capture/audio_monitor/api.py +88 -0
- echotools-2.4.7/src/echotools/plat/capture/audio_monitor/config.py +15 -0
- echotools-2.4.7/src/echotools/plat/capture/audio_monitor/linux.py +297 -0
- echotools-2.4.7/src/echotools/plat/capture/audio_monitor/mac.py +174 -0
- echotools-2.4.7/src/echotools/plat/capture/audio_monitor/types.py +35 -0
- echotools-2.4.7/src/echotools/plat/capture/audio_monitor/win.py +206 -0
- echotools-2.4.7/src/echotools/plat/capture/audio_record/api.py +45 -0
- echotools-2.4.7/src/echotools/plat/capture/audio_record/config.py +33 -0
- echotools-2.4.7/src/echotools/plat/capture/audio_record/linux.py +175 -0
- echotools-2.4.7/src/echotools/plat/capture/audio_record/mac.py +204 -0
- echotools-2.4.7/src/echotools/plat/capture/audio_record/win.py +29 -0
- echotools-2.4.7/src/echotools/plat/capture/audio_record/win_wasapi.py +212 -0
- echotools-2.4.7/src/echotools/plat/capture/audio_record/win_winmm.py +131 -0
- echotools-2.4.7/src/echotools/plat/capture/screenshot/api.py +37 -0
- echotools-2.4.7/src/echotools/plat/capture/screenshot/config.py +25 -0
- echotools-2.4.7/src/echotools/plat/capture/screenshot/linux.py +312 -0
- echotools-2.4.7/src/echotools/plat/capture/screenshot/mac.py +122 -0
- echotools-2.4.7/src/echotools/plat/capture/screenshot/win.py +19 -0
- echotools-2.4.7/src/echotools/plat/capture/screenshot/win_dxgi.py +236 -0
- echotools-2.4.7/src/echotools/plat/capture/screenshot/win_gdi.py +154 -0
- echotools-2.4.7/src/echotools/plat/capture/shared/__init__.py +38 -0
- echotools-2.4.7/src/echotools/plat/capture/shared/bmp.py +47 -0
- echotools-2.4.7/src/echotools/plat/capture/shared/pcm.py +130 -0
- echotools-2.4.7/src/echotools/plat/capture/shared/platform.py +27 -0
- echotools-2.4.7/src/echotools/plat/capture/shared/wav.py +34 -0
- echotools-2.4.7/src/echotools/plat/capture/shared/win_com.py +69 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/plat/sdk/facade.py +2 -2
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools.egg-info/PKG-INFO +3 -2
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools.egg-info/SOURCES.txt +30 -4
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_coverage_core.py +1 -1
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_entml_protocol.py +1 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_ids.py +1 -1
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_multiline_parameter_comprehensive.py +1 -1
- echotools-2.4.7/src/tests/test_plat_capture.py +126 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_runtime.py +1 -1
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_scheduler.py +1 -1
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_schema_validate.py +6 -4
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_tool_id.py +0 -2
- echotools-2.4.5/src/echotools/plat/runtime/__init__.py +0 -7
- echotools-2.4.5/src/echotools/plat/scheduler/__init__.py +0 -7
- {echotools-2.4.5 → echotools-2.4.7}/setup.cfg +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/cache/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/cache/list_cache.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/cache/memory_cache.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/config/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/config/base.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/config/center.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/config/center_notify.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/config/center_template.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/config/loader.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/config/merge.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/errors/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/errors/base.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/errors/classify.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/errors/common.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/errors/http.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/ids/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/ids/generator.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/io/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/io/io_utils.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/io/printstream.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/logger/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/logger/manager.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/retry/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/base/retry/retry.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/dispatch/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/dispatch/candidate.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/dispatch/dispatcher.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/dispatch/proxy_selector.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/dispatch/race.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/dispatch/selector.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/dispatch/usage.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/__init__.py +1 -1
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/parsers/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/parsers/stream.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/parsers/stream_delta.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/parsers/stream_feed.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/parsers/stream_final.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/prompt/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/prompt/history.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/prompt/history_format.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/prompt/inject.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/prompt/prompt_helpers.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_patterns/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_patterns/invoke.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_patterns/params.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_patterns/regex.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_patterns/strip.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_schema/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_schema/mangled.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_stream/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_stream/body.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_stream/snap.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_think/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_think/core.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_think/filter.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_think/hist.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_think/hold.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_think/parse.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_tool/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_tool/comment.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_tool/fakemarkup.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_tool/invoke.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_tool/tools.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/protocols/entml_tool/values.py +3 -3
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/registry.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/shared/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/shared/entml_format.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/shared/history_markup.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/shared/loop_detect.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/shared/normalization.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/shared/schema_render.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/fncall/tool_id.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/keys.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/lifecycle/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/lifecycle/manager.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/lifecycle/updater.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/lifecycle/updater_pull.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/process/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/process/port.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/protocol/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/protocol/base.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/terminal/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/terminal/conpty.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/terminal/local/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/terminal/local/core.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/terminal/local/proc.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/terminal/local/unix.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/terminal/local/win.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/terminal/sanitize.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/terminal/session/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/terminal/session/base.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/terminal/session/client.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/terminal/ssh.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/exec/terminal/tmux.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/charmap.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/spinner.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/ui.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uicore/mixins/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uicore/mixins/uibase.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uicore/mixins/uicmds.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uicore/mixins/uiinteract.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uicore/mixins/uioutput.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uicore/mixins/uistream.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uicore/ui_console.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uicore/ui_io.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uicore/ui_log.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uicore/ui_platform.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uicore/ui_text.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uicore/ui_types.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uilayout/ui_bridge.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uilayout/ui_countdown.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uilayout/ui_layout.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uilayout/ui_misc.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uilayout/ui_panel.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uilayout/ui_table.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uilayout/ui_tree.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uiwidgets/ui_box.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uiwidgets/ui_editor.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uiwidgets/ui_progress.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uiwidgets/ui_richcli.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uiwidgets/ui_select.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uiwidgets/ui_stream.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/console/uiwidgets/ui_themes.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/events/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/events/bus.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/events/event.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/spinner/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/spinner/spinner.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/spinner/spinner_color.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/tracing/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/tracing/context.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/tracing/span.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/tracing/tracer.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/translate.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/watcher/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/watcher/file_watcher.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/application.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/broker.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/input_box/app.js +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/input_box/components/filezone.js +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/input_box/components/motionkit.js +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/input_box/components/sendbutton.js +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/input_box/components/sortablelist.css +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/input_box/components/sortablelist.js +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/input_box/components/textinput.js +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/input_box/components/voiceinput.js +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/input_box/fonts/inter-500.woff2 +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/input_box/fonts/inter-600.woff2 +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/input_box/fonts/inter400.woff2 +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/input_box/inputbox.css +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/input_box/inputbox.js +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/input_box/style.css +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/input_box/voicegif.js +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/middleware.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/stats.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/media/web/utils.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/plat/files/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/plat/files/file_util.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/plat/network/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/plat/network/http_utils.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/plat/plugin/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/plat/plugin/base.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/plat/plugin/discovery.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/plat/plugin/registry.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/plat/plugin/registry_ops.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/plat/proxy/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/plat/proxy/manager.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/plat/sdk/__init__.py +0 -0
- /echotools-2.4.5/src/echotools/plat/runtime/collector.py → /echotools-2.4.7/src/echotools/plat/sdk/runtime.py +0 -0
- {echotools-2.4.5/src/echotools/plat/scheduler → echotools-2.4.7/src/echotools/plat/sdk}/scheduler.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools/version.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/_check_parity.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/bench_import.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/conftest.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/fixtures/__init__.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/fixtures/entml_golden.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/fixtures/entml_golden_update.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/fixtures/simulated_fake_history_markup_responses.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/fixtures/simulated_fault_thinking_responses.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/fixtures/simulated_llm_tool_responses.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_adversarial_stream_splits.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_cache_retry.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_completion.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_config.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_config_extended.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_console.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_dispatch.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_dispatch_extended.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_dispatch_race.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_entml_agent_output_formats.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_entml_bare_invoke_format.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_entml_batch_stream_comprehensive.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_entml_fake_structure_markup.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_entml_golden.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_entml_invoke_known_tool_gate.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_entml_parser_comprehensive.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_entml_stream_json.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_entml_stream_json_exhaustive.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_entml_thinking_history.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_entml_thinking_parse.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_entml_tool_parse_leak.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_entml_tool_pipeline.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_entml_tools.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_errors.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_errors_common.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_events.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_events_event.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_facade.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_fake_history_markup_batch.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_fake_history_markup_invoke_preserve.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_fake_history_markup_reply_protect.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_fake_history_markup_stream.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_fault_thinking_close_batch.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_fault_thinking_close_stream.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_fncall.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_fncall_stream_thinking.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_history_markup_filter.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_imports.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_io.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_keys.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_lifecycle.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_logger.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_merge.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_model_branch_matrix.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_plugin.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_printstream.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_protocols.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_proxy.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_proxy_selector.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_simulated_llm_tool_parse.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_stream_partial_json_parity.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_stream_realtime_parse.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_tracing.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_ui_bridge.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_usage.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/src/tests/test_web.py +0 -0
- {echotools-2.4.5 → echotools-2.4.7}/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.7
|
|
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.7**(详见 [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.7**(详见 [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
|
|
|
@@ -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
|
|
|
@@ -7,7 +7,9 @@ IMPORTANT: The <entml:funtions_results> block is a top-level block, independent
|
|
|
7
7
|
|
|
8
8
|
IMPORTANT: The id-bearing comment following an invocation inside <entml:conversation_history> is written by the execution environment only when logging an already-completed turn. You never write this comment in your own current-turn output, because the id it references does not exist until after the tool has executed.
|
|
9
9
|
|
|
10
|
-
IMPORTANT:
|
|
10
|
+
IMPORTANT: You never output an HTML comment of the form <!-- Tool Result ID:… --> anywhere in your current-turn reply. This includes lines such as <!-- Tool Result ID:toolu_376919369ffb428c9f76284f --> immediately after </entml:invoke>, on their own, or embedded in visible text. Only the execution environment writes Tool Result ID comments when logging completed turns into <entml:conversation_history>; imitating that pattern in live output is always wrong.
|
|
11
|
+
|
|
12
|
+
IMPORTANT: Your turn ends immediately at the closing tag of the last <entml:invoke> block you emit. Nothing follows it — no comment, no id, no Tool Result ID HTML comment, no result, no visible text, no conclusion that depends on data you have not yet received.
|
|
11
13
|
|
|
12
14
|
IMPORTANT: Structural patterns you observe in <entml:conversation_history> — an invocation followed by an id comment — describe what the environment logged after a past turn ended. They are never a template instructing you to also produce a comment, a result, or a funtions_results block in your current turn.
|
|
13
15
|
|
{echotools-2.4.5 → echotools-2.4.7}/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()
|