@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.
Files changed (47) hide show
  1. package/BookReader/474.js +2 -0
  2. package/BookReader/474.js.map +1 -0
  3. package/BookReader/BookReader.css +39 -34
  4. package/BookReader/BookReader.js +1 -1
  5. package/BookReader/BookReader.js.map +1 -1
  6. package/BookReader/bergamot-translator-worker.js +2966 -0
  7. package/BookReader/bergamot-translator-worker.wasm +0 -0
  8. package/BookReader/ia-bookreader-bundle.js +1 -1
  9. package/BookReader/ia-bookreader-bundle.js.map +1 -1
  10. package/BookReader/images/icon_experiment.svg +1 -0
  11. package/BookReader/images/translate.svg +1 -0
  12. package/BookReader/plugins/plugin.experiments.js +1 -1
  13. package/BookReader/plugins/plugin.experiments.js.map +1 -1
  14. package/BookReader/plugins/plugin.text_selection.js +1 -1
  15. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  16. package/BookReader/plugins/plugin.translate.js +3 -0
  17. package/BookReader/plugins/plugin.translate.js.LICENSE.txt +1 -0
  18. package/BookReader/plugins/plugin.translate.js.map +1 -0
  19. package/BookReader/plugins/plugin.tts.js +1 -1
  20. package/BookReader/plugins/plugin.tts.js.map +1 -1
  21. package/BookReader/plugins/translator-worker.js +2 -0
  22. package/BookReader/plugins/translator-worker.js.map +1 -0
  23. package/BookReader/silence.mp3 +0 -0
  24. package/BookReader/translator-worker.js +475 -0
  25. package/package.json +6 -3
  26. package/src/BookNavigator/book-navigator.js +1 -0
  27. package/src/BookReader/Mode1UpLit.js +6 -1
  28. package/src/BookReader/Mode2UpLit.js +11 -1
  29. package/src/BookReader/Navbar/Navbar.js +61 -0
  30. package/src/BookReader/options.js +12 -8
  31. package/src/BookReader.js +67 -140
  32. package/src/assets/images/icon_experiment.svg +1 -0
  33. package/src/assets/images/translate.svg +1 -0
  34. package/src/assets/silence.mp3 +0 -0
  35. package/src/css/_BRnav.scss +0 -24
  36. package/src/css/_BRsearch.scss +1 -5
  37. package/src/css/_TextSelection.scss +38 -9
  38. package/src/plugins/plugin.experiments.js +34 -9
  39. package/src/plugins/plugin.text_selection.js +17 -20
  40. package/src/plugins/translate/TranslationManager.js +170 -0
  41. package/src/plugins/translate/plugin.translate.js +489 -0
  42. package/src/plugins/tts/AbstractTTSEngine.js +3 -4
  43. package/src/plugins/tts/PageChunk.js +28 -9
  44. package/src/plugins/tts/WebTTSEngine.js +5 -7
  45. package/src/plugins/tts/plugin.tts.js +40 -4
  46. package/src/plugins/tts/utils.js +21 -22
  47. 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: 5,
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
- const chunks = await $.ajax({
27
- type: 'GET',
28
- url: applyVariables(pageChunkUrl, { pageIndex: leafIndex }),
29
- cache: true,
30
- xhrFields: {
31
- withCredentials: window.br.protected,
32
- },
33
- });
34
- return PageChunk._fromTextWrapperResponse(leafIndex, chunks);
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
- const audio = new Audio(SILENCE_6S_MP3);
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
- const bookLanguage = this.ttsEngine.opts.bookLanguage;
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
- voicesMenu.append($(`<optgroup label="Book Language (${bookLanguage})"> ${renderVoiceOption(bookLanguages)} </optgroup>`));
174
- voicesMenu.append($(`<optgroup label="Other Languages"> ${renderVoiceOption(otherLanguages)} </optgroup>`));
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})),