@agorapete/wllama 3.5.1-q2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitmodules +3 -0
- package/.prettierignore +38 -0
- package/AGENTS.md +1 -0
- package/CMakeLists.txt +131 -0
- package/LICENCE +21 -0
- package/README-dev.md +178 -0
- package/README.md +225 -0
- package/README_banner.png +0 -0
- package/assets/screenshot_0.png +0 -0
- package/cpp/generate_glue_prototype.js +115 -0
- package/cpp/glue.hpp +664 -0
- package/cpp/test_glue.cpp +80 -0
- package/cpp/wllama-context.h +1172 -0
- package/cpp/wllama-fs.h +148 -0
- package/cpp/wllama.cpp +187 -0
- package/cpp/wllama.h +6 -0
- package/esm/cache-manager.d.ts +130 -0
- package/esm/debug.d.ts +28 -0
- package/esm/glue/glue.d.ts +22 -0
- package/esm/glue/messages.d.ts +146 -0
- package/esm/huggingface.d.ts +31 -0
- package/esm/index.cjs +3406 -0
- package/esm/index.d.ts +8 -0
- package/esm/index.js +3387 -0
- package/esm/index.min.js +1 -0
- package/esm/index.min.js.map +1 -0
- package/esm/model-manager.d.ts +136 -0
- package/esm/storage/cos.d.ts +36 -0
- package/esm/storage/index.d.ts +33 -0
- package/esm/storage/opfs.d.ts +12 -0
- package/esm/types/oai-compat.d.ts +278 -0
- package/esm/types/types.d.ts +112 -0
- package/esm/utils.d.ts +119 -0
- package/esm/wasm/source-map.d.ts +1 -0
- package/esm/wasm/wllama.wasm +0 -0
- package/esm/wasm-from-cdn.d.ts +8 -0
- package/esm/wllama.d.ts +397 -0
- package/esm/worker.d.ts +92 -0
- package/esm/workers-code/generated.d.ts +4 -0
- package/guides/intro-v2.md +132 -0
- package/guides/intro-v3.1.md +40 -0
- package/guides/intro-v3.md +230 -0
- package/index.ts +1 -0
- package/package.json +71 -0
- package/scripts/bisect_test.sh +33 -0
- package/scripts/build_hf_space.sh +26 -0
- package/scripts/build_source_map.js +269 -0
- package/scripts/build_wasm.sh +19 -0
- package/scripts/build_worker.sh +38 -0
- package/scripts/check_debug_build.js +30 -0
- package/scripts/check_package_size.js +25 -0
- package/scripts/docker-compose.yml +76 -0
- package/scripts/generate_wasm_from_cdn.js +24 -0
- package/scripts/http_server.js +44 -0
- package/scripts/post_build.sh +32 -0
- package/src/cache-manager.ts +358 -0
- package/src/debug.ts +111 -0
- package/src/glue/glue.ts +291 -0
- package/src/glue/messages.ts +773 -0
- package/src/huggingface.ts +151 -0
- package/src/index.ts +8 -0
- package/src/mjs.test.ts +44 -0
- package/src/model-manager.test.ts +200 -0
- package/src/model-manager.ts +359 -0
- package/src/storage/cos.test.ts +83 -0
- package/src/storage/cos.ts +171 -0
- package/src/storage/index.ts +40 -0
- package/src/storage/opfs.ts +119 -0
- package/src/types/oai-compat.ts +342 -0
- package/src/types/types.ts +133 -0
- package/src/utils.test.ts +231 -0
- package/src/utils.ts +403 -0
- package/src/wasm/source-map.ts +7 -0
- package/src/wasm/wllama.js +1 -0
- package/src/wasm/wllama.wasm +0 -0
- package/src/wasm-from-cdn.ts +13 -0
- package/src/wllama.test.ts +392 -0
- package/src/wllama.ts +1138 -0
- package/src/wllama.wgpu.test.ts +62 -0
- package/src/worker.ts +443 -0
- package/src/workers-code/generated.ts +11 -0
- package/src/workers-code/llama-cpp.js +511 -0
- package/src/workers-code/opfs-utils.js +150 -0
- package/tsconfig.build.json +34 -0
- package/tsup.config.ts +23 -0
- package/vitest.config.ts +61 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { expect, test, describe } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
joinBuffers,
|
|
4
|
+
bufToText,
|
|
5
|
+
padDigits,
|
|
6
|
+
sumArr,
|
|
7
|
+
isString,
|
|
8
|
+
delay,
|
|
9
|
+
absoluteUrl,
|
|
10
|
+
parseShardNumber,
|
|
11
|
+
parseModelUrl,
|
|
12
|
+
sortFileByShard,
|
|
13
|
+
isValidGgufFile,
|
|
14
|
+
} from './utils';
|
|
15
|
+
|
|
16
|
+
describe('joinBuffers', () => {
|
|
17
|
+
test('joins multiple buffers correctly', () => {
|
|
18
|
+
const buf1 = new Uint8Array([1, 2, 3]);
|
|
19
|
+
const buf2 = new Uint8Array([4, 5]);
|
|
20
|
+
|
|
21
|
+
const result = joinBuffers([buf1, buf2]);
|
|
22
|
+
|
|
23
|
+
expect(result).toEqual(new Uint8Array([1, 2, 3, 4, 5]));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('bufToText', () => {
|
|
28
|
+
test('converts buffer to string', () => {
|
|
29
|
+
const buffer = new TextEncoder().encode('hello world');
|
|
30
|
+
expect(bufToText(buffer)).toBe('hello world');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('padDigits', () => {
|
|
35
|
+
test('pads number with zeros', () => {
|
|
36
|
+
expect(padDigits(5, 3)).toBe('005');
|
|
37
|
+
expect(padDigits(42, 4)).toBe('0042');
|
|
38
|
+
expect(padDigits(1234, 2)).toBe('1234');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('sumArr', () => {
|
|
43
|
+
test('sums array of numbers', () => {
|
|
44
|
+
expect(sumArr([1, 2, 3])).toBe(6);
|
|
45
|
+
expect(sumArr([])).toBe(0);
|
|
46
|
+
expect(sumArr([5])).toBe(5);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('isString', () => {
|
|
51
|
+
test('checks if value is string', () => {
|
|
52
|
+
expect(isString('test')).toBe(true);
|
|
53
|
+
expect(isString('')).toBe(true);
|
|
54
|
+
expect(isString(null)).toBe(false);
|
|
55
|
+
expect(isString(undefined)).toBe(false);
|
|
56
|
+
expect(isString(123)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('delay', () => {
|
|
61
|
+
test('delays execution', async () => {
|
|
62
|
+
const start = Date.now();
|
|
63
|
+
await delay(100);
|
|
64
|
+
const elapsed = Date.now() - start;
|
|
65
|
+
expect(elapsed).toBeGreaterThanOrEqual(100);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('absoluteUrl', () => {
|
|
70
|
+
test('converts relative path to absolute url', () => {
|
|
71
|
+
// Mock document.baseURI
|
|
72
|
+
Object.defineProperty(document, 'baseURI', {
|
|
73
|
+
value: 'http://example.com/app/',
|
|
74
|
+
writable: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(absoluteUrl('test.html')).toBe('http://example.com/app/test.html');
|
|
78
|
+
expect(absoluteUrl('/test.html')).toBe('http://example.com/test.html');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('shard processing', () => {
|
|
83
|
+
test('parseShardNumber extracts correct info', () => {
|
|
84
|
+
expect(parseShardNumber('abcdef-123456-00001-of-00005.gguf')).toEqual({
|
|
85
|
+
baseURL: 'abcdef-123456',
|
|
86
|
+
current: 1,
|
|
87
|
+
total: 5,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(parseShardNumber('abcdef-123456.9090-q8_0.gguf')).toEqual({
|
|
91
|
+
baseURL: 'abcdef-123456.9090-q8_0.gguf',
|
|
92
|
+
current: 1,
|
|
93
|
+
total: 1,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(
|
|
97
|
+
parseShardNumber('abcdef-123456-00001-of-00005.gguf?param=value')
|
|
98
|
+
).toEqual({
|
|
99
|
+
baseURL: 'abcdef-123456',
|
|
100
|
+
current: 1,
|
|
101
|
+
total: 5,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(
|
|
105
|
+
parseShardNumber('abcdef-123456-00001-of-00005.gguf?no-inline')
|
|
106
|
+
).toEqual({
|
|
107
|
+
baseURL: 'abcdef-123456',
|
|
108
|
+
current: 1,
|
|
109
|
+
total: 5,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(
|
|
113
|
+
parseShardNumber(
|
|
114
|
+
'abcdef-123456-00001-of-00005.gguf?param1=value1¶m2=value2'
|
|
115
|
+
)
|
|
116
|
+
).toEqual({
|
|
117
|
+
baseURL: 'abcdef-123456',
|
|
118
|
+
current: 1,
|
|
119
|
+
total: 5,
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('parseModelUrl generates correct shard URLs', () => {
|
|
124
|
+
const singleFile = 'model.gguf';
|
|
125
|
+
expect(parseModelUrl(singleFile)).toEqual(['model.gguf']);
|
|
126
|
+
|
|
127
|
+
const shardedFile = 'model-00001-of-00003.gguf';
|
|
128
|
+
expect(parseModelUrl(shardedFile)).toEqual([
|
|
129
|
+
'model-00001-of-00003.gguf',
|
|
130
|
+
'model-00002-of-00003.gguf',
|
|
131
|
+
'model-00003-of-00003.gguf',
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
const complexPath = 'https://example.com/models/llama-00001-of-00002.gguf';
|
|
135
|
+
expect(parseModelUrl(complexPath)).toEqual([
|
|
136
|
+
'https://example.com/models/llama-00001-of-00002.gguf',
|
|
137
|
+
'https://example.com/models/llama-00002-of-00002.gguf',
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
const shardedFileWithQuery = 'model-00001-of-00003.gguf?param=value';
|
|
141
|
+
expect(parseModelUrl(shardedFileWithQuery)).toEqual([
|
|
142
|
+
'model-00001-of-00003.gguf?param=value',
|
|
143
|
+
'model-00002-of-00003.gguf?param=value',
|
|
144
|
+
'model-00003-of-00003.gguf?param=value',
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
const complexPathWithQuery =
|
|
148
|
+
'https://example.com/models/llama-00001-of-00002.gguf?no-inline';
|
|
149
|
+
expect(parseModelUrl(complexPathWithQuery)).toEqual([
|
|
150
|
+
'https://example.com/models/llama-00001-of-00002.gguf?no-inline',
|
|
151
|
+
'https://example.com/models/llama-00002-of-00002.gguf?no-inline',
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('sortFileByShard sorts files by shard number', () => {
|
|
156
|
+
const files = [
|
|
157
|
+
new File(
|
|
158
|
+
[],
|
|
159
|
+
'e2fc714c4727ee9395f324cd2e7f331f-model-00003-of-00005.gguf'
|
|
160
|
+
),
|
|
161
|
+
new File(
|
|
162
|
+
[],
|
|
163
|
+
'187ef4436122d1cc2f40dc2b92f0eba0-model-00001-of-00005.gguf'
|
|
164
|
+
),
|
|
165
|
+
new File(
|
|
166
|
+
[],
|
|
167
|
+
'c4357687ea2b461cb07cf0a0a3de939f-model-00002-of-00005.gguf'
|
|
168
|
+
),
|
|
169
|
+
new File(
|
|
170
|
+
[],
|
|
171
|
+
'6a4d40512eabd63221cbdf3df4636cd7-model-00005-of-00005.gguf'
|
|
172
|
+
),
|
|
173
|
+
new File(
|
|
174
|
+
[],
|
|
175
|
+
'0952e4c6ba320f5278605eb5333eec0f-model-00004-of-00005.gguf'
|
|
176
|
+
),
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
sortFileByShard(files);
|
|
180
|
+
|
|
181
|
+
expect(files.map((f) => parseShardNumber(f.name).current)).toEqual([
|
|
182
|
+
1, 2, 3, 4, 5,
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
// Single file should not be affected
|
|
186
|
+
const singleFile = [new File([], 'model.gguf')];
|
|
187
|
+
sortFileByShard(singleFile);
|
|
188
|
+
expect(singleFile[0].name).toBe('model.gguf');
|
|
189
|
+
|
|
190
|
+
// Regular blobs should not be affected
|
|
191
|
+
const blobs = [new Blob(), new Blob()];
|
|
192
|
+
sortFileByShard(blobs);
|
|
193
|
+
expect(blobs.length).toBe(2);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('GGUF file validation', () => {
|
|
198
|
+
test('isValidGgufFile should correctly validate GGUF files', () => {
|
|
199
|
+
// Basic valid cases
|
|
200
|
+
expect(isValidGgufFile('model.gguf')).toBe(true);
|
|
201
|
+
expect(isValidGgufFile('path/to/model.gguf')).toBe(true);
|
|
202
|
+
|
|
203
|
+
// With Vite query parameters
|
|
204
|
+
expect(isValidGgufFile('model.gguf?no-inline')).toBe(true);
|
|
205
|
+
expect(isValidGgufFile('foo.gguf?no-inline')).toBe(true);
|
|
206
|
+
|
|
207
|
+
// With query parameters
|
|
208
|
+
expect(isValidGgufFile('model.gguf?param=value')).toBe(true);
|
|
209
|
+
expect(isValidGgufFile('model.gguf?param1¶m2')).toBe(true);
|
|
210
|
+
expect(isValidGgufFile('model.gguf?no-inline&v=123')).toBe(true);
|
|
211
|
+
expect(isValidGgufFile('model.gguf?param1=value1¶m2=value2')).toBe(
|
|
212
|
+
true
|
|
213
|
+
);
|
|
214
|
+
expect(isValidGgufFile('model.gguf?param=value&special=!@#$%^&*()')).toBe(
|
|
215
|
+
true
|
|
216
|
+
);
|
|
217
|
+
expect(isValidGgufFile('model.gguf?')).toBe(true);
|
|
218
|
+
|
|
219
|
+
// With fragments
|
|
220
|
+
expect(isValidGgufFile('model.gguf?param=value#fragment')).toBe(true);
|
|
221
|
+
|
|
222
|
+
// Invalid cases
|
|
223
|
+
expect(isValidGgufFile('model.bin')).toBe(false);
|
|
224
|
+
expect(isValidGgufFile('model.gguf.bin')).toBe(false);
|
|
225
|
+
expect(isValidGgufFile('modelgguf')).toBe(false);
|
|
226
|
+
expect(isValidGgufFile('path/to/model.txt?ext=gguf')).toBe(false);
|
|
227
|
+
|
|
228
|
+
// Case sensitivity
|
|
229
|
+
expect(isValidGgufFile('model.GGUF?param=value')).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
});
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
export const joinBuffers = (buffers: Uint8Array[]): Uint8Array => {
|
|
2
|
+
const totalSize = buffers.reduce((acc, buf) => acc + buf.length, 0);
|
|
3
|
+
const output = new Uint8Array(totalSize);
|
|
4
|
+
output.set(buffers[0], 0);
|
|
5
|
+
for (let i = 1; i < buffers.length; i++) {
|
|
6
|
+
output.set(buffers[i], buffers[i - 1].length);
|
|
7
|
+
}
|
|
8
|
+
return output;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const textDecoder = new TextDecoder();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Convert list of bytes (number) to text
|
|
15
|
+
* @param buffer
|
|
16
|
+
* @returns a string
|
|
17
|
+
*/
|
|
18
|
+
export const bufToText = (buffer: ArrayBuffer | Uint8Array): string => {
|
|
19
|
+
return textDecoder.decode(buffer);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get default stdout/stderr config for wasm module
|
|
24
|
+
*/
|
|
25
|
+
export const getWModuleConfig = (pathConfig: {
|
|
26
|
+
[filename: string]: string;
|
|
27
|
+
}) => {
|
|
28
|
+
return {
|
|
29
|
+
noInitialRun: true,
|
|
30
|
+
print: function (text: any) {
|
|
31
|
+
if (arguments.length > 1)
|
|
32
|
+
text = Array.prototype.slice.call(arguments).join(' ');
|
|
33
|
+
console.log(text);
|
|
34
|
+
},
|
|
35
|
+
printErr: function (text: any) {
|
|
36
|
+
if (arguments.length > 1)
|
|
37
|
+
text = Array.prototype.slice.call(arguments).join(' ');
|
|
38
|
+
console.warn(text);
|
|
39
|
+
},
|
|
40
|
+
// @ts-ignore
|
|
41
|
+
locateFile: function (filename: string, basePath: string) {
|
|
42
|
+
const p = pathConfig[filename];
|
|
43
|
+
console.log(`Loading "${filename}" from "${p}"`);
|
|
44
|
+
return p;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export interface ShardInfo {
|
|
50
|
+
baseURL: string;
|
|
51
|
+
current: number;
|
|
52
|
+
total: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const URL_PARTS_REGEX = /-(\d{5})-of-(\d{5})\.gguf(?:\?.*)?$/;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse shard number and total from a file name or URL
|
|
59
|
+
*/
|
|
60
|
+
export const parseShardNumber = (fnameOrUrl: string): ShardInfo => {
|
|
61
|
+
const matches = fnameOrUrl.match(URL_PARTS_REGEX);
|
|
62
|
+
if (!matches) {
|
|
63
|
+
return {
|
|
64
|
+
baseURL: fnameOrUrl,
|
|
65
|
+
current: 1,
|
|
66
|
+
total: 1,
|
|
67
|
+
};
|
|
68
|
+
} else {
|
|
69
|
+
return {
|
|
70
|
+
baseURL: fnameOrUrl.replace(URL_PARTS_REGEX, ''),
|
|
71
|
+
current: parseInt(matches[1]),
|
|
72
|
+
total: parseInt(matches[2]),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parses a model URL and returns an array of URLs based on the following patterns:
|
|
79
|
+
* - If the input URL is an array, it returns the array itself.
|
|
80
|
+
* - If the input URL is a string in the `gguf-split` format, it returns an array containing the URL of each shard in ascending order.
|
|
81
|
+
* - Otherwise, it returns an array containing the input URL as a single element array.
|
|
82
|
+
* @param modelUrl URL or list of URLs
|
|
83
|
+
*/
|
|
84
|
+
export const parseModelUrl = (modelUrl: string): string[] => {
|
|
85
|
+
const { baseURL, current, total } = parseShardNumber(modelUrl);
|
|
86
|
+
if (current == total && total == 1) {
|
|
87
|
+
return [modelUrl];
|
|
88
|
+
} else {
|
|
89
|
+
const queryMatch = modelUrl.match(/\.gguf(\?.*)?$/);
|
|
90
|
+
const queryParams = queryMatch?.[1] ?? '';
|
|
91
|
+
const paddedShardIds = Array.from({ length: total }, (_, index) =>
|
|
92
|
+
(index + 1).toString().padStart(5, '0')
|
|
93
|
+
);
|
|
94
|
+
return paddedShardIds.map(
|
|
95
|
+
(current) =>
|
|
96
|
+
`${baseURL}-${current}-of-${total.toString().padStart(5, '0')}.gguf${queryParams}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if the given blobs are files or not, then sort them by shard number
|
|
103
|
+
*/
|
|
104
|
+
export const sortFileByShard = (blobs: Blob[]): void => {
|
|
105
|
+
const isFiles = blobs.every((b) => !!(b as File).name);
|
|
106
|
+
if (isFiles && blobs.length > 1) {
|
|
107
|
+
const files = blobs as File[];
|
|
108
|
+
files.sort((a, b) => {
|
|
109
|
+
const infoA = parseShardNumber(a.name);
|
|
110
|
+
const infoB = parseShardNumber(b.name);
|
|
111
|
+
return infoA.current - infoB.current;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const isMmproj = async (blob: Blob): Promise<boolean> => {
|
|
117
|
+
const META_NAME = 'general.architecture';
|
|
118
|
+
const META_VAL = 'clip';
|
|
119
|
+
const tmp = blob.slice(0, 128 * 1024);
|
|
120
|
+
const header = await tmp.arrayBuffer();
|
|
121
|
+
|
|
122
|
+
const buf = new Uint8Array(header);
|
|
123
|
+
const nameBytes = new TextEncoder().encode(META_NAME);
|
|
124
|
+
const valBytes = new TextEncoder().encode(META_VAL);
|
|
125
|
+
|
|
126
|
+
// Find offset of META_NAME in buffer
|
|
127
|
+
let offset = -1;
|
|
128
|
+
outer: for (let i = 0; i <= buf.length - nameBytes.length; i++) {
|
|
129
|
+
for (let j = 0; j < nameBytes.length; j++) {
|
|
130
|
+
if (buf[i + j] !== nameBytes[j]) continue outer;
|
|
131
|
+
}
|
|
132
|
+
offset = i;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
if (offset === -1) return false;
|
|
136
|
+
|
|
137
|
+
// Read valLen as uint64 at offset+8*3 (little-endian, read low 32 bits)
|
|
138
|
+
if (offset + 8 * 4 + 4 > buf.length) return false;
|
|
139
|
+
const view = new DataView(header);
|
|
140
|
+
const valLen = view.getBigUint64(offset + 8 * 3, true);
|
|
141
|
+
if (valLen !== 4n) return false;
|
|
142
|
+
|
|
143
|
+
// Read 4 bytes at offset+8*4, compare with META_VAL bytes
|
|
144
|
+
for (let i = 0; i < valBytes.length; i++) {
|
|
145
|
+
if (buf[offset + 8 * 4 + i] !== valBytes[i]) return false;
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
151
|
+
|
|
152
|
+
export const absoluteUrl = (relativePath: string) =>
|
|
153
|
+
new URL(relativePath, document.baseURI).href;
|
|
154
|
+
|
|
155
|
+
export const padDigits = (number: number, digits: number) => {
|
|
156
|
+
return (
|
|
157
|
+
Array(Math.max(digits - String(number).length + 1, 0)).join('0') + number
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const sumArr = (arr: number[]) =>
|
|
162
|
+
arr.reduce((prev, curr) => prev + curr, 0);
|
|
163
|
+
|
|
164
|
+
export const isString = (value: any): boolean => !!value?.startsWith;
|
|
165
|
+
|
|
166
|
+
export const MMPROJ_FILE_NAME = 'mmproj.gguf';
|
|
167
|
+
|
|
168
|
+
type ModelShard = { blob: Blob; name: string };
|
|
169
|
+
export const prepareBlobs = async (
|
|
170
|
+
blobsInp: Blob[]
|
|
171
|
+
): Promise<{
|
|
172
|
+
llm: ModelShard[];
|
|
173
|
+
mmproj: ModelShard | null;
|
|
174
|
+
all: ModelShard[];
|
|
175
|
+
}> => {
|
|
176
|
+
const blobs: Blob[] = [];
|
|
177
|
+
let blobMmproj: Blob | null = null;
|
|
178
|
+
|
|
179
|
+
for (const blob of blobsInp) {
|
|
180
|
+
if (await isMmproj(blob)) {
|
|
181
|
+
blobMmproj = blob;
|
|
182
|
+
} else {
|
|
183
|
+
blobs.push(blob);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// prepare model-XXXXX-of-XXXXX.gguf blobs
|
|
188
|
+
sortFileByShard(blobs);
|
|
189
|
+
const result = blobs.map((blob, i) => ({
|
|
190
|
+
blob,
|
|
191
|
+
name: `model-${padDigits(i + 1, 5)}-of-${padDigits(blobs.length, 5)}.gguf`,
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
// prepare mmproj.gguf blob
|
|
195
|
+
if (blobMmproj) {
|
|
196
|
+
result.push({
|
|
197
|
+
blob: blobMmproj,
|
|
198
|
+
name: MMPROJ_FILE_NAME,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
llm: result.filter((f) => f.name !== MMPROJ_FILE_NAME),
|
|
204
|
+
mmproj: blobMmproj ? { blob: blobMmproj, name: MMPROJ_FILE_NAME } : null,
|
|
205
|
+
all: result,
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Browser feature detection
|
|
211
|
+
* Copied from https://unpkg.com/wasm-feature-detect?module (Apache License)
|
|
212
|
+
*/
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @returns true if browser support multi-threads
|
|
216
|
+
*/
|
|
217
|
+
export const isSupportMultiThread = () =>
|
|
218
|
+
(async (e) => {
|
|
219
|
+
try {
|
|
220
|
+
return (
|
|
221
|
+
'undefined' != typeof MessageChannel &&
|
|
222
|
+
new MessageChannel().port1.postMessage(new SharedArrayBuffer(1)),
|
|
223
|
+
WebAssembly.validate(e)
|
|
224
|
+
);
|
|
225
|
+
} catch (e) {
|
|
226
|
+
return !1;
|
|
227
|
+
}
|
|
228
|
+
})(
|
|
229
|
+
new Uint8Array([
|
|
230
|
+
0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 5, 4, 1, 3, 1,
|
|
231
|
+
1, 10, 11, 1, 9, 0, 65, 0, 254, 16, 2, 0, 26, 11,
|
|
232
|
+
])
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @returns true if browser support wasm "native" exception handler
|
|
237
|
+
*/
|
|
238
|
+
const isSupportExceptions = async () =>
|
|
239
|
+
WebAssembly.validate(
|
|
240
|
+
new Uint8Array([
|
|
241
|
+
0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 10, 8, 1, 6,
|
|
242
|
+
0, 6, 64, 25, 11, 11,
|
|
243
|
+
])
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @returns true if browser support wasm SIMD
|
|
248
|
+
*/
|
|
249
|
+
const isSupportSIMD = async () =>
|
|
250
|
+
WebAssembly.validate(
|
|
251
|
+
new Uint8Array([
|
|
252
|
+
0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, 10, 10,
|
|
253
|
+
1, 8, 0, 65, 0, 253, 15, 253, 98, 11,
|
|
254
|
+
])
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* @returns true if browser support JSPI
|
|
259
|
+
*/
|
|
260
|
+
export const isSupportJSPI = () => {
|
|
261
|
+
return !!(WebAssembly as any).Suspending;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @returns true if brower support WebGPU. Note: for browser without JSPI support, compat mode will be used.
|
|
266
|
+
*/
|
|
267
|
+
export const isSupportWebGPU = () => {
|
|
268
|
+
return !!(navigator as any).gpu;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* @returns true if browser support WASM Memory64
|
|
273
|
+
*/
|
|
274
|
+
export const isSupportMem64 = (): boolean => {
|
|
275
|
+
try {
|
|
276
|
+
new WebAssembly.Memory({
|
|
277
|
+
address: 'i64',
|
|
278
|
+
initial: 1n, // 1 page (64 KiB)
|
|
279
|
+
} as any);
|
|
280
|
+
return true;
|
|
281
|
+
} catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Throws an error if the environment is not compatible
|
|
288
|
+
*/
|
|
289
|
+
export const checkEnvironmentCompatible = async (): Promise<void> => {
|
|
290
|
+
if (!(await isSupportExceptions())) {
|
|
291
|
+
throw new Error('WebAssembly runtime does not support exception handling');
|
|
292
|
+
}
|
|
293
|
+
if (!(await isSupportSIMD())) {
|
|
294
|
+
throw new Error('WebAssembly runtime does not support SIMD');
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Check if browser is Safari
|
|
300
|
+
* Source: https://github.com/DamonOehlman/detect-browser/blob/master/src/index.ts
|
|
301
|
+
*/
|
|
302
|
+
export const isSafari = (): boolean => {
|
|
303
|
+
return (
|
|
304
|
+
isSafariMobile() ||
|
|
305
|
+
!!navigator.userAgent.match(/Version\/([0-9\._]+).*Safari/)
|
|
306
|
+
); // safari
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Check if browser is Firefox
|
|
311
|
+
*/
|
|
312
|
+
export const isFirefox = (): boolean => {
|
|
313
|
+
return !!navigator.userAgent.match(/Firefox\/([0-9\.]+)(?:\s|$)/);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Regular expression to validate GGUF file paths/URLs
|
|
318
|
+
* Matches paths ending with .gguf and optional query parameters
|
|
319
|
+
*/
|
|
320
|
+
export const GGUF_FILE_REGEX = /^.*\.gguf(?:\?.*)?$/;
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Validates if a given string is a valid GGUF file path/URL
|
|
324
|
+
* @param path The file path or URL to validate
|
|
325
|
+
* @returns true if the path is a valid GGUF file path/URL
|
|
326
|
+
*/
|
|
327
|
+
export const isValidGgufFile = (path: string): boolean => {
|
|
328
|
+
return GGUF_FILE_REGEX.test(path);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Check if browser is Safari iOS / iPad / iPhone
|
|
333
|
+
* Source: https://github.com/DamonOehlman/detect-browser/blob/master/src/index.ts
|
|
334
|
+
*/
|
|
335
|
+
export const isSafariMobile = (): boolean => {
|
|
336
|
+
return !!navigator.userAgent.match(/Version\/([0-9\._]+).*Mobile.*Safari.*/); // ios
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Create a worker from a string
|
|
341
|
+
*/
|
|
342
|
+
export const createWorker = (workerCode: string | Blob): Worker => {
|
|
343
|
+
const workerURL = URL.createObjectURL(
|
|
344
|
+
isString(workerCode)
|
|
345
|
+
? new Blob([workerCode], { type: 'text/javascript' })
|
|
346
|
+
: (workerCode as Blob)
|
|
347
|
+
);
|
|
348
|
+
return new Worker(workerURL, { type: 'module' });
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Convert callback to async iterator
|
|
353
|
+
*/
|
|
354
|
+
export const cbToAsyncIter =
|
|
355
|
+
<A extends any[], T>(
|
|
356
|
+
fn: (
|
|
357
|
+
...args: [
|
|
358
|
+
...args: A,
|
|
359
|
+
callback: (val?: T, done?: boolean, err?: Error) => void,
|
|
360
|
+
]
|
|
361
|
+
) => void
|
|
362
|
+
) =>
|
|
363
|
+
(...args: A): AsyncIterable<T> => {
|
|
364
|
+
let values: Promise<[T, boolean]>[] = [];
|
|
365
|
+
let resolve: (x: [T, boolean]) => void;
|
|
366
|
+
let reject: (e: Error) => void;
|
|
367
|
+
values.push(
|
|
368
|
+
new Promise((res, rej) => {
|
|
369
|
+
resolve = res;
|
|
370
|
+
reject = rej;
|
|
371
|
+
})
|
|
372
|
+
);
|
|
373
|
+
fn(...args, (val?: T, done?: boolean, err?: Error) => {
|
|
374
|
+
if (err) {
|
|
375
|
+
reject(err);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
resolve([val!, done!]);
|
|
379
|
+
values.push(
|
|
380
|
+
new Promise((res, rej) => {
|
|
381
|
+
resolve = res;
|
|
382
|
+
reject = rej;
|
|
383
|
+
})
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
return (async function* () {
|
|
387
|
+
let val: T;
|
|
388
|
+
for (let i = 0, done = false; !done; i++) {
|
|
389
|
+
[val, done] = await values[i];
|
|
390
|
+
delete values[i];
|
|
391
|
+
if (val !== undefined) yield val;
|
|
392
|
+
}
|
|
393
|
+
})();
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Check if we can use async file read, where the wasm env can asynchronously read a Blob.
|
|
398
|
+
* Please refer to README-dev.md for more details.
|
|
399
|
+
*/
|
|
400
|
+
export const canUseAsyncFileRead = (compat: boolean) =>
|
|
401
|
+
isSupportJSPI() || compat;
|
|
402
|
+
|
|
403
|
+
export const needCompat = () => !isSupportJSPI() || !isSupportMem64();
|