af92b8cc71
generate, generate-from-idea, URL import, and photo import all now have the AI set recipeType directly (defaulting to "dish" whenever ambiguous, per instruction), with cookMins force-cleared for drinks as a backstop in case the model doesn't follow the prompt guidance. 1-serving-by-default for unspecified drink quantities is left to the model's own judgment of the request text, since there's no reliable way to detect "was a serving count stated" without re-parsing intent. Drink recipes get a distinct default icon (no cover photo case), and Explore gains the same dish/drink Type filter My Recipes already had. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
55 lines
2.4 KiB
TypeScript
55 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";
|
|
|
|
const ImportedRecipeSchema = z.object({
|
|
found: z.boolean().describe("false if the image does not contain a recognizable recipe (e.g. random photo, no visible ingredients/instructions)"),
|
|
recipeType: z.enum(["dish", "drink"]).describe("\"drink\" only for a beverage/cocktail (no cooking step) — \"dish\" for anything else, including if unsure."),
|
|
title: z.string(),
|
|
description: z.string().optional(),
|
|
baseServings: z.number().int().min(1).optional(),
|
|
prepMins: z.number().int().min(0).optional(),
|
|
cookMins: z.number().int().min(0).optional(),
|
|
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
dietaryTags: dietaryTagsSchema.optional(),
|
|
ingredients: z.array(ingredientSchema(z.string())),
|
|
steps: z.array(stepSchema),
|
|
});
|
|
|
|
export type ImportedRecipe = z.infer<typeof ImportedRecipeSchema>;
|
|
|
|
const LANG: Record<string, string> = { en: "English", fr: "French" };
|
|
|
|
export async function importFromPhoto(
|
|
imageBase64: string,
|
|
mimeType: "image/jpeg" | "image/png" | "image/webp",
|
|
config?: AiConfig,
|
|
locale?: string,
|
|
): Promise<ImportedRecipe> {
|
|
const model = resolveModel(config);
|
|
const lang = LANG[locale ?? "en"] ?? "English";
|
|
const langInstruction = lang !== "English" ? ` Write the entire recipe (title, description, ingredient names, step instructions) in ${lang}, regardless of the language used in the image.` : "";
|
|
|
|
const { object } = await generateObject({
|
|
model,
|
|
schema: ImportedRecipeSchema,
|
|
system:
|
|
`You are a recipe extraction specialist. Extract the complete recipe from the provided image. Be precise with ingredient quantities and cooking instructions. If the image does not contain a recognizable recipe (no visible ingredients or cooking instructions), set found to false and leave the other fields minimal/empty. Set recipeType to "drink" only for a beverage/cocktail with no cooking involved — never set cookMins for a drink.${langInstruction}`,
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{ type: "image", image: imageBase64, mediaType: mimeType },
|
|
{ type: "text", text: "Extract the complete recipe from this image." },
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
if (object.recipeType === "drink") {
|
|
return { ...object, cookMins: undefined };
|
|
}
|
|
return object;
|
|
}
|