大家好,今天我要分享一个超有趣的项目开发经历——一个能"看懂"图片并教你说英语的AI应用!想象一下:拍张香蕉照片,手机立刻告诉你"banana"并朗读例句,是不是很神奇?下面让我拆解整个开发过程,保证让你笑着学会React和Blob的妙用!
🌟 项目核心:图片变单词的魔法
// App.jsx中的魔法咒语
const userPrompt = `分析图片内容,找出最能描述图片的一个英文单词,尽量选择更简单的A1~A2的词汇。
返回JSON数据:
{
"image_discription": "图片描述",
"representative_word": "图片代表的英文单词",
"example_sentence": "结合英文单词和图片描述,给出一个简单的例句",
"explaination": "结合图片解释英文单词,段落以Look at...开头,将段落分句,每一句单独一行,解释的最后给一个日常生活有关的问句",
"explaination_replys": ["根据explaination给出的回复1", "根据explaination给出的回复2"]
}`;
这个提示词就像给AI的"寻宝地图",告诉它:
1️⃣ 找出图片中最具代表性的A1~A2级单词(如"apple"而不是"malus domestica")
2️⃣ 生成包含该单词的简单例句
3️⃣ 用"Look at..."开头的分段解释
4️⃣ 准备两个互动回复(方便后续扩展对话功能)
上述userPrompt就是我们驱动大模型的prompt,也就是指令,通常是给它一个身份,任务步骤,限制,从而驱动我们的大模型
当用户上传图片时,会发生这样的奇幻旅程:
这就是我们这个项目的最重要的功能,拍照提取英文单词,最后生成的例句让大模型给读出来,满足了用户随时随地学英语的需求
🔧 核心技术揭秘
🎤 音频魔法:Base64变声波
最酷的技术当属Blob对象的运用!
const getAudioUrl = (base64Data) => {
// Base64解码大法
const byteCharacters = atob(base64Data);
// 字符转ASCII码(0-255)
const byteArrays = [];
for (let i = 0; i < byteCharacters.length; i++) {
byteArrays.push(byteCharacters.charCodeAt(i));
}
// 创建Blob音频对象
const blob = new Blob([new Uint8Array(byteArrays)], { type: 'audio/mp3' });
// 生成临时播放URL
return URL.createObjectURL(blob);
}
这个过程就像把一封密信变成声音:
-
解码:
atob()解开Base64的"密码锁" 🔓⚠️ 注意:如果传入的
base64Data包含了 Data URI Scheme 前缀(如"data:audio/mp3;base64,..."),你需要先去掉前缀再传给这个函数。 -
转码:``把字符变成0-255的数字(ASCII码)
-
塑形:用
Uint8Array把数字变成二进制数组 -
造物:
new Blob()把二进制捏成MP3音频 -
开门:
URL.createObjectURL()创建临时播放门牌号 🚪
我们把一个图片的base64编码格式作为形参调用形参,可以看看每一步的结果
// 编解码
// base64? google 推出的图片格式 图片二进制的表达
// 字符 base64编码 来表达图片
// a 代表ascii => binary(二进制编码)
// 将base64编码的字符串 解码为原始的二进制数据
const base64Data = atob('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')
console.log(base64Data)
const byteArray = []
// utf8 编码 实例化 unit8 特殊类型的数组 0-255 之间的整数
for (let i = 0; i < base64Data.length; i++) {
byteArray[i] = base64Data.charCodeAt(i)
}
// 全是数字
console.log(byteArray);
// png,doc,pdf ....
// 实例化一个二进制对象 底层的二进制对象
// html5 里面的王者对象 文件切片
const blob = new Blob([new Uint8Array(byteArray)], { type: 'image/jpeg' })
console.log(blob);
// html5 URL 对象
const imageUrl = URL.createObjectURL(blob)
console.log(imageUrl);
document.getElementById('blobImage').src = imageUrl
可以看到最后返回给我们一个临时 URL,只能在我们的本地访问,可以直接访问我们的图片
那么我们将base64音频数据转换成可播放的url有什么好处呢?
将使用 Base64 音频数据并转换为可播放 URL 优点:
| 优点编号 | 优点描述 | 说明 |
|---|---|---|
| ✅ 1 | 无需额外请求服务器资源 | Base64 数据已内嵌在响应中,减少 HTTP 请求,提升加载速度。 |
| ✅ 2 | 便于封装和传递音频数据 | Base64 是字符串格式,易于在网络传输、本地缓存或数据库存储。 |
| ✅ 3 | 避免跨域限制(CORS) | 使用 URL.createObjectURL(blob) 生成的地址不触发 CORS 问题。 |
| ✅ 4 | 支持即时播放和预览 | 用户上传音频后可立即播放,适合预览、录音回放等场景。 |
| ✅ 5 | 兼容现代浏览器 | 所有主流浏览器都支持 atob、Blob 和 createObjectURL 等方法。 |
🖼 图片上传:文件变数据流
在PictureCard组件中,文件读取就像变魔术:
const uploadImgData = (e) => {
const file = (e.target).files?.[0];
if (!file) { return; }
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const data = reader.result;
setImgPreview(data);
uploadImg(data);
resolve(data);
}
reader.onerror = (error) => { reject(error); };
})
}
const file = (e.target).files?.[0];中?.是可选链操作符,如果对象file.[0]存在就返回其内容,否则就是undefinedconst reader = new FileReader();reader.readAsDataURL(file);是实例化一个对象,可以将图片内容,转换成base64格式const data = reader.result;setImgPreview(data);uploadImg(data);resolve(data);得到我们的base64数据,更换预览图片的src,将图片传给月之暗面的大模型,让其帮我们分析
🧠 AI视觉分析:给图片"开天眼"
在App组件中,我们调用Kimi API进行图片理解:
await fetch('https://api.moonshot.cn/v1/chat/completions', {
method: 'POST',
headers: { Authorization: `Bearer ${import.meta.env.VITE_KIMI_API_KEY}` },
body: JSON.stringify({
model: 'moonshot-v1-8k-vision-preview',
messages: [{
role: 'user',
content: [
{ type: "image_url", image_url: { url: imageData } },
{ type: "text", text: userPrompt }
]
}]
})
})
Authorization: Bearer ${import.meta.env.VITE_KIMI_API_KEY} 授权码,放在我们的env.local文件中,防止泄露
这个请求包含两大关键信息:
- 图片数据:Base64格式的图片本体
- 提示词:详细说明需要AI做什么
返回的数据格式是JSON,更好处理
🎨 用户体验设计:让学习像玩游戏
📱 界面布局:极简三明治结构
<div className="container">
<PictureCard /> {/* 顶部卡片:图片+单词 */}
<div className="output"> {/* 底部输出区 */}
<div>{sentence}</div> {/* 例句展示 */}
<div className="details"> {/* 折叠详情 */}
<button onClick={() => setDetailExpand(!detailExpand)}>
Talk about it
</button>
{detailExpand && <div>...</div>} {/* 展开内容 */}
</div>
</div>
</div>
交互逻辑:
- 默认只显示单词卡片和例句
- 点击"Talk about it"展开详情区(类似游戏中的"更多信息"按钮)
- 详情区显示图片预览+分段解释+互动回复
🔊 语音交互:让单词"开口说话"
当获取到音频URL后,点击播放按钮触发:
// PictureCard组件中的播放逻辑
const playAudio = () => {
new Audio(audio).play(); // 创建隐形播放器
}
// 播放器UI
{audio && (
<div className="playAudio" onClick={playAudio}>
<img src="play-icon.png" alt="play" />
</div>
)}
这个设计暗藏小心机:
- 使用
new Audio()创建隐形播放器,无需在DOM中添加<audio>标签 - 用户点击图标时感觉像"戳一下单词就发声",符合移动端交互习惯
最后的效果,当我们点击播放按钮,就会帮我们把,例句给读出来
🧩 组件通信艺术:父子组件悄悄话
项目中完美展示了React的组件通信哲学:
// 父组件App.jsx
function App() {
const [word, setWord] = useState('请上传图片');
// 关键回调函数:传给子组件
const uploadImg = (imageData) => {
// 处理图片并更新状态
}
return <PictureCard uploadImg={uploadImg} />
}
// 子组件PictureCard.jsx
const PictuerCard = ({ uploadImg }) => {
const uploadImgData = (e) => {
// ...
uploadImg(data); // 调用父组件方法
}
}
这就像父子间的暗号:
- 父组件说:"儿砸,给你个对讲机(uploadImg函数)" 📞
- 子组件拍到照片后:"爸!这是照片Base64!(调用uploadImg)"
- 父组件开始AI分析并更新状态
- 子组件自动重新渲染显示新单词
💡 学到的超实用技巧
- Blob七十二变
通过index.html的示例,我深刻理解了Blob的威力:
// 将Base64图片转成真实URL
const base64Data = atob('UklGRiAHAABXRUJ...');
const byteArray = new Uint8Array(base64Data.length);
for (let i = 0; i < base64Data.length; i++) {
byteArray[i] = base64Data.charCodeAt(i); // 字符转ASCII
}
const blob = new Blob([byteArray], { type: 'image/jpeg' });
document.getElementById('blobImage').src = URL.createObjectURL(blob);
这个过程让我想到《哈利波特》的变形术:
atob():如"咒立停"解除Base64封印Uint8Array:像魔杖把字符变成数字矩阵new Blob():宛如赫敏用"恢复如初"重组物体createObjectURL():最后幻影移形创建访问入口
- 环境变量妙用
敏感信息全放在.env.local:
VITE_KIMI_API_KEY=sk-xxxxxx
VITE_AUDIO_ACCESS_TOKEN=xxxxxx
代码中通过import.meta.env调用:
const token = import.meta.env.VITE_AUDIO_ACCESS_TOKEN;
既安全又方便,像给钥匙配了个隐形保险箱 🔐
- 条件渲染的优雅之道
展开/收起功能用状态切换超简洁:
<button onClick={() => setDetailExpand(!detailExpand)}>
{detailExpand ? "收起详情" : "展开详情"}
</button>
{detailExpand && (
<div className="expand">
<img src={imgPreview} />
{explaination.map((line, i) => (
<div key={i}>{line}</div>
))}
</div>
)}
🚀 遇到的坑与填坑大法
- Base64数据过大问题
当用户上传高清图片时,Base64字符串可能导致API请求缓慢。解决方案:
// 在图片上传前压缩
reader.onload = () => {
const img = new Image();
img.src = reader.result;
img.onload = () => {
const canvas = document.createElement('canvas');
// 按比例缩小画布
ctx.drawImage(img, 0, 0, width, height);
const compressedData = canvas.toDataURL('image/jpeg', 0.7);
}
}
2. 音频URL内存泄漏
每次生成新音频时需释放旧URL:
// App.jsx中
useEffect(() => {
return () => {
if (audio) URL.revokeObjectURL(audio); // 组件卸载时释放
};
}, [audio]);
3. 移动端文件上传样式
隐藏默认的<input type="file">,用<label>定制按钮:
#selectImage { /* 隐藏原始输入 */
position: absolute;
opacity: 0;
}
.upload { /* 定制上传区域 */
border: 2px dashed #ccc;
padding: 20px;
text-align: center;
}
🌈 项目未来升级方向
- 语音输入
添加SpeechRecognition API实现对话练习:
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();
recognition.onresult = (event) => {
const speechResult = event.results[0][0].transcript;
// 验证用户是否读对单词
};
2. 单词收藏夹
利用localStorage保存生词:
const [favorites, setFavorites] = useState(() => {
const saved = localStorage.getItem('favorites');
return saved ? JSON.parse(saved) : [];
});
useEffect(() => {
localStorage.setItem('favorites', JSON.stringify(favorites));
}, [favorites]);
3. AR实时识别
整合TensorFlow.js实现摄像头动态识别:
const model = await cocoSsd.load();
const predictions = await model.detect(document.getElementById('webcam'));
💎 总结:Blob是块宝
通过这个项目,我深刻体会到Blob对象的前端核心地位:
- 文件转换:
File → Base64 → Blob → ObjectURL - 音视频处理:无需服务器中转
- 大数据切片:适合分片上传下载
Blob三定律:
- 一切文件皆可Blob
- 一切Blob皆可URL化
- 一切URL皆可释放(revokeObjectURL)
最后用一张图总结技术架构:
这个项目就像用代码搭乐高:Blob是基础积木,React是组装说明书,AI是电动马达。当你看到用户对着香蕉图片惊喜地说出"banana"时,那种成就感比吃十根香蕉还香甜!🍌