@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/src/run-ai.js ADDED
@@ -0,0 +1,151 @@
1
+ 'use strict'
2
+
3
+ const runAi = async spec => {
4
+ const parseJson = raw => {
5
+ try {
6
+ return JSON.parse(raw)
7
+ } catch (error) {
8
+ const start = String(raw).indexOf('{')
9
+ const end = String(raw).lastIndexOf('}')
10
+ if (start !== -1 && end > start) {
11
+ try {
12
+ return JSON.parse(raw.slice(start, end + 1))
13
+ } catch {}
14
+ }
15
+ const preview = String(raw).replace(/\s+/g, ' ').slice(0, 160)
16
+ throw new Error(`LanguageModel did not return JSON: ${preview}`, { cause: error })
17
+ }
18
+ }
19
+
20
+ const names = {
21
+ prompt: 'LanguageModel',
22
+ summarize: 'Summarizer',
23
+ translate: 'Translator',
24
+ detectLanguage: 'LanguageDetector'
25
+ }
26
+
27
+ if (spec.api === 'availability') {
28
+ const probes = {
29
+ languageModel: {
30
+ name: 'LanguageModel',
31
+ opts: {
32
+ expectedInputs: [{ type: 'text', languages: ['en'] }],
33
+ expectedOutputs: [{ type: 'text', languages: ['en'] }]
34
+ }
35
+ },
36
+ summarizer: {
37
+ name: 'Summarizer',
38
+ opts: { expectedInputLanguages: ['en'], outputLanguage: 'en' }
39
+ },
40
+ translator: {
41
+ name: 'Translator',
42
+ opts: { sourceLanguage: 'en', targetLanguage: 'es' }
43
+ },
44
+ languageDetector: {
45
+ name: 'LanguageDetector',
46
+ opts: { expectedInputLanguages: ['en'] }
47
+ }
48
+ }
49
+ const apis = {}
50
+ const ctors = {}
51
+ for (const [api, { name, opts }] of Object.entries(probes)) {
52
+ const Ctor = globalThis[name]
53
+ ctors[api] = typeof Ctor
54
+ if (typeof Ctor === 'undefined') {
55
+ apis[api] = 'unavailable'
56
+ continue
57
+ }
58
+ try {
59
+ apis[api] = await Ctor.availability(opts)
60
+ } catch {
61
+ apis[api] = 'unavailable'
62
+ }
63
+ }
64
+ return {
65
+ apis,
66
+ env: {
67
+ hardwareConcurrency: globalThis.navigator && globalThis.navigator.hardwareConcurrency,
68
+ deviceMemory: globalThis.navigator && globalThis.navigator.deviceMemory,
69
+ ...Object.fromEntries(Object.entries(ctors).map(([key, value]) => [`ctor_${key}`, value]))
70
+ }
71
+ }
72
+ }
73
+
74
+ const schema = spec.schema || spec.responseConstraint
75
+ const name = schema && spec.api === 'summarize' ? 'LanguageModel' : names[spec.api]
76
+ const Ctor = globalThis[name]
77
+ if (typeof Ctor === 'undefined') throw new Error(`${name} is not available`)
78
+
79
+ const createKeys = {
80
+ prompt: ['initialPrompts', 'expectedInputs', 'expectedOutputs', 'temperature', 'topK'],
81
+ summarize: [
82
+ 'type',
83
+ 'format',
84
+ 'length',
85
+ 'sharedContext',
86
+ 'expectedInputLanguages',
87
+ 'outputLanguage',
88
+ 'expectedContextLanguages'
89
+ ],
90
+ translate: ['sourceLanguage', 'targetLanguage'],
91
+ detectLanguage: ['expectedInputLanguages']
92
+ }
93
+
94
+ const keys =
95
+ name === 'LanguageModel' ? createKeys.prompt : createKeys[spec.api] || createKeys.prompt
96
+ const createOpts = {}
97
+ for (const key of keys) {
98
+ if (spec[key] !== undefined) createOpts[key] = spec[key]
99
+ }
100
+
101
+ if (spec.api === 'translate' && (!createOpts.sourceLanguage || !createOpts.targetLanguage)) {
102
+ throw new Error('Translator requires sourceLanguage and targetLanguage')
103
+ }
104
+
105
+ let availability = await Ctor.availability(createOpts)
106
+ if (availability === 'unavailable') throw new Error(`${name} is unavailable`)
107
+
108
+ if (availability === 'downloading') {
109
+ const started = Date.now()
110
+ while (availability === 'downloading' && Date.now() - started < 240000) {
111
+ await new Promise(resolve => setTimeout(resolve, 1000))
112
+ availability = await Ctor.availability(createOpts)
113
+ }
114
+ if (availability === 'unavailable' || availability === 'downloading') {
115
+ throw new Error(`${name} is ${availability}`)
116
+ }
117
+ }
118
+
119
+ const instance = await Ctor.create({
120
+ ...createOpts,
121
+ monitor (m) {
122
+ m.addEventListener('downloadprogress', e => {
123
+ console.log(`download ${name} ${Math.round(e.loaded * 100)}%`)
124
+ })
125
+ }
126
+ })
127
+ try {
128
+ const pageText =
129
+ (globalThis.document && globalThis.document.body && globalThis.document.body.innerText) || ''
130
+ const input = spec.text !== undefined ? spec.text : pageText
131
+ if (spec.api === 'prompt' || (schema && name === 'LanguageModel')) {
132
+ let prompt = input
133
+ if (spec.prompt) prompt = input ? `${spec.prompt}\n\n${input}` : spec.prompt
134
+ else if (schema) prompt = `Extract metadata from this page.\n\n${input}`
135
+ const result = await instance.prompt(
136
+ prompt,
137
+ schema ? { responseConstraint: schema } : undefined
138
+ )
139
+ return schema ? parseJson(result) : result
140
+ }
141
+ if (spec.api === 'summarize') {
142
+ return await instance.summarize(input, spec.context ? { context: spec.context } : undefined)
143
+ }
144
+ if (spec.api === 'translate') return await instance.translate(input)
145
+ return await instance.detect(input)
146
+ } finally {
147
+ if (typeof instance.destroy === 'function') instance.destroy()
148
+ }
149
+ }
150
+
151
+ module.exports = runAi
package/src/unpack.js ADDED
@@ -0,0 +1,145 @@
1
+ 'use strict'
2
+
3
+ const { pipeline } = require('node:stream/promises')
4
+ const { createInflateRaw } = require('node:zlib')
5
+ const { open } = require('node:fs/promises')
6
+ const { Readable } = require('node:stream')
7
+ const path = require('node:path')
8
+
9
+ const {
10
+ createReadStream,
11
+ createWriteStream,
12
+ existsSync,
13
+ mkdirSync,
14
+ renameSync,
15
+ rmSync,
16
+ unlinkSync,
17
+ writeFileSync
18
+ } = require('node:fs')
19
+
20
+ const debug = require('debug-logfmt')('browserless:ai')
21
+ const { hasFile, cacheRoot } = require('./find-dir')
22
+
23
+ const installed = dir => {
24
+ if (!hasFile(dir, 'weights.bin')) return
25
+ const hasAdaptation =
26
+ ['prompt', 'summarize', 'detect'].some(name =>
27
+ hasFile(path.join(dir, name), 'model-info.pb')
28
+ ) || hasFile(dir, 'model-info.pb')
29
+ if (!hasAdaptation) return
30
+ return { dir }
31
+ }
32
+
33
+ const writeZip = async (input, dest) => {
34
+ if (input == null) {
35
+ if (!existsSync(dest)) throw new Error('download did not write a zip')
36
+ return dest
37
+ }
38
+ if (typeof input === 'string') return path.resolve(input)
39
+ mkdirSync(path.dirname(dest), { recursive: true })
40
+ if (Buffer.isBuffer(input) || input instanceof Uint8Array) {
41
+ writeFileSync(dest, input)
42
+ return dest
43
+ }
44
+ const body = input.body || input.Body || input
45
+ const stream = typeof body.getReader === 'function' ? Readable.fromWeb(body) : body
46
+ await pipeline(stream, createWriteStream(dest))
47
+ return dest
48
+ }
49
+
50
+ const safeDest = (dir, name) => {
51
+ if (name.endsWith('/')) return
52
+ const dest = path.join(dir, name)
53
+ const rel = path.relative(dir, dest)
54
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
55
+ throw new Error(`refusing to extract ${name}`)
56
+ }
57
+ return dest
58
+ }
59
+
60
+ const unzipJs = async (zipPath, dir) => {
61
+ const fd = await open(zipPath, 'r')
62
+ try {
63
+ let pos = 0
64
+ const read = async (length, at = pos) => {
65
+ const buf = Buffer.alloc(length)
66
+ const { bytesRead } = await fd.read(buf, 0, length, at)
67
+ return buf.subarray(0, bytesRead)
68
+ }
69
+
70
+ while (true) {
71
+ const sig = await read(4, pos)
72
+ if (sig.length < 4 || sig.readUInt32LE(0) !== 0x04034b50) break
73
+ const rest = await read(26, pos + 4)
74
+ const flags = rest.readUInt16LE(2)
75
+ const method = rest.readUInt16LE(4)
76
+ const compressed = rest.readUInt32LE(14)
77
+ const nameLen = rest.readUInt16LE(22)
78
+ const extraLen = rest.readUInt16LE(24)
79
+ const name = (await read(nameLen, pos + 30)).toString()
80
+ if (flags & 0x08) {
81
+ throw new Error(`unsupported zip entry with data descriptor (${name})`)
82
+ }
83
+ const dataStart = pos + 30 + nameLen + extraLen
84
+ const dest = safeDest(dir, name)
85
+ pos = dataStart + compressed
86
+ if (!dest) continue
87
+ if (method !== 0 && method !== 8) {
88
+ throw new Error(`unsupported zip method ${method} (${name})`)
89
+ }
90
+ mkdirSync(path.dirname(dest), { recursive: true })
91
+ process.stderr.write(`extracting ${name}\n`)
92
+ if (compressed === 0) {
93
+ writeFileSync(dest, '')
94
+ continue
95
+ }
96
+ const source = createReadStream(zipPath, {
97
+ start: dataStart,
98
+ end: dataStart + compressed - 1
99
+ })
100
+ const destStream = createWriteStream(dest)
101
+ if (method === 8) await pipeline(source, createInflateRaw(), destStream)
102
+ else await pipeline(source, destStream)
103
+ }
104
+ } finally {
105
+ await fd.close()
106
+ }
107
+ }
108
+
109
+ const unzip = async (zipPath, dir) => {
110
+ mkdirSync(dir, { recursive: true })
111
+ await unzipJs(zipPath, dir)
112
+ }
113
+
114
+ const unpack = async (get, { dir, force = false } = {}) => {
115
+ if (get == null) throw new Error('unpack requires a zip path or download function')
116
+ dir = path.resolve(dir || process.env.BROWSERLESS_AI_DIR || cacheRoot())
117
+ mkdirSync(dir, { recursive: true })
118
+
119
+ const already = installed(dir)
120
+ debug('unpack', { dir, force, installed: Boolean(already) })
121
+ if (already && !force) return already
122
+
123
+ const staging = `${dir}.partial`
124
+ rmSync(staging, { recursive: true, force: true })
125
+ mkdirSync(staging, { recursive: true })
126
+ try {
127
+ const dest = path.join(staging, 'bundle.zip')
128
+ const zipPath =
129
+ typeof get === 'function' ? await writeZip(await get(dest), dest) : path.resolve(get)
130
+ if (!existsSync(zipPath)) throw new Error(`missing zip: ${zipPath}`)
131
+
132
+ await unzip(zipPath, staging)
133
+ if (zipPath === dest) unlinkSync(dest)
134
+ const paths = installed(staging)
135
+ if (!paths) throw new Error('bundle did not contain nano weights and an adaptation')
136
+ rmSync(dir, { recursive: true, force: true })
137
+ renameSync(staging, dir)
138
+ return { dir }
139
+ } catch (error) {
140
+ rmSync(staging, { recursive: true, force: true })
141
+ throw error
142
+ }
143
+ }
144
+
145
+ module.exports = unpack