feat: fix performance on convert to romaji
This commit is contained in:
parent
df2b77eb7d
commit
daac9d9e60
4 changed files with 135 additions and 66 deletions
|
|
@ -189,6 +189,11 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
const scrobbler = new LastFMScrobbler();
|
||||
const lyricsManager = new LyricsManager(api);
|
||||
|
||||
// Pre-load Kuroshiro for romaji conversion in background (always load so it's ready instantly)
|
||||
lyricsManager.loadKuroshiro().catch(err => {
|
||||
console.warn('Failed to pre-load Kuroshiro:', err);
|
||||
});
|
||||
|
||||
const currentTheme = themeManager.getTheme();
|
||||
themeManager.setTheme(currentTheme);
|
||||
trackListSettings.getMode();
|
||||
|
|
|
|||
136
js/lyrics.js
136
js/lyrics.js
|
|
@ -8,6 +8,9 @@ import {
|
|||
} from "./utils.js";
|
||||
import { sidePanelManager } from "./side-panel.js";
|
||||
|
||||
// Dictionary path for kuromoji (loaded from CDN)
|
||||
const KUROMOJI_DICT_PATH = "https://unpkg.com/kuromoji@0.1.2/dict/";
|
||||
|
||||
export class LyricsManager {
|
||||
constructor(api) {
|
||||
this.api = api;
|
||||
|
|
@ -24,10 +27,11 @@ export class LyricsManager {
|
|||
this.originalLyricsData = null;
|
||||
this.kuroshiroLoaded = false;
|
||||
this.kuroshiroLoading = false;
|
||||
this.convertedNodes = new WeakSet(); // Track already converted nodes
|
||||
this.romajiTextCache = new Map(); // Cache: originalText -> convertedRomaji
|
||||
this.convertedTracksCache = new Set(); // Track IDs that have been fully converted
|
||||
}
|
||||
|
||||
// Load Kuroshiro from CDN
|
||||
// Load Kuroshiro from CDN (npm package uses Node.js path which doesn't work in browser)
|
||||
async loadKuroshiro() {
|
||||
if (this.kuroshiroLoaded) return true;
|
||||
if (this.kuroshiroLoading) {
|
||||
|
|
@ -53,7 +57,7 @@ export class LyricsManager {
|
|||
);
|
||||
}
|
||||
|
||||
// Load Kuromoji analyzer from CDN with proper dictionary path
|
||||
// Load Kuromoji analyzer from CDN
|
||||
if (!window.KuromojiAnalyzer) {
|
||||
await this.loadScript(
|
||||
"https://unpkg.com/kuroshiro-analyzer-kuromoji@1.1.0/dist/kuroshiro-analyzer-kuromoji.min.js",
|
||||
|
|
@ -67,10 +71,10 @@ export class LyricsManager {
|
|||
|
||||
this.kuroshiro = new Kuroshiro();
|
||||
|
||||
// Initialize with custom dictionary path from unpkg
|
||||
// Initialize with dictionary path from CDN
|
||||
await this.kuroshiro.init(
|
||||
new KuromojiAnalyzer({
|
||||
dictPath: "https://unpkg.com/kuromoji@0.1.2/dict/",
|
||||
dictPath: KUROMOJI_DICT_PATH,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -89,6 +93,11 @@ export class LyricsManager {
|
|||
// Helper to load external scripts
|
||||
loadScript(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Check if script already exists
|
||||
if (document.querySelector(`script[src="${src}"]`)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const script = document.createElement("script");
|
||||
script.src = src;
|
||||
script.onload = resolve;
|
||||
|
|
@ -104,10 +113,15 @@ export class LyricsManager {
|
|||
return /[\u3040-\u30FF\u31F0-\u9FFF]/.test(text);
|
||||
}
|
||||
|
||||
// Convert Japanese text to Romaji (including Kanji)
|
||||
// Convert Japanese text to Romaji (including Kanji) with caching
|
||||
async convertToRomaji(text) {
|
||||
if (!text) return text;
|
||||
|
||||
// Check cache first
|
||||
if (this.romajiTextCache.has(text)) {
|
||||
return this.romajiTextCache.get(text);
|
||||
}
|
||||
|
||||
// Make sure Kuroshiro is loaded
|
||||
if (!this.kuroshiroLoaded) {
|
||||
const success = await this.loadKuroshiro();
|
||||
|
|
@ -129,6 +143,8 @@ export class LyricsManager {
|
|||
mode: "spaced",
|
||||
romajiSystem: "hepburn",
|
||||
});
|
||||
// Cache the result
|
||||
this.romajiTextCache.set(text, result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
|
|
@ -327,14 +343,21 @@ export class LyricsManager {
|
|||
const observeRoot = amLyricsElement.shadowRoot || amLyricsElement;
|
||||
|
||||
this.romajiObserver = new MutationObserver((mutations) => {
|
||||
// Only process if new text content was added (ignore attribute changes like highlight)
|
||||
const hasNewContent = mutations.some(
|
||||
(mutation) =>
|
||||
mutation.type === "childList" && mutation.addedNodes.length > 0,
|
||||
);
|
||||
// Check if any relevant mutation occurred
|
||||
const hasRelevantChange = mutations.some((mutation) => {
|
||||
// New nodes added
|
||||
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
|
||||
return true;
|
||||
}
|
||||
// Text content changed
|
||||
if (mutation.type === "characterData" && mutation.target.textContent) {
|
||||
// Only trigger if the text contains Japanese
|
||||
return this.containsJapanese(mutation.target.textContent);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!hasNewContent) {
|
||||
// Ignore highlight changes and other attribute mutations
|
||||
if (!hasRelevantChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -348,27 +371,17 @@ export class LyricsManager {
|
|||
});
|
||||
|
||||
// Observe all child nodes for changes (in shadow DOM if it exists)
|
||||
// Only watch for new nodes, not attribute changes (to avoid highlight spam)
|
||||
// Watch for new nodes AND text content changes to catch when lyrics refresh
|
||||
this.romajiObserver.observe(observeRoot, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: false, // Don't watch text changes, only new nodes
|
||||
characterData: true, // Watch text changes to catch lyric refreshes
|
||||
attributes: false, // Don't watch attribute changes (highlight, etc)
|
||||
});
|
||||
|
||||
// Initial conversion if Romaji mode is enabled
|
||||
// Initial conversion if Romaji mode is enabled - single attempt, no periodic polling
|
||||
if (this.isRomajiMode) {
|
||||
// Try immediately and after delays to catch lyrics when they load
|
||||
this.convertLyricsContent(amLyricsElement);
|
||||
setTimeout(async () => {
|
||||
await this.convertLyricsContent(amLyricsElement);
|
||||
}, 500);
|
||||
setTimeout(async () => {
|
||||
await this.convertLyricsContent(amLyricsElement);
|
||||
}, 1500);
|
||||
setTimeout(async () => {
|
||||
await this.convertLyricsContent(amLyricsElement);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -405,14 +418,7 @@ export class LyricsManager {
|
|||
}
|
||||
|
||||
// Convert Japanese text to Romaji (using async/await for Kuroshiro)
|
||||
let convertedCount = 0;
|
||||
|
||||
for (const textNode of textNodes) {
|
||||
// Skip if already converted
|
||||
if (this.convertedNodes.has(textNode)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!textNode.parentElement) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -443,18 +449,21 @@ export class LyricsManager {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Check if contains Japanese
|
||||
// Check if contains Japanese - convert if we find Japanese
|
||||
if (this.containsJapanese(originalText)) {
|
||||
const romajiText = await this.convertToRomaji(originalText);
|
||||
|
||||
// Only update if conversion produced different text
|
||||
if (romajiText && romajiText !== originalText) {
|
||||
textNode.textContent = romajiText;
|
||||
// Mark as converted
|
||||
this.convertedNodes.add(textNode);
|
||||
convertedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark this track as converted
|
||||
if (this.currentTrackId) {
|
||||
this.convertedTracksCache.add(this.currentTrackId);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the observer
|
||||
|
|
@ -467,8 +476,6 @@ export class LyricsManager {
|
|||
clearTimeout(this.observerTimeout);
|
||||
this.observerTimeout = null;
|
||||
}
|
||||
// Clear converted nodes tracking when stopping
|
||||
this.convertedNodes = new WeakSet();
|
||||
}
|
||||
|
||||
// Toggle Romaji mode
|
||||
|
|
@ -480,12 +487,10 @@ export class LyricsManager {
|
|||
if (this.isRomajiMode) {
|
||||
// Turning ON: Setup observer and convert immediately
|
||||
this.setupLyricsObserver(amLyricsElement);
|
||||
// Also try immediate conversion (don't wait for timeout)
|
||||
await this.convertLyricsContent(amLyricsElement);
|
||||
} else {
|
||||
// Turning OFF: Stop observer (original lyrics should remain)
|
||||
// Turning OFF: Stop observer
|
||||
// Note: To restore original Japanese, we'd need to reload the component
|
||||
// For now, the converted text stays until lyrics are reloaded
|
||||
this.stopLyricsObserver();
|
||||
}
|
||||
}
|
||||
|
|
@ -602,49 +607,52 @@ async function renderLyricsComponent(
|
|||
|
||||
container.appendChild(amLyrics);
|
||||
|
||||
// Wait for lyrics to load in the component, then setup observer
|
||||
// Setup observer IMMEDIATELY to catch lyrics as they load (not after waiting)
|
||||
// This is critical - observer must be running before lyrics arrive from LRCLIB
|
||||
lyricsManager.setupLyricsObserver(amLyrics);
|
||||
|
||||
// If Romaji mode is enabled, ensure Kuroshiro is ready
|
||||
if (lyricsManager.isRomajiMode && !lyricsManager.kuroshiroLoaded) {
|
||||
await lyricsManager.loadKuroshiro();
|
||||
}
|
||||
|
||||
// Wait for lyrics to appear, then do an immediate conversion
|
||||
const waitForLyrics = () => {
|
||||
return new Promise((resolve) => {
|
||||
// Check if lyrics are already loaded
|
||||
const hasLyrics =
|
||||
amLyrics.querySelector(".lyric-line, [class*='lyric']") ||
|
||||
(amLyrics.textContent && amLyrics.textContent.length > 50);
|
||||
const checkForLyrics = () => {
|
||||
const hasLyrics =
|
||||
amLyrics.querySelector(".lyric-line, [class*='lyric']") ||
|
||||
(amLyrics.shadowRoot && amLyrics.shadowRoot.querySelector("[class*='lyric']")) ||
|
||||
(amLyrics.textContent && amLyrics.textContent.length > 50);
|
||||
return hasLyrics;
|
||||
};
|
||||
|
||||
if (hasLyrics) {
|
||||
if (checkForLyrics()) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait up to 10 seconds for lyrics to load
|
||||
// Check more frequently (200ms) for faster response
|
||||
let attempts = 0;
|
||||
const maxAttempts = 20;
|
||||
const maxAttempts = 25; // 5 seconds max
|
||||
const interval = setInterval(() => {
|
||||
attempts++;
|
||||
const hasContent =
|
||||
amLyrics.querySelector(".lyric-line, [class*='lyric']") ||
|
||||
(amLyrics.textContent && amLyrics.textContent.length > 50);
|
||||
if (hasContent || attempts >= maxAttempts) {
|
||||
if (checkForLyrics() || attempts >= maxAttempts) {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}
|
||||
}, 500);
|
||||
}, 200);
|
||||
});
|
||||
};
|
||||
|
||||
await waitForLyrics();
|
||||
|
||||
// Setup observer to convert lyrics to Romaji
|
||||
lyricsManager.setupLyricsObserver(amLyrics);
|
||||
|
||||
// If Romaji mode is already enabled, convert after shadow DOM is ready
|
||||
// Convert immediately after lyrics detected
|
||||
if (lyricsManager.isRomajiMode) {
|
||||
// Ensure Kuroshiro is loaded before converting
|
||||
if (!lyricsManager.kuroshiroLoaded) {
|
||||
await lyricsManager.loadKuroshiro();
|
||||
}
|
||||
// Add small delay to ensure shadow DOM is fully populated
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
await lyricsManager.convertLyricsContent(amLyrics);
|
||||
// One retry after 500ms in case more lyrics load
|
||||
setTimeout(() => lyricsManager.convertLyricsContent(amLyrics), 500);
|
||||
}
|
||||
|
||||
const cleanup = setupSync(track, audioPlayer, amLyrics);
|
||||
|
|
|
|||
56
package-lock.json
generated
56
package-lock.json
generated
|
|
@ -8,6 +8,10 @@
|
|||
"name": "monochrome",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"kuroshiro": "^1.2.0",
|
||||
"kuroshiro-analyzer-kuromoji": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^7.3.0",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
|
|
@ -1502,7 +1506,6 @@
|
|||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
|
@ -3044,6 +3047,11 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/doublearray": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/doublearray/-/doublearray-0.0.2.tgz",
|
||||
"integrity": "sha512-aw55FtZzT6AmiamEj2kvmR6BuFqvYgKZUkfQ7teqVRNqD5UE0rw8IeW/3gieHNKQ5sPuDKlljWEn4bzv5+1bHw=="
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
|
|
@ -4273,6 +4281,43 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/kuromoji": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/kuromoji/-/kuromoji-0.1.2.tgz",
|
||||
"integrity": "sha512-V0dUf+C2LpcPEXhoHLMAop/bOht16Dyr+mDiIE39yX3vqau7p80De/koFqpiTcL1zzdZlc3xuHZ8u5gjYRfFaQ==",
|
||||
"dependencies": {
|
||||
"async": "^2.0.1",
|
||||
"doublearray": "0.0.2",
|
||||
"zlibjs": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kuromoji/node_modules/async": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"node_modules/kuroshiro": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/kuroshiro/-/kuroshiro-1.2.0.tgz",
|
||||
"integrity": "sha512-yBGCK9oDOY3LGZ/KXaN9m7ADcAuSczOR2FoMRYwHLUlis3/o/uxdMVROAjENFO0NQJgALhIdWxI/vIBVrMCk9w==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/kuroshiro-analyzer-kuromoji": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/kuroshiro-analyzer-kuromoji/-/kuroshiro-analyzer-kuromoji-1.1.0.tgz",
|
||||
"integrity": "sha512-BSJFhpsQdPwfFLfjKxfLA9iL+/PC6LCR9vgwgb5Jc7jZwk9ilX8SAV6CwhAQZY611tiuhbB52ONYKDO8hgY1bA==",
|
||||
"dependencies": {
|
||||
"kuromoji": "^0.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/leven": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||
|
|
@ -4287,7 +4332,6 @@
|
|||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
|
|
@ -6306,6 +6350,14 @@
|
|||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/zlibjs": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz",
|
||||
"integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,5 +25,9 @@
|
|||
"vite": "^7.3.0",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"wanakana": "^5.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"kuroshiro": "^1.2.0",
|
||||
"kuroshiro-analyzer-kuromoji": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue