1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-07-24 15:49:42 +02:00

feat: Add brute strategy to ingredient processor (#744)

* fix UI column width

* words

* update parser to support diff strats

* add new model url

* make button more visible

* fix nutrition error

* feat(backend):  add 'brute' strategy for parsing ingredients

* satisfy linter

* update UI for creation page

* feat(backend):  log 422 errors in detail when not in PRODUCTION

* add strategy selector

Co-authored-by: Hayden <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-10-16 16:06:13 -08:00 committed by GitHub
parent 60908e5a88
commit 3b920babe3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 961 additions and 131 deletions

View file

@ -1,23 +1,69 @@
<template>
<v-container v-if="recipe">
<v-container>
<BaseCardSectionTitle title="Ingredients Processor"> </BaseCardSectionTitle>
<BaseCardSectionTitle title="Ingredients Processor">
To use the ingredient parser, click the "Parse All" button and the process will start. When the processed
ingredients are available, you can look through the items and verify that they were parsed correctly. The models
confidence score is displayed on the right of the title item. This is an average of all scores and may not be
wholey accurate.
<div class="mt-6">
Alerts will be displayed if a matching foods or unit is found but does not exists in the database.
</div>
<v-divider class="my-4"> </v-divider>
<div class="mb-n4">
Select Parser
<BaseOverflowButton
v-model="parser"
btn-class="mx-2"
:items="[
{
text: 'Natural Language Processor ',
value: 'nlp',
},
{
text: 'Brute Parser',
value: 'brute',
},
]"
/>
</div>
</BaseCardSectionTitle>
<v-card-actions class="justify-end">
<BaseButton color="info">
<BaseButton color="info" @click="fetchParsed">
<template #icon> {{ $globals.icons.foods }}</template>
Parse All
</BaseButton>
<BaseButton save> Save All </BaseButton>
<BaseButton save @click="saveAll"> Save All </BaseButton>
</v-card-actions>
</v-card>
<v-expansion-panels v-model="panels" multiple>
<v-expansion-panel v-for="(ing, index) in ingredients" :key="index">
<v-expansion-panel-header class="my-0 py-0">
{{ recipe.recipeIngredient[index].note }}
<v-expansion-panel v-for="(ing, index) in parsedIng" :key="index">
<v-expansion-panel-header class="my-0 py-0" disable-icon-rotate>
{{ ing.input }}
<template #actions>
<v-icon left :color="isError(ing) ? 'error' : 'success'">
{{ isError(ing) ? $globals.icons.alert : $globals.icons.check }}
</v-icon>
<div class="my-auto" :color="isError(ing) ? 'error-text' : 'success-text'">
{{ asPercentage(ing.confidence.average) }}
</div>
</template>
</v-expansion-panel-header>
<v-expansion-panel-content class="pb-0 mb-0">
<RecipeIngredientEditor v-model="ingredients[index]" />
<RecipeIngredientEditor v-model="parsedIng[index].ingredient" />
<v-card-actions>
<v-spacer></v-spacer>
<BaseButton
v-if="errors[index].foodError && errors[index].foodErrorMessage !== ''"
color="warning"
small
@click="createFood(ing.ingredient.food, index)"
>
{{ errors[index].foodErrorMessage }}
</BaseButton>
</v-card-actions>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
@ -26,19 +72,32 @@
</template>
<script lang="ts">
import { defineComponent, reactive, ref, toRefs, useRoute, watch } from "@nuxtjs/composition-api";
import { defineComponent, ref, useRoute, useRouter } from "@nuxtjs/composition-api";
import { Food, ParsedIngredient, Parser } from "~/api/class-interfaces/recipes";
import RecipeIngredientEditor from "~/components/Domain/Recipe/RecipeIngredientEditor.vue";
import { useApiSingleton } from "~/composables/use-api";
import { useRecipeContext } from "~/composables/use-recipe-context";
import { useFoods } from "~/composables/use-recipe-foods";
import { useUnits } from "~/composables/use-recipe-units";
import { RecipeIngredientUnit } from "~/types/api-types/recipe";
interface Error {
ingredientIndex: number;
unitError: Boolean;
unitErrorMessage: string;
foodError: Boolean;
foodErrorMessage: string;
}
export default defineComponent({
components: {
RecipeIngredientEditor,
},
setup() {
const state = reactive({
panels: null,
});
const panels = ref<number[]>([]);
const route = useRoute();
const router = useRouter();
const slug = route.value.params.slug;
const api = useApiSingleton();
@ -48,14 +107,150 @@ export default defineComponent({
const ingredients = ref<any[]>([]);
watch(recipe, () => {
const copy = recipe?.value?.recipeIngredient || [];
ingredients.value = [...copy];
});
// =========================================================
// Parser Logic
const parser = ref<Parser>("nlp");
const parsedIng = ref<any[]>([]);
async function fetchParsed() {
if (!recipe.value) {
return;
}
const raw = recipe.value.recipeIngredient.map((ing) => ing.note);
const { response, data } = await api.recipes.parseIngredients(parser.value, raw);
console.log({ response });
if (data) {
parsedIng.value = data;
console.log(data);
// @ts-ignore
errors.value = data.map((ing, index: number) => {
const unitError = !checkForUnit(ing.ingredient.unit);
const foodError = !checkForFood(ing.ingredient.food);
let unitErrorMessage = "";
let foodErrorMessage = "";
if (unitError || foodError) {
if (unitError) {
if (ing?.ingredient?.unit?.name) {
unitErrorMessage = `Create missing unit '${ing?.ingredient?.unit?.name || "No unit"}'`;
}
}
if (foodError) {
if (ing?.ingredient?.food?.name) {
foodErrorMessage = `Create missing food '${ing.ingredient.food.name || "No food"}'?`;
}
panels.value.push(index);
}
}
return {
ingredientIndex: index,
unitError,
unitErrorMessage,
foodError,
foodErrorMessage,
};
});
}
}
function isError(ing: ParsedIngredient) {
if (!ing?.confidence?.average) {
return true;
}
return !(ing.confidence.average >= 0.75);
}
function asPercentage(num: number) {
return Math.round(num * 100).toFixed(2) + "%";
}
// =========================================================
// Food and Ingredient Logic
const { foods, workingFoodData, actions } = useFoods();
const { units } = useUnits();
const errors = ref<Error[]>([]);
function checkForUnit(unit: RecipeIngredientUnit) {
if (units.value && unit?.name) {
return units.value.some((u) => u.name === unit.name);
}
return false;
}
function checkForFood(food: Food) {
if (foods.value && food?.name) {
return foods.value.some((f) => f.name === food.name);
}
return false;
}
async function createFood(food: Food, index: number) {
workingFoodData.name = food.name;
await actions.createOne();
errors.value[index].foodError = false;
}
// =========================================================
// Save All Loginc
async function saveAll() {
let ingredients = parsedIng.value.map((ing) => {
return {
...ing.ingredient,
};
});
console.log(ingredients);
ingredients = ingredients.map((ing) => {
if (!foods.value || !units.value) {
return ing;
}
// Get food from foods
const food = foods.value.find((f) => f.name === ing.food.name);
ing.food = food || null;
// Get unit from units
const unit = units.value.find((u) => u.name === ing.unit.name);
ing.unit = unit || null;
console.log(ing);
return ing;
});
if (!recipe.value) {
return;
}
recipe.value.recipeIngredient = ingredients;
const { response } = await api.recipes.updateOne(recipe.value.slug, recipe.value);
if (response?.status === 200) {
router.push("/recipe/" + recipe.value.slug);
}
}
return {
...toRefs(state),
api,
parser,
saveAll,
createFood,
errors,
actions,
workingFoodData,
isError,
panels,
asPercentage,
fetchParsed,
parsedIng,
recipe,
loading,
ingredients,
@ -69,5 +264,3 @@ export default defineComponent({
});
</script>
<style scoped>
</style>