TimInf commited on
Commit
f324ef3
·
verified ·
1 Parent(s): b968ba2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +122 -55
app.py CHANGED
@@ -6,6 +6,7 @@ import json
6
  from fastapi import FastAPI
7
  from fastapi.responses import JSONResponse
8
  from pydantic import BaseModel
 
9
 
10
  # Lade RecipeBERT Modell (für semantische Zutat-Kombination)
11
  bert_model_name = "alexdseo/RecipeBERT"
@@ -40,7 +41,6 @@ def get_embedding(text):
40
 
41
  def average_embedding(embedding_list):
42
  """Berechnet den Durchschnitt einer Liste von Embeddings"""
43
- # Sicherstellen, dass embedding_list Tupel von (Name, Embedding) enthält
44
  tensors = torch.stack([emb for _, emb in embedding_list])
45
  return tensors.mean(dim=0)
46
 
@@ -59,60 +59,121 @@ def get_cosine_similarity(vec1, vec2):
59
  return 0
60
  return dot_product / (norm_a * norm_b)
61
 
62
- def get_combined_scores(query_vector, embedding_list, all_good_embeddings, avg_weight=0.6):
63
- """Berechnet einen kombinierten Score unter Berücksichtigung der Ähnlichkeit zum Durchschnitt und zu einzelnen Zutaten"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  results = []
65
- for name, emb in embedding_list:
66
  avg_similarity = get_cosine_similarity(query_vector, emb)
67
  individual_similarities = [get_cosine_similarity(good_emb, emb)
68
  for _, good_emb in all_good_embeddings]
69
  avg_individual_similarity = sum(individual_similarities) / len(individual_similarities) if individual_similarities else 0
70
- combined_score = avg_weight * avg_similarity + (1 - avg_weight) * avg_individual_similarity
71
- results.append((name, emb, combined_score))
 
 
 
 
 
 
72
  results.sort(key=lambda x: x[2], reverse=True)
73
  return results
74
 
75
- # Die vollständige find_best_ingredients Funktion, die du bereitgestellt hast
76
- def find_best_ingredients(required_ingredients, available_ingredients, max_ingredients=6, avg_weight=0.6):
77
  """
78
- Findet die besten Zutaten basierend auf RecipeBERT Embeddings.
 
 
79
  """
80
- required_ingredients = list(set(required_ingredients))
81
- available_ingredients = list(set([i for i in available_ingredients if i not in required_ingredients]))
82
-
83
- if not required_ingredients and available_ingredients:
84
- random_ingredient = random.choice(available_ingredients)
85
- required_ingredients = [random_ingredient]
86
- available_ingredients = [i for i in available_ingredients if i != random_ingredient]
87
- print(f"No required ingredients provided. Randomly selected: {random_ingredient}")
88
-
89
- if not required_ingredients or len(required_ingredients) >= max_ingredients:
90
- return required_ingredients[:max_ingredients]
91
-
92
- if not available_ingredients:
93
- return required_ingredients
94
-
95
- embed_required = [(e, get_embedding(e)) for e in required_ingredients]
96
- embed_available = [(e, get_embedding(e)) for e in available_ingredients]
97
-
98
- num_to_add = min(max_ingredients - len(required_ingredients), len(available_ingredients))
99
-
100
- final_ingredients = embed_required.copy()
101
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  for _ in range(num_to_add):
103
- avg = average_embedding(final_ingredients)
104
- candidates = get_combined_scores(avg, embed_available, final_ingredients, avg_weight)
 
105
 
106
  if not candidates:
107
  break
108
 
109
- best_name, best_embedding, _ = candidates[0]
110
-
111
- final_ingredients.append((best_name, best_embedding))
 
 
112
 
113
- embed_available = [item for item in embed_available if item[0] != best_name]
114
-
115
- return [name for name, _ in final_ingredients]
 
 
116
 
117
  def skip_special_tokens(text, special_tokens):
118
  """Entfernt spezielle Tokens aus dem Text"""
@@ -219,17 +280,18 @@ def generate_recipe_with_t5(ingredients_list, max_retries=5):
219
  "directions": ["Fehler beim Generieren der Rezeptanweisungen"]
220
  }
221
 
222
- def process_recipe_request_logic(required_ingredients, available_ingredients, max_ingredients, max_retries):
223
  """
224
  Kernlogik zur Verarbeitung einer Rezeptgenerierungsanfrage.
 
