Wills17 commited on
Commit
196f770
·
verified ·
1 Parent(s): 3c97f41

Update FastAPI_app.py

Browse files
Files changed (1) hide show
  1. FastAPI_app.py +99 -161
FastAPI_app.py CHANGED
@@ -1,5 +1,4 @@
1
  # FastAPI application for Fridge2Dish
2
- # Fallback: OpenChef-3B-v2 (GGUF) via llama-cpp-python
3
 
4
  # import libraries
5
  import os
@@ -18,29 +17,15 @@ from fastapi.templating import Jinja2Templates
18
  from fastapi.middleware.cors import CORSMiddleware
19
 
20
  # import ML libraries
 
21
  import tensorflow as tf
22
  import google.generativeai as genai
 
23
 
24
- # llama-cpp-python for GGUF fallback
25
 
26
- try:
27
- from llama_cpp import Llama
28
- except Exception as e:
29
- Llama = None
30
- print("Warning: llama_cpp not available. Install llama-cpp-python to use local OpenChef fallback.", e)
31
 
32
 
33
- # -----------------------------
34
- # CONFIG — adjust this path
35
- # -----------------------------
36
- # Set LOCAL_GGUF_PATH to the path of your OpenChef-3B-v2 GGUF file that you've
37
- # uploaded into the repo/persistent storage. Example:
38
- # LOCAL_GGUF_PATH = "/data/OpenChef-3B-v2.Q4_K_M.gguf"
39
- #
40
- # Developer note: replace the value below with the actual uploaded file path.
41
- LOCAL_GGUF_PATH = "models/OpenChef-3B-v2.Q4_K_M.gguf"
42
- # -----------------------------
43
-
44
 
45
  # Ingredient model (load once)
46
  MODEL_PATH = "models/ingredient_model.h5"
@@ -50,22 +35,84 @@ if not os.path.exists(MODEL_PATH):
50
  MODEL = tf.keras.models.load_model(MODEL_PATH)
51
 
52
 
53
- # Class names from train folder, otherwise manual.
54
  if os.path.isdir("dataset/dataset_2/train"):
55
  CLASS_NAMES = sorted(os.listdir("dataset/dataset_2/train"))
56
 
57
  else:
58
  CLASS_NAMES = [
59
- 'apple', 'banana', 'beetroot', 'bell pepper', 'cabbage', 'capsicum', 'carrot', 'cauliflower', 'chilli pepper',
60
- 'corn', 'cucumber', 'eggplant', 'garlic', 'ginger', 'grapes', 'jalepeno', 'kiwi', 'lemon', 'lettuce', 'mango',
61
- 'onion', 'orange', 'paprika', 'pear', 'peas', 'pineapple', 'pomegranate', 'potato', 'raddish', 'soy beans',
62
- 'spinach', 'sweetcorn', 'sweetpotato', 'tomato', 'turnip', 'watermelon']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
 
65
  # Infer uploaded image function
66
  def infer_image(pil_image):
67
  """
68
- Returns a list of dicts: [{ "name": CapitalizedName, "confidence": 0.xx }, ...]
69
  """
70
  img = pil_image.resize((224, 224))
71
  arr = np.expand_dims(np.array(img) / 255.0, axis=0)
@@ -73,106 +120,12 @@ def infer_image(pil_image):
73
  top_idxs = np.argsort(preds)[::-1][:5]
74
  ingredients = []
75
  for i in top_idxs:
76
- ingredients.append({
77
- "name": CLASS_NAMES[i].capitalize(),
78
- "confidence": float(preds[i])
79
- })
80
  if not ingredients:
81
  return [{"name": "Unknown", "confidence": 0.0}]
82
- return ingredients
83
-
84
-
85
- # Protect loading by locking.
86
- _llama_lock = threading.Lock()
87
- _llama_model = None
88
-
89
-
90
- def load_local_openchef():
91
- """Load the OpenChef GGUF via llama-cpp-python. Thread-safe and cached."""
92
- global _llama_model
93
- if _llama_model is not None:
94
- return _llama_model
95
-
96
- if Llama is None:
97
- raise RuntimeError("llama_cpp is not installed. Install 'llama-cpp-python' to use local OpenChef fallback.")
98
-
99
- with _llama_lock:
100
- if _llama_model is not None:
101
- return _llama_model
102
-
103
- if not os.path.exists(LOCAL_GGUF_PATH):
104
- # be explicit about missing model
105
- raise FileNotFoundError(
106
- f"Local OpenChef GGUF not found at {LOCAL_GGUF_PATH}. "
107
- "Place the .gguf file there or update LOCAL_GGUF_PATH."
108
- )
109
-
110
- # instantiate; adjust n_ctx if needed
111
- print(f"[openchef] Loading GGUF model from {LOCAL_GGUF_PATH} ...")
112
- _llama_model = Llama(model_path=LOCAL_GGUF_PATH, n_ctx=2048)
113
- print("[openchef] Loaded.")
114
- return _llama_model
115
-
116
-
117
- def generate_recipe_local_openchef(ingredient_names: list, max_tokens: int = 512, temperature: float = 0.7):
118
- """
119
- Generate a markdown recipe using the local OpenChef (GGUF).
120
- Returns plain text (markdown).
121
- """
122
- llama = load_local_openchef()
123
 
