// vtt loader export type Record = { text?: string, time_start?: string, time_end?: string, ext_param?: unknown }; export type NullRecord = Record | null; function loadVtt(vttStr: string) { const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/; const lines = vttStr.replace(/\r?\n/g, '\n').split('\n'); const data: Record[] = []; let lineBuf: string[] = [], record: NullRecord = null; // check lines for (const l of lines) { const m = l.match(rx); if (m) { if (lineBuf.length > 0) { lineBuf.pop(); } if (record !== null) { record.text = lineBuf.join('\n'); data.push(record); } record = { time_start: m[1], time_end: m[2], ext_param: m[3].split(' ').map(x => x.split(':')).reduce((p: any, c: any) => (p[c[0]] = c[1]) && p, {}), }; lineBuf = []; continue; } lineBuf.push(l); } if (record !== null) { if (lineBuf[lineBuf.length - 1] === '') { lineBuf.pop(); } record.text = lineBuf.join('\n'); data.push(record); } return data; } // ass specific function convertToAss(vttStr: string, lang: string, fontSize: number, fontName?: string){ let ass = [ '\ufeff[Script Info]', `Title: ${lang}`, 'ScriptType: v4.00+', 'PlayResX: 1280', 'PlayResY: 720', 'WrapStyle: 0', 'ScaledBorderAndShadow: yes', '', '[V4+ Styles]', 'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, ' + 'Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, ' + 'BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding', `Style: Main,${fontName || 'Noto Sans'},${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,10,10,10,1`, `Style: MainTop,${fontName || 'Noto Sans'},${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,8,10,10,10,1`, '', '[Events]', 'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text', ]; const vttData = loadVtt(vttStr); for (const l of vttData) { const line = convertToAssLine(l, 'Main'); ass = ass.concat(line); } return ass.join('\r\n') + '\r\n'; } function convertToAssLine(l: Record, style: string) { const start = convertTime(l.time_start as string); const end = convertTime(l.time_end as string); const text = convertToAssText(l.text as string); // debugger if ((l.ext_param as any).line === '7%') { style = 'MainTop'; } if ((l.ext_param as any).line === '10%') { style = 'MainTop'; } return `Dialogue: 0,${start},${end},${style},,0,0,0,,${text}`; } function convertToAssText(text: string) { text = text .replace(/\r/g, '') .replace(/\n/g, '\\N') .replace(/\\N +/g, '\\N') .replace(/ +\\N/g, '\\N') .replace(/(\\N)+/g, '\\N') .replace(/]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}') .replace(/]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}') .replace(/]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}') .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&') .replace(/<[^>]>/g, '') .replace(/\\N$/, '') .replace(/ +$/, ''); return text; } // srt specific function convertToSrt(vttStr: string){ let srt: string[] = [], srtLineIdx = 0; const vttData = loadVtt(vttStr); for (const l of vttData) { srtLineIdx++; const line = convertToSrtLine(l, srtLineIdx); srt = srt.concat(line); } return srt.join('\r\n') + '\r\n'; } function convertToSrtLine(l: Record, idx: number) : string { const bom = idx == 1 ? '\ufeff' : ''; const start = convertTime(l.time_start as string, true); const end = convertTime(l.time_end as string, true); const text = l.text; return `${bom}${idx}\r\n${start} --> ${end}\r\n${text}\r\n`; } // time parser function convertTime(time: string, srtFormat = false) { const mTime = time.match(/([\d:]*)\.?(\d*)/); if (!mTime){ return srtFormat ? '00:00:00,000' : '0:00:00.00'; } return toSubsTime(mTime[0], srtFormat); } function toSubsTime(str: string, srtFormat: boolean) : string { const n: string[] = [], x: (string|number)[] = str.split(/[:.]/).map(x => Number(x)); let sx; const msLen = srtFormat ? 3 : 2; const hLen = srtFormat ? 2 : 1; x[3] = '0.' + ('' + x[3]).padStart(3, '0'); sx = (x[0] as number)*60*60 + (x[1] as number)*60 + (x[2] as number) + Number(x[3]); sx = sx.toFixed(msLen).split('.'); n.unshift(padTimeNum((srtFormat ? ',' : '.'), sx[1], msLen)); sx = Number(sx[0]); n.unshift(padTimeNum(':', sx%60, 2)); n.unshift(padTimeNum(':', Math.floor(sx/60)%60, 2)); n.unshift(padTimeNum('', Math.floor(sx/3600)%60, hLen)); return n.join(''); } function padTimeNum(sep: string, input: string|number , pad:number){ return sep + ('' + input).padStart(pad, '0'); } // export module const _default = (vttStr: string, toSrt: boolean, lang = 'English', fontSize: number, fontName?: string) => { const convert = toSrt ? convertToSrt : convertToAss; return convert(vttStr, lang, fontSize, fontName); }; export default _default;