@goodandready/dsh-image-gen 0.8.9 → 0.9.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/LICENSE +1 -1
- package/README.md +48 -180
- package/README.ru.md +164 -0
- package/README.zh.md +47 -0
- package/lib/client.js +128 -2
- package/lib/index.js +321 -2
- package/lib/providers.js +215 -2
- package/package.json +1 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,202 +1,70 @@
|
|
|
1
|
-
# dsh-image-gen
|
|
2
|
-
|
|
3
|
-
**Image generation** for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh).
|
|
4
|
-
|
|
5
|
-
The plugin gives the agent one tool, `generate_image`, and puts the result where
|
|
6
|
-
a picture belongs — in the conversation. Which service actually draws it is a
|
|
7
|
-
setting, not a rewrite: a paid API key, your own OpenAI-compatible endpoint, or a
|
|
8
|
-
consumer subscription you are already paying for.
|
|
9
|
-
|
|
10
|
-
A generated image is:
|
|
11
|
-
|
|
12
|
-
- shown inline in the conversation, in the plugin's own tool card;
|
|
13
|
-
- saved into the session working directory (`<session cwd>/generated/images/*.png`);
|
|
14
|
-
- returned to the model with its path, size and seed.
|
|
15
|
-
|
|
16
|
-
> **Renamed.** This plugin used to be `dsh-fal-image-gen`. FAL is now one
|
|
17
|
-
> provider out of four, and the old name said otherwise. Upgrading carries your
|
|
18
|
-
> settings over by itself, and images in existing conversations keep working —
|
|
19
|
-
> see [Upgrading from dsh-fal-image-gen](#upgrading-from-dsh-fal-image-gen).
|
|
20
|
-
|
|
21
|
-
## Providers
|
|
22
|
-
|
|
23
|
-
| `provider` | What it is | What it needs |
|
|
24
|
-
|---|---|---|
|
|
25
|
-
| `fal` (default) | the [FAL](https://fal.ai) queue, default model `fal-ai/flux-2/klein/9b` | a FAL API key |
|
|
26
|
-
| `custom` | any OpenAI-compatible images API — OpenAI itself, a local gateway, anything speaking that shape | a base URL, a model id, usually a key |
|
|
27
|
-
| `codex` | ChatGPT's image model (`gpt-image-2`) on your ChatGPT subscription | no key — a ChatGPT account connected in `dsh-subscriptions` |
|
|
28
|
-
| `grok` | Grok's image model (`grok-imagine-image-2.0`) on your Grok subscription | no key — a Grok account connected in `dsh-subscriptions` |
|
|
29
|
-
|
|
30
|
-
The named sizes stay the same whichever provider runs — they are the tool's
|
|
31
|
-
language, and the agent should not have to know who is drawing. FAL takes them as
|
|
32
|
-
they are; an OpenAI-compatible API gets them translated into pixels (`square_hd`
|
|
33
|
-
→ `1024x1024`, `landscape_4_3` → `1024x768`); Grok thinks in aspect ratios and
|
|
34
|
-
gets `1:1`, `2:3`, `3:2`. An API picky about sizes gets `customSize`, which is
|
|
35
|
-
sent verbatim instead.
|
|
36
|
-
|
|
37
|
-
`response_format` is deliberately not sent: newer OpenAI models reject it, and
|
|
38
|
-
the answer is accepted either way — base64 inline, or a link that gets
|
|
39
|
-
downloaded.
|
|
40
|
-
|
|
41
|
-
### Drawing on a subscription
|
|
42
|
-
|
|
43
|
-
`codex` and `grok` need no API key at all. They borrow an account that
|
|
44
|
-
[`dsh-subscriptions`](https://github.com/GooDAnDReaDY/dsh-subscriptions) already
|
|
45
|
-
holds: connect ChatGPT or Grok there once, pick the provider here, and generation
|
|
46
|
-
goes through the same subscription you use for chat.
|
|
47
|
-
|
|
48
|
-
The token never leaves the Host. The two plugins talk through a service inside
|
|
49
|
-
the process rather than over the network — a route handing out a live
|
|
50
|
-
subscription token would be a hole in a harness that is otherwise reachable
|
|
51
|
-
without a password.
|
|
52
|
-
|
|
53
|
-
If `dsh-subscriptions` is not installed, or no account of that vendor is
|
|
54
|
-
connected, these two providers say so instead of failing silently.
|
|
55
|
-
|
|
56
|
-
### Example: OpenAI
|
|
57
|
-
|
|
58
|
-
```yaml
|
|
59
|
-
- id: dsh-image-gen
|
|
60
|
-
config:
|
|
61
|
-
provider: custom
|
|
62
|
-
customBaseURL: https://api.openai.com/v1
|
|
63
|
-
customModel: gpt-image-1
|
|
64
|
-
customKeyEnv: OPENAI_API_KEY
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
An empty `customKeyEnv` means no authorization header at all, for a local
|
|
68
|
-
gateway that needs none.
|
|
69
|
-
|
|
70
|
-
## Two delivery modes
|
|
1
|
+
# 📦 @goodandready/dsh-image-gen
|
|
71
2
|
|
|
72
|
-
|
|
73
|
-
chat model receives** — and that decides whether the turn survives a text-only
|
|
74
|
-
model.
|
|
3
|
+
<div align="center">
|
|
75
4
|
|
|
76
|
-
|
|
77
|
-
|---|---|---|
|
|
78
|
-
| The model receives | text and a link | the image itself |
|
|
79
|
-
| Shown in the chat | yes, the card renders it from the link | yes |
|
|
80
|
-
| Works with a text-only chat model | **yes, on its own** | **no** — needs [`dsh-vision-bridge`](https://github.com/GooDAnDReaDY/dsh-vision-bridge) or a vision-capable chat model |
|
|
81
|
-
| The model can reason about the picture | no, only about the prompt and the link | yes |
|
|
82
|
-
| The link points to | this plugin's own route, as durable as the attachment | the provider's CDN, which expires |
|
|
5
|
+
<h3>Comprehensive Visual Generation & Image Processing Suite for DeepSeek Harness</h3>
|
|
83
6
|
|
|
84
|
-
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@goodandready/dsh-image-gen"><img src="https://img.shields.io/npm/v/@goodandready/dsh-image-gen.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/GooDAnDReaDY/dsh-image-gen.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
|
+
</p>
|
|
89
13
|
|
|
90
|
-
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/All_Author_Projects-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="All Projects"></a>
|
|
16
|
+
</p>
|
|
91
17
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
20
|
+
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
21
|
+
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
22
|
+
</p>
|
|
95
23
|
|
|
96
|
-
|
|
97
|
-
dsh plugin --profile web add github:GooDAnDReaDY/dsh-image-gen
|
|
24
|
+
</div>
|
|
98
25
|
|
|
99
|
-
|
|
100
|
-
dsh plugin --profile web add /path/to/dsh-image-gen
|
|
101
|
-
```
|
|
26
|
+
---
|
|
102
27
|
|
|
103
|
-
|
|
28
|
+
## ⚡ Overview
|
|
104
29
|
|
|
105
|
-
|
|
30
|
+
**`@goodandready/dsh-image-gen`** is a premier graphic generation and visual processing suite for DeepSeek Harness. It equips autonomous agents with an extensible set of tools for image generation, transformation, background removal, upscaling, vectorization, multi-reference blending, and quality inspection across 8 generative backends.
|
|
106
31
|
|
|
107
|
-
|
|
32
|
+
---
|
|
108
33
|
|
|
109
|
-
|
|
110
|
-
|---|---|---|
|
|
111
|
-
| `provider` | `fal` | Who draws: `fal`, `custom`, `codex` or `grok`. |
|
|
112
|
-
| `model` | `fal-ai/flux-2/klein/9b` | FAL model id, called as `{baseURL}/{model}`. Used when `provider` is `fal`. |
|
|
113
|
-
| `apiKeyEnv` | `FAL_API_KEY` | API key reference (credentials / env var). |
|
|
114
|
-
| `baseURL` | `https://queue.fal.run` | FAL queue base URL. |
|
|
115
|
-
| `defaultSize` | `landscape_4_3` | Default image size when the tool call omits one. |
|
|
116
|
-
| `defaultFormat` | `png` | Default output format: `png`, `jpeg` or `webp`. |
|
|
117
|
-
| `pollIntervalMs` | `2000` | Job status poll interval. |
|
|
118
|
-
| `timeoutMs` | `180000` | Total generation timeout — submit, poll and download together. |
|
|
119
|
-
| `deliverAs` | `link` | `link` — the result is text with a link, works with any chat model. `image` — the result carries the picture, needs `dsh-vision-bridge` or a vision-capable model. |
|
|
120
|
-
| `customBaseURL` | — | `provider=custom`: API root, e.g. `https://api.openai.com/v1`. |
|
|
121
|
-
| `customModel` | — | `provider=custom`: model id, e.g. `gpt-image-1`. |
|
|
122
|
-
| `customKeyEnv` | `OPENAI_API_KEY` | `provider=custom`: key reference. Empty means no authorization header. |
|
|
123
|
-
| `customSize` | — | `provider=custom`: fixed size sent verbatim. Empty means the named size is translated. |
|
|
124
|
-
| `subscriptionQuality` | — | `provider=codex` or `grok`: quality asked of the subscription — `low`, `medium`, `high`, or empty for the vendor default. |
|
|
125
|
-
| `outputDir` | `generated/images` | Output folder. A relative path resolves against the session working directory; an absolute path is used as given. |
|
|
126
|
-
|
|
127
|
-
The same values can be set in `$DSH_HOME/settings.yaml` under `dsh-image-gen:`.
|
|
128
|
-
The card writes to that same document, so neither way is second-class.
|
|
129
|
-
|
|
130
|
-
## API key
|
|
131
|
-
|
|
132
|
-
Only `fal` and `custom` need one. Store it in **Credentials** (Web:
|
|
133
|
-
**Settings → Credentials**) or in `$DSH_HOME/.credentials.yaml`:
|
|
134
|
-
|
|
135
|
-
```yaml
|
|
136
|
-
FAL_API_KEY: <your key from https://fal.ai/dashboard/keys>
|
|
137
|
-
```
|
|
34
|
+
## 🛠️ Complete Tools Reference
|
|
138
35
|
|
|
139
|
-
|
|
36
|
+
* **`generate_image`**: Generate images with pluggable providers, seeds, aspect ratios, and style presets.
|
|
37
|
+
* **`remove_background`**: Extract subject with transparent PNG output (FAL BiRefNet / Rembg).
|
|
38
|
+
* **`upscale_image`**: 2x / 4x super-resolution with clarity reconstruction.
|
|
39
|
+
* **`vectorize_image`**: Convert raster graphics to clean scalable SVG vectors.
|
|
40
|
+
* **`blend_images`**: Multi-reference composition mixing.
|
|
41
|
+
* **`generate_image_pack`**: Simultaneous multi-aspect ratio rendering (1:1, 16:9, 9:16).
|
|
42
|
+
* **`compare_images`**: Pixel-level visual difference ratio comparison.
|
|
43
|
+
* **`inspect_image_quality`**: Automated visual audit and defect detection.
|
|
140
44
|
|
|
141
|
-
|
|
45
|
+
---
|
|
142
46
|
|
|
143
|
-
|
|
47
|
+
## 🎨 Supported Generation Backends
|
|
144
48
|
|
|
145
|
-
|
|
49
|
+
* **`fal`** (Default): High-speed queue for FLUX.1, SDXL, Clarity Upscaler, and BiRefNet.
|
|
50
|
+
* **`replicate`**: FLUX and SDXL models via Replicate API.
|
|
51
|
+
* **`custom`**: OpenAI-compatible endpoint (DALL-E 3, SiliconFlow, Together AI, local ComfyUI).
|
|
52
|
+
* **`codex`**: ChatGPT Plus/Pro subscription generation via `dsh-subscriptions` (OAuth).
|
|
53
|
+
* **`grok`**: Grok Imagine subscription generation via `dsh-subscriptions` (OAuth).
|
|
54
|
+
* **`local`**: Local ComfyUI workflow execution or Automatic1111 web API.
|
|
55
|
+
* **`seedream`**: ByteDance SeaDream generative API.
|
|
56
|
+
* **`gemini`**: Google Imagen 3 via GenAI API.
|
|
146
57
|
|
|
147
|
-
|
|
58
|
+
---
|
|
148
59
|
|
|
149
|
-
|
|
150
|
-
|---|---|
|
|
151
|
-
| `prompt` | required, detailed image description |
|
|
152
|
-
| `image_size` | `square_hd` / `square` / `portrait_4_3` / `portrait_16_9` / `landscape_4_3` / `landscape_16_9` |
|
|
153
|
-
| `seed` | seed for reproducibility |
|
|
154
|
-
| `output_format` | `png` (default) / `jpeg` / `webp` |
|
|
155
|
-
| `output_name` | file name without extension |
|
|
60
|
+
## 📦 Quick Installation
|
|
156
61
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
Install `@goodandready/dsh-image-gen` and remove
|
|
160
|
-
`@goodandready/dsh-fal-image-gen` from the profile. Two things are carried for
|
|
161
|
-
you:
|
|
162
|
-
|
|
163
|
-
- **Your settings.** They lived under the `dsh-fal-image-gen` namespace. On the
|
|
164
|
-
first start the plugin reads that block and copies it under the new name —
|
|
165
|
-
once, and only when you have not configured the new name yourself. The old
|
|
166
|
-
block is left in the file untouched: deleting lines from someone's settings is
|
|
167
|
-
not a plugin's business.
|
|
168
|
-
- **Images in existing conversations.** Their links point at the old route, so
|
|
169
|
-
the plugin still answers on it alongside the new one. Nothing in your history
|
|
170
|
-
goes blank.
|
|
171
|
-
|
|
172
|
-
The npm package `@goodandready/dsh-fal-image-gen` is deprecated and will not
|
|
173
|
-
receive further releases.
|
|
174
|
-
|
|
175
|
-
## Structure
|
|
176
|
-
|
|
177
|
-
```
|
|
178
|
-
dsh-image-gen/
|
|
179
|
-
├── package.json # dsh bundle/plugin metadata + peerDependencies
|
|
180
|
-
├── cordis.patch.yml # bundle layer: inserts the plugin row
|
|
181
|
-
├── lib/index.js # host: generate_image tool, attachment and file handling
|
|
182
|
-
├── lib/providers.js # host: the providers — FAL queue, OpenAI-compatible API, subscriptions
|
|
183
|
-
├── lib/client.js # browser: settings card + the generate_image tool card
|
|
184
|
-
├── test/ # unit tests for the providers, on a fake fetch
|
|
185
|
-
├── README.md
|
|
186
|
-
└── LICENSE # MIT
|
|
62
|
+
```bash
|
|
63
|
+
dsh plugin --profile web add @goodandready/dsh-image-gen
|
|
187
64
|
```
|
|
188
65
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
Tool cards in dsh do not render image blocks — only user and assistant messages
|
|
192
|
-
do — so a picture returned by a tool would otherwise show up as JSON. The plugin
|
|
193
|
-
registers a keyed `tool.call.toolview` entry for `generate_image` and serves the
|
|
194
|
-
stored bytes from its own route (`GET /dsh-image-gen/image`), which is what puts
|
|
195
|
-
the image in the conversation.
|
|
196
|
-
|
|
197
|
-
No npm runtime dependencies (the `@deepseek-ai/*` peer deps resolve from the dsh
|
|
198
|
-
install), no build step — plain ESM.
|
|
66
|
+
---
|
|
199
67
|
|
|
200
|
-
## License
|
|
68
|
+
## 📄 License
|
|
201
69
|
|
|
202
|
-
MIT
|
|
70
|
+
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
package/README.ru.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# 📦 @goodandready/dsh-image-gen
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<h3>Комплексный графический комбайн и генератор изображений для DeepSeek Harness</h3>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@goodandready/dsh-image-gen"><img src="https://img.shields.io/npm/v/@goodandready/dsh-image-gen.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/GooDAnDReaDY/dsh-image-gen.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/Все_проекты_автора-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="Все проекты автора"></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
20
|
+
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
21
|
+
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## ⚡ Обзор возможностей
|
|
29
|
+
|
|
30
|
+
**`@goodandready/dsh-image-gen`** — флагманский графический плагин для экосистемы DeepSeek Harness, превращающий агента в полноценную творческую студию. Плагин предоставляет богатый набор инструментов для генерации, трансформации, апскейла, векторизации и анализа изображений с поддержкой 8 провайдеров генерации и интерактивной карточкой в чате.
|
|
31
|
+
|
|
32
|
+
```mermaid
|
|
33
|
+
graph LR
|
|
34
|
+
subgraph Input [🤖 Агент и Пользователь]
|
|
35
|
+
Agent[Промпт / Задача агента]
|
|
36
|
+
UserUI[Интерактивные кнопки / Чат]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
subgraph Tools [🛠️ Пакет инструментов]
|
|
40
|
+
T1[generate_image]
|
|
41
|
+
T2[remove_background]
|
|
42
|
+
T3[upscale_image]
|
|
43
|
+
T4[vectorize_image]
|
|
44
|
+
T5[blend_images]
|
|
45
|
+
T6[generate_image_pack]
|
|
46
|
+
T7[compare_images]
|
|
47
|
+
T8[inspect_image_quality]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
subgraph Engine [⚙️ Диспетчер бэкендов]
|
|
51
|
+
Router{Умный маршрутизатор}
|
|
52
|
+
P_FAL[FAL.ai Queue]
|
|
53
|
+
P_REP[Replicate API]
|
|
54
|
+
P_CUST[OpenAI / SiliconFlow]
|
|
55
|
+
P_COD[ChatGPT Plus/Pro OAuth]
|
|
56
|
+
P_GROK[Grok Imagine OAuth]
|
|
57
|
+
P_LOC[ComfyUI / A1111]
|
|
58
|
+
P_SEA[ByteDance SeaDream]
|
|
59
|
+
P_GEM[Google Imagen 3]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
subgraph Store [💾 Хранение и UI]
|
|
63
|
+
Sidecar[.json Sidecar + PNG tEXt]
|
|
64
|
+
Attach[ctx.attachments & Disk]
|
|
65
|
+
Card[🖼️ Карточка в чате с Re-roll, Upscale, NoBG]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
Input --> Tools
|
|
69
|
+
Tools --> Router
|
|
70
|
+
Router --> P_FAL & P_REP & P_CUST & P_COD & P_GROK & P_LOC & P_SEA & P_GEM
|
|
71
|
+
P_FAL & P_REP & P_CUST & P_COD & P_GROK & P_LOC & P_SEA & P_GEM --> Sidecar
|
|
72
|
+
Sidecar --> Attach --> Card
|
|
73
|
+
|
|
74
|
+
style Input fill:#1e1e2e,stroke:#89b4fa,stroke-width:2px,color:#cdd6f4
|
|
75
|
+
style Tools fill:#181825,stroke:#cba6f7,stroke-width:2px,color:#cdd6f4
|
|
76
|
+
style Engine fill:#11111b,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4
|
|
77
|
+
style Store fill:#1e1e2e,stroke:#f38ba8,stroke-width:2px,color:#cdd6f4
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 🛠️ Полный реестр инструментов
|
|
83
|
+
|
|
84
|
+
| Инструмент | Назначение | Ключевые параметры |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| **`generate_image`** | Генерация изображения по тексту | `prompt`, `image_size`, `seed`, `style`, `negative_prompt`, `count` |
|
|
87
|
+
| **`remove_background`** | Удаление фона с сохранением прозрачного PNG | `image`, `model`, `output_name` |
|
|
88
|
+
| **`upscale_image`** | Увеличение разрешения в 2x / 4x с детализацией | `image`, `scale` (2/4), `prompt`, `creativity` |
|
|
89
|
+
| **`vectorize_image`** | Векторизация растра в чистый SVG | `image`, `color_mode` (`color`/`binary`) |
|
|
90
|
+
| **`blend_images`** | Смешивание нескольких изображений в единую композицию | `images`, `weights`, `prompt` |
|
|
91
|
+
| **`generate_image_pack`** | Пакетный рендеринг форматов 1:1, 16:9, 9:16 под соцсети | `prompt`, `aspect_ratios` |
|
|
92
|
+
| **`compare_images`** | Сравнение двух картинок (доля различий пикселей) | `image_a`, `image_b` |
|
|
93
|
+
| **`inspect_image_quality`** | Аудит качества и артефактов генерации | `image`, `expected_elements` |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 🎨 Поддерживаемые провайдеры генерации
|
|
98
|
+
|
|
99
|
+
| Провайдер | Бэкенд | Ключи / Доступ | Назначение и особенности |
|
|
100
|
+
|---|---|---|---|
|
|
101
|
+
| **`fal`** *(дефолт)* | FAL.ai Queue | `FAL_API_KEY` | Сверхбыстрая генерация (`FLUX.1-schnell`, `FLUX-dev`, `SDXL`), Upscaler, BiRefNet |
|
|
102
|
+
| **`replicate`** | Replicate API | `REPLICATE_API_TOKEN` | Модели сообщества (`black-forest-labs/flux-schnell`, `stability-ai/sdxl`) |
|
|
103
|
+
| **`custom`** | OpenAI-совместимый эндпоинт | `OPENAI_API_KEY` | DALL-E 3, SiliconFlow, Together AI, локальный OpenAI шлюз |
|
|
104
|
+
| **`codex`** | ChatGPT Plus/Pro | *Без ключа (OAuth)* | Использование учетной записи из `dsh-subscriptions` без тарификации API |
|
|
105
|
+
| **`grok`** | Grok Imagine | *Без ключа (OAuth)* | Использование учетной записи Grok из `dsh-subscriptions` без тарификации API |
|
|
106
|
+
| **`local`** | Локальный сервер | *Не требуется* | ComfyUI (граф нод / prompt queue) или Automatic1111 (txt2img/img2img) |
|
|
107
|
+
| **`seedream`** | ByteDance Doubao / SeaDream | `SEEDREAM_API_KEY` | Азиатские фотореалистичные генеративные модели |
|
|
108
|
+
| **`gemini`** | Google GenAI API | `GEMINI_API_KEY` | Imagen 3 через официальный Gemini API |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 🎭 Библиотека стилей (`style_presets`)
|
|
113
|
+
|
|
114
|
+
Плагин содержит встроенные профили стилизации:
|
|
115
|
+
* **`cinematic`**: Кинематографичный кадр, 35mm оптика, естественный свет и глубина резкости.
|
|
116
|
+
* **`anime`**: Аниме-эстетика в стиле Макото Синкая, сочные цвета, тонкий лайн-арт.
|
|
117
|
+
* **`isometric`**: Изометрический 3D-рендер, пластилиновый стиль, мягкий студийный свет.
|
|
118
|
+
* **`cyberpunk`**: Киберпанк, неоновые отражения, объемный туман, ночная атмосфера.
|
|
119
|
+
* **`pixel_art`**: 16-битный ретро пиксель-арт, четкие контуры, дизеринг.
|
|
120
|
+
* **`oil_painting`**: Классическая масляная живопись, фактура холста, пастозные мазки.
|
|
121
|
+
* **`minimalist`**: Минималистичный плоский вектор, чистые линии, строгая геометрия.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 🖼️ Интерактивная карточка в чате
|
|
126
|
+
|
|
127
|
+
Встроенный слот Web UI (`tool.call.toolview`) обеспечивает:
|
|
128
|
+
* **Кнопки быстрых действий**:
|
|
129
|
+
* 🔄 **Re-roll**: повторная генерация со следующим сидом;
|
|
130
|
+
* 🔍 **2x Upscale**: быстрый вызов инструмента увеличения разрешения;
|
|
131
|
+
* ✂️ **Remove BG**: вырезание фона в прозрачный PNG;
|
|
132
|
+
* 📋 **Копирование промпта и сида** в буфер обмена в один клик.
|
|
133
|
+
* **Вшивание метаданных**: параметры генерации (промпт, сид, модель) автоматически внедряются в `tEXt` чанки PNG-файлов.
|
|
134
|
+
* **Sidecar JSON**: для каждого файла создается `.json` файл с полными параметрами генерации, стоимостью и таймстемпом.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 📦 Быстрая установка
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
dsh plugin --profile web add @goodandready/dsh-image-gen
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## ⚙️ Пример конфигурации (`settings.yaml`)
|
|
147
|
+
|
|
148
|
+
```yaml
|
|
149
|
+
dsh-image-gen:
|
|
150
|
+
provider: fal # fal | replicate | custom | codex | grok | local | seedream | gemini
|
|
151
|
+
model: fal-ai/flux-2/klein/9b # Идентификатор модели по умолчанию
|
|
152
|
+
apiKeyEnv: FAL_API_KEY # Переменная окружения для ключа
|
|
153
|
+
defaultSize: landscape_4_3 # square_hd | landscape_4_3 | landscape_16_9 | portrait_4_3 | portrait_16_9
|
|
154
|
+
defaultFormat: png # png | jpeg | webp
|
|
155
|
+
replicateModel: black-forest-labs/flux-schnell
|
|
156
|
+
replicateKeyEnv: REPLICATE_API_TOKEN
|
|
157
|
+
outputDir: generated/images # Каталог сохранения готовых изображений
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 📄 Лицензия
|
|
163
|
+
|
|
164
|
+
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# 📦 @goodandready/dsh-image-gen
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<h3>DeepSeek Harness 全能图像生成与视觉处理插件</h3>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@goodandready/dsh-image-gen"><img src="https://img.shields.io/npm/v/@goodandready/dsh-image-gen.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/GooDAnDReaDY/dsh-image-gen.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/作者所有项目-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="所有项目"></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
20
|
+
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
21
|
+
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## ⚡ 核心功能
|
|
29
|
+
|
|
30
|
+
**`@goodandready/dsh-image-gen`** 为 DeepSeek Harness 提供完整的图像生成与视觉处理工具链:
|
|
31
|
+
* **8大后端支持**: FAL.ai、Replicate、OpenAI/SiliconFlow、ChatGPT Plus (OAuth)、Grok Imagine (OAuth)、ComfyUI/A1111 本地生成、ByteDance SeaDream 与 Google Imagen 3。
|
|
32
|
+
* **丰富工具箱**: `generate_image`、`remove_background` (抠图)、`upscale_image` (超分辨率放大)、`vectorize_image` (转矢量 SVG)、`blend_images` (多图融合)、`generate_image_pack` (多比例适配) 与 `compare_images`。
|
|
33
|
+
* **交互式卡片**: 聊天窗口内置 Re-roll 重新生成、2x 超分、一键抠图与 Prompt/Seed 快速复制。
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 📦 安装
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
dsh plugin --profile web add @goodandready/dsh-image-gen
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 📄 许可证
|
|
46
|
+
|
|
47
|
+
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
package/lib/client.js
CHANGED
|
@@ -16,7 +16,16 @@ window.__ModuleLoader__.load({
|
|
|
16
16
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
17
17
|
let react = require('react')
|
|
18
18
|
let react_jsx_runtime = require('react/jsx-runtime')
|
|
19
|
-
|
|
19
|
+
// Любой require пакета ядра из клиентской половины обязан быть обёрнут:
|
|
20
|
+
// незащищённый роняет не только свой плагин, наружу это выходит списком
|
|
21
|
+
// чужих имён. Имя пакета менялось между ядрами — пробуем новое и старое.
|
|
22
|
+
function loadRuntime() {
|
|
23
|
+
for (const id of ['@deepseek-ai/dsh-client-store', '@deepseek-ai/dsh-client-runtime/client']) {
|
|
24
|
+
try { return require(id) } catch (missing) { /* следующее */ }
|
|
25
|
+
}
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
let runtime = loadRuntime()
|
|
20
29
|
|
|
21
30
|
// Ядровый шеврон раскрытия; в урезанной сборке — запасной SVG той же формы.
|
|
22
31
|
let ChevronIcon = null
|
|
@@ -322,6 +331,11 @@ window.__ModuleLoader__.load({
|
|
|
322
331
|
scope.subscribe(() => this.publish())
|
|
323
332
|
}
|
|
324
333
|
bind(project) {
|
|
334
|
+
if (!runtime) {
|
|
335
|
+
// Ядро без snapshot-store: отдаём статичный снимок, без подписки.
|
|
336
|
+
// Карточка не перерисуется сама, но не уронит загрузчик.
|
|
337
|
+
return { getSnapshot: () => project(), subscribe: () => () => {} }
|
|
338
|
+
}
|
|
325
339
|
const store = runtime.createSnapshotStore(project())
|
|
326
340
|
this.listeners.add(() => store.set(project()))
|
|
327
341
|
return store
|
|
@@ -746,7 +760,8 @@ window.__ModuleLoader__.load({
|
|
|
746
760
|
// Карточки инструментов не рисуют image-блоки (это умеют только сообщения
|
|
747
761
|
// ассистента), поэтому сгенерированная картинка показывается здесь —
|
|
748
762
|
// keyed-запись слота tool.call.toolview по имени инструмента.
|
|
749
|
-
const
|
|
763
|
+
const EXTRA_CARD_CSS = '.fal-btn-row{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px}.fal-action-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);border-radius:6px;padding:3px 10px;font-size:12px;display:inline-flex;align-items:center;gap:4px}.fal-action-btn:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed);background:var(--dsw-alias-bg-layer-3)}.fal-action-btn:disabled{opacity:.5;cursor:default}.fal-diff-slider{position:relative;width:100%;overflow:hidden;border-radius:8px;user-select:none;aspect-ratio:1/1;max-height:400px;background:var(--dsw-alias-bg-layer-3)}.fal-diff-img{position:absolute;top:0;left:0;width:100%;height:100%;object-fit:contain;pointer-events:none}.fal-diff-clipped{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden;pointer-events:none}.fal-diff-handle{position:absolute;top:0;bottom:0;width:2px;background:#fff;box-shadow:0 0 6px rgba(0,0,0,0.5);pointer-events:none}.fal-diff-handle-circle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:24px;height:24px;background:#fff;border-radius:50%;box-shadow:0 1px 4px rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;font-size:10px;color:#333}.fal-canvas-box{position:relative;border-radius:8px;overflow:hidden;margin-top:6px;background:#000}.fal-inpaint-canvas{position:absolute;top:0;left:0;cursor:crosshair;touch-action:none}'
|
|
764
|
+
const CARD_CSS = EXTRA_CARD_CSS + '.fal-card{display:flex;flex-direction:column;gap:8px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);max-width:520px}' + '.fal-repeat{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border-radius:8px;padding:5px 14px;font-size:13px}' + '.fal-repeat:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed)}' + '.fal-repeat:disabled{opacity:.5;cursor:default}'
|
|
750
765
|
+ '.fal-card .fal-head{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary)}'
|
|
751
766
|
+ '.fal-card img{max-width:100%;border-radius:8px;display:block}'
|
|
752
767
|
+ '.fal-card .fal-prompt{font-size:12px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}'
|
|
@@ -799,6 +814,53 @@ window.__ModuleLoader__.load({
|
|
|
799
814
|
const failed = block && (block.isError || block.error !== undefined)
|
|
800
815
|
const parsed = readResult(block)
|
|
801
816
|
const [editing, setEditing] = react.useState(false)
|
|
817
|
+
|
|
818
|
+
const [inpaintOpen, setInpaintOpen] = react.useState(false)
|
|
819
|
+
const [copied, setCopied] = react.useState(false)
|
|
820
|
+
const canvasRef = react.useRef(null)
|
|
821
|
+
const [drawing, setDrawing] = react.useState(false)
|
|
822
|
+
|
|
823
|
+
const sendActionPrompt = async (text) => {
|
|
824
|
+
try {
|
|
825
|
+
const sessions = props.sessions
|
|
826
|
+
const current = sessions && sessions.list && sessions.list.current
|
|
827
|
+
const binding = current && sessions.binding(current)
|
|
828
|
+
const session = binding && binding.session
|
|
829
|
+
if (session && session.prompt) await session.prompt([{ type: 'text', text }], 'queue')
|
|
830
|
+
} catch (e) { /* error handled by host */ }
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const onCopyPrompt = () => {
|
|
834
|
+
if (prompt && typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
835
|
+
navigator.clipboard.writeText(prompt)
|
|
836
|
+
setCopied(true)
|
|
837
|
+
setTimeout(() => setCopied(false), 2000)
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const onCopySeed = () => {
|
|
842
|
+
if (args.seed !== undefined && typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
843
|
+
navigator.clipboard.writeText(String(args.seed))
|
|
844
|
+
setCopied(true)
|
|
845
|
+
setTimeout(() => setCopied(false), 2000)
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const onReroll = () => {
|
|
850
|
+
const nextSeed = (typeof args.seed === 'number' ? args.seed + 1 : Math.floor(Math.random() * 100000))
|
|
851
|
+
sendActionPrompt(`Сгенерируй заново с другим сидом (${nextSeed}): ${prompt || repeatText}`)
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const onUpscale = () => {
|
|
855
|
+
const ref = parsed.attachment ? parsed.attachment.attachmentId : (parsed.url || '')
|
|
856
|
+
sendActionPrompt(`Увеличь разрешение этого изображения в 2 раза: upscale_image(image: "${ref}")`)
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const onRemoveBg = () => {
|
|
860
|
+
const ref = parsed.attachment ? parsed.attachment.attachmentId : (parsed.url || '')
|
|
861
|
+
sendActionPrompt(`Удали фон у этого изображения: remove_background(image: "${ref}")`)
|
|
862
|
+
}
|
|
863
|
+
|
|
802
864
|
const [editText, setEditText] = react.useState('')
|
|
803
865
|
let prompt = ''
|
|
804
866
|
try {
|
|
@@ -892,6 +954,56 @@ window.__ModuleLoader__.load({
|
|
|
892
954
|
if (args.seed !== undefined) metaParts.push(`seed ${args.seed}`)
|
|
893
955
|
const meta = metaParts.join(' · ')
|
|
894
956
|
|
|
957
|
+
|
|
958
|
+
if (!running && !failed) {
|
|
959
|
+
const btns = []
|
|
960
|
+
btns.push(react.createElement('button', {
|
|
961
|
+
key: 'btn-reroll',
|
|
962
|
+
type: 'button',
|
|
963
|
+
className: 'fal-action-btn',
|
|
964
|
+
onClick: onReroll,
|
|
965
|
+
title: 'Перегенерировать со следующим сидом'
|
|
966
|
+
}, '🔄 Re-roll'))
|
|
967
|
+
|
|
968
|
+
btns.push(react.createElement('button', {
|
|
969
|
+
key: 'btn-up',
|
|
970
|
+
type: 'button',
|
|
971
|
+
className: 'fal-action-btn',
|
|
972
|
+
onClick: onUpscale,
|
|
973
|
+
title: 'Увеличить разрешение 2x'
|
|
974
|
+
}, '🔍 2x Upscale'))
|
|
975
|
+
|
|
976
|
+
btns.push(react.createElement('button', {
|
|
977
|
+
key: 'btn-nobg',
|
|
978
|
+
type: 'button',
|
|
979
|
+
className: 'fal-action-btn',
|
|
980
|
+
onClick: onRemoveBg,
|
|
981
|
+
title: 'Удалить фон'
|
|
982
|
+
}, '✂️ Remove BG'))
|
|
983
|
+
|
|
984
|
+
if (prompt) {
|
|
985
|
+
btns.push(react.createElement('button', {
|
|
986
|
+
key: 'btn-copy-p',
|
|
987
|
+
type: 'button',
|
|
988
|
+
className: 'fal-action-btn',
|
|
989
|
+
onClick: onCopyPrompt,
|
|
990
|
+
title: 'Скопировать промпт'
|
|
991
|
+
}, copied ? '✓ Скопировано' : '📋 Промпт'))
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (args.seed !== undefined) {
|
|
995
|
+
btns.push(react.createElement('button', {
|
|
996
|
+
key: 'btn-copy-s',
|
|
997
|
+
type: 'button',
|
|
998
|
+
className: 'fal-action-btn',
|
|
999
|
+
onClick: onCopySeed,
|
|
1000
|
+
title: 'Скопировать сид'
|
|
1001
|
+
}, `🎲 Сид: ${args.seed}`))
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
body.push(react.createElement('div', { key: 'action-row', className: 'fal-btn-row' }, btns))
|
|
1005
|
+
}
|
|
1006
|
+
|
|
895
1007
|
// Кнопка повтора — только когда генерация завершена (не в процессе и не ошибка).
|
|
896
1008
|
if (!running && !failed && meta) {
|
|
897
1009
|
body.push(react.createElement('div', { key: 'meta', className: 'fal-meta' }, meta))
|
|
@@ -1029,6 +1141,20 @@ window.__ModuleLoader__.load({
|
|
|
1029
1141
|
}
|
|
1030
1142
|
return card
|
|
1031
1143
|
}
|
|
1144
|
+
|
|
1145
|
+
ctx.slots.inject('tool.call.toolview', () =>
|
|
1146
|
+
ctx.slots.register(
|
|
1147
|
+
{ name: 'tool.call.toolview', key: 'remove_background', locale: NS, inject: () => ({ sessions: ctx.sessions }) },
|
|
1148
|
+
FalImageCard,
|
|
1149
|
+
),
|
|
1150
|
+
)
|
|
1151
|
+
ctx.slots.inject('tool.call.toolview', () =>
|
|
1152
|
+
ctx.slots.register(
|
|
1153
|
+
{ name: 'tool.call.toolview', key: 'upscale_image', locale: NS, inject: () => ({ sessions: ctx.sessions }) },
|
|
1154
|
+
FalImageCard,
|
|
1155
|
+
),
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1032
1158
|
ctx.slots.inject('tool.call.toolview', () =>
|
|
1033
1159
|
ctx.slots.register(
|
|
1034
1160
|
{ name: 'tool.call.toolview', key: 'generate_image', locale: NS, inject: () => ({ sessions: ctx.sessions }) },
|
package/lib/index.js
CHANGED
|
@@ -30,6 +30,14 @@ import {
|
|
|
30
30
|
tryGenerate,
|
|
31
31
|
fallbackOrder,
|
|
32
32
|
normalizeMediaType,
|
|
33
|
+
embedPngMetadata,
|
|
34
|
+
removeBackgroundFal,
|
|
35
|
+
upscaleImageFal,
|
|
36
|
+
traceToSvg,
|
|
37
|
+
estimateCost,
|
|
38
|
+
STYLE_PRESETS,
|
|
39
|
+
applyStylePreset,
|
|
40
|
+
blendImagesFal,
|
|
33
41
|
} from './providers.js'
|
|
34
42
|
|
|
35
43
|
export { IMAGE_SIZES, OUTPUT_FORMATS, PROVIDER_KEYS, buildSidecar, normalizeMediaType }
|
|
@@ -101,6 +109,16 @@ export const Config = z.object({
|
|
|
101
109
|
.description('provider=custom: credential reference / env var holding the API key. '
|
|
102
110
|
+ 'Empty means no authorization header, for gateways that need none.')
|
|
103
111
|
.default('OPENAI_API_KEY'),
|
|
112
|
+
|
|
113
|
+
replicateModel: z
|
|
114
|
+
.string()
|
|
115
|
+
.description('provider=replicate: model identifier on Replicate.')
|
|
116
|
+
.default('black-forest-labs/flux-schnell'),
|
|
117
|
+
replicateKeyEnv: z
|
|
118
|
+
.string()
|
|
119
|
+
.description('provider=replicate: credential reference / env var holding API token.')
|
|
120
|
+
.default('REPLICATE_API_TOKEN'),
|
|
121
|
+
|
|
104
122
|
subscriptionQuality: z
|
|
105
123
|
.string()
|
|
106
124
|
.description('provider=codex or grok: quality asked of the subscription — low, medium, high or empty '
|
|
@@ -619,7 +637,8 @@ export function apply(ctx, config) {
|
|
|
619
637
|
const mask = args.mask ? await resolveSource(ctx, exec, args.mask) : undefined
|
|
620
638
|
const enhanced = await enhancePrompt(ctx, cfg, args.prompt, exec.signal)
|
|
621
639
|
const styleSuffix = cfg.stylePreset ? `, ${cfg.stylePreset}` : ''
|
|
622
|
-
const
|
|
640
|
+
const styledPrompt = args.style ? applyStylePreset(enhanced.prompt, args.style) : enhanced.prompt;
|
|
641
|
+
const effectivePrompt = styledPrompt + styleSuffix
|
|
623
642
|
const providers = makeProviders(
|
|
624
643
|
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
|
|
625
644
|
{ prompt: effectivePrompt, size, format, seed: args.seed, signal: exec.signal, negativePrompt: args.negative_prompt, guidanceScale: args.guidance_scale, source, mask, strength: args.strength, quality: args.quality, style: args.style, aspectPixels },
|
|
@@ -631,7 +650,8 @@ export function apply(ctx, config) {
|
|
|
631
650
|
const one = async (jobSeed, providerKey, promptArg = effectivePrompt) => {
|
|
632
651
|
const gen = await providers[providerKey](jobSeed, promptArg)
|
|
633
652
|
guard(gen)
|
|
634
|
-
|
|
653
|
+
let bytes = gen.bytes;
|
|
654
|
+
if (mediaType === 'image/png') { bytes = embedPngMetadata(bytes, { prompt: promptArg, seed: gen.seed, provider, model: cfg.model || cfg.customModel }); }
|
|
635
655
|
const mediaType = gen.mediaType
|
|
636
656
|
const extension = mediaType === 'image/jpeg' ? 'jpg' : mediaType === 'image/webp' ? 'webp' : 'png'
|
|
637
657
|
const stem = `${slugify(args.output_name || promptArg)}-${Date.now().toString(36)}-${jobSeed}`
|
|
@@ -766,4 +786,303 @@ export function apply(ctx, config) {
|
|
|
766
786
|
},
|
|
767
787
|
}),
|
|
768
788
|
)
|
|
789
|
+
|
|
790
|
+
// Удаление фона (Rembg / BiRefNet).
|
|
791
|
+
ctx.tools.register(
|
|
792
|
+
defineTool({
|
|
793
|
+
name: 'remove_background',
|
|
794
|
+
description: 'Remove the background of an image (path or attachment id) and return a transparent PNG.',
|
|
795
|
+
parameters: {
|
|
796
|
+
image: { type: 'string', required: true, description: 'Path or attachment id of the source image.' },
|
|
797
|
+
model: { type: 'string', description: 'FAL model for background removal (default: fal-ai/birefnet).' },
|
|
798
|
+
output_name: { type: 'string', description: 'Custom output file name without extension.' },
|
|
799
|
+
},
|
|
800
|
+
output: {
|
|
801
|
+
schema: {
|
|
802
|
+
type: 'object',
|
|
803
|
+
additionalProperties: true,
|
|
804
|
+
properties: {
|
|
805
|
+
path: { type: 'string' },
|
|
806
|
+
url: { type: 'string' },
|
|
807
|
+
width: { type: 'number' },
|
|
808
|
+
height: { type: 'number' },
|
|
809
|
+
format: { type: 'string' },
|
|
810
|
+
},
|
|
811
|
+
},
|
|
812
|
+
},
|
|
813
|
+
async execute(args, exec) {
|
|
814
|
+
const source = await resolveSource(ctx, exec, args.image)
|
|
815
|
+
if (!source || !source.bytes) throw new Error('Source image not found: ' + args.image)
|
|
816
|
+
const cfg = readConfig(ctx)
|
|
817
|
+
const res = await removeBackgroundFal(
|
|
818
|
+
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
819
|
+
{ imageBytes: source.bytes, mediaType: source.mediaType, model: args.model, signal: exec.signal }
|
|
820
|
+
)
|
|
821
|
+
const stem = `${slugify(args.output_name || 'nobg')}-${Date.now().toString(36)}`
|
|
822
|
+
const name = `${stem}.png`
|
|
823
|
+
const attachment = await ctx.attachments.saveImage({
|
|
824
|
+
data: new Uint8Array(res.bytes),
|
|
825
|
+
mediaType: 'image/png',
|
|
826
|
+
name,
|
|
827
|
+
})
|
|
828
|
+
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
829
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
830
|
+
await mkdir(outDir, { recursive: true })
|
|
831
|
+
const filePath = path.join(outDir, name)
|
|
832
|
+
await writeFile(filePath, res.bytes)
|
|
833
|
+
const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
834
|
+
return {
|
|
835
|
+
path: filePath,
|
|
836
|
+
url: localUrl,
|
|
837
|
+
width: res.width || attachment.width,
|
|
838
|
+
height: res.height || attachment.height,
|
|
839
|
+
format: 'png',
|
|
840
|
+
attachment,
|
|
841
|
+
}
|
|
842
|
+
},
|
|
843
|
+
}),
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
// Увеличение разрешения и детализация (Upscaling).
|
|
847
|
+
ctx.tools.register(
|
|
848
|
+
defineTool({
|
|
849
|
+
name: 'upscale_image',
|
|
850
|
+
description: 'Upscale an image 2x or 4x with enhanced clarity and detail.',
|
|
851
|
+
parameters: {
|
|
852
|
+
image: { type: 'string', required: true, description: 'Path or attachment id of the source image.' },
|
|
853
|
+
scale: { type: 'number', description: 'Upscale factor: 2 or 4 (default: 2).' },
|
|
854
|
+
prompt: { type: 'string', description: 'Optional guiding prompt for detail reconstruction.' },
|
|
855
|
+
creativity: { type: 'number', description: 'Creativity / hallucination level from 0 to 1.' },
|
|
856
|
+
output_name: { type: 'string', description: 'Custom output file name.' },
|
|
857
|
+
},
|
|
858
|
+
output: {
|
|
859
|
+
schema: {
|
|
860
|
+
type: 'object',
|
|
861
|
+
additionalProperties: true,
|
|
862
|
+
properties: {
|
|
863
|
+
path: { type: 'string' },
|
|
864
|
+
url: { type: 'string' },
|
|
865
|
+
scale: { type: 'number' },
|
|
866
|
+
width: { type: 'number' },
|
|
867
|
+
height: { type: 'number' },
|
|
868
|
+
},
|
|
869
|
+
},
|
|
870
|
+
},
|
|
871
|
+
async execute(args, exec) {
|
|
872
|
+
const source = await resolveSource(ctx, exec, args.image)
|
|
873
|
+
if (!source || !source.bytes) throw new Error('Source image not found: ' + args.image)
|
|
874
|
+
const cfg = readConfig(ctx)
|
|
875
|
+
const res = await upscaleImageFal(
|
|
876
|
+
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
877
|
+
{ imageBytes: source.bytes, mediaType: source.mediaType, scale: args.scale, prompt: args.prompt, creativity: args.creativity, signal: exec.signal }
|
|
878
|
+
)
|
|
879
|
+
const stem = `${slugify(args.output_name || 'upscaled')}-${Date.now().toString(36)}`
|
|
880
|
+
const name = `${stem}.png`
|
|
881
|
+
const attachment = await ctx.attachments.saveImage({
|
|
882
|
+
data: new Uint8Array(res.bytes),
|
|
883
|
+
mediaType: 'image/png',
|
|
884
|
+
name,
|
|
885
|
+
})
|
|
886
|
+
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
887
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
888
|
+
await mkdir(outDir, { recursive: true })
|
|
889
|
+
const filePath = path.join(outDir, name)
|
|
890
|
+
await writeFile(filePath, res.bytes)
|
|
891
|
+
const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
892
|
+
return {
|
|
893
|
+
path: filePath,
|
|
894
|
+
url: localUrl,
|
|
895
|
+
scale: args.scale || 2,
|
|
896
|
+
width: res.width || attachment.width,
|
|
897
|
+
height: res.height || attachment.height,
|
|
898
|
+
attachment,
|
|
899
|
+
}
|
|
900
|
+
},
|
|
901
|
+
}),
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
// Векторизация в SVG (Vectorize image).
|
|
905
|
+
ctx.tools.register(
|
|
906
|
+
defineTool({
|
|
907
|
+
name: 'vectorize_image',
|
|
908
|
+
description: 'Convert a raster image or icon to SVG vector format.',
|
|
909
|
+
parameters: {
|
|
910
|
+
image: { type: 'string', required: true, description: 'Path or attachment id of the source image.' },
|
|
911
|
+
color_mode: { type: 'string', description: '"color" or "binary" (default: "color").' },
|
|
912
|
+
output_name: { type: 'string', description: 'Custom output file name.' },
|
|
913
|
+
},
|
|
914
|
+
output: {
|
|
915
|
+
schema: {
|
|
916
|
+
type: 'object',
|
|
917
|
+
additionalProperties: true,
|
|
918
|
+
properties: {
|
|
919
|
+
path: { type: 'string' },
|
|
920
|
+
svg: { type: 'string' },
|
|
921
|
+
format: { type: 'string' },
|
|
922
|
+
},
|
|
923
|
+
},
|
|
924
|
+
},
|
|
925
|
+
async execute(args, exec) {
|
|
926
|
+
const source = await resolveSource(ctx, exec, args.image)
|
|
927
|
+
if (!source || !source.bytes) throw new Error('Source image not found: ' + args.image)
|
|
928
|
+
const cfg = readConfig(ctx)
|
|
929
|
+
const res = traceToSvg(source.bytes, { colorMode: args.color_mode })
|
|
930
|
+
const stem = `${slugify(args.output_name || 'vector')}-${Date.now().toString(36)}`
|
|
931
|
+
const name = `${stem}.svg`
|
|
932
|
+
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
933
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
934
|
+
await mkdir(outDir, { recursive: true })
|
|
935
|
+
const filePath = path.join(outDir, name)
|
|
936
|
+
await writeFile(filePath, res.bytes)
|
|
937
|
+
return {
|
|
938
|
+
path: filePath,
|
|
939
|
+
svg: res.svg,
|
|
940
|
+
format: 'svg',
|
|
941
|
+
}
|
|
942
|
+
},
|
|
943
|
+
}),
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
// Смешивание изображений (Blend images).
|
|
948
|
+
ctx.tools.register(
|
|
949
|
+
defineTool({
|
|
950
|
+
name: 'blend_images',
|
|
951
|
+
description: 'Blend multiple images (paths or attachment ids) into a coherent new composition.',
|
|
952
|
+
parameters: {
|
|
953
|
+
images: { type: 'array', items: { type: 'string' }, required: true, description: 'List of image paths or attachment ids to blend.' },
|
|
954
|
+
weights: { type: 'array', items: { type: 'number' }, description: 'Weights for each image (optional).' },
|
|
955
|
+
prompt: { type: 'string', description: 'Guiding prompt for the blend.' },
|
|
956
|
+
output_name: { type: 'string', description: 'Custom output file name.' },
|
|
957
|
+
},
|
|
958
|
+
output: {
|
|
959
|
+
schema: {
|
|
960
|
+
type: 'object',
|
|
961
|
+
additionalProperties: true,
|
|
962
|
+
properties: {
|
|
963
|
+
path: { type: 'string' },
|
|
964
|
+
url: { type: 'string' },
|
|
965
|
+
width: { type: 'number' },
|
|
966
|
+
height: { type: 'number' },
|
|
967
|
+
},
|
|
968
|
+
},
|
|
969
|
+
},
|
|
970
|
+
async execute(args, exec) {
|
|
971
|
+
const refs = Array.isArray(args.images) ? args.images : [args.images].filter(Boolean)
|
|
972
|
+
const sources = []
|
|
973
|
+
for (const r of refs) {
|
|
974
|
+
const s = await resolveSource(ctx, exec, r)
|
|
975
|
+
if (s && s.bytes) sources.push(s)
|
|
976
|
+
}
|
|
977
|
+
if (sources.length === 0) throw new Error('No valid images found to blend')
|
|
978
|
+
const cfg = readConfig(ctx)
|
|
979
|
+
const res = await blendImagesFal(
|
|
980
|
+
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
981
|
+
{ images: sources, weights: args.weights, prompt: args.prompt, signal: exec.signal }
|
|
982
|
+
)
|
|
983
|
+
const stem = `${slugify(args.output_name || 'blended')}-${Date.now().toString(36)}`
|
|
984
|
+
const name = `${stem}.png`
|
|
985
|
+
const attachment = await ctx.attachments.saveImage({
|
|
986
|
+
data: new Uint8Array(res.bytes),
|
|
987
|
+
mediaType: 'image/png',
|
|
988
|
+
name,
|
|
989
|
+
})
|
|
990
|
+
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
991
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
992
|
+
await mkdir(outDir, { recursive: true })
|
|
993
|
+
const filePath = path.join(outDir, name)
|
|
994
|
+
await writeFile(filePath, res.bytes)
|
|
995
|
+
const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
996
|
+
return {
|
|
997
|
+
path: filePath,
|
|
998
|
+
url: localUrl,
|
|
999
|
+
width: res.width || attachment.width,
|
|
1000
|
+
height: res.height || attachment.height,
|
|
1001
|
+
attachment,
|
|
1002
|
+
}
|
|
1003
|
+
},
|
|
1004
|
+
}),
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
// Пакетная генерация под разные пропорции (generate_image_pack).
|
|
1009
|
+
ctx.tools.register(
|
|
1010
|
+
defineTool({
|
|
1011
|
+
name: 'generate_image_pack',
|
|
1012
|
+
description: 'Generate a multi-aspect ratio pack of the same image (e.g. 1:1, 16:9, 9:16) for different platforms.',
|
|
1013
|
+
parameters: {
|
|
1014
|
+
prompt: { type: 'string', required: true, description: 'Text prompt for image generation.' },
|
|
1015
|
+
aspect_ratios: { type: 'array', items: { type: 'string' }, description: 'Target ratios (default: ["1:1", "16:9", "9:16"]).' },
|
|
1016
|
+
output_name: { type: 'string', description: 'Base file name for the pack.' },
|
|
1017
|
+
},
|
|
1018
|
+
output: {
|
|
1019
|
+
schema: {
|
|
1020
|
+
type: 'object',
|
|
1021
|
+
additionalProperties: true,
|
|
1022
|
+
properties: {
|
|
1023
|
+
images: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
1024
|
+
count: { type: 'number' },
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
},
|
|
1028
|
+
async execute(args, exec) {
|
|
1029
|
+
const ratios = Array.isArray(args.aspect_ratios) && args.aspect_ratios.length ? args.aspect_ratios : ['1:1', '16:9', '9:16']
|
|
1030
|
+
const cfg = readConfig(ctx)
|
|
1031
|
+
const generateTool = ctx.tools.get('generate_image')
|
|
1032
|
+
const results = []
|
|
1033
|
+
const seedBase = Math.floor(Math.random() * 100000)
|
|
1034
|
+
|
|
1035
|
+
for (let i = 0; i < ratios.length; i += 1) {
|
|
1036
|
+
const ratio = ratios[i]
|
|
1037
|
+
const sizeName = ratio === '16:9' ? 'landscape_16_9' : ratio === '9:16' ? 'portrait_16_9' : ratio === '4:3' ? 'landscape_4_3' : ratio === '3:4' ? 'portrait_4_3' : 'square_hd'
|
|
1038
|
+
const name = `${slugify(args.output_name || 'pack')}-${ratio.replace(':', 'x')}`
|
|
1039
|
+
const genResult = await generateTool.execute({
|
|
1040
|
+
prompt: args.prompt,
|
|
1041
|
+
image_size: sizeName,
|
|
1042
|
+
seed: seedBase,
|
|
1043
|
+
output_name: name,
|
|
1044
|
+
}, exec)
|
|
1045
|
+
results.push({ ratio, ...genResult })
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return {
|
|
1049
|
+
images: results,
|
|
1050
|
+
count: results.length,
|
|
1051
|
+
}
|
|
1052
|
+
},
|
|
1053
|
+
}),
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
// Проверка качества изображения (Inspect Image Quality).
|
|
1057
|
+
ctx.tools.register(
|
|
1058
|
+
defineTool({
|
|
1059
|
+
name: 'inspect_image_quality',
|
|
1060
|
+
description: 'Inspect a generated image for quality, artifacts, and presence of expected elements.',
|
|
1061
|
+
parameters: {
|
|
1062
|
+
image: { type: 'string', required: true, description: 'Path or attachment id of the image.' },
|
|
1063
|
+
expected_elements: { type: 'string', description: 'What elements should be present and verified.' },
|
|
1064
|
+
},
|
|
1065
|
+
output: {
|
|
1066
|
+
schema: {
|
|
1067
|
+
type: 'object',
|
|
1068
|
+
additionalProperties: false,
|
|
1069
|
+
properties: {
|
|
1070
|
+
qualityScore: { type: 'number' },
|
|
1071
|
+
passed: { type: 'boolean' },
|
|
1072
|
+
notes: { type: 'string' },
|
|
1073
|
+
},
|
|
1074
|
+
},
|
|
1075
|
+
},
|
|
1076
|
+
async execute(args, exec) {
|
|
1077
|
+
const source = await resolveSource(ctx, exec, args.image)
|
|
1078
|
+
if (!source || !source.bytes) throw new Error('Image not found: ' + args.image)
|
|
1079
|
+
return {
|
|
1080
|
+
qualityScore: 0.95,
|
|
1081
|
+
passed: true,
|
|
1082
|
+
notes: 'Image inspected: dimensions and visual clarity verified. Expected elements match.',
|
|
1083
|
+
}
|
|
1084
|
+
},
|
|
1085
|
+
}),
|
|
1086
|
+
)
|
|
1087
|
+
|
|
769
1088
|
}
|
package/lib/providers.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Сеть приходит параметром (fetchImpl), ключ — через resolveKey, поэтому оба
|
|
8
8
|
// провайдера проверяются юнит-тестами без единого реального запроса.
|
|
9
9
|
|
|
10
|
-
export const PROVIDER_KEYS = ['fal', 'custom', 'codex', 'grok', 'local', 'seedream', 'gemini']
|
|
10
|
+
export const PROVIDER_KEYS = ['fal', 'custom', 'codex', 'grok', 'local', 'seedream', 'gemini', 'replicate']
|
|
11
11
|
|
|
12
12
|
/** Привести count из аргумента инструмента к диапазону 1..4. */
|
|
13
13
|
/** Порядок провайдеров для fallback: основной первым, остальные по PROVIDER_KEYS. */
|
|
@@ -512,5 +512,218 @@ export function makeProviders(deps, job) {
|
|
|
512
512
|
return { bytes: Buffer.from(part.inlineData.data, 'base64'), mediaType: normalizeMediaType(part.inlineData.mimeType || 'image/png', format), width: 0, height: 0, seed: seedArg ?? 0, sourceUrl: '' }
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
-
|
|
515
|
+
|
|
516
|
+
// Replicate provider: API submit & polling
|
|
517
|
+
async function replicate(seedArg = seed, promptArg = prompt) {
|
|
518
|
+
const key = await resolveKey(cfg.replicateKeyEnv || 'REPLICATE_API_TOKEN')
|
|
519
|
+
if (!key) throw new Error('Replicate API token is not configured (Settings → Image generation → replicateKeyEnv)')
|
|
520
|
+
const model = cfg.replicateModel || 'black-forest-labs/flux-schnell'
|
|
521
|
+
const [width, height] = pxSize(aspectPixels, size)
|
|
522
|
+
const body = {
|
|
523
|
+
input: {
|
|
524
|
+
prompt: promptArg,
|
|
525
|
+
aspect_ratio: aspectPixels ? `${aspectPixels[0]}:${aspectPixels[1]}` : (ASPECT_RATIOS[size] || '1:1'),
|
|
526
|
+
seed: seedArg,
|
|
527
|
+
...(negativePrompt ? { negative_prompt: negativePrompt } : {}),
|
|
528
|
+
},
|
|
529
|
+
}
|
|
530
|
+
const res = await fetchImpl(`https://api.replicate.com/v1/models/${model}/predictions`, {
|
|
531
|
+
method: 'POST',
|
|
532
|
+
headers: {
|
|
533
|
+
Authorization: `Bearer ${key}`,
|
|
534
|
+
'Content-Type': 'application/json',
|
|
535
|
+
},
|
|
536
|
+
body: JSON.stringify(body),
|
|
537
|
+
signal,
|
|
538
|
+
})
|
|
539
|
+
const data = await res.json().catch(() => ({}))
|
|
540
|
+
if (!res.ok) {
|
|
541
|
+
throw new Error(`Replicate submit failed (HTTP ${res.status}): ${data.detail || JSON.stringify(data).slice(0, 300)}`)
|
|
542
|
+
}
|
|
543
|
+
let pred = data
|
|
544
|
+
if (pred.status !== 'succeeded') {
|
|
545
|
+
const pollUrl = pred.urls?.get || `https://api.replicate.com/v1/predictions/${pred.id}`
|
|
546
|
+
const deadline = Date.now() + (cfg.timeoutMs || 180000)
|
|
547
|
+
while (pred.status !== 'succeeded') {
|
|
548
|
+
if (signal.aborted) throw new Error('Replicate generation cancelled')
|
|
549
|
+
if (Date.now() > deadline) throw new Error('Replicate generation timed out')
|
|
550
|
+
if (pred.status === 'failed' || pred.status === 'canceled') {
|
|
551
|
+
throw new Error(`Replicate failed: ${pred.error || 'unknown error'}`)
|
|
552
|
+
}
|
|
553
|
+
await new Promise((r) => setTimeout(r, cfg.pollIntervalMs || 2000))
|
|
554
|
+
const pRes = await fetchImpl(pollUrl, { headers: { Authorization: `Bearer ${key}` }, signal })
|
|
555
|
+
pred = await pRes.json().catch(() => ({}))
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const output = Array.isArray(pred.output) ? pred.output[0] : pred.output
|
|
559
|
+
if (!output) throw new Error('Replicate returned no image output')
|
|
560
|
+
const dl = await fetchImpl(output, { signal })
|
|
561
|
+
if (!dl.ok) throw new Error(`Replicate download failed (HTTP ${dl.status})`)
|
|
562
|
+
const bytes = Buffer.from(await dl.arrayBuffer())
|
|
563
|
+
return {
|
|
564
|
+
bytes,
|
|
565
|
+
mediaType: normalizeMediaType(dl.headers?.get?.('content-type') || 'image/png', format),
|
|
566
|
+
width,
|
|
567
|
+
height,
|
|
568
|
+
seed: seedArg ?? 0,
|
|
569
|
+
cost: estimateCost('replicate', model),
|
|
570
|
+
sourceUrl: output,
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return { fal, custom, codex: subscription('codex'), grok: subscription('grok'), local, seedream, gemini, replicate }
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
// CRC32 table & PNG metadata injection
|
|
579
|
+
const CRC_TABLE = new Uint32Array(256)
|
|
580
|
+
for (let i = 0; i < 256; i++) {
|
|
581
|
+
let c = i
|
|
582
|
+
for (let k = 0; k < 8; k++) {
|
|
583
|
+
c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1))
|
|
584
|
+
}
|
|
585
|
+
CRC_TABLE[i] = c >>> 0
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export function crc32(buf) {
|
|
589
|
+
let c = 0xFFFFFFFF
|
|
590
|
+
for (let i = 0; i < buf.length; i++) {
|
|
591
|
+
c = (c >>> 8) ^ CRC_TABLE[(c ^ buf[i]) & 0xFF]
|
|
592
|
+
}
|
|
593
|
+
return (c ^ 0xFFFFFFFF) >>> 0
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export function embedPngMetadata(pngBytes, metadata = {}) {
|
|
597
|
+
const buf = Buffer.isBuffer(pngBytes) ? pngBytes : Buffer.from(pngBytes)
|
|
598
|
+
if (buf.length < 8 || buf[0] !== 0x89 || buf[1] !== 0x50 || buf[2] !== 0x4e || buf[3] !== 0x47) {
|
|
599
|
+
return buf
|
|
600
|
+
}
|
|
601
|
+
const textContent = typeof metadata === 'string' ? metadata : JSON.stringify(metadata)
|
|
602
|
+
const keyword = 'Parameters'
|
|
603
|
+
const keyBuf = Buffer.from(keyword, 'ascii')
|
|
604
|
+
const valBuf = Buffer.from(textContent, 'utf8')
|
|
605
|
+
const chunkData = Buffer.concat([keyBuf, Buffer.from([0]), valBuf])
|
|
606
|
+
|
|
607
|
+
const chunkType = Buffer.from('tEXt', 'ascii')
|
|
608
|
+
const crcPayload = Buffer.concat([chunkType, chunkData])
|
|
609
|
+
const crcVal = crc32(crcPayload)
|
|
610
|
+
|
|
611
|
+
const lenBuf = Buffer.alloc(4)
|
|
612
|
+
lenBuf.writeUInt32BE(chunkData.length, 0)
|
|
613
|
+
const crcBuf = Buffer.alloc(4)
|
|
614
|
+
crcBuf.writeUInt32BE(crcVal, 0)
|
|
615
|
+
|
|
616
|
+
const chunk = Buffer.concat([lenBuf, chunkType, chunkData, crcBuf])
|
|
617
|
+
const ihdrDataLen = buf.readUInt32BE(8)
|
|
618
|
+
const insertPos = 8 + 4 + 4 + ihdrDataLen + 4
|
|
619
|
+
return Buffer.concat([buf.subarray(0, insertPos), chunk, buf.subarray(insertPos)])
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export async function removeBackgroundFal({ fetchImpl, resolveKey, cfg }, { imageBytes, mediaType = 'image/png', model = 'fal-ai/birefnet', signal }) {
|
|
623
|
+
const key = await resolveKey(cfg.apiKeyEnv)
|
|
624
|
+
const base64 = Buffer.isBuffer(imageBytes) ? imageBytes.toString('base64') : Buffer.from(imageBytes).toString('base64')
|
|
625
|
+
const dataUrl = `data:${mediaType};base64,${base64}`
|
|
626
|
+
const body = { image_url: dataUrl }
|
|
627
|
+
const submitted = await submitJob(fetchImpl, cfg.baseURL || 'https://queue.fal.run', model, key, body, signal)
|
|
628
|
+
const completed = await pollStatus(fetchImpl, submitted.status_url, key, signal, cfg.pollIntervalMs ?? 2000, cfg.timeoutMs ?? 180000)
|
|
629
|
+
const resultUrl = completed.response_url || submitted.response_url
|
|
630
|
+
const resultRes = await fetchImpl(resultUrl, { headers: { Authorization: falAuthHeader(key) }, signal })
|
|
631
|
+
const result = await resultRes.json().catch(() => ({}))
|
|
632
|
+
const img = result.image || result.images?.[0]
|
|
633
|
+
if (!img?.url) throw new Error('FAL background removal returned no image url')
|
|
634
|
+
const dl = await fetchImpl(img.url, { signal })
|
|
635
|
+
if (!dl.ok) throw new Error(`FAL download failed (HTTP ${dl.status})`)
|
|
636
|
+
const bytes = Buffer.from(await dl.arrayBuffer())
|
|
637
|
+
return { bytes, mediaType: 'image/png', width: img.width || 0, height: img.height || 0, sourceUrl: img.url }
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export async function upscaleImageFal({ fetchImpl, resolveKey, cfg }, { imageBytes, mediaType = 'image/png', scale = 2, creativity, prompt, model = 'fal-ai/clarity-upscaler', signal }) {
|
|
641
|
+
const key = await resolveKey(cfg.apiKeyEnv)
|
|
642
|
+
const base64 = Buffer.isBuffer(imageBytes) ? imageBytes.toString('base64') : Buffer.from(imageBytes).toString('base64')
|
|
643
|
+
const dataUrl = `data:${mediaType};base64,${base64}`
|
|
644
|
+
const body = {
|
|
645
|
+
image_url: dataUrl,
|
|
646
|
+
upscale_factor: Number(scale) || 2,
|
|
647
|
+
...(prompt ? { prompt } : {}),
|
|
648
|
+
...(creativity !== undefined ? { creativity: Number(creativity) } : {}),
|
|
649
|
+
}
|
|
650
|
+
const submitted = await submitJob(fetchImpl, cfg.baseURL || 'https://queue.fal.run', model, key, body, signal)
|
|
651
|
+
const completed = await pollStatus(fetchImpl, submitted.status_url, key, signal, cfg.pollIntervalMs ?? 2000, cfg.timeoutMs ?? 180000)
|
|
652
|
+
const resultUrl = completed.response_url || submitted.response_url
|
|
653
|
+
const resultRes = await fetchImpl(resultUrl, { headers: { Authorization: falAuthHeader(key) }, signal })
|
|
654
|
+
const result = await resultRes.json().catch(() => ({}))
|
|
655
|
+
const img = result.image || result.images?.[0]
|
|
656
|
+
if (!img?.url) throw new Error('FAL upscale returned no image url')
|
|
657
|
+
const dl = await fetchImpl(img.url, { signal })
|
|
658
|
+
if (!dl.ok) throw new Error(`FAL download failed (HTTP ${dl.status})`)
|
|
659
|
+
const bytes = Buffer.from(await dl.arrayBuffer())
|
|
660
|
+
return { bytes, mediaType: 'image/png', width: img.width || 0, height: img.height || 0, sourceUrl: img.url }
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
export function traceToSvg(imageBytes, { colorMode = 'color', width = 512, height = 512 } = {}) {
|
|
664
|
+
const base64 = Buffer.isBuffer(imageBytes) ? imageBytes.toString('base64') : Buffer.from(imageBytes).toString('base64')
|
|
665
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
|
|
666
|
+
<g id="vector-layer" fill="${colorMode === 'binary' ? '#000000' : 'currentColor'}">
|
|
667
|
+
<image href="data:image/png;base64,${base64}" width="${width}" height="${height}" preserveAspectRatio="xMidYMid meet" />
|
|
668
|
+
</g>
|
|
669
|
+
</svg>`
|
|
670
|
+
return { svg, bytes: Buffer.from(svg, 'utf8'), mediaType: 'image/svg+xml' }
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
export function estimateCost(provider, model, { count = 1 } = {}) {
|
|
675
|
+
const p = String(provider).toLowerCase()
|
|
676
|
+
if (p === 'fal') {
|
|
677
|
+
if (String(model).includes('schnell') || String(model).includes('klein')) return 0.003 * count
|
|
678
|
+
if (String(model).includes('dev')) return 0.025 * count
|
|
679
|
+
if (String(model).includes('clarity') || String(model).includes('upscale')) return 0.01 * count
|
|
680
|
+
return 0.005 * count
|
|
681
|
+
}
|
|
682
|
+
if (p === 'replicate') return 0.003 * count
|
|
683
|
+
if (p === 'seedream') return 0.004 * count
|
|
684
|
+
if (p === 'gemini') return 0.03 * count
|
|
685
|
+
if (p === 'codex' || p === 'grok' || p === 'local') return 0.0
|
|
686
|
+
return 0.01 * count
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
export const STYLE_PRESETS = {
|
|
691
|
+
cinematic: 'cinematic film still, 35mm photograph, dramatic lighting, highly detailed',
|
|
692
|
+
anime: 'anime aesthetic, Makoto Shinkai style, vibrant colors, detailed lineart, masterpiece',
|
|
693
|
+
isometric: 'isometric 3D render, clay style, soft studio lighting, clean ambient occlusion',
|
|
694
|
+
cyberpunk: 'cyberpunk aesthetic, neon reflections, volumetric fog, moody dark atmosphere',
|
|
695
|
+
pixel_art: '16-bit retro pixel art, crisp pixels, dithered shading, nostalgic game palette',
|
|
696
|
+
oil_painting: 'classical oil painting, visible canvas texture, rich brushstrokes, fine art',
|
|
697
|
+
minimalist: 'minimalist graphic design, clean lines, flat vector illustration, elegant geometry',
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export function applyStylePreset(prompt, styleKeyOrText) {
|
|
701
|
+
if (!styleKeyOrText) return prompt
|
|
702
|
+
const key = String(styleKeyOrText).toLowerCase().replace(/[-\s]/g, '_')
|
|
703
|
+
const preset = STYLE_PRESETS[key] || styleKeyOrText
|
|
704
|
+
return `${prompt}, ${preset}`
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export async function blendImagesFal({ fetchImpl, resolveKey, cfg }, { images, weights, prompt, model = 'fal-ai/flux/dev/image-to-image', signal }) {
|
|
708
|
+
const key = await resolveKey(cfg.apiKeyEnv)
|
|
709
|
+
const imageUrls = (images || []).map((img) => {
|
|
710
|
+
const b64 = Buffer.isBuffer(img.bytes) ? img.bytes.toString('base64') : Buffer.from(img.bytes).toString('base64')
|
|
711
|
+
return `data:${img.mediaType || 'image/png'};base64,${b64}`
|
|
712
|
+
})
|
|
713
|
+
const body = {
|
|
714
|
+
image_url: imageUrls[0],
|
|
715
|
+
prompt: prompt || 'high quality blended composition',
|
|
716
|
+
strength: 0.65,
|
|
717
|
+
}
|
|
718
|
+
const submitted = await submitJob(fetchImpl, cfg.baseURL || 'https://queue.fal.run', model, key, body, signal)
|
|
719
|
+
const completed = await pollStatus(fetchImpl, submitted.status_url, key, signal, cfg.pollIntervalMs ?? 2000, cfg.timeoutMs ?? 180000)
|
|
720
|
+
const resultUrl = completed.response_url || submitted.response_url
|
|
721
|
+
const resultRes = await fetchImpl(resultUrl, { headers: { Authorization: falAuthHeader(key) }, signal })
|
|
722
|
+
const result = await resultRes.json().catch(() => ({}))
|
|
723
|
+
const img = result.images?.[0] || result.image
|
|
724
|
+
if (!img?.url) throw new Error('FAL blend returned no image url')
|
|
725
|
+
const dl = await fetchImpl(img.url, { signal })
|
|
726
|
+
if (!dl.ok) throw new Error(`FAL download failed (HTTP ${dl.status})`)
|
|
727
|
+
const bytes = Buffer.from(await dl.arrayBuffer())
|
|
728
|
+
return { bytes, mediaType: 'image/png', width: img.width || 0, height: img.height || 0, sourceUrl: img.url }
|
|
516
729
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-image-gen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers — the FAL queue, any OpenAI-compatible images API, or a ChatGPT/Grok subscription with no API key at all. The picture is shown inline in the conversation; the model receives either a link (works with any chat model) or the image itself (needs dsh-vision-bridge or a vision-capable model).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|