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.
- package/CHANGELOG.md +11 -1
- package/README.md +63 -1
- package/artemis-main/.dockerignore +15 -0
- package/artemis-main/.env.example +34 -0
- package/artemis-main/CONTRIBUTING.md +35 -0
- package/artemis-main/Dockerfile +68 -0
- package/artemis-main/LICENSE +202 -0
- package/artemis-main/Makefile +115 -0
- package/artemis-main/README.md +322 -0
- package/artemis-main/README_CN.md +320 -0
- package/artemis-main/apps/__init__.py +15 -0
- package/artemis-main/apps/admin_console/__init__.py +19 -0
- package/artemis-main/apps/admin_console/core/__init__.py +13 -0
- package/artemis-main/apps/admin_console/core/config.py +43 -0
- package/artemis-main/apps/admin_console/core/security.py +178 -0
- package/artemis-main/apps/admin_console/core/state.py +243 -0
- package/artemis-main/apps/admin_console/database/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/connection.py +60 -0
- package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
- package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
- package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
- package/artemis-main/apps/admin_console/index.html +3787 -0
- package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
- package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
- package/artemis-main/apps/admin_console/routers/media.py +198 -0
- package/artemis-main/apps/admin_console/routers/replay.py +91 -0
- package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
- package/artemis-main/apps/admin_console/routers/steps.py +109 -0
- package/artemis-main/apps/admin_console/routers/stream.py +56 -0
- package/artemis-main/apps/admin_console/routers/system.py +761 -0
- package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
- package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
- package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
- package/artemis-main/apps/admin_console/server.py +508 -0
- package/artemis-main/apps/admin_console/services/__init__.py +13 -0
- package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
- package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
- package/artemis-main/apps/admin_console/services/media_service.py +623 -0
- package/artemis-main/apps/admin_console/services/model_service.py +148 -0
- package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
- package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
- package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
- package/artemis-main/apps/showcase_ui/README.md +31 -0
- package/artemis-main/apps/showcase_ui/angular.json +132 -0
- package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
- package/artemis-main/apps/showcase_ui/package.json +39 -0
- package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
- package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
- package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
- package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
- package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
- package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
- package/artemis-main/apps/showcase_ui/src/index.html +37 -0
- package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
- package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
- package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
- package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
- package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
- package/artemis-main/artemis/__init__.py +44 -0
- package/artemis-main/artemis/__main__.py +18 -0
- package/artemis-main/artemis/_legacy_imports.py +94 -0
- package/artemis-main/artemis/_version.py +30 -0
- package/artemis-main/artemis/agents/__init__.py +17 -0
- package/artemis-main/artemis/agents/checker/checker.json +9 -0
- package/artemis-main/artemis/agents/checker/checker.py +859 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
- package/artemis-main/artemis/agents/explorer/constants.py +52 -0
- package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
- package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
- package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
- package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
- package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
- package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
- package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
- package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
- package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
- package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
- package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
- package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
- package/artemis-main/artemis/agents/flash/runner.py +1167 -0
- package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
- package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
- package/artemis-main/artemis/agents/operator/operator.json +3 -0
- package/artemis-main/artemis/agents/operator/operator.py +1394 -0
- package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
- package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
- package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
- package/artemis-main/artemis/agents/planner/planner.json +32 -0
- package/artemis-main/artemis/agents/planner/planner.py +436 -0
- package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
- package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
- package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
- package/artemis-main/artemis/agents/validator/categories.py +33 -0
- package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
- package/artemis-main/artemis/agents/validator/incidents.py +120 -0
- package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
- package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
- package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
- package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
- package/artemis-main/artemis/agents/validator/validator.py +188 -0
- package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
- package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
- package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
- package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
- package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
- package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
- package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
- package/artemis-main/artemis/clients/accessibility_client.py +438 -0
- package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
- package/artemis-main/artemis/config/__init__.py +309 -0
- package/artemis-main/artemis/config/agent.py +1098 -0
- package/artemis-main/artemis/config/constants.py +151 -0
- package/artemis-main/artemis/config/core/__init__.py +24 -0
- package/artemis-main/artemis/config/core/engine.py +118 -0
- package/artemis-main/artemis/config/core/interpolator.py +78 -0
- package/artemis-main/artemis/config/llm.py +267 -0
- package/artemis-main/artemis/config/paths.py +244 -0
- package/artemis-main/artemis/config/runtime.py +197 -0
- package/artemis-main/artemis/config/settings.py +315 -0
- package/artemis-main/artemis/constants.py +47 -0
- package/artemis-main/artemis/context.py +289 -0
- package/artemis-main/artemis/controllers/__init__.py +41 -0
- package/artemis-main/artemis/controllers/controller_factory.py +41 -0
- package/artemis-main/artemis/controllers/device_controller.py +29 -0
- package/artemis-main/artemis/controllers/types.py +37 -0
- package/artemis-main/artemis/controllers/unified_controller.py +649 -0
- package/artemis-main/artemis/core/__init__.py +21 -0
- package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
- package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
- package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
- package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
- package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
- package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
- package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
- package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
- package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
- package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
- package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
- package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
- package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
- package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
- package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
- package/artemis-main/artemis/core/tool_declaration.py +43 -0
- package/artemis-main/artemis/core/tool_failure.py +56 -0
- package/artemis-main/artemis/data_engine/context_vars.py +24 -0
- package/artemis-main/artemis/data_engine/engine.py +1615 -0
- package/artemis-main/artemis/data_engine/history_reader.py +142 -0
- package/artemis-main/artemis/data_engine/models.py +131 -0
- package/artemis-main/artemis/data_engine/storage.py +1245 -0
- package/artemis-main/artemis/data_engine/trace.py +727 -0
- package/artemis-main/artemis/drivers/__init__.py +48 -0
- package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
- package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
- package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
- package/artemis-main/artemis/drivers/android/recorder.py +57 -0
- package/artemis-main/artemis/drivers/base.py +290 -0
- package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
- package/artemis-main/artemis/drivers/factory.py +80 -0
- package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
- package/artemis-main/artemis/drivers/types.py +67 -0
- package/artemis-main/artemis/graph/checkpoints.py +1243 -0
- package/artemis-main/artemis/graph/graph.py +1055 -0
- package/artemis-main/artemis/graph/perception.py +234 -0
- package/artemis-main/artemis/graph/state.py +151 -0
- package/artemis-main/artemis/graph/visibility.py +238 -0
- package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
- package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
- package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
- package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
- package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
- package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
- package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
- package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
- package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
- package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
- package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
- package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
- package/artemis-main/artemis/interfaces/cli/main.py +109 -0
- package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
- package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
- package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
- package/artemis-main/artemis/llm/__init__.py +25 -0
- package/artemis-main/artemis/llm/fake_model.py +179 -0
- package/artemis-main/artemis/llm/google/__init__.py +57 -0
- package/artemis-main/artemis/llm/google/provider.py +185 -0
- package/artemis-main/artemis/llm/google/usage.py +148 -0
- package/artemis-main/artemis/llm/parser.py +32 -0
- package/artemis-main/artemis/llm/reliability.py +267 -0
- package/artemis-main/artemis/llm/router.py +389 -0
- package/artemis-main/artemis/llm/schemas.py +36 -0
- package/artemis-main/artemis/llm/structured.py +196 -0
- package/artemis-main/artemis/main.py +58 -0
- package/artemis-main/artemis/mcp/__init__.py +20 -0
- package/artemis-main/artemis/mcp/action_executor.py +760 -0
- package/artemis-main/artemis/mcp/action_manifest.py +320 -0
- package/artemis-main/artemis/mcp/action_names.py +172 -0
- package/artemis-main/artemis/mcp/action_server.py +202 -0
- package/artemis-main/artemis/mcp/action_session.py +245 -0
- package/artemis-main/artemis/mcp/action_specs.py +868 -0
- package/artemis-main/artemis/mcp/action_types.py +86 -0
- package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
- package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
- package/artemis-main/artemis/mcp/actuators/base.py +113 -0
- package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
- package/artemis-main/artemis/mcp/adb_server.py +490 -0
- package/artemis-main/artemis/mcp/observation.py +133 -0
- package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
- package/artemis-main/artemis/memory/__init__.py +90 -0
- package/artemis-main/artemis/memory/chunking.py +1843 -0
- package/artemis-main/artemis/memory/context_policy.py +226 -0
- package/artemis-main/artemis/memory/step_capsule.md +73 -0
- package/artemis-main/artemis/memory/step_memory.py +281 -0
- package/artemis-main/artemis/memory/transcript.py +798 -0
- package/artemis-main/artemis/platform/__init__.py +61 -0
- package/artemis-main/artemis/platform/base.py +104 -0
- package/artemis-main/artemis/platform/darwin.py +184 -0
- package/artemis-main/artemis/platform/linux.py +207 -0
- package/artemis-main/artemis/platform/windows.py +193 -0
- package/artemis-main/artemis/resources/__init__.py +46 -0
- package/artemis-main/artemis/runtime/__init__.py +108 -0
- package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
- package/artemis-main/artemis/runtime/awake_lease.py +283 -0
- package/artemis-main/artemis/runtime/awake_service.py +343 -0
- package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
- package/artemis-main/artemis/runtime/daemon_client.py +422 -0
- package/artemis-main/artemis/runtime/device_lock.py +1013 -0
- package/artemis-main/artemis/runtime/device_pool.py +478 -0
- package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
- package/artemis-main/artemis/runtime/process_probe.py +98 -0
- package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
- package/artemis-main/artemis/runtime/supervisor.py +121 -0
- package/artemis-main/artemis/runtime/trace_store.py +375 -0
- package/artemis-main/artemis/sdk/__init__.py +26 -0
- package/artemis-main/artemis/sdk/agent.py +485 -0
- package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
- package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
- package/artemis-main/artemis/sdk/constants.py +15 -0
- package/artemis-main/artemis/sdk/run_outcome.py +58 -0
- package/artemis-main/artemis/sdk/types/__init__.py +61 -0
- package/artemis-main/artemis/sdk/types/agent.py +89 -0
- package/artemis-main/artemis/sdk/utils.py +28 -0
- package/artemis-main/artemis/services/llm.py +1040 -0
- package/artemis-main/artemis/services/token_meter.py +267 -0
- package/artemis-main/artemis/telemetry/__init__.py +35 -0
- package/artemis-main/artemis/telemetry/models.py +64 -0
- package/artemis-main/artemis/telemetry/replay.py +34 -0
- package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
- package/artemis-main/artemis/telemetry/tracer.py +85 -0
- package/artemis-main/artemis/toolchain/__init__.py +52 -0
- package/artemis-main/artemis/toolchain/descriptors.py +92 -0
- package/artemis-main/artemis/toolchain/resolver.py +185 -0
- package/artemis-main/artemis/tools/base.py +232 -0
- package/artemis-main/artemis/tools/command_tool.py +1240 -0
- package/artemis-main/artemis/tools/committee_prompts.json +5 -0
- package/artemis-main/artemis/tools/committee_tool.py +422 -0
- package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
- package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
- package/artemis-main/artemis/tools/explorer_tool.py +581 -0
- package/artemis-main/artemis/tools/history/__init__.py +398 -0
- package/artemis-main/artemis/tools/history/replay.py +120 -0
- package/artemis-main/artemis/tools/history/screenshot.py +139 -0
- package/artemis-main/artemis/tools/history/search.py +563 -0
- package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
- package/artemis-main/artemis/tools/index.py +47 -0
- package/artemis-main/artemis/tools/log_tool.py +108 -0
- package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
- package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
- package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
- package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
- package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
- package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
- package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
- package/artemis-main/artemis/tools/scratchpad.py +819 -0
- package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
- package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
- package/artemis-main/artemis/tools/video_tool.py +261 -0
- package/artemis-main/artemis/tools/wait_tool.py +97 -0
- package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
- package/artemis-main/artemis/utils/coordinates.py +606 -0
- package/artemis-main/artemis/utils/credentials_validator.py +201 -0
- package/artemis-main/artemis/utils/cv_canvas.py +189 -0
- package/artemis-main/artemis/utils/cython_compat.py +45 -0
- package/artemis-main/artemis/utils/element_hit_test.py +80 -0
- package/artemis-main/artemis/utils/image_diff.py +335 -0
- package/artemis-main/artemis/utils/image_hash.py +80 -0
- package/artemis-main/artemis/utils/notes.py +470 -0
- package/artemis-main/artemis/utils/ocr_api.py +105 -0
- package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
- package/artemis-main/artemis/utils/plan_grammar.py +513 -0
- package/artemis-main/artemis/utils/python_executor.py +213 -0
- package/artemis-main/artemis/utils/startup_progress.py +60 -0
- package/artemis-main/artemis/utils/task_tree.py +1174 -0
- package/artemis-main/artemis/utils/text.py +37 -0
- package/artemis-main/artemis/utils/ui_filter.py +651 -0
- package/artemis-main/artemis/utils/video.py +685 -0
- package/artemis-main/artemis/utils/visualization.py +802 -0
- package/artemis-main/config/artemis.example.jsonc +12 -0
- package/artemis-main/mcp_server/README.md +111 -0
- package/artemis-main/mcp_server/__init__.py +21 -0
- package/artemis-main/mcp_server/__main__.py +20 -0
- package/artemis-main/mcp_server/background/__init__.py +15 -0
- package/artemis-main/mcp_server/background/task_runner.py +453 -0
- package/artemis-main/mcp_server/base.py +49 -0
- package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
- package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
- package/artemis-main/mcp_server/notifiers/base.py +45 -0
- package/artemis-main/mcp_server/notifiers/composite.py +87 -0
- package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
- package/artemis-main/mcp_server/notifiers/file.py +69 -0
- package/artemis-main/mcp_server/notifiers/script.py +90 -0
- package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
- package/artemis-main/mcp_server/rules.md +68 -0
- package/artemis-main/mcp_server/server.py +106 -0
- package/artemis-main/mcp_server/tools/__init__.py +29 -0
- package/artemis-main/mcp_server/tools/device_state.py +107 -0
- package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
- package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
- package/artemis-main/mcp_server/tools/task_manager.py +627 -0
- package/artemis-main/mcp_server/tools/task_runner.py +548 -0
- package/artemis-main/mcp_server/utils/__init__.py +15 -0
- package/artemis-main/mcp_server/utils/device_utils.py +180 -0
- package/artemis-main/mcp_server/utils/env_utils.py +65 -0
- package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
- package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
- package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
- package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
- package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
- package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
- package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
- package/artemis-main/packages/artemis-client/README.md +112 -0
- package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
- package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
- package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
- package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
- package/artemis-main/pyproject.toml +259 -0
- package/artemis-main/pyright-core.json +17 -0
- package/artemis-main/scripts/check_dependency_sources.py +68 -0
- package/artemis-main/scripts/install_deps.ps1 +520 -0
- package/artemis-main/scripts/install_deps.sh +533 -0
- package/artemis-main/scripts/quality_ratchet.py +97 -0
- package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
- package/artemis-main/scripts/run_edge_case_part.py +327 -0
- package/artemis-main/scripts/run_ui.py +242 -0
- package/artemis-main/scripts/start.ps1 +500 -0
- package/artemis-main/scripts/test_all_edge_cases.py +269 -0
- package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
- package/artemis-main/setup.py +164 -0
- package/artemis-main/start.bat +35 -0
- package/artemis-main/start.sh +423 -0
- package/artemis-main/third_party/mobile_use/LICENSE +190 -0
- package/artemis-main/third_party/mobile_use/METADATA +27 -0
- package/artemis-main/third_party/mobile_use/NOTICE +12 -0
- package/artemis-main/third_party/mobile_use/__init__.py +19 -0
- package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
- package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
- package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
- package/artemis-main/third_party/mobile_use/config/output.py +184 -0
- package/artemis-main/third_party/mobile_use/context.py +119 -0
- package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
- package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
- package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
- package/artemis-main/third_party/mobile_use/graph.py +54 -0
- package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
- package/artemis-main/third_party/mobile_use/main.py +130 -0
- package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
- package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
- package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
- package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
- package/artemis-main/third_party/mobile_use/testing.py +59 -0
- package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
- package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
- package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
- package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
- package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
- package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
- package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
- package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
- package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
- package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
- package/artemis-main/uv.lock +4908 -0
- package/bin/android-midscene-automation.js +1 -5
- package/bin/artemis-config.mjs +15 -0
- package/bin/artemis-embedded.py +56 -0
- package/bin/start-artemis.mjs +30 -0
- package/bin/start-dev.mjs +112 -0
- package/bin/test_artemis_embedded.py +44 -0
- package/package.json +10 -8
- package/server/artemis.ts +34 -0
- package/server/http-api.ts +13 -1
- package/src/App.vue +72 -23
- package/src/api.ts +5 -0
- package/src/appium-recorder/AppiumPage.vue +1 -1
- package/src/appium-recorder/components/ComponentInspector.vue +1 -1
- package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
- package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
- package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
- package/src/appium-recorder/flow-appearance.ts +4 -4
- package/src/components/AppSidebar.vue +8 -7
- package/src/components/MidsceneMaintenanceNotice.vue +17 -0
- package/src/components/analytics/AnalysisCharts.vue +4 -4
- package/src/components/analytics/ReportAnalysis.vue +1 -1
- package/src/components/generator/TestCaseFileUpload.vue +1 -1
- package/src/components/help/HelpCenter.vue +56 -37
- package/src/pages/AiGeneratorPage.vue +1 -1
- package/src/pages/ArtemisPage.vue +72 -0
- package/src/pages/ConfigPage.vue +18 -19
- package/src/style.css +369 -186
- 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
|