Experiment using browser's built in translation API
Spec: https://webmachinelearning.github.io/translation-api/ API may change in the future
This commit is contained in:
parent
23e9d034e9
commit
945d2ac206
5 changed files with 395 additions and 240 deletions
|
@ -28,6 +28,7 @@ import Menu2 from '../components/menu2';
|
||||||
import supportedLanguages from '../data/status-supported-languages';
|
import supportedLanguages from '../data/status-supported-languages';
|
||||||
import urlRegex from '../data/url-regex';
|
import urlRegex from '../data/url-regex';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
import { langDetector } from '../utils/browser-translator';
|
||||||
import db from '../utils/db';
|
import db from '../utils/db';
|
||||||
import emojifyText from '../utils/emojify-text';
|
import emojifyText from '../utils/emojify-text';
|
||||||
import i18nDuration from '../utils/i18n-duration';
|
import i18nDuration from '../utils/i18n-duration';
|
||||||
|
@ -1875,6 +1876,12 @@ const getCustomEmojis = pmem(_getCustomEmojis, {
|
||||||
});
|
});
|
||||||
|
|
||||||
const detectLangs = async (text) => {
|
const detectLangs = async (text) => {
|
||||||
|
if (langDetector) {
|
||||||
|
const langs = await langDetector.detect(text);
|
||||||
|
if (langs?.length) {
|
||||||
|
return langs.slice(0, 2).map((lang) => lang.detectedLanguage);
|
||||||
|
}
|
||||||
|
}
|
||||||
const { detectAll } = await import('tinyld/light');
|
const { detectAll } = await import('tinyld/light');
|
||||||
const langs = detectAll(text);
|
const langs = detectAll(text);
|
||||||
if (langs?.length) {
|
if (langs?.length) {
|
||||||
|
|
|
@ -41,6 +41,7 @@ import Modal from '../components/modal';
|
||||||
import NameText from '../components/name-text';
|
import NameText from '../components/name-text';
|
||||||
import Poll from '../components/poll';
|
import Poll from '../components/poll';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
import { langDetector } from '../utils/browser-translator';
|
||||||
import emojifyText from '../utils/emojify-text';
|
import emojifyText from '../utils/emojify-text';
|
||||||
import enhanceContent from '../utils/enhance-content';
|
import enhanceContent from '../utils/enhance-content';
|
||||||
import FilterContext from '../utils/filter-context';
|
import FilterContext from '../utils/filter-context';
|
||||||
|
@ -253,7 +254,6 @@ const SIZE_CLASS = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const detectLang = pmem(async (text) => {
|
const detectLang = pmem(async (text) => {
|
||||||
const { detectAll } = await import('tinyld/light');
|
|
||||||
text = text?.trim();
|
text = text?.trim();
|
||||||
|
|
||||||
// Ref: https://github.com/komodojp/tinyld/blob/develop/docs/benchmark.md
|
// Ref: https://github.com/komodojp/tinyld/blob/develop/docs/benchmark.md
|
||||||
|
@ -261,7 +261,29 @@ const detectLang = pmem(async (text) => {
|
||||||
if (text?.length > 500) {
|
if (text?.length > 500) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (langDetector) {
|
||||||
|
const langs = await langDetector.detect(text);
|
||||||
|
console.groupCollapsed(
|
||||||
|
'💬 DETECTLANG BROWSER',
|
||||||
|
langs.slice(0, 3).map((l) => l.detectedLanguage),
|
||||||
|
);
|
||||||
|
console.log(text, langs.slice(0, 3));
|
||||||
|
console.groupEnd();
|
||||||
|
const lang = langs[0];
|
||||||
|
if (lang?.detectedLanguage && lang?.confidence > 0.5) {
|
||||||
|
return lang.detectedLanguage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { detectAll } = await import('tinyld/light');
|
||||||
const langs = detectAll(text);
|
const langs = detectAll(text);
|
||||||
|
console.groupCollapsed(
|
||||||
|
'💬 DETECTLANG TINYLD',
|
||||||
|
langs.slice(0, 3).map((l) => l.lang),
|
||||||
|
);
|
||||||
|
console.log(text, langs.slice(0, 3));
|
||||||
|
console.groupEnd();
|
||||||
const lang = langs[0];
|
const lang = langs[0];
|
||||||
if (lang?.lang && lang?.accuracy > 0.5) {
|
if (lang?.lang && lang?.accuracy > 0.5) {
|
||||||
// If > 50% accurate, use it
|
// If > 50% accurate, use it
|
||||||
|
|
|
@ -6,6 +6,10 @@ import pThrottle from 'p-throttle';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
|
||||||
import sourceLanguages from '../data/lingva-source-languages';
|
import sourceLanguages from '../data/lingva-source-languages';
|
||||||
|
import {
|
||||||
|
translate as browserTranslate,
|
||||||
|
supportsBrowserTranslator,
|
||||||
|
} from '../utils/browser-translator';
|
||||||
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
|
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
|
||||||
import localeCode2Text from '../utils/localeCode2Text';
|
import localeCode2Text from '../utils/localeCode2Text';
|
||||||
import pmem from '../utils/pmem';
|
import pmem from '../utils/pmem';
|
||||||
|
@ -95,7 +99,22 @@ function TranslationBlock({
|
||||||
const apiSourceLang = useRef('auto');
|
const apiSourceLang = useRef('auto');
|
||||||
|
|
||||||
if (!onTranslate) {
|
if (!onTranslate) {
|
||||||
onTranslate = mini ? throttledLingvaTranslate : lingvaTranslate;
|
// onTranslate = supportsBrowserTranslator
|
||||||
|
// ? browserTranslate
|
||||||
|
// : mini
|
||||||
|
// ? throttledLingvaTranslate
|
||||||
|
// : lingvaTranslate;
|
||||||
|
onTranslate = async (...args) => {
|
||||||
|
if (supportsBrowserTranslator) {
|
||||||
|
const result = await browserTranslate(...args);
|
||||||
|
if (result && !result.error) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mini
|
||||||
|
? await throttledLingvaTranslate(...args)
|
||||||
|
: await lingvaTranslate(...args);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const translate = async () => {
|
const translate = async () => {
|
||||||
|
|
476
src/locales/en.po
generated
476
src/locales/en.po
generated
File diff suppressed because it is too large
Load diff
107
src/utils/browser-translator.js
Normal file
107
src/utils/browser-translator.js
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
export const supportsBrowserTranslator =
|
||||||
|
'ai' in self && 'translator' in self.ai;
|
||||||
|
|
||||||
|
// https://developer.chrome.com/docs/ai/language-detection
|
||||||
|
export let langDetector;
|
||||||
|
if (supportsBrowserTranslator) {
|
||||||
|
try {
|
||||||
|
const languageDetectorCapabilities =
|
||||||
|
await self.ai.languageDetector.capabilities();
|
||||||
|
const canDetect = languageDetectorCapabilities.capabilities;
|
||||||
|
if (canDetect === 'no') {
|
||||||
|
// The language detector isn't usable.
|
||||||
|
// return;
|
||||||
|
}
|
||||||
|
if (canDetect === 'readily') {
|
||||||
|
// The language detector can immediately be used.
|
||||||
|
langDetector = await self.ai.languageDetector.create();
|
||||||
|
} else {
|
||||||
|
// The language detector can be used after model download.
|
||||||
|
langDetector = await self.ai.languageDetector.create({
|
||||||
|
monitor(m) {
|
||||||
|
m.addEventListener('downloadprogress', (e) => {
|
||||||
|
console.log(
|
||||||
|
`Detector: Downloaded ${e.loaded} of ${e.total} bytes.`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await langDetector.ready;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://developer.chrome.com/docs/ai/translator-api
|
||||||
|
export const translate = async (text, source, target) => {
|
||||||
|
let detectedSourceLanguage;
|
||||||
|
const originalSource = source;
|
||||||
|
if (source === 'auto') {
|
||||||
|
try {
|
||||||
|
const results = await langDetector.detect(text);
|
||||||
|
source = results[0].detectedLanguage;
|
||||||
|
detectedSourceLanguage = source;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return {
|
||||||
|
error: e,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.groupCollapsed(
|
||||||
|
'💬 BROWSER TRANSLATE',
|
||||||
|
originalSource,
|
||||||
|
detectedSourceLanguage,
|
||||||
|
target,
|
||||||
|
);
|
||||||
|
console.log(text);
|
||||||
|
try {
|
||||||
|
const translatorCapabilities = await self.ai.translator.capabilities();
|
||||||
|
const canTranslate = translatorCapabilities.languagePairAvailable(
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
);
|
||||||
|
if (canTranslate === 'no') {
|
||||||
|
console.groupEnd();
|
||||||
|
return {
|
||||||
|
error: `Unsupported language pair: ${source} -> ${target}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let translator;
|
||||||
|
if (canTranslate === 'readily') {
|
||||||
|
translator = await self.ai.translator.create({
|
||||||
|
sourceLanguage: source,
|
||||||
|
targetLanguage: target,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
translator = await self.ai.translator.create({
|
||||||
|
sourceLanguage: source,
|
||||||
|
targetLanguage: target,
|
||||||
|
monitor(m) {
|
||||||
|
m.addEventListener('downloadprogress', (e) => {
|
||||||
|
console.log(
|
||||||
|
`Translate ${source} -> ${target}: Downloaded ${e.loaded} of ${e.total} bytes.`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await translator.translate(text);
|
||||||
|
console.log(content);
|
||||||
|
console.groupEnd();
|
||||||
|
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
detectedSourceLanguage,
|
||||||
|
provider: 'browser',
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.groupEnd();
|
||||||
|
console.error(e);
|
||||||
|
return {
|
||||||
|
error: e,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
Loading…
Add table
Reference in a new issue