Transcript export (.txt/.srt) and model storage manager (M6)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0084e0435a
commit
8f2da530e2
9 files changed
+743
-12
No files matched your search
@@ -58,3 +58,123 @@ export async function getDownloadedModels(
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Storage management (M6): per-model size + delete.
|
||||
//
|
||||
// The functions below take an injectable Cache-Storage-shaped parameter so the
|
||||
// logic is unit-testable under `bun test` (no `caches` global there); the
|
||||
// structural types cover exactly the subset of the Cache API we touch, and the
|
||||
// real `caches` satisfies them. Scope is the transformers-cache only — the
|
||||
// service worker's `whisper-runtime-*` cache (onnxruntime .wasm, shared by all
|
||||
// models and versioned/cleaned by the SW itself) is deliberately not surfaced.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** The slice of `Cache` the storage helpers use. */
|
||||
export interface StorageCache {
|
||||
keys(): Promise<ReadonlyArray<{ url: string }>>;
|
||||
match(
|
||||
request: { url: string },
|
||||
): Promise<{ blob(): Promise<{ size: number }> } | undefined>;
|
||||
delete(request: { url: string }): Promise<boolean>;
|
||||
}
|
||||
|
||||
/** The slice of `CacheStorage` the storage helpers use. */
|
||||
export interface StorageCacheStorage {
|
||||
keys(): Promise<string[]>;
|
||||
open(name: string): Promise<StorageCache>;
|
||||
}
|
||||
|
||||
/** Per-model cache usage, for the storage management panel. */
|
||||
export interface ModelStorageInfo {
|
||||
modelId: string;
|
||||
/** Number of cached files (weights + tokenizer/config aux files). */
|
||||
fileCount: number;
|
||||
/** Exact stored bytes, summed from the cached response bodies. */
|
||||
bytes: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure matcher: which catalog model owns a cached URL? Same segment-boundary
|
||||
* rule as `matchCachedModels` (`/<modelId>/`), so `.en` variants never match
|
||||
* their multilingual sibling. Any file counts here — a partially downloaded
|
||||
* model still occupies storage and must be listable/deletable.
|
||||
*/
|
||||
export function modelIdForUrl(
|
||||
url: string,
|
||||
modelIds: readonly string[],
|
||||
): string | undefined {
|
||||
return modelIds.find((modelId) => url.includes(`/${modelId}/`));
|
||||
}
|
||||
|
||||
function defaultCacheStorage(): StorageCacheStorage | undefined {
|
||||
// The cast is sound: we only ever hand `match`/`delete` the `Request`
|
||||
// objects that `keys()` itself returned (lib.dom types the params wider).
|
||||
return typeof caches === "undefined"
|
||||
? undefined
|
||||
: (caches as unknown as StorageCacheStorage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure cached storage per model: file count and exact byte size (summed
|
||||
* `blob().size` of every cached response). Models with nothing cached are
|
||||
* omitted. Deliberately lazy/heavier than `getDownloadedModels` — it reads
|
||||
* every cached body, so call it when the user opens the storage panel, not on
|
||||
* every render. Returns [] where Cache Storage is unavailable.
|
||||
*/
|
||||
export async function getModelStorageInfo(
|
||||
modelIds: readonly string[],
|
||||
cacheStorage: StorageCacheStorage | undefined = defaultCacheStorage(),
|
||||
): Promise<ModelStorageInfo[]> {
|
||||
if (!cacheStorage) return [];
|
||||
try {
|
||||
const byModel = new Map<string, ModelStorageInfo>();
|
||||
for (const name of await cacheStorage.keys()) {
|
||||
if (!name.includes("transformers")) continue;
|
||||
const cache = await cacheStorage.open(name);
|
||||
for (const request of await cache.keys()) {
|
||||
const modelId = modelIdForUrl(request.url, modelIds);
|
||||
if (modelId === undefined) continue;
|
||||
const response = await cache.match(request);
|
||||
if (!response) continue;
|
||||
const size = (await response.blob()).size;
|
||||
const info = byModel.get(modelId) ?? { modelId, fileCount: 0, bytes: 0 };
|
||||
info.fileCount += 1;
|
||||
info.bytes += size;
|
||||
byModel.set(modelId, info);
|
||||
}
|
||||
}
|
||||
// Catalog order, so the panel lists models the same way the picker does.
|
||||
return modelIds
|
||||
.map((modelId) => byModel.get(modelId))
|
||||
.filter((info): info is ModelStorageInfo => info !== undefined);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete every cached file belonging to `modelId` (weights and aux files)
|
||||
* from the transformers cache(s). Returns true if anything was deleted. The
|
||||
* weights re-download on the model's next use — nothing but cache lives here.
|
||||
*/
|
||||
export async function deleteModelFromCache(
|
||||
modelId: string,
|
||||
cacheStorage: StorageCacheStorage | undefined = defaultCacheStorage(),
|
||||
): Promise<boolean> {
|
||||
if (!cacheStorage) return false;
|
||||
try {
|
||||
let deleted = false;
|
||||
for (const name of await cacheStorage.keys()) {
|
||||
if (!name.includes("transformers")) continue;
|
||||
const cache = await cacheStorage.open(name);
|
||||
for (const request of await cache.keys()) {
|
||||
if (modelIdForUrl(request.url, [modelId]) !== modelId) continue;
|
||||
if (await cache.delete(request)) deleted = true;
|
||||
}
|
||||
}
|
||||
return deleted;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user