Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Changelog

All notable changes to Silphe are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/); versioning is
[Semantic Versioning](https://semver.org/).

## [0.1.0] — 2026-06-11

First public release: the reusable instrument.

### Added
- `silphe.model.MovementModel` — cross-platform generation of human-fidelity
pointer paths (ballistic overshoot, corrective sub-movements, continuous
micro-tremor, heavy-tailed dwell). Pure standard library; deterministic with a
seeded RNG.
- `silphe.cursor.HumanCursor` / `RobotCursor` — drive the real OS cursor with a
trusted (`isTrusted`) click on Windows. Import is safe on any platform;
driving is cleanly guarded off Windows.
- `silphe.analysis` — quantify a recorded session into an aggregate movement
signature: Fitts fit, corrective reversals, hold tremor (amplitude +
dominant frequency), and a tracking lag / offset / noise decomposition.
- Calibration game and dashboards as console entry points: `silphe-play`,
`silphe-arc`, `silphe-analyze`, `silphe-lag`, `silphe-demo`.
- Apache-2.0 license; PyPI publishing via OIDC Trusted Publishing (no stored
token).
336 changes: 201 additions & 135 deletions LICENSE

Large diffs are not rendered by default.

81 changes: 58 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@

> Your mouse has a signature as personal as your handwriting. Silphe learns it — and shows you how it moves, holds, hunts, and drifts over time.

**Silphe** (σίλφη — Ancient Greek for the small creature that runs in the dark) is a tiny, fun, **fully local** desktop game that captures how *you, specifically,* move a pointer. Not whether you hit the target — *how you miss it on the way there:* the overshoot, the correction, the tremor, the chase.
**Silphe** (σίλφη — Ancient Greek for the small creature that runs in the dark) is a tiny, **fully local** instrument for your own visuomotor signature: how *you, specifically,* move a pointer. Not whether you hit the target — *how you miss it on the way there:* the overshoot, the correction, the tremor, the chase.

It began as a mouse-calibration chore and turned into something more interesting: a privacy-first instrument for your own visuomotor signature, and how it changes.
It has two halves:

- a **library** that *generates* human-fidelity pointer movement and *quantifies* the movement you record, and
- a **game** that captures your movement while you play.

It began as a mouse-calibration chore and turned into something more interesting.

## Why it's interesting

Expand All @@ -13,7 +18,46 @@ It began as a mouse-calibration chore and turned into something more interesting
- **It drifts.** Reaction, accuracy, tremor, tracking — they shift with the time of day, fatigue, a new medication, and the years. Silphe plots the **arc**.
- **Your data never leaves your machine.** Local capture, local model, local analysis. No cloud, no telemetry. Your silly walk is nobody's business but yours.

## The games (calibration in a clown costume)
## Install

```bash
pip install silphe
```

Pure standard library — no third-party runtime dependencies. Generating and analyzing movement works on **any** OS; driving the real OS cursor (`silphe.cursor`) is Windows-only.

## Use the library

Generate a human-fidelity path — overshoot, corrections, tremor, dwell — on any platform:

```python
import random
from silphe import MovementModel

model = MovementModel(rng=random.Random(0)) # seed for reproducibility
path = model.plan(0, 0, 400, 250) # -> [(x, y, dt), ...]
```

Drive the real cursor with a trusted OS click (Windows):

```python
from silphe import HumanCursor
HumanCursor().click(960, 540)
```

Quantify a recorded session into an aggregate signature:

```python
from silphe import load_recordings, session_signature

trials, _ = load_recordings() # ~/.silphe/recordings by default
sig = session_signature(trials)
print(sig["acquire"]["fitts"], sig["hold"]["tremor_hz"], sig["track"]["lag_ms"])
```

See the [session schema](https://github.com/martymcenroe/silphe/blob/main/docs/0005-session-schema.md).

## Play (calibration in a clown costume)

A green-garden field with four tasks:

Expand All @@ -23,38 +67,29 @@ A green-garden field with four tasks:
- **Andvari** — hunt the roach through the maze: it runs the dark, hides under silver cells, and you switch tools (swatter → pick, press **T**) to flush it out and finish it

```bash
python src/silphe/calibrate.py # play (mouse)
python src/silphe/calibrate.py trackpad # tag the session as trackpad
silphe-play # play (mouse)
silphe-play trackpad # tag the session as a trackpad
```

## See yourself
Then see yourself:

```bash
python src/silphe/analyze.py # this session's aggregate signature
python src/silphe/analyze_lag.py # are you late? temporal lag vs spatial offset vs noise
python src/silphe/arc.py # the longitudinal dashboard — your fingerprint over time
python src/silphe/human_cursor.py # the cursor model: a human-fidelity move (Windows)
python src/silphe/range_demo.py # human vs robot cursor, side by side (Windows)
silphe-analyze # this session's aggregate signature
silphe-lag # are you late? temporal lag vs spatial offset vs noise
silphe-arc # the longitudinal dashboard — your fingerprint over time
silphe-demo # human vs robot cursor, side by side (Windows)
```

Everything is pure standard library (tkinter + ctypes) — nothing to install to play.
From a source checkout, the same modules run via `python -m silphe.calibrate`, `python -m silphe.analyze`, and so on.

## The science, briefly

Fitts's law, corrective sub-movements, physiological tremor (4–12 Hz), smooth-pursuit lag, and the difference between getting *faster* and merely *learning the board*. See [`docs/0003-the-science.md`](docs/0003-the-science.md).
Fitts's law, corrective sub-movements, physiological tremor (4–12 Hz), smooth-pursuit lag, and the difference between getting *faster* and merely *learning the board*. See [the science](https://github.com/martymcenroe/silphe/blob/main/docs/0003-the-science.md).

## Privacy

Local-first, always — your movement never leaves your computer. See [`docs/0002-privacy.md`](docs/0002-privacy.md).

## Install (soon)

```bash
pip install silphe
```

Coming — see the launch plan in [`docs/0001-launch-plan.md`](docs/0001-launch-plan.md).
Local-first, always — your movement never leaves your computer. See [the privacy note](https://github.com/martymcenroe/silphe/blob/main/docs/0002-privacy.md).

## License

PolyForm Noncommercial 1.0.0see [LICENSE](LICENSE).
[Apache-2.0](https://github.com/martymcenroe/silphe/blob/main/LICENSE)permissive, with a patent grant. Use it, fork it, build on it.
108 changes: 108 additions & 0 deletions docs/0004-pypi-publish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Publishing Silphe to PyPI (and actually reserving the name)

Silphe publishes via **OIDC Trusted Publishing**: GitHub Actions mints a
short-lived token and PyPI trusts it. **No API token is stored anywhere.**

This is the silphe-specific version of AssemblyZero runbook
`0934-pypi-trusted-publisher-setup.md`, with two corrections that runbook still
needs (tracked in AssemblyZero #1582 and #1583).

## The one thing everyone gets wrong

> **Registering a "pending publisher" does NOT reserve the name.**

PyPI's own docs:

> "A 'pending' publisher does not create a project or reserve a project's name
> until it is actually used to publish. If you create a 'pending' publisher but
> another user registers the project name before you actually publish to it,
> your 'pending' publisher will be invalidated."
>
> — <https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/>

**The only action that reserves `silphe` is publishing a real release.** Until
the first `v*.*.*` tag publishes successfully, the name is free for anyone to
take — and if they take it, your pending publisher silently invalidates.

## Credentials — what you actually need

- **PyPI has no "Sign in with GitHub."** Account login is username/email +
password + **2FA** (authenticator app or passkey) + recovery codes. It's in
your password manager from a prior project's setup — reuse that account.
- The GitHub connection in Step 1 is only the OIDC *publish* trust, never login.
- **You need zero API token** for this path. Nothing to generate or store.

## Pre-flight (done by the library PR)

- [x] `pyproject.toml` → `[project] name = "silphe"`, `version = "0.1.0"`
- [x] `.github/workflows/release.yml` present, `environment: pypi`, tag `v*.*.*`
- [x] `poetry build` produces a wheel + sdist
- [x] Library PR merged to `main`

## Step 1 — Register the pending publisher (browser, ~2 min)

1. Log in at <https://pypi.org/manage/account/publishing/> (your existing account).
2. Under **"Add a new pending publisher,"** fill in exactly:

| Field | Value |
|---|---|
| PyPI Project Name | `silphe` |
| Owner | `martymcenroe` |
| Repository name | `silphe` |
| Workflow filename | `release.yml` |
| Environment name | `pypi` |

3. Click **Add**. This configures trust. It does **not** yet reserve the name —
Step 2 does.

## Step 2 — Publish (the tag push that reserves the name)

From `main`, with the tag matching `pyproject.toml`'s version:

```bash
git tag v0.1.0
git push origin v0.1.0
```

Watch it:

```bash
gh run watch --repo martymcenroe/silphe
```

The workflow builds the distributions and publishes via OIDC. On success the
release is live at <https://pypi.org/project/silphe/0.1.0/> within seconds, the
name is **now reserved**, and the pending publisher promotes to a permanent
trusted publisher.

## Step 3 — Verify

```bash
pip install silphe
python -c "import silphe; print(silphe.__version__)" # 0.1.0
silphe-analyze # entry point resolves
```

## Subsequent releases

Bump the version, tag, push the tag. No browser steps ever again.

```bash
poetry version patch
git commit -am "chore: bump to $(poetry version -s)"
git tag "v$(poetry version -s)"
git push origin main "v$(poetry version -s)"
```

## If it fails

| Symptom | Cause | Fix |
|---|---|---|
| `Trusted publisher not found` | Step 1 not done, or a field mismatched | Re-check owner / repo / `release.yml` / `pypi` match exactly |
| `Project name is not available` | Someone else already took `silphe` | The name is gone; choose another in `pyproject.toml` and re-register |
| Workflow never starts | Tag isn't `v*.*.*` | `v0.1.0` matches; `0.1.0` does not |

## References

- AssemblyZero `0934-pypi-trusted-publisher-setup.md` — the fleet runbook (corrections tracked in AZ #1582, #1583)
- PyPI Trusted Publishers — <https://docs.pypi.org/trusted-publishers/>
57 changes: 57 additions & 0 deletions docs/0005-session-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Session recording schema

A **session** is a JSON Lines file named `session-<unixtime>-<device>.jsonl`,
written by the calibration game (`silphe-play`) into the recordings directory
(`$SILPHE_RECORDINGS`, default `~/.silphe/recordings`). Each line is one JSON
object — a **trial**.

Load them with `silphe.analysis.load_session(path)` or
`silphe.analysis.load_recordings()`.

## Common keys (every trial)

| Key | Type | Meaning |
|---|---|---|
| `kind` | string | `"acquire"`, `"track"`, `"hold"`, or `"evasive"` |
| `samples` | `[[t, x, y], ...]` | the cursor trace; `t` = seconds from trial start, `x`/`y` = screen pixels |
| `reaction_s` | number | seconds to first movement |
| `device` | string | `"mouse"`, `"trackpad"`, … (as tagged at launch) |
| `os` | string | `platform.system()` |

## Per kind

**acquire** — hit a small target.

| Key | Meaning |
|---|---|
| `target` | `{x, y, r}` — target center + radius |
| `home` | `{x, y}` — where the cursor started |
| `click` | `{x, y, err}` — click point + miss distance (px) |

**hold** — stay still on a pixel (tremor test).

| Key | Meaning |
|---|---|
| `target` | `{x, y, r}` — the pixel + tolerance |

**track** — follow a smoothly drifting dot (smooth pursuit).

| Key | Meaning |
|---|---|
| `dot` | `[[t, x, y], ...]` — the target's trace |
| `locked_at` | seconds when the player first locked on (analysis uses only the post-lock tail) |
| `on_target_pct` | percent of post-lock time on the dot |

**evasive** ("Andvari") — hunt a maze roach.

| Key | Meaning |
|---|---|
| `path` | `[[t, x, y], ...]` — the roach's trace |
| `hits` | total hits to kill it |
| `switches` | `[[t, tool], ...]` — tool switches |

## Privacy

These files are local biometric data. The repo's `recordings/` directory is
gitignored and never leaves your machine. The analysis helpers return aggregate
numbers only — never raw coordinates.
4 changes: 2 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 37 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,28 +1,61 @@
[project]
name = "silphe"
version = "0.1.0"
description = ""
description = "Generate and quantify human-fidelity pointer movement — your mouse's signature, captured locally."
authors = [
{name = "Marty McEnroe",email = "cto@thrivetech.ai"}
{name = "Marty McEnroe", email = "cto@thrivetech.ai"}
]
license = {text = "PolyForm-Noncommercial-1.0.0"}
license = "Apache-2.0"
license-files = ["LICENSE"]
readme = "README.md"
requires-python = ">=3.10,<4.0"
dependencies = [
keywords = [
"human cursor", "mouse movement", "pointer", "fitts law", "visuomotor",
"biometrics", "tremor", "bot detection", "automation", "human-like",
]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Scientific/Engineering :: Human Machine Interfaces",
"Topic :: Software Development :: Testing",
"Typing :: Typed",
]
dependencies = []

[project.urls]
Homepage = "https://thrivetech.ai/silphe"
Repository = "https://github.com/martymcenroe/silphe"
Issues = "https://github.com/martymcenroe/silphe/issues"

[project.scripts]
silphe-play = "silphe.calibrate:main"
silphe-arc = "silphe.arc:main"
silphe-analyze = "silphe.analyze:main"
silphe-lag = "silphe.analyze_lag:main"
silphe-demo = "silphe.range_demo:main"

[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"

[tool.poetry]
packages = [{include = "silphe", from = "src"}]

[dependency-groups]
dev = [
"pytest (>=9.0.3,<10.0.0)",
"pytest-cov (>=7.1.0,<8.0.0)"
]

[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
Expand Down
Loading
Loading