File size: 12,192 Bytes
b7333e0
908127c
 
3df2cca
 
f0ebec2
 
3df2cca
f0ebec2
b7333e0
908127c
 
 
 
 
a1a1a43
b7333e0
 
 
908127c
a1a1a43
b7333e0
 
 
 
 
 
 
908127c
 
 
 
 
 
 
 
 
 
 
 
eb874d4
 
b7333e0
eb874d4
 
908127c
 
 
 
 
 
 
 
 
 
 
 
b7333e0
eb874d4
f0ebec2
eb874d4
f0ebec2
3df2cca
 
f0ebec2
908127c
eb874d4
 
 
908127c
eb874d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908127c
eb874d4
 
 
 
 
908127c
 
a1a1a43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3df2cca
a1a1a43
 
 
3df2cca
f0ebec2
908127c
a1a1a43
3df2cca
 
 
a1a1a43
3df2cca
 
 
a1a1a43
3df2cca
a1a1a43
3df2cca
a1a1a43
 
 
3df2cca
a1a1a43
 
 
 
 
3df2cca
 
 
 
b7333e0
3df2cca
 
a1a1a43
3df2cca
 
 
a1a1a43
3df2cca
a1a1a43
3df2cca
 
 
 
 
b7333e0
3df2cca
 
 
 
 
 
 
 
 
 
 
 
b7333e0
3df2cca
 
 
 
 
a1a1a43
3df2cca
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
from transformers import FlaxAutoModelForSeq2SeqLM, AutoTokenizer, AutoModel
import torch
import numpy as np
import random
import json
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from pydantic import BaseModel

# Lade RecipeBERT Modell
bert_model_name = "alexdseo/RecipeBERT"
bert_tokenizer = AutoTokenizer.from_pretrained(bert_model_name)
bert_model = AutoModel.from_pretrained(bert_model_name)
bert_model.eval() # Setze das Modell in den Evaluationsmodus

# Lade T5 Rezeptgenerierungsmodell
MODEL_NAME_OR_PATH = "flax-community/t5-recipe-generation"
t5_tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME_OR_PATH, use_fast=True)
t5_model = FlaxAutoModelForSeq2SeqLM.from_pretrained(MODEL_NAME_OR_PATH) # Modell wird jetzt auch geladen

# Token Mapping für die T5 Modell-Ausgabe
special_tokens = t5_tokenizer.all_special_tokens
tokens_map = {
    "<sep>": "--",
    "<section>": "\n"
}

# --- RecipeBERT-spezifische Funktionen (unverändert) ---
def get_embedding(text):
    """Berechnet das Embedding für einen Text mit Mean Pooling über alle Tokens."""
    inputs = bert_tokenizer(text, return_tensors="pt", truncation=True, padding=True)
    with torch.no_grad():
        outputs = bert_model(**inputs)
    attention_mask = inputs['attention_mask']
    token_embeddings = outputs.last_hidden_state
    input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
    sum_embeddings = torch.sum(token_embeddings * input_mask_expanded, 1)
    sum_mask = torch.clamp(input_mask_expanded.sum(1), min=1e-9)
    return (sum_embeddings / sum_mask).squeeze(0)

def average_embedding(embedding_list):
    """Berechnet den Durchschnitt einer Liste von Embeddings."""
    tensors = torch.stack(embedding_list)
    return tensors.mean(dim=0)

def get_cosine_similarity(vec1, vec2):
    """Berechnet die Cosinus-Ähnlichkeit zwischen zwei Vektoren."""
    if torch.is_tensor(vec1): vec1 = vec1.detach().numpy()
    if torch.is_tensor(vec2): vec2 = vec2.detach().numpy()
    vec1 = vec1.flatten()
    vec2 = vec2.flatten()
    dot_product = np.dot(vec1, vec2)
    norm_a = np.linalg.norm(vec1)
    norm_b = np.linalg.norm(vec2)
    if norm_a == 0 or norm_b == 0: return 0
    return dot_product / (norm_a * norm_b)

