diff --git a/.cspell-wordlist.txt b/.cspell-wordlist.txt index e8a0412c75..3c17403a49 100644 --- a/.cspell-wordlist.txt +++ b/.cspell-wordlist.txt @@ -448,3 +448,4 @@ Premul eurollm doclayout netp +Uyghur diff --git a/apps/nlp/app/_layout.tsx b/apps/nlp/app/_layout.tsx index 7d637faa15..69520828e7 100644 --- a/apps/nlp/app/_layout.tsx +++ b/apps/nlp/app/_layout.tsx @@ -48,6 +48,13 @@ export default function Layout() { title: 'Privacy Filter', }} /> + router.navigate('privacy-filter/')}> Privacy Filter + router.navigate('translation/')}> + Translation + router.navigate('llm/')}> LLM diff --git a/apps/nlp/app/translation/index.tsx b/apps/nlp/app/translation/index.tsx new file mode 100644 index 0000000000..e45b0ffc8c --- /dev/null +++ b/apps/nlp/app/translation/index.tsx @@ -0,0 +1,303 @@ +import React, { useState } from 'react'; +import { + View, + Text, + TextInput, + ScrollView, + StyleSheet, + TouchableOpacity, + KeyboardAvoidingView, + Platform, +} from 'react-native'; +import { useTranslator, models, type nlp, type TranslatorModel } from 'react-native-executorch'; +import ScreenWrapper from '../../components/ScreenWrapper'; +import { ModelStatus } from '../../components/ModelStatus'; +import { Button } from '../../components/Button'; +import { theme } from '../../theme'; + +/* cspell:disable */ +const SAMPLES: Record = { + en: 'The weather is beautiful today. Shall we go for a walk in the park?\nI will bring something to drink.', + de: 'Das Wetter ist heute wunderschön. Sollen wir im Park spazieren gehen?\nIch bringe etwas zu trinken mit.', + es: 'Hoy hace un tiempo precioso. ¿Vamos a dar un paseo por el parque?\nYo llevaré algo de beber.', + fr: 'Il fait très beau aujourd’hui. On va se promener dans le parc ?\nJ’apporterai quelque chose à boire.', + pl: 'Dzisiaj jest piękna pogoda. Może pójdziemy na spacer do parku?\nPrzyniosę coś do picia.', + zh: '今天天气真好。我们去公园散步好吗?\n我会带点喝的。', + ja: '今日はとても良い天気です。公園に散歩に行きませんか?\n飲み物を持っていきます。', +}; +/* cspell:enable */ + +type Pair = { label: string; languages: nlp.TranslationLanguages }; +const pair = (sourceLanguage: string, targetLanguage: string): Pair => ({ + label: `${sourceLanguage.toUpperCase()} → ${targetLanguage.toUpperCase()}`, + languages: { sourceLanguage, targetLanguage }, +}); + +// Opus-MT ships one model per pair; EuroLLM and Hy-MT2 translate every pair with one. +const OPUS_MT: { pair: Pair; model: TranslatorModel }[] = [ + { pair: pair('en', 'de'), model: models.translation.OPUS_MT_EN_DE.DEFAULT }, + { pair: pair('de', 'en'), model: models.translation.OPUS_MT_DE_EN.DEFAULT }, + { pair: pair('en', 'es'), model: models.translation.OPUS_MT_EN_ES.DEFAULT }, + { pair: pair('es', 'en'), model: models.translation.OPUS_MT_ES_EN.DEFAULT }, + { pair: pair('en', 'fr'), model: models.translation.OPUS_MT_EN_FR.DEFAULT }, + { pair: pair('fr', 'en'), model: models.translation.OPUS_MT_FR_EN.DEFAULT }, +]; +const EUROLLM_PAIRS = [...OPUS_MT.map((m) => m.pair), pair('en', 'pl'), pair('pl', 'en')]; +const HY_MT2_PAIRS = [ + ...EUROLLM_PAIRS, + pair('en', 'zh'), + pair('zh', 'en'), + pair('en', 'ja'), + pair('ja', 'en'), +]; + +const FAMILIES = [ + { + label: 'Opus-MT', + description: + 'One small model per language pair. Translates a sentence at a time and streams the result as it is decoded.', + pairs: OPUS_MT.map((m) => m.pair), + modelFor: (i: number) => OPUS_MT[i]!.model, + }, + { + label: 'EuroLLM', + description: + 'One 1.7 GB model for 35 languages, so switching the pair does not reload it. Translates a line at a time.', + pairs: EUROLLM_PAIRS, + modelFor: () => models.translation.EUROLLM_1_7B.DEFAULT as TranslatorModel, + }, + { + label: 'Hy-MT2', + description: + 'One 1.2 GB model for 38 languages and dialects, strongest on Chinese. Translates a line at a time.', + pairs: HY_MT2_PAIRS, + modelFor: () => models.translation.HY_MT2_1_8B.DEFAULT as TranslatorModel, + }, +]; + +function TranslationContent() { + const [family, setFamily] = useState(0); + const [selected, setSelected] = useState(0); + const { pairs, modelFor, description } = FAMILIES[family]!; + const active = pairs[selected]!; + const { isReady, downloadProgress, error, translate, translateStop } = useTranslator( + modelFor(selected) + ); + + const [text, setText] = useState(SAMPLES[active.languages.sourceLanguage] ?? ''); + const [output, setOutput] = useState(''); + const [busy, setBusy] = useState(false); + const [runError, setRunError] = useState(null); + const [elapsedMs, setElapsedMs] = useState(null); + + const select = (nextFamily: number, nextPair: number) => { + if (nextFamily === family && nextPair === selected) return; + translateStop?.(); + setFamily(nextFamily); + setSelected(nextPair); + const source = FAMILIES[nextFamily]!.pairs[nextPair]!.languages.sourceLanguage; + setText(SAMPLES[source] ?? ''); + setOutput(''); + setRunError(null); + setElapsedMs(null); + }; + + const run = async () => { + if (!translate || !text.trim()) return; + setBusy(true); + setRunError(null); + setOutput(''); + setElapsedMs(null); + try { + const startedAt = Date.now(); + const result = await translate(text, active.languages, (piece) => + setOutput((prev) => prev + piece) + ); + setElapsedMs(Date.now() - startedAt); + setOutput(result); + } catch (e: any) { + const msg = e?.message ?? String(e); + if (!/disposed/i.test(msg)) setRunError(msg); + } finally { + setBusy(false); + } + }; + + const chips = (labels: string[], current: number, onPress: (i: number) => void) => ( + + {labels.map((label, i) => ( + onPress(i)} + > + {label} + + ))} + + ); + + return ( + + + + Translation + {description} + + Model + {chips( + FAMILIES.map((f) => f.label), + family, + (i) => select(i, 0) + )} + + Language pair + {chips( + pairs.map((p) => p.label), + selected, + (i) => select(family, i) + )} + + + + + {runError && ( + + {runError} + + )} + + + Text to translate + + +