@uimaxbai/am-lyrics 1.0.7 → 1.0.9
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/src/AmLyrics.d.ts +15 -0
- package/dist/src/AmLyrics.d.ts.map +1 -1
- package/dist/src/am-lyrics.js +380 -28
- package/dist/src/am-lyrics.js.map +1 -1
- package/dist/src/react.js +380 -28
- package/dist/src/react.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/AmLyrics.ts +458 -30
package/dist/src/AmLyrics.d.ts
CHANGED
|
@@ -62,6 +62,21 @@ export declare class AmLyrics extends LitElement {
|
|
|
62
62
|
private static parseQueryMetadata;
|
|
63
63
|
private static searchLyricsPlusCatalog;
|
|
64
64
|
private static fetchLyricsFromYouLyPlus;
|
|
65
|
+
/**
|
|
66
|
+
* Parse LRC subtitle format into LyricsLine[].
|
|
67
|
+
* Handles "[mm:ss.xx] text" lines.
|
|
68
|
+
*/
|
|
69
|
+
private static parseLrcSubtitles;
|
|
70
|
+
/**
|
|
71
|
+
* Fetch lyrics from Tidal API.
|
|
72
|
+
* Picks 2 random servers, tries search + lyrics on each.
|
|
73
|
+
*/
|
|
74
|
+
private static fetchLyricsFromTidal;
|
|
75
|
+
/**
|
|
76
|
+
* Fetch lyrics from LRCLIB.
|
|
77
|
+
* Uses search endpoint, prefers synced lyrics.
|
|
78
|
+
*/
|
|
79
|
+
private static fetchLyricsFromLrclib;
|
|
65
80
|
private static convertKPoeLyrics;
|
|
66
81
|
private static toMilliseconds;
|
|
67
82
|
firstUpdated(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AmLyrics.d.ts","sourceRoot":"","sources":["../../src/AmLyrics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,UAAU,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"AmLyrics.d.ts","sourceRoot":"","sources":["../../src/AmLyrics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,UAAU,EAAE,MAAM,KAAK,CAAC;AA2F5C,qBAAa,QAAS,SAAQ,UAAU;IACtC,MAAM,CAAC,MAAM,0BA+mCX;IAGF,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,OAAO,CAAC,EAAE,MAAM,CAAC;IAGjB,IAAI,CAAC,EAAE,MAAM,CAAC;IAGd,SAAS,CAAC,EAAE,MAAM,CAAC;IAGnB,OAAO,CAAC,cAAc,CAAmC;IAGzD,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,SAAS,CAAC,EAAE,MAAM,CAAC;IAGnB,cAAc,CAAC,EAAE,MAAM,CAAC;IAGxB,cAAc,SAAa;IAG3B,oBAAoB,SAA+B;IAGnD,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,UAAU,UAAQ;IAGlB,WAAW,UAAQ;IAGnB,OAAO,CAAC,gBAAgB,CAAS;IAGjC,OAAO,CAAC,eAAe,CAAS;YAElB,kBAAkB;YAKlB,iBAAiB;YAsBjB,iBAAiB;YAKjB,gBAAgB;IAyC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,OAAO,CAAC,YAAY,CAAK;IAEzB,IACI,WAAW,CAAC,KAAK,EAAE,MAAM,EAM5B;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAGD,OAAO,CAAC,SAAS,CAAS;IAG1B,OAAO,CAAC,MAAM,CAAC,CAAe;IAE9B,OAAO,CAAC,iBAAiB,CAAgB;IAEzC,OAAO,CAAC,qBAAqB,CAAkC;IAE/D,OAAO,CAAC,2BAA2B,CAAkC;IAErE,OAAO,CAAC,gBAAgB,CAAkC;IAE1D,OAAO,CAAC,sBAAsB,CAAkC;IAGhE,OAAO,CAAC,YAAY,CAAuB;IAE3C,OAAO,CAAC,gBAAgB,CAAC,CAAS;IAElC,OAAO,CAAC,kBAAkB,CAGZ;IAEd,OAAO,CAAC,wBAAwB,CAGlB;IAGd,OAAO,CAAC,eAAe,CAAC,CAAc;IAEtC,OAAO,CAAC,qBAAqB,CAAuB;IAEpD,OAAO,CAAC,mBAAmB,CAAC,CAAS;IAGrC,OAAO,CAAC,eAAe,CAAS;IAEhC,OAAO,CAAC,oBAAoB,CAAS;IAErC,OAAO,CAAC,cAAc,CAAS;IAE/B,OAAO,CAAC,gBAAgB,CAAC,CAAgC;IAGzD,OAAO,CAAC,iBAAiB,CAAqB;IAG9C,OAAO,CAAC,aAAa,CAA0B;IAE/C,OAAO,CAAC,wBAAwB,CAA4B;IAE5D,OAAO,CAAC,qBAAqB,CAA4B;IAGzD,OAAO,CAAC,oBAAoB,CAGZ;IAEhB,OAAO,CAAC,mBAAmB,CAAK;IAEhC,OAAO,CAAC,cAAc,CAAqB;IAE3C,OAAO,CAAC,mBAAmB,CAAC,CAAgC;IAE5D,OAAO,CAAC,sBAAsB,CAAC,CAAgC;IAG/D,OAAO,CAAC,eAAe,CAAK;IAE5B,OAAO,CAAC,cAAc,CAA0B;IAEhD,iBAAiB;IAKjB,oBAAoB;YAUN,WAAW;YA4DX,cAAc;YAoBd,iBAAiB;YASjB,mBAAmB;IAiGjC,OAAO,CAAC,MAAM,CAAC,kBAAkB;mBAkCZ,uBAAuB;mBA6CvB,wBAAwB;IAyE7C;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,iBAAiB;IA4DhC;;;OAGG;mBACkB,oBAAoB;IA0FzC;;;OAGG;mBACkB,qBAAqB;IAyE1C,OAAO,CAAC,MAAM,CAAC,iBAAiB;IA2JhC,OAAO,CAAC,MAAM,CAAC,cAAc;IAa7B,YAAY;IAkBZ;;;;;OAKG;IACH,OAAO,CAAC,cAAc;IAoLtB,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC;IAuEjE;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAqC/B,OAAO,CAAC,gBAAgB,CAAgC;IAExD,OAAO,CAAC,aAAa,CAA8C;IAEnE,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,qBAAqB;IAsE7B,OAAO,CAAC,MAAM,CAAC,WAAW;IAI1B,OAAO,CAAC,gBAAgB;IA2BxB,OAAO,CAAC,qBAAqB;IAW7B,OAAO,CAAC,qBAAqB;IAiC7B;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAgC/B,OAAO,CAAC,sBAAsB;IAgE9B,OAAO,CAAC,wBAAwB;IA0ChC,OAAO,CAAC,eAAe;IA4CvB,OAAO,CAAC,eAAe;IA4EvB,OAAO,CAAC,MAAM,CAAC,0BAA0B;IAkBzC,OAAO,CAAC,kBAAkB;IA2C1B,OAAO,CAAC,oBAAoB;IAyB5B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAa3B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAoJ1B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IA+C7B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAiD/B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,uBAAuB;IA+JtC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa;IAwC5B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,cAAc;IAS7B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAuErC,OAAO,CAAC,eAAe;IA4HvB,OAAO,CAAC,WAAW;IAyBnB,OAAO,CAAC,YAAY;IA2DpB,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAUjC,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAYlC,OAAO,CAAC,cAAc;IAuCtB,MAAM;CA4qBP"}
|
package/dist/src/am-lyrics.js
CHANGED
|
@@ -310,16 +310,29 @@ class GoogleService {
|
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
const VERSION = '1.0.
|
|
313
|
+
const VERSION = '1.0.9';
|
|
314
314
|
const INSTRUMENTAL_THRESHOLD_MS = 7000; // Show dots for gaps >= 7s
|
|
315
315
|
const KPOE_SERVERS = [
|
|
316
316
|
'https://lyricsplus.binimum.org',
|
|
317
|
+
'https://lyricsplus.atomix.one',
|
|
318
|
+
'https://lyricsplus-seven.vercel.app',
|
|
317
319
|
'https://lyricsplus.prjktla.workers.dev',
|
|
318
320
|
'https://lyrics-plus-backend.vercel.app',
|
|
319
|
-
'https://lyricsplus.onrender.com',
|
|
320
|
-
'https://lyricsplus.prjktla.online',
|
|
321
321
|
];
|
|
322
322
|
const DEFAULT_KPOE_SOURCE_ORDER = 'apple,lyricsplus,musixmatch,spotify,musixmatch-word';
|
|
323
|
+
const TIDAL_SERVERS = [
|
|
324
|
+
'https://arran.monochrome.tf',
|
|
325
|
+
'https://api.monochrome.tf/',
|
|
326
|
+
'https://triton.squid.wtf',
|
|
327
|
+
'https://wolf.qqdl.site',
|
|
328
|
+
'https://maus.qqdl.site',
|
|
329
|
+
'https://vogel.qqdl.site',
|
|
330
|
+
'https://katze.qqdl.site',
|
|
331
|
+
'https://hund.qqdl.site',
|
|
332
|
+
'https://tidal.kinoplus.online',
|
|
333
|
+
'https://hifi-one.spotisaver.net',
|
|
334
|
+
'https://hifi-two.spotisaver.net',
|
|
335
|
+
];
|
|
323
336
|
class AmLyrics extends i {
|
|
324
337
|
constructor() {
|
|
325
338
|
super(...arguments);
|
|
@@ -466,6 +479,26 @@ class AmLyrics extends i {
|
|
|
466
479
|
return;
|
|
467
480
|
}
|
|
468
481
|
}
|
|
482
|
+
// Fallback: Tidal
|
|
483
|
+
if (resolvedMetadata?.metadata) {
|
|
484
|
+
const tidalResult = await AmLyrics.fetchLyricsFromTidal(resolvedMetadata.metadata, resolvedMetadata.catalogIsrc);
|
|
485
|
+
if (tidalResult && tidalResult.lines.length > 0) {
|
|
486
|
+
this.lyrics = tidalResult.lines;
|
|
487
|
+
this.lyricsSource = 'Tidal';
|
|
488
|
+
await this.onLyricsLoaded();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// Fallback: LRCLIB
|
|
493
|
+
if (resolvedMetadata?.metadata) {
|
|
494
|
+
const lrclibResult = await AmLyrics.fetchLyricsFromLrclib(resolvedMetadata.metadata);
|
|
495
|
+
if (lrclibResult && lrclibResult.lines.length > 0) {
|
|
496
|
+
this.lyrics = lrclibResult.lines;
|
|
497
|
+
this.lyricsSource = 'LRCLIB';
|
|
498
|
+
await this.onLyricsLoaded();
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
469
502
|
this.lyrics = undefined;
|
|
470
503
|
this.lyricsSource = null;
|
|
471
504
|
}
|
|
@@ -654,7 +687,9 @@ class AmLyrics extends i {
|
|
|
654
687
|
}
|
|
655
688
|
params.append('source', DEFAULT_KPOE_SOURCE_ORDER);
|
|
656
689
|
let fallbackResult = null;
|
|
657
|
-
|
|
690
|
+
// Shuffle servers so we pick a random one first, with all others as fallback
|
|
691
|
+
const shuffledServers = [...KPOE_SERVERS].sort(() => Math.random() - 0.5);
|
|
692
|
+
for (const base of shuffledServers) {
|
|
658
693
|
const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base;
|
|
659
694
|
const url = `${normalizedBase}/v2/lyrics/get?${params.toString()}`;
|
|
660
695
|
let payload = null;
|
|
@@ -689,6 +724,196 @@ class AmLyrics extends i {
|
|
|
689
724
|
}
|
|
690
725
|
return fallbackResult;
|
|
691
726
|
}
|
|
727
|
+
/**
|
|
728
|
+
* Parse LRC subtitle format into LyricsLine[].
|
|
729
|
+
* Handles "[mm:ss.xx] text" lines.
|
|
730
|
+
*/
|
|
731
|
+
static parseLrcSubtitles(lrc) {
|
|
732
|
+
if (!lrc || typeof lrc !== 'string')
|
|
733
|
+
return [];
|
|
734
|
+
const lines = [];
|
|
735
|
+
const rawLines = lrc.split('\n');
|
|
736
|
+
const parsed = [];
|
|
737
|
+
for (const raw of rawLines) {
|
|
738
|
+
const match = raw.match(/^\[(\d{1,3}):(\d{2})\.(\d{2,3})\]\s?(.*)$/);
|
|
739
|
+
if (!match) {
|
|
740
|
+
// Skip non-timestamped lines (headers like [ti:], [ar:], etc.)
|
|
741
|
+
// eslint-disable-next-line no-continue
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
const minutes = parseInt(match[1], 10);
|
|
745
|
+
const seconds = parseInt(match[2], 10);
|
|
746
|
+
let centiseconds = parseInt(match[3], 10);
|
|
747
|
+
// Handle both mm:ss.xx (centiseconds) and mm:ss.xxx (milliseconds)
|
|
748
|
+
if (match[3].length === 3) {
|
|
749
|
+
centiseconds = Math.round(centiseconds / 10);
|
|
750
|
+
}
|
|
751
|
+
const timestamp = (minutes * 60 + seconds) * 1000 + centiseconds * 10;
|
|
752
|
+
const text = match[4] || '';
|
|
753
|
+
parsed.push({ timestamp, text });
|
|
754
|
+
}
|
|
755
|
+
for (let i = 0; i < parsed.length; i += 1) {
|
|
756
|
+
const { timestamp, text } = parsed[i];
|
|
757
|
+
// Endtime is the start of the next line, or timestamp + 5s for the last line
|
|
758
|
+
const endtime = i + 1 < parsed.length ? parsed[i + 1].timestamp : timestamp + 5000;
|
|
759
|
+
// Skip empty lines (instrumental gaps)
|
|
760
|
+
if (!text.trim()) {
|
|
761
|
+
// eslint-disable-next-line no-continue
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const syllable = {
|
|
765
|
+
text,
|
|
766
|
+
part: false,
|
|
767
|
+
timestamp,
|
|
768
|
+
endtime,
|
|
769
|
+
lineSynced: true,
|
|
770
|
+
};
|
|
771
|
+
lines.push({
|
|
772
|
+
text: [syllable],
|
|
773
|
+
background: false,
|
|
774
|
+
backgroundText: [],
|
|
775
|
+
oppositeTurn: false,
|
|
776
|
+
timestamp,
|
|
777
|
+
endtime,
|
|
778
|
+
isWordSynced: false,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
return lines;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Fetch lyrics from Tidal API.
|
|
785
|
+
* Picks 2 random servers, tries search + lyrics on each.
|
|
786
|
+
*/
|
|
787
|
+
static async fetchLyricsFromTidal(metadata, isrc) {
|
|
788
|
+
const title = metadata.title?.trim();
|
|
789
|
+
const artist = metadata.artist?.trim();
|
|
790
|
+
if (!title || !artist)
|
|
791
|
+
return null;
|
|
792
|
+
// Pick 2 random unique servers
|
|
793
|
+
const shuffled = [...TIDAL_SERVERS].sort(() => Math.random() - 0.5);
|
|
794
|
+
const serversToTry = shuffled.slice(0, 2);
|
|
795
|
+
for (const base of serversToTry) {
|
|
796
|
+
try {
|
|
797
|
+
const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base;
|
|
798
|
+
// Step 1: Search for the track
|
|
799
|
+
const searchQuery = `${title} ${artist}`;
|
|
800
|
+
const searchParams = new URLSearchParams({ s: searchQuery });
|
|
801
|
+
// eslint-disable-next-line no-await-in-loop
|
|
802
|
+
const searchResponse = await fetch(`${normalizedBase}/search/?${searchParams.toString()}`);
|
|
803
|
+
if (!searchResponse.ok) {
|
|
804
|
+
// eslint-disable-next-line no-continue
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
// eslint-disable-next-line no-await-in-loop
|
|
808
|
+
const searchData = await searchResponse.json();
|
|
809
|
+
const items = searchData?.data?.items;
|
|
810
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
811
|
+
// eslint-disable-next-line no-continue
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
// Find best match: prefer ISRC match, then first result
|
|
815
|
+
let bestTrack = items[0];
|
|
816
|
+
if (isrc) {
|
|
817
|
+
const isrcMatch = items.find((item) => item.isrc && item.isrc.toLowerCase() === isrc.toLowerCase());
|
|
818
|
+
if (isrcMatch) {
|
|
819
|
+
bestTrack = isrcMatch;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
const trackId = bestTrack?.id;
|
|
823
|
+
if (!trackId) {
|
|
824
|
+
// eslint-disable-next-line no-continue
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
// Step 2: Fetch lyrics
|
|
828
|
+
// eslint-disable-next-line no-await-in-loop
|
|
829
|
+
const lyricsResponse = await fetch(`${normalizedBase}/lyrics/?id=${trackId}`);
|
|
830
|
+
if (!lyricsResponse.ok) {
|
|
831
|
+
// eslint-disable-next-line no-continue
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
// eslint-disable-next-line no-await-in-loop
|
|
835
|
+
const lyricsData = await lyricsResponse.json();
|
|
836
|
+
const subtitles = lyricsData?.lyrics?.subtitles;
|
|
837
|
+
if (subtitles && typeof subtitles === 'string') {
|
|
838
|
+
const lines = AmLyrics.parseLrcSubtitles(subtitles);
|
|
839
|
+
if (lines.length > 0) {
|
|
840
|
+
const provider = lyricsData?.lyrics?.lyricsProvider || 'Tidal';
|
|
841
|
+
return {
|
|
842
|
+
lines,
|
|
843
|
+
source: `Tidal (${provider})`,
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
catch {
|
|
849
|
+
// Try next server
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Fetch lyrics from LRCLIB.
|
|
856
|
+
* Uses search endpoint, prefers synced lyrics.
|
|
857
|
+
*/
|
|
858
|
+
static async fetchLyricsFromLrclib(metadata) {
|
|
859
|
+
const title = metadata.title?.trim();
|
|
860
|
+
const artist = metadata.artist?.trim();
|
|
861
|
+
if (!title || !artist)
|
|
862
|
+
return null;
|
|
863
|
+
try {
|
|
864
|
+
const searchQuery = `${title} ${artist}`;
|
|
865
|
+
const params = new URLSearchParams({ q: searchQuery });
|
|
866
|
+
const response = await fetch(`https://lrclib.net/api/search?${params.toString()}`, {
|
|
867
|
+
headers: {
|
|
868
|
+
'User-Agent': `apple-music-web-components/${VERSION}`,
|
|
869
|
+
},
|
|
870
|
+
});
|
|
871
|
+
if (!response.ok)
|
|
872
|
+
return null;
|
|
873
|
+
const results = await response.json();
|
|
874
|
+
if (!Array.isArray(results) || results.length === 0)
|
|
875
|
+
return null;
|
|
876
|
+
// Prefer results with synced lyrics
|
|
877
|
+
const withSynced = results.find((r) => r.syncedLyrics && typeof r.syncedLyrics === 'string');
|
|
878
|
+
const bestMatch = withSynced || results[0];
|
|
879
|
+
// Try synced lyrics first
|
|
880
|
+
if (bestMatch.syncedLyrics) {
|
|
881
|
+
const lines = AmLyrics.parseLrcSubtitles(bestMatch.syncedLyrics);
|
|
882
|
+
if (lines.length > 0) {
|
|
883
|
+
return { lines, source: 'LRCLIB' };
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
// Fall back to plain lyrics (unsynced)
|
|
887
|
+
if (bestMatch.plainLyrics && typeof bestMatch.plainLyrics === 'string') {
|
|
888
|
+
const plainLines = bestMatch.plainLyrics
|
|
889
|
+
.split('\n')
|
|
890
|
+
.filter((l) => l.trim());
|
|
891
|
+
if (plainLines.length > 0) {
|
|
892
|
+
const lines = plainLines.map((text) => ({
|
|
893
|
+
text: [
|
|
894
|
+
{
|
|
895
|
+
text,
|
|
896
|
+
part: false,
|
|
897
|
+
timestamp: 0,
|
|
898
|
+
endtime: 0,
|
|
899
|
+
},
|
|
900
|
+
],
|
|
901
|
+
background: false,
|
|
902
|
+
backgroundText: [],
|
|
903
|
+
oppositeTurn: false,
|
|
904
|
+
timestamp: 0,
|
|
905
|
+
endtime: 0,
|
|
906
|
+
isWordSynced: false,
|
|
907
|
+
}));
|
|
908
|
+
return { lines, source: 'LRCLIB (unsynced)' };
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
// LRCLIB fetch failed
|
|
914
|
+
}
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
692
917
|
static convertKPoeLyrics(payload) {
|
|
693
918
|
if (!payload) {
|
|
694
919
|
return null;
|
|
@@ -2265,28 +2490,160 @@ class AmLyrics extends i {
|
|
|
2265
2490
|
wordGroups.push([syllable]);
|
|
2266
2491
|
}
|
|
2267
2492
|
}
|
|
2493
|
+
// Pre-compute isGrowable per "visual word": adjacent groups whose text
|
|
2494
|
+
// doesn't end with whitespace form one visual word (e.g. "a"+"live" = "alive").
|
|
2495
|
+
// We evaluate growable on the combined text/duration, then propagate
|
|
2496
|
+
// the result to each individual group so it renders through the
|
|
2497
|
+
// single-syllable path (which supports char-level glow).
|
|
2498
|
+
const groupGrowable = new Array(wordGroups.length).fill(false);
|
|
2499
|
+
// Visual word info for growable char-level glow:
|
|
2500
|
+
// Each group stores the combined visual word's text, duration, and
|
|
2501
|
+
// the char offset of this group within the visual word.
|
|
2502
|
+
const vwFullText = new Array(wordGroups.length).fill('');
|
|
2503
|
+
const vwFullDuration = new Array(wordGroups.length).fill(0);
|
|
2504
|
+
const vwCharOffset = new Array(wordGroups.length).fill(0);
|
|
2505
|
+
const vwStartMs = new Array(wordGroups.length).fill(0);
|
|
2506
|
+
const vwEndMs = new Array(wordGroups.length).fill(0);
|
|
2507
|
+
{
|
|
2508
|
+
let vwStart = 0;
|
|
2509
|
+
while (vwStart < wordGroups.length) {
|
|
2510
|
+
let vwEnd = vwStart;
|
|
2511
|
+
while (vwEnd < wordGroups.length - 1) {
|
|
2512
|
+
const grp = wordGroups[vwEnd];
|
|
2513
|
+
const lastText = grp[grp.length - 1].text;
|
|
2514
|
+
if (/\s$/.test(lastText))
|
|
2515
|
+
break;
|
|
2516
|
+
vwEnd += 1;
|
|
2517
|
+
}
|
|
2518
|
+
// Compute combined properties for this visual word
|
|
2519
|
+
const combinedText = wordGroups
|
|
2520
|
+
.slice(vwStart, vwEnd + 1)
|
|
2521
|
+
.flatMap(g => g.map(s => s.text))
|
|
2522
|
+
.join('')
|
|
2523
|
+
.trim();
|
|
2524
|
+
const combinedStart = wordGroups[vwStart][0].timestamp;
|
|
2525
|
+
const lastGrp = wordGroups[vwEnd];
|
|
2526
|
+
const combinedEnd = lastGrp[lastGrp.length - 1].endtime;
|
|
2527
|
+
const combinedDuration = combinedEnd - combinedStart;
|
|
2528
|
+
const isCJK = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(combinedText);
|
|
2529
|
+
const isRTL = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u0590-\u05FF]/.test(combinedText);
|
|
2530
|
+
const hasHyphen = combinedText.includes('-');
|
|
2531
|
+
const isGrowableVW = !isCJK &&
|
|
2532
|
+
!isRTL &&
|
|
2533
|
+
!hasHyphen &&
|
|
2534
|
+
combinedText.length <= 7 &&
|
|
2535
|
+
combinedText.length > 0 &&
|
|
2536
|
+
combinedDuration >= 900 &&
|
|
2537
|
+
combinedDuration >= combinedText.length * 300 &&
|
|
2538
|
+
(combinedText.length >= 4 ||
|
|
2539
|
+
combinedDuration / combinedText.length >= 600);
|
|
2540
|
+
let charOff = 0;
|
|
2541
|
+
for (let gi = vwStart; gi <= vwEnd; gi += 1) {
|
|
2542
|
+
groupGrowable[gi] = isGrowableVW;
|
|
2543
|
+
vwFullText[gi] = combinedText;
|
|
2544
|
+
vwFullDuration[gi] = combinedDuration;
|
|
2545
|
+
vwCharOffset[gi] = charOff;
|
|
2546
|
+
vwStartMs[gi] = combinedStart;
|
|
2547
|
+
vwEndMs[gi] = combinedEnd;
|
|
2548
|
+
const grpText = wordGroups[gi].map(s => s.text).join('');
|
|
2549
|
+
charOff += grpText.replace(/\s/g, '').length;
|
|
2550
|
+
}
|
|
2551
|
+
vwStart = vwEnd + 1;
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2268
2554
|
// Create main vocals using YouLyPlus syllable structure
|
|
2269
2555
|
const mainVocalElement = b `<p class="main-vocal-container">
|
|
2270
|
-
${wordGroups.map(group => {
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2556
|
+
${wordGroups.map((group, groupIdx) => {
|
|
2557
|
+
const isGrowable = groupGrowable[groupIdx];
|
|
2558
|
+
// For growable visual words spanning multiple groups:
|
|
2559
|
+
// skip continuation groups (rendered by the first group)
|
|
2560
|
+
if (isGrowable && vwCharOffset[groupIdx] > 0) {
|
|
2561
|
+
return '';
|
|
2562
|
+
}
|
|
2277
2563
|
// Check if ANY syllable in group is line-synced
|
|
2278
2564
|
const groupLineSynced = group.some(s => s.lineSynced);
|
|
2279
|
-
//
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2565
|
+
// For growable multi-group visual words, combine all text
|
|
2566
|
+
// into one syllable so the wipe + glow animates as one unit
|
|
2567
|
+
if (isGrowable && vwFullText[groupIdx].length > 0) {
|
|
2568
|
+
const wordText = vwFullText[groupIdx];
|
|
2569
|
+
const wordDuration = vwFullDuration[groupIdx];
|
|
2570
|
+
const startTimeMs = vwStartMs[groupIdx];
|
|
2571
|
+
const endTimeMs = vwEndMs[groupIdx];
|
|
2572
|
+
const numChars = wordText.length;
|
|
2573
|
+
// Collect all text from groups in this visual word
|
|
2574
|
+
// by scanning forward while vwCharOffset is consecutive
|
|
2575
|
+
let combinedRawText = '';
|
|
2576
|
+
for (let gi = groupIdx; gi < wordGroups.length; gi += 1) {
|
|
2577
|
+
if (gi > groupIdx && vwCharOffset[gi] === 0)
|
|
2578
|
+
break;
|
|
2579
|
+
if (gi > groupIdx && !groupGrowable[gi])
|
|
2580
|
+
break;
|
|
2581
|
+
combinedRawText += wordGroups[gi].map(s => s.text).join('');
|
|
2582
|
+
}
|
|
2583
|
+
const syllableContent = b `${combinedRawText
|
|
2584
|
+
.split('')
|
|
2585
|
+
.map((char, charIndex) => {
|
|
2586
|
+
if (char === ' ') {
|
|
2587
|
+
return ' ';
|
|
2588
|
+
}
|
|
2589
|
+
const charStartPercent = charIndex / numChars;
|
|
2590
|
+
const minDuration = 1000;
|
|
2591
|
+
const maxDuration = 5000;
|
|
2592
|
+
const easingPower = 3;
|
|
2593
|
+
const progress = Math.min(1, Math.max(0, (wordDuration - minDuration) /
|
|
2594
|
+
(maxDuration - minDuration)));
|
|
2595
|
+
const easedProgress = progress ** easingPower;
|
|
2596
|
+
const isLongWord = numChars > 5;
|
|
2597
|
+
const isShortDuration = wordDuration < 1500;
|
|
2598
|
+
let maxDecayRate = 0;
|
|
2599
|
+
if (isLongWord || isShortDuration) {
|
|
2600
|
+
let decayStrength = 0;
|
|
2601
|
+
if (isLongWord)
|
|
2602
|
+
decayStrength += Math.min((numChars - 5) / 3, 1.0) * 0.4;
|
|
2603
|
+
if (isShortDuration)
|
|
2604
|
+
decayStrength +=
|
|
2605
|
+
Math.max(0, 1.0 - (wordDuration - 1000) / 500) * 0.4;
|
|
2606
|
+
maxDecayRate = Math.min(decayStrength, 0.85);
|
|
2607
|
+
}
|
|
2608
|
+
const positionInWord = numChars > 1 ? charIndex / (numChars - 1) : 0;
|
|
2609
|
+
const decayFactor = 1.0 - positionInWord * maxDecayRate;
|
|
2610
|
+
const charProgress = easedProgress * decayFactor;
|
|
2611
|
+
const baseGrowth = numChars <= 3 ? 0.07 : 0.05;
|
|
2612
|
+
const charMaxScale = 1.0 + baseGrowth + charProgress * 0.1;
|
|
2613
|
+
const charShadowIntensity = 0.4 + charProgress * 0.4;
|
|
2614
|
+
const normalizedGrowth = (charMaxScale - 1.0) / 0.13;
|
|
2615
|
+
const charTranslateYPeak = -normalizedGrowth * 6;
|
|
2616
|
+
const position = (charIndex + 0.5) / numChars;
|
|
2617
|
+
const horizontalOffset = (position - 0.5) * 2 * ((charMaxScale - 1.0) * 25);
|
|
2618
|
+
return b `<span
|
|
2619
|
+
class="char"
|
|
2620
|
+
data-char-index="${charIndex}"
|
|
2621
|
+
data-syllable-char-index="${charIndex}"
|
|
2622
|
+
data-wipe-start="${charStartPercent.toFixed(4)}"
|
|
2623
|
+
data-wipe-duration="${(1 / numChars).toFixed(4)}"
|
|
2624
|
+
data-horizontal-offset="${horizontalOffset.toFixed(2)}"
|
|
2625
|
+
data-max-scale="${charMaxScale.toFixed(3)}"
|
|
2626
|
+
data-shadow-intensity="${charShadowIntensity.toFixed(3)}"
|
|
2627
|
+
data-translate-y-peak="${charTranslateYPeak.toFixed(3)}"
|
|
2628
|
+
>${char}</span
|
|
2629
|
+
>`;
|
|
2630
|
+
})}`;
|
|
2631
|
+
return b `<span class="lyrics-word growable">
|
|
2632
|
+
<span class="lyrics-syllable-wrap">
|
|
2633
|
+
<span
|
|
2634
|
+
class="lyrics-syllable ${groupLineSynced
|
|
2635
|
+
? 'line-synced'
|
|
2636
|
+
: ''}"
|
|
2637
|
+
data-start-time="${startTimeMs}"
|
|
2638
|
+
data-end-time="${endTimeMs}"
|
|
2639
|
+
data-duration="${wordDuration}"
|
|
2640
|
+
data-syllable-index="0"
|
|
2641
|
+
data-wipe-ratio="1"
|
|
2642
|
+
>${syllableContent}</span
|
|
2643
|
+
>
|
|
2644
|
+
</span>
|
|
2645
|
+
</span>`;
|
|
2646
|
+
}
|
|
2290
2647
|
// For single-syllable groups, use original logic
|
|
2291
2648
|
if (group.length === 1) {
|
|
2292
2649
|
const syllable = group[0];
|
|
@@ -2311,7 +2668,7 @@ class AmLyrics extends i {
|
|
|
2311
2668
|
>${syllable.romanizedText}</span
|
|
2312
2669
|
>`
|
|
2313
2670
|
: '';
|
|
2314
|
-
// For growable words
|
|
2671
|
+
// For growable words (single-group visual word), use char glow
|
|
2315
2672
|
const syllableContent = isGrowable
|
|
2316
2673
|
? b `${text.split('').map((char, charIndex) => {
|
|
2317
2674
|
if (char === ' ') {
|
|
@@ -2319,14 +2676,12 @@ class AmLyrics extends i {
|
|
|
2319
2676
|
}
|
|
2320
2677
|
const numChars = trimmedText.length;
|
|
2321
2678
|
const charStartPercent = charIndex / text.length;
|
|
2322
|
-
// YouLyPlus emphasisMetrics calculation
|
|
2323
2679
|
const minDuration = 1000;
|
|
2324
2680
|
const maxDuration = 5000;
|
|
2325
2681
|
const easingPower = 3;
|
|
2326
2682
|
const progress = Math.min(1, Math.max(0, (durationMs - minDuration) /
|
|
2327
2683
|
(maxDuration - minDuration)));
|
|
2328
2684
|
const easedProgress = progress ** easingPower;
|
|
2329
|
-
// Decay calculation for long/short words
|
|
2330
2685
|
const isLongWord = numChars > 5;
|
|
2331
2686
|
const isShortDuration = durationMs < 1500;
|
|
2332
2687
|
let maxDecayRate = 0;
|
|
@@ -2340,7 +2695,6 @@ class AmLyrics extends i {
|
|
|
2340
2695
|
Math.max(0, 1.0 - (durationMs - 1000) / 500) * 0.4;
|
|
2341
2696
|
maxDecayRate = Math.min(decayStrength, 0.85);
|
|
2342
2697
|
}
|
|
2343
|
-
// Per-character calculations (exact YouLyPlus logic)
|
|
2344
2698
|
const positionInWord = numChars > 1 ? charIndex / (numChars - 1) : 0;
|
|
2345
2699
|
const decayFactor = 1.0 - positionInWord * maxDecayRate;
|
|
2346
2700
|
const charProgress = easedProgress * decayFactor;
|
|
@@ -2349,10 +2703,8 @@ class AmLyrics extends i {
|
|
|
2349
2703
|
const charShadowIntensity = 0.4 + charProgress * 0.4;
|
|
2350
2704
|
const normalizedGrowth = (charMaxScale - 1.0) / 0.13;
|
|
2351
2705
|
const charTranslateYPeak = -normalizedGrowth * 6;
|
|
2352
|
-
// Horizontal offset (simplified - YouLyPlus uses actual text width measurement)
|
|
2353
2706
|
const position = (charIndex + 0.5) / numChars;
|
|
2354
2707
|
const horizontalOffset = (position - 0.5) * 2 * ((charMaxScale - 1.0) * 25);
|
|
2355
|
-
// MOVED TO DATA ATTRIBUTES and removed style attribute to avoid Lit conflict
|
|
2356
2708
|
return b `<span
|
|
2357
2709
|
class="char"
|
|
2358
2710
|
data-char-index="${charIndex}"
|