Add automatic image captioning mode

7149f46c9aaadd31b8add999c06be4a5a7ac5888

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

4 files changed, +85 -7Ignore whitespace
public/script.js+5 -0
@@ -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);
34063407
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,
public/scripts/chats.js+3 -0
@@ -417,6 +417,7 @@ function embedMessageFile(messageId, messageBlock) {
417 }417 }
418418
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();
public/scripts/extensions/caption/index.js+72 -7
@@ -1,6 +1,6 @@
1import { ensureImageFormatSupported, getBase64Async, isTrueBoolean, saveBase64AsFile } from '../../utils.js';1import { ensureImageFormatSupported, getBase64Async, isTrueBoolean, saveBase64AsFile } from '../../utils.js';
2import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js';2import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js';
3import { callPopup, getRequestHeaders, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';3import { callPopup, eventSource, event_types, getRequestHeaders, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';
4import { getMessageTimeStamp } from '../../RossAscends-mods.js';4import { getMessageTimeStamp } from '../../RossAscends-mods.js';
5import { SECRET_KEYS, secret_state } from '../../secrets.js';5import { SECRET_KEYS, secret_state } from '../../secrets.js';
6import { getMultimodalCaption } from '../shared.js';6import { getMultimodalCaption } from '../shared.js';
@@ -84,12 +84,11 @@ async function setSpinnerIcon() {
84}84}
8585
86/**86/**
87 * Sends a captioned message to the chat.87 * Wraps a caption with a message template.
88 * @param {string} caption Caption text88 * @param {string} caption Raw caption
89 * @param {string} image Image URL89 * @returns {Promise<string>} Wrapped caption
90 */90 */
91async function sendCaptionedMessage(caption, image) {91async 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;
9493
95 if (!/{{caption}}/i.test(template)) {94 if (!/{{caption}}/i.test(template)) {
@@ -101,7 +100,7 @@ async function sendCaptionedMessage(caption, image) {
101100
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 }
113112
113 return messageText;
114}
115
116/**
117 * Appends caption to an existing message.
118 * @param {Object} data Message data
119 * @returns {Promise<void>}
120 */
121async 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 */
158async 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);
451516
452 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'caption',517 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'caption',
453 callback: captionCommandCallback,518 callback: captionCommandCallback,
public/scripts/extensions/caption/settings.html+5 -0
@@ -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>&lcub;&lcub;caption&rcub;&rcub;</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>&lcub;&lcub;caption&rcub;&rcub;</code> <span data-i18n="macro)">macro)</span></small></label>
94 <textarea id="caption_template" class="text_pole" rows="2" placeholder="&lt; Use default &gt;">{{TEMPLATE_DEFAULT}}</textarea>94 <textarea id="caption_template" class="text_pole" rows="2" placeholder="&lt; Use default &gt;">{{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>