MakPr016 commited on
Commit
a2543e1
Β·
1 Parent(s): 17622db

Updated files + Dockerfile

Browse files
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile CHANGED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ ENV TRANSFORMERS_CACHE=/tmp/cache
6
+ ENV HF_HOME=/tmp/cache
7
+ ENV TORCH_HOME=/tmp/cache
8
+ ENV EASYOCR_MODULE_PATH=/tmp/cache
9
+ ENV PYTHONUNBUFFERED=1
10
+
11
+ RUN mkdir -p /tmp/cache && chmod 777 /tmp/cache
12
+
13
+ RUN apt-get update && apt-get install -y \
14
+ libgl1 libglib2.0-0 libsm6 libxext6 libxrender-dev libgomp1 \
15
+ poppler-utils tesseract-ocr tesseract-ocr-eng libmagic1 \
16
+ git git-lfs curl wget && \
17
+ rm -rf /var/lib/apt/lists/*
18
+
19
+ COPY requirements.txt .
20
+ RUN pip install --no-cache-dir --upgrade pip && \
21
+ pip install --no-cache-dir -r requirements.txt
22
+
23
+ RUN python -c "import easyocr; easyocr.Reader(['en'], gpu=False, download_enabled=True)"
24
+
25
+ COPY app/ ./app/
26
+
27
+ # Download NER model from your HuggingFace repo
28
+ RUN mkdir -p models/radiolo_clinic_ner && \
29
+ git clone https://huggingface.co/MakPr016/clinical-ner-model models/radiolo_clinic_ner && \
30
+ echo "βœ“ NER model downloaded from HuggingFace"
31
+
32
+ ENV HOST=0.0.0.0
33
+ ENV PORT=7860
34
+ ENV LAB_NER_MODEL_PATH=/app/models/radiolo_clinic_ner
35
+
36
+ RUN mkdir -p logs
37
+
38
+ EXPOSE 7860
39
+
40
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \
41
+ CMD curl -f http://localhost:7860/health || exit 1
42
+
43
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
app/crypto_utils.py CHANGED
@@ -16,15 +16,11 @@ class CryptoManager:
16
  if not secret_key_hex:
17
  raise ValueError("Secret key is required")
18
 
19
- # Check if it's already the right length
20
  if len(secret_key_hex) == 64:
21
- # 64 hex chars = 32 bytes (correct)
22
  self.secret_key = bytes.fromhex(secret_key_hex)
23
  elif len(secret_key_hex) == 32:
24
- # If someone passes 32 chars thinking it's bytes, warn them
25
  print(f"⚠️ WARNING: Key is only 32 characters (16 bytes)")
26
  print(f" Should be 64 hex characters for 32 bytes")
27
- # Try to use as-is but it will fail
28
  self.secret_key = secret_key_hex.encode('utf-8')
29
  else:
30
  raise ValueError(f"Secret key must be 64 hex characters (got {len(secret_key_hex)})")
@@ -55,12 +51,10 @@ class CryptoManager:
55
  Encrypt JSON data with compression
56
  Returns dict with base64-encoded ciphertext and nonce
57
  """
58
- # Convert to JSON and compress
59
  json_data = json.dumps(data).encode('utf-8')
60
  compressed = gzip.compress(json_data, compresslevel=6)
61
  compressed_b64 = base64.b64encode(compressed).decode('utf-8')
62
 
63
- # Encrypt
64
  nonce = random(SecretBox.NONCE_SIZE)
65
  ciphertext = self.box.encrypt(compressed_b64.encode('utf-8'), nonce)
66
 
@@ -74,15 +68,12 @@ class CryptoManager:
74
  Decrypt and decompress JSON data
75
  """
76
  try:
77
- # Decrypt
78
  decrypted = self.decrypt(ciphertext, nonce)
79
 
80
- # Decompress
81
  compressed_b64 = decrypted.decode('utf-8')
82
  compressed_bytes = base64.b64decode(compressed_b64)
83
  decompressed = gzip.decompress(compressed_bytes)
84
 
85
- # Parse JSON
86
  return json.loads(decompressed.decode('utf-8'))
87
  except Exception as e:
88
  raise ValueError(f"Decryption/decompression failed. {e}")
 
16
  if not secret_key_hex:
17
  raise ValueError("Secret key is required")
18
 
 
19
  if len(secret_key_hex) == 64:
 
20
  self.secret_key = bytes.fromhex(secret_key_hex)
21
  elif len(secret_key_hex) == 32:
 
22
  print(f"⚠️ WARNING: Key is only 32 characters (16 bytes)")
23
  print(f" Should be 64 hex characters for 32 bytes")
 
24
  self.secret_key = secret_key_hex.encode('utf-8')
25
  else:
26
  raise ValueError(f"Secret key must be 64 hex characters (got {len(secret_key_hex)})")
 
51
  Encrypt JSON data with compression
52
  Returns dict with base64-encoded ciphertext and nonce
53
  """
 
54
  json_data = json.dumps(data).encode('utf-8')
55
  compressed = gzip.compress(json_data, compresslevel=6)
56
  compressed_b64 = base64.b64encode(compressed).decode('utf-8')
57
 
 
58
  nonce = random(SecretBox.NONCE_SIZE)
59
  ciphertext = self.box.encrypt(compressed_b64.encode('utf-8'), nonce)
60
 
 
68
  Decrypt and decompress JSON data
69
  """
70
  try:
 
71
  decrypted = self.decrypt(ciphertext, nonce)
72
 
 
73
  compressed_b64 = decrypted.decode('utf-8')
74
  compressed_bytes = base64.b64decode(compressed_b64)
75
  decompressed = gzip.decompress(compressed_bytes)
76
 
 
77
  return json.loads(decompressed.decode('utf-8'))
78
  except Exception as e:
79
  raise ValueError(f"Decryption/decompression failed. {e}")
app/main.py CHANGED
@@ -107,9 +107,6 @@ async def health_check():
107
  "supported_tests": 16
108
  }
109
 
110
- # ============================================================================
111
- # NEW: UNENCRYPTED TEST ENDPOINT (for testing only)
112
- # ============================================================================
113
 
114
  @app.post("/test-analyze", tags=["Testing"])
115
  async def test_analyze(file: UploadFile = File(...)):
@@ -123,13 +120,11 @@ async def test_analyze(file: UploadFile = File(...)):
123
  if not lab_processor:
124
  raise HTTPException(status_code=503, detail="Lab processor not loaded")
125
 
126
- # Read uploaded file
127
  file_bytes = await file.read()
128
  filename = file.filename
129
 
130
- print(f"\nπŸ“„ Processing test file: {filename} ({len(file_bytes)} bytes)")
131
 
132
- # Determine file type from extension
133
  if filename.lower().endswith('.pdf'):
134
  file_type = "pdf"
135
  extracted_text, ocr_used = extract_text_from_pdf(file_bytes)
@@ -145,10 +140,9 @@ async def test_analyze(file: UploadFile = File(...)):
145
  if not extracted_text or len(extracted_text.strip()) < 10:
146
  raise HTTPException(status_code=400, detail="Could not extract sufficient text from file")
147
 
148
- print(f"βœ“ Extracted {len(extracted_text)} characters (OCR: {ocr_used})")
149
 
150
- # Process with lab processor
151
- print("🧠 Processing with NER + ClinicalDistilBERT...")
152
  lab_analysis = lab_processor.extract_and_format(
153
  extracted_text,
154
  report_id=f"test_{int(time.time())}",
@@ -157,10 +151,9 @@ async def test_analyze(file: UploadFile = File(...)):
157
 
158
  processing_time = time.time() - start_time
159
 
160
- print(f"βœ… Processing complete in {processing_time:.2f}s")
161
- print(f" Tests extracted: {lab_analysis.get('metadata', {}).get('tests_extracted', 0)}\n")
162
 
163
- # Return unencrypted response
164
  response_data = {
165
  "status": "success",
166
  "processing_time": round(processing_time, 3),
@@ -184,9 +177,6 @@ async def test_analyze(file: UploadFile = File(...)):
184
  traceback.print_exc()
185
  raise HTTPException(status_code=500, detail=f"Internal error: {str(e)}")
186
 
187
- # ============================================================================
188
- # ENCRYPTED ENDPOINT (production)
189
- # ============================================================================
190
 
191
  @app.post("/analyze-lab-secure", tags=["Lab Analysis"])
192
  async def analyze_lab_secure(request: EncryptedRequest):
@@ -196,7 +186,6 @@ async def analyze_lab_secure(request: EncryptedRequest):
196
  if not lab_processor:
197
  raise HTTPException(status_code=503, detail="Lab processor not loaded")
198
 
199
- # Decrypt request
200
  decrypted_data = crypto_manager.decrypt(request.ciphertext, request.nonce)
201
  compressed_b64 = decrypted_data.decode('utf-8')
202
  compressed_bytes = base64.b64decode(compressed_b64)
@@ -208,7 +197,6 @@ async def analyze_lab_secure(request: EncryptedRequest):
208
  file_type = payload['file_type']
209
  file_bytes = base64.b64decode(file_data_b64)
210
 
211
- # Extract text
212
  if file_type == "pdf":
213
  extracted_text, ocr_used = extract_text_from_pdf(file_bytes)
214
  if not extracted_text or len(extracted_text.strip()) < 10:
@@ -223,7 +211,6 @@ async def analyze_lab_secure(request: EncryptedRequest):
223
  else:
224
  raise HTTPException(status_code=400, detail="Invalid file_type. Must be 'pdf' or 'image'")
225
 
226
- # Process with lab processor
227
  lab_analysis = lab_processor.extract_and_format(
228
  extracted_text,
229
  report_id=f"lab_{int(time.time())}",
@@ -245,7 +232,6 @@ async def analyze_lab_secure(request: EncryptedRequest):
245
  **lab_analysis
246
  }
247
 
248
- # Encrypt response
249
  encrypted_response = crypto_manager.encrypt_json(response_data)
250
 
251
  return {
 
107
  "supported_tests": 16
108
  }
109
 
 
 
 
110
 
111
  @app.post("/test-analyze", tags=["Testing"])
112
  async def test_analyze(file: UploadFile = File(...)):
 
120
  if not lab_processor:
121
  raise HTTPException(status_code=503, detail="Lab processor not loaded")
122
 
 
123
  file_bytes = await file.read()
124
  filename = file.filename
125
 
126
+ # print(f"\nπŸ“„ Processing test file: {filename} ({len(file_bytes)} bytes)")
127
 
 
128
  if filename.lower().endswith('.pdf'):
129
  file_type = "pdf"
130
  extracted_text, ocr_used = extract_text_from_pdf(file_bytes)
 
140
  if not extracted_text or len(extracted_text.strip()) < 10:
141
  raise HTTPException(status_code=400, detail="Could not extract sufficient text from file")
142
 
143
+ # print(f"βœ“ Extracted {len(extracted_text)} characters (OCR: {ocr_used})")
144
 
145
+ # print("🧠 Processing with NER + ClinicalDistilBERT...")
 
146
  lab_analysis = lab_processor.extract_and_format(
147
  extracted_text,
148
  report_id=f"test_{int(time.time())}",
 
151
 
152
  processing_time = time.time() - start_time
153
 
154
+ # print(f"βœ… Processing complete in {processing_time:.2f}s")
155
+ # print(f" Tests extracted: {lab_analysis.get('metadata', {}).get('tests_extracted', 0)}\n")
156
 
 
157
  response_data = {
158
  "status": "success",
159
  "processing_time": round(processing_time, 3),
 
177
  traceback.print_exc()
178
  raise HTTPException(status_code=500, detail=f"Internal error: {str(e)}")
179
 
 
 
 
180
 
181
  @app.post("/analyze-lab-secure", tags=["Lab Analysis"])
182
  async def analyze_lab_secure(request: EncryptedRequest):
 
186
  if not lab_processor:
187
  raise HTTPException(status_code=503, detail="Lab processor not loaded")
188
 
 
189
  decrypted_data = crypto_manager.decrypt(request.ciphertext, request.nonce)
190
  compressed_b64 = decrypted_data.decode('utf-8')
191
  compressed_bytes = base64.b64decode(compressed_b64)
 
197
  file_type = payload['file_type']
198
  file_bytes = base64.b64decode(file_data_b64)
199
 
 
200
  if file_type == "pdf":
201
  extracted_text, ocr_used = extract_text_from_pdf(file_bytes)
202
  if not extracted_text or len(extracted_text.strip()) < 10:
 
211
  else:
212
  raise HTTPException(status_code=400, detail="Invalid file_type. Must be 'pdf' or 'image'")
213
 
 
214
  lab_analysis = lab_processor.extract_and_format(
215
  extracted_text,
216
  report_id=f"lab_{int(time.time())}",
 
232
  **lab_analysis
233
  }
234
 
 
235
  encrypted_response = crypto_manager.encrypt_json(response_data)
236
 
237
  return {
decrypt_response.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Decrypt and analyze API response from Postman
3
+ """
4
+ import base64
5
+ import gzip
6
+ import json
7
+ import sys
8
+ import os
9
+ from nacl.secret import SecretBox
10
+
11
+ # Your hex key from .env
12
+ SECRET_KEY_HEX = "9aa68717fafd68f30b6bdd7f8af0f273b4ddb31aadc0f7a3a42db86dfdde0195"
13
+
14
+ # Convert hex to bytes (32 bytes)
15
+ SECRET_KEY = bytes.fromhex(SECRET_KEY_HEX)
16
+
17
+ print(f"βœ“ Key length: {len(SECRET_KEY)} bytes")
18
+
19
+ def decrypt_response(ciphertext, nonce):
20
+ """Decrypt the encrypted response"""
21
+ try:
22
+ box = SecretBox(SECRET_KEY) # Use bytes directly, not .encode()
23
+ ciphertext_bytes = base64.b64decode(ciphertext)
24
+ nonce_bytes = base64.b64decode(nonce)
25
+
26
+ decrypted = box.decrypt(ciphertext_bytes, nonce_bytes)
27
+ decompressed = gzip.decompress(base64.b64decode(decrypted))
28
+ return json.loads(decompressed.decode('utf-8'))
29
+ except Exception as e:
30
+ print(f"❌ Decryption error: {e}")
31
+ import traceback
32
+ traceback.print_exc()
33
+ return None
34
+
35
+ def print_summary(result):
36
+ """Print a nice summary of the results"""
37
+ print("\n" + "="*70)
38
+ print("LAB REPORT ANALYSIS SUMMARY")
39
+ print("="*70)
40
+
41
+ # Basic info
42
+ print(f"\nπŸ“‹ Report Information:")
43
+ print(f" Report ID: {result.get('report_id', 'N/A')}")
44
+ print(f" Report Type: {result.get('report_type', 'N/A')}")
45
+ print(f" Processing Time: {result.get('processing_time', 'N/A')}s")
46
+ print(f" OCR Used: {result.get('ocr_used', 'N/A')}")
47
+ print(f" OCR Engine: {result.get('ocr_engine', 'N/A')}")
48
+
49
+ # Test results
50
+ test_results = result.get('test_results', [])
51
+ print(f"\nπŸ§ͺ Test Results: {len(test_results)} tests")
52
+
53
+ if test_results:
54
+ for i, test in enumerate(test_results, 1):
55
+ status_icon = "βœ“" if test['status'] == 'normal' else "⚠️"
56
+ print(f"\n{status_icon} {i}. {test['test_name']}")
57
+ print(f" Value: {test['value']} {test['unit']}")
58
+ print(f" Status: {test['status'].upper()}")
59
+ if test['status'] != 'normal':
60
+ print(f" β†’ {test['clinical_significance']}")
61
+
62
+ # Abnormal results
63
+ abnormal = result.get('abnormal_results', [])
64
+ if abnormal:
65
+ print(f"\n⚠️ ABNORMAL RESULTS ({len(abnormal)}):")
66
+ for abn in abnormal:
67
+ print(f" β€’ {abn['test_name']} ({abn['severity']})")
68
+
69
+ # Clinical Insights
70
+ clinical_insights = result.get('clinical_insights', {})
71
+ if clinical_insights:
72
+ print(f"\n🧬 CLINICAL INSIGHTS:")
73
+ print(f" Relevance Score: {clinical_insights.get('clinical_relevance_score', 0)}/100")
74
+ if clinical_insights.get('abnormality_patterns'):
75
+ for pattern in clinical_insights['abnormality_patterns']:
76
+ print(f" β€’ {pattern}")
77
+
78
+ print("\n" + "="*70)
79
+
80
+ def main():
81
+ print("="*70)
82
+ print("LAB REPORT API RESPONSE DECRYPTOR")
83
+ print("="*70)
84
+
85
+ response_file = sys.argv[1] if len(sys.argv) > 1 else "tests/samples/response.json"
86
+
87
+ if not os.path.exists(response_file):
88
+ print(f"❌ File not found: {response_file}")
89
+ return
90
+
91
+ print(f"\nπŸ“‚ Reading: {response_file}")
92
+
93
+ with open(response_file, 'r') as f:
94
+ encrypted_response = json.load(f)
95
+
96
+ if 'ciphertext' not in encrypted_response or 'nonce' not in encrypted_response:
97
+ print("❌ Invalid response format")
98
+ return
99
+
100
+ print("πŸ”“ Decrypting...")
101
+
102
+ decrypted = decrypt_response(
103
+ encrypted_response['ciphertext'],
104
+ encrypted_response['nonce']
105
+ )
106
+
107
+ if not decrypted:
108
+ return
109
+
110
+ print("βœ… Successfully decrypted!")
111
+
112
+ # Save
113
+ output_file = response_file.replace('.json', '_decrypted.json')
114
+ with open(output_file, 'w') as f:
115
+ json.dump(decrypted, f, indent=2)
116
+
117
+ print(f"πŸ’Ύ Saved to: {output_file}")
118
+
119
+ # Print summary
120
+ print_summary(decrypted)
121
+
122
+ print("\nβœ… Done!")
123
+
124
+ if __name__ == "__main__":
125
+ main()
generate_postman_request.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Generate encrypted request payload for Postman
3
+ """
4
+ import base64
5
+ import gzip
6
+ import json
7
+ import sys
8
+ import os
9
+ from nacl.secret import SecretBox
10
+ from nacl.utils import random
11
+
12
+ # Your 64-character hex key from .env
13
+ SECRET_KEY_HEX = "9aa68717fafd68f30b6bdd7f8af0f273b4ddb31aadc0f7a3a42db86dfdde0195"
14
+
15
+ # Convert hex to bytes (32 bytes)
16
+ SECRET_KEY = bytes.fromhex(SECRET_KEY_HEX)
17
+
18
+ if len(SECRET_KEY) != 32:
19
+ print(f"❌ ERROR: Key must be 32 bytes, got {len(SECRET_KEY)}")
20
+ sys.exit(1)
21
+
22
+ print(f"βœ“ Key length: {len(SECRET_KEY)} bytes")
23
+
24
+ # Get file path
25
+ if len(sys.argv) > 1:
26
+ file_path = sys.argv[1]
27
+ else:
28
+ file_path = "tests/samples/sample1.pdf"
29
+
30
+ if not os.path.exists(file_path):
31
+ print(f"❌ File not found: {file_path}")
32
+ sys.exit(1)
33
+
34
+ print(f"πŸ“„ Reading: {file_path}")
35
+
36
+ # Read your test file
37
+ with open(file_path, 'rb') as f:
38
+ file_data = f.read()
39
+
40
+ print(f"βœ“ Read {len(file_data):,} bytes")
41
+
42
+ # Determine file type
43
+ if file_path.lower().endswith('.pdf'):
44
+ file_type = "pdf"
45
+ elif file_path.lower().endswith(('.jpg', '.jpeg', '.png', '.tiff', '.bmp')):
46
+ file_type = "image"
47
+ else:
48
+ file_type = "image" # default
49
+
50
+ print(f"πŸ“‹ File type: {file_type}")
51
+
52
+ # Prepare payload
53
+ payload = {
54
+ "filename": os.path.basename(file_path),
55
+ "file_type": file_type,
56
+ "file_data": base64.b64encode(file_data).decode('utf-8'),
57
+ "patient_id": "TEST_001"
58
+ }
59
+
60
+ print("πŸ” Encrypting...")
61
+
62
+ # Encrypt
63
+ json_data = json.dumps(payload).encode('utf-8')
64
+ compressed = gzip.compress(json_data, compresslevel=6)
65
+ compressed_b64 = base64.b64encode(compressed).decode('utf-8')
66
+
67
+ box = SecretBox(SECRET_KEY) # Use bytes directly, not .encode()
68
+ nonce = random(SecretBox.NONCE_SIZE)
69
+ ciphertext = box.encrypt(compressed_b64.encode('utf-8'), nonce)
70
+
71
+ encrypted_request = {
72
+ "ciphertext": base64.b64encode(ciphertext.ciphertext).decode('utf-8'),
73
+ "nonce": base64.b64encode(nonce).decode('utf-8')
74
+ }
75
+
76
+ # Save for Postman
77
+ with open('postman_request.json', 'w') as f:
78
+ json.dump(encrypted_request, f, indent=2)
79
+
80
+ print("\n" + "="*70)
81
+ print("βœ“ Generated postman_request.json")
82
+ print("="*70)
83
+
84
+ print("\nπŸ“‹ COPY THIS JSON FOR POSTMAN:")
85
+ print("-"*70)
86
+ print(json.dumps(encrypted_request, indent=2))
87
+ print("-"*70)
88
+
89
+ print("\nπŸ“ POSTMAN INSTRUCTIONS:")
90
+ print("1. Method: POST")
91
+ print("2. URL: http://localhost:8000/analyze-lab-secure")
92
+ print("3. Headers: Content-Type = application/json")
93
+ print("4. Body β†’ raw β†’ JSON")
94
+ print("5. Paste the JSON above")
95
+ print("6. Click Send")
96
+ print("\nβœ… Done!")