TimInf commited on
Commit
c59cc6c
·
verified ·
1 Parent(s): 823cad9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +86 -398
app.py CHANGED
@@ -1,473 +1,161 @@
1
- import gradio as gr
2
- from transformers import FlaxAutoModelForSeq2SeqLM, AutoTokenizer, AutoModel
3
  import torch
4
  import numpy as np
5
  import random
6
  import json
7
- from fastapi import FastAPI, Request
8
  from fastapi.responses import JSONResponse
9
  from pydantic import BaseModel
10
 
11
- # Lade RecipeBERT Modell (für semantische Zutat-Kombination)
12
  bert_model_name = "alexdseo/RecipeBERT"
13
  bert_tokenizer = AutoTokenizer.from_pretrained(bert_model_name)
14
  bert_model = AutoModel.from_pretrained(bert_model_name)
15
  bert_model.eval() # Setze das Modell in den Evaluationsmodus
16
 
17
- # Lade T5 Rezeptgenerierungsmodell
18
- MODEL_NAME_OR_PATH = "flax-community/t5-recipe-generation"
19
- t5_tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME_OR_PATH, use_fast=True)
20
- t5_model = FlaxAutoModelForSeq2SeqLM.from_pretrained(MODEL_NAME_OR_PATH)
21
-
22
- # Token Mapping für die T5 Modell-Ausgabe
23
- special_tokens = t5_tokenizer.all_special_tokens
24
- tokens_map = {
25
- "<sep>": "--",
26
- "<section>": "\n"
27
- }
28
 
 
29
  def get_embedding(text):
30
- """Berechnet das Embedding für einen Text mit Mean Pooling über alle Tokens"""
31
  inputs = bert_tokenizer(text, return_tensors="pt", truncation=True, padding=True)
32
  with torch.no_grad():
33
  outputs = bert_model(**inputs)
34
-
35
- # Mean Pooling - Mittelwert aller Token-Embeddings
36
  attention_mask = inputs['attention_mask']
37
  token_embeddings = outputs.last_hidden_state
38
  input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
39
  sum_embeddings = torch.sum(token_embeddings * input_mask_expanded, 1)
40
  sum_mask = torch.clamp(input_mask_expanded.sum(1), min=1e-9)
41
-
42
  return (sum_embeddings / sum_mask).squeeze(0)
43
 
44
  def average_embedding(embedding_list):
45
- """Berechnet den Durchschnitt einer Liste von Embeddings"""
46
- # Sicherstellen, dass embedding_list Tupel von (Name, Embedding) enthält
47
- tensors = torch.stack([emb for _, emb in embedding_list])
48
  return tensors.mean(dim=0)
49
 
50
  def get_cosine_similarity(vec1, vec2):
51
- """Berechnet die Cosinus-Ähnlichkeit zwischen zwei Vektoren"""
52
- if torch.is_tensor(vec1):
53
- vec1 = vec1.detach().numpy()
54
- if torch.is_tensor(vec2):
55
- vec2 = vec2.detach().numpy()
56
-
57
- # Stelle sicher, dass die Vektoren die richtige Form haben (flachen sie bei Bedarf ab)
58
  vec1 = vec1.flatten()
59
  vec2 = vec2.flatten()
60
-
61
  dot_product = np.dot(vec1, vec2)
62
  norm_a = np.linalg.norm(vec1)
63
  norm_b = np.linalg.norm(vec2)
64
-
65
- # Division durch Null vermeiden
66
- if norm_a == 0 or norm_b == 0:
67
- return 0
68
-
69
  return dot_product / (norm_a * norm_b)
70
 
