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 scrobbler = new LastFMScrobbler();
|
||||||
const lyricsManager = new LyricsManager(api);
|
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();
|
const currentTheme = themeManager.getTheme();
|
||||||
themeManager.setTheme(currentTheme);
|
themeManager.setTheme(currentTheme);
|
||||||
trackListSettings.getMode();
|
trackListSettings.getMode();
|
||||||
|
|
|
||||||
136
js/lyrics.js
136
js/lyrics.js
|
|
@ -8,6 +8,9 @@ import {
|
||||||
} from "./utils.js";
|
} from "./utils.js";
|
||||||
import { sidePanelManager } from "./side-panel.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 {
|
export class LyricsManager {
|
||||||
constructor(api) {
|
constructor(api) {
|
||||||
this.api = api;
|
this.api = api;
|
||||||
|
|
@ -24,10 +27,11 @@ export class LyricsManager {
|
||||||
this.originalLyricsData = null;
|
this.originalLyricsData = null;
|
||||||
this.kuroshiroLoaded = false;
|
this.kuroshiroLoaded = false;
|
||||||
this.kuroshiroLoading = 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() {
|
async loadKuroshiro() {
|
||||||
if (this.kuroshiroLoaded) return true;
|
if (this.kuroshiroLoaded) return true;
|
||||||
if (this.kuroshiroLoading) {
|
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) {
|
if (!window.KuromojiAnalyzer) {
|
||||||
await this.loadScript(
|
await this.loadScript(
|
||||||
"https://unpkg.com/kuroshiro-analyzer-kuromoji@1.1.0/dist/kuroshiro-analyzer-kuromoji.min.js",
|
"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();
|
this.kuroshiro = new Kuroshiro();
|
||||||
|
|
||||||
// Initialize with custom dictionary path from unpkg
|
// Initialize with dictionary path from CDN
|
||||||
await this.kuroshiro.init(
|
await this.kuroshiro.init(
|
||||||
new KuromojiAnalyzer({
|
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
|
// Helper to load external scripts
|
||||||
loadScript(src) {
|
loadScript(src) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
// Check if script already exists
|
||||||
|
if (document.querySelector(`script[src="${src}"]`)) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const script = document.createElement("script");
|
const script = document.createElement("script");
|
||||||
script.src = src;
|
script.src = src;
|
||||||
script.onload = resolve;
|
script.onload = resolve;
|
||||||
|
|
@ -104,10 +113,15 @@ export class LyricsManager {
|
||||||
return /[\u3040-\u30FF\u31F0-\u9FFF]/.test(text);
|
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) {
|
async convertToRomaji(text) {
|
||||||
if (!text) return text;
|
if (!text) return text;
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
if (this.romajiTextCache.has(text)) {
|
||||||
|
return this.romajiTextCache.get(text);
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure Kuroshiro is loaded
|
// Make sure Kuroshiro is loaded
|
||||||
if (!this.kuroshiroLoaded) {
|
if (!this.kuroshiroLoaded) {
|
||||||
const success = await this.loadKuroshiro();
|
const success = await this.loadKuroshiro();
|
||||||
|
|
@ -129,6 +143,8 @@ export class LyricsManager {
|
||||||
mode: "spaced",
|
mode: "spaced",
|
||||||
romajiSystem: "hepburn",
|
romajiSystem: "hepburn",
|
||||||
});
|
});
|
||||||
|
// Cache the result
|
||||||
|
this.romajiTextCache.set(text, result);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
|
|
@ -327,14 +343,21 @@ export class LyricsManager {
|
||||||
const observeRoot = amLyricsElement.shadowRoot || amLyricsElement;
|
const observeRoot = amLyricsElement.shadowRoot || amLyricsElement;
|
||||||
|
|
||||||
this.romajiObserver = new MutationObserver((mutations) => {
|
this.romajiObserver = new MutationObserver((mutations) => {
|
||||||
// Only process if new text content was added (ignore attribute changes like highlight)
|
// Check if any relevant mutation occurred
|
||||||
const hasNewContent = mutations.some(
|
const hasRelevantChange = mutations.some((mutation) => {
|
||||||
(mutation) =>
|
// New nodes added
|
||||||
mutation.type === "childList" && mutation.addedNodes.length > 0,
|
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) {
|
if (!hasRelevantChange) {
|
||||||
// Ignore highlight changes and other attribute mutations
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,27 +371,17 @@ export class LyricsManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Observe all child nodes for changes (in shadow DOM if it exists)
|
// 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, {
|
this.romajiObserver.observe(observeRoot, {
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: 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)
|
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) {
|
if (this.isRomajiMode) {
|
||||||
// Try immediately and after delays to catch lyrics when they load
|
|
||||||
this.convertLyricsContent(amLyricsElement);
|
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)
|
// Convert Japanese text to Romaji (using async/await for Kuroshiro)
|
||||||
let convertedCount = 0;
|
|
||||||
|
|
||||||
for (const textNode of textNodes) {
|
for (const textNode of textNodes) {
|
||||||
// Skip if already converted
|
|
||||||
if (this.convertedNodes.has(textNode)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!textNode.parentElement) {
|
if (!textNode.parentElement) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -443,18 +449,21 @@ export class LyricsManager {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if contains Japanese
|
// Check if contains Japanese - convert if we find Japanese
|
||||||
if (this.containsJapanese(originalText)) {
|
if (this.containsJapanese(originalText)) {
|
||||||
const romajiText = await this.convertToRomaji(originalText);
|
const romajiText = await this.convertToRomaji(originalText);
|
||||||
|
|
||||||
|
// Only update if conversion produced different text
|
||||||
if (romajiText && romajiText !== originalText) {
|
if (romajiText && romajiText !== originalText) {
|
||||||
textNode.textContent = romajiText;
|
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
|
// Stop the observer
|
||||||
|
|
@ -467,8 +476,6 @@ export class LyricsManager {
|
||||||
clearTimeout(this.observerTimeout);
|
clearTimeout(this.observerTimeout);
|
||||||
this.observerTimeout = null;
|
this.observerTimeout = null;
|
||||||
}
|
}
|
||||||
// Clear converted nodes tracking when stopping
|
|
||||||
this.convertedNodes = new WeakSet();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle Romaji mode
|
// Toggle Romaji mode
|
||||||
|
|
@ -480,12 +487,10 @@ export class LyricsManager {
|
||||||
if (this.isRomajiMode) {
|
if (this.isRomajiMode) {
|
||||||
// Turning ON: Setup observer and convert immediately
|
// Turning ON: Setup observer and convert immediately
|
||||||
this.setupLyricsObserver(amLyricsElement);
|
this.setupLyricsObserver(amLyricsElement);
|
||||||
// Also try immediate conversion (don't wait for timeout)
|
|
||||||
await this.convertLyricsContent(amLyricsElement);
|
await this.convertLyricsContent(amLyricsElement);
|
||||||
} else {
|
} 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
|
// Note: To restore original Japanese, we'd need to reload the component
|
||||||
// For now, the converted text stays until lyrics are reloaded
|
|
||||||
this.stopLyricsObserver();
|
this.stopLyricsObserver();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -602,49 +607,52 @@ async function renderLyricsComponent(
|
||||||
|
|
||||||
container.appendChild(amLyrics);
|
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 = () => {
|
const waitForLyrics = () => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
// Check if lyrics are already loaded
|
// Check if lyrics are already loaded
|
||||||
const hasLyrics =
|
const checkForLyrics = () => {
|
||||||
amLyrics.querySelector(".lyric-line, [class*='lyric']") ||
|
const hasLyrics =
|
||||||
(amLyrics.textContent && amLyrics.textContent.length > 50);
|
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();
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait up to 10 seconds for lyrics to load
|
// Check more frequently (200ms) for faster response
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
const maxAttempts = 20;
|
const maxAttempts = 25; // 5 seconds max
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
attempts++;
|
attempts++;
|
||||||
const hasContent =
|
if (checkForLyrics() || attempts >= maxAttempts) {
|
||||||
amLyrics.querySelector(".lyric-line, [class*='lyric']") ||
|
|
||||||
(amLyrics.textContent && amLyrics.textContent.length > 50);
|
|
||||||
if (hasContent || attempts >= maxAttempts) {
|
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 200);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
await waitForLyrics();
|
await waitForLyrics();
|
||||||
|
|
||||||
// Setup observer to convert lyrics to Romaji
|
// Convert immediately after lyrics detected
|
||||||
lyricsManager.setupLyricsObserver(amLyrics);
|
|
||||||
|
|
||||||
// If Romaji mode is already enabled, convert after shadow DOM is ready
|
|
||||||
if (lyricsManager.isRomajiMode) {
|
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);
|
await lyricsManager.convertLyricsContent(amLyrics);
|
||||||
|
// One retry after 500ms in case more lyrics load
|
||||||
|
setTimeout(() => lyricsManager.convertLyricsContent(amLyrics), 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanup = setupSync(track, audioPlayer, amLyrics);
|
const cleanup = setupSync(track, audioPlayer, amLyrics);
|
||||||
|
|
|
||||||
56
package-lock.json
generated
56
package-lock.json
generated
|
|
@ -8,6 +8,10 @@
|
||||||
"name": "monochrome",
|
"name": "monochrome",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"kuroshiro": "^1.2.0",
|
||||||
|
"kuroshiro-analyzer-kuromoji": "^1.1.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^7.3.0",
|
"vite": "^7.3.0",
|
||||||
"vite-plugin-pwa": "^1.2.0",
|
"vite-plugin-pwa": "^1.2.0",
|
||||||
|
|
@ -1502,7 +1506,6 @@
|
||||||
"version": "7.28.4",
|
"version": "7.28.4",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
|
|
@ -3044,6 +3047,11 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
|
@ -4273,6 +4281,43 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/leven": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||||
|
|
@ -4287,7 +4332,6 @@
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.debounce": {
|
"node_modules/lodash.debounce": {
|
||||||
|
|
@ -6306,6 +6350,14 @@
|
||||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"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": "^7.3.0",
|
||||||
"vite-plugin-pwa": "^1.2.0",
|
"vite-plugin-pwa": "^1.2.0",
|
||||||
"wanakana": "^5.3.1"
|
"wanakana": "^5.3.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"kuroshiro": "^1.2.0",
|
||||||
|
"kuroshiro-analyzer-kuromoji": "^1.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue