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,649 @@
|
|
|
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 os
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import signal
|
|
19
|
+
import subprocess
|
|
20
|
+
import tempfile
|
|
21
|
+
import time
|
|
22
|
+
from typing import Any
|
|
23
|
+
from uuid import uuid4
|
|
24
|
+
|
|
25
|
+
from artemis.config.paths import get_temp_dir
|
|
26
|
+
from artemis.context import ArtemisContext
|
|
27
|
+
from artemis.drivers.factory import get_driver
|
|
28
|
+
from artemis.utils.video import (
|
|
29
|
+
ANDROID_RECORDING_SEGMENT_SECONDS,
|
|
30
|
+
await_scrcpy_first_frame,
|
|
31
|
+
build_scrcpy_record_command,
|
|
32
|
+
get_android_display_state,
|
|
33
|
+
remux_recording_to_mp4,
|
|
34
|
+
render_timeline_clip,
|
|
35
|
+
write_recording_manifest,
|
|
36
|
+
)
|
|
37
|
+
from third_party.mobile_use.controllers.unified_controller import UnifiedMobileControllerBase
|
|
38
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
39
|
+
from third_party.mobile_use.utils.video import (
|
|
40
|
+
DEFAULT_MAX_DURATION_SECONDS,
|
|
41
|
+
RecordingSession,
|
|
42
|
+
VideoRecordingResult,
|
|
43
|
+
abort_recording,
|
|
44
|
+
get_active_session,
|
|
45
|
+
no_active_recording,
|
|
46
|
+
recording_already_active,
|
|
47
|
+
remove_active_session,
|
|
48
|
+
set_active_session,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
logger = get_logger(__name__)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class UnifiedMobileController(UnifiedMobileControllerBase):
|
|
55
|
+
def __init__(self, ctx: ArtemisContext):
|
|
56
|
+
super().__init__(ctx, get_driver(ctx))
|
|
57
|
+
self._segment_cache: dict[tuple[str, int, float, float], VideoRecordingResult] = {}
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
async def _spawn_scrcpy(command: list[str]) -> asyncio.subprocess.Process:
|
|
61
|
+
kwargs: dict[str, Any] = {
|
|
62
|
+
"stdout": asyncio.subprocess.PIPE,
|
|
63
|
+
"stderr": asyncio.subprocess.PIPE,
|
|
64
|
+
}
|
|
65
|
+
if os.name == "nt":
|
|
66
|
+
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
67
|
+
return await asyncio.create_subprocess_exec(*command, **kwargs)
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
async def _stop_scrcpy(process: asyncio.subprocess.Process) -> None:
|
|
71
|
+
"""Ask scrcpy to flush its recorder before falling back to termination."""
|
|
72
|
+
if process.returncode is not None:
|
|
73
|
+
return
|
|
74
|
+
try:
|
|
75
|
+
process.send_signal(signal.CTRL_BREAK_EVENT if os.name == "nt" else signal.SIGINT)
|
|
76
|
+
await asyncio.wait_for(process.wait(), timeout=8.0)
|
|
77
|
+
except (ProcessLookupError, TimeoutError):
|
|
78
|
+
if process.returncode is None:
|
|
79
|
+
process.terminate()
|
|
80
|
+
await asyncio.wait_for(process.wait(), timeout=5.0)
|
|
81
|
+
|
|
82
|
+
def _get_device_id(self) -> str:
|
|
83
|
+
if self.ctx and self.ctx.device and self.ctx.device.device_id:
|
|
84
|
+
return self.ctx.device.device_id
|
|
85
|
+
return getattr(self._driver, "device_id", None) or "default"
|
|
86
|
+
|
|
87
|
+
async def extract_segment_metadata(
|
|
88
|
+
self,
|
|
89
|
+
start_time: float,
|
|
90
|
+
end_time: float | None = None,
|
|
91
|
+
output_path: Path | None = None,
|
|
92
|
+
) -> VideoRecordingResult:
|
|
93
|
+
"""Get a video segment for a specific time range (relative to video start)."""
|
|
94
|
+
device_id = self._get_device_id()
|
|
95
|
+
|
|
96
|
+
# Handle mock driver
|
|
97
|
+
if (
|
|
98
|
+
getattr(self._driver, "is_mock", False)
|
|
99
|
+
or getattr(getattr(self.ctx, "device", None), "mobile_platform", None) == "mock"
|
|
100
|
+
or os.environ.get("ARTEMIS_MOCK_DRIVER") == "1"
|
|
101
|
+
):
|
|
102
|
+
mock_video = Path("/tmp/mock_recording.mp4")
|
|
103
|
+
return VideoRecordingResult(
|
|
104
|
+
success=True,
|
|
105
|
+
video_path=mock_video,
|
|
106
|
+
message="Mock segment extracted",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
session = get_active_session(device_id)
|
|
110
|
+
if not session:
|
|
111
|
+
return no_active_recording(device_id)
|
|
112
|
+
|
|
113
|
+
cache_key = None
|
|
114
|
+
if end_time is not None:
|
|
115
|
+
cache_key = (
|
|
116
|
+
str(session.video_id),
|
|
117
|
+
session.generation,
|
|
118
|
+
round(start_time, 1),
|
|
119
|
+
round(end_time, 1),
|
|
120
|
+
)
|
|
121
|
+
cached_res = self._segment_cache.get(cache_key)
|
|
122
|
+
if (
|
|
123
|
+
cached_res
|
|
124
|
+
and cached_res.success
|
|
125
|
+
and cached_res.video_path
|
|
126
|
+
and cached_res.video_path.exists()
|
|
127
|
+
):
|
|
128
|
+
logger.info(
|
|
129
|
+
"Reusing generation-scoped trimmed video segment for range "
|
|
130
|
+
f"{cache_key[2]}s to {cache_key[3]}s"
|
|
131
|
+
)
|
|
132
|
+
return cached_res
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
mkv_path = session.local_video_path
|
|
136
|
+
if not mkv_path:
|
|
137
|
+
return VideoRecordingResult(
|
|
138
|
+
success=False,
|
|
139
|
+
message="Recording file not found",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
current_time = time.time()
|
|
143
|
+
video_duration = current_time - session.start_time
|
|
144
|
+
|
|
145
|
+
# Strict timeline alignment:
|
|
146
|
+
# DataEngine session start time is the reference T0 (0.0s).
|
|
147
|
+
# session.start_time is the wall-clock time of the first frame in
|
|
148
|
+
# the recording (the file's t=0), estimated at recording start.
|
|
149
|
+
offset = 0.0
|
|
150
|
+
if session.data_engine_start_time is not None:
|
|
151
|
+
offset = max(0.0, session.start_time - session.data_engine_start_time)
|
|
152
|
+
|
|
153
|
+
video_start_relative_time = start_time - offset
|
|
154
|
+
video_end_relative_time = end_time - offset if end_time is not None else None
|
|
155
|
+
|
|
156
|
+
safe_duration = max(0.0, video_duration - 0.5)
|
|
157
|
+
|
|
158
|
+
truncation_warning = None
|
|
159
|
+
if video_end_relative_time is not None and video_end_relative_time > safe_duration:
|
|
160
|
+
truncation_warning = (
|
|
161
|
+
f"Video segment truncated. Requested end time {end_time:.1f}s "
|
|
162
|
+
f"exceeded available safe duration. Truncated to {safe_duration + offset:.1f}s."
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if video_end_relative_time is None or video_end_relative_time > safe_duration:
|
|
166
|
+
video_end_relative_time = safe_duration
|
|
167
|
+
|
|
168
|
+
if video_start_relative_time < 0:
|
|
169
|
+
video_start_relative_time = 0.0
|
|
170
|
+
|
|
171
|
+
if video_start_relative_time >= video_end_relative_time:
|
|
172
|
+
if safe_duration > 0:
|
|
173
|
+
video_end_relative_time = safe_duration
|
|
174
|
+
video_start_relative_time = max(
|
|
175
|
+
0.0,
|
|
176
|
+
safe_duration - ((end_time - start_time) if end_time else 2.0),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if video_start_relative_time >= video_end_relative_time:
|
|
180
|
+
return VideoRecordingResult(
|
|
181
|
+
success=False,
|
|
182
|
+
message=(
|
|
183
|
+
f"Invalid time range or requested too close to current time. Available duration: {safe_duration:.1f}s"
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
trim_output_dir = tempfile.mkdtemp(
|
|
188
|
+
prefix="video_trimmed_",
|
|
189
|
+
dir=get_temp_dir("trimmed_videos"),
|
|
190
|
+
)
|
|
191
|
+
trim_output_path = Path(trim_output_dir) / "segment.mp4"
|
|
192
|
+
|
|
193
|
+
success = False
|
|
194
|
+
for attempt in range(3):
|
|
195
|
+
timeline_segments = [dict(record) for record in session.android_segment_records]
|
|
196
|
+
active_start = session.android_segment_started_at or session.start_time
|
|
197
|
+
if mkv_path.exists():
|
|
198
|
+
timeline_segments.append(
|
|
199
|
+
{
|
|
200
|
+
"path": mkv_path,
|
|
201
|
+
"start": max(0.0, active_start - session.start_time),
|
|
202
|
+
"end": max(0.0, current_time - session.start_time),
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
success = await render_timeline_clip(
|
|
206
|
+
timeline_segments,
|
|
207
|
+
video_start_relative_time,
|
|
208
|
+
video_end_relative_time,
|
|
209
|
+
trim_output_path,
|
|
210
|
+
)
|
|
211
|
+
if success and trim_output_path.exists():
|
|
212
|
+
break
|
|
213
|
+
logger.warning(f"ffmpeg trim attempt {attempt + 1} failed, retrying in 0.5s...")
|
|
214
|
+
await asyncio.sleep(0.5)
|
|
215
|
+
|
|
216
|
+
if not success or not trim_output_path.exists():
|
|
217
|
+
return VideoRecordingResult(
|
|
218
|
+
success=False,
|
|
219
|
+
message="Failed to trim video segment after retries",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
actual_start = video_start_relative_time + offset
|
|
223
|
+
actual_end = video_end_relative_time + offset
|
|
224
|
+
file_size_mb = trim_output_path.stat().st_size / (1024 * 1024)
|
|
225
|
+
duration = video_end_relative_time - video_start_relative_time
|
|
226
|
+
|
|
227
|
+
message = f"Video segment retrieved for range {actual_start:.1f}s to {actual_end:.1f}s"
|
|
228
|
+
res = VideoRecordingResult(
|
|
229
|
+
success=True,
|
|
230
|
+
message=message,
|
|
231
|
+
video_path=trim_output_path,
|
|
232
|
+
file_size_mb=round(file_size_mb, 2),
|
|
233
|
+
duration_seconds=round(duration, 2),
|
|
234
|
+
actual_start_relative_time=actual_start,
|
|
235
|
+
warning=truncation_warning,
|
|
236
|
+
video_id=session.video_id,
|
|
237
|
+
generation=session.generation,
|
|
238
|
+
sealed_until=session.sealed_until,
|
|
239
|
+
source_revision=(f"{session.video_id}:{session.generation}:{round(actual_end, 3)}"),
|
|
240
|
+
)
|
|
241
|
+
if cache_key is not None:
|
|
242
|
+
self._segment_cache[cache_key] = res
|
|
243
|
+
return res
|
|
244
|
+
|
|
245
|
+
except Exception as e:
|
|
246
|
+
logger.error(f"Failed to get video segment: {e}")
|
|
247
|
+
return VideoRecordingResult(
|
|
248
|
+
success=False,
|
|
249
|
+
message=f"Failed to get video segment: {e}",
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
@staticmethod
|
|
253
|
+
def _segment_mp4_path(source_path: Path, index: int) -> Path:
|
|
254
|
+
return source_path.parent / (
|
|
255
|
+
"recording.mp4" if index == 0 else f"recording_{index:03d}.mp4"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
@staticmethod
|
|
259
|
+
async def _remux_segment_record(record: dict[str, Any]) -> bool:
|
|
260
|
+
source_path = Path(record["path"])
|
|
261
|
+
output_path = Path(record["output_path"])
|
|
262
|
+
success = await remux_recording_to_mp4(source_path, output_path)
|
|
263
|
+
if success:
|
|
264
|
+
record["path"] = output_path
|
|
265
|
+
try:
|
|
266
|
+
source_path.unlink()
|
|
267
|
+
except OSError:
|
|
268
|
+
pass
|
|
269
|
+
return success
|
|
270
|
+
|
|
271
|
+
def _finalize_current_segment(self, session: RecordingSession, end_time: float) -> None:
|
|
272
|
+
source_path = session.local_video_path
|
|
273
|
+
if not source_path or not source_path.exists():
|
|
274
|
+
return
|
|
275
|
+
if any(Path(record["path"]) == source_path for record in session.android_segment_records):
|
|
276
|
+
return
|
|
277
|
+
start_time = session.android_segment_started_at or session.start_time
|
|
278
|
+
output_path = self._segment_mp4_path(source_path, session.android_segment_index)
|
|
279
|
+
session.android_video_segments.append(source_path)
|
|
280
|
+
session.android_segment_records.append(
|
|
281
|
+
{
|
|
282
|
+
"path": source_path,
|
|
283
|
+
"output_path": output_path,
|
|
284
|
+
"start": max(0.0, start_time - session.start_time),
|
|
285
|
+
"end": max(0.0, end_time - session.start_time),
|
|
286
|
+
"rotation": session.android_rotation,
|
|
287
|
+
"generation": session.generation,
|
|
288
|
+
"conversion_scheduled": True,
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
session.android_conversion_tasks.append(
|
|
292
|
+
asyncio.create_task(self._remux_segment_record(session.android_segment_records[-1]))
|
|
293
|
+
)
|
|
294
|
+
session.sealed_until = max(
|
|
295
|
+
session.sealed_until,
|
|
296
|
+
max(0.0, end_time - session.start_time),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
@staticmethod
|
|
300
|
+
def _segment_session_offsets(
|
|
301
|
+
session: RecordingSession, mp4_paths: list[Path]
|
|
302
|
+
) -> dict[Path, float]:
|
|
303
|
+
"""Map each finalized segment MP4 to its first-frame offset from session start.
|
|
304
|
+
|
|
305
|
+
``record["start"]`` is relative to the recording's own t=0 (the first
|
|
306
|
+
frame of the first segment); shifting it by the recording/DataEngine
|
|
307
|
+
anchor difference yields the session-relative offset the UI needs to
|
|
308
|
+
align step timestamps with the playlist.
|
|
309
|
+
"""
|
|
310
|
+
anchor = session.data_engine_start_time
|
|
311
|
+
recording_shift = (session.start_time - anchor) if anchor is not None else 0.0
|
|
312
|
+
offsets: dict[Path, float] = {}
|
|
313
|
+
for record in session.android_segment_records:
|
|
314
|
+
output_path = Path(record.get("output_path") or record.get("path") or "")
|
|
315
|
+
if not output_path.name:
|
|
316
|
+
continue
|
|
317
|
+
offsets[output_path] = max(0.0, float(record.get("start", 0.0)) + recording_shift)
|
|
318
|
+
for path in mp4_paths:
|
|
319
|
+
# Emergency fallback remux of the live file has no record; it is
|
|
320
|
+
# the whole recording, so it starts at the recording anchor.
|
|
321
|
+
offsets.setdefault(path, max(0.0, recording_shift))
|
|
322
|
+
return offsets
|
|
323
|
+
|
|
324
|
+
def _record_recording_failure(self, session: RecordingSession, message: str) -> None:
|
|
325
|
+
if self.ctx and self.ctx.data_engine:
|
|
326
|
+
self.ctx.data_engine.record_video_failure(
|
|
327
|
+
video_id=session.video_id,
|
|
328
|
+
device_id=session.device_id,
|
|
329
|
+
local_video_path=session.local_video_path,
|
|
330
|
+
start_time=session.start_time,
|
|
331
|
+
error=message,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
async def _start_next_recording_segment(
|
|
335
|
+
self, session: RecordingSession, display_state: tuple[int, int, int] | None
|
|
336
|
+
) -> bool:
|
|
337
|
+
session.android_segment_index += 1
|
|
338
|
+
session.generation = session.android_segment_index
|
|
339
|
+
output_dir = session.local_video_path.parent
|
|
340
|
+
new_video_path = output_dir / f"recording_{session.android_segment_index:03d}.mkv"
|
|
341
|
+
process = await self._spawn_scrcpy(
|
|
342
|
+
build_scrcpy_record_command("scrcpy", session.device_id, new_video_path)
|
|
343
|
+
)
|
|
344
|
+
spawned_at = time.time()
|
|
345
|
+
first_frame_at = await await_scrcpy_first_frame(process, spawned_at)
|
|
346
|
+
if process.returncode is not None:
|
|
347
|
+
stderr = await process.stderr.read()
|
|
348
|
+
logger.error(f"Failed to start scrcpy segment: {stderr.decode(errors='replace')}")
|
|
349
|
+
return False
|
|
350
|
+
session.process = process
|
|
351
|
+
session.local_video_path = new_video_path
|
|
352
|
+
session.android_segment_started_at = first_frame_at
|
|
353
|
+
logger.info(
|
|
354
|
+
f"scrcpy segment {session.android_segment_index} first frame estimated "
|
|
355
|
+
f"{first_frame_at - spawned_at:.2f}s after spawn"
|
|
356
|
+
)
|
|
357
|
+
if display_state:
|
|
358
|
+
session.android_rotation, session.capture_width, session.capture_height = display_state
|
|
359
|
+
return True
|
|
360
|
+
|
|
361
|
+
async def _recording_watchdog(self, device_id: str) -> None:
|
|
362
|
+
"""Roll fixed-orientation segments and recover scrcpy crashes."""
|
|
363
|
+
try:
|
|
364
|
+
while True:
|
|
365
|
+
await asyncio.sleep(0.5)
|
|
366
|
+
session = get_active_session(device_id)
|
|
367
|
+
if not session or not session.is_active or not session.process:
|
|
368
|
+
return
|
|
369
|
+
now = time.time()
|
|
370
|
+
display_state = await get_android_display_state(device_id)
|
|
371
|
+
rotated = bool(
|
|
372
|
+
display_state
|
|
373
|
+
and session.android_rotation is not None
|
|
374
|
+
and display_state[0] != session.android_rotation
|
|
375
|
+
)
|
|
376
|
+
segment_age = now - (session.android_segment_started_at or session.start_time)
|
|
377
|
+
crashed = session.process.returncode is not None
|
|
378
|
+
if not crashed and not rotated and segment_age < ANDROID_RECORDING_SEGMENT_SECONDS:
|
|
379
|
+
continue
|
|
380
|
+
|
|
381
|
+
if not crashed:
|
|
382
|
+
await self._stop_scrcpy(session.process)
|
|
383
|
+
self._finalize_current_segment(session, time.time())
|
|
384
|
+
reason = (
|
|
385
|
+
"rotation" if rotated else "time limit" if not crashed else "recorder crash"
|
|
386
|
+
)
|
|
387
|
+
logger.info(f"Rolling screen recording segment after {reason}")
|
|
388
|
+
if not await self._start_next_recording_segment(session, display_state):
|
|
389
|
+
return
|
|
390
|
+
except asyncio.CancelledError:
|
|
391
|
+
return
|
|
392
|
+
except Exception as e:
|
|
393
|
+
logger.error(f"Error in recording supervisor for {device_id}: {e}")
|
|
394
|
+
|
|
395
|
+
async def start_video_recording(
|
|
396
|
+
self,
|
|
397
|
+
output_dir: Path | None = None,
|
|
398
|
+
max_duration_seconds: int = DEFAULT_MAX_DURATION_SECONDS,
|
|
399
|
+
) -> VideoRecordingResult:
|
|
400
|
+
"""Start screen recording on Android device using scrcpy."""
|
|
401
|
+
self._segment_cache.clear()
|
|
402
|
+
device_id = self._get_device_id()
|
|
403
|
+
|
|
404
|
+
# Check mock driver first
|
|
405
|
+
if (
|
|
406
|
+
getattr(self._driver, "is_mock", False)
|
|
407
|
+
or getattr(getattr(self.ctx, "device", None), "mobile_platform", None) == "mock"
|
|
408
|
+
or os.environ.get("ARTEMIS_MOCK_DRIVER") == "1"
|
|
409
|
+
):
|
|
410
|
+
await self._driver.start_video_recording(output_dir)
|
|
411
|
+
return VideoRecordingResult(success=True, message="Mock recording started")
|
|
412
|
+
|
|
413
|
+
if already_active := recording_already_active(device_id):
|
|
414
|
+
return already_active
|
|
415
|
+
|
|
416
|
+
try:
|
|
417
|
+
if not output_dir:
|
|
418
|
+
output_dir = Path(
|
|
419
|
+
tempfile.mkdtemp(prefix="scrcpy_", dir=get_temp_dir("recordings"))
|
|
420
|
+
)
|
|
421
|
+
else:
|
|
422
|
+
output_dir = Path(output_dir)
|
|
423
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
424
|
+
|
|
425
|
+
local_video_path = output_dir / "recording.mkv"
|
|
426
|
+
video_id = uuid4()
|
|
427
|
+
start_time = time.time()
|
|
428
|
+
display_state = await get_android_display_state(device_id)
|
|
429
|
+
data_engine_start_time = (
|
|
430
|
+
self.ctx.data_engine.session_start_time
|
|
431
|
+
if (self.ctx and self.ctx.data_engine)
|
|
432
|
+
else start_time
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
session = RecordingSession(
|
|
436
|
+
video_id=video_id,
|
|
437
|
+
device_id=device_id,
|
|
438
|
+
start_time=start_time,
|
|
439
|
+
data_engine_start_time=data_engine_start_time,
|
|
440
|
+
local_video_path=local_video_path,
|
|
441
|
+
capture_width=getattr(getattr(self.ctx, "device", None), "device_width", None),
|
|
442
|
+
capture_height=getattr(getattr(self.ctx, "device", None), "device_height", None),
|
|
443
|
+
android_rotation=display_state[0] if display_state else None,
|
|
444
|
+
android_segment_started_at=start_time,
|
|
445
|
+
is_active=True,
|
|
446
|
+
)
|
|
447
|
+
if display_state:
|
|
448
|
+
session.capture_width, session.capture_height = display_state[1:]
|
|
449
|
+
|
|
450
|
+
# Start scrcpy in background
|
|
451
|
+
cmd = build_scrcpy_record_command("scrcpy", device_id, local_video_path)
|
|
452
|
+
|
|
453
|
+
process = await self._spawn_scrcpy(cmd)
|
|
454
|
+
spawned_at = time.time()
|
|
455
|
+
|
|
456
|
+
session.process = process
|
|
457
|
+
set_active_session(device_id, session)
|
|
458
|
+
|
|
459
|
+
logger.info(f"Started scrcpy recording on {device_id}, saving to {local_video_path}")
|
|
460
|
+
|
|
461
|
+
# The file's t=0 is the first captured frame, about a second after
|
|
462
|
+
# spawn. Anchor the recording timeline there: segment offsets and
|
|
463
|
+
# analyzer clip trimming all treat session.start_time as that t=0.
|
|
464
|
+
first_frame_at = await await_scrcpy_first_frame(process, spawned_at)
|
|
465
|
+
if process.returncode is not None:
|
|
466
|
+
stderr = await process.stderr.read()
|
|
467
|
+
err_msg = stderr.decode()
|
|
468
|
+
logger.error(f"scrcpy failed to start on {device_id}: {err_msg}")
|
|
469
|
+
if self.ctx and self.ctx.data_engine:
|
|
470
|
+
self.ctx.data_engine.record_video_start(
|
|
471
|
+
video_id=video_id,
|
|
472
|
+
device_id=device_id,
|
|
473
|
+
local_video_path=local_video_path,
|
|
474
|
+
start_time=session.start_time,
|
|
475
|
+
)
|
|
476
|
+
self._record_recording_failure(session, f"scrcpy failed to start: {err_msg}")
|
|
477
|
+
remove_active_session(device_id)
|
|
478
|
+
return VideoRecordingResult(
|
|
479
|
+
success=False,
|
|
480
|
+
message=f"scrcpy failed to start: {err_msg}",
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
session.start_time = first_frame_at
|
|
484
|
+
session.android_segment_started_at = first_frame_at
|
|
485
|
+
timeline_note = ""
|
|
486
|
+
if session.data_engine_start_time is not None:
|
|
487
|
+
timeline_note = (
|
|
488
|
+
"; recording timeline offset from session start: "
|
|
489
|
+
f"{first_frame_at - session.data_engine_start_time:.2f}s"
|
|
490
|
+
)
|
|
491
|
+
logger.info(
|
|
492
|
+
f"scrcpy first frame estimated {first_frame_at - spawned_at:.2f}s after spawn"
|
|
493
|
+
f"{timeline_note}"
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
if self.ctx and self.ctx.data_engine:
|
|
497
|
+
self.ctx.data_engine.record_video_start(
|
|
498
|
+
video_id=video_id,
|
|
499
|
+
device_id=device_id,
|
|
500
|
+
local_video_path=local_video_path,
|
|
501
|
+
start_time=session.start_time,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
session.watchdog_task = asyncio.create_task(self._recording_watchdog(device_id))
|
|
505
|
+
|
|
506
|
+
return VideoRecordingResult(
|
|
507
|
+
success=True,
|
|
508
|
+
message=f"Recording started on {device_id}",
|
|
509
|
+
video_id=session.video_id,
|
|
510
|
+
generation=session.generation,
|
|
511
|
+
sealed_until=session.sealed_until,
|
|
512
|
+
source_revision=f"{session.video_id}:{session.generation}:active",
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
except Exception as e:
|
|
516
|
+
logger.error(f"Failed to start scrcpy recording: {e}")
|
|
517
|
+
return abort_recording(device_id, "start", e)
|
|
518
|
+
|
|
519
|
+
async def stop_video_recording(self) -> VideoRecordingResult:
|
|
520
|
+
"""Stop scrcpy recording and return the converted MP4 video file."""
|
|
521
|
+
self._segment_cache.clear()
|
|
522
|
+
device_id = self._get_device_id()
|
|
523
|
+
|
|
524
|
+
# Check mock driver first
|
|
525
|
+
if (
|
|
526
|
+
getattr(self._driver, "is_mock", False)
|
|
527
|
+
or getattr(getattr(self.ctx, "device", None), "mobile_platform", None) == "mock"
|
|
528
|
+
or os.environ.get("ARTEMIS_MOCK_DRIVER") == "1"
|
|
529
|
+
):
|
|
530
|
+
p = await self._driver.stop_video_recording()
|
|
531
|
+
return VideoRecordingResult(
|
|
532
|
+
success=True,
|
|
533
|
+
video_path=Path(p) if p else None,
|
|
534
|
+
message="Mock recording stopped",
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
session = get_active_session(device_id)
|
|
538
|
+
if not session:
|
|
539
|
+
return no_active_recording(device_id)
|
|
540
|
+
|
|
541
|
+
# Mark inactive and cancel watchdog task
|
|
542
|
+
session.is_active = False
|
|
543
|
+
if session.watchdog_task and not session.watchdog_task.done():
|
|
544
|
+
session.watchdog_task.cancel()
|
|
545
|
+
try:
|
|
546
|
+
await session.watchdog_task
|
|
547
|
+
except asyncio.CancelledError:
|
|
548
|
+
pass
|
|
549
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
550
|
+
logger.debug(
|
|
551
|
+
f"Recording watchdog for {device_id} ended with an error: {exc}", exc_info=True
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
try:
|
|
555
|
+
process = session.process
|
|
556
|
+
if process is not None:
|
|
557
|
+
try:
|
|
558
|
+
await self._stop_scrcpy(process)
|
|
559
|
+
except Exception as proc_e:
|
|
560
|
+
logger.warning(f"Error terminating scrcpy process: {proc_e}")
|
|
561
|
+
|
|
562
|
+
output_path = session.local_video_path
|
|
563
|
+
has_existing_recording = (output_path and output_path.exists()) or any(
|
|
564
|
+
Path(r.get("output_path", "")).exists() or Path(r.get("path", "")).exists()
|
|
565
|
+
for r in session.android_segment_records
|
|
566
|
+
)
|
|
567
|
+
if not has_existing_recording:
|
|
568
|
+
message = "Recording file not found on disk"
|
|
569
|
+
self._record_recording_failure(session, message)
|
|
570
|
+
remove_active_session(device_id)
|
|
571
|
+
return VideoRecordingResult(
|
|
572
|
+
success=False,
|
|
573
|
+
message=message,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
self._finalize_current_segment(session, time.time())
|
|
577
|
+
for record in session.android_segment_records:
|
|
578
|
+
if not record.get("conversion_scheduled"):
|
|
579
|
+
record["conversion_scheduled"] = True
|
|
580
|
+
session.android_conversion_tasks.append(
|
|
581
|
+
asyncio.create_task(self._remux_segment_record(record))
|
|
582
|
+
)
|
|
583
|
+
if session.android_conversion_tasks:
|
|
584
|
+
await asyncio.gather(*session.android_conversion_tasks, return_exceptions=True)
|
|
585
|
+
mp4_paths = [
|
|
586
|
+
Path(record["output_path"])
|
|
587
|
+
for record in session.android_segment_records
|
|
588
|
+
if Path(record["output_path"]).exists()
|
|
589
|
+
and Path(record["output_path"]).stat().st_size > 0
|
|
590
|
+
]
|
|
591
|
+
|
|
592
|
+
# Emergency fallback: if no segment MP4 was produced, attempt to remux local_video_path directly
|
|
593
|
+
if (
|
|
594
|
+
not mp4_paths
|
|
595
|
+
and output_path
|
|
596
|
+
and output_path.exists()
|
|
597
|
+
and output_path.stat().st_size > 0
|
|
598
|
+
):
|
|
599
|
+
fallback_mp4 = (
|
|
600
|
+
output_path.parent / "recording.mp4"
|
|
601
|
+
if output_path.name != "recording.mp4"
|
|
602
|
+
else output_path.with_name("recording_converted.mp4")
|
|
603
|
+
)
|
|
604
|
+
if await remux_recording_to_mp4(output_path, fallback_mp4):
|
|
605
|
+
mp4_paths.append(fallback_mp4)
|
|
606
|
+
|
|
607
|
+
if not mp4_paths:
|
|
608
|
+
message = (
|
|
609
|
+
"Recording finalization failed; no complete browser-safe video was produced"
|
|
610
|
+
)
|
|
611
|
+
self._record_recording_failure(session, message)
|
|
612
|
+
remove_active_session(device_id)
|
|
613
|
+
return VideoRecordingResult(success=False, message=message)
|
|
614
|
+
|
|
615
|
+
final_video_path = mp4_paths[0]
|
|
616
|
+
output_dir = final_video_path.parent
|
|
617
|
+
manifest_path = await write_recording_manifest(
|
|
618
|
+
output_dir, mp4_paths, self._segment_session_offsets(session, mp4_paths)
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
remove_active_session(device_id)
|
|
622
|
+
|
|
623
|
+
# Persist update to local database if Data Engine is active
|
|
624
|
+
if self.ctx and self.ctx.data_engine:
|
|
625
|
+
self.ctx.data_engine.record_video_stop(
|
|
626
|
+
video_id=session.video_id,
|
|
627
|
+
device_id=device_id,
|
|
628
|
+
local_video_path=final_video_path,
|
|
629
|
+
start_time=session.start_time,
|
|
630
|
+
end_time=time.time(),
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
return VideoRecordingResult(
|
|
634
|
+
success=True,
|
|
635
|
+
message=f"Recording stopped, saved {len(mp4_paths)} video segments",
|
|
636
|
+
video_path=final_video_path,
|
|
637
|
+
video_id=session.video_id,
|
|
638
|
+
generation=session.generation,
|
|
639
|
+
sealed_until=session.sealed_until,
|
|
640
|
+
source_revision=f"{session.video_id}:{session.generation}:ready",
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
except Exception as e:
|
|
644
|
+
logger.error(f"Failed to stop scrcpy recording: {e}")
|
|
645
|
+
self._record_recording_failure(session, str(e))
|
|
646
|
+
return abort_recording(device_id, "stop", e)
|
|
647
|
+
|
|
648
|
+
async def cleanup(self) -> None:
|
|
649
|
+
await self._driver.disconnect()
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
"""Core framework primitives.
|
|
16
|
+
|
|
17
|
+
Live submodules: ``artemis.core.tool_declaration`` and ``artemis.core.diagnostics``
|
|
18
|
+
(imported directly by module path by their consumers).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
__all__: list[str] = []
|