type TTSMark = { offset: number; name: string; text: string; }; export const parseSSMLMarks = (ssml: string) => { ssml = ssml.replace(/]*>/i, ''); ssml = ssml.replace(/<\/speak>/i, ''); const markRegex = //g; let plainText = ''; const marks: TTSMark[] = []; let match; while ((match = markRegex.exec(ssml)) !== null) { const markTagEndIndex = markRegex.lastIndex; const nextMarkIndex = ssml.indexOf(']+>/g, '') .replace(/\r\n/g, ' ') .replace(/\r/g, ' ') .replace(/\n/g, ' ') .trimStart(); plainText += cleanedChunk; const offset = plainText.length - cleanedChunk.length; const markName = match[1]!; marks.push({ offset, name: markName, text: cleanedChunk }); } return { plainText, marks }; }; export const findSSMLMark = (charIndex: number, marks: TTSMark[]) => { let left = 0; let right = marks.length - 1; let result: TTSMark | null = null; while (left <= right) { const mid = Math.floor((left + right) / 2); const mark = marks[mid]!; if (mark.offset <= charIndex) { result = mark; left = mid + 1; } else { right = mid - 1; } } return result; }; 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(); } if (!lang || lang.startsWith('en')) { if (/[\p{Script=Hangul}]/u.test(ssml)) { lang = 'ko'; } else if (/[\p{Script=Hiragana}\p{Script=Katakana}]/u.test(ssml)) { lang = 'ja'; } else if (/[\p{Script=Han}]/u.test(ssml)) { lang = 'zh'; } } return lang; };