fix(tts): resolve Edge TTS being blocked in certain regions (#2741)

This should close #2739 and close #1821.
This commit is contained in:
Huang Xin
2025-12-19 10:24:51 +08:00
committed by GitHub
parent 54fdf5f1fd
commit 8a43c58fd4
39 changed files with 520 additions and 143 deletions
Generated
+83 -2
View File
@@ -48,6 +48,7 @@ dependencies = [
"tauri-plugin-sign-in-with-apple",
"tauri-plugin-single-instance",
"tauri-plugin-updater",
"tauri-plugin-websocket",
"tauri-plugin-window-state",
"thiserror 2.0.17",
"tokio",
@@ -1126,6 +1127,12 @@ dependencies = [
"syn 2.0.111",
]
[[package]]
name = "data-encoding"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
name = "data-url"
version = "0.3.2"
@@ -2196,7 +2203,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
"webpki-roots 1.0.4",
]
[[package]]
@@ -4354,7 +4361,7 @@ dependencies = [
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"webpki-roots",
"webpki-roots 1.0.4",
]
[[package]]
@@ -4886,6 +4893,17 @@ dependencies = [
"stable_deref_trait",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha2"
version = "0.10.9"
@@ -5736,6 +5754,25 @@ dependencies = [
"zip",
]
[[package]]
name = "tauri-plugin-websocket"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38a350aadc270a89379acfb37c649c9cbde72e303f00a4f22e3b0e5b8637de9f"
dependencies = [
"futures-util",
"http",
"log",
"rand 0.9.2",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.17",
"tokio",
"tokio-tungstenite",
]
[[package]]
name = "tauri-plugin-window-state"
version = "2.4.1"
@@ -6032,6 +6069,22 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-tungstenite"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857"
dependencies = [
"futures-util",
"log",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tungstenite",
"webpki-roots 0.26.11",
]
[[package]]
name = "tokio-util"
version = "0.7.17"
@@ -6245,6 +6298,25 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tungstenite"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442"
dependencies = [
"bytes",
"data-encoding",
"http",
"httparse",
"log",
"rand 0.9.2",
"rustls",
"rustls-pki-types",
"sha1",
"thiserror 2.0.17",
"utf-8",
]
[[package]]
name = "typeid"
version = "1.0.3"
@@ -6697,6 +6769,15 @@ dependencies = [
"system-deps",
]
[[package]]
name = "webpki-roots"
version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
dependencies = [
"webpki-roots 1.0.4",
]
[[package]]
name = "webpki-roots"
version = "1.0.4"
+4
View File
@@ -74,6 +74,7 @@
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-shell": "~2.3.3",
"@tauri-apps/plugin-updater": "^2.9.0",
"@tauri-apps/plugin-websocket": "~2.4.1",
"@zip.js/zip.js": "^2.7.53",
"abortcontroller-polyfill": "^1.7.8",
"app-store-server-api": "^0.17.1",
@@ -92,6 +93,7 @@
"i18next-http-backend": "^3.0.1",
"iso-639-2": "^3.0.2",
"iso-639-3": "^3.0.1",
"isomorphic-ws": "^5.0.0",
"js-md5": "^0.8.3",
"jwt-decode": "^4.0.0",
"marked": "^15.0.12",
@@ -112,6 +114,7 @@
"styled-jsx": "^5.1.7",
"tinycolor2": "^1.6.0",
"uuid": "^11.1.0",
"ws": "^8.18.3",
"zod": "^4.0.8",
"zustand": "5.0.6"
},
@@ -131,6 +134,7 @@
"@types/semver": "^7.7.0",
"@types/tinycolor2": "^1.4.6",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.48.0",
"@typescript-eslint/parser": "^8.48.0",
"@vitejs/plugin-react": "^5.1.1",
@@ -715,9 +715,7 @@
"Validating...": "جارٍ التحقق...",
"View All": "عرض الكل",
"Forward": "إلى الأمام",
"OPDS Catalog": "كتالوج OPDS",
"Home": "الصفحة الرئيسية",
"Library": "المكتبة",
"{{count}} items_zero": "{{count}} عناصر",
"{{count}} items_one": "{{count}} عنصر",
"{{count}} items_two": "{{count}} عنصران",
@@ -805,5 +803,7 @@
"Successfully imported {{count}} book(s)_many": "تم استيراد {{count}} كتابًا",
"Successfully imported {{count}} book(s)_other": "تم استيراد {{count}} كتاب",
"Count": "العدد",
"Start Page": "الصفحة الأولى"
"Start Page": "الصفحة الأولى",
"Search in OPDS Catalog...": "البحث في كتالوج OPDS...",
"Please log in to use advanced TTS features.": "يرجى تسجيل الدخول لاستخدام ميزات تحويل النص إلى كلام المتقدمة."
}
@@ -699,9 +699,7 @@
"Validating...": "যাচাই করা হচ্ছে...",
"View All": "সব দেখুন",
"Forward": "ফরোয়ার্ড",
"OPDS Catalog": "OPDS ক্যাটালগ",
"Home": "হোম",
"Library": "লাইব্রেরি",
"{{count}} items_one": "{{count}} আইটেম",
"{{count}} items_other": "{{count}} আইটেম",
"Download completed": "ডাউনলোড সম্পন্ন হয়েছে",
@@ -765,5 +763,7 @@
"Successfully imported {{count}} book(s)_one": "সফলভাবে ১টি বই আমদানি করা হয়েছে",
"Successfully imported {{count}} book(s)_other": "সফলভাবে {{count}}টি বই আমদানি করা হয়েছে",
"Count": "গণনা",
"Start Page": "শুরু পৃষ্ঠা"
"Start Page": "শুরু পৃষ্ঠা",
"Search in OPDS Catalog...": "OPDS ক্যাটালগে অনুসন্ধান করুন...",
"Please log in to use advanced TTS features.": "উন্নত TTS বৈশিষ্ট্যগুলি ব্যবহার করতে লগইন করুন।"
}
@@ -695,9 +695,7 @@
"Validating...": "བདེན་སྦྱོར་བཞིན་...",
"View All": "ཡོངས་ལྟ་བ་",
"Forward": "མདུན་དུ་",
"OPDS Catalog": "OPDS དཀར་ཆག",
"Home": "གཙོ་ངོས་",
"Library": "དེབ་མཛོད་",
"{{count}} items_other": "{{count}} རྣམ་གྲངས་",
"Download completed": "ཕབ་ལེན་རྫོགས་སོང་།",
"Download failed": "ཕབ་ལེན་ཕམ་པ་",
@@ -755,5 +753,7 @@
"From Directory": "སྐོར་འདེམས་པ་ནས།",
"Successfully imported {{count}} book(s)_other": "སྤྲིན་ནས་ཕབ་སྟེ་འབེབས་ {{count}} དེབ་འདེམས་སོང་",
"Count": "ཨང་",
"Start Page": "ཤོག་ངོས་དང་པོ"
"Start Page": "ཤོག་ངོས་དང་པོ",
"Search in OPDS Catalog...": "OPDS དཀར་ཆག་ནང་འཚོལ།...",
"Please log in to use advanced TTS features.": "དབང་བསྐྱོད་ཀྱི TTS རྣམ་པ་ཚུགས་སྤྱོད་བྱས་མ་ཐུབ།"
}
@@ -699,9 +699,7 @@
"Validating...": "Wird überprüft...",
"View All": "Alle anzeigen",
"Forward": "Weiter",
"OPDS Catalog": "OPDS-Katalog",
"Home": "Start",
"Library": "Bibliothek",
"{{count}} items_one": "{{count}} Element",
"{{count}} items_other": "{{count}} Elemente",
"Download completed": "Download abgeschlossen",
@@ -765,5 +763,7 @@
"Successfully imported {{count}} book(s)_one": "Erfolgreich 1 Buch importiert",
"Successfully imported {{count}} book(s)_other": "Erfolgreich {{count}} Bücher importiert",
"Count": "Anzahl",
"Start Page": "Startseite"
"Start Page": "Startseite",
"Search in OPDS Catalog...": "Im OPDS-Katalog suchen...",
"Please log in to use advanced TTS features.": "Bitte melde dich an, um erweiterte TTS-Funktionen zu nutzen."
}
@@ -699,9 +699,7 @@
"Validating...": "Γίνεται έλεγχος...",
"View All": "Προβολή όλων",
"Forward": "Μπροστά",
"OPDS Catalog": "Κατάλογος OPDS",
"Home": "Αρχική",
"Library": "Βιβλιοθήκη",
"{{count}} items_one": "{{count}} στοιχείο",
"{{count}} items_other": "{{count}} στοιχεία",
"Download completed": "Η λήψη ολοκληρώθηκε",
@@ -765,5 +763,7 @@
"Successfully imported {{count}} book(s)_one": "Επιτυχής εισαγωγή 1 βιβλίου",
"Successfully imported {{count}} book(s)_other": "Επιτυχής εισαγωγή {{count}} βιβλίων",
"Count": "Πλήθος",
"Start Page": "Αρχική Σελίδα"
"Start Page": "Αρχική Σελίδα",
"Search in OPDS Catalog...": "Αναζήτηση στον κατάλογο OPDS...",
"Please log in to use advanced TTS features.": "Παρακαλώ συνδεθείτε για να χρησιμοποιήσετε προηγμένες λειτουργίες TTS."
}
@@ -703,9 +703,7 @@
"Validating...": "Validando...",
"View All": "Ver todo",
"Forward": "Adelante",
"OPDS Catalog": "Catálogo OPDS",
"Home": "Inicio",
"Library": "Biblioteca",
"{{count}} items_one": "{{count}} elemento",
"{{count}} items_many": "{{count}} elementos",
"{{count}} items_other": "{{count}} elementos",
@@ -775,5 +773,7 @@
"Successfully imported {{count}} book(s)_many": "Se importaron correctamente {{count}} libros",
"Successfully imported {{count}} book(s)_other": "Se importaron correctamente {{count}} libros",
"Count": "Cuenta",
"Start Page": "Página de inicio"
"Start Page": "Página de inicio",
"Search in OPDS Catalog...": "Buscar en el catálogo OPDS...",
"Please log in to use advanced TTS features.": "Por favor, inicie sesión para usar funciones avanzadas de TTS."
}
@@ -699,9 +699,7 @@
"Validating...": "در حال بررسی...",
"View All": "مشاهده همه",
"Forward": "بعدی",
"OPDS Catalog": "فهرست OPDS",
"Home": "خانه",
"Library": "کتابخانه",
"{{count}} items_one": "{{count}} مورد",
"{{count}} items_other": "{{count}} مورد",
"Download completed": "دانلود کامل شد",
@@ -765,5 +763,7 @@
"Successfully imported {{count}} book(s)_one": "با موفقیت 1 کتاب وارد شد",
"Successfully imported {{count}} book(s)_other": "با موفقیت {{count}} کتاب وارد شد",
"Count": "تعداد",
"Start Page": "صفحه شروع"
"Start Page": "صفحه شروع",
"Search in OPDS Catalog...": "جستجو در فهرست OPDS...",
"Please log in to use advanced TTS features.": "لطفاً برای استفاده از ویژگی‌های پیشرفته TTS وارد شوید."
}
@@ -703,9 +703,7 @@
"Validating...": "Validation...",
"View All": "Tout afficher",
"Forward": "Suivant",
"OPDS Catalog": "Catalogue OPDS",
"Home": "Accueil",
"Library": "Bibliothèque",
"{{count}} items_one": "{{count}} élément",
"{{count}} items_many": "{{count}} éléments",
"{{count}} items_other": "{{count}} éléments",
@@ -775,5 +773,7 @@
"Successfully imported {{count}} book(s)_many": "Importation réussie de {{count}} livres",
"Successfully imported {{count}} book(s)_other": "Importation réussie de {{count}} livres",
"Count": "Nombre",
"Start Page": "Page de départ"
"Start Page": "Page de départ",
"Search in OPDS Catalog...": "Rechercher dans le catalogue OPDS...",
"Please log in to use advanced TTS features.": "Veuillez vous connecter pour utiliser les fonctionnalités avancées de TTS."
}
@@ -699,9 +699,7 @@
"Validating...": "मान्य किया जा रहा है...",
"View All": "सभी देखें",
"Forward": "आगे",
"OPDS Catalog": "OPDS कैटलॉग",
"Home": "होम",
"Library": "लाइब्रेरी",
"{{count}} items_one": "{{count}} आइटम",
"{{count}} items_other": "{{count}} आइटम",
"Download completed": "डाउनलोड पूरा हुआ",
@@ -765,5 +763,7 @@
"Successfully imported {{count}} book(s)_one": "सफलतापूर्वक 1 पुस्तक आयात की गई",
"Successfully imported {{count}} book(s)_other": "सफलतापूर्वक {{count}} पुस्तकों का आयात किया गया",
"Count": "गणना",
"Start Page": "प्रारंभ पृष्ठ"
"Start Page": "प्रारंभ पृष्ठ",
"Search in OPDS Catalog...": "OPDS कैटलॉग में खोजें...",
"Please log in to use advanced TTS features.": "उन्नत TTS सुविधाओं का उपयोग करने के लिए कृपया लॉग इन करें।"
}
@@ -695,9 +695,7 @@
"Validating...": "Memvalidasi...",
"View All": "Lihat Semua",
"Forward": "Maju",
"OPDS Catalog": "Katalog OPDS",
"Home": "Beranda",
"Library": "Perpustakaan",
"{{count}} items_other": "{{count}} item",
"Download completed": "Unduhan selesai",
"Download failed": "Unduhan gagal",
@@ -755,5 +753,7 @@
"From Directory": "Dari Direktori",
"Successfully imported {{count}} book(s)_other": "Berhasil mengimpor {{count}} buku",
"Count": "Jumlah",
"Start Page": "Halaman Awal"
"Start Page": "Halaman Awal",
"Search in OPDS Catalog...": "Cari di Katalog OPDS...",
"Please log in to use advanced TTS features.": "Silakan masuk untuk menggunakan fitur TTS lanjutan."
}
@@ -703,9 +703,7 @@
"Validating...": "Convalida in corso...",
"View All": "Vedi tutto",
"Forward": "Avanti",
"OPDS Catalog": "Catalogo OPDS",
"Home": "Home",
"Library": "Libreria",
"{{count}} items_one": "{{count}} elemento",
"{{count}} items_many": "{{count}} elementi",
"{{count}} items_other": "{{count}} elementi",
@@ -775,5 +773,7 @@
"Successfully imported {{count}} book(s)_many": "Importati con successo {{count}} libri",
"Successfully imported {{count}} book(s)_other": "Importati con successo {{count}} libri",
"Count": "Conteggio",
"Start Page": "Pagina iniziale"
"Start Page": "Pagina iniziale",
"Search in OPDS Catalog...": "Cerca nel catalogo OPDS...",
"Please log in to use advanced TTS features.": "Effettua il login per utilizzare le funzionalità TTS avanzate."
}
@@ -695,9 +695,7 @@
"Validating...": "検証中...",
"View All": "すべて表示",
"Forward": "進む",
"OPDS Catalog": "OPDSカタログ",
"Home": "ホーム",
"Library": "ライブラリ",
"{{count}} items_other": "{{count}} 件",
"Download completed": "ダウンロード完了",
"Download failed": "ダウンロード失敗",
@@ -755,5 +753,7 @@
"From Directory": "ディレクトリから",
"Successfully imported {{count}} book(s)_other": "成功裏に{{count}}冊の本をインポートしました",
"Count": "件数",
"Start Page": "開始ページ"
"Start Page": "開始ページ",
"Search in OPDS Catalog...": "OPDSカタログ内を検索...",
"Please log in to use advanced TTS features.": "高度なTTS機能を使用するにはログインしてください。"
}
@@ -695,9 +695,7 @@
"Validating...": "검증 중...",
"View All": "모두 보기",
"Forward": "다음",
"OPDS Catalog": "OPDS 카탈로그",
"Home": "홈",
"Library": "라이브러리",
"{{count}} items_other": "{{count}}개 항목",
"Download completed": "다운로드 완료",
"Download failed": "다운로드 실패",
@@ -755,5 +753,7 @@
"From Directory": "디렉토리에서",
"Successfully imported {{count}} book(s)_other": "성공적으로 {{count}} 권의 책이 가져와졌습니다",
"Count": "개수",
"Start Page": "시작 페이지"
"Start Page": "시작 페이지",
"Search in OPDS Catalog...": "OPDS 카탈로그에서 검색...",
"Please log in to use advanced TTS features.": "고급 TTS 기능을 사용하려면 로그인하세요."
}
@@ -695,9 +695,7 @@
"Validating...": "Mengesahkan...",
"View All": "Lihat Semua",
"Forward": "Maju",
"OPDS Catalog": "Katalog OPDS",
"Home": "Laman Utama",
"Library": "Perpustakaan",
"{{count}} items_other": "{{count}} item",
"Download completed": "Muat turun selesai",
"Download failed": "Muat turun gagal",
@@ -755,5 +753,7 @@
"From Directory": "Dari Direktori",
"Successfully imported {{count}} book(s)_other": "Berjaya mengimport {{count}} buku",
"Count": "Jumlah",
"Start Page": "Halaman Awal"
"Start Page": "Halaman Awal",
"Search in OPDS Catalog...": "Cari dalam Katalog OPDS...",
"Please log in to use advanced TTS features.": "Sila log masuk untuk menggunakan ciri TTS lanjutan."
}
@@ -699,9 +699,7 @@
"Validating...": "Valideren...",
"View All": "Alles bekijken",
"Forward": "Verder",
"OPDS Catalog": "OPDS-catalogus",
"Home": "Startpagina",
"Library": "Bibliotheek",
"{{count}} items_one": "{{count}} item",
"{{count}} items_other": "{{count}} items",
"Download completed": "Download voltooid",
@@ -765,5 +763,7 @@
"Successfully imported {{count}} book(s)_one": "Succesvol 1 boek geïmporteerd",
"Successfully imported {{count}} book(s)_other": "Succesvol {{count}} boeken geïmporteerd",
"Count": "Aantal",
"Start Page": "Startpagina"
"Start Page": "Startpagina",
"Search in OPDS Catalog...": "Zoeken in OPDS-catalogus...",
"Please log in to use advanced TTS features.": "Log in om geavanceerde TTS-functies te gebruiken."
}
@@ -707,9 +707,7 @@
"Validating...": "Weryfikacja...",
"View All": "Pokaż wszystkie",
"Forward": "Dalej",
"OPDS Catalog": "Katalog OPDS",
"Home": "Strona główna",
"Library": "Biblioteka",
"{{count}} items_one": "{{count}} element",
"{{count}} items_few": "{{count}} elementy",
"{{count}} items_many": "{{count}} elementów",
@@ -785,5 +783,7 @@
"Successfully imported {{count}} book(s)_many": "Pomyślnie zaimportowano {{count}} książek",
"Successfully imported {{count}} book(s)_other": "Pomyślnie zaimportowano {{count}} książek",
"Count": "Liczba",
"Start Page": "Strona startowa"
"Start Page": "Strona startowa",
"Search in OPDS Catalog...": "Szukaj w katalogu OPDS...",
"Please log in to use advanced TTS features.": "Zaloguj się, aby korzystać z zaawansowanych funkcji TTS."
}
@@ -703,9 +703,7 @@
"Validating...": "Validando...",
"View All": "Ver todos",
"Forward": "Avançar",
"OPDS Catalog": "Catálogo OPDS",
"Home": "Início",
"Library": "Biblioteca",
"{{count}} items_one": "{{count}} item",
"{{count}} items_many": "{{count}} itens",
"{{count}} items_other": "{{count}} item",
@@ -775,5 +773,7 @@
"Successfully imported {{count}} book(s)_many": "Importados com sucesso {{count}} livros",
"Successfully imported {{count}} book(s)_other": "Importados com sucesso {{count}} livros",
"Count": "Contagem",
"Start Page": "Página Inicial"
"Start Page": "Página Inicial",
"Search in OPDS Catalog...": "Pesquisar no Catálogo OPDS...",
"Please log in to use advanced TTS features.": "Por favor, faça login para usar recursos avançados de TTS."
}
@@ -707,9 +707,7 @@
"Validating...": "Проверка...",
"View All": "Посмотреть все",
"Forward": "Вперёд",
"OPDS Catalog": "Каталог OPDS",
"Home": "Главная",
"Library": "Библиотека",
"{{count}} items_one": "{{count}} элемент",
"{{count}} items_few": "{{count}} элемента",
"{{count}} items_many": "{{count}} элементов",
@@ -785,5 +783,7 @@
"Successfully imported {{count}} book(s)_many": "Успешно импортировано {{count}} книг",
"Successfully imported {{count}} book(s)_other": "Успешно импортировано {{count}} книг",
"Count": "Количество",
"Start Page": "Начальная страница"
"Start Page": "Начальная страница",
"Search in OPDS Catalog...": "Поиск в каталоге OPDS...",
"Please log in to use advanced TTS features.": "Пожалуйста, войдите в систему, чтобы использовать расширенные функции TTS."
}
@@ -699,9 +699,7 @@
"Validating...": "තහවුරු කරමින්...",
"View All": "සියල්ල බලන්න",
"Forward": "ඉදිරියට",
"OPDS Catalog": "OPDS දත්තසමුදා",
"Home": "මුල් පිටුව",
"Library": "පුස්තකාලය",
"{{count}} items_one": "{{count}} අයිතමය",
"{{count}} items_other": "{{count}} අයිතම",
"Download completed": "බාගත කිරීම සම්පූර්ණයි",
@@ -765,5 +763,7 @@
"Successfully imported {{count}} book(s)_one": "සාර්ථකව ආයාත කළ 1 පොත",
"Successfully imported {{count}} book(s)_other": "සාර්ථකව ආයාත කළ {{count}} පොත්",
"Count": "ගණන",
"Start Page": "ආරම්භක පිටුව"
"Start Page": "ආරම්භක පිටුව",
"Search in OPDS Catalog...": "OPDS දත්තසමුදා තුළ සෙවීම...",
"Please log in to use advanced TTS features.": "උසස් TTS විශේෂාංග භාවිතා කිරීමට කරුණාකර පිවිසෙන්න."
}
@@ -699,9 +699,7 @@
"Validating...": "Verifierar...",
"View All": "Visa alla",
"Forward": "Framåt",
"OPDS Catalog": "OPDS-katalog",
"Home": "Start",
"Library": "Bibliotek",
"{{count}} items_one": "{{count}} objekt",
"{{count}} items_other": "{{count}} objekt",
"Download completed": "Nedladdning klar",
@@ -765,5 +763,7 @@
"Successfully imported {{count}} book(s)_one": "Importerat 1 bok",
"Successfully imported {{count}} book(s)_other": "Importerat {{count}} böcker",
"Count": "Antal",
"Start Page": "Start sida"
"Start Page": "Start sida",
"Search in OPDS Catalog...": "Sök i OPDS-katalog...",
"Please log in to use advanced TTS features.": "Logga in för att använda avancerade TTS-funktioner."
}
@@ -699,9 +699,7 @@
"Validating...": "சரிபார்க்கப்படுகிறது...",
"View All": "அனைத்தையும் பார்க்கவும்",
"Forward": "முன்னேற்று",
"OPDS Catalog": "OPDS பட்டியல்",
"Home": "முகப்பு",
"Library": "நூலகம்",
"{{count}} items_one": "{{count}} பொருள்",
"{{count}} items_other": "{{count}} பொருட்கள்",
"Download completed": "பதிவிறக்கம் முடிந்தது",
@@ -765,5 +763,7 @@
"Successfully imported {{count}} book(s)_one": "வெற்றிகரமாக 1 புத்தகம் இறக்குமதி செய்யப்பட்டது",
"Successfully imported {{count}} book(s)_other": "வெற்றிகரமாக {{count}} புத்தகங்கள் இறக்குமதி செய்யப்பட்டது",
"Count": "எண்ணிக்கை",
"Start Page": "தொடக்கப் பக்கம்"
"Start Page": "தொடக்கப் பக்கம்",
"Search in OPDS Catalog...": "OPDS பட்டியலில் தேடவும்...",
"Please log in to use advanced TTS features.": "மேம்பட்ட TTS அம்சங்களை பயன்படுத்த உள்நுழையவும்."
}
@@ -695,9 +695,7 @@
"Validating...": "กำลังตรวจสอบ...",
"View All": "ดูทั้งหมด",
"Forward": "ไปข้างหน้า",
"OPDS Catalog": "แคตตาล็อก OPDS",
"Home": "หน้าแรก",
"Library": "ห้องสมุด",
"{{count}} items_other": "{{count}} รายการ",
"Download completed": "ดาวน์โหลดเสร็จสิ้น",
"Download failed": "ดาวน์โหลดล้มเหลว",
@@ -755,5 +753,7 @@
"From Directory": "จากไดเรกทอรี",
"Successfully imported {{count}} book(s)_other": "นำเข้า {{count}} หนังสือเรียบร้อยแล้ว",
"Count": "นับ",
"Start Page": "หน้าเริ่มต้น"
"Start Page": "หน้าเริ่มต้น",
"Search in OPDS Catalog...": "ค้นหาในแคตตาล็อก OPDS...",
"Please log in to use advanced TTS features.": "กรุณาเข้าสู่ระบบเพื่อใช้ฟีเจอร์ TTS ขั้นสูง"
}
@@ -699,9 +699,7 @@
"Validating...": "Doğrulanıyor...",
"View All": "Tümünü Görüntüle",
"Forward": "İleri",
"OPDS Catalog": "OPDS Kataloğu",
"Home": "Ana Sayfa",
"Library": "Kütüphane",
"{{count}} items_one": "{{count}} öğe",
"{{count}} items_other": "{{count}} öğe",
"Download completed": "İndirme tamamlandı",
@@ -765,5 +763,7 @@
"Successfully imported {{count}} book(s)_one": "Başarıyla 1 kitap içe aktarıldı",
"Successfully imported {{count}} book(s)_other": "Başarıyla {{count}} kitap içe aktarıldı",
"Count": "Sayım",
"Start Page": "Başlangıç Sayfası"
"Start Page": "Başlangıç Sayfası",
"Search in OPDS Catalog...": "OPDS Kataloğunda ara...",
"Please log in to use advanced TTS features.": "Gelişmiş TTS özelliklerini kullanmak için lütfen giriş yapın."
}
@@ -707,9 +707,7 @@
"Validating...": "Перевірка...",
"View All": "Переглянути все",
"Forward": "Вперед",
"OPDS Catalog": "Каталог OPDS",
"Home": "Головна",
"Library": "Бібліотека",
"{{count}} items_one": "{{count}} елемент",
"{{count}} items_few": "{{count}} елементи",
"{{count}} items_many": "{{count}} елементів",
@@ -785,5 +783,7 @@
"Successfully imported {{count}} book(s)_many": "Успішно імпортовано {{count}} книг",
"Successfully imported {{count}} book(s)_other": "Успішно імпортовано {{count}} книг",
"Count": "Кількість",
"Start Page": "Початкова сторінка"
"Start Page": "Початкова сторінка",
"Search in OPDS Catalog...": "Пошук у каталозі OPDS...",
"Please log in to use advanced TTS features.": "Будь ласка, увійдіть, щоб використовувати розширені функції TTS."
}
@@ -695,9 +695,7 @@
"Validating...": "Đang xác thực...",
"View All": "Xem tất cả",
"Forward": "Tiếp",
"OPDS Catalog": "Danh mục OPDS",
"Home": "Trang chủ",
"Library": "Thư viện",
"{{count}} items_other": "{{count}} mục",
"Download completed": "Tải xuống hoàn tất",
"Download failed": "Tải xuống thất bại",
@@ -755,5 +753,7 @@
"From Directory": "Từ thư mục",
"Successfully imported {{count}} book(s)_other": "Đã nhập thành công {{count}} sách",
"Count": "Số lượng",
"Start Page": "Trang bắt đầu"
"Start Page": "Trang bắt đầu",
"Search in OPDS Catalog...": "Tìm kiếm trong danh mục OPDS...",
"Please log in to use advanced TTS features.": "Vui lòng đăng nhập để sử dụng các tính năng TTS nâng cao."
}
@@ -695,9 +695,7 @@
"Validating...": "验证中...",
"View All": "查看全部",
"Forward": "前进",
"OPDS Catalog": "OPDS 目录",
"Home": "首页",
"Library": "图书馆",
"{{count}} items_other": "{{count}} 项",
"Download completed": "下载完成",
"Download failed": "下载失败",
@@ -755,5 +753,7 @@
"From Directory": "从文件夹导入",
"Successfully imported {{count}} book(s)_other": "成功导入 {{count}} 本书",
"Count": "数量",
"Start Page": "起始页"
"Start Page": "起始页",
"Search in OPDS Catalog...": "在 OPDS 目录中搜索...",
"Please log in to use advanced TTS features.": "请登录以使用高级 TTS 功能"
}
@@ -695,9 +695,7 @@
"Validating...": "驗證中...",
"View All": "檢視全部",
"Forward": "前往",
"OPDS Catalog": "OPDS 目錄",
"Home": "首頁",
"Library": "圖書館",
"{{count}} items_other": "{{count}} 項",
"Download completed": "下載完成",
"Download failed": "下載失敗",
@@ -755,5 +753,7 @@
"From Directory": "從目錄導入",
"Successfully imported {{count}} book(s)_other": "成功導入 {{count}} 本書",
"Count": "數量",
"Start Page": "起始頁"
"Start Page": "起始頁",
"Search in OPDS Catalog...": "在 OPDS 目錄中搜尋...",
"Please log in to use advanced TTS features.": "請登入以使用進階 TTS 功能"
}
+1
View File
@@ -52,6 +52,7 @@ tauri-plugin-haptics = "2"
tauri-plugin-persisted-scope = "2"
tauri-plugin-native-bridge = { path = "./plugins/tauri-plugin-native-bridge" }
tauri-plugin-native-tts = { path = "./plugins/tauri-plugin-native-tts" }
tauri-plugin-websocket = "2"
[target."cfg(target_os = \"macos\")".dependencies]
rand = "0.8"
@@ -116,6 +116,7 @@
}
]
},
"websocket:default",
"dialog:default",
"os:default",
"core:window:default",
+1
View File
@@ -144,6 +144,7 @@ struct SingleInstancePayload {
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let builder = tauri::Builder::default()
.plugin(tauri_plugin_websocket::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_oauth::init())
.invoke_handler(tauri::generate_handler![
@@ -0,0 +1,131 @@
import { NextRequest, NextResponse } from 'next/server';
import { EdgeSpeechTTS, EdgeTTSPayload } from '@/libs/edgeTTS';
import { validateUserAndToken } from '@/utils/access';
const getLangFromVoice = (voiceId: string): string => {
const match = voiceId.match(/^([a-z]{2}-[A-Z]{2})/);
return match ? match[1]! : 'en-US';
};
const isValidVoice = (voiceId: string): boolean => {
return EdgeSpeechTTS.voices.some((v) => v.id === voiceId);
};
export async function POST(request: NextRequest) {
const { user, token } = await validateUserAndToken(request.headers.get('authorization'));
if (!user || !token) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 403 });
}
try {
const body = await request.json();
const { input: text, voice, speed = 1.0 } = body;
let { rate, lang } = body;
if (!text || typeof text !== 'string') {
return NextResponse.json(
{ error: { message: 'Missing or invalid "input" field', type: 'invalid_request_error' } },
{ status: 400 },
);
}
if (!voice || typeof voice !== 'string') {
return NextResponse.json(
{ error: { message: 'Missing or invalid "voice" field', type: 'invalid_request_error' } },
{ status: 400 },
);
}
if (!isValidVoice(voice)) {
return NextResponse.json(
{
error: {
message: `Invalid voice "${voice}". Use GET /api/tts/edge to list available voices.`,
type: 'invalid_request_error',
},
},
{ status: 400 },
);
}
lang = lang || getLangFromVoice(voice);
// Calculate rate (OpenAI speed ranges from 0.25 to 4.0, Edge TTS rate is 0.5 to 2.0)
const clampedSpeed = Math.max(0.25, Math.min(4.0, speed));
let mappedSpeed: number;
if (clampedSpeed <= 1.0) {
mappedSpeed = 0.5 + ((clampedSpeed - 0.25) / (1.0 - 0.25)) * (1.0 - 0.5);
} else {
mappedSpeed = 1.0 + ((clampedSpeed - 1.0) / (4.0 - 1.0)) * (2.0 - 1.0);
}
rate = rate || mappedSpeed;
const payload: EdgeTTSPayload = {
lang,
text,
voice,
rate,
pitch: 1.0,
};
const tts = new EdgeSpeechTTS();
const response = await tts.create(payload);
const arrayBuffer = await response.arrayBuffer();
return new NextResponse(arrayBuffer, {
status: 200,
headers: {
'Content-Type': 'audio/mpeg',
'Content-Length': arrayBuffer.byteLength.toString(),
},
});
} catch (error) {
console.error('Edge TTS API error:', error);
return NextResponse.json(
{
error: {
message: error instanceof Error ? error.message : 'Internal server error',
type: 'internal_error',
},
},
{ status: 500 },
);
}
}
export async function GET(request: NextRequest) {
const { user, token } = await validateUserAndToken(request.headers.get('authorization'));
if (!user || !token) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 403 });
}
try {
const query = request.nextUrl.searchParams;
const lang = query.get('lang') || '';
let voices = EdgeSpeechTTS.voices;
if (lang) {
voices = voices.filter((v) => v.lang.toLowerCase().includes(lang.toLowerCase()));
}
const formattedVoices = voices.map((voice) => ({
id: voice.id,
name: voice.name,
language: voice.lang,
}));
return NextResponse.json({
voices: formattedVoices,
});
} catch (error) {
console.error('Error listing voices:', error);
return NextResponse.json(
{
error: {
message: 'Failed to list voices',
type: 'internal_error',
},
},
{ status: 500 },
);
}
}
@@ -1,6 +1,7 @@
import clsx from 'clsx';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useAuth } from '@/context/AuthContext';
import { useThemeStore } from '@/store/themeStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useBookDataStore } from '@/store/bookDataStore';
@@ -36,6 +37,7 @@ interface TTSControlProps {
const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
const _ = useTranslation();
const { appService } = useEnv();
const { user } = useAuth();
const { safeAreaInsets } = useThemeStore();
const { settings } = useSettingsStore();
const { getBookData } = useBookDataStore();
@@ -173,6 +175,14 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
if (!bookData || !bookData.book) return;
const { title, author, coverImageUrl } = bookData.book;
const handleNeedAuth = () => {
eventDispatcher.dispatch('toast', {
message: _('Please log in to use advanced TTS features.'),
type: 'error',
timeout: 5000,
});
};
const handleSpeakMark = (e: Event) => {
const progress = getProgress(bookKey);
const { sectionLabel } = progress || {};
@@ -243,9 +253,11 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
}
};
ttsController.addEventListener('tts-need-auth', handleNeedAuth);
ttsController.addEventListener('tts-speak-mark', handleSpeakMark);
ttsController.addEventListener('tts-highlight-mark', handleHighlightMark);
return () => {
ttsController.removeEventListener('tts-need-auth', handleNeedAuth);
ttsController.removeEventListener('tts-speak-mark', handleSpeakMark);
ttsController.removeEventListener('tts-highlight-mark', handleHighlightMark);
};
@@ -326,7 +338,7 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
setTtsClientsInitialized(false);
setShowIndicator(true);
const ttsController = new TTSController(appService, view);
const ttsController = new TTSController(appService, view, !!user?.id);
await ttsController.init();
await ttsController.initViewTTS(viewSettings.ttsHighlightOptions);
const ssml = view.tts?.from(ttsFromRange);
+151 -43
View File
@@ -1,13 +1,18 @@
import { md5 } from 'js-md5';
import WebSocket from 'isomorphic-ws';
import { createHash } from 'crypto';
import { randomMd5 } from '@/utils/misc';
import { LRUCache } from '@/utils/lru';
import { genSSML } from '@/utils/ssml';
import { fetchWithAuth } from '@/utils/fetch';
import { getNodeAPIBaseUrl, isTauriAppPlatform } from '@/services/environment';
const EDGE_SPEECH_URL =
'wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1';
const EDGE_API_TOKEN = '6A5AA1D4EAFF4E9FB37E23D68491D6F4';
const CHROMIUM_FULL_VERSION = '130.0.2849.68';
const CHROMIUM_FULL_VERSION = '143.0.3650.75';
const CHROMIUM_MAJOR_VERSION = CHROMIUM_FULL_VERSION.split('.')[0];
const EDGE_TTS_VOICES = {
'af-ZA': ['af-ZA-AdriNeural', 'af-ZA-WillemNeural'],
'am-ET': ['am-ET-AmehaNeural', 'am-ET-MekdesNeural'],
@@ -214,6 +219,17 @@ const generateSecMsGec = () => {
return createHash('sha256').update(strToHash, 'ascii').digest('hex').toUpperCase();
};
const generateMuid = () => {
// Generate 16 random bytes (32 hex characters)
const array = new Uint8Array(16);
crypto.getRandomValues(array);
return Array.from(array)
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
.toUpperCase();
};
const genVoiceList = (voices: Record<string, string[]>) => {
return Object.entries(voices).flatMap(([lang, voices]) => {
return voices.map((id) => {
@@ -236,6 +252,8 @@ const hashPayload = (payload: EdgeTTSPayload): string => {
return md5(base);
};
export type EDGE_TTS_PROTOCOL = 'wss' | 'https';
export class EdgeSpeechTTS {
static voices = genVoiceList(EDGE_TTS_VOICES);
private static audioCache = new LRUCache<string, Blob>(200);
@@ -244,8 +262,36 @@ export class EdgeSpeechTTS {
URL.revokeObjectURL(url);
}
});
private protocol: EDGE_TTS_PROTOCOL = 'wss';
constructor() {}
constructor(protocol?: EDGE_TTS_PROTOCOL) {
if (protocol) {
this.protocol = protocol;
}
}
async #fetchEdgeSpeechHttp({ lang, text, voice, rate }: EdgeTTSPayload): Promise<Response> {
const url = getNodeAPIBaseUrl() + '/tts/edge';
const response = await fetchWithAuth(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
input: text,
voice,
rate,
lang,
}),
});
if (!response.ok) {
throw new Error(`Edge TTS HTTP request failed: ${response.status} ${response.statusText}`);
}
return response;
}
async #fetchEdgeSpeechWs({ lang, text, voice, rate }: EdgeTTSPayload): Promise<Response> {
const connectId = randomMd5();
@@ -257,6 +303,19 @@ export class EdgeSpeechTTS {
});
const url = `${EDGE_SPEECH_URL}?${params.toString()}`;
const date = new Date().toString();
const baseHeaders = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' +
` (KHTML, like Gecko) Chrome/${CHROMIUM_MAJOR_VERSION}.0.0.0 Safari/537.36` +
` Edg/${CHROMIUM_MAJOR_VERSION}.0.0.0`,
'Accept-Encoding': 'gzip, deflate, br, zstd',
'Accept-Language': 'en-US,en;q=0.9',
Pragma: 'no-cache',
'Cache-Control': 'no-cache',
Origin: 'chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold',
'Sec-WebSocket-Version': '13',
Cookie: `muid=${generateMuid()};`,
};
const configHeaders = {
'Content-Type': 'application/json; charset=utf-8',
Path: 'speech.config',
@@ -314,56 +373,105 @@ export class EdgeSpeechTTS {
const content = genSendContent(contentHeaders, ssml);
const config = genSendContent(configHeaders, configContent);
return new Promise((resolve, reject) => {
const ws = new WebSocket(url);
ws.binaryType = 'arraybuffer';
let audioData = new ArrayBuffer(0);
ws.addEventListener('open', () => {
ws.send(config);
ws.send(content);
});
ws.addEventListener('message', (event: MessageEvent) => {
if (typeof event.data === 'string') {
const { headers } = getHeadersAndData(event.data);
if (headers['Path'] === 'turn.end') {
ws.close();
if (!audioData.byteLength) {
return reject(new Error('No audio data received.'));
if (isTauriAppPlatform()) {
return new Promise(async (resolve, reject) => {
try {
const TauriWebSocket = (await import('@tauri-apps/plugin-websocket')).default;
const ws = await TauriWebSocket.connect(url, { headers: baseHeaders });
let audioData = new ArrayBuffer(0);
const messageUnlisten = await ws.addListener((msg) => {
if (msg.type === 'Text') {
const { headers } = getHeadersAndData(msg.data as string);
if (headers['Path'] === 'turn.end') {
ws.disconnect();
messageUnlisten();
if (!audioData.byteLength) {
return reject(new Error('No audio data received.'));
}
const res = new Response(audioData);
resolve(res);
}
} else if (msg.type === 'Binary') {
let buffer: ArrayBufferLike;
if (msg.data instanceof Uint8Array) {
buffer = msg.data.buffer;
} else {
buffer = new Uint8Array(msg.data).buffer;
}
const dataView = new DataView(buffer);
const headerLength = dataView.getInt16(0);
if (buffer.byteLength > headerLength + 2) {
const newBody = buffer.slice(2 + headerLength);
const merged = new Uint8Array(audioData.byteLength + newBody.byteLength);
merged.set(new Uint8Array(audioData), 0);
merged.set(new Uint8Array(newBody), audioData.byteLength);
audioData = merged.buffer;
}
}
const res = new Response(audioData);
resolve(res);
}
} else if (event.data instanceof ArrayBuffer) {
const dataView = new DataView(event.data);
const headerLength = dataView.getInt16(0);
if (event.data.byteLength > headerLength + 2) {
const newBody = event.data.slice(2 + headerLength);
const merged = new Uint8Array(audioData.byteLength + newBody.byteLength);
merged.set(new Uint8Array(audioData), 0);
merged.set(new Uint8Array(newBody), audioData.byteLength);
audioData = merged.buffer;
}
});
await ws.send(config);
await ws.send(content);
} catch (error) {
reject(new Error(`WebSocket error occurred: ${error}`));
}
});
} else {
return new Promise((resolve, reject) => {
const ws = new WebSocket(url, {
headers: baseHeaders,
});
ws.binaryType = 'arraybuffer';
ws.addEventListener('close', () => {
if (!audioData.byteLength) {
reject(new Error('No audio data received.'));
}
});
let audioData = new ArrayBuffer(0);
ws.addEventListener('error', () => {
ws.close();
reject(new Error('WebSocket error occurred.'));
ws.addEventListener('open', () => {
ws.send(config);
ws.send(content);
});
ws.addEventListener('message', (event: WebSocket.MessageEvent) => {
if (typeof event.data === 'string') {
const { headers } = getHeadersAndData(event.data);
if (headers['Path'] === 'turn.end') {
ws.close();
if (!audioData.byteLength) {
return reject(new Error('No audio data received.'));
}
const res = new Response(audioData);
resolve(res);
}
} else if (event.data instanceof ArrayBuffer) {
const dataView = new DataView(event.data);
const headerLength = dataView.getInt16(0);
if (event.data.byteLength > headerLength + 2) {
const newBody = event.data.slice(2 + headerLength);
const merged = new Uint8Array(audioData.byteLength + newBody.byteLength);
merged.set(new Uint8Array(audioData), 0);
merged.set(new Uint8Array(newBody), audioData.byteLength);
audioData = merged.buffer;
}
}
});
ws.addEventListener('close', () => {
if (!audioData.byteLength) {
reject(new Error('No audio data received.'));
}
});
ws.addEventListener('error', () => {
reject(new Error('WebSocket error occurred.'));
});
});
});
}
}
async create(payload: EdgeTTSPayload): Promise<Response> {
return this.#fetchEdgeSpeechWs(payload);
if (this.protocol === 'https') {
return this.#fetchEdgeSpeechHttp(payload);
} else {
return this.#fetchEdgeSpeechWs(payload);
}
}
async createAudioUrl(payload: EdgeTTSPayload): Promise<string> {
@@ -1,6 +1,6 @@
import { getUserLocale } from '@/utils/misc';
import { TTSClient, TTSMessageEvent } from './TTSClient';
import { EdgeSpeechTTS, EdgeTTSPayload } from '@/libs/edgeTTS';
import { EdgeSpeechTTS, EdgeTTSPayload, EDGE_TTS_PROTOCOL } from '@/libs/edgeTTS';
import { parseSSMLMarks } from '@/utils/ssml';
import { TTSController } from './TTSController';
import { TTSUtils } from './TTSUtils';
@@ -18,7 +18,7 @@ export class EdgeTTSClient implements TTSClient {
#rate = 1.0;
#pitch = 1.0;
#edgeTTS: EdgeSpeechTTS;
#edgeTTS: EdgeSpeechTTS | null = null;
#audioElement: HTMLAudioElement | null = null;
#isPlaying = false;
#pausedAt = 0;
@@ -27,10 +27,10 @@ export class EdgeTTSClient implements TTSClient {
constructor(controller?: TTSController) {
this.controller = controller;
this.#edgeTTS = new EdgeSpeechTTS();
}
async init() {
async init(protocol: EDGE_TTS_PROTOCOL = 'wss') {
this.#edgeTTS = new EdgeSpeechTTS(protocol);
this.#voices = EdgeSpeechTTS.voices;
try {
await this.#edgeTTS.create({
@@ -42,7 +42,15 @@ export class EdgeTTSClient implements TTSClient {
});
this.initialized = true;
} catch {
this.initialized = false;
if (protocol === 'wss') {
if (this.controller?.isAuthenticated) {
await this.init('https');
} else {
this.controller?.dispatchEvent(new CustomEvent('tts-need-auth'));
}
} else {
this.initialized = false;
}
}
return this.initialized;
}
@@ -75,7 +83,7 @@ export class EdgeTTSClient implements TTSClient {
const voiceId = await this.getVoiceIdFromLang(voiceLang);
this.#currentVoiceId = voiceId;
await this.#edgeTTS
.createAudioUrl(this.getPayload(voiceLang, mark.text, voiceId))
?.createAudioUrl(this.getPayload(voiceLang, mark.text, voiceId))
.catch((err) => {
console.warn('Error preloading mark', i, err);
});
@@ -88,7 +96,7 @@ export class EdgeTTSClient implements TTSClient {
if (signal.aborted) break;
const { language: voiceLang } = mark;
const voiceId = await this.getVoiceIdFromLang(voiceLang);
await this.#edgeTTS.createAudioUrl(this.getPayload(voiceLang, mark.text, voiceId));
await this.#edgeTTS?.createAudioUrl(this.getPayload(voiceLang, mark.text, voiceId));
} catch (err) {
console.warn('Error preloading mark (bg)', i, err);
}
@@ -120,7 +128,7 @@ export class EdgeTTSClient implements TTSClient {
const { language: voiceLang } = mark;
const voiceId = await this.getVoiceIdFromLang(voiceLang);
this.#speakingLang = voiceLang;
const audioUrl = await this.#edgeTTS.createAudioUrl(
const audioUrl = await this.#edgeTTS?.createAudioUrl(
this.getPayload(voiceLang, mark.text, voiceId),
);
if (signal.aborted) {
@@ -160,7 +168,7 @@ export class EdgeTTSClient implements TTSClient {
resolve({ code: 'error', message: 'Audio playback error' });
};
this.#isPlaying = true;
audio.src = audioUrl;
audio.src = audioUrl || '';
audio.play().catch((err) => {
cleanUp();
console.error('Failed to play audio:', err);
@@ -25,6 +25,7 @@ const HIGHLIGHT_KEY = 'tts-highlight';
export class TTSController extends EventTarget {
appService: AppService | null = null;
view: FoliateView;
isAuthenticated: boolean = false;
#nossmlCnt: number = 0;
#currentSpeakAbortController: AbortController | null = null;
#currentSpeakPromise: Promise<void> | null = null;
@@ -43,7 +44,7 @@ export class TTSController extends EventTarget {
options: TTSHighlightOptions = { style: 'highlight', color: 'gray' };
constructor(appService: AppService | null, view: FoliateView) {
constructor(appService: AppService | null, view: FoliateView, isAuthenticated: boolean = false) {
super();
this.ttsWebClient = new WebSpeechClient(this);
this.ttsEdgeClient = new EdgeTTSClient(this);
@@ -54,6 +55,7 @@ export class TTSController extends EventTarget {
this.ttsClient = this.ttsWebClient;
this.appService = appService;
this.view = view;
this.isAuthenticated = isAuthenticated;
}
async init() {
+1 -1
View File
@@ -1,6 +1,6 @@
name = "readest-web"
main = ".open-next/worker.js"
compatibility_date = "2025-02-04"
compatibility_date = "2025-11-17"
compatibility_flags = ["nodejs_compat"]
[observability]
+33 -5
View File
@@ -115,6 +115,9 @@ importers:
'@tauri-apps/plugin-updater':
specifier: ^2.9.0
version: 2.9.0
'@tauri-apps/plugin-websocket':
specifier: ~2.4.1
version: 2.4.1
'@zip.js/zip.js':
specifier: ^2.7.53
version: 2.7.53
@@ -169,6 +172,9 @@ importers:
iso-639-3:
specifier: ^3.0.1
version: 3.0.1
isomorphic-ws:
specifier: ^5.0.0
version: 5.0.0(ws@8.18.3)
js-md5:
specifier: ^0.8.3
version: 0.8.3
@@ -229,6 +235,9 @@ importers:
uuid:
specifier: ^11.1.0
version: 11.1.0
ws:
specifier: ^8.18.3
version: 8.18.3
zod:
specifier: ^4.0.8
version: 4.0.10
@@ -281,6 +290,9 @@ importers:
'@types/uuid':
specifier: ^10.0.0
version: 10.0.0
'@types/ws':
specifier: ^8.18.1
version: 8.18.1
'@typescript-eslint/eslint-plugin':
specifier: ^8.48.0
version: 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.28.0(jiti@1.21.7))(typescript@5.7.2))(eslint@9.28.0(jiti@1.21.7))(typescript@5.7.2)
@@ -3092,6 +3104,9 @@ packages:
'@tauri-apps/plugin-updater@2.9.0':
resolution: {integrity: sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg==}
'@tauri-apps/plugin-websocket@2.4.1':
resolution: {integrity: sha512-OGxbYlJv8+REcCsx1+noE17HCfpsvMH9t7EH77jUa2AduxvazrBijtTwBHJQQKWIVgpCgYxlDLgZoRoW9kkNYA==}
'@testing-library/dom@10.4.0':
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
engines: {node: '>=18'}
@@ -5163,6 +5178,11 @@ packages:
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
engines: {node: '>=0.10.0'}
isomorphic-ws@5.0.0:
resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==}
peerDependencies:
ws: '*'
iterator.prototype@1.1.3:
resolution: {integrity: sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==}
engines: {node: '>= 0.4'}
@@ -7169,8 +7189,8 @@ packages:
utf-8-validate:
optional: true
ws@8.18.2:
resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==}
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
@@ -11440,7 +11460,7 @@ snapshots:
'@types/phoenix': 1.6.6
'@types/ws': 8.18.1
tslib: 2.8.1
ws: 8.18.2
ws: 8.18.3
transitivePeerDependencies:
- bufferutil
- utf-8-validate
@@ -11578,6 +11598,10 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.9.1
'@tauri-apps/plugin-websocket@2.4.1':
dependencies:
'@tauri-apps/api': 2.9.1
'@testing-library/dom@10.4.0':
dependencies:
'@babel/code-frame': 7.26.2
@@ -14069,6 +14093,10 @@ snapshots:
isobject@3.0.1: {}
isomorphic-ws@5.0.0(ws@8.18.3):
dependencies:
ws: 8.18.3
iterator.prototype@1.1.3:
dependencies:
define-properties: 1.2.1
@@ -14126,7 +14154,7 @@ snapshots:
whatwg-encoding: 3.1.1
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
ws: 8.18.2
ws: 8.18.3
xml-name-validator: 5.0.0
transitivePeerDependencies:
- bufferutil
@@ -16365,7 +16393,7 @@ snapshots:
ws@8.18.0: {}
ws@8.18.2: {}
ws@8.18.3: {}
xml-name-validator@5.0.0: {}