diff --git a/README.md b/README.md index ad302df..243c80c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,93 @@ -# croissantllm-translate -A micro application based on CroissantLLM and running in the browser with WebLLM. Translates texts between French and English without sharing your content with the server. +
+ +# CroissantLLM-Translate + +[Demo App](https://numerique-gouv.github.io/croissant-translate/) + +
+ +## Overview + +A micro application based on CroissantLLM and running entirely in the browser with WebLLM. Translates texts between French and English without sharing your content with the server. + +- Fully private = No conversation data ever leaves your computer +- Runs in the browser = No server needed and no install needed! +- Works offline +- Easy-to-use interface + +This tool is built on top of [WebLLM](https://github.com/mlc-ai/web-llm), a package that brings language model inferebce directly onto web browsers with hardware acceleration. + +## System Requirements + +To run this, you need a modern browser with support for WebGPU. According to [caniuse](https://caniuse.com/?search=WebGPU), WebGPU is supported on: + +- Google Chrome +- Microsoft Edge +- All Chronium-based browsers + +It's also available in Firefox Nightly, but it needs to be enabled manually through the dom.webgpu.enabled flag. Safari on MacOS also has experimental support for WebGPU which can be enabled through the WebGPU experimental feature. + +In addition to WebGPU support, you need to have enough available RAM on your device to run the model (~3,5Gb). + +You can check if your browser or your device support WebGPU by visiting [webgpureport](https://webgpureport.org/). + +## Supported model + +We use [CroissantLLM](https://huggingface.co/croissantllm), a 1.3B language model pretrained on a set of 3T English and French tokens, which runs swiftly on consumer-grade local hardware. This model was developed by CentraleSupélec, and it takes up 2.7 Gb of storage in the browser's cache. + +To use CroissantLLM with WebLLM, we compiled the original model following the compilation process of [MLC](https://mlc.ai/). We quantized the model into several formats (from q0f32 to q3f16) and you can find all the files of each quantized model on Croissant's Hugging Face repository. We choose to use CroissantLLM with the half precision model, because it had the best performance-to-memory usage ratio. + +You can also use another model. To do that, you can compile your own model and weights with [MLC LLM](https://github.com/mlc-ai/mlc-llm). Then you just need to update [app-config](./src/app-config.ts) with: + +- The URL to model artifacts, such as weights and meta-data. +- The URL to web assembly library (i.e. wasm file) that contains the executables to accelerate the model computations. +- The name of the model. + +You also need to change the custom prompt added before the user's text in the [prompt](./src/prompt.ts) file. + +If you need further information, you can check the [MLC LLM documentation](https://llm.mlc.ai/docs/deploy/javascript.html) on how to add new model weights and libraries to WebLLM. + +## Try it out + +You can [try it here](https://numerique-gouv.github.io/croissant-translate/). You can run the project locally and contribute to improve the interface, speed up initial model loading time and fix bugs, by following these steps: + +### Prerequisite + +- NodeJS >= 20 - https://nodejs.org/ +- NPM + +### Setup & Run The Project + +If you're looking to make changes, run the development environment with live reload: + +```sh +# Clone the repository +git clone https://github.com/numerique-gouv/croissant-translate.git + +# Enter the project folder +cd ./croissant-translate + +# Install dependencies +npm install + +# Start the project for development +npm run dev +``` + +### Building the project for production + +To compile the react code yourself, run: + +```sh +# Compile and minify the project for publishing, outputs to `dist/` +npm run build + +# Build the project for publishing and preview it locally. Do not use this as a production server as it's not designed for it +npm run preview +``` + +This repository has a workflow that automatically deploys the site to GitHub Pages whenever the main branch is modified + +## License + +This work is released under the MIT License (see [LICENSE](./LICENSE)). diff --git a/src/App.tsx b/src/App.tsx index e5e2328..fccdae9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; -import './App.css'; +import { ActionIcon, Button, Textarea, Tooltip } from '@mantine/core'; +import { IconSwitchHorizontal, IconSwitchVertical } from '@tabler/icons-react'; import { ChatCompletionMessageParam, CreateWebWorkerEngine, @@ -7,10 +8,11 @@ import { InitProgressReport, hasModelInCache, } from '@mlc-ai/web-llm'; + +import './App.css'; import { appConfig } from './app-config'; import Progress from './components/Progress'; -import { ActionIcon, Button, Textarea, Tooltip } from '@mantine/core'; -import { IconSwitchHorizontal, IconSwitchVertical } from '@tabler/icons-react'; +import { promt_description } from './prompt'; declare global { interface Window { @@ -28,18 +30,10 @@ if (appConfig.useIndexedDBCache) { function App() { const selectedModel = 'CroissantLLMChat-v0.1-q0f16'; - const promptSentenceEnglishToFrench = - "Pouvez-vous traduire ce texte en francais sans ajouter d'informations ? Voici le texte :"; - const promptSentenceFrenchToEnglish = - 'Can you translate this text in english for me without adding informations: '; - const promptFrenchToEnglish = - 'Translate these words in english. Just write the word translated, nothing else: '; - const promptEnglishToFrench = - 'Traduis ces mots en francais. Ecris juste la traduction : '; const [engine, setEngine] = useState(null); const [progress, setProgress] = useState('Not loaded'); const [progressPercentage, setProgressPercentage] = useState(0); - const [isFecthing, setIsFetching] = useState(false); + const [isFetching, setIsFetching] = useState(false); const [isGenerating, setIsGenerating] = useState(false); const [runtimeStats, setRuntimeStats] = useState(''); const [input, setInput] = useState(''); @@ -49,7 +43,6 @@ function App() { const [errorBrowserMessage, setErrorBrowserMessage] = useState( null ); - //const [showModal, setShowModal] = useState(false); useEffect(() => { const compatibleBrowser = checkBrowser(); @@ -65,6 +58,9 @@ function App() { setOutput(''); }, [switched]); + /** + * Check if the browser is compatible with WebGPU. + */ const checkBrowser = () => { const userAgent = navigator.userAgent; let compatibleBrowser = true; @@ -96,8 +92,10 @@ function App() { return compatibleBrowser; }; + /** + * Callback for the progress of the model initialization. + */ const initProgressCallback = (report: InitProgressReport) => { - //console.log(report); if ( modelInCache === true || report.text.startsWith('Loading model from cache') @@ -105,7 +103,7 @@ function App() { setOutput('Chargement du modèle dans la RAM...'); } else { setOutput( - 'Téléchargement des points du modèle dans le cache de votre navigateur, cela peut prendre quelques minutes.' + 'Téléchargement des poids du modèle dans le cache de votre navigateur, cela peut prendre quelques minutes.' ); } @@ -119,8 +117,10 @@ function App() { setProgress(report.text); }; + /** + * Load the engine. + */ const loadEngine = async () => { - console.log('Loading engine...'); setIsFetching(true); setOutput('Chargement du modèle...'); @@ -138,6 +138,9 @@ function App() { return engine; }; + /** + * Send the input to the engine and get the output text translated. + */ const onSend = async (inputUser: string) => { if (inputUser === '') { return; @@ -147,11 +150,7 @@ function App() { let loadedEngine = engine; - const paragraphs = inputUser.split('\n'); - if (!loadedEngine) { - console.log('Engine not loaded'); - try { loadedEngine = await loadEngine(); } catch (error) { @@ -162,6 +161,8 @@ function App() { } } + const paragraphs = inputUser.split('\n'); + try { await loadedEngine.resetChat(); @@ -178,16 +179,17 @@ function App() { let prompt = ''; if (words.length > 5) { prompt = switched - ? promptSentenceEnglishToFrench - : promptSentenceFrenchToEnglish; + ? promt_description.promptSentenceEnglishToFrench + : promt_description.promptSentenceFrenchToEnglish; } else { - prompt = switched ? promptEnglishToFrench : promptFrenchToEnglish; + prompt = switched + ? promt_description.promptEnglishToFrench + : promt_description.promptFrenchToEnglish; } const userMessage: ChatCompletionMessageParam = { role: 'user', content: prompt + paragraph, }; - console.log(userMessage); const completion = await loadedEngine.chat.completions.create({ stream: true, messages: [userMessage], @@ -212,11 +214,8 @@ function App() { } setOutput(assistantMessage); - setIsGenerating(false); - setRuntimeStats(await loadedEngine.runtimeStatsText()); - console.log(await loadedEngine.runtimeStatsText()); } catch (error) { setIsGenerating(false); console.log('EXECPTION'); @@ -226,6 +225,9 @@ function App() { } }; + /** + * Reset the chat engine and the user input. + */ const reset = async () => { if (!engine) { console.log('Engine not loaded'); @@ -236,6 +238,9 @@ function App() { setOutput(''); }; + /** + * Stop the generation. + */ const onStop = () => { if (!engine) { console.log('Engine not loaded'); @@ -246,6 +251,9 @@ function App() { engine.interruptGenerate(); }; + /** + * Check if the model is in the cache. + */ const checkModelInCache = async () => { const isInChache = await hasModelInCache(selectedModel, appConfig); setModelInCache(isInChache); @@ -254,38 +262,6 @@ function App() { return ( <> - {/* setShowModal(false)} - withCloseButton={false} - centered - size='xl' - > -

