Add automatic image captioning mode
| @@ -408,6 +408,7 @@ export const event_types = { | |||
| 408 | MESSAGE_EDITED: 'message_edited', | 408 | MESSAGE_EDITED: 'message_edited', |
| 409 | MESSAGE_DELETED: 'message_deleted', | 409 | MESSAGE_DELETED: 'message_deleted', |
| 410 | MESSAGE_UPDATED: 'message_updated', | 410 | MESSAGE_UPDATED: 'message_updated', |
| 411 | MESSAGE_FILE_EMBEDDED: 'message_file_embedded', | ||
| 411 | IMPERSONATE_READY: 'impersonate_ready', | 412 | IMPERSONATE_READY: 'impersonate_ready', |
| 412 | CHAT_CHANGED: 'chat_id_changed', | 413 | CHAT_CHANGED: 'chat_id_changed', |
| 413 | GENERATION_STARTED: 'generation_started', | 414 | GENERATION_STARTED: 'generation_started', |
| @@ -3404,6 +3405,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | |||
| 3404 | let regexedMessage = getRegexedString(message, regexType, options); | 3405 | let regexedMessage = getRegexedString(message, regexType, options); |
| 3405 | regexedMessage = await appendFileContent(chatItem, regexedMessage); | 3406 | regexedMessage = await appendFileContent(chatItem, regexedMessage); |
| 3406 | 3407 | ||
| 3408 | if (chatItem?.extra?.append_title && chatItem?.extra?.title) { | ||
| 3409 | regexedMessage = `${regexedMessage}\n\n${chatItem.extra.title}`; | ||
| 3410 | } | ||
| 3411 | |||
| 3407 | return { | 3412 | return { |
| 3408 | ...chatItem, | 3413 | ...chatItem, |
| 3409 | mes: regexedMessage, | 3414 | mes: regexedMessage, |
| @@ -417,6 +417,7 @@ function embedMessageFile(messageId, messageBlock) { | |||
| 417 | } | 417 | } |
| 418 | 418 | ||
| 419 | await populateFileAttachment(message, 'embed_file_input'); | 419 | await populateFileAttachment(message, 'embed_file_input'); |
| 420 | await eventSource.emit(event_types.MESSAGE_FILE_EMBEDDED, messageId); | ||
| 420 | appendMediaToMessage(message, messageBlock); | 421 | appendMediaToMessage(message, messageBlock); |
| 421 | await saveChatConditional(); | 422 | await saveChatConditional(); |
| 422 | } | 423 | } |
| @@ -614,6 +615,8 @@ async function deleteMessageImage() { | |||
| 614 | const message = chat[mesId]; | 615 | const message = chat[mesId]; |
| 615 | delete message.extra.image; | 616 | delete message.extra.image; |
| 616 | delete message.extra.inline_image; | 617 | delete message.extra.inline_image; |
| 618 | delete message.extra.title; | ||
| 619 | delete message.extra.append_title; | ||
| 617 | mesBlock.find('.mes_img_container').removeClass('img_extra'); | 620 | mesBlock.find('.mes_img_container').removeClass('img_extra'); |
| 618 | mesBlock.find('.mes_img').attr('src', ''); | 621 | mesBlock.find('.mes_img').attr('src', ''); |
| 619 | await saveChatConditional(); | 622 | await saveChatConditional(); |
| @@ -1,6 +1,6 @@ | |||
| 1 | import { ensureImageFormatSupported, getBase64Async, isTrueBoolean, saveBase64AsFile } from '../../utils.js'; | 1 | import { ensureImageFormatSupported, getBase64Async, isTrueBoolean, saveBase64AsFile } from '../../utils.js'; |
| 2 | import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js'; | 2 | import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js'; |
| 3 | import { callPopup, getRequestHeaders, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js'; | 3 | import { callPopup, eventSource, event_types, getRequestHeaders, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js'; |
| 4 | import { getMessageTimeStamp } from '../../RossAscends-mods.js'; | 4 | import { getMessageTimeStamp } from '../../RossAscends-mods.js'; |
| 5 | import { SECRET_KEYS, secret_state } from '../../secrets.js'; | 5 | import { SECRET_KEYS, secret_state } from '../../secrets.js'; |
| 6 | import { getMultimodalCaption } from '../shared.js'; | 6 | import { getMultimodalCaption } from '../shared.js'; |
| @@ -84,12 +84,11 @@ async function setSpinnerIcon() { | |||
| 84 | } | 84 | } |
| 85 | 85 | ||
| 86 | /** | 86 | /** |
| 87 | * Sends a captioned message to the chat. | 87 | * Wraps a caption with a message template. |
| 88 | * @param {string} caption Caption text | 88 | * @param {string} caption Raw caption |
| 89 | * @param {string} image Image URL | 89 | * @returns {Promise<string>} Wrapped caption |
| 90 | */ | 90 | */ |
| 91 | async function sendCaptionedMessage(caption, image) { | 91 | async function wrapCaptionTemplate(caption) { |
| 92 | const context = getContext(); | ||
| 93 | let template = extension_settings.caption.template || TEMPLATE_DEFAULT; | 92 | let template = extension_settings.caption.template || TEMPLATE_DEFAULT; |
| 94 | 93 | ||
| 95 | if (!/{{caption}}/i.test(template)) { | 94 | if (!/{{caption}}/i.test(template)) { |
| @@ -101,7 +100,7 @@ async function sendCaptionedMessage(caption, image) { | |||
| 101 | 100 | ||
| 102 | if (extension_settings.caption.refine_mode) { | 101 | if (extension_settings.caption.refine_mode) { |
| 103 | messageText = await callPopup( | 102 | messageText = await callPopup( |
| 104 | '<h3>Review and edit the generated message:</h3>Press "Cancel" to abort the caption sending.', | 103 | '<h3>Review and edit the generated caption:</h3>Press "Cancel" to abort the caption sending.', |
| 105 | 'input', | 104 | 'input', |
| 106 | messageText, | 105 | messageText, |
| 107 | { rows: 5, okButton: 'Send' }); | 106 | { rows: 5, okButton: 'Send' }); |
| @@ -111,6 +110,55 @@ async function sendCaptionedMessage(caption, image) { | |||
| 111 | } | 110 | } |
| 112 | } | 111 | } |
| 113 | 112 | ||
| 113 | return messageText; | ||
| 114 | } | ||
| 115 | |||
| 116 | /** | ||
| 117 | * Appends caption to an existing message. | ||
| 118 | * @param {Object} data Message data | ||
| 119 | * @returns {Promise<void>} | ||
| 120 | */ | ||
| 121 | async function captionExistingMessage(data) { | ||
| 122 | if (!(data?.extra?.image)) { | ||
| 123 | return; | ||
| 124 | } | ||
| 125 | |||
| 126 | const imageData = await fetch(data.extra.image); | ||
| 127 | const blob = await imageData.blob(); | ||
| 128 | const type = imageData.headers.get('Content-Type'); | ||
| 129 | const file = new File([blob], 'image.png', { type }); | ||
| 130 | const caption = await getCaptionForFile(file, null, true); | ||
| 131 | |||
| 132 | if (!caption) { | ||
| 133 | console.warn('Failed to generate a caption for the image.'); | ||
| 134 | return; | ||
| 135 | } | ||
| 136 | |||
| 137 | const wrappedCaption = await wrapCaptionTemplate(caption); | ||
| 138 | |||
| 139 | const messageText = String(data.mes).trim(); | ||
| 140 | |||
| 141 | if (!messageText) { | ||
| 142 | data.extra.inline_image = false; | ||
| 143 | data.mes = wrappedCaption; | ||
| 144 | data.extra.title = wrappedCaption; | ||
| 145 | } | ||
| 146 | else { | ||
| 147 | data.extra.inline_image = true; | ||
| 148 | data.extra.append_title = true; | ||
| 149 | data.extra.title = wrappedCaption; | ||
| 150 | } | ||
| 151 | } | ||
| 152 | |||
| 153 | /** | ||
| 154 | * Sends a captioned message to the chat. | ||
| 155 | * @param {string} caption Caption text | ||
| 156 | * @param {string} image Image URL | ||
| 157 | */ | ||
| 158 | async function sendCaptionedMessage(caption, image) { | ||
| 159 | const messageText = await wrapCaptionTemplate(caption); | ||
| 160 | |||
| 161 | const context = getContext(); | ||
| 114 | const message = { | 162 | const message = { |
| 115 | name: context.name1, | 163 | name: context.name1, |
| 116 | is_user: true, | 164 | is_user: true, |
| @@ -423,6 +471,7 @@ jQuery(async function () { | |||
| 423 | $('#caption_refine_mode').prop('checked', !!(extension_settings.caption.refine_mode)); | 471 | $('#caption_refine_mode').prop('checked', !!(extension_settings.caption.refine_mode)); |
| 424 | $('#caption_allow_reverse_proxy').prop('checked', !!(extension_settings.caption.allow_reverse_proxy)); | 472 | $('#caption_allow_reverse_proxy').prop('checked', !!(extension_settings.caption.allow_reverse_proxy)); |
| 425 | $('#caption_prompt_ask').prop('checked', !!(extension_settings.caption.prompt_ask)); | 473 | $('#caption_prompt_ask').prop('checked', !!(extension_settings.caption.prompt_ask)); |
| 474 | $('#caption_auto_mode').prop('checked', !!(extension_settings.caption.auto_mode)); | ||
| 426 | $('#caption_source').val(extension_settings.caption.source); | 475 | $('#caption_source').val(extension_settings.caption.source); |
| 427 | $('#caption_prompt').val(extension_settings.caption.prompt); | 476 | $('#caption_prompt').val(extension_settings.caption.prompt); |
| 428 | $('#caption_template').val(extension_settings.caption.template); | 477 | $('#caption_template').val(extension_settings.caption.template); |
| @@ -448,6 +497,22 @@ jQuery(async function () { | |||
| 448 | extension_settings.caption.prompt_ask = $('#caption_prompt_ask').prop('checked'); | 497 | extension_settings.caption.prompt_ask = $('#caption_prompt_ask').prop('checked'); |
| 449 | saveSettingsDebounced(); | 498 | saveSettingsDebounced(); |
| 450 | }); | 499 | }); |
| 500 | $('#caption_auto_mode').on('input', () => { | ||
| 501 | extension_settings.caption.auto_mode = !!$('#caption_auto_mode').prop('checked'); | ||
| 502 | saveSettingsDebounced(); | ||
| 503 | }); | ||
| 504 | |||
| 505 | const onMessageEvent = async (index) => { | ||
| 506 | if (!extension_settings.caption.auto_mode) { | ||
| 507 | return; | ||
| 508 | } | ||
| 509 | |||
| 510 | const data = getContext().chat[index]; | ||
| 511 | await captionExistingMessage(data); | ||
| 512 | }; | ||
| 513 | |||
| 514 | eventSource.on(event_types.MESSAGE_SENT, onMessageEvent); | ||
| 515 | eventSource.on(event_types.MESSAGE_FILE_EMBEDDED, onMessageEvent); | ||
| 451 | 516 | ||
| 452 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'caption', | 517 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'caption', |
| 453 | callback: captionCommandCallback, | 518 | callback: captionCommandCallback, |
| @@ -92,6 +92,11 @@ | |||
| 92 | </div> | 92 | </div> |
| 93 | <label for="caption_template"><span data-i18n="Message Template">Message Template</span> <small><span data-i18n="(use _space">(use </span> <code>{{caption}}</code> <span data-i18n="macro)">macro)</span></small></label> | 93 | <label for="caption_template"><span data-i18n="Message Template">Message Template</span> <small><span data-i18n="(use _space">(use </span> <code>{{caption}}</code> <span data-i18n="macro)">macro)</span></small></label> |
| 94 | <textarea id="caption_template" class="text_pole" rows="2" placeholder="< Use default >">{{TEMPLATE_DEFAULT}}</textarea> | 94 | <textarea id="caption_template" class="text_pole" rows="2" placeholder="< Use default >">{{TEMPLATE_DEFAULT}}</textarea> |
| 95 | <label class="checkbox_label" for="caption_auto_mode"> | ||
| 96 | <input id="caption_auto_mode" type="checkbox" class="checkbox"> | ||
| 97 | <span data-i18n="Automatically caption images">Automatically caption images</span> | ||
| 98 | <i class="fa-solid fa-info-circle" title="Automatically caption images when they are pasted into the chat or attached to messages."></i> | ||
| 99 | </label> | ||
| 95 | <label class="checkbox_label margin-bot-10px" for="caption_refine_mode"> | 100 | <label class="checkbox_label margin-bot-10px" for="caption_refine_mode"> |
| 96 | <input id="caption_refine_mode" type="checkbox" class="checkbox"> | 101 | <input id="caption_refine_mode" type="checkbox" class="checkbox"> |
| 97 | <span data-i18n="Edit captions before saving">Edit captions before saving</span> | 102 | <span data-i18n="Edit captions before saving">Edit captions before saving</span> |