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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ coverage.xml
# Unleashed session transcripts (auto-generated, untracked)
data/unleashed/

# Silphe recordings — local biometric movement data, never published
recordings/
**/recordings/

# Agent-parked files (mv $file $file.bak / $file.parked-{timestamp})
# CLAUDE.md "Destroying uncommitted state" principle: the agent uses
# mv-to-bak instead of rm to preserve recoverability. Ignored here so
Expand Down
76 changes: 40 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,60 @@
# silphe
# Silphe

> One-line description of what this project does.
> 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.

## Overview
**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.

Brief (2-3 sentence) description of the project's purpose and value.
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.

## Status
## Why it's interesting

| Aspect | Status |
|--------|--------|
| Development | Active |
| Documentation | In Progress |
| Tests | None |
- **Everyone clones voices; nobody clones movement.** Your pointer path is as individual as a fingerprint — and far less guarded.
- **Predictive vs. reactive.** Track a smooth target and you ride it with ~zero lag. Chase an evasive one and you're ~200 ms behind — pure human reaction time. Silphe measures both, separately.
- **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.

## Quick Start
## The games (calibration in a clown costume)

```bash
# Installation
poetry install # or npm install
A green-garden field with four tasks:

- **Acquire** — hit the small gold target (Fitts's law: distance × size)
- **Track** — follow a slowly drifting dot (smooth pursuit)
- **Hold** — keep dead still on a single red pixel (tremor)
- **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

# Run
poetry run python src/main.py # or npm start
```bash
python src/silphe/calibrate.py # play (mouse)
python src/silphe/calibrate.py trackpad # tag the session as trackpad
```

## Project Structure
## See yourself

```
silphe/
├── src/ # Application source code
├── tests/ # Test suites
├── docs/ # Documentation
└── tools/ # Development utilities
```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)
```

## Documentation
Everything is pure standard library (tkinter + ctypes) — nothing to install to play.

- [Architecture](docs/adrs/) - Architecture Decision Records
- [Standards](docs/standards/) - Coding and process standards
- [Runbooks](docs/runbooks/) - Operational procedures
## The science, briefly

## Development
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).

