7 changed files with 458 additions and 0 deletions
@ -0,0 +1 @@ |
|||||||
|
"Your google auth credentials." |
@ -0,0 +1,35 @@ |
|||||||
|
import os |
||||||
|
from google.oauth2.credentials import Credentials |
||||||
|
from google_auth_oauthlib.flow import InstalledAppFlow |
||||||
|
from google.auth.transport.requests import Request |
||||||
|
from googleapiclient.discovery import build # Add this import |
||||||
|
|
||||||
|
SCOPES = ["https://www.googleapis.com/auth/calendar.events"] |
||||||
|
|
||||||
|
def authenticate_google_calender(): |
||||||
|
creds = None |
||||||
|
token_path = r"C:\Users\Legion\Desktop\projects\medical_prescription_to_google_calender\token.json" |
||||||
|
|
||||||
|
if os.path.exists(token_path): |
||||||
|
creds = Credentials.from_authorized_user_file(token_path, SCOPES) |
||||||
|
|
||||||
|
if not creds or not creds.valid: |
||||||
|
if creds and creds.expired and creds.refresh_token: |
||||||
|
creds.refresh(Request()) |
||||||
|
else: |
||||||
|
flow = InstalledAppFlow.from_client_secrets_file(r"C:\Users\Legion\Desktop\projects\medical_prescription_to_google_calender\credentials.json", SCOPES) |
||||||
|
creds = flow.run_local_server(port=0) |
||||||
|
|
||||||
|
with open(token_path, "w") as token_file: |
||||||
|
token_file.write(creds.to_json()) |
||||||
|
|
||||||
|
# Build and return the service instead of just credentials |
||||||
|
try: |
||||||
|
service = build('calendar', 'v3', credentials=creds) |
||||||
|
return service |
||||||
|
except Exception as e: |
||||||
|
print(f"Error building service: {e}") |
||||||
|
return None |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
authenticate_google_calender() |
@ -0,0 +1,64 @@ |
|||||||
|
from googleapiclient.discovery import build |
||||||
|
from calendar_auth import authenticate_google_calender |
||||||
|
from parsing_json import format_calendar_events |
||||||
|
from datetime import datetime, timedelta |
||||||
|
|
||||||
|
def create_event(service, event_details): |
||||||
|
"""Creates an event in Google Calendar.""" |
||||||
|
try: |
||||||
|
event = service.events().insert(calendarId='primary', body=event_details).execute() |
||||||
|
print(f"Event created: {event.get('htmlLink')}") |
||||||
|
except Exception as e: |
||||||
|
print(f"Error creating event: {str(e)}") |
||||||
|
|
||||||
|
def convert_time_to_24hr(time_str): |
||||||
|
"""Converts time from '10:30 am' format to '10:30:00'""" |
||||||
|
if time_str and time_str.lower() != 'none': |
||||||
|
try: |
||||||
|
parsed_time = datetime.strptime(time_str, '%I:%M %p') |
||||||
|
return parsed_time.strftime('%H:%M:%S') |
||||||
|
except ValueError: |
||||||
|
return '09:00:00' |
||||||
|
return '09:00:00' |
||||||
|
|
||||||
|
def convert_to_gcal_events(formatted_events): |
||||||
|
"""Converts formatted events into Google Calendar's format.""" |
||||||
|
gcal_events = [] |
||||||
|
|
||||||
|
for event in formatted_events: |
||||||
|
gcal_event = { |
||||||
|
'summary': event['summary'], |
||||||
|
'reminders': { |
||||||
|
'useDefault': False, |
||||||
|
'overrides': [{'method': 'popup', 'minutes': 10}] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
# Check if it's an all-day event (has 'date') or timed event (has 'dateTime') |
||||||
|
if 'date' in event['start']: |
||||||
|
# All-day event (like tests and follow-ups) |
||||||
|
gcal_event['start'] = { |
||||||
|
'date': event['start']['date'], |
||||||
|
'timeZone': 'Asia/Kolkata' |
||||||
|
} |
||||||
|
gcal_event['end'] = { |
||||||
|
'date': event['end']['date'], |
||||||
|
'timeZone': 'Asia/Kolkata' |
||||||
|
} |
||||||
|
else: |
||||||
|
# Timed event (like medicine schedules) |
||||||
|
start_dt = datetime.strptime(event['start']['dateTime'], '%Y-%m-%dT%H:%M:%S') |
||||||
|
end_dt = start_dt + timedelta(minutes=30) |
||||||
|
|
||||||
|
gcal_event['start'] = { |
||||||
|
'dateTime': start_dt.isoformat(), |
||||||
|
'timeZone': 'Asia/Kolkata' |
||||||
|
} |
||||||
|
gcal_event['end'] = { |
||||||
|
'dateTime': end_dt.isoformat(), |
||||||
|
'timeZone': 'Asia/Kolkata' |
||||||
|
} |
||||||
|
|
||||||
|
gcal_events.append(gcal_event) |
||||||
|
|
||||||
|
return gcal_events |
@ -0,0 +1,26 @@ |
|||||||
|
from ocr import * |
||||||
|
from calendar_auth import * |
||||||
|
from create_calender_events import * |
||||||
|
from parsing_json import * |
||||||
|
from preprocess import * |
||||||
|
|
||||||
|
image_path = r"C:\Users\Legion\Desktop\projects\medical_prescription_to_google_calender\test_data\prescription_page-0001.jpg" |
||||||
|
|
||||||
|
extracted_text = extract_text_from_image(image_path=image_path) |
||||||
|
print(extracted_text) |
||||||
|
cleaned_text = clean_text(extracted_text) |
||||||
|
print(cleaned_text) |
||||||
|
structured_data = preprocess_extracted_text(cleaned_text) |
||||||
|
print(structured_data) |
||||||
|
final_structured_data = process_dates(structured_data) |
||||||
|
print(final_structured_data) |
||||||
|
formatted_calender_events = format_calendar_events(final_structured_data) |
||||||
|
print(formatted_calender_events) |
||||||
|
validated_events = [validate_event(event) for event in formatted_calender_events] |
||||||
|
for event in validated_events[:5]: |
||||||
|
print(json.dumps(event, indent=2)) |
||||||
|
service = authenticate_google_calender() |
||||||
|
gcal_events = convert_to_gcal_events(validated_events) |
||||||
|
|
||||||
|
for event in gcal_events: |
||||||
|
create_event(service, event) |
@ -0,0 +1,71 @@ |
|||||||
|
import os |
||||||
|
from openai import OpenAI |
||||||
|
from dotenv import load_dotenv |
||||||
|
import base64 |
||||||
|
from PIL import Image |
||||||
|
import re |
||||||
|
|
||||||
|
load_dotenv() |
||||||
|
|
||||||
|
openai_api_key = os.getenv("OPENAI_API_KEY") |
||||||
|
|
||||||
|
MODEL = "gpt-4o" |
||||||
|
|
||||||
|
openai = OpenAI() |
||||||
|
|
||||||
|
def encode_image(image_path): |
||||||
|
with open(image_path, "rb") as image_file: |
||||||
|
return base64.b64encode(image_file.read()).decode("utf-8") |
||||||
|
|
||||||
|
def extract_text_from_image(image_path): |
||||||
|
response = openai.chat.completions.create( |
||||||
|
model = MODEL, |
||||||
|
max_tokens = 1000, |
||||||
|
messages=[ |
||||||
|
{ |
||||||
|
"role": "system", "content": """You are an OCR assistant that extracts text from medical |
||||||
|
prescription images. Extract all the text exactly as it |
||||||
|
appears in the prescription image. Dont include images. Only |
||||||
|
extract text.""" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"role": "user", |
||||||
|
"content": [ |
||||||
|
{ |
||||||
|
"type": "text", |
||||||
|
"text": "Extract text from this image: " |
||||||
|
}, |
||||||
|
{ |
||||||
|
"type": "image_url", |
||||||
|
"image_url": { |
||||||
|
"url": f"data:image/jpeg;base64,{encode_image(image_path)}" |
||||||
|
} |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
] |
||||||
|
) |
||||||
|
return response.choices[0].message.content |
||||||
|
|
||||||
|
import re |
||||||
|
|
||||||
|
def clean_text(text): |
||||||
|
# Remove all hyphens |
||||||
|
text = re.sub(r'-', ' ', text) |
||||||
|
|
||||||
|
# Remove excessive non-word characters but keep necessary punctuation |
||||||
|
text = re.sub(r'[^\w\s.,()%/]', '', text) |
||||||
|
|
||||||
|
# Remove multiple spaces and ensure single spaces |
||||||
|
text = re.sub(r'\s+', ' ', text) |
||||||
|
|
||||||
|
# Replace multiple newlines with a single newline |
||||||
|
text = re.sub(r'\n+', '\n', text) |
||||||
|
|
||||||
|
# Ensure spacing around punctuation marks |
||||||
|
text = re.sub(r'([.,])([^\s])', r'\1 \2', text) |
||||||
|
|
||||||
|
return text.strip() |
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,120 @@ |
|||||||
|
import json |
||||||
|
import re |
||||||
|
from datetime import datetime, timedelta |
||||||
|
|
||||||
|
# Default number of days to schedule indefinitely recurring events (1 year) |
||||||
|
DEFAULT_DURATION_DAYS = 365 |
||||||
|
|
||||||
|
# Function to assign a default time for general terms like "before breakfast", etc. |
||||||
|
def assign_time(timing): |
||||||
|
time_mappings = { |
||||||
|
"random": "09:00 AM", |
||||||
|
"daily": "09:00 AM", |
||||||
|
"before breakfast": "07:00 AM", |
||||||
|
"after breakfast": "08:30 AM", |
||||||
|
"before lunch": "12:00 PM", |
||||||
|
"after lunch": "01:30 PM", |
||||||
|
"before dinner": "07:00 PM", |
||||||
|
"after dinner": "08:30 PM", |
||||||
|
} |
||||||
|
return time_mappings.get(timing.lower(), timing) |
||||||
|
|
||||||
|
# Function to extract the recurrence pattern |
||||||
|
def get_recurrence_interval(timing): |
||||||
|
""" Extracts interval days from 'every X days', 'once a week', or 'once a month'. """ |
||||||
|
timing = timing.lower().strip() |
||||||
|
|
||||||
|
if "every alternate day" in timing: |
||||||
|
return 2 # Every other day (every 2 days) |
||||||
|
elif match := re.search(r"every (\d+) days", timing): |
||||||
|
return int(match.group(1)) # Extract number of days |
||||||
|
elif "once a week" in timing: |
||||||
|
return 7 # Every 7 days (once a week) |
||||||
|
elif "once a month" in timing: |
||||||
|
return "monthly" # Special case for monthly scheduling |
||||||
|
elif timing in ["daily", "every day"]: |
||||||
|
return 1 # Every day |
||||||
|
else: |
||||||
|
return None # Not a recurring event |
||||||
|
|
||||||
|
# Function to convert AM/PM time format to 24-hour format |
||||||
|
def convert_to_24hr(time_str): |
||||||
|
return datetime.strptime(time_str, "%I:%M %p").strftime("%H:%M") |
||||||
|
|
||||||
|
# Function to generate Google Calendar events |
||||||
|
def format_calendar_events(processed_data): |
||||||
|
events = [] |
||||||
|
start_date = datetime.today().date() |
||||||
|
|
||||||
|
# Medicines |
||||||
|
if "medicines" in processed_data: |
||||||
|
for med in processed_data["medicines"]: |
||||||
|
if med.get("name"): |
||||||
|
event_time = assign_time(med.get("timing", "09:00 AM")) |
||||||
|
interval_days = get_recurrence_interval(med["timing"]) |
||||||
|
|
||||||
|
# If no interval, assume daily (default behavior) |
||||||
|
if interval_days is None: |
||||||
|
interval_days = 1 |
||||||
|
|
||||||
|
# Generate events for 1 year if no duration is given |
||||||
|
event_date = start_date |
||||||
|
for _ in range(365 if interval_days != "monthly" else 12): |
||||||
|
if interval_days == "monthly": |
||||||
|
event_date = (event_date.replace(day=1) + timedelta(days=32)).replace(day=1) # Jump to the next month |
||||||
|
else: |
||||||
|
event_date += timedelta(days=interval_days) |
||||||
|
|
||||||
|
event = { |
||||||
|
"summary": f"Take {med['name']} ({med.get('dosage', 'Dosage not specified')})", |
||||||
|
"start": { |
||||||
|
"dateTime": f"{event_date.isoformat()}T{convert_to_24hr(event_time)}:00", |
||||||
|
"timeZone": "Asia/Kolkata" |
||||||
|
}, |
||||||
|
"end": { |
||||||
|
"dateTime": f"{event_date.isoformat()}T{convert_to_24hr(event_time)}:59", |
||||||
|
"timeZone": "Asia/Kolkata" |
||||||
|
} |
||||||
|
} |
||||||
|
events.append(event) |
||||||
|
|
||||||
|
# Tests |
||||||
|
if "tests" in processed_data: |
||||||
|
for test in processed_data["tests"]: |
||||||
|
if test.get("name") and test.get("dueDate"): # Use 'dueDate' instead of 'date' |
||||||
|
event = { |
||||||
|
"summary": f"Medical Test: {test['name']}", |
||||||
|
"start": {"date": test["dueDate"]}, # Fix here |
||||||
|
"end": {"date": test["dueDate"]}, # Fix here |
||||||
|
"timeZone": "Asia/Kolkata" |
||||||
|
} |
||||||
|
events.append(event) |
||||||
|
|
||||||
|
|
||||||
|
# Follow-ups |
||||||
|
if "follow_ups" in processed_data: |
||||||
|
for follow_up in processed_data["follow_ups"]: |
||||||
|
if follow_up.get("date"): |
||||||
|
event = { |
||||||
|
"summary": "Doctor Follow-up Appointment", |
||||||
|
"start": {"date": follow_up["date"]}, |
||||||
|
"end": {"date": follow_up["date"]}, |
||||||
|
"timeZone": "Asia/Kolkata" |
||||||
|
} |
||||||
|
events.append(event) |
||||||
|
|
||||||
|
return events |
||||||
|
|
||||||
|
# Function to validate events before sending to Google Calendar |
||||||
|
def validate_event(event): |
||||||
|
required_fields = { |
||||||
|
"summary": "Untitled Event", |
||||||
|
"start": {"dateTime": datetime.today().isoformat(), "timeZone": "Asia/Kolkata"}, |
||||||
|
"end": {"dateTime": (datetime.today() + timedelta(minutes=30)).isoformat(), "timeZone": "Asia/Kolkata"} |
||||||
|
} |
||||||
|
|
||||||
|
for field, default_value in required_fields.items(): |
||||||
|
if field not in event or event[field] is None: |
||||||
|
event[field] = default_value |
||||||
|
|
||||||
|
return event |
@ -0,0 +1,141 @@ |
|||||||
|
import os |
||||||
|
from openai import OpenAI |
||||||
|
from dotenv import load_dotenv |
||||||
|
import json |
||||||
|
from datetime import datetime, timedelta |
||||||
|
|
||||||
|
load_dotenv() |
||||||
|
|
||||||
|
openai_api = os.getenv("OPENAI_API_KEY") |
||||||
|
MODEL = "gpt-4o-mini" |
||||||
|
openai = OpenAI() |
||||||
|
|
||||||
|
system_prompt = """You are a medical assistant that processes prescription text. |
||||||
|
Your goal is to extract medicines, tests, and follow-ups in a structured JSON format. |
||||||
|
|
||||||
|
### **Instructions:** |
||||||
|
- Extract **medicines**, **dosages**, and **timings** if available. |
||||||
|
- **Convert vague timings** into precise values: |
||||||
|
- **Before breakfast** → `07:30 AM` |
||||||
|
- **After lunch** → `02:00 PM` |
||||||
|
- **Before dinner** → `07:00 PM` |
||||||
|
- **After dinner** → `10:00 PM` |
||||||
|
- **30 minutes before breakfast** → `07:00 AM` |
||||||
|
- If **"daily"** is mentioned without a time, **assign a logical time** between **08:00 AM - 10:00 PM**. |
||||||
|
- If the prescription says **"every alternate day"**, return `"interval": 2` instead of just `"daily"`. |
||||||
|
|
||||||
|
### **Tests & Follow-ups:** |
||||||
|
- Extract **medical tests** and their required dates. |
||||||
|
- Convert relative times (e.g., `"after 3 months"`) into **exact calendar dates**, using the prescription date. |
||||||
|
- If the prescription date is missing, use today's date. |
||||||
|
- Follow-up should **only be included if required**, not just for general check-ups. |
||||||
|
|
||||||
|
### **Output Format:** |
||||||
|
Return **only valid JSON**, structured as follows: |
||||||
|
|
||||||
|
{ |
||||||
|
"medicines": [ |
||||||
|
{ |
||||||
|
"name": "<Medicine Name>", |
||||||
|
"dosage": "<Dosage>", |
||||||
|
"timing": "<Time>", |
||||||
|
"interval": <Interval in days (if applicable)> |
||||||
|
} |
||||||
|
], |
||||||
|
"tests": [ |
||||||
|
{ |
||||||
|
"name": "<Test Name>", |
||||||
|
"date": "<YYYY-MM-DD>" |
||||||
|
} |
||||||
|
], |
||||||
|
"follow_ups": [ |
||||||
|
{ |
||||||
|
"date": "<YYYY-MM-DD>" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
""" |
||||||
|
|
||||||
|
def clean_json_string(json_str): |
||||||
|
"""Clean and validate JSON string before parsing.""" |
||||||
|
try: |
||||||
|
start = json_str.find('{') |
||||||
|
end = json_str.rfind('}') + 1 |
||||||
|
if start >= 0 and end > 0: |
||||||
|
json_str = json_str[start:end] |
||||||
|
|
||||||
|
# Remove any extra whitespace |
||||||
|
json_str = json_str.strip() |
||||||
|
|
||||||
|
# Attempt to parse the JSON |
||||||
|
return json.loads(json_str) |
||||||
|
except json.JSONDecodeError as e: |
||||||
|
print(f"Failed to parse JSON. Raw response:\n{json_str}") |
||||||
|
print(f"Error: {str(e)}") |
||||||
|
return None |
||||||
|
|
||||||
|
def preprocess_extracted_text(extracted_text): |
||||||
|
"""Calls GPT-4o-mini to process prescription text into structured JSON.""" |
||||||
|
try: |
||||||
|
response = openai.chat.completions.create( |
||||||
|
model=MODEL, |
||||||
|
messages=[ |
||||||
|
{ |
||||||
|
"role": "system", |
||||||
|
"content": system_prompt, |
||||||
|
}, |
||||||
|
{ |
||||||
|
"role": "user", |
||||||
|
"content": f"Process this prescription and return ONLY valid JSON:\n\n{extracted_text}" |
||||||
|
} |
||||||
|
], |
||||||
|
temperature=0.3 # Lower temperature for more consistent JSON output |
||||||
|
) |
||||||
|
|
||||||
|
# Get the response content |
||||||
|
content = response.choices[0].message.content |
||||||
|
|
||||||
|
# Clean and parse the JSON |
||||||
|
parsed_data = clean_json_string(content) |
||||||
|
|
||||||
|
if parsed_data is None: |
||||||
|
return { |
||||||
|
"medicines": [], |
||||||
|
"tests": [], |
||||||
|
"follow_ups": [] |
||||||
|
} |
||||||
|
|
||||||
|
return parsed_data |
||||||
|
|
||||||
|
except Exception as e: |
||||||
|
print(f"Error in API call or processing: {str(e)}") |
||||||
|
return { |
||||||
|
"medicines": [], |
||||||
|
"tests": [], |
||||||
|
"follow_ups": [] |
||||||
|
} |
||||||
|
|
||||||
|
def process_dates(data): |
||||||
|
"""Adjusts test dates and follow-up based on the prescription date or today's date.""" |
||||||
|
try: |
||||||
|
# Extract prescription date (if available) or use today's date |
||||||
|
prescription_date = datetime.strptime("02 JANUARY 2025", "%d %B %Y").date() |
||||||
|
|
||||||
|
# Process test dates |
||||||
|
for test in data.get("tests", []): |
||||||
|
if isinstance(test, dict) and "date" not in test and "after_months" in test: |
||||||
|
test_date = prescription_date + timedelta(days=test["after_months"] * 30) |
||||||
|
test["date"] = test_date.strftime("%Y-%m-%d") |
||||||
|
|
||||||
|
# Process follow-up dates |
||||||
|
follow_ups = data.get("follow_ups", []) |
||||||
|
for follow_up in follow_ups: |
||||||
|
if isinstance(follow_up, dict) and "date" not in follow_up and "after_months" in follow_up: |
||||||
|
follow_up_date = prescription_date + timedelta(days=follow_up["after_months"] * 30) |
||||||
|
follow_up["date"] = follow_up_date.strftime("%Y-%m-%d") |
||||||
|
|
||||||
|
return data |
||||||
|
|
||||||
|
except Exception as e: |
||||||
|
print(f"Error processing dates: {str(e)}") |
||||||
|
return data |
Loading…
Reference in new issue