71
- def get_combined_scores(query_vector, embedding_list, all_good_embeddings, avg_weight=0.6):
72
- """Berechnet einen kombinierten Score unter Berücksichtigung der Ähnlichkeit zum Durchschnitt und zu einzelnen Zutaten"""
73
- results = []
74
-
75
- for name, emb in embedding_list:
76
- # Ähnlichkeit zum Durchschnittsvektor
77
- avg_similarity = get_cosine_similarity(query_vector, emb)
78
-
79
- # Durchschnittliche Ähnlichkeit zu individuellen Zutaten
80
- individual_similarities = [get_cosine_similarity(good_emb, emb)
81
- for _, good_emb in all_good_embeddings]
82
- # Vermeide Division durch Null, falls all_good_embeddings leer ist
83
- avg_individual_similarity = sum(individual_similarities) / len(individual_similarities) if individual_similarities else 0
84
-
85
- # Kombinierter Score (gewichteter Durchschnitt)
86
- combined_score = avg_weight * avg_similarity + (1 - avg_weight) * avg_individual_similarity
87
-
88
- results.append((name, emb, combined_score))
89
 
90
- # Sortiere nach kombiniertem Score (absteigend)
91
- results.sort(key=lambda x: x[2], reverse=True)
92
- return results
93
-
94
- # Die von dir bereitgestellte, korrigierte find_best_ingredients Funktion
95
- def find_best_ingredients(required_ingredients, available_ingredients, max_ingredients=6, avg_weight=0.6):
96
  """
97
- Findet die besten Zutaten basierend auf RecipeBERT Embeddings.
98
  """
99
- # Ensure no duplicates in lists
100
  required_ingredients = list(set(required_ingredients))
101
  available_ingredients = list(set([i for i in available_ingredients if i not in required_ingredients]))
102
-
103
- # Special case: If no required ingredients, randomly select one from available ingredients
104
- if not required_ingredients and available_ingredients:
105
- # Randomly select 1 ingredient as starting point
106
- random_ingredient = random.choice(available_ingredients)
107
- required_ingredients = [random_ingredient]
108
- available_ingredients = [i for i in available_ingredients if i != random_ingredient]
109
- print(f"No required ingredients provided. Randomly selected: {random_ingredient}")
110
-
111
- # If still no ingredients or already at max capacity
112
- if not required_ingredients or len(required_ingredients) >= max_ingredients:
113
- return required_ingredients[:max_ingredients]
114
-
115
- # If no additional ingredients available
116
- if not available_ingredients:
117
- return required_ingredients
118
-
119
- # Calculate embeddings for all ingredients
120
- embed_required = [(e, get_embedding(e)) for e in required_ingredients]
121
- embed_available = [(e, get_embedding(e)) for e in available_ingredients]
122
-
123
- # Number of ingredients to add
124
- num_to_add = min(max_ingredients - len(required_ingredients), len(available_ingredients))
125
-
126
- # Copy required ingredients to final list
127
- final_ingredients = embed_required.copy()
128
-
129
- # Add best ingredients
130
- for _ in range(num_to_add):
131
- # Calculate average vector of current combination
132
- avg = average_embedding(final_ingredients)
133
-
134
- # Calculate combined scores for all candidates
135
- candidates = get_combined_scores(avg, embed_available, final_ingredients, avg_weight)
136
-
137
- # If no candidates left, break
138
- if not candidates:
139
- break
140
-
141
- # Choose best ingredient
142
- best_name, best_embedding, _ = candidates[0]
143
-
144
- # Add best ingredient to final list
145
- final_ingredients.append((best_name, best_embedding))
146
-
147
- # Remove ingredient from available ingredients
148
- embed_available = [item for item in embed_available if item[0] != best_name]
149
-
150
- # Extract only ingredient names
151
- return [name for name, _ in final_ingredients]
152
-
153
- def skip_special_tokens(text, special_tokens):
154
- """Removes special tokens from text"""
155
- for token in special_tokens:
156
- text = text.replace(token, "")
157
- return text
158
-
159
- def target_postprocessing(texts, special_tokens):
160
- """Post-processes generated text"""
161
- if not isinstance(texts, list):
162
- texts = [texts]
163
-
164
- new_texts = []
165
- for text in texts:
166
- text = skip_special_tokens(text, special_tokens)
167
-
168
- for k, v in tokens_map.items():
169
- text = text.replace(k, v)
170
 
