打造宠物专属时尚:基于AI的宠物智能衣柜设计

147 阅读7分钟

前言

今天我们使用coze工作流写一个关于宠物App智能衣柜的功能。

效果图

80c67deb9f178cffa2dde9f22b781646.png

工作流设计

image.png

各节点展示

用户需要上传两张图片———宠物图和衣服图,开始节点传入三个参数:

image.png

添加插件:图片理解

image.png

image.png

text填入: 这应该是一张宠物图片,请详细描述该宠物的外貌特征,包括体型,表情,毛色,毛发,种类等

点击输出的右上角查看示例可以查看输入输出参数的说明:

image.png

添加一个大模型,命名为“宠物特征提取”,将上个插件的response_for_model参数作为输入,写上系统提示词用户提示词

image.png

接着衣服图片的流程和宠物图片类似,不再赘述

衣服特征提取的系统提示词:你是时尚设计师,负责从衣服图片和描述中,提取出衣服的外观特征,例如主色、材质、款式、图案、风格、冬装或夏装、高低领等。请用简练准确的词语总结衣服的视觉特性。

再添加一个图像生成,将特征提取大模型输出的pet_featuresclothes_features作为输入,还有开始节点的风格style image.png 正向提示词:希望生成什么样的

以{{pet_features}}为主体,请将衣服智能融合到宠物图片上,保持宠物四足站姿。生成风格为:{{style}}。穿搭效果需贴合宠物自然体型,不要拟人化。
# 动物形象描述
{{pet_features}}

# 衣服描述
{{clothes_features}}

# 注意
- 衣服需自然贴合宠物体型,宠物保持四足站立姿势
- 强化宠物独特外貌特征,提高辨识度
- 不要拟人化,不出现任何人类特征或姿势
-背景不要杂乱

负向提示词:不要生成以下

拟人化,宠物站立成人姿势,出现人类手脚,夸张表情,添加多余配饰,出现背景干扰物,虚化或模糊衣服细节,漏掉衣服特征。

最后,结束节点:

image.png

代码解析

<input
    id="selectPetImage"
    ref={uploadPetRef}
    type="file"
    accept="image/*"
    onChange={() => updateImageData("pet")}
    className={styles.fileInput}
/>
<label htmlFor="selectPetImage" className="upload">
    <img src={petPreview} alt="宠物图片预览" className={styles.preview} />
</label>

这里利用 HTML 的 <label htmlFor="..."> 特性,把不可见的 type="file" 输入框与一个可视元素(如图片)绑定,实现标签关联输入,无障碍使用。

useRef获取input dom元素,当点击文件上传,调用updateImageData()方法。

<button 
    className={styles.button} 
    onClick={generate}
    disabled={isGenerating}
>
    {isGenerating ? <Loading type="spinner" color="#fff" /> : ''} 
    {isGenerating ? '生成中...' : '生成'}
</button>

点击上传按钮调用generate方法。

文件上传的过程

1. 文件预览(FileReader API)

用户上传宠物和衣服的图片后,使用 FileReader API 读取文件内容,生成 Base64 编码的 Data URL 用于图片预览。

  • FileReader.readAsDataURL(file) 允许将文件读取为 Base64 编码的图像数据,并通过 onload 事件返回结果。
  • 用户上传图片时,通过 setPetPreviewsetClothesPreview 更新预览图像。

2. 表单数据上传(FormData API)

FormData 用于创建一个表单数据对象,将文件添加到表单并通过 fetch 发送至服务器。

  • formData.append('file', input.files[0]) : 将上传的文件添加到 FormData 中,file 是表单字段的名称。
  • fetch 用于发送 HTTP 请求,通过 POST 方法将 FormData 对象作为请求体上传文件。

3. 生成图像

