Merge pull request #3803 from SillyTavern/ffmpeg-videobg Upload video bg via converter extension
Signed| @@ -55,4 +55,15 @@ declare global { | |||
| 55 | * @param provider Translation provider | 55 | * @param provider Translation provider |
| 56 | */ | 56 | */ |
| 57 | async function translate(text: string, lang: string, provider: string = null): Promise<string>; | 57 | async function translate(text: string, lang: string, provider: string = null): Promise<string>; |
| 58 | |||
| 59 | interface ConvertVideoArgs { | ||
| 60 | buffer: Uint8Array; | ||
| 61 | name: string; | ||
| 62 | } | ||
| 63 | |||
| 64 | /** | ||
| 65 | * Converts a video file to an animated WebP format using FFmpeg. | ||
| 66 | * @param args - The arguments for the conversion function. | ||
| 67 | */ | ||
| 68 | function convertVideoToAnimatedWebp(args: ConvertVideoArgs): Promise<Uint8Array>; | ||
| 58 | } | 69 | } |
| @@ -4964,7 +4964,7 @@ | |||
| 4964 | <div id="bg_menu_content" class="bg_list"> | 4964 | <div id="bg_menu_content" class="bg_list"> |
| 4965 | <form id="form_bg_download" class="bg_example no-border no-shadow" action="javascript:void(null);" method="post" enctype="multipart/form-data"> | 4965 | <form id="form_bg_download" class="bg_example no-border no-shadow" action="javascript:void(null);" method="post" enctype="multipart/form-data"> |
| 4966 | <label class="input-file"> | 4966 | <label class="input-file"> |
| 4967 | <input type="file" id="add_bg_button" name="avatar" accept="image/png, image/jpeg, image/jpg, image/gif, image/bmp"> | 4967 | <input type="file" id="add_bg_button" name="avatar" accept="image/*, video/*"> |
| 4968 | <div class="bg_example no-border no-shadow add_bg_but" style="background-image: url('/img/addbg3.png');"></div> | 4968 | <div class="bg_example no-border no-shadow add_bg_but" style="background-image: url('/img/addbg3.png');"></div> |
| 4969 | </label> | 4969 | </label> |
| 4970 | </form> | 4970 | </form> |
| @@ -1,7 +1,7 @@ | |||
| 1 | import { Fuse } from '../lib.js'; | 1 | import { Fuse } from '../lib.js'; |
| 2 | 2 | ||
| 3 | import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveSettingsDebounced } from '../script.js'; | 3 | import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveSettingsDebounced } from '../script.js'; |
| 4 | import { saveMetadataDebounced } from './extensions.js'; | 4 | import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js'; |
| 5 | import { SlashCommand } from './slash-commands/SlashCommand.js'; | 5 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 6 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; | 6 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 7 | import { flashHighlight, stringFormat } from './utils.js'; | 7 | import { flashHighlight, stringFormat } from './utils.js'; |
| @@ -78,7 +78,7 @@ function getChatBackgroundsList() { | |||
| 78 | } | 78 | } |
| 79 | 79 | ||
| 80 | function getBackgroundPath(fileUrl) { | 80 | function getBackgroundPath(fileUrl) { |
| 81 | return `backgrounds/${fileUrl}`; | 81 | return `backgrounds/${encodeURIComponent(fileUrl)}`; |
| 82 | } | 82 | } |
| 83 | 83 | ||
| 84 | function highlightLockedBackground() { | 84 | function highlightLockedBackground() { |
| @@ -218,7 +218,7 @@ async function onCopyToSystemBackgroundClick(e) { | |||
| 218 | const formData = new FormData(); | 218 | const formData = new FormData(); |
| 219 | formData.set('avatar', file); | 219 | formData.set('avatar', file); |
| 220 | 220 | ||
| 221 | uploadBackground(formData); | 221 | await uploadBackground(formData); |
| 222 | 222 | ||
| 223 | const list = chat_metadata[LIST_METADATA_KEY] || []; | 223 | const list = chat_metadata[LIST_METADATA_KEY] || []; |
| 224 | const index = list.indexOf(bgNames.oldBg); | 224 | const index = list.indexOf(bgNames.oldBg); |
| @@ -439,7 +439,7 @@ async function delBackground(bg) { | |||
| 439 | }); | 439 | }); |
| 440 | } | 440 | } |
| 441 | 441 | ||
| 442 | function onBackgroundUploadSelected() { | 442 | async function onBackgroundUploadSelected() { |
| 443 | const form = $('#form_bg_download').get(0); | 443 | const form = $('#form_bg_download').get(0); |
| 444 | 444 | ||
| 445 | if (!(form instanceof HTMLFormElement)) { | 445 | if (!(form instanceof HTMLFormElement)) { |
| @@ -448,34 +448,82 @@ function onBackgroundUploadSelected() { | |||
| 448 | } | 448 | } |
| 449 | 449 | ||
| 450 | const formData = new FormData(form); | 450 | const formData = new FormData(form); |
| 451 | uploadBackground(formData); | 451 | await convertFileIfVideo(formData); |
| 452 | await uploadBackground(formData); | ||
| 452 | form.reset(); | 453 | form.reset(); |
| 453 | } | 454 | } |
| 454 | 455 | ||
| 455 | /** | 456 | /** |
| 457 | * Converts a video file to an animated webp format if the file is a video. | ||
| 458 | * @param {FormData} formData | ||
| 459 | * @returns {Promise<void>} | ||
| 460 | */ | ||
| 461 | async function convertFileIfVideo(formData) { | ||
| 462 | const file = formData.get('avatar'); | ||
| 463 | if (!(file instanceof File)) { | ||
| 464 | return; | ||
| 465 | } | ||
| 466 | if (!file.type.startsWith('video/')) { | ||
| 467 | return; | ||
| 468 | } | ||
| 469 | if (typeof globalThis.convertVideoToAnimatedWebp !== 'function') { | ||
| 470 | toastr.warning(t`Click here to install the Video Background Loader extension`, t`Video background uploads require a downloadable add-on`, { | ||
| 471 | timeOut: 0, | ||
| 472 | extendedTimeOut: 0, | ||
| 473 | onclick: () => openThirdPartyExtensionMenu('https://github.com/SillyTavern/Extension-VideoBackgroundLoader'), | ||
| 474 | }); | ||
| 475 | return; | ||
| 476 | } | ||
| 477 | |||
| 478 | let toastMessage = jQuery(); | ||
| 479 | try { | ||
| 480 | toastMessage = toastr.info(t`Preparing video for upload. This may take several minutes.`, t`Please wait`, { timeOut: 0, extendedTimeOut: 0 }); | ||
| 481 | const sourceBuffer = await file.arrayBuffer(); | ||
| 482 | const convertedBuffer = await globalThis.convertVideoToAnimatedWebp({ buffer: new Uint8Array(sourceBuffer), name: file.name }); | ||
| 483 | const convertedFileName = file.name.replace(/\.[^/.]+$/, '.webp'); | ||
| 484 | const convertedFile = new File([convertedBuffer], convertedFileName, { type: 'image/webp' }); | ||
| 485 | formData.set('avatar', convertedFile); | ||
| 486 | toastMessage.remove(); | ||
| 487 | } catch (error) { | ||
| 488 | formData.delete('avatar'); | ||
| 489 | toastMessage.remove(); | ||
| 490 | console.error('Error converting video to animated webp:', error); | ||
| 491 | toastr.error(t`Error converting video to animated webp`); | ||
| 492 | } | ||
| 493 | } | ||
| 494 | |||
| 495 | /** | ||
| 456 | * Uploads a background to the server | 496 | * Uploads a background to the server |
| 457 | * @param {FormData} formData | 497 | * @param {FormData} formData |
| 458 | */ | 498 | */ |
| 459 | function uploadBackground(formData) { | 499 | async function uploadBackground(formData) { |
| 460 | jQuery.ajax({ | 500 | try { |
| 461 | type: 'POST', | 501 | if (!formData.has('avatar')) { |
| 462 | url: '/api/backgrounds/upload', | 502 | console.log('No file provided. Background upload cancelled.'); |
| 463 | data: formData, | 503 | return; |
| 464 | beforeSend: function () { | 504 | } |
| 465 | }, | 505 | |
| 466 | cache: false, | 506 | const headers = getRequestHeaders(); |
| 467 | contentType: false, | 507 | delete headers['Content-Type']; |
| 468 | processData: false, | 508 | |
| 469 | success: async function (bg) { | 509 | const response = await fetch('/api/backgrounds/upload', { |
| 510 | method: 'POST', | ||
| 511 | headers: headers, | ||
| 512 | body: formData, | ||
| 513 | cache: 'no-cache', | ||
| 514 | }); | ||
| 515 | |||
| 516 | if (!response.ok) { | ||
| 517 | throw new Error('Failed to upload background'); | ||
| 518 | } | ||
| 519 | |||
| 520 | const bg = await response.text(); | ||
| 470 | setBackground(bg, generateUrlParameter(bg, false)); | 521 | setBackground(bg, generateUrlParameter(bg, false)); |
| 471 | await getBackgrounds(); | 522 | await getBackgrounds(); |
| 472 | highlightNewBackground(bg); | 523 | highlightNewBackground(bg); |
| 473 | }, | 524 | } catch (error) { |
| 474 | error: function (jqXHR, exception) { | 525 | console.error('Error uploading background:', error); |
| 475 | console.log(exception); | 526 | } |
| 476 | console.log(jqXHR); | ||
| 477 | }, | ||
| 478 | }); | ||
| 479 | } | 527 | } |
| 480 | 528 | ||
| 481 | /** | 529 | /** |