f0340859fa
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
48 lines
2.4 KiB
TypeScript
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;
|
|
}
|