This project follows [AssemblyZero](https://github.com/martymcenroe/AssemblyZero) conventions:
- Worktree isolation for all code changes
- Pre-merge gates (implementation + test reports)
- Session logging for agent continuity
## Privacy

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

PolyForm Noncommercial 1.0.0 - See LICENSE file.
## Install (soon)

---
```bash
pip install silphe
```

Coming — see the launch plan in [`docs/0001-launch-plan.md`](docs/0001-launch-plan.md).

## License

*Managed under [AssemblyZero](https://github.com/martymcenroe/AssemblyZero) governance.*
PolyForm Noncommercial 1.0.0 — see [LICENSE](LICENSE).
31 changes: 31 additions & 0 deletions docs/0001-launch-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 0001 — Silphe Launch Plan (GTM)

**Status:** draft (2026-06-11). Planning conventions reused from the boostgauge `blueprint/` methodology.

## Positioning
"Everyone's cloning voices; nobody's cloning movement." Silphe is a fun, local, privacy-first desktop toy that learns your pointer signature — with a serious second life as a longitudinal visuomotor / cognitive instrument.

## Channels
- **PyPI:** `pip install silphe`. Package name is secured (`silphe`). See the publish issue.
- **Landing page:** **thrivetech.ai/silphe** — what it is, the privacy promise, a GIF of Andvari, the install one-liner, a link to GitHub.
- **GitHub:** flip the repo public at launch (currently private); good README (done), screenshots/GIFs.
- **Show HN:** the hook — "I built a game that learns how you move the mouse — and can watch it drift when you're tired." Title candidates in the GTM issue.

## Support inboxes (on thrivetech.ai)
- `hello@thrivetech.ai` — general
- `support@thrivetech.ai` — user support
- `privacy@thrivetech.ai` — privacy / data requests (required by the privacy page)
- `security@thrivetech.ai` — vulnerability reports
- `press@thrivetech.ai` — media

## Collateral checklist
- [ ] Landing page (thrivetech.ai/silphe)
- [ ] Privacy page published (`docs/0002-privacy.md`)
- [ ] Screenshots + a short GIF (green garden, Andvari, the arc view)
- [ ] Show HN post draft
- [ ] PyPI long-description (the README)
- [ ] Support inboxes provisioned

## The two-sided story (keep the wall)
1. The **toy** — open-source, free fun. Ships openly: the capture, the games, the visualization, the arc.
2. The **applications** — impairment-aware action gating, decline / medication-effect detection, the patient-co-built cognitive game. These stay **IP**, never in the public repo. Patents filed in `patent-general`: #173, #174, #175, #177, #178. The published tool is the capture/visualization layer only.
21 changes: 21 additions & 0 deletions docs/0002-privacy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Silphe Privacy

**Short version: your movement data never leaves your computer.**

## What Silphe records
When you play, Silphe records the path of your pointer — positions and timestamps — and a few per-task numbers (reaction time, accuracy, tremor, tracking). That's all.

## Where it goes
Nowhere. It is written to a `recordings/` folder on your own machine and stays there. There is:
- **No cloud upload.**
- **No telemetry, no analytics, no "anonymized" exhaust.**
- **No account, no login, no network calls at all** for the core capture, analysis, and arc view.

## Why this matters
The exact wobble of your hand is about as personal as data gets — it can reveal fatigue, intoxication, and motor or cognitive change. That is precisely why Silphe is built local-first. Your signature is yours.

## Your data, your control
The recordings are plain text (JSONL) in `recordings/`. Read them, move them, or delete them whenever you like — they're just files on your disk.

## Contact
privacy@thrivetech.ai
23 changes: 23 additions & 0 deletions docs/0003-the-science.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 0003 — The Science

## Fitts's law
Time to acquire a target depends on its distance and size: `MT = a + b·log2(2D/W)`. The log term (the "index of difficulty," in bits) falls out of your overshoot-and-correct homing — you halve the remaining gap, halve it again, until you land. Your personal `a` (start/stop overhead) and `b` (cost per bit of difficulty) are part of your signature.

## Predictive vs. reactive tracking
A real, measurable distinction:
- **Smooth pursuit** of a *predictable* target → near-zero lag. You *predict* its path and ride it.
- **Reactive** tracking of an *evasive* target → ~200 ms lag. You can't predict it, so you *react* — and human visuomotor reaction is ~150–250 ms.

Silphe's **Track** task measures the first; the **Andvari** hunt measures the second. (First real session: ~7 ms lag on the smooth dot, ~230 ms on the roach — textbook. A clean separation of predictive from reactive control.)

## Tremor
Everyone has physiological tremor (~4–12 Hz); it shows in the steady-hold task. A mouse's mass and surface friction low-pass it (the mouse *hides* your tremor); a trackpad, with almost no inertia, reveals far more. So the **input device is a labeled variable** — same hand, different signature.

## The arc — and the metacognition of compensation
Over a night of play, reaction time can fall and tracking accuracy can rise. But getting *faster* is not the only way to improve, and Silphe must not confuse the two.

A player can get **better at predicting where the target tends to get stuck** — learning the board, the pattern — and use that to compensate for reaction time that is *not actually getting quicker.* That is **metacognition substituting strategy for speed.** It is, in particular, how an older brain stays sharp against the slow decline of raw reaction: not by reacting faster, but by needing to react less.

So the instrument's central question is not "are you scoring better?" but: **are you genuinely quicker, or have you just learned the room?** Telling earned speed from learned compensation — and watching which one carries a person as they age — is a core thing Silphe is built to study.

*(This distinction was the operator's own observation on night one, watching his roach-hunting scores climb while suspecting, correctly, that his reaction time hadn't moved at all.)*
3 changes: 3 additions & 0 deletions src/silphe/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Silphe — capture your own pointer-movement signature, and watch it drift. Local-first."""

__version__ = "0.1.0"
115 changes: 115 additions & 0 deletions src/silphe/analyze.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""
analyze.py — read the local calibration recordings and print AGGREGATE stats only.

No raw coordinates leave this script. It prints summary numbers (movement times,
overshoot, corrections, hold jitter + frequency, a Fitts fit) so we can fit the
movement model to the operator without dumping his every twitch.

poetry run python talos-mouse-host/analyze.py
"""

from __future__ import annotations

import glob
import json
import math
import os
import statistics as st

REC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "recordings")


def load():
trials = []
files = sorted(glob.glob(os.path.join(REC, "session-*.jsonl")))
for fp in files:
with open(fp, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line:
trials.append(json.loads(line))
return trials, len(files)


def acquire_stats(t):
s = t["samples"]
if len(s) < 3:
return None
tx, ty, r = t["target"]["x"], t["target"]["y"], t["target"]["r"]
hx, hy = t["home"]["x"], t["home"]["y"]
mt = s[-1][0] - s[0][0]
plen = sum(math.hypot(s[i + 1][1] - s[i][1], s[i + 1][2] - s[i][2])
for i in range(len(s) - 1))
straight = math.hypot(tx - hx, ty - hy)
eff = plen / straight if straight else 1.0
rev, prev, shrinking = 0, None, True
for (_t, x, y) in s:
d = math.hypot(x - tx, y - ty)
if prev is not None:
if d > prev + 0.5 and shrinking:
rev += 1
shrinking = False
elif d < prev - 0.5:
shrinking = True
prev = d
ID = math.log2(straight / (2 * r) + 1) # Shannon index of difficulty (bits)
return dict(mt=mt, eff=eff, rev=rev, err=t["click"]["err"], ID=ID)


def hold_stats(t):
s = t["samples"]
if len(s) < 10:
return None
tend = s[-1][0]
held = [p for p in s if p[0] >= tend - 1.5] or s # just the steady-hold tail
xs, ys = [p[1] for p in held], [p[2] for p in held]
jit = math.hypot(st.pstdev(xs), st.pstdev(ys))
mx = st.mean(xs)
dur = held[-1][0] - held[0][0]
crossings = sum(1 for i in range(len(xs) - 1) if (xs[i] - mx) * (xs[i + 1] - mx) < 0)
freq = (crossings / 2) / dur if dur > 0 else 0.0
return dict(jit=jit, freq=freq)


def main():
trials, nfiles = load()
if not trials:
print("No recordings found in", REC)
print("(Play a session: python talos-mouse-host/calibrate.py)")
return

acq = [a for a in (acquire_stats(t) for t in trials if t.get("kind") == "acquire") if a]
hold = [h for h in (hold_stats(t) for t in trials if t.get("kind") == "hold") if h]
col = lambda rows, k: [r[k] for r in rows]

print(f"Parsed {len(trials)} trials from {nfiles} session file(s).")

print(f"\n=== ACQUIRE - the'attempt' ({len(acq)} trials) ===")
if acq:
print(f" movement time : mean {st.mean(col(acq,'mt')):.2f}s "
f"(min {min(col(acq,'mt')):.2f}, max {max(col(acq,'mt')):.2f})")
print(f" final error : mean {st.mean(col(acq,'err')):.1f}px")
print(f" path wander : mean {st.mean(col(acq,'eff')):.2f}x the straight line")
print(f" corrections : mean {st.mean(col(acq,'rev')):.1f} per acquire "
f"(max {max(col(acq,'rev'))}) <-- the 'tremor in the attempt'")
print(f" difficulty : {min(col(acq,'ID')):.1f}-{max(col(acq,'ID')):.1f} bits (low = easy)")
ids, mts = col(acq, 'ID'), col(acq, 'mt')
mid, mmt = st.mean(ids), st.mean(mts)
denom = sum((i - mid) ** 2 for i in ids)
if denom > 0:
b = sum((ids[i] - mid) * (mts[i] - mmt) for i in range(len(ids))) / denom
a = mmt - b * mid
print(f" Fitts fit : MT = {a:.2f} + {b:.2f}*ID "
f"(your a={a*1000:.0f}ms base, b={b*1000:.0f}ms per bit)")

print(f"\n=== HOLD - thesteady tremor ({len(hold)} trials) ===")
if hold:
print(f" jitter amp : mean {st.mean(col(hold,'jit')):.2f}px while holding still")
print(f" dominant freq : mean {st.mean(col(hold,'freq')):.1f} Hz")
print(" per hold : " + ", ".join(
f"{h['jit']:.1f}px@{h['freq']:.0f}Hz" for h in hold))
print()


if __name__ == "__main__":
main()
Loading
Loading