171
- new_texts.append(text)
172
 
173
- return new_texts
174
-
175
- def validate_recipe_ingredients(recipe_ingredients, expected_ingredients, tolerance=0):
176
- """
177
- Validates if the recipe contains approximately the expected ingredients.
178
- """
179
- recipe_count = len([ing for ing in recipe_ingredients if ing and ing.strip()])
180
- expected_count = len(expected_ingredients)
181
- return abs(recipe_count - expected_count) == tolerance
182
-
183
- def generate_recipe_with_t5(ingredients_list, max_retries=5):
184
- """Generiert ein Rezept mit dem T5 Rezeptgenerierungsmodell mit Validierung."""
185
- original_ingredients = ingredients_list.copy()
186
-
187
- for attempt in range(max_retries):
188
- try:
189
- # Für Wiederholungsversuche nach dem ersten Versuch, mische die Zutaten
190
- if attempt > 0:
191
- current_ingredients = original_ingredients.copy()
192
- random.shuffle(current_ingredients)
193
- else:
194
- current_ingredients = ingredients_list
195
-
196
- # Formatiere Zutaten als kommaseparierten String
197
- ingredients_string = ", ".join(current_ingredients)
198
- prefix = "items: "
199
-
200
- # Generationseinstellungen
201
- generation_kwargs = {
202
- "max_length": 512,
203
- "min_length": 64,
204
- "do_sample": True,
205
- "top_k": 60,
206
- "top_p": 0.95
207
- }
208
- # print(f"Versuch {attempt + 1}: {prefix + ingredients_string}")
209
-
210
- # Tokenisiere Eingabe
211
- inputs = t5_tokenizer(
212
- prefix + ingredients_string,
213
- max_length=256,
214
- padding="max_length",
215
- truncation=True,
216
- return_tensors="jax"
217
- )
218
-
219
- # Generiere Text
220
- output_ids = t5_model.generate(
221
- input_ids=inputs.input_ids,
222
- attention_mask=inputs.attention_mask,
223
- **generation_kwargs
224
- )
225
-
226
- # Dekodieren und Nachbearbeiten
227
- generated = output_ids.sequences
228
- generated_text = target_postprocessing(
229
- t5_tokenizer.batch_decode(generated, skip_special_tokens=False),
230
- special_tokens
231
- )[0]
232
-
233
- # Abschnitte parsen
234
- recipe = {}
235
- sections = generated_text.split("\n")
236
- for section in sections:
237
- section = section.strip()
238
- if section.startswith("title:"):
239
- recipe["title"] = section.replace("title:", "").strip().capitalize()
240
- elif section.startswith("ingredients:"):
241
- ingredients_text = section.replace("ingredients:", "").strip()
242
- recipe["ingredients"] = [item.strip().capitalize() for item in ingredients_text.split("--") if item.strip()]
243
- elif section.startswith("directions:"):
244
- directions_text = section.replace("directions:", "").strip()
245
- recipe["directions"] = [step.strip().capitalize() for step in directions_text.split("--") if step.strip()]
246
-
247
- # Wenn der Titel fehlt, erstelle einen
248
- if "title" not in recipe:
249
- recipe["title"] = f"Rezept mit {', '.join(current_ingredients[:3])}"
250
-
251
- # Stelle sicher, dass alle Abschnitte existieren
252
- if "ingredients" not in recipe:
253
- recipe["ingredients"] = current_ingredients
254
- if "directions" not in recipe:
255
- recipe["directions"] = ["Keine Anweisungen generiert"]
256
-
257
- # Validiere das Rezept
258
- if validate_recipe_ingredients(recipe["ingredients"], original_ingredients):
259
- # print(f"Erfolg bei Versuch {attempt + 1}: Rezept hat die richtige Anzahl von Zutaten")
260
- return recipe
261
- else:
262
- # print(f"Versuch {attempt + 1} fehlgeschlagen: Erwartet {len(original_ingredients)} Zutaten, erhalten {len(recipe['ingredients'])}")
263
- if attempt == max_retries - 1:
264
- # print("Maximale Wiederholungsversuche erreicht, letztes generiertes Rezept wird zurückgegeben")
265
- return recipe
266
-
267
- except Exception as e:
268
- # print(f"Fehler bei der Rezeptgenerierung Versuch {attempt + 1}: {str(e)}")
269
- if attempt == max_retries - 1:
270
- return {
271
- "title": f"Rezept mit {original_ingredients[0] if original_ingredients else 'Zutaten'}",
272
- "ingredients": original_ingredients,
273
- "directions": ["Fehler beim Generieren der Rezeptanweisungen"]
274
- }
275
-
276
- # Fallback (sollte nicht erreicht werden)
277
  return {
278
- "title": f"Rezept mit {original_ingredients[0] if original_ingredients else 'Zutaten'}",
279
- "ingredients": original_ingredients,
280
- "directions": ["Fehler beim Generieren der Rezeptanweisungen"]
 
 
 
 
 
281
  }
