This commit is contained in:
@@ -466,5 +466,6 @@
|
||||
"Open Library": "المكتبة المفتوحة",
|
||||
"Fiction, Science, History": "خيال، علوم، تاريخ",
|
||||
"Select Cover Image": "اختر صورة الغلاف",
|
||||
"Open Book in New Window": "فتح الكتاب في نافذة جديدة"
|
||||
"Open Book in New Window": "فتح الكتاب في نافذة جديدة",
|
||||
"Voices for {{lang}}": "الأصوات لـ {{lang}}"
|
||||
}
|
||||
|
||||
@@ -450,5 +450,6 @@
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Belletristik, Wissenschaft, Geschichte",
|
||||
"Select Cover Image": "Coverbild auswählen",
|
||||
"Open Book in New Window": "Buch in neuem Fenster öffnen"
|
||||
"Open Book in New Window": "Buch in neuem Fenster öffnen",
|
||||
"Voices for {{lang}}": "Stimmen für {{lang}}"
|
||||
}
|
||||
|
||||
@@ -450,5 +450,6 @@
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Μυθοπλασία, Επιστήμη, Ιστορία",
|
||||
"Select Cover Image": "Επιλέξτε εικόνα εξωφύλλου",
|
||||
"Open Book in New Window": "Άνοιγμα βιβλίου σε νέο παράθυρο"
|
||||
"Open Book in New Window": "Άνοιγμα βιβλίου σε νέο παράθυρο",
|
||||
"Voices for {{lang}}": "Φωνές για {{lang}}"
|
||||
}
|
||||
|
||||
@@ -454,5 +454,6 @@
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Ficción, Ciencia, Historia",
|
||||
"Select Cover Image": "Seleccionar imagen de portada",
|
||||
"Open Book in New Window": "Abre el libro en una nueva ventana"
|
||||
"Open Book in New Window": "Abre el libro en una nueva ventana",
|
||||
"Voices for {{lang}}": "Voces para {{lang}}"
|
||||
}
|
||||
|
||||
@@ -454,5 +454,6 @@
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Fiction, Science, Histoire",
|
||||
"Select Cover Image": "Sélectionner l'image de couverture",
|
||||
"Open Book in New Window": "Ouvrir dans une nouvelle fenêtre"
|
||||
"Open Book in New Window": "Ouvrir dans une nouvelle fenêtre",
|
||||
"Voices for {{lang}}": "Voix pour {{lang}}"
|
||||
}
|
||||
|
||||
@@ -450,5 +450,6 @@
|
||||
"Open Library": "ओपन लाइब्रेरी",
|
||||
"Fiction, Science, History": "काल्पनिक, विज्ञान, इतिहास",
|
||||
"Select Cover Image": "कवर इमेज चुनें",
|
||||
"Open Book in New Window": "नई विंडो में पुस्तक खोलें"
|
||||
"Open Book in New Window": "नई विंडो में पुस्तक खोलें",
|
||||
"Voices for {{lang}}": "{{lang}} के लिए आवाज़ें"
|
||||
}
|
||||
|
||||
@@ -446,5 +446,6 @@
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Fiksi, Sains, Sejarah",
|
||||
"Select Cover Image": "Pilihan Gambar Sampul",
|
||||
"Open Book in New Window": "Buka Buku di Jendela Baru"
|
||||
"Open Book in New Window": "Buka Buku di Jendela Baru",
|
||||
"Voices for {{lang}}": "Suara untuk {{lang}}"
|
||||
}
|
||||
|
||||
@@ -454,5 +454,6 @@
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Narrativa, Scienza, Storia",
|
||||
"Select Cover Image": "Seleziona immagine di copertina",
|
||||
"Open Book in New Window": "Apri libro in una nuova finestra"
|
||||
"Open Book in New Window": "Apri libro in una nuova finestra",
|
||||
"Voices for {{lang}}": "Voci per {{lang}}"
|
||||
}
|
||||
|
||||
@@ -446,5 +446,6 @@
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "小説、科学、歴史",
|
||||
"Select Cover Image": "表紙画像を選択",
|
||||
"Open Book in New Window": "新しいウィンドウで本を開く"
|
||||
"Open Book in New Window": "新しいウィンドウで本を開く",
|
||||
"Voices for {{lang}}": "{{lang}}の音声"
|
||||
}
|
||||
|
||||
@@ -446,5 +446,6 @@
|
||||
"Open Library": "오픈 라이브러리",
|
||||
"Fiction, Science, History": "소설, 과학, 역사",
|
||||
"Select Cover Image": "표지 이미지 선택",
|
||||
"Open Book in New Window": "새 창에서 책 열기"
|
||||
"Open Book in New Window": "새 창에서 책 열기",
|
||||
"Voices for {{lang}}": "{{lang}}의 음성"
|
||||
}
|
||||
|
||||
@@ -450,5 +450,6 @@
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Fictie, Wetenschap, Geschiedenis",
|
||||
"Select Cover Image": "Selecteer omslagafbeelding",
|
||||
"Open Book in New Window": "Open boek in nieuw venster"
|
||||
"Open Book in New Window": "Open boek in nieuw venster",
|
||||
"Voices for {{lang}}": "Stemmen voor {{lang}}"
|
||||
}
|
||||
|
||||
@@ -458,5 +458,6 @@
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Beletrystyka, Nauka, Historia",
|
||||
"Select Cover Image": "Zaznacz obraz okładki",
|
||||
"Open Book in New Window": "Otwórz książkę w nowym oknie"
|
||||
"Open Book in New Window": "Otwórz książkę w nowym oknie",
|
||||
"Voices for {{lang}}": "{{lang}} Głosy"
|
||||
}
|
||||
|
||||
@@ -454,5 +454,6 @@
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Ficção, Ciência, História",
|
||||
"Select Cover Image": "Selecionar imagem da capa",
|
||||
"Open Book in New Window": "Abrir livro em nova janela"
|
||||
"Open Book in New Window": "Abrir livro em nova janela",
|
||||
"Voices for {{lang}}": "Vozes para {{lang}}"
|
||||
}
|
||||
|
||||
@@ -458,5 +458,6 @@
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Художественная литература, Наука, История",
|
||||
"Select Cover Image": "Выбрать изображение обложки",
|
||||
"Open Book in New Window": "Открыть книгу в новом окне"
|
||||
"Open Book in New Window": "Открыть книгу в новом окне",
|
||||
"Voices for {{lang}}": "Голоса для {{lang}}"
|
||||
}
|
||||
|
||||
@@ -446,5 +446,6 @@
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "นิยาย, วิทยาศาสตร์, ประวัติศาสตร์",
|
||||
"Select Cover Image": "เลือกภาพปก",
|
||||
"Open Book in New Window": "เปิดหนังสือในหน้าต่างใหม่"
|
||||
"Open Book in New Window": "เปิดหนังสือในหน้าต่างใหม่",
|
||||
"Voices for {{lang}}": "เสียงสำหรับ {{lang}}"
|
||||
}
|
||||
|
||||
@@ -450,5 +450,6 @@
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Kurgu, Bilim, Tarih",
|
||||
"Select Cover Image": "Kapak Resmi Seç",
|
||||
"Open Book in New Window": "Kitabı Yeni Pencerede Aç"
|
||||
"Open Book in New Window": "Kitabı Yeni Pencerede Aç",
|
||||
"Voices for {{lang}}": "{{lang}} için Sesler"
|
||||
}
|
||||
|
||||
@@ -458,5 +458,6 @@
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Художня література, Наукова фантастика, Історія",
|
||||
"Select Cover Image": "Оберіть зображення обкладинки",
|
||||
"Open Book in New Window": "Відкрити книгу в новому вікні"
|
||||
"Open Book in New Window": "Відкрити книгу в новому вікні",
|
||||
"Voices for {{lang}}": "Голоси для {{lang}}"
|
||||
}
|
||||
|
||||
@@ -446,5 +446,6 @@
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Tiểu thuyết, Khoa học, Lịch sử",
|
||||
"Select Cover Image": "Chọn ảnh bìa",
|
||||
"Open Book in New Window": "Open Sách trong Cửa sổ Mới"
|
||||
"Open Book in New Window": "Open Sách trong Cửa sổ Mới",
|
||||
"Voices for {{lang}}": "Giọng nói cho {{lang}}"
|
||||
}
|
||||
|
||||
@@ -317,7 +317,7 @@
|
||||
"Quota Exceeded": "额度不足",
|
||||
"{{percentage}}% of Daily Translation Characters Used.": "已使用 {{percentage}}% 的每日翻译字符数",
|
||||
"Translation Characters": "翻译字数",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} 种声音",
|
||||
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} 种语音",
|
||||
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "出了点问题。请不要担心,我们的团队已经收到通知并正在修复中。",
|
||||
"Error Details:": "错误详情:",
|
||||
"Try Again": "重试",
|
||||
@@ -446,5 +446,6 @@
|
||||
"Open Library": "开放图书馆",
|
||||
"Fiction, Science, History": "小说,科学,历史",
|
||||
"Select Cover Image": "选择封面图片",
|
||||
"Open Book in New Window": "在新窗口中打开书籍"
|
||||
"Open Book in New Window": "在新窗口中打开书籍",
|
||||
"Voices for {{lang}}": "{{lang}} 的语音"
|
||||
}
|
||||
|
||||
@@ -446,5 +446,6 @@
|
||||
"Open Library": "開放圖書館",
|
||||
"Fiction, Science, History": "小說,科學,歷史",
|
||||
"Select Cover Image": "選擇封面圖片",
|
||||
"Open Book in New Window": "在新視窗中打開書籍"
|
||||
"Open Book in New Window": "在新視窗中打開書籍",
|
||||
"Voices for {{lang}}": "{{lang}} 的語音"
|
||||
}
|
||||
|
||||
@@ -212,11 +212,7 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey }) => {
|
||||
await ttsController.initViewTTS();
|
||||
const ssml = view.tts?.from(ttsFromRange);
|
||||
if (ssml) {
|
||||
let lang = parseSSMLLang(ssml) || 'en';
|
||||
// We will not trust 'en' language from ssml, as it may be a fallback or hardcoded value
|
||||
if (lang === 'en' && primaryLang && primaryLang !== 'en') {
|
||||
lang = primaryLang.split('-')[0]!;
|
||||
}
|
||||
let lang = parseSSMLLang(ssml, primaryLang) || 'en';
|
||||
setIsPlaying(true);
|
||||
setTtsLang(lang);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { TranslationFunc, useTranslation } from '@/hooks/useTranslation';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useDefaultIconSize, useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { getLanguageName } from '@/utils/lang';
|
||||
|
||||
type TTSPanelProps = {
|
||||
bookKey: string;
|
||||
@@ -178,7 +179,19 @@ const TTSPanel = ({
|
||||
useEffect(() => {
|
||||
const fetchVoices = async () => {
|
||||
const voiceGroups = await onGetVoices(ttsLang);
|
||||
setVoiceGroups(voiceGroups);
|
||||
const voicesCount = voiceGroups.reduce((acc, group) => acc + group.voices.length, 0);
|
||||
if (!voiceGroups || voicesCount === 0) {
|
||||
console.warn('No voices found for TTSPanel');
|
||||
setVoiceGroups([
|
||||
{
|
||||
id: 'no-voices',
|
||||
name: _('Voices for {{lang}}', { lang: getLanguageName(ttsLang) }),
|
||||
voices: [],
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
setVoiceGroups(voiceGroups);
|
||||
}
|
||||
};
|
||||
fetchVoices();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -289,48 +302,44 @@ const TTSPanel = ({
|
||||
>
|
||||
{voiceGroups.map((voiceGroup, index) => {
|
||||
return (
|
||||
voiceGroup.voices.length > 0 && (
|
||||
<div key={voiceGroup.id} className=''>
|
||||
<div className='flex items-center gap-2 px-2 py-1'>
|
||||
<span
|
||||
style={{ width: `${defaultIconSize}px`, height: `${defaultIconSize}px` }}
|
||||
></span>
|
||||
<span className='text-sm text-gray-400 sm:text-xs'>
|
||||
{_('{{engine}}: {{count}} voices', {
|
||||
engine: _(voiceGroup.name),
|
||||
count: voiceGroup.voices.length,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
{voiceGroup.voices.map((voice, voiceIndex) => (
|
||||
<li
|
||||
key={`${index}-${voiceGroup.id}-${voiceIndex}`}
|
||||
onClick={() => !voice.disabled && handleSelectVoice(voice.id, voice.lang)}
|
||||
>
|
||||
<div className='flex items-center px-2'>
|
||||
<span
|
||||
style={{
|
||||
width: `${defaultIconSize}px`,
|
||||
height: `${defaultIconSize}px`,
|
||||
}}
|
||||
>
|
||||
{selectedVoice === voice.id && (
|
||||
<MdCheck className='text-base-content' />
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={clsx(
|
||||
'text-base sm:text-sm',
|
||||
voice.disabled && 'text-gray-400',
|
||||
)}
|
||||
>
|
||||
{_(voice.name)}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
<div key={voiceGroup.id} className=''>
|
||||
<div className='flex items-center gap-2 px-2 py-1'>
|
||||
<span
|
||||
style={{ width: `${defaultIconSize}px`, height: `${defaultIconSize}px` }}
|
||||
></span>
|
||||
<span className='text-sm text-gray-400 sm:text-xs'>
|
||||
{_('{{engine}}: {{count}} voices', {
|
||||
engine: _(voiceGroup.name),
|
||||
count: voiceGroup.voices.length,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
{voiceGroup.voices.map((voice, voiceIndex) => (
|
||||
<li
|
||||
key={`${index}-${voiceGroup.id}-${voiceIndex}`}
|
||||
onClick={() => !voice.disabled && handleSelectVoice(voice.id, voice.lang)}
|
||||
>
|
||||
<div className='flex items-center px-2'>
|
||||
<span
|
||||
style={{
|
||||
width: `${defaultIconSize}px`,
|
||||
height: `${defaultIconSize}px`,
|
||||
}}
|
||||
>
|
||||
{selectedVoice === voice.id && <MdCheck className='text-base-content' />}
|
||||
</span>
|
||||
<span
|
||||
className={clsx(
|
||||
'text-base sm:text-sm',
|
||||
voice.disabled && 'text-gray-400',
|
||||
)}
|
||||
>
|
||||
{_(voice.name)}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getUserLocale } from '@/utils/misc';
|
||||
import { TTSClient, TTSMessageEvent } from './TTSClient';
|
||||
import { EdgeSpeechTTS, EdgeTTSPayload } from '@/libs/edgeTTS';
|
||||
import { parseSSMLLang, parseSSMLMarks } from '@/utils/ssml';
|
||||
import { parseSSMLMarks } from '@/utils/ssml';
|
||||
import { TTSController } from './TTSController';
|
||||
import { TTSUtils } from './TTSUtils';
|
||||
import { TTSGranularity, TTSVoice, TTSVoicesGroup } from './types';
|
||||
@@ -60,19 +60,14 @@ export class EdgeTTSClient implements TTSClient {
|
||||
};
|
||||
|
||||
async *speak(ssml: string, signal: AbortSignal, preload = false) {
|
||||
const { marks } = parseSSMLMarks(ssml);
|
||||
let defaultLang = parseSSMLLang(ssml) || 'en';
|
||||
if (defaultLang === 'en' && this.#primaryLang && this.#primaryLang !== 'en') {
|
||||
defaultLang = this.#primaryLang;
|
||||
}
|
||||
const { marks } = parseSSMLMarks(ssml, this.#primaryLang);
|
||||
|
||||
if (preload) {
|
||||
// preload the first 2 marks immediately and the rest in the background
|
||||
const maxImmediate = 2;
|
||||
for (let i = 0; i < Math.min(maxImmediate, marks.length); i++) {
|
||||
const mark = marks[i]!;
|
||||
const { language } = mark;
|
||||
const voiceLang = language || defaultLang;
|
||||
const { language: voiceLang } = mark;
|
||||
const voiceId = await this.getVoiceIdFromLang(voiceLang);
|
||||
this.#currentVoiceId = voiceId;
|
||||
await this.#edgeTTS
|
||||
@@ -86,8 +81,7 @@ export class EdgeTTSClient implements TTSClient {
|
||||
for (let i = maxImmediate; i < marks.length; i++) {
|
||||
const mark = marks[i]!;
|
||||
try {
|
||||
const { language } = mark;
|
||||
const voiceLang = language || defaultLang;
|
||||
const { language: voiceLang } = mark;
|
||||
const voiceId = await this.getVoiceIdFromLang(voiceLang);
|
||||
await this.#edgeTTS.createAudio(this.getPayload(voiceLang, mark.text, voiceId));
|
||||
} catch (err) {
|
||||
@@ -116,8 +110,7 @@ export class EdgeTTSClient implements TTSClient {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
const { language } = mark;
|
||||
const voiceLang = language || defaultLang;
|
||||
const { language: voiceLang } = mark;
|
||||
const voiceId = await this.getVoiceIdFromLang(voiceLang);
|
||||
this.#speakingLang = voiceLang;
|
||||
const blob = await this.#edgeTTS.createAudio(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { addPluginListener, PluginListener } from '@tauri-apps/api/core';
|
||||
import { getUserLocale } from '@/utils/misc';
|
||||
import { parseSSMLLang, parseSSMLMarks } from '@/utils/ssml';
|
||||
import { parseSSMLMarks } from '@/utils/ssml';
|
||||
import { stubTranslation as _ } from '@/utils/misc';
|
||||
import { TTSClient, TTSMessageEvent } from './TTSClient';
|
||||
import { TTSGranularity, TTSMark, TTSVoice, TTSVoicesGroup } from './types';
|
||||
@@ -100,13 +100,12 @@ export class NativeTTSClient implements TTSClient {
|
||||
return this.initialized;
|
||||
}
|
||||
|
||||
async *speakMark(mark: TTSMark, preload: boolean, defaultLang: string, signal: AbortSignal) {
|
||||
async *speakMark(mark: TTSMark, preload: boolean, signal: AbortSignal) {
|
||||
if (preload) {
|
||||
yield { code: 'end', message: 'Dummy preload finished' } as TTSMessageEvent;
|
||||
return;
|
||||
}
|
||||
const { language } = mark;
|
||||
const voiceLang = language || defaultLang;
|
||||
const { language: voiceLang } = mark;
|
||||
const voiceId = await this.getVoiceIdFromLang(voiceLang);
|
||||
this.#currentVoiceId = voiceId;
|
||||
this.#speakingLang = voiceLang;
|
||||
@@ -179,14 +178,10 @@ export class NativeTTSClient implements TTSClient {
|
||||
}
|
||||
|
||||
async *speak(ssml: string, signal: AbortSignal, preload: boolean = false) {
|
||||
const { marks } = parseSSMLMarks(ssml);
|
||||
let defaultLang = parseSSMLLang(ssml) || 'en';
|
||||
if (defaultLang === 'en' && this.#primaryLang && this.#primaryLang !== 'en') {
|
||||
defaultLang = this.#primaryLang;
|
||||
}
|
||||
const { marks } = parseSSMLMarks(ssml, this.#primaryLang);
|
||||
|
||||
for (const mark of marks) {
|
||||
for await (const ev of this.speakMark(mark, preload, defaultLang, signal)) {
|
||||
for await (const ev of this.speakMark(mark, preload, signal)) {
|
||||
if (signal.aborted) {
|
||||
yield { code: 'error', message: 'Aborted' } as TTSMessageEvent;
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getUserLocale } from '@/utils/misc';
|
||||
import { TTSClient, TTSMessageEvent } from './TTSClient';
|
||||
import { parseSSMLLang, parseSSMLMarks } from '@/utils/ssml';
|
||||
import { parseSSMLMarks } from '@/utils/ssml';
|
||||
import { TTSGranularity, TTSMark, TTSVoice, TTSVoicesGroup } from './types';
|
||||
import { WEB_SPEECH_BLACKLISTED_VOICES } from './TTSData';
|
||||
import { TTSController } from './TTSController';
|
||||
@@ -18,7 +18,7 @@ interface TTSBoundaryEvent {
|
||||
|
||||
async function* speakWithMarks(
|
||||
ssml: string,
|
||||
defaultLang: string,
|
||||
primaryLang: string,
|
||||
getRate: () => number,
|
||||
getPitch: () => number,
|
||||
getVoice: (lang: string) => Promise<SpeechSynthesisVoice | null>,
|
||||
@@ -26,14 +26,11 @@ async function* speakWithMarks(
|
||||
setSpeakingLang: (lang: string) => void,
|
||||
dispatchSpeakMark: (mark: TTSMark) => void,
|
||||
) {
|
||||
const { marks } = parseSSMLMarks(ssml);
|
||||
|
||||
const { marks } = parseSSMLMarks(ssml, primaryLang);
|
||||
const synth = window.speechSynthesis;
|
||||
|
||||
const utterance = new SpeechSynthesisUtterance();
|
||||
for (const mark of marks) {
|
||||
const { language } = mark;
|
||||
const voiceLang = language || defaultLang;
|
||||
const { language: voiceLang } = mark;
|
||||
dispatchSpeakMark(mark);
|
||||
utterance.text = mark.text;
|
||||
utterance.rate = getRate();
|
||||
@@ -149,14 +146,9 @@ export class WebSpeechClient implements TTSClient {
|
||||
// no need to preload for web speech
|
||||
if (preload) return;
|
||||
|
||||
let defaultLang = parseSSMLLang(ssml) || 'en';
|
||||
if (defaultLang === 'en' && this.#primaryLang && this.#primaryLang !== 'en') {
|
||||
defaultLang = this.#primaryLang;
|
||||
}
|
||||
|
||||
for await (const ev of speakWithMarks(
|
||||
ssml,
|
||||
defaultLang,
|
||||
this.#primaryLang,
|
||||
() => this.#rate,
|
||||
() => this.#pitch,
|
||||
this.getWebSpeechVoiceFromLang,
|
||||
|
||||
@@ -23,5 +23,5 @@ export type TTSMark = {
|
||||
offset: number;
|
||||
name: string;
|
||||
text: string;
|
||||
language?: string;
|
||||
language: string;
|
||||
};
|
||||
|
||||
@@ -68,7 +68,32 @@ export const isSameLang = (lang1?: string | null, lang2?: string | null): boolea
|
||||
return normalizedLang1 === normalizedLang2;
|
||||
};
|
||||
|
||||
export const isValidLang = (lang?: string) => {
|
||||
if (!lang) return false;
|
||||
const code = lang.split('-')[0]!.toLowerCase();
|
||||
return iso6392.some((l) => l.iso6391 === code || l.iso6392B === code);
|
||||
};
|
||||
|
||||
export const code6392to6391 = (code: string): string => {
|
||||
const lang = iso6392.find((l) => l.iso6392B === code);
|
||||
return lang?.iso6391 || '';
|
||||
};
|
||||
|
||||
export const getLanguageName = (code: string): string => {
|
||||
const lang = normalizedLangCode(code);
|
||||
const language = iso6392.find((l) => l.iso6391 === lang || l.iso6392B === lang);
|
||||
return language ? language.name : lang;
|
||||
};
|
||||
|
||||
export const inferLangFromScript = (text: string, lang: string): string => {
|
||||
if (!lang || lang === 'en') {
|
||||
if (/[\p{Script=Hangul}]/u.test(text)) {
|
||||
return 'ko';
|
||||
} else if (/[\p{Script=Hiragana}\p{Script=Katakana}]/u.test(text)) {
|
||||
return 'ja';
|
||||
} else if (/[\p{Script=Han}]/u.test(text)) {
|
||||
return 'zh';
|
||||
}
|
||||
}
|
||||
return lang;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TTSMark } from '@/services/tts/types';
|
||||
import { inferLangFromScript, isSameLang, isValidLang } from './lang';
|
||||
|
||||
const cleanTextContent = (text: string) =>
|
||||
text.replace(/\r\n/g, ' ').replace(/\r/g, ' ').replace(/\n/g, ' ').trimStart();
|
||||
@@ -16,9 +17,28 @@ export const genSSML = (lang: string, text: string, voice: string, rate: number)
|
||||
`;
|
||||
};
|
||||
|
||||
export const parseSSMLMarks = (ssml: string) => {
|
||||
const speakMatch = ssml.match(/<speak[^>]*xml:lang="([^"]+)"[^>]*>/i);
|
||||
const defaultLang = speakMatch?.[1] ?? 'en';
|
||||
export const parseSSMLLang = (ssml: string, primaryLang?: string): string => {
|
||||
let lang = 'en';
|
||||
const match = ssml.match(/xml:lang\s*=\s*"([^"]+)"/);
|
||||
if (match && match[1]) {
|
||||
const parts = match[1].split('-');
|
||||
lang =
|
||||
parts.length > 1
|
||||
? `${parts[0]!.toLowerCase()}-${parts[1]!.toUpperCase()}`
|
||||
: parts[0]!.toLowerCase();
|
||||
|
||||
if (!isValidLang(lang)) {
|
||||
lang = 'en';
|
||||
}
|
||||
}
|
||||
if (lang === 'en' && primaryLang && !isSameLang(lang, primaryLang)) {
|
||||
lang = primaryLang.split('-')[0]!.toLowerCase();
|
||||
}
|
||||
return inferLangFromScript(ssml, lang);
|
||||
};
|
||||
|
||||
export const parseSSMLMarks = (ssml: string, primaryLang?: string) => {
|
||||
const defaultLang = parseSSMLLang(ssml, primaryLang) || 'en';
|
||||
ssml = ssml.replace(/<speak[^>]*>/i, '').replace(/<\/speak>/i, '');
|
||||
|
||||
let plainText = '';
|
||||
@@ -93,29 +113,3 @@ export const findSSMLMark = (charIndex: number, marks: TTSMark[]) => {
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const inferLangFromScript = (text: string, lang: string | null): string | null => {
|
||||
if (!lang || lang.startsWith('en')) {
|
||||
if (/[\p{Script=Hangul}]/u.test(text)) {
|
||||
return 'ko';
|
||||
} else if (/[\p{Script=Hiragana}\p{Script=Katakana}]/u.test(text)) {
|
||||
return 'ja';
|
||||
} else if (/[\p{Script=Han}]/u.test(text)) {
|
||||
return 'zh';
|
||||
}
|
||||
}
|
||||
return lang;
|
||||
};
|
||||
|
||||
export const parseSSMLLang = (ssml: string): string | null => {
|
||||
let lang = null;
|
||||
const match = ssml.match(/xml:lang\s*=\s*"([^"]+)"/);
|
||||
if (match && match[1]) {
|
||||
const parts = match[1].split('-');
|
||||
lang =
|
||||
parts.length > 1
|
||||
? `${parts[0]!.toLowerCase()}-${parts[1]!.toUpperCase()}`
|
||||
: parts[0]!.toLowerCase();
|
||||
}
|
||||
return inferLangFromScript(ssml, lang);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user