124
- # clean ingredient list string
125
- ing_str = ", ".join(ingredient_names)
126
-
127
- prompt = f"""You are a concise AI chef. Use ONLY these ingredients: {ing_str}
128
-
129
- Rules:
130
- - Title on one line.
131
- - One-sentence description.
132
- - "### Ingredients" followed by a bullet list with approximate quantities.
133
- - "### Steps" followed by 6-8 numbered concise steps.
134
- - Optionally a "Tip:" line at the end.
135
- - No extra commentary, no apologias. Return only the recipe in markdown.
136
-
137
- Recipe:
138
- """
139
-
140
- # llama-cpp-python returns dict with 'choices' etc or direct text depending on version
141
- # Use completion with stop tokens to keep output concise.
142
- try:
143
- resp = llama.create(
144
- prompt=prompt,
145
- max_tokens=max_tokens,
146
- temperature=temperature,
147
- top_p=0.95,
148
- stop=["\n\n\n"]
149
- )
150
- except TypeError:
151
- # older/newer llama-cpp-python API differences
152
- resp = llama(prompt, max_tokens=max_tokens, temperature=temperature)
153
-
154
- # extract text
155
- # resp may be dict-like: {'choices': [{'text': '...'}], ...}
156
- text = ""
157
- try:
158
- if isinstance(resp, dict) and "choices" in resp:
159
- # new style
160
- text = resp["choices"][0].get("text", "").strip()
161
- elif hasattr(resp, "choices"):
162
- text = resp.choices[0].text.strip()
163
- elif isinstance(resp, str):
164
- text = resp.strip()
165
- else:
166
- # fallback, str conversion
167
- text = str(resp).strip()
168
- except Exception:
169
- text = str(resp).strip()
170
-
171
- # sanity clean: if the model repeated the prompt, strip it
172
- if text.startswith("Recipe:"):
173
- text = text.split("Recipe:", 1)[1].strip()
174
-
175
- return text
176
 
177
 
178
  # initialize FastAPI app
@@ -182,7 +135,7 @@ app = FastAPI(
182
  version="3.0.0"
183
  )
184
 
185
- # static/templates
186
  app.mount("/static", StaticFiles(directory="static"), name="static")
187
  templates = Jinja2Templates(directory="templates")
188
 
@@ -195,32 +148,22 @@ app.add_middleware(
195
  allow_headers=["*"],
196
  )
197
 
198
-
199
-
200
- # ROUTES
201
-
202
- # Home Route
203
  @app.get("/", response_class=HTMLResponse)
204
  def home(request: Request):
205
  return templates.TemplateResponse("index.html", {"request": request})
206
 
207
 
208
- # upload-image route
209
  @app.post("/upload-image/")
210
- async def upload_image(
211
- file: UploadFile = File(...),
212
- user_api_key: str = Form(alias="api_key", default="")
213
- ):
214
-
215
  try:
216
- if not file.filename.lower().endswith((".jpg", ".jpeg", ".png")):
217
  raise HTTPException(status_code=400, detail="Invalid image format.")
218
 
219
- # load image
220
  img_bytes = await file.read()
221
  pil_img = Image.open(io.BytesIO(img_bytes)).convert("RGB")
222
 
223
- # detect ingredients
224
  start = time.time()
225
  ingredients = infer_image(pil_img)
226
  end = time.time()
