Google: Add Imagen image generation
| @@ -58,6 +58,7 @@ import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnum | |||
| 58 | import { ToolManager } from '../../tool-calling.js'; | 58 | import { ToolManager } from '../../tool-calling.js'; |
| 59 | import { MacrosParser } from '../../macros.js'; | 59 | import { MacrosParser } from '../../macros.js'; |
| 60 | import { t } from '../../i18n.js'; | 60 | import { t } from '../../i18n.js'; |
| 61 | import { oai_settings } from '../../openai.js'; | ||
| 61 | 62 | ||
| 62 | export { MODULE_NAME }; | 63 | export { MODULE_NAME }; |
| 63 | 64 | ||
| @@ -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 | }; |
| 89 | 91 | ||
| 90 | const initiators = { | 92 | const initiators = { |
| @@ -330,6 +332,10 @@ const defaultSettings = { | |||
| 330 | 332 | ||
| 331 | // BFL API settings | 333 | // 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 | }; |
| 334 | 340 | ||
| 335 | const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed); | 341 | const 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); | ||
| 512 | 520 | ||
| 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 | ]; |
| 1281 | 1290 | ||
| 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 | } |
| 1502 | 1514 | ||
| 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 | } |
| 1698 | 1713 | ||
| 1699 | for (const model of models) { | 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 | function loadNovelSchedulers() { | 2056 | function 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 | } |
| 2109 | 2142 | ||
| 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 | } |
| 2203 | 2239 | ||
| 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 | } |
| 2783 | 2822 | ||
| 2784 | if (!result.data) { | 2823 | if (!result.data) { |
| @@ -2916,20 +2955,39 @@ 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 width | 2956 | * @param {number} width Target width |
| 2918 | * @param {number} height Target height | 2957 | * @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 string | 2959 | * @returns {string} Closest aspect ratio as a string |
| 2920 | */ | 2960 | */ |
| 2921 | function getClosestAspectRatio(width, height) { | 2961 | function getClosestAspectRatio(width, height, source) { |
| 2922 | const aspectRatios = { | 2962 | function getAspectRatios() { |
| 2923 | '16:9': 16 / 9, | 2963 | switch (source) { |
| 2924 | '1:1': 1, | 2964 | case 'stability': |
| 2925 | '21:9': 21 / 9, | 2965 | return { |
| 2926 | '2:3': 2 / 3, | 2966 | '16:9': 16 / 9, |
| 2927 | '3:2': 3 / 2, | 2967 | '1:1': 1, |
| 2928 | '4:5': 4 / 5, | 2968 | '21:9': 21 / 9, |
| 2929 | '5:4': 5 / 4, | 2969 | '2:3': 2 / 3, |
| 2930 | '9:16': 9 / 16, | 2970 | '3:2': 3 / 2, |
| 2931 | '9:21': 9 / 21, | 2971 | '4:5': 4 / 5, |
| 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 | const aspectRatio = width / height; | 2992 | const aspectRatio = width / height; |
| 2935 | 2993 | ||
| @@ -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 | */ | ||
| 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 | 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 | } |
| 3918 | 4012 | ||
| @@ -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); |
| 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 | 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')); |
| @@ -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> |
| 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 | <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> |
| @@ -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 | |||
| 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 | const aimlapi = express.Router(); | 1330 | const aimlapi = express.Router(); |
| 1342 | 1331 | ||
| 1343 | aimlapi.post('/models', async (request, response) => { | 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 | router.use('/aimlapi', aimlapi); | 1424 | router.use('/aimlapi', aimlapi); |