Google: Add Imagen image generation

2d28f5cdaf2eaeca4444422ed61503997c8d6b6e

Cohee <18619528+Cohee1207@users.noreply.github.com>

4 files changed, +213 -25Ignore whitespace
public/scripts/extensions/stable-diffusion/index.js+117 -14
@@ -58,6 +58,7 @@ import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnum
5858import { ToolManager } from '../../tool-calling.js';
5959import { MacrosParser } from '../../macros.js';
6060import { t } from '../../i18n.js';
61+import { oai_settings } from '../../openai.js';
6162
6263export { MODULE_NAME };
6364
@@ -85,6 +86,7 @@ const sources = {
8586 bfl: 'bfl',
8687 falai: 'falai',
8788 xai: 'xai',
89+ google: 'google',
8890};
8991
9092const initiators = {
@@ -330,6 +332,10 @@ const defaultSettings = {
330332
331333 // BFL API settings
332334 bfl_upsampling: false,
335+
336+ // Google settings
337+ google_api: 'makersuite',
338+ google_enhance: true,
333339};
334340
335341const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);
@@ -509,6 +515,8 @@ async function loadSettings() {
509515 $('#sd_huggingface_model_id').val(extension_settings.sd.huggingface_model_id);
510516 $('#sd_function_tool').prop('checked', extension_settings.sd.function_tool);
511517 $('#sd_bfl_upsampling').prop('checked', extension_settings.sd.bfl_upsampling);
518+ $('#sd_google_api').val(extension_settings.sd.google_api);
519+ $('#sd_google_enhance').prop('checked', extension_settings.sd.google_enhance);
512520
513521 for (const style of extension_settings.sd.styles) {
514522 const option = document.createElement('option');
@@ -1277,6 +1285,7 @@ async function onModelChange() {
12771285 sources.bfl,
12781286 sources.falai,
12791287 sources.xai,
1288+ sources.google,
12801289 ];
12811290
12821291 if (cloudSources.includes(extension_settings.sd.source)) {
@@ -1498,6 +1507,9 @@ async function loadSamplers() {
14981507 case sources.xai:
14991508 samplers = ['N/A'];
15001509 break;
1510+ case sources.google:
1511+ samplers = ['N/A'];
1512+ break;
15011513 }
15021514
15031515 for (const sampler of samplers) {
@@ -1694,6 +1706,9 @@ async function loadModels() {
16941706 case sources.xai:
16951707 models = await loadXAIModels();
16961708 break;
1709+ case sources.google:
1710+ models = await loadGoogleModels();
1711+ break;
16971712 }
16981713
16991714 for (const model of models) {
@@ -2023,6 +2038,21 @@ async function loadNovelModels() {
20232038 ];
20242039}
20252040
2041+async function loadGoogleModels() {
2042+ return [
2043+ 'imagen-4.0-generate-preview-06-06',
2044+ 'imagen-4.0-fast-generate-preview-06-06',
2045+ 'imagen-4.0-ultra-generate-preview-06-06',
2046+ 'imagen-3.0-generate-002',
2047+ 'imagen-3.0-generate-001',
2048+ 'imagen-3.0-fast-generate-001',
2049+ 'imagen-3.0-capability-001',
2050+ 'imagegeneration@006',
2051+ 'imagegeneration@005',
2052+ 'imagegeneration@002',
2053+ ].map(name => ({ value: name, text: name }));
2054+}
2055+
20262056function loadNovelSchedulers() {
20272057 return ['karras', 'native', 'exponential', 'polyexponential'];
20282058}
@@ -2105,6 +2135,9 @@ async function loadSchedulers() {
21052135 case sources.xai:
21062136 schedulers = ['N/A'];
21072137 break;
2138+ case sources.google:
2139+ schedulers = ['N/A'];
2140+ break;
21082141 }
21092142
21102143 for (const scheduler of schedulers) {
@@ -2199,6 +2232,9 @@ async function loadVaes() {
21992232 case sources.xai:
22002233 vaes = ['N/A'];
22012234 break;
2235+ case sources.google:
2236+ vaes = ['N/A'];
2237+ break;
22022238 }
22032239
22042240 for (const vae of vaes) {
@@ -2779,6 +2815,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
27792815 case sources.xai:
27802816 result = await generateXAIImage(prefixedPrompt, negativePrompt, signal);
27812817 break;
2818+ case sources.google:
2819+ result = await generateGoogleImage(prefixedPrompt, negativePrompt, signal);
2820+ break;
27822821 }
27832822
27842823 if (!result.data) {
@@ -2916,20 +2955,39 @@ async function generateExtrasImage(prompt, negativePrompt, signal) {
29162955 * Gets an aspect ratio for Stability that is the closest to the given width and height.
29172956 * @param {number} width Target width
29182957 * @param {number} height Target height
2958+ * @param {'google'|'stability'} source Source of the request, used to determine aspect ratio
29192959 * @returns {string} Closest aspect ratio as a string
29202960 */
29212961function getClosestAspectRatio(width, height, source) {
29222962 const aspectRatiosfunction =getAspectRatios() {
2923- '16:9': 16 / 9,
2963+ switch (source) {
2924- '1:1': 1,
2964+ case 'stability':
2925- '21:9': 21 / 9,
2965+ return {
29262966 '216:39': 216 / 39,
29272967 '31:21': 3 / 21,
29282968 '421:59': 421 / 59,
29292969 '52:43': 52 / 43,
29302970 '93:162': 93 / 162,
29312971 '94:215': 94 / 215,
2932- };
2972+ '5:4': 5 / 4,
2973+ '9:16': 9 / 16,
2974+ '9:21': 9 / 21,
2975+ };
2976+ case 'google':
2977+ return {
2978+ '1:1': 1,
2979+ '16:9': 16 / 9,
2980+ '9:16': 9 / 16,
2981+ '4:3': 4 / 3,
2982+ '3:4': 3 / 4,
2983+ };
2984+ default:
2985+ console.warn(`Unknown source "${source}" for aspect ratio calculation.`);
2986+ return null;
2987+ }
2988+ }
2989+
2990+ const aspectRatios = getAspectRatios() || { '1:1': 1 };
29332991
29342992 const aspectRatio = width / height;
29352993
@@ -2968,7 +3026,7 @@ async function generateStabilityImage(prompt, negativePrompt, signal) {
29683026 payload: {
29693027 prompt: prompt.slice(0, PROMPT_LIMIT),
29703028 negative_prompt: negativePrompt.slice(0, PROMPT_LIMIT),
29713029 aspect_ratio: getClosestAspectRatio(extension_settings.sd.width, extension_settings.sd.height, 'stability'),
29723030 seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
29733031 style_preset: extension_settings.sd.stability_style_preset,
29743032 output_format: IMAGE_FORMAT,
@@ -3619,7 +3677,41 @@ async function generateFalaiImage(prompt, negativePrompt, signal) {
36193677 return { format: 'jpg', data: data.image };
36203678 } else {
36213679 const text = await result.text();
36223680 console.logthrow new Error(text);
3681+ }
3682+}
3683+
3684+/**
3685+ * Generates an image using the Google Vertex AI API.
3686+ * @param {string} prompt The main instruction used to guide the image generation.
3687+ * @param {string} negativePrompt The instruction used to restrict the image generation.
3688+ * @param {AbortSignal} signal An AbortSignal object that can be used to cancel the request.
3689+ * @returns {Promise<{format: string, data: string}>} A promise that resolves when the image generation and processing are complete.
3690+ */
3691+async function generateGoogleImage(prompt, negativePrompt, signal) {
3692+ const result = await fetch('/api/google/generate-image', {
3693+ method: 'POST',
3694+ headers: getRequestHeaders(),
3695+ signal: signal,
3696+ body: JSON.stringify({
3697+ prompt: prompt,
3698+ aspect_ratio: getClosestAspectRatio(extension_settings.sd.width, extension_settings.sd.height, 'google'),
3699+ negative_prompt: negativePrompt,
3700+ model: extension_settings.sd.model,
3701+ enhance: extension_settings.sd.google_enhance,
3702+ api: extension_settings.sd.google_api || 'makersuite',
3703+ seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
3704+ vertexai_auth_mode: oai_settings.vertexai_auth_mode,
3705+ vertexai_region: oai_settings.vertexai_region,
3706+ vertexai_express_project_id: oai_settings.vertexai_express_project_id,
3707+ }),
3708+ });
3709+
3710+ if (result.ok) {
3711+ const data = await result.json();
3712+ return { format: 'jpg', data: data.image };
3713+ } else {
3714+ const text = await result.text();
36233715 throw new Error(text);
36243716 }
36253717}
@@ -3913,6 +4005,8 @@ function isValidState() {
39134005 return secret_state[SECRET_KEYS.FALAI];
39144006 case sources.xai:
39154007 return secret_state[SECRET_KEYS.XAI];
4008+ case sources.google:
4009+ return secret_state[SECRET_KEYS.MAKERSUITE] || secret_state[SECRET_KEYS.VERTEXAI] || secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT];
39164010 }
39174011}
39184012
@@ -4573,6 +4667,15 @@ jQuery(async () => {
45734667 $('#sd_function_tool').on('input', onFunctionToolInput);
45744668 $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);
45754669
4670+ $('#sd_google_api').on('input', function () {
4671+ extension_settings.sd.google_api = String($(this).val());
4672+ saveSettingsDebounced();
4673+ });
4674+ $('#sd_google_enhance').on('input', function () {
4675+ extension_settings.sd.google_enhance = $(this).prop('checked');
4676+ saveSettingsDebounced();
4677+ });
4678+
45764679 if (!CSS.supports('field-sizing', 'content')) {
45774680 $('.sd_settings .inline-drawer-toggle').on('click', function () {
45784681 initScrollHeight($('#sd_prompt_prefix'));
public/scripts/extensions/stable-diffusion/settings.html+21 -0
@@ -43,6 +43,7 @@
4343 <option value="drawthings">DrawThings HTTP API</option>
4444 <option value="extras">Extras API (deprecated)</option>
4545 <option value="falai">FAL.AI</option>
46+ <option value="google">Google AI</option>
4647 <option value="huggingface">HuggingFace Inference API (serverless)</option>
4748 <option value="nanogpt">NanoGPT</option>
4849 <option value="novel">NovelAI Diffusion</option>
@@ -281,6 +282,26 @@
281282 </div>
282283 </div>
283284
285+ <div data-sd-source="google">
286+ <div class="flex-container">
287+ <div class="flex1">
288+ <label for="sd_google_api" data-i18n="API Type">API Type</label>
289+ <select id="sd_google_api" class="text_pole">
290+ <option value="makersuite">Google AI Studio</option>
291+ <option value="vertexai">Google Vertex AI</option>
292+ </select>
293+ </div>
294+ </div>
295+ <div class="flex-container">
296+ <label class="flex1 checkbox_label" for="sd_google_enhance" title="Enables prompt enhancing (passes prompts through an LLM to add detail).">
297+ <input id="sd_google_enhance" type="checkbox" />
298+ <span data-i18n="Enhance">
299+ Enhance
300+ </span>
301+ </label>
302+ </div>
303+ </div>
304+
284305 <div class="flex-container">
285306 <div class="flex1">
286307 <label for="sd_model" data-i18n="Model">Model</label>
src/endpoints/google.js+65 -0
@@ -418,3 +418,68 @@ router.post('/generate-native-tts', async (request, response) => {
418418 return response.end();
419419 }
420420});
421+
422+router.post('/generate-image', async (request, response) => {
423+ try {
424+ const model = request.body.model || 'imagen-3.0-generate-002';
425+ const { url, headers, apiName } = await getGoogleApiConfig(request, model, 'predict');
426+
427+ // block_none for safetySetting is currently not supported.
428+ // AI Studio is stricter than Vertex AI.
429+ const safetySetting = request.body.api === 'vertexai'
430+ ? 'block_only_high'
431+ : 'block_low_and_above';
432+
433+ const requestBody = {
434+ instances: [{
435+ prompt: request.body.prompt || '',
436+ }],
437+ parameters: {
438+ sampleCount: 1,
439+ seed: Number(request.body.seed ?? Math.floor(Math.random() * 1000000)),
440+ enhancePrompt: Boolean(request.body.enhance ?? false),
441+ negativePrompt: request.body.negative_prompt || undefined,
442+ aspectRatio: String(request.body.aspect_ratio || '1:1'),
443+ personGeneration: 'allow_all',
444+ language: 'auto',
445+ safetySetting: safetySetting,
446+ addWatermark: false,
447+ outputOptions: {
448+ mimeType: 'image/jpeg',
449+ compressionQuality: 100,
450+ },
451+ },
452+ };
453+
454+ console.debug(`${apiName} image generation request:`, model, requestBody);
455+
456+ const result = await fetch(url, {
457+ method: 'POST',
458+ headers: headers,
459+ body: JSON.stringify(requestBody),
460+ });
461+
462+ if (!result.ok) {
463+ const errorText = await result.text();
464+ console.warn(`${apiName} image generation error: ${result.status} ${result.statusText}`, errorText);
465+ return response.sendStatus(500);
466+ }
467+
468+ /** @type {any} */
469+ const data = await result.json();
470+ const imagePart = data?.predictions?.[0]?.bytesBase64Encoded;
471+
472+ if (!imagePart) {
473+ console.warn(`${apiName} image generation error: No image data found in response`);
474+ return response.sendStatus(500);
475+ }
476+
477+ return response.send({ image: imagePart });
478+ } catch (error) {
479+ console.error('Google Image generation failed:', error);
480+ if (!response.headersSent) {
481+ return response.sendStatus(500);
482+ }
483+ return response.end();
484+ }
485+});
src/endpoints/stable-diffusion.js+10 -11
@@ -1327,17 +1327,6 @@ xai.post('/generate', async (request, response) => {
13271327 }
13281328});
13291329
1330-router.use('/comfy', comfy);
1331-router.use('/together', together);
1332-router.use('/drawthings', drawthings);
1333-router.use('/pollinations', pollinations);
1334-router.use('/stability', stability);
1335-router.use('/huggingface', huggingface);
1336-router.use('/nanogpt', nanogpt);
1337-router.use('/bfl', bfl);
1338-router.use('/falai', falai);
1339-router.use('/xai', xai);
1340-
13411330const aimlapi = express.Router();
13421331
13431332aimlapi.post('/models', async (request, response) => {
@@ -1422,4 +1411,14 @@ aimlapi.post('/generate-image', async (req, res) => {
14221411 }
14231412});
14241413
1414+router.use('/comfy', comfy);
1415+router.use('/together', together);
1416+router.use('/drawthings', drawthings);
1417+router.use('/pollinations', pollinations);
1418+router.use('/stability', stability);
1419+router.use('/huggingface', huggingface);
1420+router.use('/nanogpt', nanogpt);
1421+router.use('/bfl', bfl);
1422+router.use('/falai', falai);
1423+router.use('/xai', xai);
14251424router.use('/aimlapi', aimlapi);