Files
Epicure/apps/web/lib/ai/features/generate-recipe-from-recognition.ts
T
Arnaud f0340859fa feat: split photo-import into vision recognition + text generation
The photo-import flow used one vision-capable model to both read the
photo and structure the full recipe (quantities, steps, timing) in a
single call. Split it into two: a vision model recognizes what's in
the picture, then a text model reconstructs the recipe from that
description — same pattern the pantry photo-scan already uses for
recognition, and lets structuring use whichever model is actually
configured for text generation. Still counts as one AI-quota unit.

v0.37.0
2026-07-17 16:12:39 +02:00

48 lines
2.4 KiB
TypeScript

import { generateObject } from "ai";
import { z } from "zod";
import { resolveModel, type AiConfig } from "../factory";
import { dietaryTagsSchema, ingredientSchema, stepSchema } from "./recipe-schema";
import type { PhotoRecognition } from "./recognize-photo";
const RecipeOutputSchema = z.object({
title: z.string(),
description: z.string().optional(),
baseServings: z.number().int().min(1).max(100),
prepMins: z.number().int().min(0).optional(),
cookMins: z.number().int().min(0).optional(),
difficulty: z.enum(["easy", "medium", "hard"]),
dietaryTags: dietaryTagsSchema,
ingredients: z.array(ingredientSchema(z.number())),
steps: z.array(stepSchema),
});
export type GeneratedRecipeFromRecognition = z.infer<typeof RecipeOutputSchema>;
/** Step 2 of the photo-import flow: a text model reconstructs the full recipe
* (quantities, steps, timing) from what the vision model recognized in
* `recognizePhoto` — it never sees the photo itself. */
export async function generateRecipeFromRecognition(
recognition: PhotoRecognition,
config?: AiConfig & { language?: string },
): Promise<GeneratedRecipeFromRecognition> {
const model = resolveModel(config);
const lang = config?.language ?? "en";
const langInstruction = lang !== "en" ? ` Write the entire recipe (title, description, ingredient names, step instructions) in ${lang}.` : "";
const drinkInstruction = recognition.recipeType === "drink"
? " This is a drink: never set cookMins, and default baseServings to 1 unless the description implies more than one serving."
: "";
const { object } = await generateObject({
model,
schema: RecipeOutputSchema,
system:
`You are a professional chef and culinary writer. A vision model has already identified what's in a photo — reconstruct the complete, precise recipe implied by that description. For ingredients: quantity must be a number only (e.g. 0.25, 1.5, 2), unit is a separate string. Never combine quantity and unit into one string.${drinkInstruction}${langInstruction}`,
prompt: `Photo shows: "${recognition.title}"\nVisible ingredients: ${recognition.visibleIngredients.join(", ") || "none clearly visible"}\nDescription: ${recognition.description}\n\nWrite the complete recipe for this ${recognition.recipeType}.`,
});
if (recognition.recipeType === "drink") {
return { ...object, cookMins: undefined };
}
return object;
}