282
 
283
- # Diese Funktion wird von der Gradio-UI und der FastAPI-Route aufgerufen.
284
- # Sie ist für die Kernlogik zuständig.
285
  def process_recipe_request_logic(required_ingredients, available_ingredients, max_ingredients, max_retries):
286
  """
287
  Kernlogik zur Verarbeitung einer Rezeptgenerierungsanfrage.
288
- Ausgelagert, um von verschiedenen Endpunkten aufgerufen zu werden.
289
  """
290
  if not required_ingredients and not available_ingredients:
291
  return {"error": "Keine Zutaten angegeben"}
292
-
293
  try:
294
- # Optimale Zutaten finden
295
  optimized_ingredients = find_best_ingredients(
296
- required_ingredients,
297
- available_ingredients,
298
- max_ingredients
299
  )
300
-
301
- # Rezept mit optimierten Zutaten generieren
302
- recipe = generate_recipe_with_t5(optimized_ingredients, max_retries)
303
-
304
- # Ergebnis formatieren
305
  result = {
306
  'title': recipe['title'],
307
  'ingredients': recipe['ingredients'],
308
  'directions': recipe['directions'],
309
- 'used_ingredients': optimized_ingredients
310
  }
311
  return result
312
-
313
  except Exception as e:
314
  return {"error": f"Fehler bei der Rezeptgenerierung: {str(e)}"}
315
 
