| """Date parsing, chemical reference DB, and spoilage scoring utilities.""" |
|
|
| from datetime import datetime, timedelta |
| from dateutil import parser as date_parser |
| import math |
| import re |
|
|
| |
| CHEMICAL_DB = { |
| "paracetamol": {"category": "active_ingredient", "risk_level": "safe"}, |
| "acetaminophen": {"category": "active_ingredient", "risk_level": "safe"}, |
| "ibuprofen": {"category": "active_ingredient", "risk_level": "safe"}, |
| "amoxicillin": {"category": "active_ingredient", "risk_level": "safe"}, |
| "amoxicilline": {"category": "active_ingredient", "risk_level": "safe"}, |
| "cetirizine": {"category": "active_ingredient", "risk_level": "safe"}, |
| "loratadine": {"category": "active_ingredient", "risk_level": "safe"}, |
| "metformin": {"category": "active_ingredient", "risk_level": "safe"}, |
| "omeprazole": {"category": "active_ingredient", "risk_level": "safe"}, |
| "pantoprazole": {"category": "active_ingredient", "risk_level": "safe"}, |
| "azithromycin": {"category": "active_ingredient", "risk_level": "safe"}, |
| "ciprofloxacin": {"category": "active_ingredient", "risk_level": "safe"}, |
| "doxycycline": {"category": "active_ingredient", "risk_level": "safe"}, |
| "metronidazole": {"category": "active_ingredient", "risk_level": "safe"}, |
| "diclofenac": {"category": "active_ingredient", "risk_level": "safe"}, |
| "naproxen": {"category": "active_ingredient", "risk_level": "safe"}, |
| "aspirin": {"category": "active_ingredient", "risk_level": "safe"}, |
| "salbutamol": {"category": "active_ingredient", "risk_level": "safe"}, |
| "prednisolone": {"category": "active_ingredient", "risk_level": "safe"}, |
| "dexamethasone": {"category": "active_ingredient", "risk_level": "safe"}, |
| "montelukast": {"category": "active_ingredient", "risk_level": "safe"}, |
| "levocetirizine": {"category": "active_ingredient", "risk_level": "safe"}, |
| "hydroxyzine": {"category": "active_ingredient", "risk_level": "caution"}, |
| "promethazine": {"category": "active_ingredient", "risk_level": "caution"}, |
| "chlorpheniramine": {"category": "active_ingredient", "risk_level": "safe"}, |
| "phenylephrine": {"category": "active_ingredient", "risk_level": "caution"}, |
| "pseudoephedrine": {"category": "active_ingredient", "risk_level": "caution"}, |
| "guaifenesin": {"category": "active_ingredient", "risk_level": "safe"}, |
| "ambroxol": {"category": "active_ingredient", "risk_level": "safe"}, |
| "bromhexine": {"category": "active_ingredient", "risk_level": "safe"}, |
| "terbutaline": {"category": "active_ingredient", "risk_level": "safe"}, |
| "theophylline": {"category": "active_ingredient", "risk_level": "caution"}, |
| "ranitidine": {"category": "active_ingredient", "risk_level": "caution"}, |
| "famotidine": {"category": "active_ingredient", "risk_level": "safe"}, |
| "loperamide": {"category": "active_ingredient", "risk_level": "safe"}, |
| "ondansetron": {"category": "active_ingredient", "risk_level": "safe"}, |
| "domperidone": {"category": "active_ingredient", "risk_level": "caution"}, |
| "metoclopramide": {"category": "active_ingredient", "risk_level": "caution"}, |
| "sucralfate": {"category": "active_ingredient", "risk_level": "safe"}, |
| "misoprostol": {"category": "active_ingredient", "risk_level": "danger"}, |
| "sodium_benzoate": {"category": "preservative", "risk_level": "caution"}, |
| "potassium_sorbate": {"category": "preservative", "risk_level": "safe"}, |
| "methylparaben": {"category": "preservative", "risk_level": "caution"}, |
| "propylparaben": {"category": "preservative", "risk_level": "caution"}, |
| "benzoic_acid": {"category": "preservative", "risk_level": "caution"}, |
| "sorbic_acid": {"category": "preservative", "risk_level": "safe"}, |
| "edta": {"category": "preservative", "risk_level": "safe"}, |
| "disodium_edta": {"category": "preservative", "risk_level": "safe"}, |
| "glycerin": {"category": "solvent", "risk_level": "safe"}, |
| "glycerol": {"category": "solvent", "risk_level": "safe"}, |
| "propylene_glycol": {"category": "solvent", "risk_level": "safe"}, |
| "sorbitol": {"category": "solvent", "risk_level": "safe"}, |
| "mannitol": {"category": "solvent", "risk_level": "safe"}, |
| "ethanol": {"category": "solvent", "risk_level": "caution"}, |
| "alcohol": {"category": "solvent", "risk_level": "caution"}, |
| "water": {"category": "solvent", "risk_level": "safe"}, |
| "purified_water": {"category": "solvent", "risk_level": "safe"}, |
| "sodium_chloride": {"category": "excipient", "risk_level": "safe"}, |
| "calcium_carbonate": {"category": "excipient", "risk_level": "safe"}, |
| "microcrystalline_cellulose": {"category": "binder", "risk_level": "safe"}, |
| "methylcellulose": {"category": "binder", "risk_level": "safe"}, |
| "hydroxypropyl_methylcellulose": {"category": "binder", "risk_level": "safe"}, |
| "povidone": {"category": "binder", "risk_level": "safe"}, |
| "croscarmellose_sodium": {"category": "disintegrant", "risk_level": "safe"}, |
| "starch": {"category": "filler", "risk_level": "safe"}, |
| "lactose": {"category": "filler", "risk_level": "safe"}, |
| "magnesium_stearate": {"category": "lubricant", "risk_level": "safe"}, |
| "talc": {"category": "lubricant", "risk_level": "safe"}, |
| "titanium_dioxide": {"category": "colorant", "risk_level": "caution"}, |
| "sunset_yellow": {"category": "colorant", "risk_level": "caution"}, |
| "tartrazine": {"category": "colorant", "risk_level": "caution"}, |
| "brilliant_blue": {"category": "colorant", "risk_level": "safe"}, |
| "allura_red": {"category": "colorant", "risk_level": "caution"}, |
| "carmine": {"category": "colorant", "risk_level": "safe"}, |
| "sucrose": {"category": "sweetener", "risk_level": "safe"}, |
| "sugar": {"category": "sweetener", "risk_level": "safe"}, |
| "aspartame": {"category": "sweetener", "risk_level": "caution"}, |
| "saccharin": {"category": "sweetener", "risk_level": "caution"}, |
| "xylitol": {"category": "sweetener", "risk_level": "safe"}, |
| "sodium_saccharin": {"category": "sweetener", "risk_level": "caution"}, |
| "citric_acid": {"category": "acidifier", "risk_level": "safe"}, |
| "sodium_citrate": {"category": "buffer", "risk_level": "safe"}, |
| "tartaric_acid": {"category": "acidifier", "risk_level": "safe"}, |
| "phosphoric_acid": {"category": "acidifier", "risk_level": "caution"}, |
| "sodium_hydroxide": {"category": "ph_adjuster", "risk_level": "caution"}, |
| "hydrochloric_acid": {"category": "ph_adjuster", "risk_level": "danger"}, |
| "banana_flavor": {"category": "flavoring", "risk_level": "safe"}, |
| "orange_flavor": {"category": "flavoring", "risk_level": "safe"}, |
| "vanilla_flavor": {"category": "flavoring", "risk_level": "safe"}, |
| "peppermint_oil": {"category": "flavoring", "risk_level": "safe"}, |
| "mint": {"category": "flavoring", "risk_level": "safe"}, |
| |
| "ashwagandha": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "withania_somnifera": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "tulsi": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "holy_basil": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "ocimum_tenuiflorum": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "turmeric": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "curcuma_longa": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "curcumin": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "brahmi": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "bacopa_monnieri": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "amla": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "emblica_officinalis": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "giloy": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "guduchi": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "tinospora_cordifolia": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "neem": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "azadirachta_indica": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "shatavari": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "asparagus_racemosus": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "mulethi": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "licorice": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "glycyrrhiza": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "haritaki": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "terminalia_chebula": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "bibhitaki": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "terminalia_bellirica": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "viraki": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "piper_longum": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "pippali": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "ginger": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "zingiber_officinale": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "black_pepper": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "piper_nigrum": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "fenugreek": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "methi": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "trigonella_foenum": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "cardamom": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "elaichi": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "elettaria_cardamomum": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "cinnamon": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "dalchini": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "cinnamomum": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "cloves": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "laung": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "syzygium_aromaticum": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "ajwain": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "carom_seeds": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "trachyspermum_ammi": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "cumin": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "jeera": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "cuminum_cyminum": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "coriander": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "dhania": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "coriandrum_sativum": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "guggulu": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "commiphora_mukul": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "boswellia": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "shallaki": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "gudmar": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "gymnema_sylvestre": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "karela": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "bitter_gourd": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "momordica_charantia": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "kutaja": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "holarrhena_antidysenterica": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "musta": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "cyperus_rotundus": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "bilva": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "bael": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "aegle_marmelos": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "udumbara": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "ficus_racemosa": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "kushtha": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "sarsaparilla": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "hemidesmus_indicus": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "anantmool": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| "sariva": {"category": "herbal_ingredient", "risk_level": "safe"}, |
| } |
|
|
|
|
| def parse_date(date_str: str) -> datetime | None: |
| """Parse date from various formats commonly found on medicine packaging.""" |
| if not date_str: |
| return None |
| date_str = str(date_str).strip() |
| |
| date_str = re.sub(r'(\w+)\.(\d)', r'\1 \2', date_str) |
|
|
| |
| patterns = [ |
| r"(\d{2})/(\d{2})/(\d{4})", |
| r"(\d{2})/(\d{2})/(\d{2})", |
| r"(\d{4})-(\d{2})-(\d{2})", |
| r"(\d{2})-(\d{2})-(\d{4})", |
| r"(\d{2})\.(\d{2})\.(\d{4})", |
| r"(\d{2})/(\d{4})", |
| r"(\d{2})-(\d{4})", |
| r"(\w+)\s+(\d{4})", |
| r"(\d{4})", |
| r"(\d{1,2})\s+(\w+)\s+(\d{4})", |
| ] |
|
|
| for pattern in patterns: |
| match = re.search(pattern, date_str) |
| if match: |
| groups = match.groups() |
| try: |
| if len(groups) == 3 and len(groups[2]) == 4: |
| |
| day, month, year = groups |
| return datetime(int(year), int(month), int(day)) |
| elif len(groups) == 3 and len(groups[0]) == 4: |
| |
| year, month, day = groups |
| return datetime(int(year), int(month), int(day)) |
| elif len(groups) == 3 and len(groups[0]) <= 2: |
| |
| day, month_str, year = groups |
| return date_parser.parse(f"{month_str} {day} {year}") |
| elif len(groups) == 2 and len(groups[1]) == 4: |
| |
| return date_parser.parse(f"{groups[0]} {groups[1]}") |
| elif len(groups) == 1 and len(groups[0]) == 4: |
| return datetime(int(groups[0]), 1, 1) |
| except (ValueError, TypeError): |
| continue |
|
|
| |
| try: |
| return date_parser.parse(date_str, dayfirst=True) |
| except (ValueError, TypeError): |
| return None |
|
|
|
|
| def calculate_spoilage_score( |
| visual_level: int, |
| bacteria_level: int, |
| days_until_expiry: int, |
| shelf_life_days: int, |
| color_deviation: float = 0.0, |
| dynamic_expiry_days: int = None, |
| ) -> int: |
| """Calculate spoilage score (0-100) from weighted factors. |
| |
| Weights: visual (35%), bacteria (25%), date (20%), color (10%), dynamic expiry (10%) |
| """ |
| |
| visual_score = min(max(visual_level, 0), 100) |
|
|
| |
| bacteria_score = min(max(bacteria_level, 0), 100) |
|
|
| |
| if shelf_life_days > 0 and days_until_expiry is not None: |
| date_ratio = max(0, 1 - (days_until_expiry / shelf_life_days)) |
| date_score = int(date_ratio * 100) |
| else: |
| date_score = 50 |
|
|
| |
| color_score = int(min(max(color_deviation, 0), 1.0) * 100) |
|
|
| |
| if dynamic_expiry_days is not None and dynamic_expiry_days >= 0: |
| |
| dynamic_score = max(0, 100 - int(dynamic_expiry_days / 3.65)) |
| else: |
| dynamic_score = 50 |
|
|
| |
| score = int( |
| visual_score * 0.35 + |
| bacteria_score * 0.25 + |
| date_score * 0.20 + |
| color_score * 0.10 + |
| dynamic_score * 0.10 |
| ) |
| return min(max(score, 0), 100) |
|
|
|
|
| def get_spoilage_verdict(score: int) -> str: |
| """Return verdict string from spoilage score.""" |
| if score > 60: |
| return "SPOILED" |
| elif score > 30: |
| return "WARNING" |
| else: |
| return "SAFE" |
|
|
|
|
| def get_verdict_color(score: int) -> str: |
| """Return hex color for verdict.""" |
| if score > 60: |
| return "#FF4444" |
| elif score > 30: |
| return "#FFAA00" |
| else: |
| return "#44BB44" |
|
|
|
|
| def enrich_chemicals(vlm_chemicals: list[dict]) -> list[dict]: |
| """Enrich VLM-extracted chemicals with reference DB data.""" |
| enriched = [] |
| for chem in vlm_chemicals: |
| name = chem.get("name", "").lower().strip() |
| |
| db_entry = CHEMICAL_DB.get(name) |
| if not db_entry: |
| |
| normalized = name.replace(" ", "_").replace("-", "_") |
| db_entry = CHEMICAL_DB.get(normalized) |
| if not db_entry: |
| |
| for key, val in CHEMICAL_DB.items(): |
| if key in name or name in key: |
| db_entry = val |
| break |
|
|
| enriched.append({ |
| "name": chem.get("name", "Unknown"), |
| "quantity": chem.get("quantity"), |
| "category": db_entry["category"] if db_entry else chem.get("category", "other"), |
| "risk_level": db_entry["risk_level"] if db_entry else chem.get("risk_level", "unknown"), |
| }) |
| return enriched |
|
|
|
|
| def parse_quantity(qty_str) -> float: |
| """Extract numeric value from quantity string like '5mg', '2.5ml', '100mcg'. |
| |
| Returns the numeric value for chart bar widths, or 1.0 as fallback. |
| """ |
| if not qty_str: |
| return 1.0 |
| match = re.search(r'([\d.]+)', str(qty_str)) |
| return float(match.group(1)) if match else 1.0 |
|
|
|
|
| |
|
|
| def calculate_theoretical_growth( |
| ingredients: list[str], |
| preservatives: list[str], |
| shelf_life_days: int, |
| days_since_mfg: int, |
| spoilage_level: int, |
| vlm_bacteria_level: int = 0, |
| ) -> dict: |
| """Calculate theoretical bacteria growth using logistic model. |
| |
| P(t) = K / (1 + ((K - P0) / P0) * e^(-rt)) |
| |
| vlm_bacteria_level: VLM's visual contamination estimate (0-100), |
| used to calibrate the growth curve. |
| """ |
| P0 = 10 |
| K = 10000 |
| r = 0.1 |
|
|
| |
| sugar_keywords = ["sucrose", "sugar", "sorbitol", "mannitol", "maltodextrin", "glucose", "fructose"] |
| for ing in ingredients: |
| if any(s in ing.lower() for s in sugar_keywords): |
| r *= 1.3 |
| break |
|
|
| |
| if preservatives: |
| r *= 0.3 |
| else: |
| r *= 1.5 |
|
|
| |
| |
| |
| if vlm_bacteria_level > 60: |
| r *= 2.0 |
| elif vlm_bacteria_level > 30: |
| r *= 1.5 |
| elif vlm_bacteria_level < 10: |
| r *= 0.5 |
|
|
| |
| if spoilage_level > 50: |
| P0 = 1000 |
|
|
| |
| growth_curve = {} |
| for day in [0, 30, 60, 90, 180, 365]: |
| growth = K / (1 + ((K - P0) / P0) * math.exp(-r * day)) |
| growth_pct = min(100, int((growth / K) * 100)) |
| growth_curve[f"day_{day}"] = growth_pct |
|
|
| |
| critical_day = shelf_life_days |
| for day in range(0, shelf_life_days + 1, 10): |
| growth = K / (1 + ((K - P0) / P0) * math.exp(-r * day)) |
| if growth / K > 0.6: |
| critical_day = day |
| break |
|
|
| current_day = min(days_since_mfg, 365) |
| current_growth = K / (1 + ((K - P0) / P0) * math.exp(-r * current_day)) |
| current_growth_pct = min(100, int((current_growth / K) * 100)) |
|
|
| return { |
| "current_growth_level": current_growth_pct, |
| "vlm_bacteria_level": vlm_bacteria_level, |
| "growth_curve": growth_curve, |
| "shelf_life_days": shelf_life_days, |
| "critical_threshold_day": critical_day, |
| "days_to_critical": max(0, critical_day - days_since_mfg), |
| "factors": { |
| "preservative_effectiveness": "high" if preservatives else "low", |
| "storage_impact": "optimal", |
| "visual_contamination": "severe" if spoilage_level > 60 else "mild" if spoilage_level > 30 else "none", |
| "preservatives_detected": preservatives, |
| "days_since_manufacturing": days_since_mfg, |
| }, |
| } |
|
|
|
|
| def calculate_dynamic_expiry( |
| mfg_date, exp_date, spoilage_assessment: dict, |
| color_deviation: float, preservatives: list, |
| ) -> dict: |
| """Calculate dynamic expiry based on visual indicators. |
| |
| When dates are missing, defaults to a 1-year shelf life: |
| - Missing mfg_date: assumes 180 days old |
| - Missing exp_date: assumes 180 days remaining |
| """ |
| today = datetime.now() |
|
|
| if not mfg_date: |
| mfg_date = today - timedelta(days=180) |
| if not exp_date: |
| exp_date = today + timedelta(days=180) |
|
|
| shelf_life = (exp_date - mfg_date).days |
| adjustment = 0 |
| factors = {} |
|
|
| |
| if spoilage_assessment.get("discoloration"): |
| reduction = int(shelf_life * 0.20) |
| adjustment += reduction |
| factors["visual_degradation"] = {"percentage": 20, "days_reduced": reduction, "reason": "discoloration"} |
|
|
| if spoilage_assessment.get("cloudiness"): |
| reduction = int(shelf_life * 0.10) |
| adjustment += reduction |
| factors["cloudiness"] = {"percentage": 10, "days_reduced": reduction, "reason": "cloudiness"} |
|
|
| if spoilage_assessment.get("sediment"): |
| reduction = int(shelf_life * 0.25) |
| adjustment += reduction |
| factors["sediment"] = {"percentage": 25, "days_reduced": reduction, "reason": "sediment"} |
|
|
| if not spoilage_assessment.get("seal_intact", True): |
| reduction = int(shelf_life * 0.30) |
| adjustment += reduction |
| factors["seal_damage"] = {"percentage": 30, "days_reduced": reduction, "reason": "broken seal"} |
|
|
| |
| if color_deviation > 0.3: |
| reduction = int(shelf_life * color_deviation * 0.5) |
| adjustment += reduction |
| factors["color_deviation"] = { |
| "percentage": int(color_deviation * 50), |
| "days_reduced": reduction, |
| "reason": f"color deviation {color_deviation:.2f}", |
| } |
|
|
| |
| if not preservatives: |
| reduction = int(shelf_life * 0.30) |
| adjustment += reduction |
| factors["no_preservatives"] = {"percentage": 30, "days_reduced": reduction, "reason": "no preservatives detected"} |
|
|
| adjusted_shelf_life = max(30, shelf_life - adjustment) |
| dynamic_expiry = mfg_date + timedelta(days=adjusted_shelf_life) |
| today = datetime.now() |
|
|
| return { |
| "shelf_life_days": shelf_life, |
| "adjusted_shelf_life_days": adjusted_shelf_life, |
| "days_until_dynamic_expiry": max(0, (dynamic_expiry - today).days), |
| "days_until_static_expiry": max(0, (exp_date - today).days), |
| "adjustment_factors": factors, |
| "total_adjustment_percentage": min(90, int((adjustment / shelf_life) * 100)), |
| "dynamic_expiry_date": dynamic_expiry.strftime("%Y-%m-%d"), |
| "static_expiry_date": exp_date.strftime("%Y-%m-%d"), |
| } |
|
|
|
|
| def estimate_color_from_spoilage(spoilage_assessment: dict) -> dict: |
| """Estimate color deviation from spoilage assessment.""" |
| deviation = 0.0 |
| indicators = [] |
|
|
| if spoilage_assessment.get("discoloration"): |
| deviation += 0.4 |
| indicators.append({"type": "oxidation", "severity": "moderate", "confidence": 0.7}) |
|
|
| if spoilage_assessment.get("cloudiness"): |
| deviation += 0.2 |
| indicators.append({"type": "turbidity", "severity": "mild", "confidence": 0.6}) |
|
|
| spoilage_level = spoilage_assessment.get("spoilage_level", 0) |
| if spoilage_level > 60: |
| deviation += 0.3 |
| indicators.append({"type": "general_degradation", "severity": "severe", "confidence": 0.8}) |
| elif spoilage_level > 30: |
| deviation += 0.1 |
| indicators.append({"type": "general_degradation", "severity": "mild", "confidence": 0.6}) |
|
|
| return { |
| "color_deviation": min(1.0, deviation), |
| "color_spoilage_score": int(min(1.0, deviation) * 100), |
| "degradation_indicators": indicators, |
| "estimated_days_since_optimal": int(deviation * 180), |
| } |
|
|