Files
Epicure/apps/web/lib/ai/features/import-photo.ts
T
Arnaud af92b8cc71 feat: auto-classify AI recipes as dish/drink (v0.33.0)
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>
2026-07-14 15:17:33 +02:00

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;
}