alexa-ai 2.3.0 → 2.4.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/CHANGELOG.md +23 -0
- package/README.md +14 -0
- package/examples/diagnose.js +17 -17
- package/examples/text2img-standalone.js +15 -15
- package/package.json +1 -1
- package/src/AlexaAI.js +9 -5
- package/src/core/Config.js +6 -0
- package/src/core/DeepAIClient.js +64 -27
- package/src/services/ImageDescriber.js +1 -1
- package/test/run-tests.js +36 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ All notable changes to `alexa-ai` are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [2.4.0] — 2026-09-07
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **`detectNsfw()` (and every other `/api/*` helper) now works on free keys.**
|
|
11
|
+
A registered key refused with "Pro members in good standing" gets one
|
|
12
|
+
anonymous browser-dialect retry (`anonymousApiFallback`, on by default) —
|
|
13
|
+
the same mechanism that made `generateImage()` work. Applies to
|
|
14
|
+
`generateImage`, `editImage`, `upscaleImage`, `colorizeImage`,
|
|
15
|
+
`detectNsfw`, `summarizeText` and `deepai.runApi()`.
|
|
16
|
+
- `detectNsfw()` dropped its `opts` argument instead of passing it through.
|
|
17
|
+
- CLI output of the bundled examples is plain ASCII now — emoji and
|
|
18
|
+
typographic dashes rendered as mojibake in Windows consoles with a
|
|
19
|
+
non-UTF-8 codepage.
|
|
20
|
+
- When both the registered and the anonymous attempt fail, the error message
|
|
21
|
+
reports both refusals.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- `generateImage()` route 1 carries the browser fields into its built-in
|
|
25
|
+
anonymous retry, so a Pro refusal recovers directly with `via: 'api'`.
|
|
26
|
+
- Client media helpers accept an `options` argument (third/fourth parameter)
|
|
27
|
+
and default their anonymous dialect per endpoint (`generation_source=img`
|
|
28
|
+
for model pages; none for image-editor/torch-srgan, matching the site).
|
|
29
|
+
|
|
7
30
|
## [2.3.0] — 2026-09-07
|
|
8
31
|
|
|
9
32
|
### Added
|
package/README.md
CHANGED
|
@@ -894,6 +894,20 @@ github.com/lexiforest/curl-impersonate/releases). Quota and auth errors are
|
|
|
894
894
|
never re-driven through other transports. Run `node examples/diagnose.js` to
|
|
895
895
|
see which transport your network accepts.
|
|
896
896
|
|
|
897
|
+
**`anonymousApiFallback` (2.4.0).** Every `/api/*` helper — `generateImage`,
|
|
898
|
+
`editImage`, `upscaleImage`, `colorizeImage`, `detectNsfw`, `summarizeText`
|
|
899
|
+
and plain `deepai.runApi()` — automatically retries once with a fresh
|
|
900
|
+
anonymous key (browser dialect per endpoint) when the registered key is
|
|
901
|
+
refused with "Pro members in good standing". This is what the website does
|
|
902
|
+
for free visitors, so all of these now work on free keys from a residential
|
|
903
|
+
IP. Disable globally with `anonymousApiFallback: false` in the constructor.
|
|
904
|
+
On failure both refusals are reported (`<registered refusal> | anonymous
|
|
905
|
+
retry: <anonymous refusal>`).
|
|
906
|
+
|
|
907
|
+
**Windows consoles.** CLI output from the bundled examples is plain ASCII on
|
|
908
|
+
purpose — emoji and typographic dashes render as garbage (Chinese-looking
|
|
909
|
+
mojibake) in cmd.exe/PowerShell with a non-UTF-8 codepage.
|
|
910
|
+
|
|
897
911
|
**`generateImage()` returns `{ ok: false, error: 'DEEPAI_QUOTA_EXCEEDED' }`**
|
|
898
912
|
`/api/text2img` is Pro-only for registered keys ("APIs are only available for
|
|
899
913
|
Pro members in good standing"), the anonymous browser-shaped retry was
|
package/examples/diagnose.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
/**
|
|
4
|
-
* diagnose.js
|
|
4
|
+
* diagnose.js - DeepAI text2img connectivity diagnostic.
|
|
5
5
|
*
|
|
6
6
|
* Run ON THE MACHINE where the browser playground works:
|
|
7
7
|
*
|
|
@@ -93,19 +93,19 @@ function runCurl(binary, impersonate) {
|
|
|
93
93
|
|
|
94
94
|
function label(t) {
|
|
95
95
|
const b = String(t.body).slice(0, 140).replace(/\s+/g, ' ');
|
|
96
|
-
if (t.status === 200 && /share_url|output_url/.test(t.body)) return
|
|
97
|
-
if (/valid Api-Key/i.test(b)) return
|
|
98
|
-
if (/try this model/i.test(b)) return
|
|
99
|
-
if (/Pro members/i.test(b)) return
|
|
100
|
-
if (/try it exceeded/i.test(b)) return
|
|
101
|
-
return
|
|
96
|
+
if (t.status === 200 && /share_url|output_url/.test(t.body)) return `[OK] SUCCESS - image generated (HTTP 200)`;
|
|
97
|
+
if (/valid Api-Key/i.test(b)) return `[FAIL] key rejected (unexpected - fresh key was used)`;
|
|
98
|
+
if (/try this model/i.test(b)) return `[FAIL] transport refused ("Please try this model on deepai.org")`;
|
|
99
|
+
if (/Pro members/i.test(b)) return `[FAIL] account-level refusal (needs a Pro key)`;
|
|
100
|
+
if (/try it exceeded/i.test(b)) return `[WARN] free quota exhausted for this device/IP - retry later or change --device-id`;
|
|
101
|
+
return `[FAIL] HTTP ${t.status}: ${b}`;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
(async () => {
|
|
105
|
-
console.log(`DeepAI text2img diagnostic
|
|
105
|
+
console.log(`DeepAI text2img diagnostic - device ${deviceId.slice(0, 8)}...\n`);
|
|
106
106
|
const rows = [];
|
|
107
|
-
try { rows.push(['1. Node fetch (library default)', label(await viaFetch())]); } catch (e) { rows.push(['1. Node fetch',
|
|
108
|
-
try { rows.push(['2. system curl', label(runCurl('curl', false))]); } catch (e) { rows.push(['2. system curl',
|
|
107
|
+
try { rows.push(['1. Node fetch (library default)', label(await viaFetch())]); } catch (e) { rows.push(['1. Node fetch', `[WARN] ${e.message}`]); }
|
|
108
|
+
try { rows.push(['2. system curl', label(runCurl('curl', false))]); } catch (e) { rows.push(['2. system curl', `[WARN] ${e.message.split('\n')[0]}`]); }
|
|
109
109
|
|
|
110
110
|
let impBin = opt.imp;
|
|
111
111
|
if (!impBin) {
|
|
@@ -115,20 +115,20 @@ function label(t) {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
if (impBin) {
|
|
118
|
-
try { rows.push([`3. curl-impersonate (${impBin})`, label(runCurl(impBin, true))]); } catch (e) { rows.push(['3. curl-impersonate',
|
|
118
|
+
try { rows.push([`3. curl-impersonate (${impBin})`, label(runCurl(impBin, true))]); } catch (e) { rows.push(['3. curl-impersonate', `[WARN] ${e.message.split('\n')[0]}`]); }
|
|
119
119
|
} else {
|
|
120
|
-
rows.push(['3. curl-impersonate', '
|
|
120
|
+
rows.push(['3. curl-impersonate', '[SKIP] skipped - binary not found (see README for install)']);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
for (const [name, v] of rows) console.log(`${name.padEnd(34)} ${v}`);
|
|
124
124
|
console.log(`
|
|
125
125
|
Reading the results:
|
|
126
|
-
- A
|
|
126
|
+
- A [OK] on ANY line -> that transport works; use it (library: transport option,
|
|
127
127
|
standalone: --transport).
|
|
128
|
-
-
|
|
128
|
+
- [FAIL] on line 1 only -> non-browser TLS stack refused; use 'curl' or
|
|
129
129
|
'impersonate'.
|
|
130
|
-
-
|
|
131
|
-
-
|
|
130
|
+
- [FAIL] on lines 1+2, [OK] on 3 -> strict browser-TLS matching; use 'impersonate'.
|
|
131
|
+
- [FAIL] everywhere -> the IP is refused for anonymous generation, or the free
|
|
132
132
|
quota is exhausted. Compare with the browser: DevTools
|
|
133
|
-
|
|
133
|
+
-> Network -> generate -> text2img request -> Response.`);
|
|
134
134
|
})();
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
/**
|
|
4
|
-
* text2img-standalone.js
|
|
4
|
+
* text2img-standalone.js - zero-dependency DeepAI text-to-image CLI.
|
|
5
5
|
*
|
|
6
|
-
* Speaks the anonymous browser dialect: a fresh single-use `tryit
|
|
6
|
+
* Speaks the anonymous browser dialect: a fresh single-use `tryit-...` key
|
|
7
7
|
* (hashed over the User-Agent) per run, browser-identical headers and a
|
|
8
8
|
* multipart/form-data body, plus a stable `deepai_device_id` cookie.
|
|
9
9
|
*
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Transports: 'fetch' (default, Node fetch) | 'curl' (system curl) |
|
|
20
20
|
* 'impersonate' (curl-impersonate binary, Chrome TLS profile). Some
|
|
21
|
-
* networks serve non-browser TLS stacks a refusal
|
|
21
|
+
* networks serve non-browser TLS stacks a refusal - if 'fetch' fails with
|
|
22
22
|
* "Please try this model on deepai.org", try 'curl', then 'impersonate'.
|
|
23
23
|
*
|
|
24
24
|
* Requires Node.js 18+. Anonymous generation is refused from
|
|
@@ -27,20 +27,20 @@
|
|
|
27
27
|
|
|
28
28
|
const API_URL = 'https://api.deepai.org/api/text2img';
|
|
29
29
|
const SALT = 'hackers_become_a_little_stinkier_every_time_they_hack';
|
|
30
|
-
// Keep this EXACT string in sync with the User-Agent header below
|
|
30
|
+
// Keep this EXACT string in sync with the User-Agent header below - the key
|
|
31
31
|
// hash is computed over it and the server recomputes it from the request.
|
|
32
32
|
const USER_AGENT =
|
|
33
33
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36';
|
|
34
34
|
|
|
35
35
|
// ---------------------------------------------------------------------------
|
|
36
36
|
// Deterministic key hash (see DeepAIClient._islandHash in the engine).
|
|
37
|
-
// The integer/bit-level behaviour is intentional
|
|
37
|
+
// The integer/bit-level behaviour is intentional - do not simplify it.
|
|
38
38
|
// ---------------------------------------------------------------------------
|
|
39
39
|
function islandHash(input) {
|
|
40
40
|
const a = [];
|
|
41
41
|
for (let b = 0; 64 > b; ) a[b] = 0 | (4294967296 * Math.sin(++b % Math.PI));
|
|
42
42
|
let d, e, f, g = [(d = 1732584193), (e = 4023233417), ~d, ~e], h = [];
|
|
43
|
-
const l = unescape(encodeURI(input)) + '
|
|
43
|
+
const l = unescape(encodeURI(input)) + '\u0080';
|
|
44
44
|
let k = l.length;
|
|
45
45
|
let c = (--k / 4 + 2) | 15;
|
|
46
46
|
for (h[--c] = 8 * k; ~k; ) h[k >> 2] |= l.charCodeAt(k) << (8 * k--);
|
|
@@ -120,19 +120,19 @@ async function main() {
|
|
|
120
120
|
Accept: '*/*',
|
|
121
121
|
'Accept-Language': 'en-US,en;q=0.9',
|
|
122
122
|
Cookie: `deepai_device_id=${opts.deviceId}`,
|
|
123
|
-
// NOTE: do NOT set Content-Type yourself
|
|
123
|
+
// NOTE: do NOT set Content-Type yourself - undici adds the multipart
|
|
124
124
|
// boundary. A manual Content-Type without the boundary is rejected.
|
|
125
125
|
};
|
|
126
126
|
|
|
127
127
|
console.log(`Prompt : ${opts.prompt}`);
|
|
128
|
-
console.log(`Key : ${opts.key ? '(registered key
|
|
128
|
+
console.log(`Key : ${opts.key ? '(registered key - needs Pro)' : apiKey + ' (fresh, single-use)'}`);
|
|
129
129
|
console.log('POST : ' + API_URL);
|
|
130
130
|
|
|
131
131
|
let res;
|
|
132
132
|
if (opts.transport === 'curl' || opts.transport === 'impersonate') {
|
|
133
133
|
const { execFile } = require('child_process');
|
|
134
134
|
const binary = opts.transport === 'impersonate' ? (opts.imp || 'curl-impersonate') : 'curl';
|
|
135
|
-
// the chrome136 profile sends its own Mac Chrome UA
|
|
135
|
+
// the chrome136 profile sends its own Mac Chrome UA - the anonymous
|
|
136
136
|
// key hash must be derived from exactly that UA
|
|
137
137
|
const profileUa = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36';
|
|
138
138
|
const apiKey = opts.key || freshTryItKey(opts.transport === 'impersonate' ? profileUa : USER_AGENT);
|
|
@@ -169,10 +169,10 @@ async function main() {
|
|
|
169
169
|
console.error(`\nFAILED HTTP ${res.status}`);
|
|
170
170
|
console.error(raw.slice(0, 500));
|
|
171
171
|
const s = String(data?.status || data?.err || '');
|
|
172
|
-
if (/valid Api-Key/i.test(s)) console.error('\n
|
|
173
|
-
else if (/try this model on deepai\.org/i.test(s)) console.error('\n
|
|
174
|
-
else if (/Pro members/i.test(s)) console.error('\n
|
|
175
|
-
else if (/try it exceeded/i.test(s)) console.error('\n
|
|
172
|
+
if (/valid Api-Key/i.test(s)) console.error('\n-> The key is invalid or already used. Keys are single-use and must be hashed for the exact User-Agent sent; this script mints a fresh one each run.');
|
|
173
|
+
else if (/try this model on deepai\.org/i.test(s)) console.error('\n-> DeepAI is refusing anonymous generation from your IP (datacenter/VPN) or the Origin header is missing. Run from a residential IP and keep the Origin/Referer headers.');
|
|
174
|
+
else if (/Pro members/i.test(s)) console.error('\n-> Your registered key is on the free plan; /api/* needs Pro. Use the anonymous mode (omit --key) or upgrade.');
|
|
175
|
+
else if (/try it exceeded/i.test(s)) console.error('\n-> Free quota for this device/IP is exhausted. Try a different --device-id or wait for the reset.');
|
|
176
176
|
process.exit(1);
|
|
177
177
|
}
|
|
178
178
|
|
|
@@ -190,10 +190,10 @@ async function main() {
|
|
|
190
190
|
require('fs').writeFileSync(out, buf);
|
|
191
191
|
console.log(`Saved : ${out} (${(buf.length / 1024).toFixed(1)} KB)`);
|
|
192
192
|
} else {
|
|
193
|
-
console.log(`(download skipped: HTTP ${img.status}
|
|
193
|
+
console.log(`(download skipped: HTTP ${img.status} - open the URL above in a browser)`);
|
|
194
194
|
}
|
|
195
195
|
} catch (e) {
|
|
196
|
-
console.log(`(download failed: ${e.message}
|
|
196
|
+
console.log(`(download failed: ${e.message} - open the URL above in a browser)`);
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alexa-ai",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "AI engine for the Alexa WhatsApp bot: DeepAI-powered chat with PostgreSQL-backed long-term memory, cross-chat identity (@lid <-> phone), vision/OCR, image generation and web search.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
package/src/AlexaAI.js
CHANGED
|
@@ -642,10 +642,14 @@ class AlexaAI {
|
|
|
642
642
|
// size/quality fields).
|
|
643
643
|
if (!chatToolOnly) {
|
|
644
644
|
try {
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
645
|
+
const browserFields = AlexaAI._browserImageFields(aspectRatio || '1:1', apiFields);
|
|
646
|
+
const extra = this.client.usingTryItKey ? browserFields : apiFields;
|
|
647
|
+
const data = await this.client.text2img(text, extra, {
|
|
648
|
+
signal,
|
|
649
|
+
// used when a registered key is refused and runApi()
|
|
650
|
+
// retries anonymously
|
|
651
|
+
anonymousExtraFields: browserFields,
|
|
652
|
+
});
|
|
649
653
|
const url = AlexaAI._outputUrl(data);
|
|
650
654
|
if (url) return { ok: true, url, id: data.id || null, error: null, via: 'api', raw: data };
|
|
651
655
|
errors.push('text2img: no output_url in response');
|
|
@@ -784,7 +788,7 @@ class AlexaAI {
|
|
|
784
788
|
if (!field) return { ...AlexaAI._mediaError('detectNsfw', 'NSFW_FAILED'), score: null, nsfw: null };
|
|
785
789
|
const threshold = typeof opts.threshold === 'number' ? opts.threshold : 0.7;
|
|
786
790
|
try {
|
|
787
|
-
const data = await this.client.detectNsfw(field);
|
|
791
|
+
const data = await this.client.detectNsfw(field, {}, opts);
|
|
788
792
|
const score = typeof data?.output?.nsfw_score === 'number' ? data.output.nsfw_score : null;
|
|
789
793
|
return { ok: true, score, nsfw: score == null ? null : score >= threshold, error: null, raw: data };
|
|
790
794
|
} catch (err) {
|
package/src/core/Config.js
CHANGED
|
@@ -126,6 +126,12 @@ class Config {
|
|
|
126
126
|
this.curlImpersonatePath =
|
|
127
127
|
opts.curlImpersonatePath || process.env.DEEPAI_CURL_IMPERSONATE || null;
|
|
128
128
|
this.curlImpersonateTarget = opts.curlImpersonateTarget || 'chrome136';
|
|
129
|
+
// ---- /api/* anonymous fallback ----------------------------------------
|
|
130
|
+
// When a registered key is refused ("Pro members in good standing"),
|
|
131
|
+
// retry once with a fresh anonymous key in the browser dialect
|
|
132
|
+
// (options.anonymousExtraFields). Mirrors how the website keeps
|
|
133
|
+
// working for free visitors. Disable with anonymousApiFallback:false.
|
|
134
|
+
this.anonymousApiFallback = opts.anonymousApiFallback !== false;
|
|
129
135
|
|
|
130
136
|
// ---- Anonymous device identity ---------------------------------------
|
|
131
137
|
// Stable device identifier sent as the `deepai_device_id` cookie.
|
package/src/core/DeepAIClient.js
CHANGED
|
@@ -615,9 +615,48 @@ class DeepAIClient {
|
|
|
615
615
|
* @returns {Promise<object>} e.g. `{ id, output_url }`
|
|
616
616
|
*/
|
|
617
617
|
async runApi(name, fields = {}, options = {}) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
618
|
+
try {
|
|
619
|
+
const url = `${this.config.url('api')}/${String(name).replace(/^\/+/, '')}`;
|
|
620
|
+
const entries = this._buildApiFields(fields, options);
|
|
621
|
+
return await this._apiFormRequest(url, entries, options);
|
|
622
|
+
} catch (err) {
|
|
623
|
+
// A registered key refused for plan reasons ("Pro members in
|
|
624
|
+
// good standing") gets one anonymous browser-shaped retry, the
|
|
625
|
+
// same way the website keeps serving free visitors.
|
|
626
|
+
if (
|
|
627
|
+
err instanceof QuotaExceededError &&
|
|
628
|
+
this.config.anonymousApiFallback !== false &&
|
|
629
|
+
!options._anonymous &&
|
|
630
|
+
!this.usingTryItKey
|
|
631
|
+
) {
|
|
632
|
+
const anonFields = { ...(options.anonymousExtraFields || {}), ...fields };
|
|
633
|
+
if (this.config.debug) this.log.warn?.(`[AlexaAI] ${name} refused for the registered key; retrying anonymously`);
|
|
634
|
+
return this._runAnonymousApi(name, anonFields, options).catch((anonErr) => {
|
|
635
|
+
anonErr.message = `${err.message} | anonymous retry: ${anonErr.message}`;
|
|
636
|
+
throw anonErr;
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
throw err;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Run one `/api/<name>` call with a one-shot anonymous key regardless of
|
|
645
|
+
* the configured key. The active keys are swapped out for the duration
|
|
646
|
+
* of the call and restored afterwards.
|
|
647
|
+
* @private
|
|
648
|
+
*/
|
|
649
|
+
async _runAnonymousApi(name, fields, options = {}) {
|
|
650
|
+
const previousKeys = this._keys;
|
|
651
|
+
const previousIndex = this._keyIndex;
|
|
652
|
+
this._keys = [DeepAIClient.generateTryItKey(this.config.userAgent)];
|
|
653
|
+
this._keyIndex = 0;
|
|
654
|
+
try {
|
|
655
|
+
return await this.runApi(name, fields, { ...options, _anonymous: true });
|
|
656
|
+
} finally {
|
|
657
|
+
this._keys = previousKeys;
|
|
658
|
+
this._keyIndex = previousIndex;
|
|
659
|
+
}
|
|
621
660
|
}
|
|
622
661
|
|
|
623
662
|
/**
|
|
@@ -849,47 +888,45 @@ class DeepAIClient {
|
|
|
849
888
|
}
|
|
850
889
|
|
|
851
890
|
/**
|
|
852
|
-
* Run a classic `/api/<name>` call with a one-shot anonymous
|
|
853
|
-
* regardless of the configured key
|
|
854
|
-
* registered key is refused ("Pro members only"); a fresh key is minted
|
|
855
|
-
* for this single request.
|
|
891
|
+
* Run a classic `/api/<name>` call with a one-shot anonymous key,
|
|
892
|
+
* regardless of the configured key (public wrapper).
|
|
856
893
|
*/
|
|
857
894
|
async runApiWithTryItKey(name, fields = {}, options = {}) {
|
|
858
|
-
|
|
859
|
-
const previousIndex = this._keyIndex;
|
|
860
|
-
this._keys = [DeepAIClient.generateTryItKey(this.config.userAgent)];
|
|
861
|
-
this._keyIndex = 0;
|
|
862
|
-
try {
|
|
863
|
-
return await this.runApi(name, fields, options);
|
|
864
|
-
} finally {
|
|
865
|
-
this._keys = previousKeys;
|
|
866
|
-
this._keyIndex = previousIndex;
|
|
867
|
-
}
|
|
895
|
+
return this._runAnonymousApi(name, { ...(options.anonymousExtraFields || {}), ...fields }, { ...options, _anonymous: true });
|
|
868
896
|
}
|
|
869
897
|
|
|
870
898
|
/** Prompt-driven image edit (`/api/image-editor`). */
|
|
871
|
-
async editImage(image, text, extra = {}) {
|
|
872
|
-
return this.runApi(STANDARD_APIS.imageEditor, { image, text, ...extra });
|
|
899
|
+
async editImage(image, text, extra = {}, options = {}) {
|
|
900
|
+
return this.runApi(STANDARD_APIS.imageEditor, { image, text, ...extra }, options);
|
|
873
901
|
}
|
|
874
902
|
|
|
875
903
|
/** 4x upscale (`/api/torch-srgan`). */
|
|
876
|
-
async upscaleImage(image, extra = {}) {
|
|
877
|
-
return this.runApi(STANDARD_APIS.superResolution, { image, ...extra });
|
|
904
|
+
async upscaleImage(image, extra = {}, options = {}) {
|
|
905
|
+
return this.runApi(STANDARD_APIS.superResolution, { image, ...extra }, options);
|
|
878
906
|
}
|
|
879
907
|
|
|
880
908
|
/** Colourise a black-and-white photo (`/api/colorizer`). */
|
|
881
|
-
async colorizeImage(image, extra = {}) {
|
|
882
|
-
return this.runApi(STANDARD_APIS.colorizer, { image, ...extra }
|
|
909
|
+
async colorizeImage(image, extra = {}, options = {}) {
|
|
910
|
+
return this.runApi(STANDARD_APIS.colorizer, { image, ...extra }, {
|
|
911
|
+
anonymousExtraFields: { generation_source: 'img' },
|
|
912
|
+
...options,
|
|
913
|
+
});
|
|
883
914
|
}
|
|
884
915
|
|
|
885
916
|
/** NSFW score (`/api/nsfw-detector`). */
|
|
886
|
-
async detectNsfw(image, extra = {}) {
|
|
887
|
-
return this.runApi(STANDARD_APIS.nsfwDetector, { image, ...extra }
|
|
917
|
+
async detectNsfw(image, extra = {}, options = {}) {
|
|
918
|
+
return this.runApi(STANDARD_APIS.nsfwDetector, { image, ...extra }, {
|
|
919
|
+
anonymousExtraFields: { generation_source: 'img' },
|
|
920
|
+
...options,
|
|
921
|
+
});
|
|
888
922
|
}
|
|
889
923
|
|
|
890
924
|
/** Abstractive summary (`/api/summarization`). */
|
|
891
|
-
async summarize(text, extra = {}) {
|
|
892
|
-
return this.runApi(STANDARD_APIS.summarization, { text, ...extra }
|
|
925
|
+
async summarize(text, extra = {}, options = {}) {
|
|
926
|
+
return this.runApi(STANDARD_APIS.summarization, { text, ...extra }, {
|
|
927
|
+
anonymousExtraFields: { generation_source: 'img' },
|
|
928
|
+
...options,
|
|
929
|
+
});
|
|
893
930
|
}
|
|
894
931
|
|
|
895
932
|
/** Sentiment labels (`/api/sentiment-analysis`). */
|
|
@@ -154,7 +154,7 @@ class ImageDescriber {
|
|
|
154
154
|
sawRefusal = true;
|
|
155
155
|
this._modelsRefused.add(model);
|
|
156
156
|
if (this.config.debug) {
|
|
157
|
-
this.log.warn?.(`[AlexaAI] ${model} cannot see attachments
|
|
157
|
+
this.log.warn?.(`[AlexaAI] ${model} cannot see attachments - trying the next model`);
|
|
158
158
|
}
|
|
159
159
|
continue;
|
|
160
160
|
}
|
package/test/run-tests.js
CHANGED
|
@@ -695,7 +695,7 @@ section('DeepAIClient — the whole endpoint surface (mocked transport)');
|
|
|
695
695
|
const ai = new AlexaAI({ key: '11111111-2222-3333-4444-555555555555', postgresUrl: 'postgres://u:p@localhost/db', autoMigrate: false });
|
|
696
696
|
const result = await ai.generateImage('a cute orange cat', { aspectRatio: '16:9' });
|
|
697
697
|
global.fetch = realFetch;
|
|
698
|
-
ok('generateImage recovers
|
|
698
|
+
ok('generateImage recovers through the built-in anonymous fallback', result.ok === true && result.via === 'api');
|
|
699
699
|
const anon = calls.find((c) => DeepAIClient.isTryItKey(c.key));
|
|
700
700
|
ok(
|
|
701
701
|
'anonymous retry carries the browser fields',
|
|
@@ -707,6 +707,41 @@ section('DeepAIClient — the whole endpoint surface (mocked transport)');
|
|
|
707
707
|
anon.form.quality === 'true'
|
|
708
708
|
);
|
|
709
709
|
ok('share_url is preferred over output_url', result.url === 'https://deepai.org/generated-image.png');
|
|
710
|
+
|
|
711
|
+
// detectNsfw: same Pro refusal, same anonymous recovery
|
|
712
|
+
calls.length = 0;
|
|
713
|
+
global.fetch = async (url, init = {}) => {
|
|
714
|
+
const form = {};
|
|
715
|
+
for (const [k, v] of init.body.entries()) form[k] = v;
|
|
716
|
+
calls.push({ url, key: init.headers['api-key'], form });
|
|
717
|
+
const first = calls.filter((c) => /nsfw-detector$/.test(c.url)).length === 1;
|
|
718
|
+
return {
|
|
719
|
+
status: first ? 402 : 200,
|
|
720
|
+
headers: { get: () => 'application/json' },
|
|
721
|
+
text: async () =>
|
|
722
|
+
first
|
|
723
|
+
? JSON.stringify({ status: 'APIs are only available for Pro members in good standing' })
|
|
724
|
+
: JSON.stringify({ id: 'n1', output: { nsfw_score: 0.13 } }),
|
|
725
|
+
};
|
|
726
|
+
};
|
|
727
|
+
const nsfw = await ai.detectNsfw(Buffer.from('fake-image-bytes'));
|
|
728
|
+
global.fetch = realFetch;
|
|
729
|
+
ok('detectNsfw recovers through the anonymous fallback', nsfw.ok === true && nsfw.score === 0.13 && nsfw.nsfw === false);
|
|
730
|
+
const anonNsfw = calls.find((c) => DeepAIClient.isTryItKey(c.key));
|
|
731
|
+
ok('anonymous nsfw retry uses the model-page dialect', anonNsfw && anonNsfw.form.generation_source === 'img');
|
|
732
|
+
|
|
733
|
+
// anonymousApiFallback:false keeps the refusal
|
|
734
|
+
const strict = new AlexaAI({ key: '11111111-2222-3333-4444-555555555555', postgresUrl: 'postgres://u:p@localhost/db', autoMigrate: false, anonymousApiFallback: false });
|
|
735
|
+
calls.length = 0;
|
|
736
|
+
let fetchCalls = 0;
|
|
737
|
+
global.fetch = async (url, init = {}) => {
|
|
738
|
+
fetchCalls++;
|
|
739
|
+
return { status: 402, headers: { get: () => 'application/json' }, text: async () => JSON.stringify({ status: 'APIs are only available for Pro members in good standing' }) };
|
|
740
|
+
};
|
|
741
|
+
const refused = await strict.detectNsfw(Buffer.from('fake-image-bytes'));
|
|
742
|
+
global.fetch = realFetch;
|
|
743
|
+
ok('anonymousApiFallback:false surfaces the refusal', refused.ok === false && refused.error === 'DEEPAI_QUOTA_EXCEEDED');
|
|
744
|
+
ok('no anonymous retry was made', fetchCalls === 1);
|
|
710
745
|
}
|
|
711
746
|
|
|
712
747
|
{
|