android-midscene-automation 0.2.4 → 1.1.0
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.
- package/CHANGELOG.md +7 -0
- package/README.md +35 -0
- package/artemis-main/.dockerignore +15 -0
- package/artemis-main/.env.example +34 -0
- package/artemis-main/CONTRIBUTING.md +35 -0
- package/artemis-main/Dockerfile +68 -0
- package/artemis-main/LICENSE +202 -0
- package/artemis-main/Makefile +115 -0
- package/artemis-main/README.md +322 -0
- package/artemis-main/README_CN.md +320 -0
- package/artemis-main/apps/__init__.py +15 -0
- package/artemis-main/apps/admin_console/__init__.py +19 -0
- package/artemis-main/apps/admin_console/core/__init__.py +13 -0
- package/artemis-main/apps/admin_console/core/config.py +43 -0
- package/artemis-main/apps/admin_console/core/security.py +178 -0
- package/artemis-main/apps/admin_console/core/state.py +243 -0
- package/artemis-main/apps/admin_console/database/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/connection.py +60 -0
- package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
- package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
- package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
- package/artemis-main/apps/admin_console/index.html +3787 -0
- package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
- package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
- package/artemis-main/apps/admin_console/routers/media.py +198 -0
- package/artemis-main/apps/admin_console/routers/replay.py +91 -0
- package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
- package/artemis-main/apps/admin_console/routers/steps.py +109 -0
- package/artemis-main/apps/admin_console/routers/stream.py +56 -0
- package/artemis-main/apps/admin_console/routers/system.py +761 -0
- package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
- package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
- package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
- package/artemis-main/apps/admin_console/server.py +508 -0
- package/artemis-main/apps/admin_console/services/__init__.py +13 -0
- package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
- package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
- package/artemis-main/apps/admin_console/services/media_service.py +623 -0
- package/artemis-main/apps/admin_console/services/model_service.py +148 -0
- package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
- package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
- package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
- package/artemis-main/apps/showcase_ui/README.md +31 -0
- package/artemis-main/apps/showcase_ui/angular.json +132 -0
- package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
- package/artemis-main/apps/showcase_ui/package.json +39 -0
- package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
- package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
- package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
- package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
- package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
- package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
- package/artemis-main/apps/showcase_ui/src/index.html +37 -0
- package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
- package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
- package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
- package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
- package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
- package/artemis-main/artemis/__init__.py +44 -0
- package/artemis-main/artemis/__main__.py +18 -0
- package/artemis-main/artemis/_legacy_imports.py +94 -0
- package/artemis-main/artemis/_version.py +30 -0
- package/artemis-main/artemis/agents/__init__.py +17 -0
- package/artemis-main/artemis/agents/checker/checker.json +9 -0
- package/artemis-main/artemis/agents/checker/checker.py +859 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
- package/artemis-main/artemis/agents/explorer/constants.py +52 -0
- package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
- package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
- package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
- package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
- package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
- package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
- package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
- package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
- package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
- package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
- package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
- package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
- package/artemis-main/artemis/agents/flash/runner.py +1167 -0
- package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
- package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
- package/artemis-main/artemis/agents/operator/operator.json +3 -0
- package/artemis-main/artemis/agents/operator/operator.py +1394 -0
- package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
- package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
- package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
- package/artemis-main/artemis/agents/planner/planner.json +32 -0
- package/artemis-main/artemis/agents/planner/planner.py +436 -0
- package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
- package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
- package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
- package/artemis-main/artemis/agents/validator/categories.py +33 -0
- package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
- package/artemis-main/artemis/agents/validator/incidents.py +120 -0
- package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
- package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
- package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
- package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
- package/artemis-main/artemis/agents/validator/validator.py +188 -0
- package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
- package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
- package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
- package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
- package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
- package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
- package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
- package/artemis-main/artemis/clients/accessibility_client.py +438 -0
- package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
- package/artemis-main/artemis/config/__init__.py +309 -0
- package/artemis-main/artemis/config/agent.py +1098 -0
- package/artemis-main/artemis/config/constants.py +151 -0
- package/artemis-main/artemis/config/core/__init__.py +24 -0
- package/artemis-main/artemis/config/core/engine.py +118 -0
- package/artemis-main/artemis/config/core/interpolator.py +78 -0
- package/artemis-main/artemis/config/llm.py +267 -0
- package/artemis-main/artemis/config/paths.py +244 -0
- package/artemis-main/artemis/config/runtime.py +197 -0
- package/artemis-main/artemis/config/settings.py +315 -0
- package/artemis-main/artemis/constants.py +47 -0
- package/artemis-main/artemis/context.py +289 -0
- package/artemis-main/artemis/controllers/__init__.py +41 -0
- package/artemis-main/artemis/controllers/controller_factory.py +41 -0
- package/artemis-main/artemis/controllers/device_controller.py +29 -0
- package/artemis-main/artemis/controllers/types.py +37 -0
- package/artemis-main/artemis/controllers/unified_controller.py +649 -0
- package/artemis-main/artemis/core/__init__.py +21 -0
- package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
- package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
- package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
- package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
- package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
- package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
- package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
- package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
- package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
- package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
- package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
- package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
- package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
- package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
- package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
- package/artemis-main/artemis/core/tool_declaration.py +43 -0
- package/artemis-main/artemis/core/tool_failure.py +56 -0
- package/artemis-main/artemis/data_engine/context_vars.py +24 -0
- package/artemis-main/artemis/data_engine/engine.py +1615 -0
- package/artemis-main/artemis/data_engine/history_reader.py +142 -0
- package/artemis-main/artemis/data_engine/models.py +131 -0
- package/artemis-main/artemis/data_engine/storage.py +1245 -0
- package/artemis-main/artemis/data_engine/trace.py +727 -0
- package/artemis-main/artemis/drivers/__init__.py +48 -0
- package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
- package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
- package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
- package/artemis-main/artemis/drivers/android/recorder.py +57 -0
- package/artemis-main/artemis/drivers/base.py +290 -0
- package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
- package/artemis-main/artemis/drivers/factory.py +80 -0
- package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
- package/artemis-main/artemis/drivers/types.py +67 -0
- package/artemis-main/artemis/graph/checkpoints.py +1243 -0
- package/artemis-main/artemis/graph/graph.py +1055 -0
- package/artemis-main/artemis/graph/perception.py +234 -0
- package/artemis-main/artemis/graph/state.py +151 -0
- package/artemis-main/artemis/graph/visibility.py +238 -0
- package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
- package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
- package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
- package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
- package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
- package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
- package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
- package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
- package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
- package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
- package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
- package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
- package/artemis-main/artemis/interfaces/cli/main.py +109 -0
- package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
- package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
- package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
- package/artemis-main/artemis/llm/__init__.py +25 -0
- package/artemis-main/artemis/llm/fake_model.py +179 -0
- package/artemis-main/artemis/llm/google/__init__.py +57 -0
- package/artemis-main/artemis/llm/google/provider.py +185 -0
- package/artemis-main/artemis/llm/google/usage.py +148 -0
- package/artemis-main/artemis/llm/parser.py +32 -0
- package/artemis-main/artemis/llm/reliability.py +267 -0
- package/artemis-main/artemis/llm/router.py +389 -0
- package/artemis-main/artemis/llm/schemas.py +36 -0
- package/artemis-main/artemis/llm/structured.py +196 -0
- package/artemis-main/artemis/main.py +58 -0
- package/artemis-main/artemis/mcp/__init__.py +20 -0
- package/artemis-main/artemis/mcp/action_executor.py +760 -0
- package/artemis-main/artemis/mcp/action_manifest.py +320 -0
- package/artemis-main/artemis/mcp/action_names.py +172 -0
- package/artemis-main/artemis/mcp/action_server.py +202 -0
- package/artemis-main/artemis/mcp/action_session.py +245 -0
- package/artemis-main/artemis/mcp/action_specs.py +868 -0
- package/artemis-main/artemis/mcp/action_types.py +86 -0
- package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
- package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
- package/artemis-main/artemis/mcp/actuators/base.py +113 -0
- package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
- package/artemis-main/artemis/mcp/adb_server.py +490 -0
- package/artemis-main/artemis/mcp/observation.py +133 -0
- package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
- package/artemis-main/artemis/memory/__init__.py +90 -0
- package/artemis-main/artemis/memory/chunking.py +1843 -0
- package/artemis-main/artemis/memory/context_policy.py +226 -0
- package/artemis-main/artemis/memory/step_capsule.md +73 -0
- package/artemis-main/artemis/memory/step_memory.py +281 -0
- package/artemis-main/artemis/memory/transcript.py +798 -0
- package/artemis-main/artemis/platform/__init__.py +61 -0
- package/artemis-main/artemis/platform/base.py +104 -0
- package/artemis-main/artemis/platform/darwin.py +184 -0
- package/artemis-main/artemis/platform/linux.py +207 -0
- package/artemis-main/artemis/platform/windows.py +193 -0
- package/artemis-main/artemis/resources/__init__.py +46 -0
- package/artemis-main/artemis/runtime/__init__.py +108 -0
- package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
- package/artemis-main/artemis/runtime/awake_lease.py +283 -0
- package/artemis-main/artemis/runtime/awake_service.py +343 -0
- package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
- package/artemis-main/artemis/runtime/daemon_client.py +422 -0
- package/artemis-main/artemis/runtime/device_lock.py +1013 -0
- package/artemis-main/artemis/runtime/device_pool.py +478 -0
- package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
- package/artemis-main/artemis/runtime/process_probe.py +98 -0
- package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
- package/artemis-main/artemis/runtime/supervisor.py +121 -0
- package/artemis-main/artemis/runtime/trace_store.py +375 -0
- package/artemis-main/artemis/sdk/__init__.py +26 -0
- package/artemis-main/artemis/sdk/agent.py +485 -0
- package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
- package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
- package/artemis-main/artemis/sdk/constants.py +15 -0
- package/artemis-main/artemis/sdk/run_outcome.py +58 -0
- package/artemis-main/artemis/sdk/types/__init__.py +61 -0
- package/artemis-main/artemis/sdk/types/agent.py +89 -0
- package/artemis-main/artemis/sdk/utils.py +28 -0
- package/artemis-main/artemis/services/llm.py +1040 -0
- package/artemis-main/artemis/services/token_meter.py +267 -0
- package/artemis-main/artemis/telemetry/__init__.py +35 -0
- package/artemis-main/artemis/telemetry/models.py +64 -0
- package/artemis-main/artemis/telemetry/replay.py +34 -0
- package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
- package/artemis-main/artemis/telemetry/tracer.py +85 -0
- package/artemis-main/artemis/toolchain/__init__.py +52 -0
- package/artemis-main/artemis/toolchain/descriptors.py +92 -0
- package/artemis-main/artemis/toolchain/resolver.py +185 -0
- package/artemis-main/artemis/tools/base.py +232 -0
- package/artemis-main/artemis/tools/command_tool.py +1240 -0
- package/artemis-main/artemis/tools/committee_prompts.json +5 -0
- package/artemis-main/artemis/tools/committee_tool.py +422 -0
- package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
- package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
- package/artemis-main/artemis/tools/explorer_tool.py +581 -0
- package/artemis-main/artemis/tools/history/__init__.py +398 -0
- package/artemis-main/artemis/tools/history/replay.py +120 -0
- package/artemis-main/artemis/tools/history/screenshot.py +139 -0
- package/artemis-main/artemis/tools/history/search.py +563 -0
- package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
- package/artemis-main/artemis/tools/index.py +47 -0
- package/artemis-main/artemis/tools/log_tool.py +108 -0
- package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
- package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
- package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
- package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
- package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
- package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
- package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
- package/artemis-main/artemis/tools/scratchpad.py +819 -0
- package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
- package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
- package/artemis-main/artemis/tools/video_tool.py +261 -0
- package/artemis-main/artemis/tools/wait_tool.py +97 -0
- package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
- package/artemis-main/artemis/utils/coordinates.py +606 -0
- package/artemis-main/artemis/utils/credentials_validator.py +201 -0
- package/artemis-main/artemis/utils/cv_canvas.py +189 -0
- package/artemis-main/artemis/utils/cython_compat.py +45 -0
- package/artemis-main/artemis/utils/element_hit_test.py +80 -0
- package/artemis-main/artemis/utils/image_diff.py +335 -0
- package/artemis-main/artemis/utils/image_hash.py +80 -0
- package/artemis-main/artemis/utils/notes.py +470 -0
- package/artemis-main/artemis/utils/ocr_api.py +105 -0
- package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
- package/artemis-main/artemis/utils/plan_grammar.py +513 -0
- package/artemis-main/artemis/utils/python_executor.py +213 -0
- package/artemis-main/artemis/utils/startup_progress.py +60 -0
- package/artemis-main/artemis/utils/task_tree.py +1174 -0
- package/artemis-main/artemis/utils/text.py +37 -0
- package/artemis-main/artemis/utils/ui_filter.py +651 -0
- package/artemis-main/artemis/utils/video.py +685 -0
- package/artemis-main/artemis/utils/visualization.py +802 -0
- package/artemis-main/config/artemis.example.jsonc +12 -0
- package/artemis-main/mcp_server/README.md +111 -0
- package/artemis-main/mcp_server/__init__.py +21 -0
- package/artemis-main/mcp_server/__main__.py +20 -0
- package/artemis-main/mcp_server/background/__init__.py +15 -0
- package/artemis-main/mcp_server/background/task_runner.py +453 -0
- package/artemis-main/mcp_server/base.py +49 -0
- package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
- package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
- package/artemis-main/mcp_server/notifiers/base.py +45 -0
- package/artemis-main/mcp_server/notifiers/composite.py +87 -0
- package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
- package/artemis-main/mcp_server/notifiers/file.py +69 -0
- package/artemis-main/mcp_server/notifiers/script.py +90 -0
- package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
- package/artemis-main/mcp_server/rules.md +68 -0
- package/artemis-main/mcp_server/server.py +106 -0
- package/artemis-main/mcp_server/tools/__init__.py +29 -0
- package/artemis-main/mcp_server/tools/device_state.py +107 -0
- package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
- package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
- package/artemis-main/mcp_server/tools/task_manager.py +627 -0
- package/artemis-main/mcp_server/tools/task_runner.py +548 -0
- package/artemis-main/mcp_server/utils/__init__.py +15 -0
- package/artemis-main/mcp_server/utils/device_utils.py +180 -0
- package/artemis-main/mcp_server/utils/env_utils.py +65 -0
- package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
- package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
- package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
- package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
- package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
- package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
- package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
- package/artemis-main/packages/artemis-client/README.md +112 -0
- package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
- package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
- package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
- package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
- package/artemis-main/pyproject.toml +259 -0
- package/artemis-main/pyright-core.json +17 -0
- package/artemis-main/scripts/check_dependency_sources.py +68 -0
- package/artemis-main/scripts/install_deps.ps1 +520 -0
- package/artemis-main/scripts/install_deps.sh +533 -0
- package/artemis-main/scripts/quality_ratchet.py +97 -0
- package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
- package/artemis-main/scripts/run_edge_case_part.py +327 -0
- package/artemis-main/scripts/run_ui.py +242 -0
- package/artemis-main/scripts/start.ps1 +500 -0
- package/artemis-main/scripts/test_all_edge_cases.py +269 -0
- package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
- package/artemis-main/setup.py +164 -0
- package/artemis-main/start.bat +35 -0
- package/artemis-main/start.sh +423 -0
- package/artemis-main/third_party/mobile_use/LICENSE +190 -0
- package/artemis-main/third_party/mobile_use/METADATA +27 -0
- package/artemis-main/third_party/mobile_use/NOTICE +12 -0
- package/artemis-main/third_party/mobile_use/__init__.py +19 -0
- package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
- package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
- package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
- package/artemis-main/third_party/mobile_use/config/output.py +184 -0
- package/artemis-main/third_party/mobile_use/context.py +119 -0
- package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
- package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
- package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
- package/artemis-main/third_party/mobile_use/graph.py +54 -0
- package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
- package/artemis-main/third_party/mobile_use/main.py +130 -0
- package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
- package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
- package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
- package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
- package/artemis-main/third_party/mobile_use/testing.py +59 -0
- package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
- package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
- package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
- package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
- package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
- package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
- package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
- package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
- package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
- package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
- package/artemis-main/uv.lock +4908 -0
- package/bin/android-midscene-automation.js +1 -5
- package/bin/artemis-config.mjs +15 -0
- package/bin/artemis-embedded.py +56 -0
- package/bin/start-artemis.mjs +30 -0
- package/bin/start-dev.mjs +112 -0
- package/bin/test_artemis_embedded.py +44 -0
- package/package.json +4 -2
- package/server/artemis.ts +34 -0
- package/server/http-api.ts +12 -0
- package/src/App.vue +10 -5
- package/src/api.ts +5 -0
- package/src/appium-recorder/AppiumPage.vue +1 -1
- package/src/appium-recorder/components/ComponentInspector.vue +1 -1
- package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
- package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
- package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
- package/src/appium-recorder/flow-appearance.ts +4 -4
- package/src/components/AppSidebar.vue +8 -7
- package/src/components/MidsceneMaintenanceNotice.vue +17 -0
- package/src/components/analytics/AnalysisCharts.vue +4 -4
- package/src/components/analytics/ReportAnalysis.vue +1 -1
- package/src/components/generator/TestCaseFileUpload.vue +1 -1
- package/src/components/help/HelpCenter.vue +56 -37
- package/src/pages/AiGeneratorPage.vue +1 -1
- package/src/pages/ArtemisPage.vue +72 -0
- package/src/pages/ConfigPage.vue +18 -19
- package/src/style.css +340 -121
- package/src/types.ts +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Text extraction and manipulation utilities."""
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def safe_extract_text(content: Any) -> str:
|
|
21
|
+
"""Safely extracts a flat string from potentially complex or nested LLM response content.
|
|
22
|
+
|
|
23
|
+
Guarantees returning a string object to prevent AttributeError when calling
|
|
24
|
+
string methods.
|
|
25
|
+
"""
|
|
26
|
+
if isinstance(content, str):
|
|
27
|
+
return content
|
|
28
|
+
elif isinstance(content, list):
|
|
29
|
+
return "".join(safe_extract_text(item) for item in content)
|
|
30
|
+
elif isinstance(content, dict):
|
|
31
|
+
if "text" in content:
|
|
32
|
+
return safe_extract_text(content["text"])
|
|
33
|
+
return str(content)
|
|
34
|
+
elif content is None:
|
|
35
|
+
return ""
|
|
36
|
+
else:
|
|
37
|
+
return str(content)
|
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _parse_bounds(bounds: Any) -> dict[str, int] | None:
|
|
24
|
+
"""Normalize bounds from different formats to a standard dictionary.
|
|
25
|
+
|
|
26
|
+
Supports: 1. String format: "[x1,y1][x2,y2]" 2. Dictionary format: {"x":
|
|
27
|
+
int, "y": int, "width": int, "height": int} 3. Dictionary format: {"left":
|
|
28
|
+
int, "top": int, "right": int, "bottom": int} 4. Cached format:
|
|
29
|
+
"parsed_bounds" inside element dict
|
|
30
|
+
"""
|
|
31
|
+
if not bounds:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
if isinstance(bounds, dict):
|
|
35
|
+
if "parsed_bounds" in bounds and isinstance(bounds["parsed_bounds"], dict):
|
|
36
|
+
return bounds["parsed_bounds"]
|
|
37
|
+
if "left" in bounds and "top" in bounds:
|
|
38
|
+
return {
|
|
39
|
+
"left": int(bounds.get("left", 0)),
|
|
40
|
+
"top": int(bounds.get("top", 0)),
|
|
41
|
+
"right": int(bounds.get("right", 0)),
|
|
42
|
+
"bottom": int(bounds.get("bottom", 0)),
|
|
43
|
+
}
|
|
44
|
+
if "x" in bounds and "y" in bounds:
|
|
45
|
+
x = int(bounds.get("x", 0))
|
|
46
|
+
y = int(bounds.get("y", 0))
|
|
47
|
+
width = int(bounds.get("width", 0))
|
|
48
|
+
height = int(bounds.get("height", 0))
|
|
49
|
+
return {
|
|
50
|
+
"left": x,
|
|
51
|
+
"top": y,
|
|
52
|
+
"right": x + width,
|
|
53
|
+
"bottom": y + height,
|
|
54
|
+
}
|
|
55
|
+
if "bounds" in bounds and isinstance(bounds["bounds"], str):
|
|
56
|
+
res = _parse_bounds(bounds["bounds"])
|
|
57
|
+
if res:
|
|
58
|
+
bounds["parsed_bounds"] = res
|
|
59
|
+
return res
|
|
60
|
+
|
|
61
|
+
if isinstance(bounds, str):
|
|
62
|
+
match = re.match(r"\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]", bounds)
|
|
63
|
+
if match:
|
|
64
|
+
x1, y1, x2, y2 = map(int, match.groups())
|
|
65
|
+
return {"left": x1, "top": y1, "right": x2, "bottom": y2}
|
|
66
|
+
return None
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _intersects_screen(bounds: dict[str, int], screen_width: int, screen_height: int) -> bool:
|
|
71
|
+
"""Check if element intersects screen bounds."""
|
|
72
|
+
left = bounds.get("left", 0)
|
|
73
|
+
top = bounds.get("top", 0)
|
|
74
|
+
right = bounds.get("right", 0)
|
|
75
|
+
bottom = bounds.get("bottom", 0)
|
|
76
|
+
return not (right <= 0 or bottom <= 0 or left >= screen_width or top >= screen_height)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _meets_min_size(bounds: dict[str, int], min_size: int) -> bool:
|
|
80
|
+
"""Check if element meets minimum size requirements."""
|
|
81
|
+
width = bounds.get("right", 0) - bounds.get("left", 0)
|
|
82
|
+
height = bounds.get("bottom", 0) - bounds.get("top", 0)
|
|
83
|
+
return width > min_size and height > min_size
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _clip_bounds(
|
|
87
|
+
node: dict[str, Any],
|
|
88
|
+
bounds_data: dict[str, int],
|
|
89
|
+
ancestor_bounds: dict[str, int],
|
|
90
|
+
) -> dict[str, int]:
|
|
91
|
+
"""Clips bounds to ancestor bounds and updates the node inplace."""
|
|
92
|
+
left = max(bounds_data["left"], ancestor_bounds.get("left", 0))
|
|
93
|
+
top = max(bounds_data["top"], ancestor_bounds.get("top", 0))
|
|
94
|
+
right = min(bounds_data["right"], ancestor_bounds.get("right", 0))
|
|
95
|
+
bottom = min(bounds_data["bottom"], ancestor_bounds.get("bottom", 0))
|
|
96
|
+
|
|
97
|
+
is_fully_clipped = False
|
|
98
|
+
if left >= right or top >= bottom:
|
|
99
|
+
is_fully_clipped = True
|
|
100
|
+
# Clamp to a point on the edge of ancestor bounds
|
|
101
|
+
left = right = max(
|
|
102
|
+
ancestor_bounds.get("left", 0),
|
|
103
|
+
min(bounds_data["left"], ancestor_bounds.get("right", 0)),
|
|
104
|
+
)
|
|
105
|
+
top = bottom = max(
|
|
106
|
+
ancestor_bounds.get("top", 0),
|
|
107
|
+
min(bounds_data["top"], ancestor_bounds.get("bottom", 0)),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
new_bounds = {"left": left, "top": top, "right": right, "bottom": bottom}
|
|
111
|
+
|
|
112
|
+
if is_fully_clipped:
|
|
113
|
+
node["is_clipped"] = True
|
|
114
|
+
|
|
115
|
+
# The cached parse is what every later consumer (and _parse_bounds itself)
|
|
116
|
+
# reads first; leaving it stale would hand out the unclipped rectangle.
|
|
117
|
+
if isinstance(node.get("parsed_bounds"), dict):
|
|
118
|
+
node["parsed_bounds"] = new_bounds.copy()
|
|
119
|
+
|
|
120
|
+
# Re-serialize bounds into the node matching original format
|
|
121
|
+
orig = node.get("bounds")
|
|
122
|
+
if isinstance(orig, str):
|
|
123
|
+
node["bounds"] = f"[{left},{top}][{right},{bottom}]"
|
|
124
|
+
elif isinstance(orig, dict):
|
|
125
|
+
if "left" in orig and "top" in orig:
|
|
126
|
+
node["bounds"] = new_bounds.copy()
|
|
127
|
+
elif "x" in orig and "y" in orig:
|
|
128
|
+
node["bounds"] = {
|
|
129
|
+
"x": left,
|
|
130
|
+
"y": top,
|
|
131
|
+
"width": right - left,
|
|
132
|
+
"height": bottom - top,
|
|
133
|
+
}
|
|
134
|
+
return new_bounds
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _is_semantic_empty(node: dict[str, Any]) -> bool:
|
|
138
|
+
"""Check if a node has no semantic value or interactive capability."""
|
|
139
|
+
# Check text
|
|
140
|
+
text = node.get("text")
|
|
141
|
+
if text and str(text).strip():
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
# Check content description
|
|
145
|
+
content_desc = (
|
|
146
|
+
node.get("content-desc")
|
|
147
|
+
or node.get("contentDescription")
|
|
148
|
+
or node.get("content-description")
|
|
149
|
+
)
|
|
150
|
+
if content_desc and str(content_desc).strip():
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
# Check clickable
|
|
154
|
+
clickable = node.get("clickable")
|
|
155
|
+
if clickable is True or str(clickable).lower() == "true":
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
# Check focusable
|
|
159
|
+
focusable = node.get("focusable")
|
|
160
|
+
if focusable is True or str(focusable).lower() == "true":
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
# Check if it's a valid image container
|
|
164
|
+
class_name = str(node.get("className") or node.get("class") or "").lower()
|
|
165
|
+
if "image" in class_name or "icon" in class_name or "photo" in class_name:
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _merge_complementary_children(children: list[dict], parent_node: dict) -> list[dict]:
|
|
172
|
+
"""Strategy 2: Merge complementary siblings under a clickable parent."""
|
|
173
|
+
# Rule 1: Parent must be clickable or focusable
|
|
174
|
+
clickable = parent_node.get("clickable")
|
|
175
|
+
focusable = parent_node.get("focusable")
|
|
176
|
+
if not (
|
|
177
|
+
clickable is True
|
|
178
|
+
or str(clickable).lower() == "true"
|
|
179
|
+
or focusable is True
|
|
180
|
+
or str(focusable).lower() == "true"
|
|
181
|
+
):
|
|
182
|
+
return children
|
|
183
|
+
|
|
184
|
+
# Classify children
|
|
185
|
+
textual_children = []
|
|
186
|
+
decoration_children = []
|
|
187
|
+
|
|
188
|
+
for child in children:
|
|
189
|
+
# Rule 3: No child is a form control
|
|
190
|
+
class_name = str(child.get("className") or child.get("class") or "").lower()
|
|
191
|
+
if any(kw in class_name for kw in ["checkbox", "switch", "radio", "edittext"]):
|
|
192
|
+
return children
|
|
193
|
+
|
|
194
|
+
text = child.get("text")
|
|
195
|
+
content_desc = (
|
|
196
|
+
child.get("content-desc")
|
|
197
|
+
or child.get("contentDescription")
|
|
198
|
+
or child.get("content-description")
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if (text and str(text).strip()) or (content_desc and str(content_desc).strip()):
|
|
202
|
+
textual_children.append(child)
|
|
203
|
+
else:
|
|
204
|
+
# Check if it's a decoration (not clickable, not focusable)
|
|
205
|
+
c_clickable = child.get("clickable")
|
|
206
|
+
c_focusable = child.get("focusable")
|
|
207
|
+
if not (
|
|
208
|
+
c_clickable is True
|
|
209
|
+
or str(c_clickable).lower() == "true"
|
|
210
|
+
or c_focusable is True
|
|
211
|
+
or str(c_focusable).lower() == "true"
|
|
212
|
+
):
|
|
213
|
+
decoration_children.append(child)
|
|
214
|
+
else:
|
|
215
|
+
# It's an interactive element without text, don't merge as decoration
|
|
216
|
+
return children
|
|
217
|
+
|
|
218
|
+
# Rule 2: Exactly ONE textual child and at least one decoration child
|
|
219
|
+
if (
|
|
220
|
+
len(textual_children) == 1
|
|
221
|
+
and len(decoration_children) >= 1
|
|
222
|
+
and len(children) == len(textual_children) + len(decoration_children)
|
|
223
|
+
):
|
|
224
|
+
# Perform merge
|
|
225
|
+
txt_child = textual_children[0]
|
|
226
|
+
|
|
227
|
+
# Compute Union Bounds
|
|
228
|
+
all_bounds = []
|
|
229
|
+
for c in children:
|
|
230
|
+
b = _parse_bounds(c.get("bounds"))
|
|
231
|
+
if b:
|
|
232
|
+
all_bounds.append(b)
|
|
233
|
+
|
|
234
|
+
if not all_bounds:
|
|
235
|
+
return children
|
|
236
|
+
|
|
237
|
+
left = min(b["left"] for b in all_bounds)
|
|
238
|
+
top = min(b["top"] for b in all_bounds)
|
|
239
|
+
right = max(b["right"] for b in all_bounds)
|
|
240
|
+
bottom = max(b["bottom"] for b in all_bounds)
|
|
241
|
+
|
|
242
|
+
merged_node = txt_child.copy()
|
|
243
|
+
|
|
244
|
+
# Update bounds matching original format if possible, or just dict
|
|
245
|
+
orig = txt_child.get("bounds")
|
|
246
|
+
if isinstance(orig, str):
|
|
247
|
+
merged_node["bounds"] = f"[{left},{top}][{right},{bottom}]"
|
|
248
|
+
else:
|
|
249
|
+
merged_node["bounds"] = {
|
|
250
|
+
"left": left,
|
|
251
|
+
"top": top,
|
|
252
|
+
"right": right,
|
|
253
|
+
"bottom": bottom,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# Concatenate text and content-desc
|
|
257
|
+
desc_parts = []
|
|
258
|
+
for c in children:
|
|
259
|
+
desc = (
|
|
260
|
+
c.get("content-desc") or c.get("contentDescription") or c.get("content-description")
|
|
261
|
+
)
|
|
262
|
+
if desc and str(desc).strip():
|
|
263
|
+
desc_parts.append(f"[{str(desc).strip()}]")
|
|
264
|
+
elif "image" in str(c.get("className") or c.get("class") or "").lower():
|
|
265
|
+
# User tip: use [Icon] for images without desc
|
|
266
|
+
desc_parts.append("[Icon]")
|
|
267
|
+
|
|
268
|
+
text_parts = []
|
|
269
|
+
for c in children:
|
|
270
|
+
txt = c.get("text")
|
|
271
|
+
if txt and str(txt).strip():
|
|
272
|
+
text_parts.append(str(txt).strip())
|
|
273
|
+
|
|
274
|
+
if desc_parts:
|
|
275
|
+
merged_node["content-desc"] = " ".join(desc_parts)
|
|
276
|
+
if text_parts:
|
|
277
|
+
merged_node["text"] = " ".join(text_parts)
|
|
278
|
+
|
|
279
|
+
return [merged_node]
|
|
280
|
+
|
|
281
|
+
return children
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _prune_parent_child_redundancy(
|
|
285
|
+
parent: dict[str, Any], children: list[dict[str, Any]]
|
|
286
|
+
) -> list[dict[str, Any]]:
|
|
287
|
+
"""Strategy 3: Remove redundant children that have the same text as the parent."""
|
|
288
|
+
parent_text = str(parent.get("text") or "").strip()
|
|
289
|
+
parent_desc = str(
|
|
290
|
+
parent.get("content-desc")
|
|
291
|
+
or parent.get("contentDescription")
|
|
292
|
+
or parent.get("content-description")
|
|
293
|
+
or ""
|
|
294
|
+
).strip()
|
|
295
|
+
|
|
296
|
+
if not parent_text and not parent_desc:
|
|
297
|
+
return children
|
|
298
|
+
|
|
299
|
+
new_children = []
|
|
300
|
+
for child in children:
|
|
301
|
+
child_text = str(child.get("text") or "").strip()
|
|
302
|
+
child_desc = str(
|
|
303
|
+
child.get("content-desc")
|
|
304
|
+
or child.get("contentDescription")
|
|
305
|
+
or child.get("content-description")
|
|
306
|
+
or ""
|
|
307
|
+
).strip()
|
|
308
|
+
|
|
309
|
+
text_matches = False
|
|
310
|
+
if parent_text and child_text and parent_text == child_text:
|
|
311
|
+
text_matches = True
|
|
312
|
+
elif parent_desc and child_text and parent_desc == child_text:
|
|
313
|
+
text_matches = True
|
|
314
|
+
elif parent_text and child_desc and parent_text == child_desc:
|
|
315
|
+
text_matches = True
|
|
316
|
+
elif parent_desc and child_desc and parent_desc == child_desc:
|
|
317
|
+
text_matches = True
|
|
318
|
+
|
|
319
|
+
if text_matches:
|
|
320
|
+
class_name = str(child.get("className") or child.get("class") or "").lower()
|
|
321
|
+
is_form_control = any(
|
|
322
|
+
kw in class_name for kw in ["checkbox", "switch", "radio", "edittext"]
|
|
323
|
+
)
|
|
324
|
+
has_children = bool(child.get("children"))
|
|
325
|
+
|
|
326
|
+
clickable = child.get("clickable")
|
|
327
|
+
focusable = child.get("focusable")
|
|
328
|
+
is_interactive = (
|
|
329
|
+
clickable is True
|
|
330
|
+
or str(clickable).lower() == "true"
|
|
331
|
+
or focusable is True
|
|
332
|
+
or str(focusable).lower() == "true"
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if is_form_control or has_children or is_interactive:
|
|
336
|
+
# Keep but erase text
|
|
337
|
+
merged_node = child.copy()
|
|
338
|
+
if "text" in merged_node:
|
|
339
|
+
merged_node["text"] = ""
|
|
340
|
+
if "content-desc" in merged_node:
|
|
341
|
+
merged_node["content-desc"] = ""
|
|
342
|
+
if "contentDescription" in merged_node:
|
|
343
|
+
merged_node["contentDescription"] = ""
|
|
344
|
+
if "content-description" in merged_node:
|
|
345
|
+
merged_node["content-description"] = ""
|
|
346
|
+
new_children.append(merged_node)
|
|
347
|
+
else:
|
|
348
|
+
# Remove child
|
|
349
|
+
continue
|
|
350
|
+
else:
|
|
351
|
+
new_children.append(child)
|
|
352
|
+
|
|
353
|
+
return new_children
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _filter_and_prune(
|
|
357
|
+
node: dict[str, Any],
|
|
358
|
+
screen_width: int,
|
|
359
|
+
screen_height: int,
|
|
360
|
+
min_element_size: int = 5,
|
|
361
|
+
use_dynamic_min_size: bool = True,
|
|
362
|
+
clip_bounds: bool = True,
|
|
363
|
+
semantic_pruning: str = "hoist",
|
|
364
|
+
ancestor_bounds: dict[str, int] | None = None,
|
|
365
|
+
) -> list[dict[str, Any]]:
|
|
366
|
+
"""Recursively filters, clips bounds, and applies semantic pruning strategies."""
|
|
367
|
+
if ancestor_bounds is None:
|
|
368
|
+
ancestor_bounds = {
|
|
369
|
+
"left": 0,
|
|
370
|
+
"top": 0,
|
|
371
|
+
"right": screen_width,
|
|
372
|
+
"bottom": screen_height,
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
bounds_data = _parse_bounds(node.get("bounds"))
|
|
376
|
+
|
|
377
|
+
if bounds_data:
|
|
378
|
+
# We no longer check _intersects_screen to return [] immediately,
|
|
379
|
+
# to ensure zero info loss. Out-of-bounds elements will be clipped to a point.
|
|
380
|
+
|
|
381
|
+
if clip_bounds:
|
|
382
|
+
bounds_data = _clip_bounds(node, bounds_data, ancestor_bounds)
|
|
383
|
+
|
|
384
|
+
# Dynamic min size logic: use relative percentage (0.5% of short side) if enabled
|
|
385
|
+
actual_min_size = min_element_size
|
|
386
|
+
if use_dynamic_min_size:
|
|
387
|
+
short_side = min(screen_width, screen_height)
|
|
388
|
+
dynamic_min_size = int(short_side * 0.005)
|
|
389
|
+
actual_min_size = max(min_element_size, dynamic_min_size)
|
|
390
|
+
|
|
391
|
+
# If it's fully clipped, we skip min size check to preserve it
|
|
392
|
+
if not node.get("is_clipped", False) and not _meets_min_size(bounds_data, actual_min_size):
|
|
393
|
+
return []
|
|
394
|
+
|
|
395
|
+
# Recursively process children
|
|
396
|
+
filtered_node = node.copy()
|
|
397
|
+
filtered_children = []
|
|
398
|
+
if "children" in node:
|
|
399
|
+
for child in node.get("children", []):
|
|
400
|
+
if isinstance(child, dict):
|
|
401
|
+
filtered_children.extend(
|
|
402
|
+
_filter_and_prune(
|
|
403
|
+
child,
|
|
404
|
+
screen_width,
|
|
405
|
+
screen_height,
|
|
406
|
+
min_element_size,
|
|
407
|
+
use_dynamic_min_size,
|
|
408
|
+
clip_bounds,
|
|
409
|
+
semantic_pruning,
|
|
410
|
+
ancestor_bounds=bounds_data if bounds_data else ancestor_bounds,
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
# Apply Strategy 3: Parent-Child Redundancy Removal
|
|
415
|
+
if filtered_children:
|
|
416
|
+
filtered_children = _prune_parent_child_redundancy(filtered_node, filtered_children)
|
|
417
|
+
|
|
418
|
+
# Apply Strategy 2: Sibling Merging
|
|
419
|
+
if filtered_children:
|
|
420
|
+
filtered_children = _merge_complementary_children(filtered_children, filtered_node)
|
|
421
|
+
|
|
422
|
+
filtered_node["children"] = filtered_children
|
|
423
|
+
|
|
424
|
+
# Apply semantic pruning strategies
|
|
425
|
+
if _is_semantic_empty(filtered_node):
|
|
426
|
+
if semantic_pruning == "safe":
|
|
427
|
+
# Only eliminate if it's a leaf node
|
|
428
|
+
if not filtered_node.get("children"):
|
|
429
|
+
return []
|
|
430
|
+
elif semantic_pruning == "hoist":
|
|
431
|
+
# Skip this node and hoist its valid children up
|
|
432
|
+
return filtered_node.get("children", [])
|
|
433
|
+
|
|
434
|
+
return [filtered_node]
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _detect_fixed_system_bars(
|
|
438
|
+
ui_hierarchy: list[dict[str, Any]], screen_width: int, screen_height: int
|
|
439
|
+
) -> list[dict[str, int]]:
|
|
440
|
+
"""Detect fixed, non-scrollable system or navigation bars anchored to the extreme top/bottom edges.
|
|
441
|
+
|
|
442
|
+
Enforces strict triple guardrails to guarantee near-zero false positives:
|
|
443
|
+
|
|
444
|
+
1. Edge & Width Guarantee: Must anchor to top (top==0) or bottom
|
|
445
|
+
(bottom==screen_height) and span >= 95% of screen width.
|
|
446
|
+
2. Non-Scrollable Root Branch: The candidate node and all its
|
|
447
|
+
descendants/ancestors must NOT be scrollable.
|
|
448
|
+
3. Identity & Height Bounds: High-confidence system package/IDs OR strict
|
|
449
|
+
navigation bar height bounds.
|
|
450
|
+
"""
|
|
451
|
+
fixed_bars = []
|
|
452
|
+
|
|
453
|
+
def check_scrollable(node: dict[str, Any]) -> bool:
|
|
454
|
+
if node.get("scrollable") is True or str(node.get("scrollable", "")).lower() == "true":
|
|
455
|
+
return True
|
|
456
|
+
for child in node.get("children", []):
|
|
457
|
+
if isinstance(child, dict) and check_scrollable(child):
|
|
458
|
+
return True
|
|
459
|
+
return False
|
|
460
|
+
|
|
461
|
+
def scan_for_bars(node: dict[str, Any], has_scrollable_ancestor: bool):
|
|
462
|
+
is_curr_scrollable = has_scrollable_ancestor or (
|
|
463
|
+
node.get("scrollable") is True or str(node.get("scrollable", "")).lower() == "true"
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
bounds = _parse_bounds(node.get("bounds"))
|
|
467
|
+
if bounds and not is_curr_scrollable and not check_scrollable(node):
|
|
468
|
+
width = bounds["right"] - bounds["left"]
|
|
469
|
+
height = bounds["bottom"] - bounds["top"]
|
|
470
|
+
package_name = str(node.get("package") or node.get("packageName") or "").lower()
|
|
471
|
+
res_id = str(node.get("resource-id") or node.get("resourceId") or "").lower()
|
|
472
|
+
class_name = str(node.get("className") or node.get("class") or "").lower()
|
|
473
|
+
|
|
474
|
+
is_full_width = width >= int(screen_width * 0.95)
|
|
475
|
+
is_high_conf_id = (
|
|
476
|
+
any(
|
|
477
|
+
kw in res_id or kw in class_name
|
|
478
|
+
for kw in [
|
|
479
|
+
"bottom_navigation",
|
|
480
|
+
"navigation_bar",
|
|
481
|
+
"bottom_bar",
|
|
482
|
+
"tab_layout",
|
|
483
|
+
"action_bar",
|
|
484
|
+
]
|
|
485
|
+
)
|
|
486
|
+
or package_name == "com.android.systemui"
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# Check bottom fixed bar (e.g., bottom navigation bar / system navigation bar)
|
|
490
|
+
if bounds["bottom"] >= screen_height and is_full_width and height > 0:
|
|
491
|
+
if is_high_conf_id or (25 <= height <= int(screen_height * 0.15)):
|
|
492
|
+
fixed_bars.append(bounds.copy())
|
|
493
|
+
return # Don't recurse into children of confirmed fixed bars
|
|
494
|
+
|
|
495
|
+
# Check top fixed bar (e.g., status bar / top action bar)
|
|
496
|
+
if bounds["top"] <= 0 and is_full_width and height > 0:
|
|
497
|
+
if package_name == "com.android.systemui" or (
|
|
498
|
+
15 <= height <= int(screen_height * 0.12)
|
|
499
|
+
):
|
|
500
|
+
fixed_bars.append(bounds.copy())
|
|
501
|
+
return
|
|
502
|
+
|
|
503
|
+
for child in node.get("children", []):
|
|
504
|
+
if isinstance(child, dict):
|
|
505
|
+
scan_for_bars(child, is_curr_scrollable)
|
|
506
|
+
|
|
507
|
+
for root_node in ui_hierarchy:
|
|
508
|
+
if isinstance(root_node, dict):
|
|
509
|
+
scan_for_bars(root_node, False)
|
|
510
|
+
|
|
511
|
+
return fixed_bars
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _clamp_by_fixed_bars(
|
|
515
|
+
node: dict[str, Any], fixed_bars: list[dict[str, int]], min_size: int
|
|
516
|
+
) -> bool:
|
|
517
|
+
"""Non-destructively clamp the vertical bounds of a node if it overlaps with confirmed fixed system bars.
|
|
518
|
+
|
|
519
|
+
Returns False if the remaining visible height is smaller than min_size
|
|
520
|
+
(silent discard of thin slivers).
|
|
521
|
+
"""
|
|
522
|
+
bounds = _parse_bounds(node.get("bounds"))
|
|
523
|
+
if not bounds or not fixed_bars:
|
|
524
|
+
return True
|
|
525
|
+
|
|
526
|
+
left, top, right, bottom = (
|
|
527
|
+
bounds["left"],
|
|
528
|
+
bounds["top"],
|
|
529
|
+
bounds["right"],
|
|
530
|
+
bounds["bottom"],
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
for bar in fixed_bars:
|
|
534
|
+
# Check horizontal overlap
|
|
535
|
+
if right <= bar["left"] or left >= bar["right"]:
|
|
536
|
+
continue
|
|
537
|
+
|
|
538
|
+
# If it's the fixed bar itself or fully inside the bar, keep it if it belongs to the bar
|
|
539
|
+
if (
|
|
540
|
+
left == bar["left"]
|
|
541
|
+
and top == bar["top"]
|
|
542
|
+
and right == bar["right"]
|
|
543
|
+
and bottom == bar["bottom"]
|
|
544
|
+
):
|
|
545
|
+
return True
|
|
546
|
+
|
|
547
|
+
# Clamp against a bottom fixed bar
|
|
548
|
+
if bar["bottom"] >= bottom and top < bar["top"] < bottom:
|
|
549
|
+
bottom = min(bottom, bar["top"])
|
|
550
|
+
|
|
551
|
+
# Clamp against a top fixed bar
|
|
552
|
+
if bar["top"] <= top and top < bar["bottom"] < bottom:
|
|
553
|
+
top = max(top, bar["bottom"])
|
|
554
|
+
|
|
555
|
+
# If remaining height is too small, silently discard this occluded sliver
|
|
556
|
+
if bottom - top <= max(min_size, 20):
|
|
557
|
+
return False
|
|
558
|
+
|
|
559
|
+
new_bounds = {"left": left, "top": top, "right": right, "bottom": bottom}
|
|
560
|
+
orig = node.get("bounds")
|
|
561
|
+
if isinstance(orig, str):
|
|
562
|
+
node["bounds"] = f"[{left},{top}][{right},{bottom}]"
|
|
563
|
+
elif isinstance(orig, dict):
|
|
564
|
+
if "left" in orig and "top" in orig:
|
|
565
|
+
node["bounds"] = new_bounds.copy()
|
|
566
|
+
elif "x" in orig and "y" in orig:
|
|
567
|
+
node["bounds"] = {
|
|
568
|
+
"x": left,
|
|
569
|
+
"y": top,
|
|
570
|
+
"width": right - left,
|
|
571
|
+
"height": bottom - top,
|
|
572
|
+
}
|
|
573
|
+
return True
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def filter_node(
|
|
577
|
+
node: dict[str, Any],
|
|
578
|
+
screen_width: int,
|
|
579
|
+
screen_height: int,
|
|
580
|
+
min_element_size: int = 5,
|
|
581
|
+
use_dynamic_min_size: bool = True,
|
|
582
|
+
clip_bounds: bool = True,
|
|
583
|
+
semantic_pruning: str = "hoist",
|
|
584
|
+
) -> dict[str, Any] | None:
|
|
585
|
+
"""Recursively filter a UI node and its children based on screen bounds, size, and semantics.
|
|
586
|
+
|
|
587
|
+
Returns None if the node does not meet the criteria.
|
|
588
|
+
"""
|
|
589
|
+
res_list = _filter_and_prune(
|
|
590
|
+
node,
|
|
591
|
+
screen_width,
|
|
592
|
+
screen_height,
|
|
593
|
+
min_element_size,
|
|
594
|
+
use_dynamic_min_size,
|
|
595
|
+
clip_bounds,
|
|
596
|
+
semantic_pruning,
|
|
597
|
+
)
|
|
598
|
+
if not res_list:
|
|
599
|
+
return None
|
|
600
|
+
|
|
601
|
+
if len(res_list) == 1:
|
|
602
|
+
return res_list[0]
|
|
603
|
+
|
|
604
|
+
# If the root node itself was hoisted resulting in multiple children, wrap or attach
|
|
605
|
+
new_node = node.copy()
|
|
606
|
+
new_node["children"] = res_list
|
|
607
|
+
return new_node
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def filter_ui_hierarchy(
|
|
611
|
+
ui_hierarchy: list[dict[str, Any]],
|
|
612
|
+
screen_width: int = 1080,
|
|
613
|
+
screen_height: int = 2400,
|
|
614
|
+
min_element_size: int = 5,
|
|
615
|
+
use_dynamic_min_size: bool = True,
|
|
616
|
+
clip_bounds: bool = True,
|
|
617
|
+
semantic_pruning: str = "hoist",
|
|
618
|
+
) -> list[dict[str, Any]]:
|
|
619
|
+
"""Pure function to filter an entire list of UI elements (either flat or nested).
|
|
620
|
+
|
|
621
|
+
Removes elements outside screen bounds, smaller than minimum size, or
|
|
622
|
+
semantically empty. Applies non-destructive safe-bounds clamping against
|
|
623
|
+
confirmed fixed system/navigation bars.
|
|
624
|
+
"""
|
|
625
|
+
# 1. Detect high-confidence fixed system bars before flattening
|
|
626
|
+
fixed_bars = _detect_fixed_system_bars(ui_hierarchy, screen_width, screen_height)
|
|
627
|
+
|
|
628
|
+
filtered_elements = []
|
|
629
|
+
for element in ui_hierarchy:
|
|
630
|
+
if isinstance(element, dict):
|
|
631
|
+
filtered_elements.extend(
|
|
632
|
+
_filter_and_prune(
|
|
633
|
+
element,
|
|
634
|
+
screen_width=screen_width,
|
|
635
|
+
screen_height=screen_height,
|
|
636
|
+
min_element_size=min_element_size,
|
|
637
|
+
use_dynamic_min_size=use_dynamic_min_size,
|
|
638
|
+
clip_bounds=clip_bounds,
|
|
639
|
+
semantic_pruning=semantic_pruning,
|
|
640
|
+
)
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
# 2. Apply safe-bounds clamping against fixed system bars on final candidate elements
|
|
644
|
+
if fixed_bars:
|
|
645
|
+
clamped_elements = []
|
|
646
|
+
for el in filtered_elements:
|
|
647
|
+
if _clamp_by_fixed_bars(el, fixed_bars, min_element_size):
|
|
648
|
+
clamped_elements.append(el)
|
|
649
|
+
return clamped_elements
|
|
650
|
+
|
|
651
|
+
return filtered_elements
|