225
  """
226
- if not required_ingredients and not available_ingredients:
227
  return {"error": "Keine Zutaten angegeben"}
228
  try:
 
229
  optimized_ingredients = find_best_ingredients(
230
- required_ingredients, available_ingredients, max_ingredients
231
  )
232
- # KORRIGIERT: Aufruf der echten T5-Generierungsfunktion
233
  recipe = generate_recipe_with_t5(optimized_ingredients, max_retries)
234
  result = {
235
  'title': recipe['title'],
@@ -239,34 +301,42 @@ def process_recipe_request_logic(required_ingredients, available_ingredients, ma
239
  }
240
  return result
241
  except Exception as e:
 
 
242
  return {"error": f"Fehler bei der Rezeptgenerierung: {str(e)}"}
243
 
244
  # --- FastAPI-Implementierung ---
245
- app = FastAPI(title="AI Recipe Generator API") # Ohne Gradio-spezifische Titelzusätze
 
 
 
 
 
 
246
 
247
  class RecipeRequest(BaseModel):
248
  required_ingredients: list[str] = []
249
- available_ingredients: list[str] = []
 
250
  max_ingredients: int = 7
251
  max_retries: int = 5
252
- # Optional: Für Abwärtskompatibilität, falls 'ingredients' als Top-Level-Feld gesendet wird
253
  ingredients: list[str] = []
254
 
255
- @app.post("/generate_recipe") # Der API-Endpunkt für Flutter
256
  async def generate_recipe_api(request_data: RecipeRequest):
257
  """
258
  Standard-REST-API-Endpunkt für die Flutter-App.
259
  Nimmt direkt JSON-Daten an und gibt direkt JSON zurück.
260
  """
261
- # Wenn required_ingredients leer ist, aber ingredients vorhanden sind,
262
- # verwende ingredients für Abwärtskompatibilität.
263
  final_required_ingredients = request_data.required_ingredients
264
  if not final_required_ingredients and request_data.ingredients:
265
  final_required_ingredients = request_data.ingredients
266
 
 
267
  result_dict = process_recipe_request_logic(
268
  final_required_ingredients,
269
- request_data.available_ingredients,
270
  request_data.max_ingredients,
271
  request_data.max_retries
272
  )
@@ -274,9 +344,6 @@ async def generate_recipe_api(request_data: RecipeRequest):
274
 
275
  @app.get("/")
276
  async def read_root():
277
- return {"message": "AI Recipe Generator API is running (FastAPI only)!"} # Angepasste Nachricht
278
-
279
- # Hier gibt es KEINEN Gradio-Mount oder Gradio-Launch-Befehl
280
- # Das `app` Objekt ist eine reine FastAPI-Instanz
281
- print("INFO: Pure FastAPI application script finished execution and defined 'app' variable.")
282
 
 
 
6
  from fastapi import FastAPI
7
  from fastapi.responses import JSONResponse
8
  from pydantic import BaseModel
9
+ from datetime import datetime, timedelta # Importieren für Datumsberechnungen
10
 
11
  # Lade RecipeBERT Modell (für semantische Zutat-Kombination)
12
  bert_model_name = "alexdseo/RecipeBERT"
 
41
 
42
  def average_embedding(embedding_list):
43
  """Berechnet den Durchschnitt einer Liste von Embeddings"""
 
44
  tensors = torch.stack([emb for _, emb in embedding_list])
45
  return tensors.mean(dim=0)
46
 
 
59
  return 0
60
  return dot_product / (norm_a * norm_b)
61
 
62
+ # NEUE FUNKTION: Berechnet den Altersbonus für eine Zutat
63
+ def calculate_age_bonus(date_added_str: str, category: str) -> float:
64
+ """
65
+ Berechnet einen prozentualen Bonus basierend auf dem Alter der Zutat.
66
+ - Standard: 0.5% pro Tag, max. 10%.
67
+ - Gemüse: 2.0% pro Tag, max. 10%.
68
+ """
69
+ try:
70
+ date_added = datetime.fromisoformat(date_added_str.replace('Z', '+00:00')) # Handle 'Z' for UTC
71
+ except ValueError:
72
+ print(f"Warning: Could not parse date_added_str: {date_added_str}. Returning 0 bonus.")
73
+ return 0.0
74
+
75
+ today = datetime.now()
76
+ days_since_added = (today - date_added).days
77
+
78
+ if days_since_added < 0: # Zutat aus der Zukunft? Ungültig.
79
+ return 0.0
80
+
81
+ if category and category.lower() == "vegetables":
82
+ daily_bonus = 0.02 # 2% pro Tag für Gemüse
83
+ else:
84
+ daily_bonus = 0.005 # 0.5% pro Tag für andere
85
+
86
+ bonus = days_since_added * daily_bonus
87
+ return min(bonus, 0.10) # Max 10% (0.10)
88
+
89
+ def get_combined_scores(query_vector, embedding_list_with_details, all_good_embeddings, avg_weight=0.6):
90
+ """
91
+ Berechnet einen kombinierten Score unter Berücksichtigung der Ähnlichkeit zum Durchschnitt und zu einzelnen Zutaten.
92
+ Jetzt inklusive Altersbonus.
93
+ embedding_list_with_details: Liste von Tupeln (Name, Embedding, DateAddedStr, Category)
94
+ """
95
  results = []
96
+ for name, emb, date_added_str, category in embedding_list_with_details:
97
  avg_similarity = get_cosine_similarity(query_vector, emb)
98
  individual_similarities = [get_cosine_similarity(good_emb, emb)
99
  for _, good_emb in all_good_embeddings]
100
  avg_individual_similarity = sum(individual_similarities) / len(individual_similarities) if individual_similarities else 0
101
+
102
+ base_combined_score = avg_weight * avg_similarity + (1 - avg_weight) * avg_individual_similarity
103
+
104
+ # NEU: Altersbonus hinzufügen
105
+ age_bonus = calculate_age_bonus(date_added_str, category)
106
+ final_combined_score = base_combined_score + age_bonus
107
+
108
+ results.append((name, emb, final_combined_score, date_added_str, category)) # Behalte Details für Debug oder zukünftige Nutzung
109
  results.sort(key=lambda x: x[2], reverse=True)
110
  return results
111
 
112
+ def find_best_ingredients(required_ingredients_names, available_ingredients_details, max_ingredients=6, avg_weight=0.6):
 
113
  """