316
- # Diese Funktion ist für den internen Gradio 'API-Test'-Tab gedacht,
317
- # der einen JSON-String als Eingabe erwartet und einen JSON-String zurückgibt.
318
- # Sie wird NICHT von deiner Flutter-App direkt aufgerufen, da die Flutter-App
319
- # die /api/generate_recipe_rest FastAPI-Route direkt nutzt.
320
- def flutter_api_generate_recipe(ingredients_data: str): # Typ-Hint für Klarheit
321
- """
322
- Flutter-freundliche API-Funktion für den Gradio-API-Test-Tab.
323
- Verarbeitet JSON-String-Eingabe und gibt JSON-String-Ausgabe zurück.
324
- """
325
- try:
326
- data = json.loads(ingredients_data) # Muss ein JSON-String sein
327
-
328
- required_ingredients = data.get('required_ingredients', [])
329
- available_ingredients = data.get('available_ingredients', [])
330
- max_ingredients = data.get('max_ingredients', 7)
331
- max_retries = data.get('max_retries', 5)
332
-
333
- # Rufe die Kernlogik auf
334
- result_dict = process_recipe_request_logic(
335
- required_ingredients, available_ingredients, max_ingredients, max_retries
336
- )
337
- return json.dumps(result_dict) # Gibt einen JSON-STRING zurück
338
-
339
- except Exception as e:
340
- # Logge den Fehler für Debugging im Space-Log
341
- print(f"Error in flutter_api_generate_recipe: {str(e)}")
342
- return json.dumps({"error": f"Internal API Error: {str(e)}"})
343
-
344
- def gradio_ui_generate_recipe(required_ingredients_text, available_ingredients_text, max_ingredients_val, max_retries_val):
345
- """Gradio UI Funktion für die Web-Oberfläche"""
346
- try:
347
- required_ingredients = [ing.strip() for ing in required_ingredients_text.split(',') if ing.strip()]
348
- available_ingredients = [ing.strip() for ing in available_ingredients_text.split(',') if ing.strip()]
349
-
350
- # Rufe die Kernlogik auf
351
- result = process_recipe_request_logic(
352
- required_ingredients, available_ingredients, max_ingredients_val, max_retries_val
353
- )
354
-
355
- if 'error' in result:
356
- return result['error'], "", "", ""
357
-
358
- ingredients_list = '\n'.join([f"• {ing}" for ing in result['ingredients']])
359
- directions_list = '\n'.join([f"{i+1}. {dir}" for i, dir in enumerate(result['directions'])])
360
- used_ingredients = ', '.join(result['used_ingredients'])
361
-
362
- return (
363
- result['title'],
364
- ingredients_list,
365
- directions_list,
366
- used_ingredients
367
- )
368
-
369
- except Exception as e:
370
- # Fehlermeldung für die Gradio UI
371
- return f"Fehler: {str(e)}", "", "", ""
372
-
373
- # Erstelle die Gradio Oberfläche
374
- with gr.Blocks(title="AI Rezept Generator") as demo:
375
- gr.Markdown("# 🍳 AI Rezept Generator")
376
- gr.Markdown("Generiere Rezepte mit KI und intelligenter Zutat-Kombination!")
377
-
378
- with gr.Tab("Web-Oberfläche"):
379
- with gr.Row():
380
- with gr.Column():
381
- required_ing = gr.Textbox(
382
- label="Benötigte Zutaten (kommasepariert)",
383
- placeholder="Hähnchen, Reis, Zwiebel",
384
- lines=2
385
- )
386
- available_ing = gr.Textbox(
387
- label="Verfügbare Zutaten (kommasepariert, optional)",
388
- placeholder="Knoblauch, Tomate, Pfeffer, Kräuter",
389
- lines=2
390
- )
391
- max_ing = gr.Slider(3, 10, value=7, step=1, label="Maximale Zutaten")
392
- max_retries = gr.Slider(1, 10, value=5, step=1, label="Max. Wiederholungsversuche")
393
-
394
- generate_btn = gr.Button("Rezept generieren", variant="primary")
395
-
396
- with gr.Column():
397
- title_output = gr.Textbox(label="Rezepttitel", interactive=False)
398
- ingredients_output = gr.Textbox(label="Zutaten", lines=8, interactive=False)
399
- directions_output = gr.Textbox(label="Anweisungen", lines=10, interactive=False)
400
- used_ingredients_output = gr.Textbox(label="Verwendete Zutaten", interactive=False)
401
-
402
- generate_btn.click(
403
- fn=gradio_ui_generate_recipe,
404
- inputs=[required_ing, available_ing, max_ing, max_retries],
405
- outputs=[title_output, ingredients_output, directions_output, used_ingredients_output]
406
- )
407
-
408
- with gr.Tab("API-Test"):
409
- gr.Markdown("### Teste die Flutter API (via 'hugging_face_chat_gradio' Client)")
410
- gr.Markdown("Dieser Tab zeigt, wie die Eingabe für die 'generate_recipe_for_flutter'-API aussehen sollte.")
411
-
412
- api_input = gr.Textbox(
413
- label="JSON-Eingabe (für API-Aufruf)",
414
- placeholder='{"required_ingredients": ["chicken", "rice"], "available_ingredients": ["onion", "garlic"], "max_ingredients": 6}',
415
- lines=4
416
- )
417
- api_output = gr.Textbox(label="JSON-Ausgabe", lines=15, interactive=False)
418
- api_test_btn = gr.Button("API testen", variant="secondary")
419
-
420
- # Hier wird die Funktion weiterhin für den Gradio-eigenen API-Test-Tab verwendet.
421
- api_test_btn.click(
422
- fn=flutter_api_generate_recipe,
423
- inputs=[api_input],
424
- outputs=[api_output],
425
- api_name="generate_recipe_for_flutter" # Dies ist der api_name, den das Flutter-Paket verwendet
426
- )
427
-
428
- gr.Examples(
429
- examples=[
430
- ['{"required_ingredients": ["chicken", "rice"], "available_ingredients": ["onion", "garlic", "tomato"], "max_ingredients": 6}'],
431
- ['{"ingredients": ["pasta"], "available_ingredients": ["cheese", "mushrooms", "cream"], "max_ingredients": 5}']
432
- ],
433
- inputs=[api_input]
434
- )
435
-
436
- # --- FastAPI-Integration ---
437
- app = FastAPI()
438
 
