前言
今天我们使用coze工作流写一个关于宠物App智能衣柜的功能。
效果图
工作流设计
各节点展示
用户需要上传两张图片———宠物图和衣服图,开始节点传入三个参数:
添加插件:图片理解
text填入: 这应该是一张宠物图片,请详细描述该宠物的外貌特征,包括体型,表情,毛色,毛发,种类等
点击输出的右上角查看示例可以查看输入输出参数的说明:
添加一个大模型,命名为“宠物特征提取”,将上个插件的
response_for_model参数作为输入,写上系统提示词和用户提示词
接着衣服图片的流程和宠物图片类似,不再赘述
衣服特征提取的系统提示词:你是时尚设计师,负责从衣服图片和描述中,提取出衣服的外观特征,例如主色、材质、款式、图案、风格、冬装或夏装、高低领等。请用简练准确的词语总结衣服的视觉特性。
再添加一个图像生成,将特征提取大模型输出的pet_features和clothes_features作为输入,还有开始节点的风格style
正向提示词:希望生成什么样的
以{{pet_features}}为主体,请将衣服智能融合到宠物图片上,保持宠物四足站姿。生成风格为:{{style}}。穿搭效果需贴合宠物自然体型,不要拟人化。
# 动物形象描述
{{pet_features}}
# 衣服描述
{{clothes_features}}
# 注意
- 衣服需自然贴合宠物体型,宠物保持四足站立姿势
- 强化宠物独特外貌特征,提高辨识度
- 不要拟人化,不出现任何人类特征或姿势
-背景不要杂乱
负向提示词:不要生成以下
拟人化,宠物站立成人姿势,出现人类手脚,夸张表情,添加多余配饰,出现背景干扰物,虚化或模糊衣服细节,漏掉衣服特征。
最后,结束节点:
代码解析
<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事件返回结果。- 用户上传图片时,通过
setPetPreview或setClothesPreview更新预览图像。
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)
}
}
具体步骤:
input.files: 这是一个FileList对象,包含用户选择的文件。我们通过input.files[0]获取第一个文件。FileReader.readAsDataURL(file): 这行代码将文件读取为一个 Data URL。Data URL 是一种将二进制数据编码为字符串格式的方式,通常用于图像文件。在这个示例中,它将返回一个 Base64 编码的图像数据 URL。onload事件: 当文件读取成功时,onload事件被触发,e.target.result包含了读取后的数据(即 Data URL)。- 设置状态:
setPetPreview或setClothesPreview被调用来更新宠物或衣服的预览图片。
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
}
具体步骤:
- 创建
FormData实例:const formData = new FormData()。这是一个空的表单数据对象,可以用来添加字段和文件。 append()方法:formData.append('file', input.files[0])将文件添加到FormData对象中。'file'是表单字段名,input.files[0]是文件对象。- 发送请求: 使用
fetch方法发送一个POST请求,将FormData对象作为请求体 (body) 发送到服务器。服务器会解析这个FormData,从中提取文件。 - 处理响应: 响应体被解析为 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