@browserless/ai 13.9.3
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/LICENSE.md +21 -0
- package/README.md +128 -0
- package/examples/index.js +21 -0
- package/package.json +68 -0
- package/scripts/docker-ci.sh +35 -0
- package/scripts/install-model.js +21 -0
- package/scripts/pack-model.js +172 -0
- package/scripts/probe.js +33 -0
- package/scripts/util.js +274 -0
- package/src/find-dir.js +28 -0
- package/src/index.js +313 -0
- package/src/run-ai.js +151 -0
- package/src/unpack.js +145 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright © 2019 Microlink <hello@microlink.io> (microlink.io)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img style="width: 500px; margin:3rem 0 1.5rem;" src="https://github.com/microlinkhq/browserless/raw/master/static/logo-banner.png#gh-light-mode-only" alt="browserless">
|
|
3
|
+
<img style="width: 500px; margin:3rem 0 1.5rem;" src="https://github.com/microlinkhq/browserless/raw/master/static/logo-banner-light.png#gh-dark-mode-only" alt="browserless">
|
|
4
|
+
<br><br>
|
|
5
|
+
<a href="https://microlink.io"><img src="https://img.shields.io/badge/powered_by-microlink.io-blue?style=flat-square&color=%23EA407B" alt="Powered by microlink.io"></a>
|
|
6
|
+
<img src="https://img.shields.io/github/tag/microlinkhq/browserless.svg?style=flat-square" alt="Last version">
|
|
7
|
+
<a href="https://coveralls.io/github/microlinkhq/browserless"><img src="https://img.shields.io/coveralls/microlinkhq/browserless.svg?style=flat-square" alt="Coverage Status"></a>
|
|
8
|
+
<a href="https://www.npmjs.org/package/browserless"><img src="https://img.shields.io/npm/dm/browserless.svg?style=flat-square" alt="NPM Status"></a>
|
|
9
|
+
<br><br>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
> @browserless/ai: Run Chrome Built-in AI from Node.js.
|
|
13
|
+
|
|
14
|
+
See the [ai section](https://browserless.js.org/#/?id=ai) on our website for more information.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
Using npm:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install @browserless/ai --save
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires Node.js 24+ and a [browserless](https://www.npmjs.com/package/browserless) peer.
|
|
25
|
+
|
|
26
|
+
## About
|
|
27
|
+
|
|
28
|
+
This package runs [Chrome Built-in AI](https://developer.chrome.com/docs/ai/built-in-apis) from Node.js. It evaluates Prompt, Summarizer, Translator, and Language Detector on a browserless page, so you can call those APIs from a script instead of a visible Chrome window.
|
|
29
|
+
|
|
30
|
+
Chrome for Testing does not download Gemini Nano for you. Pack a model on a machine that already has it, unpack it into one directory, then pass that `dir` to `createAi` / `launch`.
|
|
31
|
+
|
|
32
|
+
Chrome’s docs allow the foundation model on **GPU (>4 GB VRAM) or CPU (16 GB RAM, 4+ cores)** at runtime. The 22 GB profile-volume space and unmetered connection apply only when desktop Chrome first downloads Nano, not when you launch with a packed `dir`.
|
|
33
|
+
|
|
34
|
+
### Usage
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
const createAi = require('@browserless/ai')
|
|
38
|
+
|
|
39
|
+
const { dir } = await createAi.unpack(createAi.download)
|
|
40
|
+
const ai = createAi({ dir })
|
|
41
|
+
|
|
42
|
+
await ai.capabilities()
|
|
43
|
+
await ai.detectLanguage('https://example.com', { text: 'Hello, how are you today?' })
|
|
44
|
+
await ai.summarize('https://example.com', { type: 'tldr' })
|
|
45
|
+
await ai.prompt('https://example.com', { prompt: 'What is this page about?' })
|
|
46
|
+
const schema = { type: 'object', properties: { title: { type: 'string' } } }
|
|
47
|
+
await ai.extract('https://example.com', { schema })
|
|
48
|
+
await ai.translate('https://example.com', { text: 'Hello', sourceLanguage: 'en', targetLanguage: 'es' })
|
|
49
|
+
await ai.close()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`unpack` accepts a local zip path or a download function. `createAi.download` is a signed R2 GetObject (`R2_ENDPOINT`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`). The bucket and object key can live in `R2_ENDPOINT` (`https://<account>.r2.cloudflarestorage.com/<bucket>/<key>`), or in `R2_BUCKET` / `R2_KEY`. The function can return a path, `Buffer`, stream, or S3 `GetObject` result, or write to the `dest` path it receives. If the directory is already unpacked, `unpack` reuses it unless you pass `{ force: true }`.
|
|
53
|
+
|
|
54
|
+
If you already have a browserless instance:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
const createBrowser = require('browserless')
|
|
58
|
+
const createAi = require('@browserless/ai')
|
|
59
|
+
|
|
60
|
+
const browser = createBrowser(createAi.launch({ dir: process.env.BROWSERLESS_AI_DIR }))
|
|
61
|
+
const ai = createAi(async teardown => {
|
|
62
|
+
const browserless = await browser.createContext()
|
|
63
|
+
teardown(() => browserless.destroyContext())
|
|
64
|
+
return browserless
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Each page-processing method is `(url, options)`. Pass `options.text` to use your own input; otherwise the package reads `document.body.innerText` after navigation. `capabilities` accepts `{ timeout, url }` and supplies defaults.
|
|
69
|
+
|
|
70
|
+
| Method | Create options |
|
|
71
|
+
|--------|----------------|
|
|
72
|
+
| `prompt` / `extract` | `temperature`, `topK`, `initialPrompts`, `expectedInputs`, `expectedOutputs` |
|
|
73
|
+
| `summarize` | `type`, `format`, `length`, `sharedContext`, `expectedInputLanguages`, `outputLanguage`, `expectedContextLanguages` |
|
|
74
|
+
| `translate` | `sourceLanguage`, `targetLanguage` (required) |
|
|
75
|
+
| `detectLanguage` | `expectedInputLanguages` |
|
|
76
|
+
|
|
77
|
+
### Options
|
|
78
|
+
|
|
79
|
+
| Option | Type | Default | Description |
|
|
80
|
+
|--------|------|---------|-------------|
|
|
81
|
+
| `dir` | `string` | `BROWSERLESS_AI_DIR` | Unpacked model tree (`nano/`, `prompt/`, `summarize/`, `detect/`) |
|
|
82
|
+
| `timeout` | `number` | `300000` | Launch and evaluate timeout (ms). `protocolTimeout` follows it |
|
|
83
|
+
|
|
84
|
+
`unpack(source, { dir, force })` writes to `dir` when you pass one. Otherwise it uses `BROWSERLESS_AI_DIR`, or `~/.cache/browserless-ai`.
|
|
85
|
+
|
|
86
|
+
### Pack a model
|
|
87
|
+
|
|
88
|
+
From a machine that already has Nano (desktop Chrome, or `BROWSERLESS_AI_DIR`):
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
pnpm --filter @browserless/ai pack-model
|
|
92
|
+
pnpm --filter @browserless/ai pack-model -- --upload
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Writes `/tmp/browserless-ai-nano.zip`. `--upload` also pushes it to R2 (`R2_ENDPOINT`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`; bucket/key may be in the endpoint URL). Tests run `install-model` first and download that object when the same variables are set.
|
|
96
|
+
|
|
97
|
+
### Example
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
pnpm --filter @browserless/ai start
|
|
101
|
+
pnpm --filter @browserless/ai start https://example.com
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Methods
|
|
105
|
+
|
|
106
|
+
| Method | Chrome API | Chrome |
|
|
107
|
+
|--------|------------|--------|
|
|
108
|
+
| `prompt` | `LanguageModel` | 148 |
|
|
109
|
+
| `extract` | `LanguageModel` + `schema` / `responseConstraint` | 148 |
|
|
110
|
+
| `summarize` | `Summarizer` | 138 |
|
|
111
|
+
| `detectLanguage` | `LanguageDetector` | 138 |
|
|
112
|
+
| `translate` | `Translator` | 138 |
|
|
113
|
+
| `capabilities` | feature detect | — |
|
|
114
|
+
|
|
115
|
+
`extract` requires `schema` or `responseConstraint`. `translate` requires `sourceLanguage` and `targetLanguage`. Language Detector and Translator download separately from Gemini Nano. Prompt and Summarizer use Nano.
|
|
116
|
+
|
|
117
|
+
### How it fits in the monorepo
|
|
118
|
+
|
|
119
|
+
This is an **extended functionality package**. It is not wired into `browserless` core — install `@browserless/ai` when you need Chrome Built-in AI from Node.js.
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
**@browserless/ai** © [Microlink](https://microlink.io), released under the [MIT](https://github.com/microlinkhq/browserless/blob/master/LICENSE.md) License.<br>
|
|
124
|
+
Authored and maintained by [Microlink](https://microlink.io) with help from [contributors](https://github.com/microlinkhq/browserless/contributors).
|
|
125
|
+
|
|
126
|
+
The [logo](https://thenounproject.com/term/browser/288309/) has been designed by [xinh studio](https://xinh.studio).
|
|
127
|
+
|
|
128
|
+
> [microlink.io](https://microlink.io) · GitHub [microlinkhq](https://github.com/microlinkhq) · X [@microlinkhq](https://x.com/microlinkhq)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const createAi = require('..')
|
|
4
|
+
|
|
5
|
+
const url = process.argv[2] || 'https://example.com'
|
|
6
|
+
const text = process.argv[3] || 'Hello, how are you today?'
|
|
7
|
+
|
|
8
|
+
const main = async () => {
|
|
9
|
+
const ai = createAi()
|
|
10
|
+
try {
|
|
11
|
+
console.log(await ai.capabilities({ url }))
|
|
12
|
+
console.log(await ai.detectLanguage(url, { text }))
|
|
13
|
+
} finally {
|
|
14
|
+
await ai.close()
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
main().catch(error => {
|
|
19
|
+
console.error(error)
|
|
20
|
+
process.exit(1)
|
|
21
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@browserless/ai",
|
|
3
|
+
"description": "Run Chrome Built-in AI APIs (Prompt, Summarizer, Translator, Language Detector) from Node.js using browserless.",
|
|
4
|
+
"homepage": "https://browserless.js.org",
|
|
5
|
+
"version": "13.9.3",
|
|
6
|
+
"main": "src",
|
|
7
|
+
"author": {
|
|
8
|
+
"email": "hello@microlink.io",
|
|
9
|
+
"name": "microlink.io",
|
|
10
|
+
"url": "https://microlink.io"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"directory": "packages/ai",
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/microlinkhq/browserless.git#master"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/microlinkhq/browserless/issues"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"ai",
|
|
22
|
+
"browserless",
|
|
23
|
+
"chrome",
|
|
24
|
+
"gemini-nano",
|
|
25
|
+
"headless",
|
|
26
|
+
"language-detector",
|
|
27
|
+
"prompt",
|
|
28
|
+
"puppeteer",
|
|
29
|
+
"summarizer",
|
|
30
|
+
"translator"
|
|
31
|
+
],
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"ava": "5",
|
|
34
|
+
"browserless": "workspace:*",
|
|
35
|
+
"puppeteer": "25"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"browserless": ">=13"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">= 24"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"examples",
|
|
45
|
+
"scripts",
|
|
46
|
+
"src"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"install-model": "node scripts/install-model.js",
|
|
50
|
+
"pack-model": "node scripts/pack-model.js",
|
|
51
|
+
"pretest": "DEBUG=browserless:ai node scripts/install-model.js",
|
|
52
|
+
"start": "node examples/index.js",
|
|
53
|
+
"test": "DEBUG=browserless:ai ava"
|
|
54
|
+
},
|
|
55
|
+
"license": "MIT",
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
59
|
+
"ava": {
|
|
60
|
+
"serial": true,
|
|
61
|
+
"timeout": "10m",
|
|
62
|
+
"workerThreads": false
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"debug-logfmt": "~1.4.12"
|
|
66
|
+
},
|
|
67
|
+
"gitHead": "625617f1fd88449f2ed6f4e1e1287627a1cc1016"
|
|
68
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
root=$(CDPATH= cd -- "$(dirname "$0")/../../.." && pwd)
|
|
5
|
+
ai="$root/packages/ai"
|
|
6
|
+
image=${IMAGE:-kikobeats/microlink-api:ai}
|
|
7
|
+
cmd=${1:-test}
|
|
8
|
+
|
|
9
|
+
if ! docker image inspect "$image" >/dev/null 2>&1; then
|
|
10
|
+
weights=$(find "$ai/model" -name weights.bin -print -quit 2>/dev/null || true)
|
|
11
|
+
if [ ! -f "$weights" ]; then
|
|
12
|
+
BROWSERLESS_AI_DIR="$ai/model" pnpm --filter @browserless/ai install-model
|
|
13
|
+
fi
|
|
14
|
+
docker build --platform linux/amd64 -t "$image" -f "$ai/Dockerfile" "$ai"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
exec docker run --rm --platform linux/amd64 --shm-size=2g \
|
|
18
|
+
-e CI=1 \
|
|
19
|
+
-e DEBUG=browserless:ai \
|
|
20
|
+
-e DISPLAY=:99 \
|
|
21
|
+
-e LIBGL_ALWAYS_SOFTWARE=1 \
|
|
22
|
+
-v "$root:/src:ro" \
|
|
23
|
+
-v browserless-ai-ci-work:/work \
|
|
24
|
+
-v browserless-ai-ci-chrome:/root/.cache/puppeteer \
|
|
25
|
+
"$image" \
|
|
26
|
+
with-xvfb bash -lc "set -eu
|
|
27
|
+
tar -C /src --exclude node_modules --exclude .git -cf - . | tar -C /work -xf -
|
|
28
|
+
cd /work
|
|
29
|
+
pnpm install --dangerously-allow-all-builds
|
|
30
|
+
pnpm --filter @browserless/ai exec puppeteer browsers install chrome
|
|
31
|
+
if [ \"$cmd\" = test ]; then
|
|
32
|
+
pnpm --filter @browserless/ai test
|
|
33
|
+
else
|
|
34
|
+
pnpm --filter @browserless/ai exec node $cmd
|
|
35
|
+
fi"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const debug = require('debug-logfmt')('browserless:ai')
|
|
4
|
+
const createAi = require('..')
|
|
5
|
+
const { credentials } = require('./util')
|
|
6
|
+
|
|
7
|
+
const main = async () => {
|
|
8
|
+
const env = credentials()
|
|
9
|
+
if (!env.endpoint || !env.bucket || !env.accessKey || !env.secretKey) {
|
|
10
|
+
if (process.env.CI) process.stderr.write('skip model download: missing R2 credentials\n')
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
const { dir } = await createAi.unpack(createAi.download)
|
|
14
|
+
debug('install-model', { dir })
|
|
15
|
+
process.stdout.write(`${dir}\n`)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
main().catch(error => {
|
|
19
|
+
console.error(error.message || error)
|
|
20
|
+
process.exit(1)
|
|
21
|
+
})
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { crc32 } = require('node:zlib')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
const os = require('node:os')
|
|
6
|
+
|
|
7
|
+
const { createReadStream, createWriteStream, existsSync, statSync } = require('node:fs')
|
|
8
|
+
|
|
9
|
+
const { hasFile } = require('../src/find-dir')
|
|
10
|
+
const { credentials, objectUrl, uploadFile } = require('./util')
|
|
11
|
+
|
|
12
|
+
const ZIP32_MAX = 0xffffffff
|
|
13
|
+
|
|
14
|
+
const chromeSupport = (...parts) =>
|
|
15
|
+
process.platform === 'darwin'
|
|
16
|
+
? path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', ...parts)
|
|
17
|
+
: undefined
|
|
18
|
+
|
|
19
|
+
const parseArgs = argv => {
|
|
20
|
+
const extra = argv.slice(2).filter(flag => flag !== '--upload')
|
|
21
|
+
if (extra.length) throw new Error('usage: pack-model.js [--upload]')
|
|
22
|
+
const dir = process.env.BROWSERLESS_AI_DIR
|
|
23
|
+
const model = process.env.BROWSERLESS_AI_MODEL || dir || chromeSupport('OptGuideOnDeviceModel')
|
|
24
|
+
const adaptation =
|
|
25
|
+
process.env.BROWSERLESS_AI_ADAPTATION || dir || chromeSupport('optimization_guide_model_store')
|
|
26
|
+
if (!model) throw new Error('set BROWSERLESS_AI_DIR or BROWSERLESS_AI_MODEL')
|
|
27
|
+
if (!adaptation) throw new Error('set BROWSERLESS_AI_DIR or BROWSERLESS_AI_ADAPTATION')
|
|
28
|
+
return {
|
|
29
|
+
model,
|
|
30
|
+
adaptation,
|
|
31
|
+
out: path.join(os.tmpdir(), 'browserless-ai-nano.zip'),
|
|
32
|
+
upload: argv.includes('--upload')
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ADAPTATIONS = [
|
|
37
|
+
{ name: 'prompt', target: 49 },
|
|
38
|
+
{ name: 'summarize', target: 51 },
|
|
39
|
+
{ name: 'detect', target: 2 }
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
const resolveModelDir = root => {
|
|
43
|
+
const dir = hasFile(root, 'weights.bin')
|
|
44
|
+
if (!dir) throw new Error(`No weights.bin under ${root}`)
|
|
45
|
+
for (const name of ['weights.bin', 'manifest.json', 'on_device_model_execution_config.pb']) {
|
|
46
|
+
if (!existsSync(path.join(dir, name))) throw new Error(`Missing ${name} in ${dir}`)
|
|
47
|
+
}
|
|
48
|
+
return dir
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const resolveFeatureDir = (root, { name, target }) =>
|
|
52
|
+
hasFile(path.join(root, name), 'model-info.pb') ||
|
|
53
|
+
hasFile(path.join(root, String(target)), 'model-info.pb')
|
|
54
|
+
|
|
55
|
+
const crcAndSize = async file => {
|
|
56
|
+
const { size } = statSync(file)
|
|
57
|
+
if (size > ZIP32_MAX) throw new Error(`${file} is too large for ZIP32`)
|
|
58
|
+
let crc = 0
|
|
59
|
+
for await (const chunk of createReadStream(file)) crc = crc32(chunk, crc)
|
|
60
|
+
return { size, crc }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const localHeader = (name, { size, crc }) => {
|
|
64
|
+
const nameBuf = Buffer.from(name)
|
|
65
|
+
const header = Buffer.alloc(30)
|
|
66
|
+
header.writeUInt32LE(0x04034b50, 0)
|
|
67
|
+
header.writeUInt16LE(20, 4)
|
|
68
|
+
header.writeUInt32LE(crc, 14)
|
|
69
|
+
header.writeUInt32LE(size, 18)
|
|
70
|
+
header.writeUInt32LE(size, 22)
|
|
71
|
+
header.writeUInt16LE(nameBuf.length, 26)
|
|
72
|
+
return Buffer.concat([header, nameBuf])
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const centralHeader = (name, { size, crc, offset }) => {
|
|
76
|
+
const nameBuf = Buffer.from(name)
|
|
77
|
+
const header = Buffer.alloc(46)
|
|
78
|
+
header.writeUInt32LE(0x02014b50, 0)
|
|
79
|
+
header.writeUInt16LE(20, 4)
|
|
80
|
+
header.writeUInt16LE(20, 6)
|
|
81
|
+
header.writeUInt32LE(crc, 16)
|
|
82
|
+
header.writeUInt32LE(size, 20)
|
|
83
|
+
header.writeUInt32LE(size, 24)
|
|
84
|
+
header.writeUInt16LE(nameBuf.length, 28)
|
|
85
|
+
header.writeUInt32LE(offset, 42)
|
|
86
|
+
return Buffer.concat([header, nameBuf])
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const write = (stream, buf) =>
|
|
90
|
+
new Promise((resolve, reject) => {
|
|
91
|
+
stream.write(buf, err => (err ? reject(err) : resolve()))
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const writeZip = async (dest, entries) => {
|
|
95
|
+
const out = createWriteStream(dest)
|
|
96
|
+
const centrals = []
|
|
97
|
+
let offset = 0
|
|
98
|
+
|
|
99
|
+
for (const { name, file } of entries) {
|
|
100
|
+
process.stderr.write(`packing ${name}\n`)
|
|
101
|
+
const info = await crcAndSize(file)
|
|
102
|
+
const header = localHeader(name, info)
|
|
103
|
+
if (offset + header.length + info.size > ZIP32_MAX) {
|
|
104
|
+
throw new Error('archive exceeds ZIP32 size')
|
|
105
|
+
}
|
|
106
|
+
await write(out, header)
|
|
107
|
+
for await (const chunk of createReadStream(file)) await write(out, chunk)
|
|
108
|
+
centrals.push({ name, ...info, offset })
|
|
109
|
+
offset += header.length + info.size
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let centralSize = 0
|
|
113
|
+
for (const entry of centrals) {
|
|
114
|
+
const header = centralHeader(entry.name, entry)
|
|
115
|
+
await write(out, header)
|
|
116
|
+
centralSize += header.length
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const end = Buffer.alloc(22)
|
|
120
|
+
end.writeUInt32LE(0x06054b50, 0)
|
|
121
|
+
end.writeUInt16LE(centrals.length, 8)
|
|
122
|
+
end.writeUInt16LE(centrals.length, 10)
|
|
123
|
+
end.writeUInt32LE(centralSize, 12)
|
|
124
|
+
end.writeUInt32LE(offset, 16)
|
|
125
|
+
await write(out, end)
|
|
126
|
+
|
|
127
|
+
await new Promise((resolve, reject) => out.end(err => (err ? reject(err) : resolve())))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const main = async () => {
|
|
131
|
+
const opts = parseArgs(process.argv)
|
|
132
|
+
const modelDir = resolveModelDir(opts.model)
|
|
133
|
+
const version = path.basename(modelDir)
|
|
134
|
+
const entries = [
|
|
135
|
+
{ name: `nano/${version}/weights.bin`, file: path.join(modelDir, 'weights.bin') },
|
|
136
|
+
{ name: `nano/${version}/manifest.json`, file: path.join(modelDir, 'manifest.json') },
|
|
137
|
+
{
|
|
138
|
+
name: `nano/${version}/on_device_model_execution_config.pb`,
|
|
139
|
+
file: path.join(modelDir, 'on_device_model_execution_config.pb')
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
let packed = 0
|
|
144
|
+
for (const feature of ADAPTATIONS) {
|
|
145
|
+
const dir = resolveFeatureDir(opts.adaptation, feature)
|
|
146
|
+
if (!dir) continue
|
|
147
|
+
packed++
|
|
148
|
+
for (const name of ['model.tflite', 'model-info.pb', 'on_device_model_execution_config.pb']) {
|
|
149
|
+
const file = path.join(dir, name)
|
|
150
|
+
if (existsSync(file)) entries.push({ name: `${feature.name}/${name}`, file })
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!packed) throw new Error(`No prompt/summarize/detect adaptations under ${opts.adaptation}`)
|
|
154
|
+
|
|
155
|
+
await writeZip(opts.out, entries)
|
|
156
|
+
console.log(opts.out)
|
|
157
|
+
if (!opts.upload) return
|
|
158
|
+
|
|
159
|
+
const env = credentials()
|
|
160
|
+
if (!env.endpoint) throw new Error('set R2_ENDPOINT or R2_ACCOUNT_ID')
|
|
161
|
+
if (!env.bucket) throw new Error('set R2_BUCKET, or include the bucket in R2_ENDPOINT')
|
|
162
|
+
if (!env.accessKey || !env.secretKey) {
|
|
163
|
+
throw new Error('set R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY')
|
|
164
|
+
}
|
|
165
|
+
await uploadFile(env, opts.out)
|
|
166
|
+
console.log(objectUrl(env))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
main().catch(error => {
|
|
170
|
+
console.error(error.message || error)
|
|
171
|
+
process.exit(1)
|
|
172
|
+
})
|
package/scripts/probe.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const createAi = require('..')
|
|
4
|
+
|
|
5
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
6
|
+
|
|
7
|
+
const main = async () => {
|
|
8
|
+
const ai = createAi()
|
|
9
|
+
try {
|
|
10
|
+
let available
|
|
11
|
+
for (let i = 0; i < 4; i++) {
|
|
12
|
+
available = await ai.capabilities()
|
|
13
|
+
console.log(JSON.stringify({ i, available }))
|
|
14
|
+
if (available.languageModel === 'available') break
|
|
15
|
+
await sleep(60000)
|
|
16
|
+
}
|
|
17
|
+
const language = await ai.detectLanguage('https://example.com', {
|
|
18
|
+
text: 'Hello, how are you today?'
|
|
19
|
+
})
|
|
20
|
+
console.log(JSON.stringify({ language }))
|
|
21
|
+
const reply = await ai.prompt('https://example.com', {
|
|
22
|
+
prompt: 'Reply with the single word OK.'
|
|
23
|
+
})
|
|
24
|
+
console.log(JSON.stringify({ reply }))
|
|
25
|
+
} finally {
|
|
26
|
+
await ai.close()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
main().catch(error => {
|
|
31
|
+
console.error(error)
|
|
32
|
+
process.exit(1)
|
|
33
|
+
})
|