Google: Add Imagen image generation

2d28f5cdaf2eaeca4444422ed61503997c8d6b6e

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

4 files changed, +203 -15Showing whitespace changes
public/scripts/extensions/stable-diffusion/index.js+107 -4
@@ -58,6 +58,7 @@ import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnum
58import { ToolManager } from '../../tool-calling.js';58import { ToolManager } from '../../tool-calling.js';
59import { MacrosParser } from '../../macros.js';59import { MacrosParser } from '../../macros.js';
60import { t } from '../../i18n.js';60import { t } from '../../i18n.js';
61import { oai_settings } from '../../openai.js';
6162
62export { MODULE_NAME };63export { MODULE_NAME };
6364
@@ -85,6 +86,7 @@ const sources = {
85 bfl: 'bfl',86 bfl: 'bfl',
86 falai: 'falai',87 falai: 'falai',
87 xai: 'xai',88 xai: 'xai',
89 google: 'google',
88};90};
8991
90const initiators = {92const initiators = {
@@ -330,6 +332,10 @@ const defaultSettings = {
330332
331 // BFL API settings333 // BFL API settings
332 bfl_upsampling: false,334 bfl_upsampling: false,
335
336 // Google settings
337 google_api: 'makersuite',
338 google_enhance: true,
333};339};
334340
335const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);341const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);
@@ -509,6 +515,8 @@ async function loadSettings() {
509 $('#sd_huggingface_model_id').val(extension_settings.sd.huggingface_model_id);515 $('#sd_huggingface_model_id').val(extension_settings.sd.huggingface_model_id);
510 $('#sd_function_tool').prop('checked', extension_settings.sd.function_tool);516 $('#sd_function_tool').prop('checked', extension_settings.sd.function_tool);
511 $('#sd_bfl_upsampling').prop('checked', extension_settings.sd.bfl_upsampling);517 $('#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
513 for (const style of extension_settings.sd.styles) {521 for (const style of extension_settings.sd.styles) {
514 const option = document.createElement('option');522 const option = document.createElement('option');
@@ -1277,6 +1285,7 @@ async function onModelChange() {
1277 sources.bfl,1285 sources.bfl,
1278 sources.falai,1286 sources.falai,
1279 sources.xai,1287 sources.xai,
1288 sources.google,
1280 ];1289 ];
12811290
1282 if (cloudSources.includes(extension_settings.sd.source)) {1291 if (cloudSources.includes(extension_settings.sd.source)) {
@@ -1498,6 +1507,9 @@ async function loadSamplers() {
1498 case sources.xai:1507 case sources.xai:
1499 samplers = ['N/A'];1508 samplers = ['N/A'];
1500 break;1509 break;
1510 case sources.google:
1511 samplers = ['N/A'];
1512 break;
1501 }1513 }
15021514
1503 for (const sampler of samplers) {1515 for (const sampler of samplers) {
@@ -1694,6 +1706,9 @@ async function loadModels() {
1694 case sources.xai:1706 case sources.xai:
1695 models = await loadXAIModels();1707 models = await loadXAIModels();
1696 break;1708 break;
1709 case sources.google:
1710 models = await loadGoogleModels();
1711 break;
1697 }1712 }
16981713
1699 for (const model of models) {1714 for (const model of models) {
@@ -2023,6 +2038,21 @@ async function loadNovelModels() {
2023 ];2038 ];
2024}2039}
20252040
2041async 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
2026function loadNovelSchedulers() {2056function loadNovelSchedulers() {
2027 return ['karras', 'native', 'exponential', 'polyexponential'];2057 return ['karras', 'native', 'exponential', 'polyexponential'];
2028}2058}
@@ -2105,6 +2135,9 @@ async function loadSchedulers() {
2105 case sources.xai:2135 case sources.xai:
2106 schedulers = ['N/A'];2136 schedulers = ['N/A'];
2107 break;2137 break;
2138 case sources.google:
2139 schedulers = ['N/A'];
2140 break;
2108 }2141 }
21092142
2110 for (const scheduler of schedulers) {2143 for (const scheduler of schedulers) {
@@ -2199,6 +2232,9 @@ async function loadVaes() {
2199 case sources.xai:2232 case sources.xai:
2200 vaes = ['N/A'];2233 vaes = ['N/A'];
2201 break;2234 break;
2235 case sources.google:
2236 vaes = ['N/A'];
2237 break;
2202 }2238 }
22032239
2204 for (const vae of vaes) {2240 for (const vae of vaes) {
@@ -2779,6 +2815,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
2779 case sources.xai:2815 case sources.xai:
2780 result = await generateXAIImage(prefixedPrompt, negativePrompt, signal);2816 result = await generateXAIImage(prefixedPrompt, negativePrompt, signal);
2781 break;2817 break;
2818 case sources.google:
2819 result = await generateGoogleImage(prefixedPrompt, negativePrompt, signal);
2820 break;
2782 }2821 }
27832822
2784 if (!result.data) {2823 if (!result.data) {
@@ -2916,10 +2955,14 @@ async function generateExtrasImage(prompt, negativePrompt, signal) {
2916 * Gets an aspect ratio for Stability that is the closest to the given width and height.2955 * Gets an aspect ratio for Stability that is the closest to the given width and height.
2917 * @param {number} width Target width2956 * @param {number} width Target width
2918 * @param {number} height Target height2957 * @param {number} height Target height
2958 * @param {'google'|'stability'} source Source of the request, used to determine aspect ratio
2919 * @returns {string} Closest aspect ratio as a string2959 * @returns {string} Closest aspect ratio as a string
2920 */2960 */
2921function getClosestAspectRatio(width, height) {2961function getClosestAspectRatio(width, height, source) {
2922 const aspectRatios = {2962 function getAspectRatios() {
2963 switch (source) {
2964 case 'stability':
2965 return {
2923 '16:9': 16 / 9,2966 '16:9': 16 / 9,
2924 '1:1': 1,2967 '1:1': 1,
2925 '21:9': 21 / 9,2968 '21:9': 21 / 9,
@@ -2930,6 +2973,21 @@ function getClosestAspectRatio(width, height) {
2930 '9:16': 9 / 16,2973 '9:16': 9 / 16,
2931 '9:21': 9 / 21,2974 '9:21': 9 / 21,
2932 };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
2934 const aspectRatio = width / height;2992 const aspectRatio = width / height;
29352993
@@ -2968,7 +3026,7 @@ async function generateStabilityImage(prompt, negativePrompt, signal) {
2968 payload: {3026 payload: {
2969 prompt: prompt.slice(0, PROMPT_LIMIT),3027 prompt: prompt.slice(0, PROMPT_LIMIT),
2970 negative_prompt: negativePrompt.slice(0, PROMPT_LIMIT),3028 negative_prompt: negativePrompt.slice(0, PROMPT_LIMIT),
2971 aspect_ratio: getClosestAspectRatio(extension_settings.sd.width, extension_settings.sd.height),3029 aspect_ratio: getClosestAspectRatio(extension_settings.sd.width, extension_settings.sd.height, 'stability'),
2972 seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,3030 seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
2973 style_preset: extension_settings.sd.stability_style_preset,3031 style_preset: extension_settings.sd.stability_style_preset,
2974 output_format: IMAGE_FORMAT,3032 output_format: IMAGE_FORMAT,
@@ -3619,7 +3677,41 @@ async function generateFalaiImage(prompt, negativePrompt, signal) {
3619 return { format: 'jpg', data: data.image };3677 return { format: 'jpg', data: data.image };
3620 } else {3678 } else {
3621 const text = await result.text();3679 const text = await result.text();
3622 console.log(text);3680 throw 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 */
3691async 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();
3623 throw new Error(text);3715 throw new Error(text);
3624 }3716 }
3625}3717}
@@ -3913,6 +4005,8 @@ function isValidState() {
3913 return secret_state[SECRET_KEYS.FALAI];4005 return secret_state[SECRET_KEYS.FALAI];
3914 case sources.xai:4006 case sources.xai:
3915 return secret_state[SECRET_KEYS.XAI];4007 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];
3916 }4010 }
3917}4011}
39184012
@@ -4573,6 +4667,15 @@ jQuery(async () => {
4573 $('#sd_function_tool').on('input', onFunctionToolInput);4667 $('#sd_function_tool').on('input', onFunctionToolInput);
4574 $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);4668 $('#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
4576 if (!CSS.supports('field-sizing', 'content')) {4679 if (!CSS.supports('field-sizing', 'content')) {
4577 $('.sd_settings .inline-drawer-toggle').on('click', function () {4680 $('.sd_settings .inline-drawer-toggle').on('click', function () {
4578 initScrollHeight($('#sd_prompt_prefix'));4681 initScrollHeight($('#sd_prompt_prefix'));
public/scripts/extensions/stable-diffusion/settings.html+21 -0
@@ -43,6 +43,7 @@
43 <option value="drawthings">DrawThings HTTP API</option>43 <option value="drawthings">DrawThings HTTP API</option>
44 <option value="extras">Extras API (deprecated)</option>44 <option value="extras">Extras API (deprecated)</option>
45 <option value="falai">FAL.AI</option>45 <option value="falai">FAL.AI</option>
46 <option value="google">Google AI</option>
46 <option value="huggingface">HuggingFace Inference API (serverless)</option>47 <option value="huggingface">HuggingFace Inference API (serverless)</option>
47 <option value="nanogpt">NanoGPT</option>48 <option value="nanogpt">NanoGPT</option>
48 <option value="novel">NovelAI Diffusion</option>49 <option value="novel">NovelAI Diffusion</option>
@@ -281,6 +282,26 @@
281 </div>282 </div>
282 </div>283 </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
284 <div class="flex-container">305 <div class="flex-container">
285 <div class="flex1">306 <div class="flex1">
286 <label for="sd_model" data-i18n="Model">Model</label>307 <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) => {
418 return response.end();418 return response.end();
419 }419 }
420});420});
421
422router.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) => {
1327 }1327 }
1328});1328});
13291329
1330router.use('/comfy', comfy);
1331router.use('/together', together);
1332router.use('/drawthings', drawthings);
1333router.use('/pollinations', pollinations);
1334router.use('/stability', stability);
1335router.use('/huggingface', huggingface);
1336router.use('/nanogpt', nanogpt);
1337router.use('/bfl', bfl);
1338router.use('/falai', falai);
1339router.use('/xai', xai);
1340
1341const aimlapi = express.Router();1330const aimlapi = express.Router();
13421331
1343aimlapi.post('/models', async (request, response) => {1332aimlapi.post('/models', async (request, response) => {
@@ -1422,4 +1411,14 @@ aimlapi.post('/generate-image', async (req, res) => {
1422 }1411 }
1423});1412});
14241413
1414router.use('/comfy', comfy);
1415router.use('/together', together);
1416router.use('/drawthings', drawthings);
1417router.use('/pollinations', pollinations);
1418router.use('/stability', stability);
1419router.use('/huggingface', huggingface);
1420router.use('/nanogpt', nanogpt);
1421router.use('/bfl', bfl);
1422router.use('/falai', falai);
1423router.use('/xai', xai);
1425router.use('/aimlapi', aimlapi);1424router.use('/aimlapi', aimlapi);