diff --git a/src/equicordplugins/vcNarratorCustom/index.tsx b/src/equicordplugins/vcNarratorCustom/index.tsx index d62c9d11..23723f26 100644 --- a/src/equicordplugins/vcNarratorCustom/index.tsx +++ b/src/equicordplugins/vcNarratorCustom/index.tsx @@ -24,6 +24,51 @@ import { UserStore, } from "@webpack/common"; +/// +// Create an in-memory cache (temporary, lost on restart) +const ttsCache = new Map(); + +// Helper function to open (or create) an IndexedDB database. +function openDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open("VcNarratorDB", 1); + request.onupgradeneeded = () => { + const db = request.result; + // Create an object store called "voices" if it doesn't already exist. + if (!db.objectStoreNames.contains("voices")) { + db.createObjectStore("voices"); + } + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); +} + +// Function to get a cached voice line from IndexedDB. +async function getCachedVoiceFromDB(cacheKey: string): Promise { + const db = await openDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction("voices", "readonly"); + const store = tx.objectStore("voices"); + const request = store.get(cacheKey); + request.onsuccess = () => resolve(request.result || null); + request.onerror = () => reject(request.error); + }); +} + +// Function to store a voice line in IndexedDB. +async function setCachedVoiceInDB(cacheKey: string, blob: Blob): Promise { + const db = await openDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction("voices", "readwrite"); + const store = tx.objectStore("voices"); + const request = store.put(blob, cacheKey); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); +} + +/// interface VoiceState { userId: string; channelId?: string; @@ -43,10 +88,42 @@ const VoiceStateStore = findByPropsLazy( // Filtering out events is not as simple as just dropping duplicates, as otherwise mute, unmute, mute would // not say the second mute, which would lead you to believe they're unmuted -async function speak( - text: string, -) { +/// +async function speak(text: string) { if (text.trim().length === 0) return; + + // Create a unique cache key using the voice setting and the text. + const cacheKey = `${settings.store.customVoice}_${text}`; + + // 1. Check the in-memory cache (fast check) + if (ttsCache.has(cacheKey)) { + const cachedUrl = ttsCache.get(cacheKey)!; + const audio = new Audio(cachedUrl); + audio.volume = settings.store.volume; + audio.playbackRate = settings.store.rate; + audio.play(); + return; + } + + // 2. Check the persistent IndexedDB cache. + try { + const cachedBlob = await getCachedVoiceFromDB(cacheKey); + if (cachedBlob) { + // Create a URL from the stored Blob. + const url = URL.createObjectURL(cachedBlob); + // Save it in the in-memory cache for next time. + ttsCache.set(cacheKey, url); + const audio = new Audio(url); + audio.volume = settings.store.volume; + audio.playbackRate = settings.store.rate; + audio.play(); + return; + } + } catch (err) { + console.error("Error accessing IndexedDB:", err); + } + + // 3. If not found in either cache, fetch from the TTS API. const response = await fetch( "https://tiktok-tts.weilnet.workers.dev/api/generation", { @@ -76,12 +153,23 @@ async function speak( const blob = new Blob([new Uint8Array(binaryData)], { type: "audio/mpeg" }); const url = URL.createObjectURL(blob); + // Save the URL in the in-memory cache. + ttsCache.set(cacheKey, url); + + // Store the Blob in IndexedDB for persistence. + try { + await setCachedVoiceInDB(cacheKey, blob); + } catch (err) { + console.error("Error storing in IndexedDB:", err); + } + const audio = new Audio(url); audio.volume = settings.store.volume; audio.playbackRate = settings.store.rate; audio.play(); } +/// function clean(str: string) { const replacer = settings.store.latinOnly ? /[^\p{Script=Latin}\p{Number}\p{Punctuation}\s]/gu