ak-gemini 2.1.5 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/base.js +52 -13
- package/code-agent.js +190 -40
- package/image-generator.js +186 -0
- package/index.cjs +368 -54
- package/index.js +3 -1
- package/package.json +3 -2
- package/types.d.ts +78 -3
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview ImageGenerator — Generate images via Gemini's Nano Banana models.
|
|
3
|
+
*
|
|
4
|
+
* Extends BaseGemini for auth/client reuse but overrides init() to skip chat session
|
|
5
|
+
* creation (image gen is stateless). Mirrors the Embedding class pattern.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```javascript
|
|
9
|
+
* import { ImageGenerator } from 'ak-gemini';
|
|
10
|
+
* import { writeFileSync } from 'node:fs';
|
|
11
|
+
*
|
|
12
|
+
* const gen = new ImageGenerator({ apiKey: 'your-key' });
|
|
13
|
+
* const result = await gen.generate('A cat astronaut on the moon');
|
|
14
|
+
* writeFileSync('cat.png', Buffer.from(result.images[0].data, 'base64'));
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import BaseGemini from './base.js';
|
|
19
|
+
import log from './logger.js';
|
|
20
|
+
import { writeFileSync } from 'node:fs';
|
|
21
|
+
|
|
22
|
+
const DEFAULT_IMAGE_MODEL = 'gemini-3.1-flash-image-preview';
|
|
23
|
+
|
|
24
|
+
export default class ImageGenerator extends BaseGemini {
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {import('./types.d.ts').ImageGeneratorOptions} [options={}]
|
|
28
|
+
*/
|
|
29
|
+
constructor(options = {}) {
|
|
30
|
+
if (options.modelName === undefined) {
|
|
31
|
+
options = { ...options, modelName: DEFAULT_IMAGE_MODEL };
|
|
32
|
+
}
|
|
33
|
+
if (options.systemPrompt === undefined) {
|
|
34
|
+
options = { ...options, systemPrompt: null };
|
|
35
|
+
}
|
|
36
|
+
super(options);
|
|
37
|
+
|
|
38
|
+
this.aspectRatio = options.aspectRatio || null;
|
|
39
|
+
this.imageSize = options.imageSize || null;
|
|
40
|
+
this.personGeneration = options.personGeneration || null;
|
|
41
|
+
this.includeText = options.includeText ?? false;
|
|
42
|
+
|
|
43
|
+
log.debug(`ImageGenerator created with model: ${this.modelName}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validate API connection only; no chat session (stateless).
|
|
48
|
+
* @param {boolean} [force=false]
|
|
49
|
+
*/
|
|
50
|
+
async init(force = false) {
|
|
51
|
+
if (this._initialized && !force) return;
|
|
52
|
+
|
|
53
|
+
log.debug(`Initializing ${this.constructor.name} with model: ${this.modelName}...`);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await this.genAIClient.models.list();
|
|
57
|
+
log.debug(`${this.constructor.name}: API connection successful.`);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
throw new Error(`${this.constructor.name} initialization failed: ${e.message}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this._initialized = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build a FRESH config — Gemini image models reject safetySettings/temp/topK/topP/thinkingConfig.
|
|
67
|
+
* Do NOT spread this.chatConfig.
|
|
68
|
+
* @private
|
|
69
|
+
*/
|
|
70
|
+
_buildConfig(overrides = {}) {
|
|
71
|
+
const includeText = overrides.includeText ?? this.includeText;
|
|
72
|
+
const config = { responseModalities: includeText ? ['IMAGE', 'TEXT'] : ['IMAGE'] };
|
|
73
|
+
|
|
74
|
+
const imageConfig = {};
|
|
75
|
+
const aspectRatio = overrides.aspectRatio || this.aspectRatio;
|
|
76
|
+
const imageSize = overrides.imageSize || this.imageSize;
|
|
77
|
+
const personGeneration = overrides.personGeneration || this.personGeneration;
|
|
78
|
+
if (aspectRatio) imageConfig.aspectRatio = aspectRatio;
|
|
79
|
+
if (imageSize) imageConfig.imageSize = imageSize;
|
|
80
|
+
if (personGeneration) imageConfig.personGeneration = personGeneration;
|
|
81
|
+
if (Object.keys(imageConfig).length > 0) config.imageConfig = imageConfig;
|
|
82
|
+
|
|
83
|
+
return config;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Generate one or more images from a text prompt.
|
|
88
|
+
* Optionally accepts `inputImages` for image editing / multi-image composition.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} prompt
|
|
91
|
+
* @param {import('./types.d.ts').ImageGenerateOptions} [opts={}]
|
|
92
|
+
* @returns {Promise<import('./types.d.ts').ImageGenerationResult>}
|
|
93
|
+
*/
|
|
94
|
+
async generate(prompt, opts = {}) {
|
|
95
|
+
if (!this._initialized) await this.init();
|
|
96
|
+
|
|
97
|
+
/** @type {any[]} */
|
|
98
|
+
const parts = [{ text: prompt }];
|
|
99
|
+
if (Array.isArray(opts.inputImages)) {
|
|
100
|
+
for (const img of opts.inputImages) {
|
|
101
|
+
parts.push({ inlineData: { data: img.data, mimeType: img.mimeType } });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const result = await this._withRetry(() => this.genAIClient.models.generateContent({
|
|
106
|
+
model: this.modelName,
|
|
107
|
+
contents: [{ role: 'user', parts }],
|
|
108
|
+
config: this._buildConfig(opts)
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
this._captureMetadata(result);
|
|
112
|
+
this._cumulativeUsage = {
|
|
113
|
+
promptTokens: this.lastResponseMetadata.promptTokens,
|
|
114
|
+
responseTokens: this.lastResponseMetadata.responseTokens,
|
|
115
|
+
totalTokens: this.lastResponseMetadata.totalTokens,
|
|
116
|
+
attempts: 1
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const images = [];
|
|
120
|
+
let text = '';
|
|
121
|
+
const responseParts = result.candidates?.[0]?.content?.parts || [];
|
|
122
|
+
for (const part of responseParts) {
|
|
123
|
+
if (part.inlineData?.data) {
|
|
124
|
+
images.push({
|
|
125
|
+
data: part.inlineData.data,
|
|
126
|
+
mimeType: part.inlineData.mimeType || 'image/png'
|
|
127
|
+
});
|
|
128
|
+
} else if (part.text) {
|
|
129
|
+
text += part.text;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (images.length === 0) {
|
|
134
|
+
log.warn('ImageGenerator: no images returned. Check prompt or safety filters.');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { images, text: text || null, usage: this.getLastUsage() };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Convenience: write one or all images to disk.
|
|
142
|
+
* If multiple images, suffixes with `_N` before extension.
|
|
143
|
+
* @param {import('./types.d.ts').ImageGenerationResult} result
|
|
144
|
+
* @param {string} filePath
|
|
145
|
+
* @returns {string[]} Written file paths
|
|
146
|
+
*/
|
|
147
|
+
save(result, filePath) {
|
|
148
|
+
if (!result?.images?.length) {
|
|
149
|
+
log.warn('ImageGenerator.save(): no images to save.');
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
const paths = [];
|
|
153
|
+
const dot = filePath.lastIndexOf('.');
|
|
154
|
+
const base = dot >= 0 ? filePath.slice(0, dot) : filePath;
|
|
155
|
+
const ext = dot >= 0 ? filePath.slice(dot) : '.png';
|
|
156
|
+
result.images.forEach((img, i) => {
|
|
157
|
+
const out = result.images.length === 1 ? filePath : `${base}_${i}${ext}`;
|
|
158
|
+
writeFileSync(out, Buffer.from(img.data, 'base64'));
|
|
159
|
+
paths.push(out);
|
|
160
|
+
});
|
|
161
|
+
return paths;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── No-ops (image gen is stateless) ──
|
|
165
|
+
|
|
166
|
+
/** @returns {any[]} Always returns empty array */
|
|
167
|
+
getHistory() { return []; }
|
|
168
|
+
|
|
169
|
+
/** No-op for ImageGenerator */
|
|
170
|
+
async clearHistory() {}
|
|
171
|
+
|
|
172
|
+
/** No-op for ImageGenerator */
|
|
173
|
+
async seed() {
|
|
174
|
+
log.warn('ImageGenerator.seed() is a no-op — image generation does not support few-shot.');
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {any} _nextPayload
|
|
180
|
+
* @throws {Error} ImageGenerator does not support token estimation
|
|
181
|
+
* @returns {Promise<{ inputTokens: number }>}
|
|
182
|
+
*/
|
|
183
|
+
async estimate(_nextPayload) {
|
|
184
|
+
throw new Error('ImageGenerator does not support token estimation. Use generate() directly.');
|
|
185
|
+
}
|
|
186
|
+
}
|