Google: Add Imagen image generation
| @@ -58,6 +58,7 @@ import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnum | ||
| 58 | 58 | import { ToolManager } from '../../tool-calling.js'; |
| 59 | 59 | import { MacrosParser } from '../../macros.js'; |
| 60 | 60 | import { t } from '../../i18n.js'; |
| 61 | +import { oai_settings } from '../../openai.js'; | |
| 61 | 62 | |
| 62 | 63 | export { MODULE_NAME }; |
| 63 | 64 | |
| @@ -85,6 +86,7 @@ const sources = { | ||
| 85 | 86 | bfl: 'bfl', |
| 86 | 87 | falai: 'falai', |
| 87 | 88 | xai: 'xai', |
| 89 | + google: 'google', | |
| 88 | 90 | }; |
| 89 | 91 | |
| 90 | 92 | const initiators = { |
| @@ -330,6 +332,10 @@ const defaultSettings = { | ||
| 330 | 332 | |
| 331 | 333 | // BFL API settings |
| 332 | 334 | bfl_upsampling: false, |
| 335 | + | |
| 336 | + // Google settings | |
| 337 | + google_api: 'makersuite', | |
| 338 | + google_enhance: true, | |
| 333 | 339 | }; |
| 334 | 340 | |
| 335 | 341 | const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed); |
| @@ -509,6 +515,8 @@ async function loadSettings() { | ||
| 509 | 515 | $('#sd_huggingface_model_id').val(extension_settings.sd.huggingface_model_id); |
| 510 | 516 | $('#sd_function_tool').prop('checked', extension_settings.sd.function_tool); |
| 511 | 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); | |
| 512 | 520 | |
| 513 | 521 | for (const style of extension_settings.sd.styles) { |
| 514 | 522 | const option = document.createElement('option'); |
| @@ -1277,6 +1285,7 @@ async function onModelChange() { | ||
| 1277 | 1285 | sources.bfl, |
| 1278 | 1286 | sources.falai, |
| 1279 | 1287 | sources.xai, |
| 1288 | + sources.google, | |
| 1280 | 1289 | ]; |
| 1281 | 1290 | |
| 1282 | 1291 | if (cloudSources.includes(extension_settings.sd.source)) { |
| @@ -1498,6 +1507,9 @@ async function loadSamplers() { | ||
| 1498 | 1507 | case sources.xai: |
| 1499 | 1508 | samplers = ['N/A']; |
| 1500 | 1509 | break; |
| 1510 | + case sources.google: | |
| 1511 | + samplers = ['N/A']; | |
| 1512 | + break; | |
| 1501 | 1513 | } |
| 1502 | 1514 | |
| 1503 | 1515 | for (const sampler of samplers) { |
| @@ -1694,6 +1706,9 @@ async function loadModels() { | ||
| 1694 | 1706 | case sources.xai: |
| 1695 | 1707 | models = await loadXAIModels(); |
| 1696 | 1708 | break; |
| 1709 | + case sources.google: | |
| 1710 | + models = await loadGoogleModels(); | |
| 1711 | + break; | |
| 1697 | 1712 | } |
| 1698 | 1713 | |
| 1699 | 1714 | for (const model of models) { |
| @@ -2023,6 +2038,21 @@ async function loadNovelModels() { | ||
| 2023 | 2038 | ]; |
| 2024 | 2039 | } |
| 2025 | 2040 | |
| 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 | + | |
| 2026 | 2056 | function loadNovelSchedulers() { |
| 2027 | 2057 | return ['karras', 'native', 'exponential', 'polyexponential']; |
| 2028 | 2058 | } |
| @@ -2105,6 +2135,9 @@ async function loadSchedulers() { | ||
| 2105 | 2135 | case sources.xai: |
| 2106 | 2136 | schedulers = ['N/A']; |
| 2107 | 2137 | break; |
| 2138 | + case sources.google: | |
| 2139 | + schedulers = ['N/A']; | |
| 2140 | + break; | |
| 2108 | 2141 | } |
| 2109 | 2142 | |
| 2110 | 2143 | for (const scheduler of schedulers) { |
| @@ -2199,6 +2232,9 @@ async function loadVaes() { | ||
| 2199 | 2232 | case sources.xai: |
| 2200 | 2233 | vaes = ['N/A']; |
| 2201 | 2234 | break; |
| 2235 | + case sources.google: | |
| 2236 | + vaes = ['N/A']; | |
| 2237 | + break; | |
| 2202 | 2238 | } |
| 2203 | 2239 | |
| 2204 | 2240 | for (const vae of vaes) { |
| @@ -2779,6 +2815,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | ||
| 2779 | 2815 | case sources.xai: |
| 2780 | 2816 | result = await generateXAIImage(prefixedPrompt, negativePrompt, signal); |
| 2781 | 2817 | break; |
| 2818 | + case sources.google: | |
| 2819 | + result = await generateGoogleImage(prefixedPrompt, negativePrompt, signal); | |
| 2820 | + break; | |
| 2782 | 2821 | } |
| 2783 | 2822 | |
| 2784 | 2823 | if (!result.data) { |
| @@ -2916,20 +2955,39 @@ async function generateExtrasImage(prompt, negativePrompt, signal) { | ||
| 2916 | 2955 | * Gets an aspect ratio for Stability that is the closest to the given width and height. |
| 2917 | 2956 | * @param {number} width Target width |
| 2918 | 2957 | * @param {number} height Target height |
| 2958 | + * @param {'google'|'stability'} source Source of the request, used to determine aspect ratio | |
| 2919 | 2959 | * @returns {string} Closest aspect ratio as a string |
| 2920 | 2960 | */ |
| 2921 | 2961 | function getClosestAspectRatio(width, height, source) { |
| 2922 | 2962 | 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 { | |
| 2926 | 2966 | '216:39': 216 / 39, |
| 2927 | 2967 | '31:21': 3 / 21, |
| 2928 | 2968 | '421:59': 421 / 59, |
| 2929 | 2969 | '52:43': 52 / 43, |
| 2930 | 2970 | '93:162': 93 / 162, |
| 2931 | 2971 | '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 }; | |
| 2933 | 2991 | |
| 2934 | 2992 | const aspectRatio = width / height; |
| 2935 | 2993 | |
| @@ -2968,7 +3026,7 @@ async function generateStabilityImage(prompt, negativePrompt, signal) { | ||
| 2968 | 3026 | payload: { |
| 2969 | 3027 | prompt: prompt.slice(0, PROMPT_LIMIT), |
| 2970 | 3028 | negative_prompt: negativePrompt.slice(0, PROMPT_LIMIT), |
| 2971 | 3029 | aspect_ratio: getClosestAspectRatio(extension_settings.sd.width, extension_settings.sd.height, 'stability'), |
| 2972 | 3030 | seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, |
| 2973 | 3031 | style_preset: extension_settings.sd.stability_style_preset, |
| 2974 | 3032 | output_format: IMAGE_FORMAT, |
| @@ -3619,7 +3677,41 @@ async function generateFalaiImage(prompt, negativePrompt, signal) { | ||
| 3619 | 3677 | return { format: 'jpg', data: data.image }; |
| 3620 | 3678 | } else { |
| 3621 | 3679 | const text = await result.text(); |
| 3622 | 3680 | 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(); | |
| 3623 | 3715 | throw new Error(text); |
| 3624 | 3716 | } |
| 3625 | 3717 | } |
| @@ -3913,6 +4005,8 @@ function isValidState() { | ||
| 3913 | 4005 | return secret_state[SECRET_KEYS.FALAI]; |
| 3914 | 4006 | case sources.xai: |
| 3915 | 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 | } |
| 3918 | 4012 | |
| @@ -4573,6 +4667,15 @@ jQuery(async () => { | ||
| 4573 | 4667 | $('#sd_function_tool').on('input', onFunctionToolInput); |
| 4574 | 4668 | $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput); |
| 4575 | 4669 | |
| 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 | 4679 | if (!CSS.supports('field-sizing', 'content')) { |
| 4577 | 4680 | $('.sd_settings .inline-drawer-toggle').on('click', function () { |
| 4578 | 4681 | initScrollHeight($('#sd_prompt_prefix')); |
| @@ -43,6 +43,7 @@ | ||
| 43 | 43 | <option value="drawthings">DrawThings HTTP API</option> |
| 44 | 44 | <option value="extras">Extras API (deprecated)</option> |
| 45 | 45 | <option value="falai">FAL.AI</option> |
| 46 | + <option value="google">Google AI</option> | |
| 46 | 47 | <option value="huggingface">HuggingFace Inference API (serverless)</option> |
| 47 | 48 | <option value="nanogpt">NanoGPT</option> |
| 48 | 49 | <option value="novel">NovelAI Diffusion</option> |
| @@ -281,6 +282,26 @@ | ||
| 281 | 282 | </div> |
| 282 | 283 | </div> |
| 283 | 284 | |
| 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 | 305 | <div class="flex-container"> |
| 285 | 306 | <div class="flex1"> |
| 286 | 307 | <label for="sd_model" data-i18n="Model">Model</label> |
| @@ -418,3 +418,68 @@ router.post('/generate-native-tts', async (request, response) => { | ||
| 418 | 418 | return response.end(); |
| 419 | 419 | } |
| 420 | 420 | }); |
| 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 | +}); | |
| @@ -1327,17 +1327,6 @@ xai.post('/generate', async (request, response) => { | ||
| 1327 | 1327 | } |
| 1328 | 1328 | }); |
| 1329 | 1329 | |
| 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 | - | |
| 1341 | 1330 | const aimlapi = express.Router(); |
| 1342 | 1331 | |
| 1343 | 1332 | aimlapi.post('/models', async (request, response) => { |
| @@ -1422,4 +1411,14 @@ aimlapi.post('/generate-image', async (req, res) => { | ||
| 1422 | 1411 | } |
| 1423 | 1412 | }); |
| 1424 | 1413 | |
| 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); | |
| 1425 | 1424 | router.use('/aimlapi', aimlapi); |