forked from akai/readest
This commit is contained in:
@@ -99,7 +99,7 @@ const TTSControl = () => {
|
||||
const view = getView(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
const bookData = getBookData(bookKey);
|
||||
if (!view || !viewSettings || !bookData) return;
|
||||
if (!view || !viewSettings || !bookData || !bookData.book) return;
|
||||
if (bookData.book?.format === 'PDF') {
|
||||
eventDispatcher.dispatch('toast', {
|
||||
message: _('TTS not supported for PDF'),
|
||||
@@ -108,6 +108,7 @@ const TTSControl = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const primaryLang = bookData.book.primaryLanguage;
|
||||
setBookKey(bookKey);
|
||||
|
||||
if (ttsControllerRef.current) {
|
||||
@@ -128,9 +129,13 @@ const TTSControl = () => {
|
||||
await ttsController.initViewTTS();
|
||||
const ssml = view.tts?.from(range);
|
||||
if (ssml) {
|
||||
const lang = parseSSMLLang(ssml) || 'en';
|
||||
setTtsLang(lang);
|
||||
let lang = parseSSMLLang(ssml) || 'en';
|
||||
// We will not trust 'en' language from ssml, as it may be a fallback or hardcoded value
|
||||
if (lang === 'en' && primaryLang && primaryLang !== 'en') {
|
||||
lang = primaryLang.split('-')[0]!;
|
||||
}
|
||||
setIsPlaying(true);
|
||||
setTtsLang(lang);
|
||||
|
||||
ttsController.setLang(lang);
|
||||
ttsController.setRate(viewSettings.ttsRate);
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
formatTitle,
|
||||
formatAuthors,
|
||||
getFilename,
|
||||
getPrimaryLanguage,
|
||||
} from '@/utils/book';
|
||||
import { partialMD5 } from '@/utils/md5';
|
||||
import { BookDoc, DocumentLoader } from '@/libs/document';
|
||||
@@ -198,6 +199,7 @@ export abstract class BaseAppService implements AppService {
|
||||
format,
|
||||
title: formatTitle(loadedBook.metadata.title),
|
||||
author: formatAuthors(loadedBook.metadata.author, loadedBook.metadata.language),
|
||||
primaryLanguage: getPrimaryLanguage(loadedBook.metadata.language),
|
||||
createdAt: existingBook ? existingBook.createdAt : Date.now(),
|
||||
uploadedAt: existingBook ? existingBook.uploadedAt : null,
|
||||
deletedAt: transient ? Date.now() : null,
|
||||
@@ -208,6 +210,7 @@ export abstract class BaseAppService implements AppService {
|
||||
if (existingBook) {
|
||||
existingBook.title = book.title;
|
||||
existingBook.author = book.author;
|
||||
existingBook.primaryLanguage = existingBook.primaryLanguage ?? book.primaryLanguage;
|
||||
}
|
||||
|
||||
if (!(await this.fs.exists(getDir(book), 'Books'))) {
|
||||
|
||||
@@ -588,3 +588,5 @@ export const TRANSLATED_LANGS = {
|
||||
'zh-CN': '简体中文',
|
||||
'zh-TW': '正體中文',
|
||||
};
|
||||
|
||||
export const SUPPORTED_LANGS: Record<string, string> = { ...TRANSLATED_LANGS, zh: '中文' };
|
||||
|
||||
@@ -6,6 +6,7 @@ import { TTSGranularity } from '@/types/view';
|
||||
import { TTSUtils } from './TTSUtils';
|
||||
|
||||
export class EdgeTTSClient implements TTSClient {
|
||||
#primaryLang = 'en';
|
||||
#rate = 1.0;
|
||||
#pitch = 1.0;
|
||||
#voice: TTSVoice | null = null;
|
||||
@@ -50,7 +51,10 @@ export class EdgeTTSClient implements TTSClient {
|
||||
preload = false,
|
||||
): AsyncGenerator<TTSMessageEvent> {
|
||||
const { marks } = parseSSMLMarks(ssml);
|
||||
const lang = parseSSMLLang(ssml) || 'en';
|
||||
let lang = parseSSMLLang(ssml) || 'en';
|
||||
if (lang === 'en' && this.#primaryLang && this.#primaryLang !== 'en') {
|
||||
lang = this.#primaryLang;
|
||||
}
|
||||
let voiceId = 'en-US-AriaNeural';
|
||||
if (!this.#voice || this.#currentVoiceLang !== lang) {
|
||||
const preferredVoiceId = TTSUtils.getPreferredVoice('edge-tts', lang);
|
||||
@@ -207,6 +211,10 @@ export class EdgeTTSClient implements TTSClient {
|
||||
}
|
||||
}
|
||||
|
||||
setPrimaryLang(lang: string) {
|
||||
this.#primaryLang = lang;
|
||||
}
|
||||
|
||||
async setRate(rate: number) {
|
||||
// The Edge TTS API uses rate in [0.5 .. 2.0].
|
||||
this.#rate = rate;
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface TTSClient {
|
||||
pause(): Promise<void>;
|
||||
resume(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
setPrimaryLang(lang: string): void;
|
||||
setRate(rate: number): Promise<void>;
|
||||
setPitch(pitch: number): Promise<void>;
|
||||
setVoice(voice: string): Promise<void>;
|
||||
|
||||
@@ -219,6 +219,12 @@ export class TTSController extends EventTarget {
|
||||
|
||||
async setLang(lang: string) {
|
||||
this.ttsLang = lang;
|
||||
this.setPrimaryLang(lang);
|
||||
}
|
||||
|
||||
async setPrimaryLang(lang: string) {
|
||||
this.ttsEdgeClient.setPrimaryLang(lang);
|
||||
this.ttsWebClient.setPrimaryLang(lang);
|
||||
}
|
||||
|
||||
async setRate(rate: number) {
|
||||
|
||||
@@ -173,6 +173,7 @@ async function* speakWithMarks(
|
||||
}
|
||||
|
||||
export class WebSpeechClient implements TTSClient {
|
||||
#primaryLang = 'en';
|
||||
#rate = 1.0;
|
||||
#pitch = 1.0;
|
||||
#voice: SpeechSynthesisVoice | null = null;
|
||||
@@ -215,7 +216,10 @@ export class WebSpeechClient implements TTSClient {
|
||||
// no need to preload for web speech
|
||||
if (preload) return;
|
||||
|
||||
const lang = parseSSMLLang(ssml) || 'en';
|
||||
let lang = parseSSMLLang(ssml) || 'en';
|
||||
if (lang === 'en' && this.#primaryLang && this.#primaryLang !== 'en') {
|
||||
lang = this.#primaryLang;
|
||||
}
|
||||
if (!this.#voice || this.#currentVoiceLang !== lang) {
|
||||
const preferredVoiceId = TTSUtils.getPreferredVoice('web-speech', lang);
|
||||
const preferredVoice = this.#voices.find((v) => v.voiceURI === preferredVoiceId);
|
||||
@@ -262,6 +266,10 @@ export class WebSpeechClient implements TTSClient {
|
||||
this.#synth.cancel();
|
||||
}
|
||||
|
||||
setPrimaryLang(lang: string) {
|
||||
this.#primaryLang = lang;
|
||||
}
|
||||
|
||||
async setRate(rate: number) {
|
||||
// The Web Speech API uses utterance.rate in [0.1 .. 10],
|
||||
this.#rate = rate;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { updateTocCFI, updateTocID } from '@/utils/toc';
|
||||
import { useSettingsStore } from './settingsStore';
|
||||
import { useBookDataStore } from './bookDataStore';
|
||||
import { useLibraryStore } from './libraryStore';
|
||||
import { getPrimaryLanguage } from '@/utils/book';
|
||||
|
||||
interface ViewState {
|
||||
/* Unique key for each book view */
|
||||
@@ -136,6 +137,9 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
}, {});
|
||||
updateTocCFI(bookDoc, bookDoc.toc, sections);
|
||||
}
|
||||
// Set the book's language for formerly imported books, newly imported books have this field set
|
||||
book.primaryLanguage =
|
||||
book.primaryLanguage ?? getPrimaryLanguage(bookDoc.metadata.language);
|
||||
useBookDataStore.setState((state) => ({
|
||||
booksData: {
|
||||
...state.booksData,
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface Book {
|
||||
|
||||
lastUpdated?: number; // deprecated in favor of updatedAt
|
||||
progress?: [number, number]; // Add progress field: [current, total], 1-based page number
|
||||
primaryLanguage?: string;
|
||||
}
|
||||
|
||||
export interface BookGroupType {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Book, BookConfig, BookProgress, WritingMode } from '@/types/book';
|
||||
import { getUserLang, isContentURI, isValidURL, makeSafeFilename } from './misc';
|
||||
import { getStorageType } from './object';
|
||||
import { getDirFromLanguage } from './rtl';
|
||||
import { SUPPORTED_LANGS } from '@/services/constants';
|
||||
|
||||
export const getDir = (book: Book) => {
|
||||
return `${book.hash}`;
|
||||
@@ -107,11 +108,17 @@ export const formatPublisher = (publisher: string | LanguageMap) => {
|
||||
return typeof publisher === 'string' ? publisher : formatLanguageMap(publisher);
|
||||
};
|
||||
|
||||
export const formatLanguage = (lang: string | string[] | undefined) => {
|
||||
return Array.isArray(lang) ? lang.join(', ') : lang;
|
||||
const langCodeToLangName = (langCode: string) => {
|
||||
return SUPPORTED_LANGS[langCode] || langCode.toUpperCase();
|
||||
};
|
||||
|
||||
export const primaryLanguage = (lang: string | string[] | undefined) => {
|
||||
export const formatLanguage = (lang: string | string[] | undefined): string => {
|
||||
return Array.isArray(lang)
|
||||
? lang.map(langCodeToLangName).join(', ')
|
||||
: langCodeToLangName(lang || '');
|
||||
};
|
||||
|
||||
export const getPrimaryLanguage = (lang: string | string[] | undefined) => {
|
||||
return Array.isArray(lang) ? lang[0] : lang;
|
||||
};
|
||||
|
||||
@@ -159,6 +166,6 @@ export const getBookDirFromWritingMode = (writingMode: WritingMode) => {
|
||||
};
|
||||
|
||||
export const getBookDirFromLanguage = (language: string | string[] | undefined) => {
|
||||
const lang = primaryLanguage(language) || '';
|
||||
const lang = getPrimaryLanguage(language) || '';
|
||||
return getDirFromLanguage(lang);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user