@uimaxbai/am-lyrics 1.1.1 → 1.1.4

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/bun.lock CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "lockfileVersion": 1,
3
+ "configVersion": 0,
3
4
  "workspaces": {
4
5
  "": {
5
6
  "name": "am-lyrics",
@@ -84,6 +84,9 @@ export declare class AmLyrics extends LitElement {
84
84
  * Uses search endpoint, prefers synced lyrics.
85
85
  */
86
86
  private static fetchLyricsFromLrclib;
87
+ private static fetchLyricsFromGenius;
88
+ private static calculateLineAlignments;
89
+ private static parseTTML;
87
90
  private static convertKPoeLyrics;
88
91
  private static toMilliseconds;
89
92
  firstUpdated(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"AmLyrics.d.ts","sourceRoot":"","sources":["../../src/AmLyrics.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AmLyrics.d.ts","sourceRoot":"","sources":["../../src/AmLyrics.ts"],"names":[],"mappings":"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"}
@@ -309,7 +309,7 @@ class GoogleService {
309
309
  }
310
310
  }
311
311
 
312
- const VERSION = '1.1.1';
312
+ const VERSION = '1.1.4';
313
313
  const INSTRUMENTAL_THRESHOLD_MS = 7000; // Show dots for gaps >= 7s
314
314
  const KPOE_SERVERS = [
315
315
  'https://lyricsplus.binimum.org',
@@ -332,6 +332,7 @@ const TIDAL_SERVERS = [
332
332
  'https://hifi-one.spotisaver.net',
333
333
  'https://hifi-two.spotisaver.net',
334
334
  ];
335
+ const GENIUS_WORKER_URL = 'https://fetch-genius.samidy.workers.dev/';
335
336
  class AmLyrics extends i {
336
337
  constructor() {
337
338
  super(...arguments);
@@ -505,9 +506,20 @@ class AmLyrics extends i {
505
506
  });
506
507
  }
507
508
  }
509
+ if (collectedSources.length === 0 && resolvedMetadata?.metadata) {
510
+ const geniusResult = await AmLyrics.fetchLyricsFromGenius(resolvedMetadata.metadata);
511
+ if (geniusResult && geniusResult.lines.length > 0) {
512
+ collectedSources.push({
513
+ lines: geniusResult.lines,
514
+ source: 'Genius',
515
+ });
516
+ }
517
+ }
508
518
  this.hasFetchedAllProviders =
509
519
  collectedSources.length === 0 ||
510
- collectedSources.some(s => s.source === 'LRCLIB' || s.source === 'Tidal');
520
+ collectedSources.some(s => s.source === 'LRCLIB' ||
521
+ s.source === 'Tidal' ||
522
+ s.source === 'Genius');
511
523
  if (collectedSources.length > 0) {
512
524
  this.availableSources = AmLyrics.mergeAndSortSources(collectedSources);
513
525
  this.currentSourceIndex = 0;
@@ -584,6 +596,8 @@ class AmLyrics extends i {
584
596
  return 14;
585
597
  if (lower.includes('lrclib') && isUnsynced)
586
598
  return 15;
599
+ if (lower.includes('genius'))
600
+ return 16;
587
601
  return 20;
588
602
  }
589
603
  static mergeAndSortSources(collectedSources) {
@@ -627,6 +641,12 @@ class AmLyrics extends i {
627
641
  newSources.push({ lines: lrclibResult.lines, source: 'LRCLIB' });
628
642
  }
629
643
  }
644
+ if (!this.availableSources.some(s => s.source.toLowerCase().includes('genius'))) {
645
+ const geniusResult = await AmLyrics.fetchLyricsFromGenius(resolvedMetadata.metadata);
646
+ if (geniusResult && geniusResult.lines.length > 0) {
647
+ newSources.push({ lines: geniusResult.lines, source: 'Genius' });
648
+ }
649
+ }
630
650
  if (newSources.length > 0) {
631
651
  this.availableSources = AmLyrics.mergeAndSortSources([
632
652
  ...this.availableSources,
@@ -831,6 +851,80 @@ class AmLyrics extends i {
831
851
  return 10;
832
852
  };
833
853
  const allResults = [];
854
+ // Try cache API first
855
+ try {
856
+ const cacheParams = new URLSearchParams({
857
+ track: title,
858
+ artist,
859
+ });
860
+ if (metadata.album) {
861
+ cacheParams.append('album', metadata.album);
862
+ }
863
+ if (metadata.durationMs && metadata.durationMs > 0) {
864
+ cacheParams.append('duration', Math.round(metadata.durationMs / 1000).toString());
865
+ }
866
+ const cacheUrl = `https://lyrics-api.binimum.org/?${cacheParams.toString()}`;
867
+ const cacheRes = await fetch(cacheUrl);
868
+ if (cacheRes.ok) {
869
+ const cacheData = await cacheRes.json();
870
+ if (cacheData.results && cacheData.results.length > 0) {
871
+ const result = cacheData.results[0];
872
+ if (result.timing_type === 'word' && result.lyricsUrl) {
873
+ const ttmlRes = await fetch(result.lyricsUrl);
874
+ if (ttmlRes.ok) {
875
+ const ttmlText = await ttmlRes.text();
876
+ const lines = AmLyrics.parseTTML(ttmlText);
877
+ if (lines && lines.length > 0) {
878
+ allResults.push({ lines, source: 'BiniLyrics' });
879
+ return allResults;
880
+ }
881
+ }
882
+ }
883
+ else {
884
+ // Not word type, try QQ
885
+ const qqParams = new URLSearchParams(params);
886
+ qqParams.set('source', 'qq');
887
+ const qqUrl = `https://lyricsplus.binimum.org/v2/lyrics/get?${qqParams.toString()}`;
888
+ try {
889
+ const qqRes = await fetch(qqUrl);
890
+ if (qqRes.ok) {
891
+ const payload = await qqRes.json();
892
+ const lines = AmLyrics.convertKPoeLyrics(payload);
893
+ const hasWordSync = lines?.some((line) => line.text &&
894
+ Array.isArray(line.text) &&
895
+ line.text.length > 1);
896
+ if (lines && lines.length > 0 && hasWordSync) {
897
+ allResults.push({ lines, source: 'QQ (Cache Fallback)' });
898
+ return allResults;
899
+ }
900
+ }
901
+ }
902
+ catch (qqError) {
903
+ // Ignore QQ fetch error
904
+ }
905
+ // If QQ fails or has no word sync, fall back to bini lyrics
906
+ if (result.lyricsUrl) {
907
+ const ttmlRes = await fetch(result.lyricsUrl);
908
+ if (ttmlRes.ok) {
909
+ const ttmlText = await ttmlRes.text();
910
+ const lines = AmLyrics.parseTTML(ttmlText);
911
+ if (lines && lines.length > 0) {
912
+ allResults.push({
913
+ lines,
914
+ source: 'Apple Music (Cache Fallback)',
915
+ });
916
+ return allResults;
917
+ }
918
+ }
919
+ }
920
+ }
921
+ }
922
+ }
923
+ }
924
+ catch (e) {
925
+ // eslint-disable-next-line no-console
926
+ console.error('Cache API failed', e);
927
+ }
834
928
  // Shuffle servers so we pick a random one first, with all others as fallback
835
929
  // Limit to 2 servers to prevent unnecessary API spam when Apple lyrics are missing
836
930
  const shuffledServers = [...KPOE_SERVERS]
@@ -910,6 +1004,7 @@ class AmLyrics extends i {
910
1004
  if (!match) {
911
1005
  // Skip non-timestamped lines (headers like [ti:], [ar:], etc.)
912
1006
  // eslint-disable-next-line no-continue
1007
+ // eslint-disable-next-line no-continue
913
1008
  continue;
914
1009
  }
915
1010
  const minutes = parseInt(match[1], 10);
@@ -929,6 +1024,7 @@ class AmLyrics extends i {
929
1024
  const endtime = i + 1 < parsed.length ? parsed[i + 1].timestamp : timestamp + 5000;
930
1025
  // Skip empty lines (instrumental gaps)
931
1026
  if (!text.trim()) {
1027
+ // eslint-disable-next-line no-continue
932
1028
  // eslint-disable-next-line no-continue
933
1029
  continue;
934
1030
  }
@@ -972,6 +1068,7 @@ class AmLyrics extends i {
972
1068
  // eslint-disable-next-line no-await-in-loop
973
1069
  const searchResponse = await fetch(`${normalizedBase}/search/?${searchParams.toString()}`);
974
1070
  if (!searchResponse.ok) {
1071
+ // eslint-disable-next-line no-continue
975
1072
  // eslint-disable-next-line no-continue
976
1073
  continue;
977
1074
  }
@@ -979,6 +1076,7 @@ class AmLyrics extends i {
979
1076
  const searchData = await searchResponse.json();
980
1077
  const items = searchData?.data?.items;
981
1078
  if (!Array.isArray(items) || items.length === 0) {
1079
+ // eslint-disable-next-line no-continue
982
1080
  // eslint-disable-next-line no-continue
983
1081
  continue;
984
1082
  }
@@ -992,6 +1090,7 @@ class AmLyrics extends i {
992
1090
  }
993
1091
  const trackId = bestTrack?.id;
994
1092
  if (!trackId) {
1093
+ // eslint-disable-next-line no-continue
995
1094
  // eslint-disable-next-line no-continue
996
1095
  continue;
997
1096
  }
@@ -999,6 +1098,7 @@ class AmLyrics extends i {
999
1098
  // eslint-disable-next-line no-await-in-loop
1000
1099
  const lyricsResponse = await fetch(`${normalizedBase}/lyrics/?id=${trackId}`);
1001
1100
  if (!lyricsResponse.ok) {
1101
+ // eslint-disable-next-line no-continue
1002
1102
  // eslint-disable-next-line no-continue
1003
1103
  continue;
1004
1104
  }
@@ -1032,7 +1132,7 @@ class AmLyrics extends i {
1032
1132
  if (!title || !artist)
1033
1133
  return null;
1034
1134
  try {
1035
- const searchQuery = `${title} ${artist}`;
1135
+ const searchQuery = `${artist} ${title}`;
1036
1136
  const params = new URLSearchParams({ q: searchQuery });
1037
1137
  const response = await fetch(`https://lrclib.net/api/search?${params.toString()}`, {
1038
1138
  headers: {
@@ -1085,6 +1185,268 @@ class AmLyrics extends i {
1085
1185
  }
1086
1186
  return null;
1087
1187
  }
1188
+ static async fetchLyricsFromGenius(metadata) {
1189
+ const title = metadata.title?.trim();
1190
+ const artist = metadata.artist?.trim();
1191
+ if (!title || !artist)
1192
+ return null;
1193
+ try {
1194
+ const params = new URLSearchParams({ title, artist });
1195
+ const response = await fetch(`${GENIUS_WORKER_URL}?${params.toString()}`);
1196
+ if (!response.ok)
1197
+ return null;
1198
+ const data = await response.json();
1199
+ if (data.lyrics) {
1200
+ const plainLines = data.lyrics
1201
+ .split('\n')
1202
+ .map((l) => l.trim())
1203
+ .filter((l) => l && !l.startsWith('['));
1204
+ if (plainLines.length > 0) {
1205
+ const lines = plainLines.map((text) => ({
1206
+ text: [
1207
+ {
1208
+ text,
1209
+ part: false,
1210
+ timestamp: 0,
1211
+ endtime: 0,
1212
+ },
1213
+ ],
1214
+ background: false,
1215
+ backgroundText: [],
1216
+ oppositeTurn: false,
1217
+ timestamp: 0,
1218
+ endtime: 0,
1219
+ isWordSynced: false,
1220
+ }));
1221
+ return { lines, source: 'Genius' };
1222
+ }
1223
+ }
1224
+ }
1225
+ catch {
1226
+ // eslint-disable-next-line no-console
1227
+ console.error('No Genius lyrics found');
1228
+ }
1229
+ return null;
1230
+ }
1231
+ static calculateLineAlignments(lineSingers, agentTypes) {
1232
+ const lineSideAssignments = new Array(lineSingers.length).fill(undefined);
1233
+ let currentSideIsLeft = true;
1234
+ let lastPersonSingerId = null;
1235
+ let rightCount = 0;
1236
+ let totalCount = 0;
1237
+ lineSingers.forEach((singerId, index) => {
1238
+ let sideClass;
1239
+ if (singerId) {
1240
+ let type = agentTypes[singerId];
1241
+ if (!type) {
1242
+ if (singerId === 'v1000') {
1243
+ type = 'group';
1244
+ }
1245
+ else if (singerId === 'v2000') {
1246
+ type = 'other';
1247
+ }
1248
+ else {
1249
+ type = 'person';
1250
+ }
1251
+ }
1252
+ if (type === 'group') {
1253
+ sideClass = 'start';
1254
+ }
1255
+ else {
1256
+ if (lastPersonSingerId === null) {
1257
+ if (type === 'other') {
1258
+ currentSideIsLeft = false;
1259
+ }
1260
+ else {
1261
+ currentSideIsLeft = true;
1262
+ }
1263
+ }
1264
+ else if (singerId !== lastPersonSingerId) {
1265
+ currentSideIsLeft = !currentSideIsLeft;
1266
+ }
1267
+ sideClass = currentSideIsLeft ? 'start' : 'end';
1268
+ lastPersonSingerId = singerId;
1269
+ }
1270
+ }
1271
+ if (sideClass) {
1272
+ totalCount += 1;
1273
+ if (sideClass === 'end')
1274
+ rightCount += 1;
1275
+ }
1276
+ lineSideAssignments[index] = sideClass;
1277
+ });
1278
+ if (totalCount > 0 && Math.round((rightCount / totalCount) * 100) >= 85) {
1279
+ const flip = (s) => {
1280
+ if (s === 'start')
1281
+ return 'end';
1282
+ if (s === 'end')
1283
+ return 'start';
1284
+ return s;
1285
+ };
1286
+ for (let i = 0; i < lineSideAssignments.length; i += 1) {
1287
+ lineSideAssignments[i] = flip(lineSideAssignments[i]);
1288
+ }
1289
+ }
1290
+ return lineSideAssignments;
1291
+ }
1292
+ static parseTTML(ttmlString) {
1293
+ try {
1294
+ const parser = new DOMParser();
1295
+ const doc = parser.parseFromString(ttmlString, 'text/xml');
1296
+ const translations = {};
1297
+ const transliterations = {};
1298
+ const agentMap = {};
1299
+ const agents = doc.getElementsByTagName('ttm:agent');
1300
+ for (let i = 0; i < agents.length; i += 1) {
1301
+ const agent = agents[i];
1302
+ const id = agent.getAttribute('xml:id');
1303
+ const type = agent.getAttribute('type');
1304
+ if (id && type) {
1305
+ agentMap[id] = type;
1306
+ }
1307
+ }
1308
+ const translationNodes = doc.getElementsByTagName('translation');
1309
+ for (let i = 0; i < translationNodes.length; i += 1) {
1310
+ const texts = translationNodes[i].getElementsByTagName('text');
1311
+ for (let j = 0; j < texts.length; j += 1) {
1312
+ const textNode = texts[j];
1313
+ const key = textNode.getAttribute('for');
1314
+ if (key && textNode.textContent) {
1315
+ translations[key] = textNode.textContent;
1316
+ }
1317
+ }
1318
+ }
1319
+ const transliterationNodes = doc.getElementsByTagName('transliteration');
1320
+ for (let i = 0; i < transliterationNodes.length; i += 1) {
1321
+ const texts = transliterationNodes[i].getElementsByTagName('text');
1322
+ for (let j = 0; j < texts.length; j += 1) {
1323
+ const textNode = texts[j];
1324
+ const key = textNode.getAttribute('for');
1325
+ if (key && textNode.textContent) {
1326
+ transliterations[key] = textNode.textContent
1327
+ .trim()
1328
+ .replace(/\s+/g, ' ');
1329
+ }
1330
+ }
1331
+ }
1332
+ const timeToMs = (timeStr) => {
1333
+ if (!timeStr)
1334
+ return 0;
1335
+ const parts = timeStr.split(':');
1336
+ let seconds = 0;
1337
+ if (parts.length === 2) {
1338
+ seconds = parseInt(parts[0], 10) * 60 + parseFloat(parts[1]);
1339
+ }
1340
+ else if (parts.length === 3) {
1341
+ seconds =
1342
+ parseInt(parts[0], 10) * 3600 +
1343
+ parseInt(parts[1], 10) * 60 +
1344
+ parseFloat(parts[2]);
1345
+ }
1346
+ else {
1347
+ seconds = parseFloat(parts[0]);
1348
+ }
1349
+ return Math.round(seconds * 1000);
1350
+ };
1351
+ const lines = [];
1352
+ const pNodes = doc.getElementsByTagName('p');
1353
+ const lineSingers = [];
1354
+ for (let i = 0; i < pNodes.length; i += 1) {
1355
+ lineSingers.push(pNodes[i].getAttribute('ttm:agent') || undefined);
1356
+ }
1357
+ const alignments = AmLyrics.calculateLineAlignments(lineSingers, agentMap);
1358
+ for (let i = 0; i < pNodes.length; i += 1) {
1359
+ const p = pNodes[i];
1360
+ const key = p.getAttribute('itunes:key');
1361
+ const beginMs = timeToMs(p.getAttribute('begin'));
1362
+ const endMs = timeToMs(p.getAttribute('end'));
1363
+ let songPart;
1364
+ if (p.parentNode && p.parentNode.tagName === 'div') {
1365
+ songPart =
1366
+ p.parentNode.getAttribute('itunes:songPart') ||
1367
+ undefined;
1368
+ }
1369
+ const mainSyllables = [];
1370
+ const bgSyllables = [];
1371
+ const spans = p.getElementsByTagName('span');
1372
+ if (spans.length > 0) {
1373
+ for (let j = 0; j < spans.length; j += 1) {
1374
+ const span = spans[j];
1375
+ if (span.getAttribute('ttm:role') === 'x-bg') {
1376
+ const bgInnerSpans = span.getElementsByTagName('span');
1377
+ for (let k = 0; k < bgInnerSpans.length; k += 1) {
1378
+ const bgSpan = bgInnerSpans[k];
1379
+ let bgText = bgSpan.textContent || '';
1380
+ const nextNode = bgSpan.nextSibling;
1381
+ if (nextNode &&
1382
+ nextNode.nodeType === 3 &&
1383
+ /^\s/.test(nextNode.textContent || '') &&
1384
+ !bgText.endsWith(' ')) {
1385
+ bgText += ' ';
1386
+ }
1387
+ bgSyllables.push({
1388
+ text: bgText,
1389
+ timestamp: timeToMs(bgSpan.getAttribute('begin')),
1390
+ endtime: timeToMs(bgSpan.getAttribute('end')),
1391
+ part: false,
1392
+ });
1393
+ }
1394
+ // eslint-disable-next-line no-continue
1395
+ continue;
1396
+ }
1397
+ if (span.parentNode &&
1398
+ span.parentNode.getAttribute?.('ttm:role') === 'x-bg') {
1399
+ // eslint-disable-next-line no-continue
1400
+ continue;
1401
+ }
1402
+ let text = span.textContent || '';
1403
+ const nextNode = span.nextSibling;
1404
+ if (nextNode &&
1405
+ nextNode.nodeType === 3 &&
1406
+ /^\s/.test(nextNode.textContent || '') &&
1407
+ !text.endsWith(' ')) {
1408
+ text += ' ';
1409
+ }
1410
+ mainSyllables.push({
1411
+ text,
1412
+ timestamp: timeToMs(span.getAttribute('begin')),
1413
+ endtime: timeToMs(span.getAttribute('end')),
1414
+ part: false,
1415
+ });
1416
+ }
1417
+ }
1418
+ else {
1419
+ mainSyllables.push({
1420
+ text: p.textContent?.trim() || '',
1421
+ timestamp: beginMs,
1422
+ endtime: endMs,
1423
+ part: false,
1424
+ lineSynced: true,
1425
+ });
1426
+ }
1427
+ const alignment = alignments[i];
1428
+ lines.push({
1429
+ text: mainSyllables,
1430
+ background: bgSyllables.length > 0,
1431
+ backgroundText: bgSyllables,
1432
+ timestamp: beginMs,
1433
+ endtime: endMs,
1434
+ isWordSynced: spans.length > 0,
1435
+ alignment,
1436
+ songPart,
1437
+ translation: key ? translations[key] : undefined,
1438
+ romanizedText: key ? transliterations[key] : undefined,
1439
+ oppositeTurn: alignment === 'end',
1440
+ });
1441
+ }
1442
+ return lines;
1443
+ }
1444
+ catch (e) {
1445
+ // eslint-disable-next-line no-console
1446
+ console.error('Failed to parse TTML', e);
1447
+ return null;
1448
+ }
1449
+ }
1088
1450
  static convertKPoeLyrics(payload) {
1089
1451
  if (!payload) {
1090
1452
  return null;
@@ -1106,45 +1468,21 @@ class AmLyrics extends i {
1106
1468
  const lines = [];
1107
1469
  // If type is 'Line', we revert to line-by-line highlighting by skipping syllabus parsing
1108
1470
  const isLineType = payload.type === 'Line' || payload.type === 'line';
1109
- // Convert metadata.agents to alignment map
1110
- const agents = payload.metadata?.agents ?? {};
1111
- const agentEntries = Object.entries(agents);
1112
- const singerAlignmentMap = {};
1113
- if (agentEntries.length > 0) {
1114
- agentEntries.sort((a, b) => a[0].localeCompare(b[0]));
1115
- const personAgents = agentEntries.filter(
1116
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1117
- ([_, agentData]) => agentData.type === 'person');
1118
- const personIndexMap = new Map();
1119
- personAgents.forEach(([agentKey], personIndex) => {
1120
- personIndexMap.set(agentKey, personIndex);
1121
- });
1122
- agentEntries.forEach(([agentKey, agentData]) => {
1123
- const mappedKey = agentData.alias || agentKey;
1124
- if (agentData.type === 'group') {
1125
- singerAlignmentMap[mappedKey] = 'start';
1126
- }
1127
- else if (agentData.type === 'other') {
1128
- singerAlignmentMap[mappedKey] = 'end';
1129
- }
1130
- else if (agentData.type === 'person') {
1131
- const personIndex = personIndexMap.get(agentKey);
1132
- if (personIndex !== undefined) {
1133
- singerAlignmentMap[mappedKey] =
1134
- personIndex % 2 === 0 ? 'start' : 'end';
1135
- }
1136
- }
1471
+ // Convert metadata.agents to type map
1472
+ const agentTypes = {};
1473
+ if (payload.metadata?.agents) {
1474
+ Object.entries(payload.metadata.agents).forEach(([key, agent]) => {
1475
+ const mappedKey = agent.alias || key;
1476
+ agentTypes[mappedKey] = agent.type;
1137
1477
  });
1138
1478
  }
1139
- for (const entry of sanitizedEntries) {
1479
+ const lineSingers = sanitizedEntries.map((entry) => entry.element?.singer);
1480
+ const alignments = AmLyrics.calculateLineAlignments(lineSingers, agentTypes);
1481
+ for (let i = 0; i < sanitizedEntries.length; i += 1) {
1482
+ const entry = sanitizedEntries[i];
1140
1483
  const start = AmLyrics.toMilliseconds(entry.time);
1141
1484
  const duration = AmLyrics.toMilliseconds(entry.duration);
1142
- // Determine alignment
1143
- let alignment;
1144
- const singerId = entry.element?.singer;
1145
- if (singerId && singerAlignmentMap[singerId]) {
1146
- alignment = singerAlignmentMap[singerId];
1147
- }
1485
+ const alignment = alignments[i];
1148
1486
  const lineText = typeof entry.text === 'string' ? entry.text : '';
1149
1487
  const lineStart = AmLyrics.toMilliseconds(entry.time);
1150
1488
  const lineDuration = AmLyrics.toMilliseconds(entry.duration);
@@ -1195,10 +1533,8 @@ class AmLyrics extends i {
1195
1533
  // If syllabus data matches, map it to main syllables
1196
1534
  if (Array.isArray(transliteration.syllabus) &&
1197
1535
  transliteration.syllabus.length === mainSyllables.length) {
1198
- transliteration.syllabus.forEach((s, i) => {
1199
- if (mainSyllables[i]) {
1200
- mainSyllables[i].romanizedText = s.text;
1201
- }
1536
+ transliteration.syllabus.forEach((s, idx) => {
1537
+ mainSyllables[idx].romanizedText = s.text;
1202
1538
  });
1203
1539
  }
1204
1540
  }
@@ -1208,10 +1544,11 @@ class AmLyrics extends i {
1208
1544
  text: mainSyllables,
1209
1545
  background: backgroundSyllables.length > 0,
1210
1546
  backgroundText: backgroundSyllables,
1211
- oppositeTurn: Array.isArray(entry.element)
1212
- ? entry.element.includes('opposite') ||
1213
- entry.element.includes('right')
1214
- : false,
1547
+ oppositeTurn: alignment === 'end' ||
1548
+ (Array.isArray(entry.element)
1549
+ ? entry.element.includes('opposite') ||
1550
+ entry.element.includes('right')
1551
+ : false),
1215
1552
  timestamp: lineStart,
1216
1553
  endtime: start + duration,
1217
1554
  isWordSynced: isLineType ? false : hasWordSync,