LiamKhoaLe commited on
Commit
543e178
·
1 Parent(s): f12a3b4

Upd inline img

Browse files
Files changed (2) hide show
  1. api/chatbot.py +88 -24
  2. search/engines/image.py +113 -11
api/chatbot.py CHANGED
@@ -18,7 +18,7 @@ class GeminiClient:
18
  logger.warning("FlashAPI not set - Gemini client will use fallback responses")
19
  self.client = None
20
  else:
21
- self.client = genai.Client(api_key=gemini_flash_api_key)
22
 
23
  def generate_content(self, prompt: str, model: str = "gemini-2.5-flash", temperature: float = 0.7) -> str:
24
  """Generate content using Gemini API"""
@@ -230,13 +230,18 @@ class CookingTutorChatbot:
230
  title = image.get('title', '')
231
  source_url = image.get('source_url', '')
232
  source = image.get('source', 'unknown')
 
 
 
 
 
233
 
234
  # Generate contextual alt text and caption
235
  alt_text = self._generate_image_alt_text(title, query, i)
236
  caption = self._generate_image_caption(title, query, i)
237
 
238
- # Determine image placement context
239
- placement_context = self._determine_image_placement(query, i)
240
 
241
  enhanced_image = {
242
  'id': f"img_{i+1}",
@@ -250,13 +255,27 @@ class CookingTutorChatbot:
250
  'display_order': i + 1,
251
  'aspect_ratio': '16:9', # Default, can be detected later
252
  'loading': 'lazy', # For performance
253
- 'type': 'cooking_image'
 
 
254
  }
255
 
256
  enhanced_images.append(enhanced_image)
257
 
258
  return enhanced_images
259
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  def _generate_image_alt_text(self, title: str, query: str, index: int) -> str:
261
  """Generate descriptive alt text for accessibility"""
262
  if title and len(title) > 10:
@@ -274,36 +293,72 @@ class CookingTutorChatbot:
274
  return f"Related cooking image {index + 1}"
275
 
276
  def _generate_image_caption(self, title: str, query: str, index: int) -> str:
277
- """Generate contextual caption for the image"""
278
  if title and len(title) > 5:
279
  return title
280
 
281
- # Generate contextual captions
282
  query_lower = query.lower()
283
- if 'pad thai' in query_lower:
284
- return f"Pad Thai cooking example {index + 1}"
285
- elif 'fusion' in query_lower:
286
- return f"Fusion cooking inspiration {index + 1}"
287
- elif 'western' in query_lower:
288
- return f"Western cooking technique {index + 1}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  else:
290
- return f"Related cooking example {index + 1}"
 
 
 
 
 
 
 
 
291
 
292
  def _determine_image_placement(self, query: str, index: int) -> str:
293
- """Determine where the image should be placed in the text"""
294
  query_lower = query.lower()
295
 
 
296
  if index == 0:
297
- if 'recipe' in query_lower or 'ingredient' in query_lower:
 
298
  return 'after_ingredients'
299
- elif 'technique' in query_lower or 'method' in query_lower:
300
  return 'after_technique_intro'
 
 
301
  else:
302
  return 'after_intro'
303
  elif index == 1:
 
304
  return 'after_instructions'
305
- else:
 
306
  return 'after_tips'
 
 
 
307
 
308
  def _integrate_images_inline(self, text: str, images: List[Dict]) -> str:
309
  """Integrate images inline with text using placeholders for frontend rendering"""
@@ -327,16 +382,25 @@ class CookingTutorChatbot:
327
  for line in lines:
328
  line_lower = line.lower().strip()
329
 
330
- # Detect section types
331
- if any(keyword in line_lower for keyword in ['ingredients:', 'ingredient list:', 'what you need:']):
 
 
 
332
  if current_section['content'].strip():
333
  sections.append(current_section)
334
  current_section = {'type': 'ingredients', 'content': line + '\n', 'images': []}
335
- elif any(keyword in line_lower for keyword in ['instructions:', 'directions:', 'how to cook:', 'steps:']):
 
 
 
