deploy commited on
Commit
fa38fe6
·
0 Parent(s):

Deploy Medicine Scanner v1.0.3 — Gradio SDK

Browse files
README.md ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: MedOS Medicine Scanner
3
+ emoji: 💊
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: gradio
7
+ sdk_version: 5.25.0
8
+ python_version: 3.11
9
+ app_file: app.py
10
+ pinned: false
11
+ license: mit
12
+ ---
13
+
14
+ # MedOS Medicine Scanner
15
+
16
+ AI-powered medicine label scanner that extracts structured information from
17
+ photos of medicine packaging, labels, and prescriptions.
18
+
19
+ ## Features
20
+
21
+ - **Multimodal AI**: Uses Qwen2.5-VL via HuggingFace Inference API for
22
+ intelligent label understanding
23
+ - **Structured JSON output**: Returns data compatible with MedOS "My Medicines"
24
+ - **REST API**: POST `/api/scan` for programmatic access
25
+ - **Mobile-friendly**: Supports camera capture directly from phone
26
+ - **Free**: Runs on HuggingFace Spaces with free inference
27
+
28
+ ## API Usage
29
+
30
+ ```bash
31
+ curl -X POST https://your-space.hf.space/api/scan \
32
+ -F "image=@medicine_photo.jpg"
33
+ ```
34
+
35
+ Response:
36
+ ```json
37
+ {
38
+ "success": true,
39
+ "medicine": {
40
+ "name": "Amoxicillin",
41
+ "brandName": "Amoxil",
42
+ "activeIngredient": "Amoxicillin Trihydrate",
43
+ "dose": "500mg",
44
+ "form": "capsule",
45
+ "category": "Antibiotic",
46
+ "quantity": 1,
47
+ "expiryDate": "2027-03",
48
+ "notes": "Take 1 capsule 3 times daily with food"
49
+ }
50
+ }
51
+ ```
52
+
53
+ ## Environment Variables
54
+
55
+ | Variable | Required | Description |
56
+ |----------|----------|-------------|
57
+ | `HF_TOKEN` | No | HuggingFace token for higher rate limits |
58
+ | `MODEL_ID` | No | Override default model (default: `Qwen/Qwen2.5-VL-3B-Instruct`) |
app.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MedOS Medicine Scanner — HuggingFace Space (Gradio SDK)
3
+
4
+ Scan medicine labels with AI (Qwen2.5-VL) and return structured JSON.
5
+ Uses HF Spaces native Gradio SDK — no Docker needed.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import os
11
+ import io
12
+ import time
13
+
14
+ import gradio as gr
15
+ import numpy as np
16
+ from PIL import Image
17
+
18
+ from scanner import scan_medicine
19
+
20
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def scan_image_ui(image_dict, hf_token: str) -> tuple[str, str]:
25
+ """Gradio interface handler."""
26
+ if image_dict is None:
27
+ return "Please upload or capture a medicine image.", "{}"
28
+
29
+ start = time.time()
30
+
31
+ # gr.Image returns filepath string or numpy array depending on type
32
+ if isinstance(image_dict, str):
33
+ pil_image = Image.open(image_dict)
34
+ elif isinstance(image_dict, np.ndarray):
35
+ pil_image = Image.fromarray(image_dict)
36
+ else:
37
+ pil_image = Image.fromarray(np.asarray(image_dict))
38
+
39
+ result = scan_medicine(pil_image, hf_token=(hf_token or "").strip())
40
+ elapsed = time.time() - start
41
+
42
+ if not result["success"]:
43
+ err = f"Scan failed: {result['error']}"
44
+ return err, json.dumps(result, indent=2)
45
+
46
+ med = result["medicine"]
47
+ model_short = (result.get("model_used") or "unknown").split("/")[-1]
48
+
49
+ lines = [f"Medicine: {med['name']}"]
50
+ if med.get("brandName"):
51
+ lines.append(f"Brand: {med['brandName']}")
52
+ lines.append(f"Dose: {med['dose']}")
53
+ lines.append(f"Form: {med['form'].capitalize()}")
54
+ if med.get("activeIngredient"):
55
+ lines.append(f"Active ingredient: {med['activeIngredient']}")
56
+ if med.get("category"):
57
+ lines.append(f"Category: {med['category']}")
58
+ if med.get("expiryDate"):
59
+ lines.append(f"Expiry: {med['expiryDate']}")
60
+ if med.get("notes"):
61
+ lines.append(f"Instructions: {med['notes']}")
62
+ lines.append(f"\nScanned in {elapsed:.1f}s using {model_short}")
63
+
64
+ summary = "\n".join(lines)
65
+ api_json = json.dumps(
66
+ {"success": True, "medicine": med, "model_used": result.get("model_used"),
67
+ "scan_time_ms": int(elapsed * 1000)},
68
+ indent=2, ensure_ascii=False,
69
+ )
70
+ return summary, api_json
71
+
72
+
73
+ # ============================================================
74
+ # Gradio Interface
75
+ # ============================================================
76
+
77
+ EXAMPLES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "examples")
78
+
79
+ example_images = []
80
+ if os.path.isdir(EXAMPLES_DIR):
81
+ for fname in sorted(os.listdir(EXAMPLES_DIR)):
82
+ if fname.lower().endswith((".jpg", ".jpeg", ".png")):
83
+ example_images.append([os.path.join(EXAMPLES_DIR, fname), ""])
84
+
85
+ demo = gr.Interface(
86
+ fn=scan_image_ui,
87
+ inputs=[
88
+ gr.Image(label="Medicine Image", type="filepath"),
89
+ gr.Textbox(
90
+ label="HuggingFace Token (optional — auto-provided when used from MedOS)",
91
+ placeholder="hf_... — only needed for standalone use",
92
+ type="password",
93
+ ),
94
+ ],
95
+ outputs=[
96
+ gr.Textbox(label="Extracted Information", lines=10),
97
+ gr.Textbox(label="API Response (JSON)", lines=12),
98
+ ],
99
+ examples=example_images if example_images else None,
100
+ cache_examples=False,
101
+ title="MedOS Medicine Scanner",
102
+ description=(
103
+ "Scan medicine packages, labels, or prescriptions with your camera. "
104
+ "The AI extracts drug name, dosage, form, expiry date, and instructions automatically.\n\n"
105
+ "**From MedOS:** Token is provided automatically — just scan.\n"
106
+ "**Standalone:** Enter a free [HuggingFace token](https://huggingface.co/settings/tokens/new?ownUserPermissions=inference.serverless.write&tokenType=fineGrained) "
107
+ "with *'Make calls to Inference Providers'* permission."
108
+ ),
109
+ article=(
110
+ "**Privacy:** Images are processed via HuggingFace Inference API and are not stored.\n\n"
111
+ "**Disclaimer:** For informational purposes only. Always verify with a pharmacist."
112
+ ),
113
+ flagging_mode="never",
114
+ )
115
+
116
+ if __name__ == "__main__":
117
+ demo.launch()
examples/amoxicillin_500mg.jpg ADDED
examples/cetirizine_10mg.jpg ADDED
examples/ibuprofen_400mg.jpg ADDED
examples/metformin_850mg.jpg ADDED
examples/vitamin_d3_1000iu.jpg ADDED
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ Pillow>=10.0.0
2
+ requests>=2.31.0
3
+ numpy
scanner.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MedOS Medicine Scanner — AI-powered medicine label extraction.
3
+
4
+ Uses HuggingFace Inference Providers (router.huggingface.co) with
5
+ the huggingface_hub InferenceClient for automatic provider selection
6
+ and failover across 10+ vision-language models.
7
+
8
+ Token requirement: HF token with "Make calls to Inference Providers"
9
+ permission. Create one at: https://huggingface.co/settings/tokens/new?
10
+ ownUserPermissions=inference.serverless.write&tokenType=fineGrained
11
+ """
12
+
13
+ import base64
14
+ import json
15
+ import os
16
+ import re
17
+ import io
18
+ import logging
19
+ from typing import Optional
20
+
21
+ from huggingface_hub import InferenceClient
22
+ from PIL import Image
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
27
+
28
+ # ============================================================
29
+ # VLM fallback chain — verified working models only.
30
+ # Tested 2026-04-07 with actual image inference.
31
+ # ============================================================
32
+ FALLBACK_MODELS = [
33
+ "Qwen/Qwen2.5-VL-72B-Instruct", # Best quality, Qwen VLM 72B
34
+ "google/gemma-3-27b-it", # Google Gemma 3, strong VLM
35
+ ]
36
+
37
+ VALID_FORMS = [
38
+ "tablet", "capsule", "syrup", "inhaler",
39
+ "injection", "cream", "drops", "patch", "other",
40
+ ]
41
+
42
+ VALID_CATEGORIES = [
43
+ "Diabetes", "Pain Relief", "Cardiovascular", "Respiratory",
44
+ "Antibiotic", "Supplement", "Mental Health", "Thyroid",
45
+ "Gastrointestinal", "Allergy", "Other",
46
+ ]
47
+
48
+ EXTRACTION_PROMPT = """You are a medicine label scanner. Analyze this image of a medicine package, label, bottle, or prescription.
49
+
50
+ Extract ALL visible information and return ONLY a JSON object with these exact fields:
51
+
52
+ {
53
+ "name": "Generic/medicine name (e.g. Amoxicillin)",
54
+ "brandName": "Brand name if visible (e.g. Amoxil)",
55
+ "activeIngredient": "Active ingredient(s) with amounts",
56
+ "dose": "Dosage strength (e.g. 500mg, 10mg/5mL)",
57
+ "form": "One of: tablet, capsule, syrup, inhaler, injection, cream, drops, patch, other",
58
+ "category": "One of: Diabetes, Pain Relief, Cardiovascular, Respiratory, Antibiotic, Supplement, Mental Health, Thyroid, Gastrointestinal, Allergy, Other",
59
+ "quantity": 1,
60
+ "expiryDate": "Expiry date in YYYY-MM format if visible",
61
+ "notes": "Dosage instructions, warnings, or other important info from the label"
62
+ }
63
+
64
+ Rules:
65
+ - Return ONLY the JSON object, no markdown, no explanation
66
+ - Use null for fields you cannot determine from the image
67
+ - For "form", pick the closest match from the allowed values
68
+ - For "category", pick the most appropriate medical category
69
+ - Include dosage instructions in "notes" if visible
70
+ - If multiple medicines are visible, extract only the primary/most prominent one
71
+ - If this is NOT a medicine image, return: {"error": "No medicine detected in image"}
72
+ - NEVER provide dosage recommendations — only extract what is printed on the label
73
+ - If you are uncertain about any field, use null rather than guessing"""
74
+
75
+
76
+ def encode_image(image: Image.Image, max_size: int = 1024) -> str:
77
+ """Resize and base64-encode an image for the API."""
78
+ w, h = image.size
79
+ if max(w, h) > max_size:
80
+ ratio = max_size / max(w, h)
81
+ image = image.resize((int(w * ratio), int(h * ratio)), Image.LANCZOS)
82
+ if image.mode not in ("RGB", "L"):
83
+ image = image.convert("RGB")
84
+
85
+ buf = io.BytesIO()
86
+ image.save(buf, format="JPEG", quality=85)
87
+ return base64.b64encode(buf.getvalue()).decode("utf-8")
88
+
89
+
90
+ def call_vlm(image_b64: str, model: str, token: str) -> Optional[str]:
91
+ """
92
+ Call a vision-language model via HuggingFace InferenceClient.
93
+ Uses router.huggingface.co with automatic provider selection.
94
+ """
95
+ try:
96
+ client = InferenceClient(token=token or None)
97
+
98
+ # Build message with image
99
+ response = client.chat_completion(
100
+ model=model,
101
+ messages=[
102
+ {
103
+ "role": "user",
104
+ "content": [
105
+ {"type": "text", "text": EXTRACTION_PROMPT},
106
+ {
107
+ "type": "image_url",
108
+ "image_url": {
109
+ "url": f"data:image/jpeg;base64,{image_b64}",
110
+ },
111
+ },
112
+ ],
113
+ }
114
+ ],
115
+ max_tokens=800,
116
+ temperature=0.1,
117
+ )
118
+
119
+ if response and response.choices:
120
+ return response.choices[0].message.content
121
+ return None
122
+
123
+ except Exception as e:
124
+ err_msg = str(e)
125
+ # Truncate long error messages for cleaner logs
126
+ if len(err_msg) > 200:
127
+ err_msg = err_msg[:200] + "..."
128
+ logger.warning("Model %s failed: %s", model, err_msg)
129
+ return None
130
+
131
+
132
+ def parse_json_response(text: str) -> Optional[dict]:
133
+ """Extract JSON from model response, handling markdown fences."""
134
+ if not text:
135
+ return None
136
+ text = text.strip()
137
+
138
+ try:
139
+ return json.loads(text)
140
+ except json.JSONDecodeError:
141
+ pass
142
+
143
+ patterns = [
144
+ r"```json\s*(.*?)\s*```",
145
+ r"```\s*(.*?)\s*```",
146
+ r"\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}",
147
+ ]
148
+ for pattern in patterns:
149
+ match = re.search(pattern, text, re.DOTALL)
150
+ if match:
151
+ try:
152
+ candidate = match.group(1) if "```" in pattern else match.group(0)
153
+ return json.loads(candidate)
154
+ except (json.JSONDecodeError, IndexError):
155
+ continue
156
+ return None
157
+
158
+
159
+ def normalize_form(form_str: Optional[str]) -> str:
160
+ if not form_str:
161
+ return "other"
162
+ form_lower = form_str.lower().strip()
163
+ if form_lower in VALID_FORMS:
164
+ return form_lower
165
+ mappings = {
166
+ "tab": "tablet", "pill": "tablet", "caplet": "tablet",
167
+ "cap": "capsule", "gel cap": "capsule", "softgel": "capsule",
168
+ "liquid": "syrup", "solution": "syrup", "suspension": "syrup",
169
+ "oral solution": "syrup", "elixir": "syrup",
170
+ "ointment": "cream", "gel": "cream", "lotion": "cream",
171
+ "topical": "cream", "balm": "cream",
172
+ "eye drop": "drops", "ear drop": "drops", "nasal": "drops",
173
+ "spray": "inhaler", "aerosol": "inhaler", "nebul": "inhaler",
174
+ "vial": "injection", "ampule": "injection", "ampoule": "injection",
175
+ "syringe": "injection", "iv": "injection", "im": "injection",
176
+ "transdermal": "patch", "plaster": "patch",
177
+ }
178
+ for key, val in mappings.items():
179
+ if key in form_lower:
180
+ return val
181
+ return "other"
182
+
183
+
184
+ def normalize_category(cat_str: Optional[str]) -> Optional[str]:
185
+ if not cat_str:
186
+ return None
187
+ cat_lower = cat_str.lower().strip()
188
+ for valid in VALID_CATEGORIES:
189
+ if valid.lower() in cat_lower or cat_lower in valid.lower():
190
+ return valid
191
+ cat_map = {
192
+ "antibiotic": "Antibiotic", "anti-biotic": "Antibiotic",
193
+ "antimicrobial": "Antibiotic", "antifungal": "Antibiotic",
194
+ "pain": "Pain Relief", "analgesic": "Pain Relief",
195
+ "nsaid": "Pain Relief", "anti-inflammatory": "Pain Relief",
196
+ "heart": "Cardiovascular", "blood pressure": "Cardiovascular",
197
+ "hypertension": "Cardiovascular", "cholesterol": "Cardiovascular",
198
+ "statin": "Cardiovascular", "cardiac": "Cardiovascular",
199
+ "lung": "Respiratory", "asthma": "Respiratory",
200
+ "bronch": "Respiratory", "cough": "Respiratory",
201
+ "diabetes": "Diabetes", "insulin": "Diabetes",
202
+ "metformin": "Diabetes", "glucose": "Diabetes",
203
+ "vitamin": "Supplement", "mineral": "Supplement",
204
+ "iron": "Supplement", "calcium": "Supplement",
205
+ "omega": "Supplement", "probiotic": "Supplement",
206
+ "antidepressant": "Mental Health", "anxiety": "Mental Health",
207
+ "ssri": "Mental Health", "psychiatric": "Mental Health",
208
+ "sleep": "Mental Health", "sedative": "Mental Health",
209
+ "thyroid": "Thyroid", "levothyroxine": "Thyroid",
210
+ "stomach": "Gastrointestinal", "acid": "Gastrointestinal",
211
+ "antacid": "Gastrointestinal", "ppi": "Gastrointestinal",
212
+ "laxative": "Gastrointestinal", "diarr": "Gastrointestinal",
213
+ "allergy": "Allergy", "antihistamine": "Allergy",
214
+ "cetirizine": "Allergy", "loratadine": "Allergy",
215
+ }
216
+ for key, val in cat_map.items():
217
+ if key in cat_lower:
218
+ return val
219
+ return "Other"
220
+
221
+
222
+ def normalize_expiry(date_str: Optional[str]) -> Optional[str]:
223
+ if not date_str:
224
+ return None
225
+ date_str = date_str.strip()
226
+ if re.match(r"^\d{4}-\d{2}$", date_str):
227
+ return date_str
228
+ m = re.match(r"^(\d{4})-(\d{2})-\d{2}$", date_str)
229
+ if m:
230
+ return f"{m.group(1)}-{m.group(2)}"
231
+ m = re.match(r"^(\d{1,2})[/-](\d{4})$", date_str)
232
+ if m:
233
+ return f"{m.group(2)}-{int(m.group(1)):02d}"
234
+ m = re.match(r"^(\d{4})[/-](\d{1,2})$", date_str)
235
+ if m:
236
+ return f"{m.group(1)}-{int(m.group(2)):02d}"
237
+ months = {
238
+ "jan": "01", "feb": "02", "mar": "03", "apr": "04",
239
+ "may": "05", "jun": "06", "jul": "07", "aug": "08",
240
+ "sep": "09", "oct": "10", "nov": "11", "dec": "12",
241
+ }
242
+ m = re.match(r"^([a-zA-Z]+)\s*(\d{4})$", date_str)
243
+ if m:
244
+ mon = m.group(1)[:3].lower()
245
+ if mon in months:
246
+ return f"{m.group(2)}-{months[mon]}"
247
+ return None
248
+
249
+
250
+ def build_medicine_item(raw: dict) -> dict:
251
+ if "error" in raw:
252
+ return {"error": raw["error"]}
253
+ name = (raw.get("name") or "").strip()
254
+ if not name:
255
+ return {"error": "Could not extract medicine name from image"}
256
+ dose = (raw.get("dose") or "").strip() or "See label"
257
+
258
+ result = {"name": name, "dose": dose, "form": normalize_form(raw.get("form")), "quantity": 1}
259
+
260
+ for field, key in [("brandName", "brandName"), ("activeIngredient", "activeIngredient")]:
261
+ val = (raw.get(key) or "").strip()
262
+ if val and val.lower() != "null":
263
+ result[field] = val
264
+
265
+ category = normalize_category(raw.get("category"))
266
+ if category:
267
+ result["category"] = category
268
+ expiry = normalize_expiry(raw.get("expiryDate"))
269
+ if expiry:
270
+ result["expiryDate"] = expiry
271
+ notes = (raw.get("notes") or "").strip()
272
+ if notes and notes.lower() != "null":
273
+ result["notes"] = notes
274
+ qty = raw.get("quantity")
275
+ if isinstance(qty, int) and qty > 0:
276
+ result["quantity"] = qty
277
+ return result
278
+
279
+
280
+ def scan_medicine(image: Image.Image, hf_token: str = "") -> dict:
281
+ """
282
+ Main entry point: scan a medicine image and return structured data.
283
+
284
+ Requires a HuggingFace token with "Make calls to Inference Providers"
285
+ permission. Tries 10 models in cascade until one succeeds.
286
+ """
287
+ token = hf_token or HF_TOKEN
288
+ if not token:
289
+ return {
290
+ "success": False,
291
+ "error": (
292
+ "HuggingFace token required. Enter your token in the field below, "
293
+ "or set HF_TOKEN as a Space secret. The token needs 'Make calls to "
294
+ "Inference Providers' permission: https://huggingface.co/settings/tokens"
295
+ ),
296
+ "medicine": None,
297
+ "raw_response": None,
298
+ "model_used": None,
299
+ }
300
+
301
+ image_b64 = encode_image(image)
302
+
303
+ raw_response = None
304
+ model_used = None
305
+ last_error = ""
306
+
307
+ for model in FALLBACK_MODELS:
308
+ logger.info("Trying model: %s", model)
309
+ raw_response = call_vlm(image_b64, model, token)
310
+ if raw_response:
311
+ model_used = model
312
+ break
313
+
314
+ if not raw_response:
315
+ return {
316
+ "success": False,
317
+ "error": (
318
+ "All models are currently unavailable. This usually means rate limits. "
319
+ "Please wait a moment and try again. If this persists, ensure your "
320
+ "HF token has 'Make calls to Inference Providers' permission."
321
+ ),
322
+ "medicine": None,
323
+ "raw_response": None,
324
+ "model_used": None,
325
+ }
326
+
327
+ parsed = parse_json_response(raw_response)
328
+ if not parsed:
329
+ return {
330
+ "success": False,
331
+ "error": "Could not parse model response as JSON",
332
+ "medicine": None,
333
+ "raw_response": raw_response,
334
+ "model_used": model_used,
335
+ }
336
+
337
+ medicine = build_medicine_item(parsed)
338
+ if "error" in medicine:
339
+ return {
340
+ "success": False,
341
+ "error": medicine["error"],
342
+ "medicine": None,
343
+ "raw_response": raw_response,
344
+ "model_used": model_used,
345
+ }
346
+
347
+ return {
348
+ "success": True,
349
+ "medicine": medicine,
350
+ "raw_response": raw_response,
351
+ "model_used": model_used,
352
+ }