439
  class RecipeRequest(BaseModel):
440
  required_ingredients: list[str] = []
441
  available_ingredients: list[str] = []
442
  max_ingredients: int = 7
443
- max_retries: int = 5
 
444
 
445
- @app.post("/generate_recipe") # KORRIGIERT: Der Endpunkt ist jetzt /generate_recipe
446
- async def generate_recipe_rest_api(request_data: RecipeRequest):
447
- """
448
- Standard-REST-API-Endpunkt für die Flutter-App.
449
- Nimmt direkt JSON-Daten an und gibt direkt JSON zurück.
450
- """
451
- required_ingredients = request_data.required_ingredients
452
- available_ingredients = request_data.available_ingredients
453
- max_ingredients = request_data.max_ingredients
454
- max_retries = request_data.max_retries
455
 
456
  result_dict = process_recipe_request_logic(
457
- required_ingredients, available_ingredients, max_ingredients, max_retries
 
 
 
458
  )
459
-
460
  return JSONResponse(content=result_dict)
461
 
462
- # Gradio-App als Sub-App in die FastAPI-App mounten
463
- # Dies ist der Standardweg, um Gradio in eine FastAPI-Anwendung einzubetten.
464
- # Der Gradio-Teil wird dann unter dem Wurzelpfad '/'.
465
- app = gr.mount_gradio_app(app, demo, path="/") # Gradio unter dem Wurzelpfad mounten
466
-
467
- # Wenn du deine App lokal ausführst, kannst du FastAPI mit Uvicorn starten:
468
- # if __name__ == "__main__":
469
- # import uvicorn
470
- # uvicorn.run(app, host="0.0.0.0", port=8000)
471
 
472
- # Für Hugging Face Spaces ist der if __name__ == "__main__": Block nicht nötig,
473
- # da Spaces Uvicorn automatisch startet und die "app"-Variable sucht.
 
1
+ from transformers import AutoTokenizer, AutoModel
 
2
  import torch
3
  import numpy as np
4
  import random
5
  import json
6
+ from fastapi import FastAPI
7
  from fastapi.responses import JSONResponse
8
  from pydantic import BaseModel
9
 
10
+ # Lade NUR RecipeBERT Modell
11
  bert_model_name = "alexdseo/RecipeBERT"
12
  bert_tokenizer = AutoTokenizer.from_pretrained(bert_model_name)
13
  bert_model = AutoModel.from_pretrained(bert_model_name)
14
  bert_model.eval() # Setze das Modell in den Evaluationsmodus
15
 
16
+ # T5-Modell und -Logik KOMPLETT ENTFERNT für diesen Schritt
17
+ # special_tokens und tokens_map sind nicht mehr relevant, bleiben aber als Kommentar
 
 
 
 
 
 
 
 
 
18
 
19
+ # --- RecipeBERT-spezifische Funktionen ---
20
  def get_embedding(text):
