From 0d034af51928a533243db579bb5179a32ec0a108 Mon Sep 17 00:00:00 2001 From: LeapwardKoex Date: Tue, 14 Jan 2025 19:49:13 +1300 Subject: [PATCH 1/3] Add basic furigana generation --- package-lock.json | 69 ++++++++++++++++++-- package.json | 5 +- src/background/FuriganaHandler.ts | 94 ++++++++++++++++++++++++++++ src/background/index.ts | 16 +++++ src/content/FloatingWindowHandler.ts | 26 ++++---- src/content/index.ts | 4 +- src/interfaces/message.ts | 2 + src/offscreen/index.ts | 9 ++- webpack.config.js | 4 ++ 9 files changed, 210 insertions(+), 19 deletions(-) create mode 100644 src/background/FuriganaHandler.ts diff --git a/package-lock.json b/package-lock.json index 009581e..e675c5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,25 +1,29 @@ { - "name": "namida", - "version": "1.0.0", + "name": "namida ocr", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "namida", - "version": "1.0.0", + "name": "namida ocr", + "version": "1.1.1", "license": "ISC", "dependencies": { "@tensorflow/tfjs": "^4.11.0", "@upscalerjs/default-model": "^1.0.0-beta.17", "@upscalerjs/esrgan-medium": "^1.0.0-beta.13", "@upscalerjs/esrgan-thick": "^1.0.0-beta.16", + "install": "^0.13.0", + "kuromoji": "^0.1.2", "tesseract.js": "^5.1.1", "upscaler": "^1.0.0-beta.19" }, "devDependencies": { "@types/chrome": "^0.0.287", + "@types/kuromoji": "^0.1.3", "@types/webextension-polyfill": "^0.12.1", "copy-webpack-plugin": "^12.0.2", + "path-browserify": "^1.0.1", "ts-loader": "^9.5.1", "typescript": "^5.7.2", "webextension-polyfill": "^0.12.0", @@ -291,6 +295,12 @@ "@types/har-format": "*" } }, + "node_modules/@types/doublearray": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/doublearray/-/doublearray-0.0.32.tgz", + "integrity": "sha512-HloTru3I3a55runIVqZX1YBQi2L5A4peNQPh33yshzB4ttt1qHCnHPkuhy9Djy/cTx7i5xJvxItKRPCmvnfpGw==", + "dev": true + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -344,6 +354,15 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/kuromoji": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@types/kuromoji/-/kuromoji-0.1.3.tgz", + "integrity": "sha512-u+YwX6eJj6Fmm0F5qunsyA+X8HSiyRNNE5ON3itD3tERax4meq9tv+S7bjTMXkPjqbdBGUmH2maGDCuEvpODwg==", + "dev": true, + "dependencies": { + "@types/doublearray": "*" + } + }, "node_modules/@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -731,6 +750,14 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1070,6 +1097,11 @@ "node": ">=0.4.0" } }, + "node_modules/doublearray": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/doublearray/-/doublearray-0.0.2.tgz", + "integrity": "sha512-aw55FtZzT6AmiamEj2kvmR6BuFqvYgKZUkfQ7teqVRNqD5UE0rw8IeW/3gieHNKQ5sPuDKlljWEn4bzv5+1bHw==" + }, "node_modules/electron-to-chromium": { "version": "1.5.76", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.76.tgz", @@ -1405,6 +1437,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/install": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz", + "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", @@ -1567,6 +1607,16 @@ "node": ">=0.10.0" } }, + "node_modules/kuromoji": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/kuromoji/-/kuromoji-0.1.2.tgz", + "integrity": "sha512-V0dUf+C2LpcPEXhoHLMAop/bOht16Dyr+mDiIE39yX3vqau7p80De/koFqpiTcL1zzdZlc3xuHZ8u5gjYRfFaQ==", + "dependencies": { + "async": "^2.0.1", + "doublearray": "0.0.2", + "zlibjs": "^0.3.1" + } + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -1588,6 +1638,11 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", @@ -1721,6 +1776,12 @@ "node": ">=6" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", diff --git a/package.json b/package.json index 6bd206f..e0f133a 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "namida ocr", + "name": "namida_ocr", "version": "1.1.1", "description": "", "main": "index.js", @@ -14,8 +14,10 @@ "license": "ISC", "devDependencies": { "@types/chrome": "^0.0.287", + "@types/kuromoji": "^0.1.3", "@types/webextension-polyfill": "^0.12.1", "copy-webpack-plugin": "^12.0.2", + "path-browserify": "^1.0.1", "ts-loader": "^9.5.1", "typescript": "^5.7.2", "webextension-polyfill": "^0.12.0", @@ -27,6 +29,7 @@ "@upscalerjs/default-model": "^1.0.0-beta.17", "@upscalerjs/esrgan-medium": "^1.0.0-beta.13", "@upscalerjs/esrgan-thick": "^1.0.0-beta.16", + "kuromoji": "^0.1.2", "tesseract.js": "^5.1.1", "upscaler": "^1.0.0-beta.19" } diff --git a/src/background/FuriganaHandler.ts b/src/background/FuriganaHandler.ts new file mode 100644 index 0000000..70c0533 --- /dev/null +++ b/src/background/FuriganaHandler.ts @@ -0,0 +1,94 @@ + + +import * as kuromoji from 'kuromoji'; +import { NamidaMessageAction } from '../interfaces/message'; +import { runtime } from 'webextension-polyfill'; + +export class FuriganaHandler { + private static logTag = `[${FuriganaHandler.name}]`; + private static tokenizerBuilderPromise: Promise> | undefined; + + constructor() { + } + + private static async initializeTokenizer() { + if (!this.tokenizerBuilderPromise) { + this.tokenizerBuilderPromise = new Promise>((resolve, reject) => { + try { + const builder = kuromoji.builder({ dicPath: "../libs/kuromoji" }); + builder.build((err, tokenizer) => { + if (err) { + this.tokenizerBuilderPromise = undefined; + console.error(this.logTag, "Failed to create furigana builder", err); + reject(err); + return; + } + resolve(tokenizer); + }); + } + catch (err) { + console.error(this.logTag, "Failed to create furigana builder", err); + this.tokenizerBuilderPromise = undefined; + } + }) + } + return this.tokenizerBuilderPromise; + } + + public static async generateFurigana(data: string) { + console.debug(this.logTag, "Getting furigana tokenizer"); + var tokenizer = await this.initializeTokenizer(); + console.debug(this.logTag, "Created furigana tokenizer"); + var tokenized = tokenizer.tokenize(data); + console.debug(this.logTag, "Tonikenized input", tokenized); + return tokenized; + } + + public static async generateFuriganaFromContent(input: String) { + const output = await runtime.sendMessage({ + action: NamidaMessageAction.GenerateFurigana, data: input + }) as kuromoji.IpadicFeatures[]; + console.log(this.logTag, `Generated furigana: ${output} from ${input}`) + + return FuriganaHandler.convertToHtml(output); + } + + public static toKatakana(hiragana: string) { + return hiragana.replace(/[\u3041-\u3096]/g, ch => + String.fromCharCode(ch.charCodeAt(0) + 0x60) + ); + } + + public static isAllKana(str: string) { + return /^[\u3040-\u309F\u30A0-\u30FF]+$/.test(str); + } + + public static convertToHtml(data: kuromoji.IpadicFeatures[]) { + let html = ""; + + for (const token of data) { + if (token.surface_form === "\\n") { + html += "
"; + continue; + } + + if (!token.reading || token.reading === "*") { + html += token.surface_form; + continue; + } + + if (FuriganaHandler.isAllKana(token.surface_form)) { + const katakanaForm = FuriganaHandler.toKatakana(token.surface_form); + + if (katakanaForm === token.reading) { + html += token.surface_form; // no furigana + continue; + } + } + + html += `${token.surface_form}${token.reading}`; + } + + return html; + } +} diff --git a/src/background/index.ts b/src/background/index.ts index 5dc831e..fae6d3c 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -3,6 +3,7 @@ import { NamidaMessage, NamidaMessageAction, NamidaOcrFromOffscreenData, NamidaT import { TesseractOcrHandler } from "./TesseractOcrHandler"; import { Upscaler } from "./Upscaler"; import { Settings } from "../interfaces/Storage"; +import { FuriganaHandler } from "./FuriganaHandler"; console.log('Background script loaded'); @@ -51,6 +52,21 @@ runtime.onMessage.addListener((message, sender) => { return Upscaler.upscaleImageWithAIFromBackground(namidaMessage.data as NamidaTensorflowUpscaleData); } + case NamidaMessageAction.GenerateFurigana: { + if (globalThis.XMLHttpRequest) { + return FuriganaHandler.generateFurigana(namidaMessage.data); + } + else { + return ensureOffscreenDocument().then(() => { + return runtime.sendMessage( + { + action: NamidaMessageAction.GenerateFuriganaOffscreen, + data: namidaMessage.data + }); + }); + } + } + case NamidaMessageAction.RecognizeImage: { return Settings.getPageSegMode().then((pageSegMode) => { if (globalThis.Worker) { diff --git a/src/content/FloatingWindowHandler.ts b/src/content/FloatingWindowHandler.ts index ec70b3b..91e16ab 100644 --- a/src/content/FloatingWindowHandler.ts +++ b/src/content/FloatingWindowHandler.ts @@ -7,7 +7,7 @@ export class FloatingWindow { private speechHandler = new SpeechSynthesisHandler("ja-JP"); static floatingMessageTimer: number | undefined; - constructor(text: string | undefined) { + constructor(config: { text: string | undefined, html: string | undefined }) { // Remove existing message if it's still visible if (FloatingWindow.floatingMessageEl) { FloatingWindow.floatingMessageEl.remove(); @@ -42,7 +42,7 @@ export class FloatingWindow { // Title const titleEl = document.createElement('span'); titleEl.style.fontWeight = 'bold'; - if (text) { + if (config.text || config.html) { titleEl.innerText = "Recognized text:"; } else { titleEl.innerText = "Failed to recognize text, please try again."; @@ -74,14 +74,18 @@ export class FloatingWindow { const textContainer = document.createElement('div'); textContainer.style.background = '#333'; textContainer.style.borderRadius = '6px'; - textContainer.style.padding = '10px'; + textContainer.style.padding = config.html ? '20px' : '10px'; textContainer.style.marginTop = '8px'; - textContainer.style.fontSize = '20px'; + textContainer.style.fontSize = '28px'; textContainer.style.lineHeight = '1.4'; - if (text) { - textContainer.innerText = text; - } else { + if (config.html) { + textContainer.innerHTML = config.html; + } + else if (config.text) { + textContainer.innerText = config.text; + } + else { textContainer.innerText = ""; } @@ -104,13 +108,13 @@ export class FloatingWindow { speakButton.addEventListener('click', () => { // Only speak if text is defined - if (text) { + if (config.text) { if (TTSWrapper.isSpeaking()) { TTSWrapper.cancel(); speakButton.innerText = 'Speak'; } else { - this.speechHandler.speak(text).finally(() => { + this.speechHandler.speak(config.text).finally(() => { speakButton.innerText = 'Speak'; }); speakButton.textContent = 'Speaking...' @@ -124,14 +128,14 @@ export class FloatingWindow { // Add all elements to the main container floatingDiv.appendChild(headerRow); // Only show the text container if we have recognized text or want to show something - if (text) { + if (config.text || config.html) { floatingDiv.appendChild(textContainer); } // We'll conditionally add the button row only if the speak button is shown Settings.getShowSpeakButton().then(async (showSpeakButton) => { const voice = await this.speechHandler.voiceForLanguage(); - const canSpeak = Boolean(text) && Boolean(voice); + const canSpeak = Boolean(config.text) && Boolean(voice); if (showSpeakButton && canSpeak) { floatingDiv.appendChild(buttonRow); } diff --git a/src/content/index.ts b/src/content/index.ts index 8ecd8f4..6c47281 100644 --- a/src/content/index.ts +++ b/src/content/index.ts @@ -8,6 +8,7 @@ import { Settings } from "../interfaces/Storage"; import { ClipboardHandler } from "./ClipboardHandler"; import { FloatingWindow } from "./FloatingWindowHandler"; import { TextProcessorHandler } from "./TextProcessorHandler"; +import { FuriganaHandler } from "../background/FuriganaHandler"; console.debug('Content script loaded'); @@ -43,8 +44,9 @@ class SnippingTool { console.debug(SnippingTool.logTag, "Got data: " + croppedDataURL); const recognizedText = await this.ocr.recognizeFromContent(croppedDataURL); const spacesRemovedText = TextProcessorHandler.removeSpaces(recognizedText); + const furigana = await FuriganaHandler.generateFuriganaFromContent(spacesRemovedText ?? ""); ClipboardHandler.copyText(spacesRemovedText); - new FloatingWindow(spacesRemovedText); + new FloatingWindow({ text: spacesRemovedText, html: furigana }); if (await Settings.getSaveOcrCrop()) { console.debug(SnippingTool.logTag, "Saving Image"); this.saveHandler.downloadImage(croppedDataURL, 'snippet.png'); diff --git a/src/interfaces/message.ts b/src/interfaces/message.ts index 5e3d9c4..c2d0aec 100644 --- a/src/interfaces/message.ts +++ b/src/interfaces/message.ts @@ -6,6 +6,8 @@ export enum NamidaMessageAction { UpscaleImage, RecognizeImage, RecognizeImageOffscreen, + GenerateFurigana, + GenerateFuriganaOffscreen } export interface NamidaMessage { diff --git a/src/offscreen/index.ts b/src/offscreen/index.ts index 476e1a7..f17946b 100644 --- a/src/offscreen/index.ts +++ b/src/offscreen/index.ts @@ -1,6 +1,7 @@ import { runtime } from "webextension-polyfill"; import { NamidaMessage, NamidaMessageAction, NamidaOcrFromOffscreenMessage } from "../interfaces/message"; import { TesseractOcrHandler } from "../background/TesseractOcrHandler"; +import { FuriganaHandler } from "../background/FuriganaHandler"; console.debug("Loading offscreen document"); @@ -13,8 +14,12 @@ console.debug("Loading offscreen document"); })().catch(console.error); runtime.onMessage.addListener((message) => { - const namidaMessage = message as NamidaOcrFromOffscreenMessage; + const namidaMessage = message as NamidaMessage; if (namidaMessage.action === NamidaMessageAction.RecognizeImageOffscreen) { - return TesseractOcrHandler.recognizeFromOffscreen(namidaMessage.data.imageData, namidaMessage.data.pageSegMode); + const namidaOcrMessage = message as NamidaOcrFromOffscreenMessage; + return TesseractOcrHandler.recognizeFromOffscreen(namidaOcrMessage.data.imageData, namidaOcrMessage.data.pageSegMode); + } + if (namidaMessage.action === NamidaMessageAction.GenerateFuriganaOffscreen) { + return FuriganaHandler.generateFurigana(namidaMessage.data); } }); diff --git a/webpack.config.js b/webpack.config.js index 07a789c..9aafe88 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -37,6 +37,9 @@ module.exports = (env) => { }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'], + fallback: { + "path": require.resolve("path-browserify") + } }, module: { rules: [ @@ -61,6 +64,7 @@ module.exports = (env) => { { from: 'node_modules/tesseract.js/dist/worker.min.js', to: 'libs/tesseract-worker/worker.min.js' }, { from: 'lang/jpn_vert.traineddata', to: 'libs/tesseract-lang/jpn_vert.traineddata' }, { from: 'node_modules/@upscalerjs/esrgan-medium/models/x2', to: 'libs/tensorflow/x2' }, + { from: 'node_modules/kuromoji/dict', to: 'libs/kuromoji' }, { from: 'assets/', to: 'assets/', globOptions: { ignore: [ From 558bb102c8685b3a5c4f8449513e939e9c0820f4 Mon Sep 17 00:00:00 2001 From: LeapwardKoex Date: Tue, 14 Jan 2025 22:35:50 +1300 Subject: [PATCH 2/3] Add setting for selecting the type of furigana --- src/background/FuriganaHandler.ts | 28 ++++++++++++++++++++++----- src/interfaces/Storage.ts | 28 ++++++++++++++++++++++++++- src/ui/index.ts | 32 +++++++++++++++++++++++++++++-- src/ui/popup.html | 11 +++++++++++ src/ui/styles.css | 23 +++++++++++++++++++++- 5 files changed, 113 insertions(+), 9 deletions(-) diff --git a/src/background/FuriganaHandler.ts b/src/background/FuriganaHandler.ts index 70c0533..0374d66 100644 --- a/src/background/FuriganaHandler.ts +++ b/src/background/FuriganaHandler.ts @@ -3,6 +3,13 @@ import * as kuromoji from 'kuromoji'; import { NamidaMessageAction } from '../interfaces/message'; import { runtime } from 'webextension-polyfill'; +import { Settings } from '../interfaces/Storage'; + +export enum FuriganaType { + None, + Hiragana, + Katakana, +} export class FuriganaHandler { private static logTag = `[${FuriganaHandler.name}]`; @@ -49,8 +56,8 @@ export class FuriganaHandler { action: NamidaMessageAction.GenerateFurigana, data: input }) as kuromoji.IpadicFeatures[]; console.log(this.logTag, `Generated furigana: ${output} from ${input}`) - - return FuriganaHandler.convertToHtml(output); + const furiganaType = await Settings.getFuriganaType(); + return FuriganaHandler.convertToHtml(output, furiganaType); } public static toKatakana(hiragana: string) { @@ -59,11 +66,17 @@ export class FuriganaHandler { ); } + public static toHiragana(katakana: string): string { + return katakana.replace(/[\u30A1-\u30F6]/g, ch => + String.fromCharCode(ch.charCodeAt(0) - 0x60) + ); + } + public static isAllKana(str: string) { return /^[\u3040-\u309F\u30A0-\u30FF]+$/.test(str); } - public static convertToHtml(data: kuromoji.IpadicFeatures[]) { + public static convertToHtml(data: kuromoji.IpadicFeatures[], furiganaType: FuriganaType) { let html = ""; for (const token of data) { @@ -72,7 +85,7 @@ export class FuriganaHandler { continue; } - if (!token.reading || token.reading === "*") { + if (!token.reading || token.reading === "*" || furiganaType == FuriganaType.None) { html += token.surface_form; continue; } @@ -86,7 +99,12 @@ export class FuriganaHandler { } } - html += `${token.surface_form}${token.reading}`; + let tokenReading = token.reading; + if (furiganaType == FuriganaType.Hiragana) { + tokenReading = this.toHiragana(tokenReading); + } + + html += `${token.surface_form}${tokenReading}`; } return html; diff --git a/src/interfaces/Storage.ts b/src/interfaces/Storage.ts index fc5ebac..bea4f81 100644 --- a/src/interfaces/Storage.ts +++ b/src/interfaces/Storage.ts @@ -1,6 +1,7 @@ import { PSM } from "tesseract.js"; import { UpscaleMethod } from "../content/ScreenshotHandler"; import { storage } from "webextension-polyfill"; +import { FuriganaType } from "../background/FuriganaHandler"; export enum StorageKey { UpscalingMode = "UpscalingMode", @@ -8,7 +9,14 @@ export enum StorageKey { SaveOcrCrop = "SaveOcrCrop", ShowSpeakButton = "ShowSpeakButton", PreferredVoices = "PreferredVoices", - WindowTimeout = "WindowTimeout" + WindowTimeout = "WindowTimeout", + FuriganaType = "FuriganaType" +} + +export enum FuriganaTypeString { + None = 'none', + Hiragana = 'hiragana', + Katakana = 'katakana' } export enum UpscalingModeString { @@ -24,6 +32,19 @@ export enum PageSegModeString { } export class Settings { + private static getFuriganaTypeString(settingString: string | undefined) { + if (settingString === FuriganaTypeString.None) { + return FuriganaType.None; + } + else if (settingString === FuriganaTypeString.Hiragana) { + return FuriganaType.Hiragana; + } + if (settingString === FuriganaTypeString.Katakana) { + return FuriganaType.Katakana; + } + return FuriganaType.Hiragana; + } + private static getUpscalingModeFromString(settingString: string | undefined) { if (settingString === UpscalingModeString.None) { return UpscaleMethod.None; @@ -56,6 +77,11 @@ export class Settings { return Number(value ?? "30000"); } + public static async getFuriganaType() { + const values = await storage.sync.get(StorageKey.FuriganaType); + return this.getFuriganaTypeString((values[StorageKey.FuriganaType] as string | undefined)); + } + public static async getUpscalingMode() { const values = await storage.sync.get(StorageKey.UpscalingMode); return this.getUpscalingModeFromString((values[StorageKey.UpscalingMode] as string | undefined)); diff --git a/src/ui/index.ts b/src/ui/index.ts index b4d438c..17af902 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -1,11 +1,13 @@ import { commands, runtime, storage, tabs } from "webextension-polyfill"; -import { Settings, StorageKey, UpscalingModeString } from "../interfaces/Storage"; +import { FuriganaTypeString, Settings, StorageKey, UpscalingModeString } from "../interfaces/Storage"; import { SpeechSynthesisHandler } from "../content/SpeechHandler"; import { NamidaVoice, TTSWrapper } from "../content/TTSWrapper"; import { BrowserType, getCurrentBrowser, isWindows } from "../interfaces/browserInfo"; +import { FuriganaType } from "../background/FuriganaHandler"; document.addEventListener('DOMContentLoaded', () => { const windowTimeoutSelect = document.getElementById("window-timeout") as HTMLSelectElement; + const furiganaTypeSelect = document.getElementById("furigana-type") as HTMLSelectElement; const upscalingSelect = document.getElementById("upscaling-mode") as HTMLSelectElement; const pageSegSelect = document.getElementById("page-seg-mode") as HTMLSelectElement; const voiceSelect = document.getElementById("voice-selection") as HTMLSelectElement; @@ -15,9 +17,16 @@ document.addEventListener('DOMContentLoaded', () => { const speakeDemoButton = document.getElementById("voice-demo-button") as HTMLButtonElement; const changeShortcut = document.getElementById("change-shortcut") as HTMLButtonElement; - loadSettings(windowTimeoutSelect, upscalingSelect, pageSegSelect, saveOcrCropCheckbox, showSpeakButtonCheckbox); + loadSettings(windowTimeoutSelect, furiganaTypeSelect, upscalingSelect, pageSegSelect, saveOcrCropCheckbox, showSpeakButtonCheckbox); // Attach listeners to save new values + furiganaTypeSelect.addEventListener("change", () => { + const record: Record = {}; + record[StorageKey.FuriganaType] = furiganaTypeSelect.value; + updateFuriganaExample(furiganaTypeSelect.value as FuriganaTypeString) + storage.sync.set(record); + }); + windowTimeoutSelect.addEventListener("change", () => { const record: Record = {}; record[StorageKey.WindowTimeout] = windowTimeoutSelect.value; @@ -107,6 +116,7 @@ document.addEventListener('DOMContentLoaded', () => { async function loadSettings( windowTimeoutSelect: HTMLSelectElement, + furiganaTypeSelect: HTMLSelectElement, upscalingSelect: HTMLSelectElement, pageSegSelect: HTMLSelectElement, saveOcrCropCheckbox: HTMLInputElement, @@ -114,6 +124,9 @@ async function loadSettings( ) { const values = await storage.sync.get(null); + furiganaTypeSelect.value = + (values[StorageKey.FuriganaType] as string | undefined) || FuriganaTypeString.Hiragana; + updateFuriganaExample(furiganaTypeSelect.value as FuriganaTypeString) windowTimeoutSelect.value = (values[StorageKey.WindowTimeout] as string | undefined) || "30000"; upscalingSelect.value = @@ -184,4 +197,19 @@ function populateVoiceSelection( // If no preferred voice is set, select the first available voice voiceSelect.selectedIndex = 0; } +} + +function updateFuriganaExample(furiganaType: FuriganaTypeString) { + const furiganaExample = document.getElementById("furigana-example") as HTMLSpanElement; + switch (furiganaType) { + case FuriganaTypeString.None: + furiganaExample.innerHTML = "日本語"; + break; + case FuriganaTypeString.Hiragana: + furiganaExample.innerHTML = "ほん"; + break; + case FuriganaTypeString.Katakana: + furiganaExample.innerHTML = "ホン"; + break; + } } \ No newline at end of file diff --git a/src/ui/popup.html b/src/ui/popup.html index 633191d..6b4a031 100644 --- a/src/ui/popup.html +++ b/src/ui/popup.html @@ -32,6 +32,17 @@

