diff --git a/public/global.d.ts b/public/global.d.ts index 670b291a9..c860f9028 100644 --- a/public/global.d.ts +++ b/public/global.d.ts @@ -55,4 +55,15 @@ declare global { * @param provider Translation provider */ async function translate(text: string, lang: string, provider: string = null): Promise; + + interface ConvertVideoArgs { + buffer: Uint8Array; + name: string; + } + + /** + * Converts a video file to an animated WebP format using FFmpeg. + * @param args - The arguments for the conversion function. + */ + function convertVideoToAnimatedWebp(args: ConvertVideoArgs): Promise; } diff --git a/public/index.html b/public/index.html index c8b671eaa..3235598ee 100644 --- a/public/index.html +++ b/public/index.html @@ -4964,7 +4964,7 @@
diff --git a/public/scripts/backgrounds.js b/public/scripts/backgrounds.js index 64ffbeabd..72df695d1 100644 --- a/public/scripts/backgrounds.js +++ b/public/scripts/backgrounds.js @@ -1,7 +1,7 @@ import { Fuse } from '../lib.js'; import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveSettingsDebounced } from '../script.js'; -import { saveMetadataDebounced } from './extensions.js'; +import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { flashHighlight, stringFormat } from './utils.js'; @@ -78,7 +78,7 @@ function getChatBackgroundsList() { } function getBackgroundPath(fileUrl) { - return `backgrounds/${fileUrl}`; + return `backgrounds/${encodeURIComponent(fileUrl)}`; } function highlightLockedBackground() { @@ -218,7 +218,7 @@ async function onCopyToSystemBackgroundClick(e) { const formData = new FormData(); formData.set('avatar', file); - uploadBackground(formData); + await uploadBackground(formData); const list = chat_metadata[LIST_METADATA_KEY] || []; const index = list.indexOf(bgNames.oldBg); @@ -439,7 +439,7 @@ async function delBackground(bg) { }); } -function onBackgroundUploadSelected() { +async function onBackgroundUploadSelected() { const form = $('#form_bg_download').get(0); if (!(form instanceof HTMLFormElement)) { @@ -448,34 +448,82 @@ function onBackgroundUploadSelected() { } const formData = new FormData(form); - uploadBackground(formData); + await convertFileIfVideo(formData); + await uploadBackground(formData); form.reset(); } +/** + * Converts a video file to an animated webp format if the file is a video. + * @param {FormData} formData + * @returns {Promise} + */ +async function convertFileIfVideo(formData) { + const file = formData.get('avatar'); + if (!(file instanceof File)) { + return; + } + if (!file.type.startsWith('video/')) { + return; + } + if (typeof globalThis.convertVideoToAnimatedWebp !== 'function') { + toastr.warning(t`Click here to install the Video Background Loader extension`, t`Video background uploads require a downloadable add-on`, { + timeOut: 0, + extendedTimeOut: 0, + onclick: () => openThirdPartyExtensionMenu('https://github.com/SillyTavern/Extension-VideoBackgroundLoader'), + }); + return; + } + + let toastMessage = jQuery(); + try { + toastMessage = toastr.info(t`Preparing video for upload. This may take several minutes.`, t`Please wait`, { timeOut: 0, extendedTimeOut: 0 }); + const sourceBuffer = await file.arrayBuffer(); + const convertedBuffer = await globalThis.convertVideoToAnimatedWebp({ buffer: new Uint8Array(sourceBuffer), name: file.name }); + const convertedFileName = file.name.replace(/\.[^/.]+$/, '.webp'); + const convertedFile = new File([convertedBuffer], convertedFileName, { type: 'image/webp' }); + formData.set('avatar', convertedFile); + toastMessage.remove(); + } catch (error) { + formData.delete('avatar'); + toastMessage.remove(); + console.error('Error converting video to animated webp:', error); + toastr.error(t`Error converting video to animated webp`); + } +} + /** * Uploads a background to the server * @param {FormData} formData */ -function uploadBackground(formData) { - jQuery.ajax({ - type: 'POST', - url: '/api/backgrounds/upload', - data: formData, - beforeSend: function () { - }, - cache: false, - contentType: false, - processData: false, - success: async function (bg) { - setBackground(bg, generateUrlParameter(bg, false)); - await getBackgrounds(); - highlightNewBackground(bg); - }, - error: function (jqXHR, exception) { - console.log(exception); - console.log(jqXHR); - }, - }); +async function uploadBackground(formData) { + try { + if (!formData.has('avatar')) { + console.log('No file provided. Background upload cancelled.'); + return; + } + + const headers = getRequestHeaders(); + delete headers['Content-Type']; + + const response = await fetch('/api/backgrounds/upload', { + method: 'POST', + headers: headers, + body: formData, + cache: 'no-cache', + }); + + if (!response.ok) { + throw new Error('Failed to upload background'); + } + + const bg = await response.text(); + setBackground(bg, generateUrlParameter(bg, false)); + await getBackgrounds(); + highlightNewBackground(bg); + } catch (error) { + console.error('Error uploading background:', error); + } } /**