Transformers.js 文档

构建 React 应用程序

您正在查看 main 版本,该版本需要从源代码安装。如果您想进行常规 npm 安装,请查看最新的稳定版本 (v3.0.0)。
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(例如,https://#: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 hooks 来实现这两个目标。

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

    import { pipeline, TextStreamer } from '@huggingface/transformers';
    
    class MyTranslationPipeline {
      static task = 'translation';
      static model = 'Xenova/nllb-200-distilled-600M';
      static instance = null;
    
      static async getInstance(progress_callback = 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'
    import './App.css'
    
    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(() => {
        // 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:此组件将允许用户选择输入和输出语言。在此处查看完整语言列表:here

    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 元素用于输入和输出文本,以及一个按钮来触发翻译。修改 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%;
      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 函数,该函数将在用户单击“翻译”按钮时被调用。这会将消息(包含输入文本、源语言和目标语言)发送到 worker 线程进行处理。我们还将禁用该按钮,以便用户不会多次单击它。在 App 函数中的 return 语句之前添加以下代码

const translate = () => {
  setDisabled(true);
  setOutput('');
  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.
  const translator = await MyTranslationPipeline.getInstance(x => {
      // We also add a progress callback to the pipeline so that we can
      // track model loading.
      self.postMessage(x);
  });

  // Capture partial output as it streams from the pipeline
  const streamer = new TextStreamer(translator.tokenizer, {
      skip_prompt: true,
      skip_special_tokens: true,
      callback_function: function (text) {
          self.postMessage({
              status: 'update',
              output: text
          });
      }
  });

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

      // Allows for partial output to be captured
      streamer,
  });

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

最后,让我们填写 src/App.jsx 中的 onMessageReceived 函数,该函数将响应来自 worker 线程的消息来更新应用程序状态。将以下代码添加到我们之前定义的 useEffect hook 中

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(o => o + 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,但您可以将其部署到任何您喜欢的地方!如果您还没有,可以在 here 创建一个免费的 Hugging Face 帐户。

  1. 访问 https://huggingface.co/new-space 并填写表格。请记住选择“Static”作为 space 类型。
  2. 转到“Files”→“Add file”→“Upload files”。将 dist 文件夹中的 index.html 文件和 public/ 文件夹拖到上传框中,然后单击“Upload”。上传完成后,向下滚动到按钮并单击“Commit changes to main”。

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

< > 在 GitHub 上更新