Pin patient speech to a body map and auto-generate physical therapy charts
Translation pending
AI summary · README
HealthJavaScript · CSSGitHub @scudmeister-dotcomOnlineOwner 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.
A streamlined EMR for physical therapy clinics, built around one core idea:
the therapist just talks, and the chart writes itself. While the patient
speaks, TheraChart pins what they say onto a body map, files measurements into
the right sections, keeps the full word-for-word transcript, and lets you
click any finding to see exactly where it was said.
New here? Read ABOUT.md — what TheraChart is, what it gives
physical therapists, and why it beats a hand-written paper chart. It's the
heart and soul of the project.Read the full README ↗A streamlined EMR for physical therapy clinics, built around one core idea: the therapist just talks, and the chart writes itself. While the patient speaks, TheraChart pins what they say onto a body map, files measurements into the right sections, keeps the full word-for-word transcript, and lets you click any finding to see exactly where it was said.
New here? Read ABOUT.md — what TheraChart is, what it gives physical therapists, and why it beats a hand-written paper chart. It's the heart and soul of the project.
Runs on phone, iPad, or computer as a responsive web app. Deploy to Vercel, self-host a clinic server, or serve it statically — see DEPLOY.md.
Clinic mode (shared database, recommended) — run the included zero-dependency server on one machine in the clinic:
node server.js # serves the app + shared database on :8080
Open http://<that-machine>:8080 from every device. Logins authenticate
server-side, changes sync between devices within seconds, and the reminder
scheduler runs every minute (set REMINDER_WEBHOOK=<url> to forward each due
reminder as JSON to a real SMS/email gateway; otherwise reminders are marked
sent and logged). Data lives in data/therachart.json on that machine —
hardware your facility controls, never a third-party cloud.
On-device mode — serve the folder statically and each device keeps its own records (the sync badge shows which mode you're in):
python3 -m http.server 8000
# open http://localhost:8000 in Chrome or Edge
Sign in with any demo account (PIN 1234):
| Account | Role | Demonstrates |
|---|---|---|
| Maria Santos, PT | Therapist | Full documentation workflow |
| Jose Ramirez, PT | Therapist | Expired license — EMR access blocked |
| Carlo Mendoza, PT | Therapist | Access voided — cannot sign in |
| Ana Dela Cruz | Front desk | Intake + scheduling, no clinical docs |
| Grace Lim, PT | Admin | Facility settings, staff licenses |
Aim it at a section. Every narrative box on a note carries its own 🎤 Dictate button. Press the one on Subjective and what you say goes into Subjective — not wherever the classifier decides it belongs. The dictation bar says where the mic is pointed for as long as it is open, and pressing another section's button re-aims the same microphone rather than opening a second one.
This answers the failure that costs the most time: when automatic filing puts a sentence in the wrong section, finding it and moving it costs more than typing the note would have — and it is wrong most often on the one question it cannot see, whether a line is the patient's report or the therapist's observation. A therapist holding the mic at a section has already answered that, so a stated target skips the classifier rather than being fed to it as one more hint.
Or let it roam. The Listen & dictate live button in the dictation bar is unchanged, and still the right tool while the patient is talking and nobody is holding a screen: it routes sentence by sentence into every section a spoken paragraph touches. Measurements, body-map pins and outcome scores are extracted the same way whichever button is open — and a value that reached a table is never also written into the prose, so one finding can't sit in two places free to disagree.
Open a patient, start a Daily note / Evaluation / Progress report, press 🎤 Listen, and speak — in English & Tagalog or English & Cebuano, whichever pairing the dictation bar is set to (both languages of the pairing are transcribed together, code-switching included; the body-part lexicon understands all three languages at once):
PT01–PT06, OT01–OT06,
ST01–ST06), plus the A01–A04 equipment add-ons. A line is a code and
a number of units; the sheet totals to a . Prices are per
clinic and set by an — they start
empty, and a code with no price is left out of the subtotal rather than
counted as free. The price is stamped onto each line as it is entered, so a
signed note keeps the money it was signed for when the price list moves.Dictation is Google Cloud Speech-to-Text — Chirp 2, and there is no engine
picker: the page records short WAV segments and posts them to the server's
/api/stt, which proxies to Google under your Google Cloud BAA. Audio is
held only in memory and sent immediately — never written to the device.
The dictation bar's one choice is what you'll be speaking: English &
Tagalog (the default) or English & Cebuano. Each sends a single language
code (fil-PH / ceb-PH) — Chirp 2 refuses a list of codes, and doesn't need
one: it's a universal model that transcribes code-switched Taglish under the PH
code, which is the point of the pairs.
The clinical vocabulary is boosted at the source. Chirp 2 has never seen a
physiotherapy chart, so it picks the ordinary English word over the
abbreviation every time — MMT comes back as "MPT", AROM as "a ROM", therex
as "there ex". The server sends a short phrase-adaptation list with each
request (MMT, AROM, PROM, goniometer, the special-test names…) so the
clinical reading wins. Chirp 2's feature support varies by region, so the list
is probed once: if Google rejects it, the server says so in the log and
transcribes without it from then on rather than failing or retrying every
segment. A second, deterministic pass repairs the same handful of known
misreadings in the recogniser's output, each one guarded by the words
around it — "MPT" becomes MMT next to a muscle grade and stays "MPT" next to
a therapist's name. The dictation bar names any word it changed.
Automatic gain control is off. It is every browser's default and it is
wrong in a clinic: it raises the microphone whenever the room goes quiet, which
is exactly when the only thing left to amplify is the conversation at the next
plinth — so a therapist pausing to think got the room brought up to meet them.
It also moves the signal level voiceGate() measures, underneath the gate, on
its own schedule. Echo cancellation and noise suppression stay on; both are
narrow-band and neither rescales speech the way AGC does.
There is no English-only choice, and that's measured rather than assumed. On
a 148-word clinical script, en-US beats the PH codes only on American-accented
English (8.8% vs 10.8% word error). On Filipino-accented English the PH codes
are level or ahead (en-US 27.7% / 20.3% against fil-PH 27.0% / 20.3% and
ceb-PH 26.4% / 19.6%). The failure is one-sided: Tagalog spoken while the code
sat on en-US lost whole utterances — a four-second segment came back as
"oppo" — and was billed for them regardless. English spoken under fil-PH costs
a word or two. The server therefore treats en-US as the default fil-PH, which
also covers a device still sending it from a cached copy of the app.
Where the server has no Google credentials (the preview, a local demo) the
bar falls back to the browser's own Web Speech API and says so. That streams
audio to the browser vendor's consumer service with no healthcare data
agreement (HIPAA BAA / RA 10173) — fine for demo data, never for real PHI.
Set GCP_PROJECT and Google credentials and Chirp 2 takes over automatically.
Full step-by-step (account, BAA, APIs, Cloud Run hosting): see GOOGLE_SETUP.md.
Off by default, and decided per clinic — an admin enables it in Facility Admin for their own clinic only, and no other clinic on the server is affected. Once on, for patients who consent, the Google Cloud dictation audio is kept briefly so a clinician can replay it to double-check the transcript. The audio is auto-deleted the moment the note is signed, or after the clinic's retention window (default 7 days) — whichever comes first — and consent is recorded in the chart. It never touches the browser engine (that audio goes straight to the vendor). Kept audio lives server-side (interim: the data dir; Cloud Storage with encryption + lifecycle auto-delete at go-live). This is the one disclosed exception to "TheraChart doesn't store audio."
TheraChart captures in two passes so the therapist gets instant feedback and a verified final record:
Live pass (while you talk) — every finished sentence is transcribed, pinned to the body map, and filed into sections in real time, so the PT can watch what's being logged and catch problems immediately.
Review & clean-up pass (at the end) — press ✦ Review & clean up with AI and the whole transcript is re-read by AI, which:
A review screen then lets the user edit everything — relabel any speaker, fix any wording, edit or uncheck any finding — before applying. Applying writes the speaker-labeled transcript back, rebuilds the body-map findings, and records a live-vs-cleanup comparison ("what changed") you can reopen any time. Transcript lines and finding summaries are also editable inline at any point, independent of the AI pass.
The AI pass uses Google Gemini when the clinic server is configured with a key; otherwise a local, on-device reviewer runs (no network, no key), so the feature always works. Enable Gemini on the clinic server:
GEMINI_API_KEY=... GEMINI_MODEL=gemini-3.8-flash node server.js
# for PHI/compliance, point GEMINI_BASE_URL at Vertex AI Gemini under a BAA
Only transcript text (never audio) is sent for refinement. Because that text can contain PHI, using the consumer Gemini API means sending PHI to Google; the Privacy panel explains the compliant path (paid Vertex AI Gemini under a signed BAA) and the local reviewer alternative.
Bringing a patient over from paper? On the patient's chart, ⇪ Import visit history (PDF) sends a scanned document to Gemini, which reads it into one structured entry per visit — dates, note type (eval / daily / progress / discharge), subjective/objective/assessment/treatment, body-map findings, and measurements (ROM, MMT, pain, special tests). A review screen shows every extracted visit for you to check against the scan and edit; likely duplicates of existing visits are pre-unchecked, and a name-mismatch warning fires if the document doesn't match the open chart. Applying creates each kept visit as a locked historical document dated to its original visit, e-signed with an "imported from scanned document" attestation (corrections go through the normal amendment flow). This needs Gemini configured — there is no offline reader for scans, so without a key the feature reports itself unavailable rather than guessing. The whole document (which usually contains the patient's details) is what's sent to Gemini; the Privacy panel discloses this.
Because a chart onboarded this way can hold years of visits, the clinical insights pass keeps only the 12 most recent visits in full and compresses everything older into a history digest (date range, recurring regions, ROM/pain endpoints, key assessments) — so the AI context stays about a page no matter how long the chart gets, and trends still span the whole history.
Beyond a single visit, TheraChart can reason across the whole chart. In any document, ✦ Clinical insights looks at the current findings and the patient's history and surfaces:
It is decision support for a licensed PT, not a diagnosis, and says so.
It uses Gemini when a key is configured, otherwise a local heuristic — same as
the clean-up pass. See DEPLOY.md to run it on Vercel with your
GEMINI_API_KEY.
Devices that belong to a clinic server keep working when it's unreachable:
sync-conflict entries).On the clinic's WiFi, devices connect directly (no internet needed at all). For home visits or working from home, install a mesh VPN like Tailscale on the server and each phone — devices then reach the clinic server from anywhere over an encrypted tunnel, without exposing it to the public internet. Note that phone browsers require HTTPS (or localhost) to allow the microphone, so give the server a certificate when deploying (Tailscale can issue one).
TheraChart is an installable web app (PWA). Open the clinic server's address on the phone, then Add to Home Screen (Safari share menu on iOS, Chrome menu on Android) — it launches fullscreen like a native app, with its own icon, and the app shell loads instantly from cache. Dictation, the body chart, printing to PDF, and sync all work from the phone; speech-to-text runs on Google Cloud (or the browser engine), so phones don't need any special hardware.
The parsing brain (parser.js) and data rules (store.js) are DOM-free and
checked offline:
node test/tenancy.test.js # 73 checks: clinic isolation, authorization, asset exposure
node test/workflow.test.js # 20 checks: a clinical journey across two synced devices
node test/migration.test.js # 18 checks: today's code reading a pre-tenancy database
node test/parser.test.js # 222 checks: EN/TL/CEB parsing, measurements, AROM/PROM, dictation repair
node test/store.test.js # 29 checks: licenses, e-sign locking, amendments, calendar
node test/merge.test.js # 13 checks: offline merge never loses records
node test/refine.test.js # 30 checks: speaker split, sections, measurements
node test/insights.test.js # 15 checks: connections, red flags, recommendations
node test/clinical.test.js # 160 checks: service catalogue, outcome item scoring, goal prompts, MCID trending
node test/import.test.js # 38 checks: PDF-record extraction, history digest, imported docs
The suites above are DOM-free — fast, and they never flake. What they can't see is the screen. Five Playwright tests cover the paths where a silent break would be worst and which no server-side check can catch:
npm run e2e # headless
npm run e2e:headed # watch it drive the app
npx playwright show-trace test-results/<test>/trace.zip # replay a failure
They cover: a therapist's chart rendering, clinic isolation on screen and in
localStorage, dictation pinning the body map and filing a measurement,
signing locking a note, and settings staying inside one clinic. The server is
started fresh against a throwaway data directory each run, so they never touch
data/.
Playwright is a dev dependency — it never ships. Browser binaries live in a
shared user cache outside the repo, and .gcloudignore keeps node_modules/
and test/ out of the deploy entirely. First run needs npx playwright install chromium.
Unit tests pin the local heuristic's exact behaviour. The eval harness grades whichever engine is configured — local or Gemini — on properties that must hold regardless of model or prompt wording (speaker attribution, findings grounded in the transcript, nothing invented, red flags caught):
npm run eval # local engine — free and deterministic
GEMINI_API_KEY=... npm run eval # score the real model
node test/eval/run.js --runs 3 # repeat, to see model variance
node test/eval/run.js --save-baseline # record the current score as the bar
Each run is scored against test/eval/baseline.<engine>.json and names any
assertion that flipped, exiting non-zero on a regression — so edit a prompt,
re-run, and see a number instead of guessing. Safety-critical assertions
(attribution, hallucination, red flags) carry extra weight.
The scanned-record import is scored separately, because its fixtures are documents rather than transcripts:
GEMINI_VERTEX=1 GCP_PROJECT=... npm run eval:extract
node test/eval/extract.js --runs 3 # repeat, to see model variance
node test/eval/extract.js --fixture scan_4visit
The scan fixtures are image-only PDFs — no text layer, so the model has to
OCR them the way it OCRs a real chart scan. A PDF built from text operators is
far easier and hides exactly the failures this eval exists to catch, so it is
kept only as a control. test/eval/fixtures/make-scan.py regenerates the
bitmaps deterministically (needs Pillow; they are gitignored). Document reading
has no local fallback, so this eval needs a real engine and exits 2 without one.
Simulated degradation is a stand-in. To score a genuine capture, print the clean pages and photograph or scan them — a phone is fine, and is closer to how records actually arrive than a flatbed is:
python3 test/eval/fixtures/make-scan.py --print # print_*.pdf, clean and printable
Save the capture as real_<name>.pdf (or .jpg/.png) in
test/eval/fixtures/, using the same name as the page you printed —
print_4visit.pdf → real_4visit.jpg. The eval scores it automatically and
skips it until the file exists. Capture twice if you can: once with a scanner
app's auto-enhance, once as a plain photo. The enhanced version is what users
will send; the plain photo keeps the shadow and keystone that actually break
OCR. Handwritten fixtures are advisory — reported, never gating.
index.html / styles.css — shell and design system (light + dark)server.js — zero-dependency clinic server: shared database, server-side
login, reminder scheduler, static hostingsync.js — client sync layer (auto-detects the server, degrades to
on-device mode)parser.js — multilingual body-part lexicon, symptom summarizer,
measurement extraction, section classifier, speaker split + local refiner,
coordinate-by-name (saved findings re-pin to the current mannequin)clinical.js — billing (the clinic's service catalogue and the visit
subtotal; the legacy CPT catalogue and 8-minute rule are still exported and
tested but no longer reachable from the UI), outcome-measure catalogue and
MCID trending, plan-of-care goal dates — DOM-free and checked offlinestore.js — on-device data layer: users, patients, documents, calendar,
audit log, license gatingapp.js — application: routing, views, dictation, body maps, printingtest/ — offline checkers for parser and storeimage.jpg.