PD Care is a peritoneal dialysis exit-site imaging and infection alert system. This repository contains the patient-facing frontend, backend API/inference services, PostgreSQL metadata storage, and SeaweedFS object-storage services that together power guided capture, AI screening, clinical review, and alert workflows.
- Classification training repository
- Detection model training repository
- Classification model repository
- Detection model repository
- Standardize exit-site image capture quality for peritoneal dialysis patients.
- Provide AI-assisted infection risk alerts while keeping diagnosis and treatment decisions with physicians.
- Build a reviewable image database for care follow-up and research.
| Role | Function Scope | Key Requirements |
|---|---|---|
| Patient | Capture images and receive guidance/alerts | Guided UI, alignment prompts, image upload |
| Healthcare staff | Review image history and model outputs | Filtering/sorting, detail review, manual annotation, follow-up tracking |
| Backend admin | Operate and maintain platform services | User management, model versioning, data access control |
- Patient-side guided capture
- Single-step image capture with on-screen guidance (lighting, exit-site alignment, catheter-line visibility).
- Backend validation and AI pipeline
- Preliminary image checks reject low-quality uploads with explicit reasons.
- Exit-site presence pre-screen (optional): CLIP vision encoder + sklearn linear classifier (logistic-style probe on frozen embeddings) gates whether an upload is treated as exit-site imagery for screening. Frames that fail the gate are persisted as rejected with an explicit reason so random or off-topic photos do not run infection inference. Inference errors on this step fail open so screening still proceeds if the probe cannot run.
- CNN multiclass classifier produces the guided-capture pathology labels used for suspected-infection alerting (distinct from presence pre-screening).
- Clinical review workflow
- Healthcare staff review upload history, model outputs, rejected reasons, and follow-up status.
- Filtered/sorted table workflows and export support operational monitoring.
- Follow-up communication
LINEAlertnotification channel sends guidance alerts for suspected cases.- Alert messages are assistive only and do not constitute diagnosis.
This architecture is aligned with service boundaries and dependencies in docker-compose.yml.
frontendis the HTTPS entrypoint and depends on a healthybackend.backendprovides API, inference, and workflow logic, and depends on healthypostgresandseaweedfs-s3.- SeaweedFS object storage is composed as:
seaweedfs-s3 -> seaweedfs-filer -> (seaweedfs-master + seaweedfs-volume).
- Persistence responsibilities:
postgresstores structured metadata and workflow records.- SeaweedFS S3-compatible endpoint stores image assets and related binary objects.
flowchart LR
userClient[UserClientBrowserLIFF] --> frontendSvc[frontend]
frontendSvc --> backendSvc[backend]
backendSvc --> postgresSvc[postgres]
backendSvc --> seaweedS3Svc[seaweedfs_s3]
seaweedS3Svc --> seaweedFilerSvc[seaweedfs_filer]
seaweedFilerSvc --> seaweedMasterSvc[seaweedfs_master]
seaweedFilerSvc --> seaweedVolumeSvc[seaweedfs_volume]
flowchart TD
patientApp[PatientFrontendLIFF] --> apiBackend[BackendAPI]
apiBackend --> precheck[ImagePrecheck]
precheck --> presenceGate["Exit-site presence gate (CLIP + linear probe)"]
presenceGate -->|accept| infectionScreen[CNN infection-oriented classifier]
presenceGate -->|reject| rejectedUpload[Rejected upload record]
infectionScreen --> reviewPortal[HealthcareReviewPortal]
infectionScreen --> lineNotify[LINEAlert]
reviewPortal --> dataExport[FilteredDataExport]
When pre-screen is disabled, or inference throws, the backend does not reject uploads on this gate (fail-open toward infection screening).
- Exit-site presence pre-screen: CLIP image features + sklearn linear classifier; configurable via
PRESCREEN_*env (see compose /apps/backendconfig). Separate from pathology classification. LINEAlert: downstream notification component triggered by screening outcomes.
Start the frontend and backend together from the repository root:
npm run devThe root npm run dev command starts both servers at the same time and prefixes log lines with color-coded FRONTEND and BACKEND labels.
npm run dev:frontend
npm run dev:backend
npm run dev
npm run build
npm run lint
npm run test
npm run docker:up
npm run docker:downAfter npm install at the repository root, Husky installs Git hooks automatically.
pre-commitrunsnpm run lintpre-pushrunsnpm run lint
If you need to bypass hooks for an emergency commit or push, use --no-verify.
apps/
backend/ FastAPI inference API
frontend/ Next.js application
docker-compose.yml
package.json
README.md
The frontend is a Next.js app for patient capture and the broader PD Care web experience.
- Runs locally at
http://localhost:3000 - Supports local API configuration through
NEXT_PUBLIC_API_BASE_URL - Uses the monorepo root scripts or
apps/frontendscripts for development
NEXT_PUBLIC_API_BASE_URL guidance:
- Local direct backend access:
http://localhost:8000 - Reverse-proxy / same-origin production (recommended):
/api - External dedicated API domain:
https://api.example.com
Start from the monorepo root:
npm run dev:frontendOr run it directly:
cd apps/frontend
npm install
npm run devThe backend is a FastAPI inference service that serves the production PyTorch checkpoint and exposes screening endpoints.
- Accepts
multipart/form-dataimage uploads - Downloads the model on startup when
MODEL_PATHis missing - Supports GPU-aware runtime with
DEVICE=auto - Includes health and readiness probes
- Returns both multiclass probabilities and binary infection screening output
Local development:
cd apps/backend
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install --upgrade pip
python3 -m pip install -r requirements-dev.txt
cp .env.example .env
set -a
. ./.env
set +a
python3 -m alembic -c alembic.ini upgrade head
python3 -m uvicorn app.main:app --reloadImportant backend environment variables:
MODEL_URL: checkpoint download URLMODEL_PATH: local checkpoint pathDEVICE:auto,cuda, orcpuTHRESHOLD: infection screening thresholdMAX_UPLOAD_MB: maximum upload sizeMODEL_BACKBONE: fallback backbone forstate_dictreconstructionMODEL_ARCH: fallback architecture whenMODEL_BACKBONE=none- Presence pre-screen (optional):
PRESCREEN_ENABLED,PRESCREEN_MODEL_REPO_ID,PRESCREEN_MODEL_REVISION,PRESCREEN_REJECT_REASON,PRESCREEN_MODEL_CACHE_DIR, plusHF_TOKENwhen downloading the probe bundle from Hugging Face Hub
The root docker-compose.yml starts:
frontendonhttps://localhost(host port443)backendonhttp://localhost:8000postgreson127.0.0.1:5432by default (override bind/port withPDCARE_POSTGRES_PORT_BIND)- SeaweedFS S3 on
http://localhost:8333
PostgreSQL network access modes:
- Local-only mode (default, recommended):
PDCARE_POSTGRES_PORT_BIND=127.0.0.1:5432- Keep
PDCARE_POSTGRES_HBA_FILEunset (Postgres uses data-directory defaultpg_hba.conf).
- Controlled remote mode (exception only):
- Set an explicit host bind like
PDCARE_POSTGRES_PORT_BIND=0.0.0.0:5432 - Set
PDCARE_POSTGRES_HBA_FILE=/etc/pd-care/postgres/pg_hba.remote.conf - Require host/cloud firewall allowlists before exposing
5432 - Validate exposure after startup (
docker ps --format '{{.Names}} {{.Ports}}' | awk '/postgres/'and./ops/security/postgres_audit.sh)
- Set an explicit host bind like
Bring the full stack up with:
npm run docker:upOr start a single service:
npm run docker:up:frontend
npm run docker:up:backendThe compose file does not require apps/backend/.env. Backend defaults are set in compose, and you can override them from your shell with PDCARE_* variables.
Object storage persistence notes:
- SeaweedFS data is persisted with Docker named volumes for
seaweedfs-master,seaweedfs-volume, andseaweedfs-filer. npm run docker:up(and normaldocker compose downwithout-v) preserves uploaded objects in named volumes.- Avoid
docker compose down -vunless you intentionally want to remove all persisted object-storage metadata and chunks.
To verify persistence across restarts:
npm run docker:up
# Upload a test image through the app/API
npm run docker:down
npm run docker:up
# Verify the same uploaded image is still retrievableCommon overrides:
PDCARE_POSTGRES_PASSWORD(set in project root.env; keep this synchronized withPDCARE_DATABASE_URL)PDCARE_DATABASE_URLPDCARE_POSTGRES_PORT_BIND(defaults to127.0.0.1:5432; set0.0.0.0:5432only if you explicitly need remote DB access)PDCARE_POSTGRES_HBA_FILE(optional; defaults to in-data-dirpg_hba.conf, set to/etc/pd-care/postgres/pg_hba.remote.conffor controlled remote mode)PDCARE_S3_ENDPOINT_URLPDCARE_S3_REGIONPDCARE_S3_ACCESS_KEYPDCARE_S3_SECRET_KEYPDCARE_S3_BUCKET_NAMEPDCARE_IMAGE_ACCESS_TOKEN_SECRETPDCARE_IMAGE_ACCESS_TOKEN_TTL_SECONDSPDCARE_PRESCREEN_ENABLED,PDCARE_PRESCREEN_MODEL_REPO_ID,PDCARE_PRESCREEN_MODEL_REVISION,PDCARE_PRESCREEN_REJECT_REASON,PDCARE_PRESCREEN_MODEL_CACHE_DIR,PDCARE_HF_TOKEN(CLIP+logreg bundle and Hub access)
PostgreSQL persistence/auth note:
- PostgreSQL named volume data survives
npm run docker:down, so changingPDCARE_POSTGRES_PASSWORDin compose does not rotate credentials for an already-initialized data directory. - Recommended: keep a root
.envfile (ignored by git) with bothPDCARE_POSTGRES_PASSWORDandPDCARE_DATABASE_URLso application and DB credentials are managed in one place. - If backend startup fails with
password authentication failed for user "postgres"while using persistent volumes, align credentials and DB state manually:
docker exec pd-care-postgres-1 sh -lc \
"psql -U postgres -d postgres -c \"ALTER USER postgres WITH PASSWORD '<new-password>';\""
docker exec pd-care-postgres-1 sh -lc \
"db_exists=\$(psql -U postgres -d postgres -tAc \"SELECT 1 FROM pg_database WHERE datname='pd_care'\"); \
[ \"\$db_exists\" = \"1\" ] || psql -U postgres -d postgres -c \"CREATE DATABASE pd_care\""- Adjust the password/database names in those commands to match your
PDCARE_DATABASE_URL. - Security operations helpers:
./ops/security/collect_forensics.sh <case-id>to capture DB/container evidence with hash manifest../ops/security/postgres_audit.shfor periodic IOC + exposure checks../ops/security/postgres-incident-runbook.mdfor the standardized incident workflow.
Frontend runtime/build overrides:
NEXT_PUBLIC_API_BASE_URL(defaults to/api)NEXT_PUBLIC_LIFF_IDLETSENCRYPT_DOMAIN(used to resolve cert/key under/etc/letsencrypt/live/<domain>/)PDCARE_HTTPS_PORT(defaults to443on the host)
TLS note:
- Compose frontend uses
apps/frontend/tls-gateway.cjsand expects valid cert files mounted from host/etc/letsencrypt. - If your certs are not present at the default domain path, set
LETSENCRYPT_DOMAINor provide your own TLS paths through env.
The default compose file now works on CPU-only hosts. The backend still honors DEVICE=auto, so it will use CUDA automatically when GPU access is available and fall back to CPU otherwise.
To require GPU access on a compatible host, include the GPU override file:
npm run docker:up:gpuIf your host only has deprecated docker-compose v1 and you hit KeyError: 'ContainerConfig' while recreating containers, use the same fallback the npm scripts use:
docker-compose down --remove-orphans
docker-compose up --buildThat override expects:
- NVIDIA drivers must be installed on the host
- NVIDIA Container Toolkit must be configured
- Docker Compose must be able to launch the service with the NVIDIA runtime
curl http://127.0.0.1:8000/healthz
curl http://127.0.0.1:8000/readyzcurl -X POST http://127.0.0.1:8000/v1/predict \
-H "accept: application/json" \
-F "file=@/path/to/image.jpg;type=image/jpeg"Example response shape:
{
"predicted_class_index": 4,
"predicted_class_name": "class_4",
"predicted_probability": 0.97,
"class_probabilities": [
{ "class_index": 0, "class_name": "class_0", "probability": 0.01 }
],
"screening": {
"infection_class_index": 4,
"infection_class_name": "class_4",
"infection_probability": 0.97,
"threshold": 0.5,
"is_infection_positive": true
}
}Infection-oriented classifier (multiclass backbone) — production screening path:
- 5 output classes:
class_0throughclass_4 class_4is the infection-positive class- preprocessing uses
Resize(384) -> CenterCrop(384) -> ToTensor() -> ImageNet normalize
If a future checkpoint is exported as a plain state_dict, set MODEL_BACKBONE and related fallback environment variables correctly so the service can reconstruct the model before loading weights.
Exit-site presence pre-screen (CLIP + linear probe) — optional upload gate:
- Loads a Hugging Face snapshot (probe weights + bundle metadata such as CLIP checkpoint id and decision threshold); default repo id is set in
docker-compose.yml(ruby0322/pd-exit-site-clip-linear-probe). - At inference time the service embeds each image with CLIP, then applies the sklearn linear probe scored like logistic regression (
predict_probaordecision_functionfallback). - Does not use a YOLO detector in the deployed patient-upload path for this gate. Separate YOLO-based detection experimentation and training live in
ntuh-pd-exit-site-detectionand are not described here as runtime dependencies for presence compliance unless you intentionally wire something else later.