package openai import ( "context" "encoding/json" "fmt" "strings" "github.com/food-ai/backend/internal/adapters/ai" ) // RecognizeReceipt uses the vision model to extract food items from a receipt photo. func (c *Client) RecognizeReceipt(ctx context.Context, imageBase64, mimeType string) (*ai.ReceiptResult, error) { prompt := `Ты — OCR-система для чеков из продуктовых магазинов. Проанализируй фото чека и извлеки список продуктов питания. Для каждого продукта определи: - name: название на русском языке (убери артикулы, коды, лишние символы) - quantity: количество (число) - unit: единица (г, кг, мл, л, шт, уп) - category: dairy | meat | produce | bakery | frozen | beverages | other - confidence: 0.0–1.0 Позиции, которые не являются едой (бытовая химия, табак, алкоголь) — пропусти. Позиции с нечитаемым текстом — добавь в unrecognized. Верни ТОЛЬКО валидный JSON без markdown: { "items": [ {"name": "Молоко 2.5%", "quantity": 1, "unit": "л", "category": "dairy", "confidence": 0.95} ], "unrecognized": [ {"raw_text": "ТОВ АРТИК 1ШТ", "price": 89.0} ] }` text, err := c.generateVisionContent(ctx, prompt, imageBase64, mimeType) if err != nil { return nil, fmt.Errorf("recognize receipt: %w", err) } var result ai.ReceiptResult if err := parseJSON(text, &result); err != nil { return nil, fmt.Errorf("parse receipt result: %w", err) } if result.Items == nil { result.Items = []ai.RecognizedItem{} } if result.Unrecognized == nil { result.Unrecognized = []ai.UnrecognizedItem{} } return &result, nil } // RecognizeProducts uses the vision model to identify food items in a photo (fridge, shelf, etc.). func (c *Client) RecognizeProducts(ctx context.Context, imageBase64, mimeType string) ([]ai.RecognizedItem, error) { prompt := `Ты — система распознавания продуктов питания. Посмотри на фото и определи все видимые продукты питания. Для каждого продукта оцени: - name: название на русском языке - quantity: приблизительное количество (число) - unit: единица (г, кг, мл, л, шт) - category: dairy | meat | produce | bakery | frozen | beverages | other - confidence: 0.0–1.0 Только продукты питания. Пустые упаковки и несъедобные предметы — пропусти. Верни ТОЛЬКО валидный JSON без markdown: { "items": [ {"name": "Яйца", "quantity": 10, "unit": "шт", "category": "dairy", "confidence": 0.9} ] }` text, err := c.generateVisionContent(ctx, prompt, imageBase64, mimeType) if err != nil { return nil, fmt.Errorf("recognize products: %w", err) } var result struct { Items []ai.RecognizedItem `json:"items"` } if err := parseJSON(text, &result); err != nil { return nil, fmt.Errorf("parse products result: %w", err) } if result.Items == nil { return []ai.RecognizedItem{}, nil } return result.Items, nil } // RecognizeDish uses the vision model to identify a dish and estimate its nutritional content. func (c *Client) RecognizeDish(ctx context.Context, imageBase64, mimeType string) (*ai.DishResult, error) { prompt := `Ты — диетолог и кулинарный эксперт. Посмотри на фото блюда и определи: - dish_name: название блюда на русском языке - weight_grams: приблизительный вес порции в граммах - calories: калорийность порции (приблизительно) - protein_g, fat_g, carbs_g: БЖУ на порцию - confidence: 0.0–1.0 - similar_dishes: до 3 похожих блюд (для поиска рецептов) Верни ТОЛЬКО валидный JSON без markdown: { "dish_name": "Паста Карбонара", "weight_grams": 350, "calories": 520, "protein_g": 22, "fat_g": 26, "carbs_g": 48, "confidence": 0.85, "similar_dishes": ["Паста с беконом", "Спагетти"] }` text, err := c.generateVisionContent(ctx, prompt, imageBase64, mimeType) if err != nil { return nil, fmt.Errorf("recognize dish: %w", err) } var result ai.DishResult if err := parseJSON(text, &result); err != nil { return nil, fmt.Errorf("parse dish result: %w", err) } if result.SimilarDishes == nil { result.SimilarDishes = []string{} } return &result, nil } // ClassifyIngredient uses the text model to classify an unknown food item // and build an ingredient_mappings record for it. func (c *Client) ClassifyIngredient(ctx context.Context, name string) (*ai.IngredientClassification, error) { prompt := fmt.Sprintf(`Classify the food product: "%s". Return ONLY valid JSON without markdown: { "canonical_name": "turkey_breast", "aliases": ["turkey breast"], "translations": [ {"lang": "ru", "name": "грудка индейки", "aliases": ["грудка индейки", "филе индейки"]} ], "category": "meat", "default_unit": "g", "calories_per_100g": 135, "protein_per_100g": 29, "fat_per_100g": 1, "carbs_per_100g": 0, "storage_days": 3 }`, name) messages := []map[string]string{ {"role": "user", "content": prompt}, } text, err := c.generateContent(ctx, messages) if err != nil { return nil, fmt.Errorf("classify ingredient: %w", err) } var result ai.IngredientClassification if err := parseJSON(text, &result); err != nil { return nil, fmt.Errorf("parse classification: %w", err) } return &result, nil } // parseJSON strips optional markdown fences and unmarshals JSON. func parseJSON(text string, dst any) error { text = strings.TrimSpace(text) if strings.HasPrefix(text, "```") { text = strings.TrimPrefix(text, "```json") text = strings.TrimPrefix(text, "```") text = strings.TrimSuffix(text, "```") text = strings.TrimSpace(text) } return json.Unmarshal([]byte(text), dst) }