@@ -232,11 +175,10 @@ async def upload_image(
232
  api_key = (user_api_key or "").strip()
233
 
234
  if api_key:
235
- # try Gemini first
236
  try:
237
  genai.configure(api_key=api_key)
238
- model = genai.GenerativeModel("gemini-2.5-flash")
239
-
240
  prompt = f"""
241
  You are an AI chef. Create a short recipe using only: {', '.join(ingredient_names)}.
242
  Include:
@@ -244,43 +186,39 @@ async def upload_image(
244
  - One-sentence description
245
  - Ingredients list with quantities
246
  - 6-10 concise steps
247
- - Optional fun tips or variations
248
- Return results in markdown format.
249
  """
250
-
251
  print("Trying Gemini...")
252
  response = model.generate_content(prompt)
253
  recipe_text = response.text.strip()
254
- print("\nGemini succeeded.")
255
-
256
  except Exception as e_gemini:
257
- print("\nGemini failed:", e_gemini)
258
- # fallback to local OpenChef
259
  try:
260
- recipe_text = generate_recipe_local_openchef(ingredient_names)
261
- except Exception as e_local:
262
- print("\nLocal OpenChef failed:", e_local)
263
- raise e_local
264
 
265
  else:
266
- # no API key: use local OpenChef fallback
267
  try:
268
- print("\nNo API key provided —> Using local OpenChef fallback.")
269
- recipe_text = generate_recipe_local_openchef(ingredient_names)
270
- except Exception as e_local:
271
- print("Local OpenChef failed:", e_local)
272
- raise e_local
273
 
274
  return {"ingredients": ingredients, "recipe": recipe_text}
275
 
276
  except HTTPException:
277
- # re-raise known HTTP errors
278
  raise
279
  except Exception as e:
280
  traceback.print_exc()
281
- raise HTTPException(status_code=500, detail=f"Server Error: {str(e)}")
282
-
283
-
284
  # Health check
285
  @app.get("/health")
286
  def health():
@@ -289,4 +227,4 @@ def health():
289
 
290
  # Run app
291
  if __name__ == "__main__":
292
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  # FastAPI application for Fridge2Dish
 
2
 
3
  # import libraries
4
  import os
 
17
  from fastapi.middleware.cors import CORSMiddleware
18
 
19
  # import ML libraries
20
+ import torch
21
  import tensorflow as tf
22
  import google.generativeai as genai
23
+ from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
24
 
 
25
 
 
 
 
 
 
26
 
27
 
28
+ # Configuration settings
 
 
 
 
 
 
 
 
 
 
29
 
30
  # Ingredient model (load once)
31
  MODEL_PATH = "models/ingredient_model.h5"
 
35
  MODEL = tf.keras.models.load_model(MODEL_PATH)
36
 
37
 
38
+ # Class names
39
  if os.path.isdir("dataset/dataset_2/train"):
40
  CLASS_NAMES = sorted(os.listdir("dataset/dataset_2/train"))
41
 
42
  else:
43
  CLASS_NAMES = [
44
+ 'apple', 'banana', 'beetroot', 'bell pepper', 'cabbage', 'capsicum', 'carrot', 'cauliflower',
45
+ 'chilli pepper', 'corn', 'cucumber', 'eggplant', 'garlic', 'ginger', 'grapes', 'jalepeno',
46
+ 'kiwi', 'lemon', 'lettuce', 'mango', 'onion', 'orange', 'paprika', 'pear', 'peas',
47
+ 'pineapple', 'pomegranate', 'potato', 'raddish', 'soy beans', 'spinach', 'sweetcorn',
48
+ 'sweetpotato', 'tomato', 'turnip', 'watermelon'
49
+ ]
50
+
51
+
52
+ # Thread-safe lazy loading
53
+ _lock = threading.Lock()
54
+ _tokenizer = None
55
+ _model = None
56
+
57
+ def load_gemma2_2b():
58
+ global _tokenizer, _model
59
+ if _model is not None:
60
+ return _tokenizer, _model
61
+
62
+ with _lock:
63
+ if _model is not None:
64
+ return _tokenizer, _model
65
+
66
+ print("[Fallback] Loading Gemma-2-2B-it 4-bit (this takes ~20 seconds first time)...")
67
+ quantization_config = BitsAndBytesConfig(
68
+ load_in_4bit=True,
69
+ bnb_4bit_compute_dtype=torch.float16,
70
+ bnb_4bit_quant_type="nf4"
71
+ )
72
+
73
+ _tokenizer = AutoTokenizer.from_pretrained("google/gemma-2-2b-it", token=False)
74
+ _model = AutoModelForCausalLM.from_pretrained(
75
+ "google/gemma-2-2b-it",
76
+ device_map="auto",
77
+ quantization_config=quantization_config,
78
+ torch_dtype=torch.float16,
79
+ trust_remote_code=True
80
+ )
81
+ print("[Fallback] Gemma-2-2B ready!")
82
+ return _tokenizer, _model
83
+
84
+ def generate_recipe_gemma(ingredient_names):
85
+ tokenizer, model = load_gemma2_2b()
86
+
87
+ prompt = f"""<start_of_turn>user
88
+ You are an AI chef. Create a short recipe using only: {', '.join(ingredient_names)}.
89
+ Include:
90
+ - Recipe name
91
+ - One-sentence description
92
+ - Ingredients list with quantities
93
+ - 6-10 concise steps
94
+ - Optional tips
95
+ RETURN RESULT IN MARKDOWN FORMAT ONLY.<end_of_turn>
96
+ <start_of_turn>model
97
+ """
98
+
99
+ inputs = tokenizer(prompt, return_tensors="pt")
100
+ outputs = model.generate(
101
+ inputs.input_ids,
102
+ max_new_tokens=512,
103
+ temperature=0.8,
104
+ top_p=0.9,
105
+ do_sample=True
106
+ )
107
+ recipe_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
108
+ # Strip the prompt part
109
+ return recipe_text.split("<start_of_turn>model")[-1].strip()
110
 
111
 
112
  # Infer uploaded image function
113
  def infer_image(pil_image):
114
  """
115
+ Returns a list of dicts: [{ "name": ing_1, "confidence": 0.xx }, ...]
116
  """
117
  img = pil_image.resize((224, 224))
118
  arr = np.expand_dims(np.array(img) / 255.0, axis=0)
 
120
  top_idxs = np.argsort(preds)[::-1][:5]
121
  ingredients = []
122
  for i in top_idxs:
123
+ ingredients.append({"name": CLASS_NAMES[i].capitalize(), "confidence": float(preds[i])})
124
+
 
 
125
  if not ingredients:
126
  return [{"name": "Unknown", "confidence": 0.0}]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
+ return ingredients
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
 
131
  # initialize FastAPI app
 
135
  version="3.0.0"
136
  )