336
  if current_section['content'].strip():
337
  sections.append(current_section)
338
  current_section = {'type': 'instructions', 'content': line + '\n', 'images': []}
339
- elif any(keyword in line_lower for keyword in ['tips:', 'troubleshooting:', 'notes:', 'variations:']):
 
 
 
340
  if current_section['content'].strip():
341
  sections.append(current_section)
342
  current_section = {'type': 'tips', 'content': line + '\n', 'images': []}
@@ -482,8 +546,8 @@ class CookingTutorChatbot:
482
  doc_id = extract_numeric_id(citation_id)
483
 
484
  if doc_id is not None and doc_id in url_mapping:
485
- url = url_mapping[doc_id]
486
- urls.append(f'<{url}>')
487
  logger.info(f"[CITATION] Replacing <#{citation_id}> with {url}")
488
  else:
489
  if doc_id is None:
@@ -506,7 +570,7 @@ class CookingTutorChatbot:
506
  # Process citations with this pattern
507
  processed_response = re.sub(pattern, replace_citation, processed_response)
508
  total_citations_processed += sum(len([id_str.strip() for id_str in citation_content.split(',')])
509
- for citation_content in citations_found)
510
  logger.info(f"[CITATION] Processed {len(citations_found)} citation groups with pattern: {pattern}")
511
 
512
  # Fallback: Handle any remaining malformed citations
 
18
  logger.warning("FlashAPI not set - Gemini client will use fallback responses")
19
  self.client = None
20
  else:
21
+ self.client = genai.Client(api_key=gemini_flash_api_key)
22
 
23
  def generate_content(self, prompt: str, model: str = "gemini-2.5-flash", temperature: float = 0.7) -> str:
24
  """Generate content using Gemini API"""
 
230
  title = image.get('title', '')
231
  source_url = image.get('source_url', '')
232
  source = image.get('source', 'unknown')
233
+ image_type = image.get('image_type', 'general')
234
+ query_context = image.get('query_context', 'general')
235
+
236
+ # Set current image type for caption generation
237
+ self._current_image_type = image_type
238
 
239
  # Generate contextual alt text and caption
240
  alt_text = self._generate_image_alt_text(title, query, i)
241
  caption = self._generate_image_caption(title, query, i)
242
 
243
+ # Determine image placement context based on image type
244
+ placement_context = self._determine_image_placement_by_type(image_type, query, i)
245
 
246
  enhanced_image = {
247
  'id': f"img_{i+1}",
 
255
  'display_order': i + 1,
256
  'aspect_ratio': '16:9', # Default, can be detected later
257
  'loading': 'lazy', # For performance
258
+ 'type': 'cooking_image',
259
+ 'image_type': image_type,
260
+ 'query_context': query_context
261
  }
262
 
263
  enhanced_images.append(enhanced_image)
264
 
265
  return enhanced_images
266
 
267
+ def _determine_image_placement_by_type(self, image_type: str, query: str, index: int) -> str:
268
+ """Determine image placement based on image type for optimal inline display"""
269
+ if image_type == 'ingredients':
270
+ return 'after_ingredients'
271
+ elif image_type == 'technique':
272
+ return 'after_instructions'
273
+ elif image_type == 'final_dish':
274
+ return 'after_tips'
275
+ else:
276
+ # Fallback to original logic
277
+ return self._determine_image_placement(query, index)
278
+
279
  def _generate_image_alt_text(self, title: str, query: str, index: int) -> str:
280
  """Generate descriptive alt text for accessibility"""
281
  if title and len(title) > 10:
 
293
  return f"Related cooking image {index + 1}"
294
 
295
  def _generate_image_caption(self, title: str, query: str, index: int) -> str:
296
+ """Generate contextual caption for the image based on image type"""
297
  if title and len(title) > 5:
298
  return title
299
 
300
+ # Generate contextual captions based on image type
301
  query_lower = query.lower()