21
+ """Berechnet das Embedding für einen Text mit Mean Pooling über alle Tokens."""
22
  inputs = bert_tokenizer(text, return_tensors="pt", truncation=True, padding=True)
23
  with torch.no_grad():
24
  outputs = bert_model(**inputs)
 
 
25
  attention_mask = inputs['attention_mask']
26
  token_embeddings = outputs.last_hidden_state
27
  input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
28
  sum_embeddings = torch.sum(token_embeddings * input_mask_expanded, 1)
29
  sum_mask = torch.clamp(input_mask_expanded.sum(1), min=1e-9)
 
30
  return (sum_embeddings / sum_mask).squeeze(0)
31
 
32
  def average_embedding(embedding_list):
33
+ """Berechnet den Durchschnitt einer Liste von Embeddings."""
34
+ tensors = torch.stack(embedding_list) # embedding_list enthält hier direkt die Tensoren
 
35
  return tensors.mean(dim=0)
36
 
37
  def get_cosine_similarity(vec1, vec2):
38
+ """Berechnet die Cosinus-Ähnlichkeit zwischen zwei Vektoren."""
39
+ if torch.is_tensor(vec1): vec1 = vec1.detach().numpy()
40
+ if torch.is_tensor(vec2): vec2 = vec2.detach().numpy()
 
 
 
 
41
  vec1 = vec1.flatten()
42
  vec2 = vec2.flatten()
 
43
  dot_product = np.dot(vec1, vec2)
44
  norm_a = np.linalg.norm(vec1)
45
  norm_b = np.linalg.norm(vec2)
46
+ if norm_a == 0 or norm_b == 0: return 0
 
 
 
 
47
  return dot_product / (norm_a * norm_b)
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
+ # find_best_ingredients (modifiziert, um die ähnlichste Zutat mit RecipeBERT zu finden)
51
+ def find_best_ingredients(required_ingredients, available_ingredients, max_ingredients=6):
 
 
 
 
52
  """
53
+ Findet die besten Zutaten: Alle benötigten + EINE ähnlichste aus den verfügbaren Zutaten.
54
  """
 
55
  required_ingredients = list(set(required_ingredients))
56
  available_ingredients = list(set([i for i in available_ingredients if i not in required_ingredients]))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
+ final_ingredients = required_ingredients.copy()
59
 
60
+ # Nur wenn wir noch Platz haben und zusätzliche Zutaten verfügbar sind
61
+ if len(final_ingredients) < max_ingredients and len(available_ingredients) > 0:
62
+ if final_ingredients:
63
+ # Berechne den Durchschnitts-Embedding der benötigten Zutaten
64
+ required_embeddings = [get_embedding(ing) for ing in required_ingredients]
65
+ avg_required_embedding = average_embedding(required_embeddings)
66
+
67
+ best_additional_ingredient = None
68
+ highest_similarity = -1.0
69
+
70
+ # Finde die ähnlichste Zutat aus den verfügbaren
71
+ for avail_ing in available_ingredients:
72
+ avail_embedding = get_embedding(avail_ing)
73
+ similarity = get_cosine_similarity(avg_required_embedding, avail_embedding)
74
+ if similarity > highest_similarity:
75
+ highest_similarity = similarity
76
+ best_additional_ingredient = avail_ing
77
+
78
+ if best_additional_ingredient:
79
+ final_ingredients.append(best_additional_ingredient)
80
+ print(f"INFO: Added '{best_additional_ingredient}' (similarity: {highest_similarity:.2f}) as most similar.")
81
+ else:
82
+ # Wenn keine benötigten Zutaten, wähle zufällig eine aus den verfügbaren (wie zuvor)
83
+ random_ingredient = random.choice(available_ingredients)
84
+ final_ingredients.append(random_ingredient)
85
+ print(f"INFO: No required ingredients. Added random available ingredient: '{random_ingredient}'.")
86
+
87
+ # Begrenze auf max_ingredients, falls durch Zufall/ähnlichster Auswahl zu viele hinzugefügt wurden
88
+ return final_ingredients[:max_ingredients]
89
+
90
+
91
+ # mock_generate_recipe (bleibt gleich)
92
+ def mock_generate_recipe(ingredients_list):
93
+ """Generiert ein Mock-Rezept, da T5-Modell entfernt ist."""
94
+ title = f"Einfaches Rezept mit {', '.join(ingredients_list[:3])}" if ingredients_list else "Einfaches Testrezept"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  return {
96
+ "title": title,
97
+ "ingredients": ingredients_list, # Die "generierten" Zutaten sind einfach die Eingabe
98
+ "directions": [
99
+ "Dies ist ein generierter Text von RecipeBERT (ohne T5).",
100
+ "Das Laden des RecipeBERT-Modells war erfolgreich!",
101
+ f"Basierend auf deinen Eingaben wurde '{ingredients_list[-1]}' als ähnlichste Zutat hinzugefügt." if len(ingredients_list) > 1 else "Keine zusätzliche Zutat hinzugefügt."
102
+ ],
103
+ "used_ingredients": ingredients_list # In diesem Mock-Fall sind alle "used"
104
  }
