From adcb49f9f0a3f63e12efab817b1db8c4f90cb351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ali=20Can=20K=C3=B6se?= Date: Sat, 18 Apr 2026 15:57:52 +0300 Subject: [PATCH 1/2] feat: add diet parsing endpoint and schemas for diet plan analysis --- app/api/endpoints.py | 31 ++++++++++++- app/models/schemas.py | 21 +++++++++ app/services/diet_parser_service.py | 71 +++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 app/services/diet_parser_service.py diff --git a/app/api/endpoints.py b/app/api/endpoints.py index 95bf97a..115faaa 100644 --- a/app/api/endpoints.py +++ b/app/api/endpoints.py @@ -22,7 +22,9 @@ map_gemini_to_macros, GEMINI_API_KEY ) -from app.models.schemas import ComplianceRequest, ComplianceResponse +from app.models.schemas import ComplianceRequest, ComplianceResponse, DietParseRequest, DietPlanParsedResponse +from app.services.diet_parser_service import parse_diet_plan +import base64 logger = logging.getLogger(__name__) @@ -418,3 +420,30 @@ async def analyze_compliance(request: ComplianceRequest): logger.error(f"Error checking compliance: {str(e)}", exc_info=True) raise HTTPException(status_code=500, detail=f"Unexpected error: {str(e)}") +@router.post("/parse-diet") +async def parse_diet(request: DietParseRequest): + try: + image_bytes = None + if request.image_base64: + # Remove header if exists, like 'data:image/jpeg;base64,' + base64_data = request.image_base64 + if "," in base64_data: + base64_data = base64_data.split(",")[1] + try: + image_bytes = base64.b64decode(base64_data) + except Exception as decode_err: + logger.error(f"Failed to decode base64 image: {decode_err}") + return {"success": False, "message": "Geçersiz görsel formatı", "data": None} + + parsed_data = await parse_diet_plan(raw_text=request.raw_text, image_data=image_bytes) + + if not parsed_data: + return {"success": False, "message": "Diyet listesi analiz edilemedi", "data": None} + + return {"success": True, "message": "Analiz başarılı", "data": parsed_data} + + except Exception as e: + logger.error(f"Error in parse_diet: {str(e)}", exc_info=True) + return {"success": False, "message": str(e), "data": None} + + diff --git a/app/models/schemas.py b/app/models/schemas.py index 029a203..a07a28f 100644 --- a/app/models/schemas.py +++ b/app/models/schemas.py @@ -106,3 +106,24 @@ class ComplianceRequest(BaseModel): class ComplianceResponse(BaseModel): success: bool = Field(..., description="Status") feedback: str = Field(..., description="AI feedback string") + +class DietMealSchema(BaseModel): + meal_type: str = Field(..., description="BREAKFAST, LUNCH, DINNER, or SNACK") + food_name: str + target_calories: int = 0 + target_protein: float = 0.0 + target_carbs: float = 0.0 + target_fat: float = 0.0 + +class DietDaySchema(BaseModel): + day_number: int + total_target_calories: int = 0 + meals: List[DietMealSchema] + +class DietPlanParsedResponse(BaseModel): + plan_name: str + days: List[DietDaySchema] + +class DietParseRequest(BaseModel): + raw_text: Optional[str] = None + image_base64: Optional[str] = None diff --git a/app/services/diet_parser_service.py b/app/services/diet_parser_service.py new file mode 100644 index 0000000..8023a26 --- /dev/null +++ b/app/services/diet_parser_service.py @@ -0,0 +1,71 @@ +import base64 +import json +import logging +from typing import Optional + +from google import genai +from google.genai import types + +from app.core.config import GEMINI_API_KEY +from app.models.schemas import DietPlanParsedResponse + +logger = logging.getLogger(__name__) + +def get_gemini_client(): + if not GEMINI_API_KEY: + return None + return genai.Client(api_key=GEMINI_API_KEY) + + +async def parse_diet_plan(raw_text: Optional[str] = None, image_data: Optional[bytes] = None) -> Optional[dict]: + client = get_gemini_client() + if not client: + logger.error("Gemini client not initialized.") + return None + + prompt = ( + "Sen profesyonel bir diyetisyensin. Görseldeki veya metindeki 7 günlük diyet listesini analiz et. " + "Her öğün için protein, karbonhidrat ve yağ değerlerini profesyonel olarak TAHMİN ET ve JSON formatında döndür. " + "Dönecek JSON içerisinde Türkçe karakterleri (ş, ğ, ç, ö, ü, ı) doğrudan kullan ve unicode escape sequence kullanma. " + "JSON çıktısı tam olarak aşağıdaki yapıya uygun olmalıdır:\n" + "{\n" + ' "plan_name": "Plan Name",\n' + ' "days": [\n' + " {\n" + ' "day_number": 1,\n' + ' "total_target_calories": 2000,\n' + ' "meals": [\n' + " {\n" + ' "meal_type": "BREAKFAST",\n' + ' "food_name": "Food Name",\n' + ' "target_calories": 500,\n' + ' "target_protein": 30.0,\n' + ' "target_carbs": 40.0,\n' + ' "target_fat": 15.0\n' + " }\n" + " ]\n" + " }\n" + " ]\n" + "}\n" + ) + + contents = [prompt] + if raw_text: + contents.append(raw_text) + if image_data: + contents.append( + types.Part.from_bytes(data=image_data, mime_type="image/jpeg",) + ) + + try: + response = await client.aio.models.generate_content( + model='gemini-1.5-flash', + contents=contents, + config=types.GenerateContentConfig(response_mime_type="application/json") + ) + + parsed_result = json.loads(response.text) + return parsed_result + except Exception as e: + logger.error(f"Error in parse_diet_plan: {e}") + return None From 4519ee60dc8265b36d8af75f6c198dcedf028128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ali=20Can=20K=C3=B6se?= Date: Sat, 18 Apr 2026 20:52:54 +0300 Subject: [PATCH 2/2] feat: enhance diet parser with GEMINI_MODEL support and improved JSON response handling --- app/services/diet_parser_service.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/app/services/diet_parser_service.py b/app/services/diet_parser_service.py index 8023a26..8a0a46e 100644 --- a/app/services/diet_parser_service.py +++ b/app/services/diet_parser_service.py @@ -6,7 +6,7 @@ from google import genai from google.genai import types -from app.core.config import GEMINI_API_KEY +from app.core.config import GEMINI_API_KEY, GEMINI_MODEL from app.models.schemas import DietPlanParsedResponse logger = logging.getLogger(__name__) @@ -14,7 +14,7 @@ def get_gemini_client(): if not GEMINI_API_KEY: return None - return genai.Client(api_key=GEMINI_API_KEY) + return genai.Client(api_key=GEMINI_API_KEY, http_options={'api_version': 'v1beta'}) async def parse_diet_plan(raw_text: Optional[str] = None, image_data: Optional[bytes] = None) -> Optional[dict]: @@ -26,6 +26,7 @@ async def parse_diet_plan(raw_text: Optional[str] = None, image_data: Optional[b prompt = ( "Sen profesyonel bir diyetisyensin. Görseldeki veya metindeki 7 günlük diyet listesini analiz et. " "Her öğün için protein, karbonhidrat ve yağ değerlerini profesyonel olarak TAHMİN ET ve JSON formatında döndür. " + "Görseldeki veya metindeki diyet planının kaç günlük olduğunu saptayarak her bir günü ayrı ayrı analiz et. Her günün öğünlerini (BREAKFAST, LUNCH, DINNER, SNACK) belirle ve her besin için protein, karbonhidrat ve yağ değerlerini profesyonelce TAHMİN ET. Eğer listede belirli bir gün sayısı belirtilmemişse, mevcut tüm veriyi işle." "Dönecek JSON içerisinde Türkçe karakterleri (ş, ğ, ç, ö, ü, ı) doğrudan kullan ve unicode escape sequence kullanma. " "JSON çıktısı tam olarak aşağıdaki yapıya uygun olmalıdır:\n" "{\n" @@ -59,13 +60,29 @@ async def parse_diet_plan(raw_text: Optional[str] = None, image_data: Optional[b try: response = await client.aio.models.generate_content( - model='gemini-1.5-flash', + model=GEMINI_MODEL, contents=contents, config=types.GenerateContentConfig(response_mime_type="application/json") ) - parsed_result = json.loads(response.text) - return parsed_result + raw_response = response.text.strip() + if raw_response.startswith("```json"): + raw_response = raw_response[7:] + elif raw_response.startswith("```"): + raw_response = raw_response[3:] + + if raw_response.endswith("```"): + raw_response = raw_response[:-3] + + raw_response = raw_response.strip() + + try: + parsed_result = json.loads(raw_response) + return parsed_result + except Exception as json_err: + logger.error(f"Error parsing JSON from Gemini: {json_err} - Raw Output:\n{response.text}") + return None + except Exception as e: logger.error(f"Error in parse_diet_plan: {e}") return None