feat: add custom image API extension

This commit is contained in:
2026-08-12 16:21:33 +08:00
commit b440a33196
4 changed files with 878 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
{
"Image Auto Generation": "自动生成图片",
"Insert In Current Message": "插入当前消息",
"Create New Message": "创建新消息",
"Image Insert Type": "图片插入类型",
"Disable": "禁用",
"Enable Custom Image API": "启用自定义图片 API",
"Custom Image API Base URL": "自定义图片 API Base URL",
"Custom Image Model ID": "自定义图片模型 ID",
"Custom Image API Key": "图片 API Key",
"Clear Custom Image API Key": "清除图片 API Key",
"Custom Image API Key Hint": "留空时使用酒馆的通用 API Key。插件 Key 只保存在当前浏览器,不会打包进插件。",
"Image Size": "图片尺寸",
"Response Format": "返回格式",
"Auto": "自动",
"URL": "URL",
"Base64 JSON": "Base64 JSON",
"enabled": "已启用",
"disabled": "已禁用",
"Enable Prompt Injection": "启用提示注入",
"Prompt Template": "提示模板",
"Regex": "正则表达式",
"Inline Replace Mode": "行内替换模式",
"Position": "注入位置",
"Depth": "深度",
"at Depth System": "深度系统消息",
"at Depth User": "深度用户消息",
"at Depth AI": "深度助手消息"
}
+755
View File
@@ -0,0 +1,755 @@
// The main script for the extension
// The following are examples of some basic extension functionality
//You'll likely need to import extension_settings, getContext, and loadExtensionSettings from extensions.js
import { extension_settings, getContext } from '../../../extensions.js';
//You'll likely need to import some other functions from the main script
import {
saveSettingsDebounced,
eventSource,
event_types,
updateMessageBlock,
ensureMessageMediaIsArray,
} from '../../../../script.js';
import { appendMediaToMessage } from '../../../../script.js';
import { regexFromString } from '../../../utils.js';
import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
import { findSecret, SECRET_KEYS } from '../../../secrets.js';
// 扩展名称和路径
const extensionName = 'st-image-auto-generation';
// /scripts/extensions/third-party
const extensionFolderPath = `/scripts/extensions/third-party/${extensionName}`;
const apiKeyStorageKey = `${extensionName}.apiKey`;
// 插入类型常量
const INSERT_TYPE = {
DISABLED: 'disabled',
INLINE: 'inline',
NEW_MESSAGE: 'new',
REPLACE: 'replace',
};
/**
* Escapes characters for safe inclusion inside HTML attribute values.
* @param {string} value
* @returns {string}
*/
function escapeHtmlAttribute(value) {
if (typeof value !== 'string') {
return '';
}
return value
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
// 默认设置
const defaultSettings = {
insertType: INSERT_TYPE.DISABLED,
customApi: {
enabled: true,
baseUrl: 'https://freeapi.dgbmc.top/v1',
model: 'grok-imagine-image',
size: '1024x1024',
responseFormat: 'auto',
},
promptInjection: {
enabled: true,
prompt: `<image_generation>
You must insert a <pic prompt="example prompt"> at end of the reply. Prompts are used for stable diffusion image generation, based on the plot and character to output appropriate prompts to generate captivating images.
</image_generation>`,
regex: '/<pic[^>]*\\sprompt="([^"]*)"[^>]*?>/g',
position: 'deep_system', // deep_system, deep_user, deep_assistant
depth: 0, // 0表示添加到末尾,>0表示从末尾往前数第几个位置
},
};
/**
* Reads the API key stored for this extension in the current browser.
* It is deliberately kept out of extension_settings so shared settings
* exports and plugin packages do not contain the key.
* @returns {string}
*/
function getStoredApiKey() {
try {
return window.localStorage.getItem(apiKeyStorageKey) || '';
} catch {
return '';
}
}
/**
* Stores or clears the API key for this extension in the current browser.
* @param {string} value
*/
function setStoredApiKey(value) {
try {
const normalizedValue = String(value || '').trim();
if (normalizedValue) {
window.localStorage.setItem(apiKeyStorageKey, normalizedValue);
} else {
window.localStorage.removeItem(apiKeyStorageKey);
}
} catch (error) {
console.warn(`[${extensionName}] 无法保存图片 API Key:`, error);
}
}
// 从设置更新UI
function updateUI() {
const settings = extension_settings[extensionName];
if (!settings) {
return;
}
// 根据insertType设置开关状态
$('#auto_generation').toggleClass(
'selected',
settings.insertType !== INSERT_TYPE.DISABLED,
);
// 只在表单元素存在时更新它们
if ($('#image_generation_insert_type').length) {
$('#image_generation_insert_type').val(
settings.insertType,
);
$('#custom_image_api_enabled').prop('checked', settings.customApi.enabled);
$('#custom_image_api_base_url').val(settings.customApi.baseUrl);
$('#custom_image_api_model').val(settings.customApi.model);
$('#custom_image_api_size').val(settings.customApi.size);
$('#custom_image_api_response_format').val(settings.customApi.responseFormat);
$('#custom_image_api_key').val(getStoredApiKey());
$('#custom_image_api_settings').toggle(settings.customApi.enabled);
$('#prompt_injection_enabled').prop(
'checked',
settings.promptInjection.enabled,
);
$('#prompt_injection_text').val(
settings.promptInjection.prompt,
);
$('#prompt_injection_regex').val(
settings.promptInjection.regex,
);
$('#prompt_injection_position').val(
settings.promptInjection.position,
);
$('#prompt_injection_depth').val(
settings.promptInjection.depth,
);
}
}
// 加载设置
async function loadSettings() {
extension_settings[extensionName] = extension_settings[extensionName] || {};
const settings = extension_settings[extensionName];
if (settings.insertType === undefined) {
settings.insertType = defaultSettings.insertType;
}
settings.customApi = {
...defaultSettings.customApi,
...(settings.customApi || {}),
};
settings.customApi.enabled = settings.customApi.enabled !== false;
settings.customApi.baseUrl = String(
settings.customApi.baseUrl || defaultSettings.customApi.baseUrl,
).trim();
settings.customApi.model = String(
settings.customApi.model || defaultSettings.customApi.model,
).trim();
settings.customApi.size = String(
settings.customApi.size || defaultSettings.customApi.size,
).trim();
if (!['auto', 'url', 'b64_json'].includes(settings.customApi.responseFormat)) {
settings.customApi.responseFormat = defaultSettings.customApi.responseFormat;
}
settings.promptInjection = {
...defaultSettings.promptInjection,
...(settings.promptInjection || {}),
};
updateUI();
}
// 创建设置页面
async function createSettings(settingsHtml) {
// 创建一个容器来存放设置,确保其正确显示在扩展设置面板中
if (!$('#image_auto_generation_container').length) {
$('#extensions_settings2').append(
'<div id="image_auto_generation_container" class="extension_container"></div>',
);
}
// 使用传入的settingsHtml而不是重新获取
$('#image_auto_generation_container').empty().append(settingsHtml);
// 添加设置变更事件处理
$('#image_generation_insert_type').on('change', function () {
const newValue = $(this).val();
extension_settings[extensionName].insertType = newValue;
updateUI();
saveSettingsDebounced();
});
$('#custom_image_api_enabled').on('change', function () {
extension_settings[extensionName].customApi.enabled = $(this).prop('checked');
updateUI();
saveSettingsDebounced();
});
$('#custom_image_api_base_url').on('input', function () {
extension_settings[extensionName].customApi.baseUrl = String($(this).val()).trim();
saveSettingsDebounced();
});
$('#custom_image_api_model').on('input', function () {
extension_settings[extensionName].customApi.model = String($(this).val()).trim();
saveSettingsDebounced();
});
$('#custom_image_api_size').on('change', function () {
extension_settings[extensionName].customApi.size = String($(this).val()).trim();
saveSettingsDebounced();
});
$('#custom_image_api_response_format').on('change', function () {
extension_settings[extensionName].customApi.responseFormat = $(this).val();
saveSettingsDebounced();
});
$('#custom_image_api_key').on('input', function () {
setStoredApiKey($(this).val());
});
$('#custom_image_api_key_clear').on('click', function () {
setStoredApiKey('');
$('#custom_image_api_key').val('');
toastr.success('图片 API Key 已清除');
});
// 添加提示词注入设置的事件处理
$('#prompt_injection_enabled').on('change', function () {
extension_settings[extensionName].promptInjection.enabled =
$(this).prop('checked');
saveSettingsDebounced();
});
$('#prompt_injection_text').on('input', function () {
extension_settings[extensionName].promptInjection.prompt =
$(this).val();
saveSettingsDebounced();
});
$('#prompt_injection_regex').on('input', function () {
extension_settings[extensionName].promptInjection.regex = $(this).val();
saveSettingsDebounced();
});
$('#prompt_injection_position').on('change', function () {
extension_settings[extensionName].promptInjection.position =
$(this).val();
saveSettingsDebounced();
});
// 深度设置事件处理
$('#prompt_injection_depth').on('input', function () {
const value = parseInt(String($(this).val()));
extension_settings[extensionName].promptInjection.depth = isNaN(value)
? 0
: value;
saveSettingsDebounced();
});
// 初始化设置值
updateUI();
}
// 设置变更处理函数
function onExtensionButtonClick() {
// 直接访问扩展设置面板
const extensionsDrawer = $('#extensions-settings-button .drawer-toggle');
// 如果抽屉是关闭的,点击打开它
if ($('#rm_extensions_block').hasClass('closedDrawer')) {
extensionsDrawer.trigger('click');
}
// 等待抽屉打开后滚动到我们的设置容器
setTimeout(() => {
// 找到我们的设置容器
const container = $('#image_auto_generation_container');
if (container.length) {
// 滚动到设置面板位置
$('#rm_extensions_block').animate(
{
scrollTop:
container.offset().top -
$('#rm_extensions_block').offset().top +
$('#rm_extensions_block').scrollTop(),
},
500,
);
// 使用SillyTavern原生的抽屉展开方式
// 检查抽屉内容是否可见
const drawerContent = container.find('.inline-drawer-content');
const drawerHeader = container.find('.inline-drawer-header');
// 只有当内容被隐藏时才触发展开
if (drawerContent.is(':hidden') && drawerHeader.length) {
// 直接使用原生点击事件触发,而不做任何内部处理
drawerHeader.trigger('click');
}
}
}, 500);
}
// 初始化扩展
$(function () {
(async function () {
// 获取设置HTML (只获取一次)
const settingsHtml = await $.get(
`${extensionFolderPath}/settings.html`,
);
// 添加扩展到菜单
$('#extensionsMenu')
.append(`<div id="auto_generation" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-robot"></div>
<span data-i18n="Image Auto Generation">Image Auto Generation</span>
</div>`);
// 修改点击事件,打开设置面板而不是切换状态
$('#auto_generation').off('click').on('click', onExtensionButtonClick);
await loadSettings();
// 创建设置 - 将获取的HTML传递给createSettings
await createSettings(settingsHtml);
// 确保设置面板可见时,设置值是正确的
$('#extensions-settings-button').on('click', function () {
setTimeout(() => {
updateUI();
}, 200);
});
})();
});
// 获取消息角色
function getMesRole() {
// 确保对象路径存在
if (
!extension_settings[extensionName] ||
!extension_settings[extensionName].promptInjection ||
!extension_settings[extensionName].promptInjection.position
) {
return 'system'; // 默认返回system角色
}
switch (extension_settings[extensionName].promptInjection.position) {
case 'deep_system':
return 'system';
case 'deep_user':
return 'user';
case 'deep_assistant':
return 'assistant';
default:
return 'system';
}
}
/**
* Builds an OpenAI-compatible image generation endpoint from a base URL.
* A full /images/generations URL is also accepted for convenience.
* @param {string} baseUrl
* @returns {string}
*/
function getImageGenerationUrl(baseUrl) {
const normalizedBaseUrl = String(baseUrl || '').trim().replace(/\/+$/, '');
if (!normalizedBaseUrl) {
throw new Error('自定义图片 API 地址不能为空');
}
return /\/images\/generations$/i.test(normalizedBaseUrl)
? normalizedBaseUrl
: `${normalizedBaseUrl}/images/generations`;
}
/**
* Reads an error body without leaking the API key into the error message.
* @param {Response} response
* @returns {Promise<string>}
*/
async function getImageApiError(response) {
const text = await response.text();
if (!text) {
return `${response.status} ${response.statusText}`.trim();
}
try {
const data = JSON.parse(text);
return (
data?.error?.message ||
(typeof data?.error === 'string' ? data.error : '') ||
data?.message ||
text
);
} catch {
return text;
}
}
/**
* Extracts a browser-loadable image URL from an OpenAI-compatible response.
* @param {any} data
* @returns {string}
*/
function extractImageUrl(data) {
const item = Array.isArray(data?.data)
? data.data[0]
: Array.isArray(data?.images)
? data.images[0]
: data?.data;
if (typeof item === 'string' && item.trim()) {
return item.trim();
}
const imageUrl = item?.url || item?.image_url || data?.url;
if (typeof imageUrl === 'string' && imageUrl.trim()) {
return imageUrl.trim();
}
const base64 = item?.b64_json || item?.base64 || data?.b64_json;
if (typeof base64 === 'string' && base64.trim()) {
if (base64.startsWith('data:')) {
return base64;
}
const format = String(item?.format || data?.format || 'png')
.replace(/[^a-z0-9.+-]/gi, '')
.toLowerCase() || 'png';
return `data:image/${format};base64,${base64}`;
}
return '';
}
/**
* Generates an image through a user-configured OpenAI-compatible endpoint.
* @param {string} prompt
* @returns {Promise<string>}
*/
async function generateCustomImage(prompt) {
const customApi = extension_settings[extensionName].customApi;
const apiKey = getStoredApiKey() || await findSecret(SECRET_KEYS.GENERIC);
const headers = { 'Content-Type': 'application/json' };
const model = String(customApi.model || '').trim();
const size = String(customApi.size || '').trim();
if (!model) {
throw new Error('自定义图片模型 ID 不能为空');
}
if (!size) {
throw new Error('图片尺寸不能为空');
}
if (apiKey) {
const normalizedKey = String(apiKey).trim();
headers.Authorization = /^bearer\s/i.test(normalizedKey)
? normalizedKey
: `Bearer ${normalizedKey}`;
}
const body = {
prompt: prompt.trim(),
model,
size,
n: 1,
};
if (customApi.responseFormat !== 'auto') {
body.response_format = customApi.responseFormat;
}
const response = await fetch(getImageGenerationUrl(customApi.baseUrl), {
method: 'POST',
headers,
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(await getImageApiError(response));
}
const imageUrl = extractImageUrl(await response.json());
if (!imageUrl) {
throw new Error('图片 API 返回中没有找到 url 或 b64_json');
}
return imageUrl;
}
/**
* Generates an image using either the custom endpoint or SillyTavern's native
* stable-diffusion command.
* @param {string} prompt
* @param {string} insertType
* @returns {Promise<string>}
*/
async function generateImage(prompt, insertType) {
const settings = extension_settings[extensionName];
if (settings.customApi.enabled) {
return await generateCustomImage(prompt);
}
const command = SlashCommandParser.commands['sd'];
if (!command?.callback) {
throw new Error('酒馆图片生成命令尚未加载,请刷新页面后重试');
}
return await command.callback(
{
quiet: insertType === INSERT_TYPE.NEW_MESSAGE ? 'false' : 'true',
},
prompt,
);
}
/**
* Adds a custom-API result as a new assistant message.
* @param {any} context
* @param {string} prompt
* @param {string} imageUrl
* @returns {Promise<void>}
*/
async function addImageMessage(context, prompt, imageUrl) {
const message = {
name: context.name2 || 'Image Generation',
is_user: false,
is_system: false,
send_date: new Date().toISOString(),
mes: '',
extra: {
media: [
{
type: 'image',
url: imageUrl,
title: prompt,
source: 'generated',
},
],
media_display: 'gallery',
media_index: 0,
inline_image: false,
},
};
context.chat.push(message);
const messageId = context.chat.length - 1;
await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId, 'extension');
context.addOneMessage(message);
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId, 'extension');
await context.saveChat();
}
// 监听CHAT_COMPLETION_PROMPT_READY事件以注入提示词
eventSource.on(
event_types.CHAT_COMPLETION_PROMPT_READY,
async function (eventData) {
try {
// 确保设置对象和promptInjection对象都存在
if (
!extension_settings[extensionName] ||
!extension_settings[extensionName].promptInjection ||
!extension_settings[extensionName].promptInjection.enabled ||
extension_settings[extensionName].insertType ===
INSERT_TYPE.DISABLED
) {
return;
}
const prompt =
extension_settings[extensionName].promptInjection.prompt;
const depth =
extension_settings[extensionName].promptInjection.depth || 0;
const role = getMesRole();
console.log(
`[${extensionName}] 准备注入提示词: 角色=${role}, 深度=${depth}`,
);
console.log(
`[${extensionName}] 提示词内容: ${prompt.substring(0, 50)}...`,
);
// 根据depth参数决定插入位置
if (depth === 0) {
// 添加到末尾
eventData.chat.push({ role: role, content: prompt });
console.log(`[${extensionName}] 提示词已添加到聊天末尾`);
} else {
// 从末尾向前插入
eventData.chat.splice(-depth, 0, {
role: role,
content: prompt,
});
console.log(
`[${extensionName}] 提示词已插入到聊天中,从末尾往前第 ${depth} 个位置`,
);
}
} catch (error) {
console.error(`[${extensionName}] 提示词注入错误:`, error);
toastr.error(`提示词注入错误: ${error}`);
}
},
);
// 监听消息接收事件
eventSource.on(event_types.MESSAGE_RECEIVED, handleIncomingMessage);
async function handleIncomingMessage() {
// 确保设置对象存在
if (
!extension_settings[extensionName] ||
extension_settings[extensionName].insertType === INSERT_TYPE.DISABLED
) {
return;
}
const context = getContext();
const message = context.chat[context.chat.length - 1];
// 检查是否是AI消息
if (!message || message.is_user) {
return;
}
// 确保promptInjection对象和regex属性存在
if (
!extension_settings[extensionName].promptInjection ||
!extension_settings[extensionName].promptInjection.regex ||
typeof message.mes !== 'string'
) {
console.error('Prompt injection settings not properly initialized');
return;
}
// 使用正则表达式search
const imgTagRegex = regexFromString(
extension_settings[extensionName].promptInjection.regex,
);
// const testRegex = regexFromString(extension_settings[extensionName].promptInjection.regex);
let matches;
if (imgTagRegex.global) {
matches = [...message.mes.matchAll(imgTagRegex)];
} else {
const singleMatch = message.mes.match(imgTagRegex);
matches = singleMatch ? [singleMatch] : [];
}
console.log(imgTagRegex, matches);
if (matches.length > 0) {
// 延迟执行图片生成,确保消息首先显示出来
setTimeout(async () => {
try {
toastr.info(`Generating ${matches.length} images...`);
const insertType = extension_settings[extensionName].insertType;
const messageId = context.chat.indexOf(message);
if (messageId < 0 || context.chat[messageId] !== message) {
throw new Error('当前聊天已发生变化,已取消图片插入');
}
// 在当前消息中插入图片
if (!message.extra) {
message.extra = {};
}
ensureMessageMediaIsArray(message);
if (!Array.isArray(message.extra.media)) {
message.extra.media = [];
}
// 获取消息元素用于稍后更新
const messageElement = $(`.mes[mesid="${messageId}"]`);
// 处理每个匹配的图片标签
for (const match of matches) {
const prompt =
typeof match?.[1] === 'string' ? match[1] : '';
if (!prompt.trim()) {
continue;
}
// @ts-ignore
const imageUrl = await generateImage(prompt, insertType);
if (typeof imageUrl !== 'string' || !imageUrl.trim()) {
throw new Error('图片生成接口返回了空结果');
}
const customApiEnabled = extension_settings[extensionName].customApi.enabled;
if (insertType === INSERT_TYPE.NEW_MESSAGE) {
// Native /sd already creates the new message when quiet=false.
if (customApiEnabled) {
await addImageMessage(context, prompt, imageUrl);
}
continue;
}
// 统一插入到extra里
if (insertType === INSERT_TYPE.INLINE) {
message.extra.media.push({
type: 'image',
url: imageUrl,
title: prompt,
source: 'generated',
});
message.extra.media_display = 'gallery';
message.extra.media_index = message.extra.media.length - 1;
message.extra.inline_image = true;
// 更新UI
appendMediaToMessage(message, messageElement);
// 保存聊天记录
await context.saveChat();
} else if (insertType === INSERT_TYPE.REPLACE) {
// Find the original image tag in the message
const originalTag =
typeof match?.[0] === 'string' ? match[0] : '';
if (!originalTag) {
continue;
}
// Replace it with an actual image tag
const escapedUrl = escapeHtmlAttribute(imageUrl);
const escapedPrompt = escapeHtmlAttribute(prompt);
const newImageTag = `<img src="${escapedUrl}" title="${escapedPrompt}" alt="${escapedPrompt}">`;
message.mes = message.mes.replace(originalTag, newImageTag);
// Update the message display using updateMessageBlock
updateMessageBlock(messageId, message);
await eventSource.emit(event_types.MESSAGE_UPDATED, messageId);
// Save the chat
await context.saveChat();
}
}
toastr.success(
`${matches.length} images generated successfully`,
);
} catch (error) {
toastr.error(`Image generation error: ${error}`);
console.error('Image generation error:', error);
}
}, 0); //防阻塞UI渲染
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"display_name": "Image Auto Generation - Custom API",
"requires": [],
"optional": [],
"js": "index.js",
"author": "wickedcode01",
"version": "1.1.0",
"homePage": "https://github.com/wickedcode01/st-image-auto-generation",
"auto_update": false,
"i18n": {
"zh-cn": "i18n/zh-cn.json"
}
}
+81
View File
@@ -0,0 +1,81 @@
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b data-i18n="Image Auto Generation">Image Auto Generation</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div class="flex-container flexnowrap">
<label for="image_generation_insert_type" class="flex1" data-i18n="Image Insert Type">Image Insert Type</label>
<select id="image_generation_insert_type" class="text_pole widthNatural margin0">
<option data-i18n="Insert In Current Message" value="inline">Insert In Current Message</option>
<option data-i18n="Inline Replace Mode" value="replace">Inline Replace Mode</option>
<option data-i18n="Create New Message" value="new">Create New Message</option>
<option data-i18n="Disable" value="disabled">Disable</option>
</select>
</div>
<div class="flex-container flexnowrap">
<label class="checkbox_label" for="custom_image_api_enabled">
<span data-i18n="Enable Custom Image API">Enable Custom Image API</span>
<input type="checkbox" id="custom_image_api_enabled" class="checkbox">
</label>
</div>
<div id="custom_image_api_settings">
<div>
<label for="custom_image_api_base_url" class="flex1" data-i18n="Custom Image API Base URL">Custom Image API Base URL</label>
<input id="custom_image_api_base_url" class="text_pole" type="url" placeholder="https://example.com/v1">
</div>
<div>
<label for="custom_image_api_model" class="flex1" data-i18n="Custom Image Model ID">Custom Image Model ID</label>
<input id="custom_image_api_model" class="text_pole" type="text" placeholder="grok-imagine-image">
</div>
<div>
<label for="custom_image_api_key" class="flex1" data-i18n="Custom Image API Key">Custom Image API Key</label>
<div class="flex-container flexnowrap">
<input id="custom_image_api_key" class="text_pole flex1" type="password" autocomplete="off" placeholder="sk-...">
<button id="custom_image_api_key_clear" type="button" class="menu_button menu_button_icon" data-i18n="[title]Clear Custom Image API Key" title="Clear Custom Image API Key" aria-label="Clear Custom Image API Key">
<i class="fa-solid fa-eraser"></i>
</button>
</div>
<small data-i18n="Custom Image API Key Hint">留空时使用酒馆的通用 API Key。插件 Key 只保存在当前浏览器,不会打包进插件。</small>
</div>
<div class="flex-container flexnowrap">
<label for="custom_image_api_size" class="flex1" data-i18n="Image Size">Image Size</label>
<input id="custom_image_api_size" class="text_pole widthUnset" type="text" placeholder="1024x1024">
</div>
<div class="flex-container flexnowrap">
<label for="custom_image_api_response_format" class="flex1" data-i18n="Response Format">Response Format</label>
<select id="custom_image_api_response_format" class="text_pole widthNatural margin0">
<option value="auto" data-i18n="Auto">Auto</option>
<option value="url" data-i18n="URL">URL</option>
<option value="b64_json" data-i18n="Base64 JSON">Base64 JSON</option>
</select>
</div>
</div>
<div class="flex-container flexnowrap">
<label class="checkbox_label" for="prompt_injection_enabled">
<span data-i18n="Enable Prompt Injection">Enable Prompt Injection</span>
<input type="checkbox" id="prompt_injection_enabled" class="checkbox">
</label>
</div>
<div class="">
<div><label for="prompt_injection_text" class="flex1" data-i18n="Prompt Template">Prompt Template</label></div>
<textarea id="prompt_injection_text" class="text_pole textarea_compact" rows="3" placeholder="Prompt that instruct AI to generate image tag"></textarea>
</div>
<div class="">
<div><label for="prompt_injection_regex" class="flex1" data-i18n="Regex">Regex</label></div>
<textarea id="prompt_injection_regex" class="text_pole textarea_compact" rows="2" placeholder="Your regex here"></textarea>
</div>
<div class="flex-container flexnowrap">
<label for="prompt_injection_position" class="flex1" data-i18n="Position">Position</label>
<select id="prompt_injection_position" class="text_pole widthNatural margin0">
<option value="deep_system" data-i18n="at Depth System">Deep System</option>
<option value="deep_user" data-i18n="at Depth User">Deep User</option>
<option value="deep_assistant" data-i18n="at Depth AI">Deep Assistant</option>
</select>
</div>
<div class="flex-container flexnowrap">
<label for="prompt_injection_depth" class="flex1" data-i18n="Depth">Depth</label>
<input id="prompt_injection_depth" class="text_pole widthUnset" type="number" min="0" max="100">
</div>
</div>
</div>