# Project export: med sec

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: Your health, heard in real time: an AI voice system that turns everyday conversations into continuous and hidden health insight
- Devpost: https://devpost.com/software/th26
- GitHub: https://github.com/propadiene-1/TreeHacks-2026
- Video: https://www.youtube.com/embed/TYIjwvTpaOA?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — propadiene-1 (34 commits), Isita (14 commits), Nishka Kacheria (10 commits), Ashita (6 commits)

## Devpost submission (written by the team)

### Inspiration

Your definition of pain changes over time, especially if you have a chronic or pre-existing condition. Multiple studies find that when people experience consistent pain, they tend to "get used to it" over time and don't see a doctor, even when the situation can be objectively harmful. On top of this, doctors and nurses don't have the bandwidth to follow up on every patient, and some have found that up 40-80% of the information they share is immediately forgotten. Our goal was to create an app that proactively checks in on users, creates a long-term profile of their health conditions, and acts as a supplement to their interaction with doctors and nurses.

### What it does

Since we're aiming to engage the elderly, the check-ins operate over the phone. The main UI has a call button, where the user can input their number and start a phone call. After hanging up, the agent creates a personalized check-up schedule based on the conversation and determines the best frequency at which to check in (e.g. daily or weekly). The agent remembers context from past meetings and uses it to inform the conversation in the next meeting, allowing it to aggregate a profile for the user. Over time, the agent observes long-term trends about the patient's health and pain. Our algorithm tracks the patients' own pain ratings on a scale of 1 to 7, but we also track and display an internal definition of "actual pain" which factor in external factors such as mood, physical condition, and habituation. For each call log, we record and display biotrackers so that users can review the conditions of each meeting.

### How we built it

We focused on two main components-- proactive checkups, and long-term analysis. For independent check-ups, we started with a basic conversational pipeline, using Twilio to initiate a phone call and iteratively call the OpenAI API for follow-up questions. After each call ends, we call a scheduler that reads the transcript and independently creates and executes a follow-up schedule. The execution happens through node-cron, which we use to make calls to Twilio on an internally held schedule. To make it easy for users to input their phone number and view the long-term data, we built a web app using Node.js and Express. We also wrote custom backend pipelines for the scheduling, symptom tracking, and long-term pain analysis. After building the basic infrastructure, we implemented more intricate features. Highlights include real-time biomarker tracking (tone, breathing patterns, etc.) with Twilio media stream, integrating an exponential model for scheduling, and custom pain correction algorithm that accounts for habituation ("getting used to the pain") to gauge realistic pain levels for users with chronic conditions. Fundamentally, the problem we're trying to fix is that people normalize their own health over time, and doctor's appointments only provide local updates on information. Our app provides global information. What was shocking pain a few months ago might now just be another Tuesday, and a patient at the doctor's office might not be able to contextualize how they're feeling at that one moment in time versus in general. We also tracked different ailments, separating out pain in different parts of the body to more clearly distinguish different symptoms over time. Our app takes into account a set of the existing biomarkers, semantic analysis, a user-focused pain rating over time, pain in different body parts that might be interfering with each other, and context clues from the user's speech and more features in a novel algorithm to generate expected pain values. Daily calls can be tiring, so we implemented a scheduler that exponentially decays symptom checking for fading symptoms (in terms of their actual pain). This uses the severity of symptom as well as frequency and last check to indicate it should be checked at a later vs earlier date, so that patients aren't flooded with information and don't have to keep reiterating their symptoms or be reminded of previous ailments. We focused on making the app as easily usable as possible. We also take a novel approach by treating voice as a compounded health biomarker in extracting acoustic and temporal features such as breathing cadence, vocal stability, pauses, energy distribution, and spectral characteristics during natural conversation. Research shows vocal signals can reflect respiratory strain, neurological changes, stress, fatigue, and medication side effects because speech production integrates respiratory, cognitive, and motor systems. Our system continuously analyzes these signals in real time without storing raw audio, building a personalized baseline and detecting deviations over time. By combining conversational context with passive vocal biomarkers, MedSec transforms everyday speech into a longitudinal health signal that helps users recognize emerging patterns earlier.

### Challenges we ran into

One of our main challenges was collecting enough quantifiable data to build a profile, without sacrificing the user's experience (especially because they're already speaking to an AI). We found that it was difficult to standardize symptom tracking across multiple sessions while keeping it conversational, because the LLM is biased towards semantics. We handled this by internally keeping a patient health profile throughout the conversation, to inform the agent's follow-up questions and gently prod for more data. Our goal was to ease the user into providing more information without interrogating them with a checklist of symptoms.

### Accomplishments we're proud of

Medsec agentically provides homecare and checks back in, allowing for more support when it's hard to get appointments with healthcare professionals. Our UI landing page is intuitive and has all the information you need, and we're proud of its accessibility and seamlessness (especially for our target audience). Agent uses exponential decay, mood, health, to calculate reported pain. Medsec measures biomarkers for other factors influencing reported pain and health, monitoring other decline. Visualizations for doctors and other stakeholders

### What we learned