- Ce site est un outil de traduction 100% souverain et confidentiel. - Contrairement à d'autres outils de traduction comme ChatGPT, le modèle - utilisé fonctionnent entièrement dans votre navigateur, ce qui - signifie que : -

-
    -
  • Vos données ne quittent jamais votre ordinateur.
  • -
  • - Après le téléchargement initial du modèle, vous pouvez déconnecter - votre WiFi, et la traduction fonctionnera toujours hors ligne. -
  • -
-

- Note : le premier message peut prendre un certain temps à traiter car - le modèle doit être entièrement téléchargé sur votre ordinateur. Mais - lors de vos prochaines visites sur ce site, le modèle se chargera - rapidement à partir du stockage local de votre ordinateur. -

-

Navigateurs supportés : Chrome, Edge (WebGPU requis)

-

- Ce projet est open source. Consultez la page Github pour plus de - détails et pour soumettre des bugs et des demandes de fonctionnalités. -

-
*/}

Traduction Anglais/Français

Un service 100% souverain et confidentiel

@@ -303,22 +279,6 @@ function App() {

)} - {/* - - */} - - {/* - - */} - {modelInCache !== null && (

Modèle téléchargé dans le cache de votre navigateur :{' '} @@ -333,7 +293,7 @@ function App() { autosize minRows={15} maxRows={15} - disabled={isFecthing} + disabled={isFetching} variant='filled' size='lg' label={switched ? 'Anglais' : 'Français'} @@ -348,7 +308,7 @@ function App() { variant='transparent' color='black' size='xl' - data-disabled={isFecthing || isGenerating} + data-disabled={isFetching || isGenerating} onClick={() => setSwitched((prevState) => !prevState)} className='switch-button' > @@ -362,7 +322,7 @@ function App() { variant='transparent' color='black' size='xl' - disabled={isFecthing || isGenerating} + disabled={isFetching || isGenerating} onClick={() => setSwitched((prevState) => !prevState)} className='switch-button' > @@ -377,19 +337,12 @@ function App() { autosize minRows={15} maxRows={15} - disabled={isFecthing} + disabled={isFetching} variant='filled' size='lg' label={switched ? 'Français' : 'Anglais'} className='textarea' /> - {/* - */}

@@ -397,8 +350,8 @@ function App() { variant='light' color='black' onClick={reset} - disabled={isGenerating || isFecthing} - loading={isFecthing} + disabled={isGenerating || isFetching} + loading={isFetching} > Effacer @@ -407,8 +360,8 @@ function App() { variant='light' color='black' onClick={() => onSend(input)} - disabled={isGenerating || isFecthing} - loading={isGenerating || isFecthing} + disabled={isGenerating || isFetching} + loading={isGenerating || isFetching} > Traduire @@ -418,7 +371,7 @@ function App() { onClick={onStop} color='black' disabled={!isGenerating} - loading={isFecthing} + loading={isFetching} > Stop diff --git a/src/prompt.ts b/src/prompt.ts new file mode 100644 index 0000000..e47caf0 --- /dev/null +++ b/src/prompt.ts @@ -0,0 +1,17 @@ +export enum Prompt { + SentenceEnglishToFrench = 'promptSentenceEnglishToFrench', + SentenceFrenchToEnglish = 'promptSentenceFrenchToEnglish', + FrenchToEnglish = 'promptFrenchToEnglish', + EnglishToFrench = 'promptEnglishToFrench', +} + +export const promt_description: { [key in Prompt]: string } = { + promptSentenceEnglishToFrench: + "Pouvez-vous traduire ce texte en francais sans ajouter d'informations ? Voici le texte :", + promptSentenceFrenchToEnglish: + 'Can you translate this text in english for me without adding informations: ', + promptFrenchToEnglish: + 'Translate these words in english. Just write the word translated, nothing else: ', + promptEnglishToFrench: + 'Traduis ces mots en francais. Ecris juste la traduction : ', +};