# find_best_ingredients (unverändert, nutzt RecipeBERT für eine ähnlichste Zutat)
def find_best_ingredients(required_ingredients, available_ingredients, max_ingredients=6):
    """
    Findet die besten Zutaten: Alle benötigten + EINE ähnlichste aus den verfügbaren Zutaten.
    """
    required_ingredients = list(set(required_ingredients))
    available_ingredients = list(set([i for i in available_ingredients if i not in required_ingredients]))

    final_ingredients = required_ingredients.copy()

    # Nur wenn wir noch Platz haben und zusätzliche Zutaten verfügbar sind
    if len(final_ingredients) < max_ingredients and len(available_ingredients) > 0:
        if final_ingredients:
            required_embeddings = [get_embedding(ing) for ing in required_ingredients]
            avg_required_embedding = average_embedding(required_embeddings)
            
            best_additional_ingredient = None
            highest_similarity = -1.0

            for avail_ing in available_ingredients:
                avail_embedding = get_embedding(avail_ing)
                similarity = get_cosine_similarity(avg_required_embedding, avail_embedding)
                if similarity > highest_similarity:
                    highest_similarity = similarity
                    best_additional_ingredient = avail_ing
            
            if best_additional_ingredient:
                final_ingredients.append(best_additional_ingredient)
                print(f"INFO: Added '{best_additional_ingredient}' (similarity: {highest_similarity:.2f}) as most similar.")
        else:
            random_ingredient = random.choice(available_ingredients)
            final_ingredients.append(random_ingredient)
            print(f"INFO: No required ingredients. Added random available ingredient: '{random_ingredient}'.")

    return final_ingredients[:max_ingredients]


# skip_special_tokens (unverändert, wird von generate_recipe_with_t5 genutzt)
def skip_special_tokens(text, special_tokens):
    """Entfernt spezielle Tokens aus dem Text"""
    for token in special_tokens:
        text = text.replace(token, "")
    return text

# target_postprocessing (unverändert, wird von generate_recipe_with_t5 genutzt)
def target_postprocessing(texts, special_tokens):
    """Post-processed generierten Text"""
    if not isinstance(texts, list):
        texts = [texts]

    new_texts = []
    for text in texts:
        text = skip_special_tokens(text, special_tokens)

        for k, v in tokens_map.items():
            text = text.replace(k, v)

        new_texts.append(text)

    return new_texts

# validate_recipe_ingredients (unverändert, wird von generate_recipe_with_t5 genutzt)
def validate_recipe_ingredients(recipe_ingredients, expected_ingredients, tolerance=0):
    """
    Validiert, ob das Rezept ungefähr die erwarteten Zutaten enthält.
    """
    recipe_count = len([ing for ing in recipe_ingredients if ing and ing.strip()])
    expected_count = len(expected_ingredients)
    return abs(recipe_count - expected_count) == tolerance


