android-midscene-automation 0.2.4 → 1.1.1

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.
Files changed (525) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +159 -29
  3. package/artemis-main/.dockerignore +15 -0
  4. package/artemis-main/.env.example +34 -0
  5. package/artemis-main/CONTRIBUTING.md +35 -0
  6. package/artemis-main/Dockerfile +68 -0
  7. package/artemis-main/LICENSE +202 -0
  8. package/artemis-main/Makefile +115 -0
  9. package/artemis-main/README.md +322 -0
  10. package/artemis-main/README_CN.md +320 -0
  11. package/artemis-main/apps/__init__.py +15 -0
  12. package/artemis-main/apps/admin_console/__init__.py +19 -0
  13. package/artemis-main/apps/admin_console/core/__init__.py +13 -0
  14. package/artemis-main/apps/admin_console/core/config.py +43 -0
  15. package/artemis-main/apps/admin_console/core/security.py +178 -0
  16. package/artemis-main/apps/admin_console/core/state.py +243 -0
  17. package/artemis-main/apps/admin_console/database/__init__.py +13 -0
  18. package/artemis-main/apps/admin_console/database/connection.py +60 -0
  19. package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
  20. package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
  21. package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
  22. package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
  23. package/artemis-main/apps/admin_console/index.html +3787 -0
  24. package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
  25. package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
  26. package/artemis-main/apps/admin_console/routers/media.py +198 -0
  27. package/artemis-main/apps/admin_console/routers/replay.py +91 -0
  28. package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
  29. package/artemis-main/apps/admin_console/routers/steps.py +109 -0
  30. package/artemis-main/apps/admin_console/routers/stream.py +56 -0
  31. package/artemis-main/apps/admin_console/routers/system.py +761 -0
  32. package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
  33. package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
  34. package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
  35. package/artemis-main/apps/admin_console/server.py +508 -0
  36. package/artemis-main/apps/admin_console/services/__init__.py +13 -0
  37. package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
  38. package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
  39. package/artemis-main/apps/admin_console/services/media_service.py +623 -0
  40. package/artemis-main/apps/admin_console/services/model_service.py +148 -0
  41. package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
  42. package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
  43. package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
  44. package/artemis-main/apps/showcase_ui/README.md +31 -0
  45. package/artemis-main/apps/showcase_ui/angular.json +132 -0
  46. package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
  47. package/artemis-main/apps/showcase_ui/package.json +39 -0
  48. package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
  49. package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
  50. package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
  51. package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
  52. package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
  53. package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
  54. package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
  55. package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
  56. package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
  57. package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
  58. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
  59. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
  60. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
  61. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
  62. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
  63. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
  64. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
  65. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
  66. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
  67. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
  68. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
  69. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
  70. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
  71. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
  72. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
  73. package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
  74. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
  75. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
  76. package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
  77. package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
  78. package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
  79. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
  80. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
  81. package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
  82. package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
  83. package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
  84. package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
  85. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
  86. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
  87. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
  88. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
  89. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
  90. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
  91. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
  92. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
  93. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
  94. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
  95. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
  96. package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
  97. package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
  98. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
  99. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
  100. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
  101. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
  102. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
  103. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
  104. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
  105. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
  106. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
  107. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
  108. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
  109. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
  110. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
  111. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
  112. package/artemis-main/apps/showcase_ui/src/index.html +37 -0
  113. package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
  114. package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
  115. package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
  116. package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
  117. package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
  118. package/artemis-main/artemis/__init__.py +44 -0
  119. package/artemis-main/artemis/__main__.py +18 -0
  120. package/artemis-main/artemis/_legacy_imports.py +94 -0
  121. package/artemis-main/artemis/_version.py +30 -0
  122. package/artemis-main/artemis/agents/__init__.py +17 -0
  123. package/artemis-main/artemis/agents/checker/checker.json +9 -0
  124. package/artemis-main/artemis/agents/checker/checker.py +859 -0
  125. package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
  126. package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
  127. package/artemis-main/artemis/agents/explorer/constants.py +52 -0
  128. package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
  129. package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
  130. package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
  131. package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
  132. package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
  133. package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
  134. package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
  135. package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
  136. package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
  137. package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
  138. package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
  139. package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
  140. package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
  141. package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
  142. package/artemis-main/artemis/agents/flash/runner.py +1167 -0
  143. package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
  144. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
  145. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
  146. package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
  147. package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
  148. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
  149. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
  150. package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
  151. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
  152. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
  153. package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
  154. package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
  155. package/artemis-main/artemis/agents/operator/operator.json +3 -0
  156. package/artemis-main/artemis/agents/operator/operator.py +1394 -0
  157. package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
  158. package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
  159. package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
  160. package/artemis-main/artemis/agents/planner/planner.json +32 -0
  161. package/artemis-main/artemis/agents/planner/planner.py +436 -0
  162. package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
  163. package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
  164. package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
  165. package/artemis-main/artemis/agents/validator/categories.py +33 -0
  166. package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
  167. package/artemis-main/artemis/agents/validator/incidents.py +120 -0
  168. package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
  169. package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
  170. package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
  171. package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
  172. package/artemis-main/artemis/agents/validator/validator.py +188 -0
  173. package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
  174. package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
  175. package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
  176. package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
  177. package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
  178. package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
  179. package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
  180. package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
  181. package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
  182. package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
  183. package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
  184. package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
  185. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
  186. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
  187. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
  188. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
  189. package/artemis-main/artemis/clients/accessibility_client.py +438 -0
  190. package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
  191. package/artemis-main/artemis/config/__init__.py +309 -0
  192. package/artemis-main/artemis/config/agent.py +1098 -0
  193. package/artemis-main/artemis/config/constants.py +151 -0
  194. package/artemis-main/artemis/config/core/__init__.py +24 -0
  195. package/artemis-main/artemis/config/core/engine.py +118 -0
  196. package/artemis-main/artemis/config/core/interpolator.py +78 -0
  197. package/artemis-main/artemis/config/llm.py +267 -0
  198. package/artemis-main/artemis/config/paths.py +244 -0
  199. package/artemis-main/artemis/config/runtime.py +197 -0
  200. package/artemis-main/artemis/config/settings.py +315 -0
  201. package/artemis-main/artemis/constants.py +47 -0
  202. package/artemis-main/artemis/context.py +289 -0
  203. package/artemis-main/artemis/controllers/__init__.py +41 -0
  204. package/artemis-main/artemis/controllers/controller_factory.py +41 -0
  205. package/artemis-main/artemis/controllers/device_controller.py +29 -0
  206. package/artemis-main/artemis/controllers/types.py +37 -0
  207. package/artemis-main/artemis/controllers/unified_controller.py +649 -0
  208. package/artemis-main/artemis/core/__init__.py +21 -0
  209. package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
  210. package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
  211. package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
  212. package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
  213. package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
  214. package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
  215. package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
  216. package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
  217. package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
  218. package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
  219. package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
  220. package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
  221. package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
  222. package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
  223. package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
  224. package/artemis-main/artemis/core/tool_declaration.py +43 -0
  225. package/artemis-main/artemis/core/tool_failure.py +56 -0
  226. package/artemis-main/artemis/data_engine/context_vars.py +24 -0
  227. package/artemis-main/artemis/data_engine/engine.py +1615 -0
  228. package/artemis-main/artemis/data_engine/history_reader.py +142 -0
  229. package/artemis-main/artemis/data_engine/models.py +131 -0
  230. package/artemis-main/artemis/data_engine/storage.py +1245 -0
  231. package/artemis-main/artemis/data_engine/trace.py +727 -0
  232. package/artemis-main/artemis/drivers/__init__.py +48 -0
  233. package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
  234. package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
  235. package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
  236. package/artemis-main/artemis/drivers/android/recorder.py +57 -0
  237. package/artemis-main/artemis/drivers/base.py +290 -0
  238. package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
  239. package/artemis-main/artemis/drivers/factory.py +80 -0
  240. package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
  241. package/artemis-main/artemis/drivers/types.py +67 -0
  242. package/artemis-main/artemis/graph/checkpoints.py +1243 -0
  243. package/artemis-main/artemis/graph/graph.py +1055 -0
  244. package/artemis-main/artemis/graph/perception.py +234 -0
  245. package/artemis-main/artemis/graph/state.py +151 -0
  246. package/artemis-main/artemis/graph/visibility.py +238 -0
  247. package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
  248. package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
  249. package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
  250. package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
  251. package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
  252. package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
  253. package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
  254. package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
  255. package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
  256. package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
  257. package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
  258. package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
  259. package/artemis-main/artemis/interfaces/cli/main.py +109 -0
  260. package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
  261. package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
  262. package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
  263. package/artemis-main/artemis/llm/__init__.py +25 -0
  264. package/artemis-main/artemis/llm/fake_model.py +179 -0
  265. package/artemis-main/artemis/llm/google/__init__.py +57 -0
  266. package/artemis-main/artemis/llm/google/provider.py +185 -0
  267. package/artemis-main/artemis/llm/google/usage.py +148 -0
  268. package/artemis-main/artemis/llm/parser.py +32 -0
  269. package/artemis-main/artemis/llm/reliability.py +267 -0
  270. package/artemis-main/artemis/llm/router.py +389 -0
  271. package/artemis-main/artemis/llm/schemas.py +36 -0
  272. package/artemis-main/artemis/llm/structured.py +196 -0
  273. package/artemis-main/artemis/main.py +58 -0
  274. package/artemis-main/artemis/mcp/__init__.py +20 -0
  275. package/artemis-main/artemis/mcp/action_executor.py +760 -0
  276. package/artemis-main/artemis/mcp/action_manifest.py +320 -0
  277. package/artemis-main/artemis/mcp/action_names.py +172 -0
  278. package/artemis-main/artemis/mcp/action_server.py +202 -0
  279. package/artemis-main/artemis/mcp/action_session.py +245 -0
  280. package/artemis-main/artemis/mcp/action_specs.py +868 -0
  281. package/artemis-main/artemis/mcp/action_types.py +86 -0
  282. package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
  283. package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
  284. package/artemis-main/artemis/mcp/actuators/base.py +113 -0
  285. package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
  286. package/artemis-main/artemis/mcp/adb_server.py +490 -0
  287. package/artemis-main/artemis/mcp/observation.py +133 -0
  288. package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
  289. package/artemis-main/artemis/memory/__init__.py +90 -0
  290. package/artemis-main/artemis/memory/chunking.py +1843 -0
  291. package/artemis-main/artemis/memory/context_policy.py +226 -0
  292. package/artemis-main/artemis/memory/step_capsule.md +73 -0
  293. package/artemis-main/artemis/memory/step_memory.py +281 -0
  294. package/artemis-main/artemis/memory/transcript.py +798 -0
  295. package/artemis-main/artemis/platform/__init__.py +61 -0
  296. package/artemis-main/artemis/platform/base.py +104 -0
  297. package/artemis-main/artemis/platform/darwin.py +184 -0
  298. package/artemis-main/artemis/platform/linux.py +207 -0
  299. package/artemis-main/artemis/platform/windows.py +193 -0
  300. package/artemis-main/artemis/resources/__init__.py +46 -0
  301. package/artemis-main/artemis/runtime/__init__.py +108 -0
  302. package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
  303. package/artemis-main/artemis/runtime/awake_lease.py +283 -0
  304. package/artemis-main/artemis/runtime/awake_service.py +343 -0
  305. package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
  306. package/artemis-main/artemis/runtime/daemon_client.py +422 -0
  307. package/artemis-main/artemis/runtime/device_lock.py +1013 -0
  308. package/artemis-main/artemis/runtime/device_pool.py +478 -0
  309. package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
  310. package/artemis-main/artemis/runtime/process_probe.py +98 -0
  311. package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
  312. package/artemis-main/artemis/runtime/supervisor.py +121 -0
  313. package/artemis-main/artemis/runtime/trace_store.py +375 -0
  314. package/artemis-main/artemis/sdk/__init__.py +26 -0
  315. package/artemis-main/artemis/sdk/agent.py +485 -0
  316. package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
  317. package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
  318. package/artemis-main/artemis/sdk/constants.py +15 -0
  319. package/artemis-main/artemis/sdk/run_outcome.py +58 -0
  320. package/artemis-main/artemis/sdk/types/__init__.py +61 -0
  321. package/artemis-main/artemis/sdk/types/agent.py +89 -0
  322. package/artemis-main/artemis/sdk/utils.py +28 -0
  323. package/artemis-main/artemis/services/llm.py +1040 -0
  324. package/artemis-main/artemis/services/token_meter.py +267 -0
  325. package/artemis-main/artemis/telemetry/__init__.py +35 -0
  326. package/artemis-main/artemis/telemetry/models.py +64 -0
  327. package/artemis-main/artemis/telemetry/replay.py +34 -0
  328. package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
  329. package/artemis-main/artemis/telemetry/tracer.py +85 -0
  330. package/artemis-main/artemis/toolchain/__init__.py +52 -0
  331. package/artemis-main/artemis/toolchain/descriptors.py +92 -0
  332. package/artemis-main/artemis/toolchain/resolver.py +185 -0
  333. package/artemis-main/artemis/tools/base.py +232 -0
  334. package/artemis-main/artemis/tools/command_tool.py +1240 -0
  335. package/artemis-main/artemis/tools/committee_prompts.json +5 -0
  336. package/artemis-main/artemis/tools/committee_tool.py +422 -0
  337. package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
  338. package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
  339. package/artemis-main/artemis/tools/explorer_tool.py +581 -0
  340. package/artemis-main/artemis/tools/history/__init__.py +398 -0
  341. package/artemis-main/artemis/tools/history/replay.py +120 -0
  342. package/artemis-main/artemis/tools/history/screenshot.py +139 -0
  343. package/artemis-main/artemis/tools/history/search.py +563 -0
  344. package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
  345. package/artemis-main/artemis/tools/index.py +47 -0
  346. package/artemis-main/artemis/tools/log_tool.py +108 -0
  347. package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
  348. package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
  349. package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
  350. package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
  351. package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
  352. package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
  353. package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
  354. package/artemis-main/artemis/tools/scratchpad.py +819 -0
  355. package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
  356. package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
  357. package/artemis-main/artemis/tools/video_tool.py +261 -0
  358. package/artemis-main/artemis/tools/wait_tool.py +97 -0
  359. package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
  360. package/artemis-main/artemis/utils/coordinates.py +606 -0
  361. package/artemis-main/artemis/utils/credentials_validator.py +201 -0
  362. package/artemis-main/artemis/utils/cv_canvas.py +189 -0
  363. package/artemis-main/artemis/utils/cython_compat.py +45 -0
  364. package/artemis-main/artemis/utils/element_hit_test.py +80 -0
  365. package/artemis-main/artemis/utils/image_diff.py +335 -0
  366. package/artemis-main/artemis/utils/image_hash.py +80 -0
  367. package/artemis-main/artemis/utils/notes.py +470 -0
  368. package/artemis-main/artemis/utils/ocr_api.py +105 -0
  369. package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
  370. package/artemis-main/artemis/utils/plan_grammar.py +513 -0
  371. package/artemis-main/artemis/utils/python_executor.py +213 -0
  372. package/artemis-main/artemis/utils/startup_progress.py +60 -0
  373. package/artemis-main/artemis/utils/task_tree.py +1174 -0
  374. package/artemis-main/artemis/utils/text.py +37 -0
  375. package/artemis-main/artemis/utils/ui_filter.py +651 -0
  376. package/artemis-main/artemis/utils/video.py +685 -0
  377. package/artemis-main/artemis/utils/visualization.py +802 -0
  378. package/artemis-main/config/artemis.example.jsonc +12 -0
  379. package/artemis-main/mcp_server/README.md +111 -0
  380. package/artemis-main/mcp_server/__init__.py +21 -0
  381. package/artemis-main/mcp_server/__main__.py +20 -0
  382. package/artemis-main/mcp_server/background/__init__.py +15 -0
  383. package/artemis-main/mcp_server/background/task_runner.py +453 -0
  384. package/artemis-main/mcp_server/base.py +49 -0
  385. package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
  386. package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
  387. package/artemis-main/mcp_server/notifiers/base.py +45 -0
  388. package/artemis-main/mcp_server/notifiers/composite.py +87 -0
  389. package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
  390. package/artemis-main/mcp_server/notifiers/file.py +69 -0
  391. package/artemis-main/mcp_server/notifiers/script.py +90 -0
  392. package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
  393. package/artemis-main/mcp_server/rules.md +68 -0
  394. package/artemis-main/mcp_server/server.py +106 -0
  395. package/artemis-main/mcp_server/tools/__init__.py +29 -0
  396. package/artemis-main/mcp_server/tools/device_state.py +107 -0
  397. package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
  398. package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
  399. package/artemis-main/mcp_server/tools/task_manager.py +627 -0
  400. package/artemis-main/mcp_server/tools/task_runner.py +548 -0
  401. package/artemis-main/mcp_server/utils/__init__.py +15 -0
  402. package/artemis-main/mcp_server/utils/device_utils.py +180 -0
  403. package/artemis-main/mcp_server/utils/env_utils.py +65 -0
  404. package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
  405. package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
  406. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
  407. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
  408. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
  409. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
  410. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
  411. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
  412. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
  413. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
  414. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
  415. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
  416. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
  417. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
  418. package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
  419. package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
  420. package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
  421. package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
  422. package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
  423. package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
  424. package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
  425. package/artemis-main/packages/artemis-client/README.md +112 -0
  426. package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
  427. package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
  428. package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
  429. package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
  430. package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
  431. package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
  432. package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
  433. package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
  434. package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
  435. package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
  436. package/artemis-main/pyproject.toml +259 -0
  437. package/artemis-main/pyright-core.json +17 -0
  438. package/artemis-main/scripts/check_dependency_sources.py +68 -0
  439. package/artemis-main/scripts/install_deps.ps1 +520 -0
  440. package/artemis-main/scripts/install_deps.sh +533 -0
  441. package/artemis-main/scripts/quality_ratchet.py +97 -0
  442. package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
  443. package/artemis-main/scripts/run_edge_case_part.py +327 -0
  444. package/artemis-main/scripts/run_ui.py +242 -0
  445. package/artemis-main/scripts/start.ps1 +500 -0
  446. package/artemis-main/scripts/test_all_edge_cases.py +269 -0
  447. package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
  448. package/artemis-main/setup.py +164 -0
  449. package/artemis-main/start.bat +35 -0
  450. package/artemis-main/start.sh +423 -0
  451. package/artemis-main/third_party/mobile_use/LICENSE +190 -0
  452. package/artemis-main/third_party/mobile_use/METADATA +27 -0
  453. package/artemis-main/third_party/mobile_use/NOTICE +12 -0
  454. package/artemis-main/third_party/mobile_use/__init__.py +19 -0
  455. package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
  456. package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
  457. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
  458. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
  459. package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
  460. package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
  461. package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
  462. package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
  463. package/artemis-main/third_party/mobile_use/config/output.py +184 -0
  464. package/artemis-main/third_party/mobile_use/context.py +119 -0
  465. package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
  466. package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
  467. package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
  468. package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
  469. package/artemis-main/third_party/mobile_use/graph.py +54 -0
  470. package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
  471. package/artemis-main/third_party/mobile_use/main.py +130 -0
  472. package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
  473. package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
  474. package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
  475. package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
  476. package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
  477. package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
  478. package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
  479. package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
  480. package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
  481. package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
  482. package/artemis-main/third_party/mobile_use/testing.py +59 -0
  483. package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
  484. package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
  485. package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
  486. package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
  487. package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
  488. package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
  489. package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
  490. package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
  491. package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
  492. package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
  493. package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
  494. package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
  495. package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
  496. package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
  497. package/artemis-main/uv.lock +4908 -0
  498. package/bin/android-midscene-automation.js +1 -5
  499. package/bin/artemis-config.mjs +15 -0
  500. package/bin/artemis-embedded.py +56 -0
  501. package/bin/start-artemis.mjs +30 -0
  502. package/bin/start-dev.mjs +112 -0
  503. package/bin/test_artemis_embedded.py +44 -0
  504. package/package.json +4 -2
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +12 -0
  507. package/src/App.vue +10 -5
  508. package/src/api.ts +5 -0
  509. package/src/appium-recorder/AppiumPage.vue +1 -1
  510. package/src/appium-recorder/components/ComponentInspector.vue +1 -1
  511. package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
  512. package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
  513. package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
  514. package/src/appium-recorder/flow-appearance.ts +4 -4
  515. package/src/components/AppSidebar.vue +8 -7
  516. package/src/components/MidsceneMaintenanceNotice.vue +17 -0
  517. package/src/components/analytics/AnalysisCharts.vue +4 -4
  518. package/src/components/analytics/ReportAnalysis.vue +1 -1
  519. package/src/components/generator/TestCaseFileUpload.vue +1 -1
  520. package/src/components/help/HelpCenter.vue +56 -37
  521. package/src/pages/AiGeneratorPage.vue +1 -1
  522. package/src/pages/ArtemisPage.vue +72 -0
  523. package/src/pages/ConfigPage.vue +18 -19
  524. package/src/style.css +340 -121
  525. 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] = []