Given that our project has a lot of moving parts, and a few of the pipelines are entirely internal, we learned a ton about system design-- namely designing intuitive pipelines that we could easily come back to and optimize. We also learned a lot about phone call integration, which none of us had really considered or worked with before, so we found it pretty interesting. Our team was built the day TreeHacks started, and we learned a lot about teamwork, collaboration, and making full use of different strengths and working styles to put together a product.

### What's next

for MedSec More guardrails Privacy protections and solid encryption A separate interface for doctors vs. users

## README (from the GitHub repository)

# MedSec (TreeHacks 2026)

Your AI-powered medical assistant! 

Enter your number and MedSec will call you, ask about your pain, and create a check-up schedule. Long-term, it will collect and analyze your symptoms-- including pain history, day-to-day health status, and any long-term patterns.

This project was made for TreeHacks 2026.

## Devpost: [devpost.com/software/th26](https://devpost.com/software/th26)

![Alt text for the image](./archive/main_page.png)

## Highlights

+ **Long-term symptom tracking** --- through personalized follow-ups
+ **Agentic scheduling pipeline** --- internally creates a check-in schedule based on your first meeting
+ **Live biomarker analysis** --- tracks shaky voice, pauses, tone, etc.
+ **Custom pain recalibration** --- adjusts for daily physical changes & long-term habituation
+ **Clean & intuitive UI** --- just add your number!

## Quick Start

1. **Clone the repository**
   ```bash
   git clone https://github.com/propadiene-1/TreeHacks-2026.git
   ```

2. **Install dependencies**
   ```bash
   npm install
   ```