114
+ Findet die besten Zutaten basierend auf RecipeBERT Embeddings, jetzt mit Alters- und Kategorie-Bonus.
115
+ required_ingredients_names: Liste von Strings (nur Namen)
116
+ available_ingredients_details: Liste von Dicts (Name, DateAdded, Category)
117
  """
118
+ required_ingredients_names = list(set(required_ingredients_names))
119
+
120
+ # Filtern der verfügbaren Zutaten, um sicherzustellen, dass keine Pflichtzutaten dabei sind
121
+ # und gleichzeitig die Details beibehalten
122
+ available_ingredients_filtered_details = [
123
+ item for item in available_ingredients_details
124
+ if item['name'] not in required_ingredients_names
125
+ ]
126
+
127
+ # Wenn keine Pflichtzutaten vorhanden sind, aber verfügbare, wähle eine zufällig als Pflichtzutat
128
+ if not required_ingredients_names and available_ingredients_filtered_details:
129
+ random_item = random.choice(available_ingredients_filtered_details)
130
+ required_ingredients_names = [random_item['name']]
131
+ # Entferne die zufällig gewählte Zutat aus den verfügbaren Details
132
+ available_ingredients_filtered_details = [
133
+ item for item in available_ingredients_filtered_details
134
+ if item['name'] != random_item['name']
135
+ ]
136
+ print(f"No required ingredients provided. Randomly selected: {required_ingredients_names[0]}")
137
+
138
+ if not required_ingredients_names or len(required_ingredients_names) >= max_ingredients:
139
+ return required_ingredients_names[:max_ingredients]
140
+
141
+ if not available_ingredients_filtered_details:
142
+ return required_ingredients_names
143
+
144
+ # Erstelle Embeddings für Pflichtzutaten (nur Name und Embedding)
145
+ embed_required = [(name, get_embedding(name)) for name in required_ingredients_names]
146
+
147
+ # Erstelle Embeddings für verfügbare Zutaten, inklusive ihrer Details
148
+ embed_available_with_details = [
149
+ (item['name'], get_embedding(item['name']), item['dateAdded'], item['category'])
150
+ for item in available_ingredients_filtered_details
151
+ ]
152
+
153
+ num_to_add = min(max_ingredients - len(required_ingredients_names), len(embed_available_with_details))
154
+
155
+ final_ingredients_with_embeddings = embed_required.copy() # (Name, Embedding)
156
+ final_ingredients_names = required_ingredients_names.copy() # Nur Namen zum Tracken der ausgewählten
157
+
158
  for _ in range(num_to_add):
159
+ avg = average_embedding(final_ingredients_with_embeddings)
160
+ # Sende die Liste mit den detaillierten Zutaten an get_combined_scores
161
+ candidates = get_combined_scores(avg, embed_available_with_details, final_ingredients_with_embeddings, avg_weight)
162
 
163
  if not candidates:
164
  break
165
 
166
+ best_name, best_embedding, best_score, _, _ = candidates[0] # Holen Sie den besten Kandidaten
167
+
168
+ # Füge nur den Namen und das Embedding zum final_ingredients_with_embeddings hinzu
169
+ final_ingredients_with_embeddings.append((best_name, best_embedding))
170
+ final_ingredients_names.append(best_name)
171
 
172
+ # Entferne den besten Kandidaten aus den verfügbaren
173
+ embed_available_with_details = [item for item in embed_available_with_details if item[0] != best_name]
174
+
175
+ return final_ingredients_names
176
+
177
 
178
  def skip_special_tokens(text, special_tokens):
179
  """Entfernt spezielle Tokens aus dem Text"""
 
280
  "directions": ["Fehler beim Generieren der Rezeptanweisungen"]
281
  }
282
 
283
+ def process_recipe_request_logic(required_ingredients, available_ingredients_details, max_ingredients, max_retries):
284
  """
