当用户拍摄物品图片,系统自动识别核心物体并生成对应英文单词,实时提供真人级发音、情景例句和互动解释。
今天,我将结合Kimi多模态大模型 的视觉理解和火山引擎TTS的语音合成技术,打造沉浸式语言学习工具。
注:由于文章内容篇幅过长,本文将通过两篇文章进行讲解
本文内容包括:相关大模型简介、如何获取大模型接口密钥、项目效果展示、亮点介绍、完整源码展示。
项目代码讲解分析内容请看《React智能前端:从零开始的识图学单词项目(二)》
一、核心大模型简介:
-
月之暗面(Moonshot)的 Kimi 多模态大模型:
- 功能:实现图片内容识别与英文单词提取
- 特点:支持视觉-语言跨模态理解,能解析图像内容并生成结构化文本响应
-
火山引擎 TTS 语音合成大模型:
- 功能:将文本转换为自然流畅的语音
- 特点:支持多语言、多音色、情感化发音和实时合成
二、大模型调用准备工作
为了防止文章篇幅过长,相关内容可以看看我其他文章中准备工作部分的内容:
- 获取月之暗面接口密钥方法: 《React智能前端:从零开始写的图片分析页面实战》
- 获取火山引擎接口密钥方法: 《React:智能前端---语音合成大模型》
三、效果展示与项目亮点
为了让读者更清晰地了解接下来我们要实现的功能,我将先给大家看看最终效果:


项目核心功能:
-
当用户上传图片后,通过月之暗面大模型,我们可以自动识别图片中的内容,并返回一个最适合描述图片内容的简单的英文单词。
-
点击播放图标后,我们可以通过TTS大模型,听到该单词的阅读发音。
-
点击Talk about it 单词卡片后,会弹出一个单词卡片,其中有根据单词给出的简单例句,和对英文单词的简单解释。
四、项目目录结构

四、完整代码展示
创作不易,需要的读者朋友们欢迎留赞+自取!
App.jsx
javascript
import { useState } from 'react'
import './App.css'
import PictureCard from './components/PictureCard';
import { generateAudio } from './lib/audio.js';
function App() {
const userPrompt = `分析图片内容,找出最能描述图片的一个英文单词,尽量选择更简单的A1~A2的词汇。
返回JSON数据:
{
"image_discription": "图片描述",
"representative_word": "图片代表的英文单词",
"example_sentence": "结合英文单词和图片描述,给出一个简单的例句",
"explaination": "结合图片解释英文单词,段落以Look at...开头,将段落分句,每一句单独一行,解释的最后给一个日常生活有关的问句",
"explaination_replys": ["根据explaination给出的回复1", "根据explaination给出的回复2"]
}`;
// 上传图片的状态
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')
const uploadImg = async (imageData) => {
setImgPreview(imageData);
const endpoint = 'https://api.moonshot.cn/v1/chat/completions';
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${import.meta.env.VITE_KIMI_API_KEY}`
};
setWord('分析中...');
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
})
})
const data = await response.json();
const replyData = JSON.parse(data.choices[0].message.content);
setWord(replyData.representative_word);
setSentence(replyData.example_sentence);
setExplainations(replyData.explaination.split('\n'))
setExpReply(replyData.explaination_replys);
const audioUrl = await generateAudio(replyData.representative_word);
console.log(audioUrl, 'app');
setAudio(audioUrl);
}
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>
)
}
export default App
index.jsx
ini
import './style.css'
import { useState } from 'react';
const PictureCard = (props) => {
const {
word,
audio,
uploadImg
} = props;
const [imgPreview, setImgPreview] = useState('https://res.bearbobo.com/resource/upload/W44yyxvl/upload-ih56twxirei.png')
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 playAudio = () => {
const audioEle = new Audio(audio);
audioEle.play();
}
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>
)}
export default PictureCard
audio.js
ini
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);
}
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;
}
.env.local
ini
# 月之暗面
VITE_KIMI_API_KEY= 你的月之暗面密钥
# 字节tts
VITE_AUDIO_ACCESS_TOKEN= 你的TTS大模型Token
VITE_AUDIO_APP_ID= 你的APP ID
VITE_AUDIO_CLUSTER_ID= 你的 Cluster ID
VITE_AUDIO_VOICE_NAME=zh_female_wanqudashu_moon_bigtts //可替换成你想要的音色