Juneadtechie commited on
Commit
6d7983f
Β·
1 Parent(s): e738e12

Add prescription app files

Browse files
Files changed (1) hide show
  1. app.py +206 -1
app.py CHANGED
@@ -1,3 +1,208 @@
 
 
 
 
 
 
 
 
1
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  st.title("πŸ₯ Prescription Digitization")
3
- st.write("App is running successfully!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ app.py β€” Prescription Digitization Tool
3
+ Vision-first pipeline: tries llava/moondream vision model first (most accurate),
4
+ falls back to OCR + Mistral text mode if no vision model available.
5
+ """
6
+
7
+ import io, os
8
+ import pandas as pd
9
  import streamlit as st
10
+ from PIL import Image
11
+
12
+ import db
13
+ from preprocess import preprocess_image
14
+ from ocr_engine import run_ocr
15
+ from llm_extractor import extract_from_image, extract_fields, DEFAULT_MODEL, DEFAULT_HOST
16
+
17
+ st.set_page_config(page_title="Prescription Digitization", page_icon="πŸ₯", layout="wide")
18
+ db.init_db()
19
+
20
+ for k, v in {
21
+ "ocr_text": "", "extraction_result": None,
22
+ "ocr_engine_used": "both", "uploaded_filename": "",
23
+ "image_bytes": None,
24
+ }.items():
25
+ if k not in st.session_state:
26
+ st.session_state[k] = v
27
+
28
  st.title("πŸ₯ Prescription Digitization")
29
+ st.caption("Fully offline Β· Patient data never leaves this machine")
30
+
31
+ tab_upload, tab_records, tab_export = st.tabs(
32
+ ["πŸ“€ Upload & Extract", "πŸ“‹ Saved Records", "⬇️ Export CSV"]
33
+ )
34
+
35
+ with tab_upload:
36
+ uploaded = st.file_uploader(
37
+ "Upload prescription image",
38
+ type=["jpg", "jpeg", "png"],
39
+ label_visibility="collapsed",
40
+ accept_multiple_files=False,
41
+ )
42
+
43
+ if uploaded:
44
+ raw_bytes = uploaded.read()
45
+ st.session_state.image_bytes = raw_bytes
46
+ st.session_state.uploaded_filename = uploaded.name
47
+ image = Image.open(io.BytesIO(raw_bytes))
48
+
49
+ col_orig, col_clean = st.columns(2)
50
+ with col_orig:
51
+ st.image(image, caption="Original", width=450)
52
+ with col_clean:
53
+ cleaned = preprocess_image(image, do_deskew=True)
54
+ st.image(cleaned, caption="Preprocessed", width=450)
55
+
56
+ if st.button("πŸ” Extract Information", type="primary", use_container_width=True):
57
+
58
+ # ── Try vision model first ────────────────────────────────
59
+ with st.spinner("Checking for vision model (llava/moondream)…"):
60
+ vision_result = extract_from_image(raw_bytes, host=DEFAULT_HOST)
61
+
62
+ if vision_result.success:
63
+ st.session_state.extraction_result = vision_result
64
+ st.session_state.ocr_engine_used = vision_result.mode
65
+ n = len(vision_result.records)
66
+ st.success(
67
+ f"✨ Vision model extracted **{n} visit record{'s' if n>1 else ''}** "
68
+ f"β€” review below before saving."
69
+ )
70
+
71
+ elif vision_result.error == "NO_VISION_MODEL":
72
+ # ── Fall back to OCR + Mistral ────────────────────────
73
+ st.info("No vision model found β€” using OCR + Mistral (slower, less accurate).\n\n"
74
+ "For better results: `ollama pull llava`")
75
+
76
+ with st.spinner("Running OCR (Donut + TrOCR)…"):
77
+ try:
78
+ cleaned_bytes = io.BytesIO()
79
+ cleaned.save(cleaned_bytes, format="JPEG")
80
+ ocr_result = run_ocr(cleaned, engine="both")
81
+ st.session_state.ocr_text = ocr_result.raw_text
82
+ st.session_state.ocr_engine_used = ocr_result.engine
83
+ except Exception as e:
84
+ st.error(f"OCR failed: {e}")
85
+ st.stop()
86
+
87
+ with st.spinner(f"Structuring with Ollama ({DEFAULT_MODEL})…"):
88
+ text_result = extract_fields(
89
+ st.session_state.ocr_text,
90
+ model=DEFAULT_MODEL, host=DEFAULT_HOST
91
+ )
92
+
93
+ if text_result.success:
94
+ st.session_state.extraction_result = text_result
95
+ n = len(text_result.records)
96
+ st.success(f"Found **{n} visit record{'s' if n>1 else ''}** β€” review below.")
97
+ elif text_result.error == "OLLAMA_NOT_RUNNING":
98
+ st.error("Ollama is not running.")
99
+ st.code("ollama serve")
100
+ st.session_state.extraction_result = None
101
+ else:
102
+ st.error(f"Extraction error: {text_result.error}")
103
+ st.info("Try clicking Extract Information again.")
104
+ st.session_state.extraction_result = None
105
+
106
+ else:
107
+ st.error(f"Vision extraction error: {vision_result.error}")
108
+ st.session_state.extraction_result = None
109
+
110
+ if st.session_state.ocr_text:
111
+ with st.expander("Raw OCR output", expanded=False):
112
+ st.code(st.session_state.ocr_text, language=None)
113
+
114
+ # ── Review form ───────────────────────────────────────────────────
115
+ result = st.session_state.extraction_result
116
+ if result and result.records:
117
+ st.divider()
118
+ st.subheader("✏️ Review extracted information")
119
+
120
+ meta = result.meta
121
+ if meta:
122
+ st.caption(f"Mode: {result.mode}")
123
+ mc = st.columns(min(len(meta), 4))
124
+ for col, (k, v) in zip(mc, meta.items()):
125
+ col.text_input(k.replace("_"," ").capitalize(), value=str(v), disabled=True)
126
+
127
+ for i, rec in enumerate(result.records):
128
+ label = f"Visit {i+1}"
129
+ if rec.get("visit_date"):
130
+ label += f" β€” {rec['visit_date']}"
131
+
132
+ with st.expander(label, expanded=True):
133
+ with st.form(f"form_{i}"):
134
+ edited_fields = {}
135
+ field_items = list(rec["fields"].items())
136
+
137
+ for j in range(0, len(field_items), 3):
138
+ chunk = field_items[j:j+3]
139
+ cols = st.columns(len(chunk))
140
+ for col, (key, val) in zip(cols, chunk):
141
+ edited_fields[key] = col.text_input(
142
+ key.replace("_"," ").capitalize(),
143
+ value=str(val) if val else ""
144
+ )
145
+
146
+ meds = rec.get("medications", [])
147
+ if meds:
148
+ st.markdown("**Medications**")
149
+ med_df = pd.DataFrame(meds)
150
+ for c in ["drug_name","dosage","frequency","route"]:
151
+ if c not in med_df.columns: med_df[c] = ""
152
+ edited_meds_df = st.data_editor(
153
+ med_df[["drug_name","dosage","frequency","route"]],
154
+ num_rows="dynamic", use_container_width=True, key=f"meds_{i}"
155
+ )
156
+ final_meds = edited_meds_df.fillna("").to_dict(orient="records")
157
+ else:
158
+ final_meds = []
159
+ st.caption("No medications detected β€” add manually if needed.")
160
+
161
+ if st.form_submit_button(
162
+ f"βœ… Save Visit {i+1} to Database",
163
+ type="primary", use_container_width=True
164
+ ):
165
+ clean_fields = {k: v for k, v in edited_fields.items() if v.strip()}
166
+ pid = db.save_record(
167
+ visit_date=rec.get("visit_date"),
168
+ fields=clean_fields,
169
+ medications=final_meds,
170
+ meta=meta,
171
+ ocr_engine=st.session_state.ocr_engine_used,
172
+ source_filename=st.session_state.uploaded_filename,
173
+ )
174
+ st.success(f"Visit {i+1} saved as record #{pid}.")
175
+
176
+ with tab_records:
177
+ records = db.fetch_prescriptions()
178
+ if not records:
179
+ st.info("No records saved yet.")
180
+ else:
181
+ st.dataframe(pd.DataFrame(records), use_container_width=True, hide_index=True)
182
+ st.subheader("Medications")
183
+ sel_id = st.selectbox("Select record", [r["id"] for r in records],
184
+ format_func=lambda x: f"Record #{x}")
185
+ if sel_id:
186
+ meds = db.fetch_medications(sel_id)
187
+ if meds:
188
+ st.dataframe(pd.DataFrame(meds), use_container_width=True, hide_index=True)
189
+ else:
190
+ st.caption("No medications for this record.")
191
+ st.divider()
192
+ del_id = st.number_input("Delete record by ID", min_value=0, step=1, value=0)
193
+ if st.button("πŸ—‘οΈ Delete record") and del_id > 0:
194
+ db.delete_prescription(int(del_id))
195
+ st.success(f"Deleted #{del_id}.")
196
+ st.rerun()
197
+
198
+ with tab_export:
199
+ flat = db.fetch_all_flat()
200
+ if not flat:
201
+ st.info("No data to export yet.")
202
+ else:
203
+ df = pd.DataFrame(flat)
204
+ st.dataframe(df, use_container_width=True, hide_index=True)
205
+ csv = df.to_csv(index=False).encode("utf-8")
206
+ st.download_button("⬇️ Download CSV", data=csv,
207
+ file_name="prescriptions_export.csv", mime="text/csv",
208
+ use_container_width=True)