android-midscene-automation 0.2.3 → 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.
Files changed (525) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/README.md +63 -1
  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 +10 -8
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +13 -1
  507. package/src/App.vue +72 -23
  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 +369 -186
  525. package/src/types.ts +1 -1
@@ -0,0 +1,213 @@
1
+ # Copyright 2025-2026 Minitap, Inc.
2
+ # Modifications Copyright 2026 Google LLC
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Originally from mobile-use (https://github.com/minitap-ai/mobile-use).
17
+ # See third_party/mobile_use/METADATA for the upstream source and local modifications.
18
+
19
+ import json
20
+ import os
21
+ from pathlib import Path
22
+ import subprocess
23
+ import tempfile
24
+
25
+ from third_party.mobile_use.utils.logger import get_logger
26
+ from PIL import Image
27
+
28
+ logger = get_logger(__name__)
29
+
30
+ USE_FFMPEG_GIF = os.environ.get("USE_FFMPEG_GIF", "").lower() in (
31
+ "1",
32
+ "true",
33
+ "yes",
34
+ )
35
+
36
+
37
+ def quantize_and_save_gif_from_paths(
38
+ image_paths: list[Path],
39
+ output_path: Path,
40
+ colors: int = 128,
41
+ duration: int = 100,
42
+ ) -> None:
43
+ """Create an optimized GIF from image file paths.
44
+
45
+ By default uses PIL (loads all frames into memory).
46
+ Set USE_FFMPEG_GIF=1 env var to use ffmpeg for memory-efficient streaming.
47
+
48
+ Args:
49
+ image_paths: List of paths to image files (must be sorted in desired
50
+ order)
51
+ output_path: Path where the GIF will be saved
52
+ colors: Number of colors to use in quantization (lower = smaller file)
53
+ duration: Duration of each frame in milliseconds
54
+
55
+ Raises:
56
+ ValueError: If image_paths list is empty
57
+ RuntimeError: If ffmpeg fails (when USE_FFMPEG_GIF is enabled)
58
+ """
59
+ if not image_paths:
60
+ raise ValueError("image_paths must not be empty")
61
+
62
+ if USE_FFMPEG_GIF:
63
+ _save_gif_ffmpeg(image_paths, output_path, colors, duration)
64
+ else:
65
+ _save_gif_pillow(image_paths, output_path, colors, duration)
66
+
67
+
68
+ def _save_gif_pillow(
69
+ image_paths: list[Path],
70
+ output_path: Path,
71
+ colors: int,
72
+ duration: int,
73
+ ) -> None:
74
+ """Create GIF using PIL (loads all frames into memory)."""
75
+
76
+ def frame_generator():
77
+ for path in image_paths:
78
+ with Image.open(path) as img:
79
+ if img.mode != "RGB":
80
+ img = img.convert("RGB")
81
+ yield img.quantize(colors=colors, method=2)
82
+
83
+ frames = frame_generator()
84
+ first_frame = next(frames)
85
+ first_frame.save(
86
+ output_path,
87
+ save_all=True,
88
+ append_images=frames,
89
+ loop=0,
90
+ optimize=True,
91
+ duration=duration,
92
+ )
93
+
94
+
95
+ def _save_gif_ffmpeg(
96
+ image_paths: list[Path],
97
+ output_path: Path,
98
+ colors: int,
99
+ duration: int,
100
+ ) -> None:
101
+ """Create GIF using ffmpeg (memory-efficient streaming)."""
102
+ fps = 1000 / duration
103
+
104
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
105
+ for i, path in enumerate(image_paths):
106
+ f.write(f"file '{path.absolute()}'\n")
107
+ # Last file needs no duration (ffmpeg concat demuxer uses it as the final frame)
108
+ if i < len(image_paths) - 1:
109
+ f.write(f"duration {duration / 1000}\n")
110
+ # Repeat last file to ensure it's included (ffmpeg concat demuxer quirk)
111
+ f.write(f"file '{image_paths[-1].absolute()}'\n")
112
+ concat_file = Path(f.name)
113
+
114
+ palette_path = output_path.with_suffix(".palette.png")
115
+
116
+ try:
117
+ result = subprocess.run(
118
+ [
119
+ "ffmpeg",
120
+ "-y",
121
+ "-f",
122
+ "concat",
123
+ "-safe",
124
+ "0",
125
+ "-i",
126
+ str(concat_file),
127
+ "-vf",
128
+ f"palettegen=max_colors={min(colors, 256)}:stats_mode=diff",
129
+ str(palette_path),
130
+ ],
131
+ capture_output=True,
132
+ text=True,
133
+ )
134
+ if result.returncode != 0:
135
+ raise RuntimeError(f"ffmpeg palette generation failed: {result.stderr}")
136
+
137
+ result = subprocess.run(
138
+ [
139
+ "ffmpeg",
140
+ "-y",
141
+ "-f",
142
+ "concat",
143
+ "-safe",
144
+ "0",
145
+ "-i",
146
+ str(concat_file),
147
+ "-i",
148
+ str(palette_path),
149
+ "-lavfi",
150
+ f"fps={fps},paletteuse=dither=bayer:bayer_scale=5",
151
+ "-loop",
152
+ "0",
153
+ str(output_path),
154
+ ],
155
+ capture_output=True,
156
+ text=True,
157
+ )
158
+ if result.returncode != 0:
159
+ raise RuntimeError(f"ffmpeg GIF creation failed: {result.stderr}")
160
+
161
+ finally:
162
+ concat_file.unlink(missing_ok=True)
163
+ if palette_path.exists():
164
+ palette_path.unlink()
165
+
166
+
167
+ def create_gif_from_trace_folder(trace_folder_path: Path):
168
+ image_files: list[Path] = []
169
+
170
+ for file in trace_folder_path.iterdir():
171
+ if file.suffix == ".jpeg":
172
+ image_files.append(file)
173
+
174
+ image_files.sort(key=lambda f: int(f.stem))
175
+
176
+ logger.info(f"Found {len(image_files)} images to compile")
177
+
178
+ if not image_files:
179
+ return
180
+
181
+ gif_path = trace_folder_path / "trace.gif"
182
+ quantize_and_save_gif_from_paths(image_files, gif_path)
183
+ logger.info(f"GIF created at {gif_path}")
184
+
185
+
186
+ def remove_images_from_trace_folder(trace_folder_path: Path):
187
+ for file in trace_folder_path.iterdir():
188
+ if file.suffix == ".jpeg":
189
+ file.unlink()
190
+
191
+
192
+ def create_steps_json_from_trace_folder(trace_folder_path: Path):
193
+ """Compile legacy timestamp-named step files without consuming manifests."""
194
+ steps = []
195
+ for file in trace_folder_path.iterdir():
196
+ if file.suffix == ".json" and file.stem.isdigit():
197
+ with open(file, encoding="utf-8", errors="ignore") as f:
198
+ json_content = f.read()
199
+ steps.append({"timestamp": int(file.stem), "data": json_content})
200
+
201
+ steps.sort(key=lambda f: f["timestamp"])
202
+
203
+ logger.info(f"Found {len(steps)} steps to compile")
204
+
205
+ with open(trace_folder_path / "steps.json", "w", encoding="utf-8", errors="ignore") as f:
206
+ f.write(json.dumps(steps))
207
+
208
+
209
+ def remove_steps_json_from_trace_folder(trace_folder_path: Path):
210
+ """Remove only legacy timestamp-named step files, preserving manifests."""
211
+ for file in trace_folder_path.iterdir():
212
+ if file.suffix == ".json" and file.stem.isdigit():
213
+ file.unlink()
@@ -0,0 +1,39 @@
1
+ # Copyright 2025-2026 Minitap, Inc.
2
+ # Modifications Copyright 2026 Google LLC
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Originally from mobile-use (https://github.com/minitap-ai/mobile-use).
17
+ # See third_party/mobile_use/METADATA for the upstream source and local modifications.
18
+
19
+ import subprocess
20
+
21
+
22
+ def run_shell_command_on_host(command: str) -> str:
23
+ """Helper to run a shell command on the host and return the output."""
24
+ try:
25
+ result = subprocess.run(
26
+ command,
27
+ stdin=subprocess.DEVNULL,
28
+ shell=True,
29
+ check=True,
30
+ capture_output=True,
31
+ text=True,
32
+ )
33
+ return result.stdout.strip()
34
+ except subprocess.CalledProcessError as e:
35
+ # Log the error and stderr for better debugging
36
+ error_message = (
37
+ f"Command '{command}' failed with exit code {e.returncode}.\nStderr: {e.stderr.strip()}"
38
+ )
39
+ raise RuntimeError(error_message) from e
@@ -0,0 +1,398 @@
1
+ # Copyright 2025-2026 Minitap, Inc.
2
+ # Modifications Copyright 2026 Google LLC
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Originally from mobile-use (https://github.com/minitap-ai/mobile-use).
17
+ # See third_party/mobile_use/METADATA for the upstream source and local modifications.
18
+
19
+ """Video recording utilities for mobile devices.
20
+
21
+ Provides shared types and utilities for video recording across platforms.
22
+ """
23
+
24
+ import asyncio
25
+ from pathlib import Path
26
+ import platform
27
+ from typing import Any
28
+ from uuid import UUID
29
+
30
+ from pydantic import BaseModel, ConfigDict
31
+
32
+ from artemis.utils.video import (
33
+ get_ffmpeg_path,
34
+ get_ffprobe_path,
35
+ is_ffmpeg_drawtext_supported,
36
+ is_ffmpeg_installed,
37
+ )
38
+ from third_party.mobile_use.utils.logger import get_logger
39
+
40
+ logger = get_logger(__name__)
41
+
42
+ DEFAULT_MAX_DURATION_SECONDS = 900 # 15 minutes
43
+ ANDROID_DEVICE_VIDEO_PATH = "/sdcard/screen_recording.mp4"
44
+
45
+ # Expanded for Gemini File API (Supports up to 2GB).
46
+ # Target 100MB to allow 3-5min crisp video and prevent blurring for long durations.
47
+ MAX_VIDEO_SIZE_MB = 500
48
+ MAX_VIDEO_SIZE_BYTES = MAX_VIDEO_SIZE_MB * 1024 * 1024
49
+
50
+
51
+ class RecordingSession(BaseModel):
52
+ """Tracks an active video recording session."""
53
+
54
+ model_config = ConfigDict(arbitrary_types_allowed=True)
55
+
56
+ video_id: UUID
57
+ device_id: str
58
+ start_time: float
59
+ process: Any = None
60
+ data_engine_start_time: float | None = None
61
+ local_video_path: Path | None = None
62
+ capture_width: int | None = None
63
+ capture_height: int | None = None
64
+ android_device_path: str = ANDROID_DEVICE_VIDEO_PATH
65
+ android_video_segments: list[Path] = []
66
+ android_segment_index: int = 0
67
+ android_restart_task: asyncio.Task | None = None
68
+ watchdog_task: asyncio.Task | None = None
69
+ android_rotation: int | None = None
70
+ android_segment_started_at: float | None = None
71
+ android_segment_records: list[dict[str, Any]] = []
72
+ android_conversion_tasks: list[asyncio.Task] = []
73
+ generation: int = 0
74
+ sealed_until: float = 0.0
75
+ is_active: bool = True
76
+ errors: list[str] = []
77
+
78
+
79
+ class VideoRecordingResult(BaseModel):
80
+ """Result of a video recording operation."""
81
+
82
+ success: bool
83
+ message: str
84
+ video_path: Path | None = None
85
+ file_size_mb: float | None = None
86
+ duration_seconds: float | None = None
87
+ actual_start_relative_time: float | None = None
88
+ warning: str | None = None
89
+ video_id: UUID | None = None
90
+ generation: int | None = None
91
+ sealed_until: float | None = None
92
+ source_revision: str | None = None
93
+
94
+ model_config = ConfigDict(arbitrary_types_allowed=True)
95
+
96
+
97
+ # Global session storage - keyed by device_id
98
+ _active_recordings: dict[str, RecordingSession] = {}
99
+
100
+
101
+ def get_active_session(device_id: str) -> RecordingSession | None:
102
+ """Get the active recording session for a device."""
103
+ return _active_recordings.get(device_id)
104
+
105
+
106
+ def set_active_session(device_id: str, session: RecordingSession) -> None:
107
+ """Set the active recording session for a device."""
108
+ _active_recordings[device_id] = session
109
+
110
+
111
+ def remove_active_session(device_id: str) -> RecordingSession | None:
112
+ """Remove and return the active recording session for a device."""
113
+ return _active_recordings.pop(device_id, None)
114
+
115
+
116
+ def has_active_session(device_id: str) -> bool:
117
+ """Check if there's an active recording session for a device."""
118
+ return device_id in _active_recordings
119
+
120
+
121
+ def recording_already_active(device_id: str) -> VideoRecordingResult | None:
122
+ """Failure result if the device is already recording, otherwise ``None``."""
123
+ if has_active_session(device_id):
124
+ return VideoRecordingResult(
125
+ success=False,
126
+ message=f"Recording already in progress for device {device_id}",
127
+ )
128
+ return None
129
+
130
+
131
+ def no_active_recording(device_id: str) -> VideoRecordingResult:
132
+ """Failure result for an operation that needs an active recording."""
133
+ return VideoRecordingResult(
134
+ success=False,
135
+ message=f"No active recording for device {device_id}",
136
+ )
137
+
138
+
139
+ def abort_recording(device_id: str, action: str, error: Exception) -> VideoRecordingResult:
140
+ """Drop the device's session after a failed ``action`` ("start"/"stop") and report it."""
141
+ remove_active_session(device_id)
142
+ return VideoRecordingResult(
143
+ success=False,
144
+ message=f"Failed to {action} recording: {error}",
145
+ )
146
+
147
+
148
+ class FFmpegNotInstalledError(Exception):
149
+ """Raised when ffmpeg is required but not installed."""
150
+
151
+ def __init__(self):
152
+ os_name = platform.system().lower()
153
+ if os_name == "darwin": # macOS
154
+ install_instructions = "brew install ffmpeg"
155
+ elif os_name == "windows":
156
+ install_instructions = "Download from https://www.ffmpeg.org/download.html"
157
+ else: # Linux and others
158
+ install_instructions = (
159
+ "Install via your package manager (e.g., apt install ffmpeg,"
160
+ " dnf install ffmpeg) or download from"
161
+ " https://www.ffmpeg.org/download.html"
162
+ )
163
+
164
+ message = (
165
+ "\n\n❌ ffmpeg is required for video recording but is not"
166
+ " installed.\n\nPlease install ffmpeg first:\n →"
167
+ f" {install_instructions}\n\nAfter installation, restart Artemis.\n"
168
+ )
169
+ super().__init__(message)
170
+
171
+
172
+ def check_ffmpeg_available() -> None:
173
+ """Check if ffmpeg is installed and raise an error if not.
174
+
175
+ Raises:
176
+ FFmpegNotInstalledError: If ffmpeg is not found in PATH.
177
+ """
178
+ if not is_ffmpeg_installed():
179
+ raise FFmpegNotInstalledError()
180
+
181
+
182
+ async def probe_duration(input_path: Path) -> float:
183
+ """Return the container duration of a video in seconds, using ffprobe.
184
+
185
+ Raises:
186
+ ValueError: If ffprobe reports no parseable duration.
187
+ OSError: If ffprobe cannot be started.
188
+ """
189
+ duration_cmd = [
190
+ get_ffprobe_path(),
191
+ "-v",
192
+ "error",
193
+ "-show_entries",
194
+ "format=duration",
195
+ "-of",
196
+ "default=noprint_wrappers=1:nokey=1",
197
+ str(input_path),
198
+ ]
199
+ proc = await asyncio.create_subprocess_exec(
200
+ *duration_cmd,
201
+ stdout=asyncio.subprocess.PIPE,
202
+ stderr=asyncio.subprocess.PIPE,
203
+ )
204
+ stdout, _ = await proc.communicate()
205
+ return float(stdout.decode().strip())
206
+
207
+
208
+ async def compress_video_for_api(
209
+ input_path: Path,
210
+ target_size_bytes: int = MAX_VIDEO_SIZE_BYTES,
211
+ force_compress: bool = False,
212
+ start_offset_seconds: float = 0.0,
213
+ slowdown_factor: float = 1.0,
214
+ ) -> Path:
215
+ """Compress a video to fit within API size limits using ffmpeg.
216
+
217
+ Uses a two-pass approach:
218
+ 1. First check if video is already small enough
219
+ 2. If not, compress with reduced resolution and bitrate
220
+
221
+ Args:
222
+ input_path: Path to the input video file
223
+ target_size_bytes: Target maximum file size in bytes
224
+ force_compress: If True, always perform compression (e.g., to extract
225
+ frames at 15fps)
226
+ start_offset_seconds: Offset to add to the burned-in timestamp
227
+ slowdown_factor: Factor to slow down the video by (e.g. 5.0 for 5x
228
+ slower) to increase API frame sampling rate
229
+
230
+ Returns:
231
+ Path to the compressed video (may be same as input if no compression
232
+ needed)
233
+ """
234
+ if not input_path.exists():
235
+ raise FileNotFoundError(f"Video file not found: {input_path}")
236
+
237
+ current_size = input_path.stat().st_size
238
+ logger.info(f"Video size: {current_size / 1024 / 1024:.2f} MB")
239
+
240
+ if current_size <= target_size_bytes and not force_compress and slowdown_factor == 1.0:
241
+ logger.info(
242
+ "Video already within size limit and force_compress=False, no compression needed"
243
+ )
244
+ return input_path
245
+
246
+ logger.info(
247
+ f"Compressing video to fit within {target_size_bytes / 1024 / 1024:.1f}"
248
+ f" MB (slowdown={slowdown_factor})"
249
+ )
250
+
251
+ output_path = input_path.parent / f"compressed_{input_path.name}"
252
+
253
+ # Use Constant Rate Factor (CRF) for high-fidelity UI text readability.
254
+ # CRF dynamically allocates bitrates depending on scene motion.
255
+ logger.info("Compressing video using CRF=26 for high-fidelity UI text readability.")
256
+
257
+ # Compress with ffmpeg: reduce resolution to 720p max, use CRF
258
+ # and check if drawtext is supported
259
+ vf_parts = ["setpts=PTS-STARTPTS", "scale='min(720,iw)':'-2'"]
260
+ if is_ffmpeg_drawtext_supported():
261
+ vf_parts.append(
262
+ "drawtext=text='TS\\:"
263
+ f" %{{expr_int_format\\:trunc(t+{start_offset_seconds})\\:d}}"
264
+ " s':x=w-tw-30:y=120+th+20:fontcolor=white@0.6:fontsize=44:borderw=4:"
265
+ "bordercolor=red@0.6:box=1:boxcolor=yellow@0.4:boxborderw=10:font='Sans"
266
+ " Bold'"
267
+ )
268
+ else:
269
+ logger.warning(
270
+ "ffmpeg 'drawtext' filter not supported on this system. Skipping"
271
+ " burned-in timestamp overlay."
272
+ )
273
+
274
+ if slowdown_factor != 1.0:
275
+ vf_parts.append(f"setpts={slowdown_factor}*PTS")
276
+
277
+ vf_filter = ",".join(vf_parts)
278
+
279
+ compress_cmd = [
280
+ get_ffmpeg_path(),
281
+ "-y",
282
+ "-i",
283
+ str(input_path),
284
+ "-vf",
285
+ vf_filter,
286
+ "-r",
287
+ "15", # Set framerate to 15 fps
288
+ "-c:v",
289
+ "libx264",
290
+ "-preset",
291
+ "fast",
292
+ "-crf",
293
+ "26", # High quality for UI elements
294
+ "-pix_fmt",
295
+ "yuv420p",
296
+ "-c:a",
297
+ "aac",
298
+ "-b:a",
299
+ "64k",
300
+ str(output_path),
301
+ ]
302
+
303
+ try:
304
+ proc = await asyncio.create_subprocess_exec(
305
+ *compress_cmd,
306
+ stdout=asyncio.subprocess.PIPE,
307
+ stderr=asyncio.subprocess.PIPE,
308
+ )
309
+ _, stderr = await proc.communicate()
310
+
311
+ if proc.returncode != 0:
312
+ err_msg = stderr.decode().strip()
313
+ logger.error(f"ffmpeg compression failed (code {proc.returncode}): {err_msg}")
314
+ if force_compress or slowdown_factor != 1.0:
315
+ raise RuntimeError(
316
+ f"Video compression/slowdown failed (code {proc.returncode}): {err_msg}"
317
+ )
318
+ return input_path # Return original if compression fails and was optional
319
+
320
+ new_size = output_path.stat().st_size
321
+ logger.info(
322
+ f"Compressed: {current_size / 1024 / 1024:.2f} MB -> {new_size / 1024 / 1024:.2f} MB"
323
+ )
324
+
325
+ return output_path
326
+
327
+ except Exception as e:
328
+ logger.error(f"Video compression failed: {e}")
329
+ if force_compress or slowdown_factor != 1.0:
330
+ raise RuntimeError(f"Video compression/slowdown failed: {e}")
331
+ return input_path # Return original if compression fails and was optional
332
+
333
+
334
+ async def extract_audio_from_video(input_path: Path) -> Path:
335
+ """Extract audio from a video file using ffmpeg.
336
+
337
+ Saves as .mp3.
338
+ """
339
+ if not input_path.exists():
340
+ raise FileNotFoundError(f"Video file not found: {input_path}")
341
+
342
+ probe_cmd = [
343
+ get_ffprobe_path(),
344
+ "-v",
345
+ "error",
346
+ "-select_streams",
347
+ "a:0",
348
+ "-show_entries",
349
+ "stream=index",
350
+ "-of",
351
+ "csv=p=0",
352
+ str(input_path),
353
+ ]
354
+ probe = await asyncio.create_subprocess_exec(
355
+ *probe_cmd,
356
+ stdout=asyncio.subprocess.PIPE,
357
+ stderr=asyncio.subprocess.PIPE,
358
+ )
359
+ probe_stdout, _ = await probe.communicate()
360
+ if probe.returncode != 0 or not probe_stdout.strip():
361
+ raise ValueError("Video has no audio stream")
362
+
363
+ output_path = input_path.parent / f"audio_{input_path.stem}.mp3"
364
+ logger.info(f"Extracting audio from {input_path} to {output_path}")
365
+
366
+ cmd = [
367
+ get_ffmpeg_path(),
368
+ "-y",
369
+ "-i",
370
+ str(input_path),
371
+ "-vn",
372
+ "-c:a",
373
+ "libmp3lame",
374
+ "-b:a",
375
+ "128k",
376
+ str(output_path),
377
+ ]
378
+
379
+ try:
380
+ proc = await asyncio.create_subprocess_exec(
381
+ *cmd,
382
+ stdout=asyncio.subprocess.PIPE,
383
+ stderr=asyncio.subprocess.PIPE,
384
+ )
385
+ _, stderr = await proc.communicate()
386
+
387
+ if proc.returncode != 0:
388
+ err_msg = stderr.decode(errors="replace").strip()
389
+ concise_error = "\n".join(err_msg.splitlines()[-8:])
390
+ logger.error(f"ffmpeg audio extraction failed: {concise_error}")
391
+ raise RuntimeError(
392
+ f"ffmpeg audio extraction failed (code {proc.returncode}): {concise_error}"
393
+ )
394
+
395
+ return output_path
396
+
397
+ except Exception:
398
+ raise