137
 
138
+ # static and templates
139
  app.mount("/static", StaticFiles(directory="static"), name="static")
140
  templates = Jinja2Templates(directory="templates")
141
 
 
148
  allow_headers=["*"],
149
  )
150
 
151
+ # Home route
 
 
 
 
152
  @app.get("/", response_class=HTMLResponse)
153
  def home(request: Request):
154
  return templates.TemplateResponse("index.html", {"request": request})
155
 
156
 
157
+ # Upload-image route
158
  @app.post("/upload-image/")
159
+ async def upload_image(file: UploadFile = File(...), user_api_key: str = Form(alias="api_key", default="")):
 
 
 
 
160
  try:
161
+ if not file.filename.lower().endswith((".jpg", ".jpeg", ".png", ".webp", ".gif")):
162
  raise HTTPException(status_code=400, detail="Invalid image format.")
163
 
 
164
  img_bytes = await file.read()
165
  pil_img = Image.open(io.BytesIO(img_bytes)).convert("RGB")
166
 
 
167
  start = time.time()
168
  ingredients = infer_image(pil_img)
169
  end = time.time()
 
175
  api_key = (user_api_key or "").strip()
176
 
177
  if api_key:
 
178
  try:
179
  genai.configure(api_key=api_key)
180
+ model = genai.GenerativeModel("gemini-1.5-pro")
181
+
182
  prompt = f"""
183
  You are an AI chef. Create a short recipe using only: {', '.join(ingredient_names)}.
184
  Include:
 
186
  - One-sentence description
187
  - Ingredients list with quantities
188
  - 6-10 concise steps
189
+ - Optional tips
190
+ RETURN RESULT IN MARKDOWN FORMAT ONLY.
191
  """
192
+
193
  print("Trying Gemini...")
194
  response = model.generate_content(prompt)
195
  recipe_text = response.text.strip()
196
+ print("Gemini succeeded.")
197
+
198
  except Exception as e_gemini:
199
+ print("Gemini failed:", e_gemini)
 
200
  try:
201
+ recipe_text = generate_recipe_gemma(ingredient_names)
202
+ except Exception as e_local1:
203
+ print("Gemma local failed:", e_local1)
204
+ raise e_local1
205
 
206
  else:
 
207
  try:
208
+ print("No API key Using Gemma fallback.")
209
+ recipe_text = generate_recipe_gemma(ingredient_names)
210
+ except Exception as e_local2:
211
+ print("Gemma local failed:", e_local2)
212
+ raise e_local2
213
 
214
  return {"ingredients": ingredients, "recipe": recipe_text}
215
 
216
  except HTTPException:
 
217
  raise
218
  except Exception as e:
219
  traceback.print_exc()
220
+
221
+
 
222
  # Health check
223
  @app.get("/health")
224
  def health():
 
227
 
228
  # Run app
229
  if __name__ == "__main__":
230
+ uvicorn.run(app, host="0.0.0.0", port=7860)