React智能前端:从零开始的识图学单词项目(一)

当用户拍摄物品图片,系统自动识别核心物体并生成对应英文单词,实时提供真人级发音、情景例句和互动解释。

今天,我将结合Kimi多模态大模型 的视觉理解和火山引擎TTS的语音合成技术,打造沉浸式语言学习工具。

注:由于文章内容篇幅过长,本文将通过两篇文章进行讲解

本文内容包括:相关大模型简介、如何获取大模型接口密钥、项目效果展示、亮点介绍、完整源码展示。

项目代码讲解分析内容请看《React智能前端:从零开始的识图学单词项目(二)》

一、核心大模型简介:

  1. 月之暗面(Moonshot)的 Kimi 多模态大模型

    • 功能:实现图片内容识别与英文单词提取
    • 特点:支持视觉-语言跨模态理解,能解析图像内容并生成结构化文本响应
  2. 火山引擎 TTS 语音合成大模型

    • 功能:将文本转换为自然流畅的语音
    • 特点:支持多语言、多音色、情感化发音和实时合成

二、大模型调用准备工作

为了防止文章篇幅过长,相关内容可以看看我其他文章中准备工作部分的内容:


三、效果展示与项目亮点

为了让读者更清晰地了解接下来我们要实现的功能,我将先给大家看看最终效果:

项目核心功能:

  • 当用户上传图片后,通过月之暗面大模型,我们可以自动识别图片中的内容,并返回一个最适合描述图片内容的简单的英文单词。

  • 点击播放图标后,我们可以通过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 //可替换成你想要的音色
相关推荐
前端小趴菜059 分钟前
React-React.memo-props比较机制
前端·javascript·react.js
摸鱼仙人~1 小时前
styled-components:现代React样式解决方案
前端·react.js·前端框架
RadiumAg3 小时前
记一道有趣的面试题
前端·javascript
yangzhi_emo3 小时前
ES6笔记2
开发语言·前端·javascript
yanlele3 小时前
我用爬虫抓取了 25 年 5 月掘金热门面试文章
前端·javascript·面试
墨风如雪3 小时前
告别“面目全非”!腾讯混元3D变身“建模艺术家”,建模效率直接起飞!
aigc
烛阴4 小时前
void 0 的奥秘:解锁 JavaScript 中 undefined 的正确打开方式
前端·javascript
初遇你时动了情5 小时前
腾讯地图 vue3 使用 封装 地图组件
javascript·vue.js·腾讯地图
dssxyz5 小时前
uniapp打包微信小程序主包过大问题_uniapp 微信小程序时主包太大和vendor.js过大
javascript·微信小程序·uni-app
ohMyGod_1237 小时前
React16,17,18,19新特性更新对比
前端·javascript·react.js