Transformers.js 文档

构建 React 应用程序

Hugging Face's logo
加入 Hugging Face 社区

并获得增强文档体验的访问权限

开始使用

构建 React 应用程序

在本教程中,我们将构建一个简单的 React 应用程序,使用 Transformers.js 进行多语言翻译!最终产品将如下所示

Demo

有用链接

先决条件

步骤 1:初始化项目

在本教程中,我们将使用 Vite 来初始化我们的项目。Vite 是一个构建工具,它允许我们以最少的配置快速设置 React 应用程序。在您的终端中运行以下命令

npm create vite@latest react-translator -- --template react

如果提示安装 create-vite,请键入 y 并按 Enter

接下来,进入项目目录并安装必要的开发依赖项

cd react-translator
npm install

为了测试我们的应用程序是否正常工作,我们可以运行以下命令

npm run dev

访问终端中显示的 URL(例如,http://localhost:5173/)应该会显示默认的“React + Vite”登录页面。您可以通过在终端中按 Ctrl + C 来停止开发服务器。

步骤 2:安装和配置 Transformers.js

现在我们进入有趣的部分:向我们的应用程序添加机器学习!首先,使用以下命令从 NPM 安装 Transformers.js

npm install @huggingface/transformers

对于此应用程序,我们将使用 Xenova/nllb-200-distilled-600M 模型,该模型可以在 200 种语言之间进行多语言翻译。在我们开始之前,有两件事需要注意

  1. ML 推理在计算上可能非常密集,因此最好在与主(UI)线程分开的单独线程中加载和运行模型。
  2. 由于模型相当大(> 1 GB),因此我们不希望在用户点击“翻译”按钮之前下载它。

我们可以通过使用 Web Worker 和一些 React hook 来实现这两个目标。

  1. src 目录中创建一个名为 worker.js 的文件。此脚本将为我们完成所有繁重的工作,包括加载和运行翻译管道。为了确保模型只加载一次,我们将创建 MyTranslationPipeline 类,该类使用 单例模式 在第一次调用 getInstance 时延迟创建管道的单个实例,并在所有后续调用中使用此管道

    import { pipeline } from '@huggingface/transformers';
    
    class MyTranslationPipeline {
      static task = 'translation';
      static model = 'Xenova/nllb-200-distilled-600M';
      static instance = null;
    
      static async getInstance(progress_callback = null) {
        if (this.instance === null) {
          this.instance = pipeline(this.task, this.model, { progress_callback });
        }
    
        return this.instance;
      }
    }
  2. 修改 src 目录中的 App.jsx。初始化 React 项目时会自动创建此文件,并且其中包含一些样板代码。在 App 函数内,让我们创建 web worker 并使用 useRef hook 存储对它的引用

    // Remember to import the relevant hooks
    import { useEffect, useRef, useState } from 'react'
    
    function App() {
      // Create a reference to the worker object.
      const worker = useRef(null);
    
      // We use the `useEffect` hook to setup the worker as soon as the `App` component is mounted.
      useEffect(() => {
        if (!worker.current) {
          // Create the worker if it does not yet exist.
          worker.current = new Worker(new URL('./worker.js', import.meta.url), {
              type: 'module'
          });
        }
    
        // Create a callback function for messages from the worker thread.
        const onMessageReceived = (e) => {
          // TODO: Will fill in later
        };
    
        // Attach the callback function as an event listener.
        worker.current.addEventListener('message', onMessageReceived);
    
        // Define a cleanup function for when the component is unmounted.
        return () => worker.current.removeEventListener('message', onMessageReceived);
      });
    
      return (
        // TODO: Rest of our app goes here...
      )
    }
    
    export default App
    

步骤 3:设计用户界面

我们建议再次使用 npm run dev 启动开发服务器(如果尚未运行),以便您可以实时查看您的更改。

首先,让我们定义我们的组件。在 src 目录中创建一个名为 components 的文件夹,并创建以下文件

  1. LanguageSelector.jsx:此组件将允许用户选择输入和输出语言。查看完整的语言列表 此处

    const LANGUAGES = {
      "Acehnese (Arabic script)": "ace_Arab",
      "Acehnese (Latin script)": "ace_Latn",
      "Afrikaans": "afr_Latn",
      ...
      "Zulu": "zul_Latn",
    }
    
    export default function LanguageSelector({ type, onChange, defaultLanguage }) {
      return (
        <div className='language-selector'>
          <label>{type}: </label>
          <select onChange={onChange} defaultValue={defaultLanguage}>
            {Object.entries(LANGUAGES).map(([key, value]) => {
              return <option key={key} value={value}>{key}</option>
            })}
          </select>
        </div>
      )
    }
  1. Progress.jsx:此组件将显示每个模型文件下载的进度。
    export default function Progress({ text, percentage }) {
      percentage = percentage ?? 0;
      return (
        <div className="progress-container">
          <div className='progress-bar' style={{ 'width': `${percentage}%` }}>
            {text} ({`${percentage.toFixed(2)}%`})
          </div>
        </div>
      );
    }

我们现在可以通过将这些导入添加到文件顶部,在 App.jsx 中使用这些组件

import LanguageSelector from './components/LanguageSelector';
import Progress from './components/Progress';

我们还需要添加一些状态变量来跟踪应用程序中的某些内容,例如模型加载、语言、输入文本和输出文本。将以下代码添加到src/App.jsxApp函数的开头。

function App() {

  // Model loading
  const [ready, setReady] = useState(null);
  const [disabled, setDisabled] = useState(false);
  const [progressItems, setProgressItems] = useState([]);

  // Inputs and outputs
  const [input, setInput] = useState('I love walking my dog.');
  const [sourceLanguage, setSourceLanguage] = useState('eng_Latn');
  const [targetLanguage, setTargetLanguage] = useState('fra_Latn');
  const [output, setOutput] = useState('');

  // rest of the code...
}

接下来,我们可以将自定义组件添加到主App组件中。我们还将添加两个textarea元素用于输入和输出文本,以及一个button用于触发翻译。修改return语句使其如下所示

return (
  <>
    <h1>Transformers.js</h1>
    <h2>ML-powered multilingual translation in React!</h2>

    <div className='container'>
      <div className='language-container'>
        <LanguageSelector type={"Source"} defaultLanguage={"eng_Latn"} onChange={x => setSourceLanguage(x.target.value)} />
        <LanguageSelector type={"Target"} defaultLanguage={"fra_Latn"} onChange={x => setTargetLanguage(x.target.value)} />
      </div>

      <div className='textbox-container'>
        <textarea value={input} rows={3} onChange={e => setInput(e.target.value)}></textarea>
        <textarea value={output} rows={3} readOnly></textarea>
      </div>
    </div>

    <button disabled={disabled} onClick={translate}>Translate</button>

    <div className='progress-bars-container'>
      {ready === false && (
        <label>Loading models... (only run once)</label>
      )}
      {progressItems.map(data => (
        <div key={data.file}>
          <Progress text={data.file} percentage={data.progress} />
        </div>
      ))}
    </div>
  </>
)

暂时不用担心translate函数。我们将在下一节中定义它。

最后,我们可以添加一些CSS使我们的应用程序看起来更漂亮一些。修改src目录中的以下文件

  1. index.css:

    查看代码
    :root {
      font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
      line-height: 1.5;
      font-weight: 400;
      color: #213547;
      background-color: #ffffff;
    
      font-synthesis: none;
      text-rendering: optimizeLegibility;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      -webkit-text-size-adjust: 100%;
    }
    
    body {
      margin: 0;
      display: flex;
      place-items: center;
      min-width: 320px;
      min-height: 100vh;
    }
    
    h1 {
      font-size: 3.2em;
      line-height: 1;
    }
    
    h1,
    h2 {
      margin: 8px;
    }
    
    select {
      padding: 0.3em;
      cursor: pointer;
    }
    
    textarea {
      padding: 0.6em;
    }
    
    button {
      padding: 0.6em 1.2em;
      cursor: pointer;
      font-weight: 500;
    }
    
    button[disabled] {
      cursor: not-allowed;
    }
    
    select,
    textarea,
    button {
      border-radius: 8px;
      border: 1px solid transparent;
      font-size: 1em;
      font-family: inherit;
      background-color: #f9f9f9;
      transition: border-color 0.25s;
    }
    
    select:hover,
    textarea:hover,
    button:not([disabled]):hover {
      border-color: #646cff;
    }
    
    select:focus,
    select:focus-visible,
    textarea:focus,
    textarea:focus-visible,
    button:focus,
    button:focus-visible {
      outline: 4px auto -webkit-focus-ring-color;
    }
  2. App.css

    查看代码
    #root {
      max-width: 1280px;
      margin: 0 auto;
      padding: 2rem;
      text-align: center;
    }
    
    .language-container {
      display: flex;
      gap: 20px;
    }
    
    .textbox-container {
      display: flex;
      justify-content: center;
      gap: 20px;
      width: 800px;
    }
    
    .textbox-container>textarea, .language-selector {
      width: 50%;
    }
    
    .language-selector>select {
      width: 150px;
    }
    
    .progress-container {
      position: relative;
      font-size: 14px;
      color: white;
      background-color: #e9ecef;
      border: solid 1px;
      border-radius: 8px;
      text-align: left;
      overflow: hidden;
    }
    
    .progress-bar {
      padding: 0 4px;
      z-index: 0;
      top: 0;
      width: 1%;
      height: 100%;
      overflow: hidden;
      background-color: #007bff;
      white-space: nowrap;
    }
    
    .progress-text {
      z-index: 2;
    }
    
    .selector-container {
      display: flex;
      gap: 20px;
    }
    
    .progress-bars-container {
      padding: 8px;
      height: 140px;
    }
    
    .container {
      margin: 25px;
      display: flex;
      flex-direction: column;
      gap: 10px;
    }

