feat: add ai generate recipe

This commit is contained in:
YunYouJun
2024-03-13 16:43:58 +08:00
parent d42e3cf65d
commit d25148c731
9 changed files with 297 additions and 1 deletions

View File

@@ -0,0 +1,144 @@
<script lang="ts" setup>
import type { StuffItem } from '~/types'
import { meat, staple, vegetable } from '~/data/food'
import { useEmojiAnimation } from '~/composables/animation'
import { getRecipeImage } from '~/utils/api'
const rStore = useRecipeStore()
const curStuff = computed(() => rStore.selectedStuff)
const recipeBtnRef = ref<HTMLButtonElement>()
const { playAnimation } = useEmojiAnimation(recipeBtnRef)
const gtm = useGtm()
const recipePanelRef = ref()
const { isVisible, show } = useInvisibleElement(recipePanelRef)
function toggleStuff(item: StuffItem, category = '', _e?: Event) {
rStore.toggleStuff(item.name)
if (curStuff.value.includes(item.name))
playAnimation(item.emoji)
gtm?.trackEvent({
event: 'click',
category: `${category}_${item.name}`,
action: 'click_stuff',
label: '食材',
})
gtm?.trackEvent({
event: 'click_stuff',
action: item.name,
})
}
// cook recipe
const recipeImg = ref('')
async function cook() {
const foods = rStore.selectedStuff
const img = await getRecipeImage(foods)
recipeImg.value = img
}
</script>
<template>
<div>
<h2 m="t-4" text="xl" font="bold" p="1">
🥘 先选一下食材
</h2>
<div>
<h2 opacity="90" text="base" font="bold" p="1">
🥬 菜菜们
</h2>
<div>
<VegetableTag
v-for="item, i in vegetable" :key="i"
:active="curStuff.includes(item.name)"
@click="toggleStuff(item, 'vegetable')"
>
<span v-if="item.emoji" class="inline-flex">{{ item.emoji }}</span>
<span v-else-if="item.image" class="inline-flex">
<img class="inline-flex" w="2" h="2" width="10" height="10" :src="item.image" :alt="item.name">
</span>
<span class="inline-flex" m="l-1">{{ item.name }}</span>
</VegetableTag>
</div>
</div>
<div m="y-4">
<h2 opacity="90" text="base" font="bold" p="1">
🥩 肉肉们
</h2>
<div>
<MeatTag
v-for="item, i in meat" :key="i"
:active="curStuff.includes(item.name)"
@click="toggleStuff(item, 'meat')"
>
<span>{{ item.emoji }}</span>
<span m="l-1">{{ item.name }}</span>
</MeatTag>
</div>
</div>
<div m="y-4">
<h2 opacity="90" text="base" font="bold" p="1">
🍚 主食也要一起下锅吗不选也行
</h2>
<div>
<StapleTag
v-for="item, i in staple" :key="i"
:active="curStuff.includes(item.name)"
@click="toggleStuff(item, 'staple')"
>
<span>{{ item.emoji }}</span>
<span m="l-1">{{ item.name }}</span>
</StapleTag>
</div>
</div>
<!-- <div m="t-4">
<h2 text="xl" font="bold" p="1">
🍳 再选一下厨具
</h2>
<div>
<ToolTag
v-for="item, i in tools" :key="i"
:active="curTool === item.name"
@click="rStore.clickTool(item)"
>
<span v-if="item.emoji" class="inline-flex">
{{ item.emoji }}
</span>
<span v-else-if="item.icon" class="inline-flex">
<div :class="item.icon" />
</span>
<span class="inline-flex" m="l-1">{{ item.label || item.name }}</span>
</ToolTag>
</div>
</div> -->
<Transition>
<BasketButton ref="recipeBtnRef" :is-visible="isVisible" @click="show" />
</Transition>
<button
class="rounded bg-yellow px-4 py-2 text-orange-900 font-black shadow hover:shadow-md active:shadow-inset"
@click="cook()"
>
做美食 🥘
</button>
<div
class="recipe-panel relative shadow transition"
m="x-2 y-4" p="2"
bg="gray-400/8"
>
<div text="xl" font="bold" p="1">
🍲 来看看制作出的美食吧
</div>
<div class="cook-recipes text-center" p="2">
<img class="m-auto w-25 rounded shadow transition hover:shadow-md" src="https://yunyoujun.cn/images/avatar.jpg" alt="recipes">
</div>
</div>
</div>
</template>

View File

