@internetarchive/bookreader 5.0.0-97 → 5.0.0-99

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.
@@ -2,7 +2,11 @@
2
2
  import { html, LitElement } from 'lit';
3
3
  import { BookReaderPlugin } from '../../BookReaderPlugin.js';
4
4
  import { customElement, property } from 'lit/decorators.js';
5
- import { langs, TranslationManager } from "./TranslationManager.js";
5
+ import { TranslationManager } from "./TranslationManager.js";
6
+ import { toISO6391 } from '../tts/utils.js';
7
+ import { sortBy } from '../../../src/BookReader/utils.js';
8
+ import { TextSelectionManager } from '../../../src/util/TextSelectionManager.js';
9
+ import '@internetarchive/ia-activity-indicator/ia-activity-indicator.js';
6
10
 
7
11
  // @ts-ignore
8
12
  const BookReader = /** @type {typeof import('@/src/BookReader.js').default} */(window.BookReader);
@@ -11,6 +15,8 @@ export class TranslatePlugin extends BookReaderPlugin {
11
15
 
12
16
  options = {
13
17
  enabled: true,
18
+
19
+ /** @type {string | import('lit').TemplateResult} */
14
20
  panelDisclaimerText: "Translations are in alpha",
15
21
  }
16
22
 
@@ -27,10 +33,10 @@ export class TranslatePlugin extends BookReaderPlugin {
27
33
  toLanguages = [];
28
34
 
29
35
  /**
30
- * Current language code that is being translated From
36
+ * Current language code that is being translated From. Defaults to EN currently
31
37
  * @type {!string}
32
38
  */
33
- langFromCode = "en";
39
+ langFromCode;
34
40
 
35
41
  /**
36
42
  * Current language code that is being translated To
@@ -43,7 +49,22 @@ export class TranslatePlugin extends BookReaderPlugin {
43
49
  */
44
50
  _panel;
45
51
 
52
+ /**
53
+ * @type {boolean} userToggleTranslate - Checks if user has initiated translation
54
+ * Should synchronize with the state of TranslationManager's active state
55
+ */
56
+ userToggleTranslate;
57
+
58
+ /**
59
+ * @type {boolean} loadingModel - Shows loading animation while downloading lang model
60
+ */
61
+ loadingModel = true;
62
+
63
+
46
64
  async init() {
65
+ const currentLanguage = toISO6391(this.br.options.bookLanguage.replace(/[.,/#!$%^&*;:{}=\-_`~()]/g, ""));
66
+ this.langFromCode = currentLanguage ?? "en";
67
+
47
68
  if (!this.options.enabled) {
48
69
  return;
49
70
  }
@@ -84,10 +105,8 @@ export class TranslatePlugin extends BookReaderPlugin {
84
105
  await this.translationManager.initWorker();
85
106
  // Note await above lets _render function properly, since it gives the browser
86
107
  // time to render the rest of bookreader, which _render depends on
87
- this._render();
88
-
89
108
  this.langToCode = this.translationManager.toLanguages[0].code;
90
-
109
+ this._render();
91
110
  }
92
111
 
93
112
  /** @param {HTMLElement} page*/
@@ -114,7 +133,8 @@ export class TranslatePlugin extends BookReaderPlugin {
114
133
 
115
134
  /** @param {HTMLElement} page */
116
135
  async translateRenderedLayer(page, priority) {
117
- if (this.br.mode == this.br.constModeThumb) {
136
+ // Do not run translation if in thumbnail mode or if user did not initiate transations
137
+ if (this.br.mode == this.br.constModeThumb || !this.userToggleTranslate || this.langFromCode == this.langToCode) {
118
138
  return;
119
139
  }
120
140
 
@@ -123,7 +143,7 @@ export class TranslatePlugin extends BookReaderPlugin {
123
143
  let pageTranslationLayer;
124
144
  if (!page.querySelector('.BRPageLayer.BRtranslateLayer')) {
125
145
  pageTranslationLayer = document.createElement('div');
126
- pageTranslationLayer.classList.add('BRPageLayer', 'BRtranslateLayer');
146
+ pageTranslationLayer.classList.add('BRPageLayer', 'BRtranslateLayer', 'BRtranslateLayerLoading');
127
147
  pageTranslationLayer.setAttribute('lang', `${this.langToCode}`);
128
148
  page.prepend(pageTranslationLayer);
129
149
  } else {
@@ -141,7 +161,7 @@ export class TranslatePlugin extends BookReaderPlugin {
141
161
  });
142
162
  const paragraphs = this.getParagraphsOnPage(page);
143
163
 
144
- paragraphs.forEach(async (paragraph, pidx) => {
164
+ const paragraphTranslationPromises = paragraphs.map(async (paragraph, pidx) => {
145
165
  let translatedParagraph = page.querySelector(`[data-translate-index='${pageIndex}-${pidx}']`);
146
166
  if (!translatedParagraph) {
147
167
  translatedParagraph = document.createElement('p');
@@ -149,6 +169,11 @@ export class TranslatePlugin extends BookReaderPlugin {
149
169
  translatedParagraph.setAttribute('data-translate-index', `${pageIndex}-${pidx}`);
150
170
  translatedParagraph.className = 'BRparagraphElement';
151
171
  const originalParagraphStyle = paragraphs[pidx];
172
+ // check text selection paragraphs for header/footer roles
173
+ if (paragraph.classList.contains('ocr-role-header-footer')) {
174
+ translatedParagraph.ariaHidden = "true";
175
+ translatedParagraph.classList.add('ocr-role-header-footer');
176
+ }
152
177
  const fontSize = `${parseInt($(originalParagraphStyle).css("font-size"))}px`;
153
178
 
154
179
  $(translatedParagraph).css({
@@ -159,28 +184,15 @@ export class TranslatePlugin extends BookReaderPlugin {
159
184
  "width": $(originalParagraphStyle).css("width"),
160
185
  "font-size": fontSize,
161
186
  });
162
-
163
- // Note: We'll likely want to switch to using the same logic as
164
- // TextSelectionPlugin's selection, which allows for e.g. click-to-flip
165
- // to work simultaneously with text selection.
166
- translatedParagraph.addEventListener('mousedown', (e) => {
167
- e.stopPropagation();
168
- e.stopImmediatePropagation();
169
- });
170
-
171
- translatedParagraph.addEventListener('mouseup', (e) => {
172
- e.stopPropagation();
173
- e.stopImmediatePropagation();
174
- });
175
-
176
- translatedParagraph.addEventListener('dragstart', (e) =>{
177
- e.preventDefault();
178
- });
179
187
  pageTranslationLayer.append(translatedParagraph);
180
188
  }
181
189
 
182
190
  if (paragraph.textContent.length !== 0) {
183
191
  const pagePriority = parseFloat(pageIndex) + priority + pidx;
192
+ this.translationManager.getTranslationModel(this.langFromCode, this.langToCode).then(() => {
193
+ this._panel.loadingModel = false;
194
+ this.loadingModel = false;
195
+ });
184
196
  const translatedText = await this.translationManager.getTranslation(this.langFromCode, this.langToCode, pageIndex, pidx, paragraph.textContent, pagePriority);
185
197
  // prevent duplicate spans from appearing if exists
186
198
  translatedParagraph.firstElementChild?.remove();
@@ -199,10 +211,39 @@ export class TranslatePlugin extends BookReaderPlugin {
199
211
  }
200
212
  }
201
213
  });
214
+
215
+ this.textSelectionManager?.stopPageFlip(this.br.refs.$brContainer);
216
+ await Promise.all(paragraphTranslationPromises);
217
+ this.br.trigger('translateLayerRendered', {
218
+ leafIndex: pageIndex,
219
+ translateLayer: pageTranslationLayer,
220
+ });
221
+ }
222
+
223
+ /**
224
+ * Get the translation layers for a specific leaf index.
225
+ * @param {number} leafIndex
226
+ * @returns {Promise<HTMLElement[]>}
227
+ */
228
+ async getTranslateLayers(leafIndex) {
229
+ const pageContainerElements = this.br.getActivePageContainerElementsForIndex(leafIndex);
230
+ const translateLayer = $(pageContainerElements).filter(`[data-index='${leafIndex}']`).find('.BRtranslateLayer');
231
+ if (translateLayer.length) return translateLayer.toArray();
232
+
233
+ return new Promise((res, rej) => {
234
+ const handler = async (_, extraParams) => {
235
+ if (extraParams.leafIndex == leafIndex) {
236
+ this.br.off('translateLayerRendered', handler); // remember to detach translateLayer
237
+ res([extraParams.translateLayer]);
238
+ }
239
+ };
240
+ this.br.on('translateLayerRendered', handler);
241
+ });
202
242
  }
203
243
 
204
244
  clearAllTranslations() {
205
245
  document.querySelectorAll('.BRtranslateLayer').forEach(el => el.remove());
246
+ document.querySelectorAll('.showingTranslation').forEach(el => el.classList.remove('showingTranslation'));
206
247
  }
207
248
 
208
249
  /**
@@ -247,16 +288,21 @@ export class TranslatePlugin extends BookReaderPlugin {
247
288
 
248
289
  // Update the from language
249
290
  this.langFromCode = selectedLangFrom;
291
+ this._panel.requestUpdate();
250
292
 
251
293
  // Add 'From' language to 'To' list if not already present
252
294
  if (!this.translationManager.toLanguages.some(lang => lang.code === selectedLangFrom)) {
253
- this.translationManager.toLanguages.push({ code: selectedLangFrom, name: langs[selectedLangFrom] });
295
+ this.translationManager.toLanguages.push({
296
+ code: selectedLangFrom,
297
+ name: this.translationManager.fromLanguages.find((entry) => entry.code == selectedLangFrom).name,
298
+ });
254
299
  }
255
300
 
256
301
  // Update the 'To' languages list and set the correct 'To' language
257
302
  this._panel.toLanguages = this.translationManager.toLanguages;
258
303
 
259
304
  console.log(this.langFromCode, this.langToCode);
305
+ this._render();
260
306
  if (this.langFromCode !== this.langToCode) {
261
307
  this.translateActivePageContainerElements();
262
308
  }
@@ -265,12 +311,32 @@ export class TranslatePlugin extends BookReaderPlugin {
265
311
  handleToLangChange = async (e) => {
266
312
  this.clearAllTranslations();
267
313
  this.langToCode = e.detail.value;
314
+ this._render();
268
315
  this.translateActivePageContainerElements();
269
316
  }
270
317
 
318
+ handleToggleTranslation = async () => {
319
+ this.userToggleTranslate = !this.userToggleTranslate;
320
+ this.translationManager.active = this.userToggleTranslate;
321
+ // Init textSelectionManager only after the translation is active
322
+ if (!this.textSelectionManager) {
323
+ this.textSelectionManager = new TextSelectionManager('.BRtranslateLayer', this.br, {selectionElement: [".BRlineElement"]}, 1);
324
+ this.textSelectionManager.init();
325
+ }
326
+ this._render();
327
+ if (!this.userToggleTranslate) {
328
+ this.clearAllTranslations();
329
+ this.br.trigger('translationDisabled', { });
330
+ this.textSelectionManager.detach();
331
+ } else {
332
+ this.br.trigger('translationEnabled', { });
333
+ this.translateActivePageContainerElements();
334
+ this.textSelectionManager.attach();
335
+ }
336
+ }
271
337
 
272
338
  /**
273
- * Update the table of contents based on array of TOC entries.
339
+ * Update translation side menu
274
340
  */
275
341
  _render() {
276
342
  this.br.shell.menuProviders['translate'] = {
@@ -282,13 +348,22 @@ export class TranslatePlugin extends BookReaderPlugin {
282
348
  this._panel = e.target;
283
349
  this._panel.fromLanguages = this.translationManager.fromLanguages;
284
350
  this._panel.toLanguages = this.translationManager.toLanguages;
351
+ this._panel.userTranslationActive = this.userToggleTranslate;
352
+ this._panel.detectedToLang = this.langToCode;
353
+ this._panel.detectedFromLang = this.langFromCode;
354
+ this._panel.loadingModel = this.loadingModel;
285
355
  }
286
356
  }"
287
357
  @langFromChanged="${this.handleFromLangChange}"
288
358
  @langToChanged="${this.handleToLangChange}"
359
+ @toggleTranslation="${this.handleToggleTranslation}"
289
360
  .fromLanguages="${this.translationManager.fromLanguages}"
290
361
  .toLanguages="${this.translationManager.toLanguages}"
291
362
  .disclaimerMessage="${this.options.panelDisclaimerText}"
363
+ .userTranslationActive=${this.userToggleTranslate}
364
+ .detectedFromLang=${this.langFromCode}
365
+ .detectedToLang=${this.langToCode}
366
+ .loadingModel=${this.loadingModel}
292
367
  class="translate-panel"
293
368
  />`,
294
369
  };
@@ -303,6 +378,10 @@ export class BrTranslatePanel extends LitElement {
303
378
  @property({ type: Array }) toLanguages = []; // List of obj {code, name}
304
379
  @property({ type: String }) prevSelectedLang = ''; // Tracks the previous selected language for the "To" dropdown
305
380
  @property({ type: String }) disclaimerMessage = '';
381
+ @property({ type: Boolean }) userTranslationActive = false;
382
+ @property({ type: String }) detectedFromLang = '';
383
+ @property({ type: String }) detectedToLang = '';
384
+ @property({ type: Boolean }) loadingModel;
306
385
 
307
386
  /** @override */
308
387
  createRenderRoot() {
@@ -316,42 +395,60 @@ export class BrTranslatePanel extends LitElement {
316
395
  }
317
396
 
318
397
  render() {
319
- const showPrevLangButton =
320
- this.prevSelectedLang &&
321
- (this.prevSelectedLang !== this._getSelectedLang('to') || this._getSelectedLang('from') === this._getSelectedLang('to'));
322
-
323
- return html`<div class="app">
324
- <div class="panel panel--from">
398
+ return html`<div class="app" style="margin-top: 5%;padding-right: 5px;">
399
+ <div
400
+ class="disclaimer"
401
+ id="disclaimerMessage"
402
+ 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);"
403
+ >${this.disclaimerMessage}</div>
404
+
405
+ <div class="panel panel--to" style="padding: 0 10px;">
325
406
  <label>
326
- From
327
- <select id="lang-from" name="from" class="lang-select" @change="${this._onLangFromChange}">
328
- ${this.fromLanguages.map(
329
- lang => html`<option value="${lang.code}">${lang.name}</option>`,
330
- )}
407
+ <span style="font-size: 12px;color: #ccc;">Translate To</span>
408
+ <select id="lang-to" name="to" class="lang-select" style="display:block; width:100%;" @change="${this._onLangToChange}">
409
+ ${sortBy(this.toLanguages, ((lang) => lang.name.toLowerCase()
410
+ )).map((lang) => {
411
+ return html`<option value="${lang.code}"
412
+ ?selected=${lang.code == this.detectedToLang}
413
+ > ${lang.name ? lang.name : lang.code} </option>`;
414
+ })
415
+ }
331
416
  </select>
332
417
  </label>
333
418
  </div>
334
- <div class="panel panel--to">
335
- <label>
336
- To
337
- <select id="lang-to" name="to" class="lang-select" @change="${this._onLangToChange}">
338
- ${this.toLanguages.map(
339
- lang => html`<option value="${lang.code}">${lang.name}</option>`,
340
- )}
419
+
420
+ <div class="panel panel--start" style="text-align: right;padding: 0 10px;/*! font-size: 18px; */margin-top: 10px;">
421
+ <button class="start-translation-brn" @click="${this._toggleTranslation}">
422
+ ${this.userTranslationActive ? "Stop Translating" : "Translate"}
423
+ </button>
424
+ </div>
425
+
426
+ <div class="panel panel--from" style="font-size: 12px;color: #ccc;text-align: center;padding: 8px 10px;">
427
+ <details style="display: contents">
428
+ <summary style="text-decoration: underline white; cursor:pointer; display:inline-block">
429
+ <i>
430
+ Source: ${this._getLangName(this.detectedFromLang)} ${this.prevSelectedLang ? "" : "(detected)"}
431
+ </i> Change
432
+ </summary>
433
+ <select id="lang-from" name="from" class="lang-select" value=${this.detectedFromLang} @change="${this._onLangFromChange}" style="width:65%; margin-top: 3%; margin-bottom: 3%">
434
+ ${sortBy(this.fromLanguages, ((lang) => lang.name.toLowerCase()
435
+ )).map((lang) => {
436
+ return html`<option value="${lang.code}"
437
+ ?selected=${lang.code == this.detectedFromLang}
438
+ >${lang.name ? lang.name : lang.code} </option>`;
439
+ })
440
+ }
341
441
  </select>
342
- </label>
343
- ${showPrevLangButton
344
- ? html`<button class="prev-lang-btn" @click="${this._onPrevLangClick}">
345
- ${this._getLangName(this.prevSelectedLang)}
346
- </button>`
347
- : ''}
442
+ </details>
443
+ <div class="footer" id="status" style="margin-top:5%">
444
+ ${this._statusWarning()}
445
+ </div>
446
+
447
+ <div class="lang-models-loading">
448
+ ${this._languageModelStatus()}
348
449
  </div>
349
- <div class="footer" id="status"></div>
350
- <br/>
351
- <div class="disclaimer" id="disclaimerMessage"> ${this.disclaimerMessage} </div>
352
450
  </div>`;
353
451
  }
354
-
355
452
  _onLangFromChange(event) {
356
453
  const langFromChangedEvent = new CustomEvent('langFromChanged', {
357
454
  detail: { value: event.target.value },
@@ -364,6 +461,8 @@ export class BrTranslatePanel extends LitElement {
364
461
  if (this._getSelectedLang('to') !== this._getSelectedLang('from')) {
365
462
  this.prevSelectedLang = this._getSelectedLang('from');
366
463
  }
464
+ this.loadingModel = true;
465
+ this.detectedFromLang = event.target.value;
367
466
  }
368
467
 
369
468
  _onLangToChange(event) {
@@ -378,30 +477,12 @@ export class BrTranslatePanel extends LitElement {
378
477
  if (this._getSelectedLang('from') !== event.target.value) {
379
478
  this.prevSelectedLang = this._getSelectedLang('from');
380
479
  }
381
- }
382
-
383
- _onPrevLangClick() {
384
- const prevLang = this.prevSelectedLang;
385
- if (prevLang == this._getSelectedLang('from')) {
386
- console.log("_onPrevLangClick: will not change since prevLang is the same as the current 'To' language code");
387
- return;
388
- }
389
- this.prevSelectedLang = this._getSelectedLang('to'); // Update prevSelectedLang to current "To" value
390
- const langToChangedEvent = new CustomEvent('langToChanged', {
391
- detail: { value: prevLang },
392
- bubbles: true,
393
- composed: true,
394
- });
395
- this.dispatchEvent(langToChangedEvent);
396
-
397
- // Update the "To" dropdown to the previous language
398
- const toDropdown = this.querySelector('#lang-to');
399
- if (toDropdown) {
400
- toDropdown.value = prevLang;
401
- }
480
+ this.loadingModel = true;
481
+ this.detectedToLang = event.target.value;
402
482
  }
403
483
 
404
484
  _getSelectedLang(type) {
485
+ /** @type {HTMLSelectElement} */
405
486
  const dropdown = this.querySelector(`#lang-${type}`);
406
487
  return dropdown ? dropdown.value : '';
407
488
  }
@@ -410,5 +491,35 @@ export class BrTranslatePanel extends LitElement {
410
491
  const lang = [...this.fromLanguages, ...this.toLanguages].find(lang => lang.code === code);
411
492
  return lang ? lang.name : '';
412
493
  }
413
- }
414
494
 
495
+ _toggleTranslation(event) {
496
+ const toggleTranslateEvent = new CustomEvent('toggleTranslation', {
497
+ detail: {value: event.target.value},
498
+ bubbles: true,
499
+ composed:true,
500
+ });
501
+ this.userTranslationActive = !this.userTranslationActive;
502
+ this.dispatchEvent(toggleTranslateEvent);
503
+ }
504
+
505
+ // TODO: Hardcoded warning message for now but should add more statuses
506
+ _statusWarning() {
507
+ if (this.detectedFromLang == this.detectedToLang) {
508
+ return "Translate To language is the same as the Source language";
509
+ }
510
+ return "";
511
+ }
512
+
513
+ _languageModelStatus() {
514
+ if (this.userTranslationActive) {
515
+ if (this.loadingModel) {
516
+ return html`
517
+ <ia-activity-indicator mode="processing" style="display:block; width: 40px; height: 40px; margin: 0 auto;"></ia-activity-indicator>
518
+ <p>Downloading language model</p>
519
+ `;
520
+ }
521
+ return html`<p>Language model loaded</p>`;
522
+ }
523
+ return "";
524
+ }
525
+ }
@@ -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})),