522H0134-NguyenNhatHuy commited on
Commit
26a38e1
Β·
verified Β·
1 Parent(s): f21dc20

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +199 -170
app.py CHANGED
@@ -27,77 +27,87 @@ from langchain_core.chat_history import InMemoryChatMessageHistory
27
  from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
28
  from pydub import AudioSegment
29
  from pydub.utils import which
30
-
31
- # Local imports (assumed to be available)
32
- from args import get_parser
33
- from model import get_model
34
- from output_utils import prepare_output
35
 
36
  # ============== DEVICE CONFIG ==============
37
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
38
- map_loc = None if torch.cuda.is_available() else "cpu"
 
39
  logging.getLogger("pytube").setLevel(logging.ERROR)
40
 
41
- # ============== LOAD TRANSLATION MODELS ==============
42
- model_envit5_name = "VietAI/envit5-translation"
43
- try:
44
- tokenizer_envit5 = AutoTokenizer.from_pretrained(model_envit5_name)
45
- model_envit5 = AutoModelForSeq2SeqLM.from_pretrained(
46
- model_envit5_name,
47
- torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32
48
- ).to(device)
49
- pipe_envit5 = pipeline(
50
- "text2text-generation",
51
- model=model_envit5,
52
- tokenizer=tokenizer_envit5,
53
- device=0 if torch.cuda.is_available() else -1,
54
- max_new_tokens=512,
55
- do_sample=False
56
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  except Exception as e:
58
  print(f"Error loading Vietnamese model: {e}")
59
  pipe_envit5 = None
60
 
61
  models = {
62
- "Japanese": {"model_name": "Helsinki-NLP/opus-mt-en-jap"},
63
  "Chinese": {"model_name": "Helsinki-NLP/opus-mt-en-zh"}
64
  }
65
 
66
  for lang in models:
67
  try:
68
- tokenizer = AutoTokenizer.from_pretrained(models[lang]["model_name"])
69
- model = AutoModelForSeq2SeqLM.from_pretrained(
70
- models[lang]["model_name"],
71
- torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32
72
- ).to(device)
73
- models[lang]["pipe"] = pipeline(
74
- "translation",
75
- model=model,
76
- tokenizer=tokenizer,
77
- device=0 if torch.cuda.is_available() else -1,
78
- max_length=512,
79
- batch_size=4 if torch.cuda.is_available() else 1,
80
- truncation=True
81
- )
82
  except Exception as e:
83
  print(f"Error loading {lang} model: {e}")
84
  models[lang]["pipe"] = None
85
 
86
- # ============== LOAD CHATBOT MODEL ==============
87
- chatbot_tokenizer = AutoTokenizer.from_pretrained("bigscience/bloomz-560m")
88
  chatbot_model = AutoModelForCausalLM.from_pretrained(
89
- "bigscience/bloomz-560m",
90
- torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32
91
- ).to(device)
 
 
 
 
 
 
 
 
 
 
92
 
93
  chatbot_pipeline = pipeline(
94
  "text-generation",
95
  model=chatbot_model,
96
  tokenizer=chatbot_tokenizer,
97
- device=0 if torch.cuda.is_available() else -1,
98
- max_new_tokens=100,
99
  do_sample=True,
100
- temperature=0.6,
101
  top_p=0.9,
102
  pad_token_id=chatbot_tokenizer.eos_token_id,
103
  batch_size=1
@@ -107,7 +117,7 @@ llm = HuggingFacePipeline(pipeline=chatbot_pipeline)
107
  # LangChain Chatbot Setup
108
  prompt = ChatPromptTemplate.from_template("""
109
  You are a professional culinary assistant. You will answer the user's question directly based on the provided recipe.
110
- Do not repeat the recipe or question in your answer. Be concise.
111
 
112
  Dish: {title}
113
  Ingredients: {ingredients}
@@ -117,7 +127,6 @@ User Question: {question}
117
  Answer:
118
  """)
119
 
120
-
121
  chain = prompt | llm
122
  chat_histories = {}
123
 
@@ -136,7 +145,65 @@ chatbot_chain = RunnableWithMessageHistory(
136
  # ============== GLOBAL STATE ==============
137
  current_recipe_context = {"context": "", "title": "", "ingredients": [], "instructions": [], "image": None}
138
 
139
- # ============== RECIPE FORMAT & TRANSLATE ==============
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  def format_recipe(title, ingredients, instructions, lang):
141
  emoji = {"title": "🍽️", "ingredients": "πŸ§‚", "instructions": "πŸ“–"}
142
  titles = {
@@ -160,7 +227,8 @@ def format_recipe(title, ingredients, instructions, lang):
160
  result.extend([f"{i+1}. {step}" for i, step in enumerate(instructions)])
161
  return "\n".join(result)
162
 
163
- def translate_section(text, lang):
 
164
  if lang == "English (original)":
165
  return text
166
 
@@ -168,26 +236,13 @@ def translate_section(text, lang):
168
  if pipe_envit5 is None:
169
  return f"❗ Vietnamese translation model not available"
170
  try:
171
- max_chunk_length = 400
172
- if len(text) > max_chunk_length:
173
- sentences = text.split('. ')
174
- chunks = []
175
- current_chunk = ""
176
- for sentence in sentences:
177
- if len(current_chunk) + len(sentence) < max_chunk_length:
178
- current_chunk += sentence + ". "
179
- else:
180
- chunks.append(current_chunk)
181
- current_chunk = sentence + ". "
182
- if current_chunk:
183
- chunks.append(current_chunk)
184
- else:
185
- chunks = [text]
186
-
187
  translated_chunks = []
188
  for chunk in chunks:
189
  chunk = f"en-vi: {chunk}"
190
- translated = pipe_envit5(chunk, max_new_tokens=512)[0]["generated_text"]
191
  translated = translated.replace("vi: vi: ", "").replace("vi: Vi: ", "").replace("vi: ", "").strip()
192
  translated_chunks.append(translated)
193
 
@@ -200,25 +255,12 @@ def translate_section(text, lang):
200
  return f"❗ Translation model for {lang} not available"
201
 
202
  try:
203
- max_chunk_length = 400
204
- if len(text) > max_chunk_length:
205
- sentences = text.split('. ')
206
- chunks = []
207
- current_chunk = ""
208
- for sentence in sentences:
209
- if len(current_chunk) + len(sentence) < max_chunk_length:
210
- current_chunk += sentence + ". "
211
- else:
212
- chunks.append(current_chunk)
213
- current_chunk = sentence + ". "
214
- if current_chunk:
215
- chunks.append(current_chunk)
216
- else:
217
- chunks = [text]
218
-
219
  translated_chunks = []
220
  for chunk in chunks:
221
- translated = models[lang]["pipe"](chunk, max_length=512)[0]["translation_text"]
222
  translated_chunks.append(translated)
223
 
224
  return " ".join(translated_chunks)
@@ -229,9 +271,9 @@ def translate_section(text, lang):
229
  def translate_recipe(lang):
230
  if not current_recipe_context["title"]:
231
  return "❗ Please generate a recipe from an image first."
232
- title = translate_section(current_recipe_context["title"], lang)
233
- ingrs = [translate_section(i, lang) for i in current_recipe_context["ingredients"]]
234
- instrs = [translate_section(s, lang) for s in current_recipe_context["instructions"]]
235
  return format_recipe(title, ingrs, instrs, lang)
236
 
237
  # ============== NUTRITION ANALYSIS ==============
@@ -239,58 +281,60 @@ def nutrition_analysis(ingredient_input):
239
  ingredients = " ".join(ingredient_input.strip().split())
240
  api_url = f'https://api.api-ninjas.com/v1/nutrition?query={ingredients}'
241
  headers = {'X-Api-Key': 'AHVy+tpkUoueBNdaFs9nCg==sFZTMRn8ikZVzx6E'}
242
- response = requests.get(api_url, headers=headers)
243
- if response.status_code != 200:
244
- return "❌ API error or quota exceeded.", None, None, None
245
- data = response.json()
246
- df = pd.DataFrame(data)
247
- numeric_cols = []
248
- for col in df.columns:
249
- if col == "name":
250
- continue
251
- df[col] = pd.to_numeric(df[col], errors="coerce")
252
- if df[col].notna().sum() > 0:
253
- numeric_cols.append(col)
254
- if df.empty or len(numeric_cols) < 3:
255
- return "⚠️ Insufficient numerical data for charts (need at least 3 metrics).", None, None, None
256
- draw_cols = numeric_cols[:3]
257
- fig_bar = px.bar(df, x="name", y=draw_cols[0], title=f"Bar Chart: {draw_cols[0]}", text_auto=True)
258
- pie_data = df[[draw_cols[1], "name"]].dropna()
259
- if pie_data[draw_cols[1]].sum() > 0:
260
- fig_pie = px.pie(pie_data, names="name", values=draw_cols[1], title=f"Pie Chart: {draw_cols[1]}")
261
- else:
262
- fig_pie = px.bar(title="⚠️ Insufficient data for pie chart")
263
- fig_line = px.line(df, x="name", y=draw_cols[2], markers=True, title=f"Line Chart: {draw_cols[2]}")
264
- return "βœ… Analysis successful!", fig_bar, fig_pie, fig_line
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
  def load_recipe_ingredients():
267
  if not current_recipe_context["ingredients"]:
268
  return "⚠️ No ingredients available. Generate a recipe first."
269
  return "\n".join(current_recipe_context["ingredients"])
270
 
271
- # ============== CHATBOT ==============
272
  def clean_response(response):
273
- # Remove everything before "Answer:" if present
274
  if "Answer:" in response:
275
  response = response.split("Answer:")[-1]
276
-
277
- # Remove potential repetitions of Dish, Ingredients, Instructions
278
  response = re.sub(r"Dish:.*?(Ingredients:|Instructions:).*?", "", response, flags=re.DOTALL)
279
  response = re.sub(r"Ingredients:.*?(Instructions:).*?", "", response, flags=re.DOTALL)
280
  response = re.sub(r"Instructions:.*", "", response, flags=re.DOTALL)
281
-
282
- # Remove redundant system info
283
  response = re.sub(r"You are a professional culinary assistant.*?Answer:", "", response, flags=re.DOTALL)
284
-
285
- # Remove duplicate user question inside response (very common in these LLM outputs)
286
  response = re.sub(r"User Question:.*", "", response, flags=re.DOTALL)
287
-
288
- # Final strip + cleanup
289
  return response.strip()
290
 
291
-
292
  def validate_cooking_time(question, instructions):
293
- # Extract cooking times from instructions
294
  time_pattern = r"(\d+)\s*(minutes|minute)"
295
  total_time = 0
296
  for instr in instructions:
@@ -298,7 +342,6 @@ def validate_cooking_time(question, instructions):
298
  for match in matches:
299
  total_time += int(match[0])
300
 
301
- # Check if user question contains a time
302
  user_time = re.search(time_pattern, question)
303
  if user_time:
304
  user_minutes = int(user_time.group(1))
@@ -310,7 +353,6 @@ def generate_chat_response(message, session_id="default"):
310
  if not current_recipe_context["title"]:
311
  return "Please generate a recipe from an image before asking about the dish."
312
 
313
- # Validate cooking time if relevant
314
  correction = validate_cooking_time(message, current_recipe_context["instructions"])
315
 
316
  response = chatbot_chain.invoke(
@@ -329,7 +371,6 @@ def generate_chat_response(message, session_id="default"):
329
 
330
  return response.strip()
331
 
332
-
333
  def chat_with_bot(message, chat_history, session_id="default"):
334
  if not message.strip():
335
  return "", chat_history
@@ -338,45 +379,40 @@ def chat_with_bot(message, chat_history, session_id="default"):
338
  chat_history.append({"role": "assistant", "content": response})
339
  return "", chat_history
340
 
341
- # ============== IMAGE TO RECIPE ==============
342
- with open("ingr_vocab.pkl", 'rb') as f:
343
- ingrs_vocab = pickle.load(f)
344
- with open("instr_vocab.pkl", 'rb') as f:
345
- vocab = pickle.load(f)
346
-
347
- args = get_parser()
348
- args.maxseqlen = 15
349
- args.ingrs_only = False
350
- model_ic = get_model(args, len(ingrs_vocab), len(vocab))
351
- model_ic.load_state_dict(torch.load("modelbest.ckpt", map_location=map_loc, weights_only=True))
352
- model_ic.to(device).eval()
353
-
354
- transform = transforms.Compose([
355
- transforms.Resize(256),
356
- transforms.CenterCrop(224),
357
- transforms.ToTensor(),
358
- transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
359
- ])
360
-
361
- def generate_recipe(image):
362
  if image is None:
363
  return "❗ Please upload an image."
 
364
  current_recipe_context["image"] = image
365
- image = transform(image.convert("RGB")).unsqueeze(0).to(device)
366
- with torch.no_grad():
367
- outputs = model_ic.sample(image, greedy=True, temperature=1.0, beam=-1, true_ingrs=None)
368
- ids = (outputs['ingr_ids'].cpu().numpy(), outputs['recipe_ids'].cpu().numpy())
369
- outs, valid = prepare_output(ids[1][0], ids[0][0], ingrs_vocab, vocab)
370
- if not valid['is_valid']:
371
- return f"❌ Invalid recipe: {valid['reason']}"
372
- current_recipe_context.update({
373
- "title": outs['title'],
374
- "ingredients": outs['ingrs'],
375
- "instructions": outs['recipe']
376
- })
377
- return format_recipe(outs['title'], outs['ingrs'], outs['recipe'], "English (original)")
378
-
379
- # ============== GOOGLE TTS ==============
 
 
 
 
 
 
 
 
 
 
 
 
380
  languages_tts = {
381
  "English": "en",
382
  "Chinese": "zh-CN",
@@ -407,22 +443,18 @@ def google_tts(text, lang):
407
  if not text or text.startswith("❗"):
408
  return None, gr.update(visible=False)
409
 
410
- # Clean text for TTS
411
  clean_text = text.replace("**", "").replace("###", "").replace("- ", "")
412
  for emoji in ["🍽️", "πŸ§‚", "πŸ“–"]:
413
  clean_text = clean_text.replace(emoji, "")
414
 
415
- # Split into chunks (Google TTS max ~200 chars)
416
- max_chunk_length = 200
417
  chunks = [clean_text[i:i+max_chunk_length] for i in range(0, len(clean_text), max_chunk_length)]
418
  if not chunks:
419
  return None, gr.update(visible=False)
420
 
421
- # Fetch audio chunks asynchronously
422
  lang_code = languages_tts.get(lang, "en")
423
  audio_contents = asyncio.run(fetch_all_tts_audio(chunks, lang_code))
424
 
425
- # Filter out failed requests
426
  audio_files = []
427
  for i, content in enumerate(audio_contents):
428
  if content:
@@ -433,7 +465,6 @@ def google_tts(text, lang):
433
  if not audio_files:
434
  return None, gr.update(visible=False)
435
 
436
- # Combine audio if FFmpeg is available
437
  if len(audio_files) == 1:
438
  return audio_files[0], gr.update(visible=True)
439
 
@@ -449,7 +480,6 @@ def google_tts(text, lang):
449
  return output_file, gr.update(visible=True)
450
  except Exception as e:
451
  print(f"Error combining audio files: {e}")
452
- # Fallback to first chunk
453
  for i in range(1, len(audio_files)):
454
  os.unlink(audio_files[i])
455
  return audio_files[0], gr.update(visible=True)
@@ -546,7 +576,7 @@ with gr.Blocks(theme=gr.themes.Soft(), title="AI Recipe Generator") as demo:
546
  save_pdf_btn = gr.Button("Save as PDF", variant="secondary", elem_id="action-btn")
547
  pdf_output = gr.File(label="Download Recipe PDF", interactive=False)
548
  recipe_output = gr.Markdown("### Your recipe will appear here", elem_classes="recipe-box")
549
- gen_btn.click(generate_recipe, inputs=image_input, outputs=recipe_output)
550
  save_pdf_btn.click(fn=generate_pdf_recipe, outputs=[pdf_output, recipe_output])
551
 
552
  with gr.Tab("🌍 Translate & TTS"):
@@ -656,5 +686,4 @@ with gr.Blocks(theme=gr.themes.Soft(), title="AI Recipe Generator") as demo:
656
  """
657
 
658
  if __name__ == "__main__":
659
- demo.launch()
660
-
 
27
  from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
28
  from pydub import AudioSegment
29
  from pydub.utils import which
30
+ from functools import lru_cache
31
+ import onnxruntime as ort
 
 
 
32
 
33
  # ============== DEVICE CONFIG ==============
34
+ device = torch.device("cpu") # Force CPU usage
35
+ map_loc = "cpu"
36
+ torch.set_num_threads(1) # Reduce thread contention
37
  logging.getLogger("pytube").setLevel(logging.ERROR)
38
 
39
+ # ============== LOAD TRANSLATION MODELS (OPTIMIZED) ==============
40
+ def load_translation_model(model_name, task="translation"):
41
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
42
+ model = AutoModelForSeq2SeqLM.from_pretrained(
43
+ model_name,
44
+ torch_dtype=torch.float32,
 
 
 
 
 
 
 
 
 
45
  )
46
+
47
+ # Apply dynamic quantization
48
+ model = torch.quantization.quantize_dynamic(
49
+ model,
50
+ {torch.nn.Linear},
51
+ dtype=torch.qint8
52
+ )
53
+
54
+ model.eval()
55
+ model.to('cpu')
56
+
57
+ return pipeline(
58
+ task,
59
+ model=model,
60
+ tokenizer=tokenizer,
61
+ device=-1,
62
+ max_length=256,
63
+ batch_size=1,
64
+ truncation=True
65
+ )
66
+
67
+ # Load models with optimizations
68
+ try:
69
+ pipe_envit5 = load_translation_model("VietAI/envit5-translation", "text2text-generation")
70
  except Exception as e:
71
  print(f"Error loading Vietnamese model: {e}")
72
  pipe_envit5 = None
73
 
74
  models = {
75
+ "Japanese": {"model_name": "Helsinki-NLP/opus-mt-en-ja"}, # Smaller model
76
  "Chinese": {"model_name": "Helsinki-NLP/opus-mt-en-zh"}
77
  }
78
 
79
  for lang in models:
80
  try:
81
+ models[lang]["pipe"] = load_translation_model(models[lang]["model_name"])
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  except Exception as e:
83
  print(f"Error loading {lang} model: {e}")
84
  models[lang]["pipe"] = None
85
 
86
+ # ============== LOAD CHATBOT MODEL (OPTIMIZED) ==============
87
+ chatbot_tokenizer = AutoTokenizer.from_pretrained("microsoft/phi-2", trust_remote_code=True)
88
  chatbot_model = AutoModelForCausalLM.from_pretrained(
89
+ "microsoft/phi-2",
90
+ torch_dtype=torch.float32,
91
+ trust_remote_code=True
92
+ )
93
+
94
+ # Apply quantization
95
+ chatbot_model = torch.quantization.quantize_dynamic(
96
+ chatbot_model,
97
+ {torch.nn.Linear},
98
+ dtype=torch.qint8
99
+ )
100
+
101
+ chatbot_model.to('cpu').eval()
102
 
103
  chatbot_pipeline = pipeline(
104
  "text-generation",
105
  model=chatbot_model,
106
  tokenizer=chatbot_tokenizer,
107
+ device=-1,
108
+ max_new_tokens=80,
109
  do_sample=True,
110
+ temperature=0.7,
111
  top_p=0.9,
112
  pad_token_id=chatbot_tokenizer.eos_token_id,
113
  batch_size=1
 
117
  # LangChain Chatbot Setup
118
  prompt = ChatPromptTemplate.from_template("""
119
  You are a professional culinary assistant. You will answer the user's question directly based on the provided recipe.
120
+ Be concise and helpful.
121
 
122
  Dish: {title}
123
  Ingredients: {ingredients}
 
127
  Answer:
128
  """)
129
 
 
130
  chain = prompt | llm
131
  chat_histories = {}
132
 
 
145
  # ============== GLOBAL STATE ==============
146
  current_recipe_context = {"context": "", "title": "", "ingredients": [], "instructions": [], "image": None}
147
 
148
+ # ============== RECIPE MODEL (OPTIMIZED) ==============
149
+ with open("ingr_vocab.pkl", 'rb') as f:
150
+ ingrs_vocab = pickle.load(f)
151
+ with open("instr_vocab.pkl", 'rb') as f:
152
+ vocab = pickle.load(f)
153
+
154
+ # Optimized transform with smaller image size
155
+ transform = transforms.Compose([
156
+ transforms.Resize(128),
157
+ transforms.CenterCrop(112),
158
+ transforms.ToTensor(),
159
+ transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
160
+ ])
161
+
162
+ # Load model with optimizations
163
+ def get_model_optimized(args, ingr_vocab_size, instr_vocab_size):
164
+ from model import get_model # Local import to avoid circular dependency
165
+ model = get_model(args, ingr_vocab_size, instr_vocab_size)
166
+ model.load_state_dict(torch.load("modelbest.ckpt", map_location="cpu"))
167
+
168
+ # Apply optimizations
169
+ model = torch.jit.script(model) # TorchScript compilation
170
+ model = model.to('cpu').eval()
171
+
172
+ # Apply dynamic quantization
173
+ model = torch.quantization.quantize_dynamic(
174
+ model,
175
+ {torch.nn.Linear},
176
+ dtype=torch.qint8
177
+ )
178
+
179
+ return model
180
+
181
+ # Initialize model
182
+ args = type('', (), {})() # Simple args object
183
+ args.maxseqlen = 15
184
+ args.ingrs_only = False
185
+ model_ic = get_model_optimized(args, len(ingrs_vocab), len(vocab))
186
+
187
+ # Convert to ONNX for faster inference
188
+ def convert_to_onnx():
189
+ if not os.path.exists("modelbest.onnx"):
190
+ dummy_input = torch.randn(1, 3, 112, 112).to('cpu')
191
+ torch.onnx.export(
192
+ model_ic,
193
+ dummy_input,
194
+ "modelbest.onnx",
195
+ export_params=True,
196
+ opset_version=11,
197
+ do_constant_folding=True,
198
+ input_names=['input'],
199
+ output_names=['output'],
200
+ dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}}
201
+ )
202
+ return ort.InferenceSession("modelbest.onnx", providers=['CPUExecutionProvider'])
203
+
204
+ ort_session = convert_to_onnx()
205
+
206
+ # ============== RECIPE FUNCTIONS ==============
207
  def format_recipe(title, ingredients, instructions, lang):
208
  emoji = {"title": "🍽️", "ingredients": "πŸ§‚", "instructions": "πŸ“–"}
209
  titles = {
 
227
  result.extend([f"{i+1}. {step}" for i, step in enumerate(instructions)])
228
  return "\n".join(result)
229
 
230
+ @lru_cache(maxsize=32)
231
+ def translate_section_cached(text, lang):
232
  if lang == "English (original)":
233
  return text
234
 
 
236
  if pipe_envit5 is None:
237
  return f"❗ Vietnamese translation model not available"
238
  try:
239
+ max_chunk_length = 300 # Reduced from 400
240
+ chunks = [text[i:i+max_chunk_length] for i in range(0, len(text), max_chunk_length)]
241
+
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  translated_chunks = []
243
  for chunk in chunks:
244
  chunk = f"en-vi: {chunk}"
245
+ translated = pipe_envit5(chunk, max_new_tokens=256)[0]["generated_text"] # Reduced tokens
246
  translated = translated.replace("vi: vi: ", "").replace("vi: Vi: ", "").replace("vi: ", "").strip()
247
  translated_chunks.append(translated)
248
 
 
255
  return f"❗ Translation model for {lang} not available"
256
 
257
  try:
258
+ max_chunk_length = 300 # Reduced from 400
259
+ chunks = [text[i:i+max_chunk_length] for i in range(0, len(text), max_chunk_length)]
260
+
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  translated_chunks = []
262
  for chunk in chunks:
263
+ translated = models[lang]["pipe"](chunk, max_length=256)[0]["translation_text"] # Reduced length
264
  translated_chunks.append(translated)
265
 
266
  return " ".join(translated_chunks)
 
271
  def translate_recipe(lang):
272
  if not current_recipe_context["title"]:
273
  return "❗ Please generate a recipe from an image first."
274
+ title = translate_section_cached(current_recipe_context["title"], lang)
275
+ ingrs = [translate_section_cached(i, lang) for i in current_recipe_context["ingredients"]]
276
+ instrs = [translate_section_cached(s, lang) for s in current_recipe_context["instructions"]]
277
  return format_recipe(title, ingrs, instrs, lang)
278
 
279
  # ============== NUTRITION ANALYSIS ==============
 
281
  ingredients = " ".join(ingredient_input.strip().split())
282
  api_url = f'https://api.api-ninjas.com/v1/nutrition?query={ingredients}'
283
  headers = {'X-Api-Key': 'AHVy+tpkUoueBNdaFs9nCg==sFZTMRn8ikZVzx6E'}
284
+ try:
285
+ response = requests.get(api_url, headers=headers, timeout=10)
286
+ if response.status_code != 200:
287
+ return "❌ API error or quota exceeded.", None, None, None
288
+
289
+ data = response.json()
290
+ if not data:
291
+ return "⚠️ No nutrition data found.", None, None, None
292
+
293
+ df = pd.DataFrame(data)
294
+ numeric_cols = []
295
+ for col in df.columns:
296
+ if col == "name":
297
+ continue
298
+ df[col] = pd.to_numeric(df[col], errors="coerce")
299
+ if df[col].notna().sum() > 0:
300
+ numeric_cols.append(col)
301
+
302
+ if df.empty or len(numeric_cols) < 3:
303
+ return "⚠️ Insufficient numerical data for charts.", None, None, None
304
+
305
+ draw_cols = numeric_cols[:3]
306
+ fig_bar = px.bar(df, x="name", y=draw_cols[0], title=f"Bar Chart: {draw_cols[0]}", text_auto=True)
307
+
308
+ pie_data = df[[draw_cols[1], "name"]].dropna()
309
+ if pie_data[draw_cols[1]].sum() > 0:
310
+ fig_pie = px.pie(pie_data, names="name", values=draw_cols[1], title=f"Pie Chart: {draw_cols[1]}")
311
+ else:
312
+ fig_pie = px.bar(title="⚠️ Insufficient data for pie chart")
313
+
314
+ fig_line = px.line(df, x="name", y=draw_cols[2], markers=True, title=f"Line Chart: {draw_cols[2]}")
315
+ return "βœ… Analysis successful!", fig_bar, fig_pie, fig_line
316
+
317
+ except Exception as e:
318
+ print(f"Nutrition analysis error: {e}")
319
+ return "❌ Error during nutrition analysis.", None, None, None
320
 
321
  def load_recipe_ingredients():
322
  if not current_recipe_context["ingredients"]:
323
  return "⚠️ No ingredients available. Generate a recipe first."
324
  return "\n".join(current_recipe_context["ingredients"])
325
 
326
+ # ============== CHATBOT FUNCTIONS ==============
327
  def clean_response(response):
 
328
  if "Answer:" in response:
329
  response = response.split("Answer:")[-1]
 
 
330
  response = re.sub(r"Dish:.*?(Ingredients:|Instructions:).*?", "", response, flags=re.DOTALL)
331
  response = re.sub(r"Ingredients:.*?(Instructions:).*?", "", response, flags=re.DOTALL)
332
  response = re.sub(r"Instructions:.*", "", response, flags=re.DOTALL)
 
 
333
  response = re.sub(r"You are a professional culinary assistant.*?Answer:", "", response, flags=re.DOTALL)
 
 
334
  response = re.sub(r"User Question:.*", "", response, flags=re.DOTALL)
 
 
335
  return response.strip()
336
 
 
337
  def validate_cooking_time(question, instructions):
 
338
  time_pattern = r"(\d+)\s*(minutes|minute)"
339
  total_time = 0
340
  for instr in instructions:
 
342
  for match in matches:
343
  total_time += int(match[0])
344
 
 
345
  user_time = re.search(time_pattern, question)
346
  if user_time:
347
  user_minutes = int(user_time.group(1))
 
353
  if not current_recipe_context["title"]:
354
  return "Please generate a recipe from an image before asking about the dish."
355
 
 
356
  correction = validate_cooking_time(message, current_recipe_context["instructions"])
357
 
358
  response = chatbot_chain.invoke(
 
371
 
372
  return response.strip()
373
 
 
374
  def chat_with_bot(message, chat_history, session_id="default"):
375
  if not message.strip():
376
  return "", chat_history
 
379
  chat_history.append({"role": "assistant", "content": response})
380
  return "", chat_history
381
 
382
+ # ============== IMAGE TO RECIPE (OPTIMIZED) ==============
383
+ def generate_recipe_with_progress(image, progress=gr.Progress()):
384
+ progress(0.1, desc="Preprocessing image...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
  if image is None:
386
  return "❗ Please upload an image."
387
+
388
  current_recipe_context["image"] = image
389
+ image_tensor = transform(image.convert("RGB")).unsqueeze(0).numpy()
390
+
391
+ progress(0.3, desc="Running model inference...")
392
+ try:
393
+ inputs = {'input': image_tensor}
394
+ outputs = ort_session.run(None, inputs)
395
+
396
+ progress(0.7, desc="Processing results...")
397
+ ids = (outputs[0], outputs[1]) # Adjust based on actual ONNX output
398
+ outs, valid = prepare_output(ids[1][0], ids[0][0], ingrs_vocab, vocab)
399
+
400
+ if not valid['is_valid']:
401
+ return f"❌ Invalid recipe: {valid['reason']}"
402
+
403
+ current_recipe_context.update({
404
+ "title": outs['title'],
405
+ "ingredients": outs['ingrs'],
406
+ "instructions": outs['recipe']
407
+ })
408
+
409
+ progress(0.9, desc="Formatting output...")
410
+ return format_recipe(outs['title'], outs['ingrs'], outs['recipe'], "English (original)")
411
+ except Exception as e:
412
+ print(f"Recipe generation error: {e}")
413
+ return f"❌ Error generating recipe: {str(e)}"
414
+
415
+ # ============== TTS FUNCTIONS ==============
416
  languages_tts = {
417
  "English": "en",
418
  "Chinese": "zh-CN",
 
443
  if not text or text.startswith("❗"):
444
  return None, gr.update(visible=False)
445
 
 
446
  clean_text = text.replace("**", "").replace("###", "").replace("- ", "")
447
  for emoji in ["🍽️", "πŸ§‚", "πŸ“–"]:
448
  clean_text = clean_text.replace(emoji, "")
449
 
450
+ max_chunk_length = 150 # Reduced from 200
 
451
  chunks = [clean_text[i:i+max_chunk_length] for i in range(0, len(clean_text), max_chunk_length)]
452
  if not chunks:
453
  return None, gr.update(visible=False)
454
 
 
455
  lang_code = languages_tts.get(lang, "en")
456
  audio_contents = asyncio.run(fetch_all_tts_audio(chunks, lang_code))
457
 
 
458
  audio_files = []
459
  for i, content in enumerate(audio_contents):
460
  if content:
 
465
  if not audio_files:
466
  return None, gr.update(visible=False)
467
 
 
468
  if len(audio_files) == 1:
469
  return audio_files[0], gr.update(visible=True)
470
 
 
480
  return output_file, gr.update(visible=True)
481
  except Exception as e:
482
  print(f"Error combining audio files: {e}")
 
483
  for i in range(1, len(audio_files)):
484
  os.unlink(audio_files[i])
485
  return audio_files[0], gr.update(visible=True)
 
576
  save_pdf_btn = gr.Button("Save as PDF", variant="secondary", elem_id="action-btn")
577
  pdf_output = gr.File(label="Download Recipe PDF", interactive=False)
578
  recipe_output = gr.Markdown("### Your recipe will appear here", elem_classes="recipe-box")
579
+ gen_btn.click(generate_recipe_with_progress, inputs=image_input, outputs=recipe_output)
580
  save_pdf_btn.click(fn=generate_pdf_recipe, outputs=[pdf_output, recipe_output])
581
 
582
  with gr.Tab("🌍 Translate & TTS"):
 
686
  """
687
 
688
  if __name__ == "__main__":
689
+ demo.launch()