Clinical knowledge graph extraction from therapy sessions β end-to-end, on-device, GPU-accelerated.
Fine-tunes ModernBERT-large to extract structured clinical knowledge from therapy transcripts, stores it in a per-patient graph database, and surfaces it through a desktop GUI and a graph-grounded chat interface β all running locally with no PHI leaving the machine.
Raw therapy audio goes in. A structured, queryable clinical knowledge graph comes out.
π Audio Recording
β
βΌ
βββββββββββββββββββββββββββββββ
β Diarization Pipeline β distil-whisper + pyannote
β Speaker ID + Transcript β word-level timestamps
βββββββββββββββ¬ββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββ
β Knowledge Graph API β ModernBERT-large backbone
β β
β ββββββββββββ β NER β ModernBERT + CRF
β β NER βββentitiesβββΊ β 7 clinical entity types
β ββββββββββββ β
β ββββββββββββ β RE β ModernBERT + Entity Pooling
β β RE βββrelationsβββΊβ 7 relation predicates
β ββββββββββββ β + epistemic metadata
βββββββββββββββ¬ββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββ
β LadybugDB (Graph Store) β per-patient .lbug files
β Nodes + Edges + Meta β Cypher-style query API
βββββββββββββββ¬ββββββββββββββββ
β
βββββββββ΄βββββββββ
βΌ βΌ
ββββββββββββ ββββββββββββββββββββ
β Graph β β Graph RAG Chat β clinician Q&A grounded
β GUI β β API β in the patient's graph
ββββββββββββ ββββββββββββββββββββ
Tauri + React
| Entity | Examples |
|---|---|
Symptom |
panic attacks, insomnia, dissociation |
Trigger |
crowded spaces, phone calls, confrontation |
Emotion |
anxious, overwhelmed, ashamed |
Person |
my father, my partner, my boss |
Coping_Mechanism |
breathing exercises, alcohol, journaling |
Life_Event |
childhood trauma, divorce, job loss |
Behavior |
avoidance, outbursts, self-isolation |
CAUSES Β· WORSENS Β· IMPROVES Β· RELATES_TO Β· EXPERIENCES Β· TRIGGERS Β· NONE
Every extracted relation carries epistemic metadata: who proposed it (Patient / Therapist) and how the patient responded (Affirmed / Denied / Avoided / Realized_Later).
ModernBERT-large (8192-token context, bfloat16, SDPA attention) with a linear emission layer feeding a Conditional Random Field decoder. The CRF transition matrix is pre-initialized to hard-enforce IOB grammar β I-X tokens can only follow B-X or I-X, not other entity types and not the start of a sequence.
Two-phase training:
- Phase 1 β Backbone frozen, CRF head only (1 epoch, lr=1e-3). Stabilizes random emissions before any gradient flows into the pretrained weights.
- Phase 2 β Full fine-tune with early stopping (lr=2e-6, patience=2). Best checkpoint selected by eval loss.
Four special tokens ([E1], [/E1], [E2], [/E2]) are injected around entity spans in the input. Their embeddings are trained from scratch in Phase 1 while the backbone is frozen; Phase 2 fine-tunes everything together. At inference, the hidden states at each entity's marker positions are mean-pooled and concatenated (2048-dim) for relation classification.
Sliding-window chunking (default 4096 spaCy tokens, 1000-token overlap) handles transcripts of arbitrary length. A center-weighted proximity score resolves entity and relation duplicates across window boundaries β spans closer to the window center are preferred, preventing boundary artifacts from biasing the graph.
therapy-bert/
βββ knowledge_graph_api.py # FastAPI service β NER + RE β LadybugDB (port 8086)
βββ graph_rag_api.py # Graph-grounded chat API (port 8091)
βββ ner_crf_layer.py # ModernBERT_CRF model definition
βββ modern_bert_re_layers.py # ModernBERT_Entity_Pooling_RE model definition
βββ modern_bert_ner_trainer.py # NER training (2-phase)
βββ modern_bert_re_trainer.py # RE training (2-phase)
βββ ner_inference_pipeline.py # NER inference
βββ re_inference_pipeline.py # RE inference
βββ ladybug_db/
β βββ db_manager.py # Graph database (Cypher-style API)
βββ patients/ # Per-patient .lbug graph files
βββ synthetic-data-gen/
β βββ generator.py # LLM transcript generation (3,000 sessions)
β βββ entity_shard_generator.py # LLM entity annotation pass
β βββ re_shard_generator.py # LLM relation annotation pass
β βββ iob_labelling.py # Span β BIO tag conversion
β βββ create_splits.py # Train / val / test splits
β βββ validation.py # Pydantic schemas (entity + relation types)
β βββ config.py # Pipeline configuration
βββ diarization/
β βββ diarization_engine.py # Whisper + Pyannote diarization
β βββ diarization_api.py # FastAPI wrapper (port 8085)
βββ representation-engineering/ # Experimental: Qwen3 subtext shadow vectors
βββ tauri-app/therapy-bert-gui/ # Desktop GUI β Tauri (Rust) + React/TS/Vite
βββ therapy-modernbert-ner-final/ # Trained NER model (~1.5 GB)
βββ therapy-modernbert-re-final/ # Trained RE model (~790 MB)
βββ requirements.txt
| Model | Role |
|---|---|
answerdotai/ModernBERT-large |
NER + RE backbone (8192-token context, bfloat16) |
distil-whisper/distil-large-v3.5-ct2 |
Speech recognition with word-level timestamps |
pyannote/speaker-diarization-community-1 |
Speaker diarization |
Qwen/Qwen3-0.6B-Base |
Experimental subtext representation engineering |
| Local LLM (via Open AI Responses API) | Synthetic data generation |
# Python 3.10+
pip install -r requirements.txt
# For the desktop GUI (requires Rust + Node)
cd tauri-app/therapy-bert-gui
npm installStandalone dev (top-level scripts):
Top level APIs are dev implementations, packaged versions in ./tauri-app/therapy-bert-gui/backend are bundled with the Tauri app.
# Diarization API (port 8085)
python diarization/diarization_api.py
# Knowledge Graph API β NER + RE extraction (port 8086)
python knowledge_graph_api.py
# Graph RAG Chat API (port 8091)
python graph_rag_api.py
# SQLite server β patient & session storage (port 8088)
python tauri-app/therapy-bert-gui/backend/diarization/sqlite_helper_api.py
# Representation Engineering API (port 8087)
python tauri-app/therapy-bert-gui/backend/representation-engineering/representation_extraction_api.pyWhen running the full desktop app, start all five services (diarization, knowledge graph, graph RAG, SQLite, representation engineering) plus the GUI.
curl -X POST http://localhost:8086/api/knowledge-graph \
-H "Content-Type: application/json" \
-d '{
"patient_id": "001",
"transcript_payload": {
"transcript": [
{"speaker": "Patient", "text": "I have been more anxious lately."},
{"speaker": "Therapist", "text": "What seems to trigger it?"},
{"speaker": "Patient", "text": "Crowds. Any time I am in a crowd I get a panic attack."}
]
},
"inference_config": {
"max_context_tokens": 4096,
"window_overlap_tokens": 1000,
"relation_batch_size": 8
}
}'Poll for results:
curl http://localhost:8086/api/jobs/<job_id># NER β ModernBERT + CRF
python modern_bert_ner_trainer.py
# RE β ModernBERT + Entity Pooling
python modern_bert_re_trainer.py# 1. Generate therapy transcripts
python synthetic-data-gen/generator.py
# 2. Entity annotation pass
python synthetic-data-gen/entity_shard_generator.py
# 3. Relation annotation pass
python synthetic-data-gen/re_shard_generator.py
# 4. BIO tag conversion
python synthetic-data-gen/iob_labelling.py
# 5. Create train/val/test splits
python synthetic-data-gen/create_splits.pycd tauri-app/therapy-bert-gui
npm run tauri devrepresentation-engineering/ explores shadow vectors β the difference in hidden states between a literal surface reading and the clinical subtext of a patient's statement β using Qwen3-0.6B-Base. The goal is to steer the model toward surfacing what the patient is communicating beneath the words, without any supervised signal.
- Fully on-device. No transcript, patient ID, or graph data is ever sent to an external API. Everything runs locally.
- Long-context by default. ModernBERT's 8192-token context window means an entire session can often be processed in a single forward pass if compute resources allow.
- Epistemic metadata. Relations aren't just edges β they carry who proposed the connection and whether the patient accepted, denied, or avoided it (coming soon). This is clinically meaningful signal that most KG systems discard.
- Schema-first. Entity and relation types are defined once in
synthetic-data-gen/validation.pyand referenced everywhere β training data, inference, and the graph DB β so a schema change propagates cleanly. - Sliding windows, not truncation. When a transcript exceeds the model's context, overlapping windows are used with center-weighted deduplication rather than silently dropping content.