Upload 29 files
Browse files- Dockerfile +27 -0
- core/__pycache__/database.cpython-38.pyc +0 -0
- core/config.py +0 -0
- core/database.py +12 -0
- core/models/__pycache__/appointment.cpython-38.pyc +0 -0
- core/models/__pycache__/user.cpython-38.pyc +0 -0
- core/models/appointment.py +13 -0
- core/models/user.py +11 -0
- main.py +26 -0
- requirements.txt +0 -0
- routes/__init__.py +2 -0
- routes/__pycache__/__init__.cpython-38.pyc +0 -0
- routes/__pycache__/appointments.cpython-38.pyc +0 -0
- routes/__pycache__/users.cpython-38.pyc +0 -0
- routes/appointments.py +88 -0
- routes/users.py +47 -0
- schemas/__pycache__/appointment.cpython-38.pyc +0 -0
- schemas/__pycache__/user.cpython-38.pyc +0 -0
- schemas/appointment.py +14 -0
- schemas/user.py +13 -0
- semantic_specialist_model.pkl +3 -0
- utils/__pycache__/auth.cpython-38.pyc +0 -0
- utils/__pycache__/security.cpython-38.pyc +0 -0
- utils/__pycache__/specialist_predictor.cpython-38.pyc +0 -0
- utils/__pycache__/stt_processor.cpython-38.pyc +0 -0
- utils/auth.py +13 -0
- utils/security.py +9 -0
- utils/specialist_predictor.py +17 -0
- utils/stt_processor.py +55 -0
Dockerfile
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use a slim Python base image
|
2 |
+
FROM python:3.10-slim
|
3 |
+
|
4 |
+
# Prevents Python from writing pyc files to disk and buffers logs immediately
|
5 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
6 |
+
ENV PYTHONUNBUFFERED=1
|
7 |
+
|
8 |
+
# Set working directory in the container
|
9 |
+
WORKDIR /app
|
10 |
+
|
11 |
+
# Install system dependencies (for audio, etc.)
|
12 |
+
RUN apt-get update && apt-get install -y ffmpeg libsndfile1 && rm -rf /var/lib/apt/lists/*
|
13 |
+
|
14 |
+
# Copy dependency file first to leverage Docker cache
|
15 |
+
COPY requirements.txt .
|
16 |
+
|
17 |
+
# Install Python dependencies
|
18 |
+
RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt
|
19 |
+
|
20 |
+
# Copy the rest of the app
|
21 |
+
COPY . .
|
22 |
+
|
23 |
+
# Expose the FastAPI port (Hugging Face prefers 7860, but 8000 is also fine)
|
24 |
+
EXPOSE 8000
|
25 |
+
|
26 |
+
# Run the app using Uvicorn
|
27 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
core/__pycache__/database.cpython-38.pyc
ADDED
Binary file (493 Bytes). View file
|
|
core/config.py
ADDED
File without changes
|
core/database.py
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from sqlalchemy import create_engine
|
2 |
+
from sqlalchemy.ext.declarative import declarative_base
|
3 |
+
from sqlalchemy.orm import sessionmaker
|
4 |
+
|
5 |
+
SQLALCHEMY_DATABASE_URL = "sqlite:///./appointments.db"
|
6 |
+
|
7 |
+
engine = create_engine(
|
8 |
+
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
9 |
+
)
|
10 |
+
|
11 |
+
SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)
|
12 |
+
Base = declarative_base()
|
core/models/__pycache__/appointment.cpython-38.pyc
ADDED
Binary file (613 Bytes). View file
|
|
core/models/__pycache__/user.cpython-38.pyc
ADDED
Binary file (619 Bytes). View file
|
|
core/models/appointment.py
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from sqlalchemy import Column, Integer, String
|
2 |
+
from core.database import Base
|
3 |
+
|
4 |
+
class Appointment(Base):
|
5 |
+
__tablename__ = "appointments"
|
6 |
+
|
7 |
+
id = Column(Integer, primary_key=True, index=True)
|
8 |
+
patient_name = Column(String, nullable=False)
|
9 |
+
age = Column(Integer, nullable=False)
|
10 |
+
symptoms = Column(String, nullable=False)
|
11 |
+
specialist = Column(String, nullable=False)
|
12 |
+
|
13 |
+
|
core/models/user.py
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from sqlalchemy import Column, Integer, String
|
2 |
+
from core.database import Base
|
3 |
+
|
4 |
+
class User(Base):
|
5 |
+
__tablename__ = "users"
|
6 |
+
|
7 |
+
id = Column(Integer, primary_key=True, index=True)
|
8 |
+
full_name = Column(String, nullable=False)
|
9 |
+
email = Column(String, unique=True, index=True, nullable=False)
|
10 |
+
password = Column(String, nullable=False)
|
11 |
+
specialization = Column(String, nullable=True) # Only for doctors
|
main.py
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI
|
2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
3 |
+
from core.database import engine
|
4 |
+
from core.models.appointment import Appointment
|
5 |
+
from core.models.user import User
|
6 |
+
from routes import appointments, users
|
7 |
+
|
8 |
+
# Create FastAPI app first
|
9 |
+
app = FastAPI()
|
10 |
+
|
11 |
+
# ✅ Enable CORS BEFORE including any routers
|
12 |
+
app.add_middleware(
|
13 |
+
CORSMiddleware,
|
14 |
+
allow_origins=["http://localhost:3000"], # React frontend
|
15 |
+
allow_credentials=True,
|
16 |
+
allow_methods=["*"],
|
17 |
+
allow_headers=["*"],
|
18 |
+
)
|
19 |
+
|
20 |
+
# Create tables
|
21 |
+
Appointment.__table__.create(bind=engine, checkfirst=True)
|
22 |
+
User.__table__.create(bind=engine, checkfirst=True)
|
23 |
+
|
24 |
+
# Register routers
|
25 |
+
app.include_router(appointments.router)
|
26 |
+
app.include_router(users.router)
|
requirements.txt
ADDED
Binary file (368 Bytes). View file
|
|
routes/__init__.py
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
from . import appointments
|
2 |
+
from . import users # ✅ This should import the file
|
routes/__pycache__/__init__.cpython-38.pyc
ADDED
Binary file (191 Bytes). View file
|
|
routes/__pycache__/appointments.cpython-38.pyc
ADDED
Binary file (2.03 kB). View file
|
|
routes/__pycache__/users.cpython-38.pyc
ADDED
Binary file (1.67 kB). View file
|
|
routes/appointments.py
ADDED
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Depends
|
2 |
+
from sqlalchemy.orm import Session
|
3 |
+
from schemas.appointment import AppointmentInput
|
4 |
+
from core.database import SessionLocal
|
5 |
+
from core.models.appointment import Appointment
|
6 |
+
|
7 |
+
router = APIRouter()
|
8 |
+
|
9 |
+
# Dependency
|
10 |
+
def get_db():
|
11 |
+
db = SessionLocal()
|
12 |
+
try:
|
13 |
+
yield db
|
14 |
+
finally:
|
15 |
+
db.close()
|
16 |
+
|
17 |
+
@router.post("/appointments/create")
|
18 |
+
def create_appointment(data: AppointmentInput, db: Session = Depends(get_db)):
|
19 |
+
new_appointment = Appointment(
|
20 |
+
patient_name=data.patient_name,
|
21 |
+
age=data.age,
|
22 |
+
symptoms=data.symptoms,
|
23 |
+
specialist=data.specialist
|
24 |
+
)
|
25 |
+
db.add(new_appointment)
|
26 |
+
db.commit()
|
27 |
+
db.refresh(new_appointment)
|
28 |
+
|
29 |
+
return {
|
30 |
+
"message": "Appointment saved to database",
|
31 |
+
"data": {
|
32 |
+
"id": new_appointment.id,
|
33 |
+
"patient_name": new_appointment.patient_name,
|
34 |
+
"age": new_appointment.age,
|
35 |
+
"symptoms": new_appointment.symptoms,
|
36 |
+
"specialist": new_appointment.specialist
|
37 |
+
}
|
38 |
+
}
|
39 |
+
from fastapi import APIRouter, UploadFile, File
|
40 |
+
from utils.stt_processor import simulate_stt # make sure path is correct
|
41 |
+
import shutil
|
42 |
+
import os
|
43 |
+
from utils.specialist_predictor import predict_specialist
|
44 |
+
|
45 |
+
@router.post("/appointments/voice")
|
46 |
+
async def create_appointment_from_voice(audio: UploadFile = File(...), db: Session = Depends(get_db)):
|
47 |
+
# 1. Save temp audio
|
48 |
+
temp_path = f"temp_{audio.filename}"
|
49 |
+
with open(temp_path, "wb") as buffer:
|
50 |
+
shutil.copyfileobj(audio.file, buffer)
|
51 |
+
|
52 |
+
try:
|
53 |
+
# 2. Extract symptoms from audio
|
54 |
+
data = simulate_stt(temp_path)
|
55 |
+
symptoms = data.get("symptoms", "")
|
56 |
+
|
57 |
+
# 3. Predict specialist using model
|
58 |
+
specialist, score = predict_specialist(symptoms)
|
59 |
+
|
60 |
+
# 4. Create appointment object
|
61 |
+
new_appointment = Appointment(
|
62 |
+
patient_name=data.get("patient_name", "Unknown"),
|
63 |
+
age=data.get("age", 0),
|
64 |
+
symptoms=symptoms,
|
65 |
+
specialist=specialist
|
66 |
+
)
|
67 |
+
|
68 |
+
db.add(new_appointment)
|
69 |
+
db.commit()
|
70 |
+
db.refresh(new_appointment)
|
71 |
+
|
72 |
+
# 5. Return response
|
73 |
+
return {
|
74 |
+
"message": "Appointment created successfully from voice",
|
75 |
+
"predicted_specialist": specialist,
|
76 |
+
"similarity_score": round(score, 4),
|
77 |
+
"appointment": {
|
78 |
+
"id": new_appointment.id,
|
79 |
+
"patient_name": new_appointment.patient_name,
|
80 |
+
"age": new_appointment.age,
|
81 |
+
"symptoms": symptoms,
|
82 |
+
"specialist": specialist
|
83 |
+
}
|
84 |
+
}
|
85 |
+
|
86 |
+
finally:
|
87 |
+
# Clean up file
|
88 |
+
os.remove(temp_path)
|
routes/users.py
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Depends,HTTPException
|
2 |
+
from sqlalchemy.orm import Session
|
3 |
+
from core.database import SessionLocal
|
4 |
+
from core.models.user import User
|
5 |
+
from schemas.user import UserCreate,UserLogin
|
6 |
+
from utils.security import hash_password
|
7 |
+
from utils.auth import create_access_token # adjust based on your folder structure
|
8 |
+
|
9 |
+
from passlib.hash import bcrypt
|
10 |
+
router = APIRouter(
|
11 |
+
prefix="/users", # ✅ important for proper routing like /users/signup
|
12 |
+
tags=["Users"]
|
13 |
+
)
|
14 |
+
def get_db():
|
15 |
+
db = SessionLocal()
|
16 |
+
try:
|
17 |
+
yield db
|
18 |
+
finally:
|
19 |
+
db.close()
|
20 |
+
print("✅ users.router loaded")
|
21 |
+
@router.post("/signup")
|
22 |
+
def create_user(user: UserCreate, db: Session = Depends(get_db)):
|
23 |
+
db_user = User(
|
24 |
+
full_name=user.full_name,
|
25 |
+
email=user.email,
|
26 |
+
password=hash_password(user.password), # ⚠️ Should hash password before storing
|
27 |
+
specialization=user.specialization
|
28 |
+
)
|
29 |
+
db.add(db_user)
|
30 |
+
db.commit()
|
31 |
+
db.refresh(db_user)
|
32 |
+
return {"message": "User created successfully", "user_id": db_user.id}
|
33 |
+
|
34 |
+
|
35 |
+
@router.post("/login")
|
36 |
+
def login(user: UserLogin, db: Session = Depends(get_db)):
|
37 |
+
db_user = db.query(User).filter(User.email == user.email).first()
|
38 |
+
if not db_user:
|
39 |
+
raise HTTPException(status_code=400, detail="Invalid email or password")
|
40 |
+
|
41 |
+
if not bcrypt.verify(user.password, db_user.password):
|
42 |
+
raise HTTPException(status_code=400, detail="Invalid email or password")
|
43 |
+
|
44 |
+
# Generate JWT token
|
45 |
+
token = create_access_token(data={"sub": db_user.email})
|
46 |
+
|
47 |
+
return {"access_token": token, "token_type": "bearer", "user_id": db_user.id, "name": db_user.full_name}
|
schemas/__pycache__/appointment.cpython-38.pyc
ADDED
Binary file (756 Bytes). View file
|
|
schemas/__pycache__/user.cpython-38.pyc
ADDED
Binary file (585 Bytes). View file
|
|
schemas/appointment.py
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter
|
2 |
+
from pydantic import BaseModel
|
3 |
+
|
4 |
+
router = APIRouter()
|
5 |
+
class AppointmentInput(BaseModel):
|
6 |
+
patient_name: str
|
7 |
+
age: int
|
8 |
+
symptoms: str
|
9 |
+
specialist: str
|
10 |
+
|
11 |
+
@router.post("/appointments/create")
|
12 |
+
async def create_appointment(data: AppointmentInput):
|
13 |
+
print(f"Received appointment: {data}")
|
14 |
+
return {"message": "Appointment created", "data": data}
|
schemas/user.py
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pydantic import BaseModel
|
2 |
+
|
3 |
+
class UserCreate(BaseModel):
|
4 |
+
full_name: str
|
5 |
+
email: str
|
6 |
+
password: str
|
7 |
+
specialization: str
|
8 |
+
|
9 |
+
|
10 |
+
|
11 |
+
class UserLogin(BaseModel):
|
12 |
+
email: str
|
13 |
+
password: str
|
semantic_specialist_model.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:9472ff0898bdaadba953f7e56549c804c21dbfb5dec7a2a1f57ade6dc80a8813
|
3 |
+
size 572198
|
utils/__pycache__/auth.cpython-38.pyc
ADDED
Binary file (671 Bytes). View file
|
|
utils/__pycache__/security.cpython-38.pyc
ADDED
Binary file (593 Bytes). View file
|
|
utils/__pycache__/specialist_predictor.cpython-38.pyc
ADDED
Binary file (785 Bytes). View file
|
|
utils/__pycache__/stt_processor.cpython-38.pyc
ADDED
Binary file (2.06 kB). View file
|
|
utils/auth.py
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime, timedelta
|
2 |
+
from jose import JWTError, jwt
|
3 |
+
|
4 |
+
SECRET_KEY = "your-secret-key" # Use a strong secret and store securely
|
5 |
+
ALGORITHM = "HS256"
|
6 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
7 |
+
|
8 |
+
def create_access_token(data: dict, expires_delta: timedelta = None):
|
9 |
+
to_encode = data.copy()
|
10 |
+
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
|
11 |
+
to_encode.update({"exp": expire})
|
12 |
+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
13 |
+
return encoded_jwt
|
utils/security.py
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from passlib.context import CryptContext
|
2 |
+
|
3 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
4 |
+
|
5 |
+
def hash_password(password: str) -> str:
|
6 |
+
return pwd_context.hash(password)
|
7 |
+
|
8 |
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
9 |
+
return pwd_context.verify(plain_password, hashed_password)
|
utils/specialist_predictor.py
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from sentence_transformers import SentenceTransformer, util
|
2 |
+
import torch
|
3 |
+
import joblib
|
4 |
+
|
5 |
+
# Load model components once
|
6 |
+
bundle = joblib.load("semantic_specialist_model.pkl")
|
7 |
+
model = SentenceTransformer(bundle["model_name"])
|
8 |
+
known_embeddings = bundle["known_embeddings"]
|
9 |
+
symptom_specialist_pairs = bundle["symptom_specialist_pairs"]
|
10 |
+
|
11 |
+
def predict_specialist(symptom_text: str):
|
12 |
+
input_embedding = model.encode(symptom_text, convert_to_tensor=True)
|
13 |
+
similarities = util.pytorch_cos_sim(input_embedding, known_embeddings)[0]
|
14 |
+
top_idx = similarities.argmax().item()
|
15 |
+
specialist = symptom_specialist_pairs[top_idx][1]
|
16 |
+
score = similarities[top_idx].item()
|
17 |
+
return specialist, score
|
utils/stt_processor.py
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import speech_recognition as sr
|
2 |
+
import re
|
3 |
+
import os
|
4 |
+
from pydub import AudioSegment
|
5 |
+
|
6 |
+
def convert_to_wav_pydub(input_path: str, output_path: str = "converted_temp.wav") -> str:
|
7 |
+
audio = AudioSegment.from_file(input_path)
|
8 |
+
audio = audio.set_frame_rate(16000).set_channels(1).set_sample_width(2) # 16-bit PCM
|
9 |
+
audio.export(output_path, format="wav")
|
10 |
+
return output_path
|
11 |
+
|
12 |
+
def transcribe_audio(audio_file_path: str) -> str:
|
13 |
+
recognizer = sr.Recognizer()
|
14 |
+
|
15 |
+
# Convert to proper WAV if needed
|
16 |
+
if not audio_file_path.lower().endswith(".wav"):
|
17 |
+
converted_path = convert_to_wav_pydub(audio_file_path) # ✅ FIXED HERE
|
18 |
+
delete_after_use = True
|
19 |
+
else:
|
20 |
+
converted_path = convert_to_wav_pydub(audio_file_path) # Even if WAV, reconvert to ensure PCM format
|
21 |
+
delete_after_use = True
|
22 |
+
|
23 |
+
try:
|
24 |
+
with sr.AudioFile(converted_path) as source:
|
25 |
+
audio_data = recognizer.record(source)
|
26 |
+
text = recognizer.recognize_google(audio_data)
|
27 |
+
print("✅ Transcribed Text:", text)
|
28 |
+
return text
|
29 |
+
except sr.UnknownValueError:
|
30 |
+
print("⚠️ Could not understand audio")
|
31 |
+
return "Could not understand audio"
|
32 |
+
except sr.RequestError as e:
|
33 |
+
print("❌ API Request Error:", e)
|
34 |
+
return f"Request failed: {e}"
|
35 |
+
finally:
|
36 |
+
if delete_after_use and os.path.exists(converted_path):
|
37 |
+
try:
|
38 |
+
os.remove(converted_path)
|
39 |
+
except PermissionError:
|
40 |
+
print("⚠️ Warning: File could not be deleted, still in use.")
|
41 |
+
|
42 |
+
def simulate_stt(audio_file_path: str) -> dict:
|
43 |
+
raw_text = transcribe_audio(audio_file_path)
|
44 |
+
|
45 |
+
# Extract structured data using regex
|
46 |
+
name_match = re.search(r"my name is ([a-zA-Z ]+?)(?= i am| and|,|\.|$)", raw_text, re.IGNORECASE)
|
47 |
+
age_match = re.search(r"i am (\d+) years old", raw_text, re.IGNORECASE)
|
48 |
+
symptoms_match = re.search(r"suffering from (.+)", raw_text, re.IGNORECASE)
|
49 |
+
|
50 |
+
return {
|
51 |
+
"patient_name": name_match.group(1).strip() if name_match else "Unknown",
|
52 |
+
"age": int(age_match.group(1)) if age_match else 0,
|
53 |
+
"symptoms": symptoms_match.group(1).strip() if symptoms_match else "Not mentioned",
|
54 |
+
"preferred_doctor": "Not specified"
|
55 |
+
}
|