302
+
303
+ # Check if we have image type information
304
+ image_type = getattr(self, '_current_image_type', 'general')
305
+
306
+ if image_type == 'ingredients':
307
+ if 'pad thai' in query_lower:
308
+ return "Fresh ingredients for Pad Thai"
309
+ elif 'fusion' in query_lower:
310
+ return "Ingredients for fusion cooking"
311
+ else:
312
+ return f"Fresh ingredients {index + 1}"
313
+ elif image_type == 'technique':
314
+ if 'pad thai' in query_lower:
315
+ return "Pad Thai cooking technique"
316
+ elif 'fusion' in query_lower:
317
+ return "Fusion cooking technique"
318
+ else:
319
+ return f"Cooking technique {index + 1}"
320
+ elif image_type == 'final_dish':
321
+ if 'pad thai' in query_lower:
322
+ return "Completed Pad Thai dish"
323
+ elif 'fusion' in query_lower:
324
+ return "Fusion cooking result"
325
+ else:
326
+ return f"Final dish {index + 1}"
327
  else:
328
+ # Fallback to original logic
329
+ if 'pad thai' in query_lower:
330
+ return f"Pad Thai cooking example {index + 1}"
331
+ elif 'fusion' in query_lower:
332
+ return f"Fusion cooking inspiration {index + 1}"
333
+ elif 'western' in query_lower:
334
+ return f"Western cooking technique {index + 1}"
335
+ else:
336
+ return f"Related cooking example {index + 1}"
337
 
338
  def _determine_image_placement(self, query: str, index: int) -> str:
339
+ """Determine where the image should be placed in the text for optimal inline display"""
340
  query_lower = query.lower()
341
 
342
+ # More intelligent placement based on content type and image index
343
  if index == 0:
344
+ # First image: place early in the content for immediate visual impact
345
+ if any(keyword in query_lower for keyword in ['ingredient', 'ingredients', 'what you need']):
346
  return 'after_ingredients'
347
+ elif any(keyword in query_lower for keyword in ['technique', 'method', 'how to']):
348
  return 'after_technique_intro'
349
+ elif any(keyword in query_lower for keyword in ['recipe', 'cook', 'make']):
350
+ return 'after_intro'
351
  else:
352
  return 'after_intro'
353
  elif index == 1:
354
+ # Second image: place in the middle of instructions
355
  return 'after_instructions'
356
+ elif index == 2:
357
+ # Third image: place after tips or at the end
358
  return 'after_tips'
359
+ else:
360
+ # Additional images: distribute evenly
361
+ return 'after_instructions'
362
 
363
  def _integrate_images_inline(self, text: str, images: List[Dict]) -> str:
364
  """Integrate images inline with text using placeholders for frontend rendering"""
 
382
  for line in lines:
383
  line_lower = line.lower().strip()
384
 
385
+ # Detect section types with more comprehensive patterns
386
+ if any(keyword in line_lower for keyword in [
387
+ 'ingredients:', 'ingredient list:', 'what you need:', 'materials:',
388
+ 'you will need:', 'ingredients list:', 'for this recipe:'
389
+ ]):
390
  if current_section['content'].strip():
391
  sections.append(current_section)
392
  current_section = {'type': 'ingredients', 'content': line + '\n', 'images': []}
393
+ elif any(keyword in line_lower for keyword in [
394
+ 'instructions:', 'directions:', 'how to cook:', 'steps:', 'method:',
395
+ 'cooking steps:', 'preparation:', 'how to make:', 'procedure:'
396
+ ]):
397
  if current_section['content'].strip():
398
  sections.append(current_section)
399
  current_section = {'type': 'instructions', 'content': line + '\n', 'images': []}
400
+ elif any(keyword in line_lower for keyword in [
401
+ 'tips:', 'troubleshooting:', 'notes:', 'variations:', 'suggestions:',
402
+ 'pro tips:', 'helpful hints:', 'cooking tips:', 'advice:'
403
+ ]):
404
  if current_section['content'].strip():
405
  sections.append(current_section)
406
  current_section = {'type': 'tips', 'content': line + '\n', 'images': []}
 
546
  doc_id = extract_numeric_id(citation_id)
