🪄 用 React 玩转「图片识词 + 语音 TTS」:月影大佬的 AI 英语私教是怎么炼成的?


前言:

你好,我是卷王队形中那只还在啃 React 的小白。这次掘金写这篇文章,带你完整拆解一个 AI 小项目:

上传一张图,自动帮你找出一个最简单的英文单词 ,顺带生成解释、例句,最后用 TTS 读给你听!

这不比隔壁花大几千请外教香?


一、项目简介:这玩意儿是干嘛的?

这个小项目核心功能一句话就能讲明白:

"分析用户上传的图片 ➜ 找出最能代表图片的一个英文单词(简单词汇,A1-A2 级别) ➜ 自动生成例句 + 段落解释 + 语音朗读。"

简单粗暴,但背后集合了:

  • 图像识别(用 Moonshot Vision)
  • 自动文本生成(AIGC大语言模型 Chat 完成)
  • TTS(来自火山引擎的文字转语音)

而整个流程用 React + 简单的后端服务就能跑通,关键代码就散落在三个文件里:

  • App.jsx --- 项目入口,状态管理大本营
  • PictureCard.jsx --- 图片上传和语音播放的可爱小卡片
  • /lib/audio.js --- TTS 核心逻辑(把文字变成人声 MP3)

二、项目结构:前端是怎么组织的?

大体目录结构长这样(主要文件):

路径 说明
my-picture-ai-app/ 项目根目录
src/ 源码文件夹
src/App.jsx 应用入口,管理状态和逻辑
src/components/ 组件文件夹
src/components/PictureCard.jsx 图片上传 + 音频播放组件
src/lib/ 公共库文件夹
src/lib/audio.js 文字转语音核心逻辑
src/App.css 全局样式
src/style.css 局部样式

是不是清爽?真·月影大佬手把手模板。


三、核心流程拆解(含关键代码)


1️⃣ App.jsx --- 全局状态调度中心

这里干了几件大事:

1. 定义核心状态

jsx 复制代码
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('默认示例图URL');

意思很简单:

  • word:识别出来的英文单词
  • sentence:例句
  • explainations:段落解释,分句处理后是数组
  • expReply:针对解释的可能对话回复
  • audio:生成的音频 URL
  • detailExpand:是否展开详细信息
  • imgPreview:图片预览 URL

2. 核心函数 uploadImg

这段就是把图片丢给 Moonshot,拿回来 AI 分析结果的全过程。 这一段中imageData由子组件PictureCard中的uploadImgData自定义函数转成了chrome浏览器提供的base64格式的文件然后通过props再'汇报'给父组件,此过程完成了一个单向数据流的操作

