React智能前端:从零开始的AI识图学单词实战(二)

107 阅读5分钟

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

今天,我将结合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个关键字段:
    1. image_discription:图片描述文本
    2. representative_word:核心英文单词
    3. example_sentence:使用单词的例句
    4. explaination:分行的单词解释
    5. 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:发音URL
      • word:当前单词
      • uploadImg:图片处理函数
  • 条件渲染

    • 使用三元运算符根据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:从父组件传递的发音URL
    • uploadImg:从父组件传递的回调函数,用于处理图片上传

2.2 本地状态管理

const [imgPreview, setImgPreview] = useState('https://res.bearbobo.com/resource/upload/W44yyxvl/upload-ih56twxirei.png')

功能:管理组件的本地状态
分析

  • useState Hook:创建本地状态变量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:应用ID
      • clusterId:服务集群ID
      • voiceName:语音类型(如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:采样率24kHz
      • emotion: 'happy':情感化发音
    • 请求参数
      • reqid:随机生成的唯一请求ID
      • text:要转换的文本内容
      • pure_english_opt: '1':启用纯英文优化
  • API请求发送

    • fetch API:发送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]
    

结语:

至此,识图学单词项目的所有代码分析皆已完成,如果该文章对你有用,不妨点个赞再走!