forked from akai/readest
fix(tts): resolve Edge TTS being blocked in certain regions (#2741)
This should close #2739 and close #1821.
This commit is contained in:
Generated
+83
-2
@@ -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"
|
||||
|
||||
@@ -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 功能"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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]
|
||||
|
||||
Generated
+33
-5
@@ -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: {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user