步骤 4:连接所有内容

现在我们已经设置了一个基本的用户界面,我们终于可以将所有内容连接在一起了。

首先,让我们定义translate函数,该函数将在用户点击“翻译”按钮时调用。这会向工作线程发送一条消息(包含输入文本、源语言和目标语言)以进行处理。我们还将禁用按钮,以防止用户多次点击。在App函数的return语句之前添加以下代码

const translate = () => {
  setDisabled(true);
  worker.current.postMessage({
    text: input,
    src_lang: sourceLanguage,
    tgt_lang: targetLanguage,
  });
}

现在,让我们在src/worker.js中添加一个事件监听器来监听来自主线程的消息。我们将使用self.postMessage将消息(例如,模型加载进度和文本流)发送回主线程。

// Listen for messages from the main thread
self.addEventListener('message', async (event) => {
  // Retrieve the translation pipeline. When called for the first time,
  // this will load the pipeline and save it for future use.
  let translator = await MyTranslationPipeline.getInstance(x => {
      // We also add a progress callback to the pipeline so that we can
      // track model loading.
      self.postMessage(x);
  });

  // Actually perform the translation
  let output = await translator(event.data.text, {
      tgt_lang: event.data.tgt_lang,
      src_lang: event.data.src_lang,

      // Allows for partial output
      callback_function: x => {
          self.postMessage({
              status: 'update',
              output: translator.tokenizer.decode(x[0].output_token_ids, { skip_special_tokens: true })
          });
      }
  });

  // Send the output back to the main thread
  self.postMessage({
      status: 'complete',
      output: output,
  });
});

