forked from akai/readest
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2251d65de4 | |||
| 463ea39467 | |||
| 87bc842525 | |||
| 2ad67a0503 | |||
| 9f894e1d9e |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.2",
|
||||
"version": "0.9.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.9.3": {
|
||||
"date": "2025-01-11",
|
||||
"notes": [
|
||||
"Hotfix occasionally current speak not stopped when changing voice or rate",
|
||||
"Inherit tts rate when changing tts engine"
|
||||
]
|
||||
},
|
||||
"0.9.2": {
|
||||
"date": "2025-01-10",
|
||||
"notes": [
|
||||
|
||||
@@ -20,6 +20,7 @@ const TTSControl = () => {
|
||||
const [ttsLang, setTtsLang] = useState<string>('en');
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [showIndicator, setShowIndicator] = useState(false);
|
||||
const [showPanel, setShowPanel] = useState(false);
|
||||
const [panelPosition, setPanelPosition] = useState<Position>();
|
||||
const [trianglePosition, setTrianglePosition] = useState<Position>();
|
||||
@@ -58,6 +59,7 @@ const TTSControl = () => {
|
||||
ttsControllerRef.current.stop();
|
||||
ttsControllerRef.current = null;
|
||||
}
|
||||
setShowIndicator(true);
|
||||
|
||||
try {
|
||||
const ttsController = new TTSController(view);
|
||||
@@ -95,16 +97,16 @@ const TTSControl = () => {
|
||||
if (!ttsController) return;
|
||||
|
||||
if (isPlaying) {
|
||||
ttsController.pause();
|
||||
await ttsController.pause();
|
||||
setIsPlaying(false);
|
||||
setIsPaused(true);
|
||||
} else if (isPaused) {
|
||||
// start for forward/backward/setvoice-paused
|
||||
// set rate don't pause the tts
|
||||
if (ttsController.state === 'paused') {
|
||||
ttsController.resume();
|
||||
await ttsController.resume();
|
||||
} else {
|
||||
ttsController.start();
|
||||
await ttsController.start();
|
||||
}
|
||||
setIsPlaying(true);
|
||||
setIsPaused(false);
|
||||
@@ -128,10 +130,11 @@ const TTSControl = () => {
|
||||
const handleStop = async () => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (ttsController) {
|
||||
ttsController.stop();
|
||||
await ttsController.stop();
|
||||
ttsControllerRef.current = null;
|
||||
setIsPlaying(false);
|
||||
setShowPanel(false);
|
||||
setShowIndicator(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -140,11 +143,11 @@ const TTSControl = () => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (ttsController) {
|
||||
if (ttsController.state === 'playing') {
|
||||
ttsController.pause();
|
||||
ttsController.setRate(rate);
|
||||
ttsController.start();
|
||||
await ttsController.pause();
|
||||
await ttsController.setRate(rate);
|
||||
await ttsController.start();
|
||||
} else {
|
||||
ttsController.setRate(rate);
|
||||
await ttsController.setRate(rate);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -153,11 +156,11 @@ const TTSControl = () => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (ttsController) {
|
||||
if (ttsController.state === 'playing') {
|
||||
ttsController.pause();
|
||||
ttsController.setVoice(voice);
|
||||
ttsController.start();
|
||||
await ttsController.pause();
|
||||
await ttsController.setVoice(voice);
|
||||
await ttsController.start();
|
||||
} else {
|
||||
ttsController.setVoice(voice);
|
||||
await ttsController.setVoice(voice);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -219,7 +222,7 @@ const TTSControl = () => {
|
||||
onContextMenu={handleDismissPopup}
|
||||
/>
|
||||
)}
|
||||
{ttsControllerRef.current && (
|
||||
{showIndicator && (
|
||||
<div ref={iconRef} className='absolute bottom-12 right-6 h-12 w-12'>
|
||||
<TTSIcon isPlaying={isPlaying} onClick={togglePopup} />
|
||||
</div>
|
||||
|
||||
@@ -6,12 +6,74 @@ const EDGE_SPEECH_URL =
|
||||
'wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1';
|
||||
const EDGE_API_TOKEN = '6A5AA1D4EAFF4E9FB37E23D68491D6F4';
|
||||
const EDGE_TTS_VOICES = {
|
||||
'af-ZA': ['af-ZA-AdriNeural', 'af-ZA-WillemNeural'],
|
||||
'am-ET': ['am-ET-AmehaNeural', 'am-ET-MekdesNeural'],
|
||||
'ar-AE': ['ar-AE-FatimaNeural', 'ar-AE-HamdanNeural'],
|
||||
'ar-BH': ['ar-BH-AliNeural', 'ar-BH-LailaNeural'],
|
||||
'ar-DZ': ['ar-DZ-AminaNeural', 'ar-DZ-IsmaelNeural'],
|
||||
'ar-EG': ['ar-EG-SalmaNeural', 'ar-EG-ShakirNeural'],
|
||||
'ar-IQ': ['ar-IQ-BasselNeural', 'ar-IQ-RanaNeural'],
|
||||
'ar-JO': ['ar-JO-SanaNeural', 'ar-JO-TaimNeural'],
|
||||
'ar-KW': ['ar-KW-FahedNeural', 'ar-KW-NouraNeural'],
|
||||
'ar-LB': ['ar-LB-LaylaNeural', 'ar-LB-RamiNeural'],
|
||||
'ar-LY': ['ar-LY-ImanNeural', 'ar-LY-OmarNeural'],
|
||||
'ar-MA': ['ar-MA-JamalNeural', 'ar-MA-MounaNeural'],
|
||||
'ar-OM': ['ar-OM-AbdullahNeural', 'ar-OM-AyshaNeural'],
|
||||
'ar-QA': ['ar-QA-AmalNeural', 'ar-QA-MoazNeural'],
|
||||
'ar-SA': ['ar-SA-HamedNeural', 'ar-SA-ZariyahNeural'],
|
||||
'de-DE': ['de-DE-AmalaNeural', 'de-DE-ConradNeural', 'de-DE-KatjaNeural', 'de-DE-KillianNeural'],
|
||||
'ar-SY': ['ar-SY-AmanyNeural', 'ar-SY-LaithNeural'],
|
||||
'ar-TN': ['ar-TN-HediNeural', 'ar-TN-ReemNeural'],
|
||||
'ar-YE': ['ar-YE-MaryamNeural', 'ar-YE-SalehNeural'],
|
||||
'az-AZ': ['az-AZ-BabekNeural', 'az-AZ-BanuNeural'],
|
||||
'bg-BG': ['bg-BG-BorislavNeural', 'bg-BG-KalinaNeural'],
|
||||
'bn-BD': ['bn-BD-NabanitaNeural', 'bn-BD-PradeepNeural'],
|
||||
'bn-IN': ['bn-IN-BashkarNeural', 'bn-IN-TanishaaNeural'],
|
||||
'bs-BA': ['bs-BA-GoranNeural', 'bs-BA-VesnaNeural'],
|
||||
'ca-ES': ['ca-ES-EnricNeural', 'ca-ES-JoanaNeural'],
|
||||
'cs-CZ': ['cs-CZ-AntoninNeural', 'cs-CZ-VlastaNeural'],
|
||||
'cy-GB': ['cy-GB-AledNeural', 'cy-GB-NiaNeural'],
|
||||
'da-DK': ['da-DK-ChristelNeural', 'da-DK-JeppeNeural'],
|
||||
'de-AT': ['de-AT-IngridNeural', 'de-AT-JonasNeural'],
|
||||
'de-CH': ['de-CH-JanNeural', 'de-CH-LeniNeural'],
|
||||
'de-DE': [
|
||||
'de-DE-AmalaNeural',
|
||||
'de-DE-ConradNeural',
|
||||
'de-DE-FlorianMultilingualNeural',
|
||||
'de-DE-KatjaNeural',
|
||||
'de-DE-KillianNeural',
|
||||
'de-DE-SeraphinaMultilingualNeural',
|
||||
],
|
||||
'el-GR': ['el-GR-AthinaNeural', 'el-GR-NestorasNeural'],
|
||||
'en-AU': ['en-AU-NatashaNeural', 'en-AU-WilliamNeural'],
|
||||
'en-CA': ['en-CA-ClaraNeural', 'en-CA-LiamNeural'],
|
||||
'en-GB': [
|
||||
'en-GB-LibbyNeural',
|
||||
'en-GB-MaisieNeural',
|
||||
'en-GB-RyanNeural',
|
||||
'en-GB-SoniaNeural',
|
||||
'en-GB-ThomasNeural',
|
||||
],
|
||||
'en-HK': ['en-HK-SamNeural', 'en-HK-YanNeural'],
|
||||
'en-IE': ['en-IE-ConnorNeural', 'en-IE-EmilyNeural'],
|
||||
'en-IN': ['en-IN-NeerjaExpressiveNeural', 'en-IN-NeerjaNeural', 'en-IN-PrabhatNeural'],
|
||||
'en-KE': ['en-KE-AsiliaNeural', 'en-KE-ChilembaNeural'],
|
||||
'en-NG': ['en-NG-AbeoNeural', 'en-NG-EzinneNeural'],
|
||||
'en-NZ': ['en-NZ-MitchellNeural', 'en-NZ-MollyNeural'],
|
||||
'en-PH': ['en-PH-JamesNeural', 'en-PH-RosaNeural'],
|
||||
'en-SG': ['en-SG-LunaNeural', 'en-SG-WayneNeural'],
|
||||
'en-TZ': ['en-TZ-ElimuNeural', 'en-TZ-ImaniNeural'],
|
||||
'en-US': [
|
||||
'en-US-AriaNeural',
|
||||
'en-US-AnaNeural',
|
||||
'en-US-AndrewMultilingualNeural',
|
||||
'en-US-AndrewNeural',
|
||||
'en-US-AriaNeural',
|
||||
'en-US-AvaMultilingualNeural',
|
||||
'en-US-AvaNeural',
|
||||
'en-US-BrianMultilingualNeural',
|
||||
'en-US-BrianNeural',
|
||||
'en-US-ChristopherNeural',
|
||||
'en-US-EmmaMultilingualNeural',
|
||||
'en-US-EmmaNeural',
|
||||
'en-US-EricNeural',
|
||||
'en-US-GuyNeural',
|
||||
'en-US-JennyNeural',
|
||||
@@ -19,23 +81,42 @@ const EDGE_TTS_VOICES = {
|
||||
'en-US-RogerNeural',
|
||||
'en-US-SteffanNeural',
|
||||
],
|
||||
'es-ES': ['es-ES-AlvaroNeural', 'es-ES-ElviraNeural'],
|
||||
'fr-FR': ['fr-FR-DeniseNeural', 'fr-FR-EloiseNeural', 'fr-FR-HenriNeural'],
|
||||
'es-AR': ['es-AR-ElenaNeural', 'es-AR-TomasNeural'],
|
||||
'es-BO': ['es-BO-MarceloNeural', 'es-BO-SofiaNeural'],
|
||||
'es-CL': ['es-CL-CatalinaNeural', 'es-CL-LorenzoNeural'],
|
||||
'es-CO': ['es-CO-GonzaloNeural', 'es-CO-SalomeNeural'],
|
||||
'es-CR': ['es-CR-JuanNeural', 'es-CR-MariaNeural'],
|
||||
'es-CU': ['es-CU-BelkysNeural', 'es-CU-ManuelNeural'],
|
||||
'es-DO': ['es-DO-EmilioNeural', 'es-DO-RamonaNeural'],
|
||||
'es-EC': ['es-EC-AndreaNeural', 'es-EC-LuisNeural'],
|
||||
'es-ES': ['es-ES-AlvaroNeural', 'es-ES-ElviraNeural', 'es-ES-XimenaNeural'],
|
||||
'es-US': ['es-US-AlonsoNeural', 'es-US-PalomaNeural'],
|
||||
'fr-BE': ['fr-BE-CharlineNeural', 'fr-BE-GerardNeural'],
|
||||
'fr-CA': ['fr-CA-AntoineNeural', 'fr-CA-JeanNeural', 'fr-CA-SylvieNeural', 'fr-CA-ThierryNeural'],
|
||||
'fr-CH': ['fr-CH-ArianeNeural', 'fr-CH-FabriceNeural'],
|
||||
'fr-FR': [
|
||||
'fr-FR-DeniseNeural',
|
||||
'fr-FR-EloiseNeural',
|
||||
'fr-FR-HenriNeural',
|
||||
'fr-FR-RemyMultilingualNeural',
|
||||
'fr-FR-VivienneMultilingualNeural',
|
||||
],
|
||||
'ja-JP': ['ja-JP-KeitaNeural', 'ja-JP-NanamiNeural'],
|
||||
'ko-KR': ['ko-KR-InJoonNeural', 'ko-KR-SunHiNeural'],
|
||||
'pt-BR': ['pt-BR-AntonioNeural', 'pt-BR-FranciscaNeural'],
|
||||
'ru-RU': ['ru-RU-DmitryNeural', 'ru-RU-SvetlanaNeural'],
|
||||
'ko-KR': ['ko-KR-HyunsuMultilingualNeural', 'ko-KR-InJoonNeural', 'ko-KR-SunHiNeural'],
|
||||
'pt-BR': ['pt-BR-AntonioNeural', 'pt-BR-FranciscaNeural', 'pt-BR-ThalitaMultilingualNeural'],
|
||||
'pt-PT': ['pt-PT-DuarteNeural', 'pt-PT-RaquelNeural'],
|
||||
'zh-CN': [
|
||||
'zh-CN-XiaoxiaoNeural',
|
||||
'zh-CN-XiaoyiNeural',
|
||||
'zh-CN-YunjianNeural',
|
||||
'zh-CN-liaoning-XiaobeiNeural',
|
||||
'zh-CN-shaanxi-XiaoniNeural',
|
||||
'zh-CN-YunxiNeural',
|
||||
'zh-CN-YunxiaNeural',
|
||||
'zh-CN-YunyangNeural',
|
||||
'zh-CN-liaoning-XiaobeiNeural',
|
||||
'zh-CN-shaanxi-XiaoniNeural',
|
||||
],
|
||||
'zh-TW': ['zh-TW-HsiaoChenNeural', 'zh-TW-YunJheNeural', 'zh-TW-HsiaoYuNeural'],
|
||||
'zh-HK': ['zh-HK-HiuGaaiNeural', 'zh-HK-HiuMaanNeural', 'zh-HK-WanLungNeural'],
|
||||
'zh-TW': ['zh-TW-HsiaoChenNeural', 'zh-TW-HsiaoYuNeural', 'zh-TW-YunJheNeural'],
|
||||
};
|
||||
|
||||
const genVoiceList = (voices: Record<string, string[]>) => {
|
||||
|
||||
@@ -15,7 +15,7 @@ export const SUPPORTED_FILE_EXTS = ['epub', 'mobi', 'azw', 'azw3', 'fb2', 'cbz',
|
||||
export const FILE_ACCEPT_FORMATS = SUPPORTED_FILE_EXTS.map((ext) => `.${ext}`).join(', ');
|
||||
|
||||
export const DEFAULT_READSETTINGS: ReadSettings = {
|
||||
sideBarWidth: '25%',
|
||||
sideBarWidth: '15%',
|
||||
isSideBarPinned: true,
|
||||
notebookWidth: '25%',
|
||||
isNotebookPinned: false,
|
||||
|
||||
@@ -57,7 +57,7 @@ export class EdgeTTSClient implements TTSClient {
|
||||
voiceId = this.#voice.id;
|
||||
}
|
||||
|
||||
this.stopInternal();
|
||||
await this.stopInternal();
|
||||
|
||||
// Preloading for longer ssml
|
||||
if (marks.length > 1) {
|
||||
@@ -117,7 +117,7 @@ export class EdgeTTSClient implements TTSClient {
|
||||
break;
|
||||
}
|
||||
|
||||
this.stopInternal();
|
||||
await this.stopInternal();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,10 +136,10 @@ export class EdgeTTSClient implements TTSClient {
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.stopInternal();
|
||||
await this.stopInternal();
|
||||
}
|
||||
|
||||
private stopInternal() {
|
||||
private async stopInternal() {
|
||||
this.#isPlaying = false;
|
||||
this.#pausedAt = 0;
|
||||
this.#startedAt = 0;
|
||||
@@ -153,7 +153,7 @@ export class EdgeTTSClient implements TTSClient {
|
||||
this.#sourceNode = null;
|
||||
}
|
||||
if (this.#audioContext) {
|
||||
this.#audioContext.close();
|
||||
await this.#audioContext.close();
|
||||
this.#audioContext = null;
|
||||
}
|
||||
this.#audioBuffer = null;
|
||||
|
||||
@@ -16,6 +16,7 @@ export class TTSController extends EventTarget {
|
||||
view: FoliateView;
|
||||
#nossmlCnt: number = 0;
|
||||
|
||||
ttsRate: number = 1.0;
|
||||
ttsClient: TTSClient;
|
||||
ttsWebClient: TTSClient;
|
||||
ttsEdgeClient: TTSClient;
|
||||
@@ -61,7 +62,7 @@ export class TTSController extends EventTarget {
|
||||
// FIXME: in case we are at the end of the book, need a better way to handle this
|
||||
if (this.#nossmlCnt < 10 && this.state === 'playing') {
|
||||
await this.view.next(1);
|
||||
this.forward();
|
||||
await this.forward();
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
@@ -78,7 +79,7 @@ export class TTSController extends EventTarget {
|
||||
}
|
||||
|
||||
if (lastCode === 'end' && this.state === 'playing') {
|
||||
this.forward();
|
||||
await this.forward();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,26 +104,26 @@ export class TTSController extends EventTarget {
|
||||
return this.#speak(resumeOrStart);
|
||||
}
|
||||
|
||||
pause() {
|
||||
async pause() {
|
||||
this.state = 'paused';
|
||||
this.ttsClient.pause().catch((e) => this.error(e));
|
||||
await this.ttsClient.pause().catch((e) => this.error(e));
|
||||
}
|
||||
|
||||
resume() {
|
||||
async resume() {
|
||||
this.state = 'playing';
|
||||
this.ttsClient.resume().catch((e) => this.error(e));
|
||||
await this.ttsClient.resume().catch((e) => this.error(e));
|
||||
}
|
||||
|
||||
stop() {
|
||||
async stop() {
|
||||
this.state = 'stopped';
|
||||
this.ttsClient.stop().catch((e) => this.error(e));
|
||||
await this.ttsClient.stop().catch((e) => this.error(e));
|
||||
}
|
||||
|
||||
// goto previous sentence
|
||||
async backward() {
|
||||
this.initViewTTS();
|
||||
await this.initViewTTS();
|
||||
if (this.state === 'playing') {
|
||||
this.stop();
|
||||
await this.stop();
|
||||
this.#speak(this.view.tts?.prev());
|
||||
} else {
|
||||
this.state = 'backward-paused';
|
||||
@@ -134,7 +135,7 @@ export class TTSController extends EventTarget {
|
||||
async forward() {
|
||||
await this.initViewTTS();
|
||||
if (this.state === 'playing') {
|
||||
this.stop();
|
||||
await this.stop();
|
||||
this.#speak(this.view.tts?.next());
|
||||
} else {
|
||||
this.state = 'forward-paused';
|
||||
@@ -143,7 +144,8 @@ export class TTSController extends EventTarget {
|
||||
}
|
||||
|
||||
async setRate(rate: number) {
|
||||
this.ttsClient.setRate(rate);
|
||||
this.ttsRate = rate;
|
||||
await this.ttsClient.setRate(this.ttsRate);
|
||||
}
|
||||
|
||||
async getVoices(lang: string) {
|
||||
@@ -154,11 +156,16 @@ export class TTSController extends EventTarget {
|
||||
|
||||
async setVoice(voiceId: string) {
|
||||
this.state = 'setvoice-paused';
|
||||
this.ttsClient.stop();
|
||||
if (this.ttsEdgeVoices.find((voice) => voice.id === voiceId && !voice.disabled)) {
|
||||
await this.ttsClient.stop();
|
||||
const useEdgeTTS = !!this.ttsEdgeVoices.find(
|
||||
(voice) => (voiceId === '' || voice.id === voiceId) && !voice.disabled,
|
||||
);
|
||||
if (useEdgeTTS) {
|
||||
this.ttsClient = this.ttsEdgeClient;
|
||||
await this.ttsClient.setRate(this.ttsRate);
|
||||
} else {
|
||||
this.ttsClient = this.ttsWebClient;
|
||||
await this.ttsClient.setRate(this.ttsRate);
|
||||
}
|
||||
await this.ttsClient.setVoice(voiceId);
|
||||
}
|
||||
@@ -172,8 +179,8 @@ export class TTSController extends EventTarget {
|
||||
this.state = 'stopped';
|
||||
}
|
||||
|
||||
kill() {
|
||||
async kill() {
|
||||
this.state = 'stopped';
|
||||
this.ttsClient.stop();
|
||||
await this.ttsClient.stop();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: 3bdbd3d015...b9347d80e8
Reference in New Issue
Block a user