Merge pull request #2805 from SillyTavern/gemini-vision-fix Fix Gemini multimodal with JPG images
Signed| @@ -20,7 +20,7 @@ export async function getMultimodalCaption(base64Img, prompt) { | ||
| 20 | 20 | |
| 21 | 21 | throwIfInvalidModel(useReverseProxy); |
| 22 | 22 | |
| 23 | 23 | const noPrefix = ['google', 'ollama', 'llamacpp'].includes(extension_settings.caption.multimodal_api); |
| 24 | 24 | |
| 25 | 25 | if (noPrefix && base64Img.startsWith('data:image/')) { |
| 26 | 26 | base64Img = base64Img.split(',')[1]; |
| @@ -28,7 +28,6 @@ export async function getMultimodalCaption(base64Img, prompt) { | ||
| 28 | 28 | |
| 29 | 29 | // OpenRouter has a payload limit of ~2MB. Google is 4MB, but we love democracy. |
| 30 | 30 | // Ooba requires all images to be JPEGs. Koboldcpp just asked nicely. |
| 31 | - const isGoogle = extension_settings.caption.multimodal_api === 'google'; | |
| 32 | 31 | const isOllama = extension_settings.caption.multimodal_api === 'ollama'; |
| 33 | 32 | const isLlamaCpp = extension_settings.caption.multimodal_api === 'llamacpp'; |
| 34 | 33 | const isCustom = extension_settings.caption.multimodal_api === 'custom'; |
| @@ -40,10 +39,6 @@ export async function getMultimodalCaption(base64Img, prompt) { | ||
| 40 | 39 | if ((['google', 'openrouter'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) { |
| 41 | 40 | const maxSide = 1024; |
| 42 | 41 | base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg'); |
| 43 | - | |
| 44 | - if (isGoogle) { | |
| 45 | - base64Img = base64Img.split(',')[1]; | |
| 46 | - } | |
| 47 | 42 | } |
| 48 | 43 | |
| 49 | 44 | const proxyUrl = useReverseProxy ? oai_settings.reverse_proxy : ''; |
| @@ -47,6 +47,7 @@ import { SECRET_KEYS, secret_state, writeSecret } from './secrets.js'; | ||
| 47 | 47 | |
| 48 | 48 | import { getEventSourceStream } from './sse-stream.js'; |
| 49 | 49 | import { |
| 50 | + createThumbnail, | |
| 50 | 51 | delay, |
| 51 | 52 | download, |
| 52 | 53 | getBase64Async, |
| @@ -2440,15 +2441,14 @@ class Message { | ||
| 2440 | 2441 | if (!response.ok) throw new Error('Failed to fetch image'); |
| 2441 | 2442 | const blob = await response.blob(); |
| 2442 | 2443 | image = await getBase64Async(blob); |
| 2443 | - if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) { | |
| 2444 | - image = image.split(',')[1]; | |
| 2445 | - } | |
| 2446 | 2444 | } catch (error) { |
| 2447 | 2445 | console.error('Image adding skipped', error); |
| 2448 | 2446 | return; |
| 2449 | 2447 | } |
| 2450 | 2448 | } |
| 2451 | 2449 | |
| 2450 | + image = await this.compressImage(image); | |
| 2451 | + | |
| 2452 | 2452 | const quality = oai_settings.inline_image_quality || default_settings.inline_image_quality; |
| 2453 | 2453 | this.content = [ |
| 2454 | 2454 | { type: 'text', text: textContent }, |
| @@ -2464,6 +2464,29 @@ class Message { | ||
| 2464 | 2464 | } |
| 2465 | 2465 | } |
| 2466 | 2466 | |
| 2467 | + /** | |
| 2468 | + * Compress an image if it exceeds the size threshold for the current chat completion source. | |
| 2469 | + * @param {string} image Data URL of the image. | |
| 2470 | + * @returns {Promise<string>} Compressed image as a Data URL. | |
| 2471 | + */ | |
| 2472 | + async compressImage(image) { | |
| 2473 | + if ([chat_completion_sources.OPENROUTER, chat_completion_sources.MAKERSUITE].includes(oai_settings.chat_completion_source)) { | |
| 2474 | + const sizeThreshold = 2 * 1024 * 1024; | |
| 2475 | + const dataSize = image.length * 0.75; | |
| 2476 | + const maxSide = 1024; | |
| 2477 | + if (dataSize > sizeThreshold) { | |
| 2478 | + image = await createThumbnail(image, maxSide); | |
| 2479 | + } | |
| 2480 | + } | |
| 2481 | + return image; | |
| 2482 | + } | |
| 2483 | + | |
| 2484 | + /** | |
| 2485 | + * Get the token cost of an image. | |
| 2486 | + * @param {string} dataUrl Data URL of the image. | |
| 2487 | + * @param {string} quality String representing the quality of the image. Can be 'low', 'auto', or 'high'. | |
| 2488 | + * @returns {Promise<number>} The token cost of the image. | |
| 2489 | + */ | |
| 2467 | 2490 | async getImageTokenCost(dataUrl, quality) { |
| 2468 | 2491 | if (quality === 'low') { |
| 2469 | 2492 | return Message.tokensPerImage; |
| @@ -22,8 +22,8 @@ router.post('/caption-image', jsonParser, async (request, response) => { | ||
| 22 | 22 | { text: request.body.prompt }, |
| 23 | 23 | { |
| 24 | 24 | inlineData: { |
| 25 | - mimeType: 'image/png', // It needs to specify a MIME type in data if it's not a PNG | |
| 25 | + mimeType: mimeType, | |
| 26 | - data: mimeType === 'image/png' ? base64Data : request.body.image, | |
| 26 | + data: base64Data, | |
| 27 | 27 | }, |
| 28 | 28 | }], |
| 29 | 29 | }], |
| @@ -335,10 +335,12 @@ function convertGooglePrompt(messages, model, useSysPrompt = false, charName = ' | ||
| 335 | 335 | if (part.type === 'text') { |
| 336 | 336 | parts.push({ text: part.text }); |
| 337 | 337 | } else if (part.type === 'image_url' && isMultimodal) { |
| 338 | + const mimeType = part.image_url.url.split(';')[0].split(':')[1]; | |
| 339 | + const base64Data = part.image_url.url.split(',')[1]; | |
| 338 | 340 | parts.push({ |
| 339 | 341 | inlineData: { |
| 340 | 342 | mimeType: 'image/png'mimeType, |
| 341 | 343 | data: part.image_url.urlbase64Data, |
| 342 | 344 | }, |
| 343 | 345 | }); |
| 344 | 346 | hasImage = true; |