在文件上传成功后,调用 generate 方法发起生成请求。请求包括:

  • 上传的宠物和衣服的文件 ID
  • 选定的风格(例如现实风格 realistic

在请求中,fetch 会通过 POST 请求将数据发送到服务器,生成图片,并返回图像的 URL。

API解析

这里涉及到文件上传、读取和处理的 API,主要包括以下几个部分:FileReader API 和 FormData API。我们可以分解一下这些文件相关的操作,详细讲解它们如何工作。

1. FileReader API

FileReader 是一个用于读取文件(如图片、文档等)的 API。它允许你以多种格式(如文本、数据URL、二进制字符串等)读取文件。

用法:
const reader = new FileReader();
reader.readAsDataURL(file);  // 读取文件,返回一个 Base64 编码的 Data URL

reader.onload = (e) => {
    console.log(e.target.result);  // 文件内容
}

在这段代码中,我们使用了 FileReader 来读取用户上传的图片文件,并生成图片的预览。

const updateImageData = (type) => {
    const input = type === "pet" ? uploadPetRef.current : uploadClothesRef.current
    if (!input || !input.files || input.files.length === 0) return
    const file = input.files[0]
    
    // 创建 FileReader 实例
    const reader = new FileReader()
    
    // 读取文件为 Data URL(Base64 编码)
    reader.readAsDataURL(file)
    
    // 读取成功后,执行回调函数
    reader.onload = (e) => {
        if (type === "pet") setPetPreview(e.target.result)
        else setClothesPreview(e.target.result)
    }
}
具体步骤:
  1. input.files: 这是一个 FileList 对象,包含用户选择的文件。我们通过 input.files[0] 获取第一个文件。
  2. FileReader.readAsDataURL(file) : 这行代码将文件读取为一个 Data URL。Data URL 是一种将二进制数据编码为字符串格式的方式,通常用于图像文件。在这个示例中,它将返回一个 Base64 编码的图像数据 URL。
  3. onload 事件: 当文件读取成功时,onload 事件被触发,e.target.result 包含了读取后的数据(即 Data URL)。
  4. 设置状态: setPetPreviewsetClothesPreview 被调用来更新宠物或衣服的预览图片。

2. FormData API

FormData 是一个用于构建可以通过 HTTP 请求发送的表单数据对象,它通常与 fetch 一起使用,支持上传文件和其他表单数据。

用法:
const formData = new FormData();
formData.append('file', file);  // 将文件添加到 FormData 对象中

fetch(url, {
    method: 'POST',
    body: formData,  // 发送 FormData 对象
})

在这段代码中,FormData 被用来构建上传文件的 HTTP 请求体。

const uploadFile = async (type) => {
    const formData = new FormData()
    const input = type === "pet" ? uploadPetRef.current : uploadClothesRef.current
    if (!input || !input.files || input.files.length <= 0) return null
    
    // 将选择的文件添加到 FormData 中
    formData.append('file', input.files[0])

    const res = await fetch(uploadUrl, {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${patToken}`,
        },
        body: formData,  // 将 FormData 作为请求体发送
    })
    const ret = await res.json()
    if (ret.code !== 0) {
        setStatus(ret.msg)
        return null
    }
    return ret.data.id
}
具体步骤:
  1. 创建 FormData 实例: const formData = new FormData()。这是一个空的表单数据对象,可以用来添加字段和文件。
  2. append() 方法: formData.append('file', input.files[0]) 将文件添加到 FormData 对象中。'file' 是表单字段名,input.files[0] 是文件对象。
  3. 发送请求: 使用 fetch 方法发送一个 POST 请求,将 FormData 对象作为请求体 (body) 发送到服务器。服务器会解析这个 FormData,从中提取文件。
  4. 处理响应: 响应体被解析为 JSON 格式。如果上传成功,返回文件的 ID。

3. fetch API

fetch 是 JavaScript 中用于进行 HTTP 请求的 API。在文件上传和数据提交过程中,fetch 经常与 FormData 一起使用。我们在上传文件和提交生成任务时都使用了它。

用法:
fetch(url, {
    method: 'POST',
    headers: {
        'Authorization': 'Bearer token',  // 传递认证 token(如果需要)
    },
    body: formData  // 请求体,包含表单数据(包括文件)
})

4. 小结

  • FileReader:读取文件的内容,并将其转换为 Data URL(Base64 格式),适合用于文件预览。
  • FormData:构建和发送多部分表单数据,适用于文件上传。
  • fetch:用来发送 HTTP 请求,上传文件时通常与 FormData 一起使用。

文件的处理流程就是通过 FileReader 读取文件、通过 FormData 构建请求体、使用 fetch 将文件发送到服务器。

完整代码

import useTitle from '@/hooks/useTitle'
import { useRef, useState } from 'react'
import styles from './smartimage.module.css'
import { Loading } from 'react-vant'

const SmartImage = () => {
    useTitle('智能衣柜')
    const uploadPetRef = useRef(null)
    const uploadClothesRef = useRef(null)
    const uploadUrl = 'https://api.coze.cn/v1/files/upload'
    const patToken = import.meta.env.VITE_PAT_TOKEN
    const workflowUrl = 'https://api.coze.cn/v1/workflow/run'
    const workflow_id = '7533236076422512694'

    const [petPreview, setPetPreview] = useState('https://res.bearbobo.com/resource/upload/W44yyxvl/upload-ih56twxirei.png')
    const [clothesPreview, setClothesPreview] = useState('https://res.bearbobo.com/resource/upload/W44yyxvl/upload-ih56twxirei.png')
    const [imgUrl, setImgUrl] = useState('')
    const [status, setStatus] = useState('')
    const [isGenerating, setIsGenerating] = useState(false)

    // 默认使用写实风格
    const style_en = "realistic"

    // 图片预览
    const updateImageData = (type) => {
        const input = type === "pet" ? uploadPetRef.current : uploadClothesRef.current
        if (!input || !input.files || input.files.
            length === 0) return
        const file = input.files[0]
        // 异步读取文件内容
        const reader = new FileReader()
        reader.readAsDataURL(file) // 读取为 Base64 编码的 Data URL
        reader.onload = (e) => {
            if (type === "pet") setPetPreview(e.target.result)
            else setClothesPreview(e.target.result)
        }
    }

    // 将用户选择的图片文件上传到服务器,并返回文件 ID
    const uploadFile = async (type) => {
        // 表单数据对象,构造 HTTP 请求体
        const formData = new FormData()
        const input = type === "pet" ? uploadPetRef.current : uploadClothesRef.current
        if (!input || !input.files || input.files.length <= 0) return null
        formData.append('file', input.files[0])
        const res = await fetch(uploadUrl, {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${patToken}`,
            },
            body: formData,
        })
        const ret = await res.json()
        if (ret.code !== 0) {
            setStatus(ret.msg)
            return null
        }
        return ret.data.id
    }

    const generate = async () => {
        setIsGenerating(true)
        setStatus("宠物图片上传中...")
        const pet_file_id = await uploadFile("pet")
        if (!pet_file_id) {
            setStatus("宠物图片上传失败")
            setIsGenerating(false)
            return
        }
        setStatus("衣服图片上传中...")
        const clothes_file_id = await uploadFile("clothes")
        if (!clothes_file_id) {
            setStatus("衣服图片上传失败")
            setIsGenerating(false)
            return
        }
        setStatus("图片上传成功,正在生成...")

        // 这里参数名和格式要和 workflow 对齐
        const parameters = {
            image_pet: { file_id: pet_file_id },
            image_clothes: { file_id: clothes_file_id },
            style: style_en
        }

        try {
            const res = await fetch(workflowUrl, {
                method: 'POST',
                headers: {
                    'Authorization': `Bearer ${patToken}`,
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ workflow_id, parameters }),
            })
            const ret = await res.json()
            
            if (ret.code !== 0) {
                setStatus(ret.msg)
                setIsGenerating(false)
                return
            }
            
            const data = JSON.parse(ret.data)
            setImgUrl(data.data)
            setStatus('')
        } catch (e) {
            setStatus("图片生成失败!请重试。")
            console.error("生成失败:", e)
        } finally {
            setIsGenerating(false)
        }
    }

    return (
        <div className={styles.container}>
            <header className={styles.header}>
                <h1>智能衣柜</h1>
                <p className={styles.desc}>
                    为您的宠物打造专属时尚造型
                </p>
            </header>
            
            <div className={styles.input}>
                <div className={styles.fileBox}>
                    <div className={styles.title}>
                        <h3>宠物全身照</h3>
                        <p>请上传清晰的宠物正面站立照片</p>
                    </div>
                    <input
                        id="selectPetImage"
                        ref={uploadPetRef}
                        type="file"
                        accept="image/*"
                        onChange={() => updateImageData("pet")}
                        className={styles.fileInput}
                    />
                    <label htmlFor="selectPetImage" className="upload">
                        <img src={petPreview} alt="宠物图片预览" className={styles.preview} />
                    </label>
                </div>
                
                <div className={styles.fileBox}>
                    <div className={styles.title}>
                        <h3>衣服图片</h3>
                        <p>请上传衣服的正面平铺图</p>
                    </div>
                    <input
                        id="selectClothesImage"
                        ref={uploadClothesRef}
                        type="file"
                        accept="image/*"
                        onChange={() => updateImageData("clothes")}
                        className={styles.fileInput}
                    />
                    <label htmlFor="selectClothesImage" className="upload">
                        <img src={clothesPreview} alt="衣服图片预览" className={styles.preview} />
                    </label>
                </div>
                
                <div className={styles.generate}>
                    <button 
                        className={styles.button} 
                        onClick={generate}
                        disabled={isGenerating}
                    >
                        {isGenerating ? <Loading type="spinner" color="#fff" /> : ''} 
                        {isGenerating ? '生成中...' : '生成'}
                    </button>
                </div>
            </div>
            
            <div className={styles.output}>
                <div className={styles.generated}>
                    {imgUrl ? (
                        
                        <img src={imgUrl} alt="生成效果预览" className={styles.resultImg} />
                    ) : status ? (
                        <div className={styles.status}>{status}</div>
                    ) : (
                        <div className={styles.emptyState}>
                            <p>上传图片并点击生成按钮</p>
                        </div>
                    )}
                </div>
            </div>
        </div>
    )
}

export default SmartImage