Medical Research Skills for Claude Code, Codex, Cursor & Copilot
58 open-source skills for the full medical research lifecycle. Built by physicians, tested on real publications.
Your AI running trainer — pick a persona, press start, get coached.
Translation pending
HealthTypeScript · CSSGitHub @markt1600OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
An AI running trainer for your iPhone, built as a mobile web app. Pick a persona —
Angry Ah Beng and Ah Lian (scold you in Singlish), Coach Christine
(relentless encouragement), and two more — press Start, and get coached over
your own music.Read the full README ↗An AI running trainer for your iPhone, built as a mobile web app. Pick a persona — Angry Ah Beng and Ah Lian (scold you in Singlish), Coach Christine (relentless encouragement), and two more — press Start, and get coached over your own music.
Optional, like everything else: set GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
and AUTH_SECRET (see .env.example) and the app opens with a choice —
Continue with Google or Run as guest. Guests get exactly the
pre-accounts app, remembered so the question is asked once. Signed in, the
app opens on a home screen: Get Ready to Run on top, and below it a card
for every saved run — tap through for distance, average pace, moving time,
total elapsed time, fastest split, the route drawn on real OpenStreetMap
tiles (toned to match the paper stock), and a kilometre-split bar chart
(longer bar = faster split, fastest in the accent). Runs save automatically
at the finish line; anything under a minute or 50 m is dropped as a
pocket-start, and a run can be deleted from its detail page behind an
explicit confirmation.
Signing in also gives the trainer a memory: the run list is digested into last-run figures, personal bests and recency (lib/history.ts — one blob listing, no body fetches), and rides into every improvised line. Expect the intro to notice a four-day gap, or a mid-run remark when the pace closes in on a best. The shareable end-of-run card carries the same depth — route, headline numbers, a kilometre-splits strip with the fastest split called out, where the run happened, and the coach's closing line.
The account page also holds the runner's own stats — age, gender (female, male, or the "—" of not saying), height and weight, editable in either kg·cm or lb·in (stored metric; the toggle is just a display preference that follows the account). BMI is derived live from the two fields and shown read-only, together with its band under the Singapore Heart Foundation's Asian-population classification (underweight < 18.5, normal to 22.9, overweight to 27.4, obese from 27.5) — the bands are baked into the app, no lookup involved. When they're filled in, the trainer gets them alongside the live run stats and may weave a personal touch into its improvised lines — your name, mostly, with age or build as occasional seasoning. Guests have no account page fields; everything stays anonymous.
The admin screen (gated to ADMIN_EMAIL) also carries a user directory:
every account that has signed in, with run counts and last-seen, and a
click-through to each user's saved runs showing the trainer they picked and
the run's configured target (distance, treadmill time, or free run).
Profiles are written at sign-in, so accounts predating the registry appear
on their next login.
Storage is the same Vercel Blob store as the voice library — one JSON per run under an HMAC-derived per-user prefix, with the card's summary encoded in the pathname so listing a history fetches no file bodies. Auth is the classic Google authorization-code flow with a signed HttpOnly cookie — no auth library, no database.
npm install
npm run dev
The app is fully functional with no API keys (library phrases + on-device TTS).
npm test runs the simulation suites — the GPS tracker against jittered fake
fixes, the voice engine against iOS-style interruptions, the stale-audio
bookkeeping against a lagging CDN. npm run test:ui drives a production build
in a real browser. Every scenario in there earned its place by catching a real
bug; see tests/README.md.
| Variable | Purpose |
|---|---|
ANTHROPIC_API_KEY | Live phrase generation + push-to-talk replies (Claude Sonnet 5) |
ELEVENLABS_API_KEY | Voicing generated phrases, and batch-rendering the library |
ELEVENLABS_VOICE_AHBENG | ElevenLabs voice ID for Angry Ah Beng (falls back to a stock voice) |
ELEVENLABS_VOICE_AHLIAN | ElevenLabs voice ID for Ah Lian (falls back to a stock voice) |
ELEVENLABS_VOICE_COACH | ElevenLabs voice ID for Coach Christine (falls back to a stock voice) |
ELEVENLABS_VOICE_FLIRTY | ElevenLabs voice ID for Coach Cassie (falls back to a stock voice) |
ELEVENLABS_VOICE_LOANSHARK | ElevenLabs voice ID for Ah Long (falls back to a stock voice) |
BLOB_READ_WRITE_TOKEN | Auto-injected when you add a Vercel Blob store — enables first-launch in-app library rendering |
ADMIN_PIN | Protects the admin page and credit-spending endpoints (unset = open, for local dev) |
ADMIN_EMAIL | Hides the ⚙ Admin link from everyone but these Google accounts (comma-separated; needs sign-in configured) |
DEFAULT_PLAN | The plan for accounts with none pinned: full (default, pre-launch) or free (once the subscription is on sale) |
REVENUECAT_SECRET_KEY | RevenueCat secret API key — lets the server read who is subscribed (the sync route and the webhook) |
REVENUECAT_WEBHOOK_SECRET | The Authorization value you set on the RevenueCat webhook pointing at /api/plan/webhook |
NEXT_PUBLIC_SUPPORT_EMAIL | Shown as the contact on /privacy (without it the page points at the App Store listing's support link) |
NEXT_PUBLIC_REVENUECAT_IOS_KEY | RevenueCat public iOS SDK key — the shell configures the purchases plugin with it (public by design) |
Weather (Open-Meteo) and reverse geocoding (BigDataCloud) are keyless — the location/weather-aware phrases need no configuration.
Set the two persona voice IDs in lib/personas.ts to voices in your ElevenLabs
account (for Ah Beng, a cloned Singlish voice is chef's kiss), then:
ELEVENLABS_API_KEY=... npm run generate-library
This renders every un-rendered phrase into public/audio/<persona>/ and writes
a manifest.json per persona. It's idempotent — add phrases to
lib/phrases.ts and re-run to top up the library. Commit the MP3s (or put them
on Vercel Blob and adjust the URL in lib/coach.ts).
Push this repo, import it in Vercel, add the two env vars. That's it — the API routes run as serverless functions.
lib/coach.ts — decides what to say and when (milestones > pace events >
anecdotes > encouragement, with cooldowns and no-repeat shuffling).lib/audio.ts — audio session management (ambient ↔ transient for music
ducking), keep-alive loop, MP3 playback with TTS fallback, wake lock.lib/geo.ts — watchPosition with a Kalman position filter, Doppler-speed
distance integration, accuracy-scaled jitter/teleport gates and rolling 60s
pace. See "GPS accuracy" below.lib/enviro.ts — keyless weather (Open-Meteo) + reverse geocoding
(BigDataCloud), fetched from the first GPS fix, refreshed every 30 min.lib/phraseCategories.ts — which phrase categories the generator can top up,
what each one is for, and why the rest are fixed. Shared by the admin screen
and the expand endpoint so they can't disagree.app/api/phrase — Claude (claude-sonnet-5) writes one line
in-persona from the live run stats; ElevenLabs voices it; both degrade
gracefully.Shared with marktan.ai: warm newsprint ground, rust
accent, near-square corners, Fraunces for figures and headlines, Newsreader for
prose, JetBrains Mono for the wide-tracked uppercase labels that carry the
structure. Fonts are self-hosted via next/font rather than linked from Google,
because this is an installed PWA that runs outdoors on unreliable mobile data.
Two grounds, one system. :root is paper — setup, summary, admin. .theme-ink
inverts only the ground and ink tokens for the live run screen; the accent,
the type and the radii carry across untouched, so it reads as a mode rather than
a second theme. The run screen is dark on purpose: a full-brightness cream
screen glares on a night run and costs real OLED battery over an hour. The
always-on sleeve display stays true black for the same reason, only more so.
Two things to know before editing:
color is declared on .app, not only on body. Inheritance passes down the
computed colour, and body sits outside .theme-ink — declare it only on
body and the entire run screen inherits paper-dark ink onto an ink-dark
ground.--accent is the brand rust and owns all chrome and actions. A trainer's own
colour is --persona, injected per-run, and is used only where it marks
which trainer this is — the avatar tint, the selected card's edge, the route
line on the run card.A phone standing still still reports a position that wanders, and because that wander is a displacement it's always positive — sum it up naively and a stationary phone quietly clocks up a kilometre. Four things guard against it:
coords.speed comes off the
carrier phase, not from differencing positions, so it reads ~0 while you
stand still. Below STATIONARY_MPS (0.6 m/s) nothing accumulates at all.KALMAN_Q m/s between fixes, so a tight fix pulls the
estimate hard and a loose one barely moves it.Plus: a 4 s warm-up discard (cold-start fixes are the worst a receiver ever
produces), maximumAge: 0 so a cached fix is never replayed as new, and a
30 m accuracy ceiling for anything touching the route.
Targets come in three flavours (Setup → Target): a distance goal (3–15 km presets plus the half marathon) with percentage checkpoints and a talked run-in, a time goal (treadmill mode — no GPS at all), or a target pace (outdoor only) — no checkpoints, the coach just keeps checking you against the number every couple of minutes: praise while you hold it, a push when you're more than ~4% over it, silence in the narrow band between so a marginal pace doesn't get flip-flop commentary. Km markers fire in every mode.
Distance-target runs also carry a world-record moment: at the exact elapsed time where the marathon world-record holder — Kelvin Kiptum (2:00:35) for men, Ruth Chepngetich (2:09:56) for women, chosen by the account's gender with male as the default — would have finished your target at their record pace, the trainer drops a pre-rendered line: who they are, what they ran, and that they'd be crossing your finish line right now. One line per persona per preset distance per record holder, times baked into the recordings (lib/records.ts documents what to update if a record falls). On 5 and 10 km targets there's a second, earlier moment: the US high school national record holder's literal race time — Daniel Simmons' 13:26 and Jane Hedengren's 14:58 for 5 km, Rudy Chapa's 28:33 (1976!) and Mary Shea's 32:53 (1979) for 10 km — with Ah Beng and Ah Lian at their most merciless, since you've just been beaten by a schoolkid. On a male 10K, Chapa's 1976 time pips Kiptum's extrapolated split by two seconds, so the schoolboy finishes first and both lines fire back to back.
Once per run — somewhere in minutes 5–12 — a second trainer barges in
and the two argue for about 25 seconds: a fresh four-line script written
live from the run's actual stats (/api/cameo), each line voiced in its own
speaker's ElevenLabs voice. Never from a library, never the same twice, and
silently absent when the API keys aren't configured — a cameo that
half-happens is worse than none, so all four lines render or none play.
Simulated against synthetic noise, this holds a 10-minute stationary phone to 0 m of phantom distance with Doppler (13 m without), and tracks running, walking, intervals, tunnels and Doppler dropouts to within 2.5% of truth.
Distance used to be integrated from Doppler speed (a trapezoid over
coords.speed), on the argument that speed noise is symmetric where position
noise is not. A real 10 km run falsified it: the tracker's own accepted
position track measured 10.02 km while the trapezoid credited 9.74 — Doppler
dips at turns and gait noise integrate low, and the shortfall was a
consistent ~2.5–3% across runs. So the chord sum over accepted fixes is the
engine, full stop — a field run after the switch matched an Apple Watch to
0.6%, which retired the rollback toggle. Doppler is demoted to the three
jobs it is genuinely best at: declaring the phone stationary, driving the
live speed display, and crediting distance while good position fixes are
absent — an urban canyon doesn't freeze the counter, and the first chord
after re-acquire is reduced by whatever the outage already earned so no
stretch counts twice. The legacy trapezoid still runs in parallel as a
shadow reading — the summary's "correction added Xm" is the difference, and
the simulation suite keeps the legacy engine alive as its comparison
baseline. In simulation the chord engine tracks a sparse-fix (2–6 s) run to
within 3% — the case that read ~3% short on the trapezoid — holds a
stopped-mid-gap runner to ~0 phantom metres, and reads a 10-minute
stationary phone at 13 m.
Detection is only as good as the evidence available, which differs by device:
| Signal available | Pause detected | Resume detected |
|---|---|---|
| Doppler speed (normal iPhone outdoors) | ~2 s | ~2 s |
| iOS drops the speed solution while still | ~7–10 s | ~2 s |
| No speed at all (worst case) | ~10 s | ~5 s |
Three things make that usable:
Simulated across a 90-second tunnel at running pace, all four signal-loss shapes produce zero auto-pauses, and ten minutes of steady running produces no false pauses on any device profile.
transient ducks other audio; transient-solo interrupts it. Only ducking is
used, because only ducking can be reliably undone from a web app.
An interrupted app on iOS resumes when the interrupting session deactivates —
native code signals that with setActive(false, .notifyOthersOnDeactivation).
The Web Audio Session API exposes no equivalent, and the keep-alive loop that
stops Safari suspending the run means our session never falls silent by itself.
Solo mode was tried and left players paused for the rest of the run, so it is
gone rather than left as a trap.
Boosting the voice through a Web Audio gain node was tried as the alternative
and also removed. Routing an element through createMediaElementSource is
one-way, and on the target device the AudioContext reported running while
producing no audible output once the element was routed — so phrases "played"
perfectly, in silence, with nothing to fall back from. Guarding on the context
state wasn't enough, because the context lied.
The trainer now always plays straight off the audio element. If the voice is hard to make out, the levers are your music's own volume and the phone's media volume.