Add captioning for video attachments (#4749) * Add captioning for video attachments * Unify error toast titles * Add MEDIA_SOURCE enum and update media handling to include source information * Unify attachment handling logic * Add error handling for auto-captioning failures * Use string formatting for console error
Signed| @@ -70,6 +70,7 @@ declare global { | |||
| 70 | url: string; | 70 | url: string; |
| 71 | title?: string; | 71 | title?: string; |
| 72 | type: string; | 72 | type: string; |
| 73 | source?: string; | ||
| 73 | } | 74 | } |
| 74 | 75 | ||
| 75 | interface ImageGenerationAttachmentProps { | 76 | interface ImageGenerationAttachmentProps { |
| @@ -78,7 +79,10 @@ declare global { | |||
| 78 | } | 79 | } |
| 79 | 80 | ||
| 80 | interface ImageCaptionAttachmentProps { | 81 | interface ImageCaptionAttachmentProps { |
| 82 | /** Append title to the message text in case of non-inline captions. */ | ||
| 81 | append_title?: boolean; | 83 | append_title?: boolean; |
| 84 | /** Marker for captioned images to prevent auto-caption from firing again. */ | ||
| 85 | captioned?: boolean; | ||
| 82 | } | 86 | } |
| 83 | 87 | ||
| 84 | // Global namespace modules | 88 | // Global namespace modules |
| @@ -7295,6 +7295,7 @@ | |||
| 7295 | <div class="mes_media_container mes_video_container"> | 7295 | <div class="mes_media_container mes_video_container"> |
| 7296 | <div class="mes_video_controls"> | 7296 | <div class="mes_video_controls"> |
| 7297 | <div title="Expand and zoom" class="right_menu_button fa-lg fa-solid fa-magnifying-glass mes_media_enlarge" data-i18n="[title]Expand and zoom"></div> | 7297 | <div title="Expand and zoom" class="right_menu_button fa-lg fa-solid fa-magnifying-glass mes_media_enlarge" data-i18n="[title]Expand and zoom"></div> |
| 7298 | <div title="Caption" class="right_menu_button fa-lg fa-solid fa-envelope-open-text mes_img_caption" data-i18n="[title]Caption"></div> | ||
| 7298 | <div title="Delete" class="right_menu_button fa-lg fa-solid fa-trash-can mes_media_delete" data-i18n="[title]Delete"></div> | 7299 | <div title="Delete" class="right_menu_button fa-lg fa-solid fa-trash-can mes_media_delete" data-i18n="[title]Delete"></div> |
| 7299 | </div> | 7300 | </div> |
| 7300 | <video class="mes_video" controls preload="metadata"></video> | 7301 | <video class="mes_video" controls preload="metadata"></video> |
| @@ -183,7 +183,7 @@ import { | |||
| 183 | trimSpaces, | 183 | trimSpaces, |
| 184 | clamp, | 184 | clamp, |
| 185 | } from './scripts/utils.js'; | 185 | } from './scripts/utils.js'; |
| 186 | import { debounce_timeout, GENERATION_TYPE_TRIGGERS, IGNORE_SYMBOL, inject_ids, MEDIA_DISPLAY, MEDIA_TYPE, SCROLL_BEHAVIOR, SWIPE_DIRECTION } from './scripts/constants.js'; | 186 | import { debounce_timeout, GENERATION_TYPE_TRIGGERS, IGNORE_SYMBOL, inject_ids, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR, SWIPE_DIRECTION } from './scripts/constants.js'; |
| 187 | 187 | ||
| 188 | import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js'; | 188 | import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js'; |
| 189 | import { COMMENT_NAME_DEFAULT, CONNECT_API_MAP, executeSlashCommandsOnChatInput, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, stopScriptExecution, UNIQUE_APIS } from './scripts/slash-commands.js'; | 189 | import { COMMENT_NAME_DEFAULT, CONNECT_API_MAP, executeSlashCommandsOnChatInput, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, stopScriptExecution, UNIQUE_APIS } from './scripts/slash-commands.js'; |
| @@ -6414,7 +6414,7 @@ function saveImageToMessage(img, mes) { | |||
| 6414 | if (!Array.isArray(mes.extra.media)) { | 6414 | if (!Array.isArray(mes.extra.media)) { |
| 6415 | mes.extra.media = []; | 6415 | mes.extra.media = []; |
| 6416 | } | 6416 | } |
| 6417 | mes.extra.media.push({ url: img.image, type: MEDIA_TYPE.IMAGE, title: img.title }); | 6417 | mes.extra.media.push({ url: img.image, type: MEDIA_TYPE.IMAGE, title: img.title, source: MEDIA_SOURCE.API }); |
| 6418 | mes.extra.inline_image = img.inline; | 6418 | mes.extra.inline_image = img.inline; |
| 6419 | } | 6419 | } |
| 6420 | } | 6420 | } |
| @@ -57,7 +57,7 @@ import { renderTemplateAsync } from './templates.js'; | |||
| 57 | import { t } from './i18n.js'; | 57 | import { t } from './i18n.js'; |
| 58 | import { humanizedDateTime } from './RossAscends-mods.js'; | 58 | import { humanizedDateTime } from './RossAscends-mods.js'; |
| 59 | import { accountStorage } from './util/AccountStorage.js'; | 59 | import { accountStorage } from './util/AccountStorage.js'; |
| 60 | import { MEDIA_DISPLAY, MEDIA_TYPE, SCROLL_BEHAVIOR, SWIPE_DIRECTION } from './constants.js'; | 60 | import { MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR, SWIPE_DIRECTION } from './constants.js'; |
| 61 | 61 | ||
| 62 | /** | 62 | /** |
| 63 | * @typedef {Object} FileAttachment | 63 | * @typedef {Object} FileAttachment |
| @@ -216,7 +216,14 @@ export async function populateFileAttachment(message, inputId = 'file_form_input | |||
| 216 | if (!Array.isArray(message.extra.media)) { | 216 | if (!Array.isArray(message.extra.media)) { |
| 217 | message.extra.media = []; | 217 | message.extra.media = []; |
| 218 | } | 218 | } |
| 219 | message.extra.media.push({ url: imageUrl, type: mediaType, title: file.name }); | 219 | /** @type {MediaAttachment} */ |
| 220 | const mediaAttachment = { | ||
| 221 | url: imageUrl, | ||
| 222 | type: mediaType, | ||
| 223 | title: file.name, | ||
| 224 | source: MEDIA_SOURCE.UPLOAD, | ||
| 225 | }; | ||
| 226 | message.extra.media.push(mediaAttachment); | ||
| 220 | message.extra.media_index = message.extra.media.length - 1; | 227 | message.extra.media_index = message.extra.media.length - 1; |
| 221 | message.extra.inline_image = true; | 228 | message.extra.inline_image = true; |
| 222 | } else { | 229 | } else { |
| @@ -71,6 +71,17 @@ export const COMETAPI_IGNORE_PATTERNS = [ | |||
| 71 | * @enum {string} | 71 | * @enum {string} |
| 72 | * @readonly | 72 | * @readonly |
| 73 | */ | 73 | */ |
| 74 | export const MEDIA_SOURCE = { | ||
| 75 | API: 'api', | ||
| 76 | UPLOAD: 'upload', | ||
| 77 | GENERATED: 'generated', | ||
| 78 | CAPTIONED: 'captioned', | ||
| 79 | }; | ||
| 80 | |||
| 81 | /** | ||
| 82 | * @enum {string} | ||
| 83 | * @readonly | ||
| 84 | */ | ||
| 74 | export const MEDIA_DISPLAY = { | 85 | export const MEDIA_DISPLAY = { |
| 75 | LIST: 'list', | 86 | LIST: 'list', |
| 76 | GALLERY: 'gallery', | 87 | GALLERY: 'gallery', |
| @@ -10,7 +10,7 @@ import { SlashCommand } from '../../slash-commands/SlashCommand.js'; | |||
| 10 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; | 10 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
| 11 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; | 11 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 12 | import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js'; | 12 | import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js'; |
| 13 | import { MEDIA_DISPLAY, MEDIA_TYPE, SCROLL_BEHAVIOR } from '../../constants.js'; | 13 | import { MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR } from '../../constants.js'; |
| 14 | export { MODULE_NAME }; | 14 | export { MODULE_NAME }; |
| 15 | 15 | ||
| 16 | const MODULE_NAME = 'caption'; | 16 | const MODULE_NAME = 'caption'; |
| @@ -135,14 +135,18 @@ async function captionExistingMessage(message, mediaIndex) { | |||
| 135 | 135 | ||
| 136 | const mediaAttachment = message.extra.media[mediaIndex]; | 136 | const mediaAttachment = message.extra.media[mediaIndex]; |
| 137 | 137 | ||
| 138 | if (!mediaAttachment || !mediaAttachment.url || mediaAttachment.type === MEDIA_TYPE.VIDEO) { | 138 | if (!mediaAttachment || !mediaAttachment.url || mediaAttachment.type === MEDIA_TYPE.AUDIO) { |
| 139 | return; | 139 | return; |
| 140 | } | 140 | } |
| 141 | 141 | ||
| 142 | if (mediaAttachment.type === MEDIA_TYPE.VIDEO && !isVideoCaptioningAvailable()) { | ||
| 143 | throw new Error('Captioning videos is not supported for the current source.'); | ||
| 144 | } | ||
| 145 | |||
| 142 | const imageData = await fetch(mediaAttachment.url); | 146 | const imageData = await fetch(mediaAttachment.url); |
| 143 | const blob = await imageData.blob(); | 147 | const blob = await imageData.blob(); |
| 144 | const type = imageData.headers.get('Content-Type'); | 148 | const fileName = mediaAttachment.url.split('/').pop().split('?')[0] || 'image.jpg'; |
| 145 | const file = new File([blob], 'image.png', { type }); | 149 | const file = new File([blob], fileName, { type: blob.type }); |
| 146 | const caption = await getCaptionForFile(file, null, true); | 150 | const caption = await getCaptionForFile(file, null, true); |
| 147 | 151 | ||
| 148 | if (!caption) { | 152 | if (!caption) { |
| @@ -158,11 +162,12 @@ async function captionExistingMessage(message, mediaIndex) { | |||
| 158 | message.extra.inline_image = false; | 162 | message.extra.inline_image = false; |
| 159 | message.mes = wrappedCaption; | 163 | message.mes = wrappedCaption; |
| 160 | mediaAttachment.title = wrappedCaption; | 164 | mediaAttachment.title = wrappedCaption; |
| 161 | } | 165 | mediaAttachment.captioned = true; |
| 162 | else { | 166 | } else { |
| 163 | message.extra.inline_image = true; | 167 | message.extra.inline_image = true; |
| 164 | mediaAttachment.append_title = true; | 168 | mediaAttachment.append_title = true; |
| 165 | mediaAttachment.title = wrappedCaption; | 169 | mediaAttachment.title = wrappedCaption; |
| 170 | mediaAttachment.captioned = true; | ||
| 166 | } | 171 | } |
| 167 | } | 172 | } |
| 168 | 173 | ||
| @@ -170,8 +175,10 @@ async function captionExistingMessage(message, mediaIndex) { | |||
| 170 | * Sends a captioned message to the chat. | 175 | * Sends a captioned message to the chat. |
| 171 | * @param {string} caption Caption text | 176 | * @param {string} caption Caption text |
| 172 | * @param {string} image Image URL | 177 | * @param {string} image Image URL |
| 178 | * @param {string} mimeType Image MIME type | ||
| 179 | * @returns {Promise<void>} | ||
| 173 | */ | 180 | */ |
| 174 | async function sendCaptionedMessage(caption, image) { | 181 | async function sendCaptionedMessage(caption, image, mimeType) { |
| 175 | const messageText = await wrapCaptionTemplate(caption); | 182 | const messageText = await wrapCaptionTemplate(caption); |
| 176 | 183 | ||
| 177 | const context = getContext(); | 184 | const context = getContext(); |
| @@ -179,8 +186,10 @@ async function sendCaptionedMessage(caption, image) { | |||
| 179 | /** @type {MediaAttachment} */ | 186 | /** @type {MediaAttachment} */ |
| 180 | const mediaAttachment = { | 187 | const mediaAttachment = { |
| 181 | url: image, | 188 | url: image, |
| 182 | type: MEDIA_TYPE.IMAGE, | 189 | type: MEDIA_TYPE.getFromMime(mimeType) || MEDIA_TYPE.IMAGE, |
| 183 | title: messageText, | 190 | title: messageText, |
| 191 | captioned: true, | ||
| 192 | source: MEDIA_SOURCE.CAPTIONED, | ||
| 184 | }; | 193 | }; |
| 185 | /** @type {ChatMessage} */ | 194 | /** @type {ChatMessage} */ |
| 186 | const message = { | 195 | const message = { |
| @@ -351,6 +360,10 @@ async function onSelectImage(e, prompt, quiet) { | |||
| 351 | */ | 360 | */ |
| 352 | async function getCaptionForFile(file, prompt, quiet) { | 361 | async function getCaptionForFile(file, prompt, quiet) { |
| 353 | try { | 362 | try { |
| 363 | if (file.type.startsWith('video/') && !isVideoCaptioningAvailable()) { | ||
| 364 | throw new Error('Video captioning is not available for the current source.'); | ||
| 365 | } | ||
| 366 | |||
| 354 | setSpinnerIcon(); | 367 | setSpinnerIcon(); |
| 355 | const context = getContext(); | 368 | const context = getContext(); |
| 356 | const fileData = await getBase64Async(await ensureImageFormatSupported(file)); | 369 | const fileData = await getBase64Async(await ensureImageFormatSupported(file)); |
| @@ -359,13 +372,13 @@ async function getCaptionForFile(file, prompt, quiet) { | |||
| 359 | const { caption } = await doCaptionRequest(base64Data, fileData, prompt); | 372 | const { caption } = await doCaptionRequest(base64Data, fileData, prompt); |
| 360 | if (!quiet) { | 373 | if (!quiet) { |
| 361 | const imagePath = await saveBase64AsFile(base64Data, context.name2, '', extension); | 374 | const imagePath = await saveBase64AsFile(base64Data, context.name2, '', extension); |
| 362 | await sendCaptionedMessage(caption, imagePath); | 375 | await sendCaptionedMessage(caption, imagePath, file.type); |
| 363 | } | 376 | } |
| 364 | return caption; | 377 | return caption; |
| 365 | } | 378 | } |
| 366 | catch (error) { | 379 | catch (error) { |
| 367 | const errorMessage = error.message || 'Unknown error'; | 380 | const errorMessage = error.message || 'Unknown error'; |
| 368 | toastr.error(errorMessage, 'Failed to caption image.'); | 381 | toastr.error(errorMessage, 'Failed to caption'); |
| 369 | console.error(error); | 382 | console.error(error); |
| 370 | return ''; | 383 | return ''; |
| 371 | } | 384 | } |
| @@ -399,13 +412,18 @@ async function captionCommandCallback(args, prompt) { | |||
| 399 | toastr.error('The specified message does not contain an image.'); | 412 | toastr.error('The specified message does not contain an image.'); |
| 400 | return ''; | 413 | return ''; |
| 401 | } | 414 | } |
| 402 | if (mediaAttachment.type === MEDIA_TYPE.VIDEO) { | 415 | if (mediaAttachment.type === MEDIA_TYPE.AUDIO) { |
| 403 | toastr.error('The specified media is a video. Captioning videos is not supported.'); | 416 | toastr.error('The specified media is an audio file. Captioning audio files is not supported.'); |
| 417 | return ''; | ||
| 418 | } | ||
| 419 | if (mediaAttachment.type === MEDIA_TYPE.VIDEO && !isVideoCaptioningAvailable()) { | ||
| 420 | toastr.error('The specified media is a video. Captioning videos is not supported for the current source.'); | ||
| 404 | return ''; | 421 | return ''; |
| 405 | } | 422 | } |
| 406 | const fetchResult = await fetch(mediaAttachment.url); | 423 | const fetchResult = await fetch(mediaAttachment.url); |
| 407 | const blob = await fetchResult.blob(); | 424 | const blob = await fetchResult.blob(); |
| 408 | const file = new File([blob], 'image.jpg', { type: blob.type }); | 425 | const fileName = mediaAttachment.url.split('/').pop().split('?')[0] || 'image.jpg'; |
| 426 | const file = new File([blob], fileName, { type: blob.type }); | ||
| 409 | return await getCaptionForFile(file, prompt, quiet); | 427 | return await getCaptionForFile(file, prompt, quiet); |
| 410 | } catch (error) { | 428 | } catch (error) { |
| 411 | toastr.error('Failed to get image from the message. Make sure the image is accessible.'); | 429 | toastr.error('Failed to get image from the message. Make sure the image is accessible.'); |
| @@ -417,7 +435,7 @@ async function captionCommandCallback(args, prompt) { | |||
| 417 | return new Promise(resolve => { | 435 | return new Promise(resolve => { |
| 418 | const input = document.createElement('input'); | 436 | const input = document.createElement('input'); |
| 419 | input.type = 'file'; | 437 | input.type = 'file'; |
| 420 | input.accept = 'image/*'; | 438 | input.accept = 'image/*,video/*'; |
| 421 | input.onchange = async (e) => { | 439 | input.onchange = async (e) => { |
| 422 | const caption = await onSelectImage(e, prompt, quiet); | 440 | const caption = await onSelectImage(e, prompt, quiet); |
| 423 | resolve(caption); | 441 | resolve(caption); |
| @@ -427,6 +445,18 @@ async function captionCommandCallback(args, prompt) { | |||
| 427 | }); | 445 | }); |
| 428 | } | 446 | } |
| 429 | 447 | ||
| 448 | /** | ||
| 449 | * Checks if video captioning is available for the current source. | ||
| 450 | * @returns {boolean} True if video captioning is supported for the current source. | ||
| 451 | */ | ||
| 452 | function isVideoCaptioningAvailable() { | ||
| 453 | if (extension_settings.caption.source !== 'multimodal') { | ||
| 454 | return false; | ||
| 455 | } | ||
| 456 | |||
| 457 | return ['google', 'vertexai'].includes(extension_settings.caption.multimodal_api); | ||
| 458 | } | ||
| 459 | |||
| 430 | jQuery(async function () { | 460 | jQuery(async function () { |
| 431 | function addSendPictureButton() { | 461 | function addSendPictureButton() { |
| 432 | const sendButton = $(` | 462 | const sendButton = $(` |
| @@ -515,13 +545,17 @@ jQuery(async function () { | |||
| 515 | }); | 545 | }); |
| 516 | } | 546 | } |
| 517 | function addPictureSendForm() { | 547 | function addPictureSendForm() { |
| 518 | const inputHtml = '<input id="img_file" type="file" hidden accept="image/*">'; | 548 | const imgInput = document.createElement('input'); |
| 549 | imgInput.type = 'file'; | ||
| 550 | imgInput.id = 'img_file'; | ||
| 551 | imgInput.accept = 'image/*,video/*'; | ||
| 552 | imgInput.hidden = true; | ||
| 553 | imgInput.addEventListener('change', (e) => onSelectImage(e, '', false)); | ||
| 519 | const imgForm = document.createElement('form'); | 554 | const imgForm = document.createElement('form'); |
| 520 | imgForm.id = 'img_form'; | 555 | imgForm.id = 'img_form'; |
| 521 | $(imgForm).append(inputHtml); | 556 | imgForm.appendChild(imgInput); |
| 522 | $(imgForm).hide(); | 557 | imgForm.hidden = true; |
| 523 | $('#form_sheld').append(imgForm); | 558 | $('#form_sheld').append(imgForm); |
| 524 | $('#img_file').on('change', (e) => onSelectImage(e.originalEvent, '', false)); | ||
| 525 | } | 559 | } |
| 526 | async function switchMultimodalBlocks() { | 560 | async function switchMultimodalBlocks() { |
| 527 | await addRemoteEndpointModels(); | 561 | await addRemoteEndpointModels(); |
| @@ -668,13 +702,33 @@ jQuery(async function () { | |||
| 668 | saveSettingsDebounced(); | 702 | saveSettingsDebounced(); |
| 669 | }); | 703 | }); |
| 670 | 704 | ||
| 671 | const onMessageEvent = async (messageId) => { | 705 | const onMessageEvent = async (/** @type {number} */ messageId) => { |
| 672 | if (!extension_settings.caption.auto_mode) { | 706 | if (!extension_settings.caption.auto_mode) { |
| 673 | return; | 707 | return; |
| 674 | } | 708 | } |
| 675 | 709 | ||
| 676 | const message = getContext().chat[messageId]; | 710 | const message = getContext().chat[messageId]; |
| 677 | await captionExistingMessage(message, 0); | 711 | if (Array.isArray(message?.extra?.media) && message.extra.media.length > 0) { |
| 712 | for (let mediaIndex = 0; mediaIndex < message.extra.media.length; mediaIndex++) { | ||
| 713 | const mediaAttachment = message.extra.media[mediaIndex]; | ||
| 714 | if (mediaAttachment.type === MEDIA_TYPE.VIDEO && !isVideoCaptioningAvailable()) { | ||
| 715 | continue; | ||
| 716 | } | ||
| 717 | if (mediaAttachment.type === MEDIA_TYPE.AUDIO) { | ||
| 718 | continue; | ||
| 719 | } | ||
| 720 | // Skip already captioned images and non-uploaded (generated, etc.) images | ||
| 721 | if (mediaAttachment.source !== MEDIA_SOURCE.UPLOAD || mediaAttachment.captioned) { | ||
| 722 | continue; | ||
| 723 | } | ||
| 724 | try { | ||
| 725 | await captionExistingMessage(message, mediaIndex); | ||
| 726 | } catch (e) { | ||
| 727 | console.error(`Auto-captioning failed for message ID ${messageId}, media index ${mediaIndex}`, e); | ||
| 728 | continue; | ||
| 729 | } | ||
| 730 | } | ||
| 731 | } | ||
| 678 | }; | 732 | }; |
| 679 | 733 | ||
| 680 | eventSource.on(event_types.MESSAGE_SENT, onMessageEvent); | 734 | eventSource.on(event_types.MESSAGE_SENT, onMessageEvent); |
| @@ -683,21 +737,22 @@ jQuery(async function () { | |||
| 683 | $(document).on('click', '.mes_img_caption', async function () { | 737 | $(document).on('click', '.mes_img_caption', async function () { |
| 684 | const animationClass = 'fa-fade'; | 738 | const animationClass = 'fa-fade'; |
| 685 | const messageBlock = $(this).closest('.mes'); | 739 | const messageBlock = $(this).closest('.mes'); |
| 686 | const imageBlock = $(this).closest('.mes_img_container'); | 740 | const mediaContainer = $(this).closest('.mes_media_container'); |
| 687 | const messageImg = imageBlock.find('.mes_img'); | 741 | const messageMedia = mediaContainer.find('.mes_img, .mes_video'); |
| 688 | if (messageImg.hasClass(animationClass)) return; | 742 | if (messageMedia.hasClass(animationClass)) return; |
| 689 | messageImg.addClass(animationClass); | 743 | messageMedia.addClass(animationClass); |
| 690 | try { | 744 | try { |
| 691 | const messageId = Number(messageBlock.attr('mesid')); | 745 | const messageId = Number(messageBlock.attr('mesid')); |
| 692 | const imageIndex = Number(imageBlock.attr('data-index')); | 746 | const mediaIndex = Number(mediaContainer.attr('data-index')); |
| 693 | const data = getContext().chat[messageId]; | 747 | const data = getContext().chat[messageId]; |
| 694 | await captionExistingMessage(data, imageIndex); | 748 | await captionExistingMessage(data, mediaIndex); |
| 695 | appendMediaToMessage(data, messageBlock, SCROLL_BEHAVIOR.KEEP); | 749 | appendMediaToMessage(data, messageBlock, SCROLL_BEHAVIOR.KEEP); |
| 696 | await saveChatConditional(); | 750 | await saveChatConditional(); |
| 697 | } catch (e) { | 751 | } catch (e) { |
| 698 | console.error('Message image recaption failed', e); | 752 | console.error('Message image recaption failed', e); |
| 753 | toastr.error(e.message || 'Unknown error', 'Failed to caption'); | ||
| 699 | } finally { | 754 | } finally { |
| 700 | messageImg.removeClass(animationClass); | 755 | messageMedia.removeClass(animationClass); |
| 701 | } | 756 | } |
| 702 | }); | 757 | }); |
| 703 | 758 | ||
| @@ -33,12 +33,13 @@ export async function getMultimodalCaption(base64Img, prompt) { | |||
| 33 | const base64Bytes = base64Img.length * 0.75; | 33 | const base64Bytes = base64Img.length * 0.75; |
| 34 | const compressionLimit = 2 * 1024 * 1024; | 34 | const compressionLimit = 2 * 1024 * 1024; |
| 35 | const safeMimeTypes = ['image/jpeg', 'image/png', 'image/webp']; | 35 | const safeMimeTypes = ['image/jpeg', 'image/png', 'image/webp']; |
| 36 | const mimeType = base64Img?.split(';')?.[0]?.split(':')?.[1]; | 36 | const mimeType = base64Img?.split(';')?.[0]?.split(':')?.[1] || 'image/jpeg'; |
| 37 | const isImage = mimeType.startsWith('image/'); | ||
| 37 | const thumbnailNeeded = ['google', 'openrouter', 'mistral', 'groq', 'vertexai'].includes(extension_settings.caption.multimodal_api); | 38 | const thumbnailNeeded = ['google', 'openrouter', 'mistral', 'groq', 'vertexai'].includes(extension_settings.caption.multimodal_api); |
| 38 | if ((thumbnailNeeded && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) { | 39 | if ((isImage && thumbnailNeeded && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) { |
| 39 | const maxSide = 2048; | 40 | const maxSide = 2048; |
| 40 | base64Img = await createThumbnail(base64Img, maxSide, maxSide); | 41 | base64Img = await createThumbnail(base64Img, maxSide, maxSide); |
| 41 | } else if (!safeMimeTypes.includes(mimeType)) { | 42 | } else if (isImage && !safeMimeTypes.includes(mimeType)) { |
| 42 | base64Img = await createThumbnail(base64Img, null, null); | 43 | base64Img = await createThumbnail(base64Img, null, null); |
| 43 | } | 44 | } |
| 44 | if (isOllama && base64Img.startsWith('data:image/')) { | 45 | if (isOllama && base64Img.startsWith('data:image/')) { |
| @@ -52,7 +52,7 @@ import { | |||
| 52 | SlashCommandArgument, | 52 | SlashCommandArgument, |
| 53 | SlashCommandNamedArgument, | 53 | SlashCommandNamedArgument, |
| 54 | } from '../../slash-commands/SlashCommandArgument.js'; | 54 | } from '../../slash-commands/SlashCommandArgument.js'; |
| 55 | import { debounce_timeout, MEDIA_DISPLAY, MEDIA_TYPE, SCROLL_BEHAVIOR, VIDEO_EXTENSIONS } from '../../constants.js'; | 55 | import { debounce_timeout, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR, VIDEO_EXTENSIONS } from '../../constants.js'; |
| 56 | import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; | 56 | import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; |
| 57 | import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js'; | 57 | import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js'; |
| 58 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; | 58 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| @@ -4123,6 +4123,7 @@ async function sendMessage(prompt, image, generationType, additionalNegativePref | |||
| 4123 | title: prompt, | 4123 | title: prompt, |
| 4124 | generation_type: generationType, | 4124 | generation_type: generationType, |
| 4125 | negative: additionalNegativePrefix, | 4125 | negative: additionalNegativePrefix, |
| 4126 | source: MEDIA_SOURCE.GENERATED, | ||
| 4126 | }; | 4127 | }; |
| 4127 | /** @type {ChatMessage} */ | 4128 | /** @type {ChatMessage} */ |
| 4128 | const message = { | 4129 | const message = { |
| @@ -4386,6 +4387,7 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete, | |||
| 4386 | const result = { | 4387 | const result = { |
| 4387 | url: '', | 4388 | url: '', |
| 4388 | type: MEDIA_TYPE.IMAGE, | 4389 | type: MEDIA_TYPE.IMAGE, |
| 4390 | source: MEDIA_SOURCE.GENERATED, | ||
| 4389 | }; | 4391 | }; |
| 4390 | 4392 | ||
| 4391 | try { | 4393 | try { |