mobster-cli 0.4.3__py3-none-any.whl
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.
- mobile_agent/__init__.py +4 -0
- mobile_agent/__main__.py +598 -0
- mobile_agent/accounts/__init__.py +1 -0
- mobile_agent/accounts/context.py +80 -0
- mobile_agent/accounts/mcp.py +55 -0
- mobile_agent/accounts/pacing.py +318 -0
- mobile_agent/accounts/register.py +78 -0
- mobile_agent/accounts/routes.py +212 -0
- mobile_agent/accounts/routing.py +69 -0
- mobile_agent/accounts/screen.py +273 -0
- mobile_agent/accounts/stats.py +54 -0
- mobile_agent/accounts/store.py +394 -0
- mobile_agent/agent_hooks.py +123 -0
- mobile_agent/ai_plans/__init__.py +7 -0
- mobile_agent/ai_plans/agents.py +96 -0
- mobile_agent/ai_plans/claude_code.py +208 -0
- mobile_agent/ai_plans/cli.py +400 -0
- mobile_agent/ai_plans/codex.py +258 -0
- mobile_agent/ai_plans/compat_chat.py +307 -0
- mobile_agent/ai_plans/device_login.py +164 -0
- mobile_agent/ai_plans/draft_client.py +68 -0
- mobile_agent/ai_plans/gemini.py +137 -0
- mobile_agent/ai_plans/instructions.py +65 -0
- mobile_agent/ai_plans/mcp_http.py +160 -0
- mobile_agent/ai_plans/phone_tools.py +892 -0
- mobile_agent/ai_plans/providers.py +235 -0
- mobile_agent/ai_plans/routes.py +160 -0
- mobile_agent/ai_plans/runner.py +400 -0
- mobile_agent/ai_plans/vault.py +114 -0
- mobile_agent/alerts.py +337 -0
- mobile_agent/android/__init__.py +18 -0
- mobile_agent/android/adb.py +268 -0
- mobile_agent/android/conditions.py +128 -0
- mobile_agent/android/doctor.py +54 -0
- mobile_agent/android/driver.py +137 -0
- mobile_agent/android/emulator.py +216 -0
- mobile_agent/android/logs.py +85 -0
- mobile_agent/android/manager.py +225 -0
- mobile_agent/android/records.py +31 -0
- mobile_agent/android/tree.py +96 -0
- mobile_agent/answer_shapes.py +230 -0
- mobile_agent/api_devices.py +333 -0
- mobile_agent/api_errors.py +17 -0
- mobile_agent/api_get.py +249 -0
- mobile_agent/api_http.py +467 -0
- mobile_agent/api_post.py +453 -0
- mobile_agent/api_routes.py +173 -0
- mobile_agent/app_api/__init__.py +4 -0
- mobile_agent/app_api/cli.py +151 -0
- mobile_agent/app_api/endpoints.py +193 -0
- mobile_agent/app_api/mcp.py +54 -0
- mobile_agent/app_api/openapi.py +146 -0
- mobile_agent/app_api/reading.py +172 -0
- mobile_agent/app_api/register.py +10 -0
- mobile_agent/app_api/screen.py +216 -0
- mobile_agent/app_api/service.py +382 -0
- mobile_agent/app_api/sessions.py +404 -0
- mobile_agent/app_api/tokens.py +167 -0
- mobile_agent/app_choice.py +131 -0
- mobile_agent/app_icons.py +183 -0
- mobile_agent/app_killer.py +69 -0
- mobile_agent/artifacts.py +606 -0
- mobile_agent/attach/__init__.py +13 -0
- mobile_agent/attach/cli.py +46 -0
- mobile_agent/attach/flutter.py +28 -0
- mobile_agent/attach/score.py +119 -0
- mobile_agent/attach/web.py +13 -0
- mobile_agent/attachments/__init__.py +1 -0
- mobile_agent/attachments/agent.py +418 -0
- mobile_agent/attachments/extract.py +217 -0
- mobile_agent/attachments/mcp.py +104 -0
- mobile_agent/attachments/register.py +73 -0
- mobile_agent/attachments/routes.py +326 -0
- mobile_agent/attachments/service.py +170 -0
- mobile_agent/attachments/sniff.py +152 -0
- mobile_agent/attachments/store.py +441 -0
- mobile_agent/audit_verify.py +436 -0
- mobile_agent/bench/README.md +410 -0
- mobile_agent/bench/__init__.py +0 -0
- mobile_agent/bench/__main__.py +394 -0
- mobile_agent/bench/ab.py +168 -0
- mobile_agent/bench/actuation.py +368 -0
- mobile_agent/bench/agents/__init__.py +9 -0
- mobile_agent/bench/agents/base.py +105 -0
- mobile_agent/bench/agents/plan_clients.py +245 -0
- mobile_agent/bench/agents/smart.py +533 -0
- mobile_agent/bench/checks.py +358 -0
- mobile_agent/bench/decide_gate.py +214 -0
- mobile_agent/bench/diagnose.py +181 -0
- mobile_agent/bench/evals/__init__.py +14 -0
- mobile_agent/bench/evals/build.py +565 -0
- mobile_agent/bench/evals/checkpoint.py +214 -0
- mobile_agent/bench/evals/irt.py +219 -0
- mobile_agent/bench/evals/l0.py +647 -0
- mobile_agent/bench/evals/l1.py +866 -0
- mobile_agent/bench/evals/models.py +124 -0
- mobile_agent/bench/evals/pairwise.py +189 -0
- mobile_agent/bench/evals/proxy.py +90 -0
- mobile_agent/bench/evals/sets.py +149 -0
- mobile_agent/bench/evals/traces.py +446 -0
- mobile_agent/bench/fixtures.py +176 -0
- mobile_agent/bench/guard_replay.py +129 -0
- mobile_agent/bench/h2-manifest.json +20 -0
- mobile_agent/bench/h3-manifest.json +20 -0
- mobile_agent/bench/heldout-manifest.json +32 -0
- mobile_agent/bench/heldout.py +375 -0
- mobile_agent/bench/iosworld.py +901 -0
- mobile_agent/bench/iosworld_stats.py +510 -0
- mobile_agent/bench/llm_units.py +116 -0
- mobile_agent/bench/manifest.json +93 -0
- mobile_agent/bench/metrics.py +66 -0
- mobile_agent/bench/phone.py +44 -0
- mobile_agent/bench/replay.py +172 -0
- mobile_agent/bench/report.py +207 -0
- mobile_agent/bench/runner.py +456 -0
- mobile_agent/bench/safety.py +310 -0
- mobile_agent/bench/stream_timing.py +213 -0
- mobile_agent/bench/suite.py +444 -0
- mobile_agent/bench/sweep.py +176 -0
- mobile_agent/bench/tape.py +798 -0
- mobile_agent/bench/tape_replay.py +88 -0
- mobile_agent/bench/trace_evals.py +128 -0
- mobile_agent/bench/trace_view.py +81 -0
- mobile_agent/bench/truth/iphone15pro.json +138 -0
- mobile_agent/bench/truth.py +340 -0
- mobile_agent/bench/tuning.py +543 -0
- mobile_agent/bench/tuning_analysis.py +656 -0
- mobile_agent/bench/vague-heldout-manifest.json +12 -0
- mobile_agent/bench/vague_rubric.py +135 -0
- mobile_agent/bench/vague_run.py +243 -0
- mobile_agent/brand.py +454 -0
- mobile_agent/brand_fonts.py +756 -0
- mobile_agent/build_identity.py +21 -0
- mobile_agent/cable_bt.py +623 -0
- mobile_agent/cable_bt_runtime.py +128 -0
- mobile_agent/catalog.py +69 -0
- mobile_agent/chart_svg.py +415 -0
- mobile_agent/chat_here.py +189 -0
- mobile_agent/cli_constants.py +5 -0
- mobile_agent/cli_parser.py +611 -0
- mobile_agent/cli_run.py +115 -0
- mobile_agent/compiled_read/__init__.py +21 -0
- mobile_agent/compiled_read/delta.py +89 -0
- mobile_agent/compiled_read/extractors.py +202 -0
- mobile_agent/compiled_read/routes.py +56 -0
- mobile_agent/completion.py +267 -0
- mobile_agent/compose.py +48 -0
- mobile_agent/config.py +114 -0
- mobile_agent/connectors/__init__.py +1 -0
- mobile_agent/connectors/api_session.py +76 -0
- mobile_agent/connectors/callback_page.py +146 -0
- mobile_agent/connectors/client.py +582 -0
- mobile_agent/connectors/github_app.py +55 -0
- mobile_agent/connectors/graph_api.py +396 -0
- mobile_agent/connectors/imports.py +170 -0
- mobile_agent/connectors/issue.py +66 -0
- mobile_agent/connectors/keystore.py +85 -0
- mobile_agent/connectors/marks.py +222 -0
- mobile_agent/connectors/oauth.py +476 -0
- mobile_agent/connectors/picker.py +228 -0
- mobile_agent/connectors/presets.py +591 -0
- mobile_agent/connectors/register.py +48 -0
- mobile_agent/connectors/routes.py +153 -0
- mobile_agent/connectors/service.py +966 -0
- mobile_agent/connectors/shared.py +136 -0
- mobile_agent/connectors/skill.py +446 -0
- mobile_agent/connectors/slack_api.py +269 -0
- mobile_agent/connectors/steps.py +92 -0
- mobile_agent/connectors/store.py +144 -0
- mobile_agent/connectors/vault.py +72 -0
- mobile_agent/console.py +178 -0
- mobile_agent/content_queue/__init__.py +1 -0
- mobile_agent/content_queue/client.py +263 -0
- mobile_agent/content_queue/importing.py +153 -0
- mobile_agent/content_queue/media_check.py +224 -0
- mobile_agent/content_queue/prompt.py +64 -0
- mobile_agent/content_queue/register.py +91 -0
- mobile_agent/content_queue/routes.py +370 -0
- mobile_agent/content_queue/runner.py +475 -0
- mobile_agent/content_queue/store.py +521 -0
- mobile_agent/contract.py +996 -0
- mobile_agent/contract_asks.py +248 -0
- mobile_agent/contract_prompt.py +45 -0
- mobile_agent/contract_screen.py +981 -0
- mobile_agent/contract_view.py +84 -0
- mobile_agent/corpus/__init__.py +26 -0
- mobile_agent/corpus/cli.py +63 -0
- mobile_agent/corpus/consent.py +55 -0
- mobile_agent/corpus/listener.py +76 -0
- mobile_agent/corpus/register.py +9 -0
- mobile_agent/corpus/skeleton.py +72 -0
- mobile_agent/corpus/store.py +143 -0
- mobile_agent/costs.py +161 -0
- mobile_agent/decider_scope.py +40 -0
- mobile_agent/decision_provider.py +189 -0
- mobile_agent/deeplinks.py +352 -0
- mobile_agent/device_info.py +116 -0
- mobile_agent/device_manager.py +978 -0
- mobile_agent/device_status.py +195 -0
- mobile_agent/device_targets.py +217 -0
- mobile_agent/devices.py +676 -0
- mobile_agent/devkit/__init__.py +64 -0
- mobile_agent/devkit/account_cli.py +90 -0
- mobile_agent/devkit/asc.py +562 -0
- mobile_agent/devkit/asc_cli.py +214 -0
- mobile_agent/devkit/ci.py +737 -0
- mobile_agent/devkit/ci_cli.py +196 -0
- mobile_agent/devkit/ci_hosts.py +194 -0
- mobile_agent/devkit/es256.py +197 -0
- mobile_agent/devkit/expo.py +440 -0
- mobile_agent/devkit/expo_cli.py +405 -0
- mobile_agent/devkit/expo_dev.py +203 -0
- mobile_agent/devkit/expo_routes.py +90 -0
- mobile_agent/devkit/fixtures_cli.py +133 -0
- mobile_agent/devkit/fixtures_routes.py +82 -0
- mobile_agent/devkit/links.py +375 -0
- mobile_agent/devkit/links_cli.py +123 -0
- mobile_agent/devkit/links_routes.py +83 -0
- mobile_agent/devkit/nightly.py +152 -0
- mobile_agent/devkit/shots.py +203 -0
- mobile_agent/devkit/testflight.py +155 -0
- mobile_agent/devkit/testflight_cli.py +127 -0
- mobile_agent/devkit/testflight_routes.py +111 -0
- mobile_agent/devkit/xcode.py +236 -0
- mobile_agent/devkit/xcode_cli.py +101 -0
- mobile_agent/devtools.py +419 -0
- mobile_agent/doctor.py +509 -0
- mobile_agent/doctor_fix.py +467 -0
- mobile_agent/downloads.py +55 -0
- mobile_agent/drivers.py +793 -0
- mobile_agent/dry_run.py +102 -0
- mobile_agent/effect_ledger.py +83 -0
- mobile_agent/engine_health.py +378 -0
- mobile_agent/engine_hooks.py +96 -0
- mobile_agent/engines.py +408 -0
- mobile_agent/env_file.py +63 -0
- mobile_agent/env_secrets.py +149 -0
- mobile_agent/errors.py +15 -0
- mobile_agent/evals/__init__.py +3 -0
- mobile_agent/evals/app_survey.py +149 -0
- mobile_agent/evals/development/__init__.py +1 -0
- mobile_agent/evals/development/score.py +276 -0
- mobile_agent/evals/latency_report.py +312 -0
- mobile_agent/evals/offline_report.py +374 -0
- mobile_agent/evals/oracles.py +298 -0
- mobile_agent/evals/redteam/__init__.py +15 -0
- mobile_agent/evals/redteam/__main__.py +72 -0
- mobile_agent/evals/redteam/cases.py +730 -0
- mobile_agent/evals/redteam/harness.py +604 -0
- mobile_agent/evals/redteam/live.py +139 -0
- mobile_agent/evals/redteam/plain.py +85 -0
- mobile_agent/evals/vision/__init__.py +0 -0
- mobile_agent/evals/vision/images/dog_001.jpg +0 -0
- mobile_agent/evals/vision/images/dog_002.jpg +0 -0
- mobile_agent/evals/vision/images/dog_003.jpg +0 -0
- mobile_agent/evals/vision/images/dog_004.jpg +0 -0
- mobile_agent/evals/vision/images/dog_005.jpg +0 -0
- mobile_agent/evals/vision/images/dog_006.jpg +0 -0
- mobile_agent/evals/vision/images/dog_007.jpg +0 -0
- mobile_agent/evals/vision/images/dog_010.jpg +0 -0
- mobile_agent/evals/vision/images/dog_011.jpg +0 -0
- mobile_agent/evals/vision/images/dog_013.jpg +0 -0
- mobile_agent/evals/vision/images/dog_014.jpg +0 -0
- mobile_agent/evals/vision/images/dog_015.jpg +0 -0
- mobile_agent/evals/vision/images/dog_016.jpg +0 -0
- mobile_agent/evals/vision/images/dog_017.jpg +0 -0
- mobile_agent/evals/vision/images/dog_019.jpg +0 -0
- mobile_agent/evals/vision/images/dog_020.jpg +0 -0
- mobile_agent/evals/vision/images/dog_021.jpg +0 -0
- mobile_agent/evals/vision/images/dog_022.jpg +0 -0
- mobile_agent/evals/vision/images/dog_023.jpg +0 -0
- mobile_agent/evals/vision/images/dog_024.jpg +0 -0
- mobile_agent/evals/vision/images/dog_026.jpg +0 -0
- mobile_agent/evals/vision/images/dog_027.jpg +0 -0
- mobile_agent/evals/vision/images/dog_034.jpg +0 -0
- mobile_agent/evals/vision/images/dog_035.jpg +0 -0
- mobile_agent/evals/vision/images/dog_038.jpg +0 -0
- mobile_agent/evals/vision/images/dog_042.jpg +0 -0
- mobile_agent/evals/vision/images/dog_047.jpg +0 -0
- mobile_agent/evals/vision/images/dog_052.jpg +0 -0
- mobile_agent/evals/vision/images/dog_058.jpg +0 -0
- mobile_agent/evals/vision/images/dog_062.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
- mobile_agent/evals/vision/manifest.json +2355 -0
- mobile_agent/eventbus.py +110 -0
- mobile_agent/explain.py +34 -0
- mobile_agent/failure_line.py +197 -0
- mobile_agent/fanout.py +663 -0
- mobile_agent/feedback/__init__.py +2 -0
- mobile_agent/feedback/bundle.py +204 -0
- mobile_agent/feedback/outbox.py +196 -0
- mobile_agent/feedback/register.py +9 -0
- mobile_agent/feedback/routes.py +177 -0
- mobile_agent/fleet.py +841 -0
- mobile_agent/fleet_health.py +729 -0
- mobile_agent/fleet_phone.py +186 -0
- mobile_agent/frontier/__init__.py +79 -0
- mobile_agent/frontier/act.py +380 -0
- mobile_agent/frontier/actions.py +70 -0
- mobile_agent/frontier/agent.py +256 -0
- mobile_agent/frontier/approvals.py +366 -0
- mobile_agent/frontier/hooks.py +199 -0
- mobile_agent/frontier/interrupts.py +125 -0
- mobile_agent/frontier/observe.py +331 -0
- mobile_agent/frontier/phases.py +381 -0
- mobile_agent/frontier/pipeline.py +61 -0
- mobile_agent/frontier/prompt.py +384 -0
- mobile_agent/frontier/protocol.py +91 -0
- mobile_agent/frontier/safety.py +467 -0
- mobile_agent/frontier/saved.py +375 -0
- mobile_agent/frontier/screen.py +419 -0
- mobile_agent/frontier/skills.py +147 -0
- mobile_agent/frontier/stages/__init__.py +1 -0
- mobile_agent/frontier/stages/after.py +192 -0
- mobile_agent/frontier/stages/approve.py +138 -0
- mobile_agent/frontier/stages/controls.py +115 -0
- mobile_agent/frontier/stages/ends.py +102 -0
- mobile_agent/frontier/stages/gate.py +92 -0
- mobile_agent/frontier/stages/guards.py +132 -0
- mobile_agent/frontier/stages/points.py +155 -0
- mobile_agent/frontier/stages/send.py +177 -0
- mobile_agent/frontier/turn.py +151 -0
- mobile_agent/full_disk_access.py +83 -0
- mobile_agent/gemini.py +69 -0
- mobile_agent/genuine.py +120 -0
- mobile_agent/graph/__init__.py +4 -0
- mobile_agent/graph/catalog.py +789 -0
- mobile_agent/graph/chain.py +306 -0
- mobile_agent/graph/dispatch.py +98 -0
- mobile_agent/graph/draft.py +373 -0
- mobile_agent/graph/edit.py +76 -0
- mobile_agent/graph/estimate.py +189 -0
- mobile_agent/graph/expr.py +562 -0
- mobile_agent/graph/functions.py +271 -0
- mobile_agent/graph/infer.py +144 -0
- mobile_agent/graph/nodes.py +936 -0
- mobile_agent/graph/phone_steps.py +178 -0
- mobile_agent/graph/rerun.py +327 -0
- mobile_agent/graph/run.py +899 -0
- mobile_agent/graph/schema.py +842 -0
- mobile_agent/graph/state.py +161 -0
- mobile_agent/graph/values.py +198 -0
- mobile_agent/harness/__init__.py +2 -0
- mobile_agent/harness/checkpoints.py +468 -0
- mobile_agent/harness/control.py +101 -0
- mobile_agent/harness/prewarm.py +213 -0
- mobile_agent/harness/register.py +43 -0
- mobile_agent/harness/routes.py +114 -0
- mobile_agent/harness/settings.py +66 -0
- mobile_agent/harness/tools/__init__.py +67 -0
- mobile_agent/harness/tools/ask_user.py +102 -0
- mobile_agent/harness/tools/watch.py +120 -0
- mobile_agent/harness_api.py +815 -0
- mobile_agent/hedge.py +203 -0
- mobile_agent/help_topics.py +305 -0
- mobile_agent/hints.py +248 -0
- mobile_agent/history.py +69 -0
- mobile_agent/home.py +214 -0
- mobile_agent/http_pool.py +175 -0
- mobile_agent/image_policy.py +139 -0
- mobile_agent/inbox.py +148 -0
- mobile_agent/installed_apps.py +344 -0
- mobile_agent/integrations/__init__.py +1 -0
- mobile_agent/integrations/cli.py +472 -0
- mobile_agent/integrations/client_files.py +65 -0
- mobile_agent/integrations/clients.py +998 -0
- mobile_agent/integrations/doctor.py +185 -0
- mobile_agent/integrations/jsonc.py +383 -0
- mobile_agent/integrations/routes.py +135 -0
- mobile_agent/integrations/rules.py +112 -0
- mobile_agent/integrations/server_options.py +46 -0
- mobile_agent/integrations/skill.py +152 -0
- mobile_agent/integrations/skill_files.py +10 -0
- mobile_agent/integrations/textedit.py +309 -0
- mobile_agent/intents/__init__.py +17 -0
- mobile_agent/intents/atlas.py +216 -0
- mobile_agent/intents/callback.py +44 -0
- mobile_agent/intents/cli.py +55 -0
- mobile_agent/intents/harvest.py +91 -0
- mobile_agent/intents/plan.py +93 -0
- mobile_agent/ios/__init__.py +1 -0
- mobile_agent/ios/guide.py +16 -0
- mobile_agent/ios/kb.py +212 -0
- mobile_agent/ios/kb_data.py +1588 -0
- mobile_agent/ios/mcp.py +44 -0
- mobile_agent/ios/register.py +8 -0
- mobile_agent/ios/system.py +369 -0
- mobile_agent/journal.py +531 -0
- mobile_agent/keychain_acl.py +374 -0
- mobile_agent/keys.py +395 -0
- mobile_agent/latency_overlap.py +36 -0
- mobile_agent/latency_trace.py +170 -0
- mobile_agent/learned/__init__.py +15 -0
- mobile_agent/learned/app_path.py +160 -0
- mobile_agent/learned/code_read.py +38 -0
- mobile_agent/learned/parts.py +546 -0
- mobile_agent/learned/programs.py +691 -0
- mobile_agent/learned/register.py +171 -0
- mobile_agent/learned/routes.py +61 -0
- mobile_agent/learned/taught.py +329 -0
- mobile_agent/learned/teach.py +538 -0
- mobile_agent/learned/workflows.py +647 -0
- mobile_agent/learned/zero.py +41 -0
- mobile_agent/links.py +37 -0
- mobile_agent/listen.py +607 -0
- mobile_agent/lock_spike.py +278 -0
- mobile_agent/lockscreen.py +526 -0
- mobile_agent/login.py +354 -0
- mobile_agent/longform/__init__.py +15 -0
- mobile_agent/longform/ads.py +191 -0
- mobile_agent/longform/blocks.py +117 -0
- mobile_agent/longform/budget.py +265 -0
- mobile_agent/longform/context.py +285 -0
- mobile_agent/longform/costs.py +93 -0
- mobile_agent/longform/digest.py +145 -0
- mobile_agent/longform/extract.py +508 -0
- mobile_agent/longform/feed.py +505 -0
- mobile_agent/longform/followup.py +168 -0
- mobile_agent/longform/frames.py +82 -0
- mobile_agent/longform/identity.py +87 -0
- mobile_agent/longform/inbox.py +221 -0
- mobile_agent/longform/items.py +425 -0
- mobile_agent/longform/models.py +115 -0
- mobile_agent/longform/notebook.py +329 -0
- mobile_agent/longform/plan.py +614 -0
- mobile_agent/longform/recall.py +195 -0
- mobile_agent/longform/receipts.py +81 -0
- mobile_agent/longform/register.py +124 -0
- mobile_agent/longform/report.py +343 -0
- mobile_agent/longform/routes.py +102 -0
- mobile_agent/longform/session.py +644 -0
- mobile_agent/longform/store.py +146 -0
- mobile_agent/longform/synthesis.py +347 -0
- mobile_agent/mac_tools.py +223 -0
- mobile_agent/manual_control.py +143 -0
- mobile_agent/maps.py +149 -0
- mobile_agent/marks.py +92 -0
- mobile_agent/mcp_server/__init__.py +3 -0
- mobile_agent/mcp_server/__main__.py +29 -0
- mobile_agent/mcp_server/action_journal.py +24 -0
- mobile_agent/mcp_server/action_tools.py +638 -0
- mobile_agent/mcp_server/approvals.py +219 -0
- mobile_agent/mcp_server/catalog.py +92 -0
- mobile_agent/mcp_server/cli.py +194 -0
- mobile_agent/mcp_server/codes.py +147 -0
- mobile_agent/mcp_server/device_tools.py +306 -0
- mobile_agent/mcp_server/direct.py +272 -0
- mobile_agent/mcp_server/expo_tools.py +105 -0
- mobile_agent/mcp_server/phone_tools.py +510 -0
- mobile_agent/mcp_server/protocol.py +364 -0
- mobile_agent/mcp_server/registry.py +55 -0
- mobile_agent/mcp_server/run_tools.py +531 -0
- mobile_agent/mcp_server/schemas.py +425 -0
- mobile_agent/mcp_server/screen_tools.py +168 -0
- mobile_agent/mcp_server/session.py +469 -0
- mobile_agent/mcp_server/support.py +145 -0
- mobile_agent/mcp_server/tools.py +493 -0
- mobile_agent/memory/__init__.py +9 -0
- mobile_agent/memory/cli.py +207 -0
- mobile_agent/memory/extract.py +191 -0
- mobile_agent/memory/listener.py +100 -0
- mobile_agent/memory/model.py +144 -0
- mobile_agent/memory/project.py +96 -0
- mobile_agent/memory/provider.py +127 -0
- mobile_agent/memory/register.py +21 -0
- mobile_agent/memory/retrieval.py +172 -0
- mobile_agent/memory/routes.py +216 -0
- mobile_agent/memory/runs.py +124 -0
- mobile_agent/memory/store.py +806 -0
- mobile_agent/memory/writer.py +278 -0
- mobile_agent/mirroring.py +585 -0
- mobile_agent/mirroring_runtime.py +221 -0
- mobile_agent/model_clients.py +604 -0
- mobile_agent/narrate.py +813 -0
- mobile_agent/native_helpers.py +84 -0
- mobile_agent/netxray/__init__.py +1 -0
- mobile_agent/netxray/analyze.py +322 -0
- mobile_agent/netxray/attribution.py +62 -0
- mobile_agent/netxray/capture.py +88 -0
- mobile_agent/netxray/cli.py +142 -0
- mobile_agent/netxray/job.py +219 -0
- mobile_agent/netxray/mobster-pcap.c +560 -0
- mobile_agent/netxray/packets.py +410 -0
- mobile_agent/netxray/pcapng.py +223 -0
- mobile_agent/netxray/register.py +93 -0
- mobile_agent/netxray/report.py +214 -0
- mobile_agent/netxray/store.py +48 -0
- mobile_agent/netxray/summary.py +157 -0
- mobile_agent/netxray/target.py +67 -0
- mobile_agent/netxray/tool.py +66 -0
- mobile_agent/netxray/trackers.py +194 -0
- mobile_agent/notify_triggers/__init__.py +1 -0
- mobile_agent/notify_triggers/rules.py +39 -0
- mobile_agent/notify_triggers/sources.py +315 -0
- mobile_agent/notify_triggers/watch.py +135 -0
- mobile_agent/offers.py +86 -0
- mobile_agent/orchestrate.py +215 -0
- mobile_agent/outlets/__init__.py +1 -0
- mobile_agent/outlets/connect.py +249 -0
- mobile_agent/outlets/folder.py +180 -0
- mobile_agent/outlets/hooks.py +453 -0
- mobile_agent/outlets/inbound.py +472 -0
- mobile_agent/outlets/inbound_reply.py +57 -0
- mobile_agent/outlets/inbound_routes.py +81 -0
- mobile_agent/outlets/kinds.py +308 -0
- mobile_agent/outlets/mail.py +110 -0
- mobile_agent/outlets/net.py +86 -0
- mobile_agent/outlets/notion.py +244 -0
- mobile_agent/outlets/register.py +56 -0
- mobile_agent/outlets/routes.py +135 -0
- mobile_agent/outlets/service.py +580 -0
- mobile_agent/outlets/sheets.py +169 -0
- mobile_agent/outlets/slack.py +177 -0
- mobile_agent/outlets/store.py +194 -0
- mobile_agent/outlets/vault.py +80 -0
- mobile_agent/outlets/webhook.py +316 -0
- mobile_agent/output_contract.py +141 -0
- mobile_agent/owner_presence.py +211 -0
- mobile_agent/ownership.py +219 -0
- mobile_agent/packs/__init__.py +1 -0
- mobile_agent/packs/calendar.py +243 -0
- mobile_agent/packs/context.py +68 -0
- mobile_agent/packs/core.py +757 -0
- mobile_agent/packs/front_door.py +316 -0
- mobile_agent/packs/heal.py +214 -0
- mobile_agent/packs/health.py +122 -0
- mobile_agent/packs/insights.py +76 -0
- mobile_agent/packs/instagram.py +640 -0
- mobile_agent/packs/linkedin.py +106 -0
- mobile_agent/packs/maps.py +196 -0
- mobile_agent/packs/mcp.py +149 -0
- mobile_agent/packs/messages.py +270 -0
- mobile_agent/packs/notes.py +226 -0
- mobile_agent/packs/photos.py +166 -0
- mobile_agent/packs/pins.py +47 -0
- mobile_agent/packs/reddit.py +110 -0
- mobile_agent/packs/register.py +276 -0
- mobile_agent/packs/rows.py +108 -0
- mobile_agent/packs/safari.py +143 -0
- mobile_agent/packs/settings.py +253 -0
- mobile_agent/packs/social.py +300 -0
- mobile_agent/packs/threads.py +97 -0
- mobile_agent/packs/tiktok.py +455 -0
- mobile_agent/packs/tools.py +290 -0
- mobile_agent/packs/x.py +127 -0
- mobile_agent/packs/youtube.py +199 -0
- mobile_agent/parity.py +270 -0
- mobile_agent/paths.py +99 -0
- mobile_agent/perception.py +470 -0
- mobile_agent/permissions/__init__.py +32 -0
- mobile_agent/permissions/consequence.py +535 -0
- mobile_agent/permissions/exceptions.py +99 -0
- mobile_agent/permissions/gate.py +316 -0
- mobile_agent/permissions/outside.py +316 -0
- mobile_agent/permissions/policy.py +288 -0
- mobile_agent/permissions/project_policy.py +411 -0
- mobile_agent/permissions/register.py +148 -0
- mobile_agent/permissions/rules.py +291 -0
- mobile_agent/phone_io/__init__.py +146 -0
- mobile_agent/phone_io/cli.py +427 -0
- mobile_agent/phone_io/clipboard.py +78 -0
- mobile_agent/phone_io/controls.py +78 -0
- mobile_agent/phone_io/files.py +289 -0
- mobile_agent/phone_io/handoff.py +123 -0
- mobile_agent/phone_io/icloud.py +118 -0
- mobile_agent/phone_io/install.py +188 -0
- mobile_agent/phone_io/network_capture.py +188 -0
- mobile_agent/phone_io/network_cli.py +138 -0
- mobile_agent/phone_io/network_profiles.py +138 -0
- mobile_agent/phone_io/photos.py +512 -0
- mobile_agent/phone_io/recording.py +115 -0
- mobile_agent/phone_io/secrets.py +146 -0
- mobile_agent/phone_io/transfer.py +895 -0
- mobile_agent/phone_io/tunnel.py +129 -0
- mobile_agent/phone_ready.py +91 -0
- mobile_agent/pinning.py +158 -0
- mobile_agent/plan_gate.py +807 -0
- mobile_agent/plugins/__init__.py +3 -0
- mobile_agent/plugins/install.py +163 -0
- mobile_agent/plugins/manifest.py +242 -0
- mobile_agent/plugins/register.py +15 -0
- mobile_agent/plugins/routes.py +82 -0
- mobile_agent/plugins/service.py +322 -0
- mobile_agent/plugins/skills.py +76 -0
- mobile_agent/plugins/store.py +79 -0
- mobile_agent/pool.py +285 -0
- mobile_agent/power_watch.py +275 -0
- mobile_agent/premium_model.py +22 -0
- mobile_agent/project_env.py +291 -0
- mobile_agent/project_env_cli.py +74 -0
- mobile_agent/project_env_routes.py +55 -0
- mobile_agent/projects/__init__.py +27 -0
- mobile_agent/projects/ci.py +332 -0
- mobile_agent/projects/clone.py +339 -0
- mobile_agent/projects/drafts.py +158 -0
- mobile_agent/projects/folder.py +436 -0
- mobile_agent/projects/github.py +131 -0
- mobile_agent/projects/register.py +31 -0
- mobile_agent/projects/revisions.py +253 -0
- mobile_agent/projects/routes.py +234 -0
- mobile_agent/projects/runs.py +297 -0
- mobile_agent/projects/service.py +695 -0
- mobile_agent/projects/setup.py +84 -0
- mobile_agent/projects/store.py +379 -0
- mobile_agent/projects/sync.py +380 -0
- mobile_agent/projects/workflow_files.py +270 -0
- mobile_agent/replay_core/__init__.py +26 -0
- mobile_agent/replay_core/checks.py +594 -0
- mobile_agent/replay_core/engine.py +596 -0
- mobile_agent/replay_core/heal.py +364 -0
- mobile_agent/replay_core/path.py +240 -0
- mobile_agent/replay_core/recorder.py +206 -0
- mobile_agent/replay_core/store.py +171 -0
- mobile_agent/result_blocks.py +941 -0
- mobile_agent/run.py +642 -0
- mobile_agent/run_delivery.py +123 -0
- mobile_agent/run_executor.py +200 -0
- mobile_agent/run_export.py +284 -0
- mobile_agent/run_gif.py +891 -0
- mobile_agent/runner_hints.py +86 -0
- mobile_agent/runner_renewal.py +205 -0
- mobile_agent/runtime_create.py +549 -0
- mobile_agent/runtime_driver.py +458 -0
- mobile_agent/runtime_limits.py +319 -0
- mobile_agent/runtime_queue.py +394 -0
- mobile_agent/runtime_runs.py +383 -0
- mobile_agent/runtime_status.py +181 -0
- mobile_agent/runtime_teach.py +295 -0
- mobile_agent/runtime_work.py +599 -0
- mobile_agent/screen_text.py +101 -0
- mobile_agent/secret_filter.py +113 -0
- mobile_agent/serve.py +160 -0
- mobile_agent/server.py +363 -0
- mobile_agent/service_support.py +42 -0
- mobile_agent/settle/__init__.py +1 -0
- mobile_agent/settle/predict.py +102 -0
- mobile_agent/settle/replay.py +92 -0
- mobile_agent/settle/store.py +93 -0
- mobile_agent/settle/stream.py +43 -0
- mobile_agent/setup_cli.py +225 -0
- mobile_agent/setup_errors.py +87 -0
- mobile_agent/setup_service.py +527 -0
- mobile_agent/share.py +104 -0
- mobile_agent/signing.py +184 -0
- mobile_agent/sim/__init__.py +6 -0
- mobile_agent/sim/__main__.py +26 -0
- mobile_agent/sim/api.py +952 -0
- mobile_agent/sim/apps.py +172 -0
- mobile_agent/sim/biometrics.py +84 -0
- mobile_agent/sim/cli.py +271 -0
- mobile_agent/sim/doctor.py +293 -0
- mobile_agent/sim/governor.py +90 -0
- mobile_agent/sim/holders.py +88 -0
- mobile_agent/sim/models.py +62 -0
- mobile_agent/sim/registry.py +161 -0
- mobile_agent/sim/simctl.py +269 -0
- mobile_agent/sim/wda.py +618 -0
- mobile_agent/sim/xcode.py +74 -0
- mobile_agent/skills/__init__.py +12 -0
- mobile_agent/skills/codes.py +597 -0
- mobile_agent/skills/notifications.py +281 -0
- mobile_agent/skills/open_url.py +127 -0
- mobile_agent/smart_events.py +588 -0
- mobile_agent/smart_result.py +490 -0
- mobile_agent/smart_run.py +413 -0
- mobile_agent/state.py +662 -0
- mobile_agent/states.py +217 -0
- mobile_agent/steering.py +70 -0
- mobile_agent/style.py +178 -0
- mobile_agent/supervised.py +234 -0
- mobile_agent/swift/helper_bt_hid.swift +534 -0
- mobile_agent/swift/helper_doc_text.swift +184 -0
- mobile_agent/swift/helper_draft_reply.swift +170 -0
- mobile_agent/swift/helper_map.swift +110 -0
- mobile_agent/swift/helper_mirroring.swift +1099 -0
- mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
- mobile_agent/swift/helper_pdf.swift +100 -0
- mobile_agent/swift/helper_phone_audio.swift +993 -0
- mobile_agent/swift/helper_phone_screen.swift +416 -0
- mobile_agent/swift/helper_screen_text.swift +24 -0
- mobile_agent/swift/helper_transfer.swift +246 -0
- mobile_agent/swift/ocr.swift +22 -0
- mobile_agent/task_policy.py +337 -0
- mobile_agent/team_signing/__init__.py +34 -0
- mobile_agent/team_signing/apple_ca.py +37 -0
- mobile_agent/team_signing/bundle.py +136 -0
- mobile_agent/team_signing/cli.py +208 -0
- mobile_agent/team_signing/identity.py +248 -0
- mobile_agent/team_signing/ledger.py +105 -0
- mobile_agent/team_signing/prebuilt.py +61 -0
- mobile_agent/team_signing/resign.py +256 -0
- mobile_agent/team_signing/service.py +382 -0
- mobile_agent/team_signing/tooling.py +243 -0
- mobile_agent/team_signing/writes.py +268 -0
- mobile_agent/terminal_image.py +81 -0
- mobile_agent/testkit/__init__.py +1 -0
- mobile_agent/testkit/a11y_baseline.py +132 -0
- mobile_agent/testkit/api.py +403 -0
- mobile_agent/testkit/capsule.py +400 -0
- mobile_agent/testkit/capsule_cli.py +57 -0
- mobile_agent/testkit/cli.py +948 -0
- mobile_agent/testkit/cli_arguments.py +193 -0
- mobile_agent/testkit/comparison.py +288 -0
- mobile_agent/testkit/comparison_cli.py +41 -0
- mobile_agent/testkit/discover.py +257 -0
- mobile_agent/testkit/editor.py +243 -0
- mobile_agent/testkit/execute.py +515 -0
- mobile_agent/testkit/github_checks.py +248 -0
- mobile_agent/testkit/gitref.py +66 -0
- mobile_agent/testkit/heal_review.py +176 -0
- mobile_agent/testkit/health.py +480 -0
- mobile_agent/testkit/history.py +255 -0
- mobile_agent/testkit/hosted_results.py +321 -0
- mobile_agent/testkit/input_files.py +173 -0
- mobile_agent/testkit/jobs.py +285 -0
- mobile_agent/testkit/junit_verdict.py +33 -0
- mobile_agent/testkit/maestro.py +802 -0
- mobile_agent/testkit/mcp.py +562 -0
- mobile_agent/testkit/mcp_comparison.py +88 -0
- mobile_agent/testkit/minimize.py +471 -0
- mobile_agent/testkit/minimizer_cli.py +48 -0
- mobile_agent/testkit/notify.py +212 -0
- mobile_agent/testkit/phone_video.py +191 -0
- mobile_agent/testkit/pr.py +491 -0
- mobile_agent/testkit/pr_report.py +562 -0
- mobile_agent/testkit/prbuild.py +148 -0
- mobile_agent/testkit/proof_report.py +55 -0
- mobile_agent/testkit/provenance.py +119 -0
- mobile_agent/testkit/prrun.py +244 -0
- mobile_agent/testkit/recent.py +166 -0
- mobile_agent/testkit/recipes.py +146 -0
- mobile_agent/testkit/record.py +204 -0
- mobile_agent/testkit/register.py +288 -0
- mobile_agent/testkit/reports.py +998 -0
- mobile_agent/testkit/repro.py +432 -0
- mobile_agent/testkit/scenario.py +440 -0
- mobile_agent/testkit/scenario_cli.py +37 -0
- mobile_agent/testkit/snapshots_cli.py +60 -0
- mobile_agent/testkit/suite.py +670 -0
- mobile_agent/testkit/targets.py +497 -0
- mobile_agent/testkit/xctest.py +378 -0
- mobile_agent/threads/__init__.py +1 -0
- mobile_agent/threads/chat_once.py +264 -0
- mobile_agent/threads/cli.py +740 -0
- mobile_agent/threads/client.py +326 -0
- mobile_agent/threads/context.py +246 -0
- mobile_agent/threads/mcp.py +739 -0
- mobile_agent/threads/recent.py +34 -0
- mobile_agent/threads/register.py +54 -0
- mobile_agent/threads/routes.py +188 -0
- mobile_agent/threads/service.py +975 -0
- mobile_agent/threads/store.py +361 -0
- mobile_agent/threads/summary.py +117 -0
- mobile_agent/threads/term.py +101 -0
- mobile_agent/threads/workflow_cli.py +104 -0
- mobile_agent/tls.py +36 -0
- mobile_agent/traces.py +271 -0
- mobile_agent/tracks.py +235 -0
- mobile_agent/transport.py +271 -0
- mobile_agent/triggers/__init__.py +1 -0
- mobile_agent/triggers/actions.py +204 -0
- mobile_agent/triggers/model.py +146 -0
- mobile_agent/triggers/register.py +29 -0
- mobile_agent/triggers/routes.py +101 -0
- mobile_agent/triggers/service.py +387 -0
- mobile_agent/triggers/store.py +142 -0
- mobile_agent/tui/__init__.py +89 -0
- mobile_agent/tui/app.py +582 -0
- mobile_agent/tui/chrome.py +321 -0
- mobile_agent/tui/commands.py +375 -0
- mobile_agent/tui/demo_phone.py +369 -0
- mobile_agent/tui/images.py +29 -0
- mobile_agent/tui/keys.py +64 -0
- mobile_agent/tui/prompt.py +195 -0
- mobile_agent/tui/render.py +395 -0
- mobile_agent/tui/screens.py +180 -0
- mobile_agent/tui/session.py +308 -0
- mobile_agent/tui/tasks.py +287 -0
- mobile_agent/tui/theme.py +118 -0
- mobile_agent/tui/widgets.py +272 -0
- mobile_agent/update_check.py +213 -0
- mobile_agent/usage.py +184 -0
- mobile_agent/verify/__init__.py +0 -0
- mobile_agent/verify/__main__.py +41 -0
- mobile_agent/verify/a11y.py +189 -0
- mobile_agent/verify/actions.py +290 -0
- mobile_agent/verify/assertions.py +570 -0
- mobile_agent/verify/audit.py +312 -0
- mobile_agent/verify/business.py +221 -0
- mobile_agent/verify/business_run.py +131 -0
- mobile_agent/verify/business_state.py +168 -0
- mobile_agent/verify/check_schema.py +398 -0
- mobile_agent/verify/checks.py +614 -0
- mobile_agent/verify/cli.py +423 -0
- mobile_agent/verify/conditions.py +483 -0
- mobile_agent/verify/crashes.py +321 -0
- mobile_agent/verify/device_audit.py +246 -0
- mobile_agent/verify/dialogs.py +287 -0
- mobile_agent/verify/evidence.py +705 -0
- mobile_agent/verify/fixtures.py +305 -0
- mobile_agent/verify/flow.py +213 -0
- mobile_agent/verify/l10n.py +141 -0
- mobile_agent/verify/netcerts.py +67 -0
- mobile_agent/verify/netproxy.py +356 -0
- mobile_agent/verify/netrun.py +81 -0
- mobile_agent/verify/netshim.m +28 -0
- mobile_agent/verify/netshim.py +48 -0
- mobile_agent/verify/netstubs.py +132 -0
- mobile_agent/verify/overlay.py +174 -0
- mobile_agent/verify/perf.py +239 -0
- mobile_agent/verify/proof.py +192 -0
- mobile_agent/verify/replay_admission.py +48 -0
- mobile_agent/verify/report.py +952 -0
- mobile_agent/verify/run_cleanup.py +67 -0
- mobile_agent/verify/run_links.py +48 -0
- mobile_agent/verify/run_perf.py +44 -0
- mobile_agent/verify/run_secrets.py +39 -0
- mobile_agent/verify/runner.py +976 -0
- mobile_agent/verify/runs.py +57 -0
- mobile_agent/verify/screen_coverage.py +181 -0
- mobile_agent/verify/smart.py +106 -0
- mobile_agent/verify/snapshot_run.py +114 -0
- mobile_agent/verify/snapshots.py +306 -0
- mobile_agent/verify/sound.py +335 -0
- mobile_agent/verify/source_observation.py +130 -0
- mobile_agent/verify/system_surfaces/__init__.py +7 -0
- mobile_agent/verify/system_surfaces/play.py +106 -0
- mobile_agent/verify/system_surfaces/spec.py +139 -0
- mobile_agent/verify/system_surfaces/stage.py +138 -0
- mobile_agent/verify/system_surfaces/surfaces.py +121 -0
- mobile_agent/verify/tree.py +223 -0
- mobile_agent/verify/verdict.py +377 -0
- mobile_agent/verify/wire.py +392 -0
- mobile_agent/verify/wirestore.py +176 -0
- mobile_agent/waiting/__init__.py +1 -0
- mobile_agent/waiting/contacts.py +73 -0
- mobile_agent/waiting/drafts.py +136 -0
- mobile_agent/waiting/job.py +145 -0
- mobile_agent/waiting/messages.py +174 -0
- mobile_agent/waiting/rank.py +115 -0
- mobile_agent/waiting/register.py +26 -0
- mobile_agent/watch.py +502 -0
- mobile_agent/wda/__init__.py +2 -0
- mobile_agent/wda/frame_clock.py +646 -0
- mobile_agent/wda/frame_source.py +325 -0
- mobile_agent/wda/frame_store.py +115 -0
- mobile_agent/wda/gate.py +163 -0
- mobile_agent/wda/gestures.py +291 -0
- mobile_agent/wda/guards.py +94 -0
- mobile_agent/wda/identity.py +55 -0
- mobile_agent/wda/observe.py +884 -0
- mobile_agent/wda/patches.py +856 -0
- mobile_agent/wda/point_gestures.py +150 -0
- mobile_agent/wda/source.py +32 -0
- mobile_agent/wda/system_dialogs.py +571 -0
- mobile_agent/wda/text.py +694 -0
- mobile_agent/wda/timeouts.py +77 -0
- mobile_agent/wda/video.py +505 -0
- mobile_agent/wda/watchdog.py +243 -0
- mobile_agent/wireless/__init__.py +15 -0
- mobile_agent/wireless/api.py +354 -0
- mobile_agent/wireless/cli.py +287 -0
- mobile_agent/wireless/devicectl.py +229 -0
- mobile_agent/wireless/exposure.py +33 -0
- mobile_agent/wireless/mobster-wifi-pair.c +122 -0
- mobile_agent/wireless/monitor.py +332 -0
- mobile_agent/wireless/network.py +334 -0
- mobile_agent/wireless/onboarding.py +293 -0
- mobile_agent/wireless/pair.py +124 -0
- mobile_agent/wireless/probe.py +322 -0
- mobile_agent/wireless/register.py +8 -0
- mobile_agent/wireless/relay.py +179 -0
- mobile_agent/wireless/result.py +87 -0
- mobile_agent/wireless/store.py +85 -0
- mobile_agent/wireless/switch.py +77 -0
- mobile_agent/wireless/transport.py +118 -0
- mobile_agent/wireless/words.py +81 -0
- mobile_agent/wireless/xctestrun.py +83 -0
- mobile_agent/workflow_tools.py +59 -0
- mobile_agent/workflows/__init__.py +2 -0
- mobile_agent/workflows/brief.py +201 -0
- mobile_agent/workflows/cron.py +110 -0
- mobile_agent/workflows/health.py +257 -0
- mobile_agent/workflows/history.py +126 -0
- mobile_agent/workflows/records.py +442 -0
- mobile_agent/workflows/runs.py +1000 -0
- mobile_agent/workflows/scheduler.py +134 -0
- mobile_agent/workflows/service.py +973 -0
- mobile_agent/workflows/starters.py +285 -0
- mobile_agent/workflows/triggers.py +248 -0
- mobile_agent/xray/__init__.py +1 -0
- mobile_agent/xray/app_cli.py +116 -0
- mobile_agent/xray/cli.py +21 -0
- mobile_agent/xray/crawl.py +754 -0
- mobile_agent/xray/diff.py +171 -0
- mobile_agent/xray/diff_cli.py +43 -0
- mobile_agent/xray/job.py +184 -0
- mobile_agent/xray/paywall.py +188 -0
- mobile_agent/xray/policy.py +305 -0
- mobile_agent/xray/register.py +43 -0
- mobile_agent/xray/report.py +283 -0
- mobile_agent/xray/screens.py +117 -0
- mobster_cli-0.4.3.dist-info/METADATA +93 -0
- mobster_cli-0.4.3.dist-info/RECORD +1009 -0
- mobster_cli-0.4.3.dist-info/WHEEL +5 -0
- mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
- mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
- mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
mobile_agent/watch.py
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
"""Watching what the iPhone plays (WATCH): a video, a Reel, a story, a voice note."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from . import listen, native_helpers
|
|
14
|
+
from .longform.frames import as_bytes
|
|
15
|
+
|
|
16
|
+
MAX_SECONDS = 120
|
|
17
|
+
DEFAULT_SECONDS = 15
|
|
18
|
+
# How often the screen is looked at while watching.
|
|
19
|
+
SAMPLE_HZ = 4
|
|
20
|
+
# The most frames the model is shown (one contact sheet).
|
|
21
|
+
MAX_FRAMES = 12
|
|
22
|
+
# Mean colour difference (0 to 1) from the last kept frame that makes a new scene, and the least time between two.
|
|
23
|
+
SCENE_CHANGE = 0.08
|
|
24
|
+
MIN_SCENE_GAP = 0.75
|
|
25
|
+
# A frame is kept at least this often while the picture moves at all, so a slow pan is still seen.
|
|
26
|
+
MAX_SCENE_GAP = 8.0
|
|
27
|
+
# Between two samples: nothing moved.
|
|
28
|
+
STILL = 0.012
|
|
29
|
+
# A frame this close to the first one, after the picture moved away from it: the video started over.
|
|
30
|
+
LOOP_MATCH = 0.035
|
|
31
|
+
MIN_LOOP_SECONDS = 3.0
|
|
32
|
+
# until_end: the picture still this long, and the sound quiet this long (when heard), ends the watch.
|
|
33
|
+
END_STILL_SECONDS = 3.0
|
|
34
|
+
END_QUIET_SECONDS = 2.5
|
|
35
|
+
# ...but never before this, and when nothing has been heard yet, not before HEAR_GRACE_SECONDS.
|
|
36
|
+
MIN_END_SECONDS = 4.0
|
|
37
|
+
HEAR_GRACE_SECONDS = 8.0
|
|
38
|
+
# How long to wait for the iPhone's audio to start recording before watching without it.
|
|
39
|
+
AUDIO_START_TIMEOUT = 8.0
|
|
40
|
+
# Opening and closing the phone's capture resets its USB network link, which ends WebDriverAgent's runner; a watch
|
|
41
|
+
# that captured brings it back before it returns (driver.after_capture), waiting at most this long.
|
|
42
|
+
LINK_SETTLE_SECONDS = 75.0
|
|
43
|
+
# The first time, macOS asks the person whether Mobster may capture the iPhone (listen.Session.asking): the watch
|
|
44
|
+
# waits this much longer for their answer, then watches without the sound (the prompt stays up for the next WATCH).
|
|
45
|
+
PERMISSION_WAIT = 60.0
|
|
46
|
+
# On-screen text: lines in the status bar (the top 6%) are left out, and lines shown in this share of the frames
|
|
47
|
+
# (with 3 or more) are the app around the media ("Following", a username), said once, not at every scene.
|
|
48
|
+
STATUS_BAR = 0.06
|
|
49
|
+
THROUGHOUT = 0.6
|
|
50
|
+
MIN_CONFIDENCE = 0.4
|
|
51
|
+
# Contact sheet: thumbnails this wide, at most this many a row.
|
|
52
|
+
SHEET_THUMB = 210
|
|
53
|
+
SHEET_COLUMNS = 6
|
|
54
|
+
|
|
55
|
+
THUMB_SIZE = (16, 32)
|
|
56
|
+
|
|
57
|
+
ENDED = {"time": "the time asked for ran out", "looped": "the video started over", "still": "the media stopped",
|
|
58
|
+
"stopped": "the task was stopped", "no_frames": "no picture came from the phone"}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class WatchError(RuntimeError):
|
|
62
|
+
"""``code``: devices_off or no_frames; the message says what happened and what to do."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, code, message):
|
|
65
|
+
super().__init__(message)
|
|
66
|
+
self.code = code
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def thumbnail(data):
|
|
70
|
+
"""A JPEG or PNG as THUMB_SIZE colour bytes (grey misses a cut between scenes of one brightness), or None."""
|
|
71
|
+
from PIL import Image
|
|
72
|
+
try:
|
|
73
|
+
image = Image.open(io.BytesIO(data))
|
|
74
|
+
image.draft("RGB", (THUMB_SIZE[0] * 4, THUMB_SIZE[1] * 4)) # JPEG: decoded at 1/8 scale
|
|
75
|
+
return image.convert("RGB").resize(THUMB_SIZE).tobytes()
|
|
76
|
+
except Exception:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def difference(a, b):
|
|
81
|
+
"""Mean absolute difference of two thumbnails, 0 (the same) to 1."""
|
|
82
|
+
if not a or not b or len(a) != len(b):
|
|
83
|
+
return 1.0
|
|
84
|
+
return sum(abs(x - y) for x, y in zip(a, b)) / (255 * len(a))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class PhoneFrames:
|
|
88
|
+
"""The phone's newest picture: the capture's while it records (WebDriverAgent is unreliable then), else live."""
|
|
89
|
+
|
|
90
|
+
FRESH = 1.0
|
|
91
|
+
|
|
92
|
+
def __init__(self, driver, clock=time.monotonic, capture=None):
|
|
93
|
+
self.driver, self.clock, self.capture = driver, clock, capture
|
|
94
|
+
self.last_capture = None
|
|
95
|
+
self.captured = 0
|
|
96
|
+
self.video = getattr(getattr(driver, "frame_clock", None), "video", None)
|
|
97
|
+
self.viewer = None
|
|
98
|
+
if self.video is not None and callable(getattr(self.video, "subscribe", None)):
|
|
99
|
+
try:
|
|
100
|
+
self.viewer = self.video.subscribe() # keeps the stream running while watching
|
|
101
|
+
except Exception:
|
|
102
|
+
self.viewer = None
|
|
103
|
+
self.last_sequence = None
|
|
104
|
+
self.screenshots = 0
|
|
105
|
+
|
|
106
|
+
def next(self):
|
|
107
|
+
"""(bytes, new) for the newest picture (``new`` False when the stream has nothing newer), or None."""
|
|
108
|
+
latest_frame = getattr(self.capture, "latest_frame", None)
|
|
109
|
+
if callable(latest_frame):
|
|
110
|
+
frame = latest_frame()
|
|
111
|
+
if frame is not None and self.clock() - frame[2] <= self.FRESH:
|
|
112
|
+
new = frame[0] != self.last_capture
|
|
113
|
+
self.last_capture = frame[0]
|
|
114
|
+
self.captured += 1 if new else 0
|
|
115
|
+
return bytes(frame[1]), new
|
|
116
|
+
if self.captured:
|
|
117
|
+
# The capture's picture stopped: WebDriverAgent is going down with the phone's link, so asking it
|
|
118
|
+
# would only wait out its timeouts. The last picture stands.
|
|
119
|
+
return None
|
|
120
|
+
latest = getattr(self.video, "latest", None)
|
|
121
|
+
if callable(latest):
|
|
122
|
+
try:
|
|
123
|
+
frame = latest()
|
|
124
|
+
except Exception:
|
|
125
|
+
frame = None
|
|
126
|
+
if (isinstance(frame, (tuple, list)) and len(frame) > 4 and isinstance(frame[2], (bytes, bytearray))
|
|
127
|
+
and isinstance(frame[4], (int, float)) and self.clock() - frame[4] <= self.FRESH):
|
|
128
|
+
new = frame[0] != self.last_sequence
|
|
129
|
+
self.last_sequence = frame[0]
|
|
130
|
+
return bytes(frame[2]), new
|
|
131
|
+
capture = getattr(self.driver, "capture_preview", None)
|
|
132
|
+
if not callable(capture):
|
|
133
|
+
return None
|
|
134
|
+
try:
|
|
135
|
+
data = capture(timeout=3)
|
|
136
|
+
except Exception:
|
|
137
|
+
return None
|
|
138
|
+
data = as_bytes(data)
|
|
139
|
+
if not data:
|
|
140
|
+
return None
|
|
141
|
+
self.screenshots += 1
|
|
142
|
+
return bytes(data), True
|
|
143
|
+
|
|
144
|
+
def close(self):
|
|
145
|
+
if self.viewer is not None:
|
|
146
|
+
try:
|
|
147
|
+
self.video.unsubscribe(self.viewer)
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
self.viewer = None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def pick(frames):
|
|
154
|
+
"""At most MAX_FRAMES frames in time order: the first, then those that changed the picture most."""
|
|
155
|
+
if len(frames) <= MAX_FRAMES:
|
|
156
|
+
return list(frames)
|
|
157
|
+
rest = sorted(frames[1:], key=lambda frame: frame["change"], reverse=True)[:MAX_FRAMES - 1]
|
|
158
|
+
return [frames[0]] + sorted(rest, key=lambda frame: frame["t"])
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def sample(source, seconds, *, until_end=False, audio=None, cancelled=lambda: False, clock=time.monotonic,
|
|
162
|
+
sleep=time.sleep):
|
|
163
|
+
"""Look at the screen SAMPLE_HZ times a second for up to ``seconds``: (frames, ended, samples)."""
|
|
164
|
+
start = clock()
|
|
165
|
+
frames, samples, ended = [], 0, "time"
|
|
166
|
+
first = previous = None
|
|
167
|
+
moved_away, still_since = False, None
|
|
168
|
+
tick = 1.0 / SAMPLE_HZ
|
|
169
|
+
while True:
|
|
170
|
+
now = clock() - start
|
|
171
|
+
if now >= seconds:
|
|
172
|
+
break
|
|
173
|
+
if cancelled():
|
|
174
|
+
ended = "stopped"
|
|
175
|
+
break
|
|
176
|
+
got = source.next()
|
|
177
|
+
if got is not None:
|
|
178
|
+
data, new = got
|
|
179
|
+
# A stream with no newer frame is a picture that didn't change.
|
|
180
|
+
thumb = thumbnail(data) if new or previous is None else previous
|
|
181
|
+
if thumb is not None:
|
|
182
|
+
samples += 1 if new else 0
|
|
183
|
+
if first is None:
|
|
184
|
+
first = thumb
|
|
185
|
+
frames.append({"t": round(now, 2), "data": data, "thumb": thumb, "change": 1.0})
|
|
186
|
+
else:
|
|
187
|
+
away = difference(thumb, first)
|
|
188
|
+
if until_end and moved_away and away < LOOP_MATCH and now >= MIN_LOOP_SECONDS:
|
|
189
|
+
ended = "looped" # the first picture again: what follows was seen already
|
|
190
|
+
break
|
|
191
|
+
moved_away = moved_away or away >= SCENE_CHANGE
|
|
192
|
+
change = difference(thumb, frames[-1]["thumb"])
|
|
193
|
+
gap = now - frames[-1]["t"]
|
|
194
|
+
if (change >= SCENE_CHANGE and gap >= MIN_SCENE_GAP) or (gap >= MAX_SCENE_GAP and change >= STILL):
|
|
195
|
+
frames.append({"t": round(now, 2), "data": data, "thumb": thumb, "change": change})
|
|
196
|
+
if difference(thumb, previous) < STILL:
|
|
197
|
+
still_since = now if still_since is None else still_since
|
|
198
|
+
else:
|
|
199
|
+
still_since = None
|
|
200
|
+
if until_end and now >= MIN_END_SECONDS and still_since is not None and \
|
|
201
|
+
now - still_since >= END_STILL_SECONDS and _quiet(audio, now):
|
|
202
|
+
ended = "still"
|
|
203
|
+
break
|
|
204
|
+
previous = thumb
|
|
205
|
+
wait = start + (int(now / tick) + 1) * tick - clock()
|
|
206
|
+
if wait > 0:
|
|
207
|
+
sleep(wait)
|
|
208
|
+
return frames, ended, samples
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _quiet(audio, now):
|
|
212
|
+
"""Whether the sound lets an until_end watch end"""
|
|
213
|
+
if audio is None:
|
|
214
|
+
return True
|
|
215
|
+
quiet = audio.quiet_for()
|
|
216
|
+
if quiet is None:
|
|
217
|
+
return now >= HEAR_GRACE_SECONDS
|
|
218
|
+
return quiet >= END_QUIET_SECONDS
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def read_lines(frames, *, helper=None, run=subprocess.run):
|
|
222
|
+
"""Each frame's lines of text, [[{text, rect, confidence}]], read on this Mac; None when it can't read them."""
|
|
223
|
+
from .attachments.extract import helper_path
|
|
224
|
+
executable = helper or helper_path()
|
|
225
|
+
if executable is None or not frames:
|
|
226
|
+
return None
|
|
227
|
+
work = Path(tempfile.mkdtemp(prefix="mobster-watch-text-"))
|
|
228
|
+
os.chmod(work, 0o700)
|
|
229
|
+
try:
|
|
230
|
+
paths = []
|
|
231
|
+
for index, frame in enumerate(frames):
|
|
232
|
+
path = work / f"{index}.img"
|
|
233
|
+
path.write_bytes(frame["data"])
|
|
234
|
+
paths.append(str(path))
|
|
235
|
+
try:
|
|
236
|
+
completed = run([str(executable), "lines", *paths], capture_output=True, text=True, timeout=60)
|
|
237
|
+
except (OSError, subprocess.SubprocessError):
|
|
238
|
+
return None
|
|
239
|
+
for line in reversed((completed.stdout or "").strip().splitlines()):
|
|
240
|
+
try:
|
|
241
|
+
answer = json.loads(line)
|
|
242
|
+
except ValueError:
|
|
243
|
+
continue
|
|
244
|
+
images = answer.get("images") if isinstance(answer, dict) and answer.get("ok") else None
|
|
245
|
+
if isinstance(images, list) and len(images) == len(frames):
|
|
246
|
+
return images
|
|
247
|
+
return None
|
|
248
|
+
finally:
|
|
249
|
+
shutil.rmtree(work, ignore_errors=True)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _plain(text):
|
|
253
|
+
return " ".join(str(text or "").split())
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def captions(lines_per_frame):
|
|
257
|
+
"""(new text per frame, text on screen the whole time)"""
|
|
258
|
+
kept = []
|
|
259
|
+
for rows in lines_per_frame:
|
|
260
|
+
texts = []
|
|
261
|
+
for row in rows if isinstance(rows, list) else ():
|
|
262
|
+
if not isinstance(row, dict):
|
|
263
|
+
continue
|
|
264
|
+
text, rect = _plain(row.get("text")), row.get("rect")
|
|
265
|
+
confidence = row.get("confidence")
|
|
266
|
+
if len(text) < 2 or (isinstance(confidence, (int, float)) and confidence < MIN_CONFIDENCE):
|
|
267
|
+
continue
|
|
268
|
+
if isinstance(rect, list) and len(rect) == 4 and isinstance(rect[1], (int, float)) and \
|
|
269
|
+
rect[1] + (rect[3] if isinstance(rect[3], (int, float)) else 0) <= STATUS_BAR:
|
|
270
|
+
continue
|
|
271
|
+
if text.casefold() not in (t.casefold() for t in texts):
|
|
272
|
+
texts.append(text)
|
|
273
|
+
kept.append(texts)
|
|
274
|
+
counts = {}
|
|
275
|
+
for texts in kept:
|
|
276
|
+
for text in {t.casefold() for t in texts}:
|
|
277
|
+
counts[text] = counts.get(text, 0) + 1
|
|
278
|
+
always = set()
|
|
279
|
+
if len(kept) >= 3:
|
|
280
|
+
always = {text for text, count in counts.items() if count >= THROUGHOUT * len(kept)}
|
|
281
|
+
# Vision may read "Following For You" as one line in one frame and two in another: parts of the app's own
|
|
282
|
+
# text are the app's too.
|
|
283
|
+
always |= {text for text in counts if any(text in whole for whole in always)}
|
|
284
|
+
throughout = []
|
|
285
|
+
for texts in kept:
|
|
286
|
+
for text in texts:
|
|
287
|
+
if text.casefold() in always and text not in throughout:
|
|
288
|
+
throughout.append(text)
|
|
289
|
+
throughout = [text for text in throughout
|
|
290
|
+
if not any(text.casefold() in other.casefold() and text != other for other in throughout)]
|
|
291
|
+
new, before = [], set()
|
|
292
|
+
for texts in kept:
|
|
293
|
+
shown = {t.casefold() for t in texts}
|
|
294
|
+
new.append([t for t in texts if t.casefold() not in always and t.casefold() not in before])
|
|
295
|
+
before = shown
|
|
296
|
+
return new, throughout
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def clock_text(seconds):
|
|
300
|
+
seconds = max(0, int(round(seconds)))
|
|
301
|
+
return f"{seconds // 60}:{seconds % 60:02d}"
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def contact_sheet(frames, *, hide_status_bar=False):
|
|
305
|
+
"""The frames side by side, each labelled with its time, as one JPEG; None without frames."""
|
|
306
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
307
|
+
images = []
|
|
308
|
+
for frame in frames:
|
|
309
|
+
try:
|
|
310
|
+
image = Image.open(io.BytesIO(frame["data"])).convert("RGB")
|
|
311
|
+
except Exception:
|
|
312
|
+
continue
|
|
313
|
+
height = max(1, round(image.height * SHEET_THUMB / max(1, image.width)))
|
|
314
|
+
image = image.resize((SHEET_THUMB, height))
|
|
315
|
+
if hide_status_bar: # a capture shows iOS's placeholder status bar (9:41, full battery): never read as real
|
|
316
|
+
ImageDraw.Draw(image).rectangle((0, 0, SHEET_THUMB, round(height * STATUS_BAR)), fill=(0, 0, 0))
|
|
317
|
+
images.append((frame["t"], image))
|
|
318
|
+
if not images:
|
|
319
|
+
return None
|
|
320
|
+
columns = min(SHEET_COLUMNS, len(images))
|
|
321
|
+
rows = -(-len(images) // columns)
|
|
322
|
+
cell = max(image.height for _, image in images)
|
|
323
|
+
gap = 6
|
|
324
|
+
sheet = Image.new("RGB", (columns * SHEET_THUMB + (columns + 1) * gap, rows * cell + (rows + 1) * gap),
|
|
325
|
+
(17, 17, 17))
|
|
326
|
+
draw = ImageDraw.Draw(sheet)
|
|
327
|
+
try:
|
|
328
|
+
font = ImageFont.load_default(size=22)
|
|
329
|
+
except TypeError: # Pillow before 10.1
|
|
330
|
+
font = ImageFont.load_default()
|
|
331
|
+
for index, (t, image) in enumerate(images):
|
|
332
|
+
x = gap + (index % columns) * (SHEET_THUMB + gap)
|
|
333
|
+
y = gap + (index // columns) * (cell + gap)
|
|
334
|
+
sheet.paste(image, (x, y))
|
|
335
|
+
label = clock_text(t)
|
|
336
|
+
box = draw.textbbox((0, 0), label, font=font)
|
|
337
|
+
draw.rectangle((x, y, x + box[2] - box[0] + 14, y + box[3] - box[1] + 12), fill=(0, 0, 0))
|
|
338
|
+
draw.text((x + 7 - box[0], y + 6 - box[1]), label, fill=(255, 255, 255), font=font)
|
|
339
|
+
out = io.BytesIO()
|
|
340
|
+
sheet.save(out, "JPEG", quality=72)
|
|
341
|
+
return out.getvalue()
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
ORDER = {"scene": 0, "sound": 1, "said": 2}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def timeline(frames, new_text, segments, sounds=()):
|
|
348
|
+
"""Scenes, the text that appeared with them, the sounds heard and what was said, in time order"""
|
|
349
|
+
rows = []
|
|
350
|
+
for index, frame in enumerate(frames):
|
|
351
|
+
shown = new_text[index] if new_text and index < len(new_text) else []
|
|
352
|
+
rows.append({"t": round(frame["t"], 1), "kind": "scene", "n": index + 1, "text": " | ".join(shown)})
|
|
353
|
+
for sound in sounds or ():
|
|
354
|
+
if sound["label"] != "speech" or not segments:
|
|
355
|
+
rows.append({"t": sound["start"], "end": sound["end"], "kind": "sound", "text": listen.sound_text(sound),
|
|
356
|
+
"confidence": sound["confidence"]})
|
|
357
|
+
for piece in segments or ():
|
|
358
|
+
rows.append({"t": piece["start"], "end": piece["end"], "kind": "said", "text": piece["text"]})
|
|
359
|
+
rows.sort(key=lambda row: (row["t"], ORDER[row["kind"]]))
|
|
360
|
+
return rows
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def timeline_text(rows, throughout=()):
|
|
364
|
+
"""The timeline as the lines the model reads."""
|
|
365
|
+
lines = []
|
|
366
|
+
for row in rows:
|
|
367
|
+
if row["kind"] == "scene":
|
|
368
|
+
lines.append(f"{clock_text(row['t'])} scene {row['n']}" + (f": on screen “{row['text']}”" if row["text"]
|
|
369
|
+
else ""))
|
|
370
|
+
elif row["kind"] == "sound":
|
|
371
|
+
lines.append(f"{clock_text(row['t'])}-{clock_text(row['end'])} sound: {row['text']} "
|
|
372
|
+
f"(confidence {row['confidence']:.2f})")
|
|
373
|
+
else:
|
|
374
|
+
lines.append(f"{clock_text(row['t'])}-{clock_text(row['end'])} said: “{row['text']}”")
|
|
375
|
+
if throughout:
|
|
376
|
+
lines.append("On screen the whole time: " + " | ".join(f"“{text}”" for text in throughout))
|
|
377
|
+
return "\n".join(lines)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _audio_started(audio, cancelled, clock):
|
|
381
|
+
"""Whether the recording started: within AUDIO_START_TIMEOUT"""
|
|
382
|
+
if audio.wait_started(AUDIO_START_TIMEOUT):
|
|
383
|
+
return True
|
|
384
|
+
deadline = clock() + PERMISSION_WAIT
|
|
385
|
+
while audio.asking() and not cancelled() and clock() < deadline:
|
|
386
|
+
if audio.wait_started(0.5):
|
|
387
|
+
return True
|
|
388
|
+
return False
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def watch(driver, seconds=DEFAULT_SECONDS, *, until_end=False, hear=True, udid=None, locale=None,
|
|
392
|
+
cancelled=lambda: False, session=None, ocr_helper=None, run=subprocess.run, clock=time.monotonic,
|
|
393
|
+
sleep=time.sleep):
|
|
394
|
+
"""Watch (and, on a USB iPhone with ``hear``, listen to) what the phone plays for ``seconds``"""
|
|
395
|
+
if not native_helpers.devices_allowed():
|
|
396
|
+
raise WatchError("devices_off", "Devices are off in this process (MOBSTER_NO_DEVICES=1).")
|
|
397
|
+
seconds = max(1, min(MAX_SECONDS, int(seconds)))
|
|
398
|
+
audio, hearing, reason = None, None, None
|
|
399
|
+
if hear:
|
|
400
|
+
try:
|
|
401
|
+
audio = (session(seconds) if session is not None
|
|
402
|
+
else listen.session_for(seconds, udid=udid, locale=locale, frames=True, clock=clock))
|
|
403
|
+
except listen.ListenError as error:
|
|
404
|
+
hearing, reason = str(error), error.code
|
|
405
|
+
try:
|
|
406
|
+
if audio is not None and not _audio_started(audio, cancelled, clock):
|
|
407
|
+
# It failed before recording (no iPhone on the cable, permission refused): watch without it.
|
|
408
|
+
try:
|
|
409
|
+
audio.result(timeout=10)
|
|
410
|
+
hearing, reason = "The iPhone's sound didn't start recording.", "not_started"
|
|
411
|
+
except listen.ListenError as error:
|
|
412
|
+
hearing, reason = str(error), error.code
|
|
413
|
+
audio.close()
|
|
414
|
+
audio = None
|
|
415
|
+
# The USB phone's capture runs from here: iOS shows a placeholder status bar (9:41) until it stops. Hearing
|
|
416
|
+
# through iPhone Mirroring opens no capture.
|
|
417
|
+
captured = audio is not None and getattr(audio, "opens_capture", True)
|
|
418
|
+
source = PhoneFrames(driver, clock=clock, capture=audio)
|
|
419
|
+
started = clock()
|
|
420
|
+
try:
|
|
421
|
+
frames, ended, samples = sample(source, seconds, until_end=until_end, audio=audio, cancelled=cancelled,
|
|
422
|
+
clock=clock, sleep=sleep)
|
|
423
|
+
finally:
|
|
424
|
+
source.close()
|
|
425
|
+
watched = round(clock() - started, 1)
|
|
426
|
+
heard_result = None
|
|
427
|
+
hear_ms = None
|
|
428
|
+
if audio is not None:
|
|
429
|
+
begun = clock()
|
|
430
|
+
try:
|
|
431
|
+
heard_result = audio.result()
|
|
432
|
+
except listen.ListenError as error:
|
|
433
|
+
hearing, reason = str(error), error.code
|
|
434
|
+
hear_ms = round((clock() - begun) * 1000)
|
|
435
|
+
finally:
|
|
436
|
+
if audio is not None:
|
|
437
|
+
audio.close()
|
|
438
|
+
settling = _Settle(driver, clock) if captured else None
|
|
439
|
+
try:
|
|
440
|
+
result = _finish(frames, ended, samples, watched, heard_result, hearing, reason, captured, source, hear_ms,
|
|
441
|
+
ocr_helper, run, clock)
|
|
442
|
+
finally:
|
|
443
|
+
if settling is not None:
|
|
444
|
+
settling.join(LINK_SETTLE_SECONDS)
|
|
445
|
+
result["captureFrames"] = source.captured
|
|
446
|
+
if settling is not None:
|
|
447
|
+
result["ms"]["settle"], result["phoneBack"] = settling.ms, settling.ok
|
|
448
|
+
return result
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class _Settle:
|
|
452
|
+
"""Brings WebDriverAgent back after the capture closed (it resets the phone's USB network link)."""
|
|
453
|
+
|
|
454
|
+
def __init__(self, driver, clock):
|
|
455
|
+
self.ok, self.ms, self.thread = None, None, None
|
|
456
|
+
after = getattr(driver, "after_capture", None)
|
|
457
|
+
if not callable(after):
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
def bring_back():
|
|
461
|
+
begun = clock()
|
|
462
|
+
try:
|
|
463
|
+
after()
|
|
464
|
+
self.ok = True
|
|
465
|
+
except Exception: # noqa: BLE001 -- the next step's own recovery takes over
|
|
466
|
+
self.ok = False
|
|
467
|
+
self.ms = round((clock() - begun) * 1000)
|
|
468
|
+
self.thread = threading.Thread(target=bring_back, name="mobster-watch-settle", daemon=True)
|
|
469
|
+
self.thread.start()
|
|
470
|
+
|
|
471
|
+
def join(self, timeout):
|
|
472
|
+
if self.thread is not None:
|
|
473
|
+
self.thread.join(timeout)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _finish(frames, ended, samples, watched, heard_result, hearing, reason, captured, source, hear_ms, ocr_helper,
|
|
477
|
+
run, clock):
|
|
478
|
+
if not frames:
|
|
479
|
+
raise WatchError("no_frames", "No picture came from the iPhone while watching. Check that it's connected and "
|
|
480
|
+
"unlocked, then try again.")
|
|
481
|
+
shown = pick(frames)
|
|
482
|
+
begun = clock()
|
|
483
|
+
lines = read_lines(shown, helper=ocr_helper, run=run)
|
|
484
|
+
text_ms = round((clock() - begun) * 1000)
|
|
485
|
+
new_text, throughout = captions(lines) if lines is not None else ([[] for _ in shown], [])
|
|
486
|
+
segments = heard_result.get("segments", []) if heard_result else []
|
|
487
|
+
if heard_result and heard_result.get("text") and not segments:
|
|
488
|
+
segments = [{"start": 0.0, "end": watched, "text": heard_result["text"]}]
|
|
489
|
+
sounds = heard_result.get("sounds", []) if heard_result else []
|
|
490
|
+
rows = timeline(shown, new_text, segments, sounds)
|
|
491
|
+
return {"seconds": watched, "ended": ended, "frames": [{"t": frame["t"]} for frame in shown],
|
|
492
|
+
"samples": samples, "sheet": contact_sheet(shown, hide_status_bar=captured),
|
|
493
|
+
"last": frames[-1]["data"], "timeline": rows, "captured": captured,
|
|
494
|
+
"throughout": throughout, "text": timeline_text(rows, throughout),
|
|
495
|
+
"transcript": heard_result.get("text", "") if heard_result else "",
|
|
496
|
+
"words": heard_result.get("words", 0) if heard_result else 0, "sounds": sounds,
|
|
497
|
+
"heard": heard_result is not None,
|
|
498
|
+
"silent": bool(heard_result and heard_result.get("silent")), "hearing": hearing,
|
|
499
|
+
"reason": None if heard_result is not None else reason,
|
|
500
|
+
**({"note": heard_result["note"]} if heard_result and heard_result.get("note") else {}),
|
|
501
|
+
"textRead": lines is not None, "screenshots": source.screenshots,
|
|
502
|
+
"ms": {"watch": round(watched * 1000), "text": text_ms, "hear": hear_ms}}
|