Spaces:
Sleeping
Sleeping
File size: 5,903 Bytes
21ab33c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | import type { FileHash, LoadProgress, RemoteFile } from "./types";
export const TOKENIZER_CACHE = "tokenize-it-assets-v2";
const LEGACY_TOKENIZER_CACHE = "tokenize-it-assets-v1";
const MAX_TOKENIZER_BYTES = 128 * 1024 * 1024;
const CACHE_KEY_PATH = "/.tokenize-it-cache/v2";
let legacyCacheDeletion: Promise<boolean> | null = null;
async function openTokenizerCache(): Promise<Cache> {
legacyCacheDeletion ??= caches.delete(LEGACY_TOKENIZER_CACHE);
await legacyCacheDeletion;
return caches.open(TOKENIZER_CACHE);
}
function cacheKey(hash: FileHash): string {
return new URL(
`${CACHE_KEY_PATH}/${hash.algorithm}/${hash.value}`,
self.location.origin,
).href;
}
function hex(bytes: ArrayBuffer): string {
return Array.from(new Uint8Array(bytes), (byte) =>
byte.toString(16).padStart(2, "0"),
).join("");
}
async function fileDigest(
bytes: Uint8Array<ArrayBuffer>,
hash: FileHash,
): Promise<string> {
if (hash.algorithm === "sha256") {
return hex(await crypto.subtle.digest("SHA-256", bytes));
}
const header = new TextEncoder().encode(`blob ${bytes.byteLength}\0`);
const gitBlob = new Uint8Array(header.byteLength + bytes.byteLength);
gitBlob.set(header);
gitBlob.set(bytes, header.byteLength);
return hex(await crypto.subtle.digest("SHA-1", gitBlob));
}
async function verifyFileHash(
bytes: Uint8Array<ArrayBuffer>,
hash: FileHash,
): Promise<void> {
if ((await fileDigest(bytes, hash)) !== hash.value) {
throw new Error("Hugging Face 返回的文件与预期哈希不一致。");
}
}
async function readResponse(
response: Response,
cached: boolean,
onProgress?: (progress: LoadProgress) => void,
): Promise<Uint8Array<ArrayBuffer>> {
const contentLength = Number(response.headers.get("Content-Length"));
const total = Number.isFinite(contentLength) && contentLength > 0 ? contentLength : null;
if (total !== null && total > MAX_TOKENIZER_BYTES) {
throw new Error("Tokenizer 文件超过 128 MB 的浏览器加载上限。");
}
const reader = response.body?.getReader();
if (!reader) {
const bytes = new Uint8Array(await response.arrayBuffer());
if (bytes.byteLength > MAX_TOKENIZER_BYTES) {
throw new Error("Tokenizer 文件超过 128 MB 的浏览器加载上限。");
}
return bytes;
}
const chunks: Uint8Array[] = [];
let loaded = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
loaded += value.byteLength;
if (loaded > MAX_TOKENIZER_BYTES) {
await reader.cancel();
throw new Error("Tokenizer 文件超过 128 MB 的浏览器加载上限。");
}
onProgress?.({
phase: "downloading",
loaded,
total,
cached,
message: cached ? "正在读取浏览器缓存" : "正在下载 Tokenizer",
});
}
const bytes = new Uint8Array(loaded);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return bytes;
}
async function fetchFile(
file: RemoteFile,
onProgress?: (progress: LoadProgress) => void,
): Promise<Uint8Array<ArrayBuffer>> {
const cache = await openTokenizerCache();
const key = cacheKey(file.hash);
const cachedResponse = await cache.match(key);
const response = cachedResponse ?? (await fetch(file.url, { mode: "cors" }));
if (!response.ok) {
throw new Error(`Tokenizer 文件下载失败(HTTP ${response.status})。`);
}
const bytes = await readResponse(response, Boolean(cachedResponse), onProgress);
if (cachedResponse) return bytes;
await verifyFileHash(bytes, file.hash);
const cacheResponse = new Response(bytes, {
headers: {
"Content-Length": String(bytes.byteLength),
"Content-Type": response.headers.get("Content-Type") ?? "application/octet-stream",
},
status: 200,
});
// Cache quota exhaustion must not make an otherwise valid tokenizer unusable.
void cache.put(key, cacheResponse).catch(() => undefined);
return bytes;
}
export async function fetchTokenizerAsset(
file: RemoteFile,
onProgress: (progress: LoadProgress) => void,
): Promise<Uint8Array> {
return fetchFile(file, onProgress);
}
export async function fetchOptionalJson(
file: RemoteFile | null,
): Promise<Record<string, unknown> | null> {
if (!file) return null;
try {
const bytes = await fetchFile(file);
const data = JSON.parse(new TextDecoder().decode(bytes)) as unknown;
return data && typeof data === "object"
? (data as Record<string, unknown>)
: null;
} catch {
return null;
}
}
async function cachedResponseSize(response: Response): Promise<number> {
const contentLengthHeader = response.headers.get("Content-Length");
const contentLength =
contentLengthHeader === null ? Number.NaN : Number(contentLengthHeader);
if (Number.isFinite(contentLength) && contentLength >= 0) return contentLength;
return (await response.blob()).size;
}
export async function tokenizerCacheSize(): Promise<number> {
const cacheNames = (await caches.keys()).filter(
(name) => name === TOKENIZER_CACHE || name === LEGACY_TOKENIZER_CACHE,
);
const sizes = await Promise.all(
cacheNames.map(async (name) => {
const cache = await caches.open(name);
const requests = await cache.keys();
const responses = await Promise.all(
requests.map((request) => cache.match(request)),
);
const responseSizes = await Promise.all(
responses.map((response) =>
response ? cachedResponseSize(response) : 0,
),
);
return responseSizes.reduce((total, size) => total + size, 0);
}),
);
return sizes.reduce((total, size) => total + size, 0);
}
export async function clearTokenizerCache(): Promise<void> {
await Promise.all([
caches.delete(TOKENIZER_CACHE),
caches.delete(LEGACY_TOKENIZER_CACHE),
]);
}
|