105
 
106
+
 
107
  def process_recipe_request_logic(required_ingredients, available_ingredients, max_ingredients, max_retries):
108
  """
109
  Kernlogik zur Verarbeitung einer Rezeptgenerierungsanfrage.
110
+ Für diesen Test wird nur RecipeBERT zum Laden getestet und ein Mock-Rezept zurückgegeben.
111
  """
112
  if not required_ingredients and not available_ingredients:
113
  return {"error": "Keine Zutaten angegeben"}
 
114
  try:
115
+ # Hier wird die neue find_best_ingredients verwendet
116
  optimized_ingredients = find_best_ingredients(
117
+ required_ingredients, available_ingredients, max_ingredients
 
 
118
  )
119
+
120
+ # Rufe die Mock-Generierungsfunktion auf
121
+ recipe = mock_generate_recipe(optimized_ingredients)
122
+
 
123
  result = {
124
  'title': recipe['title'],
125
  'ingredients': recipe['ingredients'],
126
  'directions': recipe['directions'],
127
+ 'used_ingredients': optimized_ingredients # Jetzt wirklich die vom find_best_ingredients
128
  }
129
  return result
 
130
  except Exception as e:
131
  return {"error": f"Fehler bei der Rezeptgenerierung: {str(e)}"}
132
 
133
+ # --- FastAPI-Implementierung ---
134
+ app = FastAPI(title="AI Recipe Generator API (RecipeBERT Only Test)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
  class RecipeRequest(BaseModel):
137
  required_ingredients: list[str] = []
138
  available_ingredients: list[str] = []
139
  max_ingredients: int = 7
140
+ max_retries: int = 5 # Wird hier nicht direkt genutzt, aber im Payload beibehalten
141
+ ingredients: list[str] = [] # Für Abwärtskompatibilität
142
 
143
+ @app.post("/generate_recipe") # Der API-Endpunkt für Flutter
144
+ async def generate_recipe_api(request_data: RecipeRequest):
145
+ final_required_ingredients = request_data.required_ingredients
146
+ if not final_required_ingredients and request_data.ingredients:
147
+ final_required_ingredients = request_data.ingredients
 
 
 
 
 
148
 
149
  result_dict = process_recipe_request_logic(
150
+ final_required_ingredients,
151
+ request_data.available_ingredients,
152
+ request_data.max_ingredients,
153
+ request_data.max_retries # max_retries wird nur an die Logik übergeben, aber nicht verwendet
154
  )
 
155
  return JSONResponse(content=result_dict)
156
 
157
+ @app.get("/")
158
+ async def read_root():
159
+ return {"message": "AI Recipe Generator API is running (RecipeBERT only, 1 similar ingredient)!"} # Angepasste Nachricht
 
 
 
 
 
 
160
 
161
+ print("INFO: FastAPI application script finished execution and defined 'app' variable.")