General

+ + +
+

Preview

+ +
+
diff --git a/src/ui/styles.css b/src/ui/styles.css index 22084a2..cc118bc 100644 --- a/src/ui/styles.css +++ b/src/ui/styles.css @@ -147,6 +147,27 @@ button { border: none; border-radius: 4px; padding: 6px 12px; - font-size: 20px; + font-size: 16px; cursor: pointer; + transition: background-color 0.3s, transform 0.1s, box-shadow 0.3s; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +button:hover { + background-color: #1565c0; + /* Darker shade on hover */ + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); +} + +button:active { + background-color: #0d47a1; + /* Even darker on click */ + transform: scale(0.98); + /* Slightly shrink to indicate click */ + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} + +#furigana-example { + font-size: 30px; + margin: 0 auto; } \ No newline at end of file From b19f3324d0933fe2b6ebe46e865b2ffc89ce0c11 Mon Sep 17 00:00:00 2001 From: LeapwardKoex Date: Wed, 15 Jan 2025 20:48:35 +1300 Subject: [PATCH 3/3] Update to use spoofed extension for furigana generation --- .github/workflows/webpack.yml | 4 +-- README.md | 8 +++++ package-lock.json | 56 ++++++++++--------------------- package.json | 5 ++- src/background/FuriganaHandler.ts | 19 +++++++++-- webpack.config.js | 2 +- 6 files changed, 48 insertions(+), 46 deletions(-) diff --git a/.github/workflows/webpack.yml b/.github/workflows/webpack.yml index 3c39df8..e4758f7 100644 --- a/.github/workflows/webpack.yml +++ b/.github/workflows/webpack.yml @@ -35,7 +35,7 @@ jobs: - name: Build Firefox Plugin run: | - npm run build:firefox -- --env build_number=1.1.${{ github.run_number }} + npm run build:firefox -- --env build_number=1.2.${{ github.run_number }} shell: bash - name: Package Firefox @@ -52,7 +52,7 @@ jobs: - name: Build Chrome Plugin run: | - npm run build:chrome -- --env build_number=1.1.${{ github.run_number }} + npm run build:chrome -- --env build_number=1.2.${{ github.run_number }} shell: bash - name: Package Chrome diff --git a/README.md b/README.md index 26238ab..02d2f67 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,9 @@ - **Clipboard Copy** Upon successful OCR, the recognized text is automatically copied to your clipboard so you can quickly paste it into a dictionary or translation tool. +- **Furigana** + Choose from either Hiragana or Katakana phonetic prnounciation of Kanji + - **Text-to-Speech (TTS)** Namida OCR includes the option to speak the recognized text aloud using your browser’s TTS engine. - **Chrome**: High-quality remote Japanese voices are included by default. @@ -57,6 +60,11 @@ ## Settings +- **Furigana display** + - **None** – Display no additional kana above kanji + - **Hiragana** – Displays hiragana above kanji + - **Katakana** – Displays katakana above kanji + - **Upscaling Mode** - **Linear** – Uses basic canvas scaling (faster but lower quality). - **ESRGAN** – AI-based upscaling for sharper text. diff --git a/package-lock.json b/package-lock.json index e675c5c..696498d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,26 +1,24 @@ { - "name": "namida ocr", + "name": "namida_ocr", "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "namida ocr", + "name": "namida_ocr", "version": "1.1.1", "license": "ISC", "dependencies": { + "@leapward-koex/kuromoji": "^1.2.0", "@tensorflow/tfjs": "^4.11.0", "@upscalerjs/default-model": "^1.0.0-beta.17", "@upscalerjs/esrgan-medium": "^1.0.0-beta.13", "@upscalerjs/esrgan-thick": "^1.0.0-beta.16", - "install": "^0.13.0", - "kuromoji": "^0.1.2", "tesseract.js": "^5.1.1", "upscaler": "^1.0.0-beta.19" }, "devDependencies": { "@types/chrome": "^0.0.287", - "@types/kuromoji": "^0.1.3", "@types/webextension-polyfill": "^0.12.1", "copy-webpack-plugin": "^12.0.2", "path-browserify": "^1.0.1", @@ -98,6 +96,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@leapward-koex/kuromoji": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@leapward-koex/kuromoji/-/kuromoji-1.2.0.tgz", + "integrity": "sha512-j/Gcb3SVhV52wpGL0qY3XI0xcfKp2bKS0XSL0bx+Xvym2UjGdc1nwHEbhTt2LbbHDFrmB7d2n+6P3ibliEFGBQ==", + "dependencies": { + "async": "^2.0.1", + "doublearray": "0.0.2", + "fflate": "^0.8.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -295,12 +303,6 @@ "@types/har-format": "*" } }, - "node_modules/@types/doublearray": { - "version": "0.0.32", - "resolved": "https://registry.npmjs.org/@types/doublearray/-/doublearray-0.0.32.tgz", - "integrity": "sha512-HloTru3I3a55runIVqZX1YBQi2L5A4peNQPh33yshzB4ttt1qHCnHPkuhy9Djy/cTx7i5xJvxItKRPCmvnfpGw==", - "dev": true - }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -354,15 +356,6 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, - "node_modules/@types/kuromoji": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@types/kuromoji/-/kuromoji-0.1.3.tgz", - "integrity": "sha512-u+YwX6eJj6Fmm0F5qunsyA+X8HSiyRNNE5ON3itD3tERax4meq9tv+S7bjTMXkPjqbdBGUmH2maGDCuEvpODwg==", - "dev": true, - "dependencies": { - "@types/doublearray": "*" - } - }, "node_modules/@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -1265,6 +1258,11 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1437,14 +1435,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/install": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz", - "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", @@ -1607,16 +1597,6 @@ "node": ">=0.10.0" } }, - "node_modules/kuromoji": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/kuromoji/-/kuromoji-0.1.2.tgz", - "integrity": "sha512-V0dUf+C2LpcPEXhoHLMAop/bOht16Dyr+mDiIE39yX3vqau7p80De/koFqpiTcL1zzdZlc3xuHZ8u5gjYRfFaQ==", - "dependencies": { - "async": "^2.0.1", - "doublearray": "0.0.2", - "zlibjs": "^0.3.1" - } - }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", diff --git a/package.json b/package.json index e0f133a..291fc07 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "namida_ocr", - "version": "1.1.1", + "version": "1.2.0", "description": "", "main": "index.js", "scripts": { @@ -14,7 +14,6 @@ "license": "ISC", "devDependencies": { "@types/chrome": "^0.0.287", - "@types/kuromoji": "^0.1.3", "@types/webextension-polyfill": "^0.12.1", "copy-webpack-plugin": "^12.0.2", "path-browserify": "^1.0.1", @@ -25,11 +24,11 @@ "webpack-cli": "^6.0.1" }, "dependencies": { + "@leapward-koex/kuromoji": "^1.2.0", "@tensorflow/tfjs": "^4.11.0", "@upscalerjs/default-model": "^1.0.0-beta.17", "@upscalerjs/esrgan-medium": "^1.0.0-beta.13", "@upscalerjs/esrgan-thick": "^1.0.0-beta.16", - "kuromoji": "^0.1.2", "tesseract.js": "^5.1.1", "upscaler": "^1.0.0-beta.19" } diff --git a/src/background/FuriganaHandler.ts b/src/background/FuriganaHandler.ts index 0374d66..f101507 100644 --- a/src/background/FuriganaHandler.ts +++ b/src/background/FuriganaHandler.ts @@ -1,6 +1,6 @@ -import * as kuromoji from 'kuromoji'; +import * as kuromoji from '@leapward-koex/kuromoji'; import { NamidaMessageAction } from '../interfaces/message'; import { runtime } from 'webextension-polyfill'; import { Settings } from '../interfaces/Storage'; @@ -22,7 +22,22 @@ export class FuriganaHandler { if (!this.tokenizerBuilderPromise) { this.tokenizerBuilderPromise = new Promise>((resolve, reject) => { try { - const builder = kuromoji.builder({ dicPath: "../libs/kuromoji" }); + const builder = kuromoji.builder({ + dicPath: "../libs/kuromoji", fileNameOptions: { + base: "base.dat.zg", + check: "check.dat.zg", + tid: "tid.dat.zg", + tidPos: "tid_pos.dat.zg", + tidMap: "tid_map.dat.zg", + cc: "cc.dat.zg", + unk: "unk.dat.zg", + unkPos: "unk_pos.dat.zg", + unkMap: "unk_map.dat.zg", + unkChar: "unk_char.dat.zg", + unkCompat: "unk_compat.dat.zg", + unkInvoke: "unk_invoke.dat.zg" + } + }); builder.build((err, tokenizer) => { if (err) { this.tokenizerBuilderPromise = undefined; diff --git a/webpack.config.js b/webpack.config.js index 9aafe88..126f061 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -64,7 +64,7 @@ module.exports = (env) => { { from: 'node_modules/tesseract.js/dist/worker.min.js', to: 'libs/tesseract-worker/worker.min.js' }, { from: 'lang/jpn_vert.traineddata', to: 'libs/tesseract-lang/jpn_vert.traineddata' }, { from: 'node_modules/@upscalerjs/esrgan-medium/models/x2', to: 'libs/tensorflow/x2' }, - { from: 'node_modules/kuromoji/dict', to: 'libs/kuromoji' }, + { from: 'node_modules/@leapward-koex/kuromoji/dict_extension_spoofed', to: 'libs/kuromoji' }, { from: 'assets/', to: 'assets/', globOptions: { ignore: [