alexa-ai 2.1.2 → 2.3.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 CHANGED
@@ -4,6 +4,59 @@ 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.3.0] — 2026-09-07
8
+
9
+ ### Added
10
+ - **Multiple `/api/*` transports** (`transport` option / `DEEPAI_TRANSPORT`):
11
+ `'auto'` (default) tries `fetch`, then system `curl`, then a
12
+ `curl-impersonate` binary (Chrome TLS profile) when one is available;
13
+ `'fetch' | 'curl' | 'impersonate'` pin a single transport. A refusal of the
14
+ form "Please try this model on deepai.org" can be specific to the client
15
+ TLS stack, so `auto` retries that error through the next transport — quota
16
+ and auth errors are never re-driven. `curlImpersonatePath` /
17
+ `DEEPAI_CURL_IMPERSONATE` locate the binary.
18
+ - `examples/diagnose.js` — runs the browser-shaped request through every
19
+ transport with fresh keys and reports which one the network accepts.
20
+ - `examples/text2img-standalone.js` gained `--transport fetch|curl|impersonate`
21
+ and `--imp <path>`.
22
+
23
+ ## [2.2.1] — 2026-09-07
24
+
25
+ ### Added
26
+ - Device identity: the client keeps a stable random `deepai_device_id`
27
+ (32 bytes, base64url) and sends it as a cookie; anonymous `/api/*`
28
+ generation is rate-limited per device. Override with `deviceId` /
29
+ `DEEPAI_DEVICE_ID`.
30
+ - `examples/text2img-standalone.js` — zero-dependency CLI for anonymous
31
+ image generation using the full browser request dialect.
32
+
33
+ ## [2.2.0] — 2026-09-07
34
+
35
+ ### Fixed
36
+ - Image generation with anonymous `tryit-…` keys. The key's hex part is a
37
+ deterministic hash over the User-Agent
38
+ (`H(UA + H(UA + H(UA + digits + SALT)))`, see `DeepAIClient._islandHash`);
39
+ `generateTryItKey(userAgent)` now derives it correctly, and `headers()`
40
+ mints a fresh key per request because anonymous keys are single-use.
41
+ - `generateImage()` now sends the browser dialect for anonymous requests
42
+ (`generation_source`, `width`/`height` mapped from `aspectRatio`,
43
+ `image_generator_version`, `quality`).
44
+
45
+ ### Added
46
+ - Anonymous browser-shaped retry when a registered non-Pro key is refused
47
+ (result `via: 'anonymous'`, `runApiWithTryItKey(name, fields)`); disable
48
+ with `{ noAnonymousFallback: true }`.
49
+ - `DEEPAI_KEY` env alias alongside `DEEPAI_API_KEY`.
50
+ - `DeepAIClient.isTryItKey(key)`.
51
+
52
+ ### Changed
53
+ - Extended refusal classification ("Pro members", "model only available…",
54
+ "insufficient_credits", …) so quota errors rotate keys and report
55
+ `DEEPAI_QUOTA_EXCEEDED` correctly.
56
+ - `share_url` preferred over `output_url` in generated-image results.
57
+ - Default `userAgent` updated (the tryit key hash is computed over it; keep
58
+ `userAgent` stable if you persist keys).
59
+
7
60
  ## [2.1.2] — 2026-09-06
8
61
 
9
62
  ### Changed
package/README.md CHANGED
@@ -427,14 +427,25 @@ await ai.deepai.runApi('waifu2x', { image: buffer }); // any /api/<name> e
427
427
 
428
428
  All media arguments accept the same shapes as `chat({ image })`.
429
429
 
