@gondola-lyrics/kit 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/formats.d.ts +224 -6
- package/dist/formats.js +952 -169
- package/dist/formats.js.map +1 -1
- package/dist/generator.d.ts +1 -1
- package/dist/index.d.ts +92 -4
- package/dist/index.js +2 -2
- package/dist/parser.d.ts +92 -4
- package/dist/parser.js +2 -2
- package/dist/{language.js → rules.js} +51 -2
- package/dist/rules.js.map +1 -0
- package/dist/transforms.d.ts +10 -2
- package/dist/transforms.js +47 -87
- package/dist/transforms.js.map +1 -1
- package/package.json +1 -1
- package/dist/language.js.map +0 -1
package/dist/formats.js
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as lowerBound, i as lowerTag, o as mapShared, r as trimRuleWhitespace } from "./rules.js";
|
|
2
2
|
import { AgentType, CreditRole, LineType, LyricStatus, Timing, WordType, canonicalizeLyric, deriveLanguageUsages, deriveLineRomans, getAnnotationItemText, getContentText, getWordsText, isWordLevelLine, makeAgent, makeLineAnnotation, makeLineAnnotationRoman, makeLineAnnotationTranslation, makeLineBackground, makeLineContentText, makeLineContentWords, makeLineInstrumental, makeLineNormal, makeLyric, makeMeta, makeMetaCredit, makeText, makeTimeRange, makeUnknownEntry, makeWordAnnotation, makeWordAnnotationRoman, makeWordAnnotationRuby, makeWordAnnotationToken, makeWordNormal, makeWordSpace, removeOffset, resolveAnnotationItemTime, resolveAnnotationTokenTime, validateLyric } from "@gondola-lyrics/model/runtime";
|
|
3
|
+
//#region src/formats/krc/common.ts
|
|
4
|
+
var LINE_PATTERN$3 = /^\[(\d+),(\d+)\]/;
|
|
5
|
+
var WORD_PATTERN$3 = /<(\d+,\d+,\d+)>/g;
|
|
6
|
+
/**
|
|
7
|
+
* Reads decrypted plaintext without unwrapping XML or accepting auxiliary tracks.
|
|
8
|
+
*
|
|
9
|
+
* @returns Original content, or null when the input cannot be read as plaintext KRC.
|
|
10
|
+
*/
|
|
11
|
+
var readKrcContent = (input) => {
|
|
12
|
+
const source = typeof input === "string" ? { original: input } : input;
|
|
13
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) return null;
|
|
14
|
+
const original = source.original;
|
|
15
|
+
if (typeof original !== "string" || /^\s*</.test(original)) return null;
|
|
16
|
+
return { original };
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/formats/krc/detect.ts
|
|
20
|
+
var LANGUAGE_PATTERN = /\[language:\s*ey[A-Za-z0-9+/=]*\]/;
|
|
21
|
+
/**
|
|
22
|
+
* Recognizes native KRC from a duration line carrying a complete three-field word tag, or from the Kugou language container.
|
|
23
|
+
*
|
|
24
|
+
* Both signals live in the original plaintext, so an auxiliary track still cannot decide the format.
|
|
25
|
+
*
|
|
26
|
+
* @returns Whether the original plaintext contains a strong KRC syntax signal.
|
|
27
|
+
*/
|
|
28
|
+
var detectKrc = (input) => {
|
|
29
|
+
const source = readKrcContent(input);
|
|
30
|
+
if (!source) return false;
|
|
31
|
+
if (LANGUAGE_PATTERN.test(source.original)) return true;
|
|
32
|
+
return source.original.split(/\r\n?|\n/).some((raw) => {
|
|
33
|
+
const line = raw.trimStart();
|
|
34
|
+
const header = LINE_PATTERN$3.exec(line);
|
|
35
|
+
return !!header && /<\d+,\d+,\d+>/.test(line.slice(header[0].length));
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/formats/result.ts
|
|
40
|
+
/**
|
|
41
|
+
* Canonicalizes and validates one parser's output, so every format hands back the same shape.
|
|
42
|
+
*
|
|
43
|
+
* Canonicalization runs first because validation reports paths against the canonical tree.
|
|
44
|
+
*/
|
|
45
|
+
var finalizeLyric = (input) => {
|
|
46
|
+
const lyric = canonicalizeLyric(input);
|
|
47
|
+
return {
|
|
48
|
+
lyric,
|
|
49
|
+
diagnostics: validateLyric(lyric)
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
//#endregion
|
|
3
53
|
//#region src/utils/kana.ts
|
|
4
54
|
var KANA_COMBINATIONS = /* @__PURE__ */ new Map([
|
|
5
55
|
[12353, "うくぐつふゔ"],
|
|
@@ -31,7 +81,7 @@ var combinesKana = (first, second) => {
|
|
|
31
81
|
};
|
|
32
82
|
//#endregion
|
|
33
83
|
//#region src/utils/annotation.ts
|
|
34
|
-
var readText$
|
|
84
|
+
var readText$3 = (line) => {
|
|
35
85
|
return getContentText(line.content).trim();
|
|
36
86
|
};
|
|
37
87
|
/**
|
|
@@ -267,7 +317,7 @@ var readSources = (lines, options) => {
|
|
|
267
317
|
const preserve = options.preservePlaceholders;
|
|
268
318
|
for (const [index, line] of lines.entries()) {
|
|
269
319
|
const start = line.time?.start;
|
|
270
|
-
const text = readText$
|
|
320
|
+
const text = readText$3(line);
|
|
271
321
|
const timing = options.getTiming?.(line);
|
|
272
322
|
const precision = timing?.precision;
|
|
273
323
|
if (start !== void 0 && (preserve || text) || options.matchByOrder && text) result.push({
|
|
@@ -289,7 +339,7 @@ var createTimeIndex = (lines, options) => {
|
|
|
289
339
|
const start = line.time?.start;
|
|
290
340
|
const valid = start !== void 0 && (!options.preservePlaceholders || Number.isFinite(start));
|
|
291
341
|
starts.push(valid ? start : void 0);
|
|
292
|
-
if (options.matchByOrder && line.type === LineType.NORMAL && readText$
|
|
342
|
+
if (options.matchByOrder && line.type === LineType.NORMAL && readText$3(line)) lyricRows.push(index);
|
|
293
343
|
if (!valid) continue;
|
|
294
344
|
const queue = targets.get(start) ?? [];
|
|
295
345
|
queue.push(index);
|
|
@@ -516,6 +566,26 @@ var attachAnnotations = (lines, sources, options = {}) => {
|
|
|
516
566
|
return attachTracks(lines, sources, options, Number.isFinite(options.wordTolerance) ? Math.max(0, options.wordTolerance ?? 0) : 0);
|
|
517
567
|
};
|
|
518
568
|
//#endregion
|
|
569
|
+
//#region src/utils/encoding.ts
|
|
570
|
+
/**
|
|
571
|
+
* Decodes canonical standard base64 into bytes, identically in browsers and Node.
|
|
572
|
+
* atob decides what is decodable; the round-trip re-encode keeps the result canonical, so slacker forms stay undecoded.
|
|
573
|
+
*
|
|
574
|
+
* @param value Base64 text; whitespace, URL-safe alphabets, and misplaced padding are rejected.
|
|
575
|
+
*
|
|
576
|
+
* @returns The decoded bytes, or null when the input is not canonical base64.
|
|
577
|
+
*/
|
|
578
|
+
var decodeBase64 = (value) => {
|
|
579
|
+
if (!value) return null;
|
|
580
|
+
try {
|
|
581
|
+
const binary = atob(value);
|
|
582
|
+
if (btoa(binary).replace(/=+$/, "") !== value.replace(/=+$/, "")) return null;
|
|
583
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
584
|
+
} catch {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
//#endregion
|
|
519
589
|
//#region src/utils/time.ts
|
|
520
590
|
var CLOCK_PATTERN$1 = /^(?:(?:(\d+):)?(\d+):)?(\d+)(?:\.(\d+))?$/;
|
|
521
591
|
var OFFSET_PATTERN = /^(\d+(?:\.\d+)?)(ms|h|m|s)$/iu;
|
|
@@ -1233,6 +1303,77 @@ var serializeXml = (root, options = {}) => {
|
|
|
1233
1303
|
return chunks.join("");
|
|
1234
1304
|
};
|
|
1235
1305
|
//#endregion
|
|
1306
|
+
//#region src/formats/qrc/common.ts
|
|
1307
|
+
var LINE_PATTERN$2 = /^\[(\d+),(\d+)\]/;
|
|
1308
|
+
var XML_ROOT_NAMES = [
|
|
1309
|
+
"QrcInfos",
|
|
1310
|
+
"LyricInfo",
|
|
1311
|
+
"Lyric_1"
|
|
1312
|
+
];
|
|
1313
|
+
var XML_PATTERN = /^\s*</;
|
|
1314
|
+
var TIMED_LINE_PATTERN = /(?:^|[\r\n])[^\S\r\n]*\[\d+,\d+\]/;
|
|
1315
|
+
/**
|
|
1316
|
+
* Unwraps supported QRC XML while keeping plaintext literal.
|
|
1317
|
+
*
|
|
1318
|
+
* XML entities are decoded once by the XML reader.
|
|
1319
|
+
*
|
|
1320
|
+
* @returns The payload, or null when XML is malformed or has no supported lyric content.
|
|
1321
|
+
*/
|
|
1322
|
+
var readText$2 = (source) => {
|
|
1323
|
+
if (!XML_PATTERN.test(source)) return source;
|
|
1324
|
+
if (!XML_ROOT_NAMES.some((name) => source.includes(name))) return null;
|
|
1325
|
+
const document = parseXml(source);
|
|
1326
|
+
if (!document) return null;
|
|
1327
|
+
const root = document.children.find((node) => node.nodeType === XmlNodeType.element);
|
|
1328
|
+
if (!root || !XML_ROOT_NAMES.includes(root.localName)) return null;
|
|
1329
|
+
const pending = [root];
|
|
1330
|
+
while (pending.length) {
|
|
1331
|
+
const element = pending.pop();
|
|
1332
|
+
const content = getXmlAttribute(element, "LyricContent");
|
|
1333
|
+
if (content !== void 0) return content;
|
|
1334
|
+
for (let index = element.children.length - 1; index >= 0; index -= 1) {
|
|
1335
|
+
const child = element.children[index];
|
|
1336
|
+
if (child.nodeType === XmlNodeType.element) pending.push(child);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
return null;
|
|
1340
|
+
};
|
|
1341
|
+
/**
|
|
1342
|
+
* Validates the original payload and keeps auxiliary tracks separate so each can use its own format.
|
|
1343
|
+
*
|
|
1344
|
+
* @returns Normalized input, or null when the original content cannot be read.
|
|
1345
|
+
*/
|
|
1346
|
+
var readContent = (input) => {
|
|
1347
|
+
const value = typeof input === "string" ? { original: input } : input;
|
|
1348
|
+
if (!value || typeof value !== "object") return null;
|
|
1349
|
+
const source = value;
|
|
1350
|
+
if (typeof source.original !== "string") return null;
|
|
1351
|
+
const original = readText$2(source.original);
|
|
1352
|
+
if (original === null) return null;
|
|
1353
|
+
return {
|
|
1354
|
+
original,
|
|
1355
|
+
translations: typeof source.translations === "string" ? source.translations : void 0,
|
|
1356
|
+
romans: typeof source.romans === "string" ? source.romans : void 0
|
|
1357
|
+
};
|
|
1358
|
+
};
|
|
1359
|
+
/**
|
|
1360
|
+
* Requires a timed QRC row so metadata alone cannot select the format.
|
|
1361
|
+
*/
|
|
1362
|
+
var hasTimedLines = (text) => {
|
|
1363
|
+
return TIMED_LINE_PATTERN.test(text);
|
|
1364
|
+
};
|
|
1365
|
+
//#endregion
|
|
1366
|
+
//#region src/formats/qrc/detect.ts
|
|
1367
|
+
/**
|
|
1368
|
+
* Detects QRC from the original track so annotations cannot change format selection.
|
|
1369
|
+
*
|
|
1370
|
+
* @returns Whether plaintext or a supported XML payload contains a timed QRC row.
|
|
1371
|
+
*/
|
|
1372
|
+
var detectQrc = (input) => {
|
|
1373
|
+
const content = readContent(input);
|
|
1374
|
+
return !!content && hasTimedLines(content.original);
|
|
1375
|
+
};
|
|
1376
|
+
//#endregion
|
|
1236
1377
|
//#region src/formats/lrc/common.ts
|
|
1237
1378
|
/**
|
|
1238
1379
|
* Accepts plain lyrics or separate tracks while rejecting an unreadable original payload.
|
|
@@ -1254,7 +1395,7 @@ var WORD_OFFSET_PATTERN = /^(\d+)(?:,(\d+))?$/;
|
|
|
1254
1395
|
var WORD_CLOCK_DURATION_PATTERN = /^([^,]+),(\d+)$/;
|
|
1255
1396
|
var WORD_TAG_PATTERN = /<([^>]*)>/g;
|
|
1256
1397
|
var TAG_PATTERN = /\[([^\]]*)\]/g;
|
|
1257
|
-
var META_PATTERN
|
|
1398
|
+
var META_PATTERN = /^([A-Za-z0-9_-]+)\s*:\s*([\s\S]*)$/;
|
|
1258
1399
|
/**
|
|
1259
1400
|
* Integer tags use `offset[,duration]` in milliseconds relative to the line start.
|
|
1260
1401
|
* Clock tags are absolute and may append a duration in milliseconds, as in `<00:01.50,500>`.
|
|
@@ -1288,36 +1429,42 @@ var parseWordTime = (body, lineStart) => {
|
|
|
1288
1429
|
}
|
|
1289
1430
|
};
|
|
1290
1431
|
/**
|
|
1291
|
-
*
|
|
1292
|
-
*
|
|
1432
|
+
* Splits recognized tags from their following text while preserving literal content and empty tagged segments.
|
|
1433
|
+
*
|
|
1434
|
+
* @param pattern A global pattern whose first capture is the tag body.
|
|
1435
|
+
* @param accepts Optional format-specific tag validation.
|
|
1436
|
+
*
|
|
1437
|
+
* @returns Source pieces in their original order, including an untagged prefix when present.
|
|
1293
1438
|
*/
|
|
1294
|
-
var
|
|
1439
|
+
var scanWordTags = (content, pattern, accepts) => {
|
|
1295
1440
|
const pieces = [];
|
|
1296
1441
|
let tag;
|
|
1297
1442
|
let cursor = 0;
|
|
1298
|
-
for (const match of content.matchAll(
|
|
1443
|
+
for (const match of content.matchAll(pattern)) {
|
|
1299
1444
|
const nextTag = match[1] ?? "";
|
|
1300
|
-
if (!
|
|
1301
|
-
const
|
|
1302
|
-
|
|
1303
|
-
if (before || tag) pieces.push({
|
|
1445
|
+
if (accepts && !accepts(nextTag)) continue;
|
|
1446
|
+
const before = content.slice(cursor, match.index);
|
|
1447
|
+
if (before || tag !== void 0) pieces.push({
|
|
1304
1448
|
text: before,
|
|
1305
1449
|
tag
|
|
1306
1450
|
});
|
|
1307
1451
|
tag = nextTag;
|
|
1308
|
-
cursor =
|
|
1452
|
+
cursor = match.index + match[0].length;
|
|
1309
1453
|
}
|
|
1310
1454
|
const tail = content.slice(cursor);
|
|
1311
|
-
if (tail) pieces.push({
|
|
1455
|
+
if (tail || tag !== void 0) pieces.push({
|
|
1312
1456
|
text: tail,
|
|
1313
1457
|
tag
|
|
1314
1458
|
});
|
|
1315
|
-
else if (tag) pieces.push({
|
|
1316
|
-
text: "",
|
|
1317
|
-
tag
|
|
1318
|
-
});
|
|
1319
1459
|
return pieces;
|
|
1320
1460
|
};
|
|
1461
|
+
/**
|
|
1462
|
+
* Untagged leading text is preserved; empty timed pieces retain explicit segment boundaries.
|
|
1463
|
+
* Only recognized time tags split content; other angle-bracket text remains literal.
|
|
1464
|
+
*/
|
|
1465
|
+
var splitWordTags = (content) => {
|
|
1466
|
+
return scanWordTags(content, WORD_TAG_PATTERN, (tag) => !!parseWordTime(tag, 0));
|
|
1467
|
+
};
|
|
1321
1468
|
//#endregion
|
|
1322
1469
|
//#region src/formats/lrc/detect.ts
|
|
1323
1470
|
/**
|
|
@@ -1465,57 +1612,47 @@ var generateLrc = (lyric, options = {}) => {
|
|
|
1465
1612
|
return result;
|
|
1466
1613
|
};
|
|
1467
1614
|
//#endregion
|
|
1468
|
-
//#region src/formats/
|
|
1469
|
-
/**
|
|
1470
|
-
* Canonicalizes and validates one parser's output, so every format hands back the same shape.
|
|
1471
|
-
*
|
|
1472
|
-
* Canonicalization runs first because validation reports paths against the canonical tree.
|
|
1473
|
-
*/
|
|
1474
|
-
var finalizeLyric = (input) => {
|
|
1475
|
-
const lyric = canonicalizeLyric(input);
|
|
1476
|
-
return {
|
|
1477
|
-
lyric,
|
|
1478
|
-
diagnostics: validateLyric(lyric)
|
|
1479
|
-
};
|
|
1480
|
-
};
|
|
1481
|
-
//#endregion
|
|
1482
|
-
//#region src/formats/lrc/reader.ts
|
|
1615
|
+
//#region src/formats/lrc/parser.ts
|
|
1483
1616
|
var CLOCK_PATTERN = /^(?:\d+:){0,2}\d+(?:\.(\d+))?$/;
|
|
1484
1617
|
var readLineTiming = (tag) => {
|
|
1485
1618
|
const clock = CLOCK_PATTERN.exec(tag.trim());
|
|
1486
1619
|
return { precision: clock ? 10 ** Math.max(0, 3 - (clock[1]?.length ?? 0)) : 1 };
|
|
1487
1620
|
};
|
|
1488
1621
|
var readMeta$1 = (body) => {
|
|
1489
|
-
const match = META_PATTERN
|
|
1622
|
+
const match = META_PATTERN.exec(body);
|
|
1490
1623
|
if (!match) return;
|
|
1491
1624
|
return {
|
|
1492
1625
|
key: (match[1] ?? "").toLowerCase(),
|
|
1493
1626
|
value: (match[2] ?? "").trim()
|
|
1494
1627
|
};
|
|
1495
1628
|
};
|
|
1496
|
-
var readGroup = (tags, text, timed, meta) => {
|
|
1629
|
+
var readGroup = (tags, text, row, timed, meta, options) => {
|
|
1497
1630
|
for (const tag of tags) {
|
|
1498
1631
|
const start = isTime(tag) ? parseTime(tag) : void 0;
|
|
1499
1632
|
if (start !== void 0) {
|
|
1500
1633
|
timed.push({
|
|
1501
1634
|
text,
|
|
1502
1635
|
start,
|
|
1503
|
-
tag
|
|
1636
|
+
tag,
|
|
1637
|
+
row
|
|
1504
1638
|
});
|
|
1505
1639
|
continue;
|
|
1506
1640
|
}
|
|
1507
1641
|
const pair = readMeta$1(tag);
|
|
1508
|
-
if (pair)
|
|
1642
|
+
if (pair) {
|
|
1643
|
+
meta.push(pair);
|
|
1644
|
+
options.onMeta?.(pair, row);
|
|
1645
|
+
}
|
|
1509
1646
|
}
|
|
1510
1647
|
};
|
|
1511
1648
|
/**
|
|
1512
1649
|
* Stacked timestamps share the following text, as in `[00:10][00:20]twice`.
|
|
1513
1650
|
* Text before the first tag is discarded because it has no timing association.
|
|
1514
1651
|
*/
|
|
1515
|
-
var readTimedLines = (text) => {
|
|
1652
|
+
var readTimedLines = (text, options) => {
|
|
1516
1653
|
const timed = [];
|
|
1517
1654
|
const meta = [];
|
|
1518
|
-
for (const raw of text.replace(/\r\n?/g, "\n").split("\n")) {
|
|
1655
|
+
for (const [row, raw] of text.replace(/\r\n?/g, "\n").split("\n").entries()) {
|
|
1519
1656
|
const line = raw.trim();
|
|
1520
1657
|
if (!line) continue;
|
|
1521
1658
|
const matches = [...line.matchAll(TAG_PATTERN)];
|
|
@@ -1526,10 +1663,10 @@ var readTimedLines = (text) => {
|
|
|
1526
1663
|
const shared = line.slice(at, next);
|
|
1527
1664
|
group.push(match[1] ?? "");
|
|
1528
1665
|
if (!shared.trim()) continue;
|
|
1529
|
-
readGroup(group, shared, timed, meta);
|
|
1666
|
+
readGroup(group, shared, row, timed, meta, options);
|
|
1530
1667
|
group = [];
|
|
1531
1668
|
}
|
|
1532
|
-
readGroup(group, "", timed, meta);
|
|
1669
|
+
readGroup(group, "", row, timed, meta, options);
|
|
1533
1670
|
}
|
|
1534
1671
|
return {
|
|
1535
1672
|
timed,
|
|
@@ -1592,7 +1729,7 @@ var readWords$2 = (pieces, lineStart) => {
|
|
|
1592
1729
|
* Explicit word ranges can extend past the next line's start, so the inferred end covers both.
|
|
1593
1730
|
* A trailing open segment uses that end when it is known.
|
|
1594
1731
|
*/
|
|
1595
|
-
var readLine$
|
|
1732
|
+
var readLine$4 = (text, start, nextStart) => {
|
|
1596
1733
|
const trimmed = text.trim();
|
|
1597
1734
|
let end = nextStart !== void 0 && nextStart >= start ? nextStart : void 0;
|
|
1598
1735
|
if (!trimmed) return makeLineInstrumental({ time: makeTimeRange({
|
|
@@ -1639,15 +1776,14 @@ var readLines$1 = (timed, options) => {
|
|
|
1639
1776
|
const lines = [];
|
|
1640
1777
|
for (const [index, item] of timed.entries()) {
|
|
1641
1778
|
const next = timed[index + 1]?.start;
|
|
1642
|
-
const line = readLine$
|
|
1779
|
+
const line = readLine$4(item.text, item.start, next);
|
|
1643
1780
|
lines.push(line);
|
|
1644
|
-
options.onLine?.(line, readLineTiming(item.tag));
|
|
1781
|
+
options.onLine?.(line, readLineTiming(item.tag), item.row);
|
|
1645
1782
|
}
|
|
1646
1783
|
return lines;
|
|
1647
1784
|
};
|
|
1648
1785
|
/**
|
|
1649
|
-
* Reads one LRC track
|
|
1650
|
-
* The caller owns metadata mapping, annotation attachment, and final normalization.
|
|
1786
|
+
* Reads one LRC track in source order and performs no other operation: no metadata mapping, annotation attachment, sorting, or normalization.
|
|
1651
1787
|
*
|
|
1652
1788
|
* @param text Original lyrics or one auxiliary track in LRC syntax.
|
|
1653
1789
|
* @param options Optional observer for source timestamp information.
|
|
@@ -1655,22 +1791,23 @@ var readLines$1 = (timed, options) => {
|
|
|
1655
1791
|
* @returns Model lines in input order and metadata tags with lowercase keys and trimmed values.
|
|
1656
1792
|
*/
|
|
1657
1793
|
var readLrc = (text, options = {}) => {
|
|
1658
|
-
const { timed, meta } = readTimedLines(text);
|
|
1794
|
+
const { timed, meta } = readTimedLines(text, options);
|
|
1659
1795
|
return {
|
|
1660
1796
|
lines: readLines$1(timed, options),
|
|
1661
1797
|
metaTags: meta
|
|
1662
1798
|
};
|
|
1663
1799
|
};
|
|
1664
|
-
//#endregion
|
|
1665
|
-
//#region src/formats/lrc/parser.ts
|
|
1666
1800
|
var isBareNumber = (value) => {
|
|
1667
1801
|
return /^\d+$/.test(value.trim());
|
|
1668
1802
|
};
|
|
1669
1803
|
/**
|
|
1670
|
-
*
|
|
1671
|
-
*
|
|
1804
|
+
* Maps standard lyric metadata while preserving unmapped tags and unparseable durations in unknowns.
|
|
1805
|
+
*
|
|
1806
|
+
* @param tags Reader tags with lowercase keys and trimmed values.
|
|
1807
|
+
*
|
|
1808
|
+
* @returns Model metadata without interpreting platform-specific embedded annotations.
|
|
1672
1809
|
*/
|
|
1673
|
-
var
|
|
1810
|
+
var readLrcMetaTags = (tags) => {
|
|
1674
1811
|
const meta = makeMeta();
|
|
1675
1812
|
for (const tag of tags) switch (tag.key) {
|
|
1676
1813
|
case "length":
|
|
@@ -1714,7 +1851,7 @@ var readMetaTags = (tags) => {
|
|
|
1714
1851
|
};
|
|
1715
1852
|
var parseLrcText = (text) => {
|
|
1716
1853
|
const { lines, metaTags } = readLrc(text);
|
|
1717
|
-
const meta =
|
|
1854
|
+
const meta = readLrcMetaTags(metaTags);
|
|
1718
1855
|
if (!lines.length) return makeLyric({
|
|
1719
1856
|
format: "lrc",
|
|
1720
1857
|
status: LyricStatus.INVALID,
|
|
@@ -1759,88 +1896,22 @@ var parseLrc = (input, options = {}) => {
|
|
|
1759
1896
|
});
|
|
1760
1897
|
};
|
|
1761
1898
|
//#endregion
|
|
1762
|
-
//#region src/formats/qrc/common.ts
|
|
1763
|
-
var LINE_PATTERN = /^\[(\d+),(\d+)\]/;
|
|
1764
|
-
var XML_ROOT_NAMES = [
|
|
1765
|
-
"QrcInfos",
|
|
1766
|
-
"LyricInfo",
|
|
1767
|
-
"Lyric_1"
|
|
1768
|
-
];
|
|
1769
|
-
var XML_PATTERN = /^\s*</;
|
|
1770
|
-
var TIMED_LINE_PATTERN = /(?:^|[\r\n])[^\S\r\n]*\[\d+,\d+\]/;
|
|
1771
|
-
/**
|
|
1772
|
-
* Unwraps supported QRC XML while keeping plaintext literal.
|
|
1773
|
-
*
|
|
1774
|
-
* XML entities are decoded once by the XML reader.
|
|
1775
|
-
*
|
|
1776
|
-
* @returns The payload, or null when XML is malformed or has no supported lyric content.
|
|
1777
|
-
*/
|
|
1778
|
-
var readText$1 = (source) => {
|
|
1779
|
-
if (!XML_PATTERN.test(source)) return source;
|
|
1780
|
-
if (!XML_ROOT_NAMES.some((name) => source.includes(name))) return null;
|
|
1781
|
-
const document = parseXml(source);
|
|
1782
|
-
if (!document) return null;
|
|
1783
|
-
const root = document.children.find((node) => node.nodeType === XmlNodeType.element);
|
|
1784
|
-
if (!root || !XML_ROOT_NAMES.includes(root.localName)) return null;
|
|
1785
|
-
const pending = [root];
|
|
1786
|
-
while (pending.length) {
|
|
1787
|
-
const element = pending.pop();
|
|
1788
|
-
const content = getXmlAttribute(element, "LyricContent");
|
|
1789
|
-
if (content !== void 0) return content;
|
|
1790
|
-
for (let index = element.children.length - 1; index >= 0; index -= 1) {
|
|
1791
|
-
const child = element.children[index];
|
|
1792
|
-
if (child.nodeType === XmlNodeType.element) pending.push(child);
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
return null;
|
|
1796
|
-
};
|
|
1797
|
-
/**
|
|
1798
|
-
* Validates the original payload and keeps auxiliary tracks separate so each can use its own format.
|
|
1799
|
-
*
|
|
1800
|
-
* @returns Normalized input, or null when the original content cannot be read.
|
|
1801
|
-
*/
|
|
1802
|
-
var readContent = (input) => {
|
|
1803
|
-
const value = typeof input === "string" ? { original: input } : input;
|
|
1804
|
-
if (!value || typeof value !== "object") return null;
|
|
1805
|
-
const source = value;
|
|
1806
|
-
if (typeof source.original !== "string") return null;
|
|
1807
|
-
const original = readText$1(source.original);
|
|
1808
|
-
if (original === null) return null;
|
|
1809
|
-
return {
|
|
1810
|
-
original,
|
|
1811
|
-
translations: typeof source.translations === "string" ? source.translations : void 0,
|
|
1812
|
-
romans: typeof source.romans === "string" ? source.romans : void 0
|
|
1813
|
-
};
|
|
1814
|
-
};
|
|
1815
|
-
/**
|
|
1816
|
-
* Requires a timed QRC row so metadata alone cannot select the format.
|
|
1817
|
-
*/
|
|
1818
|
-
var hasTimedLines = (text) => {
|
|
1819
|
-
return TIMED_LINE_PATTERN.test(text);
|
|
1820
|
-
};
|
|
1821
|
-
//#endregion
|
|
1822
|
-
//#region src/formats/qrc/detect.ts
|
|
1823
|
-
/**
|
|
1824
|
-
* Detects QRC from the original track so annotations cannot change format selection.
|
|
1825
|
-
*
|
|
1826
|
-
* @returns Whether plaintext or a supported XML payload contains a timed QRC row.
|
|
1827
|
-
*/
|
|
1828
|
-
var detectQrc = (input) => {
|
|
1829
|
-
const content = readContent(input);
|
|
1830
|
-
return !!content && hasTimedLines(content.original);
|
|
1831
|
-
};
|
|
1832
|
-
//#endregion
|
|
1833
1899
|
//#region src/formats/qrc/parser.ts
|
|
1834
|
-
var WORD_PATTERN = /\((\d+),(\d+)\)/g;
|
|
1835
|
-
var META_PATTERN = /^([A-Za-z0-9_-]+)\s*:\s*([\s\S]*)$/;
|
|
1900
|
+
var WORD_PATTERN$2 = /\((\d+),(\d+)\)/g;
|
|
1901
|
+
var META_PATTERN$1 = /^([A-Za-z0-9_-]+)\s*:\s*([\s\S]*)$/;
|
|
1836
1902
|
var META_TAG_PATTERN = /\[([^\]]*)\]/g;
|
|
1837
1903
|
var RUBY_PATTERN = /\((\d+),(\d+)\)|(\d+)|([^0-9()]+)/gy;
|
|
1838
1904
|
var RUBY_WORD_PATTERN = /[\p{Script=Han}\p{Number}]/u;
|
|
1839
|
-
var SEPARATORS = /^\p{White_Space}+$/u;
|
|
1905
|
+
var SEPARATORS$3 = /^\p{White_Space}+$/u;
|
|
1840
1906
|
var NATIVE_ANNOTATION_TIMING = {
|
|
1841
1907
|
precision: 1,
|
|
1842
1908
|
rounding: 1
|
|
1843
1909
|
};
|
|
1910
|
+
/**
|
|
1911
|
+
* Reads coverage markers and optional absolute token times without interpreting timing digits as coverage.
|
|
1912
|
+
*
|
|
1913
|
+
* @returns Complete ruby sources, or undefined when any source cannot be read.
|
|
1914
|
+
*/
|
|
1844
1915
|
var readRubyEntries = (value) => {
|
|
1845
1916
|
const entries = [];
|
|
1846
1917
|
let length = 0;
|
|
@@ -1965,13 +2036,16 @@ var matchRubies = (words, sources) => {
|
|
|
1965
2036
|
/**
|
|
1966
2037
|
* Attaches whole-word ruby without changing the source text, segmentation, or timing.
|
|
1967
2038
|
*
|
|
1968
|
-
* @param lines Original
|
|
2039
|
+
* @param lines Original timed rows before filtering or sorting.
|
|
1969
2040
|
* @param sources Readings with source coverage lengths and optional token times.
|
|
1970
2041
|
*
|
|
1971
|
-
* @returns Annotated rows
|
|
2042
|
+
* @returns Annotated rows and matched source indices, preserving unchanged nodes.
|
|
1972
2043
|
*/
|
|
1973
2044
|
var attachRubies = (lines, sources) => {
|
|
1974
|
-
if (!sources.length) return
|
|
2045
|
+
if (!sources.length) return {
|
|
2046
|
+
lines,
|
|
2047
|
+
matched: /* @__PURE__ */ new Set()
|
|
2048
|
+
};
|
|
1975
2049
|
const words = [];
|
|
1976
2050
|
for (const line of lines) {
|
|
1977
2051
|
if (line.type !== LineType.NORMAL) continue;
|
|
@@ -1986,33 +2060,39 @@ var attachRubies = (lines, sources) => {
|
|
|
1986
2060
|
});
|
|
1987
2061
|
}
|
|
1988
2062
|
const matched = matchRubies(words, sources);
|
|
1989
|
-
if (!matched.size) return
|
|
2063
|
+
if (!matched.size) return {
|
|
2064
|
+
lines,
|
|
2065
|
+
matched: /* @__PURE__ */ new Set()
|
|
2066
|
+
};
|
|
1990
2067
|
const annotations = new Map([...matched].map(([source, target]) => [words[target].word, sources[source].ruby]));
|
|
1991
|
-
return
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
const
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2068
|
+
return {
|
|
2069
|
+
lines: mapShared(lines, (line) => {
|
|
2070
|
+
const content = line.content;
|
|
2071
|
+
if (!content) return line;
|
|
2072
|
+
const words = mapShared(content.words, (word) => {
|
|
2073
|
+
const ruby = annotations.get(word);
|
|
2074
|
+
if (!ruby) return word;
|
|
2075
|
+
return {
|
|
2076
|
+
...word,
|
|
2077
|
+
annotation: makeWordAnnotation({
|
|
2078
|
+
...word.annotation,
|
|
2079
|
+
rubies: [...word.annotation?.rubies ?? [], ruby]
|
|
2080
|
+
})
|
|
2081
|
+
};
|
|
2082
|
+
});
|
|
2083
|
+
return words === content.words ? line : {
|
|
2084
|
+
...line,
|
|
2085
|
+
content: {
|
|
2086
|
+
...content,
|
|
2087
|
+
words
|
|
2088
|
+
}
|
|
2003
2089
|
};
|
|
2004
|
-
})
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
content: {
|
|
2008
|
-
...content,
|
|
2009
|
-
words
|
|
2010
|
-
}
|
|
2011
|
-
};
|
|
2012
|
-
});
|
|
2090
|
+
}),
|
|
2091
|
+
matched: new Set(matched.keys())
|
|
2092
|
+
};
|
|
2013
2093
|
};
|
|
2014
2094
|
var readMeta = (body, meta) => {
|
|
2015
|
-
const match = META_PATTERN.exec(body);
|
|
2095
|
+
const match = META_PATTERN$1.exec(body);
|
|
2016
2096
|
if (!match) return;
|
|
2017
2097
|
const key = match[1].toLowerCase();
|
|
2018
2098
|
const value = match[2].trim();
|
|
@@ -2062,10 +2142,10 @@ var readTimeRange = (startText, durationText) => {
|
|
|
2062
2142
|
end: start + Number(durationText)
|
|
2063
2143
|
});
|
|
2064
2144
|
};
|
|
2065
|
-
var appendWords = (words, text, time) => {
|
|
2145
|
+
var appendWords$2 = (words, text, time) => {
|
|
2066
2146
|
for (const fragment of text.split(/(\p{White_Space}+)/u)) {
|
|
2067
2147
|
if (!fragment) continue;
|
|
2068
|
-
if (SEPARATORS.test(fragment)) words.push(makeWordSpace({ text: fragment }));
|
|
2148
|
+
if (SEPARATORS$3.test(fragment)) words.push(makeWordSpace({ text: fragment }));
|
|
2069
2149
|
else words.push(makeWordNormal({
|
|
2070
2150
|
text: fragment,
|
|
2071
2151
|
time
|
|
@@ -2076,28 +2156,36 @@ var readWords$1 = (text) => {
|
|
|
2076
2156
|
const words = [];
|
|
2077
2157
|
let cursor = 0;
|
|
2078
2158
|
let tagged = false;
|
|
2079
|
-
for (const match of text.matchAll(WORD_PATTERN)) {
|
|
2159
|
+
for (const match of text.matchAll(WORD_PATTERN$2)) {
|
|
2080
2160
|
tagged = true;
|
|
2081
|
-
appendWords(words, text.slice(cursor, match.index), readTimeRange(match[1], match[2]));
|
|
2161
|
+
appendWords$2(words, text.slice(cursor, match.index), readTimeRange(match[1], match[2]));
|
|
2082
2162
|
cursor = match.index + match[0].length;
|
|
2083
2163
|
}
|
|
2084
2164
|
if (!tagged) return;
|
|
2085
|
-
appendWords(words, text.slice(cursor));
|
|
2165
|
+
appendWords$2(words, text.slice(cursor));
|
|
2086
2166
|
return words;
|
|
2087
2167
|
};
|
|
2168
|
+
/**
|
|
2169
|
+
* Kugou writes `//` where it holds no translation, in QRC source rows and in its language payload alike.
|
|
2170
|
+
*
|
|
2171
|
+
* @returns Whether the trimmed text is that sentinel instead of real content.
|
|
2172
|
+
*/
|
|
2173
|
+
var isPlaceholderText = (text) => {
|
|
2174
|
+
return text.trim() === "//";
|
|
2175
|
+
};
|
|
2088
2176
|
var readLines = (text) => {
|
|
2089
2177
|
const lines = [];
|
|
2090
2178
|
const meta = makeMeta();
|
|
2091
2179
|
const placeholders = /* @__PURE__ */ new Set();
|
|
2092
2180
|
for (const raw of text.split(/\r\n?|\n/)) {
|
|
2093
2181
|
const line = raw.trimStart();
|
|
2094
|
-
const match = LINE_PATTERN.exec(line);
|
|
2182
|
+
const match = LINE_PATTERN$2.exec(line);
|
|
2095
2183
|
if (match) {
|
|
2096
2184
|
const time = readTimeRange(match[1], match[2]);
|
|
2097
2185
|
const text = line.slice(match[0].length);
|
|
2098
2186
|
const words = readWords$1(text);
|
|
2099
2187
|
const contentText = (words ? getWordsText(words) : text).trim();
|
|
2100
|
-
const placeholder = contentText
|
|
2188
|
+
const placeholder = isPlaceholderText(contentText);
|
|
2101
2189
|
if (placeholder) placeholders.add(lines.length);
|
|
2102
2190
|
if (!contentText || placeholder) lines.push(makeLineInstrumental({ time }));
|
|
2103
2191
|
else {
|
|
@@ -2122,7 +2210,7 @@ var readLines = (text) => {
|
|
|
2122
2210
|
placeholders
|
|
2123
2211
|
};
|
|
2124
2212
|
};
|
|
2125
|
-
var readAnnotations = (text, timings) => {
|
|
2213
|
+
var readAnnotations$1 = (text, timings) => {
|
|
2126
2214
|
if (!text) return [];
|
|
2127
2215
|
const { lines } = readLines(text);
|
|
2128
2216
|
if (lines.length) {
|
|
@@ -2130,7 +2218,7 @@ var readAnnotations = (text, timings) => {
|
|
|
2130
2218
|
return lines;
|
|
2131
2219
|
}
|
|
2132
2220
|
readLrc(text, { onLine: (line, timing) => {
|
|
2133
|
-
const annotation = getContentText(line.content)
|
|
2221
|
+
const annotation = isPlaceholderText(getContentText(line.content)) ? makeLineInstrumental({ time: line.time }) : line;
|
|
2134
2222
|
timings.set(annotation, timing);
|
|
2135
2223
|
lines.push(annotation);
|
|
2136
2224
|
} });
|
|
@@ -2160,11 +2248,11 @@ var parseQrc = (input, options = {}) => {
|
|
|
2160
2248
|
meta
|
|
2161
2249
|
}));
|
|
2162
2250
|
const timings = /* @__PURE__ */ new WeakMap();
|
|
2163
|
-
const translationLines = readAnnotations(source.translations && readText$
|
|
2164
|
-
const romanLines = readAnnotations(source.romans && readText$
|
|
2251
|
+
const translationLines = readAnnotations$1(source.translations && readText$2(source.translations), timings);
|
|
2252
|
+
const romanLines = readAnnotations$1(source.romans && readText$2(source.romans), timings);
|
|
2165
2253
|
const rounding = romanLines.some((line) => timings.get(line)?.rounding === 1) ? 1 : 0;
|
|
2166
2254
|
const wordTolerance = Number.isFinite(options.wordAnnotationTolerance) ? options.wordAnnotationTolerance ?? 0 : 0;
|
|
2167
|
-
const annotated = attachAnnotations(!rubies ? parsed.lines : attachRubies(parsed.lines, rubies), {
|
|
2255
|
+
const annotated = attachAnnotations(!rubies ? parsed.lines : attachRubies(parsed.lines, rubies).lines, {
|
|
2168
2256
|
translations: translationLines,
|
|
2169
2257
|
romans: romanLines
|
|
2170
2258
|
}, {
|
|
@@ -2186,6 +2274,441 @@ var parseQrc = (input, options = {}) => {
|
|
|
2186
2274
|
}));
|
|
2187
2275
|
};
|
|
2188
2276
|
//#endregion
|
|
2277
|
+
//#region src/formats/krc/parser.ts
|
|
2278
|
+
var SEPARATORS$2 = /^\p{White_Space}+$/u;
|
|
2279
|
+
var appendWords$1 = (words, text, time) => {
|
|
2280
|
+
for (const fragment of text.split(/(\p{White_Space}+)/u)) {
|
|
2281
|
+
if (!fragment) continue;
|
|
2282
|
+
words.push(SEPARATORS$2.test(fragment) ? makeWordSpace({ text: fragment }) : makeWordNormal({
|
|
2283
|
+
text: fragment,
|
|
2284
|
+
time
|
|
2285
|
+
}));
|
|
2286
|
+
}
|
|
2287
|
+
};
|
|
2288
|
+
var readLine$3 = (body, start, duration) => {
|
|
2289
|
+
const time = makeTimeRange({
|
|
2290
|
+
start,
|
|
2291
|
+
end: start + duration
|
|
2292
|
+
});
|
|
2293
|
+
const pieces = scanWordTags(body, WORD_PATTERN$3);
|
|
2294
|
+
if (!pieces.some((piece) => piece.tag !== void 0)) return body.trim() ? makeLineNormal({
|
|
2295
|
+
time,
|
|
2296
|
+
content: makeLineContentText(body)
|
|
2297
|
+
}) : makeLineInstrumental({ time });
|
|
2298
|
+
const words = [];
|
|
2299
|
+
for (const piece of pieces) {
|
|
2300
|
+
if (piece.tag === void 0) {
|
|
2301
|
+
appendWords$1(words, piece.text);
|
|
2302
|
+
continue;
|
|
2303
|
+
}
|
|
2304
|
+
const [offset, wordDuration] = piece.tag.split(",").map(Number);
|
|
2305
|
+
const wordStart = start + offset;
|
|
2306
|
+
appendWords$1(words, piece.text, makeTimeRange({
|
|
2307
|
+
start: wordStart,
|
|
2308
|
+
end: wordStart + wordDuration
|
|
2309
|
+
}));
|
|
2310
|
+
}
|
|
2311
|
+
return getWordsText(words).trim() ? makeLineNormal({
|
|
2312
|
+
time,
|
|
2313
|
+
content: makeLineContentWords(words)
|
|
2314
|
+
}) : makeLineInstrumental({ time });
|
|
2315
|
+
};
|
|
2316
|
+
/**
|
|
2317
|
+
* Reads native KRC rows in source order and performs no other operation: no metadata mapping, annotation attachment, sorting, or normalization.
|
|
2318
|
+
*
|
|
2319
|
+
* Explicit line and word durations survive unchanged; whitespace splitting never reallocates segment time.
|
|
2320
|
+
*
|
|
2321
|
+
* @returns Original-order model lines and metadata tags awaiting interpretation.
|
|
2322
|
+
*/
|
|
2323
|
+
var readKrc = (text) => {
|
|
2324
|
+
const lines = [];
|
|
2325
|
+
const metaTags = [];
|
|
2326
|
+
for (const raw of text.replace(/^\uFEFF/, "").split(/\r\n?|\n/)) {
|
|
2327
|
+
const line = raw.trimStart();
|
|
2328
|
+
const header = LINE_PATTERN$3.exec(line);
|
|
2329
|
+
if (header) {
|
|
2330
|
+
lines.push(readLine$3(line.slice(header[0].length), Number(header[1]), Number(header[2])));
|
|
2331
|
+
continue;
|
|
2332
|
+
}
|
|
2333
|
+
if (!line.startsWith("[")) continue;
|
|
2334
|
+
for (const tag of line.matchAll(TAG_PATTERN)) {
|
|
2335
|
+
const meta = META_PATTERN.exec(tag[1]);
|
|
2336
|
+
if (meta) metaTags.push({
|
|
2337
|
+
key: meta[1].toLowerCase(),
|
|
2338
|
+
value: meta[2].trim()
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
return {
|
|
2343
|
+
lines,
|
|
2344
|
+
metaTags
|
|
2345
|
+
};
|
|
2346
|
+
};
|
|
2347
|
+
var attachKana = (lines, meta) => {
|
|
2348
|
+
for (const entry of meta.unknowns) {
|
|
2349
|
+
if (entry.key !== "kana") continue;
|
|
2350
|
+
const sources = readRubyEntries(entry.value);
|
|
2351
|
+
if (!sources) continue;
|
|
2352
|
+
const attached = attachRubies(lines, sources);
|
|
2353
|
+
if (attached.matched.size === sources.length) meta.unknowns = meta.unknowns.filter((item) => item !== entry);
|
|
2354
|
+
return attached.lines;
|
|
2355
|
+
}
|
|
2356
|
+
return lines;
|
|
2357
|
+
};
|
|
2358
|
+
var isObject$1 = (value) => {
|
|
2359
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2360
|
+
};
|
|
2361
|
+
var readPayload = (value) => {
|
|
2362
|
+
const bytes = decodeBase64(value);
|
|
2363
|
+
if (!bytes) return;
|
|
2364
|
+
try {
|
|
2365
|
+
const payload = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
2366
|
+
return isObject$1(payload) && payload.version === 1 && Array.isArray(payload.content) ? payload : void 0;
|
|
2367
|
+
} catch {
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
};
|
|
2371
|
+
var readTrack$1 = (value) => {
|
|
2372
|
+
if (!isObject$1(value) || value.type !== 0 && value.type !== 1 || typeof value.language !== "number" || !Number.isSafeInteger(value.language) || value.language < 0 || !Array.isArray(value.lyricContent) || !value.lyricContent.every((row) => Array.isArray(row) && row.every((cell) => typeof cell === "string"))) return;
|
|
2373
|
+
return value;
|
|
2374
|
+
};
|
|
2375
|
+
var attachTrack = (lines, track) => {
|
|
2376
|
+
let result = lines;
|
|
2377
|
+
let complete = true;
|
|
2378
|
+
for (const [index, cells] of track.lyricContent.entries()) {
|
|
2379
|
+
const text = cells.join("");
|
|
2380
|
+
if (cells.length > 1) complete = false;
|
|
2381
|
+
if (!text.trim() || isPlaceholderText(text)) continue;
|
|
2382
|
+
const line = result[index];
|
|
2383
|
+
if (line.type === LineType.INSTRUMENTAL) {
|
|
2384
|
+
complete = false;
|
|
2385
|
+
continue;
|
|
2386
|
+
}
|
|
2387
|
+
const kind = track.type === 1 ? "translations" : "romans";
|
|
2388
|
+
const existing = line.annotation?.[kind] ?? [];
|
|
2389
|
+
const occupied = existing.find((item) => !item.language);
|
|
2390
|
+
if (occupied) {
|
|
2391
|
+
complete = complete && occupied.text === text;
|
|
2392
|
+
continue;
|
|
2393
|
+
}
|
|
2394
|
+
const create = track.type === 1 ? makeLineAnnotationTranslation : makeLineAnnotationRoman;
|
|
2395
|
+
const annotated = {
|
|
2396
|
+
...line,
|
|
2397
|
+
annotation: makeLineAnnotation({
|
|
2398
|
+
...line.annotation,
|
|
2399
|
+
[kind]: [...existing, create({ text })]
|
|
2400
|
+
})
|
|
2401
|
+
};
|
|
2402
|
+
if (result === lines) result = lines.slice();
|
|
2403
|
+
result[index] = annotated;
|
|
2404
|
+
}
|
|
2405
|
+
return {
|
|
2406
|
+
lines: result,
|
|
2407
|
+
complete
|
|
2408
|
+
};
|
|
2409
|
+
};
|
|
2410
|
+
/**
|
|
2411
|
+
* Attaches strictly recognized Kugou language tracks by original timed slot before sorting or filtering.
|
|
2412
|
+
*
|
|
2413
|
+
* Existing annotations take precedence; incomplete or ambiguous payloads remain in metadata.
|
|
2414
|
+
* Numeric language codes other than the verified default are retained without inventing BCP 47 tags.
|
|
2415
|
+
*
|
|
2416
|
+
* @returns Annotated lines, reusing the original array when no visible content changes.
|
|
2417
|
+
*/
|
|
2418
|
+
var attachKugouLanguage = (lines, meta) => {
|
|
2419
|
+
if (!lines.length) return lines;
|
|
2420
|
+
let result = lines;
|
|
2421
|
+
const unknowns = [];
|
|
2422
|
+
const selected = /* @__PURE__ */ new Map();
|
|
2423
|
+
for (const entry of meta.unknowns) {
|
|
2424
|
+
const payload = entry.key === "language" ? readPayload(entry.value) : void 0;
|
|
2425
|
+
if (!payload) {
|
|
2426
|
+
unknowns.push(entry);
|
|
2427
|
+
continue;
|
|
2428
|
+
}
|
|
2429
|
+
let complete = Object.keys(payload).every((key) => key === "version" || key === "content" || key === "contentV2");
|
|
2430
|
+
if ("contentV2" in payload && (!Array.isArray(payload.contentV2) || payload.contentV2.length !== 0)) complete = false;
|
|
2431
|
+
for (const value of payload.content) {
|
|
2432
|
+
const track = readTrack$1(value);
|
|
2433
|
+
if (!track || track.language !== 0 || track.lyricContent.length !== lines.length) {
|
|
2434
|
+
complete = false;
|
|
2435
|
+
continue;
|
|
2436
|
+
}
|
|
2437
|
+
if (Object.keys(value).some((key) => key !== "type" && key !== "language" && key !== "lyricContent")) complete = false;
|
|
2438
|
+
const previous = selected.get(track.type);
|
|
2439
|
+
if (previous) {
|
|
2440
|
+
complete = complete && JSON.stringify(previous) === JSON.stringify(track.lyricContent);
|
|
2441
|
+
continue;
|
|
2442
|
+
}
|
|
2443
|
+
selected.set(track.type, track.lyricContent);
|
|
2444
|
+
const attached = attachTrack(result, track);
|
|
2445
|
+
result = attached.lines;
|
|
2446
|
+
complete = complete && attached.complete;
|
|
2447
|
+
}
|
|
2448
|
+
if (!complete) unknowns.push(entry);
|
|
2449
|
+
}
|
|
2450
|
+
meta.unknowns = unknowns;
|
|
2451
|
+
return result;
|
|
2452
|
+
};
|
|
2453
|
+
/**
|
|
2454
|
+
* Parses decrypted Kugou lyrics, retaining explicit durations and attaching embedded annotations by source slot.
|
|
2455
|
+
*
|
|
2456
|
+
* A source carrying no KRC row is read as LRC, so a platform fallback keeps its container annotations.
|
|
2457
|
+
* Placeholder slots carry no annotation, so they never keep a credit line in the stream.
|
|
2458
|
+
* Kana supplies whole-word ruby; type 0 language tracks retain complete line romanization.
|
|
2459
|
+
* Incomplete metadata remains available in unknowns, and source timing violations remain visible in diagnostics.
|
|
2460
|
+
*
|
|
2461
|
+
* @param input Decrypted plaintext or an object containing its original text.
|
|
2462
|
+
*
|
|
2463
|
+
* @returns Canonical lyrics and model diagnostics; no readable timed rows yields INVALID status.
|
|
2464
|
+
*/
|
|
2465
|
+
var parseKrc = (input) => {
|
|
2466
|
+
const source = readKrcContent(input);
|
|
2467
|
+
if (!source) return finalizeLyric(makeLyric({
|
|
2468
|
+
format: "krc",
|
|
2469
|
+
status: LyricStatus.INVALID
|
|
2470
|
+
}));
|
|
2471
|
+
const native = readKrc(source.original);
|
|
2472
|
+
const parsed = native.lines.length ? native : readLrc(source.original);
|
|
2473
|
+
const meta = readLrcMetaTags(parsed.metaTags);
|
|
2474
|
+
const lines = attachKugouLanguage(attachKana(parsed.lines, meta), meta);
|
|
2475
|
+
return finalizeLyric(makeLyric({
|
|
2476
|
+
format: "krc",
|
|
2477
|
+
status: lines.length ? LyricStatus.VALID : LyricStatus.INVALID,
|
|
2478
|
+
timing: lines.some(isWordLevelLine) ? Timing.WORD : Timing.LINE,
|
|
2479
|
+
meta,
|
|
2480
|
+
lines,
|
|
2481
|
+
languages: deriveLanguageUsages(lines)
|
|
2482
|
+
}));
|
|
2483
|
+
};
|
|
2484
|
+
//#endregion
|
|
2485
|
+
//#region src/formats/kwrc/common.ts
|
|
2486
|
+
var FORMAT$1 = "kwrc";
|
|
2487
|
+
var LINE_PATTERN$1 = /^\[([^\]]*)\]/;
|
|
2488
|
+
var WORD_PATTERN$1 = /<(-?\d+,-?\d+(?:,-?\d+)?)>/g;
|
|
2489
|
+
var VISIBLE_TEXT = /[^\p{White_Space}]/u;
|
|
2490
|
+
/**
|
|
2491
|
+
* Removes only complete encoded tags so literal angle-bracket text survives.
|
|
2492
|
+
*/
|
|
2493
|
+
var readText$1 = (text) => {
|
|
2494
|
+
return text.replace(WORD_PATTERN$1, "");
|
|
2495
|
+
};
|
|
2496
|
+
/**
|
|
2497
|
+
* Accepts decoded plaintext without unwrapping XML or auxiliary tracks.
|
|
2498
|
+
*/
|
|
2499
|
+
var readKwrcContent = (input) => {
|
|
2500
|
+
const source = typeof input === "string" ? { original: input } : input;
|
|
2501
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) return null;
|
|
2502
|
+
const original = source.original;
|
|
2503
|
+
if (typeof original !== "string" || /^\s*</.test(original)) return null;
|
|
2504
|
+
return { original };
|
|
2505
|
+
};
|
|
2506
|
+
/**
|
|
2507
|
+
* Retains source slots and literal bodies before word timing or bilingual boundaries are interpreted.
|
|
2508
|
+
*/
|
|
2509
|
+
var readKwrcSource = (text) => {
|
|
2510
|
+
const lines = [];
|
|
2511
|
+
const metaTags = [];
|
|
2512
|
+
const rows = text.replace(/^\uFEFF/, "").split(/\r\n?|\n/);
|
|
2513
|
+
for (const [row, raw] of rows.entries()) {
|
|
2514
|
+
const times = [];
|
|
2515
|
+
let body = raw.trimStart();
|
|
2516
|
+
let header;
|
|
2517
|
+
while (header = LINE_PATTERN$1.exec(body)) {
|
|
2518
|
+
const tag = header[1].trim();
|
|
2519
|
+
const start = parseTime(tag);
|
|
2520
|
+
if (start !== void 0) times.push({
|
|
2521
|
+
start,
|
|
2522
|
+
clock: /^[\d:.]+$/.test(tag)
|
|
2523
|
+
});
|
|
2524
|
+
else {
|
|
2525
|
+
const meta = META_PATTERN.exec(tag);
|
|
2526
|
+
if (!meta || times.length) break;
|
|
2527
|
+
metaTags.push({
|
|
2528
|
+
key: meta[1].toLowerCase(),
|
|
2529
|
+
value: meta[2].trim()
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2532
|
+
body = body.slice(header[0].length);
|
|
2533
|
+
if (!times.length) body = body.trimStart();
|
|
2534
|
+
}
|
|
2535
|
+
for (const time of times) lines.push({
|
|
2536
|
+
row,
|
|
2537
|
+
...time,
|
|
2538
|
+
text: body
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
return {
|
|
2542
|
+
lines,
|
|
2543
|
+
metaTags
|
|
2544
|
+
};
|
|
2545
|
+
};
|
|
2546
|
+
/**
|
|
2547
|
+
* Requires complete octal values and agreement across all parameter tags, without selecting a default scale.
|
|
2548
|
+
*/
|
|
2549
|
+
var readKwrcScales = (tags) => {
|
|
2550
|
+
let value;
|
|
2551
|
+
for (const tag of tags) {
|
|
2552
|
+
if (tag.key !== "kuwo") continue;
|
|
2553
|
+
if (!/^[0-7]+$/.test(tag.value)) return;
|
|
2554
|
+
const next = Number.parseInt(tag.value, 8);
|
|
2555
|
+
if (!Number.isSafeInteger(next) || value !== void 0 && value !== next) return;
|
|
2556
|
+
value = next;
|
|
2557
|
+
}
|
|
2558
|
+
if (value === void 0) return;
|
|
2559
|
+
const start = Math.trunc(value / 10);
|
|
2560
|
+
const duration = value % 10;
|
|
2561
|
+
return start > 0 && duration > 0 ? {
|
|
2562
|
+
start,
|
|
2563
|
+
duration
|
|
2564
|
+
} : void 0;
|
|
2565
|
+
};
|
|
2566
|
+
/**
|
|
2567
|
+
* A repeated timestamp opens the previous original's translation slot before the next original.
|
|
2568
|
+
* The entire sequence must agree so one missing slot cannot shift all later translations.
|
|
2569
|
+
*/
|
|
2570
|
+
var hasKwrcTranslations = ({ lines, metaTags }) => {
|
|
2571
|
+
const markers = metaTags.filter((tag) => tag.key === "ml");
|
|
2572
|
+
if (!markers.length || markers.some((tag) => tag.value !== "1.0") || lines.length < 3 || lines.length % 2 !== 1) return false;
|
|
2573
|
+
if (!lines.every((line, index) => {
|
|
2574
|
+
return line.clock && Number.isSafeInteger(line.start) && line.row !== lines[index - 1]?.row;
|
|
2575
|
+
})) return false;
|
|
2576
|
+
let previous = lines[0];
|
|
2577
|
+
for (let index = 1; index < lines.length; index += 2) {
|
|
2578
|
+
const translation = lines[index];
|
|
2579
|
+
const original = lines[index + 1];
|
|
2580
|
+
if (translation.start <= previous.start || translation.start !== original.start) return false;
|
|
2581
|
+
if (!VISIBLE_TEXT.test(readText$1(previous.text)) && VISIBLE_TEXT.test(readText$1(translation.text))) return false;
|
|
2582
|
+
previous = original;
|
|
2583
|
+
}
|
|
2584
|
+
return true;
|
|
2585
|
+
};
|
|
2586
|
+
//#endregion
|
|
2587
|
+
//#region src/formats/kwrc/detect.ts
|
|
2588
|
+
/**
|
|
2589
|
+
* Recognizes octal parameters with encoded clock rows, or a complete marked bilingual sequence.
|
|
2590
|
+
*/
|
|
2591
|
+
var detectKwrc = (input) => {
|
|
2592
|
+
const source = readKwrcContent(input);
|
|
2593
|
+
if (!source) return false;
|
|
2594
|
+
const track = readKwrcSource(source.original);
|
|
2595
|
+
if (readKwrcScales(track.metaTags)) {
|
|
2596
|
+
for (const line of track.lines) if (line.clock && Number.isSafeInteger(line.start) && line.text.search(WORD_PATTERN$1) !== -1) return true;
|
|
2597
|
+
}
|
|
2598
|
+
return hasKwrcTranslations(track);
|
|
2599
|
+
};
|
|
2600
|
+
//#endregion
|
|
2601
|
+
//#region src/formats/kwrc/parser.ts
|
|
2602
|
+
var SEPARATORS$1 = /^\p{White_Space}+$/u;
|
|
2603
|
+
var readWordTime = (tag, lineStart, scales) => {
|
|
2604
|
+
const values = tag.split(",").map(Number);
|
|
2605
|
+
if (!Number.isSafeInteger(lineStart) || !values.every(Number.isSafeInteger)) return;
|
|
2606
|
+
const [x, y] = values;
|
|
2607
|
+
const sum = x + y;
|
|
2608
|
+
const difference = x - y;
|
|
2609
|
+
if (!Number.isSafeInteger(sum) || !Number.isSafeInteger(difference) || sum % (2 * scales.start) !== 0 || difference % (2 * scales.duration) !== 0) return;
|
|
2610
|
+
const relativeStart = Math.abs(sum / (2 * scales.start));
|
|
2611
|
+
const duration = Math.abs(difference / (2 * scales.duration));
|
|
2612
|
+
const start = lineStart + relativeStart;
|
|
2613
|
+
const end = start + duration;
|
|
2614
|
+
if (![
|
|
2615
|
+
relativeStart,
|
|
2616
|
+
duration,
|
|
2617
|
+
start,
|
|
2618
|
+
end
|
|
2619
|
+
].every(Number.isSafeInteger)) return;
|
|
2620
|
+
return makeTimeRange({
|
|
2621
|
+
start,
|
|
2622
|
+
end
|
|
2623
|
+
});
|
|
2624
|
+
};
|
|
2625
|
+
var readLine$2 = ({ text, start }, nextStart, scales) => {
|
|
2626
|
+
const boundary = nextStart !== void 0 && nextStart >= start ? nextStart : void 0;
|
|
2627
|
+
let end = boundary;
|
|
2628
|
+
const pieces = scanWordTags(text, WORD_PATTERN$1);
|
|
2629
|
+
if (!pieces.some((piece) => piece.tag !== void 0)) {
|
|
2630
|
+
if (Number.isSafeInteger(start) && splitWordTags(text).some((piece) => piece.tag !== void 0)) return readLrc(`[${start}ms]${text}${end === void 0 ? "" : `\n[${end}ms]`}`).lines[0];
|
|
2631
|
+
const time = makeTimeRange({
|
|
2632
|
+
start,
|
|
2633
|
+
end
|
|
2634
|
+
});
|
|
2635
|
+
return VISIBLE_TEXT.test(text) ? makeLineNormal({
|
|
2636
|
+
time,
|
|
2637
|
+
content: makeLineContentText(text)
|
|
2638
|
+
}) : makeLineInstrumental({ time });
|
|
2639
|
+
}
|
|
2640
|
+
const words = [];
|
|
2641
|
+
for (const piece of pieces) {
|
|
2642
|
+
const time = piece.tag !== void 0 && scales ? readWordTime(piece.tag, start, scales) : void 0;
|
|
2643
|
+
if (piece.tag !== void 0 && !time) return makeLineNormal({
|
|
2644
|
+
time: makeTimeRange({
|
|
2645
|
+
start,
|
|
2646
|
+
end: boundary
|
|
2647
|
+
}),
|
|
2648
|
+
content: makeLineContentText(text)
|
|
2649
|
+
});
|
|
2650
|
+
if (time?.end !== void 0) end = Math.max(end ?? start, time.end);
|
|
2651
|
+
for (const fragment of piece.text.split(/(\p{White_Space}+)/u)) if (fragment) words.push(SEPARATORS$1.test(fragment) ? makeWordSpace({ text: fragment }) : makeWordNormal({
|
|
2652
|
+
text: fragment,
|
|
2653
|
+
time
|
|
2654
|
+
}));
|
|
2655
|
+
}
|
|
2656
|
+
const time = makeTimeRange({
|
|
2657
|
+
start,
|
|
2658
|
+
end
|
|
2659
|
+
});
|
|
2660
|
+
return VISIBLE_TEXT.test(readText$1(text)) ? makeLineNormal({
|
|
2661
|
+
time,
|
|
2662
|
+
content: makeLineContentWords(words)
|
|
2663
|
+
}) : makeLineInstrumental({ time });
|
|
2664
|
+
};
|
|
2665
|
+
/**
|
|
2666
|
+
* Parses decoded KWRC with explicit word durations and structurally verified embedded translations.
|
|
2667
|
+
*
|
|
2668
|
+
* Missing or conflicting parameters and unsafe word calculations retain the complete row as line-level text.
|
|
2669
|
+
* Plain LRC remains readable; offset is stored as metadata without changing source timestamps.
|
|
2670
|
+
*
|
|
2671
|
+
* @param input Decoded plaintext or an object containing its original text.
|
|
2672
|
+
*
|
|
2673
|
+
* @returns Canonical lyrics and model diagnostics; no readable timed rows yields INVALID status.
|
|
2674
|
+
*/
|
|
2675
|
+
var parseKwrc = (input) => {
|
|
2676
|
+
const source = readKwrcContent(input);
|
|
2677
|
+
if (!source) return finalizeLyric(makeLyric({
|
|
2678
|
+
format: FORMAT$1,
|
|
2679
|
+
status: LyricStatus.INVALID
|
|
2680
|
+
}));
|
|
2681
|
+
const track = readKwrcSource(source.original);
|
|
2682
|
+
const bilingual = hasKwrcTranslations(track);
|
|
2683
|
+
const encoded = track.lines.some((line) => !!line.text.match(WORD_PATTERN$1));
|
|
2684
|
+
let lines;
|
|
2685
|
+
let tags = track.metaTags;
|
|
2686
|
+
if (!encoded && !bilingual) {
|
|
2687
|
+
const fallback = readLrc(source.original);
|
|
2688
|
+
lines = fallback.lines;
|
|
2689
|
+
tags = fallback.metaTags;
|
|
2690
|
+
} else {
|
|
2691
|
+
const scales = readKwrcScales(tags);
|
|
2692
|
+
const originals = bilingual ? track.lines.filter((_, index) => index % 2 === 0) : track.lines;
|
|
2693
|
+
lines = originals.map((original, index) => {
|
|
2694
|
+
const line = readLine$2(original, originals[index + 1]?.start, scales);
|
|
2695
|
+
const translation = bilingual ? readText$1(track.lines[index * 2 + 1]?.text ?? "") : "";
|
|
2696
|
+
if (VISIBLE_TEXT.test(translation)) line.annotation = makeLineAnnotation({ translations: [makeLineAnnotationTranslation({ text: translation })] });
|
|
2697
|
+
return line;
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
const last = lines.at(-1);
|
|
2701
|
+
if (last?.type === LineType.INSTRUMENTAL && (last.time?.end === void 0 || last.time.end === last.time.start)) lines.pop();
|
|
2702
|
+
return finalizeLyric(makeLyric({
|
|
2703
|
+
format: FORMAT$1,
|
|
2704
|
+
status: lines.length ? LyricStatus.VALID : LyricStatus.INVALID,
|
|
2705
|
+
timing: lines.some(isWordLevelLine) ? Timing.WORD : Timing.LINE,
|
|
2706
|
+
meta: readLrcMetaTags(tags),
|
|
2707
|
+
lines,
|
|
2708
|
+
languages: deriveLanguageUsages(lines)
|
|
2709
|
+
}));
|
|
2710
|
+
};
|
|
2711
|
+
//#endregion
|
|
2189
2712
|
//#region src/formats/ttml/common.ts
|
|
2190
2713
|
var FORMAT = "ttml";
|
|
2191
2714
|
var AMLL_HINT_PATTERN = /xmlns:amll|amll:meta/iu;
|
|
@@ -2896,7 +3419,7 @@ var readInlineAnnotations = (element, preserveSpace, language) => {
|
|
|
2896
3419
|
/**
|
|
2897
3420
|
* Reads one paragraph, pulling its inline annotations and background lines out of the word stream.
|
|
2898
3421
|
*/
|
|
2899
|
-
var readLine = (element, preserveSpace, language) => {
|
|
3422
|
+
var readLine$1 = (element, preserveSpace, language) => {
|
|
2900
3423
|
preserveSpace = readPreserveSpace(element, preserveSpace);
|
|
2901
3424
|
language = readLanguage(element, language);
|
|
2902
3425
|
const time = readLineTime(element);
|
|
@@ -3177,7 +3700,7 @@ var parseTtml = (input, options = {}) => {
|
|
|
3177
3700
|
const lineMap = /* @__PURE__ */ new Map();
|
|
3178
3701
|
const backgroundMap = /* @__PURE__ */ new Map();
|
|
3179
3702
|
for (const { element, preserveSpace, language: lineLanguage } of body ? findElements(body.element, "p", body.preserveSpace, true, body.language) : []) {
|
|
3180
|
-
const draft = readLine(element, preserveSpace, lineLanguage);
|
|
3703
|
+
const draft = readLine$1(element, preserveSpace, lineLanguage);
|
|
3181
3704
|
if (!draft) continue;
|
|
3182
3705
|
drafts.push(draft);
|
|
3183
3706
|
if (draft.key) lineMap.set(draft.key, draft);
|
|
@@ -3208,6 +3731,260 @@ var parseTtml = (input, options = {}) => {
|
|
|
3208
3731
|
}));
|
|
3209
3732
|
};
|
|
3210
3733
|
//#endregion
|
|
3734
|
+
//#region src/formats/yrc/common.ts
|
|
3735
|
+
var LINE_PATTERN = /^\[(\d+),(\d+)\]/;
|
|
3736
|
+
var WORD_PATTERN = /\((\d+,\d+,\d+)\)/g;
|
|
3737
|
+
/**
|
|
3738
|
+
* Accepts plaintext tracks on one timeline without interpreting an API response or an XML container.
|
|
3739
|
+
*
|
|
3740
|
+
* @returns Original and optional auxiliary text, or null when the original payload is unreadable.
|
|
3741
|
+
*/
|
|
3742
|
+
var readYrcContent = (input) => {
|
|
3743
|
+
const value = typeof input === "string" ? { original: input } : input;
|
|
3744
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
3745
|
+
const source = value;
|
|
3746
|
+
if (typeof source.original !== "string" || /^\s*</.test(source.original)) return null;
|
|
3747
|
+
return {
|
|
3748
|
+
original: source.original,
|
|
3749
|
+
translations: typeof source.translations === "string" ? source.translations : void 0,
|
|
3750
|
+
romans: typeof source.romans === "string" ? source.romans : void 0
|
|
3751
|
+
};
|
|
3752
|
+
};
|
|
3753
|
+
//#endregion
|
|
3754
|
+
//#region src/formats/yrc/parser.ts
|
|
3755
|
+
var SEPARATORS = /^\p{White_Space}+$/u;
|
|
3756
|
+
var CREDIT_LABEL_PATTERN = /[::]$/u;
|
|
3757
|
+
var isObject = (value) => {
|
|
3758
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
3759
|
+
};
|
|
3760
|
+
var readCredit = (fragments) => {
|
|
3761
|
+
const label = trimRuleWhitespace(fragments[0] ?? "");
|
|
3762
|
+
const raw = trimRuleWhitespace(label.replace(CREDIT_LABEL_PATTERN, ""));
|
|
3763
|
+
if (!raw) return;
|
|
3764
|
+
const names = fragments.slice(1).join("").split("/").map(trimRuleWhitespace).filter(Boolean);
|
|
3765
|
+
return names.length ? {
|
|
3766
|
+
raw,
|
|
3767
|
+
names
|
|
3768
|
+
} : void 0;
|
|
3769
|
+
};
|
|
3770
|
+
/**
|
|
3771
|
+
* Reads JSON display text and splits the joined credit content at slash separators.
|
|
3772
|
+
*
|
|
3773
|
+
* @returns A readable display line and whether all source fields are represented, or null for other JSON.
|
|
3774
|
+
*/
|
|
3775
|
+
var readJsonLine = (text) => {
|
|
3776
|
+
try {
|
|
3777
|
+
const value = JSON.parse(text);
|
|
3778
|
+
if (!isObject(value) || typeof value.t !== "number" || !Number.isSafeInteger(value.t) || value.t < 0 || !Array.isArray(value.c)) return null;
|
|
3779
|
+
const fragments = [];
|
|
3780
|
+
let complete = Object.keys(value).every((key) => key === "t" || key === "c");
|
|
3781
|
+
for (const fragment of value.c) {
|
|
3782
|
+
if (!isObject(fragment) || typeof fragment.tx !== "string") return null;
|
|
3783
|
+
fragments.push(fragment.tx);
|
|
3784
|
+
complete = complete && Object.keys(fragment).every((key) => key === "tx");
|
|
3785
|
+
}
|
|
3786
|
+
const content = fragments.join("");
|
|
3787
|
+
return /[^\p{White_Space}]/u.test(content) ? {
|
|
3788
|
+
start: value.t,
|
|
3789
|
+
text: content,
|
|
3790
|
+
complete,
|
|
3791
|
+
credit: readCredit(fragments)
|
|
3792
|
+
} : null;
|
|
3793
|
+
} catch {
|
|
3794
|
+
return null;
|
|
3795
|
+
}
|
|
3796
|
+
};
|
|
3797
|
+
var appendWords = (words, text, time) => {
|
|
3798
|
+
for (const fragment of text.split(/(\p{White_Space}+)/u)) if (fragment) words.push(SEPARATORS.test(fragment) ? makeWordSpace({ text: fragment }) : makeWordNormal({
|
|
3799
|
+
text: fragment,
|
|
3800
|
+
time
|
|
3801
|
+
}));
|
|
3802
|
+
};
|
|
3803
|
+
var readLine = (body, start, duration) => {
|
|
3804
|
+
const time = makeTimeRange({
|
|
3805
|
+
start,
|
|
3806
|
+
end: start + duration
|
|
3807
|
+
});
|
|
3808
|
+
const pieces = scanWordTags(body, WORD_PATTERN);
|
|
3809
|
+
if (!pieces.some((piece) => piece.tag !== void 0)) return /[^\p{White_Space}]/u.test(body) ? makeLineNormal({
|
|
3810
|
+
time,
|
|
3811
|
+
content: makeLineContentText(body)
|
|
3812
|
+
}) : makeLineInstrumental({ time });
|
|
3813
|
+
const words = [];
|
|
3814
|
+
for (const piece of pieces) {
|
|
3815
|
+
if (piece.tag === void 0) {
|
|
3816
|
+
appendWords(words, piece.text);
|
|
3817
|
+
continue;
|
|
3818
|
+
}
|
|
3819
|
+
const [wordStart, wordDuration] = piece.tag.split(",").map(Number);
|
|
3820
|
+
appendWords(words, piece.text, makeTimeRange({
|
|
3821
|
+
start: wordStart,
|
|
3822
|
+
end: wordStart + wordDuration
|
|
3823
|
+
}));
|
|
3824
|
+
}
|
|
3825
|
+
return words.some((word) => word.type === WordType.NORMAL) ? makeLineNormal({
|
|
3826
|
+
time,
|
|
3827
|
+
content: makeLineContentWords(words)
|
|
3828
|
+
}) : makeLineInstrumental({ time });
|
|
3829
|
+
};
|
|
3830
|
+
var readTrack = (text, timings) => {
|
|
3831
|
+
const tags = [];
|
|
3832
|
+
const native = [];
|
|
3833
|
+
const fallback = [];
|
|
3834
|
+
const displays = [];
|
|
3835
|
+
const rows = /^\s*</.test(text) ? [] : text.replace(/^\uFEFF/, "").split(/\r\n?|\n/);
|
|
3836
|
+
for (const [row, raw] of rows.entries()) {
|
|
3837
|
+
const line = raw.trimStart();
|
|
3838
|
+
const header = LINE_PATTERN.exec(line);
|
|
3839
|
+
if (header) {
|
|
3840
|
+
native.push({
|
|
3841
|
+
row,
|
|
3842
|
+
line: readLine(line.slice(header[0].length), Number(header[1]), Number(header[2]))
|
|
3843
|
+
});
|
|
3844
|
+
fallback.push("");
|
|
3845
|
+
} else if (line.startsWith("{")) {
|
|
3846
|
+
const display = readJsonLine(line);
|
|
3847
|
+
if (display) {
|
|
3848
|
+
displays.push({
|
|
3849
|
+
row,
|
|
3850
|
+
display: true,
|
|
3851
|
+
credit: !!display.credit,
|
|
3852
|
+
line: makeLineNormal({
|
|
3853
|
+
time: makeTimeRange({ start: display.start }),
|
|
3854
|
+
content: makeLineContentText(display.text)
|
|
3855
|
+
})
|
|
3856
|
+
});
|
|
3857
|
+
if (display.credit) {
|
|
3858
|
+
const { raw, names } = display.credit;
|
|
3859
|
+
for (const value of names) tags.push({
|
|
3860
|
+
row,
|
|
3861
|
+
unknown: true,
|
|
3862
|
+
tag: {
|
|
3863
|
+
key: raw,
|
|
3864
|
+
value
|
|
3865
|
+
}
|
|
3866
|
+
});
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
if (!display?.complete) tags.push({
|
|
3870
|
+
row,
|
|
3871
|
+
unknown: true,
|
|
3872
|
+
tag: {
|
|
3873
|
+
key: "yrc:json",
|
|
3874
|
+
value: raw
|
|
3875
|
+
}
|
|
3876
|
+
});
|
|
3877
|
+
fallback.push(display ? `[${display.start}ms]` : "");
|
|
3878
|
+
} else fallback.push(raw);
|
|
3879
|
+
}
|
|
3880
|
+
const clock = [];
|
|
3881
|
+
const displayRows = new Set(displays.map((entry) => entry.row));
|
|
3882
|
+
readLrc(fallback.join("\n"), {
|
|
3883
|
+
onLine: (line, timing, row) => {
|
|
3884
|
+
if (displayRows.has(row)) return;
|
|
3885
|
+
clock.push({
|
|
3886
|
+
row,
|
|
3887
|
+
line
|
|
3888
|
+
});
|
|
3889
|
+
timings?.set(line, timing);
|
|
3890
|
+
},
|
|
3891
|
+
onMeta: (tag, row) => {
|
|
3892
|
+
tags.push({
|
|
3893
|
+
row,
|
|
3894
|
+
tag
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3897
|
+
});
|
|
3898
|
+
const entries = [...displays, ...native.length ? native : clock].sort((first, second) => first.row - second.row);
|
|
3899
|
+
for (const [index, entry] of entries.entries()) {
|
|
3900
|
+
if (!entry.display) continue;
|
|
3901
|
+
const next = entries[index + 1]?.line.time?.start;
|
|
3902
|
+
if (next !== void 0 && next >= entry.line.time.start) entry.line.time.end = next;
|
|
3903
|
+
}
|
|
3904
|
+
const metaTags = tags.sort((first, second) => first.row - second.row);
|
|
3905
|
+
return {
|
|
3906
|
+
entries: entries.filter((entry) => !entry.credit),
|
|
3907
|
+
metaTags
|
|
3908
|
+
};
|
|
3909
|
+
};
|
|
3910
|
+
var readAnnotations = (text, timings) => {
|
|
3911
|
+
return text ? readTrack(text, timings).entries.filter((entry) => !entry.display).map((entry) => entry.line) : [];
|
|
3912
|
+
};
|
|
3913
|
+
/**
|
|
3914
|
+
* Parses native YRC or LRC fallback, keeping JSON display rows separate from annotation matching.
|
|
3915
|
+
* Explicit line and word durations remain unchanged, including zero durations and source timing violations.
|
|
3916
|
+
* JSON credit content becomes separate plain-text unknown entries sharing the source label and is omitted from lyric lines.
|
|
3917
|
+
*
|
|
3918
|
+
* @param input Plaintext tracks on the same timeline: yrc with ytlrc/yromalrc, or lrc with tlyric/romalrc.
|
|
3919
|
+
* @param options Independent line and word tolerances, with optional local roman recovery.
|
|
3920
|
+
*
|
|
3921
|
+
* @returns Canonical lyrics and model diagnostics; no readable timed rows yields INVALID status.
|
|
3922
|
+
*/
|
|
3923
|
+
var parseYrc = (input, options = {}) => {
|
|
3924
|
+
const source = readYrcContent(input);
|
|
3925
|
+
if (!source) return finalizeLyric(makeLyric({
|
|
3926
|
+
format: "yrc",
|
|
3927
|
+
status: LyricStatus.INVALID
|
|
3928
|
+
}));
|
|
3929
|
+
const parsed = readTrack(source.original);
|
|
3930
|
+
const meta = readLrcMetaTags(parsed.metaTags.filter((entry) => !entry.unknown).map((entry) => entry.tag));
|
|
3931
|
+
let unknownIndex = 0;
|
|
3932
|
+
meta.unknowns = parsed.metaTags.flatMap(({ tag, unknown }) => {
|
|
3933
|
+
if (unknown) return [makeUnknownEntry(tag)];
|
|
3934
|
+
const entry = meta.unknowns[unknownIndex];
|
|
3935
|
+
if (entry?.key === tag.key && entry.value === tag.value) {
|
|
3936
|
+
unknownIndex += 1;
|
|
3937
|
+
return [entry];
|
|
3938
|
+
}
|
|
3939
|
+
return [];
|
|
3940
|
+
});
|
|
3941
|
+
const body = parsed.entries.filter((entry) => !entry.display).map((entry) => entry.line);
|
|
3942
|
+
const timings = /* @__PURE__ */ new WeakMap();
|
|
3943
|
+
const annotated = attachAnnotations(body, {
|
|
3944
|
+
translations: readAnnotations(source.translations, timings),
|
|
3945
|
+
romans: readAnnotations(source.romans, timings)
|
|
3946
|
+
}, {
|
|
3947
|
+
preservePlaceholders: true,
|
|
3948
|
+
tolerance: options.annotationTolerance,
|
|
3949
|
+
wordTolerance: options.wordAnnotationTolerance,
|
|
3950
|
+
wordFallback: options.wordAnnotationFallback,
|
|
3951
|
+
getTiming: (line) => timings.get(line)
|
|
3952
|
+
});
|
|
3953
|
+
let index = 0;
|
|
3954
|
+
const lines = parsed.entries.map((entry) => {
|
|
3955
|
+
return entry.display ? entry.line : annotated[index++];
|
|
3956
|
+
});
|
|
3957
|
+
return finalizeLyric(makeLyric({
|
|
3958
|
+
format: "yrc",
|
|
3959
|
+
status: lines.length ? LyricStatus.VALID : LyricStatus.INVALID,
|
|
3960
|
+
timing: lines.some(isWordLevelLine) ? Timing.WORD : Timing.LINE,
|
|
3961
|
+
meta,
|
|
3962
|
+
lines,
|
|
3963
|
+
languages: deriveLanguageUsages(lines)
|
|
3964
|
+
}));
|
|
3965
|
+
};
|
|
3966
|
+
//#endregion
|
|
3967
|
+
//#region src/formats/yrc/detect.ts
|
|
3968
|
+
/**
|
|
3969
|
+
* Recognizes native word tags or a JSON display line accompanied by LRC content in the original track.
|
|
3970
|
+
*
|
|
3971
|
+
* @returns Whether the original plaintext carries a YRC signal stronger than its shared QRC line header.
|
|
3972
|
+
*/
|
|
3973
|
+
var detectYrc = (input) => {
|
|
3974
|
+
const source = readYrcContent(input);
|
|
3975
|
+
if (!source) return false;
|
|
3976
|
+
let display = false;
|
|
3977
|
+
const fallback = [];
|
|
3978
|
+
for (const raw of source.original.split(/\r\n?|\n/)) {
|
|
3979
|
+
const line = raw.trimStart();
|
|
3980
|
+
const header = LINE_PATTERN.exec(line);
|
|
3981
|
+
if (header && line.slice(header[0].length).match(WORD_PATTERN)) return true;
|
|
3982
|
+
if (line.startsWith("{")) display = display || !!readJsonLine(line);
|
|
3983
|
+
else fallback.push(raw);
|
|
3984
|
+
}
|
|
3985
|
+
return display && detectLrc(fallback.join("\n"));
|
|
3986
|
+
};
|
|
3987
|
+
//#endregion
|
|
3211
3988
|
//#region src/formats/index.ts
|
|
3212
3989
|
/**
|
|
3213
3990
|
* Recognition reads only the original text; the annotation tracks never decide the format.
|
|
@@ -3215,7 +3992,10 @@ var parseTtml = (input, options = {}) => {
|
|
|
3215
3992
|
* Unrecognized text returns `undefined`.
|
|
3216
3993
|
*/
|
|
3217
3994
|
var detectFormat = (input) => {
|
|
3995
|
+
if (detectKrc(input)) return "krc";
|
|
3996
|
+
if (detectYrc(input)) return "yrc";
|
|
3218
3997
|
if (detectQrc(input)) return "qrc";
|
|
3998
|
+
if (detectKwrc(input)) return "kwrc";
|
|
3219
3999
|
if (detectLrc(input)) return "lrc";
|
|
3220
4000
|
if (detectTtml(input)) return "ttml";
|
|
3221
4001
|
};
|
|
@@ -3227,9 +4007,12 @@ var parse = (input, options = {}) => {
|
|
|
3227
4007
|
const format = detectFormat(input);
|
|
3228
4008
|
if (!format) return null;
|
|
3229
4009
|
switch (format) {
|
|
4010
|
+
case "krc": return parseKrc(input);
|
|
4011
|
+
case "kwrc": return parseKwrc(input);
|
|
3230
4012
|
case "lrc": return parseLrc(input, options);
|
|
3231
4013
|
case "qrc": return parseQrc(input, options);
|
|
3232
4014
|
case "ttml": return parseTtml(input, options);
|
|
4015
|
+
case "yrc": return parseYrc(input, options);
|
|
3233
4016
|
}
|
|
3234
4017
|
};
|
|
3235
4018
|
var GENERATORS = {
|
|
@@ -3247,6 +4030,6 @@ var generate = (lyric, target, options = {}) => {
|
|
|
3247
4030
|
return GENERATORS[target](lyric, options);
|
|
3248
4031
|
};
|
|
3249
4032
|
//#endregion
|
|
3250
|
-
export { detectFormat, detectLrc, detectQrc, detectTtml, generate, generateLrc, generateTtml, parse, parseLrc, parseQrc, parseTtml, readLrc };
|
|
4033
|
+
export { META_PATTERN, TAG_PATTERN, attachRubies, detectFormat, detectKrc, detectKwrc, detectLrc, detectQrc, detectTtml, detectYrc, generate, generateLrc, generateTtml, isPlaceholderText, parse, parseKrc, parseKwrc, parseLrc, parseQrc, parseTtml, parseWordTime, parseYrc, readJsonLine, readKrc, readLrc, readLrcMetaTags, readRubyEntries, scanWordTags, splitWordTags };
|
|
3251
4034
|
|
|
3252
4035
|
//# sourceMappingURL=formats.js.map
|