{"id":22887634,"url":"https://github.com/dedinc/emunium","last_synced_at":"2026-03-12T10:19:42.682Z","repository":{"id":208725239,"uuid":"722351182","full_name":"DedInc/emunium","owner":"DedInc","description":"A Python module for automating interactions to mimic human behavior in standalone apps or browsers when using Selenium, Pyppeteer, or Playwright. Provides utilities to programmatically move the mouse cursor, click on page elements, type text, and scroll as if performed by a human user.","archived":false,"fork":false,"pushed_at":"2025-10-28T05:00:17.000Z","size":8652,"stargazers_count":90,"open_issues_count":0,"forks_count":7,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-28T06:21:48.498Z","etag":null,"topics":["automation","bypass-antibots","bypass-bot-detection-systems","captcha-bypass","human-interactions","playwright","puppeteer","pyppeteer","selenium-automation","selenium-test","simulation","standalone-automation","web-testing"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/emunium/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DedInc.png","metadata":{"files":{"readme":"README.MD","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-11-23T00:44:32.000Z","updated_at":"2025-10-28T05:50:41.000Z","dependencies_parsed_at":null,"dependency_job_id":"a4443643-ae2a-4cc7-9b49-2351f2b9e7cd","html_url":"https://github.com/DedInc/emunium","commit_stats":null,"previous_names":["dedinc/emunium"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/DedInc/emunium","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DedInc%2Femunium","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DedInc%2Femunium/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DedInc%2Femunium/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DedInc%2Femunium/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DedInc","download_url":"https://codeload.github.com/DedInc/emunium/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DedInc%2Femunium/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28659855,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-22T01:17:37.254Z","status":"online","status_checked_at":"2026-01-22T02:00:07.137Z","response_time":144,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","bypass-antibots","bypass-bot-detection-systems","captcha-bypass","human-interactions","playwright","puppeteer","pyppeteer","selenium-automation","selenium-test","simulation","standalone-automation","web-testing"],"created_at":"2024-12-13T20:37:24.342Z","updated_at":"2026-03-12T10:19:42.665Z","avatar_url":"https://github.com/DedInc.png","language":"Python","readme":"# Emunium\n\nHuman-like browser and desktop automation. No CDP, no WebDriver -- emunium drives Chrome through a custom WebSocket bridge and performs all mouse/keyboard actions at the OS level, making scripts indistinguishable from real user input. A standalone mode covers desktop apps via image template matching and OCR.\n\n![Preview](preview.gif)\n\n---\n\n## Table of Contents\n\n- [Installation](#installation)\n- [Browser mode](#browser-mode)\n- [Standalone mode](#standalone-mode)\n- [Waiting](#waiting)\n  - [Simple waits](#simple-waits)\n  - [Advanced waits with conditions](#advanced-waits-with-conditions)\n  - [Logical conditions (any\\_of, all\\_of, not\\_)](#logical-conditions)\n  - [Negative waits (detached/hidden)](#negative-waits)\n  - [Soft waits](#soft-waits)\n  - [Network waits](#network-waits)\n  - [Standalone waits (OCR/image)](#standalone-waits)\n- [Element API](#element-api)\n- [Querying elements](#querying-elements)\n- [Mouse interaction](#mouse-interaction)\n- [Keyboard interaction](#keyboard-interaction)\n- [Scrolling](#scrolling)\n- [JavaScript execution](#javascript-execution)\n- [Tab management](#tab-management)\n- [PageParser and Locator](#pageparser-and-locator)\n- [ClickType](#clicktype)\n- [Optional extras](#optional-extras)\n- [Advanced utilities](#advanced-utilities)\n- [ensure\\_chrome](#ensure_chrome)\n- [Notes and limitations](#notes-and-limitations)\n\n---\n\n## Installation\n\n```bash\npip install emunium\n```\n\nOptional extras:\n\n```bash\npip install \"emunium[standalone]\"   # image template matching (OpenCV + NumPy)\npip install \"emunium[ocr]\"          # EasyOCR text detection\npip install \"emunium[parsing]\"      # fast HTML parsing with selectolax\npip install \"emunium[keyboard]\"     # low-level keyboard input\n```\n\nChrome is downloaded automatically on first launch via `ensure_chrome()`.\n\n---\n\n## Browser mode\n\n```python\nfrom emunium import Browser, ClickType, Wait, WaitStrategy\n\nwith Browser(user_data_dir=\"my_profile\") as browser:\n    browser.goto(\"https://duckduckgo.com/\")\n\n    browser.type('input[name=\"q\"]', \"emunium automation\")\n    browser.click('button[type=\"submit\"]', click_type=ClickType.LEFT)\n\n    browser.wait(\n        \"a[data-testid='result-title-a']\",\n        strategy=WaitStrategy.STABLE,\n        condition=Wait().visible().text_not_empty().stable(duration_ms=500),\n        timeout=30,\n    )\n\n    print(browser.title, browser.url)\n\n    for link in browser.query_selector_all(\"a[data-testid='result-title-a']\")[:5]:\n        print(f\"  {link.text.strip()[:60]}  ({link.screen_x:.0f}, {link.screen_y:.0f})\")\n```\n\n`Browser` constructor:\n\n```python\nBrowser(\n    headless=False,\n    user_data_dir=None,   # persistent profile dir; temp dir if None\n    bridge_port=0,        # 0 = OS-assigned\n    bridge_timeout=60.0,  # seconds to wait for extension handshake\n)\n```\n\nProperties: `browser.url`, `browser.title`, `browser.bridge`.\n\n---\n\n## Standalone mode\n\n```python\nfrom emunium import Emunium, ClickType\n\nemu = Emunium()\nmatches = emu.find_elements(\"search_icon.png\", min_confidence=0.8)\nif matches:\n    emu.click_at(matches[0], ClickType.LEFT)\n\nfields = emu.find_elements(\"text_field.png\", min_confidence=0.85)\nif fields:\n    emu.type_at(fields[0], \"hello world\")\n```\n\nWith OCR:\n\n```python\nemu = Emunium(ocr=True, use_gpu=True, langs=[\"en\"])\nhits = emu.find_text_elements(\"Sign in\", min_confidence=0.8)\nif hits:\n    emu.click_at(hits[0])\n```\n\n---\n\n## Waiting\n\n### Simple waits\n\nAll raise `TimeoutError` on timeout:\n\n```python\nbrowser.wait_for_element(selector, timeout=10.0)\nbrowser.wait_for_xpath(xpath, timeout=10.0)\nbrowser.wait_for_text(text, timeout=10.0)\nbrowser.wait_for_idle(silence=2.0, timeout=30.0)\n```\n\n### Advanced waits with conditions\n\n`Wait()` is a fluent builder. Conditions are ANDed by default:\n\n```python\nbrowser.wait(\n    \"#results\",\n    strategy=WaitStrategy.STABLE,\n    condition=Wait().visible().text_not_empty().stable(500),\n    timeout=15,\n)\n```\n\nAvailable conditions:\n\n| Method | Description |\n|---|---|\n| `.visible()` | Non-zero dimensions, not `visibility:hidden` |\n| `.clickable()` | Visible, enabled, `pointer-events` not `none` |\n| `.stable(duration_ms=300)` | Bounding rect unchanged for N ms |\n| `.unobscured()` | Not covered by another element at center point |\n| `.hidden()` | Element exists but is not visible |\n| `.detached()` | Element removed from DOM or never appeared |\n| `.text_not_empty()` | Inner text is non-empty after trim |\n| `.text_contains(sub)` | Inner text includes substring |\n| `.has_attribute(name, value=None)` | Attribute present (optionally with value) |\n| `.without_attribute(name)` | Attribute absent |\n| `.has_class(name)` | CSS class present |\n| `.has_style(prop, value)` | Computed style property equals value |\n| `.count_gt(n)` | More than N matching elements in DOM |\n| `.count_eq(n)` | Exactly N matching elements in DOM |\n| `.custom_js(code)` | Custom JS expression; receives `el` argument |\n\n`WaitStrategy` values: `PRESENCE`, `VISIBLE`, `CLICKABLE`, `STABLE`, `UNOBSCURED`.\n\n### Logical conditions\n\nCombine conditions with OR/AND/NOT logic:\n\n```python\n# Wait for EITHER a success message OR a captcha box\nelement = browser.wait(\n    \"body\",\n    condition=Wait().any_of(\n        Wait().has_class(\"success-loaded\"),\n        Wait().text_contains(\"Verify you are human\")\n    ),\n    timeout=15,\n)\n\n# Explicit AND (same as chaining, but groups sub-conditions)\nbrowser.wait(\n    \"#panel\",\n    condition=Wait().all_of(\n        Wait().visible().text_not_empty(),\n        Wait().has_attribute(\"data-ready\", \"true\"),\n    ),\n)\n\n# NOT: wait until element is no longer disabled\nbrowser.wait(\n    \"#submit\",\n    condition=Wait().not_(Wait().has_attribute(\"disabled\")),\n)\n```\n\n### Negative waits\n\nWait for a loading spinner to be removed from the DOM:\n\n```python\nbrowser.click(\"#submit-btn\")\nbrowser.wait(\".loading-spinner\", condition=Wait().detached(), timeout=20)\n```\n\nWait for an element to become hidden (still in DOM but invisible):\n\n```python\nbrowser.wait(\".tooltip\", condition=Wait().hidden(), timeout=5)\n```\n\n### Soft waits\n\nCheck for something without crashing when it doesn't appear. Pass `raise_on_timeout=False` to get `None` instead of `TimeoutError`:\n\n```python\npromo = browser.wait(\n    \".promo-modal\",\n    condition=Wait().visible(),\n    timeout=3.0,\n    raise_on_timeout=False,\n)\nif promo:\n    promo.click()\n```\n\n### Network waits\n\nWait for a specific background API request to finish before proceeding. Uses glob-style pattern matching against response URLs:\n\n```python\nbrowser.click(\"#fetch-data\")\nresponse = browser.wait_for_response(\"*/api/v1/users*\", timeout=10.0)\nif response:\n    print(f\"API status: {response['statusCode']}\")\n```\n\n### Standalone waits\n\nPolling waits for the standalone (non-browser) mode. These call `find_elements` / `find_text_elements` in a loop:\n\n```python\nemu = Emunium()\n\n# Wait up to 10s for an image to appear on screen\nmatch = emu.wait_for_image(\"submit_button.png\", timeout=10.0, min_confidence=0.85)\nemu.click_at(match)\n\n# Wait for OCR text (requires ocr=True)\nemu_ocr = Emunium(ocr=True)\nhit = emu_ocr.wait_for_text_ocr(\"Payment Successful\", timeout=30.0)\nemu_ocr.click_at(hit)\n\n# Soft standalone wait -- returns None on timeout\nmaybe = emu.wait_for_image(\"optional.png\", timeout=3.0, raise_on_timeout=False)\n```\n\n---\n\n## Element API\n\n`Element` instances are returned by all query and wait methods.\n\nProperties: `tag`, `text`, `attrs`, `rect`, `screen_x`, `screen_y`, `center`, `visible`.\n\n```python\nelement.scroll_into_view()\nelement.hover(offset_x=None, offset_y=None, human=True)\nelement.move_to(offset_x=None, offset_y=None, human=True)\nelement.click(human=True)\nelement.double_click(human=True)\nelement.right_click(human=True)\nelement.middle_click(human=True)\nelement.type(text, characters_per_minute=280, offset=20, human=True)\nelement.drag_to(target, human=True)\nelement.focus()\nelement.get_attribute(name)\nelement.get_computed_style(prop)\nelement.refresh()  # re-query from page\n```\n\n---\n\n## Querying elements\n\n```python\nbrowser.query_selector(selector)       # -\u003e Element | None\nbrowser.query_selector_all(selector)   # -\u003e list[Element]\nbrowser.get_by_text(text, exact=False) # -\u003e list[Element]\nbrowser.get_all_interactive()          # -\u003e list[Element]\n```\n\n---\n\n## Mouse interaction\n\n```python\nbrowser.click(selector, click_type=ClickType.LEFT, human=True, timeout=10.0)\nbrowser.click_at(target, click_type=ClickType.LEFT, human=True, timeout=10.0)\nbrowser.move_to(target, offset_x=None, offset_y=None, human=True, timeout=10.0)\nbrowser.hover(target, ...)  # alias for move_to\nbrowser.drag_and_drop(source_selector, target_selector, human=True)\nbrowser.get_center(target)  # -\u003e {\"x\": int, \"y\": int}\n```\n\n`target` can be a CSS selector string or an `Element`.\n\n---\n\n## Keyboard interaction\n\n```python\nbrowser.type(selector, text, characters_per_minute=280, offset=20, human=True)\nbrowser.type_at(target, text, characters_per_minute=280, offset=20, human=True)\n```\n\nNon-ASCII text is pasted via clipboard (`pyperclip`). Install `emunium[keyboard]` for the `keyboard` library; otherwise `pyautogui` is used.\n\n---\n\n## Scrolling\n\n```python\nbrowser.scroll_to(element_or_selector)  # scroll element into viewport\nbrowser.scroll_to(x, y)                 # scroll to absolute pixel coords\n```\n\n---\n\n## JavaScript execution\n\n```python\nresult = browser.execute_script(\"return document.title\")\n```\n\n---\n\n## Tab management\n\n```python\nbrowser.new_tab(url=\"about:blank\")\nbrowser.close_tab(tab_id=None)\nbrowser.tab_info()  # -\u003e dict with url, title, tabId, status\nbrowser.page_info() # -\u003e scrollX, scrollY, innerWidth, innerHeight, readyState, ...\n```\n\n---\n\n## PageParser and Locator\n\nOffline HTML parsing with CSS selectors. No browser needed.\n\n```python\nfrom emunium import PageParser\n\nhtml = browser.execute_script(\"return document.documentElement.outerHTML\")\nparser = PageParser(html)\n\nlinks = parser.locator(\"a[href]\").all()\nbtn = parser.get_by_text(\"Sign in\", exact=True).first\ninputs = parser.get_by_role(\"textbox\").all()\nfield = parser.get_by_placeholder(\"Search\").first\nemail = parser.get_by_label(\"Email address\").first\nsubmit = parser.get_by_test_id(\"submit-btn\").first\n```\n\n`Locator` supports: `.first`, `.last`, `.nth(i)`, `.all()`, `.count()`, `.inner_text()`, `.get_attribute(name)`, `.filter(has_text=...)`.\n\nRequires `pip install \"emunium[parsing]\"`.\n\n---\n\n## ClickType\n\n```python\nfrom emunium import ClickType\n\nClickType.LEFT    # default\nClickType.RIGHT   # context menu\nClickType.MIDDLE\nClickType.DOUBLE\n```\n\n---\n\n## Optional extras\n\n| Extra | What it installs | What it unlocks |\n|---|---|---|\n| `standalone` | opencv-python, numpy | `find_elements()` image matching |\n| `ocr` | opencv-python, numpy, easyocr | `find_text_elements()` OCR |\n| `parsing` | selectolax | `PageParser` / `Locator` |\n| `keyboard` | keyboard | Low-level keystroke delivery |\n\n```bash\npip install \"emunium[standalone,parsing,keyboard]\"\n```\n\n---\n\n## Advanced utilities\n\n- `Bridge` -- the raw WebSocket transport to the Chrome extension. For custom messaging outside the `Browser` facade.\n- `CoordsStore` -- thread-safe cache for element coordinates across async workflows.\n- `ElementRecord` -- lightweight dataclass used by `CoordsStore`.\n\n---\n\n## ensure_chrome\n\n```python\nfrom emunium import ensure_chrome\n\npath = ensure_chrome()\n```\n\nDownloads the latest stable Chrome for Testing build for the current platform if not already present. Called automatically by `Browser.launch()`.\n\n---\n\n## Notes and limitations\n\n- Chrome only. The bridge extension targets Chrome/Chromium.\n- One active tab at a time. The bridge tracks a single pinned tab. `new_tab()` switches focus.\n- Parallel instances may conflict on the shared `port.json`. Use different `bridge_port` values.\n- Non-ASCII text is pasted via clipboard instead of typed keystroke-by-keystroke.\n- `headless=True` uses `--headless=new`. Coordinates still compute but the cursor is not visible. Use `human=False` in display-less environments.\n- Image matching uses multi-scale (0.9x, 1.0x, 1.1x) and multi-rotation (-10, 0, +10) search.\n\n---\n\n## License\n\nMIT\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdedinc%2Femunium","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdedinc%2Femunium","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdedinc%2Femunium/lists"}