从图像到语音,一个React项目打通AI视觉与语言能力

232 阅读3分钟

一、项目概述

1.1 应用简介

本项目是一个基于 React 的 Web 应用,用户上传一张图片后,系统会通过 Kimi Vision API 分析图片内容,提取出一个代表性的英文单词,并生成对应的例句、解释和音频。整个过程实现了图像识别 → 文字理解 → 语音合成的全流程闭环。

1.2 技术栈

  • 前端框架:React + Vite
  • 状态管理:React 内置 useStateuseEffect
  • 样式处理:CSS 模块化 + 响应式布局
  • AI 接口
    • Kimi Vision API(图像识别)
    • 火山引擎 TTS API(文字转语音)
  • 环境变量.env.local文件配置敏感信息

二、项目结构与组件划分

2.1 目录结构

src/
├── App.jsx               // 根组件
├── components/           // 组件目录
│   └── PictureCard.jsx   // 图片上传与展示组件
├── lib/                  // 工具包
│   └── audio.js          // 音频生成逻辑
├── index.css             // 全局样式
└── main.jsx              // 应用入口

2.2 组件划分原则

1. App.jsx(根组件)

  • 负责全局状态管理(如图片 Base64 数据、单词、例句、音频 URL)
  • 调用 AI 接口获取数据
  • 向子组件传递 props 并接收回调

2. PictureCard.jsx(子组件)

  • 负责图片上传、预览和音频播放功能
  • 通过回调函数将图片 Base64 数据传给父组件
  • 展示 AI 返回的单词信息及语音按钮

3. 单向数据流设计

  • 子组件负责上传图片并返回 Base64 编码
  • 父组件接收编码并调用 AI 接口
  • 获取结果后更新状态并通过 props 传递给子组件渲染

三、核心功能实现详解

3.1 图片上传与 Base64 编码

PictureCard.jsx 中实现图片上传功能,并将其转换为 Base64 编码格式:

import React, { useState } from 'react';

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

  const handleImageUpload = (e) => {
    const file = e.target.files?.[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onloadend = () => {
      setImgPreview(reader.result);
      uploadImg(reader.result); // 回调通知父组件
    };
    reader.readAsDataURL(file);
  };

  return (
    <div className="upload">
      <input type="file" accept=".jpg,.jpeg,.png,.gif" onChange={handleImageUpload} />
      <img src={imgPreview} alt="preview" />
    </div>
  );
};

export default PictureCard;

说明:使用 FileReader 实现图片文件读取,并将其转换为 Base64 编码格式,便于后续上传至 AI 服务端。


3.2 图像识别与数据解析

App.jsx 中调用 Kimi Vision API 进行图像识别:

import React, { useState } from 'react';
import PictureCard from './components/PictureCard';
import './index.css';

function App() {
  const [imgPreview, setImgPreview] = useState('');
  const [word, setWord] = useState('');
  const [sentence, setSentence] = useState('');
  const [audioUrl, setAudioUrl] = useState('');
  const [detailExpand, setDetailExpand] = useState(false);

  const userPrompt = `
    请分析这张图片,提取出一个最可能的英文单词,
    并给出一个包含这个单词的英文例句。
    输出格式为 JSON:
    {
      "representative_word": "单词",
      "example_sentence": "例句"
    }
  `;

  const uploadImg = async (imageData) => {
    setImgPreview(imageData);
    const endpoint = 'https://api.moonshot.cn/v1/chat/completions';
    const headers = {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.VITE_KIMI_API_KEY}`
    };

    setWord('分析中...');
    try {
      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);

      // 生成语音
      const url = await generateAudio(replyData.example_sentence);
      setAudioUrl(url);
    } catch (error) {
      console.error('Error uploading image:', error);
    }
  };

  const playAudio = () => {
    if (audioUrl) {
      const audio = new Audio(audioUrl);
      audio.play();
    }
  };

  return (
    <div className="container">
      <h1>📸 拍图识词</h1>
      <PictureCard uploadImg={uploadImg} />

      <div className="word">{word}</div>

      <div className="playAudio" onClick={playAudio}>
        <img width="20px" src="https://res.bearbobo.com/resource/upload/Omq2HFs8/playA-3iob5qyckpa.png" alt="logo" />
      </div>

      <button onClick={() => setDetailExpand(!detailExpand)}>Talk about it</button>
      {detailExpand && (
        <div className="expand">
          <img src={imgPreview} alt="preview" />
          <div className="explaination">
            <p>{sentence}</p>
          </div>
        </div>
      )}
    </div>
  );
}

export default App;

说明:使用 fetch 请求 Kimi Vision API,将 Base64 图片作为参数传入,并解析返回的 JSON 数据。同时触发语音合成接口生成音频。


3.3 文字转语音(TTS)

lib/audio.js 中封装 TTS 功能:

export const generateAudio = async (text) => {
  const token = process.env.VITE_AUDIO_ACCESS_TOKEN;
  const appId = process.env.VITE_AUDIO_APP_ID;
  const clusterId = process.env.VITE_AUDIO_CLUSTER_ID;
  const voiceName = process.env.VITE_AUDIO_VOICE_NAME;

  const payload = {
    app: { appid: appId, token, cluster: clusterId },
    user: { uid: 'bearbobo' },
    audio: {
      voice_name: voiceName,
      encoding: 'mp3_16k',
      speed: 0,
      volume: 50,
      pitch: 0,
      background_speech: ''
    },
    request: {
      reqid: Math.random().toString(36).substring(7),
      text,
      text_type: 'plain',
      operation: 'query'
    }
  };

  const res = await fetch('/tts/api/v1/tts', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer;${token}`,
    },
    body: JSON.stringify(payload),
  });

  const data = await res.json();
  return getAudioUrl(data.data);
};

const getAudioUrl = (base64Data) => {
  var byteCharacters = atob(base64Data);
  var byteArrays = [];
  for (var offset = 0; offset < byteCharacters.length; offset++) {
    byteArrays.push(byteCharacters.charCodeAt(offset));
  }
  var blob = new Blob([new Uint8Array(byteArrays)], { type: 'audio/mp3' });
  return URL.createObjectURL(blob);
};

说明:该模块负责将文本发送至火山引擎 TTS 接口,接收 Base64 格式的音频数据并转换为浏览器可播放的 URL。


四、项目难点与优化

4.1 难点总结

  • Base64 编码体积大:影响页面加载速度和 API 请求效率,需压缩或分段上传
  • API 调用失败处理:网络不稳定时需增加重试机制和错误提示
  • 音频播放兼容性问题:不同浏览器对 Audio 对象支持不一致,需做兼容处理

4.2 性能优化建议

  • 使用 useMemouseCallback 减少不必要的重复渲染
  • 对 AI 请求做节流或缓存处理
  • 图片上传前进行本地压缩(如使用 compressorjs

五、结语

通过本次实战项目,我们成功打造了一个融合 AI 视觉识别与语音合成的 React 应用。它不仅展示了现代前端开发如何结合 AI 技术,还为我们打开了通往教育科技、智能助手等领域的无限可能。

未来,我们可以进一步扩展功能,例如支持多语言识别、加入离线模式、集成大模型对话能力等,让这个应用真正成为一个“懂你”的英语学习伙伴。

如果你也想尝试做一个“能看、能说”的 AI 应用,不妨动手跟着这篇教程一起实践吧!