@@ -25,7 +25,7 @@ const filteredRecipes = computedAsync(async () => {
<template>
<YlfIconButton
absolute right-3 top-5
absolute right-3 top-4
class="icon-btn hover:text-yellow-400 !outline-none"
text-xl
title="切换" @click="openModal"

View File

@@ -7,63 +7,78 @@ export const vegetable: StuffItem[] = [
{
name: '土豆',
emoji: '🥔',
en: 'potato',
},
{
name: '胡萝卜',
emoji: '🥕',
en: 'carrot',
},
{
name: '花菜',
emoji: '🥦',
en: 'cauliflower',
},
{
name: '白萝卜',
emoji: '🥣',
en: 'radish',
},
{
name: '西葫芦',
emoji: '🥒',
en: 'zucchini',
},
{
name: '番茄',
emoji: '🍅',
alias: '西红柿',
en: 'tomato',
},
{
name: '芹菜',
emoji: '🥬',
en: 'celery',
},
{
name: '黄瓜',
emoji: '🥒',
en: 'cucumber',
},
{
name: '洋葱',
emoji: '🧅',
en: 'onion',
},
{
name: '莴笋',
emoji: '🎍',
en: 'lettuce',
},
{
name: '菌菇',
emoji: '🍄',
en: 'mushroom',
},
{
name: '茄子',
emoji: '🍆',
en: 'eggplant',
},
{
name: '豆腐',
emoji: '🍲',
en: 'tofu',
},
{
name: '包菜',
emoji: '🥗',
en: 'cabbage',
},
{
name: '白菜',
emoji: '🥬',
en: 'cabbage',
},
]
@@ -74,42 +89,52 @@ export const meat: StuffItem[] = [
{
name: '午餐肉',
emoji: '🥓',
en: 'bacon',
},
{
name: '香肠',
emoji: '🌭',
en: 'sausage',
},
{
name: '腊肠',
emoji: '🌭',
en: 'sausage',
},
{
name: '鸡肉',
emoji: '🐤',
en: 'chicken',
},
{
name: '猪肉',
emoji: '🐷',
en: 'pork',
},
{
name: '鸡蛋',
emoji: '🥚',
en: 'egg',
},
{
name: '虾',
emoji: '🦐',
en: 'shrimp',
},
{
name: '牛肉',
emoji: '🐮',
en: 'beef',
},
{
name: '骨头',
emoji: '🦴',
en: 'bone',
},
{
name: '鱼Todo',
emoji: '🐟',
en: 'fish',
},
]
@@ -120,18 +145,22 @@ export const staple: StuffItem[] = [
{
name: '面食',
emoji: '🍝',
en: 'noodles',
},
{
name: '面包',
emoji: '🍞',
en: 'bread',
},
{
name: '米',
emoji: '🍚',
en: 'rice',
},
{
name: '方便面',
emoji: '🍜',
en: 'instant noodles',
},
]

6
layouts/preview.vue Normal file
View File

@@ -0,0 +1,6 @@
<template>
<main class="cook-main text-center text-gray-700 dark:text-gray-200" p="t-8 b-$cook-bottom-menu-height">
<slot />
<DarkToggle absolute left-3 top-4 />
</main>
</template>

25
pages/ai.vue Normal file
View File

@@ -0,0 +1,25 @@
<script lang="ts" setup>
definePageMeta({
layout: 'preview',
})
const rStore = useRecipeStore()
</script>
<template>
<div text-4xl>
<button
class="cursor-pointer transition active:text-green-800 hover:(text-green-600)"
title="重置"
@click="rStore.reset"
>
<div v-if="rStore.selectedStuff.length" i-mdi-pot-steam-outline />
<div v-else i-mdi-pot-mix-outline />
</button>
</div>
<p text="sm" m="b-4">
好的今天我们来做菜
</p>
<AiChooseFood />
</template>

3
server/api/README.md Normal file
View File

@@ -0,0 +1,3 @@
# SD API
- [SD WebUI API](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/API)

View File

@@ -0,0 +1,70 @@
// http://localhost:3001/api/recipes/image/generate
import { meat, staple, vegetable } from '~/data/food'
// const sdBaseUrl = 'http://30.30.168.63:7860/'
const sdBaseUrl = 'https://85db1802ae46e57aab.gradio.live/'
const payload = {
// denoising_strength: 0,
prompt: 'food', // 提示词
negative_prompt: '', // 反向提示词
seed: -1, // 种子,随机数
batch_size: 2, // 每次张数
n_iter: 1, // 生成批次
steps: 50, // 生成步数
cfg_scale: 7, // 关键词相关性
width: 512, // 宽度
height: 512, // 高度
restore_faces: false, // 脸部修复
tiling: false, // 可平埔
// override_settings: {
// sd_model_checkpoint: 'wlop-any.ckpt [7331f3bc87]',
// }, // 一般用于修改本次的生成图片的stable diffusion 模型,用法需保持一致
// script_args: [
// 0,
// true,
// true,
// 'LoRA',
// 'dingzhenlora_v1(fa7c1732cc95)',
// 1,
// 1,
// ], // 一般用于lora模型或其他插件参数如示例我放入了一个lora模型 11为两个权重值一般只用到前面的权重值1
sampler_index: 'Euler', // 采样方法
}
// with api /sdapi/v1/txt2img
export interface Txt2ImgResponse {
images: string[]
}
const stuffItems = [
...vegetable,
...meat,
...staple,
]
export default defineEventHandler(async (e) => {
const body = await readBody(e)
const zhFoods = body.foods as string[]
const enFoods = zhFoods.map((food) => {
const item = stuffItems.find(item => item.name === food)
if (item)
return item.en
return ''
})
// TODO: 过滤 prompt 只能是食材
payload.prompt = `<lora:TODO:1>,food focus,transparent background,${enFoods.join(',')}`
// console.log(payload.prompt)
const data = await $fetch<Txt2ImgResponse>('/sdapi/v1/txt2img', {
baseURL: sdBaseUrl,
body: payload,
method: 'POST',
})
return data
})

View File

@@ -66,4 +66,9 @@ export interface StuffItem {
* 显示标签
*/
label?: string
/**
* 英文名称, for ai keyword
* @example 'potato'
*/
en?: string
}

14
utils/api/index.ts Normal file
View File

@@ -0,0 +1,14 @@
// filter prompt
export async function generateRecipeImage(foods: string[]) {
return $fetch('/api/recipes/image/generate', {
method: 'POST',
body: {
foods,
},
})
}
export async function getRecipeImage(foods: string[]) {
const data = await generateRecipeImage(foods)
return `data:image/png;base64,${data.images[0]}`
}