feat(tts): add an option to select target language for TTS on translated books (#2310)
This commit is contained in:
@@ -632,5 +632,9 @@
|
||||
"Tap Both Sides": "اضغط على كلا الجانبين",
|
||||
"Click Both Sides": "انقر على كلا الجانبين",
|
||||
"Swap Tap Sides": "تبديل جانبي اللمس",
|
||||
"Swap Click Sides": "تبديل جانبي النقر"
|
||||
"Swap Click Sides": "تبديل جانبي النقر",
|
||||
"Source and Translated": "المصدر والمترجم",
|
||||
"Translated Only": "المترجم فقط",
|
||||
"Source Only": "المصدر فقط",
|
||||
"TTS Text": "نص تحويل النص إلى كلام"
|
||||
}
|
||||
|
||||
@@ -616,5 +616,9 @@
|
||||
"Tap Both Sides": "দুই পাশে ট্যাপ করুন",
|
||||
"Click Both Sides": "দুই পাশে ক্লিক করুন",
|
||||
"Swap Tap Sides": "ট্যাপের পাশে পরিবর্তন করুন",
|
||||
"Swap Click Sides": "ক্লিকের পাশে পরিবর্তন করুন"
|
||||
"Swap Click Sides": "ক্লিকের পাশে পরিবর্তন করুন",
|
||||
"Source and Translated": "উৎস এবং অনূদিত",
|
||||
"Translated Only": "শুধুমাত্র অনূদিত",
|
||||
"Source Only": "শুধুমাত্র উৎস",
|
||||
"TTS Text": "TTS টেক্সট"
|
||||
}
|
||||
|
||||
@@ -612,5 +612,9 @@
|
||||
"Tap Both Sides": "གཡས་གཉིས་ལ་ཐོག་འགྲོ།",
|
||||
"Click Both Sides": "གཡས་གཉིས་ལ་ཁྱེད་ལུས་འགྲོ།",
|
||||
"Swap Tap Sides": "ཐོག་གཡས་གཉིས་བརྗེ་བ།",
|
||||
"Swap Click Sides": "ཁྱེད་ལུས་གཡས་གཉིས་བརྗེ་བ།"
|
||||
"Swap Click Sides": "ཁྱེད་ལུས་གཡས་གཉིས་བརྗེ་བ།",
|
||||
"Source and Translated": "ཡིག་སྒྱུར་སྤོ་བ།",
|
||||
"Translated Only": "མདོག་ཚད་ཀྱི་བསྡུར་བ།",
|
||||
"Source Only": "ཡིག་སྒྱུར་སྤོ་བ།",
|
||||
"TTS Text": "TTS ཡིག"
|
||||
}
|
||||
|
||||
@@ -616,5 +616,9 @@
|
||||
"Tap Both Sides": "Tippen Sie auf beide Seiten",
|
||||
"Click Both Sides": "Klicken Sie auf beide Seiten",
|
||||
"Swap Tap Sides": "Seiten für das Tippen tauschen",
|
||||
"Swap Click Sides": "Seiten für das Klicken tauschen"
|
||||
"Swap Click Sides": "Seiten für das Klicken tauschen",
|
||||
"Source and Translated": "Quell- und Übersetzungstext",
|
||||
"Translated Only": "Nur Übersetzungstext",
|
||||
"Source Only": "Nur Quelltext",
|
||||
"TTS Text": "TTS-Text"
|
||||
}
|
||||
|
||||
@@ -616,5 +616,9 @@
|
||||
"Tap Both Sides": "Ταπ και στις δύο πλευρές",
|
||||
"Click Both Sides": "Κλικ και στις δύο πλευρές",
|
||||
"Swap Tap Sides": "Ανταλλαγή πλευρών ταπ",
|
||||
"Swap Click Sides": "Ανταλλαγή πλευρών κλικ"
|
||||
"Swap Click Sides": "Ανταλλαγή πλευρών κλικ",
|
||||
"Source and Translated": "Πηγή και Μεταφρασμένο",
|
||||
"Translated Only": "Μόνο Μεταφρασμένο",
|
||||
"Source Only": "Μόνο πηγή",
|
||||
"TTS Text": "Κείμενο TTS"
|
||||
}
|
||||
|
||||
@@ -620,5 +620,9 @@
|
||||
"Tap Both Sides": "Tocar Ambos Lados",
|
||||
"Click Both Sides": "Hacer Clic en Ambos Lados",
|
||||
"Swap Tap Sides": "Intercambiar Lados de Toque",
|
||||
"Swap Click Sides": "Intercambiar Lados de Clic"
|
||||
"Swap Click Sides": "Intercambiar Lados de Clic",
|
||||
"Source and Translated": "Fuente y Traducido",
|
||||
"Translated Only": "Solo Traducido",
|
||||
"Source Only": "Solo Fuente",
|
||||
"TTS Text": "TTS Texto"
|
||||
}
|
||||
|
||||
@@ -620,5 +620,9 @@
|
||||
"Tap Both Sides": "Tapoter des Deux Côtés",
|
||||
"Click Both Sides": "Cliquer des Deux Côtés",
|
||||
"Swap Tap Sides": "Échanger les Côtés de Tap",
|
||||
"Swap Click Sides": "Échanger les Côtés de Clic"
|
||||
"Swap Click Sides": "Échanger les Côtés de Clic",
|
||||
"Source and Translated": "Source et Traduit",
|
||||
"Translated Only": "Traduit Seulement",
|
||||
"Source Only": "Source Seulement",
|
||||
"TTS Text": "TTS Texte"
|
||||
}
|
||||
|
||||
@@ -616,5 +616,9 @@
|
||||
"Tap Both Sides": "दोनों पक्षों पर टैप करें",
|
||||
"Click Both Sides": "दोनों पक्षों पर क्लिक करें",
|
||||
"Swap Tap Sides": "स्वैप टैप साइड्स",
|
||||
"Swap Click Sides": "स्वैप क्लिक साइड्स"
|
||||
"Swap Click Sides": "स्वैप क्लिक साइड्स",
|
||||
"Source and Translated": "स्रोत और अनुवादित",
|
||||
"Translated Only": "केवल अनुवादित",
|
||||
"Source Only": "केवल स्रोत",
|
||||
"TTS Text": "TTS टेक्स्ट"
|
||||
}
|
||||
|
||||
@@ -612,5 +612,9 @@
|
||||
"Tap Both Sides": "Ketuk Kedua Sisi",
|
||||
"Click Both Sides": "Klik Kedua Sisi",
|
||||
"Swap Tap Sides": "Tukar Sisi Ketuk",
|
||||
"Swap Click Sides": "Tukar Sisi Klik"
|
||||
"Swap Click Sides": "Tukar Sisi Klik",
|
||||
"Source and Translated": "Sumber dan Terjemahan",
|
||||
"Translated Only": "Hanya Terjemahan",
|
||||
"Source Only": "Hanya Sumber",
|
||||
"TTS Text": "Teks TTS"
|
||||
}
|
||||
|
||||
@@ -620,5 +620,9 @@
|
||||
"Tap Both Sides": "Tocca Entrambi i Lati",
|
||||
"Click Both Sides": "Clicca Entrambi i Lati",
|
||||
"Swap Tap Sides": "Scambia Sides Tap",
|
||||
"Swap Click Sides": "Scambia Sides Click"
|
||||
"Swap Click Sides": "Scambia Sides Click",
|
||||
"Source and Translated": "Fonte e Tradotto",
|
||||
"Translated Only": "Solo Tradotto",
|
||||
"Source Only": "Solo Fonte",
|
||||
"TTS Text": "Testo TTS"
|
||||
}
|
||||
|
||||
@@ -612,5 +612,9 @@
|
||||
"Tap Both Sides": "両側をタップ",
|
||||
"Click Both Sides": "両側をクリック",
|
||||
"Swap Tap Sides": "タップサイドを入れ替える",
|
||||
"Swap Click Sides": "クリックサイドを入れ替える"
|
||||
"Swap Click Sides": "クリックサイドを入れ替える",
|
||||
"Source and Translated": "原文と翻訳",
|
||||
"Translated Only": "翻訳のみ",
|
||||
"Source Only": "原文のみ",
|
||||
"TTS Text": "TTSテキスト"
|
||||
}
|
||||
|
||||
@@ -612,5 +612,9 @@
|
||||
"Tap Both Sides": "양쪽을 탭",
|
||||
"Click Both Sides": "양쪽을 클릭",
|
||||
"Swap Tap Sides": "탭 사이드 전환",
|
||||
"Swap Click Sides": "클릭 사이드 전환"
|
||||
"Swap Click Sides": "클릭 사이드 전환",
|
||||
"Source and Translated": "원본 및 번역",
|
||||
"Translated Only": "번역만",
|
||||
"Source Only": "원본만",
|
||||
"TTS Text": "TTS 텍스트"
|
||||
}
|
||||
|
||||
@@ -616,5 +616,9 @@
|
||||
"Tap Both Sides": "Tik op Beide Zijden",
|
||||
"Click Both Sides": "Klik op Beide Zijden",
|
||||
"Swap Tap Sides": "Wissel Tap Zijden",
|
||||
"Swap Click Sides": "Wissel Klik Zijden"
|
||||
"Swap Click Sides": "Wissel Klik Zijden",
|
||||
"Source and Translated": "Bron en Vertaling",
|
||||
"Translated Only": "Alleen Vertaling",
|
||||
"Source Only": "Alleen Bron",
|
||||
"TTS Text": "TTS Tekst"
|
||||
}
|
||||
|
||||
@@ -624,5 +624,9 @@
|
||||
"Tap Both Sides": "Dotknij obu stron",
|
||||
"Click Both Sides": "Kliknij obu stron",
|
||||
"Swap Tap Sides": "Wymień Tap Zewnętrzne",
|
||||
"Swap Click Sides": "Wymień Click Zewnętrzne"
|
||||
"Swap Click Sides": "Wymień Click Zewnętrzne",
|
||||
"Source and Translated": "Źródło i Tłumaczenie",
|
||||
"Translated Only": "Tylko Tłumaczenie",
|
||||
"Source Only": "Tylko Źródło",
|
||||
"TTS Text": "Tekst TTS"
|
||||
}
|
||||
|
||||
@@ -620,5 +620,9 @@
|
||||
"Tap Both Sides": "Toque em Ambos os Lados",
|
||||
"Click Both Sides": "Clique em Ambos os Lados",
|
||||
"Swap Tap Sides": "Trocar Lados do Toque",
|
||||
"Swap Click Sides": "Trocar Lados do Clique"
|
||||
"Swap Click Sides": "Trocar Lados do Clique",
|
||||
"Source and Translated": "Fonte e Traduzido",
|
||||
"Translated Only": "Apenas Traduzido",
|
||||
"Source Only": "Apenas Fonte",
|
||||
"TTS Text": "TTS Texto"
|
||||
}
|
||||
|
||||
@@ -624,5 +624,9 @@
|
||||
"Tap Both Sides": "Тапните обе стороны",
|
||||
"Click Both Sides": "Нажмите обе стороны",
|
||||
"Swap Tap Sides": "Переключить стороны касания",
|
||||
"Swap Click Sides": "Переключить стороны клика"
|
||||
"Swap Click Sides": "Переключить стороны клика",
|
||||
"Source and Translated": "Источник и перевод",
|
||||
"Translated Only": "Только перевод",
|
||||
"Source Only": "Только источник",
|
||||
"TTS Text": "TTS Текст"
|
||||
}
|
||||
|
||||
@@ -616,5 +616,9 @@
|
||||
"Tap Both Sides": "පැත්ත දෙකම ටැප් කරන්න",
|
||||
"Click Both Sides": "පැත්ත දෙකම ක්ලික් කරන්න",
|
||||
"Swap Tap Sides": "පැත්ත දෙකම ටැප් කරන්න",
|
||||
"Swap Click Sides": "පැත්ත දෙකම ක්ලික් කරන්න"
|
||||
"Swap Click Sides": "පැත්ත දෙකම ක්ලික් කරන්න",
|
||||
"Source and Translated": "මූලාශ්ර සහ පරිවර්තනය",
|
||||
"Translated Only": "පරිවර්තනය පමණක්",
|
||||
"Source Only": "මූලාශ්රය පමණක්",
|
||||
"TTS Text": "TTS පෙළ"
|
||||
}
|
||||
|
||||
@@ -616,5 +616,9 @@
|
||||
"Tap Both Sides": "Tryck på båda sidor",
|
||||
"Click Both Sides": "Klicka på båda sidor",
|
||||
"Swap Tap Sides": "Byt plats på tap-sidor",
|
||||
"Swap Click Sides": "Byt plats på klick-sidor"
|
||||
"Swap Click Sides": "Byt plats på klick-sidor",
|
||||
"Source and Translated": "Källa och översatt",
|
||||
"Translated Only": "Översatt endast",
|
||||
"Source Only": "Endast källa",
|
||||
"TTS Text": "TTS-text"
|
||||
}
|
||||
|
||||
@@ -616,5 +616,9 @@
|
||||
"Tap Both Sides": "இரு பக்கங்களையும் தொடுங்கள்",
|
||||
"Click Both Sides": "இரு பக்கங்களையும் கிளிக் செய்யவும்",
|
||||
"Swap Tap Sides": "இரு பக்கங்களின் இடத்தை மாற்றவும்",
|
||||
"Swap Click Sides": "இரு பக்கங்களின் இடத்தை மாற்றவும்"
|
||||
"Swap Click Sides": "இரு பக்கங்களின் இடத்தை மாற்றவும்",
|
||||
"Source and Translated": "மூல மற்றும் மொழிபெயர்க்கப்பட்டது",
|
||||
"Translated Only": "மொழிபெயர்க்கப்பட்டது மட்டும்",
|
||||
"Source Only": "மூல மட்டும்",
|
||||
"TTS Text": "TTS உரை"
|
||||
}
|
||||
|
||||
@@ -612,5 +612,9 @@
|
||||
"Tap Both Sides": "แตะทั้งสองด้าน",
|
||||
"Click Both Sides": "คลิกทั้งสองด้าน",
|
||||
"Swap Tap Sides": "เปลี่ยนด้านการแตะ",
|
||||
"Swap Click Sides": "เปลี่ยนด้านการคลิก"
|
||||
"Swap Click Sides": "เปลี่ยนด้านการคลิก",
|
||||
"Source and Translated": "ต้นฉบับและแปลแล้ว",
|
||||
"Translated Only": "แปลแล้วเท่านั้น",
|
||||
"Source Only": "ต้นฉบับเท่านั้น",
|
||||
"TTS Text": "ข้อความ TTS"
|
||||
}
|
||||
|
||||
@@ -616,5 +616,9 @@
|
||||
"Tap Both Sides": "Her İki Tarafa Dokunun",
|
||||
"Click Both Sides": "Her İki Tarafa Tıklayın",
|
||||
"Swap Tap Sides": "Dokunma Taraflarını Değiştir",
|
||||
"Swap Click Sides": "Tıklama Taraflarını Değiştir"
|
||||
"Swap Click Sides": "Tıklama Taraflarını Değiştir",
|
||||
"Source and Translated": "Kaynak ve Çeviri",
|
||||
"Translated Only": "Yalnızca Çeviri",
|
||||
"Source Only": "Yalnızca Kaynak",
|
||||
"TTS Text": "TTS Metni"
|
||||
}
|
||||
|
||||
@@ -624,5 +624,9 @@
|
||||
"Tap Both Sides": "Натисніть обидві сторони",
|
||||
"Click Both Sides": "Клікніть обидві сторони",
|
||||
"Swap Tap Sides": "Змінити сторони дотику",
|
||||
"Swap Click Sides": "Змінити сторони кліку"
|
||||
"Swap Click Sides": "Змінити сторони кліку",
|
||||
"Source and Translated": "Джерело та переклад",
|
||||
"Translated Only": "Тільки переклад",
|
||||
"Source Only": "Тільки джерело",
|
||||
"TTS Text": "Текст TTS"
|
||||
}
|
||||
|
||||
@@ -612,5 +612,9 @@
|
||||
"Tap Both Sides": "Chạm vào cả hai bên",
|
||||
"Click Both Sides": "Nhấp vào cả hai bên",
|
||||
"Swap Tap Sides": "Đổi bên chạm",
|
||||
"Swap Click Sides": "Đổi bên nhấp"
|
||||
"Swap Click Sides": "Đổi bên nhấp",
|
||||
"Source and Translated": "Nguồn và Đã dịch",
|
||||
"Translated Only": "Chỉ Đã dịch",
|
||||
"Source Only": "Chỉ Nguồn",
|
||||
"TTS Text": "Văn bản TTS"
|
||||
}
|
||||
|
||||
@@ -612,5 +612,9 @@
|
||||
"Tap Both Sides": "点击两侧翻页",
|
||||
"Click Both Sides": "点击两侧翻页",
|
||||
"Swap Tap Sides": "交换点击区域",
|
||||
"Swap Click Sides": "交换点击区域"
|
||||
"Swap Click Sides": "交换点击区域",
|
||||
"Source and Translated": "原文和译文",
|
||||
"Translated Only": "仅译文",
|
||||
"Source Only": "仅原文",
|
||||
"TTS Text": "TTS 文本"
|
||||
}
|
||||
|
||||
@@ -612,5 +612,9 @@
|
||||
"Tap Both Sides": "點擊兩側翻頁",
|
||||
"Click Both Sides": "點擊兩側翻頁",
|
||||
"Swap Tap Sides": "交換點擊區域",
|
||||
"Swap Click Sides": "交換點擊區域"
|
||||
"Swap Click Sides": "交換點擊區域",
|
||||
"Source and Translated": "原文和翻譯",
|
||||
"Translated Only": "僅翻譯",
|
||||
"Source Only": "僅原文",
|
||||
"TTS Text": "TTS 文字"
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ const TranslationToggler = ({ bookKey }: { bookKey: string }) => {
|
||||
<RiTranslateAi className={translationEnabled ? 'text-blue-500' : 'text-base-content'} />
|
||||
}
|
||||
aria-label={_('Toggle Translation')}
|
||||
disabled={!translationAvailable}
|
||||
disabled={!translationAvailable && !translationEnabled}
|
||||
onClick={() => setTranslationEnabled(!translationEnabled)}
|
||||
label={
|
||||
translationAvailable
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Insets } from '@/types/misc';
|
||||
import { Overlay } from '@/components/Overlay';
|
||||
import { fetchImageAsBase64 } from '@/utils/image';
|
||||
import { invokeUseBackgroundAudio } from '@/utils/bridge';
|
||||
import { getLocale } from '@/utils/misc';
|
||||
import Popup from '@/components/Popup';
|
||||
import TTSPanel from './TTSPanel';
|
||||
import TTSIcon from './TTSIcon';
|
||||
@@ -246,6 +247,27 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ttsController, bookKey]);
|
||||
|
||||
const getTTSTargetLang = useCallback((): string | null => {
|
||||
const ttsReadAloudText = viewSettings?.ttsReadAloudText;
|
||||
if (viewSettings?.translationEnabled && ttsReadAloudText === 'translated') {
|
||||
return viewSettings?.translateTargetLang || getLocale();
|
||||
} else if (viewSettings?.translationEnabled && ttsReadAloudText === 'source') {
|
||||
const bookData = getBookData(bookKey);
|
||||
return bookData?.book?.primaryLanguage || '';
|
||||
}
|
||||
return null;
|
||||
}, [
|
||||
bookKey,
|
||||
getBookData,
|
||||
viewSettings?.translationEnabled,
|
||||
viewSettings?.ttsReadAloudText,
|
||||
viewSettings?.translateTargetLang,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
ttsControllerRef.current?.setTargetLang(getTTSTargetLang() || '');
|
||||
}, [getTTSTargetLang]);
|
||||
|
||||
const handleTTSSpeak = async (event: CustomEvent) => {
|
||||
const { bookKey: ttsBookKey, range } = event.detail;
|
||||
if (bookKey !== ttsBookKey) return;
|
||||
@@ -311,6 +333,7 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
|
||||
ttsController.setLang(lang);
|
||||
ttsController.setRate(viewSettings.ttsRate);
|
||||
ttsController.speak(ssml);
|
||||
ttsController.setTargetLang(getTTSTargetLang() || '');
|
||||
ttsControllerRef.current = ttsController;
|
||||
setTtsController(ttsController);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
const [translationProvider, setTranslationProvider] = useState(viewSettings.translationProvider);
|
||||
const [translateTargetLang, setTranslateTargetLang] = useState(viewSettings.translateTargetLang);
|
||||
const [showTranslateSource, setShowTranslateSource] = useState(viewSettings.showTranslateSource);
|
||||
const [ttsReadAloudText, setTtsReadAloudText] = useState(viewSettings.ttsReadAloudText);
|
||||
|
||||
const resetToDefaults = useResetViewSettings();
|
||||
|
||||
@@ -35,6 +36,8 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
translationEnabled: setTranslationEnabled,
|
||||
translationProvider: setTranslationProvider,
|
||||
translateTargetLang: setTranslateTargetLang,
|
||||
showTranslateSource: setShowTranslateSource,
|
||||
ttsReadAloudText: setTtsReadAloudText,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -114,6 +117,20 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
setViewSettings(bookKey, { ...viewSettings });
|
||||
};
|
||||
|
||||
const handleSelectTTSText = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const option = event.target.value;
|
||||
setTtsReadAloudText(option);
|
||||
saveViewSettings(envConfig, bookKey, 'ttsReadAloudText', option, false, false);
|
||||
};
|
||||
|
||||
const getTTSTextOptions = () => {
|
||||
return [
|
||||
{ value: 'both', label: _('Source and Translated') },
|
||||
{ value: 'translated', label: _('Translated Only') },
|
||||
{ value: 'source', label: _('Source Only') },
|
||||
];
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (uiLanguage === viewSettings.uiLanguage) return;
|
||||
saveViewSettings(envConfig, bookKey, 'uiLanguage', uiLanguage, false, false);
|
||||
@@ -139,6 +156,12 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [showTranslateSource]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ttsReadAloudText === viewSettings.ttsReadAloudText) return;
|
||||
saveViewSettings(envConfig, bookKey, 'ttsReadAloudText', ttsReadAloudText, false, false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ttsReadAloudText]);
|
||||
|
||||
return (
|
||||
<div className={clsx('my-4 w-full space-y-6')}>
|
||||
<div className='w-full'>
|
||||
@@ -177,18 +200,25 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
checked={showTranslateSource}
|
||||
disabled={!translationEnabled}
|
||||
onChange={() => setShowTranslateSource(!showTranslateSource)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='config-item'>
|
||||
<span className=''>{_('TTS Text')}</span>
|
||||
<Select
|
||||
value={ttsReadAloudText}
|
||||
onChange={handleSelectTTSText}
|
||||
options={getTTSTextOptions()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='config-item'>
|
||||
<span className=''>{_('Translation Service')}</span>
|
||||
<Select
|
||||
value={getCurrentTranslationProviderOption().value}
|
||||
onChange={handleSelectTranslationProvider}
|
||||
options={getTranslationProviderOptions()}
|
||||
disabled={!translationEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -198,7 +228,6 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
value={getCurrentTargetLangOption().value}
|
||||
onChange={handleSelectTargetLang}
|
||||
options={getLangOptions(TRANSLATOR_LANGS)}
|
||||
disabled={!translationEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -226,6 +226,7 @@ export const DEFAULT_TRANSLATOR_CONFIG: TranslatorConfig = {
|
||||
translationProvider: 'deepl',
|
||||
translateTargetLang: '',
|
||||
showTranslateSource: true,
|
||||
ttsReadAloudText: 'both',
|
||||
};
|
||||
|
||||
export const DEFAULT_SCREEN_CONFIG: ScreenConfig = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FoliateView } from '@/types/view';
|
||||
import { AppService } from '@/types/system';
|
||||
import { parseSSMLMarks } from '@/utils/ssml';
|
||||
import { filterSSMLWithLang, parseSSMLMarks } from '@/utils/ssml';
|
||||
import { Overlayer } from 'foliate-js/overlayer.js';
|
||||
import { TTSGranularity, TTSHighlightOptions, TTSMark, TTSVoice } from './types';
|
||||
import { createRejectFilter } from '@/utils/node';
|
||||
@@ -39,6 +39,7 @@ export class TTSController extends EventTarget {
|
||||
ttsWebVoices: TTSVoice[] = [];
|
||||
ttsEdgeVoices: TTSVoice[] = [];
|
||||
ttsNativeVoices: TTSVoice[] = [];
|
||||
ttsTargetLang: string = '';
|
||||
|
||||
constructor(appService: AppService | null, view: FoliateView) {
|
||||
super();
|
||||
@@ -141,6 +142,9 @@ export class TTSController extends EventTarget {
|
||||
.replace(/\*/g, ' ')
|
||||
.replace(/·/g, ' ');
|
||||
|
||||
if (this.ttsTargetLang) {
|
||||
ssml = filterSSMLWithLang(ssml, this.ttsTargetLang);
|
||||
}
|
||||
return ssml;
|
||||
}
|
||||
|
||||
@@ -362,6 +366,10 @@ export class TTSController extends EventTarget {
|
||||
return this.ttsClient.getSpeakingLang();
|
||||
}
|
||||
|
||||
setTargetLang(lang: string) {
|
||||
this.ttsTargetLang = lang;
|
||||
}
|
||||
|
||||
dispatchSpeakMark(mark?: TTSMark) {
|
||||
this.dispatchEvent(new CustomEvent('tts-speak-mark', { detail: mark || { text: '' } }));
|
||||
if (mark) {
|
||||
|
||||
@@ -182,6 +182,7 @@ export interface TranslatorConfig {
|
||||
translationProvider: string;
|
||||
translateTargetLang: string;
|
||||
showTranslateSource: boolean;
|
||||
ttsReadAloudText: string;
|
||||
}
|
||||
|
||||
export interface ScreenConfig {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { RELOAD_BEFORE_SAVED_TIMEOUT_MS } from '@/services/constants';
|
||||
import { eventDispatcher } from './event';
|
||||
|
||||
export const saveAndReload = async () => {
|
||||
await eventDispatcher.dispatch('beforereload');
|
||||
setTimeout(() => window.location.reload(), RELOAD_BEFORE_SAVED_TIMEOUT_MS);
|
||||
};
|
||||
|
||||
@@ -116,3 +116,59 @@ export const findSSMLMark = (charIndex: number, marks: TTSMark[]) => {
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const filterSSMLWithLang = (
|
||||
ssml: string,
|
||||
targetLang: string,
|
||||
primaryLang?: string,
|
||||
): string => {
|
||||
const mainLang = parseSSMLLang(ssml, primaryLang);
|
||||
|
||||
// Normalize target language
|
||||
const normalizedTarget = code6392to6391(targetLang.toLowerCase()) || targetLang.toLowerCase();
|
||||
|
||||
// Check if target matches main language
|
||||
if (isSameLang(normalizedTarget, mainLang)) {
|
||||
// Remove all <lang> blocks that don't match the main language
|
||||
return ssml.replace(/<lang\s+xml:lang="([^"]+)"[^>]*>.*?<\/lang>/gs, (match, langAttr) => {
|
||||
const blockLang = code6392to6391(langAttr.toLowerCase()) || langAttr.toLowerCase();
|
||||
// If the lang block matches the main language, keep it as is
|
||||
if (isSameLang(blockLang, mainLang)) {
|
||||
return match;
|
||||
}
|
||||
// Otherwise remove the entire block
|
||||
return '';
|
||||
});
|
||||
}
|
||||
|
||||
// Check if target matches any <lang> block
|
||||
const langBlocks: Array<{ match: string; lang: string; content: string }> = [];
|
||||
const langBlockRegex = /<lang\s+xml:lang="([^"]+)"[^>]*>(.*?)<\/lang>/gs;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
const tempRegex = new RegExp(langBlockRegex.source, langBlockRegex.flags);
|
||||
while ((match = tempRegex.exec(ssml)) !== null) {
|
||||
const blockLang = code6392to6391(match[1]!.toLowerCase()) || match[1]!.toLowerCase();
|
||||
if (isSameLang(blockLang, normalizedTarget)) {
|
||||
langBlocks.push({
|
||||
match: match[0]!,
|
||||
lang: match[1]!,
|
||||
content: match[2]!,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (langBlocks.length > 0) {
|
||||
const speakOpenMatch = ssml.match(/<speak[^>]*>/i);
|
||||
const speakCloseMatch = ssml.match(/<\/speak>/i);
|
||||
|
||||
if (!speakOpenMatch || !speakCloseMatch) {
|
||||
return ssml;
|
||||
}
|
||||
|
||||
const combinedContent = langBlocks.map((block) => block.match).join('');
|
||||
return `${speakOpenMatch[0]}${combinedContent}${speakCloseMatch[0]}`;
|
||||
}
|
||||
|
||||
return ssml;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user