3. **Set up API keys**

   + Visit [OpenAI](https://openai.com) and get your API key.
   + Visit [Chroma Cloud](https://docs.trychroma.com/cloud/getting-started) and get your API key.

4. **Set up Twilio**

   + Make a [Twilio account](https://login.twilio.com/u/signup?state=hKFo2SAxMnVyT0ptaS1DTzlSMmhBMzJmWl9CLWpPVHhFMzVEdqFur3VuaXZlcnNhbC1sb2dpbqN0aWTZIFBhNmEyYWcxZEVLUTAzZmNPcnpTV2pxcHhvV2ZIeS14o2NpZNkgTW05M1lTTDVSclpmNzdobUlKZFI3QktZYjZPOXV1cks) and follow [these instructions](https://help.twilio.com/articles/223180048-How-to-Add-and-Remove-a-Verified-Phone-Number-or-Caller-ID-with-Twilio) to add phone numbers. 
   + Add a source number which MedSec will use to call you (TWILIO_PHONE_NUMBER).
   + Add all client numbers that MedSec should call.

5. **Set up environment variables**

   Create a file called `.env` file in the root directory:

    ```env
   OPENAI_API_KEY=[YOUR API KEY]
   PORT=3000
   TWILIO_ACCOUNT_SID = [YOUR ACCOUNT SID]
   TWILIO_AUTH_TOKEN = [YOUR AUTH TOKEN]
   TWILIO_PHONE_NUMBER = [SOURCE PHONE NUMBER]
   CHROMA_API_KEY= [YOUR API KEY]
   CHROMA_TENANT= [YOUR TENANT CODE]
   SERVER_URL= [YOUR SERVER URL]
    ```

6. **Set up ngrok (instructions [here](http://ngrok.com/docs/getting-started)).**

   Start ngrok before running the app.

7. **Run the app**
    ```bash
    npm start
    ```

## Tech Stack

- **Backend**: Node.js, Express
- **Voice Processing**: Twilio Voice API with real-time speech-to-text
- **Natural Language**: OpenAI GPT-4o (conversational AI + function calling)
- **Vector DB**: ChromaDB (semantic search)
- **Scheduling**: node-cron with adaptive recurrence logic
- **Analytics**: Custom pain recalibration algorithm + biomarker analysis
- **Frontend**: HTML, JavaScript, CSS

## Project Structure

```
TreeHacks-2026/
├── public/                 # frontend
│   ├── index.html        # main page
│   │   dashboard.html        # user data
|   |   schedule.html       #schedule page
|   |   schedule.js         
│   │   script.js           # frontend functions
|   |   style.css           # UI
├── package.json            # dependencies
|   server.js               # backend logic
├── openai-calls.js         # api calls
|   pain-correction.js     # pain correction algorithm
├── .env                   # environment variables
└── README.md
```
## Authors
- Isita B.
- Ashita B.
- Nishka K.
- Aileen L.

## Detected evidence (automated analysis)

Indexed codebase: 21 recognized source files, 157 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (23 of 23)

```
.gitignore
archive/db-module.js
archive/index.js
archive/main.py
archive/pain/add_to_server_later.js
archive/pain/archive-pain-correction.js
archive/pain/pain-analyzer.js
archive/routes/conversation.js
archive/routes/schedule.js
archive/test-extract.js
docs/PAIN_ALGORITHM.md
generate_data.js
openai-calls.js
package.json
pain-correction.js
public/dashboard.html
public/index.html
public/schedule.html
public/schedule.js
public/script.js
public/style.css
README.md
server.js
```

### Dependencies

- package.json: @chroma-core/default-embed@^0.1.9, chromadb@^3.3.0, dotenv@^17.3.1, express@^5.2.1, node-cron@^4.2.1, openai@^6.22.0, socket.io@^4.8.3, twilio@^5.12.1, ws@^8.19.0

### Recent commits (newest first)

- clean up codebase
- readme
- readme
- readme
- some files
- readme
- readme
- readme
- readme stuff
- final
- LFG IT WORKS
- merged:
- random endpoint for semantic search
- add back some changes
- add days since onset
- merge
- attempting visual updates w call logs
- add habituation to pain correction algorithm, add database update with new features
- attempting visual updates w call logs
- organize some files

## Key source files (fetched from GitHub, selected and truncated for size)

### docs/PAIN_ALGORITHM.md

```markdown
# Pain correction algorithm

## Goal

Estimate **actual pain** from a timeseries of **reported pain**, **mood**, and **physical condition**, accounting for:

- **Perception bias**: mood and physical state can inflate or deflate reported pain.
- **Baseline**: the user’s typical pain level stabilizes estimates.

All scales are 0–10 (higher = worse pain, better mood, better physical condition).

---

## Literature rationale

### 1. Mood and pain perception

- Negative affect, depression, and anxiety are associated with **higher** pain ratings (affective amplification).
- References: Bair et al., *Psychological Bulletin* (depression and pain); mood as moderator of pain perception in chronic pain samples.
- **Implication**: when mood is low, reported pain tends to be **over**-reported relative to a “neutral” perception. We correct by subtracting a bias that increases as mood decreases.

### 2. Physical condition and pain perception

- Poor physical function and deconditioning are associated with higher reported pain and pain catastrophizing (e.g. SF-36 physical function vs pain).
- **Implication**: worse physical condition → over-reporting. We subtract a bias that increases as physical condition decreases.

### 3. Baseline pain

- Chronic pain is often characterized by a personal baseline (habitual level).
- Using a running baseline (or clinician-set baseline) stabilizes estimates and reduces noise from single inflated reports.
- **Implication**: we blend the bias-corrected pain with the user’s baseline so that “actual” pain is pulled toward their typical level.

---

## Formula

### Step 1: Perception bias (over-reporting)

- Low mood and poor physical condition → positive bias (we subtract it from reported pain).

\[
\text{bias} = k_{\text{mood}} \cdot (10 - \text{mood}) + k_{\text{physical}} \cdot (10 - \text{physical})
\]

- Default: \(k_{\text{mood}} \approx 0.12\), \(k_{\text{physical}} \approx 0.10\) (tunable; bias typically in 0–2.2 range).

### Step 2: Corrected pain (single time point)

\[
\text{corrected} = \text{clamp}_{[0,10]}(\text{reported} - \text{bias})
\]

### Step 3: Baseline

- **Running baseline**: mean of corrected pain over all **prior** time points (no future data).
- **Fixed baseline**: optional user/clinician-set value (e.g. from first week or assessment).

### Step 4: Actual pain estimate

\[
\text{actual} = \alpha \cdot \text{corrected} + (1 - \alpha) \cdot \text{baseline}
\]

- Default \(\alpha \approx 0.75\) (more weight on corrected; baseline has a modest pull).
- Result is clamped to \([0, 10]\).

---

## Usage

- **Server**: `pain-correction.js` exports `actualPainFromTimeseries(timeseries, options)`.
- **API**: `POST /api/actual-pain` with body `{ timeseries, options? }` returns `{ data: [{ date, reportedPain, correctedPain, actualPain, baseline }, ...] }`.
- **Dashboard**: `createPainLineGraph('painChart', timeseries)` fetches actual pain from the API and plots reported, expected (from mood & physical), and actual (c
[truncated — 683 more characters]
```

### package.json

```
{
  "name": "treehacks-2026",
  "version": "1.0.0",
  "description": "",
  "homepage": "https://github.com/propadiene-1/TreeHacks-2026#readme",
  "bugs": {
    "url": "https://github.com/propadiene-1/TreeHacks-2026/issues"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/propadiene-1/TreeHacks-2026.git"
  },
  "license": "ISC",
  "author": "",
  "type": "commonjs",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js"
  },
  "dependencies": {
    "@chroma-core/default-embed": "^0.1.9",
    "chromadb": "^3.3.0",
    "dotenv": "^17.3.1",
    "express": "^5.2.1",
    "node-cron": "^4.2.1",
    "openai": "^6.22.0",
    "socket.io": "^4.8.3",
    "twilio": "^5.12.1",
    "ws": "^8.19.0"
  }
}

```

### archive/main.py

```python
import chromadb

client = chromadb.PersistentClient(path="./chroma_db")
print(client.list_collections())  # Shows all collections

collection = client.get_collection("call_transcripts")
data = collection.get(include=["metadatas", "documents", "embeddings"])
print(f"Total items: {len(data['ids'])}")
print("Sample:", data['documents'][0])  # First document

```

### archive/index.js

```javascript
// const express = require('express');
// const { app: scheduleApp } = require('./routes/schedule');
// const conversationApp = require('./routes/conversation');

// const app = express();
// app.use(express.static('public'));
// app.use('/', scheduleApp);
// app.use('/', conversationApp);

// app.listen(3000, () => console.log('Server running on :3000'));
```

### server.js

```javascript
// server.js (FULL FILE WITH ALL CHANGES)

const http = require('http');
const WebSocket = require('ws');

const express = require('express');
const twilio = require('twilio');
const cron = require('node-cron');
const { CloudClient } = require('chromadb');
require('dotenv').config();

const app = express();
const port = process.env.PORT || 3000;

const PUBLIC_BASE_URL =
  process.env.PUBLIC_BASE_URL ||
  process.env.SERVER_URL ||
  `http://127.0.0.1:${port}`;

const PUBLIC_WSS_URL = process.env.PUBLIC_WSS_URL || null;

function absUrl(path) {
  if (path.startsWith('http://') || path.startsWith('https://')) return path;
  return `${PUBLIC_BASE_URL}${path}`;
}

function getMediaStreamUrl(callSid) {
  if (!PUBLIC_WSS_URL) return null;
  return `${PUBLIC_WSS_URL}/media-stream?callSid=${encodeURIComponent(callSid)}`;
}

/**
 * µ-law decode
 */
function mulawDecodeSample(u) {
  u = ~u & 0xff;
  const sign = u & 0x80;
  const exponent = (u >> 4) & 0x07;
  const mantissa = u & 0x0f;
  let sample = ((mantissa << 4) + 0x08) << (exponent + 3);
  sample -= 0x84;
  return sign ? -sample : sample;
}

function decodeMulawBase64ToInt16(base64) {
  const buf = Buffer.from(base64, 'base64');
  const out = new Int16Array(buf.length);
  for (let i = 0; i < buf.length; i++) out[i] = mulawDecodeSample(buf[i]);
  return out;
}

function frameRms(int16) {
  let sumSq = 0;
  for (let i = 0; i < int16.length; i++) {
    const x = int16[i] / 32768;
    sumSq += x * x;
  }
  return Math.sqrt(sumSq / Math.max(1, int16.length));
}

function frameZcr(int16) {
  let zc = 0;
  let prev = int16[0] || 0;
  for (let i = 1; i < int16.length; i++) {
    const cur = int16[i];
    if ((prev >= 0 && cur < 0) || (prev < 0 && cur >= 0)) zc++;
    prev = cur;
  }
  return zc / Math.max(1, int16.length);
}

function rmsToDb(rms) {
  const eps = 1e-6;
  return 20 * Math.log10(Math.max(eps, rms));
}

function median(arr) {
  if (!arr || arr.length === 0) return null;
  const a = arr.slice().sort((x, y) => x - y);
  const mid = Math.floor(a.length / 2);
  if (a.length % 2) return a[mid];
  return (a[mid - 1] + a[mid]) / 2;
}

function iqr(arr) {
  if (!arr || arr.length < 4) return null;
  const a = arr.slice().sort((x, y) => x - y);
  const q1 = a[Math.floor((a.length - 1) * 0.25)];
  const q3 = a[Math.floor((a.length - 1) * 0.75)];
  return q3 - q1;
}

/**
 * Simple pitch estimator via autocorrelation on 8kHz
 * Returns Hz or null
 */
function estimateF0Hz(pcm, sampleRate = 8000) {
  const n = Math.min(pcm.length, 320);
  if (n < 160) return null;

  const x = new Float32Array(n);
  let mean = 0;
  for (let i = 0; i < n; i++) mean += pcm[i];
  mean /= n;
  let energy = 0;
  for (let i = 0; i < n; i++) {
    const v = (pcm[i] - mean) / 32768;
    x[i] = v;
    energy += v * v;
  }
  if (energy < 1e-5) return null;

  const minLag = Math.floor(sampleRate / 400);
  const maxLag = Math.floor(sampleRate / 60);
  if (maxLag >= n) return null;

  let bestLag = -1;
  let bestCorr = -1;

  for (let lag = minLag; lag <= maxLag; lag++) {
    let corr = 0;
    for (let i = 0; i < n - lag; i++) corr += x[i] * x[i + lag];
    if (corr > bestCorr) {
      bestCorr = corr;
      bestLag = lag;
    }
  }

  if (bestLag <= 0) return null;

  const norm = energy;
  const strength = bestCorr / (norm + 1e-9);
  if (strength < 0.15) return null;

  return sampleRate / bestLag;
}

/**
 * Spectral centroid and flatness using a small DFT (256)
 */
function spectralFeatures(pcm, sampleRate = 8000) {
  const N = 256;
  if (pcm.length < N) return null;

  const x = new Float32Array(N);
  for (let i = 0; i < N; i++) {
    const w = 0.5 - 0.5 * Math.cos((2 * Math.PI * i) / (N - 1));
    x[i] = (pcm[i] / 32768) * w;
  }

  const half = N / 2;
  const mags = new Float32Array(half + 1);

  for (let k = 0; k <= half; k++) {
    let re = 0;
    let im = 0;
    for (let n = 0; n < N; n++) {
      const ang = (2 * Math.PI * k * n) / N;
      re += x[n] * Math.cos(ang);
      im -= x[n] * Math.sin(ang);
    }
    mags[k] = Math.sqrt(re * re + im * im) + 1e-12;
  }

  let num = 0;
  let den = 0;
  for (let k = 1; k <= half; k++) {
    const f = (k * sampleRate) / N;
    const m = mags[k];
    num += f * m;
    den += m;
  }
  const centroid = den > 0 ? num / den : null;

  let logSum = 0;
  let linSum = 0;
  const count = half;
  for (let k = 1; k <= half; k++) {
    const m = mags[k];
    logSum += Math.log(m);
    linSum += m;
  }
  const geo = Math.exp(logSum / Math.max(1, count));
  const ar = linSum / Math.max(1, count);
  const flatness = ar > 0 ? geo / ar : null;

  let hi = 0;
  let tot = 0;
  for (let k = 1; k <= half; k++) {
    const f = (k * sampleRate) / N;
    const m = mags[k];
    tot += m;
    if (f >= 2000) hi += m;
  }
  const breath = tot > 0 ? hi / tot : null;

  return { centroid, flatness, breath };
}

// -------------------------
// In-memory call logs fallback
// -------------------------
const inMemoryCallLogsByCallSid = new Map();
const inMemoryCallLogIndex = [];
const IN_MEMORY_MAX_LOGS = 200;

function upsertInMemoryCallLog(callLogObject) {
  if (!callLogObject || !callLogObject.callSid) return;

  const callSid = callLogObject.callSid;
  inMemoryCallLogsByCallSid.set(callSid, callLogObject);

  const existingIdx = inMemoryCallLogIndex.findIndex(x => x.callSid === callSid);
  const row = {
    callSid,
    created_at: callLogObject.created_at,
    phoneNumber: callLogObject.phoneNumber || null
  };

  if (existingIdx >= 0) inMemoryCallLogIndex[existingIdx] = row;
  else inMemoryCallLogIndex.push(row);

  inMemoryCallLogIndex.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
  while (inMemoryCallLogIndex.length > IN_MEMORY_MAX_LOGS) {
    const removed = inMemoryCallLogIndex.pop();
    if (removed?.callSid) inMemoryCallLogsByCallSid.delete(removed.callSid);
  }
}

// -------------------------
// Chroma init
// -------------------------
const chroma = new CloudClient({
  apiKey: process.env.CHROMA_API_KEY,
  tenant: process.env.C
[truncated — 36739 more characters]
```

### pain-correction.js

```javascript
/**
 * Pain correction algorithm
 * Habituation-dominant model (includes chronic adaptation & acute mood effects)
 */

const CORRECTION_PARAMS = {
  // mood/physical bias (over-reporting) - reduced for acute effect
  kMood: 0.08,
  kPhysical: 0.08,
  
  // habituation correction (under-reporting) - increased for chronic effect
  peakMonths: 15,
  maxHabituationFraction: 0.45,  // up to 45% of baseline pain
  minChronicBaseline: 4,
  driftThreshold: 1.5,
  correctionWeight: 0.8          // aggressive correction
};

function moodPhysicalBias(mood, physicalCondition) {
  const { kMood, kPhysical } = CORRECTION_PARAMS;
  const moodTerm = Math.max(0, 10 - mood);
  const physicalTerm = Math.max(0, 10 - physicalCondition);
  return kMood * moodTerm + kPhysical * physicalTerm;
}

function habituationCurve(timeMonths) {
  const { peakMonths } = CORRECTION_PARAMS;
  const k = 0.3;
  const midpoint = peakMonths * 0.6;
  return 1 / (1 + Math.exp(-k * (timeMonths - midpoint)));
}

function expectedPainTrajectory(baseline, timeMonths, hasActiveTreatment = false) {
  if (!hasActiveTreatment) {
    return baseline + Math.min(timeMonths * 0.05, 0.5);
  } else {
    const maxImprovement = baseline * 0.2;
    const improvementCurve = 1 - Math.exp(-timeMonths / 6);
    return baseline - (maxImprovement * improvementCurve);
  }
}

function correctPainCombined(reportedPain, params) {
  const {
    mood = 5,
    physicalCondition = 5,
    baselinePain,
    timeMonths = 0,
    hasActiveTreatment = false
  } = params;
  
  // Effect 1: Mood/physical over-reporting (subtract) - SMALLER effect
  const moodBias = moodPhysicalBias(mood, physicalCondition);
  const afterMoodCorrection = Math.max(0, Math.min(10, reportedPain - moodBias));
  
  // Effect 2: Habituation under-reporting (add) - LARGER effect
  let habituationCorrection = 0;
  
  if (baselinePain >= CORRECTION_PARAMS.minChronicBaseline && 
      timeMonths >= 2 && 
      afterMoodCorrection < baselinePain) {
    
    const expected = expectedPainTrajectory(baselinePain, timeMonths, hasActiveTreatment);
    const drift = Math.max(0, expected - afterMoodCorrection);
    
    if (drift >= CORRECTION_PARAMS.driftThreshold) {
      const timeEffect = habituationCurve(timeMonths);
      const maxHabituation = baselinePain * CORRECTION_PARAMS.maxHabituationFraction;
      const rawHabituation = Math.min(drift * timeEffect, maxHabituation);
      habituationCorrection = rawHabituation * CORRECTION_PARAMS.correctionWeight;
    }
  }
  
  const actualPain = Math.max(0, Math.min(10, afterMoodCorrection + habituationCorrection));
  
  return {
    reportedPain,
    moodBias,
    afterMoodCorrection,
    habituationCorrection,
    actualPain,
    baseline: baselinePain
  };
}

function correctTimeseriesCombined(timeseries, options = {}) {
  const {
    baselinePain,
    hasActiveTreatment = false,
    baselinePeriodDays = 30
  } = options;
  
  if (!timeseries || timeseries.length === 0) return [];
  
  let baseline = baselinePain;
  if (baseline === undefined) {
    const baselinePoints = timeseries.filter(p => 
      p.daysSinceOnset <= baselinePeriodDays
    );
    if (baselinePoints.length > 0) {
      baseline = baselinePoints.reduce((sum, p) => sum + p.painRating, 0) / baselinePoints.length;
    } else {
      baseline = timeseries[0].painRating;
    }
  }
  
  return timeseries.map(point => {
    const timeMonths = point.daysSinceOnset / 30.44;
    
    const corrected = correctPainCombined(point.painRating, {
      mood: point.mood || 5,
      physicalCondition: point.physicalCondition || 5,
      baselinePain: baseline,
      timeMonths,
      hasActiveTreatment
    });
    
    return {
      date: point.date,
      reportedPain: corrected.reportedPain,
      moodBias: corrected.moodBias,
      afterMoodCorrection: corrected.afterMoodCorrection,
      habituationCorrection: corrected.habituationCorrection,
      actualPain: corrected.actualPain,
      baseline: baseline
    };
  });
}

function actualPainFromTimeseries(timeseries, options = {}) {
  return correctTimeseriesCombined(timeseries, options);
}

module.exports = {
  CORRECTION_PARAMS,
  moodPhysicalBias,
  habituationCurve,
  expectedPainTrajectory,
  correctPainCombined,
  correctTimeseriesCombined,
  actualPainFromTimeseries
};
```

### generate_data.js

```javascript
// scripts/populate-chroma.js - Standalone script to populate ChromaDB with fake medical call transcripts
const { CloudClient } = require('chromadb');
require('dotenv').config();

async function generateFakeTranscript(turns = 5) {
    const greetings = ["Hello! How are you feeling today?", "Hi there, what's on your mind?", "Hey! Ready to chat?"];
    const userResponses = [
        "I'm feeling a bit anxious about work",
        "My stomach has been hurting lately", 
        "I have a headache that won't go away",
        "I've been really tired all the time",
        "My chest feels tight sometimes",
        "I can't sleep well at night",
        "My joints have been aching"
    ];
    const aiFollowups = [
        "Can you tell me more about when the anxiety started?",
        "How long has your stomach been hurting?",
        "On a scale of 1-10, how bad is the headache?",
        "Have you noticed any patterns with the tiredness?",
        "Does the chest tightness come with shortness of breath?",
        "What time do you usually go to bed?",
        "Which joints are most affected?"
    ];

    let transcript = [];
    transcript.push({ speaker: 'AI', message: greetings[Math.floor(Math.random() * greetings.length)] });
    
    for (let i = 0; i < turns; i++) {
        transcript.push({ speaker: 'User', message: userResponses[Math.floor(Math.random() * userResponses.length)] });
        transcript.push({ speaker: 'AI', message: aiFollowups[Math.floor(Math.random() * aiFollowups.length)] });
    }
    
    return transcript.map(entry => `${entry.speaker}: ${entry.message}`).join('\n');
}

async function generateKeywords(transcript) {
    const keywordMap = {
        'anxious': ['anxiety', 'stress'],
        'stomach': ['stomach pain', 'digestive'],
        'headache': ['headache', 'migraine'],
        'tired': ['fatigue', 'exhaustion'],
        'chest': ['chest pain', 'cardiac'],
        'sleep': ['insomnia', 'sleep disorder'],
        'joints': ['arthritis', 'joint pain']
    };
    
    const words = transcript.toLowerCase().split(/\W+/);
    const foundKeywords = new Set();
    
    for (const word of words) {
        for (const [key, tags] of Object.entries(keywordMap)) {
            if (word.includes(key) && foundKeywords.size < 4) {
                tags.forEach(tag => foundKeywords.add(tag));
            }
        }
    }
    
    return Array.from(foundKeywords);
}

async function populateChroma() {
    console.log('🧬 Connecting to ChromaDB...');
    
    // Matches your server.js config (local port 8000)
    const chroma = new CloudClient({
        apiKey: process.env.CHROMA_API_KEY,
        tenant: process.env.CHROMA_TENANT,
        database: 'second'
        });
    const collection = await chroma.getOrCreateCollection({
        name: 'call_transcripts',
        metadata: { 'hnsw:space': 'cosine' }
    });

    const initialCount = await collection.count();
    console.log(`📊 Current collection size: ${initialCount}`);

    // Generate 25 fake transcripts
    const fakeData = [];
    for (let i = 0; i < 25; i++) {
        const turns = Math.floor(Math.random() * 4) + 3; // 3-6 turns
        const transcriptString = await generateFakeTranscript(turns);
        const keywords = await generateKeywords(transcriptString);
        const callSid = `fake_call_${Date.now() - Math.floor(Math.random() * 1000000)}`;
        
        fakeData.push({
            id: `fake_transcript_${i}_${Date.now()}`,
            document: transcriptString,
            metadata: {
                callSid,
                created_at: new Date().toISOString(),
                created_at_ts: Date.now(),
                turn_count: turns * 2 + 1, // AI greeting + user/AI pairs
                keywords: keywords.join(', ')
            }
        });
        
        console.log(`📝 Generated ${i+1}/25: CallSid=${callSid.slice(-8)}, Keywords=${keywords.join(', ')}`);
    }

    // Batch insert
    console.log('\n💾 Adding to ChromaDB...');
    await collection.add({
        ids: fakeData.map(d => d.id),
        documents: fakeData.map(d => d.document),
        metadatas: fakeData.map(d => d.metadata)
    });

    const finalCount = await collection.count();
    console.log(`✅ Populated! Total transcripts: ${finalCount} (+${finalCount - initialCount})`);

    // Test query
    console.log('\n🔍 Testing query: "anxiety"');
    const results = await collection.query({
        nResults: 3,
        queryTexts: ["can you access all records referring to mental illness"],
        include: ['metadatas', 'documents']
    });
    
    results.metadatas[0].forEach((meta, i) => {
        console.log(`  ${i+1}. ${meta.keywords} (${meta.turn_count} turns)`);
    });
}

populateChroma().catch(err => {
    console.error('❌ Error:', err.message);
    process.exit(1);
});

```

### openai-calls.js

```javascript
// ai-helper.js - OpenAI API integration for generating follow-up questions

const OpenAI = require('openai');
require('dotenv').config();

const openai = new OpenAI({
    apiKey: process.env.OPENAI_API_KEY
});

/**
 * Generate follow-up question with symptom context
 * 
 */

async function callOpenAI(systemPrompt, userPrompt) {
    const response = await openai.chat.completions.create({
        model: 'gpt-4o',
        messages: [
            { role: 'system', content: systemPrompt },
            { role: 'user', content: userPrompt }
        ],
        response_format: { type: 'json_object' },
        temperature: 0.3
    });
    return JSON.parse(response.choices[0].message.content);
}


async function generateFollowUpQuestion(transcript, symptomContext = "") {
    try {
        const response = await openai.chat.completions.create({
            model: 'gpt-4o',
            messages: [{
                role: 'system',
                content: `You are a compassionate healthcare assistant.

${symptomContext}

- Prioritize OVERDUE symptoms
- Reference previous symptom history
- If the user mentions their pain, ask them to rate it on a 1-7 scale, and mention this again if you're checking in on the symptoms.
- Ask about progression (better/worse/same)
- Keep responses brief (1-2 sentences)
- Be empathetic and natural`
            }, {
                role: 'user',
                content: transcript
            }],
            temperature: 0.7,
            max_tokens: 150
        });

        return response.choices[0].message.content.trim();
    } catch (error) {
        console.error('OpenAI error:', error);
        throw error;
    }
}

/**
 * Extract structured medical data from transcript
 * @param {Array|string} transcript - Full conversation transcript
 * @param {string} phoneNumber - Patient's phone number
 * @returns {Object} - Structured medical features
 */
async function extractDBColumns(transcript, phoneNumber) {
    try {
        // Convert array to string if needed
        const transcriptText = Array.isArray(transcript) 
            ? transcript.join("\n") 
            : transcript;
        
        console.log('Extracting medical features from transcript...');
        
        const response = await openai.chat.completions.create({
            model: "gpt-4o",
            messages: [
                {
                    role: "system",
                    content: `You are a medical data extraction assistant. Extract specific features from patient conversation transcripts. Be precise and only extract explicitly mentioned information.`
                },
                {
                    role: "user",
                    content: `Extract the following from this medical conversation transcript:\n\n${transcriptText}`
                }
            ],
            tools: [
                {
                    type: "function",
                    function: {
                        name: "extract_medical_features",
                        description: "Extract structured medical data from patient transcript",
                        parameters: {
                            type: "object",
                            properties: {
                                pain_rating: {
                                    type: "number",
                                    description: "Pain rating from 1-7. If not mentioned, record your best estimate based on what the patient has described. It must be a number from 1 to 7. If range given, return the average.",
                                    nullable: true,
                                    minimum: 1,
                                    maximum: 7
                                },
                                pain_phrases: {
                                    type: "array",
                                    items: { type: "string" },
                                    description: "List of exact phrases related to pain (e.g., 'sharp pain in chest', 'dull ache', 'throbbing headache')"
                                },
                                body_parts: {
                                    type: "array",
                                    items: { type: "string" },
                                    description: "Body parts mentioned (e.g., 'head', 'upper right thigh', 'left foot', 'stomach', 'chest', 'throat')"
                                },
                                body_part_phrases: {
                                    type: "array",
                                    items: { type: "string" },
                                    description: "Full phrases where body parts are mentioned (e.g., 'pain in my chest', 'my left foot is swollen', 'pressure on upper right thigh')"
                                },
                                daily_mood: {
                                    type: "string",
                                    description: "Overall sentiment/mood in 2-5 words (e.g., 'worried and stressed', 'calm but tired', 'anxious about symptoms')"
                                },
                                estimated_health_metrics: {
                                    type: "object",
                                    description: "Physical health measurements mentioned or estimated (e.g. body temperature--(e.g. 98.6°F, fever, normal), blood pressure-- (e.g. 00, 120/80, high, normal), heart rate -- (e.g. 72 bpm, racing, normal)), any other physical measurements mentioned",
                                }
                            },
                            required: ["pain_rating", "pain_phrases", "body_parts", "body_part_phrases", "daily_mood", "estimated_health_metrics"]
                        }
                    }
                }
            ],
        });

        // Extract the function call result
        const toolCall = response.choices[0].message.tool_calls[0];
        const extractedData = JSON.parse(toolCall.function.arguments);
        
    
[truncated — 6058 more characters]
```

### archive/db-module.js

```javascript
const { ChromaClient } = require('chromadb');
const client = new ChromaClient({
    apiKey: process.env.CHROMA_API_KEY,
    tenant: process.env.CHROMA_TENANT,
    database: 'second'
});

/**
 * Store extracted medical features in ChromaDB
 * @param {string} callSid - Unique call identifier
 * @param {string} transcript - Full conversation transcript (as string)
 * @param {Object} extractedFeatures - Output from extractDBColumns()
 */
async function storeMedicalSession(callSid, transcript, extractedFeatures) {
    try {
        const collectionName = 'call_transcripts';
        
        const collection = await client.getOrCreateCollection({
            name: collectionName
        });
        
        // ChromaDB metadata (arrays must be stringified)
        const metadata = {
            // Basic info
            phoneNumber: extractedFeatures.phoneNumber,
            timestamp: extractedFeatures.timestamp,
            
            // Pain data
            pain_rating: extractedFeatures.pain_rating,  // number or null
            pain_phrases: JSON.stringify(extractedFeatures.pain_phrases || []),
            
            // Body parts
            body_parts: JSON.stringify(extractedFeatures.body_parts || []),
            body_part_phrases: JSON.stringify(extractedFeatures.body_part_phrases || []),
            
            // Mood
            daily_mood: extractedFeatures.daily_mood || '',
            
            // Health metrics (stringify the whole object since structure may vary)
            estimated_health_metrics: JSON.stringify(extractedFeatures.estimated_health_metrics || {})
        };
        
        // Store in ChromaDB
        await collection.add({
            ids: [callSid],
            documents: [transcript],
            metadatas: [metadata]
        });
        
        console.log(`Stored medical session ${callSid} to ChromaDB`);
        
        return { success: true, callSid, collection: collectionName };
        
    } catch (error) {
        console.error('Failed to store to ChromaDB:', error);
        throw error;
    }
}

module.exports = { storeMedicalSession };
```

### public/script.js

```javascript
const callButton = document.getElementById('callButton');
const phoneInput = document.getElementById('phoneNumber');
const statusDiv = document.getElementById('status');
const queryInput = document.getElementById('queryInput');
const modal = document.getElementById('settings-modal');

callButton.addEventListener('click', async () => {
    const phoneNumber = phoneInput.value.trim();
    
    // Validate phone number
    if (!phoneNumber) {
        showStatus('Please enter a phone number', 'error');
        return;
    }
    
    // Disable button and show loading state
    callButton.disabled = true;
    callButton.textContent = 'Calling...';
    showStatus('Initiating call...', 'info');
    
    try {
        const response = await fetch('/make-call', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ phoneNumber: phoneNumber })
        });
        
        const data = await response.json();
        
        if (response.ok) {
            showStatus('Call initiated successfully!', 'success');
        } else {
            showStatus(`Error: ${data.error}`, 'error');
        }
    } catch (error) {
        showStatus('Failed to connect. Please try again.', 'error');
        console.error('Error:', error);
    } finally {
        // Re-enable button
        callButton.disabled = false;
        callButton.textContent = 'Call Now';
    }
});

function showStatus(message, type) {
    statusDiv.textContent = message;
    statusDiv.className = type;
}

//open toggle in css
document.querySelector('.settings-gear').onclick = () => modal.style.display = 'block';

//close toggle in css
document.querySelector('.close').onclick = () => modal.style.display = 'none';
window.onclick = (e) => { if (e.target === modal) modal.style.display = 'none'; }

//save demographics into localStorage
document.getElementById('save-settings').onclick = () => {
    localStorage.setItem('settings', JSON.stringify({
        age: document.getElementById('age').value,
        sex: document.getElementById('sex').value,
        conditions: document.getElementById('conditions').value,
        family: document.getElementById('family').value
    }));
    modal.style.display = 'none';   //close popup on save
};
```

[11 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]