Transcript export (.txt/.srt) and model storage manager (M6)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Fable 5 committed 2026-07-14 19:41:47 +02:00
1 parent 0084e0435a
commit 8f2da530e2
9 files changed
+743 -12

No files matched your search

+129 -2
View File
@@ -1,8 +1,16 @@
import { describe, test, expect } from "bun:test";
import { matchCachedModels } from "../src/lib/modelCache";
import {
deleteModelFromCache,
getModelStorageInfo,
matchCachedModels,
modelIdForUrl,
type StorageCache,
type StorageCacheStorage,
} from "../src/lib/modelCache";
// Pure URL matching (D-005 downloaded badge); the `caches` wrapper is
// browser-only and exercised manually.
// browser-only and exercised manually. The M6 storage helpers take an
// injectable CacheStorage-shaped object, mocked below.
const TINY = "onnx-community/whisper-tiny.en";
const BASE = "onnx-community/whisper-base";
@@ -47,3 +55,122 @@ describe("matchCachedModels", () => {
);
});
});
describe("modelIdForUrl", () => {
test("matches on the /modelId/ segment, any file type", () => {
expect(modelIdForUrl(hubUrl(TINY, "tokenizer.json"), [TINY, BASE])).toBe(TINY);
expect(modelIdForUrl(hubUrl(BASE, "onnx/encoder_model_q4.onnx"), [TINY, BASE])).toBe(
BASE,
);
});
test("'.en' variant URLs never match the multilingual sibling", () => {
const url = hubUrl(TINY, "onnx/encoder_model.onnx");
expect(modelIdForUrl(url, ["onnx-community/whisper-tiny"])).toBeUndefined();
});
test("unmatched URLs yield undefined", () => {
expect(modelIdForUrl("https://example.com/other.bin", [TINY])).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// M6 storage helpers, against a mocked Cache Storage
// ---------------------------------------------------------------------------
/** Mutable mock: cache name → (url → body size in bytes). */
function mockCacheStorage(
data: Record<string, Record<string, number>>,
): StorageCacheStorage {
const stores = new Map<string, Map<string, number>>(
Object.entries(data).map(([name, entries]) => [
name,
new Map(Object.entries(entries)),
]),
);
function openCache(name: string): StorageCache {
const entries = stores.get(name) ?? new Map<string, number>();
return {
keys: async () => [...entries.keys()].map((url) => ({ url })),
match: async (request) => {
const size = entries.get(request.url);
if (size === undefined) return undefined;
return { blob: async () => new Blob([new Uint8Array(size)]) };
},
delete: async (request) => entries.delete(request.url),
};
}
return {
keys: async () => [...stores.keys()],
open: async (name) => openCache(name),
};
}
describe("getModelStorageInfo", () => {
test("sums bytes and counts files per model, in catalog order", async () => {
const storage = mockCacheStorage({
"transformers-cache": {
[hubUrl(BASE, "onnx/encoder_model_q8.onnx")]: 2_000,
[hubUrl(TINY, "onnx/encoder_model_quantized.onnx")]: 1_000,
[hubUrl(TINY, "onnx/decoder_model_merged_quantized.onnx")]: 3_000,
[hubUrl(TINY, "tokenizer.json")]: 50,
},
});
expect(await getModelStorageInfo([TINY, BASE], storage)).toEqual([
{ modelId: TINY, fileCount: 3, bytes: 4_050 },
{ modelId: BASE, fileCount: 1, bytes: 2_000 },
]);
});
test("ignores non-transformers caches and unmatched URLs", async () => {
const storage = mockCacheStorage({
"whisper-runtime-v1": { "https://example.com/ort/ort-wasm.wasm": 9_999_999 },
"transformers-cache": {
[hubUrl(TINY, "config.json")]: 10,
"https://example.com/unrelated.bin": 123,
},
});
expect(await getModelStorageInfo([TINY, BASE], storage)).toEqual([
{ modelId: TINY, fileCount: 1, bytes: 10 },
]);
});
test("returns [] with no cache storage (non-browser contexts)", async () => {
expect(await getModelStorageInfo([TINY], undefined)).toEqual([]);
});
});
describe("deleteModelFromCache", () => {
test("removes only the target model's entries and reports deletion", async () => {
const storage = mockCacheStorage({
"transformers-cache": {
[hubUrl(TINY, "onnx/encoder_model_quantized.onnx")]: 1_000,
[hubUrl(TINY, "tokenizer.json")]: 50,
[hubUrl(BASE, "onnx/encoder_model_q8.onnx")]: 2_000,
},
});
expect(await deleteModelFromCache(TINY, storage)).toBe(true);
expect(await getModelStorageInfo([TINY, BASE], storage)).toEqual([
{ modelId: BASE, fileCount: 1, bytes: 2_000 },
]);
});
test("does not delete the '.en' sibling's files", async () => {
const multilingualTiny = "onnx-community/whisper-tiny";
const storage = mockCacheStorage({
"transformers-cache": {
[hubUrl(TINY, "onnx/encoder_model_quantized.onnx")]: 1_000,
},
});
expect(await deleteModelFromCache(multilingualTiny, storage)).toBe(false);
expect(await getModelStorageInfo([TINY], storage)).toEqual([
{ modelId: TINY, fileCount: 1, bytes: 1_000 },
]);
});
test("returns false when nothing matched or storage is unavailable", async () => {
const storage = mockCacheStorage({ "transformers-cache": {} });
expect(await deleteModelFromCache(TINY, storage)).toBe(false);
expect(await deleteModelFromCache(TINY, undefined)).toBe(false);
});
});