547
 
548
  if doc_id is not None and doc_id in url_mapping:
549
+ url = url_mapping[doc_id]
550
+ urls.append(f'<{url}>')
551
  logger.info(f"[CITATION] Replacing <#{citation_id}> with {url}")
552
  else:
553
  if doc_id is None:
 
570
  # Process citations with this pattern
571
  processed_response = re.sub(pattern, replace_citation, processed_response)
572
  total_citations_processed += sum(len([id_str.strip() for id_str in citation_content.split(',')])
573
+ for citation_content in citations_found)
574
  logger.info(f"[CITATION] Processed {len(citations_found)} citation groups with pattern: {pattern}")
575
 
576
  # Fallback: Handle any remaining malformed citations
search/engines/image.py CHANGED
@@ -18,14 +18,17 @@ class ImageSearchEngine:
18
  self.timeout = timeout
19
 
20
  def search_cooking_images(self, query: str, num_results: int = 3, language: str = "en") -> List[Dict]:
21
- """Search for cooking-related images with robust error handling"""
22
  if not query or not query.strip():
23
  logger.warning("Empty query provided for image search")
24
  return []
25
 
26
- results = []
 
27
 
28
- # Try multiple image search strategies
 
 
29
  strategies = [
30
  self._search_google_images,
31
  self._search_bing_images,
@@ -34,25 +37,124 @@ class ImageSearchEngine:
34
 
35
  for strategy in strategies:
36
  try:
37
- strategy_results = strategy(query, num_results, language)
 
 
 
 
 
 
 
 
 
38
  if strategy_results:
39
  # Filter and validate results
40
  valid_results = self._validate_image_results(strategy_results)
41
  if valid_results:
42
- results.extend(valid_results)
43
  logger.info(f"Image search strategy found {len(valid_results)} valid results")
44
- if len(results) >= num_results:
45
  break
46
  except Exception as e:
47
  logger.warning(f"Image search strategy failed: {e}")
48
  continue
49
 
50
- # Remove duplicates and return
51
- unique_results = self._remove_duplicate_images(results)
52
- final_results = unique_results[:num_results]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
- logger.info(f"Image search completed: {len(final_results)} unique results from {len(results)} total")
55
- return final_results
56
 
57
  def _validate_image_results(self, results: List[Dict]) -> List[Dict]:
58
  """Validate and clean image results"""
 
18
  self.timeout = timeout
19
 
20
  def search_cooking_images(self, query: str, num_results: int = 3, language: str = "en") -> List[Dict]:
21
+ """Search for diverse cooking-related images including ingredients, techniques, and final dishes"""
22
  if not query or not query.strip():
23
  logger.warning("Empty query provided for image search")
24
  return []
25
 
26
+ # Generate diverse search queries for comprehensive visual coverage
27
+ search_queries = self._generate_diverse_cooking_queries(query, num_results)
28
 
29
+ all_results = []
30
+
31
+ # Try multiple image search strategies with diverse queries
32
  strategies = [
33
  self._search_google_images,
34
  self._search_bing_images,
 
37
 
38
  for strategy in strategies:
39
  try:
40
+ strategy_results = []
41
+ for search_query in search_queries:
42
+ query_results = strategy(search_query['query'], search_query['max_results'], language)
43
+ if query_results:
44
+ # Add query context to results
45
+ for result in query_results:
46
+ result['query_context'] = search_query['context']
47
+ result['image_type'] = search_query['type']
48
+ strategy_results.extend(query_results)
49
+
50
  if strategy_results:
51
  # Filter and validate results
52
  valid_results = self._validate_image_results(strategy_results)
53
  if valid_results:
54
+ all_results.extend(valid_results)
55
  logger.info(f"Image search strategy found {len(valid_results)} valid results")
56
+ if len(all_results) >= num_results * 2: # Get more to filter
57
  break
58
  except Exception as e:
59
  logger.warning(f"Image search strategy failed: {e}")
60
  continue
61
 
62
+ # Remove duplicates and prioritize diverse results
63
+ unique_results = self._remove_duplicate_images(all_results)
64
+ diverse_results = self._prioritize_diverse_images(unique_results, num_results)
65
+
66
+ logger.info(f"Image search completed: {len(diverse_results)} diverse results from {len(all_results)} total")
67
+ return diverse_results
68
+
69
+ def _generate_diverse_cooking_queries(self, original_query: str, num_results: int) -> List[Dict]:
70
+ """Generate diverse search queries for comprehensive cooking image coverage"""
71
+ queries = []
72
+
73
+ # Extract key cooking terms from the original query
74
+ query_lower = original_query.lower()
75
+
76
+ # 1. Final dish query (original focus)
77
+ final_dish_query = f"{original_query} final dish completed recipe"
78
+ queries.append({
79
+ 'query': final_dish_query,
80
+ 'context': 'final_dish',
81
+ 'type': 'final_dish',
82
+ 'max_results': max(1, num_results // 3)
83
+ })
84
+
85
+ # 2. Ingredients query
86
+ ingredients_query = f"{original_query} ingredients fresh raw materials"
87
+ queries.append({
88
+ 'query': ingredients_query,
89
+ 'context': 'ingredients',
90
+ 'type': 'ingredients',
91
+ 'max_results': max(1, num_results // 3)
92
+ })
93
+
94
+ # 3. Cooking technique/process query
95
+ technique_query = f"{original_query} cooking technique process step by step"
96
+ queries.append({
97
+ 'query': technique_query,
98
+ 'context': 'technique',
99
+ 'type': 'technique',
100
+ 'max_results': max(1, num_results // 3)
101
+ })
102
+
103
+ # Add more specific queries based on the original query content
104
+ if any(keyword in query_lower for keyword in ['pad thai', 'noodles', 'pasta']):
105
+ queries.append({
106
+ 'query': f"{original_query} noodle preparation cooking technique",
107
+ 'context': 'noodle_technique',
108
+ 'type': 'technique',
109
+ 'max_results': 1
110
+ })
111
+
112
+ if any(keyword in query_lower for keyword in ['fusion', 'western', 'technique']):
113
+ queries.append({
114
+ 'query': f"{original_query} fusion cooking western technique",
115
+ 'context': 'fusion_technique',
116
+ 'type': 'technique',
117
+ 'max_results': 1
118
+ })
119
+
120
+ return queries
121
+
122
+ def _prioritize_diverse_images(self, results: List[Dict], num_results: int) -> List[Dict]:
123
+ """Prioritize diverse image types for better visual instruction"""
124
+ # Group results by type
125
+ type_groups = {
126
+ 'final_dish': [],
127
+ 'ingredients': [],
128
+ 'technique': [],
129
+ 'other': []
130
+ }
131
+
132
+ for result in results:
133
+ image_type = result.get('image_type', 'other')
134
+ if image_type in type_groups:
135
+ type_groups[image_type].append(result)
136
+ else:
137
+ type_groups['other'].append(result)
138
+
139
+ # Select diverse results
140
+ diverse_results = []
141
+
142
+ # Prioritize: 1 final dish, 1 ingredients, 1 technique, then fill with others
143
+ if type_groups['final_dish']:
144
+ diverse_results.append(type_groups['final_dish'][0])
145
+ if type_groups['ingredients'] and len(diverse_results) < num_results:
146
+ diverse_results.append(type_groups['ingredients'][0])
147
+ if type_groups['technique'] and len(diverse_results) < num_results:
148
+ diverse_results.append(type_groups['technique'][0])
149
+
150
+ # Fill remaining slots with other results
151
+ all_remaining = []
152
+ for group in type_groups.values():
153
+ all_remaining.extend(group[1:]) # Skip first item (already used)
154
+
155
+ diverse_results.extend(all_remaining[:num_results - len(diverse_results)])
156
 
157
+ return diverse_results[:num_results]
 
158
 
159
  def _validate_image_results(self, results: List[Dict]) -> List[Dict]:
160
  """Validate and clean image results"""