Skip to content

Commit 018d575

Browse files
author
Adrian Tompkins
committed
Render generated furigana in OCR text
1 parent c6ff163 commit 018d575

3 files changed

Lines changed: 89 additions & 6 deletions

File tree

src/lib/components/Reader/TextBoxes.svelte

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import { clamp, promptConfirmation } from '$lib/util';
3-
import type { AIBlock, AIPage, Page } from '$lib/types';
3+
import type { AIBlock, AIPage, AIRubySegment, Page } from '$lib/types';
44
import { settings, volumes } from '$lib/settings';
55
import {
66
showCropper,
@@ -59,6 +59,7 @@
5959
fontSize: string;
6060
writingMode: string;
6161
lines: string[];
62+
rubyLines: AIRubySegment[][] | null;
6263
area: number;
6364
useMinDimensions: boolean;
6465
isOriginalMode: boolean;
@@ -74,14 +75,25 @@
7475
.map(({ block, blockIndex }) => {
7576
const { img_height, img_width } = page;
7677
const { box, font_size, lines, vertical } = block;
78+
const ai = aiPage?.blocks.find((item) => item.block_index === blockIndex);
7779
7880
let [_xmin, _ymin, _xmax, _ymax] = box;
7981
8082
// Replace manual ellipsis with proper ellipsis character (…)
8183
// Handle both ASCII periods (...) and full-width periods (...)
82-
const processedLines = lines.map((line) =>
84+
const correctedLines =
85+
ai?.corrected_lines.length === lines.length ? ai.corrected_lines : lines;
86+
const processedLines = correctedLines.map((line) =>
8387
line.replace(/\.\.\./g, '').replace(/.../g, '')
8488
);
89+
const rubyLines =
90+
ai?.ruby_lines?.length === processedLines.length &&
91+
ai.ruby_lines.every(
92+
(rubyLine, lineIndex) =>
93+
rubyLine.map((segment) => segment.base).join('') === correctedLines[lineIndex]
94+
)
95+
? ai.ruby_lines
96+
: null;
8597
8698
const isOriginalMode = $settings.fontSize === 'original';
8799
const isAutoMode = $settings.fontSize === 'auto';
@@ -137,12 +149,13 @@
137149
fontSize,
138150
writingMode: vertical ? 'vertical-rl' : 'horizontal-tb',
139151
lines: processedLines,
152+
rubyLines,
140153
area,
141154
useMinDimensions: $settings.fontSize !== 'auto' && !isOriginalMode,
142155
isOriginalMode,
143156
lineLayouts,
144157
blockIndex,
145-
ai: aiPage?.blocks.find((item) => item.block_index === blockIndex)
158+
ai
146159
};
147160
148161
return textBox;
@@ -684,7 +697,7 @@
684697

685698
<svelte:window onpointerdown={closeAi} />
686699

687-
{#each textBoxes as { fontSize, height, left, lines, top, width, writingMode, useMinDimensions, isOriginalMode, lineLayouts, blockIndex, ai }, index (`${volumeUuid}-textBox-${index}`)}
700+
{#each textBoxes as { fontSize, height, left, lines, rubyLines, top, width, writingMode, useMinDimensions, isOriginalMode, lineLayouts, blockIndex, ai }, index (`${volumeUuid}-textBox-${index}`)}
688701
{@const usePerLine = lineLayouts !== null}
689702
<div
690703
use:handleTextBoxHover={[index, fontSize]}
@@ -729,10 +742,17 @@
729742
style:height={lineLayouts[lineIndex].wrap
730743
? `${lineLayouts[lineIndex].height}px`
731744
: undefined}
732-
style:font-size={`${lineLayouts[lineIndex].fontSize}px`}>{line}</span
745+
style:font-size={`${lineLayouts[lineIndex].fontSize}px`}
746+
>{#if rubyLines?.[lineIndex]}{#each rubyLines[lineIndex] as segment}{#if segment.reading}<ruby
747+
>{segment.base}<rt>{segment.reading}</rt></ruby
748+
>{:else}{segment.base}{/if}{/each}{:else}{line}{/if}</span
733749
>{/if}{/each}
734750
{:else}
735-
{#each lines as line}<span class="ocr-line">{line}</span>{/each}
751+
{#each lines as line, lineIndex}<span class="ocr-line"
752+
>{#if rubyLines?.[lineIndex]}{#each rubyLines[lineIndex] as segment}{#if segment.reading}<ruby
753+
>{segment.base}<rt>{segment.reading}</rt></ruby
754+
>{:else}{segment.base}{/if}{/each}{:else}{line}{/if}</span
755+
>{/each}
736756
{/if}
737757
</p>
738758
{#if ai}
@@ -877,6 +897,17 @@
877897
/* transform (translate onto the quad) is set by positionPerLine */
878898
}
879899
900+
.textBox ruby {
901+
ruby-position: over;
902+
ruby-align: center;
903+
}
904+
905+
.textBox rt {
906+
font-size: 0.45em;
907+
font-weight: 400;
908+
letter-spacing: 0;
909+
}
910+
880911
/* A quad that captured multiple print columns (base text + furigana):
881912
the text flows inside the full quad bbox at the block's reference size,
882913
wrapping into columns/rows instead of shrinking onto one line. */

src/lib/components/Reader/__tests__/TextBoxes.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,52 @@ describe('TextBoxes auto mode with lines_coords', () => {
155155
}
156156
});
157157

158+
it('renders validated AI ruby segments over corrected OCR text', () => {
159+
const { container } = render(TextBoxes, {
160+
page: makePage([blockWithCoords]),
161+
volumeUuid: 'test-uuid',
162+
aiPage: {
163+
page_index: 0,
164+
img_path: 'page_001.jpg',
165+
blocks: [
166+
{
167+
block_index: 0,
168+
block_key: 'key',
169+
source_lines: blockWithCoords.lines,
170+
corrected_lines: blockWithCoords.lines,
171+
ruby_lines: [
172+
[
173+
{ base: '総則', reading: 'そうそく' },
174+
{ base: '追加で言うのは', reading: '' }
175+
],
176+
[
177+
{ base: '結界', reading: 'けっかい' },
178+
{ base: 'の出入りを', reading: '' }
179+
],
180+
[
181+
{ base: '可能', reading: 'かのう' },
182+
{ base: 'にしても', reading: '' }
183+
]
184+
],
185+
translation: 'Ruby translation.',
186+
words: []
187+
}
188+
]
189+
}
190+
});
191+
192+
expect(Array.from(container.querySelectorAll('ruby')).map((ruby) => ruby.textContent)).toEqual([
193+
'総則そうそく',
194+
'結界けっかい',
195+
'可能かのう'
196+
]);
197+
expect(Array.from(container.querySelectorAll('rt')).map((rt) => rt.textContent)).toEqual([
198+
'そうそく',
199+
'けっかい',
200+
'かのう'
201+
]);
202+
});
203+
158204
it('opens translation and contextual word details from an AI sidecar page', async () => {
159205
const { container, getByText, queryByText } = render(TextBoxes, {
160206
page: makePage([blockWithCoords]),

src/lib/types/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,17 @@ export interface AIWord {
2020
grammar: string;
2121
}
2222

23+
export interface AIRubySegment {
24+
base: string;
25+
reading: string;
26+
}
27+
2328
export interface AIBlock {
2429
block_index: number;
2530
block_key: string;
2631
source_lines: string[];
2732
corrected_lines: string[];
33+
ruby_lines?: AIRubySegment[][];
2834
translation: string;
2935
words: AIWord[];
3036
}

0 commit comments

Comments
 (0)