feat: support builtin AI
This commit is contained in:
@@ -15,3 +15,6 @@ function _browser() {
|
||||
export const browser = _browser();
|
||||
|
||||
export const isBg = () => globalThis?.ContextType === "BACKGROUND";
|
||||
|
||||
export const isBuiltinAIAvailable =
|
||||
"LanguageDetector" in globalThis && "Translator" in globalThis;
|
||||
|
||||
168
src/libs/builtinAI.js
Normal file
168
src/libs/builtinAI.js
Normal file
@@ -0,0 +1,168 @@
|
||||
import { kissLog, logger } from "./log";
|
||||
|
||||
/**
|
||||
* Chrome 浏览器内置翻译
|
||||
*/
|
||||
class ChromeTranslator {
|
||||
#translatorMap = new Map();
|
||||
#detectorPromise = null;
|
||||
|
||||
constructor(options = {}) {
|
||||
this.onProgress = options.onProgress || this.#defaultProgressHandler;
|
||||
}
|
||||
|
||||
#defaultProgressHandler(type, progress) {
|
||||
kissLog(`Downloading ${type} model: ${progress}%`);
|
||||
}
|
||||
|
||||
#getDetectorPromise() {
|
||||
if (!this.#detectorPromise) {
|
||||
this.#detectorPromise = (async () => {
|
||||
try {
|
||||
const availability = await LanguageDetector.availability();
|
||||
if (availability === "unavailable") {
|
||||
throw new Error("LanguageDetector unavailable");
|
||||
}
|
||||
|
||||
return await LanguageDetector.create({
|
||||
monitor: (m) => this._monitorProgress(m, "detector"),
|
||||
});
|
||||
} catch (error) {
|
||||
this.#detectorPromise = null;
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
return this.#detectorPromise;
|
||||
}
|
||||
|
||||
#createTranslator(sourceLanguage, targetLanguage) {
|
||||
const key = `${sourceLanguage}_${targetLanguage}`;
|
||||
if (this.#translatorMap.has(key)) {
|
||||
return this.#translatorMap.get(key);
|
||||
}
|
||||
|
||||
const translatorPromise = (async () => {
|
||||
try {
|
||||
const avail = await Translator.availability({
|
||||
sourceLanguage,
|
||||
targetLanguage,
|
||||
});
|
||||
if (avail === "unavailable") {
|
||||
throw new Error(
|
||||
`Translator ${sourceLanguage}_${targetLanguage} unavailable`
|
||||
);
|
||||
}
|
||||
|
||||
const translator = await Translator.create({
|
||||
sourceLanguage,
|
||||
targetLanguage,
|
||||
monitor: (m) => this._monitorProgress(m, `translator (${key})`),
|
||||
});
|
||||
this.#translatorMap.set(key, translator);
|
||||
|
||||
return translator;
|
||||
} catch (error) {
|
||||
this.#translatorMap.delete(key);
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
|
||||
this.#translatorMap.set(key, translatorPromise);
|
||||
return translatorPromise;
|
||||
}
|
||||
|
||||
_monitorProgress(monitorable, type) {
|
||||
monitorable.addEventListener("downloadprogress", (e) => {
|
||||
const progress = e.total > 0 ? Math.round((e.loaded / e.total) * 100) : 0;
|
||||
this.onProgress(type, progress);
|
||||
});
|
||||
}
|
||||
|
||||
async detectLanguage(text, confidenceThreshold = 0.4) {
|
||||
if (!text) {
|
||||
return ["", "Input text cannot be empty."];
|
||||
}
|
||||
|
||||
try {
|
||||
const detector = await this.#getDetectorPromise();
|
||||
const results = await detector.detect(text);
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
return ["", "No language could be detected."];
|
||||
}
|
||||
|
||||
const { detectedLanguage, confidence } = results[0];
|
||||
if (confidence < confidenceThreshold) {
|
||||
return [
|
||||
"",
|
||||
`Confidence of test results (${detectedLanguage} ${confidence.toFixed(
|
||||
2
|
||||
)}) below the set threshold ${confidenceThreshold}。`,
|
||||
];
|
||||
}
|
||||
|
||||
return [detectedLanguage, ""];
|
||||
} catch (error) {
|
||||
kissLog("detectLanguage", error, `(${text})`);
|
||||
return ["", error.message];
|
||||
}
|
||||
}
|
||||
|
||||
async translateText(text, targetLanguage, sourceLanguage = "auto") {
|
||||
if (!text || !targetLanguage || typeof text !== "string") {
|
||||
return ["", sourceLanguage, "Input text cannot be empty."];
|
||||
}
|
||||
|
||||
try {
|
||||
let finalSourceLanguage = sourceLanguage;
|
||||
if (sourceLanguage === "auto") {
|
||||
const [detectedLanguage, detectionError] =
|
||||
await this.detectLanguage(text);
|
||||
if (detectionError || !detectedLanguage) {
|
||||
const reason =
|
||||
detectionError || "Unable to determine source language.";
|
||||
return [
|
||||
"",
|
||||
finalSourceLanguage,
|
||||
`Automatic detection of source language failed: ${reason}`,
|
||||
];
|
||||
}
|
||||
finalSourceLanguage = detectedLanguage;
|
||||
}
|
||||
|
||||
if (finalSourceLanguage === targetLanguage) {
|
||||
return ["", finalSourceLanguage, "Same lang"];
|
||||
}
|
||||
|
||||
const translator = await this.#createTranslator(
|
||||
finalSourceLanguage,
|
||||
targetLanguage
|
||||
);
|
||||
const translatedText = await translator.translate(text);
|
||||
|
||||
return [translatedText, finalSourceLanguage, ""];
|
||||
} catch (error) {
|
||||
kissLog("translateText", error, `(${text})`);
|
||||
|
||||
if (
|
||||
error &&
|
||||
error.message &&
|
||||
error.message.includes("Other generic failures occurred")
|
||||
) {
|
||||
logger.error("Generic failure detected, resetting translator cache.");
|
||||
this.#translatorMap.clear();
|
||||
}
|
||||
|
||||
return ["", sourceLanguage, error.message];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const chromeTranslator = new ChromeTranslator();
|
||||
|
||||
export const chromeDetect = (args) =>
|
||||
chromeTranslator.detectLanguage(args.text);
|
||||
export const chromeTranslate = (args) =>
|
||||
chromeTranslator.translateText(args.text, args.to, args.from);
|
||||
@@ -45,7 +45,7 @@ const newCacheReq = async (input, init) => {
|
||||
* @param {*} init
|
||||
* @returns
|
||||
*/
|
||||
export const getHttpCache = async (input, init) => {
|
||||
export const getHttpCache = async ({ input, init }) => {
|
||||
try {
|
||||
const req = await newCacheReq(input, init);
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
@@ -65,12 +65,12 @@ export const getHttpCache = async (input, init) => {
|
||||
* @param {*} init
|
||||
* @param {*} data
|
||||
*/
|
||||
export const putHttpCache = async (
|
||||
export const putHttpCache = async ({
|
||||
input,
|
||||
init,
|
||||
data,
|
||||
maxAge = DEFAULT_CACHE_TIMEOUT // todo: 从设置里面读取最大缓存时间
|
||||
) => {
|
||||
maxAge = DEFAULT_CACHE_TIMEOUT, // todo: 从设置里面读取最大缓存时间
|
||||
}) => {
|
||||
try {
|
||||
const req = await newCacheReq(input, init);
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
@@ -132,7 +132,7 @@ export const getHttpCachePolyfill = (input, init) => {
|
||||
}
|
||||
|
||||
// 油猴/网页/BackgroundPage
|
||||
return getHttpCache(input, init);
|
||||
return getHttpCache({ input, init });
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -149,5 +149,5 @@ export const putHttpCachePolyfill = (input, init, data) => {
|
||||
}
|
||||
|
||||
// 油猴/网页/BackgroundPage
|
||||
return putHttpCache(input, init, data);
|
||||
return putHttpCache({ input, init, data });
|
||||
};
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
OPT_TRANS_TENCENT,
|
||||
OPT_LANGS_TO_CODE,
|
||||
OPT_LANGS_MAP,
|
||||
API_SPE_TYPES,
|
||||
OPT_TRANS_BUILTINAI,
|
||||
OPT_LANGDETECTOR_MAP,
|
||||
} from "../config";
|
||||
import { browser } from "./browser";
|
||||
import {
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
apiMicrosoftLangdetect,
|
||||
apiBaiduLangdetect,
|
||||
apiTencentLangdetect,
|
||||
apiBuiltinAIDetect,
|
||||
} from "../apis";
|
||||
import { kissLog } from "./log";
|
||||
|
||||
@@ -21,6 +23,7 @@ const langdetectFns = {
|
||||
[OPT_TRANS_MICROSOFT]: apiMicrosoftLangdetect,
|
||||
[OPT_TRANS_BAIDU]: apiBaiduLangdetect,
|
||||
[OPT_TRANS_TENCENT]: apiTencentLangdetect,
|
||||
[OPT_TRANS_BUILTINAI]: apiBuiltinAIDetect,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -31,8 +34,8 @@ const langdetectFns = {
|
||||
export const tryDetectLang = async (text, langDetector = "-") => {
|
||||
let deLang = "";
|
||||
|
||||
// 远程识别
|
||||
if (API_SPE_TYPES.detector.has(langDetector)) {
|
||||
// 内置AI/远程识别
|
||||
if (OPT_LANGDETECTOR_MAP.has(langDetector)) {
|
||||
try {
|
||||
const lang = await langdetectFns[langDetector](text);
|
||||
if (lang) {
|
||||
|
||||
@@ -101,14 +101,14 @@ export const fetchHandle = async ({ input, init, opts }) => {
|
||||
* @param {*} args
|
||||
* @returns
|
||||
*/
|
||||
const fetchPolyfill = (args) => {
|
||||
export const fnPolyfill = ({ fn, msg = MSG_FETCH, ...args }) => {
|
||||
// 插件
|
||||
if (isExt && !isBg()) {
|
||||
return sendBgMsg(MSG_FETCH, args);
|
||||
return sendBgMsg(msg, { ...args });
|
||||
}
|
||||
|
||||
// 油猴/网页/BackgroundPage
|
||||
return fetchHandle(args);
|
||||
return fn({ ...args });
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -138,9 +138,9 @@ export const fetchData = async (
|
||||
// 通过任务池发送请求
|
||||
if (usePool) {
|
||||
const fetchPool = getFetchPool(fetchInterval, fetchLimit);
|
||||
return fetchPool.push(fetchPolyfill, { input, init, opts });
|
||||
return fetchPool.push(fnPolyfill, { fn: fetchHandle, input, init, opts });
|
||||
}
|
||||
|
||||
// 直接请求
|
||||
return fetchPolyfill({ input, init, opts });
|
||||
return fnPolyfill({ fn: fetchHandle, input, init, opts });
|
||||
};
|
||||
|
||||
@@ -274,12 +274,6 @@ export class Translator {
|
||||
#glossary = {}; // AI词典
|
||||
#textClass = {}; // 译文样式class
|
||||
#textSheet = ""; // 译文样式字典
|
||||
#apiSetting = null;
|
||||
#placeholder = {
|
||||
startDelimiter: "{",
|
||||
endDelimiter: "}",
|
||||
tagName: "i",
|
||||
};
|
||||
|
||||
#isUserscript = false;
|
||||
#transboxManager = null; // 划词翻译
|
||||
@@ -309,20 +303,30 @@ export class Translator {
|
||||
return `${Translator.BUILTIN_IGNORE_SELECTOR}, ${this.#rule.ignoreSelector}`;
|
||||
}
|
||||
|
||||
constructor(rule = {}, setting = {}, isUserscript = false) {
|
||||
this.#setting = { ...Translator.DEFAULT_OPTIONS, ...setting };
|
||||
this.#rule = { ...Translator.DEFAULT_RULE, ...rule };
|
||||
this.#apiSetting =
|
||||
// 接口参数
|
||||
// todo: 不用频繁查找计算
|
||||
get #apiSetting() {
|
||||
return (
|
||||
this.#setting.transApis.find(
|
||||
(api) => api.apiSlug === this.#rule.apiSlug
|
||||
) || DEFAULT_API_SETTING;
|
||||
) || DEFAULT_API_SETTING
|
||||
);
|
||||
}
|
||||
|
||||
// 占位符
|
||||
get #placeholder() {
|
||||
const [startDelimiter, endDelimiter] =
|
||||
this.#apiSetting.placeholder.split(" ");
|
||||
this.#placeholder = {
|
||||
return {
|
||||
startDelimiter,
|
||||
endDelimiter,
|
||||
tagName: this.#apiSetting.placetag,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(rule = {}, setting = {}, isUserscript = false) {
|
||||
this.#setting = { ...Translator.DEFAULT_OPTIONS, ...setting };
|
||||
this.#rule = { ...Translator.DEFAULT_RULE, ...rule };
|
||||
|
||||
this.#isUserscript = isUserscript;
|
||||
this.#eventName = genEventName();
|
||||
|
||||
@@ -333,3 +333,20 @@ export const parseUrlPattern = (href) => {
|
||||
}
|
||||
return href;
|
||||
};
|
||||
|
||||
/**
|
||||
* 带超时的任务
|
||||
* @param {Promise|Function} task - 任务
|
||||
* @param {number} timeout - 超时时间 (毫秒)
|
||||
* @param {string} [timeoutMsg] - 超时错误提示
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const withTimeout = (task, timeout, timeoutMsg = "Task timed out") => {
|
||||
const promise = typeof task === "function" ? task() : task;
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error(timeoutMsg)), timeout)
|
||||
),
|
||||
]);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user