当用户拍摄物品图片,系统自动识别核心物体并生成对应英文单词,实时提供真人级发音、情景例句和互动解释。
今天,我将结合Kimi多模态大模型的视觉理解和火山引擎TTS的语音合成技术,打造沉浸式语言学习工具。
注:由于文章内容篇幅过长,本文将通过两篇文章进行讲解
本文内容包括:代码详细解析,相关知识点讲解
完整源代码 和 大模型接口获取 请看《React智能前端:从零开始的AI识图学单词实战(一)》
一、App.jsx 代码详解
1.1 依赖导入模块
import { useState } from 'react'
import './App.css'
import PictureCard from './components/PictureCard';
import { generateAudio } from './lib/audio.js';
功能:引入应用所需的依赖项
分析:
useState:React的核心Hook,用于在函数组件中添加状态管理能力。它返回一个状态值和一个更新该状态的函数,触发组件重新渲染./App.css:导入组件的样式表,实现CSS模块化。Vite等构建工具会处理这些导入,将样式注入到页面中PictureCard:导入自定义子组件,体现React组件化思想。路径./components/PictureCard表示从当前目录的components文件夹导入generateAudio:从工具模块导入的异步函数,负责调用TTS服务生成单词发音。这种模块化设计使代码更易维护
1.2 AI指令模板定义
const userPrompt = `分析图片内容,找出最能描述图片的一个英文单词,尽量选择更简单的A1~A2的词汇。
返回JSON数据:
{
"image_discription": "图片描述",
"representative_word": "图片代表的英文单词",
"example_sentence": "结合英文单词和图片描述,给出一个简单的例句",
"explaination": "结合图片解释英文单词,段落以Look at...开头,将段落分句,每一句单独一行,解释的最后给一个日常生活有关的问句",
"explaination_replys": ["根据explaination给出的回复1", "根据explaination给出的回复2"]
}`;
功能:设计发送给AI的精确指令
分析:
- Prompt工程:通过结构化指令控制AI输出格式,这是大模型应用的关键技术
- JSON规范:明确定义了期望的返回数据结构,包含5个关键字段:
image_discription:图片描述文本representative_word:核心英文单词example_sentence:使用单词的例句explaination:分行的单词解释explaination_replys:互动回复选项
- 多行模板字符串:使用反引号(`)定义包含换行的文本块,保留所有空格和缩进
- 难度控制:明确要求选择A1-A2级别词汇(CEFR标准中的初级词汇)
1.3 状态管理模块
const [word, setWord] = useState('请上传图片');
const [sentence, setSentence] = useState('');
const [explainations, setExplainations] = useState([]);
const [expReply, setExpReply] = useState([]);
const [audio, setAudio] = useState('');
const [detailExpand, setDetailExpand] = useState(false);
const [imgPreview, setImgPreview] = useState('https://res.bearbobo.com/resource/upload/W44yyxvl/upload-ih56twxirei.png');
功能:声明和管理应用的状态数据
分析:
- useState:React的状态管理基础,每个useState调用创建一个状态变量和它的更新函数
- 状态分类:
- 学习数据状态:
word:当前识别的英文单词(初始提示文本)sentence:AI生成的例句explainations:单词解释的分行数组expReply:互动回复选项数组audio:单词发音的URL
- UI状态:
detailExpand:布尔值,控制详情区域的展开/折叠
- 媒体状态:
imgPreview:当前展示的图片URL或Base64数据
- 学习数据状态:
- 初始值设置:为每个状态提供合理的初始值,确保UI首次渲染正常显示
1.4 核心处理函数:uploadImg
const uploadImg = async (imageData) => {
// 1. 更新预览图和加载状态
setImgPreview(imageData);
setWord('分析中...');
// 2. 设置API请求参数
const endpoint = 'https://api.moonshot.cn/v1/chat/completions';
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${import.meta.env.VITE_KIMI_API_KEY}`
};
// 3. 调用Kimi多模态API
const response = await fetch(endpoint, {
method: 'POST',
headers: headers,
body: JSON.stringify({
model: 'moonshot-v1-8k-vision-preview',
messages: [
{
role: 'user',
content: [
{
type: "image_url",
image_url: { "url": imageData },
},
{
type: "text",
text: userPrompt,
}
]
}
],
stream: false
})
});
// 4. 处理API响应
const data = await response.json();
const replyData = JSON.parse(data.choices[0].message.content);
// 5. 更新学习数据状态
setWord(replyData.representative_word);
setSentence(replyData.example_sentence);
setExplainations(replyData.explaination.split('\n'));
setExpReply(replyData.explaination_replys);
// 6. 生成并设置单词发音
const audioUrl = await generateAudio(replyData.representative_word);
setAudio(audioUrl);
}
功能:处理图片上传、AI调用和响应处理
分析:
-
UI状态更新:
setImgPreview(imageData):立即显示用户上传的图片setWord('分析中...'):提供实时反馈,优化用户体验
-
API配置:
- 使用月之暗面提供的多模态API端点
- 通过
import.meta.env获取环境变量中的API密钥,避免硬编码敏感信息
-
Fetch API请求:
async/await处理异步操作,使代码保持线性逻辑- POST方法发送JSON格式请求体
- 多模态内容:同时发送图片(Base64)和文本指令
stream: false:等待完整响应而非流式传输
-
响应处理:
response.json():解析JSON格式的响应体- 从AI返回中选择第一条消息内容
JSON.parse():将字符串解析为JavaScript对象
-
状态更新:
- 使用setter函数更新所有学习数据状态
split('\n'):将解释文本按换行符拆分为数组,便于分行显示
-
音频生成:
- 调用
generateAudio工具函数生成单词发音 - 将获得的音频URL保存到状态中
- 调用
1.5 组件渲染逻辑
return (
<div className="container">
{/* 图片卡片组件 */}
<PictureCard
audio={audio}
word={word}
uploadImg={uploadImg}
/>
{/* 学习结果展示区 */}
<div className="output">
<div>{sentence}</div>
<div className="details">
{/* 展开/折叠按钮 */}
<button onClick={() => setDetailExpand(!detailExpand)}>Talk about it</button>
{/* 条件渲染:详细解释区域 */}
{
detailExpand ? (
<div className="expand">
<img src={imgPreview} alt="preview" />
{
explainations.map((explaination, index) => (
<div key={index} className="explaination">
{explaination}
</div>
))
}
</div>
) : (
<div className="fold" />
)
}
{/* 互动回复选项 */}
{
expReply.map((reply, index) => {
return <div key={index} className="reply">
{reply}
</div>
})
}
</div>
</div>
</div>
)
功能:渲染应用UI结构
分析:
-
组件结构:
- 根容器:
<div className="container">包裹整个应用 - 子组件:
<PictureCard>负责图片上传和单词展示 - 学习结果区:
<div className="output">展示学习内容
- 根容器:
-
组件通信:
- 通过props向子组件传递数据和回调:
audio:发音URLword:当前单词uploadImg:图片处理函数
- 通过props向子组件传递数据和回调:
-
条件渲染:
- 使用三元运算符根据
detailExpand状态切换UI - 展开时:显示图片和解释列表
- 折叠时:渲染空元素占位(实际项目可能需要调整)
- 使用三元运算符根据
-
列表渲染:
map()方法遍历explainations数组生成解释条目- 同样方法遍历
expReply生成回复选项 key={index}:为动态元素提供稳定标识,优化渲染性能
-
事件处理:
onClick事件切换详情展开状态- 注意使用箭头函数保持
this绑定
二、index.jsx
2.1 组件定义与props解构
const PictureCard = (props) => {
const {
word,
audio,
uploadImg
} = props;
功能:定义函数组件并提取父组件传递的属性
分析:
- 函数组件:使用箭头函数定义React组件,这是现代React开发的标准方式
- Props解构:直接从props对象提取需要的属性,提高代码可读性
- 接收的属性:
word:从父组件传递的当前单词audio:从父组件传递的发音URLuploadImg:从父组件传递的回调函数,用于处理图片上传
2.2 本地状态管理
const [imgPreview, setImgPreview] = useState('https://res.bearbobo.com/resource/upload/W44yyxvl/upload-ih56twxirei.png')
功能:管理组件的本地状态
分析:
useStateHook:创建本地状态变量imgPreview及其更新函数setImgPreview- 初始值:设置默认预览图片URL
- 状态作用:专门用于存储和显示当前预览图片(URL或Base64数据)
- 设计原则:遵循最小状态原则,只管理与UI直接相关的状态
2.3 图片上传处理函数
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); };
})
}
功能:处理用户选择的图片文件
分析:
-
文件获取:
e.target.files?.[0]:使用可选链操作符安全访问文件列表- 空值检查:
if (!file) return;确保文件存在
-
FileReader API:
new FileReader():创建文件读取器实例readAsDataURL(file):将文件读取为Data URL(Base64编码)- 事件处理:
onload:文件读取成功时触发onerror:读取失败时触发
-
状态更新与回调:
setImgPreview(data):更新本地预览图状态uploadImg(data):调用父组件传递的回调函数,将数据传给父组件resolve(data):返回Promise解决值
-
异步处理:
- 返回Promise对象,便于可能的后续操作
- 错误处理:通过
reject(error)传递读取错误
2.4 音频播放功能
const playAudio = () => {
const audioEle = new Audio(audio);
audioEle.play();
}
功能:播放单词发音
分析:
- Web Audio API:
new Audio(audio):创建HTMLAudioElement实例play():播放音频
- 安全防护:
- 无显式检查但依赖
audio &&渲染条件确保audio存在
- 无显式检查但依赖
- 用户体验:
- 点击按钮即时播放,无延迟
2.5 组件渲染输出
return (
<div className="card">
<input
type="file"
id="selectImage"
accept=".jpg,.png,.gif,.jpeg"
onChange={uploadImgData}
/>
<label htmlFor="selectImage" className="upload">
<img src={imgPreview} alt="preview" />
</label>
<div className="word">{word}</div>
{audio && (
<div className="playAudio" onClick={playAudio}>
<img width="20px" src="https://res.bearbobo.com/resource/upload/Omq2HFs8/playA-3iob5qyckpa.png" alt="logo" />
</div>
)}
</div>
)
功能:渲染组件的UI结构
分析:
-
文件上传区域:
<input type="file" id="selectImage" accept=".jpg,.png,.gif,.jpeg" onChange={uploadImgData} />type="file":创建文件选择输入框accept:限制可接受的文件类型(图片格式)onChange:绑定文件选择处理函数- 默认隐藏:通过CSS隐藏原生文件输入控件
-
图片预览区域:
<label htmlFor="selectImage" className="upload"> <img src={imgPreview} alt="preview" /> </label>- 无障碍设计:
<label htmlFor="selectImage">:关联隐藏的文件输入框- 点击label会触发文件选择对话框
- 动态预览:
src={imgPreview}:显示当前预览图片(URL或Base64)alt="preview":提供替代文本
- 无障碍设计:
-
单词显示区域:
<div className="word">{word}</div>- 直接显示父组件传递的
word属性 - 状态驱动:当父组件更新word时自动重新渲染
- 直接显示父组件传递的
-
音频播放控制:
{audio && ( <div className="playAudio" onClick={playAudio}> <img width="20px" src="play-icon-url" alt="logo" /> </div> )}- 条件渲染:使用
audio &&仅在audio存在时显示播放按钮 - 事件绑定:
onClick={playAudio}绑定播放函数 - 图标显示:使用图片作为播放图标
- 条件渲染:使用
三、audio.js
3.1 Base64 转音频 URL 工具函数
const getAudioUrl = (base64Data) => {
let byteArrays = [];
let byteCharacters = atob(base64Data);
for (let offset = 0; offset < byteCharacters.length; offset++) {
let byteArray = byteCharacters.charCodeAt(offset);
byteArrays.push(byteArray);
}
let blob = new Blob([new Uint8Array(byteArrays)], { type: 'audio/mp3' });
return URL.createObjectURL(blob);
}
功能:将Base64编码的音频数据转换为可播放的URL
分析:
-
Base64解码:
atob()函数:将Base64字符串解码为二进制字符串- ASCII to Binary:将编码后的ASCII字符转换为原始二进制数据
-
字节处理:
- 循环遍历每个字符:
for (let offset = 0; ...) charCodeAt(offset):获取字符的Unicode值(0-65535)- 将字符代码存入数组:创建原始字节数据
- 循环遍历每个字符:
-
二进制数据处理:
Uint8Array:创建8位无符号整型数组,表示原始字节数据- 内存优化:直接操作二进制数据,避免字符串转换开销
-
Blob对象创建:
new Blob():创建表示不可变原始数据的Blob对象- 类型指定:
{ type: 'audio/mp3' }明确音频格式为MP3 - 内存管理:Blob对象封装原始二进制数据
-
对象URL生成:
URL.createObjectURL(blob):创建引用Blob的临时URL- 生命周期:URL与文档绑定,页面卸载时自动释放内存
- 使用场景:可直接用于
<audio>标签的src属性
3.2 语音生成主函数
export const generateAudio = async (text) => {
const token = import.meta.env.VITE_AUDIO_ACCESS_TOKEN;
const appId = import.meta.env.VITE_AUDIO_APP_ID;
const clusterId = import.meta.env.VITE_AUDIO_CLUSTER_ID;
const voiceName = import.meta.env.VITE_AUDIO_VOICE_NAME;
const endpoint = '/tts/api/v1/tts';
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer;${token}`,
};
const payload = {
app: {
appid: appId,
token,
cluster: clusterId,
},
user: {
uid: 'liufengfeng',
},
audio: {
voice_type: voiceName,
encoding: 'ogg_opus',
compression_rate: 1,
rate: 24000,
speed_ratio: 1.0,
volume_ratio: 1.0,
pitch_ratio: 1.0,
emotion: 'happy',
},
request: {
reqid: Math.random().toString(36).substring(7),
text,
text_type: 'plain',
operation: 'query',
silence_duration: '125',
with_frontend: '1',
frontend_type: 'unitTson',
pure_english_opt: '1',
},
};
const res = await fetch(endpoint, {
method: 'POST',
headers,
body: JSON.stringify(payload),
});
const resData = await res.json();
const audioUrl = getAudioUrl(resData.data);
return audioUrl;
}
功能:调用TTS API将文本转换为音频
分析:
-
环境变量使用:
import.meta.env:Vite特有的环境变量访问方式- 安全实践:敏感信息(API密钥)存储在环境变量中
- 配置参数:
token:API访问令牌appId:应用IDclusterId:服务集群IDvoiceName:语音类型(如zh_female_wanqudashu_moon_bigtts)
-
API请求配置:
- 端点:
/tts/api/v1/tts(相对路径,需配置代理) - 请求头:
Content-Type: application/json:指定JSON格式Authorization: Bearer;${token}:认证令牌格式
- 端点:
-
请求体结构设计:
- 应用配置:appid, token, cluster
- 用户标识:硬编码uid(实际应使用动态ID)
- 音频参数:
voice_type:语音类型encoding: 'ogg_opus':高效的音频编码格式rate: 24000:采样率24kHzemotion: 'happy':情感化发音
- 请求参数:
reqid:随机生成的唯一请求IDtext:要转换的文本内容pure_english_opt: '1':启用纯英文优化
-
API请求发送:
fetchAPI:发送HTTP POST请求- 异步处理:
async/await语法处理异步操作 - 请求体序列化:
JSON.stringify(payload)
-
响应处理:
res.json():解析JSON格式响应- 假设响应结构:
{ data: base64AudioData } - 错误处理:实际应添加try/catch和状态码检查
-
音频数据处理:
- 调用
getAudioUrl转换Base64数据 - 返回可直接播放的Blob URL
- 调用
-
Base64 音频处理流程:
graph LR A[Base64字符串] --> B[atob解码] B --> C[二进制字符串] C --> D[字节数组转换] D --> E[Uint8Array] E --> F[Blob对象] F --> G[Object URL]
结语:
至此,识图学单词项目的所有代码分析皆已完成,如果该文章对你有用,不妨点个赞再走!