430
- **`generateImage()` on a free key.** `POST /api/text2img` is a paid endpoint:
431
- anonymous keys receive `{"status": "Out of API credits"}`. The engine tries it
432
- first because it is fast and returns a plain URL; when it is refused, it
433
- drives the same in-chat `generate_image` tool the deepai.org web client uses,
434
- which works on free chat keys. The result reports the route that answered
435
- (`via: 'api' | 'chat'`). Options: `{ apiOnly }`, `{ chatToolOnly }`,
436
- `{ aspectRatio: '16:9' }` for the chat tool, and `width` / `height` /
437
- `image_generator_version` for the API.
430
+ **`generateImage()` on a free key.** Three routes are tried, in order:
431
+
432
+ 1. `POST /api/text2img` with your key. Pro keys succeed here immediately.
433
+ With an anonymous `tryit-…` key the engine automatically sends the exact
434
+ browser dialect (`generation_source=chat`, `width`/`height` mapped from
435
+ `aspectRatio`, `image_generator_version=hd`, `quality=true`) and mints a
436
+ **fresh single-use key per request** — tryit keys are hash-validated
437
+ against the User-Agent and burned after one request.
438
+ 2. If a registered (non-Pro) key is refused with `402 "Pro members in good
439
+ standing"`, one anonymous browser-shaped retry. (`noAnonymousFallback:
440
+ true` disables it.)
441
+ 3. The legacy in-chat `generate_image` function call (kept for model
442
+ versions that still honour it).
443
+
444
+ The result reports the route that answered (`via: 'api' | 'anonymous' |
445
+ 'chat'`). Options: `{ apiOnly }`, `{ chatToolOnly }`, `{ noAnonymousFallback }`,
446
+ `{ aspectRatio: '16:9' }`, and `width` / `height` / `image_generator_version`
447
+ for the API. The returned URL prefers `share_url` (public, stable) over
448
+ `output_url`, matching the browser client.
438
449
 
439
450
  **`summarizeText()`** follows the same pattern — `/api/summarization` first, a
440
451
  stateless chat request as the fallback.
@@ -841,10 +852,56 @@ error), and the reply came from DeepAI's own web access. Point
841
852
  `webSearchProvider` at a search API you control if the public endpoints are
842
853
  blocked where the bot runs.
843
854
 
855
+ **Works in the browser playground but fails in Postman / Node.js** — three
856
+ separate requirements:
857
+
858
+ 1. **Anonymous keys are single-use and User-Agent-bound.** Each key is
859
+ `tryit-<digits>-<hash>` with
860
+ `hash = H(UA + H(UA + H(UA + digits + "hackers_become_a_little_stinkier_every_time_they_hack")))`,
861
+ validated against the request's `User-Agent` header. A reused key or
862
+ random hex → `401 "Please pass a valid Api-Key"`. A fresh, correctly
863
+ hashed key must be minted per request — `headers()` does this
864
+ automatically for `tryit-…` keys.
865
+ 2. **The body must be `multipart/form-data`** (never JSON), with
866
+ `generation_source=img` (model page) or `generation_source=chat` +
867
+ `width`/`height`/`image_generator_version=hd`/`quality=true` (chat page).
868
+ Never set `Content-Type` manually — the multipart boundary is generated.
869
+ 3. **`Origin: https://deepai.org` is required**, and anonymous generation is
870
+ refused from datacenter/VPN IPs (`401 "Please try this model on deepai.org"`) —
871
+ run from a residential IP. Passing `deviceId` (the `deepai_device_id`
872
+ cookie value) also helps the per-device quota.
873
+
874
+ `examples/text2img-standalone.js` is a zero-dependency CLI implementing the
875
+ full recipe, and `examples/diagnose.js` runs the same request through every
876
+ transport on your machine and prints which one DeepAI accepts.
877
+
878
+ **`transport` option (2.3.0).** Some networks serve non-browser TLS stacks a
879
+ refusal (`"Please try this model on deepai.org"`) even with a perfectly valid
880
+ key. `/api/*` calls therefore support multiple transports:
881
+
882
+ ```js
883
+ new AlexaAI({ key, postgresUrl, transport: 'auto' }) // default: fetch → curl → curl-impersonate
884
+ new AlexaAI({ key, postgresUrl, transport: 'curl' }) // system curl only
885
+ new AlexaAI({ key, postgresUrl, transport: 'impersonate', curlImpersonatePath: 'C:/tools/curl-impersonate.exe' })
886
+ ```
887
+
888
+ With `transport: 'auto'` (the default) the engine starts with `fetch` and, on
889
+ a transport-specific refusal, retries the request through system `curl`
890
+ (shipped with Windows 10+, macOS and Linux) and then through a
891
+ `curl-impersonate` binary if one is on PATH (a Chrome TLS profile; set
892
+ `curlImpersonatePath` or `DEEPAI_CURL_IMPERSONATE` to point at it — builds:
893
+ github.com/lexiforest/curl-impersonate/releases). Quota and auth errors are
894
+ never re-driven through other transports. Run `node examples/diagnose.js` to
895
+ see which transport your network accepts.
896
+
844
897
  **`generateImage()` returns `{ ok: false, error: 'DEEPAI_QUOTA_EXCEEDED' }`**
