165 lines
4.7 KiB
JavaScript
165 lines
4.7 KiB
JavaScript
const MODEL_CACHE_NAME = "timeline-studio-model-cache-v4";
|
|
const APP_CACHE_NAME = "timeline-studio-app-shell-v3";
|
|
const APP_SHELL_URLS = [
|
|
"/",
|
|
"/manifest.webmanifest",
|
|
"/icons/timeline-studio-icon.svg",
|
|
"/icons/timeline-studio-icon-192.png",
|
|
"/icons/timeline-studio-icon-512.png",
|
|
"/icons/timeline-studio-apple-touch.png",
|
|
];
|
|
const CACHEABLE_EXTENSIONS = [
|
|
".bin",
|
|
".css",
|
|
".js",
|
|
".json",
|
|
".model",
|
|
".mp4",
|
|
".onnx",
|
|
".png",
|
|
".safetensors",
|
|
".txt",
|
|
".wasm",
|
|
];
|
|
const HUGGING_FACE_HOSTS = new Set([
|
|
"huggingface.co",
|
|
"cdn-lfs.huggingface.co",
|
|
"cdn-lfs-us-1.hf.co",
|
|
"cdn-lfs-eu-1.hf.co",
|
|
]);
|
|
function hasCacheableExtension(pathname) {
|
|
return CACHEABLE_EXTENSIONS.some((extension) => pathname.endsWith(extension));
|
|
}
|
|
|
|
function isHuggingFaceModelRequest(url) {
|
|
if (!HUGGING_FACE_HOSTS.has(url.hostname)) {
|
|
return false;
|
|
}
|
|
|
|
// Piper's runtime owns its OPFS cache. Caching the same voice files here
|
|
// would keep a second full model copy for every non-English language.
|
|
if (url.pathname.includes("/rhasspy/piper-voices/resolve/")) return false;
|
|
return url.hostname !== "huggingface.co" || url.pathname.includes("/resolve/");
|
|
}
|
|
|
|
async function removeLegacyPiperDuplicates() {
|
|
const cache = await caches.open(MODEL_CACHE_NAME);
|
|
const keys = await cache.keys();
|
|
await Promise.all(keys.filter((request) => new URL(request.url).pathname.includes("/rhasspy/piper-voices/resolve/")).map((request) => cache.delete(request)));
|
|
}
|
|
|
|
function isRuntimeAssetRequest(url) {
|
|
if (url.origin === self.location.origin) {
|
|
return url.pathname.startsWith("/models/")
|
|
|| (url.pathname.startsWith("/assets/") && hasCacheableExtension(url.pathname));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function shouldCacheRequest(request) {
|
|
if (request.method !== "GET" || request.headers.has("range")) {
|
|
return false;
|
|
}
|
|
|
|
const url = new URL(request.url);
|
|
return isHuggingFaceModelRequest(url) || isRuntimeAssetRequest(url);
|
|
}
|
|
|
|
function withCacheStatus(response, status) {
|
|
const headers = new Headers(response.headers);
|
|
headers.set("X-Timeline-Model-Cache", status);
|
|
return new Response(response.body, {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
headers,
|
|
});
|
|
}
|
|
|
|
async function cacheFirst(request, event) {
|
|
const cache = await caches.open(MODEL_CACHE_NAME);
|
|
const cached = await cache.match(request);
|
|
if (cached) {
|
|
return withCacheStatus(cached, "hit");
|
|
}
|
|
|
|
const response = await fetch(request);
|
|
if (response.ok || response.type === "opaque") {
|
|
// Keep the service worker alive until the large model shard is durably
|
|
// committed. The live response remains streaming and is not blocked.
|
|
event.waitUntil(cache.put(request, response.clone()).catch((error) => {
|
|
if (error?.name !== "QuotaExceededError") console.warn("Model cache write failed.", error);
|
|
}));
|
|
}
|
|
return withCacheStatus(response, "miss");
|
|
}
|
|
|
|
async function networkFirst(request) {
|
|
const cache = await caches.open(APP_CACHE_NAME);
|
|
try {
|
|
const response = await fetch(request);
|
|
if (response.ok) {
|
|
cache.put(request, response.clone()).catch(() => {});
|
|
}
|
|
return response;
|
|
} catch (error) {
|
|
const cached = await cache.match(request);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
self.addEventListener("install", (event) => {
|
|
event.waitUntil(
|
|
caches
|
|
.open(APP_CACHE_NAME)
|
|
.then((cache) => cache.addAll(APP_SHELL_URLS))
|
|
.catch(() => {})
|
|
.then(() => self.skipWaiting()),
|
|
);
|
|
});
|
|
|
|
self.addEventListener("activate", (event) => {
|
|
const activeCacheNames = new Set([APP_CACHE_NAME, MODEL_CACHE_NAME]);
|
|
event.waitUntil(
|
|
caches
|
|
.keys()
|
|
.then((keys) =>
|
|
Promise.all(
|
|
keys
|
|
.filter((key) => key.startsWith("timeline-studio-") && !activeCacheNames.has(key))
|
|
.map((key) => caches.delete(key)),
|
|
),
|
|
)
|
|
.then(() => self.clients.claim()),
|
|
);
|
|
// Older Transformers.js builds created a second copy of Hugging Face model
|
|
// assets here. The service worker is now the sole cache owner.
|
|
event.waitUntil(caches.delete("transformers-cache").catch(() => false));
|
|
event.waitUntil(caches.delete("stable-audio-3-small-music-q4-v1").catch(() => false));
|
|
event.waitUntil(removeLegacyPiperDuplicates().catch(() => {}));
|
|
});
|
|
|
|
self.addEventListener("fetch", (event) => {
|
|
if (event.request.mode === "navigate") {
|
|
event.respondWith(networkFirst(event.request));
|
|
return;
|
|
}
|
|
|
|
if (!shouldCacheRequest(event.request)) {
|
|
return;
|
|
}
|
|
|
|
event.respondWith(cacheFirst(event.request, event));
|
|
});
|
|
|
|
self.addEventListener("message", (event) => {
|
|
if (event.data?.type !== "CLEAR_MODEL_CACHE") {
|
|
return;
|
|
}
|
|
|
|
event.waitUntil(caches.delete(MODEL_CACHE_NAME));
|
|
});
|