# generate_recipe_with_t5 (jetzt AKTIVIERT)
def generate_recipe_with_t5(ingredients_list, max_retries=5):
    """Generiert ein Rezept mit dem T5 Rezeptgenerierungsmodell mit Validierung."""
    original_ingredients = ingredients_list.copy()

    for attempt in range(max_retries):
        try:
            # Für Wiederholungsversuche nach dem ersten Versuch, mische die Zutaten
            if attempt > 0:
                current_ingredients = original_ingredients.copy()
                random.shuffle(current_ingredients)
            else:
                current_ingredients = ingredients_list

            # Formatiere Zutaten als kommaseparierten String
            ingredients_string = ", ".join(current_ingredients)
            prefix = "items: "

            # Generationseinstellungen
            generation_kwargs = {
                "max_length": 512,
                "min_length": 64,
                "do_sample": True,
                "top_k": 60,
                "top_p": 0.95
            }
            # print(f"Versuch {attempt + 1}: {prefix + ingredients_string}")

            # Tokenisiere Eingabe
            inputs = t5_tokenizer(
                prefix + ingredients_string,
                max_length=256,
                padding="max_length",
                truncation=True,
                return_tensors="jax"
            )

            # Generiere Text
            output_ids = t5_model.generate(
                input_ids=inputs.input_ids,
                attention_mask=inputs.attention_mask,
                **generation_kwargs
            )

            # Dekodieren und Nachbearbeiten
            generated = output_ids.sequences
            generated_text = target_postprocessing(
                t5_tokenizer.batch_decode(generated, skip_special_tokens=False),
                special_tokens
            )[0]

            # Abschnitte parsen
            recipe = {}
            sections = generated_text.split("\n")
            for section in sections:
                section = section.strip()
                if section.startswith("title:"):
                    recipe["title"] = section.replace("title:", "").strip().capitalize()
                elif section.startswith("ingredients:"):
                    ingredients_text = section.replace("ingredients:", "").strip()
                    recipe["ingredients"] = [item.strip().capitalize() for item in ingredients_text.split("--") if item.strip()]
                elif section.startswith("directions:"):
                    directions_text = section.replace("directions:", "").strip()
                    recipe["directions"] = [step.strip().capitalize() for step in directions_text.split("--") if step.strip()]

            # Wenn der Titel fehlt, erstelle einen
            if "title" not in recipe:
                recipe["title"] = f"Rezept mit {', '.join(current_ingredients[:3])}"

            # Stelle sicher, dass alle Abschnitte existieren
            if "ingredients" not in recipe:
                recipe["ingredients"] = current_ingredients
            if "directions" not in recipe:
                recipe["directions"] = ["Keine Anweisungen generiert"]

            # Validiere das Rezept
            if validate_recipe_ingredients(recipe["ingredients"], original_ingredients):
                # print(f"Erfolg bei Versuch {attempt + 1}: Rezept hat die richtige Anzahl von Zutaten")
                return recipe
            else:
                # print(f"Versuch {attempt + 1} fehlgeschlagen: Erwartet {len(original_ingredients)} Zutaten, erhalten {len(recipe['ingredients'])}")
                if attempt == max_retries - 1:
                    # print("Maximale Wiederholungsversuche erreicht, letztes generiertes Rezept wird zurückgegeben")
                    return recipe

        except Exception as e:
            # print(f"Fehler bei der Rezeptgenerierung Versuch {attempt + 1}: {str(e)}")
            if attempt == max_retries - 1:
                return {
                    "title": f"Rezept mit {original_ingredients[0] if original_ingredients else 'Zutaten'}",
                    "ingredients": original_ingredients,
                    "directions": ["Fehler beim Generieren der Rezeptanweisungen"]
                }

    # Fallback (sollte nicht erreicht werden)
    return {
        "title": f"Rezept mit {original_ingredients[0] if original_ingredients else 'Zutaten'}",
        "ingredients": original_ingredients,
        "directions": ["Fehler beim Generieren der Rezeptanweisungen"]
    }


# process_recipe_request_logic (JETZT RUFT generate_recipe_with_t5 auf)
def process_recipe_request_logic(required_ingredients, available_ingredients, max_ingredients, max_retries):
    """
    Kernlogik zur Verarbeitung einer Rezeptgenerierungsanfrage.
    Ausgelagert, um von verschiedenen Endpunkten aufgerufen zu werden.
    """
    if not required_ingredients and not available_ingredients:
        return {"error": "Keine Zutaten angegeben"}

    try:
        # Optimale Zutaten finden (mit RecipeBERT)
        optimized_ingredients = find_best_ingredients(
            required_ingredients,
            available_ingredients,
            max_ingredients
        )

        # Rezept mit optimierten Zutaten generieren (JETZT MIT T5!)
        recipe = generate_recipe_with_t5(optimized_ingredients, max_retries)

        # Ergebnis formatieren
        result = {
            'title': recipe['title'],
            'ingredients': recipe['ingredients'],
            'directions': recipe['directions'],
            'used_ingredients': optimized_ingredients
        }
        return result

    except Exception as e:
        return {"error": f"Fehler bei der Rezeptgenerierung: {str(e)}"}


# --- FastAPI-Implementierung ---
app = FastAPI(title="AI Recipe Generator API (Full Functionality)")

class RecipeRequest(BaseModel):
    required_ingredients: list[str] = []
    available_ingredients: list[str] = []
    max_ingredients: int = 7
    max_retries: int = 5
    ingredients: list[str] = [] # Für Abwärtskompatibilität

@app.post("/generate_recipe") # Der API-Endpunkt für Flutter
async def generate_recipe_api(request_data: RecipeRequest):
    final_required_ingredients = request_data.required_ingredients
    if not final_required_ingredients and request_data.ingredients:
        final_required_ingredients = request_data.ingredients

    result_dict = process_recipe_request_logic(
        final_required_ingredients,
        request_data.available_ingredients,
        request_data.max_ingredients,
        request_data.max_retries
    )
    return JSONResponse(content=result_dict)

@app.get("/")
async def read_root():
    return {"message": "AI Recipe Generator API is running (Full functionality activated)!"} # Angepasste Nachricht

print("INFO: FastAPI application script finished execution and defined 'app' variable.")