From 75e1fba005d03d0d0cf7d12744175cc5213f1f11 Mon Sep 17 00:00:00 2001 From: Thomas Beverley Date: Thu, 14 Nov 2024 15:29:45 +0000 Subject: [PATCH] Polyfill the window.translation apis ontop of the ai translation apis #1 --- .../APIHandler/AILanguageDetectorHandler.ts | 68 +++++---- .../AILanguageDetector/AILanguageDetector.ts | 4 +- .../contentscript-main/Translation.ts | 133 ++++++++++++++++++ src/extension/contentscript-main/index.ts | 4 + 4 files changed, 177 insertions(+), 32 deletions(-) create mode 100644 src/extension/contentscript-main/Translation.ts diff --git a/src/extension/background/APIHandler/AILanguageDetectorHandler.ts b/src/extension/background/APIHandler/AILanguageDetectorHandler.ts index 51d3664..f8ef59b 100644 --- a/src/extension/background/APIHandler/AILanguageDetectorHandler.ts +++ b/src/extension/background/APIHandler/AILanguageDetectorHandler.ts @@ -87,39 +87,47 @@ class AILanguageDetectorHandler { const input = payload.getString('input') const prompt = this.#getPrompt(manifest, input) const sessionId = payload.getNonEmptyString('sessionId') - - const result = JSON.parse(await AILlmSession.prompt( - sessionId, - prompt, - { - ...props, - grammar: { - type: 'object', - properties: { - detectedLanguage: { - type: 'string', - enum: AILanguageDetectorDefaultLanguages + const results: AILanguageDetectorDetectResult[] = [] + + for (let i = 0; i < 1; i++) { + const detectedLanguage = new Set(results.map(result => result.detectedLanguage)) + const languages = AILanguageDetectorDefaultLanguages.filter((language) => !detectedLanguage.has(language)) + + const result = JSON.parse(await AILlmSession.prompt( + sessionId, + prompt, + { + ...props, + grammar: { + type: 'object', + properties: { + detectedLanguage: { + type: 'string', + enum: languages + }, + confidence: { + type: 'integer', + minimum: 0, + maximum: 100 + } }, - confidence: { - type: 'integer', - minimum: 0, - maximum: 100 - } - }, - required: ['detectedLanguage', 'confidence'], - additionalProperties: false + required: ['detectedLanguage', 'confidence'], + additionalProperties: false + } + }, + { + signal: channel.abortSignal, + stream: noop } - }, - { - signal: channel.abortSignal, - stream: noop - } - )) + )) - return { - detectedLanguage: result.detectedLanguage, - confidence: result.confidence / 100 - } as AILanguageDetectorDetectResult + results.push({ + detectedLanguage: result.detectedLanguage, + confidence: result.confidence / 100 + } as AILanguageDetectorDetectResult) + } + + return results }) } diff --git a/src/extension/contentscript-main/AILanguageDetector/AILanguageDetector.ts b/src/extension/contentscript-main/AILanguageDetector/AILanguageDetector.ts index 69dde89..d1ad386 100644 --- a/src/extension/contentscript-main/AILanguageDetector/AILanguageDetector.ts +++ b/src/extension/contentscript-main/AILanguageDetector/AILanguageDetector.ts @@ -58,12 +58,12 @@ class AILanguageDetector extends AIRootModel { // MARK: Detection /* **************************************************************************/ - detect = async (input: string, options: AILanguageDetectorDetectOptions = {}):Promise => { + detect = async (input: string, options: AILanguageDetectorDetectOptions = {}):Promise => { this.#guardDestroyed() const signal = AbortSignal.any([options.signal, this.#signal].filter(Boolean)) - const result = (await IPC.request(kLanguageDetectorDetect, { props: this.#props, input }, { signal })) as AILanguageDetectorDetectResult + const result = (await IPC.request(kLanguageDetectorDetect, { props: this.#props, input }, { signal })) as AILanguageDetectorDetectResult[] return result } } diff --git a/src/extension/contentscript-main/Translation.ts b/src/extension/contentscript-main/Translation.ts new file mode 100644 index 0000000..4922123 --- /dev/null +++ b/src/extension/contentscript-main/Translation.ts @@ -0,0 +1,133 @@ +import AI from './AI' +import AILanguageDetector from './AILanguageDetector/AILanguageDetector' +import AITranslator from './AITranslator/AITranslator' + +type DetectorBridge = { + aiDetector?: AILanguageDetector + readyResolvers: Array<(value: any) => void> +} + +type TranslatorOptions = { + sourceLanguage: string + targetLanguage: string +} + +/* **************************************************************************/ +// +// MARK: ==Detector== +// +/* **************************************************************************/ + +class Detector extends EventTarget { + #bridge: DetectorBridge + + constructor (bridge: DetectorBridge) { + super() + this.#bridge = bridge + } + + get ready () { + if (this.#bridge.aiDetector) { + return Promise + } else { + return new Promise((resolve) => { + this.#bridge.readyResolvers.push(resolve) + }) + } + } + + detect = async (input: string) => { + await this.ready + return this.#bridge.aiDetector.detect(input) + } +} + +/* **************************************************************************/ +// +// MARK: ==Translator== +// +/* **************************************************************************/ + +class Translator { + #aiTranslator: AITranslator + + constructor (aiTranslator: AITranslator) { + this.#aiTranslator = aiTranslator + } + + translate = async (input: string) => { + return this.#aiTranslator.translate(input) + } +} + +/* **************************************************************************/ +// +// MARK: ==Translation== +// +/* **************************************************************************/ + +class Translation { + /* **************************************************************************/ + // MARK: Private + /* **************************************************************************/ + + #ai: AI + #downloadProgressFn: (evt: Event) => void + + /* **************************************************************************/ + // MARK: Lifecycle + /* **************************************************************************/ + + constructor (ai: AI) { + this.#ai = ai + } + + /* **************************************************************************/ + // MARK: Detection + /* **************************************************************************/ + + canDetect = async () => { + const capabilities = await this.#ai.languageDetector.capabilities() + return capabilities.available + } + + createDetector = async () => { + const bridge: DetectorBridge = { + readyResolvers: [] + } + const detector = new Detector(bridge) + + this.#ai.languageDetector.create({ + monitor: (m: EventTarget) => { + m.addEventListener('downloadprogress', (evt) => { + detector.dispatchEvent(evt) + }) + } + }).then((aiDetector: AILanguageDetector) => { + bridge.aiDetector = aiDetector + const resolvers = bridge.readyResolvers + bridge.readyResolvers = [] + resolvers.forEach((resolve) => { resolve(undefined) }) + }) + + return detector + } + + /* **************************************************************************/ + // MARK: Translation + /* **************************************************************************/ + + get downloadProgress () { return this.#downloadProgressFn } + set downloadProgress (v) { this.#downloadProgressFn = v } + + canTranslate = async (opts: TranslatorOptions) => { + const capabilities = await this.#ai.translator.capabilities() + return capabilities.languagePairAvailable(opts.sourceLanguage, opts.targetLanguage) + } + + createTranslator = async (opts: TranslatorOptions) => { + return new Translator(await this.#ai.translator.create(opts)) + } +} + +export default Translation diff --git a/src/extension/contentscript-main/index.ts b/src/extension/contentscript-main/index.ts index df86e61..64b8a0f 100644 --- a/src/extension/contentscript-main/index.ts +++ b/src/extension/contentscript-main/index.ts @@ -1,4 +1,5 @@ import AI from './AI' +import Translation from './Translation' const ai = new AI(window.ai) @@ -7,6 +8,9 @@ if (process.env.BROWSER !== 'extlib') { if (!window.ai) { genericWindow.ai = ai } + if (!genericWindow.translation) { + genericWindow.translation = new Translation(ai) + } genericWindow.aibrow = ai }