285
  Kernlogik zur Verarbeitung einer Rezeptgenerierungsanfrage.
286
+ available_ingredients_details: Liste von Dicts (Name, DateAdded, Category)
287
  """
288
+ if not required_ingredients and not available_ingredients_details:
289
  return {"error": "Keine Zutaten angegeben"}
290
  try:
291
+ # Die find_best_ingredients Funktion erwartet jetzt die detaillierte Liste
292
  optimized_ingredients = find_best_ingredients(
293
+ required_ingredients, available_ingredients_details, max_ingredients
294
  )
 
295
  recipe = generate_recipe_with_t5(optimized_ingredients, max_retries)
296
  result = {
297
  'title': recipe['title'],
 
301
  }
302
  return result
303
  except Exception as e:
304
+ import traceback
305
+ traceback.print_exc() # Dies hilft bei der Fehlersuche im Log
306
  return {"error": f"Fehler bei der Rezeptgenerierung: {str(e)}"}
307
 
308
  # --- FastAPI-Implementierung ---
309
+ app = FastAPI(title="AI Recipe Generator API")
310
+
311
+ # NEU: Model für die empfangene Zutat mit Details
312
+ class IngredientDetail(BaseModel):
313
+ name: str
314
+ dateAdded: str # Muss ein String sein, da wir ihn als ISO 8601 empfangen
315
+ category: str
316
 
317
  class RecipeRequest(BaseModel):
318
  required_ingredients: list[str] = []
319
+ # NEU: available_ingredients ist jetzt eine Liste von IngredientDetail-Objekten
320
+ available_ingredients: list[IngredientDetail] = []
321
  max_ingredients: int = 7
322
  max_retries: int = 5
323
+ # Optional: Für Abwärtskompatibilität (kann entfernt werden, wenn nicht mehr benötigt)
324
  ingredients: list[str] = []
325
 
326
+ @app.post("/generate_recipe")
327
  async def generate_recipe_api(request_data: RecipeRequest):
328
  """
329
  Standard-REST-API-Endpunkt für die Flutter-App.
330
  Nimmt direkt JSON-Daten an und gibt direkt JSON zurück.
331
  """
 
 
332
  final_required_ingredients = request_data.required_ingredients
333
  if not final_required_ingredients and request_data.ingredients:
334
  final_required_ingredients = request_data.ingredients
335
 
336
+ # Jetzt die detaillierten available_ingredients an die Logik übergeben
337
  result_dict = process_recipe_request_logic(
338
  final_required_ingredients,
339
+ request_data.available_ingredients, # Hier ist die Liste der IngredientDetail-Objekte
340
  request_data.max_ingredients,
341
  request_data.max_retries
342
  )
 
344
 
345
  @app.get("/")
346
  async def read_root():
347
+ return {"message": "AI Recipe Generator API is running (FastAPI only)!"}
 
 
 
 
348
 
349
+ print("INFO: Pure FastAPI application script finished execution and defined 'app' variable.")