UNPKG

justlyrics

Version:

A comprehensive TypeScript library for parsing, converting, and processing various lyric formats including LRC, ALRC, YRC, QRC, and more

784 lines (783 loc) 31.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LyricFormat = exports.LyricIO = exports.CoreLyric = void 0; const utils_1 = require("./utils"); var CoreLyric; (function (CoreLyric) { class Timestamp { static max(...timestamps) { return new Timestamp(Math.max(...timestamps .filter((t) => t !== null) .map((t) => t.milliSeconds))); } static min(...timestamps) { return new Timestamp(Math.min(...timestamps .filter((t) => t !== null) .map((t) => t.milliSeconds))); } constructor(milliSeconds) { this.milliSeconds = 0; this.milliSeconds = milliSeconds; } seconds() { return this.milliSeconds / 1000; } formatted() { const seconds = Math.floor(this.seconds()); const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`; } } CoreLyric.Timestamp = Timestamp; let VoiceAgentType; (function (VoiceAgentType) { VoiceAgentType["Vocal"] = "vocal"; VoiceAgentType["BackgroundVocal"] = "background-vocal"; VoiceAgentType["Other"] = "other"; })(VoiceAgentType = CoreLyric.VoiceAgentType || (CoreLyric.VoiceAgentType = {})); let LineAnnotationRole; (function (LineAnnotationRole) { LineAnnotationRole["Prononciation"] = "pron"; LineAnnotationRole["Translation"] = "trans"; })(LineAnnotationRole = CoreLyric.LineAnnotationRole || (CoreLyric.LineAnnotationRole = {})); class SyllableSyncedLine { constructor(opt) { this.syllables = opt.syllables; this.voiceAgent = opt.voiceAgent; this.annotations = opt.annotations; } toLineSyncedLine() { const lineText = this.syllables.map((s) => s.text).join(''); const lineStart = Timestamp.min(...this.syllables .filter((syl) => syl.start?.milliSeconds != 0 || syl.end?.milliSeconds != 0 || syl.text.trim() != '') .map((s) => s.start)); const lineEnd = Timestamp.max(...this.syllables .filter((syl) => syl.start?.milliSeconds != 0 || syl.end?.milliSeconds != 0 || syl.text.trim() != '') .map((s) => s.end)); return new LineSyncedLine({ text: lineText, start: lineStart, end: lineEnd, voiceAgent: this.voiceAgent, annotations: this.annotations, }); } extractAnnotations(role) { return this.toLineSyncedLine().extractAnnotations(role); } } CoreLyric.SyllableSyncedLine = SyllableSyncedLine; class LineSyncedLine { static checkOverlap(a, b) { if (a.start === null || a.end === null || b.start === null || b.end === null) { return false; } const res = a.start.milliSeconds < b.end.milliSeconds && b.start.milliSeconds < a.end.milliSeconds; return res; } static fromText(tr) { return new LineSyncedLine({ text: tr, }); } constructor(opt) { this.text = opt.text; this.start = opt.start || null; this.end = opt.end || null; this.voiceAgent = opt.voiceAgent || null; this.annotations = opt.annotations || []; } extractAnnotations(role) { return this.annotations .filter((a) => a.role === role) .map((a) => { const l2 = a.line; l2.start = this.start; l2.end = this.end; return l2; }); } } CoreLyric.LineSyncedLine = LineSyncedLine; let Utils; (function (Utils) { function extractLineAnnotations(lines, role) { return lines.flatMap((line) => { if (line instanceof SyllableSyncedLine || line instanceof LineSyncedLine) { return line.extractAnnotations(role); } else { return []; } }); } Utils.extractLineAnnotations = extractLineAnnotations; function splitWordSyllables(syl) { const res = []; let buf = ''; let remaining = syl.text; while (remaining.length > 0) { const c = remaining[0]; if ((0, utils_1.isCJKChar)(c) && buf.length > 0 && buf != '(' && buf != '(') { res.push({ text: buf, start: null, end: null, annotations: syl.annotations, }); buf = ''; } buf += c; remaining = remaining.slice(1); } if (buf.length > 0) { if (buf == ')' || buf == ')') res[res.length - 1].text += buf; else res.push({ text: buf, start: null, end: null, annotations: syl.annotations, }); } const totalDur = syl.end?.milliSeconds - syl.start?.milliSeconds; let pos = 0; // reassign time for (const i in res) { const partLength = (totalDur / syl.text.replace(/[\(\)()]/g, '').length) * res[i].text.replace(/[\(\)()]/g, '').length; const s = res[i]; s.start = new Timestamp(Math.round(syl.start?.milliSeconds + pos)); s.end = new Timestamp(Math.round(syl.start?.milliSeconds + pos + partLength)); pos += partLength; } return res; } Utils.splitWordSyllables = splitWordSyllables; function preprocessLinesForLyricify(lines) { return lines.map((line) => { if (line instanceof SyllableSyncedLine) { const newSyls = line.syllables.flatMap(splitWordSyllables); return new SyllableSyncedLine({ syllables: newSyls, voiceAgent: line.voiceAgent, annotations: line.annotations, }); } else { return line; } }); } Utils.preprocessLinesForLyricify = preprocessLinesForLyricify; })(Utils = CoreLyric.Utils || (CoreLyric.Utils = {})); })(CoreLyric || (exports.CoreLyric = CoreLyric = {})); var LyricIO; (function (LyricIO) { let Abstraction; (function (Abstraction) { let ALRC; (function (ALRC) { let ALRCStylePosition; (function (ALRCStylePosition) { ALRCStylePosition[ALRCStylePosition["Undefined"] = 0] = "Undefined"; ALRCStylePosition[ALRCStylePosition["Left"] = 1] = "Left"; ALRCStylePosition[ALRCStylePosition["Center"] = 2] = "Center"; ALRCStylePosition[ALRCStylePosition["Right"] = 3] = "Right"; })(ALRCStylePosition = ALRC.ALRCStylePosition || (ALRC.ALRCStylePosition = {})); let ALRCStyleAccent; (function (ALRCStyleAccent) { ALRCStyleAccent[ALRCStyleAccent["Normal"] = 0] = "Normal"; ALRCStyleAccent[ALRCStyleAccent["Background"] = 1] = "Background"; ALRCStyleAccent[ALRCStyleAccent["Whisper"] = 2] = "Whisper"; ALRCStyleAccent[ALRCStyleAccent["Emphasise"] = 3] = "Emphasise"; })(ALRCStyleAccent = ALRC.ALRCStyleAccent || (ALRC.ALRCStyleAccent = {})); })(ALRC = Abstraction.ALRC || (Abstraction.ALRC = {})); })(Abstraction = LyricIO.Abstraction || (LyricIO.Abstraction = {})); let Dumping; (function (Dumping) { let YRC; (function (YRC) { function createHeaderLines(data) { const headerLines = []; const lyricistsLine = []; for (let idx = 0; idx < data.lyricists.length; idx++) { const lyricist = data.lyricists[idx]; if (idx > 0) lyricistsLine.push({ tx: '/' }); lyricistsLine.push({ tx: lyricist }); } const musiciansLine = []; for (let idx = 0; idx < data.musicians.length; idx++) { const musician = data.musicians[idx]; if (idx > 0) musiciansLine.push({ tx: '/' }); musiciansLine.push({ tx: musician }); } if (lyricistsLine.length > 0) headerLines.push({ t: 0, c: [{ tx: '作词: ' }, ...lyricistsLine], }); if (musiciansLine.length > 0) headerLines.push({ t: 0, c: [{ tx: '作曲: ' }, ...musiciansLine], }); return headerLines; } YRC.createHeaderLines = createHeaderLines; })(YRC = Dumping.YRC || (Dumping.YRC = {})); let LQE; (function (LQE) { function createLQEPartHeader(data) { return Object.entries(data) .map(([key, value]) => `${key}@${value}`) .join(', '); } LQE.createLQEPartHeader = createLQEPartHeader; })(LQE = Dumping.LQE || (Dumping.LQE = {})); function dumpLYS(lines) { let editorBuffer = ''; let lineStart = null; // compile to buffer const linesData = []; const properties = []; let i = 0; for (const line of lines) { const property = (line.voiceAgent?.type == CoreLyric.VoiceAgentType.BackgroundVocal ? 6 : 3) + (line.voiceAgent?.n != 2 ? 1 : 2); properties.push(property); const lineData = []; if (line instanceof CoreLyric.SyllableSyncedLine) { for (const syllable of line.syllables) { lineStart = CoreLyric.Timestamp.min(lineStart, syllable.start); lineData.push({ text: syllable.text, start: syllable.start?.milliSeconds || 0, end: syllable.end?.milliSeconds || 0, }); } } else if (line instanceof CoreLyric.LineSyncedLine) { lineStart = line.start; lineData.push({ text: line.text, start: line.start?.milliSeconds || 0, end: line.end?.milliSeconds || 0, }); } linesData.push(lineData); if (i > 0 && lineStart != null) { const last = linesData[i - 1]; if (last instanceof CoreLyric.LineSyncedLine) { last.end = lineStart; } } i += 1; } i = 0; for (const lineData of linesData) { editorBuffer += `[${properties[i]}]`; for (const syllableData of lineData) { editorBuffer += `${syllableData.text}(${Math.round(syllableData.start)},${Math.round(syllableData.end - syllableData.start)})`; } editorBuffer += '\n'; i += 1; } return editorBuffer.trim(); } Dumping.dumpLYS = dumpLYS; function dumpLRC(lines) { let editorBuffer = ''; for (const line of lines) { let lineSyncedLine; if (line instanceof CoreLyric.SyllableSyncedLine) { lineSyncedLine = line.toLineSyncedLine(); } else { lineSyncedLine = line; } const start = lineSyncedLine.start; if (start) { const minutes = Math.floor(start.milliSeconds / 60000); const seconds = Math.floor((start.milliSeconds % 60000) / 1000); const milliseconds = start.milliSeconds % 1000; editorBuffer += `[${minutes .toString() .padStart(2, '0')}:${seconds .toString() .padStart(2, '0')}.${milliseconds .toString() .padStart(3, '0')}]${lineSyncedLine.text}\n`; } } return editorBuffer.trim(); } Dumping.dumpLRC = dumpLRC; function dumpLYL(lines) { let editorBuffer = ''; for (const line of lines) { let lineSyncedLine; if (line instanceof CoreLyric.SyllableSyncedLine) { lineSyncedLine = line.toLineSyncedLine(); } else { lineSyncedLine = line; } const start = lineSyncedLine.start; const end = lineSyncedLine.end; if (start) { editorBuffer += `[${Math.round(start.milliSeconds)},${Math.round(end?.milliSeconds || 0)}]${lineSyncedLine.text}\n`; } } return editorBuffer.trim(); } Dumping.dumpLYL = dumpLYL; function dumpQRC(lines) { let editorBuffer = ''; for (const line of lines) { let lineSyncedLine; if (line instanceof CoreLyric.SyllableSyncedLine) { lineSyncedLine = line.toLineSyncedLine(); } else { lineSyncedLine = line; } const start = lineSyncedLine.start; const end = lineSyncedLine.end; const endMilliSeconds = end?.milliSeconds || null; const durMillis = endMilliSeconds && start ? endMilliSeconds - start.milliSeconds : null; if (line instanceof CoreLyric.SyllableSyncedLine) { if (start) { editorBuffer += `[${Math.round(start.milliSeconds)},${Math.round(durMillis || 0)}]`; for (const syllable of line.syllables) { const sylDurMillis = syllable.end && syllable.start ? syllable.end.milliSeconds - syllable.start.milliSeconds : null; editorBuffer += `${syllable.text}(${Math.round(syllable.start?.milliSeconds || 0)},${Math.round(sylDurMillis || 0)})`; } editorBuffer += '\n'; } } else if (line instanceof CoreLyric.LineSyncedLine) { if (start) { editorBuffer += `[${Math.round(start.milliSeconds)},${Math.round(durMillis || 0)}]${lineSyncedLine.text}(${Math.round(start.milliSeconds)},${Math.round(durMillis || 0)})\n`; } } } return editorBuffer.trim(); } Dumping.dumpQRC = dumpQRC; function dumpYRC(lines) { let editorBuffer = ''; for (const line of lines) { let lineSyncedLine; if (line instanceof CoreLyric.SyllableSyncedLine) { lineSyncedLine = line.toLineSyncedLine(); } else { lineSyncedLine = line; } const start = lineSyncedLine.start; const end = lineSyncedLine.end; const endMilliSeconds = end?.milliSeconds || null; const durMillis = endMilliSeconds && start ? endMilliSeconds - start.milliSeconds : null; if (line instanceof CoreLyric.SyllableSyncedLine) { if (start) { editorBuffer += `[${Math.round(start.milliSeconds)},${Math.round(durMillis || 0)}]`; for (const syllable of line.syllables) { const sylDurMillis = syllable.end && syllable.start ? syllable.end.milliSeconds - syllable.start.milliSeconds : null; editorBuffer += `(${Math.round(syllable.start?.milliSeconds || 0)},${Math.round(sylDurMillis || 0)},0)${syllable.text}`; } editorBuffer += '\n'; } } else if (line instanceof CoreLyric.LineSyncedLine) { if (start) { editorBuffer += `[${Math.round(start.milliSeconds)},${Math.round(durMillis || 0)}](${Math.round(start.milliSeconds)},${Math.round(durMillis || 0)},0)${lineSyncedLine.text}\n`; } } } return editorBuffer.trim(); } Dumping.dumpYRC = dumpYRC; function dumpALRC(lines) { const alrcData = { li: {}, si: null, h: { s: [], }, l: [], }; for (const line of lines) { if (line instanceof CoreLyric.SyllableSyncedLine) { const alrcLine = { w: line.syllables.map((syllable) => ({ f: syllable.start?.milliSeconds || 0, t: syllable.end?.milliSeconds || 0, w: syllable.text, })), f: CoreLyric.Timestamp.min(...line.syllables.map((syllable) => syllable.start)).milliSeconds, t: CoreLyric.Timestamp.max(...line.syllables.map((syllable) => syllable.end)).milliSeconds, }; alrcData.l.push(alrcLine); } else if (line instanceof CoreLyric.LineSyncedLine) { const alrcLine = { tx: line.text, f: line.start?.milliSeconds || 0, t: line.end?.milliSeconds || 0, }; alrcData.l.push(alrcLine); } } return JSON.stringify(alrcData); } Dumping.dumpALRC = dumpALRC; function dumpSRT(lines) { let editorBuffer = ''; let index = 1; for (const line of lines) { let lineSyncedLine; if (line instanceof CoreLyric.SyllableSyncedLine) { lineSyncedLine = line.toLineSyncedLine(); } else { lineSyncedLine = line; } const start = lineSyncedLine.start; const end = lineSyncedLine.end; if (start && end) { const startTime = new Date(start.milliSeconds); const endTime = new Date(end.milliSeconds); const startStr = `${startTime .getUTCHours() .toString() .padStart(2, '0')}:${startTime .getUTCMinutes() .toString() .padStart(2, '0')}:${startTime .getUTCSeconds() .toString() .padStart(2, '0')},${(start.milliSeconds % 1000) .toString() .padStart(3, '0')}`; const endStr = `${endTime .getUTCHours() .toString() .padStart(2, '0')}:${endTime .getUTCMinutes() .toString() .padStart(2, '0')}:${endTime .getUTCSeconds() .toString() .padStart(2, '0')},${(end.milliSeconds % 1000) .toString() .padStart(3, '0')}`; let text = lineSyncedLine.text.trim(); if (lineSyncedLine.voiceAgent && lineSyncedLine.voiceAgent.type == CoreLyric.VoiceAgentType.BackgroundVocal) { text = `(${text})`; } editorBuffer += `${index}\n${startStr} --> ${endStr}\n${text}\n\n`; index++; } } return editorBuffer.trim(); } Dumping.dumpSRT = dumpSRT; function dumpLQE(lines) { let result = ''; const transLy = CoreLyric.Utils.extractLineAnnotations(lines, CoreLyric.LineAnnotationRole.Translation); const pronLy = CoreLyric.Utils.extractLineAnnotations(lines, CoreLyric.LineAnnotationRole.Prononciation); // original lyrics result += `[lyrics: ${LyricIO.Dumping.LQE.createLQEPartHeader({ format: 'Lyricify Syllable', // language: 'en-US', })}]\n`; result += dumpLYS(lines).trimEnd(); result += '\n\n'; // translation if (transLy.length > 0) { result += `\n[translation: ${LyricIO.Dumping.LQE.createLQEPartHeader({ format: 'LRC', // language: 'zh-CN', })}]\n`; result += dumpLRC(transLy).trimEnd(); result += '\n\n'; } // pronunciation if (pronLy.length > 0) { result += `\n[pronunciation: ${LyricIO.Dumping.LQE.createLQEPartHeader({ format: 'LRC', language: 'romaji', })}]\n`; result += dumpLRC(pronLy).trimEnd(); result += '\n\n'; } return result.trim(); } Dumping.dumpLQE = dumpLQE; function dumpSPL(lines) { function timeMark(ms) { const minutes = Math.floor(ms / 60000); const seconds = Math.floor((ms % 60000) / 1000); const milliseconds = ms % 1000; return `[${minutes.toString().padStart(2, '0')}:${seconds .toString() .padStart(2, '0')}.${milliseconds .toString() .padStart(3, '0')}]`; } let editorBuffer = ''; for (const line of lines) { let lineSyncedLine; if (line instanceof CoreLyric.SyllableSyncedLine) { lineSyncedLine = line.toLineSyncedLine(); } else { lineSyncedLine = line; } const start = lineSyncedLine.start; const end = lineSyncedLine.end; if (line instanceof CoreLyric.SyllableSyncedLine) { if (start && end) { let lastMarkedTime = null; for (const syllable of line.syllables) { if (syllable.start?.milliSeconds != lastMarkedTime) { editorBuffer += timeMark(syllable.start?.milliSeconds || 0); } editorBuffer += syllable.text; editorBuffer += timeMark(syllable.end?.milliSeconds || 0); lastMarkedTime = syllable.end?.milliSeconds || 0; } editorBuffer += '\n'; } } else if (line instanceof CoreLyric.LineSyncedLine) { if (start && end) { // editorBuffer += `${timeMark(start.milliSeconds || 0)}${lineSyncedLine.text}${timeMark(end?.milliSeconds || 0)}\n` editorBuffer += timeMark(start.milliSeconds || 0); editorBuffer += lineSyncedLine.text; editorBuffer += timeMark(end?.milliSeconds || 0); editorBuffer += '\n'; } } for (const annotation of line.annotations) { if (annotation.role == CoreLyric.LineAnnotationRole.Translation) { editorBuffer += annotation.line.text; editorBuffer += '\n'; break; } } } return editorBuffer.trim(); } Dumping.dumpSPL = dumpSPL; function selectDumpWorker(type) { switch (type) { case 'lys': return dumpLYS; case 'lyl': return dumpLYL; case 'lrc': return dumpLRC; case 'qrc': return dumpQRC; case 'alrc': return dumpALRC; case 'srt': return dumpSRT; case 'yrc': return dumpYRC; case 'spl': return dumpSPL; case 'lqe': return dumpLQE; case 'ttml': return null; // Unsupported case 'ttml_amll': return null; // Unsupported case 'apple_syllable': return null; // Unsupported case 'krc': return null; // Unsupported // Unsupported default: { console.error(`Unsupported format: ${type}`); return null; } } } function dump(type, lines) { const worker = selectDumpWorker(type); if (!worker) { return '[ERROR] Unsupported'; } let preprocessedLines; switch (type) { case 'lys': case 'lqe': case 'yrc': case 'qrc': case 'krc': preprocessedLines = CoreLyric.Utils.preprocessLinesForLyricify(lines); break; case 'lyl': case 'lrc': case 'alrc': case 'ttml': case 'ttml_amll': case 'apple_syllable': case 'srt': case 'spl': preprocessedLines = lines; break; default: throw new Error('should not happen'); } return worker(preprocessedLines); } Dumping.dump = dump; function supportDump(type) { return selectDumpWorker(type) != null; } Dumping.supportDump = supportDump; })(Dumping = LyricIO.Dumping || (LyricIO.Dumping = {})); })(LyricIO || (exports.LyricIO = LyricIO = {})); var LyricFormat; (function (LyricFormat) { LyricFormat.TYPES = { lys: { displayName: 'Lyricify Syllable', extensions: ['.lys'], }, lyl: { displayName: 'Lyricify Lines', extensions: ['.lyl'], }, lqe: { displayName: 'Lyricify Quick Export', extensions: ['.lqe'], }, lrc: { displayName: 'LRC', extensions: ['.lrc'], }, alrc: { displayName: 'ALRC', extensions: ['.alrc'], }, ttml: { displayName: 'TTML (Original)', extensions: ['.ttml'], }, ttml_amll: { displayName: 'TTML (AMLL Standards)', extensions: ['.ttml'], }, apple_syllable: { displayName: 'Apple Syllable', extensions: ['.json', '.as', '.asyl'], }, yrc: { displayName: 'YRC', extensions: ['.yrc'], }, qrc: { displayName: 'QRC (Lyricify Standards)', extensions: ['.qrc'], }, krc: { displayName: 'KRC', extensions: ['.krc'], }, srt: { displayName: 'SRT', extensions: ['.srt'], }, spl: { displayName: 'Salt Player Lyrics', extensions: ['.lrc'], }, }; LyricFormat.allTypes = Object.keys(LyricFormat.TYPES); function getLyricFormatDisplayName(formatType) { return LyricFormat.TYPES[formatType].displayName; } LyricFormat.getLyricFormatDisplayName = getLyricFormatDisplayName; function getLyricFormatFileExtensions(formatType) { return LyricFormat.TYPES[formatType].extensions; } LyricFormat.getLyricFormatFileExtensions = getLyricFormatFileExtensions; async function requestReadLyricsFile(description, languageExtensions) { const fileTypes = languageExtensions.map((ext) => `${ext}`); // @ts-ignore const fileHandles = await window.showOpenFilePicker({ types: [ { description, accept: { 'text/lyricsSpecific': [...fileTypes] }, }, ], }); if (fileHandles.length > 0) { const fileHandle = fileHandles[0]; // Access the first file handle const file = await fileHandle.getFile(); const contents = await file.text(); return contents; } return null; } LyricFormat.requestReadLyricsFile = requestReadLyricsFile; async function requestWriteLyricsFile(description, languageExtensions, content, options) { const fileTypes = languageExtensions.map((ext) => `${ext}`); // @ts-ignore const fileHandle = await window.showSaveFilePicker({ types: [ { description, accept: { 'text/lyricsSpecific': [...fileTypes] }, }, ], suggestedName: options?.fileName || 'lyrics', }); if (fileHandle) { const writable = await fileHandle.createWritable(); await writable.write(content); await writable.close(); return true; } return false; } LyricFormat.requestWriteLyricsFile = requestWriteLyricsFile; })(LyricFormat || (exports.LyricFormat = LyricFormat = {}));