mystic_CBK commited on
Commit
05f4192
·
0 Parent(s):

Initial ECG-FM API deployment

Browse files
Files changed (7) hide show
  1. Dockerfile +16 -0
  2. README.md +153 -0
  3. deploy.ps1 +57 -0
  4. deploy.sh +59 -0
  5. requirements.txt +7 -0
  6. server.py +80 -0
  7. test_client.py +104 -0
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ ENV PYTHONUNBUFFERED=1 \
4
+ HF_HOME=/root/.cache/huggingface \
5
+ PORT=7860
6
+
7
+ RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
8
+
9
+ WORKDIR /app
10
+ COPY requirements.txt .
11
+ RUN pip install --no-cache-dir -r requirements.txt && \
12
+ pip install --no-cache-dir git+https://github.com/bowang-lab/fairseq-signals.git
13
+
14
+ COPY . .
15
+ EXPOSE 7860
16
+ CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ECG-FM API Deployment
2
+
3
+ This directory contains the complete setup for deploying the ECG-FM (ECG Foundation Model) as a REST API on Hugging Face Spaces.
4
+
5
+ ## What is ECG-FM?
6
+
7
+ ECG-FM is a transformer-based foundation model for electrocardiogram analysis, pretrained on 2.5 million ECG samples. It can be used for various ECG interpretation tasks.
8
+
9
+ ## Files Overview
10
+
11
+ - **`Dockerfile`** - Container configuration for Hugging Face Spaces
12
+ - **`requirements.txt`** - Python dependencies
13
+ - **`server.py`** - FastAPI server with ECG-FM inference endpoints
14
+ - **`test_client.py`** - Test script to verify API functionality
15
+ - **`README.md`** - This documentation
16
+
17
+ ## API Endpoints
18
+
19
+ ### Health Check
20
+ - **GET** `/healthz` - Returns API health status and model loading status
21
+
22
+ ### Root
23
+ - **GET** `/` - Returns basic API information
24
+
25
+ ### Prediction
26
+ - **POST** `/predict` - Main inference endpoint
27
+
28
+ #### Input Format
29
+ ```json
30
+ {
31
+ "signal": [
32
+ [0.1, 0.2, 0.3, ...], // Lead I - 5000 samples
33
+ [0.2, 0.1, 0.4, ...], // Lead II - 5000 samples
34
+ [0.3, 0.2, 0.1, ...], // Lead III - 5000 samples
35
+ [0.1, 0.3, 0.2, ...], // Lead aVR - 5000 samples
36
+ [0.2, 0.1, 0.3, ...], // Lead aVL - 5000 samples
37
+ [0.3, 0.2, 0.1, ...], // Lead aVF - 5000 samples
38
+ [0.1, 0.2, 0.3, ...], // Lead V1 - 5000 samples
39
+ [0.2, 0.1, 0.4, ...], // Lead V2 - 5000 samples
40
+ [0.3, 0.2, 0.1, ...], // Lead V3 - 5000 samples
41
+ [0.1, 0.3, 0.2, ...], // Lead V4 - 5000 samples
42
+ [0.2, 0.1, 0.3, ...], // Lead V5 - 5000 samples
43
+ [0.3, 0.2, 0.1, ...] // Lead V6 - 5000 samples
44
+ ],
45
+ "fs": 500 // Sampling frequency in Hz (optional)
46
+ }
47
+ ```
48
+
49
+ #### Output Format
50
+ ```json
51
+ {
52
+ "output": [...], // Model predictions
53
+ "input_shape": [1, 12, 5000], // Input tensor shape
54
+ "output_shape": [...] // Output shape
55
+ }
56
+ ```
57
+
58
+ ## Deployment Steps
59
+
60
+ ### 1. Local Testing (Optional)
61
+ ```bash
62
+ # Install dependencies
63
+ pip install -r requirements.txt
64
+ pip install git+https://github.com/bowang-lab/fairseq-signals.git
65
+
66
+ # Run server locally
67
+ python server.py
68
+
69
+ # Test API
70
+ python test_client.py
71
+ ```
72
+
73
+ ### 2. Deploy to Hugging Face Spaces
74
+ 1. Create a new Space on [huggingface.co/spaces](https://huggingface.co/spaces)
75
+ - SDK: Docker
76
+ - Template: Blank
77
+ - Hardware: CPU Basic (free tier)
78
+
79
+ 2. Clone the Space repository:
80
+ ```bash
81
+ git clone https://huggingface.co/spaces/YOUR_USERNAME/ecg-fm-api
82
+ cd ecg-fm-api
83
+ ```
84
+
85
+ 3. Copy all files from this directory to the cloned repo
86
+
87
+ 4. Push to trigger build:
88
+ ```bash
89
+ git add .
90
+ git commit -m "Initial ECG-FM API deployment"
91
+ git push
92
+ ```
93
+
94
+ ### 3. Test Deployed API
95
+ ```python
96
+ import requests
97
+ import numpy as np
98
+
99
+ # Replace with your actual Space URL
100
+ SPACE_URL = "https://YOUR_USERNAME-ecg-fm-api.hf.space"
101
+
102
+ # Test with dummy data
103
+ dummy_signal = np.random.randn(12, 5000).astype(np.float32).tolist()
104
+ payload = {"signal": dummy_signal, "fs": 500}
105
+
106
+ r = requests.post(f"{SPACE_URL}/predict", json=payload)
107
+ print(r.status_code, r.json())
108
+ ```
109
+
110
+ ## Configuration
111
+
112
+ ### Environment Variables
113
+ - **`MODEL_REPO`** - Hugging Face model repository (default: "wanglab/ecg-fm")
114
+ - **`CKPT`** - Checkpoint filename (default: "mimic_iv_ecg_physionet_pretrained.pt")
115
+ - **`CFG`** - Config filename (default: "mimic_iv_ecg_physionet_pretrained.yaml")
116
+ - **`HF_TOKEN`** - Hugging Face token (optional, for private repos)
117
+
118
+ ### Model Checkpoints
119
+ Available checkpoints in `wanglab/ecg-fm`:
120
+ - `mimic_iv_ecg_physionet_pretrained.pt` - Pretrained on MIMIC-IV-ECG and PhysioNet
121
+ - `physionet_finetuned.pt` - Fine-tuned on PhysioNet 2021
122
+
123
+ ## Performance Notes
124
+
125
+ ### Free Tier Limitations (CPU Basic)
126
+ - **Hardware**: 2 vCPUs, 16 GB RAM
127
+ - **Expected latency**: 30 seconds to 2 minutes per inference
128
+ - **Concurrency**: Limited to 1-2 concurrent requests
129
+ - **Cold starts**: Model loads on first request after inactivity
130
+
131
+ ### Optimization Tips
132
+ - Use shorter input windows (e.g., 5 seconds instead of 10)
133
+ - Consider ONNX export for faster CPU inference
134
+ - Use int8 quantization for memory efficiency
135
+
136
+ ## Troubleshooting
137
+
138
+ ### Common Issues
139
+ 1. **Build fails on fairseq-signals**: Check build logs, may need specific Git commit
140
+ 2. **Out of memory**: Reduce input size or use smaller checkpoint
141
+ 3. **Model loading fails**: Verify HF_TOKEN if repo is private
142
+ 4. **Slow inference**: Expected on free CPU tier; consider GPU upgrade
143
+
144
+ ### Next Steps
145
+ - **For production**: Move to RunPod GPU for better latency
146
+ - **For optimization**: Export to ONNX, add quantization
147
+ - **For scaling**: Add authentication, rate limiting, input validation
148
+
149
+ ## Support
150
+
151
+ - ECG-FM Paper: [arxiv.org/abs/2408.05178](https://arxiv.org/abs/2408.05178)
152
+ - ECG-FM Repository: [github.com/bowang-lab/ECG-FM](https://github.com/bowang-lab/ECG-FM)
153
+ - fairseq-signals: [github.com/bowang-lab/fairseq-signals](https://github.com/bowang-lab/fairseq-signals)
deploy.ps1 ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ECG-FM API Deployment Script (PowerShell)
2
+ # This script helps deploy your ECG-FM API to Hugging Face Spaces
3
+
4
+ Write-Host "🚀 ECG-FM API Deployment Script" -ForegroundColor Green
5
+ Write-Host "================================" -ForegroundColor Green
6
+
7
+ # Check if we're in the right directory
8
+ if (-not (Test-Path "server.py") -or -not (Test-Path "Dockerfile") -or -not (Test-Path "requirements.txt")) {
9
+ Write-Host "❌ Error: Please run this script from the ECG-FM directory containing server.py, Dockerfile, and requirements.txt" -ForegroundColor Red
10
+ exit 1
11
+ }
12
+
13
+ Write-Host "✅ Found all required files" -ForegroundColor Green
14
+ Write-Host ""
15
+
16
+ # Check if git is initialized
17
+ if (-not (Test-Path ".git")) {
18
+ Write-Host "📁 Initializing git repository..." -ForegroundColor Yellow
19
+ git init
20
+ git remote add origin https://huggingface.co/spaces/mystic-cbk/ecg-fm-api
21
+ Write-Host "✅ Git repository initialized" -ForegroundColor Green
22
+ } else {
23
+ Write-Host "✅ Git repository already exists" -ForegroundColor Green
24
+ }
25
+
26
+ Write-Host ""
27
+ Write-Host "📋 Current git status:" -ForegroundColor Cyan
28
+ git status
29
+
30
+ Write-Host ""
31
+ Write-Host "🔧 Adding all files to git..." -ForegroundColor Yellow
32
+ git add .
33
+
34
+ Write-Host ""
35
+ Write-Host "💾 Committing changes..." -ForegroundColor Yellow
36
+ git commit -m "Update ECG-FM API deployment"
37
+
38
+ Write-Host ""
39
+ Write-Host "🚀 Pushing to Hugging Face Spaces..." -ForegroundColor Yellow
40
+ git push origin main
41
+
42
+ Write-Host ""
43
+ Write-Host "✅ Deployment initiated!" -ForegroundColor Green
44
+ Write-Host ""
45
+ Write-Host "📊 Next steps:" -ForegroundColor Cyan
46
+ Write-Host "1. Go to: https://huggingface.co/spaces/mystic-cbk/ecg-fm-api" -ForegroundColor White
47
+ Write-Host "2. Watch the build logs in the 'Build logs' tab" -ForegroundColor White
48
+ Write-Host "3. Wait for build to complete (5-15 minutes)" -ForegroundColor White
49
+ Write-Host "4. Test your API with: python test_client.py" -ForegroundColor White
50
+ Write-Host ""
51
+ Write-Host "🌐 Your API will be available at:" -ForegroundColor Cyan
52
+ Write-Host " https://mystic-cbk-ecg-fm-api.hf.space" -ForegroundColor White
53
+ Write-Host ""
54
+ Write-Host "📱 Test endpoints:" -ForegroundColor Cyan
55
+ Write-Host " - Health: https://mystic-cbk-ecg-fm-api.hf.space/healthz" -ForegroundColor White
56
+ Write-Host " - Root: https://mystic-cbk-ecg-fm-api.hf.space/" -ForegroundColor White
57
+ Write-Host " - Predict: POST https://mystic-cbk-ecg-fm-api.hf.space/predict" -ForegroundColor White
deploy.sh ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # ECG-FM API Deployment Script
4
+ # This script helps deploy your ECG-FM API to Hugging Face Spaces
5
+
6
+ echo "🚀 ECG-FM API Deployment Script"
7
+ echo "================================"
8
+
9
+ # Check if we're in the right directory
10
+ if [ ! -f "server.py" ] || [ ! -f "Dockerfile" ] || [ ! -f "requirements.txt" ]; then
11
+ echo "❌ Error: Please run this script from the ECG-FM directory containing server.py, Dockerfile, and requirements.txt"
12
+ exit 1
13
+ fi
14
+
15
+ echo "✅ Found all required files"
16
+ echo ""
17
+
18
+ # Check if git is initialized
19
+ if [ ! -d ".git" ]; then
20
+ echo "📁 Initializing git repository..."
21
+ git init
22
+ git remote add origin https://huggingface.co/spaces/mystic-cbk/ecg-fm-api
23
+ echo "✅ Git repository initialized"
24
+ else
25
+ echo "✅ Git repository already exists"
26
+ fi
27
+
28
+ echo ""
29
+ echo "📋 Current git status:"
30
+ git status
31
+
32
+ echo ""
33
+ echo "🔧 Adding all files to git..."
34
+ git add .
35
+
36
+ echo ""
37
+ echo "💾 Committing changes..."
38
+ git commit -m "Update ECG-FM API deployment"
39
+
40
+ echo ""
41
+ echo "🚀 Pushing to Hugging Face Spaces..."
42
+ git push origin main
43
+
44
+ echo ""
45
+ echo "✅ Deployment initiated!"
46
+ echo ""
47
+ echo "📊 Next steps:"
48
+ echo "1. Go to: https://huggingface.co/spaces/mystic-cbk/ecg-fm-api"
49
+ echo "2. Watch the build logs in the 'Build logs' tab"
50
+ echo "3. Wait for build to complete (5-15 minutes)"
51
+ echo "4. Test your API with: python test_client.py"
52
+ echo ""
53
+ echo "🌐 Your API will be available at:"
54
+ echo " https://mystic-cbk-ecg-fm-api.hf.space"
55
+ echo ""
56
+ echo "📱 Test endpoints:"
57
+ echo " - Health: https://mystic-cbk-ecg-fm-api.hf.space/healthz"
58
+ echo " - Root: https://mystic-cbk-ecg-fm-api.hf.space/"
59
+ echo " - Predict: POST https://mystic-cbk-ecg-fm-api.hf.space/predict"
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ numpy
4
+ huggingface-hub
5
+ pyyaml
6
+ einops
7
+ torch==2.3.1 --index-url https://download.pytorch.org/whl/cpu
server.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ import torch
4
+ from fastapi import FastAPI
5
+ from pydantic import BaseModel
6
+ from typing import List, Optional
7
+ from huggingface_hub import hf_hub_download
8
+ from fairseq_signals.models import build_model_from_checkpoint
9
+
10
+ # Configuration
11
+ MODEL_REPO = os.getenv("MODEL_REPO", "wanglab/ecg-fm")
12
+ CKPT = os.getenv("CKPT", "mimic_iv_ecg_physionet_pretrained.pt")
13
+ CFG = os.getenv("CFG", "mimic_iv_ecg_physionet_pretrained.yaml")
14
+ HF_TOKEN = os.getenv("HF_TOKEN") # optional if repo is public
15
+
16
+ class ECGPayload(BaseModel):
17
+ signal: List[List[float]] # shape: [leads, samples], e.g., [12, 5000]
18
+ fs: Optional[int] = None # sampling rate (optional)
19
+
20
+ app = FastAPI(title="ECG-FM API", description="ECG Foundation Model API")
21
+
22
+ model = None
23
+
24
+ def load_model():
25
+ print(f"Loading model from {MODEL_REPO}...")
26
+ try:
27
+ ckpt = hf_hub_download(MODEL_REPO, CKPT, token=HF_TOKEN)
28
+ cfg = hf_hub_download(MODEL_REPO, CFG, token=HF_TOKEN)
29
+ print(f"Checkpoint: {ckpt}")
30
+ print(f"Config: {cfg}")
31
+
32
+ m = build_model_from_checkpoint(checkpoint_path=ckpt, config_path=cfg)
33
+ m.eval()
34
+ print("Model loaded successfully!")
35
+ return m
36
+ except Exception as e:
37
+ print(f"Error loading model: {e}")
38
+ raise
39
+
40
+ @app.on_event("startup")
41
+ def _startup():
42
+ global model
43
+ model = load_model()
44
+
45
+ @app.get("/")
46
+ def root():
47
+ return {"message": "ECG-FM API is running", "model": MODEL_REPO}
48
+
49
+ @app.get("/healthz")
50
+ def healthz():
51
+ return {"status": "ok", "model_loaded": model is not None}
52
+
53
+ @app.post("/predict")
54
+ def predict(p: ECGPayload):
55
+ if model is None:
56
+ return {"error": "Model not loaded"}
57
+
58
+ try:
59
+ # Convert input to tensor [1, leads, samples]
60
+ x = torch.tensor(p.signal, dtype=torch.float32).unsqueeze(0)
61
+ print(f"Input shape: {x.shape}")
62
+
63
+ with torch.no_grad():
64
+ y = model(x)
65
+
66
+ # Handle model output format
67
+ if isinstance(y, (list, tuple)):
68
+ y = y[0]
69
+ if hasattr(y, "detach"):
70
+ y = y.detach()
71
+
72
+ out = torch.as_tensor(y).cpu().numpy().tolist()
73
+ return {"output": out, "input_shape": x.shape, "output_shape": [len(out)]}
74
+
75
+ except Exception as e:
76
+ return {"error": str(e)}
77
+
78
+ if __name__ == "__main__":
79
+ import uvicorn
80
+ uvicorn.run(app, host="0.0.0.0", port=7860)
test_client.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import numpy as np
3
+ import json
4
+
5
+ def test_api(base_url="http://localhost:7860"):
6
+ """Test the ECG-FM API endpoints"""
7
+
8
+ print(f"Testing API at: {base_url}")
9
+
10
+ # Test 1: Health check
11
+ print("\n1. Testing health endpoint...")
12
+ try:
13
+ r = requests.get(f"{base_url}/healthz")
14
+ print(f" Health status: {r.status_code}")
15
+ if r.status_code == 200:
16
+ print(f" Response: {r.json()}")
17
+ else:
18
+ print(f" Error: {r.text}")
19
+ except Exception as e:
20
+ print(f" Health check failed: {e}")
21
+ return False
22
+
23
+ # Test 2: Root endpoint
24
+ print("\n2. Testing root endpoint...")
25
+ try:
26
+ r = requests.get(f"{base_url}/")
27
+ print(f" Root status: {r.status_code}")
28
+ if r.status_code == 200:
29
+ print(f" Response: {r.json()}")
30
+ else:
31
+ print(f" Error: {r.text}")
32
+ except Exception as e:
33
+ print(f" Root check failed: {e}")
34
+
35
+ # Test 3: Predict endpoint with dummy data
36
+ print("\n3. Testing predict endpoint...")
37
+
38
+ # Create dummy 12-lead ECG data (12 leads, 5000 samples)
39
+ # This simulates 10 seconds of ECG at 500 Hz
40
+ dummy_signal = np.random.randn(12, 5000).astype(np.float32).tolist()
41
+
42
+ payload = {
43
+ "signal": dummy_signal,
44
+ "fs": 500
45
+ }
46
+
47
+ try:
48
+ r = requests.post(f"{base_url}/predict", json=payload)
49
+ print(f" Predict status: {r.status_code}")
50
+ if r.status_code == 200:
51
+ result = r.json()
52
+ print(f" Success! Output shape: {result.get('output_shape', 'unknown')}")
53
+ print(f" Input shape: {result.get('input_shape', 'unknown')}")
54
+ print(f" Output length: {len(result.get('output', []))}")
55
+ else:
56
+ print(f" Error: {r.text}")
57
+ except Exception as e:
58
+ print(f" Predict failed: {e}")
59
+
60
+ return True
61
+
62
+ def test_with_real_data(base_url="http://localhost:7860"):
63
+ """Test with more realistic ECG-like data"""
64
+ print("\n4. Testing with realistic ECG-like data...")
65
+
66
+ # Create more realistic ECG-like signal (sine wave with noise)
67
+ t = np.linspace(0, 10, 5000) # 10 seconds, 500 Hz
68
+ realistic_signal = []
69
+
70
+ for lead in range(12):
71
+ # Create a sine wave with some variation per lead
72
+ freq = 1.0 + 0.1 * lead # Different frequency per lead
73
+ signal = np.sin(2 * np.pi * freq * t) * 0.5
74
+ # Add some noise
75
+ signal += np.random.normal(0, 0.1, len(t))
76
+ realistic_signal.append(signal.tolist())
77
+
78
+ payload = {
79
+ "signal": realistic_signal,
80
+ "fs": 500
81
+ }
82
+
83
+ try:
84
+ r = requests.post(f"{base_url}/predict", json=payload)
85
+ print(f" Realistic data test status: {r.status_code}")
86
+ if r.status_code == 200:
87
+ result = r.json()
88
+ print(f" Success! Output shape: {result.get('output_shape', 'unknown')}")
89
+ else:
90
+ print(f" Error: {r.text}")
91
+ except Exception as e:
92
+ print(f" Realistic data test failed: {e}")
93
+
94
+ if __name__ == "__main__":
95
+ # Test locally first
96
+ print("=== Testing Local API ===")
97
+ test_api("http://localhost:7860")
98
+
99
+ # Test with realistic data
100
+ test_with_real_data("http://localhost:7860")
101
+
102
+ print("\n=== Testing Complete ===")
103
+ print("\nTo test your deployed Hugging Face Space, update the base_url:")
104
+ print("test_api('https://mystic-cbk-ecg-fm-api.hf.space')")