jsx 复制代码
const uploadImg = async (imageData) => {
  setImgPreview(imageData);
  setWord('分析中...');

  const endpoint = 'https://api.moonshot.cn/v1/chat/completions';
  const headers = { 
    'Content-Type': 'application/json', 
    'Authorization': `Bearer ${import.meta.env.VITE_KIMI_API_KEY}` 
  };

  const response = await fetch(endpoint, {
    method: 'POST',
    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);

  // TTS 生成
  const audioUrl = await generateAudio(replyData.example_sentence);
  setAudio(audioUrl);
};

这里有几个点可以偷学:

  • import.meta.env环境变量,管理密钥,安全一点,实际的在.env.local文件中
  • userPrompt 里直接用 JSON 模板让 LLM 生成结构化输出,避免抓瞎。
  • 图片直接转 Base64 当 URL,后端 Vision 可以吃。

PictureCard.jsx --- 图片上传 + 播放语音

小卡片逻辑超级简单,只有两块:

  • 上传图片 ➜ 转 Base64
  • 点按钮播放音频

上传图片是经典 FileReader 用法:

jsx 复制代码
const uploadImgData = (e) => {
  const file = e.target.files?.[0];//可选链运算符
  if (!file) return;

  const reader = new FileReader();//FileReader API 
  reader.readAsDataURL(file);//API
  reader.onload = () => {
    const data = reader.result;//读取结果
    setImgPreview(data);//图片预览,提升用户体验
    uploadImg(data);//传回给父组件
  };
};

点按钮放音频用 Audio

ini 复制代码
const playAudio = () => {
  const audioEle = new Audio(audio);
  audioEle.play();
};

可见:一整个复古 Vanilla 实现。


3️⃣ audio.js --- 文字转语音核心

TTS 流程也挺朴实无华:

  • 拼接请求体 ➜ 调后端 ➜ 后端给个音频 Base64 ➜ 用 atob 转字节 ➜ BlobURL.createObjectURL ➜ 给 <audio>

里面有个知识点:

jsx 复制代码
const getAudioUrl = (base64Data) => {
  const byteCharacters = atob(base64Data);
  const byteArrays = [];

  for (let offset = 0; offset < byteCharacters.length; offset++) {
    byteArrays.push(byteCharacters.charCodeAt(offset));
  }

  const blob = new Blob([new Uint8Array(byteArrays)], { type: 'audio/mp3' });
  return URL.createObjectURL(blob);
};
  • 记住 atob():ASCII to Binary,把data解码成二进制字符串(前端处理二进制老朋友)。
  • 接着charCodeAt():用for循环遍历,会把字符串转成ASCII值。
  • 再用Blob封装被Uint8Array()打包成的真实的二进制数组成浏览器可识别的文件。
  • 最后用return URL.createObjectURL(blob)返回一个临时地址,可以给 直接用。

四、亮点小结

这个项目说复杂不复杂,说简单也有点小巧思:

  • 用 Vision + LLM 做到图像语义理解(Moonshot 8K Vision)
  • 返回 JSON 保证结构化,不怕 LLM 胡说八道
  • TTS 音频用纯前端就能把 Base64 变可播放 URL

对初学者来说:

✅ React 的状态怎么拆?

✅ 事件流 + 状态传递怎么配?

✅ 调用异步接口、处理 Blob、玩文件流?

全在里面!


五、适合谁玩?

  • 学 React 的同学:练状态、练组件通信、练异步。
  • 想做 AI 应用 Demo 的同学:大语言模型 + Vision + TTS,三合一小样板。
  • 想发掘 Moonshot、Kimi 这类国内可用 LLM 的同学:怎么调怎么封装,一看就会。

六、最后一句

上传张图 ➜ 自动学个单词 ➜ 还能听一遍

要是小时候就有这玩意儿,我的四六级词汇量也不至于这么拉胯......

希望这套拆解能帮你看懂背后思路,自己也可以魔改试试:

  • 换个更复杂的 Prompt
  • 加个单词词根解释
  • 换个外语读音
  • 甚至直接做成单词卡片库!

卷就完了,咱在掘金见!


有需要源码或想看其他解读,评论区喊我,一起写起来~

相关推荐
RadiumAg1 分钟前
记一道有趣的面试题
前端·javascript
yangzhi_emo6 分钟前
ES6笔记2
开发语言·前端·javascript
yanlele22 分钟前
我用爬虫抓取了 25 年 5 月掘金热门面试文章
前端·javascript·面试
中微子2 小时前
React状态管理最佳实践
前端
烛阴2 小时前
void 0 的奥秘:解锁 JavaScript 中 undefined 的正确打开方式
前端·javascript
中微子2 小时前
JavaScript 事件与 React 合成事件完全指南:从入门到精通
前端
Hexene...2 小时前
【前端Vue】如何实现echarts图表根据父元素宽度自适应大小
前端·vue.js·echarts
天天扭码3 小时前
《很全面的前端面试题》——HTML篇
前端·面试·html
xw53 小时前
我犯了错,我于是为我的uni-app项目引入环境标志
前端·uni-app
!win !3 小时前
被老板怼后,我为uni-app项目引入环境标志
前端·小程序·uni-app