构建 React 应用程序
在本教程中,我们将构建一个简单的 React 应用程序,使用 Transformers.js 进行多语言翻译!最终产品将如下所示
有用链接
先决条件
步骤 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 种语言之间进行多语言翻译。在我们开始之前,有两件事需要注意
- ML 推理在计算上可能非常密集,因此最好在与主(UI)线程分开的单独线程中加载和运行模型。
- 由于模型相当大(> 1 GB),因此我们不希望在用户点击“翻译”按钮之前下载它。
我们可以通过使用 Web Worker 和一些 React hook 来实现这两个目标。
在
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; } }
修改
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
的文件夹,并创建以下文件
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> ) }
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.jsx
中App
函数的开头。
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
目录中的以下文件
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; }
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 帐户。
- 访问https://huggingface.co/new-space并填写表单。请记住选择“静态”作为空间类型。
- 转到“文件”→“添加文件”→“上传文件”。将
dist
文件夹中的index.html
文件和public/
文件夹拖到上传框中,然后点击“上传”。上传完成后,向下滚动到按钮并点击“提交更改到主分支”。
就是这样!您的应用程序现在应该在https://huggingface.co/spaces/<your-username>/<your-space-name>
上线了!