alexa-ai 2.1.1 → 2.2.1
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 +74 -0
- package/README.md +124 -31
- package/examples/text2img-standalone.js +159 -0
- package/index.js +2 -0
- package/package.json +2 -1
- package/src/AlexaAI.js +198 -50
- package/src/core/Config.js +23 -2
- package/src/core/DeepAIClient.js +119 -11
- package/src/services/WebAnswer.js +153 -22
- package/src/services/WebSearch.js +428 -0
- package/test/fakes.js +171 -0
- package/test/run-tests.js +1113 -0
- package/test/wrapper-methods.js +986 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,80 @@ 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.2.1] — 2026-09-07
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Device identity: the client keeps a stable random `deepai_device_id`
|
|
11
|
+
(32 bytes, base64url) and sends it as a cookie; anonymous `/api/*`
|
|
12
|
+
generation is rate-limited per device. Override with `deviceId` /
|
|
13
|
+
`DEEPAI_DEVICE_ID`.
|
|
14
|
+
- `examples/text2img-standalone.js` — zero-dependency CLI for anonymous
|
|
15
|
+
image generation using the full browser request dialect.
|
|
16
|
+
|
|
17
|
+
## [2.2.0] — 2026-09-07
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Image generation with anonymous `tryit-…` keys. The key's hex part is a
|
|
21
|
+
deterministic hash over the User-Agent
|
|
22
|
+
(`H(UA + H(UA + H(UA + digits + SALT)))`, see `DeepAIClient._islandHash`);
|
|
23
|
+
`generateTryItKey(userAgent)` now derives it correctly, and `headers()`
|
|
24
|
+
mints a fresh key per request because anonymous keys are single-use.
|
|
25
|
+
- `generateImage()` now sends the browser dialect for anonymous requests
|
|
26
|
+
(`generation_source`, `width`/`height` mapped from `aspectRatio`,
|
|
27
|
+
`image_generator_version`, `quality`).
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- Anonymous browser-shaped retry when a registered non-Pro key is refused
|
|
31
|
+
(result `via: 'anonymous'`, `runApiWithTryItKey(name, fields)`); disable
|
|
32
|
+
with `{ noAnonymousFallback: true }`.
|
|
33
|
+
- `DEEPAI_KEY` env alias alongside `DEEPAI_API_KEY`.
|
|
34
|
+
- `DeepAIClient.isTryItKey(key)`.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- Extended refusal classification ("Pro members", "model only available…",
|
|
38
|
+
"insufficient_credits", …) so quota errors rotate keys and report
|
|
39
|
+
`DEEPAI_QUOTA_EXCEEDED` correctly.
|
|
40
|
+
- `share_url` preferred over `output_url` in generated-image results.
|
|
41
|
+
- Default `userAgent` updated (the tryit key hash is computed over it; keep
|
|
42
|
+
`userAgent` stable if you persist keys).
|
|
43
|
+
|
|
44
|
+
## [2.1.2] — 2026-09-06
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
- **`searchWeb()` now searches the web itself** instead of trusting DeepAI's
|
|
48
|
+
server-side web access. Observed live with `gpt-4o-mini`: the same query
|
|
49
|
+
produced one report without any sources and one with four invented links,
|
|
50
|
+
because the model skipped the search and wrote from memory. The engine now
|
|
51
|
+
queries Bing, Bing News, Wikipedia, Google News and DuckDuckGo in parallel
|
|
52
|
+
(no API key needed; the four feeds were verified live on 2026-09-06 —
|
|
53
|
+
DuckDuckGo's html endpoint serves a bot challenge from data-centre IPs, so
|
|
54
|
+
its lite endpoint is tried first and a challenge page yields no results), hands the results to the model as numbered material, and asks
|
|
55
|
+
it to cite result numbers and never write URLs. The `*Sources:*` block is
|
|
56
|
+
built from the search results only — cited first — so a link the model made
|
|
57
|
+
up can never reach the chat. Citation markers are removed from the prose;
|
|
58
|
+
URLs the model still types are turned into citations when they match a
|
|
59
|
+
result and dropped otherwise.
|
|
60
|
+
- Result gained `grounded` (did the engine's search answer?), `providers`,
|
|
61
|
+
`via` (`'model'` | `'digest'`), and each source carries `date`, `provider`
|
|
62
|
+
and `cited`.
|
|
63
|
+
- New options: `results` (bring your own search API's results), `search:false`,
|
|
64
|
+
`providers`, `maxResults`; constructor options `webSearch`,
|
|
65
|
+
`webSearchProviders`, `webSearchTimeout`, `webSearchResults`,
|
|
66
|
+
`webSearchProvider(query, { maxResults, signal })`.
|
|
67
|
+
- When every provider fails the request falls back to DeepAI's web access as
|
|
68
|
+
before (`grounded:false`); when the search worked but the model call failed,
|
|
69
|
+
a plain digest of the results is returned (`via:'digest'`, still `ok:true`).
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
- `WebSearch` service (exported) with parsers for the DuckDuckGo html/lite
|
|
73
|
+
pages, RSS 2.0 news feeds and the MediaWiki search API, redirect unwrapping
|
|
74
|
+
(`duckduckgo.com/l/?uddg=`, `bing.com/news/apiclick.aspx?url=`), ad and
|
|
75
|
+
duplicate filtering, and provider round-robin.
|
|
76
|
+
- `WebAnswer.formatResults()`, `extractCitations()`, `stripUrls()`, `digest()`.
|
|
77
|
+
- The npm package now ships the test suite (`test/`), so the published
|
|
78
|
+
tarball is a complete snapshot of the repository at the tagged version:
|
|
79
|
+
`npm explore alexa-ai -- npm test`.
|
|
80
|
+
|
|
7
81
|
## [2.1.1] — 2026-09-06
|
|
8
82
|
|
|
9
83
|
### Fixed
|
package/README.md
CHANGED
|
@@ -427,23 +427,39 @@ 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.**
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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.
|
|
441
452
|
|
|
442
|
-
**`searchWeb()`** is a one-off research request
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
453
|
+
**`searchWeb()`** is a one-off research request that touches no user's
|
|
454
|
+
memory or history, so it can be called with no jid at all. The engine does the
|
|
455
|
+
searching itself — Bing, Bing News, Wikipedia, Google News and DuckDuckGo, in
|
|
456
|
+
parallel, no API key needed — and hands the results to the model as numbered
|
|
457
|
+
material. The model writes the report from them and cites result numbers; it
|
|
458
|
+
is told never to write a URL. The `*Sources:*` block is then built from the
|
|
459
|
+
search results only (cited ones first), so every link the bot shows is a page
|
|
460
|
+
that actually came back from a search, never one the model made up. The
|
|
461
|
+
default answer is long-form and ready to send to WhatsApp — an intro, three to
|
|
462
|
+
five `*Heading:*` sections with numbered `*Title*: detail` points, and one
|
|
447
463
|
`*Sources:*` block at the end:
|
|
448
464
|
|
|
449
465
|
```
|
|
@@ -471,15 +487,25 @@ answer still comes back under `minWords` (default 150), one follow-up turn asks
|
|
|
471
487
|
the model to rewrite it in full, and the longer reply wins — `attempts` and
|
|
472
488
|
`words` in the result show what happened.
|
|
473
489
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
490
|
+
The result tells you which path answered:
|
|
491
|
+
|
|
492
|
+
```js
|
|
493
|
+
const r = await ai.searchWeb('coffee');
|
|
494
|
+
// r.grounded true → the engine searched; sources are real search results
|
|
495
|
+
// false → no search results (host offline / providers blocked);
|
|
496
|
+
// DeepAI's own web access answered, and its sources are
|
|
497
|
+
// whatever the model reported — treat them with care
|
|
498
|
+
// r.providers ['bing', 'bing-news', 'wikipedia', 'google-news', 'duckduckgo'] — who answered
|
|
499
|
+
// r.sources [{ title, url, description, date, provider, cited }]
|
|
500
|
+
// r.via 'model' | 'digest' (the model failed but the search worked:
|
|
501
|
+
// a plain list of the results is returned)
|
|
502
|
+
// r.words, r.attempts, r.model
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
Sentences in which the model talks about itself — *"I'm a language model"*,
|
|
506
|
+
*"I can't browse the web"*, *"based on my training data"* — are removed, and
|
|
507
|
+
leftover template placeholders are dropped. Third-party names in the research
|
|
508
|
+
itself (news about OpenAI or Google) are kept verbatim.
|
|
483
509
|
|
|
484
510
|
```js
|
|
485
511
|
await ai.searchWeb('coffee', {
|
|
@@ -489,9 +515,37 @@ await ai.searchWeb('coffee', {
|
|
|
489
515
|
maxSources: 3, // how many to list in `text` (the array is not capped)
|
|
490
516
|
language: 'Sinhala', // answer language (default: the language of the query)
|
|
491
517
|
instructions: 'focus on Sri Lanka',
|
|
518
|
+
providers: ['bing-news', 'google-news'], // only news for this call
|
|
519
|
+
maxResults: 5, // results handed to the model (default 8)
|
|
520
|
+
search: false, // skip the built-in search, use DeepAI's web access
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// Bring your own search API (Brave, Serper, Tavily, …): pass its results and
|
|
524
|
+
// the built-in search is skipped — the sources block is built from them.
|
|
525
|
+
await ai.searchWeb('coffee', {
|
|
526
|
+
results: [{ title, url, description, date }],
|
|
492
527
|
});
|
|
493
528
|
```
|
|
494
529
|
|
|
530
|
+
Engine-wide settings in the constructor:
|
|
531
|
+
|
|
532
|
+
```js
|
|
533
|
+
new AlexaAI({
|
|
534
|
+
webSearch: true, // false = never search; always use DeepAI's
|
|
535
|
+
webSearchProviders: ['bing', 'bing-news', 'wikipedia', 'google-news', 'duckduckgo'],
|
|
536
|
+
webSearchTimeout: 8000, // per provider, ms
|
|
537
|
+
webSearchResults: 8, // results handed to the model
|
|
538
|
+
webSearchProvider: async (query, { maxResults, signal }) => [...], // replace the built-ins
|
|
539
|
+
});
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
The built-in providers are public pages and feeds, so they can change or be
|
|
543
|
+
rate-limited; each one is best-effort and a failure just means fewer results.
|
|
544
|
+
When none return anything the request falls back to DeepAI's server-side web
|
|
545
|
+
access — which on the free models is exactly the behaviour that motivated this
|
|
546
|
+
design: it often skips the search and writes a plausible report with invented
|
|
547
|
+
links. Check `grounded` if that matters to you.
|
|
548
|
+
|
|
495
549
|
### Moderation and administration
|
|
496
550
|
|
|
497
551
|
```js
|
|
@@ -601,7 +655,8 @@ AlexaAI orchestrator; the only class the bot touches
|
|
|
601
655
|
│ no "Alexa Mini", no self-denial)
|
|
602
656
|
├── AmnesiaGuard never lets her deny a memory she actually has
|
|
603
657
|
├── ImageDescriber vision chain: documents -> DeepAI -> OCR -> fallback
|
|
604
|
-
├──
|
|
658
|
+
├── WebSearch the engine's own web search (Bing, Bing/Google News, Wikipedia, DuckDuckGo)
|
|
659
|
+
├── WebAnswer searchWeb prompt, citation handling, sources block
|
|
605
660
|
├── Media normalises every media input shape
|
|
606
661
|
└── JidParser normalises @lid / @s.whatsapp.net / @g.us
|
|
607
662
|
```
|
|
@@ -770,7 +825,7 @@ refresh it automatically. Reinstall and verify:
|
|
|
770
825
|
```bash
|
|
771
826
|
npm uninstall alexa-ai
|
|
772
827
|
npm install github:AlexaInc/deepai
|
|
773
|
-
node -e "console.log(require('alexa-ai').version)" # must print 2.1.
|
|
828
|
+
node -e "console.log(require('alexa-ai').version)" # must print 2.1.2 or newer
|
|
774
829
|
```
|
|
775
830
|
|
|
776
831
|
`AlexaAI.version` and `AlexaAI.methods()` let the bot assert this at startup;
|
|
@@ -780,15 +835,53 @@ node -e "console.log(require('alexa-ai').version)" # must print 2.1.1 or newer
|
|
|
780
835
|
`text`**
|
|
781
836
|
Fixed in 2.1.1. Earlier builds asked the model for *"a short, direct answer"*
|
|
782
837
|
and only read sources from DeepAI's structured packet, which `gpt-4o-mini`
|
|
783
|
-
does not send.
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
838
|
+
does not send. If a particular model still answers briefly, check `attempts` /
|
|
839
|
+
`words` in the result: the engine retries once below `minWords`, and a
|
|
840
|
+
persistently short model is best swapped with the `model` option.
|
|
841
|
+
|
|
842
|
+
**`searchWeb()` sometimes has no sources, or the sources are links that do
|
|
843
|
+
not exist**
|
|
844
|
+
Fixed in 2.1.2. Before, the model was both researcher and writer: DeepAI's
|
|
845
|
+
server-side search ran unreliably on the free models, so the model often
|
|
846
|
+
wrote from memory — one run had no sources, the next had four invented ones.
|
|
847
|
+
The engine now searches the web itself and the sources block is built only
|
|
848
|
+
from those results. If `sources` is still empty, look at `grounded` in the
|
|
849
|
+
result: `false` means none of the search providers answered from your host
|
|
850
|
+
(firewall, DNS, rate limit — enable `debug:true` to see each provider's
|
|
851
|
+
error), and the reply came from DeepAI's own web access. Point
|
|
852
|
+
`webSearchProvider` at a search API you control if the public endpoints are
|
|
853
|
+
blocked where the bot runs.
|
|
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.
|
|
787
876
|
|
|
788
877
|
**`generateImage()` returns `{ ok: false, error: 'DEEPAI_QUOTA_EXCEEDED' }`**
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
878
|
+
`/api/text2img` is Pro-only for registered keys ("APIs are only available for
|
|
879
|
+
Pro members in good standing"), the anonymous browser-shaped retry was
|
|
880
|
+
refused too (DeepAI soft-blocks free image generation from datacenter IPs
|
|
881
|
+
with "Please try this model on deepai.org" — run the bot from a residential
|
|
882
|
+
IP), and the in-chat tool produced no image. Add a Pro key, more keys
|
|
883
|
+
(`keys: [...]`), or run from a non-datacenter IP. `message` carries DeepAI's
|
|
884
|
+
exact wording for each attempted route.
|
|
792
885
|
|
|
793
886
|
**Photos are answered with "I can't view images right now"**
|
|
794
887
|
Native vision needs a paid DeepAI key; on a free key only text inside the
|
|
@@ -0,0 +1,159 @@
|
|
|
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
|
+
*
|
|
17
|
+
* Requires Node.js 18+ (global fetch). Anonymous generation is refused
|
|
18
|
+
* from datacenter/VPN IPs; run from a residential network.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const API_URL = 'https://api.deepai.org/api/text2img';
|
|
22
|
+
const SALT = 'hackers_become_a_little_stinkier_every_time_they_hack';
|
|
23
|
+
// Keep this EXACT string in sync with the User-Agent header below — the key
|
|
24
|
+
// hash is computed over it and the server recomputes it from the request.
|
|
25
|
+
const USER_AGENT =
|
|
26
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36';
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Deterministic key hash (see DeepAIClient._islandHash in the engine).
|
|
30
|
+
// The integer/bit-level behaviour is intentional — do not simplify it.
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
function islandHash(input) {
|
|
33
|
+
const a = [];
|
|
34
|
+
for (let b = 0; 64 > b; ) a[b] = 0 | (4294967296 * Math.sin(++b % Math.PI));
|
|
35
|
+
let d, e, f, g = [(d = 1732584193), (e = 4023233417), ~d, ~e], h = [];
|
|
36
|
+
const l = unescape(encodeURI(input)) + '';
|
|
37
|
+
let k = l.length;
|
|
38
|
+
let c = (--k / 4 + 2) | 15;
|
|
39
|
+
for (h[--c] = 8 * k; ~k; ) h[k >> 2] |= l.charCodeAt(k) << (8 * k--);
|
|
40
|
+
for (let b = 0, m = 0; b < c; b += 16) {
|
|
41
|
+
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 ]) {
|
|
42
|
+
d = k[1] | 0;
|
|
43
|
+
e = k[2];
|
|
44
|
+
}
|
|
45
|
+
for (m = 4; m; ) g[--m] += k[m];
|
|
46
|
+
}
|
|
47
|
+
let result = '';
|
|
48
|
+
for (let i = 0; 32 > i; ) result += ((g[i >> 3] >> 4 * (1 ^ i++)) & 15).toString(16);
|
|
49
|
+
return result.split('').reverse().join('');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Mint a fresh anonymous key, valid for exactly ONE request with USER_AGENT. */
|
|
53
|
+
function freshTryItKey(userAgent = USER_AGENT) {
|
|
54
|
+
const digits = String(Math.round(Math.random() * 100000000000));
|
|
55
|
+
const H = islandHash;
|
|
56
|
+
return `tryit-${digits}-${H(userAgent + H(userAgent + H(userAgent + digits + SALT)))}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Random device id, same shape the site sets as the deepai_device_id cookie. */
|
|
60
|
+
function randomDeviceId() {
|
|
61
|
+
return Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64url');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// CLI
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
function parseArgs(argv) {
|
|
68
|
+
const opts = { prompt: '', out: null, key: null, deviceId: randomDeviceId(), aspect: null, genSource: 'img' };
|
|
69
|
+
for (let i = 0; i < argv.length; i++) {
|
|
70
|
+
const a = argv[i];
|
|
71
|
+
if (a === '--out') opts.out = argv[++i];
|
|
72
|
+
else if (a === '--key') opts.key = argv[++i];
|
|
73
|
+
else if (a === '--device-id') opts.deviceId = argv[++i];
|
|
74
|
+
else if (a === '--aspect') opts.aspect = argv[++i];
|
|
75
|
+
else if (a === '--chat-source') opts.genSource = 'chat';
|
|
76
|
+
else if (a === '--help' || a === '-h') opts.help = true;
|
|
77
|
+
else if (!opts.prompt) opts.prompt = a;
|
|
78
|
+
}
|
|
79
|
+
return opts;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const ASPECTS = { '16:9': [832, 448], '4:3': [768, 576], '1:1': [640, 640], '3:4': [576, 768], '9:16': [448, 832] };
|
|
83
|
+
|
|
84
|
+
async function main() {
|
|
85
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
86
|
+
if (opts.help || !opts.prompt) {
|
|
87
|
+
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]');
|
|
88
|
+
process.exit(opts.help ? 0 : 1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---- body: multipart/form-data, NEVER JSON -----------------------------
|
|
92
|
+
const form = new FormData();
|
|
93
|
+
form.append('text', opts.prompt);
|
|
94
|
+
form.append('generation_source', opts.genSource); // 'img' = model page, 'chat' = chat page
|
|
95
|
+
if (opts.aspect && ASPECTS[opts.aspect]) {
|
|
96
|
+
// the chat-page dialect maps aspect ratios to pixel sizes
|
|
97
|
+
const [w, h] = ASPECTS[opts.aspect];
|
|
98
|
+
form.append('width', String(w));
|
|
99
|
+
form.append('height', String(h));
|
|
100
|
+
form.append('image_generator_version', 'hd');
|
|
101
|
+
form.append('quality', 'true');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- headers: fresh single-use anonymous key (or your Pro key) ---------
|
|
105
|
+
const apiKey = opts.key || freshTryItKey();
|
|
106
|
+
const headers = {
|
|
107
|
+
'api-key': apiKey,
|
|
108
|
+
'User-Agent': USER_AGENT, // MUST match the UA the key was hashed with
|
|
109
|
+
Origin: 'https://deepai.org',
|
|
110
|
+
Referer: 'https://deepai.org/machine-learning-model/text2img',
|
|
111
|
+
Accept: '*/*',
|
|
112
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
113
|
+
Cookie: `deepai_device_id=${opts.deviceId}`,
|
|
114
|
+
// NOTE: do NOT set Content-Type yourself — undici adds the multipart
|
|
115
|
+
// boundary. A manual Content-Type without the boundary is rejected.
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
console.log(`Prompt : ${opts.prompt}`);
|
|
119
|
+
console.log(`Key : ${opts.key ? '(registered key — needs Pro)' : apiKey + ' (fresh, single-use)'}`);
|
|
120
|
+
console.log('POST : ' + API_URL);
|
|
121
|
+
|
|
122
|
+
const res = await fetch(API_URL, { method: 'POST', headers, body: form });
|
|
123
|
+
const raw = await res.text();
|
|
124
|
+
let data = null;
|
|
125
|
+
try { data = JSON.parse(raw); } catch { /* non-JSON */ }
|
|
126
|
+
|
|
127
|
+
if (!res.ok || data?.err || (data?.status && !data.share_url && !data.output_url)) {
|
|
128
|
+
console.error(`\nFAILED HTTP ${res.status}`);
|
|
129
|
+
console.error(raw.slice(0, 500));
|
|
130
|
+
const s = String(data?.status || data?.err || '');
|
|
131
|
+
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.');
|
|
132
|
+
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.');
|
|
133
|
+
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.');
|
|
134
|
+
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.');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const url = data.share_url || data.output_url;
|
|
139
|
+
console.log(`\nOK HTTP ${res.status}`);
|
|
140
|
+
console.log(`Image : ${url}`);
|
|
141
|
+
if (data.id) console.log(`ID : ${data.id}`);
|
|
142
|
+
|
|
143
|
+
// ---- optional: download the image --------------------------------------
|
|
144
|
+
const out = opts.out || `deepai-${Date.now()}.jpg`;
|
|
145
|
+
try {
|
|
146
|
+
const img = await fetch(url);
|
|
147
|
+
if (img.ok) {
|
|
148
|
+
const buf = Buffer.from(await img.arrayBuffer());
|
|
149
|
+
require('fs').writeFileSync(out, buf);
|
|
150
|
+
console.log(`Saved : ${out} (${(buf.length / 1024).toFixed(1)} KB)`);
|
|
151
|
+
} else {
|
|
152
|
+
console.log(`(download skipped: HTTP ${img.status} — open the URL above in a browser)`);
|
|
153
|
+
}
|
|
154
|
+
} catch (e) {
|
|
155
|
+
console.log(`(download failed: ${e.message} — open the URL above in a browser)`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
main().catch((e) => { console.error('ERROR:', e.message); process.exit(1); });
|
package/index.js
CHANGED
|
@@ -33,6 +33,7 @@ const ResponseFormatter = require('./src/services/ResponseFormatter');
|
|
|
33
33
|
const TriggerDetector = require('./src/services/TriggerDetector');
|
|
34
34
|
const ImageDescriber = require('./src/services/ImageDescriber');
|
|
35
35
|
const WebAnswer = require('./src/services/WebAnswer');
|
|
36
|
+
const WebSearch = require('./src/services/WebSearch');
|
|
36
37
|
const JidParser = require('./src/utils/JidParser');
|
|
37
38
|
const Media = require('./src/utils/Media');
|
|
38
39
|
const StreamParser = require('./src/core/StreamParser');
|
|
@@ -63,6 +64,7 @@ module.exports.ResponseFormatter = ResponseFormatter;
|
|
|
63
64
|
module.exports.TriggerDetector = TriggerDetector;
|
|
64
65
|
module.exports.ImageDescriber = ImageDescriber;
|
|
65
66
|
module.exports.WebAnswer = WebAnswer;
|
|
67
|
+
module.exports.WebSearch = WebSearch;
|
|
66
68
|
module.exports.JidParser = JidParser;
|
|
67
69
|
module.exports.Media = Media;
|
|
68
70
|
module.exports.StreamParser = StreamParser;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alexa-ai",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.1",
|
|
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": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"index.js",
|
|
41
41
|
"src/",
|
|
42
42
|
"examples/",
|
|
43
|
+
"test/",
|
|
43
44
|
"README.md",
|
|
44
45
|
"CHANGELOG.md",
|
|
45
46
|
"LICENSE"
|