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,335 @@
|
|
|
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 asyncio
|
|
16
|
+
import base64
|
|
17
|
+
from collections.abc import Awaitable, Callable
|
|
18
|
+
import logging
|
|
19
|
+
import math
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
import cv2
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check_ui_change(
|
|
29
|
+
img_before_bytes,
|
|
30
|
+
img_after_bytes,
|
|
31
|
+
action_item,
|
|
32
|
+
full_screen_threshold=0.0001,
|
|
33
|
+
roi_threshold=0.05,
|
|
34
|
+
):
|
|
35
|
+
"""Checks if the UI has changed after an action using image diff.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
img_before_bytes: Image bytes before action.
|
|
39
|
+
img_after_bytes: Image bytes after action.
|
|
40
|
+
action_item: Dict containing 'action' and optionally 'coordinates'.
|
|
41
|
+
full_screen_threshold: Threshold for full screen change (fraction of
|
|
42
|
+
changed pixels).
|
|
43
|
+
roi_threshold: Threshold for ROI change (fraction of changed pixels in
|
|
44
|
+
ROI).
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
bool: True if UI changed, False otherwise.
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
# 1. Decode images
|
|
51
|
+
nparr_b = np.frombuffer(img_before_bytes, np.uint8)
|
|
52
|
+
nparr_a = np.frombuffer(img_after_bytes, np.uint8)
|
|
53
|
+
img_b = cv2.imdecode(nparr_b, cv2.IMREAD_GRAYSCALE)
|
|
54
|
+
img_a = cv2.imdecode(nparr_a, cv2.IMREAD_GRAYSCALE)
|
|
55
|
+
|
|
56
|
+
if img_b is None or img_a is None:
|
|
57
|
+
logger.error("Failed to decode images.")
|
|
58
|
+
return True # Fallback to assuming change if we can't decode
|
|
59
|
+
|
|
60
|
+
h, w = img_b.shape
|
|
61
|
+
action_name = action_item.get("action")
|
|
62
|
+
coordinates = action_item.get("coordinates")
|
|
63
|
+
|
|
64
|
+
# 2. Masking: Ignore status bar (top 10%) and nav bar (bottom 5%)
|
|
65
|
+
top_crop = int(h * 0.10)
|
|
66
|
+
bottom_crop = int(h * 0.05)
|
|
67
|
+
|
|
68
|
+
content_b = img_b[top_crop : h - bottom_crop, :]
|
|
69
|
+
content_a = img_a[top_crop : h - bottom_crop, :]
|
|
70
|
+
|
|
71
|
+
# 3. Calculate absolute difference and threshold
|
|
72
|
+
diff = cv2.absdiff(content_b, content_a)
|
|
73
|
+
blurred = cv2.GaussianBlur(diff, (5, 5), 0)
|
|
74
|
+
_, thresh = cv2.threshold(blurred, 25, 255, cv2.THRESH_BINARY)
|
|
75
|
+
|
|
76
|
+
non_zero_count = cv2.countNonZero(thresh)
|
|
77
|
+
total_pixels = content_b.shape[0] * content_b.shape[1]
|
|
78
|
+
global_change_ratio = non_zero_count / total_pixels
|
|
79
|
+
|
|
80
|
+
logger.info(f"Action: {action_name} | Global UI change ratio: {global_change_ratio:.4f}")
|
|
81
|
+
|
|
82
|
+
# 4. Strategy based on action type
|
|
83
|
+
if action_name == "swipe":
|
|
84
|
+
# Swipe expects large scale movement
|
|
85
|
+
return global_change_ratio > full_screen_threshold
|
|
86
|
+
|
|
87
|
+
# For tap and others, check ROI if coordinates are available
|
|
88
|
+
if (
|
|
89
|
+
action_name
|
|
90
|
+
in [
|
|
91
|
+
"tap",
|
|
92
|
+
"long_press_on",
|
|
93
|
+
"focus_and_input_text",
|
|
94
|
+
"focus_and_clear_text",
|
|
95
|
+
]
|
|
96
|
+
and coordinates
|
|
97
|
+
and len(coordinates) == 2
|
|
98
|
+
):
|
|
99
|
+
cx, cy = coordinates
|
|
100
|
+
# Scale ROI size dynamically based on screen diagonal (reference is 1080x2400)
|
|
101
|
+
# and enforce a minimum of 15px to prevent collapsing on low-res screens
|
|
102
|
+
ref_diagonal = math.sqrt(1080**2 + 2400**2)
|
|
103
|
+
current_diagonal = math.sqrt(w**2 + h**2)
|
|
104
|
+
scale_factor = current_diagonal / ref_diagonal
|
|
105
|
+
roi_size = max(15, int(50 * scale_factor))
|
|
106
|
+
|
|
107
|
+
# Crop ROI from original images to avoid shifting issues with top_crop
|
|
108
|
+
y1 = max(0, cy - roi_size)
|
|
109
|
+
y2 = min(h, cy + roi_size)
|
|
110
|
+
x1 = max(0, cx - roi_size)
|
|
111
|
+
x2 = min(w, cx + roi_size)
|
|
112
|
+
|
|
113
|
+
roi_b = img_b[y1:y2, x1:x2]
|
|
114
|
+
roi_a = img_a[y1:y2, x1:x2]
|
|
115
|
+
|
|
116
|
+
roi_diff = cv2.absdiff(roi_b, roi_a)
|
|
117
|
+
roi_blurred = cv2.GaussianBlur(roi_diff, (3, 3), 0)
|
|
118
|
+
_, roi_thresh = cv2.threshold(roi_blurred, 20, 255, cv2.THRESH_BINARY)
|
|
119
|
+
|
|
120
|
+
roi_non_zero = cv2.countNonZero(roi_thresh)
|
|
121
|
+
roi_total = roi_b.shape[0] * roi_b.shape[1]
|
|
122
|
+
roi_change_ratio = roi_non_zero / roi_total
|
|
123
|
+
|
|
124
|
+
logger.info(f"ROI change ratio at [{cx}, {cy}]: {roi_change_ratio:.4f}")
|
|
125
|
+
|
|
126
|
+
# If ROI changed significantly, we assume action worked
|
|
127
|
+
if roi_change_ratio > roi_threshold:
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
# Fallback to global check if no ROI check was triggered or passed
|
|
131
|
+
return global_change_ratio > full_screen_threshold
|
|
132
|
+
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.error(f"Error during image diff: {e}")
|
|
135
|
+
return True # Fallback to assuming change on error
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
async def wait_for_screen_stability(
|
|
139
|
+
take_screenshot_fn: Callable[[], Awaitable[str]],
|
|
140
|
+
max_timeout: float = 1.5,
|
|
141
|
+
interval: float = 0.2,
|
|
142
|
+
stability_threshold: float = 0.001,
|
|
143
|
+
) -> str:
|
|
144
|
+
"""Wait for the screen to stabilize (nearly zero pixel changes between consecutive frames)
|
|
145
|
+
|
|
146
|
+
before returning the latest base64 screenshot.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
take_screenshot_fn: Async callback function that captures a base64
|
|
150
|
+
screenshot.
|
|
151
|
+
max_timeout: Maximum time to wait for settling (in seconds) before
|
|
152
|
+
forcing exit.
|
|
153
|
+
interval: Polling time step between screenshot samples (in seconds).
|
|
154
|
+
stability_threshold: Pixel change ratio below which the screen is
|
|
155
|
+
considered settled (e.g. 0.001 for < 0.1%).
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
str: The final settled base64 screenshot.
|
|
159
|
+
"""
|
|
160
|
+
logger.info("Starting Dynamic Screen Settling...")
|
|
161
|
+
|
|
162
|
+
# 1. Capture the initial frame
|
|
163
|
+
try:
|
|
164
|
+
last_b64 = await take_screenshot_fn()
|
|
165
|
+
last_bytes = base64.b64decode(last_b64)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.error(f"Failed to capture initial frame for settling: {e}")
|
|
168
|
+
# Return fallback immediately on failure
|
|
169
|
+
return await take_screenshot_fn()
|
|
170
|
+
|
|
171
|
+
start_time = time.time()
|
|
172
|
+
while time.time() - start_time < max_timeout:
|
|
173
|
+
await asyncio.sleep(interval)
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
curr_b64 = await take_screenshot_fn()
|
|
177
|
+
curr_bytes = base64.b64decode(curr_b64)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.warning(f"Failed to capture polling frame: {e}")
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
# 2. Decode and compare using OpenCV
|
|
183
|
+
try:
|
|
184
|
+
nparr_b = np.frombuffer(last_bytes, np.uint8)
|
|
185
|
+
nparr_a = np.frombuffer(curr_bytes, np.uint8)
|
|
186
|
+
img_b = cv2.imdecode(nparr_b, cv2.IMREAD_GRAYSCALE)
|
|
187
|
+
img_a = cv2.imdecode(nparr_a, cv2.IMREAD_GRAYSCALE)
|
|
188
|
+
|
|
189
|
+
if img_b is None or img_a is None:
|
|
190
|
+
last_b64, last_bytes = curr_b64, curr_bytes
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
h, w = img_b.shape
|
|
194
|
+
|
|
195
|
+
# Masking: crop status bar (top 10%) and navigation bar (bottom 5%)
|
|
196
|
+
# to avoid false positives from notification icons, clocks, or blinking elements
|
|
197
|
+
top_crop = int(h * 0.10)
|
|
198
|
+
bottom_crop = int(h * 0.05)
|
|
199
|
+
|
|
200
|
+
content_b = img_b[top_crop : h - bottom_crop, :]
|
|
201
|
+
content_a = img_a[top_crop : h - bottom_crop, :]
|
|
202
|
+
|
|
203
|
+
# Calculate absolute difference and filter noise
|
|
204
|
+
diff = cv2.absdiff(content_b, content_a)
|
|
205
|
+
blurred = cv2.GaussianBlur(diff, (5, 5), 0)
|
|
206
|
+
_, thresh = cv2.threshold(blurred, 25, 255, cv2.THRESH_BINARY)
|
|
207
|
+
|
|
208
|
+
non_zero_count = cv2.countNonZero(thresh)
|
|
209
|
+
total_pixels = content_b.shape[0] * content_b.shape[1]
|
|
210
|
+
change_ratio = non_zero_count / total_pixels
|
|
211
|
+
|
|
212
|
+
elapsed = time.time() - start_time
|
|
213
|
+
logger.info(
|
|
214
|
+
f"Screen settle poll: diff ratio = {change_ratio:.4f} (target <"
|
|
215
|
+
f" {stability_threshold}) | Elapsed: {elapsed:.2f}s"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# If the change ratio is below stability threshold, screen has stabilized!
|
|
219
|
+
if change_ratio < stability_threshold:
|
|
220
|
+
logger.info(f"Screen stabilized after {elapsed:.2f}s (diff: {change_ratio:.4f}).")
|
|
221
|
+
return curr_b64
|
|
222
|
+
|
|
223
|
+
last_b64, last_bytes = curr_b64, curr_bytes
|
|
224
|
+
|
|
225
|
+
except Exception as ex:
|
|
226
|
+
logger.error(f"Error during screen stability image diff calculation: {ex}")
|
|
227
|
+
last_b64, last_bytes = curr_b64, curr_bytes
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
# 3. Timeout safety fallback (e.g., dynamic playing video/animated loaders)
|
|
231
|
+
logger.warning(
|
|
232
|
+
f"Screen settle timed out after {max_timeout}s without complete"
|
|
233
|
+
" stability. Forcing return of latest frame."
|
|
234
|
+
)
|
|
235
|
+
return last_b64
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
async def wait_for_screen_data_stability(
|
|
239
|
+
get_screen_data_fn,
|
|
240
|
+
max_timeout: float = 1.5,
|
|
241
|
+
interval: float = 0.2,
|
|
242
|
+
stability_threshold: float = 0.001,
|
|
243
|
+
):
|
|
244
|
+
"""Wait for the screen to stabilize (nearly zero pixel changes between consecutive frames)
|
|
245
|
+
|
|
246
|
+
while fetching both screenshot and XML hierarchy directly
|
|
247
|
+
(ScreenDataResponse).
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
get_screen_data_fn: Async callback function that captures a
|
|
251
|
+
ScreenDataResponse.
|
|
252
|
+
max_timeout: Maximum time to wait for settling (in seconds) before
|
|
253
|
+
forcing exit.
|
|
254
|
+
interval: Polling time step between screen data samples (in seconds).
|
|
255
|
+
stability_threshold: Pixel change ratio below which the screen is
|
|
256
|
+
considered settled.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
ScreenDataResponse: The final settled ScreenDataResponse.
|
|
260
|
+
"""
|
|
261
|
+
logger.info("Starting Dynamic Screen Data Settling...")
|
|
262
|
+
|
|
263
|
+
# 1. Capture the initial frame
|
|
264
|
+
try:
|
|
265
|
+
last_data = await get_screen_data_fn()
|
|
266
|
+
last_bytes = base64.b64decode(last_data.base64)
|
|
267
|
+
except Exception as e:
|
|
268
|
+
logger.error(f"Failed to capture initial frame for settling: {e}")
|
|
269
|
+
# Return fallback immediately on failure
|
|
270
|
+
return await get_screen_data_fn()
|
|
271
|
+
|
|
272
|
+
start_time = time.time()
|
|
273
|
+
while time.time() - start_time < max_timeout:
|
|
274
|
+
await asyncio.sleep(interval)
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
curr_data = await get_screen_data_fn()
|
|
278
|
+
curr_bytes = base64.b64decode(curr_data.base64)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
logger.warning(f"Failed to capture polling frame: {e}")
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
# 2. Decode and compare using OpenCV
|
|
284
|
+
try:
|
|
285
|
+
nparr_b = np.frombuffer(last_bytes, np.uint8)
|
|
286
|
+
nparr_a = np.frombuffer(curr_bytes, np.uint8)
|
|
287
|
+
img_b = cv2.imdecode(nparr_b, cv2.IMREAD_GRAYSCALE)
|
|
288
|
+
img_a = cv2.imdecode(nparr_a, cv2.IMREAD_GRAYSCALE)
|
|
289
|
+
|
|
290
|
+
if img_b is None or img_a is None:
|
|
291
|
+
last_data, last_bytes = curr_data, curr_bytes
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
h, w = img_b.shape
|
|
295
|
+
|
|
296
|
+
# Masking: crop status bar (top 10%) and navigation bar (bottom 5%)
|
|
297
|
+
top_crop = int(h * 0.10)
|
|
298
|
+
bottom_crop = int(h * 0.05)
|
|
299
|
+
|
|
300
|
+
content_b = img_b[top_crop : h - bottom_crop, :]
|
|
301
|
+
content_a = img_a[top_crop : h - bottom_crop, :]
|
|
302
|
+
|
|
303
|
+
# Calculate absolute difference and filter noise
|
|
304
|
+
diff = cv2.absdiff(content_b, content_a)
|
|
305
|
+
blurred = cv2.GaussianBlur(diff, (5, 5), 0)
|
|
306
|
+
_, thresh = cv2.threshold(blurred, 25, 255, cv2.THRESH_BINARY)
|
|
307
|
+
|
|
308
|
+
non_zero_count = cv2.countNonZero(thresh)
|
|
309
|
+
total_pixels = content_b.shape[0] * content_b.shape[1]
|
|
310
|
+
change_ratio = non_zero_count / total_pixels
|
|
311
|
+
|
|
312
|
+
elapsed = time.time() - start_time
|
|
313
|
+
logger.info(
|
|
314
|
+
f"Screen data settle poll: diff ratio = {change_ratio:.4f}"
|
|
315
|
+
f" (target < {stability_threshold}) | Elapsed: {elapsed:.2f}s"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# If the change ratio is below stability threshold, screen has stabilized!
|
|
319
|
+
if change_ratio < stability_threshold:
|
|
320
|
+
logger.info(f"Screen stabilized after {elapsed:.2f}s (diff: {change_ratio:.4f}).")
|
|
321
|
+
return curr_data
|
|
322
|
+
|
|
323
|
+
last_data, last_bytes = curr_data, curr_bytes
|
|
324
|
+
|
|
325
|
+
except Exception as ex:
|
|
326
|
+
logger.error(f"Error during screen stability image diff calculation: {ex}")
|
|
327
|
+
last_data, last_bytes = curr_data, curr_bytes
|
|
328
|
+
continue
|
|
329
|
+
|
|
330
|
+
# 3. Timeout safety fallback
|
|
331
|
+
logger.warning(
|
|
332
|
+
f"Screen data settle timed out after {max_timeout}s without complete"
|
|
333
|
+
" stability. Forcing return of latest frame."
|
|
334
|
+
)
|
|
335
|
+
return last_data
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
"""Pure-PIL perceptual hashing (dHash) for screen-similarity hints (M4).
|
|
16
|
+
|
|
17
|
+
A 64-bit difference hash: grayscale, resize to 9x8, compare each pixel to its
|
|
18
|
+
right neighbor. Robust to JPEG re-encoding and mild rendering noise, cheap to
|
|
19
|
+
compute (one small resize), and comparable with an integer Hamming distance —
|
|
20
|
+
no model call, no historical image bytes needed at compare time.
|
|
21
|
+
|
|
22
|
+
Hashes are stored per step in ``extra_metadata`` at record time
|
|
23
|
+
(``pre_image_dhash`` / ``post_image_dhash``, 16 hex chars) and compared
|
|
24
|
+
against the current screenshot before prompt construction.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import io
|
|
28
|
+
|
|
29
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
30
|
+
|
|
31
|
+
logger = get_logger(__name__)
|
|
32
|
+
|
|
33
|
+
#: Grid width of the dHash (the hash carries HASH_SIZE * HASH_SIZE bits).
|
|
34
|
+
HASH_SIZE = 8
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def dhash_hex(image_bytes: bytes | None, hash_size: int = HASH_SIZE) -> str | None:
|
|
38
|
+
"""64-bit dHash of an encoded image, as a 16-char hex string.
|
|
39
|
+
|
|
40
|
+
Best-effort: returns ``None`` on any failure (missing/corrupt bytes,
|
|
41
|
+
Pillow unavailable) — perceptual hashing is an enhancement, never a
|
|
42
|
+
dependency.
|
|
43
|
+
"""
|
|
44
|
+
if not image_bytes:
|
|
45
|
+
return None
|
|
46
|
+
try:
|
|
47
|
+
from PIL import Image
|
|
48
|
+
|
|
49
|
+
with Image.open(io.BytesIO(image_bytes)) as img:
|
|
50
|
+
# JPEG fast-path: DCT-scaled draft decode keeps the sync cost of
|
|
51
|
+
# hashing at record time to a few milliseconds. The draft is only
|
|
52
|
+
# an intermediate resolution; the final resize below fixes the
|
|
53
|
+
# grid, so hashes stay comparable across code paths.
|
|
54
|
+
try:
|
|
55
|
+
img.draft("L", (hash_size * 8, hash_size * 8))
|
|
56
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
57
|
+
# Draft decode is an optimization only; full decode below still works.
|
|
58
|
+
logger.debug(f"dhash draft decode skipped: {exc}", exc_info=True)
|
|
59
|
+
small = img.convert("L").resize((hash_size + 1, hash_size), Image.Resampling.LANCZOS)
|
|
60
|
+
pixels = list(small.getdata())
|
|
61
|
+
bits = 0
|
|
62
|
+
for row in range(hash_size):
|
|
63
|
+
for col in range(hash_size):
|
|
64
|
+
left = pixels[row * (hash_size + 1) + col]
|
|
65
|
+
right = pixels[row * (hash_size + 1) + col + 1]
|
|
66
|
+
bits = (bits << 1) | (1 if left > right else 0)
|
|
67
|
+
return f"{bits:0{hash_size * hash_size // 4}x}"
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.debug(f"dhash_hex failed: {e}")
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def hamming_distance_hex(hash_a: str | None, hash_b: str | None) -> int | None:
|
|
74
|
+
"""Bit-level Hamming distance between two hex hashes; None if incomparable."""
|
|
75
|
+
if not hash_a or not hash_b or len(hash_a) != len(hash_b):
|
|
76
|
+
return None
|
|
77
|
+
try:
|
|
78
|
+
return bin(int(hash_a, 16) ^ int(hash_b, 16)).count("1")
|
|
79
|
+
except ValueError:
|
|
80
|
+
return None
|