diff --git a/Cargo.lock b/Cargo.lock index c51e3306..bc446759 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/apps/readest-app/package.json b/apps/readest-app/package.json index 3a8dbf3d..39c55265 100644 --- a/apps/readest-app/package.json +++ b/apps/readest-app/package.json @@ -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", diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index 6cc2795d..fa1f403a 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -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.": "يرجى تسجيل الدخول لاستخدام ميزات تحويل النص إلى كلام المتقدمة." } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index c3278c48..b2c48bda 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -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 বৈশিষ্ট্যগুলি ব্যবহার করতে লগইন করুন।" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index e32c36ed..bd988606 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -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 རྣམ་པ་ཚུགས་སྤྱོད་བྱས་མ་ཐུབ།" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index 2ecdd612..0052a745 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -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." } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index 39af627a..79b2a624 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -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." } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index 91ed2eed..22f3e746 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -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." } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index bf3c56e8..d32c40fb 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -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 وارد شوید." } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index adc5bc4c..c1e55f76 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -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." } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 3bad4d21..90698a61 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -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 सुविधाओं का उपयोग करने के लिए कृपया लॉग इन करें।" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index 74509878..59062d46 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -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." } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index 9e788e1a..761b4cf4 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -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." } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index 07c9a411..cba55a46 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -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機能を使用するにはログインしてください。" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 94845528..2152cadd 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -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 기능을 사용하려면 로그인하세요." } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index 3aa30726..4f42dfd1 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -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." } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index aac9419e..0caea1d7 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -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." } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index b27c71dc..70ef165c 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -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." } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index 700c7707..f030e5a8 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -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." } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index feb59863..a9a185cf 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -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." } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index 98fe57b9..0750947b 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -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 විශේෂාංග භාවිතා කිරීමට කරුණාකර පිවිසෙන්න." } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index 85cd235e..2a770077 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -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." } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index 9eccef8f..0ea56f26 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -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 அம்சங்களை பயன்படுத்த உள்நுழையவும்." } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index 997b78b3..abe1ee45 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -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 ขั้นสูง" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index 69361b98..f99cee26 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -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." } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index 51cc99d3..08176459 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -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." } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index 2e609faa..b0d48df6 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -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." } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index 0b2d3726..33dea6d5 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -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 功能" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index 99a5779e..458c7c26 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -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 功能" } diff --git a/apps/readest-app/src-tauri/Cargo.toml b/apps/readest-app/src-tauri/Cargo.toml index 10978b9a..67192574 100644 --- a/apps/readest-app/src-tauri/Cargo.toml +++ b/apps/readest-app/src-tauri/Cargo.toml @@ -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" diff --git a/apps/readest-app/src-tauri/capabilities/default.json b/apps/readest-app/src-tauri/capabilities/default.json index 21bdb6e2..710d6c8e 100644 --- a/apps/readest-app/src-tauri/capabilities/default.json +++ b/apps/readest-app/src-tauri/capabilities/default.json @@ -116,6 +116,7 @@ } ] }, + "websocket:default", "dialog:default", "os:default", "core:window:default", diff --git a/apps/readest-app/src-tauri/src/lib.rs b/apps/readest-app/src-tauri/src/lib.rs index afa80c57..a38e883d 100644 --- a/apps/readest-app/src-tauri/src/lib.rs +++ b/apps/readest-app/src-tauri/src/lib.rs @@ -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![ diff --git a/apps/readest-app/src/app/api/tts/edge/route.ts b/apps/readest-app/src/app/api/tts/edge/route.ts new file mode 100644 index 00000000..9fd19be0 --- /dev/null +++ b/apps/readest-app/src/app/api/tts/edge/route.ts @@ -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 }, + ); + } +} diff --git a/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx b/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx index 8a988142..b4bb990d 100644 --- a/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx +++ b/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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 = ({ 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); diff --git a/apps/readest-app/src/libs/edgeTTS.ts b/apps/readest-app/src/libs/edgeTTS.ts index 20aaac4e..0bf9a449 100644 --- a/apps/readest-app/src/libs/edgeTTS.ts +++ b/apps/readest-app/src/libs/edgeTTS.ts @@ -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) => { 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(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 { + 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 { 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 { - return this.#fetchEdgeSpeechWs(payload); + if (this.protocol === 'https') { + return this.#fetchEdgeSpeechHttp(payload); + } else { + return this.#fetchEdgeSpeechWs(payload); + } } async createAudioUrl(payload: EdgeTTSPayload): Promise { diff --git a/apps/readest-app/src/services/tts/EdgeTTSClient.ts b/apps/readest-app/src/services/tts/EdgeTTSClient.ts index 12735788..3fe6e853 100644 --- a/apps/readest-app/src/services/tts/EdgeTTSClient.ts +++ b/apps/readest-app/src/services/tts/EdgeTTSClient.ts @@ -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); diff --git a/apps/readest-app/src/services/tts/TTSController.ts b/apps/readest-app/src/services/tts/TTSController.ts index 04f16725..5edab8c0 100644 --- a/apps/readest-app/src/services/tts/TTSController.ts +++ b/apps/readest-app/src/services/tts/TTSController.ts @@ -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 | 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() { diff --git a/apps/readest-app/wrangler.toml b/apps/readest-app/wrangler.toml index cc654a61..b7280071 100644 --- a/apps/readest-app/wrangler.toml +++ b/apps/readest-app/wrangler.toml @@ -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] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf1ad34a..f8f7d5e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {}