@internetarchive/bookreader 5.0.0-96 → 5.0.0-98
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BookReader/474.js +2 -0
- package/BookReader/474.js.map +1 -0
- package/BookReader/BookReader.css +39 -34
- package/BookReader/BookReader.js +1 -1
- package/BookReader/BookReader.js.map +1 -1
- package/BookReader/bergamot-translator-worker.js +2966 -0
- package/BookReader/bergamot-translator-worker.wasm +0 -0
- package/BookReader/ia-bookreader-bundle.js +1 -1
- package/BookReader/ia-bookreader-bundle.js.map +1 -1
- package/BookReader/images/icon_experiment.svg +1 -0
- package/BookReader/images/translate.svg +1 -0
- package/BookReader/plugins/plugin.experiments.js +1 -1
- package/BookReader/plugins/plugin.experiments.js.map +1 -1
- package/BookReader/plugins/plugin.text_selection.js +1 -1
- package/BookReader/plugins/plugin.text_selection.js.map +1 -1
- package/BookReader/plugins/plugin.translate.js +3 -0
- package/BookReader/plugins/plugin.translate.js.LICENSE.txt +1 -0
- package/BookReader/plugins/plugin.translate.js.map +1 -0
- package/BookReader/plugins/plugin.tts.js +1 -1
- package/BookReader/plugins/plugin.tts.js.map +1 -1
- package/BookReader/plugins/translator-worker.js +2 -0
- package/BookReader/plugins/translator-worker.js.map +1 -0
- package/BookReader/silence.mp3 +0 -0
- package/BookReader/translator-worker.js +475 -0
- package/package.json +6 -3
- package/src/BookNavigator/book-navigator.js +1 -0
- package/src/BookReader/Mode1UpLit.js +6 -1
- package/src/BookReader/Mode2UpLit.js +11 -1
- package/src/BookReader/Navbar/Navbar.js +61 -0
- package/src/BookReader/options.js +12 -8
- package/src/BookReader.js +67 -140
- package/src/assets/images/icon_experiment.svg +1 -0
- package/src/assets/images/translate.svg +1 -0
- package/src/assets/silence.mp3 +0 -0
- package/src/css/_BRnav.scss +0 -24
- package/src/css/_BRsearch.scss +1 -5
- package/src/css/_TextSelection.scss +38 -9
- package/src/plugins/plugin.experiments.js +34 -9
- package/src/plugins/plugin.text_selection.js +17 -20
- package/src/plugins/translate/TranslationManager.js +170 -0
- package/src/plugins/translate/plugin.translate.js +489 -0
- package/src/plugins/tts/AbstractTTSEngine.js +3 -4
- package/src/plugins/tts/PageChunk.js +28 -9
- package/src/plugins/tts/WebTTSEngine.js +5 -7
- package/src/plugins/tts/plugin.tts.js +40 -4
- package/src/plugins/tts/utils.js +21 -22
- package/src/util/cache.js +20 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { html, LitElement } from 'lit';
|
|
3
|
+
import { BookReaderPlugin } from '../../BookReaderPlugin.js';
|
|
4
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
5
|
+
import { langs, TranslationManager } from "./TranslationManager.js";
|
|
6
|
+
import { toISO6391 } from '../tts/utils.js';
|
|
7
|
+
import { sortBy } from '../../../src/BookReader/utils.js';
|
|
8
|
+
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
const BookReader = /** @type {typeof import('@/src/BookReader.js').default} */(window.BookReader);
|
|
11
|
+
|
|
12
|
+
export class TranslatePlugin extends BookReaderPlugin {
|
|
13
|
+
|
|
14
|
+
options = {
|
|
15
|
+
enabled: true,
|
|
16
|
+
|
|
17
|
+
/** @type {string | import('lit').TemplateResult} */
|
|
18
|
+
panelDisclaimerText: "Translations are in alpha",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** @type {TranslationManager} */
|
|
22
|
+
translationManager = new TranslationManager();
|
|
23
|
+
|
|
24
|
+
/** @type {Worker}*/
|
|
25
|
+
worker;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Contains the list of languages available to translate to
|
|
29
|
+
* @type {string[]}
|
|
30
|
+
*/
|
|
31
|
+
toLanguages = [];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Current language code that is being translated From. Defaults to EN currently
|
|
35
|
+
* @type {!string}
|
|
36
|
+
*/
|
|
37
|
+
langFromCode
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Current language code that is being translated To
|
|
41
|
+
* @type {!string}
|
|
42
|
+
*/
|
|
43
|
+
langToCode;
|
|
44
|
+
/**
|
|
45
|
+
* @type {BrTranslatePanel} _panel - Represents a panel used in the plugin.
|
|
46
|
+
* The specific type and purpose of this panel should be defined based on its usage.
|
|
47
|
+
*/
|
|
48
|
+
_panel;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @type {boolean} userToggleTranslate - Checks if user has initiated translation
|
|
52
|
+
* Should synchronize with the state of TranslationManager's active state
|
|
53
|
+
*/
|
|
54
|
+
userToggleTranslate;
|
|
55
|
+
|
|
56
|
+
async init() {
|
|
57
|
+
const currentLanguage = toISO6391(this.br.options.bookLanguage.replace(/[.,/#!$%^&*;:{}=\-_`~()]/g, ""));
|
|
58
|
+
this.langFromCode = currentLanguage ?? "en";
|
|
59
|
+
|
|
60
|
+
if (!this.options.enabled) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.translationManager.publicPath = this.br.options.imagesBaseURL.replace(/\/+$/, '') + '/..';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {*} ev
|
|
68
|
+
* @param {object} eventProps
|
|
69
|
+
*/
|
|
70
|
+
this.br.on('textLayerRendered', async (_, {pageIndex, pageContainer}) => {
|
|
71
|
+
// Stops invalid models from running, also prevents translation on page load
|
|
72
|
+
// TODO check if model has finished loading or if it exists
|
|
73
|
+
if (!this.translationManager) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (this.translationManager.active) {
|
|
77
|
+
const pageElement = pageContainer.$container[0];
|
|
78
|
+
this.translateRenderedLayer(pageElement);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {*} ev
|
|
84
|
+
* @param {object} eventProps
|
|
85
|
+
*/
|
|
86
|
+
this.br.on('pageVisible', (_, {pageContainerEl}) => {
|
|
87
|
+
if (!this.translationManager.active) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
for (const paragraphEl of pageContainerEl.querySelectorAll('.BRtranslateLayer > .BRparagraphElement')) {
|
|
91
|
+
if (paragraphEl.textContent) {
|
|
92
|
+
this.fitVisiblePage(paragraphEl);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await this.translationManager.initWorker();
|
|
98
|
+
// Note await above lets _render function properly, since it gives the browser
|
|
99
|
+
// time to render the rest of bookreader, which _render depends on
|
|
100
|
+
this.langToCode = this.translationManager.toLanguages[0].code;
|
|
101
|
+
this._render();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** @param {HTMLElement} page*/
|
|
105
|
+
getParagraphsOnPage = (page) => {
|
|
106
|
+
return page ? Array.from(page.querySelectorAll(".BRtextLayer > .BRparagraphElement")) : [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
translateActivePageContainerElements() {
|
|
110
|
+
const currentlyActiveContainers = this.br.getActivePageContainerElements();
|
|
111
|
+
const visiblePageContainers = currentlyActiveContainers.filter((element) => {
|
|
112
|
+
return element.classList.contains('BRpage-visible');
|
|
113
|
+
});
|
|
114
|
+
const hiddenPageContainers = currentlyActiveContainers.filter((element) => {
|
|
115
|
+
return !element.classList.contains('BRpage-visible');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
for (const page of visiblePageContainers) {
|
|
119
|
+
this.translateRenderedLayer(page, 0);
|
|
120
|
+
}
|
|
121
|
+
for (const loadingPage of hiddenPageContainers) {
|
|
122
|
+
this.translateRenderedLayer(loadingPage, 1000);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** @param {HTMLElement} page */
|
|
127
|
+
async translateRenderedLayer(page, priority) {
|
|
128
|
+
// Do not run translation if in thumbnail mode or if user did not initiate transations
|
|
129
|
+
if (this.br.mode == this.br.constModeThumb || !this.userToggleTranslate || this.langFromCode == this.langToCode) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const pageIndex = page.dataset.index;
|
|
134
|
+
|
|
135
|
+
let pageTranslationLayer;
|
|
136
|
+
if (!page.querySelector('.BRPageLayer.BRtranslateLayer')) {
|
|
137
|
+
pageTranslationLayer = document.createElement('div');
|
|
138
|
+
pageTranslationLayer.classList.add('BRPageLayer', 'BRtranslateLayer', 'BRtranslateLayerLoading');
|
|
139
|
+
pageTranslationLayer.setAttribute('lang', `${this.langToCode}`);
|
|
140
|
+
page.prepend(pageTranslationLayer);
|
|
141
|
+
} else {
|
|
142
|
+
pageTranslationLayer = page.querySelector('.BRPageLayer.BRtranslateLayer');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const textLayerElement = page.querySelector('.BRtextLayer');
|
|
146
|
+
textLayerElement.classList.add('showingTranslation');
|
|
147
|
+
$(pageTranslationLayer).css({
|
|
148
|
+
"width": $(textLayerElement).css("width"),
|
|
149
|
+
"height": $(textLayerElement).css("height"),
|
|
150
|
+
"transform": $(textLayerElement).css("transform"),
|
|
151
|
+
"pointer-events": $(textLayerElement).css("pointer-events"),
|
|
152
|
+
"z-index": 3,
|
|
153
|
+
});
|
|
154
|
+
const paragraphs = this.getParagraphsOnPage(page);
|
|
155
|
+
|
|
156
|
+
const paragraphTranslationPromises = paragraphs.map(async (paragraph, pidx) => {
|
|
157
|
+
let translatedParagraph = page.querySelector(`[data-translate-index='${pageIndex}-${pidx}']`);
|
|
158
|
+
if (!translatedParagraph) {
|
|
159
|
+
translatedParagraph = document.createElement('p');
|
|
160
|
+
// set data-translate-index on the placeholder
|
|
161
|
+
translatedParagraph.setAttribute('data-translate-index', `${pageIndex}-${pidx}`);
|
|
162
|
+
translatedParagraph.className = 'BRparagraphElement';
|
|
163
|
+
const originalParagraphStyle = paragraphs[pidx];
|
|
164
|
+
// check text selection paragraphs for header/footer roles
|
|
165
|
+
if (paragraph.classList.contains('ocr-role-header-footer')) {
|
|
166
|
+
translatedParagraph.ariaHidden = "true";
|
|
167
|
+
translatedParagraph.classList.add('ocr-role-header-footer');
|
|
168
|
+
}
|
|
169
|
+
const fontSize = `${parseInt($(originalParagraphStyle).css("font-size"))}px`;
|
|
170
|
+
|
|
171
|
+
$(translatedParagraph).css({
|
|
172
|
+
"margin-left": $(originalParagraphStyle).css("margin-left"),
|
|
173
|
+
"margin-top": $(originalParagraphStyle).css("margin-top"),
|
|
174
|
+
"top": $(originalParagraphStyle).css("top"),
|
|
175
|
+
"height": $(originalParagraphStyle).css("height"),
|
|
176
|
+
"width": $(originalParagraphStyle).css("width"),
|
|
177
|
+
"font-size": fontSize,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Note: We'll likely want to switch to using the same logic as
|
|
181
|
+
// TextSelectionPlugin's selection, which allows for e.g. click-to-flip
|
|
182
|
+
// to work simultaneously with text selection.
|
|
183
|
+
translatedParagraph.addEventListener('mousedown', (e) => {
|
|
184
|
+
e.stopPropagation();
|
|
185
|
+
e.stopImmediatePropagation();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
translatedParagraph.addEventListener('mouseup', (e) => {
|
|
189
|
+
e.stopPropagation();
|
|
190
|
+
e.stopImmediatePropagation();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
translatedParagraph.addEventListener('dragstart', (e) =>{
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
});
|
|
196
|
+
pageTranslationLayer.append(translatedParagraph);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (paragraph.textContent.length !== 0) {
|
|
200
|
+
const pagePriority = parseFloat(pageIndex) + priority + pidx;
|
|
201
|
+
const translatedText = await this.translationManager.getTranslation(this.langFromCode, this.langToCode, pageIndex, pidx, paragraph.textContent, pagePriority);
|
|
202
|
+
// prevent duplicate spans from appearing if exists
|
|
203
|
+
translatedParagraph.firstElementChild?.remove();
|
|
204
|
+
|
|
205
|
+
const firstWordSpacing = paragraphs[pidx]?.firstChild?.firstChild;
|
|
206
|
+
const createSpan = document.createElement('span');
|
|
207
|
+
createSpan.className = 'BRlineElement';
|
|
208
|
+
createSpan.textContent = translatedText;
|
|
209
|
+
translatedParagraph.appendChild(createSpan);
|
|
210
|
+
|
|
211
|
+
$(createSpan).css({
|
|
212
|
+
"text-indent": $(firstWordSpacing).css('padding-left'),
|
|
213
|
+
});
|
|
214
|
+
if (page.classList.contains('BRpage-visible')) {
|
|
215
|
+
this.fitVisiblePage(translatedParagraph);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
await Promise.all(paragraphTranslationPromises);
|
|
220
|
+
this.br.trigger('translateLayerRendered', {
|
|
221
|
+
leafIndex: pageIndex,
|
|
222
|
+
translateLayer: pageTranslationLayer,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get the translation layers for a specific leaf index.
|
|
228
|
+
* @param {number} leafIndex
|
|
229
|
+
* @returns {Promise<HTMLElement[]>}
|
|
230
|
+
*/
|
|
231
|
+
async getTranslateLayers(leafIndex) {
|
|
232
|
+
const pageContainerElements = this.br.getActivePageContainerElementsForIndex(leafIndex);
|
|
233
|
+
const translateLayer = $(pageContainerElements).filter(`[data-index='${leafIndex}']`).find('.BRtranslateLayer');
|
|
234
|
+
if (translateLayer.length) return translateLayer.toArray();
|
|
235
|
+
|
|
236
|
+
return new Promise((res, rej) => {
|
|
237
|
+
const handler = async (_, extraParams) => {
|
|
238
|
+
if (extraParams.leafIndex == leafIndex) {
|
|
239
|
+
this.br.off('translateLayerRendered', handler); // remember to detach translateLayer
|
|
240
|
+
res([extraParams.translateLayer]);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
this.br.on('translateLayerRendered', handler);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
clearAllTranslations() {
|
|
248
|
+
document.querySelectorAll('.BRtranslateLayer').forEach(el => el.remove());
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @param {Element} paragEl
|
|
253
|
+
*/
|
|
254
|
+
fitVisiblePage(paragEl) {
|
|
255
|
+
// For some reason, Chrome does not detect the transform property for the translation + text layers
|
|
256
|
+
// Could not get it to fetch the transform value using $().css method
|
|
257
|
+
// Oddly enough the value is retrieved if using .style.transform instead?
|
|
258
|
+
const translateLayerEl = paragEl.parentElement;
|
|
259
|
+
if ($(translateLayerEl).css('transform') == 'none') {
|
|
260
|
+
const pageNumber = paragEl.getAttribute('data-translate-index').split('-')[0];
|
|
261
|
+
/** @type {HTMLElement} selectionTransform */
|
|
262
|
+
const textLayerEl = document.querySelector(`[data-index='${pageNumber}'] .BRtextLayer`);
|
|
263
|
+
$(translateLayerEl).css({'transform': textLayerEl.style.transform});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const originalFontSize = parseInt($(paragEl).css("font-size"));
|
|
267
|
+
let adjustedFontSize = originalFontSize;
|
|
268
|
+
while (paragEl.clientHeight < paragEl.scrollHeight && adjustedFontSize > 0) {
|
|
269
|
+
adjustedFontSize--;
|
|
270
|
+
$(paragEl).css({ "font-size": `${adjustedFontSize}px` });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const textHeight = paragEl.firstElementChild.clientHeight;
|
|
274
|
+
const scrollHeight = paragEl.scrollHeight;
|
|
275
|
+
const fits = textHeight < scrollHeight;
|
|
276
|
+
if (fits) {
|
|
277
|
+
const lines = textHeight / adjustedFontSize;
|
|
278
|
+
// Line heights for smaller paragraphs occasionally need a minor adjustment
|
|
279
|
+
const newLineHeight = scrollHeight / lines;
|
|
280
|
+
$(paragEl).css({
|
|
281
|
+
"line-height" : `${newLineHeight}px`,
|
|
282
|
+
"overflow": "visible",
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
handleFromLangChange = async (e) => {
|
|
288
|
+
this.clearAllTranslations();
|
|
289
|
+
const selectedLangFrom = e.detail.value;
|
|
290
|
+
|
|
291
|
+
// Update the from language
|
|
292
|
+
this.langFromCode = selectedLangFrom;
|
|
293
|
+
|
|
294
|
+
// Add 'From' language to 'To' list if not already present
|
|
295
|
+
if (!this.translationManager.toLanguages.some(lang => lang.code === selectedLangFrom)) {
|
|
296
|
+
this.translationManager.toLanguages.push({ code: selectedLangFrom, name: langs[selectedLangFrom] });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Update the 'To' languages list and set the correct 'To' language
|
|
300
|
+
this._panel.toLanguages = this.translationManager.toLanguages;
|
|
301
|
+
|
|
302
|
+
console.log(this.langFromCode, this.langToCode);
|
|
303
|
+
if (this.langFromCode !== this.langToCode) {
|
|
304
|
+
this.translateActivePageContainerElements();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
handleToLangChange = async (e) => {
|
|
309
|
+
this.clearAllTranslations();
|
|
310
|
+
this.langToCode = e.detail.value;
|
|
311
|
+
this.translateActivePageContainerElements();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
handleToggleTranslation = async () => {
|
|
315
|
+
this.userToggleTranslate = !this.userToggleTranslate;
|
|
316
|
+
this.translationManager.active = this.userToggleTranslate;
|
|
317
|
+
if (!this.userToggleTranslate) {
|
|
318
|
+
this.clearAllTranslations();
|
|
319
|
+
this.br.trigger('translationDisabled', { });
|
|
320
|
+
} else {
|
|
321
|
+
this.br.trigger('translationEnabled', { });
|
|
322
|
+
this.translateActivePageContainerElements();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Update translation side menu
|
|
328
|
+
*/
|
|
329
|
+
_render() {
|
|
330
|
+
this.br.shell.menuProviders['translate'] = {
|
|
331
|
+
id: 'translate',
|
|
332
|
+
icon: html`<img src='${this.br.options.imagesBaseURL}/translate.svg' width="26"/>`,
|
|
333
|
+
label: 'Translate',
|
|
334
|
+
component: html`<br-translate-panel
|
|
335
|
+
@connected="${e => {
|
|
336
|
+
this._panel = e.target;
|
|
337
|
+
this._panel.fromLanguages = this.translationManager.fromLanguages;
|
|
338
|
+
this._panel.toLanguages = this.translationManager.toLanguages;
|
|
339
|
+
}
|
|
340
|
+
}"
|
|
341
|
+
@langFromChanged="${this.handleFromLangChange}"
|
|
342
|
+
@langToChanged="${this.handleToLangChange}"
|
|
343
|
+
@toggleTranslation="${this.handleToggleTranslation}"
|
|
344
|
+
.fromLanguages="${this.translationManager.fromLanguages}"
|
|
345
|
+
.toLanguages="${this.translationManager.toLanguages}"
|
|
346
|
+
.disclaimerMessage="${this.options.panelDisclaimerText}"
|
|
347
|
+
.userTranslationActive=${false}
|
|
348
|
+
.detectedFromLang=${this.langFromCode}
|
|
349
|
+
.detectedToLang=${this.langToCode}
|
|
350
|
+
class="translate-panel"
|
|
351
|
+
/>`,
|
|
352
|
+
};
|
|
353
|
+
this.br.shell.updateMenuContents();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
BookReader?.registerPlugin('translate', TranslatePlugin);
|
|
357
|
+
|
|
358
|
+
@customElement('br-translate-panel')
|
|
359
|
+
export class BrTranslatePanel extends LitElement {
|
|
360
|
+
@property({ type: Array }) fromLanguages = []; // List of obj {code, name}
|
|
361
|
+
@property({ type: Array }) toLanguages = []; // List of obj {code, name}
|
|
362
|
+
@property({ type: String }) prevSelectedLang = ''; // Tracks the previous selected language for the "To" dropdown
|
|
363
|
+
@property({ type: String }) disclaimerMessage = '';
|
|
364
|
+
@property({ type: Boolean }) userTranslationActive = false;
|
|
365
|
+
@property({ type: String }) detectedFromLang = '';
|
|
366
|
+
@property({ type: String }) detectedToLang = '';
|
|
367
|
+
|
|
368
|
+
/** @override */
|
|
369
|
+
createRenderRoot() {
|
|
370
|
+
// Disable shadow DOM; that would require a huge rejiggering of CSS
|
|
371
|
+
return this;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
connectedCallback() {
|
|
375
|
+
super.connectedCallback();
|
|
376
|
+
this.dispatchEvent(new CustomEvent('connected'));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
render() {
|
|
380
|
+
return html`<div class="app" style="margin-top: 5%;padding-right: 5px;">
|
|
381
|
+
<div
|
|
382
|
+
class="disclaimer"
|
|
383
|
+
id="disclaimerMessage"
|
|
384
|
+
style="background-color: rgba(255,255,255,0.1);padding: 10px;border-radius: 8px;font-size: 12px;margin-bottom: 10px;color: rgba(255,255,255, 0.9);"
|
|
385
|
+
>${this.disclaimerMessage}</div>
|
|
386
|
+
|
|
387
|
+
<div class="panel panel--to" style="padding: 0 10px;">
|
|
388
|
+
<label>
|
|
389
|
+
<span style="font-size: 12px;color: #ccc;">Translate To</span>
|
|
390
|
+
<select id="lang-to" name="to" class="lang-select" style="display:block; width:100%;" @change="${this._onLangToChange}">
|
|
391
|
+
${sortBy(this.toLanguages, ((lang) => lang.name.toLowerCase()
|
|
392
|
+
)).map((lang) => {
|
|
393
|
+
return html`<option value="${lang.code}"
|
|
394
|
+
?selected=${lang.code == this.detectedToLang}
|
|
395
|
+
> ${lang.name ? lang.name : lang.code} </option>`;
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
</select>
|
|
399
|
+
</label>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
<div class="panel panel--start" style="text-align: right;padding: 0 10px;/*! font-size: 18px; */margin-top: 10px;">
|
|
403
|
+
<button class="start-translation-brn" @click="${this._toggleTranslation}">
|
|
404
|
+
${this.userTranslationActive ? "Stop Translating" : "Translate"}
|
|
405
|
+
</button>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<div class="panel panel--from" style="font-size: 12px;color: #ccc;text-align: center;padding: 8px 10px;">
|
|
409
|
+
<details style="display: contents">
|
|
410
|
+
<summary style="text-decoration: underline white; cursor:pointer; display:inline-block">
|
|
411
|
+
<i>
|
|
412
|
+
Source: ${this._getLangName(this.detectedFromLang)} ${this.prevSelectedLang ? "" : "(detected)"}
|
|
413
|
+
</i> Change
|
|
414
|
+
</summary>
|
|
415
|
+
<select id="lang-from" name="from" class="lang-select" value=${this.detectedFromLang} @change="${this._onLangFromChange}" style="width:65%; margin-top: 3%; margin-bottom: 3%">
|
|
416
|
+
${sortBy(this.fromLanguages, ((lang) => lang.name.toLowerCase()
|
|
417
|
+
)).map((lang) => {
|
|
418
|
+
return html`<option value="${lang.code}"
|
|
419
|
+
?selected=${lang.code == this.detectedFromLang}
|
|
420
|
+
>${lang.name ? lang.name : lang.code} </option>`;
|
|
421
|
+
})
|
|
422
|
+
}
|
|
423
|
+
</select>
|
|
424
|
+
</details>
|
|
425
|
+
<div class="footer" id="status" style="margin-top:5%">
|
|
426
|
+
${this._statusWarning()}
|
|
427
|
+
</div>
|
|
428
|
+
</div>`;
|
|
429
|
+
}
|
|
430
|
+
_onLangFromChange(event) {
|
|
431
|
+
const langFromChangedEvent = new CustomEvent('langFromChanged', {
|
|
432
|
+
detail: { value: event.target.value },
|
|
433
|
+
bubbles: true,
|
|
434
|
+
composed: true,
|
|
435
|
+
});
|
|
436
|
+
this.dispatchEvent(langFromChangedEvent);
|
|
437
|
+
|
|
438
|
+
// Update the prevSelectedLang if "To" is different from "From"
|
|
439
|
+
if (this._getSelectedLang('to') !== this._getSelectedLang('from')) {
|
|
440
|
+
this.prevSelectedLang = this._getSelectedLang('from');
|
|
441
|
+
}
|
|
442
|
+
this.detectedFromLang = event.target.value;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
_onLangToChange(event) {
|
|
446
|
+
const langToChangedEvent = new CustomEvent('langToChanged', {
|
|
447
|
+
detail: { value: event.target.value },
|
|
448
|
+
bubbles: true,
|
|
449
|
+
composed: true,
|
|
450
|
+
});
|
|
451
|
+
this.dispatchEvent(langToChangedEvent);
|
|
452
|
+
|
|
453
|
+
// Update the prevSelectedLang if "To" is different from "From"
|
|
454
|
+
if (this._getSelectedLang('from') !== event.target.value) {
|
|
455
|
+
this.prevSelectedLang = this._getSelectedLang('from');
|
|
456
|
+
}
|
|
457
|
+
this.detectedToLang = event.target.value;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
_getSelectedLang(type) {
|
|
461
|
+
/** @type {HTMLSelectElement} */
|
|
462
|
+
const dropdown = this.querySelector(`#lang-${type}`);
|
|
463
|
+
return dropdown ? dropdown.value : '';
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
_getLangName(code) {
|
|
467
|
+
const lang = [...this.fromLanguages, ...this.toLanguages].find(lang => lang.code === code);
|
|
468
|
+
return lang ? lang.name : '';
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
_toggleTranslation(event) {
|
|
472
|
+
const toggleTranslateEvent = new CustomEvent('toggleTranslation', {
|
|
473
|
+
detail: {value: event.target.value},
|
|
474
|
+
bubbles: true,
|
|
475
|
+
composed:true,
|
|
476
|
+
});
|
|
477
|
+
this.userTranslationActive = !this.userTranslationActive;
|
|
478
|
+
this.dispatchEvent(toggleTranslateEvent);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// TODO: Hardcoded warning message for now but should add more statuses
|
|
482
|
+
_statusWarning() {
|
|
483
|
+
if (this.detectedFromLang == this.detectedToLang) {
|
|
484
|
+
return "Translate To language is the same as the Source language";
|
|
485
|
+
}
|
|
486
|
+
return "";
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
@@ -86,9 +86,8 @@ export default class AbstractTTSEngine {
|
|
|
86
86
|
|
|
87
87
|
this._chunkIterator = new PageChunkIterator(numLeafs, leafIndex, {
|
|
88
88
|
pageChunkUrl: this.opts.pageChunkUrl,
|
|
89
|
-
pageBufferSize:
|
|
89
|
+
pageBufferSize: 3,
|
|
90
90
|
});
|
|
91
|
-
|
|
92
91
|
this.step();
|
|
93
92
|
this.events.trigger('start');
|
|
94
93
|
}
|
|
@@ -205,8 +204,8 @@ export default class AbstractTTSEngine {
|
|
|
205
204
|
}
|
|
206
205
|
|
|
207
206
|
/** Convenience wrapper for {@see AbstractTTSEngine.getBestVoice} */
|
|
208
|
-
getBestVoice() {
|
|
209
|
-
return AbstractTTSEngine.getBestBookVoice(this.getVoices(), this.opts.bookLanguage);
|
|
207
|
+
getBestVoice(languageOverride) {
|
|
208
|
+
return AbstractTTSEngine.getBestBookVoice(this.getVoices(), languageOverride || this.opts.bookLanguage);
|
|
210
209
|
}
|
|
211
210
|
|
|
212
211
|
/**
|
|
@@ -23,15 +23,34 @@ export default class PageChunk {
|
|
|
23
23
|
* @return {Promise<PageChunk[]>}
|
|
24
24
|
*/
|
|
25
25
|
static async fetch(pageChunkUrl, leafIndex) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
if (window.br.plugins.translate?.translationManager.active) {
|
|
27
|
+
const translateLayers = await window.br.plugins.translate.getTranslateLayers(leafIndex);
|
|
28
|
+
const paragraphs = Array.from(translateLayers[0].childNodes);
|
|
29
|
+
|
|
30
|
+
const pageChunks = [];
|
|
31
|
+
for (const [idx, item] of paragraphs.entries()) {
|
|
32
|
+
// Should not read paragraphs w/ header or footer roles
|
|
33
|
+
if (!item.classList.contains('ocr-role-header-footer')) {
|
|
34
|
+
const translatedChunk = new PageChunk(leafIndex, idx, item.textContent, []);
|
|
35
|
+
pageChunks.push(translatedChunk);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (pageChunks.length === 0) {
|
|
39
|
+
const placeholder = new PageChunk(leafIndex, 0, "", []);
|
|
40
|
+
pageChunks.push(placeholder);
|
|
41
|
+
}
|
|
42
|
+
return pageChunks;
|
|
43
|
+
} else {
|
|
44
|
+
const chunks = await $.ajax({
|
|
45
|
+
type: 'GET',
|
|
46
|
+
url: applyVariables(pageChunkUrl, { pageIndex: leafIndex }),
|
|
47
|
+
cache: true,
|
|
48
|
+
xhrFields: {
|
|
49
|
+
withCredentials: window.br.protected,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
return PageChunk._fromTextWrapperResponse(leafIndex, chunks);
|
|
53
|
+
}
|
|
35
54
|
}
|
|
36
55
|
|
|
37
56
|
/**
|
|
@@ -30,7 +30,11 @@ export default class WebTTSEngine extends AbstractTTSEngine {
|
|
|
30
30
|
start(leafIndex, numLeafs) {
|
|
31
31
|
// Need to run in this function to capture user intent to start playing audio
|
|
32
32
|
if ('mediaSession' in navigator) {
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* According to https://developers.google.com/web/updates/2017/02/media-session#implementation_notes , it needs to be at least 5 seconds
|
|
35
|
+
* long to allow usage of the media sessions api
|
|
36
|
+
*/
|
|
37
|
+
const audio = new Audio(br.options.imagesBaseURL + '../silence.mp3');
|
|
34
38
|
audio.loop = true;
|
|
35
39
|
|
|
36
40
|
this.events.on('pause', () => audio.pause());
|
|
@@ -386,9 +390,3 @@ export class WebTTSSound {
|
|
|
386
390
|
}
|
|
387
391
|
}
|
|
388
392
|
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* According to https://developers.google.com/web/updates/2017/02/media-session#implementation_notes , it needs to be at least 5 seconds
|
|
392
|
-
* long to allow usage of the media sessions api
|
|
393
|
-
*/
|
|
394
|
-
const SILENCE_6S_MP3 = 'data:audio/mp3;base64,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';
|
|
@@ -165,13 +165,26 @@ export class TtsPlugin extends BookReaderPlugin {
|
|
|
165
165
|
|
|
166
166
|
const renderVoicesMenu = (voicesMenu) => {
|
|
167
167
|
voicesMenu.empty();
|
|
168
|
-
|
|
168
|
+
let bookLanguage = this.ttsEngine.opts.bookLanguage;
|
|
169
|
+
if (this.br.plugins.translate?.translationManager?.active) {
|
|
170
|
+
bookLanguage = this.br.plugins.translate.langFromCode;
|
|
171
|
+
}
|
|
172
|
+
|
|
169
173
|
const bookLanguages = this.ttsEngine.getVoices().filter(v => v.lang.startsWith(bookLanguage)).sort(voiceSortOrder);
|
|
170
174
|
const otherLanguages = this.ttsEngine.getVoices().filter(v => !v.lang.startsWith(bookLanguage)).sort(voiceSortOrder);
|
|
171
175
|
|
|
172
176
|
if (this.ttsEngine.getVoices().length > 1) {
|
|
173
|
-
|
|
174
|
-
|
|
177
|
+
if (this.br.plugins.translate?.translationManager?.active) {
|
|
178
|
+
// Separate out Other Languages when translation active, not sure if too much / unwieldy
|
|
179
|
+
const toLang = this.br.plugins.translate.langToCode;
|
|
180
|
+
const translatedLanguages = this.ttsEngine.getVoices().filter(v => v.lang.startsWith(toLang)).sort(voiceSortOrder);
|
|
181
|
+
voicesMenu.append($(`<optgroup label="Book Language, Translated From (${bookLanguage})"> ${renderVoiceOption(bookLanguages)} </optgroup>`));
|
|
182
|
+
voicesMenu.append($(`<optgroup label="Book Language, Translated To (${toLang})"> ${renderVoiceOption(translatedLanguages)} </optgroup>`));
|
|
183
|
+
voicesMenu.append($(`<optgroup label="Other Languages"> ${renderVoiceOption(otherLanguages.filter(v => !v.lang.startsWith(toLang)).sort(voiceSortOrder))}`));
|
|
184
|
+
} else {
|
|
185
|
+
voicesMenu.append($(`<optgroup label="Book Language (${bookLanguage})"> ${renderVoiceOption(bookLanguages)} </optgroup>`));
|
|
186
|
+
voicesMenu.append($(`<optgroup label="Other Languages"> ${renderVoiceOption(otherLanguages)} </optgroup>`));
|
|
187
|
+
}
|
|
175
188
|
|
|
176
189
|
voicesMenu.val(this.ttsEngine.voice.voiceURI);
|
|
177
190
|
voicesMenu.show();
|
|
@@ -185,6 +198,8 @@ export class TtsPlugin extends BookReaderPlugin {
|
|
|
185
198
|
voicesMenu.on("change", ev => this.ttsEngine.setVoice(voicesMenu.val()));
|
|
186
199
|
this.ttsEngine.events.on('pause resume start', () => this.updateState());
|
|
187
200
|
this.ttsEngine.events.on('voiceschanged', () => renderVoicesMenu(voicesMenu));
|
|
201
|
+
this.br.on('translationEnabled', () => renderVoicesMenu(voicesMenu));
|
|
202
|
+
this.br.on('translationDisabled', () => renderVoicesMenu(voicesMenu));
|
|
188
203
|
this.br.refs.$BRReadAloudToolbar.find('[name=play]').on("click", this.playPause.bind(this));
|
|
189
204
|
this.br.refs.$BRReadAloudToolbar.find('[name=advance]').on("click", this.jumpForward.bind(this));
|
|
190
205
|
this.br.refs.$BRReadAloudToolbar.find('[name=review]').on("click", this.jumpBackward.bind(this));
|
|
@@ -208,6 +223,12 @@ export class TtsPlugin extends BookReaderPlugin {
|
|
|
208
223
|
}
|
|
209
224
|
|
|
210
225
|
start(startTTSEngine = true) {
|
|
226
|
+
const checkVoice = this.br.plugins.translate?.translationManager?.active ? toISO6391(this.br.plugins.translate.langToCode) : "";
|
|
227
|
+
const bookVoice = this.ttsEngine.getBestVoice(checkVoice);
|
|
228
|
+
|
|
229
|
+
const voicesMenu = this.br.refs.$BRReadAloudToolbar.find('[name=playback-voice');
|
|
230
|
+
this.ttsEngine.setVoice(bookVoice.voiceURI);
|
|
231
|
+
voicesMenu.val(bookVoice.voiceURI);
|
|
211
232
|
if (this.br.constModeThumb == this.br.mode)
|
|
212
233
|
this.br.switchMode(this.br.constMode1up);
|
|
213
234
|
|
|
@@ -291,7 +312,22 @@ export class TtsPlugin extends BookReaderPlugin {
|
|
|
291
312
|
const pageIndex = chunk.leafIndex;
|
|
292
313
|
|
|
293
314
|
this.removeHilites();
|
|
294
|
-
|
|
315
|
+
if (this.br.plugins.translate?.translationManager.active) {
|
|
316
|
+
const pageContainers = this.br.getActivePageContainerElementsForIndex(pageIndex);
|
|
317
|
+
const paragraphIndex = chunk.chunkIndex;
|
|
318
|
+
pageContainers.forEach(container => {
|
|
319
|
+
const translateElement = container.querySelector('.BRtranslateLayer');
|
|
320
|
+
const containerChildren = Array.from(translateElement.childNodes);
|
|
321
|
+
const paragraphEle = containerChildren[paragraphIndex];
|
|
322
|
+
if (!paragraphEle) { return; }
|
|
323
|
+
const [pOffHeight, pOffTop, pOffWidth, pOffLeft] = [paragraphEle.offsetHeight, paragraphEle.offsetTop, paragraphEle.offsetWidth, paragraphEle.offsetLeft];
|
|
324
|
+
const boxes = {pageIndex: [
|
|
325
|
+
{l: pOffLeft, r: pOffLeft + pOffWidth, b: pOffTop + pOffHeight, t: pOffTop},
|
|
326
|
+
]};
|
|
327
|
+
renderBoxesInPageContainerLayer('ttsHiliteLayer', boxes.pageIndex, this.br.book.getPage(pageIndex), translateElement);
|
|
328
|
+
});
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
295
331
|
// group by index; currently only possible to have chunks on one page :/
|
|
296
332
|
this._ttsBoxesByIndex = {
|
|
297
333
|
[pageIndex]: chunk.lineRects.map(([l, b, r, t]) => ({l, r, b, t})),
|