最后,让我们填写onMessageReceived函数,该函数将根据来自工作线程的消息更新应用程序状态。在前面定义的useEffect钩子内添加以下代码

const onMessageReceived = (e) => {
  switch (e.data.status) {
    case 'initiate':
      // Model file start load: add a new progress item to the list.
      setReady(false);
      setProgressItems(prev => [...prev, e.data]);
      break;

    case 'progress':
      // Model file progress: update one of the progress items.
      setProgressItems(
        prev => prev.map(item => {
          if (item.file === e.data.file) {
            return { ...item, progress: e.data.progress }
          }
          return item;
        })
      );
      break;

    case 'done':
      // Model file loaded: remove the progress item from the list.
      setProgressItems(
        prev => prev.filter(item => item.file !== e.data.file)
      );
      break;

    case 'ready':
      // Pipeline ready: the worker is ready to accept messages.
      setReady(true);
      break;

    case 'update':
      // Generation update: update the output text.
      setOutput(e.data.output);
      break;

    case 'complete':
      // Generation complete: re-enable the "Translate" button
      setDisabled(false);
      break;
  }
};

您现在可以使用npm run dev运行应用程序,并在浏览器中直接执行多语言翻译!

(可选) 步骤 5:构建和部署

要构建您的应用程序,只需运行npm run build即可。这将捆绑您的应用程序并将静态文件输出到dist文件夹。

对于此演示,我们将应用程序部署为静态的Hugging Face Space,但您可以将其部署到任何您喜欢的地方!如果您还没有,可以在这里创建一个免费的 Hugging Face 帐户

  1. 访问https://huggingface.co/new-space并填写表单。请记住选择“静态”作为空间类型。
  2. 转到“文件”→“添加文件”→“上传文件”。将dist文件夹中的index.html文件和public/文件夹拖到上传框中,然后点击“上传”。上传完成后,向下滚动到按钮并点击“提交更改到主分支”。

就是这样!您的应用程序现在应该在https://huggingface.co/spaces/<your-username>/<your-space-name>上线了!

< > GitHub 更新