845
- Both routes were refused: `/api/text2img` needs credits and the in-chat image
846
- tool hit the key's chat quota. Add more keys (`keys: [...]`) or wait for the
847
- quota to reset. `message` carries DeepAI's exact wording.
898
+ `/api/text2img` is Pro-only for registered keys ("APIs are only available for
899
+ Pro members in good standing"), the anonymous browser-shaped retry was
900
+ refused too (DeepAI soft-blocks free image generation from datacenter IPs
901
+ with "Please try this model on deepai.org" — run the bot from a residential
902
+ IP), and the in-chat tool produced no image. Add a Pro key, more keys
903
+ (`keys: [...]`), or run from a non-datacenter IP. `message` carries DeepAI's
904
+ exact wording for each attempted route.
848
905
 
849
906
  **Photos are answered with "I can't view images right now"**
850
907
  Native vision needs a paid DeepAI key; on a free key only text inside the
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * diagnose.js — DeepAI text2img connectivity diagnostic.
5
+ *
6
+ * Run ON THE MACHINE where the browser playground works:
7
+ *
8
+ * node examples/diagnose.js
9
+ * node examples/diagnose.js --device-id <your deepai_device_id cookie>
10
+ * node examples/diagnose.js --imp /path/to/curl-impersonate
11
+ *
12
+ * Each test sends the browser-shaped request with a FRESH single-use key
13
+ * through a different transport and prints the raw server response, so one
14
+ * run shows exactly which transport DeepAI accepts on your network:
15
+ *
16
+ * 1. Node fetch (the library default)
17
+ * 2. system curl
18
+ * 3. curl-impersonate with a Chrome TLS profile (if the binary is found)
19
+ *
20
+ * Zero dependencies, Node 18+.
21
+ */
22
+
23
+ const API = 'https://api.deepai.org/api/text2img';
24
+ const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36';
25
+ const PROFILE_UA = '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';
26
+ const SALT = 'hackers_become_a_little_stinkier_every_time_they_hack';
27
+
28
+ // ---- key hash (same algorithm as DeepAIClient._islandHash) ----------------
29
+ function islandHash(input) {
30
+ const a = [];
31
+ for (let b = 0; 64 > b; ) a[b] = 0 | (4294967296 * Math.sin(++b % Math.PI));
32
+ let d, e, f, g = [(d = 1732584193), (e = 4023233417), ~d, ~e], h = [];
33
+ const l = unescape(encodeURI(input)) + '\u0080';
34
+ let k = l.length;
35
+ let c = (--k / 4 + 2) | 15;
36
+ for (h[--c] = 8 * k; ~k; ) h[k >> 2] |= l.charCodeAt(k) << (8 * k--);
37
+ for (let b = 0, m = 0; b < c; b += 16) {
38
+ for (k = g; 64 > m; k = [ (f = k[3]), d + (((f = k[0] + [d & e | ~d & f, f & d | ~f & e, d ^ e ^ f, e ^ (d | ~f)][(k = m >> 4)] + a[m] + ~~h[b | [m, 5 * m + 1, 3 * m + 5, 7 * m][k] & 15]) << (k = [7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21][4 * k + (m++ % 4)])) | (f >>> -k)), d, e ]) {
39
+ d = k[1] | 0;
40
+ e = k[2];
41
+ }
42
+ for (m = 4; m; ) g[--m] += k[m];
43
+ }
44
+ let result = '';
45
+ for (let i = 0; 32 > i; ) result += ((g[i >> 3] >> 4 * (1 ^ i++)) & 15).toString(16);
46
+ return result.split('').reverse().join('');
47
+ }
48
+ const freshKey = (ua) => {
49
+ const digits = String(Math.round(Math.random() * 100000000000));
50
+ return `tryit-${digits}-${islandHash(ua + islandHash(ua + islandHash(ua + digits + SALT)))}`;
51
+ };
52
+
53
+ // ---- args -------------------------------------------------------------------
54
+ const args = process.argv.slice(2);
55
+ const opt = {};
56
+ for (let i = 0; i < args.length; i++) {
57
+ if (args[i] === '--device-id') opt.deviceId = args[++i];
58
+ else if (args[i] === '--imp') opt.imp = args[++i];
59
+ }
60
+ const deviceId = opt.deviceId || Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64url');
61
+ const cookie = `deepai_device_id=${deviceId}`;
62
+
63
+ // ---- transports ---------------------------------------------------------------
64
+ async function viaFetch() {
65
+ const form = new FormData();
66
+ form.append('text', 'a small red boat on a calm lake');
67
+ form.append('generation_source', 'img');
68
+ const res = await fetch(API, {
69
+ method: 'POST', body: form,
70
+ headers: { 'api-key': freshKey(UA), 'User-Agent': UA, Origin: 'https://deepai.org', Referer: 'https://deepai.org/machine-learning-model/text2img', Accept: '*/*', Cookie: cookie },
71
+ });
72
+ return { status: res.status, body: await res.text() };
73
+ }
74
+
75
+ function runCurl(binary, impersonate) {
76
+ const { execFileSync } = require('child_process');
77
+ const ua = impersonate ? PROFILE_UA : UA;
78
+ const a = impersonate ? ['--impersonate', 'chrome136'] : [];
79
+ a.push(API, '-sS', '--compressed', '--max-time', '60', '-X', 'POST',
80
+ '-H', `api-key: ${freshKey(ua)}`);
81
+ if (!impersonate) a.push('-H', `User-Agent: ${ua}`);
82
+ a.push('-H', 'Origin: https://deepai.org',
83
+ '-H', 'Referer: https://deepai.org/machine-learning-model/text2img',
84
+ '-H', 'Accept: */*',
85
+ '-H', `Cookie: ${cookie}`,
86
+ '-F', 'text=a small red boat on a calm lake',
87
+ '-F', 'generation_source=img',
88
+ '-w', '\n%{http_code}');
89
+ const out = execFileSync(binary, a, { encoding: 'utf8', timeout: 60000, stdio: ['ignore', 'pipe', 'pipe'] }).replace(/\r/g, '');
90
+ const cut = out.lastIndexOf('\n');
91
+ return { status: Number(out.slice(cut + 1).trim()), body: out.slice(0, cut) };
92
+ }
93
+
94
+ function label(t) {
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 `✅ SUCCESS — image generated (HTTP 200)`;
97
+ if (/valid Api-Key/i.test(b)) return `❌ key rejected (unexpected — fresh key was used)`;
98
+ if (/try this model/i.test(b)) return `❌ transport refused ("Please try this model on deepai.org")`;
99
+ if (/Pro members/i.test(b)) return `❌ account-level refusal (needs a Pro key)`;
100
+ if (/try it exceeded/i.test(b)) return `⚠️ free quota exhausted for this device/IP — retry later or change --device-id`;
101
+ return `❌ HTTP ${t.status}: ${b}`;
102
+ }
103
+
104
+ (async () => {
105
+ console.log(`DeepAI text2img diagnostic — device ${deviceId.slice(0, 8)}…\n`);
106
+ const rows = [];
107
+ try { rows.push(['1. Node fetch (library default)', label(await viaFetch())]); } catch (e) { rows.push(['1. Node fetch', `⚠️ ${e.message}`]); }
108
+ try { rows.push(['2. system curl', label(runCurl('curl', false))]); } catch (e) { rows.push(['2. system curl', `⚠️ ${e.message.split('\n')[0]}`]); }
109
+
110
+ let impBin = opt.imp;
111
+ if (!impBin) {
112
+ const { execFileSync } = require('child_process');
113
+ for (const name of ['curl-impersonate', 'curl-impersonate.exe']) {
114
+ try { execFileSync(name, ['--version'], { timeout: 5000, stdio: 'ignore' }); impBin = name; break; } catch { /* keep looking */ }
115
+ }
116
+ }
117
+ if (impBin) {
118
+ try { rows.push([`3. curl-impersonate (${impBin})`, label(runCurl(impBin, true))]); } catch (e) { rows.push(['3. curl-impersonate', `⚠️ ${e.message.split('\n')[0]}`]); }
119
+ } else {
120
+ rows.push(['3. curl-impersonate', '⏭ skipped — binary not found (see README for install)']);
121
+ }
122
+
123
+ for (const [name, v] of rows) console.log(`${name.padEnd(34)} ${v}`);
124
+ console.log(`
125
+ Reading the results:
126
+ - A ✅ on ANY line → that transport works; use it (library: transport option,
127
+ standalone: --transport).
128
+ - ❌ on line 1 only → non-browser TLS stack refused; use 'curl' or
129
+ 'impersonate'.
130
+ - ❌ on lines 1+2, ✅ on 3 → strict browser-TLS matching; use 'impersonate'.
131
+ - ❌ everywhere → the IP is refused for anonymous generation, or the free
132
+ quota is exhausted. Compare with the browser: DevTools
133
+ → Network → generate → text2img request → Response.`);
134
+ })();
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * text2img-standalone.js — zero-dependency DeepAI text-to-image CLI.
5
+ *
6
+ * Speaks the anonymous browser dialect: a fresh single-use `tryit-…` key
7
+ * (hashed over the User-Agent) per run, browser-identical headers and a
8
+ * multipart/form-data body, plus a stable `deepai_device_id` cookie.
9
+ *
10
+ * Usage:
11
+ * node examples/text2img-standalone.js "a cute orange cat"
12
+ * node examples/text2img-standalone.js "a cat" --out cat.jpg
13
+ * node examples/text2img-standalone.js "a cat" --aspect 16:9
14
+ * node examples/text2img-standalone.js "a cat" --device-id <cookieValue>
15
+ * node examples/text2img-standalone.js "a cat" --key <proKey>
16
+ * node examples/text2img-standalone.js "a cat" --transport curl
17
+ * node examples/text2img-standalone.js "a cat" --transport impersonate --imp /path/to/curl-impersonate
18
+ *
19
+ * Transports: 'fetch' (default, Node fetch) | 'curl' (system curl) |
20
+ * 'impersonate' (curl-impersonate binary, Chrome TLS profile). Some
21
+ * networks serve non-browser TLS stacks a refusal — if 'fetch' fails with
22
+ * "Please try this model on deepai.org", try 'curl', then 'impersonate'.
23
+ *
24
+ * Requires Node.js 18+. Anonymous generation is refused from
25
+ * datacenter/VPN IPs; run from a residential network.
26
+ */
27
+
28
+ const API_URL = 'https://api.deepai.org/api/text2img';
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 — the key
31
+ // hash is computed over it and the server recomputes it from the request.
32
+ const USER_AGENT =
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
+
35
+ // ---------------------------------------------------------------------------
36
+ // Deterministic key hash (see DeepAIClient._islandHash in the engine).
37
+ // The integer/bit-level behaviour is intentional — do not simplify it.
38
+ // ---------------------------------------------------------------------------
39
+ function islandHash(input) {
40
+ const a = [];
41
+ for (let b = 0; 64 > b; ) a[b] = 0 | (4294967296 * Math.sin(++b % Math.PI));
42
+ let d, e, f, g = [(d = 1732584193), (e = 4023233417), ~d, ~e], h = [];
43
+ const l = unescape(encodeURI(input)) + '€';
44
+ let k = l.length;
45
+ let c = (--k / 4 + 2) | 15;
46
+ for (h[--c] = 8 * k; ~k; ) h[k >> 2] |= l.charCodeAt(k) << (8 * k--);
47
+ for (let b = 0, m = 0; b < c; b += 16) {
48
+ for (k = g; 64 > m; k = [ (f = k[3]), d + (((f = k[0] + [d & e | ~d & f, f & d | ~f & e, d ^ e ^ f, e ^ (d | ~f)][(k = m >> 4)] + a[m] + ~~h[b | [m, 5 * m + 1, 3 * m + 5, 7 * m][k] & 15]) << (k = [7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21][4 * k + (m++ % 4)])) | (f >>> -k)), d, e ]) {
49
+ d = k[1] | 0;
50
+ e = k[2];
51
+ }
52
+ for (m = 4; m; ) g[--m] += k[m];
53
+ }
54
+ let result = '';
55
+ for (let i = 0; 32 > i; ) result += ((g[i >> 3] >> 4 * (1 ^ i++)) & 15).toString(16);
56
+ return result.split('').reverse().join('');
57
+ }
58
+
59
+ /** Mint a fresh anonymous key, valid for exactly ONE request with USER_AGENT. */
60
+ function freshTryItKey(userAgent = USER_AGENT) {
61
+ const digits = String(Math.round(Math.random() * 100000000000));
62
+ const H = islandHash;
63
+ return `tryit-${digits}-${H(userAgent + H(userAgent + H(userAgent + digits + SALT)))}`;
64
+ }
65
+
66
+ /** Random device id, same shape the site sets as the deepai_device_id cookie. */
67
+ function randomDeviceId() {
68
+ return Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64url');
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // CLI
73
+ // ---------------------------------------------------------------------------
74
+ function parseArgs(argv) {
75
+ const opts = { prompt: '', out: null, key: null, deviceId: randomDeviceId(), aspect: null, genSource: 'img', transport: 'fetch', imp: null };
76
+ for (let i = 0; i < argv.length; i++) {
77
+ const a = argv[i];
78
+ if (a === '--out') opts.out = argv[++i];
79
+ else if (a === '--key') opts.key = argv[++i];
80
+ else if (a === '--device-id') opts.deviceId = argv[++i];
81
+ else if (a === '--aspect') opts.aspect = argv[++i];
82
+ else if (a === '--chat-source') opts.genSource = 'chat';
83
+ else if (a === '--transport') opts.transport = argv[++i];
84
+ else if (a === '--imp') opts.imp = argv[++i];
85
+ else if (a === '--help' || a === '-h') opts.help = true;
86
+ else if (!opts.prompt) opts.prompt = a;
87
+ }
88
+ return opts;
89
+ }
90
+
91
+ const ASPECTS = { '16:9': [832, 448], '4:3': [768, 576], '1:1': [640, 640], '3:4': [576, 768], '9:16': [448, 832] };
92
+
93
+ async function main() {
94
+ const opts = parseArgs(process.argv.slice(2));
95
+ if (opts.help || !opts.prompt) {
96
+ console.log('Usage: node examples/text2img-standalone.js "your prompt" [--out file.jpg] [--key PRO_KEY] [--device-id VALUE] [--aspect 16:9|1:1|9:16|4:3|3:4] [--chat-source] [--transport fetch|curl|impersonate] [--imp PATH]');
97
+ process.exit(opts.help ? 0 : 1);
98
+ }
99
+
100
+ // ---- body: multipart/form-data, NEVER JSON -----------------------------
101
+ const form = new FormData();
102
+ form.append('text', opts.prompt);
103
+ form.append('generation_source', opts.genSource); // 'img' = model page, 'chat' = chat page
104
+ if (opts.aspect && ASPECTS[opts.aspect]) {
105
+ // the chat-page dialect maps aspect ratios to pixel sizes
106
+ const [w, h] = ASPECTS[opts.aspect];
107
+ form.append('width', String(w));
108
+ form.append('height', String(h));
109
+ form.append('image_generator_version', 'hd');
110
+ form.append('quality', 'true');
111
+ }
112
+
113
+ // ---- headers: fresh single-use anonymous key (or your Pro key) ---------
114
+ const apiKey = opts.key || freshTryItKey();
115
+ const headers = {
116
+ 'api-key': apiKey,
117
+ 'User-Agent': USER_AGENT, // MUST match the UA the key was hashed with
118
+ Origin: 'https://deepai.org',
119
+ Referer: 'https://deepai.org/machine-learning-model/text2img',
120
+ Accept: '*/*',
121
+ 'Accept-Language': 'en-US,en;q=0.9',
122
+ Cookie: `deepai_device_id=${opts.deviceId}`,
123
+ // NOTE: do NOT set Content-Type yourself — undici adds the multipart
124
+ // boundary. A manual Content-Type without the boundary is rejected.
125
+ };
126
+
127
+ console.log(`Prompt : ${opts.prompt}`);
128
+ console.log(`Key : ${opts.key ? '(registered key — needs Pro)' : apiKey + ' (fresh, single-use)'}`);
129
+ console.log('POST : ' + API_URL);
130
+
131
+ let res;
132
+ if (opts.transport === 'curl' || opts.transport === 'impersonate') {
133
+ const { execFile } = require('child_process');
134
+ const binary = opts.transport === 'impersonate' ? (opts.imp || 'curl-impersonate') : 'curl';
135
+ // the chrome136 profile sends its own Mac Chrome UA — the anonymous
136
+ // key hash must be derived from exactly that UA
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
+ const apiKey = opts.key || freshTryItKey(opts.transport === 'impersonate' ? profileUa : USER_AGENT);
139
+ const curlArgs = [];
140
+ if (opts.transport === 'impersonate') curlArgs.push('--impersonate', 'chrome136');
141
+ curlArgs.push(API_URL, '-sS', '--compressed', '--max-time', '120', '-X', 'POST',
142
+ '-H', `api-key: ${apiKey}`);
143
+ if (opts.transport !== 'impersonate') curlArgs.push('-H', `User-Agent: ${USER_AGENT}`);
144
+ curlArgs.push(
145
+ '-H', `Origin: ${headers.Origin}`,
146
+ '-H', `Referer: ${headers.Referer}`,
147
+ '-H', 'Accept: */*',
148
+ '-H', `Cookie: deepai_device_id=${opts.deviceId}`,
149
+ '-w', '\n%{http_code}'
150
+ );
151
+ for (const [k, v] of form.entries()) curlArgs.push('-F', `${k}=${v}`);
152
+ res = await new Promise((resolve, reject) => {
153
+ execFile(binary, curlArgs, { timeout: 120000, maxBuffer: 32 * 1024 * 1024 }, (err, stdout) => {
154
+ if (err && stdout == null) return reject(err);
155
+ const body = String(stdout).replace(/\r/g, '');
156
+ const cut = body.lastIndexOf('\n');
157
+ const status = Number(body.slice(cut + 1).trim());
158
+ resolve({ status, ok: status < 300, text: async () => body.slice(0, cut) });
159
+ });
160
+ });
161
+ } else {
162
+ res = await fetch(API_URL, { method: 'POST', headers, body: form });
163
+ }
164
+ const raw = await res.text();
165
+ let data = null;
166
+ try { data = JSON.parse(raw); } catch { /* non-JSON */ }
167
+
168
+ if (!res.ok || data?.err || (data?.status && !data.share_url && !data.output_url)) {
169
+ console.error(`\nFAILED HTTP ${res.status}`);
170
+ console.error(raw.slice(0, 500));
171
+ const s = String(data?.status || data?.err || '');
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
+ process.exit(1);
177
+ }
178
+
179
+ const url = data.share_url || data.output_url;
180
+ console.log(`\nOK HTTP ${res.status}`);
181
+ console.log(`Image : ${url}`);
182
+ if (data.id) console.log(`ID : ${data.id}`);
183
+
184
+ // ---- optional: download the image --------------------------------------
185
+ const out = opts.out || `deepai-${Date.now()}.jpg`;
186
+ try {
187
+ const img = await fetch(url);
188
+ if (img.ok) {
189
+ const buf = Buffer.from(await img.arrayBuffer());
190
+ require('fs').writeFileSync(out, buf);
191
+ console.log(`Saved : ${out} (${(buf.length / 1024).toFixed(1)} KB)`);
192
+ } else {
193
+ console.log(`(download skipped: HTTP ${img.status} — open the URL above in a browser)`);
194
+ }
195
+ } catch (e) {
196
+ console.log(`(download failed: ${e.message} — open the URL above in a browser)`);
197
+ }
198
+ }
199
+
200
+ main().catch((e) => { console.error('ERROR:', e.message); process.exit(1); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alexa-ai",
3
- "version": "2.1.2",
3
+ "version": "2.3.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": {