Skip to content

Commit

Permalink
Merge pull request #6 from Leapward-Koex/furigana
Browse files Browse the repository at this point in the history
Add furigana
  • Loading branch information
Leapward-Koex authored Jan 15, 2025
2 parents 2e5d17b + b19f332 commit 98575d6
Show file tree
Hide file tree
Showing 15 changed files with 323 additions and 26 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/webpack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
49 changes: 45 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "namida ocr",
"version": "1.1.1",
"name": "namida_ocr",
"version": "1.2.0",
"description": "",
"main": "index.js",
"scripts": {
Expand All @@ -16,13 +16,15 @@
"@types/chrome": "^0.0.287",
"@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",
"webpack": "^5.97.1",
"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",
Expand Down
127 changes: 127 additions & 0 deletions src/background/FuriganaHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@


import * as kuromoji from '@leapward-koex/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}]`;
private static tokenizerBuilderPromise: Promise<kuromoji.Tokenizer<kuromoji.IpadicFeatures>> | undefined;

constructor() {
}

private static async initializeTokenizer() {
if (!this.tokenizerBuilderPromise) {
this.tokenizerBuilderPromise = new Promise<kuromoji.Tokenizer<kuromoji.IpadicFeatures>>((resolve, reject) => {
try {
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;
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}`)
const furiganaType = await Settings.getFuriganaType();
return FuriganaHandler.convertToHtml(output, furiganaType);
}

public static toKatakana(hiragana: string) {
return hiragana.replace(/[\u3041-\u3096]/g, ch =>
String.fromCharCode(ch.charCodeAt(0) + 0x60)
);
}

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[], furiganaType: FuriganaType) {
let html = "";

for (const token of data) {
if (token.surface_form === "\\n") {
html += "<br>";
continue;
}

if (!token.reading || token.reading === "*" || furiganaType == FuriganaType.None) {
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;
}
}

let tokenReading = token.reading;
if (furiganaType == FuriganaType.Hiragana) {
tokenReading = this.toHiragana(tokenReading);
}

html += `<ruby>${token.surface_form}<rt>${tokenReading}</rt></ruby>`;
}

return html;
}
}
16 changes: 16 additions & 0 deletions src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit 98575d6

Please sign in to comment.